ggcorrplot draws a correlation matrix as a ggplot2 plot. Because the result is an ordinary ggplot object, you keep the full ggplot2 vocabulary: restyle it with a theme, add layers or annotations, and compose it with other plots using +.
This vignette walks through the package end to end: computing the inputs, choosing a glyph and layout, reordering by clustering, adding the coefficients, marking statistical significance, and restyling the result.
Every plot starts from a correlation matrix. The examples use the built-in mtcars data set.
data(mtcars)
corr <- round(cor(mtcars), 1)
corr[1:5, 1:5]
#> mpg cyl disp hp drat
#> mpg 1.0 -0.9 -0.8 -0.8 0.7
#> cyl -0.9 1.0 0.9 0.8 -0.7
#> disp -0.8 0.9 1.0 0.8 -0.7
#> hp -0.8 0.8 0.8 1.0 -0.4
#> drat 0.7 -0.7 -0.7 -0.4 1.0To mark significance later, you also need the matrix of correlation p-values. cor_pmat() computes it with stats::cor.test():
p.mat <- cor_pmat(mtcars)
p.mat[1:5, 1:5]
#> mpg cyl disp hp drat
#> mpg 0.000000e+00 6.112687e-10 9.380327e-10 1.787835e-07 1.776240e-05
#> cyl 6.112687e-10 0.000000e+00 1.802838e-12 3.477861e-09 8.244636e-06
#> disp 9.380327e-10 1.802838e-12 0.000000e+00 7.142679e-08 5.282022e-06
#> hp 1.787835e-07 3.477861e-09 7.142679e-08 0.000000e+00 9.988772e-03
#> drat 1.776240e-05 8.244636e-06 5.282022e-06 9.988772e-03 0.000000e+00ggcorrplot() never computes correlations itself — it takes a matrix you already have. That means it works equally well with a partial correlation matrix, a distance-derived similarity, or any other square matrix in [-1, 1] (and, with legend.limit = NULL, matrices outside that range such as a covariance matrix).
The default encodes each correlation as a colored square. method = "circle" encodes the value with the circle’s area instead, which reads well when you want the magnitude to pop out.
When method = "circle", circle.scale tunes the circle sizes for your output device.
Ordering the variables by hierarchical clustering brings correlated variables next to each other, so the block structure becomes visible. Set hc.order = TRUE; hc.method chooses the linkage (as in hclust()).
hc.rect then draws rectangles around the clusters obtained by cutting the tree into k groups — a quick way to highlight the blocks.
For a symmetric matrix the two triangles carry the same information, so you can show just one with type = "lower" or type = "upper".
ggcorrplot(corr, hc.order = TRUE, type = "lower", outline.color = "white")
ggcorrplot(corr, hc.order = TRUE, type = "upper", outline.color = "white")A mixed layout draws a different glyph in each triangle. Set lower.method and/or upper.method to "square", "circle", or "number"; the variable names are placed on the diagonal. A common choice is the coefficients as numbers on one side and circles on the other. Adding cell.grid = TRUE boxes every cell so the glyphs sit in a tidy grid:
ggcorrplot(corr,
lower.method = "number", upper.method = "circle",
cell.grid = TRUE, show.legend = FALSE
)With "number", the coefficient text is colored by its value on the fill ramp, darkened over a light background so the polarity still reads at a glance while a near-zero coefficient stays legible. Over a dark background the ramp is used as given, since its pale middle is then the readable end. The background is read from ggtheme, so pass a dark theme there rather than adding it afterwards with +.
lab = TRUE prints the correlation coefficient in each cell. lab_size, lab_col, and lab_fontface control its appearance.
Two options match common conventions for correlation tables:
leading.zero = FALSE drops the leading zero (.85 instead of 0.85).nsmall keeps a fixed number of decimals (e.g. nsmall = 2 shows 0.70).ggcorrplot(cor(mtcars[, 1:6]),
lab = TRUE, lab_size = 3.5,
leading.zero = FALSE, nsmall = 2,
type = "lower"
)Supplying p.mat lets the plot convey which correlations are significant at sig.level (default 0.05). There are three styles, chosen with insig. sig.level sets the cut-off used by the first two; the third labels each cell with stars at the conventional fixed thresholds instead.
insig = "pch" (default) crosses out the non-significant cells:
insig = "blank" hides them entirely:
insig = "stars" flips the emphasis: rather than crossing out the non-significant cells, it marks the significant ones with significance stars (***, **, * for p <= 0.001, 0.01, 0.05 – these thresholds are fixed and do not follow sig.level). With the default lab = FALSE this is a standalone significance map:
With lab = TRUE, the stars are appended to the coefficients instead (-0.85***), so one plot shows both the value and its significance:
colors sets the diverging gradient. A length-3 vector maps to low / mid / high; any longer vector (for example an 11-color palette) is spread evenly across the scale.
ggcorrplot(corr,
hc.order = TRUE, type = "lower", outline.color = "white",
colors = c("#6D9EC1", "white", "#E46726")
)ggtheme swaps the base ggplot2 theme, and tl.cex, tl.col, tl.srt style the variable-name labels. legend.limit controls the color-scale range — set it to NULL to use the data range, which is what you want for a covariance matrix.
ggcorrplot(corr,
hc.order = TRUE, type = "lower",
ggtheme = ggplot2::theme_minimal,
tl.col = "gray30", tl.srt = 90,
colors = c("#003C67", "white", "#8F2727")
)Because ggcorrplot() returns a ggplot object, you refine it with the usual ggplot2 grammar — titles, captions, and any additional layer:
library(ggplot2)
ggcorrplot(corr, hc.order = TRUE, type = "lower", outline.color = "white") +
labs(
title = "Correlations among mtcars variables",
caption = "Hierarchically clustered"
) +
theme(plot.title = element_text(face = "bold"))The fixed 1:1 aspect ratio comes from coord_fixed(). To let the cells fill the plotting area — useful with many long variable names — use coord.fixed = FALSE (or add your own + coord_*, since the last coordinate system wins).
Saving works the same as any ggplot:
cor_pmat()cor_pmat(x, ...) returns the symmetric matrix of p-values from stats::cor.test(), and passes ... through to it — so you can, for example, request a Spearman test:
The use argument controls how missing values are handled when deciding which cells are NA, mirroring stats::cor(): the default "pairwise.complete.obs" tests every pair with enough overlapping observations, while "everything" sets a pair to NA as soon as either variable has a missing value (so its NA pattern lines up with cor(x)).
sessionInfo()
#> R version 4.5.1 (2025-06-13)
#> Platform: aarch64-apple-darwin20
#> Running under: macOS Sequoia 15.6.1
#>
#> Matrix products: default
#> BLAS: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
#>
#> locale:
#> [1] C/fr_FR.UTF-8/fr_FR.UTF-8/C/fr_FR.UTF-8/fr_FR.UTF-8
#>
#> time zone: Europe/Paris
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] ggcorrplot_0.3.0 ggplot2_4.0.3
#>
#> loaded via a namespace (and not attached):
#> [1] gtable_0.3.6 jsonlite_2.0.0 dplyr_1.2.1 compiler_4.5.1
#> [5] tidyselect_1.2.1 Rcpp_1.1.1 stringr_1.6.0 jquerylib_0.1.4
#> [9] scales_1.4.0 yaml_2.3.12 fastmap_1.2.0 R6_2.6.1
#> [13] plyr_1.8.9 labeling_0.4.3 generics_0.1.4 knitr_1.51
#> [17] tibble_3.3.1 bslib_0.10.0 pillar_1.11.1 RColorBrewer_1.1-3
#> [21] rlang_1.2.0 cachem_1.1.0 stringi_1.8.7 xfun_0.57
#> [25] sass_0.4.10 S7_0.2.1 otel_0.2.0 cli_3.6.6
#> [29] withr_3.0.2 magrittr_2.0.4 digest_0.6.39 grid_4.5.1
#> [33] lifecycle_1.0.5 vctrs_0.7.2 evaluate_1.0.5 glue_1.8.0
#> [37] farver_2.1.2 reshape2_1.4.5 rmarkdown_2.31 tools_4.5.1
#> [41] pkgconfig_2.0.3 htmltools_0.5.9