Repository navigation
v0.22.0 — layout and stats
Minor bump: layout and stats (upstream batch 2). Default output stays
byte-identical except for the deliberate changes under "Changed", each
guarded by tests/default_output_022.rs.
Added
-
render::text::text_width(text, size)and ggplot2'scheck.overlap
priority order for axis labels, ported from the consumer'sticks.rsso
ggplot-rs owns outer-label overflow and overlap thinning centrally. -
Theme.axis_text_x_fit: AxisLabelFit { Clip, Shift, #[default] Reserve }
andTheme.axis_text_x_check_overlap(defaultfalse), with
GGPlot::axis_text_x_fit/axis_text_x_check_overlapbuilders. -
GuideLegend.byrow/max_rows, withwith_byrow/with_max_rows
builders: an explicitnrow/ncollegend grid (column-major unless
byrow), and an auto-wrapped horizontal legend that ends in a trailing
"+N more" when it still doesn't fitmax_rows. -
Position::per_panel()(defaultfalse) andPosition::validate()
(default no-op);PositionStack/PositionFillare per-panel. -
PositionStack/PositionFillgain avjustfield (default1, the
previous top-anchored behaviour) with awith_vjustbuilder and free
functionsposition_stack()/position_fill(), matching ggplot2's
position_stack(vjust =)/position_fill(vjust =)(#147).vjust != 1
also writes anymaxcolumn holding the segment's top. -
ScaleSizeContinuous::area(max_size)/GGPlot::scale_size_area(max_size)
(ggplot2'sscale_size_area(), #150): size ∝ √(value) so that area
(not radius) is proportional to the data, with 0 mapping to 0. NA/non-
finite input maps toNone. A non-finite or non-positivemax_size
warns and falls back to 6. -
geom_boxplot(stat = "identity"): newLower/Middle/Upper/
Outliersaesthetics (Aes::lower/middle/upper/outliers, D4
extensions) let a layer supply the five-number summary directly instead
of ggplot-rs computing it, matching ggplot2 (#149) —ymin/ymaxare
also required, as in ggplot2 4.0.3.outlierstakes a comma list with
optional brackets ("7, 9"or"[7, 9]"), or a single bare number; an
unparsable part is skipped with one warning. The boxplot now consumes
tooltip()/data_attr()(consumes_tooltipsis true). -
Geom::mapped_only_columns()(D3, default empty —GeomBoxplotreturns
lower/middle/upper/outliers): a raw column of that name is
dropped right afterresolve_mappingsunless the aesthetic was
actually mapped, so a column that merely happens to share the name
(e.g. a SQL alias) can't be mistaken for a mapped one, and so a raw
loweris never drawn as a boxplot hinge. Scoped toGeomBoxplot
alone — see theValue::Na/non-finite drop bullet under "Changed" for
how an unmappedlower/middle/upperon any other geom is handled
without removing it from the data (review #158's final check). -
Geom::validate_layer(pre_stat_columns, stat_name)(default no-op):
stat-dependent validation beyondrequired_aes. Under
stat = "identity",geom_boxplotrequireslower/middle/upper/
ymin/ymaxto be mapped (aValidationErrorotherwise, matching
ggplot2 4.0.3's ownstat_identityboxplot requirement); under the
default stat, it requiresy.
Changed
-
Outer x-axis labels that would cross the SVG edge are kept on-canvas by
default (allowed #1, #2 —AxisLabelFit::Reserve, the new default):
the panel narrows by just the overflow ifReservecan reserve enough
margin, and a label whose own tick has scrolled off the canvas is
dropped rather than drawn off-page. A plot whose outer labels already
fit is unaffected (tests/axis_label_fit.rs;
default_output_022'swide_axiscase, re-blessed when the Reserve
formula bug — finding 1 below — was fixed). The total reservation is
now capped so the panel can never start beyond the canvas or shrink
past its own minimum width (review #158's second re-review, LOW): a
single, centred break whose label is wider than the whole canvas falls
back toShift's draw-time adjustment for whatever still overflows
past the cap (tests/axis_label_fit.rs). -
A single horizontal legend that overflows its one-line layout now
auto-wraps to more rows instead of running off the panel (allowed #3,
D7); beyondmax_rowsit ends in a trailing "+N more" with a build
warning. A legend that already fits on one line is unaffected. A
verticalncollegend grid that still overflows the canvas after its
width is reserved now also gets a build warning (review #158's second
re-review, LOW;tests/legend_wrap.rs). -
Tooltip-template placeholders for every position column
(xmin/xmax/xend/ymin/ymax/yend/lower/middle/upper,
D9 allowed #7) are now formatted through the x/y scale, the same as the
barex/ythey're derived from — previously they fell back to the
plain display format, ignoring a custom formatter or a log-transformed
scale's inverse transform. Atooltip()template that names one of
these columns on a transformed scale, or with a custom number
formatter, now shows a different (correct) value. -
geom_boxplotoutliers now widen the trained y domain to include every
outlier value instead of silently leaving it at the whiskers' extent
(allowed #6; confirmed as a real bug with a failing test before the
fix landed — seetests/boxplot_identity.rs). A boxplot whose outliers
fall outsideymin/ymaxrenders with a taller y axis. -
A faceted
position_stack/position_fillnow computes per panel
instead of on the rows pooled across panels before faceting, matching
ggplot2 (#146). A plot with 2 or more distinct facet panels and a stack/
fill layer may render differently; one panel (or none) is unaffected. -
geom_text/geom_labelnow honour a discreteposition_dodge, via the
sameXMapperbar/col/point/errorbar/… already use (#148). A plot that
dodges text/labels on a discrete x axis renders differently; text/
labels without a dodge are unaffected. -
geom_errorbar's whisker caps shrink by the dodge group count under a
discreteposition_dodge, so caps from adjacent groups no longer
visually overlap (#151). An un-dodged errorbar is unaffected. -
A
Value::Na(e.g. a SQL NULL) in a position column that is never
legitimately discrete (ymin/ymax/xend/yend/lower/middle/
upper/…, i.e. every position column exceptx/y, which may hold
discrete category values) is now dropped with the standard "removed N
row(s) containing non-finite values" warning, the same asFloat(NaN)
already was — previously it silently mapped to 0 with no warning
(review #158 finding 4). This applies to every geom that uses one of
these columns, not justgeom_boxplot: e.g.geom_errorbar/
geom_pointrange/geom_ribbonwith aValue::Nayminnow drop that
row with a warning instead of silently drawing it from 0 — matching
ggplot2, which has always dropped a missing aesthetic's row this way.
The newlower/middle/uppercolumns also drop a non-numeric,
non-Navalue (a stray string) the same way, since they have no
legitimate discrete use. A plot whose position data has noNais
unaffected. -
lower/middle/upperare excluded from the aboveValue::Na/non-
finite drop, the pre-stat y-scale transform, and the y-scale training
when the layer never mapped the corresponding aesthetic — a raw,
unmapped column under one of these names (e.g. a forecast's confidence-
bound columns on a plaingeom_line) is otherwise left completely
untouched in the layer's data, so atooltip("{lower}")placeholder or
afacet_wrap("upper")on it still works, exactly as any other
unmapped column would (review #158's final check — round 3's universal
drop of these names from the data for every geom was itself a
regression on both of those).geom_boxplot's own hinges — mapped
understat = "identity", or computed fresh by its default stat — are
unaffected either way.
Breaking
-
Themegains two new public fields,axis_text_x_fit: AxisLabelFitand
axis_text_x_check_overlap: bool— an externalTheme { .. }
struct-literal pattern without..Default::default()(or
..theme_gray()/another preset) no longer compiles. -
GuideLegendgains two new public fields,byrow: booland
max_rows: Option<usize>— same breakage for an external
GuideLegend { .. }struct literal without..Default::default().
Neither struct is#[non_exhaustive]. -
guide::axis::draw_x_axisgained acanvas: &Rectparameter (the full
SVG viewport, not just the panel) — a deviation from the plan, needed
for the outer-label-fit edge checks (B3-1): an off-canvas label can't
be detected from the panel rect alone. An external direct caller of
draw_x_axisneeds to pass it (backend.total_area()). -
PositionStack/PositionFillare now structs with avjustfield
instead of unit structs — an externalPositionStack { .. }/
PositionFill { .. }struct-literal pattern no longer compiles (the
bare valuePositionStack/PositionFillstill does, via aconstof
the same name — dodge.rs's existingPositionDodgeprecedent). -
New
AestheticvariantsLower/Middle/Upper/Outliers: an
exhaustive externalmatchonAestheticno longer compiles. -
Geom::setup_datagained a second parameter,
warnings: &mut Vec<String>(a deviation from the plan, needed so
GeomBoxplot::setup_datacan warn on an unparsable outlier value
through the existing warnings channel rather than a new one or
eprintln!):fn setup_data(&self, data: &mut DataFrame)becomes
fn setup_data(&self, data: &mut DataFrame, warnings: &mut Vec<String>).
An externalGeomimpl overriding it needs the new parameter. -
The
arrowfeature now pinsarrow = "59"(was"53"), still with
default-features = false.GGPlot::new(batch)and theGGDataimpl for
arrow::record_batch::RecordBatchcompile unchanged against arrow 59 —
no source changes were needed insrc/data/source.rs. This lets Arrow-
native producers on arrow 59.x (e.g. the inventory-rs DuckDB/Parquet
ingest path) handRecordBatches to ggplot-rs directly, without a copy
through an intermediate arrow 53 batch (#123). Thepolarsfeature is
unaffected: polars vendors its ownpolars-arrowcrate rather than
depending on thearrowcrate, so the two can be enabled together with a
singlearrowv59.3.1 in the dependency tree. This is a semver-relevant
change forarrow-feature users: code that names arrow-53-specific types
(e.g. viaarrow = "53"pinned directly) needs to move to arrow 59 too,
since arrow does not guarantee cross-major type compatibility. Originally
#143; folded into 0.22.0.