hl_donut() and hl_chord()
labels wrap beside a ring of the minimum radius, hl_venn() set names beside circles of the minimum radius,
hl_risk_of_bias() domains (and the overall row) beside bars of risk_of_bias.min_plot, hl_sankey()
labels beside links of sankey.min_plot (new tokens), and the favours labels of hl_forest() between the
null line and the panel's edge. A value that no longer fits after the last line takes a line of its own.total_label wraps inside the hole, and is refused with the hole's width when a word does not fit.figure = NULL
and a warning that says why, instead of failing. hl_describe(), hl_compare(), hl_distribution(),
hl_survival() and hl_over_time() follow the same rule.hl_survival(), hl_kaplan_meier() and the log-rank test count the number at risk, events and deaths per
group from sorted times at once instead of rescanning every row at every time: 100,000 rows take a fraction of a
second instead of about 85 seconds. Results are unchanged and match survival::survdiff() and
survival::survfit().hl_save() refuses a figure whose frame cannot be laid out with an error of class hairline_layout_error, as the
Python package raises HairlineLayoutError, instead of saving the plot without the hairline frame. Drawing
through ggplot2 itself (printing, ggsave(), a viewer window that can still be resized) keeps the fallback and
its warning.color, color.palette,
typography, defaults.hairline, its colors, groups, sizes and outputs, and meta are mappings; a list
(typography.fonts, categorical, a group's members) may be written as its single value
(categorical: harbor); each font entry is a mapping. Anything else is an error naming the file, the key and
what it holds, instead of "$ operator is invalid for atomic vectors" or a list taken as a palette. A key given
twice reads "key 'harbor' is given twice".hl_save() refuses a dpi below 20, at which text is smaller than a pixel.hl_study(unit = ...) takes a plural noun, whose singular drops a final s, or a singular and plural
pair such as c("mouse", "mice").hl_use(output = ...) refuses an unknown export type with brand = FALSE too.spacing.min_plot_height (8 x the base size, for all panel rows together, each row at least
min_panel_height), hl_save() (and hl_layout_report(), host hand-off, HTML) makes the figure taller instead
of failing with "figure too small". hl_scatter(by =) and hl_timeseries() at 75 mm (width_mm from a host
context) used to fail. Figures with room to spare keep their size (the gallery is unchanged). It replaces the
Kaplan–Meier-only rule and the survival.min_plot token. A chart can ask for more room
(plot@meta$hairline_min_plot): hl_raincloud() does, by its bracket levels, and the study overview asks for
less (study.overview_min_row per group row), as its strip is sized from its rows. A plot printed to a device of
fixed size keeps that size; "figure too small" then names the height that fits: "make it at least 52 mm tall
(hl_save(height_mm = 52))".hl_timeseries() plots are saved at the "double" size (174 mm) by default, as in the Python package, where
they were 120 mm wide: hl_save(), hl_layout_report() and the host hand-off take size = NULL, the plot's own
size ("onehalf" for every other plot).hl_bars() lies down when its category names, or the values over its bars, are too wide for upright bars at
the figure's width (bars.label_space x the base size between neighbours), not only by label count and length:
seven or eight department names collided at 85 mm, and eight already did at 120 mm. The new size argument
names the size the bars are laid out for (the host's column, else the preset's width), as size= does in
Python, and becomes the plot's own size for hl_save().hl_raincloud() comparison brackets keep room for their p labels: a short plot grows until every bracket level
is taller than its label, instead of stacking the labels into the brackets above.hl_scatter() draws points at the axis limits whole (coord_cartesian(clip = "off")); limits end on a break,
so the panel edge cut them in half.1.25 tn, 0.8 tn;
German Bio., Mrd.), with the tick step's decimals on axes and numbers.compact_digits on bars, instead of
thirteen digits that ran past the bars and the figure. Tooltips keep the full number.numbers: the unrounded quantities their table, values and text are formatted from. Per
group n and the median and quartiles, mean and SD, range, or counts and shares; for a test its method,
statistic, degrees of freedom where defined and p (NA when undefined); for survival n, events, medians and
the log-rank test; for effects the estimates, intervals, scale (ratio or difference) and, for lm, glm and
coxph models, the reference level of each treatment-coded factor.hl_describe() names the variables with missing values; rows without a group are
counted in Overall only, and the text says how many.id =, hl_describe(), hl_compare() and hl_distribution() refuse a variable with more than one row
per id instead of counting repeated measurements as separate patients; the error names the id column and the
most rows per id, and points to one row per id or to hl_over_time().order = leaves out are counted (other_groups) and named in the text, so the analysed
count is exact when order selects groups.hl_study(order = ...) refuses a group named twice, which analysed the same rows as two samples
(Kruskal–Wallis on three samples instead of the rank-sum test on two)..group) no longer breaks hl_compare() and
hl_distribution(); the internal name never collides with a data column.hl_describe(); the aggregate is then "Overall (all)".order that names none of its groups, is a study error that the
endpoints name. hl_compare() and hl_over_time() refuse a variable with no value in any group.stats = character() is refused with a clear message.scale_y_hairline() and scale_x_hairline() take digits, the decimals of the values a reader looks up in
interactive output. A power of 89.6 % no longer shows as 90 % beside a 90 % target. Without digits
tooltips follow the shared digits rule as before; axis labels are unchanged.hl_kaplan_meier(order = ...) and hl_survival() test only the groups they draw; a group left out of order
is no longer part of the log-rank p. Groups without a single event draw without a test instead of failing.factor(c("10", "20")) is 10 and 20, not the
level codes 1 and 2.hl_save() writes every format before it replaces anything: a failed format, or a folder in the way, leaves
existing files as they were, and a failed move is an error rather than a warning.hl_effects(digits = ) reaches the forest plot as well as the table.hl_over_time() refuses date text with trailing characters and keeps clock times (2025-01-01 10:00) instead
of cutting them to the date.Count stays a column when rows are counted (the count becomes Rows).hl_raincloud() leaves missing values out and draws one group with comparisons = "all".order naming no group, empty data, text where numbers belong, and values a chart
cannot draw (an estimate outside its interval, a self-flow, negative arc values) each give a message that names
the column and row.hl_over_time()). Unknown columns, empty studies and
unknown types are named.hl_timeseries() reads ISO date text as dates. The
period mean ignores missing values. hl_study(types = c(...)) works with a named vector.hl_save() and hl_layout_report() refuse sizes and resolutions of zero or less, which crashed R in ragg.
hl_use() refuses sizes of zero or less and empty palettes. hl_format_number() formats one number.lm models in the forest plot keep their own Student-t intervals (as confint()); glm and coxph keep their
Wald intervals.order is refused instead of entering the analysis twice.hl_timeseries() keeps data columns named .mean, .group, .x or .y as they are.hl_save() names its temporary files so they never meet existing ones.order no longer adds its categories to a comparison's table: they changed the
chi-squared test's degrees of freedom and p-value although none of the group's rows were analysed.id = set, every endpoint that counts patients (hl_describe(), hl_compare(), hl_distribution(),
hl_survival()) refuses an id with more than one row, wherever its values are missing.wilcox.test() returned 0 for one order.hl_compare() keeps a decimal for quartiles of small whole numbers, as hl_distribution() does.$values of hl_compare(), hl_distribution() and hl_survival() holds the group summaries under
groups, beside test and p, so a group named "test" or "p" no longer collides with them.A host context file that is missing or broken no longer breaks every hl_save() ("$ operator is invalid for
atomic vectors"): the warning said "continuing without it", and now it does.
Charts that fit their height to their content measure on a device whose resolution drops for very large figures, so its buffer stays small; it grew with the square of the width (a 100 m width filled the machine).
A PNG or TIFF beyond 600 megapixels (about an A0 poster at 600 dpi; token export$max_megapixels) is refused
before its buffer is allocated, with the size and dpi that asked for it.
An hl_use() call that fails (a base size of 0, an unknown palette) no longer comes back with every new host
context: its arguments are kept for re-applying only once they have been applied. Every later plot in the
session failed with the first call's error.
A host context whose width_mm, base_size or dpi is not a number above zero warns once and leaves that
value out.
The bundled font is registered when the package loads: without hl_use(), plots were measured and drawn in a
system font (Helvetica on macOS) and their layout differed from the Python package's.
A host context is applied without stopping anything: a locale like "de-DE" is read as "de", one hairline
lacks ("fr") warns and is left out (it stopped library(hairline)); a style that cannot be applied (a broken
_brand.yml) warns instead of failing to load; a context file that holds a JSON array, a figure_dir or
design of the wrong kind warn and are left out. The first plot after a new context already has its style,
and a damaged hairline-figures.json is replaced with a warning instead of failing every save.
hl_use(background = ...) refuses a value that is no colour; a brand's base_size must be a number above
zero; hl_save() checks the size preset first ("unknown size") and refuses an empty name. Brand errors about a
value YAML read as empty (an unquoted #hex is a comment) say "null" instead of having no message.
Infinite values are refused with the column and row in every chart that reads numbers (hl_donut(),
hl_partition(), hl_sankey(), hl_chord(), hl_arcs(), hl_correlation(), hl_kaplan_meier(),
hl_spread(), hl_scatter(), hl_bars(), hl_timeseries(), PRISMA counts, hl_spread() marks). Text in a
number column of hl_bars(), hl_timeseries(), hl_scatter() or hl_correlation() gets the "needs numbers"
message.
Clear messages instead of internal errors for empty data (hl_donut(), hl_partition(), hl_bars()), an
empty order (now: no order), hl_kaplan_meier() without time and event columns, hl_risk_of_bias() without
domain and judgement columns, hl_swatches(character()), hl_funnel(pooled = NaN) and standard errors so
large that a ratio funnel passes 1e±10, hl_spread() marks for groups that are not drawn, a statistic named
twice or fractional digits, timeseries events that are not dates on a date or time axis (a POSIXct axis
failed on every event), and hl_bars(percent = TRUE) with negative values.
hl_bars() and hl_scatter() without by draw no legend swatch (an empty one took a legend row, and made a
scatter at 75 mm too small to draw).
Labels that leave a panel less than 2 x the base size (token spacing$min_panel_width) are refused, "figure
too narrow". hl_dotplot() and hl_forest() keep their row pitch and the figure grows to hold every row.
Study: order without group is refused; numeric order values name groups as text; values of a numeric
variable that are no numbers or infinite are study errors; infinite follow-up times are refused;
hl_compare() keeps a factor's level order (it sorted the levels alphabetically); hl_distribution() draws
the groups that have values; hl_effects() refuses a term named twice; a variable named twice is refused
(it failed with "factor level is duplicated"). Numeric levels read the same in both languages (1e-04 reads
0.0001, -0 reads 0). Titles and sentences use the group's label and keep acronyms ("Median PFS").
hl_forest() and hl_dotplot() fit one column of a two-column page (75 mm): when the columns beside the intervals
(effect, weight, extra columns) would leave the plot less than forest$min_plot, they move to a line under each
study, aligned in columns under a two-line heading, as hl_spread() does with its numbers. A 5-study forest failed
at 75 and 85 mm; one too narrow even for that is refused with the room it needs.hl_kaplan_meier() grows the figure when its frame (title, legend, header, axis, number-at-risk table) would leave
the curves less than 8 x the base size high (token survival$min_plot): at 75 and 85 mm the curves had -7 and
13 pt and the figure failed. Wider figures keep their size.hl_spread() keeps its plot on a narrow figure. The key numbers sit beside the plot while it keeps at least
8 × the base size (token spread$min_plot, as the forest plot has); on a narrower figure, such as one column of
a two-column page (75 mm), they move to a line under each row, so no statistic is dropped. The plot had shrunk
to nothing and its axis ran backwards, the tick labels piled on one another. A figure too narrow even for that
is refused with the room it needs. Rows keep their pitch and the figure grows to hold them, instead of rows
squeezed into the default height; stats = character() draws the plot to the right edge instead of failing.theme_hairline()), with the
type scale, spacing and line widths of the shared design tokens.hl_study() with endpoints for cohort description, group comparison, effects, survival and change over
time; each returns a figure, a table, values and a sentence.hl_save() for PDF, PNG, TIFF, SVG and interactive HTML; files are named by the figure and carry no
timestamps.