wilkelab / ggridges

Ridgeline plots in ggplot2
https://wilkelab.org/ggridges
GNU General Public License v2.0
412 stars 31 forks source link

Add support for periodic data density #50

Closed thk686 closed 4 years ago

thk686 commented 4 years ago

A feature suggestion: I have data that are seasonal and so are periodic on the boundary. ggridges is the best way I have found to visualize the data across a large number of cases (species).

If there is not currently a way to supply one's own density estimator through the the existing interface (I can't quite tell from the documentation) this could be added to the ggridges code using the circular package. The package has a bandwidth optimizer and density function just as in the R stats package.

clauswilke commented 4 years ago

You can always calculate the densities manually and then plot with geom_ridgeline(). However, I'm not opposed to adding support for this feature, I just would have to have a better sense of what would have to be done. Could you provide a reproducible example that I could work off of? Simple, made-up data is fine.

thk686 commented 4 years ago
library(tidyverse)
library(circular)
library(ggridges)

x <- lapply(1:20, function(i) rvonmises(100, 2 * pi * i / 20, 10))
x <- tibble(id = as.factor(rep(1:20, each = 100)), angle = unlist(x))

ggplot(x) +
  geom_density_ridges(aes(y = id, x = angle))

Take a look at circular::bw.nrd.circular and circular::density.circular

clauswilke commented 4 years ago

I've looked into this some more. I think it's too specialized for ggridges. It would make more sense if the circular package added an appropriate ggplot2 stat, like stat_circular_density(). If it works similar to the regular stat_density() then ggridges can use it. You could propose this to the authors of circular.

Below is an example of how you can create the plot by manually calculating the densities. That's always an option as well.

library(tidyverse)
library(circular)
#> 
#> Attaching package: 'circular'
#> The following objects are masked from 'package:stats':
#> 
#>     sd, var
library(ggridges)
#> 
#> Attaching package: 'ggridges'
#> The following object is masked from 'package:ggplot2':
#> 
#>     scale_discrete_manual

x <- lapply(1:20, function(i) rvonmises(100, 2 * pi * i / 20, 10))
#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter

#> Warning in as.circular(x): an object is coerced to the class 'circular' using default value for the following components:
#>   type: 'angles'
#>   units: 'radians'
#>   template: 'none'
#>   modulo: 'asis'
#>   zero: 0
#>   rotation: 'counter'
#> conversion.circularmuradians0counter
df <- tibble(
  id = as.factor(1:20),
  angles = x
) %>%
  mutate(
    density = map(angles, ~ {
      d <- density(.x, bw = bw.nrd.circular(.x))
      tibble(x = unclass(d$x), y = d$y)
    })
  ) %>%
  select(-angles) %>%
  unnest(cols = density)

ggplot(df) +
  geom_ridgeline(aes(y = id, x = x, height = y))

Created on 2019-12-30 by the reprex package (v0.3.0)