Custom Series (Marks)
ApexCharts ships a fixed catalog of series types: line, bar, area, scatter, and so on. Marks lets you add your own. You write one function, renderItem, that draws a single data point, and register it as a brand-new series type with ApexCharts.registerSeriesType. From then on your type behaves like a built-in: hover events, the shared tooltip, the legend toggle, and keyboard navigation all work with no extra wiring.
The mental model is bring your own mark shape. You describe how to draw one datum; the library handles every piece of chart machinery around it. Before Marks, the only ways to draw a shape ApexCharts did not ship were to fake it with annotations or to fork the renderer. Marks replaces both with a small, reusable function.
See it live
The chart below is a lollipop: a thin stem from the baseline up to each value, capped with a dot. ApexCharts has no lollipop type. This one is a custom series drawn by a renderItem function, and it is fully interactive: hover a stem and the shared tooltip appears, though no tooltip code was written for it. Press New data and the marks re-draw with the series update.
ApexCharts ships no lollipop type. This one is registered from a single renderItem function, yet it is a first-class series: hover a stem and the shared tooltip appears, with no tooltip code written. Loading…
Where Marks is useful
- Domain shapes ApexCharts does not ship. Lollipops for rankings, dumbbells for before/after comparison, bullets for KPI vs target, error bars and whiskers for scientific and engineering data, arrow or vector marks for flows and forecasts.
- KPI and target visuals. A bullet mark showing actual against a target and qualitative bands is a common dashboard need that is awkward to fake with stacked bars.
- Comparison charts. A dumbbell (two dots joined by a line) expresses "2019 vs 2024 per city" far more clearly than a pair of grouped bars.
- Reusable, shippable series. A mark is just a registered function, so you can package it as a small module and reuse it across projects instead of copy-pasting SVG hacks.
- Replacing annotation hacks. Any time you were drawing custom SVG through annotations to imitate a shape, Marks is the clean, interactive replacement.
Enable the feature
Marks is tree-shakeable. Import it once, then register types on the ApexCharts class:
import ApexCharts from 'apexcharts'
import 'apexcharts/features/marks'
ApexCharts.registerSeriesType exists on the class even without the import, but it warns and no-ops until the feature is bundled.
Register a series type
ApexCharts.registerSeriesType(name, def) defines a new type. The renderItem callback receives one context object per datum and draws the mark through a small primitive API:
ApexCharts.registerSeriesType('lollipop', {
renderItem(ctx) {
const { x, y, scales, api, color } = ctx
// scales.y(0) is the zero baseline in pixels
api.line({ x1: x, y1: scales.y(0), x2: x, y2: y, stroke: color, width: 2 })
api.circle({ cx: x, cy: y, r: 6, fill: color })
},
})
Use it like any built-in type
Set the registered name on chart.type (or on series[].type for a mixed chart):
const options = {
series: [{ name: 'Signups', data: [{ x: 'Jan', y: 41 }, { x: 'Feb', y: 58 }] }],
chart: { type: 'lollipop', height: 360 },
}
Every mark is tagged with its datum identity as it is drawn, so hover, the shared tooltip, the legend toggle, and keyboard navigation all work automatically. Built-in type names are guarded against shadowing, so registering 'line' or 'bar' is rejected.
The renderItem API
renderItem(ctx) is called once per datum. The ctx object carries everything you need to place and draw the mark:
ctx.datum | The raw datum from series[].data. |
ctx.x / ctx.y | The datum's resolved x and y, already converted to pixels. |
ctx.color | The series palette colour for this mark. |
ctx.scales | Value-to-pixel helpers: scales.y(value), scales.x(value), plus gridWidth, gridHeight, and band (the pixel width of one category slot). scales.y(0) is the baseline. |
ctx.api | Primitive draw calls: line, circle, rect, path, and text. Coordinates are pixels in series space. |
ctx.seriesIndex / ctx.dataPointIndex | Where this datum sits, for per-point styling. |
Range marks and auto-scaling
A scalar y is the default. When a datum's y is a [low, high] pair (a dumbbell or any range mark), set dataType: 'rangeXY' so both bounds fold into the y-axis scale and the tooltip reads "low - high":
ApexCharts.registerSeriesType('dumbbell', {
dataType: 'rangeXY',
renderItem(ctx) {
const { x, datum, scales, api } = ctx
const [start, end] = datum.y
api.line({ x1: x, y1: scales.y(start), x2: x, y2: scales.y(end), stroke: '#d0d5dd', width: 4, lineCap: 'round' })
api.circle({ cx: x, cy: scales.y(start), r: 7, fill: '#fff', stroke: '#008FFB', strokeWidth: 3 })
api.circle({ cx: x, cy: scales.y(end), r: 7, fill: '#00B894' })
},
})
For a shape whose drawn span is not simply its value (a bullet whose target and bands extend past the bar), return the occupied values from yExtent(datum, index) and they fold into the y-axis scale. Provide a tooltip(datum) function to control the tooltip text.
Shipped examples
Lollipop, dumbbell, and bullet ship as ready-to-copy samples in the Custom Series demos.
Marks vs Weave
Both are extensibility features, but at different levels, and the difference is worth being clear about:
- Marks adds a new data-driven series type: one shape per datum, tied to your data. Use it when your data needs a mark ApexCharts does not have.
- 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; a Mark defines how a series is drawn.
For decorative or one-off drawing that is not tied to each datum, a Weave plugin or an annotation is the better fit.
Marks ships as a tree-shakeable entry point; see the tree-shaking guide.