apexcharts/apexcharts.js
 Watch   
 Star   
 Fork   
1 days ago
apexcharts.js

💎 Version 8.0.0

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.

🐛 Fixes

  • Keep the chart when an update asks for a chart or series type the page never loaded.
  • Keep the drill state when a level is refused.
  • Point lean-core missing-renderer errors at files that exist.
  • Resolve typed subpaths (apexcharts/core, ssr, client, full) under node10 module resolution.
  • Warn when a boxPlot or violin has raw data and no stats feature.
  • Let a script-tag add-on load before the ApexCharts script.

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: https://github.com/apexcharts/apexcharts.js/compare/v7.9.1...v8.0.0

1 days ago
apexcharts.js

💎 Version 7.9.1

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.

🐛 Fixes

  • Watermark perspectives loaded as a script-tag add-on.
  • Keep missing-feature warnings in the minified bundle.
  • Load the library before its add-ons in displayed code.
  • Say when a chart type is not registered instead of drawing nothing.

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: https://github.com/apexcharts/apexcharts.js/compare/v7.9.0...v7.9.1

1 days ago
apexcharts.js

💎 Version 7.9.0

7.9.0 adds highlightFilter and a Basque locale, and fixes animation and tooltip defects across most chart types. Four behaviours changed, listed below.

Behaviour changes

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.

✨ New

  • 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.
  • Basque (eu) locale, and the Galician toolbar labels completed (#5319). Thanks @jlancharro.

🐛 Fixes

  • Legend toggles, updates and layout changes animate without jumps: hidden series flatten away, and the plot, pies and gauges ease to a new size.
  • Treemap click-to-zoom and the breadcrumb animate as a camera move.
  • Tooltips stay on the hovered mark on sparklines, heatmaps and horizontal bars, and keyboard focus places them where the pointer would.
  • Canvas renderer: tooltips for box plots, candlesticks and markers, and hover that survives updateSeries().
  • An option set to undefined or null no longer crashes the chart, and updateOptions({ xaxis }) keeps category names.
  • Data labels stay visible during updateSeries() (#5332), and the last label stays centred unless it would overflow.
  • Hiding a stacked area series with [x, y] data flattens it onto the series below.
  • An explicit palette passed with theme.mode to updateOptions() is kept (#5041). Thanks @jamalkamaladdin.
  • legend.showForNullSeries works with { x, y } data (#5336). Thanks @jamalkamaladdin.
  • Y-axes mapped by seriesName no longer throw when there are fewer axes than series (#3836). Thanks @jamalkamaladdin.
  • CSV export sorts numeric and date rows by value. Thanks @NotAFlightRisk.
  • 3D funnel shadows morph with their stages.

The default bundle is 298,123 B gzipped, 24.3 KB more than 7.8.0, from the animation and tooltip work.

Upgrading

npm install apexcharts@7.9.0

The detail behind each change is in the commits: https://github.com/apexcharts/apexcharts.js/compare/v7.8.0...v7.9.0

8 days ago
apexcharts.js

💎 Version 7.8.0

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.

✨ New

Drop the chip behind data labels

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.

Keep labels from landing on each other

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:

  • Separation runs along the VALUE axis, not a fixed vertical. On a horizontal bar the vertical axis is the CATEGORY axis, so nudging there walks a label into the next row; those transpose and move along x instead. Vertical-only separation cost 8 of 18 labels on a crowded horizontal bar.
  • Dropping a label is opt-in ({hide: true}). A default-on pass that silently deletes a value is worse than the overlap it set out to fix, so an unseparable pair is left exactly as it renders today.
  • Boxes are measured with getBoundingClientRect, not getBBox. getBBox is taken before the element's own transform, so a label under plotOptions.bar.dataLabels.orientation: 'vertical' measures 29x14 while it occupies 14x29. getBBox remains the SSR fallback, where the DOM shim estimates text extents but has no client rect.
  • A rotated label is an obstacle, never a mover: its y attribute runs along its own rotated axis, so writing to it would slide it sideways.
  • Radial types opt out. Radar lost 3 of 12 labels to a vertical nudge before the skip; pie and friends run their own de-overlap already.
  • Pairwise relaxation, not column packing, which would chain a dense row into one tall stack through its neighbours. Each pair splits the push and each label is capped at maxShift from its own mark.
  • Bounds are the plot with no slack, since past that edge a label is clipped. A label that legitimately starts outside, the one above a bar that reaches the top of the grid, keeps its place: the clamp restricts movement and never forces it.

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.

🐛 Fixes

Serve the ajax demos from our own db.json

Both ajax demos fetched http://my-json-server.typicode.com/apexcharts/ apexcharts.js/yearly, a third-party fake-API. That service is gone: it answers 530 with Cloudflare error 1016, an origin DNS failure, over http and https alike. So the demos have been showing "Loading..." forever on the site, and Test Reproducibility has failed on misc/axios on every push since 2026-10-01, which is why main has been red for seven runs. The e2e runner fails a sample on console errors, and a dead fetch logs one.

my-json-server was only ever serving db.json out of this repository, and that file is still here, so the fix is to read it from a CDN that serves this repository directly. The payload is the whole object rather than one key, so both demos now take .yearly off it.

The data is identical, which the snapshots confirm: both samples pass against the references captured while the old endpoint was alive, with no baseline update. The full suite is 322 passing, 0 failing - the first clean e2e run in a while, since these two were the standing failures.

These demos still pull jquery and axios themselves from cdnjs, so they are not free of the network, but they no longer depend on a service nobody here controls.

9 days ago
apexcharts.js

💎 Version 7.7.0

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.

🐛 Fixes

Centre the treemap tooltip on the tile it describes

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

Stop the browser tooltipping a label you can already read

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

Centre the column crosshair on the bar you can see

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.

Let updateOptions replace a series the transforms derive from

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.

Eight defects a viewer team hit building on the grid

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.

Leave panel headers inert unless promotion is asked for

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.

Dismiss the card when the pointer leaves the plot sideways

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.

Let updateOptions change tooltip options

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.

Hold the 100% stacked domain across updates

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.

The $2M threshold is not a revenue test

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.

13 days ago
apexcharts.js

💎 Version 7.6.1

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.

🐛 Fixes

Report a note the viewer drew as ink, not as the caller's own

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.

Stop telling opt-in types to load the full bundle

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.

16 days ago
apexcharts.js

💎 Version 7.6.0

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.

✨ New

Add the icicle chart type, opt-in

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.

Zoom along the value axis, leaving the levels alone

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.

🐛 Fixes

Data labels on a 100% stacked combo

Closes #2429

Thanks @gioboa.

Ignore followCursor for interactive tooltips

No detail was written on this commit.

Thanks @lovasoa.

Restore animations when reduced-motion is turned off

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

Address comments

No detail was written on this commit.

Thanks @gioboa.

Keep the baseline of a series declared hidden in the config

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 series declared hidden comes back from a reset EMPTY and its data is gone for the life of the chart;
  • Tooltip.tooltipUtil.isInitialSeriesSameLen() filters out collapsed rows but measures the rest, so the moment the viewer un-hides one from the legend its length of 0 is compared against its siblings'. The check fails, handleStickyCapturedSeries() falls to create(..., false), and every shared tooltip on the chart silently drops to the single series nearest the cursor. Re-hiding the series filters it out again and the tooltip comes back, which is what makes it read as a tooltip bug rather than a data one.

Restore 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.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

Thanks @mrash.

Source the hidden-series baseline repair from the collapse record

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.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

Thanks @mrash.

Let respectReducedMotion:false reach the stylesheet

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.

Prepend the injected stylesheet so page rules win

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.

Keep a hierarchy branch that omits its own value

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.

Let rangeArea charts scale the y axis to their data

No detail was written on this commit.

Thanks @mmilanovic4.

Run a zoom on the interaction clock

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.

Classify the stacked-100 label formatter from config

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.

Refuse a click that has nowhere to zoom

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.

Propagate interactive tooltip mouseleave to grouped charts

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.

20 days ago
apexcharts.js

💎 Version 7.5.1

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.

🐛 Fixes

Drop a plugin's declarations when it empties its layer

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 drawing

Scoped 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.

20 days ago
apexcharts.js

💎 Version 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 measured the same way, gzip at its default level over dist/apexcharts.min.js, so they are comparable with each other. Earlier release notes quoted a figure from the CI build, which lands a few hundred bytes apart from the committed bundle.

No API breaking changes. Upgrading is npm install apexcharts@7.5.0.

✨ New

Weave v6: 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 config

stroke.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 chart

A 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 event

Two 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.

🐛 Fixes

The LICENSE branches on which plan the product needs

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.

🧹 Housekeeping

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, which the build reports as 263.53 KB and 259.05 KB gzipped, 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.

25 days ago
apexcharts.js

💎 Version 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.

✨ New

Weave v5: api.info.stroke

stroke.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 series

It 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.

A way back out of a zoom when the toolbar is hidden

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.

🐛 Fixes

resetSeries() could not bring back a series the viewer had hidden

globals.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.
  • The legend-collapse reconcile runs before 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).

CSV export repeated rows, and a category could reach Object.prototype

handleUnequalXValues 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).

An inferred numeric x axis labelled its ticks as categories

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).

A hidden y axis with labels.align threw

drawYaxis() 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 baseline

plotOptions.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).

Server-side rendering measured every label as 0x0

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).

🧹 Housekeeping

  • The LICENSE is the canonical text now, generated from one template shared across the organisation rather than maintained by hand here. It drops a SKU that was written as one rung when it is two, and a long-standing typo.
  • Release candidates now publish under a dist-tag naming their own line, 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.