1

I would be happy for any tips for drawing something resembling contour lines for the growth advancement lines around a fungal fairy ring that advances annually uphill. This is a plot of the records of fruit bodies I have found from the years 2009-2022 in an x-y coordinate system of latitudinal/logitudinal positions:

enter image description here

Really, the "ring" is rather an advancing front(=half-circle) than an actual closed ring, gradually growing through the landscape. We have marked the positions of the fruitbodies every year with poles in an x~y coordinate system, so my file presently looks something like this for the years 2009-2022:

Year,x=horizontal position of fruitbody, y=vertical position of fruitbody

This is a dput of my data frame:

 dput(head(mydataframe)) 

structure(list(Year = c(2022L, 2014L, 2015L, 2014L, 2014L, 2015L), xpos = c(5487.5, 5475, 5475, 5450, 5425, 5400), ypos = c(262.5, 550, 537.5, 525, 500, 475)), row.names = c(NA, 6L), class = "data.frame")

I would like to draw something like the image here:

enter image description here

with gradually advancing lines drawn between annual points of observations (preferably being reasonably interpolated between points as contours in the landscape). Colours are unimportant.

There are more points in some years than in others, and the "ring" is taking some turns.

Is what I am trying really feasible, or would I be better served doing som hand-drawing sketches?

Best wishes, Christoffer

Update: This below is the entire data frame:

dput(mydataframe)

structure(list(Year = c(2022L, 2014L, 2015L, 2014L, 2014L, 2015L, 
2020L, 2014L, 2013L, 2014L, 2013L, 2014L, 2010L, 2014L, 2009L, 
2010L, 2011L, 2015L, 2017L, 2018L, 2020L, 2014L, 2010L, 2009L, 
2016L, 2020L, 2018L, 2018L, 2010L, 2011L, 2011L, 2014L, 2019L, 
2018L, 2014L, 2019L, 2009L, 2014L, 2018L, 2014L, 2019L, 2017L, 
2010L, 2012L, 2016L, 2018L, 2015L, 2017L, 2019L, 2014L, 2016L, 
2019L, 2019L, 2022L, 2011L, 2014L, 2015L, 2014L, 2016L, 2017L, 
2014L, 2017L, 2017L, 2018L, 2014L, 2017L, 2011L, 2014L, 2018L, 
2020L, 2010L, 2011L, 2017L, 2017L, 2010L, 2020L, 2020L, 2022L, 
2014L, 2020L, 2021L, 2022L, 2022L, 2022L, 2022L, 2017L, 2015L, 
2022L, 2021L, 2022L, 2022L, 2020L, 2015L, 2010L, 2014L, 2021L, 
2018L, 2022L, 2016L, 2020L, 2021L, 2018L, 2010L, 2014L, 2015L, 
2010L, 2014L, 2016L, 2018L, 2019L, 2018L, 2010L, 2014L, 2019L, 
2014L, 2012L, 2018L, 2019L, 2020L, 2018L, 2016L, 2014L, 2015L, 
2014L, 2015L, 2014L, 2010L, 2014L, 2015L, 2010L, 2009L, 2009L, 
2009L, 2010L, 2013L, 2014L, 2010L, 2011L, 2012L, 2014L, 2014L, 
2009L, 2014L, 2017L, 2018L, 2015L, 2017L, 2014L, 2018L, 2020L, 
2014L, 2022L, 2020L, 2015L, 2018L, 2020L, 2022L, 2018L, 2018L, 
2020L, 2020L, 2018L, 2018L, 2022L, 2018L, 2020L, 2022L, 2020L, 
2022L, 2022L, 2021L, 2022L, 2022L, 2022L, 2020L, 2022L, 2022L, 
2016L, 2018L, 2020L, 2017L, 2016L, 2018L, 2022L, 2022L, 2018L, 
2020L, 2021L, 2022L, 2021L, 2022L, 2017L, 2015L, 2016L, 2018L, 
2014L, 2015L, 2022L, 2014L, 2015L, 2021L, 2014L, 2011L, 2015L, 
2014L, 2019L, 2020L, 2014L, 2015L, 2019L, 2019L, 2016L, 2018L, 
2019L, 2019L, 2020L, 2020L, 2021L, 2021L, 2022L, 2021L, 2022L, 
2021L, 2022L, 2020L, 2021L, 2022L, 2010L, 2014L, 2014L, 2010L, 
2015L, 2017L, 2022L, 2016L, 2018L, 2012L, 2014L, 2014L, 2016L, 
2015L, 2014L, 2014L, 2015L, 2017L, 2016L, 2018L, 2017L, 2017L, 
2017L, 2018L, 2016L, 2018L, 2014L, 2015L, 2014L, 2015L, 2017L, 
2014L, 2011L, 2009L, 2010L, 2009L, 2010L, 2016L, 2009L, 2010L, 
2010L, 2010L, 2009L, 2012L, 2014L, 2016L, 2019L, 2020L, 2022L, 
2019L, 2022L, 2014L, 2016L, 2017L, 2022L, 2016L, 2022L, 2020L, 
2020L), xpos = c(5487.5, 5475, 5475, 5450, 5425, 5400, 5400, 
5375, 5350, 5350, 5325, 5325, 5275, 5275, 5250, 5250, 5250, 5225, 
5200, 5200, 5200, 5187.5, 5175, 5175, 5175, 5137.5, 5125, 5100, 
5075, 5075, 5075, 5075, 5075, 5062.5, 5050, 5050, 5025, 5025, 
5025, 5000, 5000, 4987.5, 4975, 4975, 4975, 4975, 4950, 4950, 
4950, 4937.5, 4937.5, 4937.5, 4925, 4925, 4900, 4900, 4900, 4862.5, 
4862.5, 4862.5, 4837.5, 4837.5, 4825, 4825, 4800, 4775, 4750, 
4750, 4750, 4750, 4675, 4675, 4675, 4675, 4650, 4650, 4650, 4612.5, 
4600, 4587.5, 4587.5, 4587.5, 4575, 4550, 4537.5, 4525, 4512.5, 
4500, 4487.5, 4487.5, 4437.5, 4425, 4412.5, 4400, 4400, 4400, 
4387.5, 4387.5, 4375, 4375, 4375, 4362.5, 4350, 4350, 4337.5, 
4325, 4325, 4325, 4325, 4325, 4312.5, 4300, 4300, 4300, 4287.5, 
4275, 4275, 4275, 4275, 4262.5, 4212.5, 4200, 4200, 4175, 4175, 
4162.5, 4150, 4150, 4137.5, 4100, 3975, 3925, 3900, 3875, 3737.5, 
3625, 3425, 3412.5, 3362.5, 3362.5, 3362.5, 3337.5, 3262.5, 2762.5, 
2750, 2737.5, 2737.5, 2712.5, 2712.5, 2712.5, 2687.5, 2687.5, 
2662.5, 2637.5, 2637.5, 2637.5, 2625, 2587.5, 2562.5, 2562.5, 
2512.5, 2487.5, 2312.5, 2225, 2212.5, 2175, 2175, 2125, 2112.5, 
2075, 2050, 2050, 2025, 2012.5, 2000, 2000, 1975, 1950, 1950, 
1950, 1937.5, 1925, 1925, 1925, 1912.5, 1900, 1900, 1900, 1900, 
1875, 1862.5, 1850, 1837.5, 1837.5, 1837.5, 1825, 1825, 1812.5, 
1787.5, 1775, 1775, 1762.5, 1750, 1750, 1737.5, 1725, 1725, 1712.5, 
1712.5, 1700, 1700, 1687.5, 1687.5, 1675, 1662.5, 1637.5, 1637.5, 
1637.5, 1637.5, 1637.5, 1612.5, 1612.5, 1587.5, 1587.5, 1575, 
1562.5, 1562.5, 1550, 1550, 1550, 1525, 1525, 1525, 1525, 1512.5, 
1512.5, 1500, 1500, 1487.5, 1462.5, 1450, 1437.5, 1412.5, 1400, 
1400, 1375, 1375, 1362.5, 1350, 1350, 1337.5, 1325, 1325, 1312.5, 
1287.5, 1275, 1225, 1225, 1125, 812.5, 800, 775, 650, 650, 587.5, 
550, 550, 475, 475, 462.5, 437.5, 437.5, 437.5, 437.5, 437.5, 
412.5, 400, 400, 387.5, 387.5, 362.5, 350, 337.5, 337.5, 87.5, 
12.5), ypos = c(262.5, 550, 537.5, 525, 500, 475, 312.5, 487.5, 
512.5, 475, 537.5, 475, 600, 475, 587.5, 562.5, 537.5, 437.5, 
350, 312.5, 250, 450, 575, 550, 387.5, 275, 337.5, 337.5, 562.5, 
537.5, 462.5, 425, 300, 325, 437.5, 287.5, 600, 437.5, 287.5, 
437.5, 287.5, 337.5, 550, 475, 362.5, 312.5, 387.5, 337.5, 287.5, 
412.5, 362.5, 300, 325, 187.5, 537.5, 437.5, 412.5, 412.5, 387.5, 
325, 412.5, 337.5, 337.5, 300, 412.5, 325, 537.5, 437.5, 300, 
250, 562.5, 537.5, 412.5, 387.5, 562.5, 375, 350, 262.5, 475, 
325, 287.5, 262.5, 262.5, 275, 275, 437.5, 475, 275, 300, 275, 
275, 350, 500, 625, 525, 325, 412.5, 300, 475, 362.5, 337.5, 
412.5, 650, 525, 500, 625, 537.5, 512.5, 437.5, 400, 425, 625, 
537.5, 400, 562.5, 637.5, 412.5, 387.5, 337.5, 425, 450, 575, 
462.5, 600, 412.5, 612.5, 750, 625, 387.5, 762.5, 800, 800, 825, 
837.5, 812.5, 862.5, 925, 900, 862.5, 812.5, 800, 937.5, 800, 
912.5, 875, 950, 887.5, 962.5, 850, 800, 962.5, 750, 800, 925, 
850, 800, 750, 850, 850, 800, 812.5, 850, 800, 662.5, 762.5, 
737.5, 687.5, 712.5, 637.5, 562.5, 575, 550, 537.5, 537.5, 587.5, 
512.5, 512.5, 687.5, 625, 562.5, 662.5, 687.5, 587.5, 512.5, 
500, 625, 550, 512.5, 487.5, 525, 475, 612.5, 737.5, 675, 587.5, 
762.5, 700, 475, 737.5, 700, 487.5, 712.5, 750, 687.5, 700, 537.5, 
512.5, 687.5, 675, 537.5, 537.5, 625, 550, 525, 537.5, 512.5, 
512.5, 487.5, 487.5, 462.5, 487.5, 462.5, 487.5, 462.5, 512.5, 
487.5, 462.5, 737.5, 637.5, 625, 725, 600, 537.5, 462.5, 575, 
525, 675, 625, 625, 562.5, 600, 625, 650, 625, 575, 762.5, 550, 
587.5, 712.5, 625, 675, 800, 712.5, 850, 837.5, 875, 875, 800, 
900, 812.5, 1000, 975, 1000, 975, 737.5, 1025, 1000, 950, 950, 
1025, 1025, 850, 775, 712.5, 675, 625, 737.5, 625, 900, 825, 
775, 687.5, 787.5, 700, 800, 775)), class = "data.frame", row.names = c(NA, 
-286L))

Update II: Ben Bolker´s excellent reply did the trick for me. enter image description here I added some features and did annual breaks. I used it on the part of the front where all years had fungal fruitbodies - no artifacts in this case.

Just briefly: If anyone has a good suggestion to change the legend labels from (2009-2010) to 2009-10 without scale_fill_discrete() and keep this colour scheme here, I am all ears.

  • Could you include a sketch of what your expected output should look like including some of the coordinates from your table. It would make life easier if you included your data as a data frame by pasting the output of `dput("your_coordinate_data_frame")` into the question. – Peter Sep 10 '22 at 10:42
  • I have added your suggested information to the original post (hopefully sufficient). – Christoffer Bugge Harder Sep 14 '22 at 13:25
  • That's certainly helpful. Could you include a more comprehensive extract from `mydataframe` the current data extract is insufficient to check a possible solution. The minimum requirement would be all the xy coordinates for at least three years to enable a realistic check. – Peter Sep 14 '22 at 16:59
  • I have pasted the entire data frame in the original post now. And I fully realise that some years are woefully underrepresented and may be better left out, but any suggestion as to doing some sensible interpolation between those year points where lots of points are found would be utmost welcome! Best, cbh – Christoffer Bugge Harder Sep 14 '22 at 22:34
  • I feel like this will a contour plot of the results of a 2D smoothing operation (GAM or local linear regression or kriging), but I'm not quite sure how to achieve it compactly ... – Ben Bolker Sep 14 '22 at 22:37
  • 1
    Your solution is nice, but you should really post it as another answer to your own question (this is encouraged) rather than making it part of your question. (Not sure about the legend question, will consider it.) – Ben Bolker Sep 15 '22 at 13:37

2 Answers2

2

Here's a start: fit a 2-D GAM to your points, then draw filled contours based on its predictions.

library(ggplot2)
library(mgcv)
## 'tensor product smooth' of x and y
m1 <- gam(Year ~ te(xpos, ypos), data = dd)
gg0 <- ggplot(dd, aes(xpos, ypos)) +
    geom_point() +
    theme_classic()
## points only, coloured
## gg0 + aes(colour = Year)  + scale_colour_viridis_c()

Construct a prediction grid and fill it in:

pframe <- with(dd,
               expand.grid(xpos = seq(min(xpos), max(xpos), length.out = 41),
                           ypos = seq(min(ypos), max(ypos), length.out = 41)))
pframe$Year <- predict(m1, newdata = pframe)   

Plot:

gg0 + geom_contour_filled(data = pframe, aes(z = Year),
                          alpha = 0.8,
                          breaks = seq(2010,2022, by = 2))
ggsave("rings.png")

enter image description here

I haven't figured out an easy way to get rid of the artifacts yet.

  • generate contours by hand with contourLines(), edit out the bits you don't want;
  • add pseudo-data in lower left and upper right corners?
  • soap-film smoother over a specified area?

Re-doing for x>4300 and with adjusted labels

yrlabs <- paste(2010:2021, 2011:2022, sep = "-")
gg1 <- gg0 + geom_contour_filled(data = pframe, aes(z = Year),
                          alpha = 0.8,
                          breaks = seq(2010,2022)) +
    scale_fill_viridis_d(name = "period", labels = yrlabs)

enter image description here

Ben Bolker
  • 211,554
  • 25
  • 370
  • 453
  • That is wonderful, Ben. Thank you very much. As you can see, in large parts of the advancing half-circle the fungal front gets "interrupted" and no fruitbodies appear after 2014-15, while the model naturally assumes that the front is uninterrupted. Thus, I ran your model on only X={4300-5500} where fungi are seen all years. This works beautifully with no artifacts. :) Just by the way, is there a better way than scale_fill_discrete() to change the legend text styles from (2009-2010) to 2009-10? Using scale_fill_discrete works but overrides the colour selection you have. – Christoffer Bugge Harder Sep 15 '22 at 13:06
  • If you're trying to emulate ggplot's default color scheme, you can check out [this question](https://stackoverflow.com/questions/8197559/emulate-ggplot2-default-color-palette) – Michael Dewar Sep 15 '22 at 14:00
0

Update II: Ben Bolker´s excellent reply did the trick for me.

enter image description here

Here, I added some features and did annual breaks. I used it on the part of the front where all years had fungal fruitbodies - no artifacts in this case. The mathematical model really does not make sense for the years where we know that there were no fungi post 2014-15, so better not apply it to all the data.