Skip to content

Commit 595b9da

Browse files
committed
fix: prevent crash when chart.events is undefined in ApexCharts v5
ApexCharts v5 accesses chart.events.beforeMount directly without null-checking, causing a TypeError crash in certain configurations (e.g. sections view). - Always include chart.events: {} in the layout config so ApexCharts never receives an undefined events object - Add a post-constructor guard to ensure chart.events is an object before render() is called - Patch ApexCharts v5.3.3 ESM to add null-guards on chart.events accesses (beforeMount, mounted, updated, legendClick) via patch-package
1 parent 6d3f1e9 commit 595b9da

File tree

7 files changed

+98
-1
lines changed

7 files changed

+98
-1
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ However, some things might be broken :grin:
5858
- [`color_threshold` experimental feature](#color_threshold-experimental-feature)
5959
- [`hidden_by_default` experimental feature](#hidden_by_default-experimental-feature)
6060
- [`brush` experimental feature](#brush-experimental-feature)
61+
- [`legend_isolate_on_click` experimental feature](#legend_isolate_on_click-experimental-feature)
6162
- [Known issues](#known-issues)
6263
- [Roadmap](#roadmap)
6364
- [Examples](#examples-1)
@@ -799,6 +800,7 @@ Generates the same result as repeating the configuration in each series:
799800
| `disable_config_validation` | boolean | `false` | v1.6.0 | If `true`, will disable the config validation. Useful if you have cards adding parameters to this one. Use at your own risk. |
800801
| `hidden_by_default` | boolean | `false` | v1.6.0 | Will allow you to use the `hidden_by_default` option. See [hidden_by_default](#hidden_by_default-experimental-feature) |
801802
| `brush` | boolean | `false` | v1.8.0 | Will display a brush which allows you to select a portion of time to display on the main chart. See [brush](#brush-experimental-feature) |
803+
| `legend_isolate_on_click` | boolean | `false` | v2.3.0 | When clicking a legend item, isolate that series (hide all others). Click again to restore all. See [legend_isolate_on_click](#legend_isolate_on_click-experimental-feature) |
802804

803805
### `color_threshold` experimental feature
804806

@@ -917,6 +919,24 @@ series:
917919
in_chart: false
918920
```
919921
922+
### `legend_isolate_on_click` experimental feature
923+
924+
This option changes the legend click behavior so that clicking a series name **isolates** that series (hides all others). Clicking the same series again restores all series. It only works correctly if all series have a unique name.
925+
926+
This is how to use it:
927+
```yaml
928+
type: custom:apexcharts-card
929+
experimental:
930+
legend_isolate_on_click: true
931+
series:
932+
- entity: sensor.living_room_temperature
933+
name: Living Room
934+
- entity: sensor.kitchen_temperature
935+
name: Kitchen
936+
- entity: sensor.bedroom_temperature
937+
name: Bedroom
938+
```
939+
920940
## Known issues
921941

922942
* Sometimes, if `smoothing` is used alongside `area` and there is missing data in the chart, the background will be glitchy. See [apexcharts.js/#2180](https://github.com/apexcharts/apexcharts.js/issues/2180)

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"rollup": "rollup -c",
1010
"lint": "eslint src/*.ts",
1111
"watch": "npm run build:types-check && rollup -c --watch",
12-
"postversion": "npm run build"
12+
"postversion": "npm run build",
13+
"postinstall": "patch-package"
1314
},
1415
"repository": {
1516
"type": "git",
@@ -68,6 +69,7 @@
6869
"conventional-changelog-conventionalcommits": "^9.1.0",
6970
"eslint": "^8.57.0",
7071
"home-assistant-js-websocket": "^9.4.0",
72+
"patch-package": "^8.0.1",
7173
"rollup-plugin-serve": "^1.1.1",
7274
"rollup-plugin-terser": "^7.0.2",
7375
"rollup-plugin-typescript2": "^0.31.2",

patches/apexcharts+5.3.3.patch

Lines changed: 38 additions & 0 deletions
Large diffs are not rendered by default.

src/apex-layouts.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export function getLayoutConfig(
4242
toolbar: {
4343
show: false,
4444
},
45+
events: {
46+
...(config.experimental?.legend_isolate_on_click ? { legendClick: getLegendIsolateClickHandler() } : {}),
47+
},
4548
},
4649
grid: {
4750
strokeDashArray: 3,
@@ -75,6 +78,7 @@ export function getLayoutConfig(
7578
show: true,
7679
formatter: getLegendFormatter(config, hass),
7780
markers: getLegendMarkers(config),
81+
...(config.experimental?.legend_isolate_on_click ? { onItemClick: { toggleDataSeries: false } } : {}),
7882
},
7983
stroke: {
8084
curve: getStrokeCurve(config, false),
@@ -492,6 +496,29 @@ function getFillType(config: ChartCardConfig, brush: boolean) {
492496
}
493497
}
494498

499+
function getLegendIsolateClickHandler() {
500+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
501+
return function (chartContext: any, seriesIndex: number, opts: any) {
502+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
503+
const series: any[] = opts.config?.series ?? [];
504+
const collapsed: number[] = opts.globals?.collapsedSeriesIndices ?? [];
505+
const visibleCount = series.length - collapsed.length;
506+
if (visibleCount === 1 && !collapsed.includes(seriesIndex)) {
507+
// Only this series is visible → restore all series
508+
series.forEach((serie) => chartContext.showSeries(serie.name));
509+
} else {
510+
// Isolate the clicked series: show it and hide all others
511+
series.forEach((serie, i) => {
512+
if (i === seriesIndex) {
513+
chartContext.showSeries(serie.name);
514+
} else {
515+
chartContext.hideSeries(serie.name);
516+
}
517+
});
518+
}
519+
};
520+
}
521+
495522
// eslint-disable-next-line @typescript-eslint/no-explicit-any
496523
function evalApexConfig(apexConfig: any): any {
497524
const eval2 = eval;

src/apexcharts-card.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,14 @@ class ChartsCard extends LitElement {
797797
(layout as any).chart.id = Math.random().toString(36).substring(7);
798798
}
799799
this._apexChart = new ApexCharts(graph, layout);
800+
// Defensive guard: ensure chart.events is always an object before render().
801+
// ApexCharts v5 accesses chart.events.beforeMount without null-checking,
802+
// which throws if chart.events is undefined after config merging.
803+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
804+
const w = (this._apexChart as any).w;
805+
if (w?.config?.chart && !w.config.chart.events) {
806+
w.config.chart.events = {};
807+
}
800808
const promises: Promise<void>[] = [];
801809
promises.push(this._apexChart.render());
802810
if (this._config.series_in_brush.length && brush) {

src/types-config-ti.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const ChartCardExternalConfig = t.iface([], {
1515
"disable_config_validation": t.opt("boolean"),
1616
"hidden_by_default": t.opt("boolean"),
1717
"brush": t.opt("boolean"),
18+
"legend_isolate_on_click": t.opt("boolean"),
1819
})),
1920
"hours_12": t.opt("boolean"),
2021
"chart_type": t.opt("ChartCardChartType"),

src/types-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface ChartCardExternalConfig {
99
disable_config_validation?: boolean;
1010
hidden_by_default?: boolean;
1111
brush?: boolean;
12+
legend_isolate_on_click?: boolean;
1213
};
1314
hours_12?: boolean;
1415
chart_type?: ChartCardChartType;

0 commit comments

Comments
 (0)