Accessibility module's forced markers make the legend symbol transparent on redraw
Legend swatch turns invisible after any redraw when the accessibility module forces markers on a series
Expected behaviour
A series with marker: { enabled: false } and legendSymbol: 'rectangle' keeps its
legend swatch visible for the life of the chart.
Actual behaviour
The swatch is painted correctly on first render, then becomes fully transparent
(opacity="0" on the symbol element) after the first redraw that re-renders the
legend — a window resize is the usual trigger. The label stays, so the legend
renders as text with no colour key.
Loading the accessibility module is enough to trigger it; no accessibility options need to be set. legend — a window resize is the usual trigger. The label stays, so the legend renders as text with no colour key.
Loading the accessibility module is enough to trigger it; no accessibility options need to be set.
Reproduction
// modules/accessibility.js loaded, no accessibility options set
const chart = Highcharts.chart('container', {
series: [{
type: 'line',
name: 'Series 1',
data: [1, 2, 3, 4, 5],
marker: { enabled: false },
legendSymbol: 'rectangle'
}]
});
const opacity = () =>
chart.series[0].legendItem.symbol.element.getAttribute('opacity');
console.log('first render:', opacity()); // "1" — correct
chart.setSize(500, 300, false); // or just resize the window
console.log('after redraw:', opacity()); // "0" — swatch is goneconsole.log('after redraw:', opacity()); // "0" — swatch is gone
Controls, same page and same Highcharts instance:
| config | swatch opacity, first render → after redraw |
| --- | --- |
| `marker.enabled: false`, accessibility loaded | `1` → **`0`** |
| `marker.enabled: false`, `accessibility: { enabled: false }` | `1` → `1` |
| markers left enabled, accessibility loaded | `1` → `1` |
### Cause
`ForcedMarkers` turns markers back on for any series that disables them, and
hides them by merging a presentation override into the **live series options**
(`Accessibility/Components/SeriesComponent/ForcedMarkers.js`, `seriesOnRender` →
`forceZeroOpacityMarkerOptions`):
```js
merge(true, options, {
marker: { enabled: true, states: { normal: { opacity: 0 } } }
}); Series#pointAttribs (Core/Series/Series.js) resolves state 'normal' and so
returns opacity: 0:
opacity = pointStateOptions.opacity ?? seriesStateOptions.opacity ?? opacity; Legend#colorizeItem (Core/Legend/Legend.js) then applies that whole attribute
hash — opacity included — to the legend swatch:
symbol?.attr(colorizeHidden(item.series ?
item.series.pointAttribs?.(item) :
item.pointAttribs?.() || { fill: item.color }));
```js
symbol?.attr(colorizeHidden(item.series ?
item.series.pointAttribs?.(item) :
item.pointAttribs?.() || { fill: item.color }));The first render escapes it because of paint order: Legend#renderItem draws
the symbol and colorizes it before the series.render that rewrites the option.
renderItem creates the symbol only once (if (!label)), but colorizeItem
runs on every legend render, so every subsequent render picks up the zero.
That paint-order dependence is what makes this look unintended rather than by design — and the zero opacity is meant to keep the forced markers invisible in the plot, so leaking it to the legend defeats the module's own goal of being visually neutral.
Series whose legendSymbol resolves to lineMarker are unaffected in practice:
drawLegendSymbol runs once, before the mutation, so no marker element is ever
created for the legend to fade out.
Suggested fix
Either:
Legend#colorizeItemshould not takeopacityfrompointAttribs()when painting a swatch (the hidden/visible state is already handled separately bycolorizeHidden); orForcedMarkersshould not write a presentation-only override into the sharedseries.optionstree — applying the zero opacity to the marker group or the point graphics would keep it scoped to the plot.
Environment
- Highcharts 13.0.1;
ForcedMarkers.jsandLegend#colorizeItemare byte-identical in 13.0.2 - Accessibility module loaded
- Chrome 141, macOS 15
Workaround
Restore the swatches on chart.events.render:
chart: {
events: {
render(event) {
for (const series of event.target.series) {
series.legendItem?.symbol?.attr({ opacity: 1 });
}
}
}
}Source: highcharts/highcharts