| Title: | Clean, Predictable Figures for Medical and Data Science |
|---|---|
| Description: | A design system for 'ggplot2' figures: one frame for titles, legends, panels and captions, a fixed spacing and type scale, colour-vision checked palettes, brand files in the 'brand.yml' format, and print-ready export. Produces output matching the 'hairline' Python package. |
| Authors: | Nikolas Schrader [aut, cre, cph] (ORCID: <https://orcid.org/0009-0001-4616-3804>) |
| Maintainer: | Nikolas Schrader <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-10-28 19:43:46 UTC |
| Source: | https://github.com/cran/hairline |
hairline picks colours for you: every chart function colours its groups, amounts and highlights on its own. This page shows which colour sets there are, how to choose another one, and how to keep a group's colour the same across a report.
Groups (arms, units, diagnoses): a categorical palette, e.g. hl_use(palette = "calm").
Two groups (intervention, control): a pair, hl_use(palette = "pair").
A printout in grey: "ink", or the palette "deep".
Amounts (counts, rates, densities): a sequential ramp, e.g. scale_fill_hairline_c("blues").
Values above and below a midpoint: a diverging ramp, scale_fill_hairline_c("blue-coral").
One group against the rest: + hl_highlight("Unit B"), or highlight = in hl_timeseries().
A single series: the accent, hl_use(accent = "#0b6e4f").
Risk of bias: the "judgement" set, applied by hl_risk_of_bias().
hl_palettes() lists every colour set with its kind, number of colours,
use, and which one is active; hl_swatches() draws them. The six
categorical palettes have eight colours each. Every set opens with its own
blue, then its own red, so two groups always read blue against red:
"daylight" (vivid; the default), "calm" (softer and lighter, for long
reports), "deep" (stays apart when printed in grey), "lagoon" (cool),
"citrus" (warm) and "bold" (saturated, for slides).
The purpose sets are "pair", "pair-teal" and "pair-print" for two
groups, "ink" for greyscale print and "judgement" for low, some
concerns and high. The ramps are "blues", "teals", "oranges",
"berries" and "greys" (sequential) and "blue-coral" and
"teal-berry" (diverging).
The number of groups decides the colours, the same in every chart: two groups take the first two colours, three groups the first three. The first two, three and four colours of each palette are its most readable sets of that size, for colour-blind readers too, and because the sets are nested a group keeps its colour when more groups are shown. A ninth group is an error, not a repeated colour: fold small groups into "Other" or split the figure into panels.
Within a session a group keeps the colour it got first, in every later
chart. Pin colours to names with hl_use(colors = c("Unit B" = "#e79d01"))
and read them back with hl_color() and hl_colors(). For colours that
stay the same across a report whatever order the charts are drawn in, put
them in the brand file under defaults: hairline: colors:; groups: gives
the units of a section tints of the section's colour.
A _brand.yml file holds an institution's palette (defaults: hairline: categorical:), found from the working directory upwards or given with
hl_use(brand = ); several files layer from institution to project, and
hl_brand_report() shows what was taken and which layer each colour came
from. While a brand is active, its ramps "brand-sequential" and
"brand-diverging" (from its sequential: and diverging: colours, else
its first one and two colours) are the default for hl_ramp(), the
continuous scales and the correlation heatmap. Any vector of hex colours can be
a palette; hl_check_palette() measures how far apart the colours are for
colour-blind readers and everyone else, and their contrast with the
background (pairs = "all" for scatter plots and small multiples).
Colours below 3:1 contrast on white stay as they are for bars and areas;
lines in them are drawn a little wider and points are filled with a darker
shade of the same hue. With hl_use(scheme = "dark") the background, text
and grid greys turn dark; data colours stay, and thin marks are shaded
lighter where needed. Text never takes a data colour.
hl_palettes(), hl_swatches(), hl_palette(), hl_ramp(), hl_use(), hl_check_palette().
hl_palettes("categorical") hl_palette("pair") hl_swatches(c("daylight", "calm", "pair", "blues"))hl_palettes("categorical") hl_palette("pair") hl_swatches(c("daylight", "calm", "pair", "blues"))
Nodes on a baseline and an arc above for each pair that exchanges
something, its width following the value; without value each row counts
once. Labels turn upright when they would collide.
hl_arcs( data, source, target, value = NULL, title = NULL, subtitle = NULL, caption = NULL, order = NULL )hl_arcs( data, source, target, value = NULL, title = NULL, subtitle = NULL, caption = NULL, order = NULL )
data |
A data frame, one row per flow (or per pair with a value). |
source, target
|
Columns naming the two ends. |
value |
Column with the amounts; |
title, subtitle, caption
|
Frame texts; the title defaults to the columns. |
order |
Node order along the baseline. |
A ggplot with the hairline frame.
moves <- data.frame(origin = c("ICU", "ICU", "Ward", "Theatre", "ICU", "Ward"), destination = c("Ward", "Ward", "Home", "ICU", "Home", "ICU")) hl_arcs(moves, "origin", "destination", order = c("Theatre", "ICU", "Ward", "Home"))moves <- data.frame(origin = c("ICU", "ICU", "Ward", "Theatre", "ICU", "Ward"), destination = c("Ward", "Ward", "Home", "ICU", "Home", "ICU")) hl_arcs(moves, "origin", "destination", order = c("Theatre", "ICU", "Ward", "Home"))
Bars per category; without value the rows are counted. Long or many
labels turn the bars horizontal; a single series is sorted by value unless
order or sort = "given" says otherwise. Values sit at the bar ends.
hl_bars( data, category, value = NULL, by = NULL, title = NULL, subtitle = NULL, caption = NULL, stacked = FALSE, percent = FALSE, horizontal = NULL, sort = NULL, order = NULL, labels = TRUE, colors = NULL )hl_bars( data, category, value = NULL, by = NULL, title = NULL, subtitle = NULL, caption = NULL, stacked = FALSE, percent = FALSE, horizontal = NULL, sort = NULL, order = NULL, labels = TRUE, colors = NULL )
data |
A data frame. |
category |
Column with the categories. |
value |
Column with the amounts; |
by |
Grouping column; groups side by side, or stacked with |
title, subtitle, caption
|
Frame texts; the title defaults to the columns. |
stacked |
Stack the groups. |
percent |
Show shares of each category instead of amounts. |
horizontal |
Force horizontal ( |
sort |
|
order |
Category order. |
labels |
Print the values: at the bar ends, or inside stacked segments with the totals at the ends. |
colors |
Named colours for this chart only, e.g. |
A ggplot with the hairline frame.
admissions <- data.frame( diagnosis = c("Sepsis", "Sepsis", "Trauma", "Cardiac", "Cardiac", "Cardiac"), sex = c("Female", "Male", "Male", "Female", "Male", "Male")) hl_bars(admissions, "diagnosis") hl_bars(admissions, "diagnosis", by = "sex", stacked = TRUE)admissions <- data.frame( diagnosis = c("Sepsis", "Sepsis", "Trauma", "Cardiac", "Cardiac", "Cardiac"), sex = c("Female", "Male", "Male", "Female", "Male", "Male")) hl_bars(admissions, "diagnosis") hl_bars(admissions, "diagnosis", by = "sex", stacked = TRUE)
Brand report
hl_brand_report(brand = NULL)hl_brand_report(brand = NULL)
brand |
A brand from |
A character string: the layers by number with their paths, then per slot the name, colour as given and as used, light slot and the layer that set it, the checks, and the pinned or grouped colours with their layers.
path <- file.path(tempdir(), "_brand.yml") writeLines(c("meta:", " name: Example Hospital", "color:", " palette:", " harbor: \"#0b3d6e\"", " poppy: \"#f2545b\"", "defaults:", " hairline:", " categorical: [harbor, poppy]"), path) hl_brand_report(hl_load_brand(path))path <- file.path(tempdir(), "_brand.yml") writeLines(c("meta:", " name: Example Hospital", "color:", " palette:", " harbor: \"#0b3d6e\"", " poppy: \"#f2545b\"", "defaults:", " hairline:", " categorical: [harbor, poppy]"), path) hl_brand_report(hl_load_brand(path))
Runs hairline's palette checks: lightness band, chroma floor, separation under simulated protanopia and deuteranopia, normal-vision separation, and contrast against the surface.
hl_check_palette(colors, surface = "#ffffff", pairs = c("adjacent", "all"))hl_check_palette(colors, surface = "#ffffff", pairs = c("adjacent", "all"))
colors |
Character vector of hex colours, in slot order. |
surface |
Background colour. |
pairs |
|
A list with passed, worst_cvd, worst_normal, head_all_pairs, light_slots, warnings, failures.
hl_check_palette(hl_palette(n = 4)) hl_check_palette(c("#1f77b4", "#2b83c4"), pairs = "all")hl_check_palette(hl_palette(n = 4)) hl_check_palette(c("#1f77b4", "#2b83c4"), pairs = "all")
Groups as arcs on a ring, each sized by its total, and ribbons through the
centre joining each pair (both directions combined), as wide as the pair's
value at both ends; without value each row counts once.
hl_chord( data, source, target, value = NULL, title = NULL, subtitle = NULL, caption = NULL, order = NULL, colors = NULL )hl_chord( data, source, target, value = NULL, title = NULL, subtitle = NULL, caption = NULL, order = NULL, colors = NULL )
data |
A data frame, one row per exchange (or per pair with a value). |
source, target
|
Columns naming the two groups. |
value |
Column with the amounts; |
title, subtitle, caption
|
Frame texts; the title defaults to the columns. |
order |
Group order around the ring, clockwise from the top. |
colors |
Named colours for this chart only; they override the colour set for the number of groups. |
A ggplot with the hairline frame.
transfers <- data.frame(origin = c("ICU", "Ward", "ICU", "Step-down", "Ward"), destination = c("Ward", "ICU", "Step-down", "Ward", "Step-down")) hl_chord(transfers, "origin", "destination")transfers <- data.frame(origin = c("ICU", "Ward", "ICU", "Step-down", "Ward"), destination = c("Ward", "ICU", "Step-down", "Ward", "Step-down")) hl_chord(transfers, "origin", "destination")
hl_color() returns an entity's colour, assigning the first free palette
slot the first time an entity is seen; the colour stays for the session.
hl_colors() returns every colour fixed or assigned so far.
hl_color(name) hl_colors()hl_color(name) hl_colors()
name |
Entity name, e.g. a ward or treatment arm. |
A hex colour, or a named character vector.
hl_use(colors = c("Unit A" = "#0a6ddd", "Unit B" = "#e79d01")) hl_color("Unit B") hl_colors() hl_use()hl_use(colors = c("Unit A" = "#0a6ddd", "Unit B" = "#e79d01")) hl_color("Unit B") hl_colors() hl_use()
Numeric variables: median (IQR) per group with the Wilcoxon rank-sum test (two groups) or Kruskal–Wallis (more), shown as a raincloud. Categorical variables: n (%) per group with the chi-squared test, shown as 100 % bars.
hl_compare(study, variable)hl_compare(study, variable)
study |
A study from |
variable |
The variable to compare. |
A hairline_result.
set.seed(1) cohort <- data.frame(arm = rep(c("Standard care", "Early mobilisation"), each = 40), age = round(stats::rnorm(80, 64, 12)), sex = sample(c("Female", "Male"), 80, TRUE), months = round(stats::rexp(80, 0.05), 1), died = stats::rbinom(80, 1, 0.4)) study <- hl_study(cohort, group = "arm", time = "months", event = "died", units = c(age = "years", months = "months")) hl_compare(study, "age")$text hl_compare(study, "sex")$tableset.seed(1) cohort <- data.frame(arm = rep(c("Standard care", "Early mobilisation"), each = 40), age = round(stats::rnorm(80, 64, 12)), sex = sample(c("Female", "Male"), 80, TRUE), months = round(stats::rexp(80, 0.05), 1), died = stats::rbinom(80, 1, 0.4)) study <- hl_study(cohort, group = "arm", time = "months", event = "died", units = c(age = "years", months = "months")) hl_compare(study, "age")$text hl_compare(study, "sex")$table
Correlations between numeric columns (all of them by default) as the lower triangle on a diverging ramp, values printed in the cells, each variable named on the empty diagonal; rows with gaps are left out.
hl_correlation( data, columns = NULL, method = "pearson", title = NULL, subtitle = NULL, caption = NULL )hl_correlation( data, columns = NULL, method = "pearson", title = NULL, subtitle = NULL, caption = NULL )
data |
A data frame. |
columns |
Columns to correlate; |
method |
|
title, subtitle, caption
|
Frame texts; the title defaults to the method. |
A ggplot with the hairline frame.
hl_correlation(datasets::mtcars, c("mpg", "disp", "hp", "wt"))hl_correlation(datasets::mtcars, c("mpg", "disp", "hp", "wt"))
One row per statistic or category, one column per group plus Overall, with an overview figure of the numeric variables (median and IQR).
hl_describe(study, stats = "median_iqr")hl_describe(study, stats = "median_iqr")
study |
A study from |
stats |
Statistics for numeric variables: |
A hairline_result: figure, table, values and text.
set.seed(1) cohort <- data.frame(arm = rep(c("Standard care", "Early mobilisation"), each = 40), age = round(stats::rnorm(80, 64, 12)), sex = sample(c("Female", "Male"), 80, TRUE), months = round(stats::rexp(80, 0.05), 1), died = stats::rbinom(80, 1, 0.4)) study <- hl_study(cohort, group = "arm", time = "months", event = "died", units = c(age = "years", months = "months")) table_one <- hl_describe(study) table_one$table table_one$figureset.seed(1) cohort <- data.frame(arm = rep(c("Standard care", "Early mobilisation"), each = 40), age = round(stats::rnorm(80, 64, 12)), sex = sample(c("Female", "Male"), 80, TRUE), months = round(stats::rexp(80, 0.05), 1), died = stats::rbinom(80, 1, 0.4)) study <- hl_study(cohort, group = "arm", time = "months", event = "died", units = c(age = "years", months = "months")) table_one <- hl_describe(study) table_one$table table_one$figure
Distribution of one numeric variable by group
hl_distribution(study, variable, stats = "median_iqr")hl_distribution(study, variable, stats = "median_iqr")
study |
A study from |
variable |
A numeric variable. |
stats |
Statistics as in |
A hairline_result; the figure is a raincloud, or a ridgeline from five groups.
set.seed(1) cohort <- data.frame(arm = rep(c("Standard care", "Early mobilisation"), each = 40), age = round(stats::rnorm(80, 64, 12)), sex = sample(c("Female", "Male"), 80, TRUE), months = round(stats::rexp(80, 0.05), 1), died = stats::rbinom(80, 1, 0.4)) study <- hl_study(cohort, group = "arm", time = "months", event = "died", units = c(age = "years", months = "months")) hl_distribution(study, "age", stats = c("median_iqr", "range"))$valuesset.seed(1) cohort <- data.frame(arm = rep(c("Standard care", "Early mobilisation"), each = 40), age = round(stats::rnorm(80, 64, 12)), sex = sample(c("Female", "Male"), 80, TRUE), months = round(stats::rexp(80, 0.05), 1), died = stats::rbinom(80, 1, 0.4)) study <- hl_study(cohort, group = "arm", time = "months", event = "died", units = c(age = "years", months = "months")) hl_distribution(study, "age", stats = c("median_iqr", "range"))$values
One ring with labels outside and leader lines, or up to three hierarchy
levels with a hierarchical key; labels = "ring" labels the arcs instead.
More than seven slices of a single ring fold into "Other". Children use
tints of their parent's colour.
hl_donut( data, label = NULL, value = NULL, levels = NULL, title = NULL, subtitle = NULL, caption = NULL, labels = NULL, total = TRUE, total_label = NULL, colors = NULL, order = NULL )hl_donut( data, label = NULL, value = NULL, levels = NULL, title = NULL, subtitle = NULL, caption = NULL, labels = NULL, total = TRUE, total_label = NULL, colors = NULL, order = NULL )
data |
A data frame. |
label |
Column naming the slices of a single ring. |
value |
Column with the amounts; |
levels |
Up to three hierarchy columns, inner ring first; replaces |
title, subtitle, caption
|
Frame texts. |
labels |
|
total |
Show the total in the centre. |
total_label |
Text under the total, e.g. |
colors |
Named colours for this chart only, e.g. |
order |
Top-level slices in this order; any others follow in their data order. |
A ggplot with the hairline frame.
discharge <- data.frame(route = c("Home", "Rehabilitation", "Ward", "Died"), patients = c(412, 168, 97, 23)) hl_donut(discharge, "route", "patients", total_label = "patients")discharge <- data.frame(route = c("Home", "Rehabilitation", "Ward", "Died"), patients = c(412, 168, 97, 23)) hl_donut(discharge, "route", "patients", total_label = "patients")
One dot per row, largest at the top. With low and high each dot gets
its interval and the numbers on the right; reference adds a vertical line.
hl_dotplot( data, label, estimate, low = NULL, high = NULL, title = NULL, subtitle = NULL, caption = NULL, sort = TRUE, ratio = FALSE, reference = NULL, digits = 2 )hl_dotplot( data, label, estimate, low = NULL, high = NULL, title = NULL, subtitle = NULL, caption = NULL, sort = TRUE, ratio = FALSE, reference = NULL, digits = 2 )
data |
A data frame. |
label, estimate
|
Columns: row label and value. |
low, high
|
Interval columns, optional. |
title, subtitle, caption
|
Frame texts; the title defaults to the columns. |
sort |
Sort by value, largest at the top. |
ratio |
Values are ratios (log axis). |
reference |
Value for a vertical reference line. |
digits |
Decimals of the numbers. |
A ggplot with the hairline frame.
rates <- data.frame(unit = c("Medical", "Surgical", "Cardiac"), mortality = c(0.13, 0.08, 0.06), low = c(0.11, 0.07, 0.04), high = c(0.15, 0.10, 0.08)) hl_dotplot(rates, "unit", "mortality", "low", "high")rates <- data.frame(unit = c("Medical", "Surgical", "Cardiac"), mortality = c(0.13, 0.08, 0.06), low = c(0.11, 0.07, 0.04), high = c(0.15, 0.10, 0.08)) hl_dotplot(rates, "unit", "mortality", "low", "high")
From a fitted lm, glm or coxph model, or a table with estimate and
interval columns; terms that are study columns take their labels.
hl_effects( study, model_or_table, measure = NULL, study_column = "study", estimate = "estimate", low = "low", high = "high", exponentiate = NULL, digits = 2, ... )hl_effects( study, model_or_table, measure = NULL, study_column = "study", estimate = "estimate", low = "low", high = "high", exponentiate = NULL, digits = 2, ... )
study |
A study from |
model_or_table |
A fitted model or a data frame. |
measure |
Prefix for the values and text, e.g. |
study_column, estimate, low, high
|
Columns of a table. |
exponentiate |
Exponentiate model terms; default for |
digits |
Decimals of the estimates. |
... |
Passed to |
A hairline_result.
set.seed(1) cohort <- data.frame(arm = rep(c("Standard care", "Early mobilisation"), each = 40), age = round(stats::rnorm(80, 64, 12)), sex = sample(c("Female", "Male"), 80, TRUE), months = round(stats::rexp(80, 0.05), 1), died = stats::rbinom(80, 1, 0.4)) study <- hl_study(cohort, group = "arm", time = "months", event = "died", units = c(age = "years", months = "months")) model <- stats::glm(died ~ age + sex, family = stats::binomial, data = cohort) hl_effects(study, model, "OR")$valuesset.seed(1) cohort <- data.frame(arm = rep(c("Standard care", "Early mobilisation"), each = 40), age = round(stats::rnorm(80, 64, 12)), sex = sample(c("Female", "Male"), 80, TRUE), months = round(stats::rexp(80, 0.05), 1), died = stats::rbinom(80, 1, 0.4)) study <- hl_study(cohort, group = "arm", time = "months", event = "died", units = c(age = "years", months = "months")) model <- stats::glm(died ~ age + sex, family = stats::binomial, data = cohort) hl_effects(study, model, "OR")$values
Looks for _brand.yml from start upwards and stops at the project root
(a folder containing .git, _quarto.yml, pyproject.toml or DESCRIPTION).
hl_find_brand(start = getwd())hl_find_brand(start = getwd())
start |
Folder to start from. |
The path, or NULL.
path <- file.path(tempdir(), "_brand.yml") writeLines(c("meta:", " name: Example Hospital", "color:", " palette:", " harbor: \"#0b3d6e\"", " poppy: \"#f2545b\"", "defaults:", " hairline:", " categorical: [harbor, poppy]"), path) hl_find_brand(tempdir())path <- file.path(tempdir(), "_brand.yml") writeLines(c("meta:", " name: Example Hospital", "color:", " palette:", " harbor: \"#0b3d6e\"", " poppy: \"#f2545b\"", "defaults:", " hairline:", " categorical: [harbor, poppy]"), path) hl_find_brand(tempdir())
A results table (study, extra columns, effect with 95 % interval, weight) beside the intervals: squares sized by weight, a pooled diamond, a solid null line, a dashed pooled line, a log axis for ratios and "favours" labels under it. Heterogeneity goes into the caption.
hl_forest( data, study = "study", estimate = "estimate", low = "low", high = "high", weight = NULL, columns = character(), pooled = NULL, ratio = TRUE, favours = NULL, heterogeneity = NULL, title = NULL, subtitle = NULL, caption = NULL, exponentiate = NULL, ... )hl_forest( data, study = "study", estimate = "estimate", low = "low", high = "high", weight = NULL, columns = character(), pooled = NULL, ratio = TRUE, favours = NULL, heterogeneity = NULL, title = NULL, subtitle = NULL, caption = NULL, exponentiate = NULL, ... )
data |
A data frame of results, a fitted |
study, estimate, low, high
|
Columns: study label, effect and its interval limits. |
weight |
Column of weights (any scale); squares get areas proportional to it. |
columns |
Named character vector of extra text columns, |
pooled |
|
ratio |
Effects are ratios (log axis, null line at 1); |
favours |
Two labels for the left and right of the null line. |
heterogeneity |
Text put at the start of the caption. |
title, subtitle, caption
|
Frame texts. |
exponentiate |
For fitted models: report exp(coefficient) (odds, rate or hazard ratios); |
... |
|
A ggplot with the hairline frame.
trials <- data.frame(study = c("Trial A", "Trial B", "Trial C"), or = c(0.62, 0.81, 0.47), low = c(0.41, 0.55, 0.25), high = c(0.94, 1.19, 0.88), weight = c(40, 35, 25)) hl_forest(trials, "study", "or", "low", "high", weight = "weight", pooled = list(estimate = 0.66, low = 0.52, high = 0.84, label = "Pooled"))trials <- data.frame(study = c("Trial A", "Trial B", "Trial C"), or = c(0.62, 0.81, 0.47), low = c(0.41, 0.55, 0.25), high = c(0.94, 1.19, 0.88), weight = c(40, 35, 25)) hl_forest(trials, "study", "or", "low", "high", weight = "weight", pooled = list(estimate = 0.66, low = 0.52, high = 0.84, label = "Pooled"))
Fixed decimals, locale separators ("en" or "de") and a true minus sign.
hl_format_number(value, digits = 1, locale = NULL) hl_format_percent(share, digits = 1, locale = NULL) hl_format_interval(estimate, low, high, digits = 2, locale = NULL)hl_format_number(value, digits = 1, locale = NULL) hl_format_percent(share, digits = 1, locale = NULL) hl_format_interval(estimate, low, high, digits = 2, locale = NULL)
value, share, estimate, low, high
|
Numbers; |
digits |
Decimal places. |
locale |
|
A character string.
hl_format_number(1234.5, 1) hl_format_number(-0.25, 2, locale = "de") hl_format_percent(0.125, 1) hl_format_interval(0.62, 0.41, 0.94)hl_format_number(1234.5, 1) hl_format_number(-0.25, 2, locale = "de") hl_format_percent(0.125, 1) hl_format_interval(0.62, 0.41, 0.94)
Effect against standard error (running down from 0) with pseudo 95 % limits around the pooled estimate, a dashed pooled line and a solid null line; the pooled estimate and Egger's test become the key numbers.
hl_funnel( data, estimate, se, title = NULL, subtitle = NULL, caption = NULL, ratio = TRUE, pooled = NULL, x_title = NULL )hl_funnel( data, estimate, se, title = NULL, subtitle = NULL, caption = NULL, ratio = TRUE, pooled = NULL, x_title = NULL )
data |
A data frame. |
estimate, se
|
Columns: effect and its standard error (on the log scale for ratios). |
title, subtitle, caption
|
Frame texts; the title defaults to the axis. |
ratio |
Effects are ratios (log axis, null at 1). |
pooled |
Pooled estimate; inverse-variance weighted when |
x_title |
Axis title; defaults to the effect column. |
A ggplot with the hairline frame.
trials <- data.frame(odds_ratio = c(0.84, 0.67, 0.91, 0.72, 0.55), se = c(0.12, 0.27, 0.09, 0.2, 0.35)) hl_funnel(trials, "odds_ratio", "se")trials <- data.frame(odds_ratio = c(0.84, 0.67, 0.91, 0.72, 0.55), se = c(0.12, 0.27, 0.09, 0.2, 0.35)) hl_funnel(trials, "odds_ratio", "se")
Keeps the named series (legend labels of the colour or fill mapping) or panels (facet label or 1-based index) in their colours and draws every other data series in the context grey.
hl_highlight(series = NULL, panels = NULL)hl_highlight(series = NULL, panels = NULL)
series |
Legend labels to keep in colour. |
panels |
Facet labels or 1-based panel numbers to keep in colour. |
An object to add to a ggplot with +.
data <- data.frame(day = rep(1:10, 3), share = c(seq(0.6, 0.9, length.out = 10), rep(0.7, 10), rep(0.8, 10)), unit = rep(c("Unit A", "Unit B", "Unit C"), each = 10)) ggplot2::ggplot(data, ggplot2::aes(day, share, colour = unit)) + ggplot2::geom_line() + ggplot2::labs(title = "Occupancy") + theme_hairline() + hl_highlight("Unit A")data <- data.frame(day = rep(1:10, 3), share = c(seq(0.6, 0.9, length.out = 10), rep(0.7, 10), rep(0.8, 10)), unit = rep(c("Unit A", "Unit B", "Unit C"), each = 10)) ggplot2::ggplot(data, ggplot2::aes(day, share, colour = unit)) + ggplot2::geom_line() + ggplot2::labs(title = "Occupancy") + theme_hairline() + hl_highlight("Unit A")
Copies the bundled 'Hairline Sans' files into your user font folder so that every PDF route (cairo, rsvg, Quartz) can embed it. Nothing is installed unless you call this function.
hl_install_font(confirm = interactive())hl_install_font(confirm = interactive())
confirm |
Ask before copying (default in interactive sessions). |
The installed file paths, invisibly.
if (interactive()) hl_install_font()if (interactive()) hl_install_font()
Step curves per group with 95 % bands (Greenwood, log scale) and censor ticks, a dotted 50 % line, medians and your statistics in the panel header, and the number at risk under the x-axis aligned to its ticks.
hl_kaplan_meier( data, time = NULL, event = NULL, by = NULL, title = NULL, subtitle = NULL, caption = NULL, x_title = "Time", endpoint = "Survival", median_line = TRUE, stats = character(), at_risk = TRUE, times = NULL, order = NULL, test = TRUE, colors = NULL )hl_kaplan_meier( data, time = NULL, event = NULL, by = NULL, title = NULL, subtitle = NULL, caption = NULL, x_title = "Time", endpoint = "Survival", median_line = TRUE, stats = character(), at_risk = TRUE, times = NULL, order = NULL, test = TRUE, colors = NULL )
data |
A data frame, or a |
time, event
|
Columns with follow-up time and event (1 = event, 0 = censored); unused for |
by |
Grouping column; one curve per group. |
title, subtitle, caption
|
Frame texts. |
x_title |
Axis title under the x-axis. |
endpoint |
Panel header name, e.g. |
median_line |
Draw the dotted 50 % line. |
stats |
Named character vector of key numbers, e.g. |
at_risk |
Show the number at risk. |
times |
Times for the number at risk; default the x-axis breaks. |
order |
Group order. |
test |
Add the log-rank p to the key numbers when there are groups. |
colors |
Named colours for this chart only, e.g. |
A ggplot with the hairline frame.
set.seed(1) trial <- data.frame(months = round(stats::rexp(60, 0.06), 1), died = stats::rbinom(60, 1, 0.6), arm = rep(c("Standard care", "Early mobilisation"), 30)) hl_kaplan_meier(trial, "months", "died", by = "arm", x_title = "Months since discharge")set.seed(1) trial <- data.frame(months = round(stats::rexp(60, 0.06), 1), died = stats::rbinom(60, 1, 0.6), arm = rep(c("Standard care", "Early mobilisation"), 30)) hl_kaplan_meier(trial, "months", "died", by = "arm", x_title = "Months since discharge")
Lays out the plot at the given size and returns the positions hairline
computes (pt from the top-left corner); the same keys as the Python
package's layout_report(), used for cross-language parity checks.
hl_layout_report(plot, width_mm = NULL, height_mm = NULL, size = "onehalf")hl_layout_report(plot, width_mm = NULL, height_mm = NULL, size = "onehalf")
plot |
A ggplot with |
width_mm, height_mm
|
Figure size in mm; override the preset. |
size |
Size preset, as in |
A named list.
plot <- ggplot2::ggplot(datasets::mtcars, ggplot2::aes(wt, mpg)) + ggplot2::geom_point() + ggplot2::labs(title = "Fuel use by weight") + theme_hairline() str(hl_layout_report(plot, size = "single")$panels)plot <- ggplot2::ggplot(datasets::mtcars, ggplot2::aes(wt, mpg)) + ggplot2::geom_point() + ggplot2::labs(title = "Fuel use by weight") + theme_hairline() str(hl_layout_report(plot, size = "single")$panels)
Replaces the legend row collected from the plot's colour and fill scales with these items, e.g. to explain line kinds instead of groups.
hl_legend(items)hl_legend(items)
items |
List of |
An object to add to a ggplot with +.
ggplot2::ggplot(datasets::mtcars, ggplot2::aes(wt, mpg)) + ggplot2::geom_point() + ggplot2::labs(title = "Fuel use by weight") + theme_hairline() + hl_legend(list(list(kind = "dot", colour = hl_palette(n = 1), label = "Car")))ggplot2::ggplot(datasets::mtcars, ggplot2::aes(wt, mpg)) + ggplot2::geom_point() + ggplot2::labs(title = "Fuel use by weight") + theme_hairline() + hl_legend(list(list(kind = "dot", colour = hl_palette(n = 1), label = "Car")))
Parses a _brand.yml file in the brand.yml format. Errors name the file,
the key path and the offending value.
hl_load_brand(path, output = NULL)hl_load_brand(path, output = NULL)
path |
Path to |
output |
Export type whose overrides apply: |
A list with name, path, names, colors_given, colors_used, adapt, font,
heading_font, heading_weight, neutral, base_size, sizes, locale, entity_colors,
entity_failures, output, sequential_hue, diverging_hues, layers (the file paths), slot_origins
and entity_origins (the layer that set each colour).
path <- file.path(tempdir(), "_brand.yml") writeLines(c("meta:", " name: Example Hospital", "color:", " palette:", " harbor: \"#0b3d6e\"", " poppy: \"#f2545b\"", "defaults:", " hairline:", " categorical: [harbor, poppy]"), path) brand <- hl_load_brand(path, output = "pdf") brand$namepath <- file.path(tempdir(), "_brand.yml") writeLines(c("meta:", " name: Example Hospital", "color:", " palette:", " harbor: \"#0b3d6e\"", " poppy: \"#f2545b\"", "defaults:", " hairline:", " categorical: [harbor, poppy]"), path) brand <- hl_load_brand(path, output = "pdf") brand$name
Small multiples per group with the raw values and a trailing mean; the table gives n, mean (SD) and median (IQR) over the whole period. ISO date text (2025-01-31) is read as dates.
hl_over_time(study, date, value, window = 7)hl_over_time(study, date, value, window = 7)
study |
A study from |
date |
Column with dates (or numbers on a time axis). |
value |
Numeric column to follow. |
window |
Observations in the trailing mean. |
A hairline_result.
set.seed(1) visits <- data.frame(day = 1:60, lactate = round(2 + sin(1:60 / 8) + stats::rnorm(60, 0, 0.3), 1)) study <- hl_study(visits, units = c(day = "days", lactate = "mmol/l")) hl_over_time(study, "day", "lactate", window = 7)$textset.seed(1) visits <- data.frame(day = 1:60, lactate = round(2 + sin(1:60 / 8) + stats::rnorm(60, 0, 0.3), 1)) study <- hl_study(visits, units = c(day = "days", lactate = "mmol/l")) hl_over_time(study, "day", "lactate", window = 7)$text
Colours for groups, in the order hairline assigns them: two groups take
slots 1-2, three groups slots 1-3, and a group keeps its colour when more
are shown. There are six categorical palettes of eight slots, each opening
blue then red so two groups always read blue against red ("daylight", the
default, "calm", "deep", "lagoon", "citrus" and "bold"), and
purpose sets for common cases: "pair", "pair-teal" and "pair-print"
for two groups, "ink" for greyscale print and "judgement" for low, some
concerns and high. hl_palettes() lists them all with their uses and
hl_swatches() draws them; make one the default with hl_use(palette = ).
For amounts, use a ramp (hl_ramp()) instead.
hl_palette(name = NULL, n = NULL)hl_palette(name = NULL, n = NULL)
name |
Palette or purpose-set name, a vector of hex colours, or |
n |
Number of categories; raises an error rather than repeating colours. |
Character vector of hex colours.
hl_palettes(), hl_swatches(), hl_check_palette(), hl_use().
hl_palette() hl_palette(n = 3) hl_palette("pair") hl_palette("calm", n = 4)hl_palette() hl_palette(n = 3) hl_palette("pair") hl_palette("calm", n = 4)
The catalogue of hairline's colours: categorical palettes and purpose sets
for groups, sequential and diverging ramps for amounts. active marks the
palette in use; a palette from a brand or hl_use(palette = c(...)) comes
first. Use a name with hl_palette(), hl_use(palette = ) or hl_ramp().
hl_palettes(kind = NULL)hl_palettes(kind = NULL)
kind |
|
A data frame with name, kind, slots, active, use and colors (hex codes separated by spaces).
hl_swatches() draws them.
hl_palettes() hl_palettes("purpose")hl_palettes() hl_palettes("purpose")
Adds a header line above a panel: the name, an optional qualifier, and key numbers right-aligned. When the numbers do not fit they move to a second line; numbers that still do not fit are dropped with a warning.
hl_panel_header( name = NULL, qualifier = NULL, stats = NULL, panel = NULL, key = NULL, x_title = NULL )hl_panel_header( name = NULL, qualifier = NULL, stats = NULL, panel = NULL, key = NULL, x_title = NULL )
name |
Header name. For faceted plots, defaults to the facet label of |
qualifier |
Muted text after the name, e.g. |
stats |
Named character vector of key numbers, e.g. |
panel |
Facet label the header belongs to; |
key |
Name of the one key number to emphasise, e.g. |
x_title |
Title under this panel's own x-axis numbers, e.g. its unit when panels measure different things. |
An object to add to a ggplot with +.
data <- data.frame(day = rep(1:10, 2), share = c(seq(0.6, 0.9, length.out = 10), rep(0.7, 10)), unit = rep(c("Unit A", "Unit B"), each = 10)) ggplot2::ggplot(data, ggplot2::aes(day, share)) + ggplot2::geom_line() + ggplot2::facet_wrap(~unit) + ggplot2::labs(title = "Occupancy") + theme_hairline() + hl_panel_header(panel = "Unit A", qualifier = "12 beds", stats = c(Mean = "75 %")) + hl_panel_header(panel = "Unit B", qualifier = "8 beds", stats = c(Mean = "70 %"))data <- data.frame(day = rep(1:10, 2), share = c(seq(0.6, 0.9, length.out = 10), rep(0.7, 10)), unit = rep(c("Unit A", "Unit B"), each = 10)) ggplot2::ggplot(data, ggplot2::aes(day, share)) + ggplot2::geom_line() + ggplot2::facet_wrap(~unit) + ggplot2::labs(title = "Occupancy") + theme_hairline() + hl_panel_header(panel = "Unit A", qualifier = "12 beds", stats = c(Mean = "75 %")) + hl_panel_header(panel = "Unit B", qualifier = "8 beds", stats = c(Mean = "70 %"))
A hierarchy as stacked rows, one per level, children directly under their parent; the documented alternative to a nested donut. Labels sit inside the bars (name and share, else the share, else none); a legend row names the top level.
hl_partition( data, levels, value = NULL, title = NULL, subtitle = NULL, caption = NULL, level_names = NULL, colors = NULL, order = NULL )hl_partition( data, levels, value = NULL, title = NULL, subtitle = NULL, caption = NULL, level_names = NULL, colors = NULL, order = NULL )
data |
A data frame. |
levels |
Hierarchy columns, top level first (at most three). |
value |
Column with the amounts; |
title, subtitle, caption
|
Frame texts. |
level_names |
Row headings; default the column names. |
colors |
Named colours for this chart only, e.g. |
order |
Top-level slices in this order; any others follow in their data order. |
A ggplot with the hairline frame.
discharge <- data.frame(route = c("Home", "Home", "Rehabilitation", "Ward"), support = c("Alone", "With support", "Inpatient", "Ward"), patients = c(250, 162, 168, 97)) hl_partition(discharge, c("route", "support"), "patients", level_names = c("Destination", "Support"))discharge <- data.frame(route = c("Home", "Home", "Rehabilitation", "Ward"), support = c("Alone", "With support", "Inpatient", "Ward"), patients = c(250, 162, 168, 97)) hl_partition(discharge, c("route", "support"), "patients", level_names = c("Destination", "Support"))
Identified, screened, sought, assessed and included records with the exclusions beside them, phases as brackets on the left. Counts that do not add up give a warning.
hl_prisma(counts, title = NULL, subtitle = NULL, caption = NULL)hl_prisma(counts, title = NULL, subtitle = NULL, caption = NULL)
counts |
Named list: identified (and registers), duplicates, automation, other_removed, screened, excluded, sought, not_retrieved, assessed, excluded_reasons (named numbers), included, reports. |
title, subtitle, caption
|
Frame texts. |
A ggplot with the hairline frame.
hl_prisma(list(identified = 1240, duplicates = 260, screened = 980, excluded = 860, sought = 120, not_retrieved = 10, assessed = 110, excluded_reasons = c("Wrong population" = 92), included = 18))hl_prisma(list(identified = 1240, duplicates = 260, screened = 980, excluded = 860, sought = 120, not_retrieved = 10, assessed = 110, excluded_reasons = c("Wrong population" = 92), included = 18))
Per group a half density, the box (interquartile range and median) and the points, with "n = …" under the group names. Comparison brackets show your test results and are stacked so they never overlap.
hl_raincloud( data, group, value = NULL, title = NULL, subtitle = NULL, caption = NULL, y_title = NULL, order = NULL, comparisons = list(), colors = NULL )hl_raincloud( data, group, value = NULL, title = NULL, subtitle = NULL, caption = NULL, y_title = NULL, order = NULL, comparisons = list(), colors = NULL )
data |
A data frame. |
group, value
|
Columns: the grouping and the measurement; or a formula |
title, subtitle, caption
|
Frame texts. |
y_title |
Axis title above the y-axis. |
order |
Group order. |
comparisons |
|
colors |
Named colours for this chart only, e.g. |
A ggplot with the hairline frame.
set.seed(1) stays <- data.frame(unit = rep(c("Surgical", "Medical"), each = 30), days = round(stats::rexp(60, 0.2), 1)) hl_raincloud(stays, "unit", "days", y_title = "Days")set.seed(1) stays <- data.frame(unit = rep(c("Surgical", "Medical"), each = 30), days = round(stats::rexp(60, 0.2), 1)) hl_raincloud(stays, "unit", "days", y_title = "Days")
Colours for amounts. Sequential ramps run light to dark in one hue
("blues", "teals", "oranges", "berries", "greys"); diverging
ramps run from one pole through a neutral midpoint to the other
("blue-coral", "teal-berry"), for values below and above a reference.
scale_colour_hairline_c() and scale_fill_hairline_c() use them in a plot.
While a brand is active, "brand-sequential" and "brand-diverging" follow
its colours (its sequential and diverging entries, else its first one
and two colours) and become the default.
hl_ramp(name = NULL, n = NULL)hl_ramp(name = NULL, n = NULL)
name |
Ramp name, e.g. |
n |
Number of colours; other counts than the stored 7 (or 13) interpolate in OKLab. |
Character vector of hex colours.
hl_ramp("blues") hl_ramp("blues", n = 3) hl_ramp("blue-coral", n = 5)hl_ramp("blues") hl_ramp("blues", n = 3) hl_ramp("blue-coral", n = 5)
A thin dashed line (threshold, target) behind the data in every panel, with an optional label at its end and an optional legend entry.
hl_reference(y = NULL, x = NULL, label = NULL, legend_label = NULL)hl_reference(y = NULL, x = NULL, label = NULL, legend_label = NULL)
y, x
|
Position of a horizontal ( |
label |
Text at the end of the line, e.g. |
legend_label |
Text for a dashed legend entry. |
An object to add to a ggplot with +.
ggplot2::ggplot(datasets::airquality, ggplot2::aes(Day, Temp)) + ggplot2::geom_point() + ggplot2::labs(title = "Daily temperature") + theme_hairline() + hl_reference(y = 80, label = "80 F")ggplot2::ggplot(datasets::airquality, ggplot2::aes(Day, Temp)) + ggplot2::geom_point() + ggplot2::labs(title = "Daily temperature") + theme_hairline() + hl_reference(y = 80, label = "80 F")
One density ridge per group on a shared x-axis, the first group on top, each overlapping the row above; all ridges share one height scale, and a short line marks each median.
hl_ridgeline( data, group, value = NULL, title = NULL, subtitle = NULL, caption = NULL, order = NULL, medians = TRUE, colors = NULL )hl_ridgeline( data, group, value = NULL, title = NULL, subtitle = NULL, caption = NULL, order = NULL, medians = TRUE, colors = NULL )
data |
A data frame. |
group, value
|
Columns: the grouping and the measurement; or a formula |
title, subtitle, caption
|
Frame texts; the title defaults to the columns. |
order |
Group order, top to bottom. |
medians |
Mark the medians. |
colors |
Named colours for this chart only, e.g. |
A ggplot with the hairline frame.
set.seed(1) diagnoses <- c("Sepsis", "Trauma", "Cardiac", "Respiratory", "Neuro") ages <- data.frame(diagnosis = rep(diagnoses, each = 40), age = round(stats::rnorm(200, rep(c(68, 45, 70, 62, 55), each = 40), 10))) hl_ridgeline(ages, "diagnosis", "age")set.seed(1) diagnoses <- c("Sepsis", "Trauma", "Cardiac", "Respiratory", "Neuro") ages <- data.frame(diagnosis = rep(diagnoses, each = 40), age = round(stats::rnorm(200, rep(c(68, 45, 70, 62, 55), each = 40), 10))) hl_ridgeline(ages, "diagnosis", "age")
One 100 % bar per domain with the number of studies judged low, some concerns or high inside, and an overall row (each study's worst judgement) below a rule. Reads a long table (study, domain, judgement) or a wide one with one column per domain.
hl_risk_of_bias( data, study, domain = NULL, judgement = NULL, domains = NULL, overall = "Overall", title = NULL, subtitle = NULL, caption = NULL )hl_risk_of_bias( data, study, domain = NULL, judgement = NULL, domains = NULL, overall = "Overall", title = NULL, subtitle = NULL, caption = NULL )
data |
A data frame. |
study |
Study column. |
domain, judgement
|
Columns of a long table. |
domains |
Domain columns of a wide table, in place of |
overall |
Name of the overall row; |
title, subtitle, caption
|
Frame texts. |
A ggplot with the hairline frame.
trials <- data.frame(study = c("Trial 1", "Trial 2"), Randomisation = c("Low", "Some concerns"), Reporting = c("Low", "High"), check.names = FALSE) hl_risk_of_bias(trials, "study", domains = c("Randomisation", "Reporting"))trials <- data.frame(study = c("Trial 1", "Trial 2"), Randomisation = c("Low", "Some concerns"), Reporting = c("Low", "High"), check.names = FALSE) hl_risk_of_bias(trials, "study", domains = c("Randomisation", "Reporting"))
Flows from left to right through two or more columns. Nodes share one
scale; later columns are ordered to reduce crossings and links are ordered
at both ends, so they never cross inside a node. Links take the colour of
their group in color_by (default the last column).
hl_sankey( data, columns, value = NULL, title = NULL, subtitle = NULL, caption = NULL, color_by = NULL, headings = NULL, colors = NULL )hl_sankey( data, columns, value = NULL, title = NULL, subtitle = NULL, caption = NULL, color_by = NULL, headings = NULL, colors = NULL )
data |
A data frame, one row per path with its value. |
columns |
The columns, left to right. |
value |
Column with the amounts; |
title, subtitle, caption
|
Frame texts. |
color_by |
Column whose groups colour the links, or |
headings |
Column headings; default the column names. |
colors |
Named colours for this chart only, e.g. |
A ggplot with the hairline frame.
budget <- data.frame(income = c("Salary", "Salary", "Benefit"), account = c("Joint", "Joint", "Savings"), spending = c("Rent", "Food", "Reserve"), euros = c(1200, 420, 150)) hl_sankey(budget, c("income", "account", "spending"), "euros")budget <- data.frame(income = c("Salary", "Salary", "Benefit"), account = c("Joint", "Joint", "Savings"), spending = c("Rent", "Food", "Reserve"), euros = c(1200, 420, 150)) hl_sankey(budget, c("income", "account", "spending"), "euros")
Writes name.<format> for each format: PDF (vector), PNG and TIFF (600 dpi
by default, TIFF LZW-compressed) and SVG (text kept as text). PDF, PNG and
SVG files are named by the plot's title, with the title and subtitle as
their description; none shows a timestamp or producer banner. PDF uses cairo
where it is available; otherwise svglite + rsvg. Both embed the bundled font
once it can be found: cairo finds it on its own outside Windows, otherwise
run hl_install_font() once. PDFs are byte-identical across runs with
cairo before 1.17.6; later versions keep a creation date inside a
compressed stream that no PDF reader shows.
hl_save( plot, name, formats = c("pdf", "png"), size = "onehalf", width_mm = NULL, height_mm = NULL, dpi = 600, background = NULL )hl_save( plot, name, formats = c("pdf", "png"), size = "onehalf", width_mm = NULL, height_mm = NULL, dpi = 600, background = NULL )
plot |
A ggplot, ideally with |
name |
Output path without extension; parent folders are created. |
formats |
Any of |
size |
Size preset: |
width_mm, height_mm
|
Explicit size in mm; overrides the preset. |
dpi |
Resolution for PNG and TIFF. |
background |
|
The written paths, invisibly.
plot <- ggplot2::ggplot(datasets::mtcars, ggplot2::aes(wt, mpg)) + ggplot2::geom_point() + ggplot2::labs(title = "Fuel use by weight") + theme_hairline() hl_save(plot, file.path(tempdir(), "fuel"), formats = "png", size = "single", dpi = 150)plot <- ggplot2::ggplot(datasets::mtcars, ggplot2::aes(wt, mpg)) + ggplot2::geom_point() + ggplot2::labs(title = "Fuel use by weight") + theme_hairline() hl_save(plot, file.path(tempdir(), "fuel"), formats = "png", size = "single", dpi = 150)
Points per group in their entity colours and, with fit = "linear", a
linear fit over all points with its 95 % band; n, r and slope become the
key numbers.
hl_scatter( data, x, y, by = NULL, title = NULL, subtitle = NULL, caption = NULL, fit = "linear", order = NULL, colors = NULL )hl_scatter( data, x, y, by = NULL, title = NULL, subtitle = NULL, caption = NULL, fit = "linear", order = NULL, colors = NULL )
data |
A data frame. |
x, y
|
Columns for the axes. |
by |
Grouping column. |
title, subtitle, caption
|
Frame texts; the title defaults to the columns. |
fit |
|
order |
Group order. |
colors |
Named colours for this chart only, e.g. |
A ggplot with the hairline frame.
hl_scatter(datasets::mtcars, "wt", "mpg", title = "Fuel use by weight")hl_scatter(datasets::mtcars, "wt", "mpg", title = "Fuel use by weight")
Active hairline settings
hl_settings()hl_settings()
The settings list set by the last hl_use(), or the defaults.
hl_settings()$fonthl_settings()$font
Per group, first on top: the range as a thin line, the interquartile range as a band in the group's colour, the median as a dot, an optional reference tick, and the key numbers right-aligned.
hl_spread( data, group, value, title = NULL, subtitle = NULL, caption = NULL, order = NULL, marks = NULL, mark_label = NULL, stats = c("median", "iqr", "mean"), percent = FALSE, digits = NULL, zero = FALSE, axis_title = NULL, colors = NULL )hl_spread( data, group, value, title = NULL, subtitle = NULL, caption = NULL, order = NULL, marks = NULL, mark_label = NULL, stats = c("median", "iqr", "mean"), percent = FALSE, digits = NULL, zero = FALSE, axis_title = NULL, colors = NULL )
data |
A data frame. |
group, value
|
Grouping column and numeric column. |
title, subtitle, caption
|
Frame texts; the title defaults to the columns. |
order |
Group order, top to bottom. |
marks |
Named reference values per group, e.g. |
mark_label |
Name of the reference in the numbers column, e.g. |
stats |
Numbers to show: any of |
percent |
Values are shares (0-1), shown as percentages. |
digits |
Decimals; default by magnitude (none for whole numbers). |
zero |
Include zero on the axis. |
axis_title |
Title under the axis numbers, e.g. the unit. |
colors |
Named colours for this chart only. |
A ggplot with the hairline frame.
set.seed(1) beds <- data.frame(unit = rep(c("Unit A", "Unit B", "Unit C"), each = 50), occupied = c(stats::rpois(50, 10), stats::rpois(50, 7), stats::rpois(50, 12))) hl_spread(beds, "unit", "occupied", marks = c("Unit A" = 12, "Unit B" = 8, "Unit C" = 14), mark_label = "Beds", zero = TRUE, axis_title = "Occupied beds")set.seed(1) beds <- data.frame(unit = rep(c("Unit A", "Unit B", "Unit C"), each = 50), occupied = c(stats::rpois(50, 10), stats::rpois(50, 7), stats::rpois(50, 12))) hl_spread(beds, "unit", "occupied", marks = c("Unit A" = 12, "Unit B" = 8, "Unit C" = 14), mark_label = "Beds", zero = TRUE, axis_title = "Occupied beds")
Names the column that plays each role, runs the checks and returns a study
for hl_describe(), hl_distribution(), hl_compare(), hl_survival()
and hl_effects(). Checks report problems (print the study) and never
change the data; errors block only the endpoints that need the column.
hl_study( data, group = NULL, time = NULL, event = NULL, id = NULL, variables = NULL, labels = NULL, types = NULL, order = NULL, unit = NULL, units = NULL )hl_study( data, group = NULL, time = NULL, event = NULL, id = NULL, variables = NULL, labels = NULL, types = NULL, order = NULL, unit = NULL, units = NULL )
data |
A data frame, one row per participant. |
group |
Column with the comparison groups. |
time, event
|
Follow-up time and event (1 = event, 0 = censored). |
id |
Participant id, checked for duplicates. |
variables |
Characteristics to describe; default every other column. |
labels |
Named display names, e.g. |
types |
Named types overriding the inference, e.g. |
order |
Group order; default order of first appearance. |
unit |
What a row is, for the texts; default "participants". |
units |
Named units of numeric columns, e.g. |
A hairline_study.
set.seed(1) cohort <- data.frame(arm = rep(c("Standard care", "Early mobilisation"), each = 40), age = round(stats::rnorm(80, 64, 12)), sex = sample(c("Female", "Male"), 80, TRUE), months = round(stats::rexp(80, 0.05), 1), died = stats::rbinom(80, 1, 0.4)) study <- hl_study(cohort, group = "arm", time = "months", event = "died", units = c(age = "years", months = "months")) studyset.seed(1) cohort <- data.frame(arm = rep(c("Standard care", "Early mobilisation"), each = 40), age = round(stats::rnorm(80, 64, 12)), sex = sample(c("Female", "Male"), 80, TRUE), months = round(stats::rexp(80, 0.05), 1), died = stats::rbinom(80, 1, 0.4)) study <- hl_study(cohort, group = "arm", time = "months", event = "died", units = c(age = "years", months = "months")) study
Kaplan–Meier curves with the log-rank test; the table gives n, events and
the median per group. A fit already made (survival::survfit) can be
passed as fit; it keeps its own intervals, and the log-rank test comes
from the study's own time, event and group columns when they are set.
hl_survival(study, fit = NULL)hl_survival(study, fit = NULL)
study |
A study from |
fit |
Optional |
A hairline_result.
set.seed(1) cohort <- data.frame(arm = rep(c("Standard care", "Early mobilisation"), each = 40), age = round(stats::rnorm(80, 64, 12)), sex = sample(c("Female", "Male"), 80, TRUE), months = round(stats::rexp(80, 0.05), 1), died = stats::rbinom(80, 1, 0.4)) study <- hl_study(cohort, group = "arm", time = "months", event = "died", units = c(age = "years", months = "months")) hl_survival(study)$valuesset.seed(1) cohort <- data.frame(arm = rep(c("Standard care", "Early mobilisation"), each = 40), age = round(stats::rnorm(80, 64, 12)), sex = sample(c("Female", "Male"), 80, TRUE), months = round(stats::rexp(80, 0.05), 1), died = stats::rbinom(80, 1, 0.4)) study <- hl_study(cohort, group = "arm", time = "months", event = "died", units = c(age = "years", months = "months")) hl_survival(study)$values
Draws palettes, purpose sets and ramps as rows of squares under their headings, each with its use, to choose colours by eye. In HTML output each square shows its slot and hex code on hover.
hl_swatches(names = NULL, title = NULL, subtitle = NULL, caption = NULL)hl_swatches(names = NULL, title = NULL, subtitle = NULL, caption = NULL)
names |
Colour sets to show, as |
title, subtitle, caption
|
Frame texts; the title defaults to "Colour sets". |
A ggplot with the hairline frame.
hl_palettes(), hl_palette(), hl_ramp().
hl_swatches() hl_swatches(c("daylight", "calm", "pair", "blues"), title = "Colours for this report")hl_swatches() hl_swatches(c("daylight", "calm", "pair", "blues"), title = "Colours for this report")
One panel per group: raw values as a thin line under a trailing rolling mean in the group's entity colour, with key numbers in the panel headers, and optional period mean, threshold, highlighted periods and events.
hl_timeseries( data, x, y, by = NULL, title = NULL, subtitle = NULL, caption = NULL, window = 7, raw = TRUE, period_mean = FALSE, percent = FALSE, threshold = NULL, threshold_label = NULL, threshold_legend = NULL, stats = c("mean", "median"), share_label = NULL, key = NULL, key_panels = NULL, highlight = NULL, qualifiers = list(), bands = list(), events = list(), order = NULL, raw_label = "Raw values", mean_label = NULL, colors = NULL )hl_timeseries( data, x, y, by = NULL, title = NULL, subtitle = NULL, caption = NULL, window = 7, raw = TRUE, period_mean = FALSE, percent = FALSE, threshold = NULL, threshold_label = NULL, threshold_legend = NULL, stats = c("mean", "median"), share_label = NULL, key = NULL, key_panels = NULL, highlight = NULL, qualifiers = list(), bands = list(), events = list(), order = NULL, raw_label = "Raw values", mean_label = NULL, colors = NULL )
data |
A data frame. |
x, y, by
|
Column names: time, value and the grouping (one panel per group). |
title, subtitle, caption
|
Frame texts. |
window |
Rolling-mean window in rows (trailing, partial windows at the start). |
raw |
Draw the raw values. |
period_mean |
Draw each panel's mean as a dashed line. |
percent |
Values are shares; label axis and key numbers as percentages. |
threshold, threshold_label, threshold_legend
|
Reference line, its label and legend entry. |
stats |
Key numbers: any of |
share_label |
Label of the |
key, key_panels
|
The key number to emphasise and the panels to emphasise it in. |
highlight |
Panels to keep in colour; the others become context grey. |
qualifiers |
Named list of qualifier texts per group, e.g. bed counts. |
bands |
List of |
events |
List of |
order |
Group order; defaults to the order of appearance. |
raw_label, mean_label
|
Legend labels of the two lines. |
colors |
Named colours for this chart only, e.g. |
A ggplot with the hairline frame.
set.seed(1) occupancy <- data.frame(date = rep(as.Date("2025-01-01") + 0:89, 2), unit = rep(c("Unit A", "Unit B"), each = 90), share = pmin(1, c(0.8 + stats::rnorm(90, 0, 0.05), 0.7 + stats::rnorm(90, 0, 0.05)))) hl_timeseries(occupancy, "date", "share", by = "unit", percent = TRUE, threshold = 0.9, threshold_label = "90 %")set.seed(1) occupancy <- data.frame(date = rep(as.Date("2025-01-01") + 0:89, 2), unit = rep(c("Unit A", "Unit B"), each = 90), share = pmin(1, c(0.8 + stats::rnorm(90, 0, 0.05), 0.7 + stats::rnorm(90, 0, 0.05)))) hl_timeseries(occupancy, "date", "share", by = "unit", percent = TRUE, threshold = 0.9, threshold_label = "90 %")
Precedence: these arguments > the brand (brand = path, or a _brand.yml
found from the working directory up to the project root; brand = FALSE
disables the lookup) > hairline's defaults.
hl_use( font = NULL, base_size = NULL, sizes = NULL, palette = NULL, locale = NULL, background = NULL, brand = NULL, colors = NULL, output = NULL, accent = NULL, scheme = NULL )hl_use( font = NULL, base_size = NULL, sizes = NULL, palette = NULL, locale = NULL, background = NULL, brand = NULL, colors = NULL, output = NULL, accent = NULL, scheme = NULL )
font |
Font family; falls back to the bundled 'Hairline Sans' with a warning when missing. |
base_size |
Body text size in pt; every other size derives from it. |
sizes |
Named list of role sizes in pt ( |
palette |
Palette or purpose-set name (e.g. |
locale |
|
background |
|
brand |
Path to a |
colors |
Named vector of entity colours, e.g. |
output |
Export type whose brand overrides apply: |
accent |
Colour of a lone series (charts without groups), made readable against the background. |
scheme |
|
The active settings, invisibly.
hl_use(base_size = 10, locale = "de", accent = "#0b6e4f") hl_settings()$locale hl_use()hl_use(base_size = 10, locale = "de", accent = "#0b6e4f") hl_settings()$locale hl_use()
The overlap of two or three sets with the count of every region inside it
and each set's name and size outside. Circles are schematic (equal sizes);
with proportional = TRUE, two circles get areas proportional to their
sizes and overlap by the shared count. Four or more sets are refused: show
the intersections as bars instead.
hl_venn( data, columns = NULL, title = NULL, subtitle = NULL, caption = NULL, colors = NULL, proportional = FALSE )hl_venn( data, columns = NULL, title = NULL, subtitle = NULL, caption = NULL, colors = NULL, proportional = FALSE )
data |
A named list of member vectors, or a data frame with one row per member. |
columns |
For a data frame, the membership columns (TRUE/FALSE or 0/1). |
title, subtitle, caption
|
Frame texts; the title defaults to the set names. |
colors |
Named colours for this chart only; they override the colour set for the number of sets. |
proportional |
Areas proportional to the counts (two sets only). |
A ggplot with the hairline frame.
hl_venn(list(Diabetes = c(1, 2, 3, 5, 8), Hypertension = c(2, 3, 4, 8, 9, 10), Obesity = c(3, 5, 9, 11)))hl_venn(list(Diabetes = c(1, 2, 3, 5, 8), Hypertension = c(2, 3, 4, 8, 9, 10), Obesity = c(3, 5, 9, 11)))
Discrete scales give every level its entity colour (see hl_color()) when
palette is NULL, or the palette's slots in order otherwise, and refuse
more categories than slots; continuous scales use a sequential or diverging ramp.
Charts made with hairline's own functions pick their colours already; these
scales are for plots built with ggplot2 directly.
scale_colour_hairline(palette = NULL, ...) scale_fill_hairline(palette = NULL, ...) scale_colour_hairline_c(ramp = NULL, ...) scale_fill_hairline_c(ramp = NULL, ...)scale_colour_hairline(palette = NULL, ...) scale_fill_hairline(palette = NULL, ...) scale_colour_hairline_c(ramp = NULL, ...) scale_fill_hairline_c(ramp = NULL, ...)
palette |
Palette or purpose-set name (see |
... |
Passed to the underlying ggplot2 scale. |
ramp |
Ramp name for continuous scales: |
A ggplot2 scale.
ggplot2::ggplot(datasets::iris, ggplot2::aes(Sepal.Length, Petal.Length, colour = Species)) + ggplot2::geom_point() + ggplot2::labs(title = "Petal by sepal length") + theme_hairline() + scale_colour_hairline() ggplot2::ggplot(ggplot2::faithfuld, ggplot2::aes(waiting, eruptions, fill = density)) + ggplot2::geom_raster() + ggplot2::labs(title = "Old Faithful eruptions") + theme_hairline() + scale_fill_hairline_c("teals")ggplot2::ggplot(datasets::iris, ggplot2::aes(Sepal.Length, Petal.Length, colour = Species)) + ggplot2::geom_point() + ggplot2::labs(title = "Petal by sepal length") + theme_hairline() + scale_colour_hairline() ggplot2::ggplot(ggplot2::faithfuld, ggplot2::aes(waiting, eruptions, fill = density)) + ggplot2::geom_raster() + ggplot2::labs(title = "Old Faithful eruptions") + theme_hairline() + scale_fill_hairline_c("teals")
Continuous position scales using hairline's 3–6 'nice' breaks, locale
number formatting and limits that end on a break. Applied automatically to
hairline plots; add explicitly to change percent or target.
scale_y_hairline(percent = FALSE, target = NULL, ...) scale_x_hairline(percent = FALSE, target = NULL, ...)scale_y_hairline(percent = FALSE, target = NULL, ...) scale_x_hairline(percent = FALSE, target = NULL, ...)
percent |
Treat values as shares and label them as percentages (unit on the top label only). |
target |
Preferred number of breaks; |
... |
Passed to |
A ggplot2 scale.
shares <- data.frame(unit = c("A", "B", "C"), share = c(0.42, 0.65, 0.81)) ggplot2::ggplot(shares, ggplot2::aes(unit, share)) + ggplot2::geom_col() + ggplot2::labs(title = "Share of beds occupied") + theme_hairline() + scale_y_hairline(percent = TRUE)shares <- data.frame(unit = c("A", "B", "C"), share = c(0.42, 0.65, 0.81)) ggplot2::ggplot(shares, ggplot2::aes(unit, share)) + ggplot2::geom_col() + ggplot2::labs(title = "Share of beds occupied") + theme_hairline() + scale_y_hairline(percent = TRUE)
Adds hairline's theme and frame to a ggplot: title, subtitle, legend row, panel headers and caption on one left edge, with the spacing scale applied wherever the plot is printed or saved.
theme_hairline(base_size = NULL, font = NULL, axes = TRUE)theme_hairline(base_size = NULL, font = NULL, axes = TRUE)
base_size |
Body text size in pt; |
font |
Font family; |
axes |
|
An object to add to a ggplot with +.
ggplot2::ggplot(datasets::mtcars, ggplot2::aes(wt, mpg)) + ggplot2::geom_point() + ggplot2::labs(title = "Fuel use by weight", subtitle = "32 cars, 1974", caption = "Motor Trend.", x = "Weight (1000 lb)", y = "Miles per gallon") + theme_hairline()ggplot2::ggplot(datasets::mtcars, ggplot2::aes(wt, mpg)) + ggplot2::geom_point() + ggplot2::labs(title = "Fuel use by weight", subtitle = "32 cars, 1974", caption = "Motor Trend.", x = "Weight (1000 lb)", y = "Miles per gallon") + theme_hairline()