--- title: "Correlation wheel plots with circlecorR" output: rmarkdown::html_vignette bibliography: references.bib vignette: > %\VignetteIndexEntry{Correlation wheel plots with circlecorR} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 8, fig.height = 7, dpi = 110, fig.align = "center" ) ``` ```{r setup} library(circlecorR) ``` ## Motivation A traditional correlation matrix carries a large amount of **redundant information**. For $k$ variables it has $k^2$ cells, but only $k(k-1)/2$ of them are unique: the diagonal is all self-correlations ($r = 1$), the two triangles mirror each other, and large blocks of *within-category* correlations are often irrelevant, and contribute to reduced statistical power. As $k$ grows, the interesting **between-category** relationships are buried in this redundancy and the figure becomes unreadable. The **correlation wheel** was introduced by Gharibans et al in the first clinical paper to show that spatial dysrhythmias as measured on the body surface, correlated with gastroduodenal symptoms. The correlation wheel shows variables at the periphery of a circle, grouped by category, and only the correlations of interest are drawn as curved links coloured by the strength of their coefficient, and filtered based on custom statistical significance thresholds. `circlecorR` reproduces these features to generate a correlation wheel plot using standard patient-per-row type dataframes in R, with optionality to customise the grouping, colours, and statistics. Since then, several notable papers in the field of gastric electrophysiology have leveraged these plots, including: * Gharibans AA, Coleman TP, Mousa H, Kunkel DC. Spatial patterns from high-resolution electrogastrography correlate with severity of symptoms in patients with functional dyspepsia and gastroparesis. Clin Gastroenterol Hepatol. 2019 Dec;17(13):2668–77. DOI: [10.1016/j.cgh.2019.04.039](https://doi.org/10.1016/j.cgh.2019.04.039) * Gharibans AA, Calder S, Varghese C, Waite S, Schamberg G, Daker C, et al. Gastric dysfunction in patients with chronic nausea and vomiting syndromes defined by a noninvasive gastric mapping device. Sci Transl Med. 2022 Sept 21;14(663):eabq3544. DOI: [10.1126/scitranslmed.abq3544](https://pmc.ncbi.nlm.nih.gov/articles/PMC10042458/) * Wang TH-H, Varghese C, Calder S, Gharibans A, Schamberg G, Bartlett A, et al. Long-term evaluation of gastric electrophysiology, symptoms and quality of life after pancreaticoduodenectomy. HPB (Oxford). 2025 Dec;27(12):1535–42. DOI: [10.1016/j.hpb.2025.09.003](https://doi.org/10.1016/j.hpb.2025.09.003) * Xu W, Wang T, Foong D, Schamberg G, Evennett N, Beban G, et al. Characterization of gastric dysfunction after fundoplication using body surface gastric mapping. J Gastrointest Surg. 2024 Mar;28(3):236–45. DOI: [10.1016/j.gassur.2023.12.023](https://doi.org/10.1016/j.gassur.2023.12.023) * Xu W, Gharibans AA, Calder S, Schamberg G, Walters A, Jang J, et al. Defining and phenotyping gastric abnormalities in long-term type 1 diabetes using a novel body surface gastric mapping device. Gastro Hep Adv. 2023 Aug 18;2(8):1120–32. DOI: [10.1016/j.gastha.2023.08.005](https://doi.org/10.1016/j.gastha.2023.08.005) ## Installation ```r # install.packages("remotes") remotes::install_github("kriz98/circlecorR", build_vignettes = TRUE) ``` *(CRAN release pending.)* Dependencies (`circlize` and `psych`) install automatically with the command above. ## Quick start: straight from your data `circlecorR` is designed to work straight from a data frame -- **one row per subject** and **one column per variable** -- with no separate step to build correlation matrices yourself. Hand that data frame directly to `corr_wheel()` and it computes the correlations (and p-values) for you. The only other thing you supply is `groups`: a named list mapping each category to its variables. This both **selects** which variables appear (any other columns, such as an ID, are ignored) and sets their **order** around the wheel. In homage to Gharibans et al seminal paper, we use a synthetic `gastro_symptoms` dataset throughout this vignette (see `?gastro_symptoms` for its exact shape). This synthetic example dataset is bundled with the package. Swap it for your own data frame (one row per subject) to use your own data. ```{r groups} groups <- list( Demographics = c("Age", "BMI"), Metrics = c("Amplitude", "Fed-Fasted AR", "Frequency", "GA-RI"), Symptoms = c("Nausea", "Early satiety", "Bloating", "Upper GI pain", "Lower GI pain", "Heartburn"), Scores = c("GCSI", "PAGI-SYM", "PAGI-QoL", "EQ-5D") ) ``` ```{r raw-data} # `gastro_symptoms` is a synthetic per-row example dataset shipped with the package head(gastro_symptoms[, 1:5]) corr_wheel( gastro_symptoms, # raw data: one row per subject groups = groups, method = "pearson", # correlation method adjust = "hochberg", # multiple-comparison adjustment (see below) sig_level = 0.05, # hide links with adjusted p > 0.05 r_threshold = 0.3, # ...and links with |r| < 0.3 r_limits = c(-0.6, 0.6) ) ``` ## Hiding self- and within-category correlations Two of the wheel's most important features are that it **never draws self-correlations** (the diagonal) and, by default, **hides within-category correlations** (`hide_within_group = TRUE`). This removes the redundant parts of the matrix and leaves the between-category structure. Crucially, this is carried through to the **statistics**. Multiple-comparison correction penalises you for the number of hypotheses tested. If self- and within-category correlations are never tested, they should not count towards that family. `corr_wheel()` therefore applies the adjustment over **only the correlations it displays**. Shrinking the family makes the correction less severe, thus improving power, while remaining statistically consistent with what is shown. ```{r family} res <- corr_wheel(gastro_symptoms, groups = groups, adjust = "hochberg", r_threshold = 0.3, r_limits = c(-0.6, 0.6)) k <- length(unlist(groups)) cat("Unique correlations in the full matrix:", k * (k - 1) / 2, "\n") cat("Correlations actually tested (the family):", res$n_tests, "\n") ``` Because the family is smaller, each raw p-value is corrected by a smaller factor. Taking Bonferroni for a transparent example, the *same* correlation is penalised by the number of tests in its family -- here `r res$n_tests` rather than `r k * (k - 1) / 2`: ```{r power} p_raw <- 5e-4 # a raw p-value for one correlation k_all <- k * (k - 1) / 2 cat("Bonferroni across the full matrix:", signif(p_raw * k_all, 3), "\n") cat("Bonferroni across the family only:", signif(p_raw * res$n_tests, 3), "\n") ``` The step-up methods (`"holm"`, `"hochberg"`, `"BH"`) behave the same way. If you want the correlation and p-value matrices themselves, for a table, a report, or any other purpose -- `compute_correlations()` is the same function `corr_wheel()` calls internally, available on its own: ```{r compute-correlations} cc <- compute_correlations(gastro_symptoms, method = "pearson") str(cc) ``` ## Customising the look ### Colour schemes `scheme` sets the category colours and the diverging link palette together, as one named preset. Built-in options are listed by `corr_wheel_schemes()`: ```{r schemes-list} corr_wheel_schemes() ``` * `"default"` -- the seaborn-like categorical palette and a blue-white-red diverging scale used throughout this vignette. * `"colorblind"` -- the Okabe-Ito categorical palette with a colourblind-safe (PuOr) diverging scale. * `"ocean"` -- hue-varied cool tones (teal, blue, indigo, violet, slate) for categories, with a blue-to-warm diverging scale. * `"vivid"` -- a brighter, higher-contrast categorical palette, still paired with the blue-white-red diverging scale. * `"alimetry"` -- black and shades of blue through cyan for most categories, with a single gold used as a sparing highlight, and a matching blue-to-yellow diverging scale. ```{r scheme-colorblind} corr_wheel(gastro_symptoms, groups = groups, r_threshold = 0.3, r_limits = c(-0.6, 0.6), scheme = "colorblind") ``` ```{r scheme-alimetry} corr_wheel(gastro_symptoms, groups = groups, r_threshold = 0.3, r_limits = c(-0.6, 0.6), scheme = "alimetry") ``` `corr_wheel_scheme()` returns a scheme's definition so you can start from a preset and tweak it -- here, lightening the midpoint of the diverging scale: ```{r scheme-tweak} s <- corr_wheel_scheme("ocean") s$palette[2] <- "grey96" corr_wheel(gastro_symptoms, groups = groups, r_threshold = 0.3, r_limits = c(-0.6, 0.6), scheme = s) ``` Or build one entirely from scratch by passing `list(colors = , palette = )` directly -- `colors` is an unnamed vector cycled across however many categories you have, and `palette` is the length-3 diverging scale (negative, midpoint, positive): ```{r scheme-custom} corr_wheel(gastro_symptoms, groups = groups, r_threshold = 0.3, r_limits = c(-0.6, 0.6), scheme = list(colors = c("#7B2CBF", "#2A9D8F", "#E76F51", "#264653"), palette = c("#2A9D8F", "white", "#E76F51"))) ``` ### Colours and labels `colors` maps categories to colours, layered on top of `scheme` (or the default palette if `scheme` is `NULL`) -- only the categories you name are overridden. `labels` gives pretty display names. Both are named vectors -- specify only the ones you want to change. ```{r colours} corr_wheel( gastro_symptoms, groups = groups, r_threshold = 0.3, r_limits = c(-0.6, 0.6), scheme = "colorblind", colors = c(Scores = "black"), # override just one category labels = c("GA-RI" = "Rhythm index") ) ``` ### Size of blocks and lines * `tile_height` -- radial thickness of the category **blocks** (smaller = thinner). * `link_lwd` -- **line** width of the links (larger = thicker). ```{r sizes} corr_wheel( gastro_symptoms, groups = groups, r_threshold = 0.3, r_limits = c(-0.6, 0.6), tile_height = 0.12, # thicker blocks link_lwd = 3 # thicker lines ) ``` ### The diverging colour scale `palette` sets the three colours at `c(-limit, 0, +limit)` (overriding `scheme`'s) and `r_limits` the scale range. ```{r palette} corr_wheel( gastro_symptoms, groups = groups, r_threshold = 0.3, palette = c("#2166AC", "white", "#B2182B"), # blue - white - red r_limits = c(-0.5, 0.5) ) ``` ## Saving to a file `corr_wheel()` draws on the active graphics device, so save it the usual way: ```{r save, eval = FALSE} png("correlation_wheel.png", width = 2500, height = 2000, res = 300) corr_wheel(gastro_symptoms, groups = groups, r_threshold = 0.3, r_limits = c(-0.6, 0.6)) dev.off() ``` ## Argument reference Everything covered above, at a glance: | What | Argument | Example | |------|----------|---------| | Category assignment & order | `groups` | named list *or* `variable = category` vector | | Colour scheme (category colours + link palette, together) | `scheme` | `"colorblind"`, `"ocean"`, `"vivid"`, `"alimetry"`, or `list(colors=, palette=)` | | Category colours | `colors` | `c(Symptoms = "#55A868", Scores = "#C44E52")` (overrides `scheme` per category) | | Pretty variable labels | `labels` | `c("GA-RI" = "Rhythm index")` | | Significance cutoff | `sig_level` | `0.05` | | Multiple-comparison adjustment | `adjust` | `"holm"`, `"hochberg"`, `"BH"`, `"none"` | | Minimum \|r\| shown | `r_threshold` | `0.3` | | Hide within-category links | `hide_within_group` | `TRUE` / `FALSE` | | Colour-scale range | `r_limits` | `c(-0.5, 0.5)` | | Link colour ramp | `palette` | `c("#2166AC", "white", "#B2182B")` (overrides `scheme`'s) | | Block size (thickness) | `tile_height` | `0.06` (thin) … `0.12` (thick) | | Line size (width) | `link_lwd` | `1.6`, `3` | | Rotation / spacing | `start_degree`, `group_gap`, `node_gap` | | | Legend / colour bar | `legend`, `colorbar` | | `corr_wheel()` returns (invisibly) the ordered variables, resolved group and colour maps, the colour function, and the masked matrix actually plotted -- handy for reproducibility or building a caption. ## References