NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
A JavaScript Chart Library
Last release 2 days ago
11 Oct 2026
Ships on a steady schedule
a new release about every 2 weeks
Some releases are documented
notes for 25 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
8 years old
298 releases · first in 2018
Weave plugins can now draw a part of each value with api.highlight(), the way the highlight filter does, instead of adding a series.
Weave plugins can now draw a part of each value with api.highlight(), the way the highlight filter does, instead of adding a series.
The rest is a fix release from a full audit of the 8.x code: about 100 fixes to updates, tooltips, data parsing, legends, exports, gauges and server rendering. A few change behaviour you may rely on:
| Change | To keep the old one |
|---|---|
| Updates no longer write into your object | read chart.w.config |
| Grouped updateOptions resolves with self | getChartByID() a sibling |
| A chart's yaxis beats window.Apex.yaxis | set it in Apex only |
| Gauge bandsStyle.gap is in pixels | raise the gap |
| Gauge needle moves at the arc's speed | needle.animation.duration |
| hydrate() keeps animations, toolbar off | pass them on the client |
Update calls on a destroyed chart now warn and resolve instead of throwing, and render() after destroy() works. CSV export writes one row per category for OHLC and range series, and the time of day when two datetime categories share a day.
The default bundle is 267,379 B gzipped, up 3,375 B (1.3%) from 8.0.1. The full bundle is 385,816 B gzipped, up 3,853 B (1.0%) from 8.0.1.
Upgrading is npm install apexcharts@8.1.0.
Full changelog, with the detail behind each change: v8.0.1...v8.1.0
One column per quarter.
A patch on 8.0.0. If you render charts on the server from data you do not control, upgrade: server output now escapes text and attribute values, so a
A patch on 8.0.0. If you render charts on the server from data you do not control, upgrade: server output now escapes text and attribute values, so a series name or label can no longer inject markup.
Line charts render and heatmaps update faster again. Two per-point scans that every unstacked line render had paid since 7.0 are gone, and a heatmap update now tweens only the cells that moved.
Weave plugins can read the raw sample behind a histogram, boxPlot or violin through api.data[].observations (detect it with api.can('observations')).
Upgrading is npm install apexcharts@8.0.1.
Full changelog, with the detail behind each change: v8.0.0...v8.0.1
8.0 takes three chart types and four features out of the default bundle, so most pages download less, and adds a bundle that has everything. If a char
8.0 takes three chart types and four features out of the default bundle, so most pages download less, and adds a bundle that has everything. If a chart stops drawing after the upgrade, the console names the line to add.
| Moved out of the default bundle | Import next to 'apexcharts' |
|---|---|
| unit (with waffle and beeswarm) | apexcharts/unit |
| sunburst | apexcharts/sunburst |
| violin | apexcharts/violin |
| drilldown | apexcharts/features/drilldown |
| waterfall | apexcharts/features/waterfall |
| dumbbell | apexcharts/features/dumbbell |
| streamgraph | apexcharts/features/streamgraph |
With script tags, add the matching file: dist/unit.js, dist/sunburst.js, dist/violin.js, or dist/features/.js for the four features. Raincloud, already opt-in, now needs dist/violin.js before dist/features/raincloud.js. Add-on tags may come before or after the ApexCharts one. Or load apexcharts.full.min.js (import ApexCharts from 'apexcharts/full' in a bundler), which has every chart type and feature.
On the lean core (apexcharts/core), violin and raincloud also need apexcharts/bar, and a boxPlot or violin of raw observations needs apexcharts/features/stats; without it the console now says so.
Unversioned CDN URLs move to 8.0 today. Pin apexcharts@7 to stay on 7, or add the add-on tags now: the files exist in 7.9.1 too.
The default bundle is 263,188 B gzipped, down 28,575 B (9.8%) from 7.9.1. The new full bundle, apexcharts.full.min.js, has every chart type and feature in one file: 381,135 B gzipped.
Upgrading is npm install apexcharts@8.0.0.
Full changelog, with the detail behind each change: v7.9.1...v8.0.0
Nothing published for this version
A patch with no API change. Three things a page can notice.
A patch with no API change. Three things a page can notice.
apexcharts.min.js now prints the console warnings it always should have. It is built with console calls stripped, so a chart asking for a feature the page had not loaded (trellis, ink, measure, the canvas renderer and the rest moved out in 7.0) failed in silence there and warned only in the unminified file. Those warnings now reach the console of a script-tag page, and so does a new one for a chart type that is not registered, which used to leave an empty chart and nothing else.
On a page that loads dist/features/perspectives.js with a script tag, the static perspectives API (ApexCharts.perspectives.decode and fromURL) now shows the trial watermark without a licence, as it always did in a bundled app. The add-on kept its own copy of the licence state, so the signal never reached the chart.
The stylesheet inlined into the bundles is now minified. The rules are unchanged; apexcharts.min.js is about 6.4 KB smaller gzipped, and dist/apexcharts.css stays readable.
The default bundle is 291,763 B gzipped, down 6,360 B (2.1%) from 7.9.0.
Upgrading is npm install apexcharts@7.9.1.
Full changelog, with the detail behind each change: v7.9.0...v7.9.1
7.9.0 adds highlightFilter and a Basque locale, and fixes animation and tooltip defects across most chart types. Four behaviours changed, listed below
7.9.0 adds highlightFilter and a Basque locale, and fixes animation and tooltip defects across most chart types. Four behaviours changed, listed below.
| Change | What you will see | To keep the old behaviour |
|---|---|---|
Unknown chart.type throws |
new ApexCharts() and updateOptions() throw and name the closest real type. They used to fail later, inside render(). |
Fix the type name. '' and null now draw a line chart. |
| Unknown options and bad axis bounds warn | A console.warn for a top-level key nothing reads, or an axis bound that cannot be parsed. Numeric strings such as yaxis.min: '10' now work. |
Correct or remove the option. |
| Box plots animate updates | Boxes morph from their previous shape on updateSeries(). |
chart.animations.dynamicAnimation.enabled: false |
| Needle gauge value label | Sits below the needle instead of under it. | Set plotOptions.radialBar.dataLabels.value.offsetY. |
TypeScript: getState().seriesNames, getState().labels and a series' name are now typed string | number.
highlightFilter (premium add-on, apexcharts/features/highlight-filter): draws each value faded and a part of it solid in front, from the same baseline. Works on bar, column, funnel, line, area, pie, donut, polarArea, radialBar and treemap.eu) locale, and the Galician toolbar labels completed (#5319). Thanks @jlancharro.updateSeries().undefined or null no longer crashes the chart, and updateOptions({ xaxis }) keeps category names.updateSeries() (#5332), and the last label stays centred unless it would overflow.[x, y] data flattens it onto the series below.theme.mode to updateOptions() is kept (#5041). Thanks @jamalkamaladdin.legend.showForNullSeries works with { x, y } data (#5336). Thanks @jamalkamaladdin.seriesName no longer throw when there are fewer axes than series (#3836). Thanks @jamalkamaladdin.The default bundle is 298,123 B gzipped, 24.3 KB more than 7.8.0, from the animation and tooltip work.
npm install apexcharts@7.9.0
The detail behind each change is in the commits: v7.8.0...v7.9.0
Two default changes to know about before upgrading, both about data labels.
Two default changes to know about before upgrading, both about data labels.
Data labels that land on each other are now nudged apart. A chart whose labels already clear each other is untouched, but anywhere two of them currently overlap, they will move. Turn it off with:
dataLabels: { avoidOverlap: false }
This matters most on a chart with two y-axes, where the axes are scaled independently and a collision is therefore not a question of how close the values are: a column at 59.5K and a line at 68K can land on the same pixel row while 51K and 51.3K sit well apart. Labels separate along the value axis, so a horizontal bar's move sideways rather than across its rows. A pair that cannot be separated is left overlapping rather than quietly dropped, since removing a value is worse than the overlap; pass { hide: true } if you would rather lose one. Rotated labels and the radial types keep their own placement.
It is on by default because a label sitting on another is never what the chart meant to say, and the pass does nothing to a chart that does not need it. Across the 320 demo charts that predate it, not one renders differently.
A waterfall's data labels have lost their pale chip. The ink follows chart.foreColor instead, which reads over a rising, falling or total bar in either theme, and on the small steps whose label sits outside the bar. The chip is still there if you want it:
dataLabels: { background: { enabled: true } }
It is worth more than it was, incidentally. The chip used to inherit the range column's white ink, and a chip takes its fill from its text, so on a light theme it was white on white.
Two performance fixes ship without a section of their own. A CPU profile of a hover sweep over a 700-series chart put 38% of all hover time inside native querySelector and another 10% in forced layout, from lookups repeated per marker rather than per gesture; both are now done once per gesture. Separately, data label backgrounds are measured in one pass instead of relaying the SVG out once per label, which on thousands of labels is the difference between seconds and milliseconds.
dataLabels.style.colors entries may be functions. That has always been true at runtime and the documentation has always said so; the TypeScript declaration said string[] and now agrees.
| gzip | |
|---|---|
| 7.7.0 default bundle | 272,129 B |
| 7.8.0 default bundle | 273,832 B |
Both are dist/apexcharts.min.js gzipped at the default level, which is the figure npm run build prints.
Upgrading is npm install apexcharts@7.8.0.
A waterfall printed every step on a pale rounded chip. On a dense bridge that is a second rectangle per bar competing with the bar it names, and the type is usually a dense bridge.
The chip was not decoration, though, so removing it alone would reopen what it was added to fix. Small steps are normal in a waterfall, and a label wider or taller than its bar is placed OUTSIDE it; the range-column defaults the type inherits draw labels in white, so those labels became white text on the chart background and the smallest steps of a P&L bridge simply vanished.
So the ink moves instead of hiding behind a chip. dataLabels.style.colors now carries a function that resolves to chart.foreColor, which is dark on a light theme and light on a dark one, and reads over an increase, decrease or total bar either way. Verified in both modes: #373d3f on light, #f6f7f8 on dark, and no rect in the DOM.
An entry in that array has always been allowed to be a function - the resolution in plotDataLabelsText calls one if it finds one - but the declaration said string[]. It now says what the implementation does.
Set dataLabels.background.enabled to get the chip back.
Reported from the field: a dual-axis chart printed "$59.53K$59.53K" where a column series and a line series coincided, and a waterfall ran its currency labels together between adjacent quarters.
dataLabelsCorrection looks like it already handles this, but it cannot. It is keyed on dataLabelsRects[i], one series, and it runs WHILE that series is plotting, so the labels it would collide with do not exist yet. It also measures the raw text rather than the background pill, which is why a few pixels of pill overlap got through even within one series.
dataLabels.avoidOverlap is a pass over every label once they are all drawn, before the pills are cut. It is on by default: a label sitting on another is never what the chart meant to say, and the pass is a no-op on a chart whose labels already clear each other.
The thing worth knowing for a multi-axis chart is that collisions are NOT a function of how close the values are. The two axes scale independently, so only pixels decide. Sweeping the line series from -30% to +30% of the columns in 1% steps: collides -27%..0%, clean +1%..+11%, collides again +12%..+17%, clean after that. 34 of 61 configurations collided, 88 pairs in total, 0 after.
Design, each point of which was a defect found by auditing 25 configurations across types, orientations and label positions:
{hide: true}). A default-on pass that silentlygetBoundingClientRect, not getBBox. getBBox isplotOptions.bar.dataLabels.orientation: 'vertical' measures 29x14 while ity attribute runs alongmaxShift from its own mark.Geometry lives in its own helper over plain numbers, so it is tested without a DOM; the DOM half needs real text metrics and is tested in the browser. Both suites assert the collision is still there with the option off, so neither can pass vacuously.
Two samples carry currency formatters and the reported shapes: a 15-step waterfall bridge and the dual-axis combo.
BREAKING CHANGE: trellis.promote now defaults to false. A header click no longer expands its panel over the grid unless you set trellis.promote: true…
One behaviour change to know about before upgrading. Clicking a trellis panel header no longer expands that panel over the grid. The interaction is now opt-in:
trellis: { by: 'region', promote: true }
An embedded trellis is usually read rather than driven, and nothing on a header says it is a button until the cursor changes over it, so taking over the whole grid on a stray click was a large surprise for an interaction nobody had asked for. chart.promotePanel(key) and chart.restorePanels() are unchanged and work either way.
trellis.minPanelHeight is new, default 80px. A grid that cannot fit its container now overflows, and says by how much, rather than shrinking its panels past the point where anyone can read them. Lower it when fitting a short container matters more than legibility.
The rest of the release is a field report from a team building a viewer on ApexCharts, closed end to end: thirteen defects across the trellis grid, the tooltip and the update path. Three reach well beyond the trellis. updateOptions({ series }) was ignored by every type whose series are derived from the caller's raw input, so a waterfall, a histogram, a dumbbell, a streamgraph or a treemap kept redrawing its first dataset forever while updateSeries worked. updateOptions could not change any tooltip option either, because the tooltip module held the config it was born with. And a column's hover band sat a full stroke width left of the bar it described.
The licence text now states the $2M threshold as total revenue, funding or financial resources, whichever is highest, across a reader's parent company, affiliates and anything under common control. A funded pre-revenue startup and a $3M-budget non-profit are both over it; the old revenue-only wording caught neither.
| gzip | |
|---|---|
| 7.6.1 default bundle | 271,606 B |
| 7.7.0 default bundle | 272,129 B |
Both are dist/apexcharts.min.js gzipped at the default level, which is the figure npm run build prints.
Upgrading is npm install apexcharts@7.7.0.
A treemap tile fills the plot area, and the legacy beside-the-cell placement threw the tooltip half a tile above the one being hovered: on a chart near the top of the page the box landed off-screen entirely.
Two faults, both in the same branch. The height terms that centre the box were swapped, cy + ttHeight / 2 - height / 2 instead of cy + height / 2 - ttHeight / 2; on a heatmap cell that is a few pixels, on a 220px tile it is 88. And cx / cy are grid-local while the result is written into style.left / style.top, which is elWrap-relative, so the grid's own offset was never added back. The horizontal clamp added for #5121 had been masking the second fault; a heatmap with y-axis labels still had its tooltip drift the width of the axis to the left.
Centre on the cell, convert grid-local to elWrap coordinates, and clamp vertically the way x already was. Arrow-mode heatmaps, which is the default, take the branch above this one and are untouched.
Measured across treemap (2 and 6 tiles), heatmap with arrow: false and followCursor: every tooltip now sits dead centre on its cell and inside the plot area. The new spec rebuilds the reported page, a 150px spacer above a 240px chart, and fails on the old placement with the box 197px off-centre.
Fixes #5321
Every axis label carried an SVG <title> holding its own text, so hovering one raised a native browser tooltip repeating what was already on screen, with nothing in the config to turn it off. xaxis.tooltip.enabled: false is a different feature, the crosshair tooltip, and rightly left it alone.
That <title> was added for #2281, whose ask was the truncated case: show the short form, put the full name on hover. It has never been accessibility machinery; nothing under src/modules/accessibility reads it, and the <title> elements the accessibility statement documents belong to the root . On a a child <title> in fact overrides the text content as the accessible name, so a redundant copy displaces the real one.
Keep it where it earns its place and drop it everywhere else. Labels are shortened in a later pass over the rendered DOM, not where the <title> is attached, so the decision can only be made once every axis is drawn and corrected; hence a sweep rather than a check at attach time.
A label drawn in full, including every y-axis value and both lines of a multiline label, now has no <title>. One cut short by labels.maxWidth, by trim with or without rotation, or by a horizontal bar's y-axis keeps its full text on hover.
Fixes #5318
With a bar stroke the hover band sat about 3px left of the painted column and the bar's right edge fell outside it. With no stroke at all it was still up to a pixel off.
handleBarTooltip derived the band's centre from the bar's cx attribute and its rendered geometry width. getColumnPaths insets the path by half the stroke on each side, so a stroked bar reports a cx that is strokeWidth/2 smaller and a width a whole strokeWidth smaller: together they drag the band a full stroke width left. parseInt on a fractional cx threw away the rest, which is the sub-pixel offset visible without a stroke. The comboBarCount > 0 branch added half the stroke back, for combo charts only, which is why the error looked smaller there.
Use the bar's rect-derived centre instead. A stroke grows a bar symmetrically, so that centre does not move with it, and the band needs no correction term at all. Horizontal bar-likes draw no x crosshair and only feed the x-axis tooltip, which still wants the bar's end, so they keep the coordinate they had.
Measured against the painted bar span: the centre error was -0.704 / -2.704 / -6.704 px for stroke widths 0 / 2 / 6 and is now 0.000 for all three, single series and grouped, with barWidth, tickWidth and fixed-width bands alike.
A waterfall computed its running totals once and then ignored every series passed through updateOptions, while updateSeries took them. So did the histogram, the dumbbell, the streamgraph, the treemap and the zoom-aware downsampler: anything whose series transform accumulates from a stash of the caller's raw input.
The stash exists because Data.parseData writes a transform's output back to config.series, so re-running against the derived rows would bin, accumulate or stack a level deeper on every render. _updateSeries already dropped the stashes when the caller redefined the input. _updateOptions never did, so the stash outlived the data it was taken from and the transform kept rebuilding the first dataset forever.
Drop them from both paths, through one helper so the list cannot drift apart again. Gated on overwriteInitialConfig, which is the same line _updateSeries draws with overwriteInitialSeries: the public API sets it, while the library's own replays (a Rewind restore, a trellis panel sync, a storyboard beat) pass false and hand back DERIVED rows. Clearing on those would feed accumulated pairs back in as deltas.
Reported against 7.1.0 and all still live on 7.6.1. Two further items in the same report turned out to be fixed already: the axis of a stackType: '100%' grid reads 0..100 correctly, and trellis: undefined does turn a trellis off. Both are left exactly as they are.
Shared y scale ignored stacking. yExtent folds individual values, so a panel piling 40 + 40 took a domain sized by the largest single value and its second-series bars started 114px above the grid top. stackedYExtent measures the per-x totals instead: positive and negative runs accumulate separately, series group by series[i].group the way the core groups them, and stackOnlyBar keeps a reference line out of the pile but on the axis. '100%' is exempt, because the core renormalizes every stack itself and a competing domain would only fight it.
Scatter dropped duplicate x values and indexed events across all panels. Union alignment is load-bearing for slot marks: it is what makes ragged bar panels pixel-align and what makes the group's index-matched tooltip sync caption the same x everywhere. A point cloud has neither property, so aligning one dropped every duplicate x with a warning and padded each panel out to the union of every panel's x values, leaving dataPointIndex naming a position in that union rather than in the panel's own data. Scatter and bubble now keep their own points; the shared x DOMAIN still comes from the union, so panels still measure identical plot rectangles.
A '100%' host height was read as 100 pixels. parseFloat('100%') is 100, so a full-height grid laid itself out for a 100px box and every panel hit the height floor. A percentage now resolves against the container's parent, the same contract Core.setSVGDimensions gives a standalone chart. Measured in a 500px box: panel height 80 to 454.
A height-only resize did not refit the panels. The observer returned early whenever the rounded width was unchanged, so with a percentage height the panels kept their first size for good. It tracks the height too.
The layout ignored its own chrome. The title, the toolbar band and the shared legend live in the wrapper beside the grid, and none of them were subtracted, so a grid overflowed its host by exactly their height. compute takes a measured chromeHeight and one refit pass applies it once the chrome exists. The trellis samples now sit inside the height they declare.
The minimum panel height still overflows a host too short for it, which is deliberate: below it a panel is unreadable and Dimensions goes degenerate. It no longer does so in silence. compute reports the overflow, the orchestrator says once how much room is missing, and the new trellis.minPanelHeight is the way out.
An update before the first render settled left a stray chart. Both host update seams tested _mounted alone, so anything arriving while _rendering was still true missed the trellis branch and fell through to the single-chart pipeline, drawing a plain chart beside the half-built grid. whenSettled() lets them wait for the mount instead.
Every updateOptions rebuilt the grid, hover state and focus with it. A change touching only how a panel PAINTS now goes to the live panels instead. It is applied by re-assembling each panel's options rather than forwarding the caller's patch, so no composed override is lost: scoped annotations, the shared colour map and the compact-tooltip rule all still hold. Anything that can move the split, a shared domain, the layout or the chrome still rebuilds, as does a promoted grid, whose heights are not the layout's.
panelMounted never reached chart.events. fireEvent only walks the addEventListener registry; every other chart event has a second call site for the config callback, and these four had none. panelMounted, trellisMounted, panelPromoted and panelRestored now fire through both, with the registry's argument order unchanged.
Header and legend colours ignored theme.mode. --apx-fore is a design token a page supplies, not something the theme sets, so the chrome always fell back to its hard-coded grey and stayed unreadable on a dark background. The wrapper publishes the already-resolved chart.foreColor, which accounts for the theme, an explicit foreColor and the token alike, and the chrome reads it underneath --apx-fore so a page-level token still wins.
The eight trellis snapshots are re-baselined: the grids are shorter by the height of their own chrome, which is the fix.
BREAKING CHANGE: trellis.promote now defaults to false. A header click no longer expands its panel over the grid unless you set trellis.promote: true. chart.promotePanel(key) and chart.restorePanels() are unchanged and work whatever the setting.
A trellis embedded in a page is usually read, not driven, and taking over the whole grid on a stray header click is a large surprise for an interaction nobody asked for. Reported from the field as a surprising default, and it is one: nothing on a header says it is a button until the cursor changes over it.
The panel-promotion demo opts in explicitly, which is also the clearer demonstration: the option that buys the affordance is now visible in the sample's own config rather than inherited.
Hovering the y-axis labels, or the padding past the last column, left the tooltip on screen: an empty 2px box pinned to the chart's top-left if nothing had been hovered yet, or the previous column's card stranded in the margin if something had. Either way the card carried no data-positioned, because it had been switched on without ever being placed.
handleStickyTooltip gets this right. It calls handleMouseOut as soon as the pointer leaves the grid, and handleMouseOut clears the class and the positioning marker. What it could not do is stop the end of axisChartsTooltips adding apexcharts-active back, three lines later and unconditionally. An activated tooltip that was never positioned keeps whatever geometry it last had, which is exactly the two shapes above. The vertical guard earlier in the same method returns instead of falling through, which is why hovering ABOVE the plot was always correct and why this hid from a code read.
handleMouseOut now records the decision and axisChartsTooltips returns on it. One flag covers every bail-out in that method, including the null-datum path in handleStickyCapturedSeries, which had the same hole.
Needs tooltip: { shared: false, intersect: false } together to see: it is intersect: false that gives a plain bar chart one listener over the whole SVG instead of one per bar, and so makes the margin hoverable at all.
Fixing that exposed a second, smaller thing. The bound read hoverX < 0, and a plot box rarely lands on whole pixels: a 1194.29px grid reports -0.36 for the leftmost column of pixels INSIDE it. That is where a line chart's first marker sits, so the tooltip hid on the point being pointed at. It had never shown because the unconditional reactivation put the card straight back. A whole pixel past the edge is now out, a fraction of one is still on it.
Reported from the field with a repro page, which is also where the measurements come from: 57 active-but-unpositioned points over a swept plot, now 0.
Toggling a theme at runtime turned the axes dark and left the tooltip white. The chart went dark everywhere a reader looks, except the one panel that appears under their cursor.
The tooltip module is built once per chart, in initModules, and outlives every update. updateOptions merges into a NEW w.config.tooltip object. The module had captured the old one at construction, so every this.tConfig.* read in it and in its sub-modules, which reach it back through ttCtx.tConfig, resolved against the options the chart was born with. Whether a given option worked came down to whether its read happened to go through this.tConfig or through w.config.tooltip: theme is read both ways, in Tooltip and in AxesTooltip, which is precisely why half a dark toggle landed.
tConfig is now a getter over the live config, so there is one way to read it and nothing to keep in sync. The three flags derived from it at construction (showOnIntersect, showTooltipTitle, fixedTooltip) are re-derived in drawTooltip, beside the other per-render config reads already there, which also gives the forced-intersect rule below them a current starting point.
Measured on a bar chart, each option set at construction then updated, against a chart born with the target value. Silently dropped before, applied now: theme, shared, intersect, x.show. Already working, unchanged: enabled, fillSeriesColor.
Found while reproducing a field report about the tooltip in the plot margin, not part of it.
A stackType: '100%' trellis drew a 0..100 axis on the first render and dropped every panel to the raw value range on the first update: 0..60 for data whose stacks all total 100%, so the bars no longer filled their panels and no two panels meant the same thing.
The shared scale used to derive a domain from the data and leave percentages to the core, on the grounds that the core renormalizes the axis anyway. It does, but only when it reads a config that carries chart.stackType, which is true of the options a chart is constructed with and never of a panel update, since an update carries only what changed. So the trellis domain lost on render one and won from then on.
'100%' draws percentages, so the domain is 0..100 whatever the numbers are. Deriving it was the mistake; it is now simply fixed, for the shared scale and for the per-row and per-column ones. First render and every update agree, and the trellis no longer depends on the core re-asserting anything.
Fresh-render labels change from 0 / 33 / 67 / 100 to 0 / 50 / 100. The old ones were the core's own default leaking through on render one alone; the new ones are what niceBounds picks for 0..100, which is the tick style every other trellis axis already uses, and rounder percentages besides.
Reported from the field, with a repro page for the update case.
It now reads annual revenue, operating budget, funding, or equivalent financial resources, whichever is highest, across the reader's parent company, affiliates and any entity under common control. A funded pre-revenue startup and a $3M-budget non-profit are both over it; under the old revenue-only wording neither was.
Generated from licences/LICENSE.template.md in the website repo. Do not edit a LICENSE by hand: run scripts/generate-licences.mjs.
Two fixes, both about a message the library gives you when you are trying to find out what is going on.
Two fixes, both about a message the library gives you when you are trying to find out what is going on.
api.drawn() answers who put a mark on this chart, and it credited a note the VIEWER drew to the page that embedded the chart. That is the one case the owner field was added for.
And the error you get for an unregistered chart type ended by advising the full bundle, which is not an escape hatch for a type that is opt-in: following it fetches a megabyte and raises the same error again.
| gzip | |
|---|---|
| 7.6.0 default bundle | 271,534 B |
| 7.6.1 default bundle | 271,606 B |
Both are dist/apexcharts.min.js gzipped at the default level, which is the figure npm run build prints.
Upgrading is npm install apexcharts@7.6.1.
Ink strokes ARE annotations, so they reach api.drawn() through the annotation contributor, which defaults every row to core: the caller put it in their config. That reported a note somebody drew on the chart as the caller's own, so a readout answering "which of this is mine" answered it wrongly, which is worse than declining to answer.
The two places this layer CREATES an annotation now set owner, and only those two. Authored by, not draggable by: _attach() walks the caller's annotations to make them draggable and stamps an apexcharts-ink-* id on any that lack one, so the id is not evidence of authorship and attributing by prefix match would take authorship away from the person who wrote it. The second test pins that by handing a caller annotation an ink-shaped id literally and asserting it still reports core.
Plan 26 P5: the phasing table already recorded this as the visible consequence of the phase being open.
The full entry does not carry ink, so the spec imports the feature; the tests would otherwise have asserted against a chart with no ink at all.
getChartClass's error ends with "or load the full apexcharts.js instead", which had been true of every chart type until icicle. An opt-in type ships only as its own sub-entry and the default bundle carries no class for it, so a reader who follows that advice fetches a megabyte and meets this same error again. It is the worst shape of wrong advice: the rest of the message is good enough to be trusted first.
RESERVED_TYPES is already exactly the set in question, reserved BECAUSE the default bundle does not carry the class, so the branch costs an import and no new list to maintain.
Also drops "after apexcharts.core.js" from the script-tag line. A sub-entry registers onto whichever shared class is present, so it works after the full bundle too, which is what this repo's own samples do. Naming core made the thing our samples do look unsupported.
Four tests in tests/unit/chart-factory-errors.spec.js; two of them fail on main. The file deliberately does not import the icicle entry, since that would register the type and make the branch unreachable. Full unit suite green: 156 files, 3599 passed.
Two things to opt into. icicle is a cartesian partition chart: one band per level of a hierarchy, each child sized inside its parent, and a click zoom
Two things to opt into. icicle is a cartesian partition chart: one band per level of a hierarchy, each child sized inside its parent, and a click zooms along the value axis while the levels stay put. It ships in its own bundle, so the default build does not carry it:
import ApexCharts from 'apexcharts/icicle'
tooltip.interactive holds a tooltip open while the pointer crosses into it, which is what makes a link or a button inside a custom tooltip reachable at all. Off by default, and it overrides followCursor, because a tooltip that trails the cursor can never be entered.
The Playwright interaction suite now runs in CI on every push and pull request. It had been green on developer machines and ungated for months, and it caught a stacked-total label regression within a day of being switched on.
| gzip | |
|---|---|
| 7.5.1 default bundle | 269,879 B |
| 7.6.0 default bundle | 271,534 B |
Both are dist/apexcharts.min.js gzipped at the default level, which is the figure npm run build prints.
Upgrading is npm install apexcharts@7.6.0.
chart.type: 'icicle' draws a hierarchy as stacked bands, one per depth level, each child cell nested inside its parent's extent along the value axis. It is the sunburst's layout in cartesian coordinates and shares its recursion, so it takes the data model we already resolve: a native children tree, or an existing drilldown config read as plain data with no dependency on the drilldown runtime.
What it adds over the two partition charts we had: labels that stay horizontal and legible at every depth, depth as a straight axis so same-depth siblings line up across branches, and direction: 'up', the flame-graph orientation, for call stacks and path funnels.
Opt-in, per the new-chart-type policy. The default bundle carries no icicle code, only the settings literals and a few string branches, and the type is reached with import ApexCharts from 'apexcharts/icicle' or by loading dist/icicle.js after the core script. Measured on the minified UMD: +508 B gzipped by default, 9,768 B gzipped for the apexcharts/icicle bundle itself, which contains no other chart class.
Four defaults differ from the sunburst's, each because filling the plot is what a treemap does and an icicle has to read as a hierarchy: leaf: 'stop' ends a shallow branch at its own band, leaving the white space that shows how deep each branch goes; tint: 0 keeps one hue per branch so the eye can follow it down; the palette lands on the shallowest level that branches, because a single-root tree is the normal shape here and colouring by root would paint everything one hue; and the legend is off, since every cell carries its own label.
Also reserves the name. registerSeriesType's built-in check asks whether a class is registered, which for an opt-in type is false on the default bundle, so a custom type could have taken icicle and then been routed to the built-in's renderer. RESERVED_TYPES closes it for this type and the next one.
Clicking a branch re-laid the chart out on both axes: the branch became the top level and every band moved, which is a re-layout rather than a zoom. The reader loses their place, and the ancestors that gave the branch its meaning leave the screen.
plotOptions.icicle.zoomType: 'value', now the default, rescales the value axis only. The branch stretches to fill the width, its subtree stretches with it, and no level moves a pixel. The ancestors stay above it, clamped to the plot, which is what makes them read as context bands. It is one affine map over extents that are already laid out, and it is the partition equivalent of chart.zoom.type: 'x'.
The values are not named 'x' and 'y' because direction decides which screen axis the value axis is; 'value' and 'both' mean the same thing whichever way the tree grows. 'both' keeps the old behaviour for charts that would rather spend the whole plot on the focused branch.
A depth cap follows the focus down in value mode, or zooming into a branch marked as having more below it would stretch it and reveal nothing, which is the one promise that mark makes.
Turning data labels on for a 100% stacked combo threw formatter is not a function and drew nothing. Defaults.stacked100 installed the percentage formatter only when chart.type was bar, and wrote formatter: undefined over the default for every other type, so a combo, which is typed line, had no formatter at all.
The percentages were wrong underneath that, too. The 100% total summed every series including the line, so the bars stacked to well under 100 and each label read a share of a number nothing on the chart showed. Only the series drawn as bars are slices of the total now, and a line or area keeps its raw value.
Closes #2429
Thanks @gioboa.
tooltip.followCursor moves the tooltip with the pointer, so a tooltip that was both interactive and cursor-following could never actually be reached: it retreated from every approach. interactive now takes precedence while it is on, across each of the positioning paths, and the configured followCursor value is left untouched so it applies again the moment interactivity is switched off.
Thanks @lovasoa.
Honoring prefers-reduced-motion was a one-way trip. A viewer who had the OS preference on when a chart mounted got a chart that never animated again, for the life of the page, even after they turned the preference back off.
The policy wrote enabled = false straight into w.config, which is the merged config that every update merges onto. Nothing ever put the original value back, and no merge could: an updateOptions({ series }) carries no opinion about animations, so the false survived it, and the next render read the same false the previous one had left behind. The media query was re-read on every render, faithfully, into a decision that could only ever go one way.
So the disable is now a latch. The values it overwrites are held in globals and restored the moment the query stops matching.
One wrinkle: while the latch is engaged the config reads false because we put it there, so an updateOptions that turns animations back on in that window would be lost when the preference lifted. Anything that is no longer false therefore gets re-stashed as it arrives. The mirror case, an explicit enabled: false sent while the latch is engaged, is indistinguishable from our own and restores the older value instead; closing that needs user intent threaded through every merge site, which is a poor trade for a case reachable only by toggling an OS setting mid-session. It is written down in the JSDoc.
Reported as animations not working at all, with the detail that made it solvable: they worked on mobile and not on desktop. That is the shape of an accessibility preference, not of a chart bug, and it is the only environment-dependent switch in the library.
Fixes #5312
ApexCharts.create() collapses every series carrying hidden: true before parseData() runs, so the baseline that parse snapshots into globals.initialSeries holds data: [] for each of them. Until 7.4.0 the first legend interaction repaired that by accident, because parseData() re-snapshotted unconditionally. It no longer does: the legend's own updates pass overwriteInitialSeries: false so that an internal re-render keeps the baseline it was given (#5283), and the emptied rows are now permanent.
Two readers get the wrong answer:
Series.resetSeries() clones the baseline into config.series, so a seriesTooltip.tooltipUtil.isInitialSeriesSameLen() filters out collapsed rows buthandleStickyCapturedSeries() falls to create(..., false), and everyRestore the caller's own data for exactly the rows the hidden flag emptied. The raw-stash baselines (histogram, dumbbell, streamgraph, waterfall, treemap and the dataReducer window) are left as parseData() wrote them, and a row whose input is itself empty is skipped, so the repair can never turn a good baseline into an empty one.
Sibling of #5118, which is the same hazard one snapshot over: initialConfig lost a collapsed series' data for a different reason and was fixed by copying the series objects at capture time.
6 tests in tests/unit/config-hidden-series-baseline.spec.js; 4 of them fail on main. Full unit suite 151 files / 3495 tests green, eslint clean, and tsc --noEmit reports the same 29 pre-existing errors as main.
Thanks @mrash.
Review feedback on #5313: read the repaired data from the record getSeriesAfterCollapsing already stores in gl.collapsedSeries / gl.ancillaryCollapsedSeries rather than from ser[i].data.
On the first pass the two are the same array, so this changes nothing there. They diverge on the one re-entry that also passes overwriteInitialSeries: true. hidden stays on the config row, so hiddenAtInit fires again, and by then appendData() has concatenated the new points onto the LIVE row -- which the collapse emptied. ser[i].data is therefore just the points that were appended, and the repair was writing those over a baseline that should still hold the series. With B declared hidden and appendData([{ data: [7] }, { data: [8] }]) the baseline for B became [8]; the record still held [4, 5, 6] and now that is what lands. It also composes with #5310, which appends into that same record.
The record's array is sliced rather than aliased in. initialSeries' setter keeps a shallow copy and _initialSeriesPeek hands it straight to the tooltip's same-length check, so sharing the array would let a later in-place edit of the record reach a captured baseline -- the one hazard the snapshot is built to rule out. .slice() is what getSeriesAfterCollapsing and riseCollapsedSeries already do at this boundary.
A non-axis collapse records one slice's VALUE, not a series' rows; the Array.isArray check leaves those rows to parseData, as the previous ser[i].data shape test did.
2 tests added to tests/unit/config-hidden-series-baseline.spec.js: the append case (red before this commit, [8] where [4, 5, 6] is expected) and a guard that the record is not aliased into the baseline. Spec is 8/8. Rebased onto main: unit suite 152 files / 3527 tests green, eslint clean, and tsc --noEmit reports 23 errors, the same 23 as main.
Thanks @mrash.
respectReducedMotion: false only ever half worked. It turned the JS tweens back on, and no JS flag can reach a stylesheet, so the @media (prefers-reduced-motion: reduce) block in apexcharts.css went on flattening every duration inside .apexcharts-canvas to 0.01ms with !important. Everything CSS-driven stayed frozen: the pie slice-offset slide, the tooltip and crosshair fades, the drilldown spinner. The only way out was to out-specify an !important rule from the page.
That block is now scoped :not(.apexcharts-ignore-reduced-motion), and Core.setupElements puts the class on the canvas when the option is false. elWrap is rebuilt on every render, so an updateOptions that flips the flag is picked up with no extra bookkeeping.
The reduced-motion fallback for the drilldown spinner had never actually run. It swaps the spin for an opacity pulse, which is what 2.3.3 asks for while still showing that the drill is working, but the blanket rule flattened the fallback too, to a single 0.01ms pass, leaving a drill with no loading indication at all. It now beats that rule on both specificity and origin.
The pie slide also gained a single styling hook. Every node that travels with a slice carries apexcharts-slice-mover, so a page no longer has to know the list (the arc, its labels, any external label group, the hover band) and write a selector per member, or discover the omission when the labels are left behind mid-slide. The hover outline band is one node re-plotted per slice rather than one node per slice, so it drops the class as it retargets: it has to jump to a newly hovered slice, and clearing the inline transition cannot cancel a page's rule on the hook.
The new tests assert computed style, not the inline value the library writes. That distinction is the whole bug. The library was already setting transition: transform 320ms on the slice and the browser was throwing it away, so a test that read node.style passed throughout. The spinner test mounts the overlay through DrilldownLoading rather than building a div, since the scoped fallback only reaches it because it lands in elWrap.
The sheet was appended to <head>. It is injected when the first chart renders, which is long after the page's own <link>s have parsed, so appending put it last in the cascade: an author rule at equal specificity lost on document order alone, wherever they had put it, and the only way through was to out-specify us by repeating a class.
Prepending makes the library the weakest author styles on the page, which is what a library's defaults should be, and matches what the Shadow DOM branch has always done with the shadow root.
Nothing load-bearing is weakened by the move. direction: ltr !important on the canvas is there to beat a direction inherited from an RTL page, and inheritance loses to any declaration at all. The transition: none in .apexcharts-disable-transitions * is there to beat our own transition declarations, which sit in the same sheet, so their order relative to it has not changed. Both now lose only to a page's deliberate, targeted !important override, which is the point of the move rather than a cost of it.
Also drops the .resize-triggers and .contract-trigger rules and the resizeanim keyframes they drive. Resize detection has been built on ResizeObserver for a long time and nothing creates those elements any more, so the rules were dead. They were also the only un-namespaced selectors in the sheet, which is the last thing that should be sitting at the bottom of the cascade waiting to collide with a page.
A partition chart may leave a branch's value out and let it be the sum of its children. That is the documented shape, and the hierarchy resolver already fills it in, in Hierarchy.fillValues.
It never got that far. Non-axis parsing runs first and required every datum to carry both x and y, so a branch with only children was dropped with a warning about pie data. With every branch dropped the series came back empty, and the renderer returns before it builds the tree, so the chart drew nothing at all: a blank plot and a warning naming the wrong chart type.
The sum is the same number either way, so roll the subtree up here. Sunburst has had this hole since it shipped; only its samples, which all give their top-level nodes a y, hid it.
The helper stays private to the module deliberately. Data.js is a shared module, so a named export here would resolve to undefined inside the split bundles.
A rangeArea chart always started its y axis at 0, so a band living far from zero, say 100k to 115k, was squashed into a thin strip at the top of the plot and chart.zoom.autoScaleYaxis had no visible effect.
setYRange lets line, area, scatter, candlestick, boxPlot, violin and vertical rangeBar charts take their lower bound from the data. rangeArea was missing from that list and fell through to the branch that pins the axis to zero. A streamgraph is unaffected, because its wiggle baseline always straddles zero, so its lower bound was never the sentinel that branch keys on.
Closes #4999
Thanks @mmilanovic4.
Clicking a cell took 800ms, which reads as lag rather than motion. The zoom was pacing itself with animations.speed, the clock for the FIRST render, where a response to a click belongs on dynamicAnimation.speed alongside every other update. Measured on the sample, the focused cell now finishes its stretch in 350ms instead of 800.
The guard sets the two clocks far apart, so a zoom that went back to reading the intro's speed would still be moving when the assertion runs rather than failing on a tolerance.
gridPadForStackedTotalDataLabels measures the stacked-total label during plotCoords(), before plotChartType builds globals.columnSeries. The formatter installed by stacked100() branched on columnSeries alone, so the reserve measured "5700" while the drawn label read "5700%", under-reserving by the "%" and pushing the label past the SVG viewport on a horizontal 100% stack. Fall back to a config-based bar classification until columnSeries exists, as CoreUtils.getPercentSeries already does.
Thanks @lovasoa.
A tree with one root is the ordinary icicle and the only flame-graph shape, and clicking that root did something: it became the focus. The layout came back identical, because a single root owns the whole value axis whether it is reached as the focus or through the roots loop, so the visible result was a breadcrumb rising for a view the reader had never left.
resolveFocus now resolves a lone root to the whole tree, the way it already resolved a leaf to its parent branch. It needs the roots to know a root is alone, so the signature takes them as an optional third argument; without them the old behaviour stands, which is what the sunburst still gets.
The same function also moved its "nothing changed" check to after the leaf demotion. It ran before, so clicking a leaf inside the focused branch reported a change with the focus unchanged and re-ran a layout pass to redraw the same picture.
A refused click has to look refused, so two things follow it. The cursor is now decided per layout instead of once at cell creation, and reads pointer only where a click would move the view: otherwise the root advertises a zoom it will not perform, which is a worse lie than the old no-op. And the breadcrumb drops the lone root's crumb, since the strip already opens with a crumb for the whole tree and relabels it, which put the same view in the trail twice with the second copy dead.
Mirror the deferred seriesHover close on the tooltip's own mouseleave, so leaving the tooltip hides siblings in the same chart.group. Add a second grouped spec covering the tooltip-exit path.
Thanks @lovasoa.
A patch for one bug in the Weave inventory that 7.5.0 introduced, found by the first thing to consume it: api.drawn() kept listing an overlay after th
A patch for one bug in the Weave inventory that 7.5.0 introduced, found by the first thing to consume it: api.drawn() kept listing an overlay after the plugin had cleared it away, because declarations were dropped only on a chart redraw and switching an overlay off does not cause one.
Nothing else changed, and no plugin needs to do anything differently.
| gzip | |
|---|---|
| 7.5.0 default bundle | 269,856 B |
| 7.5.1 default bundle | 269,879 B |
Both are dist/apexcharts.min.js gzipped at the default level, which is the figure npm run build prints.
Upgrading is npm install apexcharts@7.5.1.
api.drawn() reported drawings that were no longer on screen.
Declarations were cleared in one place, at the start of each draw pass, on the reasoning that layers are wiped there and repainted from state, so an inventory could never outlive what it described. That holds only for a plugin whose drawing is driven by chart renders.
An overlay plugin is not. Switching an overlay off is an interaction: it empties its layer and repaints, and no chart render happens, because going through one to remove a drawing would be an expensive way to do nothing. The declaration made for the previous paint then survived, and drawn() went on listing a trend line the viewer had just dismissed.
That is worse than the incompleteness the owner column exists to own up to. A reader can see that a list covers only the features which opted in; they cannot see that a row in it is stale.
Emptying the layer is the plugin saying, in the only way this API gives it, that it is drawing nothing. So that is now what it means:
api.on('draw', () => {
const layer = api.layer()
layer.line({ x1, y1, x2, y2, stroke: '#888' })
api.declare({ id: 'trend', label: 'Trend' })
})
// When the viewer switches the overlay off, with no redraw involved:
layer.clear() // the declaration goes with the drawingScoped to the plugin that cleared. Another plugin's declarations are none of its business, and a clear that emptied the whole map would let one plugin erase everyone's work from a readout.
Found by the first thing to consume drawn() rather than by review: an inventory panel that listed an overlay which had been switched off. An API with no consumer is an API whose lifecycle has not been tested, and this one was published without one.
No API breaking changes. Upgrading is npm install apexcharts@7.5.0 .
A minor release for plugin authors. Weave goes to API v6 with four additions, and the theme running through all of them is the same: the chart knew something a plugin had no way to ask for, so the plugin either guessed, or reached for the caller's config, or did without.
The largest of the four ends a pattern this library had been quietly asking plugins to use. Options indexed by series position have no per-series escape hatch, so a plugin wanting one value for its own series had to write the array covering every series and put the caller's back afterwards. api.claim() replaces that with something the host resolves at the point it reads the option, so nothing is written at all.
Everything here is additive. No plugin needs to declare apiVersion: 6 to keep working, and a plugin that wants the new surface while still running on older hosts can now ask for it by name.
| gzip | |
|---|---|
| 7.4.0 default bundle | 268,612 B |
| 7.5.0 default bundle | 269,856 B |
Both are dist/apexcharts.min.js gzipped at the default level, which is the figure npm run build prints. Earlier notes quoted a number from a shell gzip, which is a few hundred bytes apart and differs between macOS and Linux, so it could not be computed the same way twice.
No API breaking changes. Upgrading is npm install apexcharts@7.5.0.
api.capabilities and api.can()The version integer never answered the question a plugin actually has, which is "does this host have X". A plugin declaring a version newer than the host is skipped outright, so one that supports several hosts declares the lowest version it can run on and then has to discover anything above that. Until now it did so by sniffing the facade for function members, which means every plugin reimplements the same guesswork against a surface it is deliberately not supposed to know the shape of.
if (api.can('claim')) {
// ...
}api.capabilities is the whole list, for logging and support. Names describe the capability rather than the version that introduced it, and are permanent once published: removing one is a breaking change on the same terms as removing a member.
Each name is probed off the facade that was actually built rather than copied from a constant, so a member that is ever made conditional drops out of the list instead of being advertised and then missing. The list is an array with the lookup behind a closure rather than a frozen Set, because Object.freeze does not touch a Set's internal slots: add() and delete() would go on working, and one plugin could quietly edit what every later plugin is told.
scales is deliberately not a capability. It is null for non-axis charts, which is a fact about the chart rather than about the host, and advertising it would tell a plugin on a pie chart that projection is available.
api.claim(): set an option for your own series, without writing the caller's configstroke.dashArray and dataLabels.enabledOnSeries are indexed by series position. There is no per-series form of either, so a plugin that adds a computed series and wants it dashed, or wants the chart to stop printing labels over it, has had to write the array covering every series including the caller's, then restore what it found.
7.4.0 added api.info.stroke so a plugin could at least see what to restore, which made the pattern survivable rather than sound. Four things are wrong with it. The restore is stale the moment the caller calls updateOptions. It leaks if the plugin throws in between. A caller who wrote a single number gets it flattened into an array. And two plugins doing it at once fight, with the winner decided by execution order.
const claim = api.claim('stroke.dashArray', [
{ series: 'Revenue (forecast)', value: 6 },
])
claim.release()A claim says what the plugin wants, and the host answers with it where the option is READ. Nothing is written, so there is nothing to restore, releasing is a deletion, and a caller's own updateOptions composes with the claim rather than reverting it. Claims resolve in order, so two plugins on one series produce a defined winner instead of whichever happened to run last.
Name the series rather than its position where you can. A name is resolved each time the option is read, so the claim follows that series when the caller adds, removes or reorders others.
Claimable options are an allowlist, stroke.dashArray and dataLabels.enabledOnSeries to begin with, each declaring its value type so a wrong one is dropped rather than drawn. An option that is not on the list returns null rather than throwing, so a plugin built against a newer host degrades instead of breaking. Every claim is released on teardown, on destroy, and if the host disables the plugin after repeated errors, so none can outlive the plugin holding it.
api.drawn() and api.declare(): what is on the chartA plugin that wants to list what a chart is showing had no way to learn it. It knows its own overlays and can read the series off api.data, but the caller's annotations, another plugin's overlays and the ink strokes a viewer drew were all invisible to it. A layers panel and an export summary are the same question, and both were unanswerable.
api.drawn() // [{ id, kind, label, owner, visible }, ...]
api.declare({ id: 'trend-1', label: 'Trend' })drawn() reports the series, the caller's annotations, and whatever plugins have declared, each entry naming its owner. The owner is the point rather than decoration: the list is only ever as complete as the features that opted into it, so a reader can say what its inventory covers instead of presenting a partial list as everything.
declare() is how a plugin joins, called from its draw handler. Declarations are cleared with the layers at the start of every draw, so an inventory can never outlive the drawing it describes, and declaring the same id twice replaces it rather than growing a duplicate row.
Read only, deliberately. Removing or hiding another feature's output is a much larger promise than this platform makes: it would mean one plugin reaching into another's state with no way for the owner to refuse.
One limitation worth knowing. Annotations do not carry a caller id. The id on a point annotation is the key of a deferred-execution entry rather than a handle on the drawing, so entries fall back to annotation:<type>:<index> and take their label from label.text where the caller wrote one.
api.info.title, and the modifier keys on a pointer eventTwo small reads.
api.info.title is what the chart calls itself. A plugin that has to name this chart to somebody, a page-level readout listing several of them, otherwise heads each row with the container's id, which is a string written for a stylesheet rather than for a reader. The title is the name the page already chose and already shows. It is an empty string rather than undefined for an untitled chart, so it drops into a template without a guard.
api.info.title // 'Revenue by region', or ''modifiers on the pointer payload reports the keys held during the interaction. Shift-click to add to a selection is the gesture that page-level coordination wants and could not express, because the keys exist only on the DOM event, which the pointer handler had been discarding.
api.pointer((e) => {
if (e.modifiers.shift) { /* add to the selection */ }
})Always the same four booleans, shift, ctrl, alt and meta, never partial and never undefined, because a plugin writes e.modifiers.shift inside a viewer's click and a sometimes-missing key is how that becomes a crash. All four are false where there was no DOM event, which is the honest answer for a keyboard or programmatic selection.
Two changes, both generated from one template shared across the organisation.
Products with no free tier no longer print the Community sections. Their files told a reader under $2M in revenue that they could use the product for free, and said it in five separate places: the dual-license opening, the Community section itself, the non-profit budget branch, the line about work built only from free features, and the acceptance list. The runtime never agreed: those products check the licence over the whole product, so no key means a watermark whatever the customer earns.
Products that do have a free tier now say what is in it. The Community section described who qualifies, on revenue, non-profit status or educational use, and never what you actually get. ApexCharts and the others with a free tier now list which features are free and which need Premium, taken from the same lists the pricing page renders.
The Weave feature module is now 4.48 KB gzipped on top of core, up about 1.2 KB in this release, against a Tier-1 budget of roughly 5 KB. That was measured by building the default bundle with and without it, because the budget test checks Tier-1 membership rather than bytes. Worth measuring again before the next addition rather than assuming the headroom is still there.
No API breaking changes. Upgrading is npm install apexcharts@7.4.0 .
A minor release of two additions and twelve fixes, and every one of those fixes came from an outside contributor. They land on the parts of the library that only show their seams in real use: what resetSeries() restores from, CSV export, an x axis whose type was never declared, a hidden y axis, stacked corners, and server-side rendering.
Also here, and worth knowing about even though nothing visible changes: apex-commons moves from ^0.5.0 to ^0.8.0. A caret on a 0.x version pins the MINOR, so every published build since 0.5.0 resolved a commons three minors old, silently. Nothing breaks and nothing warns when a caret cannot reach a newer minor. The bundle carries commons rather than importing it at runtime, so this is the release that actually moves it.
| gzip | |
|---|---|
| 7.3.0 default bundle | 267,241 B |
| 7.4.0 default bundle | 268,389 B |
No API breaking changes. Upgrading is npm install apexcharts@7.4.0.
api.info.strokestroke.dashArray is one option indexed by series position, with no per-series escape hatch. So a plugin that adds a computed series and wants that series dashed, to say its points were derived rather than measured, has to write an array covering every series on the chart including the caller's.
Doing that blind flattens whatever dashing the caller had into solid lines, and leaves nothing to restore them from when the tool is switched off. The option was therefore unusable by a plugin that respects the chart it is attached to, which meant a derived series could only be told apart by its name and its colour.
const dashes = api.info.stroke.dashArray // scalar, or one entry per seriesIt reports what is actually configured, so the array can be rebuilt around the caller's own values and put back exactly as found. A scalar covers every series and an array is per series, both passed through as they are, since a plugin restoring what it was told has to restore the shape too. The array is copied on the way out, so a plugin cannot restyle the chart by mutating what it was handed. This is the same reason dataLabels.enabledOnSeries is already reported: an option a plugin must narrow, and cannot narrow safely without knowing the current value.
Additive, so no plugin has to declare apiVersion: 5 to keep working. Feature-detect it the way reserve and pointer are detected.
Drag-to-zoom is a deliberate gesture, so unlike the wheel and the pinch it is not withheld when the toolbar is hidden. The reset button that undoes it is, which leaves a viewer who drags across a bare chart in a window with nothing on screen that puts the range back, and no key anyone would guess.
chart: {
toolbar: { show: false },
zoom: { resetControl: 'auto' }, // the default
}So while the chart IS zoomed and nothing else can reset it, one reset control is drawn where the toolbar would have been, and it goes when the range does. A chart nobody zooms is untouched: toolbar: { show: false } still means an empty chart for everyone who does not zoom. Escape resets under the same gate, deferring to keyboard navigation and leaving the event alone so a page listening for it still hears it.
'auto' resolves the way allowMouseWheelZoom already does: a toolbar showing its reset tool counts as a way back, anything else does not. false for a page with its own control, which turns off the key too. true also forces the control on where toolbar.tools.reset is off, which is the same dead end as a hidden toolbar.
The gates on the wheel and the pinch are deliberately left where they are. This control arrives after the fact, while what an incidental wheel zoom takes first is the page scroll it swallowed, which no button hands back.
One side effect worth having: a completed drag-zoom now focuses the chart, so the +, - and 0 keys are within reach for the first time after a mouse zoom.
resetSeries() could not bring back a series the viewer had hiddenglobals.initialSeries is what resetSeries() and the toolbar's reset-home button restore from, but parseData() re-captured it from the live series on every parse. Any re-render that ran after the series was mutated baked the mutation into the baseline. A legend collapse replaces series[i].data with [] and re-renders, so by the time the viewer hits reset the baseline is the emptied series: the reset clears the collapsed indices and then restores [] onto [], and the hidden series never comes back.
parseData now owns the capture, gated on overwriteInitialSeries, which is the line the codebase already draws between a caller redefining the input and the library handing back the same data. Three more paths reached the same baseline and are closed with it:
appendData() on the raw-stash types (histogram, waterfall, dumbbell, streamgraph) appends into the stash and re-renders through update(), so the baseline stayed on the pre-append observations. update() now carries the flag through to the parse._updateSeries reassigned initialSeries to config.series right after that parse. On the derived types that is the binned counts, the accumulated pairs or the downsampled window, so a correct baseline was replaced with rows this library derived from it.parseData and empties each collapsed row on its copy, so the capture forgot the data of exactly the series the viewer had switched off. Both snapshots now come off the array the caller handed in, and only when a reconcile actually ran.chart.dataReducer charts also appended into config.series while every later parse re-reduced from the raw stash, and the two disagreeing is what made an appended point disappear on the next re-render. Fixes #5283, thanks @lazerg (#5284).
Object.prototypehandleUnequalXValues grouped rows in a plain unvalidated object keyed by the category. A category named toString threw values is not iterable, while one named __proto__ wrote through to Object.prototype, leaving every object on the page with an 0 property. It is now keyed in a Map.
The same function kept a Set of raw categories beside the map of row values. Two categories printing Date objects for the same instant held two Set entries pointing at one row, and the export repeated that row. The Set goes, since the map's keys are already the categories, while the raw string is kept for the category formatter. Thanks @81reap (#5308, #5309).
Numeric XY parsing sets isXNumeric, but an omitted xaxis.type stays configured as category on a vertical bar chart, and the two halves of the axis then disagree. Range skipped the under-30 numeric tick heuristic while Formatters rounded the generated fractional coordinates as category labels, so on a two-series x = 1..12 chart integer-looking labels landed between the bar groups they named. Inferred numeric axes now carry their own semantics through both, and inferred category labels are preserved rather than regenerated. Fixes #5086, thanks @lovasoa (#5304).
labels.align threwdrawYaxis() always creates the .apexcharts-yaxis group, keeping the rel index aligned with w.config.yaxis, but returns before adding the texts group when the axis is hidden. setYAxisTextAlignments() read the bounding box of that missing group, so the render threw on null.
It also iterated the DOM collection, which is shorter than config.yaxis whenever an axis is hidden or collapsed, so every visible axis after the hidden one was skipped. It now iterates the config and returns early where there is no texts group, mirroring the guard yAxisTitleRotate() already had. Thanks @tarzan77cz (#5305).
borderRadiusWhenStacked did nothing, and a one-point stack rounded its baselineplotOptions.bar.borderRadiusWhenStacked is a working option again. 'all' is the default and keeps the current look, both stack ends rounded, so existing charts do not change. 'last' rounds only the far end and leaves the baseline square, which is the shape asked for in the report. The option of the same name had not been read for some time and was removed as dead config.
Separately, a stack of a single data point no longer hands its baseline segment the 'top' treatment, which on a horizontal 100% stack with one category rounded the first segment on its inner edge. Closes #4845, thanks @gioboa (#5306).
The SSR DOM shim has no getBBox(), so bbox() fell back to a zero rect and every getTextRects() call in SSR reported a 0x0 label. Horizontal bar data labels add half the label height to their offset, so they sat above the bar centre instead of on it, and the axis label sizes that set the plot area collapsed the same way.
SSRElement now estimates the box for text and tspan nodes from the font size and the content, using average sans-serif metrics. Other elements keep returning an empty rect. Fixes #5299, thanks @lazerg (#5300).
rc-7.4 rather than next. A prerelease published under next is correct for about a day: the moment the stable ships, latest moves and next is left pointing at something older, which happened twice in the 7.2 and 7.3 cycles. npm i apexcharts@next now fails with "No matching version" rather than quietly installing the wrong thing, and release notes name the exact tag for anyone who wants the candidate.A single-feature release for the plugin layer. Weave reaches contract v4, and api.pointer() tells a plugin which data point the viewer is pointing at
A single-feature release for the plugin layer. Weave reaches contract v4, and api.pointer() tells a plugin which data point the viewer is pointing at or has selected.
Purely additive. No behaviour a chart already had changes, and a chart running no plugins is untouched, which the 446 bytes below reflect. Upgrading is npm install apexcharts@7.3.0.
| gzip | |
|---|---|
| 7.2.0 default bundle | 266,795 B |
| 7.3.0 default bundle | 267,241 B |
api.pointer()A plugin could draw on the chart and reserve room for its own UI, but it had no way to know what the viewer was doing. To react to a hover it had to hit-test the SVG itself, re-deriving from raw pixels an answer the chart had already worked out, and then disagree with the tooltip on the same pixel.
api.pointer(fn) forwards the chart's own dataPointMouseEnter, dataPointMouseLeave and dataPointSelection as one normalised payload, and returns an unsubscribe.
ApexCharts.registerPlugin({
name: 'coordinator',
apiVersion: 2,
setup(api) {
if (typeof api.pointer !== 'function') return
const off = api.pointer((e) => {
// e.type 'enter' | 'leave' | 'select'
// e.seriesIndex which series
// e.dataPointIndex which point
// e.category the resolved display label, e.g. 'Mar'
// e.seriesName undefined on a pie
// e.selected on 'select' only: is the point now in or out
})
},
})Three things worth knowing about the payload:
category is the resolved display label, the same string api.categories carries, not the raw slot list. A plugin coordinating two charts keys on the label, because an index means something different on every chart. Reading globals.labels directly reports 3 where the chart shows Mar.seriesName is undefined on a pie, where the series carries bare numbers, rather than a guess.selected is filled only on a select, from the chart's own selection set, so a second click on a point reads as a deselect rather than another select.Nothing here lets a plugin intercept or cancel. The chart's tooltip, selection state and the caller's own dataPoint* events are unaffected, and a handler that throws is contained rather than allowed to break the interaction it was watching. Wiring is lazy and torn down with the chart, so a chart whose plugins never ask pays nothing.
Feature-detect with typeof api.pointer === 'function' rather than declaring apiVersion: 4, on the same terms as api.reserve in 7.2.0: a host older than the version a plugin declares skips that plugin outright rather than serving it a smaller API, so one build keeps working everywhere.
Weave is in the default bundle; for the lean-core channel it is import 'apexcharts/features/weave'.
Landing just after this release, and worth flagging because it changes an install command: prereleases no longer publish under next. They now take a tag naming their own release line, so 7.4.0-rc.1 publishes under rc-7.4.
A floating next has to be maintained. The moment a stable ships, it points at something older than latest and npm i apexcharts@next quietly installs the wrong thing. Moving it back is a registry write, and npm's OIDC trusted publishing covers npm publish only, so a workflow that fixed it would need a publish-capable token in a repo that deliberately holds none. A tag naming a line never goes stale, because it states a fact rather than a position.
npm i apexcharts@next therefore now fails with "No matching version", which is the honest answer and better than an old one. Each prerelease's notes will name its exact tag.
Full Changelog: v7.2.0...v7.3.0
No API breaking changes. There is one deliberate behaviour change, described in the first fix below: a series the viewer switched off in the legend no…
A minor release built around the plugin layer and one new look for an existing type. Weave, the plugin host, gets two contract revisions: v2 hands a plugin enough context to decide whether it applies, and v3 lets one ask the chart for room to draw its own UI. Heatmap cells can now be hexagons, circles or diamonds, with a honeycomb tilemap that tessellates properly rather than approximating one.
Alongside that, eleven fixes. Six came from outside contributors, and they cluster on the parts of the library that are hardest to get right from the inside: stacked combo charts, brush selection bounds, annotation clamping and grouped tooltips.
| gzip | |
|---|---|
| 7.1.0 default bundle | 264,308 B |
| 7.2.0 default bundle | 266,813 B |
No API breaking changes. There is one deliberate behaviour change, described in the first fix below: a series the viewer switched off in the legend now stays off across a data update. Upgrading is npm install apexcharts@7.2.0.
plotOptions.heatmap.shape takes 'rect' (the default), 'hexagon', 'circle' or 'diamond'.
new ApexCharts(el, {
chart: { type: 'heatmap' },
plotOptions: { heatmap: { shape: 'hexagon' } },
series: [/* ... */],
}).render()'circle' and 'diamond' are inscribed in the cell box the heatmap already lays out. 'hexagon' is a real honeycomb: pointy-top hexagons stretched to the cell width and 4/3 of the row pitch tall, with alternate rows offset a quarter cell so every neighbouring pair shares a full edge. The lattice overhangs the grid, so that overhang is reserved as a grid-padding floor and the heatmap group gets its own widened clip; offset rows never slide under the y-axis labels and nothing is sliced at the edges.
Shaped cells render as SVG paths that keep the apexcharts-heatmap-rect class and the same attribute contract, so tooltips, keyboard navigation, legend range highlighting and the color tween on a data update all work unchanged. Corner radius applies to 'rect' only, hexagon falls back to 'rect' on a numeric or datetime x axis, and the canvas renderer declines non-rect shapes the same way it declines image fills.
Two fixes and four additions to the plugin host (import 'apexcharts/features/weave', included in the default bundle).
Every Weave plugin silently did nothing on a pie, donut or radialBar. Those charts hold one number per entry rather than a row of values, and the data snapshot called .map on it; the per-plugin guard caught the exception and disabled the plugin, so the failure looked like the plugin's fault. Each slice is now presented as a one-point series. Separately, the version gate demanded an exact match, so the first additive change to the contract would have disabled every plugin already written against v1. It is now forward-compatible: an older plugin is served, and only one needing a newer host than this is skipped.
Added in v2:
data[].raw: the caller's own data array, untouched by parsing. The normalised points are fine to read and wrong to copy, since the three accepted shapes are not interchangeable.info: chart type, axis kind, horizontal bars and the data-label config, so a plugin can decide whether it applies instead of adding a series and letting the core warn at the user.categories: display labels resolved config-first. Reading globals.categoryLabels directly gave real labels on first paint and ordinals after an updateSeries.markDerived(names): a plugin declares which series are its own, and the host keeps them out of the initial-series snapshot. Without it, resetSeries() hands the user a plugin's computed output as if it were their own data.A plugin that renders its own HTML beside the chart (a docked panel, its own toolbar) had no good way to make room for it. The chart sizes itself from the container it was handed, so a sibling inserted into that container does not narrow the chart, it gets drawn at full width underneath it.
api.reserve({ right: 220 }) // null gives the space backReservations are tracked per plugin and summed, so two plugins each asking for a right-hand gutter get one each rather than overlapping. The box is clamped to half the chart per axis, and it is applied after the auto-height calculation, so a side panel narrows the chart without also making it shorter and shifting everything below it.
Feature-detect this rather than declaring apiVersion: 3. A v2 host skips a v3 plugin outright rather than serving it a smaller API, so apiVersion: 2 plus typeof api.reserve === 'function' is the portable check.
az joins the shipped locales. Thanks @jamalkamaladdin (#5289).
updateSeries() and appendSeries() cleared the collapsed-series records, so they disagreed with updateOptions({ series }), which reconciles hidden series by name. On any chart that refreshes its data, every series the viewer had switched off in the legend came back on the next update.
Axis charts now keep those records and reconcile them through the same function the options path uses, before parsing, so a collapsed series does not get its values folded back into the axis range. Non-axis charts still clear, because their collapses are recorded per slice rather than per series and need their own reconcile.
This is the one behaviour change in the release: a series the viewer hid stays hidden across a data update.
A stacked bar found its place in the group by looking its name up in the group's series list, which holds every series in the group, line and area included, while the arrays it indexes into are pushed once per series the stacked renderer actually draws. Any column declared after a non-bar series looked past the end, found no layer beneath it and restarted from the axis, overlapping the bars instead of stacking them. The position now comes from the group's own array length, which counts drawn bars by construction. Fixes #5105, thanks @jamalkamaladdin (#5293).
createBorderRadiusArr resolves the outermost segment of each stack by bucketing every series by global index, line and area series included. In a stacked combo chart a non-bar series could claim either end of the stack, and the corner assigned to it never rendered. Each bucket is now filtered against the list the combo chart already builds of which series it draws as bars. Fixes #5296, thanks @lazerg (#5298).
Under stackType: '100%' the bar renderer narrows the percentage rows to just the series it draws as bars, but the data-label value lookup indexed that bar-only array with the global series index. A line declared ahead of a column ran the index past the end and the render threw on undefined, leaving a partly drawn chart with no axes. The lookup happens while positioning labels, before anything checks whether labels are on, so the chart died at the default of off. Order was the whole defect: declare the line last and it had always rendered.
Refs #2429, which stays open on purpose: turning data labels on in a 100% stacked combo hits a separate, older defect in the same chart, and fixing it needs a decision on what a combo's labels should read.
A point annotation's label is centered on its point with no width limit, so a long label near the left or right edge rendered outside the SVG viewport and got clipped there instead of moving inward. The label is now measured and nudged just enough to keep its full width inside the grid.
The label's background rect needed the same treatment. label.style.background is set by default and the rect is drawn around the text plus its padding, so clamping the text alone still left the drawn label overhanging by 5px at each edge. On a chart whose grid meets the SVG edge (a sparkline, or zero chart padding) that still clipped. Fixes #5106, thanks @jamalkamaladdin (#5292).
Dragging a selection handle past the first or last data point kept widening the rect into empty space. On a brush chart that out-of-range window went straight to the target chart, which then scrolled off the end of its own data: on the brush-charts sample, a right handle dragged 400px past the plot left the rect 246px wider than the grid and pushed the target 75 days beyond the last point.
The rect is now clamped to the pixel span the x-domain occupies before anything reads it, and the rect itself is rewritten rather than only the numbers reported to listeners, so what consumers receive still matches what is on screen. This is the same boundary a body drag has always obeyed, so handle resizes and rect drags now share one. Drag-to-zoom is untouched. Fixes #5123, thanks @jamalkamaladdin (#5291).
Fixes #5282, thanks @gioboa (#5288).
Fanning a hover out to a group only works if the pointer and the grid it is measured against belong to the same chart. Two consumers still read the raw event, which belongs to whichever sibling the cursor is actually over. The canvas heatmap hit test, measured against a sibling's grid, landed a chart-width off and hid that sibling's tooltip outright; followCursor placement anchored the box to the pointer and put it outside the sibling's plot. Both now read the translated pointer.
While in there, the source grid rect and pointer reads moved out of the per-sibling loop. They are identical for every member of the group, so reading them inside it cost one extra forced layout per sibling per mouse move. Trellis panels are implicitly grouped, so a sixteen-panel trellis was paying 32 layouts a move where 17 will do.
The beside-the-cell placement offsets the tooltip by its own width, so a long label on a narrow chart pushed the box off the left edge. It is now clamped horizontally, the same way the heatmap arrow-mode path already was. Thanks @jamalkamaladdin (#5290).
initialConfig lost its series when a legend item was collapsedglobals.initialConfig is meant to be the config the chart started with: resetSeries, the toolbar's reset button and custom tooltips all read it. Two things kept it from being that. The copy shared array references, so initialConfig.series was literally the live config, and a legend collapse empties series[i].data in place. The re-capture on updateOptions(overwriteInitialConfig) then read that emptied series back, so even an unrelated option change baked the collapse into the baseline. The copy is now per-series, and the re-capture only moves when the call actually redefines the series. Fixes #5118, thanks @aron-intframe (#5280).
Two fixes to api.scales, which promises that a plugin's drawings align with the series by construction.
The plugin layer is appended to a group that already carries the grid translate, but the scales baked that same translate in again, so everything a plugin drew landed shifted right and down by exactly the grid offset. The reference plugins in this repo were already written against the layer-local reading, which is how it stayed invisible: jsdom applies no transforms, so only a real browser showed the shift. The domain edges now map to 0 and gridWidth/gridHeight, matching the positions the series themselves use.
Separately, x() was built from minX/maxX, which the parser never computes for a chart laid out in equal bands. Every value projected to the same pixel, so six drawing tools on a category bar chart drew six invisible lines. Category bar and column (stacked included), rangeBar and heatmap now project the way the bar renderer lays them out. Horizontal bars are left alone, since there the x axis carries the value.
apex-commons moved to 0.7.0. A caret on a 0.x version pins the minor, so the core had been building against 0.5.0 while later minors were out. No observable change to the built bundles.dumbbell type shipped in 7.1.0 and built the chart by hand out of zipped [low, high] pairs. The samples/dumbbell demos for the real type are untouched.chart.type ships as a sub-path entry plus a UMD add-on, with an entry in the raw-sample warning map so the default bundle fails loudly rather than rendering blank. The 7.1.0 admissions (streamgraph, waterfall, dumbbell) are grandfathered.null and undefined x values the 7.1.0 date guard also un-breaks. Thanks @aron-intframe (#5281).@jamalkamaladdin, @aron-intframe, @gioboa, @lazerg and @GebleaAlex for the fixes and features in this release.
Full Changelog: v7.1.0...v7.2.0
Nothing published for this version
Nothing published for this version
No breaking changes. Upgrading is npm install apexcharts@7.1.0 .
A minor release built around four new chart types: streamgraph, waterfall, dumbbell and raincloud. Each is a thin layer over a renderer the library already trusts (rangeArea, rangeBar, violin), so they arrive with tooltips, legends, animations, exports and zooming already working, and they cost the default bundle little: streamgraph, waterfall and dumbbell together add about 12 KB gzipped. Raincloud is a Premium add-on and adds nothing unless you load it.
Also new: chart.print lays the chart out for the printed sheet. And six fixes, several of them years old.
| gzip | |
|---|---|
| 7.0.0 default bundle | 252,005 B |
| 7.1.0 default bundle | 264,326 B |
No breaking changes. Upgrading is npm install apexcharts@7.1.0.
chart.type: 'streamgraph' stacks the series as flowing bands around a baseline chosen to keep the whole picture readable: the baseline sits where the bands wiggle least, curves are monotoneCubic by default so a band never overshoots its data, and each band's name is drawn inside it, sized to the room that band actually has. Hovering a band fades the others; the legend and tooltip work as on any axis chart.
new ApexCharts(el, {
chart: { type: 'streamgraph' },
series: [
{ name: 'Drama', data: [{ x: '2024-01-01', y: 32 }, /* ... */] },
{ name: 'Comedy', data: [/* ... */] },
],
}).render()In the default bundle. For the lean-core channel it is import 'apexcharts/streamgraph' or dist/features/streamgraph.js.
chart.type: 'waterfall' accumulates the running total for you: the series holds the deltas, and a point flagged isSubtotal or isTotal draws the running total from zero at that position. Connectors bridge each bar to the next, and rising, falling and total bars take their own colors. Requested in #847.
series: [{
name: 'Operating income',
data: [
{ x: 'Net revenue', y: 8786000 },
{ x: 'Cost of sales', y: -2786000 },
{ x: 'Gross profit', isSubtotal: true },
{ x: 'Operating expenses', y: -1786000 },
{ x: 'Operating income', isTotal: true },
],
}]In the default bundle; apexcharts/waterfall / dist/features/waterfall.js for lean-core.
chart.type: 'dumbbell' compares two or more measures per category: each series is one measure, and the chart joins them with a connector per category, without asking you to zip the values into [low, high] pairs by hand. Works horizontal and as columns.
series: [
{ name: '2020', data: [{ x: 'Backend', y: 92 }, /* ... */] },
{ name: '2025', data: [{ x: 'Backend', y: 118 }, /* ... */] },
]In the default bundle; apexcharts/dumbbell / dist/features/dumbbell.js for lean-core.
chart.type: 'raincloud' shows a distribution three ways at once: the half-violin is the shape, the box is the summary, and the rain underneath is the observations themselves. You hand it the raw values and it does the rest.
series: [{
name: 'Weight gain',
data: [{ x: 'Control', points: [/* raw observations */] }],
}]Raincloud is a Premium feature and an explicit import from either channel:
import 'apexcharts/raincloud'<script src=".../dist/features/raincloud.js"></script>The paper box cannot be measured from JavaScript: clientWidth and matchMedia both report the screen during beforeprint. So chart.print re-lays the chart out to a known width for the sheet instead of letting the browser scale or cut whatever was on screen, and restores it after. On by default with a sensible width; chart: { print: { width: 800 } } to tune it, print: false to keep the old behaviour. Fixes #3352.
The ResizeObserver fires once per resize, and the handler dropped that one callback whenever an animation was still running, so a chart in a collapsing sidebar simply kept its old size. The resize is now deferred and rechecked, and each container resize draws once rather than once per observer callback. Fixes #1584.
autoScaleYaxis scaling to points nobody can seeZooming with autoScaleYaxis let one point just outside the window size the whole axis, and a series with nothing in the window at all still contributed. Stacked charts now also scale to the stacked totals inside the window rather than to individual series. Fixes #1260.
A single series with one tight pair of x values shrank every bar on the chart. A bar's slot now comes from the axis, the union of gaps across all series. Fixes #4885.
The center came from min(width, height), so a tall, narrow container pinned the circle to the top with all the leftover space below it. The circle now centres in the height it actually has. Fixes #4875.
Date x values on a non-datetime axisA Date object as x worked only when the axis was datetime; anywhere else it stringified into garbage, and an invalid Date slipped through unnoticed. Both handled in #5279, thanks @aron-intframe.
A series starting with nulls resolved the hovered index against its drawn points rather than the data, so the tooltip read the wrong point.
main, not only on pull requests. The publish gate is no longer the first time a commit meets the runner.Full Changelog: v7.0.0...v7.1.0
A major release whose headline change is a subtraction: the default bundle stops shipping every feature. Two smaller breaking changes ride along, both…
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.
plotOptions.bar.borderRadiusWhenStacked is removedRounded 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.
dataLabels.animate.enabled now defaults to trueData 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.
tickAmount on a log scaleLog-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.largeDatasetThreshold batches a series' markers into one path. Opt-in and defaulted off, because merged marker paths can never be pixel-identical to individual nodes.apexcharts.esm.js inlined its own copy while add-ons resolved apexcharts/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 a globalThis slot for the same reason.require('apexcharts/line') threw h.use is not a function in every published version that had sub-path entries, because rollup's default CJS interop assumes require() returns the export itself.features/all.js, or ships without both a sub-path export and a UMD artifact.Full Changelog: v6.10.0...v7.0.0
Nothing published for this version
A feature release built around one question: what if the arrangement of a chart's marks were something you could hand it?
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.
apexcharts/unit-shapes, 39 shapes a count can takeA 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:
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.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.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 DOMoutlined() 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.
Date on a datetime axisA 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).
unit and unit-shapes sub-entry artifacts are built and published, so apexcharts/unit and apexcharts/unit-shapes resolve for bundlers and script tags alikeFull Changelog: v6.9.0...v6.10.0
A feature release, and the largest in a while. Two ideas run through most of it.
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.
chart.type: 'histogram', a chart that takes a sampleEvery 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.
rowSeries(), so a mark can name the rows it stands forA 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:
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.
--apx-surface token 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 upseries passed to updateOptions is ignored with a warning instead of poisoning the config and crashing every later updatexaxis.labels.style.fontSize: 'inherit' no longer yields NaN (#5064). Thanks to @waterWang (#5256).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.apexcharts/features/statsFull Changelog: v6.8.0...v6.9.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 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 offsets (#5107)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:
x was computed above the isNaN(x) guard and the guard could never fireNaN x coordinateA 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.gridPad aliased config.grid.padding, so layout insets were written back into the user's own config object, accumulated across renders, and were then read by Core.resizeNonAxisCharts as though the user had asked for them. The resolved padding is now a copy, published as w.layout.gridPad.markers.size > 0, which is false for an array ([0,6] > 0 is NaN > 0), so array-sized markers got no padding and were clipped by 6.5px. It now gates on globals.markers.largestSize, covering both markers.size and markers.discrete.autoScaleYaxis no longer drops a boundary point on a brush selection (#5251)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:
yaxis.min / max, a nice scale, or a shared axischart.type: 'line', the primary consumer of plotOptions.line.colors, had the identical split-segment defect and was excluded by a type gateReversed 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).
columnDelimiter on unequal-x rowsThe 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).
resolveDataLabelOffset lives in modules/helpers/DataLabelOffset.js rather than the shared DataLabels module, so the split per-chart bundles inline it and core.js is untouchedFull Changelog: v6.7.1...v6.8.0
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
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.
unit chartplotOptions.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.
The headline is a new chart type, sunburst : a hierarchical radial chart (a nested pie and donut) that draws tree-structured data as concentric rings,
The headline is a new chart type, sunburst: a hierarchical radial chart (a nested pie and donut) that draws tree-structured data as concentric rings, one ring per level of the hierarchy, with each child arc nested inside its parent's angular span. This release also brings rounded corners and inter-slice spacing to pie and donut slices, a parliament (hemicycle) layout for the unit chart, an optional hover tooltip on point annotations, and a broad reliability and security pass. Two behavior changes are worth reading before you upgrade: a legend click on pie, donut, and polarArea now toggles the slice in and out, and the premium features now require an entitled plan rather than just any valid key. Every existing config renders unchanged.
sunburst chart typechart.type: 'sunburst' renders a hierarchy as a ring of nested arcs: the first level fills a donut around the centre hole, and each deeper level stacks outward, with every child arc constrained to the angle of its parent. It is a non-axis chart (dispatched like pie or treemap) and is tree-shakeable via import 'apexcharts/sunburst', so it adds nothing to the core bundle unless you use it. It is a free chart type, not gated.
Data is the familiar x / y shape with a children array for nesting:
new ApexCharts(el, {
chart: { type: 'sunburst' },
series: [{
data: [
{ x: 'Mobile', y: 55, children: [
{ x: 'iOS', y: 30, children: [
{ x: 'iOS 17', y: 18 },
{ x: 'iOS 16', y: 9 },
]},
{ x: 'Android', y: 23 },
]},
{ x: 'Desktop', y: 33, children: [
{ x: 'Windows', y: 20 },
{ x: 'macOS', y: 10 },
]},
],
}],
plotOptions: {
sunburst: { innerSize: '25%', borderRadius: 5, spacing: 1 },
},
})plotOptions.sunburst.innerSize sets the centre hole (percentage or pixels), borderRadius rounds the arc corners, and spacing opens a gap between neighbouring arcs. Colours, stroke, legend, and title behave as they do on pie and donut.
plotOptions.pie.borderRadius rounds the corners of each slice, and plotOptions.pie.spacing opens a gap between slices, so a pie or donut can read as a set of separated, soft-cornered segments rather than a solid wheel. Both apply to polarArea as well.
unit chartThe unit chart (introduced in 6.6.0) gains a plotOptions.unit.arc layout: a parliament or hemicycle that arranges the marks as seats in concentric arced rows across an annulus, filled in category order. It is the natural shape for seat counts and any part-to-whole where a semicircle reads better than a grid. The gather animation is now tunable too, with configurable easing and enter motion.
Point annotations can now show a hover tooltip, so an annotated marker can carry its own explanatory text without a separate custom element.
data no longer breaks the rest of the chart. A series object without a data property used to abort parsing of every series after it, leaving the parsed data out of step with the series names. It is now treated as an empty series, and the remaining series parse and stay aligned.updateSeries and updateOptions no longer hang on a failed render. Their promises now reject when the render throws, instead of never settling, so await chart.updateSeries(...) can be caught rather than leaking a pending promise.yAxisIndex and a sunburst drilldown cycle are fixed.destroy(). Animation, resize, and pending timeout callbacks are guarded against firing on a destroyed chart, and the detached SVG root is released on teardown, closing a class of update-then-destroy errors and leaks.updateOptions, annotation tooltips that could suppress the series tooltip chart-wide, an empty-candlestick crash, a redundant redraw on a resize that did not change the drawing box, and a per-render reset of the hasNullValues and invalidLogScale flags.plotOptions.sunburst, plotOptions.pie.borderRadius / spacing, and the unit arc layout options are fully typed, and chart.type accepts 'sunburst'.
unit chart type plus storyboard, link, ink, measure, contextMenu, perspectives, and history) clear the watermark only on a premium or enterprise plan. A valid pro key, or the free tier, keeps them in trial mode with the watermark and logs a one-time upgrade notice; it is not treated as an invalid key. Existing pro-plan customers using these features will now see the watermark. Everything else, every free chart type and module, is never gated.Nothing published for this version
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 tw
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.
unit chart type (premium)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.total rounds it to a fixed cell budget, e.g. 100 for a percentage waffle).grid with split: 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:
circle, square, and image (isotype pictogram, with image.tint to recolour a monochrome icon to its category colour).series: [{ name, data: [{ value, x, z, name, fillColor, id }, ...] }] gives each mark its own colour, position, size, and tooltip content.transition): group (default, per-category), flow (the anonymous crowd migrates and recolours across a regroup, the circles-to-bars effect), and identity (a specific mark persists across any regroup or relayout, keyed by id / name).auto dot size, opt-in sizeByValue bubbles, unitValue waffle scaling (1 mark = N units), and a maxUnits safety cap.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 standard fill.opacity so 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.
waffle chart typechart.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 } } },
})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.
unit (and its waffle alias) is the first premium chart type: it renders fully in trial mode with an APEXCHARTS watermark until a key is set. Every other chart type stays free and is never watermarked.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
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.licenseA few things worth knowing:
setLicense(validKey) followed by an update clears an on-screen watermark; no full re-render needed.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.
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 a chart.link.bins: { width } option is no longer silently dropped.updateSeries.series prop right after mount no longer blanks a filter-mode chart; the engine re-asserts its aggregated series.offsetX.chart.license is typed on the chart options.
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 valu
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).
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.
| 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.
dataReducer no longer mutates your data. With zoom-aware downsampling active, the reduced (windowed) view was written back into the original series array, 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.tooltip.custom function 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.
A performance release focused on updates. updateSeries is now genuinely incremental: a data-only update repaints the series and refreshes the axis chr
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).
updateSeries is now incrementalPreviously 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 |
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.Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →