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.