---
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
