Plugins (Weave)

Weave answers a question ApexCharts got for years: how do I draw my own thing on the chart without forking it or hacking the SVG? A Weave plugin is the supported answer. It registers once by name, activates per chart, subscribes to lifecycle hooks like draw, and paints into its own sandboxed layer. It can read the chart's scales, data, and theme, but it never touches internal state, so it cannot break rendering and it survives version upgrades.

That last part is the point. Because a plugin only ever draws into its own layer and only reads a fixed API, it is safe to publish to npm and drop into any chart. Weave is what makes a real third-party plugin ecosystem for ApexCharts possible.

See it live

The dashed mean line below is not part of the chart config. A plugin computes it on every draw pass: it reads the series, averages the values, converts the mean to a pixel, paints a line and label into its own layer, and emits the value back to the page. Press New data and the plugin recomputes and redraws.

Series with a plugin-drawn mean lineplugin: mean-line

The dashed mean line is not part of the chart config. A Weave plugin computes it on every draw pass and paints it into its own layer.

Where Weave is useful

  • Reference overlays. A mean, median, or target line, control limits for statistical process control, a moving average, an SLA threshold band. The shipped example is the dashed mean-line plugin above, which recomputes on every redraw.
  • Domain overlays. Support and resistance levels or forecast cones in trading, capacity lines in ops dashboards.
  • Branding. A watermark or logo layer applied consistently across every chart in an app.
  • External event markers. Deploy markers, incidents, or release dates pulled from another system and painted on top of a time series.
  • Org-wide reusable behaviors. Publish one plugin to npm so every team's charts get the same overlay or instrumentation by adding a single name to plugins.
  • Analytics instrumentation. Emit namespaced events (a hover, a threshold crossing) for product analytics without modifying chart internals.

Enable the feature

Weave is in the default bundle, so import ApexCharts from 'apexcharts' already has it and there is nothing to enable. If you are assembling from the lean core, or you want the import to state the dependency explicitly, add the feature entry point:

import ApexCharts from 'apexcharts'
import 'apexcharts/features/weave'   // already included in the default bundle

See the tree-shaking guide for every entry point and what each one costs.

Register a plugin

ApexCharts.registerPlugin(def) registers a plugin globally, once. Each plugin declares a name, an apiVersion, and a setup(api) function that subscribes to hooks. Call api.layer() inside each draw handler (layers are wiped at the start of every draw pass, so a handle cached across draws points at a detached node):

ApexCharts.registerPlugin({
  name: 'watermark',
  apiVersion: 1,
  setup(api) {
    api.on('draw', () => {
      const layer = api.layer()
      layer.text({ x: 12, y: 22, text: 'ACME', color: '#c8ccd4', size: '12px' })
    })
  },
})

ApexCharts.unregisterPlugin(name) removes it again, which is useful in tests and hot-reload.

Activate a plugin per chart

Registering makes a plugin available; a chart opts in through the plugins array. Registration is global, activation is per chart, so the same plugin can be on some charts and not others. You can pass per-chart options and an order:

const options = {
  plugins: [{ name: 'watermark', options: { text: 'ACME' } }],
}

At runtime, chart.updateOptions({ plugins: [{ name, options }] }) reconfigures an active plugin in place; the new options arrive live as api.options.

The plugin API

setup(api) receives a stable facade, never the chart's internals:

api.on(hook, fn)Subscribe to a lifecycle hook: afterParse, afterScales, draw, afterUpdate, or destroy.
api.layer(opts?)A plugin-owned SVG group with line, path, rect, circle, text, and clear. Pass { z: 'front' } or 'behind' to stack it. Wiped at the start of every draw pass.
api.scalesConvert data values to pixels for the current view: scales.x(v), scales.y(v), plus gridWidth and gridHeight.
api.dataRead-only access to the resolved series: name, color, hidden, points, and raw.
api.themeThe resolved theme: mode, foreColor, seriesColor(i), and token(name).
api.storePer-plugin state that survives redraws.
api.emit(name, detail)Send an event back to the page. It fires on the chart's bus as plugin:<name>:<event>.
api.infoWhat kind of chart this is, so a plugin can decide whether it applies. Details below. v2
api.categoriesThe display labels per x position, resolved so they survive every render path. v2
api.markDerived(names)Declare which series on the chart are the plugin's own rather than the caller's. v2
api.reserve(box)Reserve space inside the chart's container for the plugin's own UI. v3
api.pointer(fn)Subscribe to the data point the viewer is pointing at or has selected. Returns an unsubscribe. v4

Two properties of this API are what make plugins safe. First, the layer is sandboxed and cleared before each draw, so a plugin can never corrupt the chart's own output. Second, api.emit is namespaced as plugin:<name>:<event>, so a plugin's events can never trigger the chart's internal lifecycle subscribers.

Versions, and why you should not declare the newest one

The contract is at v5 as of ApexCharts 7.4. Every change so far has been additive:

VersionShipped inAdded
v16.0on, layer, scales, data, theme, store, emit, options, chart
v27.2data[].raw, api.info, api.categories, api.markDerived()
v37.2api.reserve()
v47.3api.pointer()
v57.4api.info.stroke.dashArray

The gate is forward-compatible: a host serves a plugin that declares an older version, and skips only a plugin that needs a newer host than itself. So the version you declare is a minimum requirement, not a statement of what you were built against.

That makes declaring the newest version actively harmful. A plugin declaring apiVersion: 3 is skipped outright by a 7.1 host rather than served a smaller API, and a skipped plugin is silent. Declare the oldest version your plugin genuinely cannot work without, and feature-detect the rest:

ApexCharts.registerPlugin({
  name: 'panel',
  apiVersion: 2,                                  // the minimum this needs
  setup(api) {
    if (typeof api.reserve === 'function') {       // v3, optional
      api.reserve({ right: 220 })
    }
  },
})

Before 7.2 the gate demanded an exact match, which would have disabled every v1 plugin the moment v2 landed. Plugins written for v1 run unchanged on a v5 host.

api.info: what kind of chart is this?

A plugin that adds a computed series or draws an analysis overlay cannot work on every chart type, and the alternative to asking is adding a series and letting the core warn at the user. api.info is a frozen snapshot, read fresh each time:

typeThe type the caller asked for. Survives the aliasing that rewrites e.g. raincloud to violin.
axisChartfalse for pie, donut and radialBar, where each entry is one number rather than a row of values.
datetimeXWhether the x axis is a datetime axis.
horizontalBarsThe core refuses to draw a horizontal bar in a combo, so a plugin must not add a derived series to one.
dataLabels.enabled / .enabledOnSeriesWhether the chart prints a value on each point, and for which series. There is no per-series data-label flag, so enabledOnSeries is the only way to keep labels off a computed series, and narrowing it without knowing the caller's own value would silently discard it.
stroke.dashArray (v5)The caller's own dashing. Scalar means every series, an array means per series.

stroke.dashArray exists for the same reason and against the same trap as enabledOnSeries. The option is indexed by series position with no per-series escape hatch, so a plugin that wants its own derived series dashed has to write the whole array. Reporting the current value is what lets it put back what it found instead of flattening the caller's dashed lines. There is no "unset" to report: the option defaults to 0, and 0 already means no dashing, so restoring it restores exactly what was there.

Adding a derived series

Three pieces of v2 exist for one job: computing a series from the caller's data and putting it on the chart without the chart mistaking it for the caller's own.

Copy the shape from data[].raw, not from points. points is normalised, and its x falls back to the ordinal position on render paths where the parsed x values are not populated. That is fine to read and wrong to write back, because the three accepted shapes ([1, 2], [{x, y}], [[x, y]]) are not interchangeable: hand back the wrong one and it parses to all-null and draws nothing, without an error. raw is the caller's own array, untouched by parsing.

Key on api.categories, not on an index. These are the resolved display labels, config-first. Reading globals.categoryLabels or globals.labels directly gives you real labels on first paint and ordinals after any updateSeries(), because both are populated on mount and emptied on update.

Declare what you added with api.markDerived(names). The core cannot tell a computed series from the caller's own, and several behaviours depend on the difference. The host uses it to keep your series out of the initial-series snapshot, so resetSeries() and the toolbar's reset restore the caller's data rather than your output. It is idempotent; pass an empty array when your series are gone.

Making room for your own UI

A plugin that renders its own HTML beside the chart, a docked panel or a toolbar of its own, cannot make room for it. The chart sizes itself from the element the caller handed it, so a sibling inserted into that element does not narrow the chart: the chart is drawn at full width underneath. Every workaround is worse. Writing chart.width means owning config the caller owns and losing it on their next updateOptions. Positioning over the chart means guessing a size you cannot know and being clipped by any ancestor with overflow: hidden. Narrowing the container means writing to the caller's own element.

api.reserve() has the host do the arithmetic, in the one place that already does it. The container keeps its size and the chart draws inside what is left:

api.reserve({ right: 220 })   // a right-hand gutter for your panel
api.reserve(null)             // give the space back
  • Reservations are per plugin and summed, so two plugins each asking for a right-hand gutter get one each instead of overlapping.
  • The total is clamped to half the container on each axis. A plugin may not reduce the chart it is annotating to nothing. If your UI needs more room than that, render below the chart, which you can do without asking.
  • It is applied after the auto-height calculation, so a side panel narrows the chart without also shortening it and shifting the page below.
  • Calling it with an unchanged box does nothing, so calling it on every render is free. Changing it re-renders one task later, so calling it from inside a draw handler cannot re-enter the render.

Following the pointer

The chart already resolves the series and point under the pointer, for its own tooltip and for the dataPointMouseEnter, dataPointMouseLeave and dataPointSelection events. api.pointer(fn) forwards those three as one normalised payload, so a plugin gets the host's answer instead of hit-testing the SVG itself and then disagreeing with the tooltip on the same pixel:

const off = api.pointer((e) => {
  // e.type            'enter' | 'leave' | 'select'
  // e.seriesIndex     which series
  // e.dataPointIndex  which point
  // e.category        the resolved display label, e.g. 'Mar'
  // e.seriesName      undefined on a pie, where series carry bare numbers
  // e.selected        on 'select' only: is the point now in or out
})

off()  // unsubscribe

category is the same string api.categories carries, because a plugin coordinating two charts keys on the label: an index means something different on each chart, and reading globals.labels directly reports 3 where the chart shows Mar. selected comes from the chart's own selection set, so a second click reads as a deselect rather than another select.

Nothing here lets a plugin intercept or cancel. The chart's tooltip, its selection state and the caller's own dataPoint* events are unaffected, and a handler that throws is contained rather than allowed to break the interaction it was watching. Wiring is lazy and torn down with the chart, so a chart whose plugins never ask pays nothing.

Pie, donut and radialBar

Weave works on non-axis charts. Before 7.2 every plugin silently did nothing on one: those charts hold one number per entry rather than a row of values, the data snapshot called .map on a number, and the per-plugin guard caught the exception and disabled the plugin, so the failure looked like the plugin's fault. Each slice is now presented as a one-point series, which is what it is. Check api.info.axisChart if your plugin needs a real axis.

Weave vs Marks

Both are extensibility features, but at different levels:

  • Weave adds overlays and cross-cutting behaviors that are not one-per-datum: reference lines, bands, watermarks, instrumentation. A Weave plugin decorates the whole chart.
  • Marks adds a new data-driven series type: one shape per datum, tied to your data. A Mark defines how a series is drawn.

For a one-off drawing on a single chart, annotations are simpler than either.