All notable, user-facing changes to CanvasXpress are recorded here, newest first, for people who
depend on the library. Entries are generated from the Changes comment block at the top of
build/js/canvasXpress.init.js — the single source of truth,
written on every release — so this file can never fall behind the engine.
Only the most recent releases are shown here; the exhaustive per-version history is rendered on the site at https://canvasxpress.org/updates.html and linked at the bottom of this file.
Versioning & release channels
- Version numbers are sequential
MAJOR.MINORrelease identifiers (e.g.67.4), not semantic-versioning signals: aMAJORbump marks a release milestone, not a breaking change. Package registries and release tags carry the three-part formMAJOR.MINOR.0(e.g.67.4.0). Releases ship as changes are ready — often several in a week during active development. - Backward compatibility is the default. Releases add chart types, parameters, and bug fixes without breaking existing charts. The rare change that alters an existing default or the public API is called out explicitly in its own entry below, whatever the version number.
latest(npmcanvasxpress, CRANcanvasXpress, PyPIcanvasxpress) always points at the newest release and can move on any day.- Pinning for stability: enterprises that need a fixed target should pin an exact version
(e.g.
canvasxpress@67.4.0on npm,version="67.4"on CRAN/PyPI) and upgrade deliberately after reading this file. Nothing changes underneath a pinned version.
[74.0] — 2026-11-05
- KM risk table (geomKaplanMeier.js): strata row-headers no longer overlap the time-0 at-risk count - headers end just left of the y-axis and the time-0 column is left-aligned just right of it, instead of both landing on the axis x.
- sizeLegendNested (legend.js): shows up to 5 concentric circles (not two); >5 levels reduce to an even 3/4/5-circle scale (interior values are group means). Labels stack in a centred column; elbow leaders never cut a circle.
- Side histograms (geomHistogram.js): the right (y) histogram's first count label is dropped only when it would overlap the scatter's last x-axis value (e.g. "100"+"0" merging into "1000"); non-colliding axes are unchanged.
- Label rotation now sticks (dataUtils.js changeAttribute): an explicit smpTextRotate/varTextRotate change (Customizer/context menu) pins smpTextRotateAuto/varTextRotateAuto off, so the resolver stops re-flattening the angle on redraw.
- Rotated bottom labels on vertical 1D plots no longer clip the left edge (onedplotUtilities.js getVerticalLeftSampleLabelClearance): the gutter reserves each band's reach net of its anchor offset, so a long mid-chart label stays tight.
- Grouped vertical 1D axis-label tooltip now shows the grouping-factor level (the drawn award name) instead of the raw sample id (R1, R2...) (events.js); selection identity stays on the sample.
- UpSet: added upsetSetsBarRatio, dropped the oversized bottom pad, reserve left labels for visible sets only, overlay ticks regenerate as nice values not decimated (no 0,80,100), and "Size" is a rotated y-axis title beside the bars.
- Boxplot outliers (geomBoxplot.js) now draw at the data-point size (~0.6*dataPointSize, matching showBoxplotOriginalData) instead of boxWidth/boxplotOutliersRatio, which made them far larger; capped so a thin box never enlarges them.
- Heatmap top margin (onedplotDimensions.js): rotated grouped sample-overlay names reserve only overflow beyond the top variable-overlay stack, not atop it, so a tall top-overlay band no longer opens an empty strip. Names unclipped.
[73.9] — 2026-11-05
- New model-eval config builders (eval.js): CanvasXpress.rocConfig/prConfig/confusionConfig/evalPlots turn classifier (truth,score/pred) arrays into ROC & PR Scatter2D curves (AUC/AP + reference line) and a confusion-matrix Heatmap.
- More eval builders (eval.js): importanceConfig (feature importance), calibrationConfig (reliability + ECE), learningCurveConfig (train/val lines), shapConfig (SHAP beeswarm Dotplot); confusionConfig now prints per-cell counts.
[73.8] — 2026-11-04
- recommend() advisor: dates read as time from raw values (not arrayToCX's epoch numbers) → suggest Line; a boolean class column or one measure split by a low-cardinality factor now ranks Boxplot/Bar over bare Scatter2D.
- modifyFilter('guess') now honors an explicit annotation factor instead of matching the value to a group label: a grouped boxplot/bar filtered by its grouping annotation hid every box (sample-name misroute). Fixes dashboards controls.
- Maps gain a vector-field overlay (mapVectorField:true): raw arrows / gridded quiver / integrated streamlines drawn from lat/lng + vx/vy, projected through the map and coloured by magnitude. Reuses the Scatter2D vectorfield math.
[73.7] — 2026-11-04
- getSpec/getEmbedSnippet + the recommend() instance wrapper are now on the obfuscation keep-list, so they stay callable by name in --secure builds (73.6 shipped them as this.* and renamed them; cx.getEmbedSnippet() was undefined live).
[73.6] — 2026-11-03
- Map cartograms: sizeBy-driven mapType (region|dorling|cartogram|contiguous). dorling=non-overlapping sized circles; cartogram=non-contiguous scaled shapes (mapCartogramScale/Silhouette); contiguous=Dougenik. Examples map-28/29/30.
- sizeLegendNested: nested concentric size-legend bubbles on a shared bottom edge with dashed value leaders; keeps the end values + a mean-middle bubble only when labels clear a line (2 or 3). Gated, default off; shown on map-28.
- Dorling cartogram: circles are clamped within the plot box so a bubble pushed by the overlap relaxation is never clipped at a map edge.
- Binned sizeBy with more bins than the sizes array no longer wraps (sizes[i%len]), which shrank the biggest bins back to the smallest sizes; sizes now spread across the bins so markers + legend stay monotonic (new sizeForBin).
- Customizer Shelves (scatter): assign samples to axes with the Axes section's own widget (axesCustomizerAssign — X/Y slots + Lin/Log + draggable Samples list), so Shelves and Axes share one layout and behavior.
- Customizer Shelves: the Marks (Color/Size/Shape) and Facets cards now show the factor already assigned on load (as chips, like the Meta tab) instead of only after a drag (metaCustomizerPrefillProperties).
- Customizer: removing a slot factor (chip ×) rebuilds the section the user is on (Shelves or Meta), not always Meta; metaCustomizerRemoveFactor now uses activeCustomizerRenderFn instead of a hardcoded metaCustomizer.
- Customizer Shelves (Map): a Map Type selector (region|dorling|cartogram|contiguous -> changeAttribute mapType) makes the cartograms reachable by click; colour/size stay drag channels on Marks. Facets hidden for maps.
- Customizer: live re-description — dropping a field onto an encoding slot (Color/Size/Shape/Pattern) animates the marks to the new encoding; showTransition is enabled just for that drop's redraw then restored (Meta + Shelves).
- New recommend(data,intent) + static CanvasXpress.recommend: lightweight chart-type advisor — a 2D array normalized via arrayToCX, column types read, a ranked graphType short-list returned. Deep scorer stays in the MCP.
- arrayToCX auto-picks a graphType via recommend() when the caller gives none (e.g. a dropped delimited file): a smart default (Scatter2D/Line/Heatmap/...) instead of always Bar. Any config that names a graphType is untouched.
- New getSpec() + getEmbedSnippet(): getSpec returns the chart as a plain {renderTo,data,config} spec; getEmbedSnippet returns a pasteable HTML embed (canvas + versioned CDN css/js + new CanvasXpress). Hosting/URL = dashboards.
[73.5] — 2026-11-03
- cxplot/ggplot: geom_errorbar/errorbarh now render on a continuous (Scatter2D) plot -- the scatter branch drew nothing, so bars were dropped; draws asymmetric I-beams with caps from ymin/ymax/xmin/xmax. Categorical path unchanged.
- Sankey: a node whose every link is hidden by sankeyHide is now dropped entirely (box + label), not just its ribbons; it still counts in the flow graph so source box heights and showDataValues totals stay honest (issue #320).
- cxplot/ggplot: dumbbell charts (geom_line(aes(group=pair)) over geom_point, no boxplot) now draw the paired connector lines via connectBy; previously only the points rendered. Connector colour follows the line's fixed/aes colour.
- cxplot/ggplot: stat_density(geom="line") now renders -- a GeomLine layer carrying stat=StatDensity is routed to the density path (kernel estimate + colour/group) instead of drawing a raw, empty polyline of the values.
- cxplot/ggplot: a geom_density overlay on a geom_histogram now keeps the bars -- geom_density no longer force-clears showHistogramBars when a GeomHistogram layer is present, so the histogram+density combo draws both.
- cxplot/ggplot: geom_bar(fill=
) (a fixed palette, not an aes) over a categorical x now colours each bar from the vector and drops the legend; previously every bar was grey because no colorBy carried the User colours. - R converter (ggplot.as.list): computed aes/facet expressions (round_any(x,.5), env-var columns, factor() in facet_wrap) are materialised into columns so they map; original text kept as the label. Fixes binned/env-var/faceted plots.
- R converter (ggplot.as.list): geom_bar()+stat_count over a BARE numeric x coerces that x to a factor, so it draws one bar per unique value like ggplot instead of a sample per row. geom_col/geom_histogram stay continuous.
- cxplot/ggplot: a geom_point over a grouped profile line (geom_line(aes(group=g)) on a categorical x, pivoted to a Line) keeps the Line + colour grouping and draws points as markers, instead of resetting to Scatter2D (near-empty).
- R converter (ggplot.as.list): a numeric facet variable (facet on a numeric column) is coerced to a factor so panels are discrete (one per value) like ggplot; a continuous facet otherwise binned/interpolated and left panels empty.
- cxplot/ggplot: scatter error bars -- geom_errorbarh (emitted as GeomErrorbar with an x override) is now detected as horizontal, and the axis range grows to include the bar extents so bars/caps are not clipped off-plot.
- cxplot/ggplot: point shapes 21-25 (filled with a separate outline) now render -- shape 21 maps to a filled circle and the colour aesthetic drives the point border, so a white-filled point (fill="white") shows its ring.
- cxplot/ggplot: stat_summary(fun=mean, geom="point") over a boxplot draws the per-box mean marker (shrunk via a cxplot-only boxplotMeanSizeFactor) instead of dumping every raw point; existing boxplotMean examples unchanged.
- cxplot/ggplot: stat_summary(sum/mean) point/line over a categorical x aggregates y per category via geom_group (the geom_bar grouping path) instead of plotting raw rows at a row index; converter no longer precomputes those positions.
- cxplot/ggplot: geom_raster renders as a filled grid -- drawn through the geom_tile/geom_rect path (converter emits each cell's xmin/xmax/ymin/ymax + fill) instead of routing to geom_contour, which left an uncoloured point grid.
- cxplot/ggplot: overlaid geom_histogram(data=subset) layers render as bars via geom_rect (own-data bin bounds); geom_rect now honours a fixed fill colour + alpha so overlaps blend, and base points no longer paint.
- cxplot/ggplot: a geom_line with an overridden y (aes(y=pred)) or its own overlay data is drawn as per-group segments via geom_segment, so fitted/overlay lines render without disturbing the base layers.
- cxplot/ggplot: geom_segment honours a per-segment colour array (the old Array.isArray(args) test never matched) and only arches for geom_curve (geom_line/segment stay straight).
- cxplot/ggplot: a wide scale_size(range=...) on a size aes now sizes points by ggplot's area ramp -- converter emits the size range, engine switches sizeScale=area with sizeMax from it; default range (<=6) stays on the binned path.
- ggplot size legend: the key fill now carries the scatter point alpha, so a semi-transparent geom (geom_point(alpha=0.5)) draws grey keys like ggplot instead of solid black; a fully opaque point is unchanged.
- cxplot/ggplot: a geom_rect/tile/raster with a CONTINUOUS (numeric) fill now shows a continuous colour-spectrum legend bar like ggplot, not a discrete swatch-per-value grid; a factor fill still gets per-level swatches.
- cxplot/ggplot: a regression/loess fit on a log y axis no longer blanks the plot -- the overlay y-range (transformed space) is inverted back to raw before joining the data range, so the axis no longer logs a negative to NaN.
- R converter: a fill-grouped geom_histogram(position="identity") emits each group's precomputed bin bounds + fill, so the engine draws ggplot's OVERLAPPING alpha-blended histograms (via geom_rect) with a per-level legend, not stacked.
- cxplot/ggplot: a geom_segment spike to a baseline over a continuous/date x,y (aes(xend=date, yend=0)) now renders as a Scatter2D with coloured bars from the baseline, not a blank categorical Bar.
- cxplot/ggplot: the area size legend stays a single column (thinned to fit, never split); a continuous colour indicator over a large range labels ticks in compact exponential (1.2e+9) and sizes the legend box to it.
- cxplot/ggplot: a bin2d with scale_fill_gradient(trans="log10") spreads the cell colours (and the indicator) logarithmically instead of washing every cell to the low end; the indicator labels original values at the log ticks.
- cxplot/ggplot: geom_bin2d(aes(weight=w)) colours each cell by the SUM of the weight over its points, not the count, so cells and legend match ggplot's weighted range; an unweighted bin2d is unchanged.
- cxplot/ggplot: geom_point(shape=21..25, stroke=N) now draws the point border at the stroke width (outlineWidth), so a heavy stroke makes a thick ring like ggplot instead of a default thin outline.
- R converter: geom_bar(stat="count") over a bare NUMERIC x keeps ggplot's continuous x spacing -- it emits the built bars' bounds (geom_rect path) instead of coercing x to a factor (equal-spaced bars); a discrete x is unchanged.
- cxplot/ggplot contours: the long->grid reshape sorted the numeric axes as strings (1,10,11,2,...), scrambling the z grid into tangled lines; it now sorts numerically, so geom_contour draws clean nested iso-lines.
- cxplot/ggplot contours: geom_contour(aes(colour=after_stat(level))) colours each iso-line by its level, and stat_contour(geom="polygon") renders filled contour bands (filled-contour path) instead of raw data polygons.
- cxplot/ggplot contours: a contour takes its palette from the ggplot scale (scale_fill_distiller "Spectral") via colorSpectrum, not the native Viridis default; a level-coloured line contour draws a matching level legend.
- R converter: scale_x/y_continuous(limits=..., breaks=..., labels=...) no longer drops the custom breaks/labels -- limits set min/max but the axis still emits the custom tick values/labels; previously limits alone suppressed them.
- cxplot/ggplot: a horizontal geom_segment over a scatter no longer collapses the y-axis to [y,y] -- the segment grows the range only when it reaches beyond the data, so points keep their spread instead of squashing onto the baseline.
- cxplot/ggplot: theme(axis.text.x = element_text(angle = 45)) now rotates the labels -- gg_rotate pins smpTextRotateAuto/varTextRotateAuto off so CanvasXpress's auto label rotation stops re-flattening the explicit theme angle.
- cxplot/ggplot: geom_histogram(position="dodge") now draws the fill groups side by side instead of stacked -- histogramType reads the layer position (the type-driven default always stacked on the normal per-layer dispatch).
- cxplot/ggplot: scale_x_continuous(position="top") / scale_y_continuous(position="right") move the axis (ticks + title) to that edge on a scatter -- converter emits x/yAxisPosition, engine shows xAxis2/yAxis2 and hides the primary.
- cxplot/ggplot: scatter points on a discrete colour scale (e.g. scale_colour_viridis_d) now match their legend -- gg_scales pins the emitted palette as a User scheme so points stop using the GGPlot hue default the legend never used.
- cxplot/ggplot: theme(axis.text = element_blank()) now hides the tick labels while keeping ticks and axis titles -- gg_theme_axis paints the tick text transparent (no axis-text visibility flag exists); axis.text.x/.y handled per side.
- Dotplot legend: a grouped histogram-dotplot no longer leaves the loading spinner stuck over the chart -- the legend read dataMetadata.x[colorBy] (undefined when the fill factor is in z) and threw, aborting the draw before hideMask().
- cxplot/ggplot: the geom_smooth confidence band is drawn semi-transparent again (ggplot's 0.4 ribbon alpha) -- validateColor(args.fill) was dropping the alpha, leaving a solid grey band that hid every data point beneath it.
- cxplot/ggplot: a size aesthetic (geom_point(aes(size=pop))) now uses ggplot's area ramp at the default range(1,6) too -- the >6-only gate left the binned path under-sizing the biggest bubbles; area scaling engages from 6 up.
- cxplot/ggplot: geom_dotplot(aes(fill=factor)) colours each dot by its group -- the global histogram got one index per set (not per point) so dots lost their rows, and a z-annotation colour is now resolved by variable index.
- draw() now always lifts the loading mask (try/finally around drawGraph): an exception in any draw step no longer leaves the spinner stuck over the chart; the error still propagates unchanged.
[73.4] — 2026-11-03
- Submodule an web pages content changes.
[73.3] — 2026-11-02
- Citation (#332): multi-line citations drifted north since 70.9, when drawTextMultiple started honoring 'bottom' (block grows up from centre y0); drawCitation shifts y0 down (n-1)*fontPt/2 to re-centre the block; single-line unchanged.
- New varTextRotateAuto (default on): variable labels along the bottom of a horizontal heatmap stay horizontal when the longest fits its column band, rotating to varTextRotate only when needed -- variable mirror of smpTextRotateAuto.
[73.2] — 2026-11-01
- R examples: the generator overwrote hand-fixed cXdumbbell11 data/smp files; it now writes U+00A0 for sample IDs that read.table's header trim would collapse into duplicates, so smpAnnot matches (issue #329).
[73.1] — 2026-11-01
- Data filters: new dataFilterNumberFormat (falls back to numberFormat) formats range-slider ticks and shows a short label (20M) over the min/max boxes until edited; unset keeps the legacy output.
- generateExamples (few-shot corpus generator) is now excluded from name obfuscation so tools like canvasxpress-mcp generate_few_shots.py can call it; seed Math.random for a reproducible corpus.
[73.0] — 2026-10-30
- R example cXdumbbell11: the 72.9 U+00A0 fix was never applied to the data/smp files, so wave-2 IDs still collapsed to duplicate columns under read.table and failed to match smpAnnot (setup_x error); files now use U+00A0 (issue #329).
[72.9] — 2026-10-30
- CSS load gate: the render pipeline resumed only on the injected canvasXpress.css 's onload, so a CSP-blocked/404/offline host (onload never fires) left every chart blank with no error; it now also resumes on the link's onerror.
- Heatmap: the left-most rotated bottom-axis variable label no longer spills past the left edge and clips -- the left gutter reserves its horizontal reach (floor, capped), mirroring the #322 top/title fix for sample labels (issue #327).
- R examples (cX-function.R): the generator now renders nested booleans as TRUE/FALSE (they parsed as "True") and escapes embedded double quotes in string values, so the sourced file is valid R (issue #328).
- R example cXdumbbell11: trailing-space-unique wave-2 IDs became duplicates under read.table (headers are space-trimmed, row names not), so smpAnnot rows failed to match data columns (setup_x error); files now use U+00A0 (issue #329).
- R example cXtimeseries24: a sample label with an embedded newline split the header across lines, so read.table saw more columns than names and failed; the generator now collapses tabs/newlines in names/values to a space (issue #330).
- R ggplot.as.list: ggplot2 4.0.3.9000 adds a band_gp geom_param to GeomSmooth; the converter leaked it into the emitted JSON and decompiled string, drifting the point_smooth golden fixture; both now drop it (issue #331).
[72.8] — 2026-10-26
- Demo driver: config "demo" + cx.playDemo() operate the real UI with synthetic input and a ghost cursor; the engine records it natively so demoRecording()/replayDemo() replay it in the Workflow widget.
- Customizer Theme widget: 4-up grid of larger tiles; added name icons (c_paper..c_forest) for the 8 website presets that had none. ThemeImagesGroup class moved to the inner wrapper so the exact-className click binding still works.
- Decorations: a datapoint-anchored text marker (smp/var, no value/x/y, reading the point's own value) now centres ON the point -- middle baseline, no margin push -- so a red "X" sits over the point, not above it (issue #325).
[72.7]
- Fixed the Kaplan-Meier log-rank test throwing, or giving a wrong p-value, when the earliest follow-up times are censored: no missing-slot lookup, and padding now uses the true at-risk count.
- Fixed the Kaplan-Meier log-rank p-value when both curves share a time and only one has an event there: the other curve's censoring no longer hides the event (matches a standard log-rank test on tied data).
- Fixed xAxisNumberFormat "auto" (and the other axes) padding ticks to bogus precision (0.17000000000000000) on narrow ranges: auto no longer absorbs the float-noisy tick-step decimals, stays adaptive and never pads.
[72.6] — 2026-10-23
- Heatmap: rotated left-axis labels now reserve top space so the top label clears the title (issue #322), capped at one title block so very long steep labels don't open a big empty band; unrotated heatmaps unchanged.
- Decorations: a sample-anchored text marker (smp) with no value/x/y reads that datapoint's value (variable var, else first) so a red "X" sits on the line/bar point, and maps to the plot slot so it tracks when samples hide (issue #325).
- Added "auto" number format (xAxisNumberFormat:"auto" or {style:"auto"}): tick precision follows the axis decimals, no exponent, no grouping, so 0.14 and 324516 both read cleanly (issue #324).
- highlightSmp/highlightMode on grouped Boxplot/Dotplot/Violin/Bar: marks read the group index as the sample, so a mark lit its whole group; points now use their sample and boxes/bars any member sample.
[72.5] — 2026-10-23
- Added heatmapCellSpace (gap px around each cell, inset in drawHeatmap, opt-in default 0 = legacy contiguous) and a heatmapType "tiled" preset (gap + colorSpectrumBinned + no cell box) for discrete-colour padded-tile heatmaps.
- Added colorLegendLabels: class names for a stepped colour legend (one per class, lowest first), drawn beside each swatch instead of the numeric edges; needs class breaks and colorLegendSteps "equal".
- Perf: 500K-row ggplot histogram renders ~3.5x faster: skip sub-pixel sample-label measuring, count density cells in typed arrays, native-sort large median/quantile, and pin Scatter2D for a histogram-only first render.
[72.4] — 2026-10-22
- Added heatmapCellText: an array of per-cell text labels (like Plotly's text matrix) drawn centered on standard heatmap cells, so a cell can show a value plus a significance marker such as "0.87*" (issue #322).
- Fixed objectColorTransparency ignored by XY area plots (Area with both xAxis and yAxis): draw2DArea fill paths now run the fill through getFillColor like the 1D path, so transparency applies; outline stays solid (issue #323).
- Fixed a rotated line-decoration label clipped at an endpoint on the plot edge: the left/right-aligned anchor is padded inward along the line so the whole label stays inside the panel (issue #323).
- customLegend: an inside legend (legendInside) is now kept within the x,y plotting panel for every legendPosition, with new per-legend paddingX / paddingY to inset it from the panel edges (issue #323).
[72.3] — 2026-10-22
- Toolbar/ESC Reset on an R/isGGPlot ggplot no longer reverts to the grey bare base: createCxplotFinalState captures afterRenderInitial (initStack ran before the geoms) and masterReset's replay ignores the auto cxplotFinal shelf state.
- Fixed ggplot discrete fill bars not matching their legend (bars Viridis, legend hue) for a numeric factor like factor(cyl): gg_scales NoScale keys the colour map by the \x00 level, not stale tp, so bars stop defaulting to Viridis.
[72.2] — 2026-10-20
- Toolbar/ESC Reset no longer empties a scatter with dataPointSize:"auto"; string presets are kept in setInitialConfig instead of being coerced to NaN.
- Reset (masterReset) on a 1D graph with an xAxis (e.g. a one-gene Boxplot) re-derives the visible variables once the config is restored, instead of showing every variable.
[72.1] — 2026-10-19
- getIndices scalar lookups reuse the prebuilt ordered-index hash instead of a linear o.indexOf scan, fixing an O(n^2) load on datasets with very many samples (cxplot histogram of 300k+ observations: ~147s to <1s).
- Reset (ESC / toolbar Reset) on a cxplot/ggplot replays the pristine construction afterRender captured at initStack, restoring the figure (e.g. geom_histogram) and discarding user actions; before it reverted to the bare Scatter2D base.
[72.0] — 2026-10-19
- Documentation updated.
[71.9] — 2026-10-19
- Obfuscation (--secure) now preserves the public accessors getSVGString, dataToR, dataToPython and dataToCxplot so external callers (canvasxpress-dashboards figure composer + reproducible bundle) resolve them in minified builds.
- New statInference layer: opt-in pairwise significance brackets (p/stars) on vertical 1D groups + native pAdjust (R p.adjust parity: BH/bonferroni/BY/holm/hochberg); Volcano gains a volcano config auto-seeding fold-change/FDR lines.
- statInference comparisons honor a hidden flag (skip the bracket), so a dashboard FDR-threshold slider can drop non-significant comparisons live.
- Default graphOrientation for 1D graphs (Bar/Line/Area/Dotplot etc.) is now 'vertical', matching ggplot2 and the cxplot path; types that require an orientation still force it; set graphOrientation:'horizontal' for the old layout.
- Statistics section in the Customizer and a context-menu submenu (Boxplot/Violin/Bar): brackets, correction, annotate, FDR cutoff (statInference.fdrThreshold), comparisons, transforms.
- Auto-stacked statInference brackets space wider (0.18 of range) plus a label+leg clearance floor (exact once pixels are mapped, else estimated from canvas height), so stacked p/stars labels no longer collide with the bracket above.
- Customizer Statistics: picking a different comparison (or changing its Group A/B) now rebuilds the panel so Group A/B/p-value/Height reflect the selection; the select path skipped the refresh the launcher path already did.
- Significance authoring consolidated to Statistics: the Decorate "Comparison" marker is dropped on stat-capable charts (a launcher jumps there), and statInference brackets no longer list as editable decorations (fixing a crash).
- Legacy pwc markers (geom_pwc) fold into statInference at render (migratePwcToStatInference), so the Comparisons list, Show/Hide, correction, FDR and annotate all cover them (2-group -> named, 4-group -> fractional midpoints).
- Comparisons carry an optional facet scope (a Scope dropdown on faceted charts, via bracketPassesLayoutScope), so a scoped bracket draws only in its own panel instead of stacking across every panel.
- Significance brackets now work on HORIZONTAL 1D charts: the gates allow it and the renderer draws a vertical rule with leftward legs and a right-side label (mirroring the vertical bracket).
- Fixed significance brackets jumping off-position under a data transform (e.g. log2): auto-stacked heights are already in axis space (axisSpaceValue) and are no longer transformed a second time; an explicit entered height still is.
- Heatmap/Gantt (and Upset, a Heatmap internally) still force horizontal orientation but no longer flash the 'Graph orientation switched to horizontal' notice on every render that starts from a vertical default.
- Label decoration background box now wraps the text instead of sitting a half-box low (roundrect is centred, so its y is half the measured height below the top edge), removing the empty gap under multi-line labels.
- A decoration/background image that fails or stalls (404, blocked, offline, unreachable host) no longer hangs the chart forever: onerror plus an imageTimeOut deadline render it anyway; a later load re-renders with the image.
[71.8] — 2026-10-16
- Boxplots and dotplots (incl. Cleveland/Dumbbell) now default dataPointSize to 15 so overlaid points render at ggplot2's geom_point diameter; scatter keeps the base default of 10. Applied only when the point size is unset by the user.
- smpTextRotateAuto (now a config option, default true) keeps sample and grouping-factor labels of vertical 1D plots horizontal when the longest fits its band (ggplot-like), rotating only when cramped; re-evaluated on resize.
- cxplot ggplot boxplot+jitter: a fill-mapped box now keeps its group fill at the layer alpha with group-coloured border/median instead of blanking white, and the colour factor keeps its axis labels when it IS the x/y aesthetic.
- Fit lines & confidence areas are clickable (selectableFits, default on): clicking a regression/loess/forecast fit or a KM curve/band selects, broadcasts & recolours the cohort it was computed over (a KM arm = its whole cohort).
- dataPointSize accepts "auto" (opt-in): scatter point size derives from plot area, damped by point count, so sparse/large panels get bigger dots and dense plots stay small; numeric sizes (incl. default 10) unchanged.
[71.7] — 2026-10-15
- Added sankeyHide (Sankey): names a sample annotation flagging links to drop; the ribbon is not drawn but the link stays in the flow graph, so node values, box heights and the showDataValues number are unchanged (#320).
[71.6] — 2026-10-15
- Grouped bar/boxplot ignored a filterUserData filter of raw sample names (dashboard cross-source narrowing): names resolved against group labels and the regroup kept old membership. Now filters ungrouped, regroups fresh.
- Kaplan-Meier kept the filtered curves after a reset: checkKMPlot recomputed only while variables were hidden (count differs), never when they all came back. The cache now records its visible-variable set and recomputes on any change.
- Ridgeline crashed (Object.keys of undefined) when ridgeBy named a sample (x) annotation: no Factor densities were built. Ridge offsets now apply only when they exist; otherwise a plain density is drawn.
- Histogram/Density/Ridgeline over data without variable (z) annotations asked setColorScheme for samples^2 colors (multiplier fell back to the sample count): 278k rows overflowed. The multiplier is now 1 without z.
[71.5] — 2026-10-14
- Clustered heatmap filter ignored after one pick: setDendrograms reused the cached full tree unless toDoFilter was set, so modifyFilter (filterSmpBy/filterVarBy) re-expanded filtered leaves. Fixed; order kept, tree hidden if filtered.
[71.4] — 2026-10-14
- Data table: date-typed sample (x) annotation columns in the transposed view now format as dates -- setDataTableCache's column-meta link was gated on !dataTableTransposed, where x names are never columns, so cells showed epoch millis.
- Oncoprint: the GISTIC 'no alteration' level ('') no longer becomes an empty-named sample annotation (crashed view:'table' on a name-keyed .width); the data table also guards headers missing from its column cache.
- Oncoprint: an explicit graphType 'Oncoprint' (or isOncoprint) on pre-built {y} object data now wires the data2/data3 CNA/MUT layers -- object data skips arrayToCX, so the request was a silent no-op and drew a plain heatmap.
- Oncoprint: ESC (masterReset) no longer demotes an auto-detected oncoprint to a plain heatmap -- the oncoprintCNA/MUT/EXP/Presorted layer wiring is chart identity (like scatterType) and is now carried across the reset.
- Oncoprint: switching a copy-number-only matrix to Oncoprint (Customizer/menu) now converts it to CNA layers; a CNA-only oncoprint no longer throws on the absent mutation layer.
[71.3] — 2026-10-12
- Vector fields: vectorColorValueColor now colours raw arrows (read the unsuffixed name, so it never applied); raw-mode vectorGroupPalette now colours the groups (passed to colorBy via colorKey; a user colorKey wins).
- OptionsWall: put/call flank axes auto-scale to the data again (unset optionsWallPut/CallMin/Max read as 0 via isNumeric(false), pinning 0..1 — premium curves were clipped); now isNumber(v, true).
- Maps: native XYZ raster basemap (canvasXpress.tiles.js) replaces Leaflet: showTiles + tileType/tileAlpha/tileZoomAlphaColor (leaflet* params removed); LRU cache, prev-zoom fallback, on-canvas attribution, no external load.
- Docs (#321): the existing public getSVGString() API, which returns the chart as a serialized SVG string (the programmatic form of the File menu Save as SVG), is now listed in the interface method inventory.
- Maps: panning no longer selects the region under the cursor -- endDrag's Map branch always cleared skipClick (pre-existing; Leaflet masked it) so mouseup fired a click; now preserved (a click still selects).
- Maps: legacy Leaflet config keys (useLeaflet/leafletTileType/leafletAlpha/leafletZoomAlphaColor) are input-only aliases of the tile* keys, so maps saved with the old names keep working after the Leaflet removal.
- Maps: several maps on one page no longer clobber each other's view -- topoJSON-less maps shared one geojson and wrote per-instance center/zoom/parts onto it (all but the last drew wrong; tiles exposed it); each now copies its geojson.
- Maps: orthographic moved to a native ES5 module (canvasXpress.geoNative.js); the vendored d3-geo port (canvasXpress.orthographic.js) removed (Mercator/Albers already in-house). Same seam, render-identical; map-13 pixel-gated.
- Sankey: dropped the vendored d3-collection (nest/Map) grouping for a native ES5 one; getD3Sankey renamed getSankeyLayout. Byte-identical output (19 sankey examples + ggplot alluvial fixtures unchanged); -147 lines.
- Scatter3D: new scatterType 'density' builds a 2D kernel-density surface from scattered points via the native kde2d engine and renders it as a rotatable 3D surface (reuses the function-surface path; no d3). Examples scatter3d-8..10.
- Network: de-d3'd the disjoint force-directed layout -- renamed manyBodyD3 -> networkManyBodyCharge and quadtreeD3 -> networkQuadtree and dropped the 'D3' naming/comments. Pure rename; layout output byte-identical.
- Contour: removed the dead, redundant d3-contour density engine (getD3Contour, ~649 lines, zero callers). 2D KDE density contours use the native kde2d + filledContour; no behavior change.
- Hierarchy: de-d3'd Tree/Circle-pack -- d3hierarchy.js -> hierarchyUtils.js, getD3{Hierarchy,Tree,CirclePack} -> get{Hierarchy,Tree,CirclePack}, 'd3' naming dropped. Pure rename, byte-identical.
- Scatter3D: fxSamples sets the fx/density-surface grid resolution (default 25, was hardcoded; higher = smoother + finer colour); 3D minor grid lines are now OFF by default (show3DMinorGrid; per-axis x/y/zAxisGridMinorShow refine).
- Scatter3D: fx surfaces no longer let setMinY/setMaxY (the fx domain) squish the vertical value axis — it auto-ranges; new aspect3D 'data' scales the 3D box to real per-axis data spans (true proportions; default 'cube' unchanged).
- Scatter3D: 3D axis major-tick count is now automatic (nice number per axis, bounded 4-10) not always 10 — also tightens bounds so the surface fills/centers the box; xAxis3DTicks/yAxis3DTicks/zAxis3DTicks set a target count.
- Scatter3D: new smoothSurface strokes each surface facet with its own fill colour so facets blend seamlessly (no wireframe/seams) for a soft look -- ON by default; set false to show outline3DColor edges. Best with a higher fxSamples.
- Scatter3D: 3D grid fix — each face's two directions now use their OWN axis tick count (grids meet at shared edges when counts differ) and the 2D grid rationale (grey GridMajor/Minor colour + width, not black); frame greyed.
- Scatter3D: *Axis3DTicks is now a TARGET (nice increment for ~N ticks + tight bounds) not a hard count, so a number no longer gives odd wide ranges; and the 3D plot box is larger (canvasWidth/3.3, was /4) to fill more of the panel.
- Scatter3D: 3D axis titles now lay letters along the PROJECTED axis baseline spaced by real pixel widths and follow the axis angle -- was advancing in 3D-coord units by a pixel width, which bunched/overlapped letters.
- Scatter3D: the three 3D axis titles now sit the same distance from their tick labels (a consistent lenX/3 gap) -- the Z title was farther out (step6 vs step4, and step varied per axis).
- Scatter3D: 3D tick labels skip when they would overlap the previous label on the same axis, so an axis viewed nearly edge-on (foreshortened) thins its labels instead of piling them up at the corner; normal views keep all labels.
- Scatter3D: 3D tick labels stay anchored to fixed box edges but skip when they would overlap ANY label already drawn this render -- same or other axis -- so a foreshortened axis thins instead of piling up, without flipping sides.
- Scatter3D: each axis title sits on the same anchored box edge as its ticks, a consistent gap out, so title and labels stay together.
- Scatter3D: zoom stays free but the 3D frame is now clipped (shared setClipArea, as 2D geoms) to the canvas minus the measured legend, so a zoomed-in plot is cut at the edge not painted over the legend/off-canvas; default unchanged.
- Reset (ESC / master reset) now preserves scatterType like graphType, so a 3D function surface or 3D bars no longer collapse to plain points on reset; setConfigDefaults was dropping it.
- Density Scatter3D rebuilds its kde2d surface on draw (idempotent) and pins its axes to the surface X/Y/Z columns, so an ESC/reset reconstitutes the surface with the correct Z axis, not an empty plot or a Z axis of density values.
[71.2] — 2026-10-08
- Customizer Meta panel: property slots (Color/Shape/Size/Group/Segregate/...) pre-fill at load with assigned factors as removable chips, so one can be removed at a time instead of clearing the whole slot; variable/strata shown empty.
- New themes = the canvasxpress.org style presets, same names: paper/slate/mono/sunrise (light, cx-based) and ocean/midnight/graphite/forest (always dark); one name styles the chart AND its widgets; in THEMES, Theme menu, Customizer.
- A chart with no theme on a page whose names a preset (the site's palette icon) takes that preset; explicit theme always wins. Generated 01c-theme-site.css re-tints the --cx-ui-* widget roles per preset.
- One style registry (CanvasXpress.styles + deriveStyleDef): theme also names a widgets look (modern/bms/colorful/rdc/ngs360; [chart, widgets]); dataUIStyle = default|minimal (soft borders); legacy stripped = dataTableStriped:false.
- Every chart theme now styles its widgets from its own accent (CanvasXpress.styles registry: aliases wsj/wallstreet/wallstreetjournal, ggplot/grey/gray/igray, paultol/ptol ... resolve to one style; wallstreet palette fixed).
[71.1] — 2026-10-07
- Site search (canvasxpress.org): the corpus now also indexes the companion repos (README + docs/ markdown), so results span the whole project; repo hits deep-link to GitHub in a new tab. Build/site only; no change to the core library.
- Docs site: four new feature-guide pages (Themes/Colors, Decorations, Statistical Fits & Clustering, Exporting) + Accessibility card on documentation.html, plus a TechArticle/hasPart LLM hook. Site only; no core-library change.
[71.0] — 2026-10-06
- Area graphs: new areaShowColumns (default off) draws a solid bar at each category over the band -- a TrendPlot (stacked bars + translucent area). areaColumnWidth sets bar width (0..1 of slot); composes with any areaType/areaStyle.
- Heatmap overlays: new "Violin" annotation-track type (overlay type:"Violin"), mirroring Boxplot -- a per-segment KDE violin spanning the band, honoring violinScale/violinTrim/violinQuantiles. Default-off; added to Customizer picker.
- Heatmap overlays: getOverlaysForTypePosition grouped EVERY track at a Boxplot/Violin band regardless of type, so a Boxplot/Violin next to a Default/Pie/Bar track clobbered its layout (crash); now groups only same-type tracks.
- Vector-field plots: scatterType "vectorfield" on Scatter2D (new velocity.js) = plotthis VelocityPlot + matplotlib quiver/streamplot; vectorFieldMode raw/grid/stream (arrows, grid quiver, RK4 streamlines by magnitude). Default-off.
- Hover: vector-field arrows/streamlines and clustree nodes/edges now show tooltips -- their custom marks carry ready-made HTML in idx[1], surfaced via a rawHTML branch in extractDataObjectFromIdentifiers (were silent).
- Vector-field raw arrows now colour via colorBy (group annotation): the standard group legend filters groups -- hidden arrows honour the Property legend key like the point loop (grid/stream magnitude ramps stay informational).
[70.9] — 2026-10-05
- Calculated fields: LOD aggregates sum(V,by=A), window calcs (cumsum/lag/movavg/rank/pcttotal, by=/order=), date fns (datetrunc/datediff/dateadd/fiscalyear, UTC; asof() not today()); grammar adds named args (=); grouped dialog picker.
- Scatter2D line hover: a lineBy="Series" line showed "undefined : undefined" because the Series tooltip handler matched the "Line:Series -
" identity; anchored it to ^Series: and show the group label on line hover. - Range decoration hover showed "null - null" for sample-based ranges (smp/smp2): the a||b||c tooltip chain never reached the sample-name branch because "null - null" is truthy; now uses sample names when values are absent.
- Multi-line labels (e.g. a two-line x-axis tick "12 am\nJul 31") ignored the requested baseline and were always centered, so they misaligned with single-line ticks; drawTextMultiple now honors top/bottom/middle.
- Footer "Get the data"/source links were unclickable: the bottom-toolbar trigger strip overlaps the footer band and sat above the link overlay; raised the overlay to --cx-z-footer-link (above the trigger, below the resizer).
- No tooltip when hovering a data point under a toolbar-trigger or resizer edge band (those strips sit above the events canvas): mousemoveDoc now forwards hovers over cX-Toolbar-Trigger/cX-Resizer to the canvas so tooltips still show.
- Drill-down hierarchies (drillHierarchy): click filters to the level value and regroups to the next annotation (Bar/Pie/Treemap group, Circular re-roots, Scatter2D colorBy); HTML breadcrumb, ESC steps up, peers follow; default-off.
- Clustree: new Scatter2D scatterType="clustree" layered clustering-tree from a label matrix or {nodes,edges}: node size=cells+id, colour=resolution; edges colour=count, opacity=in-prop, arrowed; resolution axis + legends; default-off.
[70.8] — 2026-10-05
- Locale number formats: numberFormat + tooltip/legend/dataLabel/*AxisNumberFormat (preset, sprintf or {locale,style,currency,decimals}); Intl-backed, memoized, axis widths follow; default off = legacy path; gate number-format.spec.js.
- Structured chart footer: notes/source(+sourceUrl link)/byline/'Get the data' CSV/footerAlign; canvas-drawn so exports keep it, DOM overlay for links; off by default, citation position unchanged; gate footer.spec.js.
- New showLineEndLabels (Line graphs): series name at each line's end in the series color, legend hidden, overlaps nudged apart, right margin reserved; drawn post-clip on Decorations; off by default; gate annotations.spec.js.
- Marker drag wrote NaN x/y for decorations.marker anchored by variable/sample (curX/curY live on the drawn copy, not the entry): isMarkerDrag/updateDragMarker now seed curX/curY from the grab point, so drag-to-place round-trips.
- New 'editorial' theme (horizontal-only light grid, no box/ticks, left title, muted greys, PaulTol palette) and responsiveRules ([{maxWidth,minWidth,config}]) width overrides that revert on resize; gate responsive.spec.js.
- Footer/citation bottom reservation was gated on citation only, so a footer without a citation overlapped the sample labels (1D + scatter dims); now reserved when either is set. Default off unchanged.
- Pie: new pieShape 'half' (semicircle, election donut with pieInnerRadius) and pieType 'parliament' (one dot per seat on a hemicycle, party wedges); new Pie context menu; gate chart-types.spec.js.
- Arrow dumbbells: new dumbbellArrowDirection 'order' (before -> after); default 'value' unchanged. Vertical arrow dumbbells drew no arrows (equal-x guard); they now point at the smaller value.
- Classed choropleths: colorSpectrumBreaksMethod jenks/quantile/equalWidth (new jenksBreaks, also a calc-field bin method) + stepped legend (colorLegendSteps); off by default; gate map-classify.spec.js.
- Binned colour: sample-column fills ignored colorSpectrumBreaks (legend used them: map and legend disagreed) and the quantised gradient table could shift values near a break up a class; now exact.
- Maps: showMapLabels (fit + no-overlap, contrast colour); mapBubbleColor + matching size legend; WORLD bubbles drew none (centroid table keyed ISO3); topoJSON without mapId crashed (now derived).
- Data table: dataTableColRenderer in-cell bars (zero baseline; summary/annotation rows excluded) and row sparklines; header menu bar toggle; dataTableColFormat takes number formats; gate datatable.spec.js.
- Data table: zero values rendered blank (value || ''), and column formats were applied to numeric-looking headers ('2023' showed as '2023.0'); both fixed.
- a11yDescription: author alt text replaces the generated aria-label (live region still announces data changes) and is added to SVG exports as role/title/desc; gate a11y-description.spec.js.
- a11y: updateData and menu/Customizer redraws never refreshed the canvas aria-label or live region (only full renders did), so both went stale; now refreshed on every data/config redraw.
- Save as PDF (File menu, Customizer, savePDF()): native vector PDF writer (pdf.js, no library); alt text tagged, chart JSON attached like PNG/SVG; PDFtoCX imports it; gate pdf-export.spec.js.
- Data table: in-cell bars/sparklines on numeric metadata (x/z) columns, e.g. a CSV's non-axis columns (the header menu offered them but drew nothing); each column draws only cells of its own kind.
- Data table: with both x and z metadata, the non-transposed table shifted z values up onto the x-annotation rows (last variable row blank/undefined); z rows are now padded in both orientations.
- Data table: clicking In-cell Bars / Remove In-cell Bars in a column header menu now closes the menu (it stayed open after the table redrew).
- Data table: pool rows/columns built beyond the data (first render starts at the default pool) showed as empty striped rows and empty header cells; now hidden. dataTableFitHeight shrinks a short table to its rows.
- Data table: dataTableFitWidth narrows a table to its visible columns (min 400px for the banner and toolbar), like dataTableFitHeight for rows; Bar #19 uses both.
- Data table: resizing a column did not update the horizontal scroller (the scroll image's style width won over its width attribute) until a view change; scroll width and offsets now follow the drag.
- Data table: with dataTableFitWidth, the table re-fits (grows or shrinks, capped at the chart width) when a column resize ends.
- dataUIStyle "minimal": stripped with every outer frame (data table, filters, Customizer, menus) in the inner hairline colour, nearly borderless, in light and dark.
- Data table: dataTableHeaderIcons (auto|always|hover) shows header menu, filter and resize icons only on hover (auto: with the minimal style); sort arrows and an active filter stay visible.
- dataUIStyle: rdc and ngs360 colours swapped (ngs360 blue, rdc grey; logos kept). Table scrollbar thumbs use each style's accent (cx, stripped, minimal, old, bms, rdc, ngs360 were 1.2-1.6:1 on the track; now >= 3:1).
- Pie: bottom legend no longer overlaps the pie (also in layouts, all items drawn, labels follow); legendColumns honoured; citation/footer reserved; parliament and upright half pies sized as half discs (fill the width).
- Gallery: new Time-Series set (25 Datawrapper/editorial charts) + 34 examples across 9 graph types, regenerated by every build from the DW reference specs (tools/dw-examples, build.py --dw-examples).
- Leaflet maps: several useLeaflet charts on one page (all-examples gallery) left all but one blank: loadScripts shared one index across callers; now per-call, and loadLeaflet loads once + queues callers.
- Config schema: optionsWallChain typed object, ribbonBy string-or-array, colorScheme options include its default 'User'; cxplot validate accepted none of these (engine always did). Rerun --schema.
- Circular connections collapsed to the centre when the innermost ring wasn't the central void (the #317 fix forced circularCenterProportion*r); now anchored at the captured inner-ring radius as a ratio of r (on-ring at any size).
[70.7] — 2026-09-30
- Removed the UI CSS variables deprecated in 70.6 (none used by CanvasXpress): --cx-color-extra-dark-gray/dark-gray/medium-gray, --cx-icon-class-a/b/c/r, --cx-color-PRESET[-hover] for 6 presets.
- bms dataUIStyle: strong accent #b529f2 -> #a325da, so accent text on its tint meets WCAG AA (4.76:1, was 3.99:1).
- Version-info tooltip was unreadable in dark UI: it set the surface background but no text colour, so text stayed dark; now uses --cx-ui-text.
- Dark UI: letter/sign icons (font size/style, #, +/-) had no fill so drew black; now --cx-ui-text. Muted #999 icon greys (cxc/cxcc) use --cx-ui-text-muted.
- Customizer: select boxes draw their own theme-coloured chevron, inset from the border (native arrow can't move); textarea return icon centred vertically; +/- spinner buttons spaced out.
- dataUIStyle: Customizer inputs and the password dialog follow each style's border (widgetsBorder applied always, so every style drew modern blue); cx and old now use the compact Customizer.
- dataUIStyle: DataTable frame/search and Customizer cards use the solid border like DataFilter cards (were faint in stripped/old/bms/colorful/ngs360); colorful Show/Hide Toolbar icon was missing.
- colorful dataUIStyle: base colour switched from purple #6a1b9a to blue #1565c0 (first uiAccentPalette colour) for borders, highlights, hovers, tables and odd-row striping; per-item accents unchanged.
- DataFilter: follows a live dataUIStyle switch (icons, colorful accents; selections kept); summary moved into the icon row (overlapped the first header); colorful graph icon visible; Filters label readable in dark.
- Dark UI keeps each dataUIStyle's accent hue (was one indigo): lightened to modern's dark contrast via getDarkAccent/--cx-ui-dark-accent*; colorful items, resize/loader, borders, links too.
- Sankey: showDataValues now appends each node's value to its label, e.g. 'Category A (27)'; off by default (issue #320).
- Code panel: new 'Show Julia code' (dataToJulia) beside R/Python/Cxplot in the menu, Customizer and toolbar, for the CanvasXpress.jl package; emits canvasxpress(Dict(...); key = value, ...).
- Dot graphs with setMin/setMax on the data edge pad the axis by one dot radius (true range kept in *AxisDataMin/Max) so edge dots aren't clipped; arrow dumbbells: head halved and capped to the shaft, shaft stops at the dot edges.
- Layout strips ignored stripTextFontStyle (scaleTextToFont rebuilt the font without the style); the style is re-applied, so bold/italic strip headers work. Default style empty, so unchanged otherwise.
- Bar/Stacked honour an explicit dataValuesFrequency ('first'|'last'|'ends') like Line graphs: only those bars get value labels (zero stack segments skipped). Unset = every bar, unchanged.
- Map markers: text callouts (type line|arrow|curved + x/y canvas fractions or textCoords [lat,lng]) with the 1D-marker connector, and shape 'none' for a callout without a symbol. Plain markers unchanged.
- Country table: Jersey (JEY), Guernsey (GGY), Isle of Man (IMN) added; aliases 'Republic of Serbia', 'North Macedonia', 'Vatican', so EUROPE.json features match data rows instead of an 'undefined' legend entry.
- Left/right-justified top legends (legendHorizontalJustification 0|1) were padded by half the widest label even for discrete legends (ramp-only intent), sitting ~30px off the plot edge; now ramps only.
- New legendJustifyTo ('panel' default | 'plot'): a justified top/bottom legend aligns to the panel edge or, like titleAlign plot::0, to the canvas edge; 1D (variable) top/bottom legends now honour the justification too.
- Bar showDataValues: a negative bar's label was offset as if the bar grew the positive way, landing inside the bar in its own colour (invisible); the offset now follows the value sign.
- New dataValuesHideOverlap (default false): in Stacked graphs a showDataValues label that does not fit its segment (width and height) is skipped instead of being drawn over the neighbours.
- dataTextColor 'auto' (Bar/Stacked showDataValues): black or white per bar/segment by WCAG luminance of its fill (getContrastingTextColor); outside bar labels keep the bar colour. Explicit colours unchanged.
- Variable legends on top/bottom use the factor-legend single-line layout in every path (faceted, colorBy 'variable', BarLine/AreaLine/DotLine/Stacked*Line), not only plain Bar/Line/Area/Stacked.
- Dual-axis graphs (BarLine/DotLine/Stacked*Line, and the AreaLine secondary line) draw point symbols on the line when lineDecoration is set explicitly ('symbol' = series shape); unset keeps plain lines.
[70.6] — 2026-09-29
- Fixed square tooltip-callout arrows in canvasXpress.min.css: the CSS minifier stripped '+' spacing and 0% units in calc()/min()/max(), voiding the rules; build now repairs them.
- UI tokens are now per chart: setUIColors wrote them on the root html element, so the last chart recoloured every other chart's widgets; new applyUITokens scopes them to each chart's cX-DOM.
- dataUIStyle presets unified + WCAG AA: widgets use --cx-ui-* roles (legacy vars still override), per-preset strong accent, --cx-border follows the preset; HTML widgets styled.
- CSS no longer restyles the host page: global rules (box-sizing, input[type=range/file], img/svg) now scoped to CanvasXpress DOM via :where([data-cx-ui-scope]).
- Calc/bin/aggregate/state dialogs rebuilt on shared UI primitives (.cX-Modal, .cX-Button, real button elements); dialogs removed on destroy(); workflow track distinct from its fill.
- Dark UI: theme cxdark/cxblue/auto or new widgetsTheme set data-cx-ui-theme; generated 01b-theme-dark.css swaps the --cx-ui-* roles, also for charts inside a dark container. Calc/aggregate field colours now meet AA.
- UI theme rules are zero-specificity (:where) so a host override like [data-cx-ui-scope] { --cx-ui-accent: #0f766e !important } also wins in dark mode. Theming documented on the Design page.
- One themed checkbox/radio control (05-ui-primitives.css) for every widget checkbox and radio, dark-aware; DataFilter checks were a near-invisible tint. bms/colorful chip tint softened to meet WCAG AA.
[70.5] — 2026-09-29
- highlightMode highlight/ghost now draw highlightSmp marks: adjustColorForHighlightSelection only checked highlightVar, so sample-only highlights never recolored (highlight) or faded with the rest (ghost). Focus unchanged.
- New static CanvasXpress.applyCalculatedFields(data, defs) and validateCalculatedField(formula, names): calculated fields on a data object with no chart, same safe parser, binning and placement as calculatedFields.
[70.4] — 2026-09-28
- Fix #274: colorLegendTitle now titles numeric colorBy (gradient) legends too, and their width fits it; before, only categorical colorBy used it. ggplot labels still take precedence.
[70.3] — 2026-09-28
- Self-contained vignettes render widgets again: the minified lib carries no contiguous "<table" token or raw NUL/non-ASCII bytes, so whole-file text post-processors (e.g. BiocStyle) can't corrupt the library. Identical output.
[70.2] — 2026-09-28
- New Scatter2D forecasts: showForecast fits ses/holt/hw (R HoltWinters, horizon+interval, axes extended), Forecast menu + Customizer; R autoplot(forecast)/geom_forecast render R's own rows. Off by default.
- Live streaming: new pushData(tick) appends samples, evicts oldest beyond streamWindow (drop-oldest), bumps dataVersion to refresh derived caches, then soft-redraws. streamWindow unset = no eviction (byte-identical), off by default.
[70.1] — 2026-09-25
- Grouped charts (e.g. Boxplot groupingFactors) no longer crash when a filter drops a whole group: the kept group order skips out-of-range indices and appends restored groups (shared setGroupIndices, legacy + tidy).
For the complete release history of every version, see the full update log.