💎 Version 7.0.0
A major release whose headline change is a subtraction: the default bundle stops shipping every feature. Two smaller breaking changes ride along, both listed below.
The number that started it: 24% of the 6.10.0 default bundle was licence-gated Premium code, which an unlicensed user could only run with a watermark on their chart. Everyone downloaded it anyway. That is now 0%.
The deeper problem was that the default path was the maximal one. Tree-shaking existed, but you only got it by reading the docs and rewriting your imports, so the overwhelming majority of projects shipped every feature whether they used one or not. Nine features now live behind an explicit import, which inverts the default: the common case is the smaller one, and a feature is paid for by the projects that ask for it.
| gzip | |
|---|---|
| 6.10.0 default bundle | 291,654 B |
| 7.0.0 default bundle | 252,005 B |
| saved | 39,649 B (-13.6%) |
If you use none of the nine, upgrading is npm install apexcharts@7 and nothing else. If you use one, it costs you one line and you stop paying for the other eight.
import ApexCharts from 'apexcharts' no longer includes these. Each is reachable from both channels:
import ApexCharts from 'apexcharts'
import 'apexcharts/features/trellis'
<script src=".../dist/apexcharts.js"></script>
<script src=".../dist/features/trellis.js"></script>
| Feature | Add-on | gzip |
|---|---|---|
| Trellis (small multiples) | features/trellis |
25.7 KB |
| Storyboard (scrollytelling) | features/storyboard |
8.0 KB |
| Perspectives (shareable views) | features/perspectives |
6.7 KB |
| Ink (annotation authoring) | features/ink |
6.2 KB |
| Canvas renderer (Strata) | features/renderer-canvas |
6.0 KB |
| Linked views & crossfilter | features/link |
5.3 KB |
| Measure ruler | features/measure |
4.6 KB |
| Rewind (undo / redo) | features/history |
3.2 KB |
| Context menu | features/context-menu |
2.3 KB |
Storyboard registers Perspectives too, so importing both is no more expensive than importing Storyboard.
Nothing fails silently. Each of the nine warns in the console when its configuration is present but the feature is not, and every warning names both routes. Where the chart can still draw something sensible it does, and says what it did instead: a trellis renders as a single chart, and renderer: 'canvas' falls back to SVG.
Everything else stays in the default bundle: all chart types, axes, tooltips, legend, toolbar, exports, annotations, keyboard navigation, morph, drilldown, themes, plugins (Weave), custom series (Marks) and design tokens (Facet).
The migration guide has the full table and how to tell whether any of it applies to you.
Rounded corners on a stacked bar are no longer a setting. Corner ownership follows the outer edge of the stack, which is what 'last' approximated and what 'all' got wrong on any stack whose last series was empty. Remove it from your config; an unknown option is ignored, so leaving it there is harmless but does nothing.
Data labels ride to their new position on a data-change update instead of snapping there. The bars, markers and axis ticks already reflowed on one clock, so a label that jumped to its final slot on the first frame arrived several hundred milliseconds before the bar it belonged to. Bar and column charts only; a label that has not moved is a per-label no-op. Set dataLabels: { animate: { enabled: false } } for the old behaviour.
One dataset, split into a grid of real charts that share a scale, a legend, a toolbar and a crosshair.
new ApexCharts(el, {
chart: { type: 'line' },
series: [
{ name: 'Revenue', region: 'North', data: north },
{ name: 'Revenue', region: 'South', data: south },
],
trellis: { by: 'region', minPanelWidth: 260 },
}).render()
trellis.by is the whole configuration. The grid owns everything shared, so panels cannot lie to each other: the y domain is the union across panels, the x window is common, and the column count is responsive from minPanelWidth alone. A series carrying no facet key repeats in every panel, which is how you get a reference line.
Also row × column for 2-D grids, trellis.data for tidy rows, virtualization above 64 panels, scoped annotations, composed export, and click-to-promote a panel to full size. Per-type guardrails keep a shared frame honest: histograms share a bin frame, violins a bandwidth, heatmaps one colour scale and a single gradient legend.
Trellis is a Premium feature.
apexcharts/pictograms gives the unit chart a mark per thing counted, independent of how those marks are arranged, so a shape and a glyph compose rather than compete.
Tree-shaking only ever helped people with a build step. A page using a <script> tag had exactly one artifact and no way to decline any of it. dist/apexcharts.core.js is the chart class with no chart types and no optional features, assembled from separate tags:
<script src=".../dist/apexcharts.core.js"></script>
<script src=".../dist/line.js"></script>
<script src=".../dist/features/legend.js"></script>
136,921 B gzipped against 252,005 for the full bundle. Purely additive: apexcharts.js is unchanged, and a page that wants everything should keep loading it rather than assembling it from parts.
Log-scale positions were computed against the wrong domain, and tickAmount was ignored outright. Fixes #1341, #3046, #3345, #4166, #4799, #4873.
Reading e.target after a deferred hover gets the shadow host, not the hovered mark, because retargeting has already happened by then. Every deferred pointer read now goes through the event. A slow hover hid the bug entirely, which is why it survived so long. Fixes #3237.
Two series stacked on ragged data lined up by ordinal rather than by x, so a series with a missing point stacked onto the wrong neighbour, displacing it by as much as 92px. Fixes #4886.
Annotation placement was gated chart-wide on dataPoints, so an empty chart dropped every annotation, including y-axis ones that are always placeable. Gating is now per annotation, on whether that annotation's geometry can be resolved. Fixes #5278.
A stacked bar's bottom cap is the top-rounded path mirrored, and that discrete class snapping over continuous geometry made caps invert or pop when a series collapsed and rose again. Also: grouped stacked charts resolved their caps chart-wide, so only the first group got a bottom radius and only the last a top one.
- Markers, not nulls, dominate a large render: about 8 µs and 16 attribute writes each.
markers.largeDatasetThresholdbatches a series' markers into one path. Opt-in and defaulted off, because merged marker paths can never be pixel-identical to individual nodes. - A null-split line now draws as one path rather than one path per segment.
- The default bundle and its add-ons share one core. Previously
apexcharts.esm.jsinlined its own copy while add-ons resolvedapexcharts/core, so an app importing both got two classes and about 130 KB gzipped of duplicate core for a 6 KB feature, and the feature never registered. The feature registry moved to aglobalThisslot for the same reason. - Fixed a long-standing interop bug:
require('apexcharts/line')threwh.use is not a functionin every published version that had sub-path entries, because rollup's default CJS interop assumesrequire()returns the export itself. - A tier-budget test now fails the build if a Tier 2 feature reappears in
features/all.js, or ships without both a sub-path export and a UMD artifact. - The e2e baseline is back to zero failures: eight snapshots had drifted by a single pixel against dense samples and were refreshed after each was checked against its reference.
Full Changelog: https://github.com/apexcharts/apexcharts.js/compare/v6.10.0...v7.0.0
💎 Version 6.10.0
A feature release built around one question: what if the arrangement of a chart's marks were something you could hand it?
The unit chart already drew one dot per thing counted, and 6.9.0 opened a seam for supplying the positions yourself. This release ships the thing that seam was for. apexcharts/unit-shapes is a companion kit of 39 shapes a count can take: a heart, a house, a globe, a checkmark, a heartbeat trace, or the figure 1,024 drawn in 1,024 dots. Each one is a function of the marks and the plot rectangle rather than a picture, so the same shape serves 40 dots in a sparkline and 3,000 in a poster.
Alongside it: outer name labels for those shapes, so a crowd of dots reads without a legend, and a fix worth reading even if you never draw a heart, because it swallowed any update that changed only a callback.
A new entry point, tree-shaken per shape. Importing one costs about 4 KB gzipped; importing the catalog is not something you need to do.
import ApexCharts from 'apexcharts'
import { heart } from 'apexcharts/unit-shapes'
new ApexCharts(el, {
chart: { type: 'unit' },
series: [57600, 16800, 4200, 3400],
labels: ['Repeat donors', 'First-time', 'Workplace drives', 'Emergency call-ups'],
plotOptions: {
unit: { layout: 'custom', positions: heart, unitValue: 100 },
},
}).render()
A shape is a plain callable, so positions: heart needs no registration step, and plotOptions.unit.positions already accepted a function: nothing in the chart had to learn about shapes.
The dots are packed, not stamped onto a template. Rows are cut across the outline, each row split into the spans that fall inside it, and the gap between dots is then bisected until the spans hold exactly the number of marks the data asks for. Density follows the shape's own area, which is why one outline covers three orders of magnitude of dot count. A thin limb, fin or tip keeps its single dot rather than dropping out, because that is what stops a shape dissolving as the count falls.
Three kinds, because things in the world are not all areas:
- Silhouettes (29) fill an outline:
heart,house,tree,leaf,flame,droplet,fish,sun,human,group,star,crown,trophy,moneybag,funnel,shield,gear,robot,bulb,flask,car,plane,rocket,battery,pin,mountain,cross,bolt,arrow. - Strokes (7) pack a thickened centreline, for a thing with no interior:
check,wifi,pulse,xmark,percent,question,spiral. A dotted checkmark still reads as a checkmark, which is how a stroke degrades where a thin silhouette feature would simply vanish. - Generated (3) compute their positions from maths and have no outline at all:
globe(latitude rings with a tilt),target(concentric bands),pyramid(tiers).
Every shape carries its metadata: which category it belongs to, how it was made, and minUnits, the count below which it stops being recognisable. Ask for fewer and the chart says so in a console warning naming the shape, rather than rendering mush.
outlined(heart) // trace the outline instead of filling it
heart.with({ order: 'cols' }) // where each series band lands inside the shape
glyphs('1,024') // the number, drawn in that many dots
preview(heart, { series }) // -> an SVG string, no chart and no DOM
outlined() gives all 29 silhouettes a hollow twin for no new artwork, since a stroked closed path is a ring. The fill order is what decides where each category sits: rows bands a shape top to bottom, cols left to right (which is why battery fills like a charge meter), centerOut puts the first series at its heart. preview() renders a shape to standalone SVG, so docs galleries, README images and launch graphics can be generated at build time or on a server from the catalog alone.
Shapes are also registrable by name (ApexCharts.registerUnitLayout), and from a script tag dist/unit-shapes.js exposes the kit as ApexUnitShapes with every shape pre-registered.
On provenance: every outline in the kit was drawn in this repository. No third-party path is admitted, permissively licensed or not, because an outline ships verbatim inside the bundle: a copied path would make its licence notice travel into every consumer's build forever. Brand marks are excluded outright. A test enforces it.
A shape packed with four categories used to need a legend, which asks the reader to match a swatch to a band. Names can now sit in the margin with a leader line to their own dots, the way a pie names its slices.
plotOptions: {
unit: { clusterLabels: { external: { show: true } } },
}
The gutter is reserved on both sides before the dot size is chosen, so the shape is sized for the room it will actually get instead of being scaled down afterwards, and it stays centred. Each label anchors on a real dot of its own band, sides are assigned from how the bands are actually arranged, and crowded labels are spaced apart in one pass before they reach the DOM. The implementation is the one pie and donut already use, extracted rather than rewritten.
Twenty new unit demos, one per shape family, including a gallery that switches between all 39 shapes with the same dots flowing from one arrangement into the next. Palettes use separated hues rather than tints of one colour: at dot size a lightness ramp cannot be read back, and the palest end disappears against the card.
update() skips a redundant render by comparing the incoming options with the previous ones, and that comparison went through JSON.stringify, which drops function values. Two configs differing only in a callback therefore serialised identically and the update was thrown away.
Any function-valued option was affected: a new dataLabels.formatter, a new custom tooltip, a new plotOptions.unit.positions. The first such update always worked, since there was nothing to compare against yet, which is what made it look like a rendering problem rather than an update problem.
Functions are now compared by identity: passing the same function twice still skips, so the optimisation keeps paying, while a different one gets the render it asked for. A caller who builds a fresh closure on every update now gets a render every time, which is the safe direction to err in, since the closure may capture new state.
A data point whose x is a Date object had its milliseconds truncated, so points inside the same second collapsed onto each other. The type definitions also refused a Date there, despite it being the natural thing to pass. Thanks to @aron-intframe (#5277).
- the
unitandunit-shapessub-entry artifacts are built and published, soapexcharts/unitandapexcharts/unit-shapesresolve for bundlers and script tags alike - shape geometry is covered by its own suite: containment against the outline it claims, spacing, the exact-count guarantee at every dot count, provenance, and a cross-check that the catalog, the exports and the type definitions can never disagree
- two authoring tools ship with the repo rather than the package: a contact sheet that renders every shape at a given count, and a winding checker that catches a subpath which would punch a hole where it meant to fill
Full Changelog: https://github.com/apexcharts/apexcharts.js/compare/v6.9.0...v6.10.0
💎 Version 6.9.0
A feature release, and the largest in a while. Two ideas run through most of it.
The first: a chart should be able to take the measurements you actually have. Three types now accept raw observations and do the statistics themselves, and rowSeries() hands the individual rows back so a mark can be opened into the data behind it.
The second: one chart type can become another, in place. The cross-type morph engine stops crossfading and starts conserving the ink, so a bar visibly comes apart into the dots it was counting, and every pairing between the mark families is now offered rather than only the ones that had been driven.
Alongside those: nested treemaps, drilldown for line and area plus async levels that survive a real backend, a pluggable layout seam for the unit chart, and spring motion where a fixed tween used to stutter.
One change worth knowing before you upgrade: ApexCharts is no longer dependency-free. It now requires apex-commons at runtime (see Internal). npm resolves it for you and the browser bundles inline it, so no action is needed, but it is a change in the package's shape.
Every other type here wants values that were already aggregated. A histogram is the one that does the aggregating: the series carries raw observations, one number per event, and the chart chooses the bin width and counts them.
chart: { type: 'histogram' },
series: [{ name: 'Latency', data: [102, 87, 143, 91, ...] }]
It renders through the bar pathway, like funnel, pyramid, gauge and waffle, so bins are drawn by code that already handles stacking, zoom, export and animation.
plotOptions.histogram.bins takes a rule ('auto' | 'fd' | 'sturges' | 'scott' | 'rice' | 'sqrt') or a fixed count. binWidth pins the boundaries when they carry meaning, range frames the axis independently of the data, normalize switches the y units to percent or density, and cumulative gives a CDF. 'auto' takes the narrower of Freedman-Diaconis and Sturges.
All series share one set of edges derived from their combined extent, so two distributions stay comparable instead of putting different bars at the same x. plotOptions.histogram.overlap (default true) then draws each series across the full bin rather than grouping them beside each other, because comparison is the reason to put two samples on one axis and grouping is the arrangement that misreads it. A single series is unaffected either way.
The binning ships behind apexcharts/features/stats, so it costs nothing if you do not use it.
A box plot required y: [min, q1, median, q3, max] and a violin required a precomputed density profile. Both asked the caller to do the statistics that give the chart its meaning, which is backwards: the numbers you have are the measurements.
Supply the observations and the library computes the rest. They go in points, the field both types already use for jitter dots, so a sample lives in exactly one place whether you summarise it or we do.
series: [{ data: [{ x: 'Phone', points: [1.2, 1.9, 3.4, ...] }] }]
Quartiles interpolate between ranks (R type 7). Whiskers default to the extremes, so nothing is hidden by default; plotOptions.boxPlot.whiskers: 'tukey' switches to the 1.5 × IQR convention.
A histogram bin, a box and a violin all stand for rows that the chart is already holding. chart.rowSeries() returns them as a series, which makes the summary and the observations two views of one dataset:
chart.updateOptions({ chart: { type: 'unit' }, series: chart.rowSeries() })
It returns null when the current type has no row source. The sources ship with the statistics behind apexcharts/features/stats; core keeps only the lookup.
The unit pairings used to read as "the old chart vanished and a new one animated", and the reason was structural: the exit was a photocopy of the whole outgoing chart fading over the incoming one. Frame by frame that is a double exposure, two pictures both half-visible, neither becoming the other. No amount of easing fixes a crossfade.
A morph between one mark and N objects now cuts the mark into exactly N cells and flies every cell to its object, corners rounding off and fill blending on the way. A summary mark is cut along its own silhouette rather than its bounding box, and a wedge along its curve, which is why a donut's hole survives being taken apart.
Beyond that, any two mark families now pair. The engine used to decline combinations simply because nobody had driven them; the only pairing still closed is the dot cluster against a partition, where the divider has no cut for a tile or an arc and the result would fall back to a fade. Treemap and sunburst pair at every level rather than only the leaves, and a box plot unfolds into its violin and folds back.
Every offered transition is now covered by a motion test that asserts the marks actually travel, rather than counting the elements that exist at the end.
A treemap could only draw two levels, a series and its rows, so anything deeper had to be flattened by hand, throwing away the structure a market map exists to show. A datum may now carry children to whatever depth the data has.
Squarify became recursive: a branch is laid out inside its parent's rect with a header strip and per-level padding, and a container's area is the sum of its children exactly, so a parent always holds what it contains. Flat inputs are untouched and render identically to the float.
The hierarchy resolver is now shared with the sunburst, including the drilldown: '<id>' adapter, which the treemap opts into with nested.drilldownAsLevels.
Drilldown was wired for line and area but inert, and worse than inert: a real click did nothing in every default configuration while the pointer cursor promised otherwise. With markers.size: 0 there is no element to click, and even with markers shown, core marks line and area markers no-pointer-events so the shared tooltip can track the plot. The feature now supplies a markers.discrete entry per drillable point, so only those points carry a dot and it reads as "these open".
Async levels close the phase that makes drilldown usable against an API. The organising idea is that a failed fetch is ordinary, not exceptional, so it must never strand the view: on a throw, a rejection, or a resolver returning something without a data array, the chart stays where it was, the breadcrumb is untouched, nothing is cached, and drillDownError fires. That last case previously no-opped in silence, which is indistinguishable from "the click did nothing". There is also a loading overlay, theme-aware, role="status" with aria-live="polite", whose spinner flattens to a pulse under prefers-reduced-motion.
Every arrangement the unit chart could draw was hard-coded, so a new one meant a core edit and the set was closed. plotOptions.unit.layout: 'custom' opens it:
plotOptions: { unit: { layout: 'custom', positions: (objects, rect) => [...] } }
positions takes (objects, rect) => [{ id, x, y, r? }], or the name of a layout registered with ApexCharts.registerUnitLayout. A layout is objects in, positions out, and nothing else: it knows nothing about animation because the engine already tweens position, radius and colour and already keeps a mark's identity across a relayout. Marks the provider omits animate out through the existing exit path; ids matching no mark are ignored.
objects carries identity and data per mark rather than just an index, so a provider can address a specific unit rather than a positional slot.
A fixed-duration tween cannot be interrupted: when the next render lands mid-flight it rebuilds the marks at the slot they had not reached yet and re-animates from a standstill. A dragged slider or a scrubbed storyboard interrupts on almost every frame, so that read as a continuous stutter. The gather now runs on a spring, which retargets and keeps its velocity.
Both state visuals a pie or donut slice had were recolourings: hover lightened the fill, and a click darkened it and redrew it at a 4px larger radius. Neither says "this slice" as plainly as motion does, and the click one was quietly dishonest, since growing the radius inflates the quantity the slice encodes. A click now slides the slice out along its own mid-angle, and hover traces an outline band. Legend clicks toggle the slice in and out through the same path.
Also new here: plotOptions.pie.borderRadius and plotOptions.pie.spacing, for pie, donut and polar area.
An exported SVG is a standalone document, and the PNG path rasterizes it through <img src="data:image/svg+xml,...">. An SVG loaded as an image cannot fetch external resources and cannot reach the page's stylesheets or its loaded fonts, so anything the export left as a URL was not merely slow to appear, it was gone. Fonts, images and patterns are now inlined into the download.
With series[].group each group is its own stack, so a grouped stacked bar should carry one total above each group's bar. It drew one label per data point instead, holding the sum of every series in the chart and centred on the middle of the whole cluster: on two groups of two that meant a single "75" floating between the bars rather than "15" over one and "60" over the other. A 100% stack also now reserves room for the total it otherwise had nowhere to put.
Three independent cases where a coordinate was measured from the wrong origin:
- the hover hit-test and bar centres, so the tooltip could caption the neighbouring bar (#5272). Thanks to @lazerg (#5275).
- datetime gridlines that fall outside the plot are now skipped rather than drawn on the axis (#5273). Thanks to @lazerg (#5274).
- an annotation label's background box (#5270). Thanks to @mrash (#5271).
tooltip.intersect anchors a pie, donut or polar area tooltip on the arc centroid, which each slice stamps on its path in its own user space. Those were read as if they were SVG-root coordinates, dropping the inner group's translate, and that translate is exactly the offset that centres a pie in a chart wider than it is tall. The caption appeared a couple of hundred pixels to the left of the slice it described. Keyboard navigation had the same arithmetic and the same bug, and now shares one helper with the pointer path.
Type defaults are applied once, when the chart is first rendered, so updateOptions({ chart: { type } }) left every choice the outgoing type had made for itself in place. A box plot that became a violin kept the five-number tooltip formatter and threw on every hover, which killed the caption and stranded the crosshair on the first category; a bar that became a box plot never acquired that formatter at all and got the plain series tooltip instead of its summary.
The leaves that decide what a chart reads, says, hit-tests or offers as interaction are now re-chosen when the type changes. The ones that decide how it is painted deliberately are not, so changing type does not restyle a chart out from under a morph in flight, and a palette chosen for a bar survives its becoming a line. Anything you set yourself is never re-chosen, including in the same update call.
- a hidden polar area series gives its slot back, and updates animate in place rather than rebuilding
- a
--apx-surfacetoken change now updates the background it had itself set, so an OS light/dark flip or a host app swapping its design system is picked up - a non-array
seriespassed toupdateOptionsis ignored with a warning instead of poisoning the config and crashing every later update - a legend toggle no longer re-bins a histogram's counts, which had made the remaining bars change shape
- the drilldown breadcrumb reserves a band above the plot, and only the room it actually lacks
- the walk back to a stacked series' baseline is corrected for line charts
- unit scatter axis chrome follows the configured axis label colour
xaxis.labels.style.fontSize: 'inherit'no longer yields NaN (#5064). Thanks to @waterWang (#5256).- a hover arriving after the grid is gone is ignored, and the tooltip arrow parks flush against the marker edge
- ApexCharts now depends on
apex-commons(^0.5.0) at runtime. The licence manager had been a vendored fork that had drifted from its origin, making the family's licence contract two implementations kept in step by hand; and the crossfilter engine was never chart-specific, so keeping the only copy inside a charting library meant a map, a grid or a tree had to install ApexCharts just to coordinate a filter. Both now come from the shared package, along with the spring primitives the unit chart uses. The browser bundles inline it, so nothing changes for script-tag users. - the e2e calendar is pinned, so date-based sample snapshots stop rotting by the day
- histogram binning moved out of core, behind
apexcharts/features/stats
Full Changelog: https://github.com/apexcharts/apexcharts.js/compare/v6.8.0...v6.9.0
💎 Version 6.8.0
A minor release: dataLabels.offsetX / offsetY now accept a function, so a label can be nudged per data point instead of per chart. Everything else is a fix, spanning sparkline layout, brush auto-scaling, CSP-safe SVG export, threshold gradients and CSV export.
One deliberate visual change: area sparklines lose the empty strip under the fill (see below). Every other existing config renders as it did on 6.7.1.
dataLabels.offsetX and dataLabels.offsetY now take number | ((opts) => number). The function receives the same { series, seriesIndex, dataPointIndex, w } signature that dataLabels.style.colors already accepts, so labels that collide between two series at the same x can be pushed apart:
dataLabels: {
offsetY: ({ seriesIndex }) => (seriesIndex === 0 ? -12 : 12),
}
dataLabels is chart-wide config, which is why a plain array keyed by data point index could not solve this: it would apply identically to every series, and the reported overlap is between series. A function also survives updateSeries, where captured indices would otherwise desync. Keep it pure, as it may be called more than once per label.
Resolution now runs through one shared helper across the line/area, bar, treemap and radar paths, which fixed four latent defects on the way:
- line, area and scatter labels all vanished when the offset was non-numeric, because
xwas computed above theisNaN(x)guard and the guard could never fire - the slope chart branch read the raw config value instead of the resolved one, yielding a
NaNx coordinate - radar passed its series index as the data point index, so per-point offsets shifted whole series
- bar and rangeBar invoked the user function a second time at draw time for a value they discard
A sparkline reserved stroke.width / 2 of grid padding at the top and bottom unconditionally. An area sparkline's fill runs to the baseline, so that bottom inset showed as a strip of empty space under the fill: 2px at the default 4px area stroke.
The inset now reserves only what the ink cannot absorb itself. Where the stroke traces the data points (line, area, scatter, unstacked), the distance from the extreme datum to the axis extreme already swallows part or all of the overhang, so only the remainder is reserved. Fills reserve nothing since they are drawn unstroked, and stroke.show: false reserves nothing at all. Anything that strokes to the baseline or fills the plot (bar, heatmap, candlestick, stacked) keeps the full reservation, as does every non-axis sparkline. Room is measured against the smallest plot the insets could leave, so the estimate errs toward over-reserving and can never clip.
Two defects found while measuring this are fixed alongside it:
Dimensions.gridPadaliasedconfig.grid.padding, so layout insets were written back into the user's own config object, accumulated across renders, and were then read byCore.resizeNonAxisChartsas though the user had asked for them. The resolved padding is now a copy, published asw.layout.gridPad.- the sparkline marker padding gate tested
markers.size > 0, which isfalsefor an array ([0,6] > 0isNaN > 0), so array-sized markers got no padding and were clipped by 6.5px. It now gates onglobals.markers.largestSize, covering bothmarkers.sizeandmarkers.discrete.
A brush selection reconstructs its x range from the selection rect's DOM bounds, so the pixel to timestamp round-trip can land xaxis.max a sub-pixel fraction below the timestamp of the boundary data point. The y-extrema window trimmed on a strict compare, so that point was excluded from the scale while its marker and the line segment leading to it were still painted, and the line escaped or clipped at the top of the grid. Reaching the same window by panning scaled correctly, which is what made it look arbitrary.
The trim window is now widened by one rendered pixel, expressed in data units from the current x-domain-to-pixel ratio rather than a fixed timestamp epsilon. Both edges are covered, since a sub-pixel overshoot on xaxis.min drops the leftmost point the same way. This also covers a programmatic zoomX() with fractional bounds, and the xaxis.min / xaxis.max reported to your selection event are unchanged.
getSvgString() and the SVG download no longer inject a <style> element, so exports work under a strict Content Security Policy. Styles are inlined onto the elements instead.
The bulk of the work was keeping export fidelity while dropping that tag. The legend stylesheet was injected into a descendant of the exported wrapper, so it was cloned and serialized anyway and still tripped CSP. Transient overlays were hidden only at the first match per selector, so a chart with several (one yaxis tooltip per y-axis, an extra element for point annotations) rendered the leftovers visibly, since their opacity: 0 came from the stylesheet the export no longer carries. Inline styles set by modules are no longer clobbered, which preserves legend.fontSize (the legend box is measured at that size, so a hardcoded 14px overflowed) and the heatmap gradient legend's deliberate overrides. Rules that the inlined subset had dropped are restored: flex-wrap and flex-direction for side and grouped-horizontal legends, alignment, legend-group display, marker positioning, the !important on hidden zero and null series, and the flip transforms used by rounded stacked bars. With injectStyleSheet: false, which is what a strict-CSP app sets, side legends had been exporting as a single horizontal row and bottom legends had stopped wrapping.
Thanks to @waterWang for the fix (#5257).
plotOptions.line.colors.threshold gradients are now positioned over the axis range. Null values in an area chart with threshold colors are handled correctly, and three further problems in the same area are fixed:
- the offset was derived from the data range while being mapped over the axis range, so the color transition drifted off the threshold whenever the axis extended past the data, via an explicit
yaxis.min/max, a nice scale, or a shared axis - the anchoring was gated on a chart-global null-values flag, which re-anchored every vertical gradient in the chart, including plain gradient fills with no threshold configured. It is now keyed off the threshold feature itself.
chart.type: 'line', the primary consumer ofplotOptions.line.colors, had the identical split-segment defect and was excluded by a type gate
Reversed axes now mirror both the boundary and the stop order, and stops are emitted in ascending order rather than relying on the SVG rule that clamps an out-of-order offset.
Thanks to @waterWang for the fix (#5261).
The unequal-x branch of exportToCSV pushed an array onto rows rather than a delimiter-joined string, so Array.prototype.toString stringified it with a hardcoded comma. Every data row separated the category from its first value with , while the header and remaining values used the configured delimiter, producing output no parser could read:
category;series 1;series 2
0,0;
1,1;1
The default , hid it entirely, which is why it went unnoticed.
Thanks to @Jaybhade for the fix (#5253).
resolveDataLabelOffsetlives inmodules/helpers/DataLabelOffset.jsrather than the sharedDataLabelsmodule, so the split per-chart bundles inline it andcore.jsis untouched- dependency bumps: undici 7.29.0 (#5250), ip-address 10.4.0 (#5249)
Full Changelog: https://github.com/apexcharts/apexcharts.js/compare/v6.7.1...v6.8.0
💎 Version 6.7.1
A patch release on top of 6.7.0: three interaction and layout fixes, most importantly a point-selection regression that broke slice clicks on pie and donut charts, plus a new vertical orientation for the unit chart's beeswarm layout. Every existing config renders unchanged.
pathMouseDown was bound to the chart instance instead of the Graphics instance that owns _togglePointSelection, so a slice click threw this._togglePointSelection is not a function and selection never toggled. The same mis-binding applied to the mouseenter, mouseleave, mousedown and touchstart listeners on markers, which affected line, area and scatter charts using dataPointSelection. Anything routing through a point click was affected, including drilldown: the "Donut with Drilldown" demo could not be drilled at all on 6.7.0.
Thanks to @andrewbusch7 for the fix (#5252) and to @kne1 for the report.
The fit-to-content branch in Core.resizeNonAxisCharts read its angular span from radialBar's angles regardless of the active chart type, so a pie, donut or sunburst always reported a full 360 and skipped the branch. A semicircle therefore reserved the whole circle's square and left a dead band between the arc and a bottom legend. The span now comes from the active type's own start and end angles, sunburst is included in the selector, and a bottom legend is re-anchored inside the shrunken wrap. Full circles are unaffected.
Clicking a second wedge while the previous zoom was still tweening started a competing animation on the same arcs, and the first zoom's late callback clobbered the second's result, leaving the chart frozen half-zoomed. Superseded frames now stop writing, and an interrupting zoom resumes from each arc's live geometry.
plotOptions.unit.scatter.orientation accepts 'horizontal' (the default) or 'vertical'. Vertical puts the value on the Y axis with category lanes as columns, and the swarm packer now packs along either axis.
plotOptions: {
unit: {
scatter: { orientation: 'vertical' },
},
}
The value-axis domain now always contains every datum in both orientations: an explicit xMin / xMax frames the axis and is extended by whole tick steps when data would fall outside it, so a dot is never drawn past the axis where it cannot be hovered.
A new beeswarm sample gallery ships alongside it: body mass by species (vertical), salary by department (horizontal, one CVD-validated hue per team), and a game-scores bubble beeswarm.
💎 Version 6.7.0
This release introduces a new Sunburst chart type, richer pie/donut styling, new unit (pictogram) layouts, and a large batch of reliability, correctness, and security fixes.
A hierarchical radial chart (a nested pie/donut) for tree-structured data. Rings go from the center hole outward, one per hierarchy level, with each child arc nested inside its parent. It ships as its own tree-shakeable entry point, so the core bundle size is unchanged if you do not use it:
import ApexCharts from 'apexcharts/sunburst'
// then:
chart: { type: 'sunburst' }
Corner rounding and inter-arc spacing are configurable via plotOptions.sunburst.borderRadius and plotOptions.sunburst.spacing.
- Pie / donut slice styling. New
plotOptions.pie.borderRadiusfor rounded slice corners andplotOptions.pie.spacingfor an inter-slice gap. The same styling applies to polarArea. - Unit (pictogram) charts, arc layout. A new parliament / hemicycle layout (
plotOptions.unit.arc) arranges marks as seats in concentric arced rows across an annulus, filled in category order. Atype: 'waffle'alias is also available for the unit chart's single-grid layout. - Unit charts, configurable enter motion. The gather animation easing and enter motion can now be tuned.
- Point annotation tooltips. Point annotations can show an optional hover tooltip.
- Legend click on pie / donut / polarArea now toggles the slice in and out. Previously a legend click darkened and expanded the slice. If your app relied on the old behavior, review this change.
- Premium features are now gated behind a Premium (or above) plan. Features and chart types that require a license now enforce it. If you use those, make sure your license plan covers them.
Data and series
- A series missing its
dataproperty no longer aborts parsing of the remaining series; it is treated as empty and the parsed series stay aligned with their names. rangeNameno longer mutates your series config; range ids are tracked internally instead.- Combo charts use the real series index for goal lines and data labels, fixing mismatched labels and goals on filtered combos.
Lifecycle and SSR
- Server-side rendering no longer crashes on image fills. Out-of-range annotation
yAxisIndexand a sunburst drilldown cycle are fixed. - Animation, resize, and timeout callbacks are guarded against firing after a chart is destroyed, and the detached SVG root is released on teardown.
updateSeries/updateOptionspromises now reject on a render failure instead of hanging.resizeNonAxisChartsis guarded against a missing parent node in shadow or detached DOM.
Rendering and interaction
- NaN / Infinity guards in polarArea, custom series, axis labels, tooltips, and gradients.
- Zoom and tooltip: fixed a stuck shift-latch on a persistent slice, a duplicate mousewheel binding, and a custom-tooltip guard.
- Empty candlestick series no longer crash; heatmap row-extent and brush-target guards added.
- A window-resize redraw is skipped when the drawing box is unchanged.
Accessibility and misc
- Keyboard-navigation listeners are removed correctly on update, fixing a listener leak per
updateOptions. - Annotation tooltips no longer suppress the series tooltip chart-wide, and are cleared on
clearAnnotations/removeAnnotation. - The drilldown breadcrumb stays clear of the chart title.
- Global flags (
hasNullValues,invalidLogScale) reset every render. - Three verified logic fixes: a scale-max typo, a selection-filter guard, and an epoch-0 timestamp.
Security
- CSV export now guards against formula / CSV injection.
Building and testing from source now requires a Node version with require(esm) support (^20.19 || ^22.12 || >=24), matching the jsdom 28 test toolchain. Running the tests on an older Node fails fast with a clear message.
Full changelog: https://github.com/apexcharts/apexcharts.js/compare/v6.6.1...v6.7.0
💎 Version 6.6.0
The headline is a new premium chart type, unit: one mark per unit of value, drawn as dot clusters, pictograms, waffles, or beeswarms, with a keyed tween that re-forms the marks whenever the data, grouping, or filter changes. It ships with six layouts, per-mark data, and a waffle alias, and it is the first premium chart type (it renders in trial mode with a watermark until a key is set). This release also adds signature verification to the license manager and fixes a zoom-out edge case. Existing configs render unchanged.
chart.type: 'unit' renders a discrete mark for every unit of value instead of a single bar or slice, so "37 of 200" reads as a countable quantity. It is a non-axis chart (dispatched like pie or treemap) and is tree-shakeable via import 'apexcharts/unit'. On every update each mark tweens from its old position to its new one, so re-grouping, filtering, or a changing count re-forms the marks rather than redrawing from scratch.
new ApexCharts(el, {
chart: { type: 'unit' },
series: [276, 266, 3],
labels: ['For', 'Against', 'Abstain'],
plotOptions: { unit: { layout: 'grouped' } },
})
Six layouts via plotOptions.unit.layout:
grouped(default): one phyllotaxis blob per category, laid out in a row.packed: one shared blob, coloured by group and sorted so the minority nests in the centre.columns: each category is a vertical bar built from stacked dots (a waffle column).grid: one waffle lattice, a part-to-whole square "pie" (grid.totalrounds it to a fixed cell budget, e.g. 100 for a percentage waffle).gridwithsplit: true: small multiples, one mini-waffle per category over a faint track, in a trellis.scatter: marks on real value axes, as a 1D beeswarm (laned by category, deterministic anti-overlap packing), a 2D value-value plot (scatter.y: 'value'), or area-scaled bubbles (scatter.sizeRange). The layout draws its own axes with a homegrown nice-number scale.
Also included:
- Shapes:
circle,square, andimage(isotype pictogram, withimage.tintto recolour a monochrome icon to its category colour). - Per-mark data: alongside flat counts, an object form
series: [{ name, data: [{ value, x, z, name, fillColor, id }, ...] }]gives each mark its own colour, position, size, and tooltip content. - Transitions (
transition):group(default, per-category),flow(the anonymous crowd migrates and recolours across a regroup, the circles-to-bars effect), andidentity(a specific mark persists across any regroup or relayout, keyed byid/name). - Sizing: numeric or
autodot size, opt-insizeByValuebubbles,unitValuewaffle scaling (1 mark = N units), and amaxUnitssafety cap. - Labels and chrome: per-cluster
clusterLabels(a curved arc over a blob or a straight label above / below a bar,position: 'top' | 'bottom'), per-mark tooltips, legend click to hide and show a category with an animated re-flow, and the standardfill.opacityso overlapping bubbles read through each other.
Nine interactive demos ship under samples/*/unit and samples/*/waffle: a workforce dot cluster, a population age-slider, a pictogram population, a team roster, a life-expectancy beeswarm, a cost-of-living bubble scatter, a scrollytelling marathon storyboard, a startup-funding walkthrough, an electricity-mix waffle, and an urbanisation small-multiple waffle.
chart.type: 'waffle' is a thin alias of unit: it presets the grid layout with square cells, so a part-to-whole waffle is one line of config. With grid.total: 100 the values are largest-remainder rounded to exactly 100 cells, so the grid always reads as percentages. The original type is preserved on chart.requestedType, and an explicit layout or shape still wins.
new ApexCharts(el, {
chart: { type: 'waffle' },
series: [35, 23, 15, 9, 8, 6, 4],
labels: ['Coal', 'Gas', 'Hydro', 'Nuclear', 'Wind', 'Solar', 'Other'],
plotOptions: { unit: { grid: { columns: 10, total: 100 } } },
})
- Zoom-out never stalls on the last category. While zooming out, the high edge is now rounded up instead of down, so the visible span grows by at least one whole category per step rather than appearing stuck at the edge.
plotOptions.unit is fully typed across all layouts and their option groups (grid, scatter, clusterLabels, sizeByValue, image, columns, tooltip), and chart.type accepts 'unit' and 'waffle', with chart.requestedType carrying the original alias.
- No breaking API changes, and no renamed or removed options.
unit(and itswafflealias) is the first premium chart type: it renders fully in trial mode with anAPEXCHARTSwatermark until a key is set. Every other chart type stays free and is never watermarked.- The unit chart is opt-in and additive; charts of every other type render unchanged.
- New unit regression tests (packing determinism, layout geometry, keyed transitions, scatter axes and bubbles, waffle cell allocation, legend toggle, and premium gating) run alongside the existing interaction and end-to-end suites.
💎 Version 6.5.0
A release built around interaction polish and one new capability. Mouse-wheel zoom is now smooth and cursor-anchored, the brush/selection now lines up exactly with the bars underneath it, and a run of interaction fixes clears up crossfilter, heatmap updates, and group tooltips. It also introduces optional license enforcement for the premium features: they keep working without a key (trial mode), just with a watermark. No chart types are gated, and existing configs render unchanged.
Seven premium modules now run under a lightweight, offline license check: storyboard, link (crossfilter / linked views), ink, measure, contextMenu, perspectives, and history. Without a valid key they still work fully in trial mode, but the chart shows an unobtrusive APEXCHARTS watermark; a valid key removes it. Everything else, every chart type and every free module, is never gated and stays silent.
ApexCharts.setLicense('APEX-...') // or per-chart via chart.license
A few things worth knowing:
- In use, not bundled. Importing a premium module without actually enabling it does not watermark; only using it does.
- Live. A late
setLicense(validKey)followed by an update clears an on-screen watermark; no full re-render needed. - One key across the family. The key format is shared with the rest of the ApexCharts family (apexgantt, apextree, apexsankey, and friends), validated offline with no network call. SSR-safe.
Wheel and trackpad zoom used to run a fixed step at most once every 400ms and drop everything in between, which read as lag. It is now coalesced per animation frame and anchored to the cursor: the data point under the pointer stays put while the window scales around it, so a trackpad's stream of small deltas feels continuous. The zoomed event fires once per gesture, not once per wheel tick.
- The brush/selection now matches the bars underneath it. On numeric and datetime bar charts, brushed ranges drifted from the columns they visibly covered (the first column lit up too early, the last could never be fully selected). Each gesture had been computing its own pixel-to-data conversion, so fixing one path quietly desynced another. There is now a single source-of-truth mapping shared by bar placement and every selection gesture (new drag, dragging the rect, resize handles, and a preselected
chart.selection.xaxis), so the reported range always equals the rectangle you see. Range-binned crossfilter histograms now span their outer bin edges too, so every bin, including both edges, is fully brushable, and achart.link.bins: { width }option is no longer silently dropped. - Heatmap y-axis labels survive a data-only update. Name-based (series-name) heatmap y-axis labels no longer flip to numeric ticks after the first fast-path
updateSeries. - Crossfilter charts stay rendered when a wrapper pushes an empty series. A React/Vue wrapper syncing its placeholder
seriesprop right after mount no longer blanks a filter-mode chart; the engine re-asserts its aggregated series. - Group tooltip no longer skips the hovered chart, and horizontal-bar data labels honor
offsetX.
chart.license is typed on the chart options.
- No breaking API changes, and no renamed or removed options.
- All chart types and free modules are never gated. The seven premium features run in trial mode with a watermark until a key is set; this is the only behavior change, and it does not block any functionality.
- Bar/column layout is unchanged: the selection fix routes bar placement through the same math it already used, so rendered positions are identical.
- New regression tests cover the license gating (per-feature on/off, in-use vs bundled, late key, SSR no-op), the selection/brush geometry consistency across all four gestures, and the crossfilter bin edges, alongside the existing unit, interaction, and end-to-end suites.
💎 Version 6.4.0
A feature release centered on heatmaps and a new bar chart race. Heatmaps gain a continuous numeric and datetime x-axis (cells positioned by real value, not by column index), optional canvas rendering for large grids, and a tooltip that now points at the cell it describes. The bar chart race animates bars and their labels as they re-rank. Two fixes round it out. Existing configs mostly render unchanged; three heatmap defaults change (tooltip placement, zoom, and label thinning), each noted below with how to restore the previous behavior.
On a numeric or datetime heatmap, cells are now placed at their real x value instead of being tiled one per column by index. Irregular spacing and gaps therefore render as real empty space: a missing hour is a gap in the grid, not a column squeezed away, and the axis shows sparse proportional date and time ticks rather than one label per cell. Rows stay categorical (one series per row).
This is what makes irregular-time and calendar-style heatmaps expressible with plain data shaping. Two new demos are built entirely on it: a server-CPU timeline where a metrics outage shows as a real gap, and a contribution-style calendar with ragged first and last weeks.
Evenly spaced heatmaps are unaffected: their layout is unchanged.
With chart.renderer: 'canvas' (or 'auto' past the render threshold) and the tree-shakable canvas feature imported, heatmap cells now paint to a single canvas instead of one <rect> per cell. The DOM node count stays flat regardless of cell count, and paints are roughly 2.8 to 3.5 times faster at high densities. Hover still resolves the exact cell under the cursor and shows its tooltip. Cells that use a fill the canvas cannot reproduce (gradient, pattern, image) fall back to SVG automatically.
| Heatmap cells | SVG | canvas |
|---|---|---|
| 10,000 | 95 ms | 27 ms |
| 50,000 | 519 ms | 170 ms |
| 100,000 | 1,083 ms | 388 ms |
import ApexCharts from 'apexcharts'
import 'apexcharts/features/renderer-canvas'
const options = {
chart: {
type: 'heatmap',
renderer: 'canvas', // or 'auto' to switch above rendererThreshold
},
// ...series
}
A reorder update now animates into a bar chart race. When you re-sort the data and update the chart, the bars slide to their new ranks and their category labels ride along automatically (whenever dynamicAnimation is on). Two opt-in flags complete the effect: dataLabels.animate rides each value label to its bar's new position, and dataLabels.countUp tweens the number from its previous value.
const options = {
chart: {
type: 'bar',
animations: { dynamicAnimation: { speed: 800 } },
},
plotOptions: { bar: { horizontal: true } },
dataLabels: {
enabled: true,
animate: { enabled: true }, // value labels ride to the new rank
countUp: { enabled: true }, // and count up or down from the last value
},
}
// On each frame, re-sort your data and call updateOptions with the new series
// and categories. Bars, category labels, and value labels animate to the new
// order together.
Both label flags are off by default and apply to bar and column charts. Rotated axis labels ride correctly too.
The heatmap tooltip now sits centered above the hovered cell with a downward arrow pointing at it, flipping below when the cell is against the top edge. This matches the horizontal-bar tooltip and makes it unambiguous which cell the tooltip describes; previously it trailed the cursor. Opt back into the old behavior with tooltip.followCursor: true. Custom heatmap tooltips keep the arrow as well.
A heatmap is a fixed grid, so wheel, drag, and pinch zoom only distorted it, and on a datetime axis it collapsed the month labels into repeats. Zoom is now disabled by default, mirroring treemap. Re-enable it with chart.zoom.enabled: true.
When a heatmap has more rows than can fit as readable labels and no custom y-axis formatter is set, it now shows every Nth label, evenly spaced, instead of an unreadable overlap. The ticks and the plot are unchanged, and setting your own yaxis.labels.formatter opts out.
- Light series no longer wash to white on hover. The lighten hover filter pushed already-bright fills all the way to white, so light-colored series lost their hue when hovered. The filter now preserves the color.
dataReducerno longer mutates your data. With zoom-aware downsampling active, the reduced (windowed) view was written back into the originalseriesarray, which is shared by reference, so later re-renders started already downsampled and could never recover the full-resolution points. The reducer now operates on a detached copy, leaving your input intact.- Custom tooltips keep their arrow. A
tooltip.customfunction replaced the tooltip's inner HTML, which discarded the arrow element. The arrow is now preserved across custom content, for every chart type.
dataLabels.animate and dataLabels.countUp (bar chart race) are typed on ApexDataLabels.
- No breaking API changes, and no renamed or removed options.
- Evenly spaced heatmaps render as before; continuous-x only repositions cells when the x values are irregular or gapped.
- Three heatmap defaults change (tooltip placement, zoom off, y-label thinning), each with a documented opt-out above.
- New regression tests cover the canvas rect batching and cell hit-testing, the above-cell tooltip placement, and the bar chart race label ride and count-up, alongside the existing unit, interaction, and end-to-end suites. Heatmap end-to-end snapshots are expected to update where continuous-x repositions cells and where y-labels are thinned.
💎 Version 6.3.0
A performance release focused on updates. updateSeries is now genuinely incremental: a data-only update repaints the series and refreshes the axis chrome in place instead of tearing the chart down and rebuilding it, so streaming and frequently-updating charts are several times faster. Large-series initial render is also markedly quicker from shared parsing work. There are no API changes and no new options: the rendered output is verified identical to 6.2.0, so existing configs render exactly as before, just faster.
The numbers below are 5-trial medians (initial render) and per-cycle medians (updates) from the reproducible harness behind the "100,000 Points" rendering benchmark, measured back to back on one machine (headless Chromium, animations off, identical seeded data).
Previously every updateSeries call re-ran the full render pipeline: parse, re-layout, and a complete DOM rebuild. It now takes a fast path that repaints only the series layer and, when the axis scale changes, redraws the grid and axes in place within the frozen layout. The canvas renderer repaints its existing bitmap instead of recreating the backing store. Anything the fast path cannot reproduce exactly (a change in series count or data length, collapsed or combo series, an active zoom) falls back to the full render automatically.
updateSeries cycle |
6.2.0 | 6.3.0 |
|---|---|---|
| 50,000 points (canvas) | 62.5 ms (16/sec) | 4.1 ms (242/sec) |
| 50,000 points (SVG) | 66.7 ms (15/sec) | 12.5 ms (80/sec) |
| 10,000 points (canvas) | 11.1 ms (90/sec) | 1.3 ms (765/sec) |
| 10,000 points (SVG) | 11.1 ms (90/sec) | 2.8 ms (362/sec) |
The parse pipeline no longer forces a deep clone of the series and several whole-series aggregate passes that most charts never read, and plain numeric [[x, y], ...] data now parses in a single typed pass that also computes the axis extrema inline (removing separate min/max scans). The path geometry itself was already fast; this release removes the surrounding per-render overhead.
| Line, single series | 6.2.0 | 6.3.0 |
|---|---|---|
| 100,000 points (canvas) | 90 ms | 29 ms |
| 100,000 points (SVG) | 106 ms | 40 ms |
| 50,000 points (canvas) | 57 ms | 25 ms |
| 50,000 points (SVG) | 65 ms | 30 ms |
| 10,000 points (canvas) | 27 ms | 19 ms |
Scatter and bubble charts share the parse pipeline, so they pick up a portion of the same improvement without any scatter-specific work.
| Scatter | 6.2.0 | 6.3.0 |
|---|---|---|
| 50,000 points (canvas) | 155 ms | 120 ms |
| 50,000 points (SVG) | 571 ms | 528 ms |
| 20,000 points (canvas) | 73 ms | 60 ms |
- Data-only updates no longer leak DOM nodes. Because the incremental path preserves the chart DOM across updates instead of clearing it, two transient elements that the full render had always discarded were accumulating: a stray crosshair backing rect and the y-axis crosshair tooltip container, added once per update. On a continuously updating chart this grew without bound. Both are now reused across updates, so the node count stays flat over any number of updates. This matters most for real-time and streaming dashboards.
- Brushing or zooming after a linked-chart update works again. A chart updated in place by a crossfilter or linked view kept a stale reference to its grid geometry, so a subsequent range brush or drag-zoom drew an empty selection. It now reads the live geometry on each interaction.
- The DOM subtree is preserved across
updateSeries. Data-only updates now keep and update the existing series, axis, and grid elements rather than replacing them. Rendered output (SVG path data and canvas pixels) is verified identical; code that re-queries chart elements after an update by class or attribute continues to work, but code that cached a specific element node reference from before an update and relied on it being replaced should re-query instead.
- No new options, no changed defaults, no TypeScript changes.
- SVG path output and canvas pixels are verified identical to 6.2.0 across the snapshot suite; the fast path is held to the full render's output by a pixel-level oracle and by format-equivalence tests on the new parse path.
- Verified by the full unit, interaction, and end-to-end snapshot suites, plus new regression tests covering the incremental update path, the parse fast lane, and the per-update node-count guard.