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.
✨ New
chart.type: 'histogram', a chart that takes a sample
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.
Box plot and violin take the sample too
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.
rowSeries(), so a mark can name the rows it stands for
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 cross-type morph conserves the ink
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.
Nested treemaps
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: line and area, and async levels
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.
A layout for the unit chart can now come from outside
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.
Unit marks travel on a spring
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.
A pie slice that moves instead of changing colour
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.
🐛 Fixes
An exported SVG carries its fonts and images (#343)
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.
A stacked total per group, not per data point
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.
Tooltips and grid chrome measured from the plot origin
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).
A slice tooltip lands on the slice
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.
A chart that changes type behaves like the type it became
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.
Others
- 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
🔧 Internal
- 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: v6.8.0...v6.9.0