| Title: | Dual-Tone and Contrast-Aware 'ggplot2' Geoms |
|---|---|
| Description: | Provides dual-stroke and contrast-aware extensions to 'ggplot2', designed for improved visibility and accessibility in complex visualizations. Includes geoms for dual-stroke segments, regression lines, curved annotations, function plots, paths, and adaptive text. Also includes utility functions for computing contrast-aware color pairs and perceptually distinct highlight palettes using WCAG-based contrast logic. |
| Authors: | Beenu Sareena [aut, cre] (ORCID: <https://orcid.org/0009-0002-7611-5499>), Heike Hofmann [aut] (ORCID: <https://orcid.org/0000-0001-6216-5183>) |
| Maintainer: | Beenu Sareena <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-07-20 07:26:33 UTC |
| Source: | https://github.com/bwanniarachchige2/ggtwotone |
Given a base color and a background, generate a pair of colors (light and dark) with sufficient perceptual contrast using WCAG or APCA methods.
adjust_contrast_pair( color, contrast = 4.5, method = "auto", background = "#FFFFFF", quiet = FALSE )adjust_contrast_pair( color, contrast = 4.5, method = "auto", background = "#FFFFFF", quiet = FALSE )
color |
A base color, as a hex string or valid R color name (e.g., "#6699CC", "darkred"). |
contrast |
Minimum desired contrast ratio (default is 4.5). |
method |
Contrast method: "WCAG", "APCA", or "auto" to try both. |
background |
Background color, as a hex string or valid R color name (default: "#FFFFFF"). |
quiet |
Logical. If TRUE, suppresses warnings. |
A list with elements light, dark, contrast, and method.
adjust_contrast_pair("#777777", contrast = 4.5, method = "auto", background = "#000000") adjust_contrast_pair("#66CCFF", contrast = 4.5, method = "APCA", background = "#FAFAFA")adjust_contrast_pair("#777777", contrast = 4.5, method = "auto", background = "#000000") adjust_contrast_pair("#66CCFF", contrast = 4.5, method = "APCA", background = "#FAFAFA")
geom_curve_dual() draws a dual-tone curved line using two strokes
(light/dark), with slight perpendicular offset to ensure visibility
across mixed backgrounds.
geom_curve_dual( mapping = NULL, data = NULL, stat = "identity", position = "identity", ..., curvature = NULL, angle = NULL, ncp = NULL, base_color = NULL, base_colour = NULL, contrast = 4.5, method_contrast = "WCAG", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE )geom_curve_dual( mapping = NULL, data = NULL, stat = "identity", position = "identity", ..., curvature = NULL, angle = NULL, ncp = NULL, base_color = NULL, base_colour = NULL, contrast = 4.5, method_contrast = "WCAG", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE )
mapping |
Set of aesthetic mappings created by |
data |
The data to be displayed in this layer. There are three options: If A A |
stat |
The statistical transformation to use on the data for this layer.
When using a
|
position |
A position adjustment to use on the data for this layer. This
can be used in various ways, including to prevent overplotting and
improving the display. The
|
... |
Other arguments passed on to
|
curvature |
Bend of the curve (positive = counter-clockwise). |
angle |
Angle between the two control points (default: 90). |
ncp |
Number of control points (default: 5). |
base_color, base_colour
|
Base color to derive the dual-tone pair from. |
contrast |
Minimum contrast ratio to aim for (default is 4.5). |
method_contrast |
Contrast algorithm to use ("WCAG", "APCA", or "auto"). |
na.rm |
If |
show.legend |
logical. Should this layer be included in the legends?
|
inherit.aes |
If |
library(ggplot2) # Basic dual-stroke curve ggplot() + geom_curve_dual( aes(x = 1, y = 1, xend = 4, yend = 4), curvature = 0.4, linewidth = 2 ) + theme_void() + theme(panel.background = element_rect(fill = "black")) b <- ggplot(mtcars, aes(wt, mpg)) + geom_point(size = 2) + theme_dark() df <- data.frame(x1 = 2.62, x2 = 3.57, y1 = 21.0, y2 = 15.0) b + geom_curve_dual( data = df, mapping = aes(x = x1, y = y1, xend = x2, yend = y2), curvature = -0.2, linewidth = 2, base_color = "green" ) # Sine wave style dual-stroke curve with points ggplot() + geom_curve_dual( aes(x = 2, y = 1, xend = 4, yend = 3), curvature = 0.3, linewidth = 2, base_color = "red" ) + geom_point(aes(x = 2, y = 1), colour = "red", size = 3) + geom_point(aes(x = 4, y = 3), colour = "blue", size = 3) + theme_dark(base_size = 14) # Curve on a grayscale tile background tile_df <- expand.grid(x = 1:6, y = 1:4) tile_df$fill <- gray.colors(nrow(tile_df), start = 0, end = 1) ggplot() + geom_tile( data = tile_df, aes(x = x, y = y, fill = fill), width = 1, height = 1 ) + geom_curve_dual( data = data.frame(x = 1, y = 1, xend = 6, yend = 4), aes(x = x, y = y, xend = xend, yend = yend), colour1 = "white", colour2 = "black", curvature = 0.4, linewidth = 2 ) + scale_fill_identity() + theme_void() + coord_fixed()library(ggplot2) # Basic dual-stroke curve ggplot() + geom_curve_dual( aes(x = 1, y = 1, xend = 4, yend = 4), curvature = 0.4, linewidth = 2 ) + theme_void() + theme(panel.background = element_rect(fill = "black")) b <- ggplot(mtcars, aes(wt, mpg)) + geom_point(size = 2) + theme_dark() df <- data.frame(x1 = 2.62, x2 = 3.57, y1 = 21.0, y2 = 15.0) b + geom_curve_dual( data = df, mapping = aes(x = x1, y = y1, xend = x2, yend = y2), curvature = -0.2, linewidth = 2, base_color = "green" ) # Sine wave style dual-stroke curve with points ggplot() + geom_curve_dual( aes(x = 2, y = 1, xend = 4, yend = 3), curvature = 0.3, linewidth = 2, base_color = "red" ) + geom_point(aes(x = 2, y = 1), colour = "red", size = 3) + geom_point(aes(x = 4, y = 3), colour = "blue", size = 3) + theme_dark(base_size = 14) # Curve on a grayscale tile background tile_df <- expand.grid(x = 1:6, y = 1:4) tile_df$fill <- gray.colors(nrow(tile_df), start = 0, end = 1) ggplot() + geom_tile( data = tile_df, aes(x = x, y = y, fill = fill), width = 1, height = 1 ) + geom_curve_dual( data = data.frame(x = 1, y = 1, xend = 6, yend = 4), aes(x = x, y = y, xend = xend, yend = yend), colour1 = "white", colour2 = "black", curvature = 0.4, linewidth = 2 ) + scale_fill_identity() + theme_void() + coord_fixed()
Draws a function (e.g., density or mathematical curve) using perceptually offset dual-stroke curved line segments.
geom_function_dual( fun, xlim = c(-3, 3), n = 701, curvature = 0, angle = 90, ncp = 5, colour1 = NULL, colour2 = NULL, base_color = NULL, contrast = 4.5, method_contrast = "WCAG", linewidth = 1.2, args = list(), smooth = TRUE, color1 = NULL, color2 = NULL, alpha = 1, ... )geom_function_dual( fun, xlim = c(-3, 3), n = 701, curvature = 0, angle = 90, ncp = 5, colour1 = NULL, colour2 = NULL, base_color = NULL, contrast = 4.5, method_contrast = "WCAG", linewidth = 1.2, args = list(), smooth = TRUE, color1 = NULL, color2 = NULL, alpha = 1, ... )
fun |
A function to evaluate (e.g., |
xlim |
Range of x-values to evaluate over (numeric vector of length 2). |
n |
Number of segments to compute (default: 201). |
curvature, angle, ncp
|
Passed to underlying |
colour1, colour2
|
Fixed top/bottom stroke colours. If only colour1 given, colour2 is derived for contrast. (Aliases color1/color2 also accepted.) |
base_color |
Optional base color to derive a contrast pair (overrides colour1/colour2 if supplied). |
contrast, method_contrast
|
Passed to adjust_contrast_pair() when deriving colors. |
linewidth |
Stroke width for the top line. |
args |
List of arguments passed to |
smooth |
Use smooth dual-stroke curves ( |
color1, color2
|
U.S.-spelling aliases for |
alpha |
Overall opacity for both strokes (0–1). |
... |
Additional arguments passed to |
A ggplot2 layer with curved segments.
library(ggplot2) base <- ggplot() + xlim(-2.05,2.05) base + geom_function_dual( fun = function(x) 0.5 * exp(-abs(x)), xlim = c(-2, 2), color1 = "#EEEEEE", color2 = "#222222", linewidth = 1, smooth = TRUE ) + theme_dark() ggplot() + geom_function_dual( fun = dnorm, xlim = c(-5, 5), base_color = "green", linewidth = 1, smooth = TRUE ) + geom_function_dual( fun = dt, args = list(df = 1), xlim = c(-5, 5), base_color = "brown", linewidth = 1, smooth = TRUE ) + theme_dark()library(ggplot2) base <- ggplot() + xlim(-2.05,2.05) base + geom_function_dual( fun = function(x) 0.5 * exp(-abs(x)), xlim = c(-2, 2), color1 = "#EEEEEE", color2 = "#222222", linewidth = 1, smooth = TRUE ) + theme_dark() ggplot() + geom_function_dual( fun = dnorm, xlim = c(-5, 5), base_color = "green", linewidth = 1, smooth = TRUE ) + geom_function_dual( fun = dt, args = list(df = 1), xlim = c(-5, 5), base_color = "brown", linewidth = 1, smooth = TRUE ) + theme_dark()
Draws a regression line with perceptually distinct dual-stroke coloring for improved visibility.
geom_lm_dual( data, mapping, method = "lm", formula = y ~ x, base_color = "#777777", contrast = 4.5, method_contrast = "WCAG", ..., linewidth = 1, show.legend = NA )geom_lm_dual( data, mapping, method = "lm", formula = y ~ x, base_color = "#777777", contrast = 4.5, method_contrast = "WCAG", ..., linewidth = 1, show.legend = NA )
data |
A data frame containing the variables. |
mapping |
Aesthetic mapping, must include |
method |
Regression method to use (default is "lm"). |
formula |
Model formula (default is |
base_color |
Base color to derive the dual-tone pair from. |
contrast |
Minimum contrast ratio to aim for (default is 4.5). |
method_contrast |
Contrast algorithm to use ("WCAG", "APCA", or "auto"). |
... |
Additional parameters passed to |
linewidth |
Total visual line thickness in mm (both side strokes together). |
show.legend |
Whether to show legend. |
A ggplot2 layer containing the dual-stroke regression line.
library(ggplot2) # Simple test with linear trend set.seed(42) df <- data.frame(x = 1:100, y = 0.5 * (1:100) + rnorm(100)) ggplot(df, aes(x, y)) + geom_point() + geom_lm_dual(data = df, mapping = aes(x = x, y = y)) + theme_minimal() # Over grayscale tiles x <- seq(1, 11, length.out = 100) y <- 0.5 * x + rnorm(100, 0, 0.3) df1 <- data.frame(x = x, y = y) # Tile fill definitions fill_colors <- data.frame( x = 1:11, fill = c("#000000", "#1b1b1b", "#444444", "#777777", "#aaaaaa", "#dddddd", "#D5D5D5", "#E5E5E5", "#F5F5F5", "#FAFAFA", "#FFFFFF") ) # Expand tile grid and join with fill colors tiles <- expand.grid(x = 1:11, y = seq(0, 1, length.out = 100)) |> merge(fill_colors, by = "x") ggplot() + geom_tile( data = tiles, aes(x = x, y = y, fill = fill), width = 1, height = 10 ) + scale_fill_identity() + geom_point( data = df1, aes(x = x, y = y), colour = "purple", size = 2 ) + ## Uncomment to use points with frames: # geom_point( # data = df1, aes(x = x, y = y), # shape = 21, colour = "white", fill = "black", size = 3 # ) + geom_lm_dual( data = df1, mapping = aes(x = x, y = y), linewidth = 2 ) + coord_fixed() + theme_minimal()library(ggplot2) # Simple test with linear trend set.seed(42) df <- data.frame(x = 1:100, y = 0.5 * (1:100) + rnorm(100)) ggplot(df, aes(x, y)) + geom_point() + geom_lm_dual(data = df, mapping = aes(x = x, y = y)) + theme_minimal() # Over grayscale tiles x <- seq(1, 11, length.out = 100) y <- 0.5 * x + rnorm(100, 0, 0.3) df1 <- data.frame(x = x, y = y) # Tile fill definitions fill_colors <- data.frame( x = 1:11, fill = c("#000000", "#1b1b1b", "#444444", "#777777", "#aaaaaa", "#dddddd", "#D5D5D5", "#E5E5E5", "#F5F5F5", "#FAFAFA", "#FFFFFF") ) # Expand tile grid and join with fill colors tiles <- expand.grid(x = 1:11, y = seq(0, 1, length.out = 100)) |> merge(fill_colors, by = "x") ggplot() + geom_tile( data = tiles, aes(x = x, y = y, fill = fill), width = 1, height = 10 ) + scale_fill_identity() + geom_point( data = df1, aes(x = x, y = y), colour = "purple", size = 2 ) + ## Uncomment to use points with frames: # geom_point( # data = df1, aes(x = x, y = y), # shape = 21, colour = "white", fill = "black", size = 3 # ) + geom_lm_dual( data = df1, mapping = aes(x = x, y = y), linewidth = 2 ) + coord_fixed() + theme_minimal()
Draws a path using two side-by-side strokes with separate colours, improving visibility across mixed or low-contrast backgrounds.
geom_path_dual( mapping = NULL, data = NULL, stat = "identity", position = "identity", ..., colour1 = NULL, colour2 = NULL, linewidth = NULL, lineend = "round", linejoin = "round", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE )geom_path_dual( mapping = NULL, data = NULL, stat = "identity", position = "identity", ..., colour1 = NULL, colour2 = NULL, linewidth = NULL, lineend = "round", linejoin = "round", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE )
mapping |
Set of aesthetic mappings created by |
data |
The data to be displayed in this layer. There are three options: If A A |
stat |
The statistical transformation to use on the data for this layer.
When using a
|
position |
A position adjustment to use on the data for this layer. This
can be used in various ways, including to prevent overplotting and
improving the display. The
|
... |
Other arguments passed on to
|
colour1 |
Colour for one side of the path. |
colour2 |
Colour for the other side of the path. |
linewidth |
Width of the full dual-stroke path. |
lineend |
Line end style. |
linejoin |
Line join style. |
na.rm |
If |
show.legend |
logical. Should this layer be included in the legends?
|
inherit.aes |
If |
A ggplot2 layer.
library(ggplot2) df <- data.frame( x = c(1, 2, 3, 4), y = c(1, 3, 2, 4) ) ggplot(df, aes(x, y)) + geom_path_dual( colour1 = "#303030", colour2 = "#E8E8E8", linewidth = 8 ) + theme_minimal() library(grid) # simple test data df <- data.frame( x = seq(0, 10, length.out = 300) ) df$y <- sin(df$x) + 0.2 * cos(3 * df$x) # striped background for visibility testing bg <- data.frame( xmin = seq(0, 9, by = 1), xmax = seq(1, 10, by = 1), ymin = -Inf, ymax = Inf, fill = gray(seq(0.15, 0.85, length.out = 10)) ) ggplot() + geom_rect( data = bg, aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = fill), inherit.aes = FALSE ) + scale_fill_identity() + geom_path_dual( data = df, aes(x = x, y = y), color = "black", color2 = "white", linewidth = 2 ) + theme_minimal(base_size = 14)library(ggplot2) df <- data.frame( x = c(1, 2, 3, 4), y = c(1, 3, 2, 4) ) ggplot(df, aes(x, y)) + geom_path_dual( colour1 = "#303030", colour2 = "#E8E8E8", linewidth = 8 ) + theme_minimal() library(grid) # simple test data df <- data.frame( x = seq(0, 10, length.out = 300) ) df$y <- sin(df$x) + 0.2 * cos(3 * df$x) # striped background for visibility testing bg <- data.frame( xmin = seq(0, 9, by = 1), xmax = seq(1, 10, by = 1), ymin = -Inf, ymax = Inf, fill = gray(seq(0.15, 0.85, length.out = 10)) ) ggplot() + geom_rect( data = bg, aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = fill), inherit.aes = FALSE ) + scale_fill_identity() + geom_path_dual( data = df, aes(x = x, y = y), color = "black", color2 = "white", linewidth = 2 ) + theme_minimal(base_size = 14)
Draws two side-by-side line segments with separate colours for improved visibility on varied backgrounds.
geom_segment_dual( mapping = NULL, data = NULL, stat = "identity", position = "identity", colour1 = NULL, colour2 = NULL, linewidth = NULL, lineend = "butt", aspect_ratio = 1, ..., arrow = NULL, arrow.fill = NULL, na.rm = FALSE, show.legend = NA, inherit.aes = TRUE )geom_segment_dual( mapping = NULL, data = NULL, stat = "identity", position = "identity", colour1 = NULL, colour2 = NULL, linewidth = NULL, lineend = "butt", aspect_ratio = 1, ..., arrow = NULL, arrow.fill = NULL, na.rm = FALSE, show.legend = NA, inherit.aes = TRUE )
mapping |
Set of aesthetic mappings created by |
data |
The data to be displayed in this layer. There are three options: If A A |
stat |
The statistical transformation to use on the data for this layer.
When using a
|
position |
A position adjustment to use on the data for this layer. This
can be used in various ways, including to prevent overplotting and
improving the display. The
|
colour1 |
Colour for one side of the dual stroke. |
colour2 |
Colour for the other side of the dual stroke. |
linewidth |
Width of the total dual stroke (in mm). |
lineend |
Line end style (round, butt, square). |
aspect_ratio |
Aspect ratio hint (currently unused by the grob logic but reserved for future layout tuning). |
... |
Other arguments passed on to
|
arrow |
specification for arrow heads, as created by |
arrow.fill |
fill colour to use for the arrow head (if closed). |
na.rm |
If |
show.legend |
logical. Should this layer be included in the legends?
|
inherit.aes |
If |
Dual-Stroke Line Segments with Side-by-Side Offset
Draws two side-by-side line segments with separate colours for improved visibility on varied backgrounds.
# Simple black background test ggplot(data.frame(x = 1, xend = 2, y = 1, yend = 2), aes(x = x, y = y, xend = xend, yend = yend)) + geom_segment_dual(colour1 = "white", colour2 = "black", linewidth = 2) + theme_void() + theme(panel.background = element_rect(fill = "gray20")) # Dual-stroke diagonal lines crossing contrasting backgrounds bg <- data.frame( xmin = c(0, 5), xmax = c(5, 10), ymin = 0, ymax = 5, fill = c("black", "white") ) line_data <- data.frame( x = c(1, 9), y = c(1, 1), xend = c(9, 1), yend = c(4, 4), colour1 = c("#D9D9D9", "#D9D9D9"), # light stroke colour2 = c("#333333", "#333333") # dark stroke ) ggplot() + geom_rect(data = bg, aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = fill), inherit.aes = FALSE) + scale_fill_identity() + geom_segment_dual( data = line_data, aes(x = x, y = y, xend = xend, yend = yend), colour1 = line_data$colour1, colour2 = line_data$colour2, linewidth = 1, inherit.aes = FALSE ) + theme_void() + coord_fixed() + ggtitle("Two Diagonal Dual-Stroke Lines in Opposite Directions") # Multiple dual-stroke segments with arrowheads and grouping df <- data.frame( x = c(1, 2, 3), xend = c(2, 3, 4), y = c(1, 2, 1), yend = c(2, 1, 2), colour1 = rep("white", 3), colour2 = rep("black", 3), group = factor(c("A", "B", "C")) ) ggplot(df) + geom_segment_dual( aes(x = x, y = y, xend = xend, yend = yend, group = group), colour1 = df$colour1, colour2 = df$colour2, linewidth = 1, arrow = arrow(length = unit(0.15, "inches"), type = "closed") ) + coord_fixed() + theme_dark()# Simple black background test ggplot(data.frame(x = 1, xend = 2, y = 1, yend = 2), aes(x = x, y = y, xend = xend, yend = yend)) + geom_segment_dual(colour1 = "white", colour2 = "black", linewidth = 2) + theme_void() + theme(panel.background = element_rect(fill = "gray20")) # Dual-stroke diagonal lines crossing contrasting backgrounds bg <- data.frame( xmin = c(0, 5), xmax = c(5, 10), ymin = 0, ymax = 5, fill = c("black", "white") ) line_data <- data.frame( x = c(1, 9), y = c(1, 1), xend = c(9, 1), yend = c(4, 4), colour1 = c("#D9D9D9", "#D9D9D9"), # light stroke colour2 = c("#333333", "#333333") # dark stroke ) ggplot() + geom_rect(data = bg, aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = fill), inherit.aes = FALSE) + scale_fill_identity() + geom_segment_dual( data = line_data, aes(x = x, y = y, xend = xend, yend = yend), colour1 = line_data$colour1, colour2 = line_data$colour2, linewidth = 1, inherit.aes = FALSE ) + theme_void() + coord_fixed() + ggtitle("Two Diagonal Dual-Stroke Lines in Opposite Directions") # Multiple dual-stroke segments with arrowheads and grouping df <- data.frame( x = c(1, 2, 3), xend = c(2, 3, 4), y = c(1, 2, 1), yend = c(2, 1, 2), colour1 = rep("white", 3), colour2 = rep("black", 3), group = factor(c("A", "B", "C")) ) ggplot(df) + geom_segment_dual( aes(x = x, y = y, xend = xend, yend = yend, group = group), colour1 = df$colour1, colour2 = df$colour2, linewidth = 1, arrow = arrow(length = unit(0.15, "inches"), type = "closed") ) + coord_fixed() + theme_dark()
Automatically adjusts text colour for readability on varying background colours using WCAG or APCA contrast.
geom_text_contrast( mapping = NULL, data = NULL, stat = "identity", position = "identity", ..., method = "auto", contrast = 4.5, base_colour = NULL, na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, background = NULL )geom_text_contrast( mapping = NULL, data = NULL, stat = "identity", position = "identity", ..., method = "auto", contrast = 4.5, base_colour = NULL, na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, background = NULL )
mapping |
Set of aesthetic mappings created by |
data |
The data to be displayed in this layer. There are three options: If A A |
stat |
The statistical transformation to use on the data for this layer.
When using a
|
position |
A position adjustment to use on the data for this layer. This
can be used in various ways, including to prevent overplotting and
improving the display. The
|
... |
Other arguments passed on to
|
method |
Contrast method to use: |
contrast |
Threshold to ensure between text and background (defaults to 4.5). |
base_colour |
Base text colour used to generate light/dark variants. |
na.rm |
If |
show.legend |
logical. Should this layer be included in the legends?
|
inherit.aes |
If |
background |
A character vector of background fill colours (hex codes), used for contrast computation. |
library(ggplot2) # Grayscale A–E tiles: testing contrast df <- data.frame( x = 1:5, y = 1, label = LETTERS[1:5], fill = c("#000000", "#222222", "#666666", "#DDDDDD", "#FFFFFF") ) ggplot(df, aes(x, y)) + geom_tile(aes(fill = fill), width = 1, height = 1) + geom_text_contrast( aes(label = label), background = df$fill, size = 7 ) + scale_fill_identity() + coord_fixed() + theme_void() + labs(title = "Contrast-Aware Text on Varying Backgrounds") library(dplyr) library(scales) set.seed(1) classes <- c("Cat", "Dog", "Rabit") cm <- expand.grid(True = classes, Predicted = classes) cm$Count <- sample(5:200, size = nrow(cm), replace = TRUE) cm <- cm %>% group_by(True) %>% mutate(Accuracy = Count / sum(Count)) pal <- c("#313695", "#74add1", "#fdae61", "#a50026") col_fun <- col_numeric(palette = pal, domain = c(0, 1)) cm$fill_hex <- col_fun(cm$Accuracy) cm$label <- sprintf("%.1f%%", 100 * cm$Accuracy) ggplot(cm, aes(Predicted, True)) + geom_tile(aes(fill = Accuracy), color = "white", linewidth = 0.8) + geom_text_contrast( aes(label = label), background = cm$fill_hex, base_colour = "#004488", method = "auto", contrast = 4.5, size = 5, fontface = "bold" ) + scale_fill_gradientn( colours = pal, limits = c(0, 1), name = "Accuracy" ) + coord_fixed() + labs( title = "Confusion Matrix with Auto-Contrast Labels", x = "Predicted Class", y = "True Class" ) + theme_minimal(base_size = 13) + theme( panel.grid = element_blank(), axis.text.x = element_text(angle = 45, hjust = 1) ) # Simulated region × risk category df_risk <- expand.grid( region = LETTERS[1:6], zone = paste0("Z", 1:6) ) df_risk$risk_level <- sample( c("Low", "Moderate", "High", "Critical", "Severe", "Extreme"), size = nrow(df_risk), replace = TRUE ) df_risk$label <- paste(df_risk$region, df_risk$zone) risk_colours <- c( "Low" = "gray80", "Moderate" = "skyblue", "High" = "orange", "Critical" = "firebrick", "Severe" = "darkred", "Extreme" = "navy" ) df_risk$fill_colour <- risk_colours[df_risk$risk_level] ggplot(df_risk, aes(x = region, y = zone, fill = risk_level)) + geom_tile(colour = "white") + geom_text_contrast( aes(label = label), background = df_risk$fill_colour, size = 3, fontface = "bold" ) + scale_fill_manual(values = risk_colours) + labs( title = "Simulated Risk Map (Auto Contrast Labels)", fill = "Risk Level" ) + theme_minimal()library(ggplot2) # Grayscale A–E tiles: testing contrast df <- data.frame( x = 1:5, y = 1, label = LETTERS[1:5], fill = c("#000000", "#222222", "#666666", "#DDDDDD", "#FFFFFF") ) ggplot(df, aes(x, y)) + geom_tile(aes(fill = fill), width = 1, height = 1) + geom_text_contrast( aes(label = label), background = df$fill, size = 7 ) + scale_fill_identity() + coord_fixed() + theme_void() + labs(title = "Contrast-Aware Text on Varying Backgrounds") library(dplyr) library(scales) set.seed(1) classes <- c("Cat", "Dog", "Rabit") cm <- expand.grid(True = classes, Predicted = classes) cm$Count <- sample(5:200, size = nrow(cm), replace = TRUE) cm <- cm %>% group_by(True) %>% mutate(Accuracy = Count / sum(Count)) pal <- c("#313695", "#74add1", "#fdae61", "#a50026") col_fun <- col_numeric(palette = pal, domain = c(0, 1)) cm$fill_hex <- col_fun(cm$Accuracy) cm$label <- sprintf("%.1f%%", 100 * cm$Accuracy) ggplot(cm, aes(Predicted, True)) + geom_tile(aes(fill = Accuracy), color = "white", linewidth = 0.8) + geom_text_contrast( aes(label = label), background = cm$fill_hex, base_colour = "#004488", method = "auto", contrast = 4.5, size = 5, fontface = "bold" ) + scale_fill_gradientn( colours = pal, limits = c(0, 1), name = "Accuracy" ) + coord_fixed() + labs( title = "Confusion Matrix with Auto-Contrast Labels", x = "Predicted Class", y = "True Class" ) + theme_minimal(base_size = 13) + theme( panel.grid = element_blank(), axis.text.x = element_text(angle = 45, hjust = 1) ) # Simulated region × risk category df_risk <- expand.grid( region = LETTERS[1:6], zone = paste0("Z", 1:6) ) df_risk$risk_level <- sample( c("Low", "Moderate", "High", "Critical", "Severe", "Extreme"), size = nrow(df_risk), replace = TRUE ) df_risk$label <- paste(df_risk$region, df_risk$zone) risk_colours <- c( "Low" = "gray80", "Moderate" = "skyblue", "High" = "orange", "Critical" = "firebrick", "Severe" = "darkred", "Extreme" = "navy" ) df_risk$fill_colour <- risk_colours[df_risk$risk_level] ggplot(df_risk, aes(x = region, y = zone, fill = risk_level)) + geom_tile(colour = "white") + geom_text_contrast( aes(label = label), background = df_risk$fill_colour, size = 3, fontface = "bold" ) + scale_fill_manual(values = risk_colours) + labs( title = "Simulated Risk Map (Auto Contrast Labels)", fill = "Risk Level" ) + theme_minimal()
Changes from your previous version:
Global hue sampling by default (hue_targets = "auto")
Optional hue repulsion near background/base (repel_from, repel_band)
Smart auto-bias toward opponent hues when bg & base are far apart
(auto_bias)
NEW: contrast_method = "auto" picks APCA for dark-on-light
and WCAG for light-on-dark
highlight_colors( n, background = "#F0F0F0", base_color = "#4a1919", contrast_bg = 4.5, contrast_base = 3, min_deltaE = 35, hue_targets = "auto", anchor_band = 180, hcl_L_range = c(25, 75), hcl_C_range = c(40, 90), min_hue_sep = 35, n_candidates = 1e+05, relax = TRUE, quiet = TRUE, repel_from = c("background", "base"), repel_band = 50, auto_bias = TRUE, bias_width = 120, bias_frac = 0.8, contrast_method = c("option", "auto") )highlight_colors( n, background = "#F0F0F0", base_color = "#4a1919", contrast_bg = 4.5, contrast_base = 3, min_deltaE = 35, hue_targets = "auto", anchor_band = 180, hcl_L_range = c(25, 75), hcl_C_range = c(40, 90), min_hue_sep = 35, n_candidates = 1e+05, relax = TRUE, quiet = TRUE, repel_from = c("background", "base"), repel_band = 50, auto_bias = TRUE, bias_width = 120, bias_frac = 0.8, contrast_method = c("option", "auto") )
n |
Integer, number of colors. |
background |
Hex/R color for the plot background. |
base_color |
Hex/R color used for non-highlight elements. |
contrast_bg |
Required contrast vs background (WCAG ratio or APCA Lc depending on backend). Default 4.5. |
contrast_base |
Required contrast vs base_color (set 0 to skip). Default 3.0. |
min_deltaE |
Minimum CIEDE2000 separation between selected colors. Default 35. |
hue_targets |
"auto", "anchored", "spread", or numeric vector of hue angles (0-360). |
anchor_band |
Degrees around base hue to sample when
|
hcl_L_range |
Allowed HCL lightness range. Default |
hcl_C_range |
Allowed HCL chroma range. Default |
min_hue_sep |
Minimum separation in degrees between chosen hues. Default 35. |
n_candidates |
Number of random candidates before selection. Default 100000. |
relax |
Progressively relax |
quiet |
Suppress relaxation messages. Default TRUE. |
repel_from |
Character vector among |
repel_band |
Degrees excluded around the repelled hues
( |
auto_bias |
Logical; if TRUE and bg/base are far apart, bias sampling toward opponent hues. |
bias_width |
Width (degrees) of opponent sampling window when
|
bias_frac |
Fraction of samples drawn from the biased window when
|
contrast_method |
|
Creates n colors by:
(1) sampling HCL colors (global or biased),
(2) filtering by WCAG/APCA contrast vs background
(and optionally vs base_color),
(3) selecting a maximally separated subset by and minimum hue
spacing.
Set contrast_base = 0 to ignore base contrast
(keeps only background contrast).
Use hue_targets = "anchored" to sample around base_color (legacy-like).
NOTE: Switch contrast backend via:
options(ggtwotone.contrast_method = "WCAG")
options(ggtwotone.contrast_method = "APCA")
or pass contrast_method = "auto" to pick per polarity.
Character vector of hex colors (length <= n) with
an info attribute.
highlight_colors( n = 4, background = "#222222", base_color = "#eeeeee" ) if (requireNamespace("ggplot2", quietly = TRUE)) { library(ggplot2) bg_hex <- "#F7F7F7" base_hex <- "#222222" set.seed(7) pal <- highlight_colors( n = 3, background = bg_hex, base_color = base_hex, contrast_method = "auto", contrast_bg = 60, contrast_base = 45, quiet = TRUE ) classes <- c("compact", "suv", "midsize", "pickup", "minivan", "2seater") counts <- c(25, 38, 42, 31, 10, 5) highlight_classes <- c("compact", "suv", "midsize") col_map <- setNames(rep(base_hex, length(classes)), classes) col_map[highlight_classes] <- pal df <- data.frame( class = classes, count = counts, fill = unname(col_map[classes]) ) ggplot(df, aes(x = reorder(class, count), y = count, fill = fill)) + geom_col(width = 0.8) + scale_fill_identity() + coord_flip() + labs( title = "Dark base on light background (auto -> APCA)", x = NULL, y = "Count" ) + theme_minimal(base_size = 12) + theme( panel.grid.major.y = element_blank(), plot.background = element_rect(fill = bg_hex, color = NA), panel.background = element_rect(fill = bg_hex, color = NA), plot.title = element_text(color = base_hex, hjust = 0.5), axis.text = element_text(color = base_hex) ) } if (requireNamespace("ggplot2", quietly = TRUE)) { library(ggplot2) bg_hex <- "#222222" base_hex <- "#EEEEEE" set.seed(7) pal <- highlight_colors( n = 3, background = bg_hex, base_color = base_hex, contrast_method = "auto", contrast_bg = 4.5, contrast_base = 3.0, quiet = TRUE ) classes <- c("compact", "suv", "midsize", "pickup", "minivan", "2seater") counts <- c(25, 38, 42, 31, 10, 5) highlight_classes <- c("compact", "suv", "midsize") col_map <- setNames(rep(base_hex, length(classes)), classes) col_map[highlight_classes] <- pal df <- data.frame( class = classes, count = counts, fill = unname(col_map[classes]) ) ggplot(df, aes(x = reorder(class, count), y = count, fill = fill)) + geom_col(width = 0.8) + scale_fill_identity() + coord_flip() + labs( title = "Light base on dark background (auto -> WCAG)", x = NULL, y = "Count" ) + theme_minimal(base_size = 12) + theme( panel.grid.major.y = element_blank(), plot.background = element_rect(fill = bg_hex, color = NA), panel.background = element_rect(fill = bg_hex, color = NA), plot.title = element_text(color = base_hex, hjust = 0.5), axis.text = element_text(color = base_hex) ) }highlight_colors( n = 4, background = "#222222", base_color = "#eeeeee" ) if (requireNamespace("ggplot2", quietly = TRUE)) { library(ggplot2) bg_hex <- "#F7F7F7" base_hex <- "#222222" set.seed(7) pal <- highlight_colors( n = 3, background = bg_hex, base_color = base_hex, contrast_method = "auto", contrast_bg = 60, contrast_base = 45, quiet = TRUE ) classes <- c("compact", "suv", "midsize", "pickup", "minivan", "2seater") counts <- c(25, 38, 42, 31, 10, 5) highlight_classes <- c("compact", "suv", "midsize") col_map <- setNames(rep(base_hex, length(classes)), classes) col_map[highlight_classes] <- pal df <- data.frame( class = classes, count = counts, fill = unname(col_map[classes]) ) ggplot(df, aes(x = reorder(class, count), y = count, fill = fill)) + geom_col(width = 0.8) + scale_fill_identity() + coord_flip() + labs( title = "Dark base on light background (auto -> APCA)", x = NULL, y = "Count" ) + theme_minimal(base_size = 12) + theme( panel.grid.major.y = element_blank(), plot.background = element_rect(fill = bg_hex, color = NA), panel.background = element_rect(fill = bg_hex, color = NA), plot.title = element_text(color = base_hex, hjust = 0.5), axis.text = element_text(color = base_hex) ) } if (requireNamespace("ggplot2", quietly = TRUE)) { library(ggplot2) bg_hex <- "#222222" base_hex <- "#EEEEEE" set.seed(7) pal <- highlight_colors( n = 3, background = bg_hex, base_color = base_hex, contrast_method = "auto", contrast_bg = 4.5, contrast_base = 3.0, quiet = TRUE ) classes <- c("compact", "suv", "midsize", "pickup", "minivan", "2seater") counts <- c(25, 38, 42, 31, 10, 5) highlight_classes <- c("compact", "suv", "midsize") col_map <- setNames(rep(base_hex, length(classes)), classes) col_map[highlight_classes] <- pal df <- data.frame( class = classes, count = counts, fill = unname(col_map[classes]) ) ggplot(df, aes(x = reorder(class, count), y = count, fill = fill)) + geom_col(width = 0.8) + scale_fill_identity() + coord_flip() + labs( title = "Light base on dark background (auto -> WCAG)", x = NULL, y = "Count" ) + theme_minimal(base_size = 12) + theme( panel.grid.major.y = element_blank(), plot.background = element_rect(fill = bg_hex, color = NA), panel.background = element_rect(fill = bg_hex, color = NA), plot.title = element_text(color = base_hex, hjust = 0.5), axis.text = element_text(color = base_hex) ) }
Inspect effective palette constraints
highlight_info(pal)highlight_info(pal)
pal |
A palette returned by highlight_colors() |
A named list of effective thresholds and counts.