@classic-homes/charts-react 0.1.37 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -77,10 +77,11 @@ var ChartError = React2.forwardRef(
77
77
  )
78
78
  }
79
79
  ),
80
- /* @__PURE__ */ jsx2("p", { className: "mb-2 text-sm font-medium text-destructive", children: message }),
80
+ /* @__PURE__ */ jsx2("p", { className: "mb-2 text-sm font-medium text-foreground", children: message }),
81
81
  onRetry && /* @__PURE__ */ jsx2(
82
82
  "button",
83
83
  {
84
+ type: "button",
84
85
  onClick: onRetry,
85
86
  className: "rounded-md bg-destructive px-3 py-1.5 text-xs font-medium text-destructive-foreground transition-colors hover:bg-destructive/90",
86
87
  children: "Retry"
@@ -156,6 +157,7 @@ var ChartContainer = React4.forwardRef(
156
157
  ...props
157
158
  }, ref) => {
158
159
  const heightStyle = typeof height === "number" ? `${height}px` : height;
160
+ const descriptionId = `${React4.useId()}-desc`;
159
161
  if (loading) {
160
162
  return /* @__PURE__ */ jsx4(ChartSkeleton, { height, className: wrapperClassName });
161
163
  }
@@ -179,12 +181,12 @@ var ChartContainer = React4.forwardRef(
179
181
  ref,
180
182
  role: "img",
181
183
  "aria-label": title,
182
- "aria-describedby": description ? `${title}-desc` : void 0,
184
+ "aria-describedby": description ? descriptionId : void 0,
183
185
  "data-testid": "chart-container",
184
186
  className: cn("rounded-lg border border-border bg-card", wrapperClassName),
185
187
  ...props,
186
188
  children: [
187
- description && /* @__PURE__ */ jsx4("span", { id: `${title}-desc`, className: "sr-only", children: description }),
189
+ description && /* @__PURE__ */ jsx4("span", { id: descriptionId, className: "sr-only", children: description }),
188
190
  /* @__PURE__ */ jsx4("div", { className: cn("h-full w-full", className), style: { height: heightStyle }, children })
189
191
  ]
190
192
  }
@@ -204,14 +206,19 @@ import {
204
206
  TitleComponent
205
207
  } from "echarts/components";
206
208
  import { CanvasRenderer } from "echarts/renderers";
207
- import { generateLineChartDescription } from "@classic-homes/charts-core";
209
+ import {
210
+ generateLineChartDescription,
211
+ getChartColors,
212
+ THEME_DARK as THEME_DARK2
213
+ } from "@classic-homes/charts-core";
208
214
 
209
215
  // src/hooks/useChart.ts
210
- import { useEffect as useEffect4, useRef as useRef2 } from "react";
216
+ import { useEffect as useEffect4, useRef } from "react";
211
217
  import * as echarts2 from "echarts/core";
218
+ import { getReplaceMerge } from "@classic-homes/charts-core";
212
219
 
213
220
  // src/hooks/useChartTheme.ts
214
- import { useEffect, useState, useCallback } from "react";
221
+ import { useEffect, useState } from "react";
215
222
  import * as echarts from "echarts/core";
216
223
  import { lightTheme, darkTheme, THEME_LIGHT, THEME_DARK } from "@classic-homes/charts-core";
217
224
  var themesRegistered = false;
@@ -221,18 +228,18 @@ function registerClassicThemes() {
221
228
  echarts.registerTheme(THEME_DARK, darkTheme);
222
229
  themesRegistered = true;
223
230
  }
231
+ function resolveTheme(preference) {
232
+ if (preference !== "auto") {
233
+ return preference === "dark" ? THEME_DARK : THEME_LIGHT;
234
+ }
235
+ if (typeof document === "undefined") return THEME_LIGHT;
236
+ return document.documentElement.classList.contains("dark") ? THEME_DARK : THEME_LIGHT;
237
+ }
224
238
  function useChartTheme(preference = "auto") {
225
- const [currentTheme, setCurrentTheme] = useState(THEME_LIGHT);
226
- const updateTheme = useCallback(() => {
227
- if (preference !== "auto") {
228
- setCurrentTheme(preference === "dark" ? THEME_DARK : THEME_LIGHT);
229
- return;
230
- }
231
- const isDark = document.documentElement.classList.contains("dark");
232
- setCurrentTheme(isDark ? THEME_DARK : THEME_LIGHT);
233
- }, [preference]);
239
+ const [currentTheme, setCurrentTheme] = useState(() => resolveTheme(preference));
234
240
  useEffect(() => {
235
241
  registerClassicThemes();
242
+ const updateTheme = () => setCurrentTheme(resolveTheme(preference));
236
243
  updateTheme();
237
244
  if (preference !== "auto") return;
238
245
  const observer = new MutationObserver((mutations) => {
@@ -246,26 +253,29 @@ function useChartTheme(preference = "auto") {
246
253
  attributes: true,
247
254
  attributeFilter: ["class"]
248
255
  });
249
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
250
- const handleMediaChange = () => updateTheme();
251
- mediaQuery.addEventListener("change", handleMediaChange);
252
256
  return () => {
253
257
  observer.disconnect();
254
- mediaQuery.removeEventListener("change", handleMediaChange);
255
258
  };
256
- }, [preference, updateTheme]);
259
+ }, [preference]);
257
260
  return currentTheme;
258
261
  }
259
262
 
260
263
  // src/hooks/useChartResize.ts
261
- import { useEffect as useEffect2, useRef } from "react";
264
+ import { useEffect as useEffect2, useLayoutEffect, useState as useState2 } from "react";
265
+ var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect2 : useLayoutEffect;
266
+ function useRefElement(ref) {
267
+ const [element, setElement] = useState2(null);
268
+ useIsomorphicLayoutEffect(() => {
269
+ setElement(ref.current);
270
+ });
271
+ return element;
272
+ }
262
273
  function useChartResize(chartRef, containerRef) {
263
- const resizeObserverRef = useRef(null);
274
+ const container = useRefElement(containerRef);
264
275
  useEffect2(() => {
265
- const container = containerRef.current;
266
276
  if (!container) return;
267
277
  let resizeTimeout;
268
- resizeObserverRef.current = new ResizeObserver(() => {
278
+ const observer = new ResizeObserver(() => {
269
279
  clearTimeout(resizeTimeout);
270
280
  resizeTimeout = setTimeout(() => {
271
281
  const chart = chartRef.current;
@@ -274,18 +284,18 @@ function useChartResize(chartRef, containerRef) {
274
284
  }
275
285
  }, 100);
276
286
  });
277
- resizeObserverRef.current.observe(container);
287
+ observer.observe(container);
278
288
  return () => {
279
289
  clearTimeout(resizeTimeout);
280
- resizeObserverRef.current?.disconnect();
290
+ observer.disconnect();
281
291
  };
282
- }, [chartRef, containerRef]);
292
+ }, [chartRef, container]);
283
293
  }
284
294
 
285
295
  // src/hooks/useReducedMotion.ts
286
- import { useState as useState2, useEffect as useEffect3 } from "react";
296
+ import { useState as useState3, useEffect as useEffect3 } from "react";
287
297
  function useReducedMotion() {
288
- const [reducedMotion, setReducedMotion] = useState2(false);
298
+ const [reducedMotion, setReducedMotion] = useState3(false);
289
299
  useEffect3(() => {
290
300
  const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
291
301
  setReducedMotion(mediaQuery.matches);
@@ -301,29 +311,39 @@ function useReducedMotion() {
301
311
  }
302
312
 
303
313
  // src/hooks/useChart.ts
304
- var SET_OPTION_OPTS = { notMerge: false, lazyUpdate: true };
314
+ var isComponentType = (mainType) => echarts2.ComponentModel.hasClass(mainType);
315
+ function applyOption(chart, prev, next) {
316
+ chart.setOption(next, {
317
+ replaceMerge: getReplaceMerge(prev, next, isComponentType),
318
+ lazyUpdate: true
319
+ });
320
+ }
305
321
  function useChart({
306
322
  option,
307
323
  theme = "auto",
308
324
  animation,
309
325
  onReady
310
326
  }) {
311
- const containerRef = useRef2(null);
312
- const chartRef = useRef2(null);
327
+ const containerRef = useRef(null);
328
+ const container = useRefElement(containerRef);
329
+ const chartRef = useRef(null);
313
330
  const currentTheme = useChartTheme(theme);
314
331
  const reducedMotion = useReducedMotion();
315
- const onReadyRef = useRef2(onReady);
332
+ const onReadyRef = useRef(onReady);
316
333
  onReadyRef.current = onReady;
317
- const optionRef = useRef2(option);
334
+ const optionRef = useRef(option);
318
335
  optionRef.current = {
319
336
  ...option,
320
- animation: animation ?? !reducedMotion
337
+ // Reduced motion always wins over the animation prop
338
+ animation: (animation ?? true) && !reducedMotion
321
339
  };
340
+ const appliedRef = useRef(null);
322
341
  useEffect4(() => {
323
- if (!containerRef.current) return;
324
- const chart = echarts2.init(containerRef.current, currentTheme);
342
+ if (!container) return;
343
+ const chart = echarts2.init(container, currentTheme);
325
344
  chartRef.current = chart;
326
- chart.setOption(optionRef.current, SET_OPTION_OPTS);
345
+ applyOption(chart, null, optionRef.current);
346
+ appliedRef.current = optionRef.current;
327
347
  onReadyRef.current?.(chart);
328
348
  return () => {
329
349
  chart.dispose();
@@ -331,11 +351,12 @@ function useChart({
331
351
  chartRef.current = null;
332
352
  }
333
353
  };
334
- }, [currentTheme]);
354
+ }, [container, currentTheme]);
335
355
  useEffect4(() => {
336
356
  const chart = chartRef.current;
337
357
  if (!chart || chart.isDisposed()) return;
338
- chart.setOption(optionRef.current, SET_OPTION_OPTS);
358
+ applyOption(chart, appliedRef.current, optionRef.current);
359
+ appliedRef.current = optionRef.current;
339
360
  }, [option, animation, reducedMotion]);
340
361
  useChartResize(chartRef, containerRef);
341
362
  return {
@@ -345,6 +366,14 @@ function useChart({
345
366
  };
346
367
  }
347
368
 
369
+ // src/hooks/useLatestRef.ts
370
+ import { useRef as useRef2 } from "react";
371
+ function useLatestRef(value) {
372
+ const ref = useRef2(value);
373
+ ref.current = value;
374
+ return ref;
375
+ }
376
+
348
377
  // src/components/core/LineChart.tsx
349
378
  import { jsx as jsx5 } from "react/jsx-runtime";
350
379
  echarts3.use([
@@ -379,6 +408,8 @@ var LineChart = React5.forwardRef(
379
408
  ...props
380
409
  }, ref) => {
381
410
  const isEmpty = !data?.categories?.length || !data?.series?.length || data.series.every((s) => !s.data?.length);
411
+ const currentTheme = useChartTheme(theme);
412
+ const chartColors = getChartColors(currentTheme);
382
413
  const option = React5.useMemo(() => {
383
414
  if (isEmpty) return {};
384
415
  return {
@@ -393,7 +424,7 @@ var LineChart = React5.forwardRef(
393
424
  axisPointer: {
394
425
  type: "cross",
395
426
  label: {
396
- backgroundColor: "#6a7985"
427
+ backgroundColor: chartColors.axisPointerLabel
397
428
  }
398
429
  }
399
430
  } : void 0,
@@ -439,41 +470,43 @@ var LineChart = React5.forwardRef(
439
470
  smooth,
440
471
  areaFilled,
441
472
  showDataPoints,
442
- stacked
473
+ stacked,
474
+ chartColors
443
475
  ]);
476
+ const latest = useLatestRef({ onClick, onHover });
444
477
  const { containerRef } = useChart({
445
478
  option,
446
- theme,
479
+ theme: currentTheme === THEME_DARK2 ? "dark" : "light",
447
480
  animation,
448
481
  onReady: (chart) => {
449
- if (onClick) {
450
- chart.on("click", (params) => {
451
- onClick({
452
- type: params.type || "click",
453
- componentType: params.componentType || "series",
454
- seriesIndex: params.seriesIndex,
455
- seriesName: params.seriesName || "",
456
- dataIndex: params.dataIndex || 0,
457
- name: params.name || "",
458
- value: params.value,
459
- color: params.color
460
- });
482
+ chart.on("click", (params) => {
483
+ const { onClick: onClick2 } = latest.current;
484
+ if (!onClick2) return;
485
+ onClick2({
486
+ type: params.type || "click",
487
+ componentType: params.componentType || "series",
488
+ seriesIndex: params.seriesIndex,
489
+ seriesName: params.seriesName || "",
490
+ dataIndex: params.dataIndex || 0,
491
+ name: params.name || "",
492
+ value: params.value,
493
+ color: params.color
461
494
  });
462
- }
463
- if (onHover) {
464
- chart.on("mouseover", (params) => {
465
- onHover({
466
- type: params.type || "mouseover",
467
- componentType: params.componentType || "series",
468
- seriesIndex: params.seriesIndex,
469
- seriesName: params.seriesName || "",
470
- dataIndex: params.dataIndex || 0,
471
- name: params.name || "",
472
- value: params.value,
473
- color: params.color
474
- });
495
+ });
496
+ chart.on("mouseover", (params) => {
497
+ const { onHover: onHover2 } = latest.current;
498
+ if (!onHover2) return;
499
+ onHover2({
500
+ type: params.type || "mouseover",
501
+ componentType: params.componentType || "series",
502
+ seriesIndex: params.seriesIndex,
503
+ seriesName: params.seriesName || "",
504
+ dataIndex: params.dataIndex || 0,
505
+ name: params.name || "",
506
+ value: params.value,
507
+ color: params.color
475
508
  });
476
- }
509
+ });
477
510
  }
478
511
  });
479
512
  const accessibilityDescription = description || (!isEmpty ? generateLineChartDescription(title, data) : void 0);
@@ -591,25 +624,26 @@ var BarChart = React6.forwardRef(
591
624
  }))
592
625
  };
593
626
  }, [data, isEmpty, showTooltip, showLegend, showGrid, isHorizontal, stacked, showValues]);
627
+ const latest = useLatestRef({ onClick });
594
628
  const { containerRef } = useChart({
595
629
  option,
596
630
  theme,
597
631
  animation,
598
632
  onReady: (chart) => {
599
- if (onClick) {
600
- chart.on("click", (params) => {
601
- onClick({
602
- type: params.type || "click",
603
- componentType: params.componentType || "series",
604
- seriesIndex: params.seriesIndex,
605
- seriesName: params.seriesName || "",
606
- dataIndex: params.dataIndex || 0,
607
- name: params.name || "",
608
- value: params.value,
609
- color: params.color
610
- });
633
+ chart.on("click", (params) => {
634
+ const { onClick: onClick2 } = latest.current;
635
+ if (!onClick2) return;
636
+ onClick2({
637
+ type: params.type || "click",
638
+ componentType: params.componentType || "series",
639
+ seriesIndex: params.seriesIndex,
640
+ seriesName: params.seriesName || "",
641
+ dataIndex: params.dataIndex || 0,
642
+ name: params.name || "",
643
+ value: params.value,
644
+ color: params.color
611
645
  });
612
- }
646
+ });
613
647
  }
614
648
  });
615
649
  const accessibilityDescription = description || (!isEmpty ? generateBarChartDescription(title, data) : void 0);
@@ -679,7 +713,7 @@ var PieChart = React7.forwardRef(
679
713
  {
680
714
  name: title,
681
715
  type: "pie",
682
- radius: innerRadius === 0 ? "70%" : ["50%", "70%"],
716
+ radius: innerRadius ? [innerRadius, "70%"] : "70%",
683
717
  center: ["50%", "45%"],
684
718
  data: data.map((item) => ({
685
719
  name: item.name,
@@ -704,23 +738,24 @@ var PieChart = React7.forwardRef(
704
738
  ]
705
739
  };
706
740
  }, [data, isEmpty, showTooltip, showLegend, showLabels, labelPosition, innerRadius, title]);
741
+ const latest = useLatestRef({ onClick });
707
742
  const { containerRef } = useChart({
708
743
  option,
709
744
  theme,
710
745
  animation,
711
746
  onReady: (chart) => {
712
- if (onClick) {
713
- chart.on("click", (params) => {
714
- onClick({
715
- type: params.type || "click",
716
- componentType: params.componentType || "series",
717
- name: params.name || "",
718
- value: params.value,
719
- percent: params.percent,
720
- color: params.color
721
- });
747
+ chart.on("click", (params) => {
748
+ const { onClick: onClick2 } = latest.current;
749
+ if (!onClick2) return;
750
+ onClick2({
751
+ type: params.type || "click",
752
+ componentType: params.componentType || "series",
753
+ name: params.name || "",
754
+ value: params.value,
755
+ percent: params.percent,
756
+ color: params.color
722
757
  });
723
- }
758
+ });
724
759
  }
725
760
  });
726
761
  const accessibilityDescription = description || (!isEmpty ? generatePieChartDescription(title, data, false) : void 0);
@@ -755,6 +790,7 @@ import {
755
790
  TitleComponent as TitleComponent4
756
791
  } from "echarts/components";
757
792
  import { CanvasRenderer as CanvasRenderer4 } from "echarts/renderers";
793
+ import { colorWithAlpha, getChartColors as getChartColors2, THEME_DARK as THEME_DARK3 } from "@classic-homes/charts-core";
758
794
  import { jsx as jsx8 } from "react/jsx-runtime";
759
795
  echarts6.use([
760
796
  EChartsLineChart2,
@@ -786,6 +822,8 @@ var AreaChart = React8.forwardRef(
786
822
  ...props
787
823
  }, ref) => {
788
824
  const isEmpty = !data?.categories?.length || !data?.series?.length || data.series.every((s) => !s.data?.length);
825
+ const currentTheme = useChartTheme(theme);
826
+ const chartColors = getChartColors2(currentTheme);
789
827
  const option = React8.useMemo(() => {
790
828
  if (isEmpty) return {};
791
829
  return {
@@ -800,7 +838,7 @@ var AreaChart = React8.forwardRef(
800
838
  axisPointer: {
801
839
  type: "cross",
802
840
  label: {
803
- backgroundColor: "#6a7985"
841
+ backgroundColor: chartColors.axisPointerLabel
804
842
  }
805
843
  }
806
844
  } : void 0,
@@ -819,52 +857,50 @@ var AreaChart = React8.forwardRef(
819
857
  show: showGrid
820
858
  }
821
859
  },
822
- series: data.series.map((series, index) => ({
823
- name: series.name,
824
- type: "line",
825
- data: series.data,
826
- smooth,
827
- stack: stacked ? "Total" : void 0,
828
- areaStyle: gradient ? {
829
- opacity: 0.8,
830
- color: new echarts6.graphic.LinearGradient(0, 0, 0, 1, [
831
- {
832
- offset: 0,
833
- color: series.color || `rgba(${60 + index * 40}, ${160 - index * 20}, ${180 - index * 30}, 0.8)`
834
- },
835
- {
836
- offset: 1,
837
- color: series.color || `rgba(${60 + index * 40}, ${160 - index * 20}, ${180 - index * 30}, 0.1)`
838
- }
839
- ])
840
- } : { opacity: 0.5 },
841
- itemStyle: series.color ? { color: series.color } : void 0,
842
- emphasis: {
843
- focus: "series"
844
- },
845
- showSymbol: false
846
- }))
860
+ series: data.series.map((series, index) => {
861
+ const base = series.color || chartColors.series[index % chartColors.series.length];
862
+ return {
863
+ name: series.name,
864
+ type: "line",
865
+ data: series.data,
866
+ smooth,
867
+ stack: stacked ? "Total" : void 0,
868
+ areaStyle: gradient ? {
869
+ opacity: 0.8,
870
+ color: new echarts6.graphic.LinearGradient(0, 0, 0, 1, [
871
+ { offset: 0, color: colorWithAlpha(base, 0.8) },
872
+ { offset: 1, color: colorWithAlpha(base, 0.1) }
873
+ ])
874
+ } : { opacity: 0.5 },
875
+ itemStyle: series.color ? { color: series.color } : void 0,
876
+ emphasis: {
877
+ focus: "series"
878
+ },
879
+ showSymbol: false
880
+ };
881
+ })
847
882
  };
848
- }, [data, isEmpty, showTooltip, showLegend, showGrid, smooth, stacked, gradient]);
883
+ }, [data, isEmpty, showTooltip, showLegend, showGrid, smooth, stacked, gradient, chartColors]);
884
+ const latest = useLatestRef({ onClick });
849
885
  const { containerRef } = useChart({
850
886
  option,
851
- theme,
887
+ theme: currentTheme === THEME_DARK3 ? "dark" : "light",
852
888
  animation,
853
889
  onReady: (chart) => {
854
- if (onClick) {
855
- chart.on("click", (params) => {
856
- onClick({
857
- type: params.type || "click",
858
- componentType: params.componentType || "series",
859
- seriesIndex: params.seriesIndex,
860
- seriesName: params.seriesName || "",
861
- dataIndex: params.dataIndex || 0,
862
- name: params.name || "",
863
- value: params.value,
864
- color: params.color
865
- });
890
+ chart.on("click", (params) => {
891
+ const { onClick: onClick2 } = latest.current;
892
+ if (!onClick2) return;
893
+ onClick2({
894
+ type: params.type || "click",
895
+ componentType: params.componentType || "series",
896
+ seriesIndex: params.seriesIndex,
897
+ seriesName: params.seriesName || "",
898
+ dataIndex: params.dataIndex || 0,
899
+ name: params.name || "",
900
+ value: params.value,
901
+ color: params.color
866
902
  });
867
- }
903
+ });
868
904
  }
869
905
  });
870
906
  const accessibilityDescription = description || (!isEmpty ? `${title}. Area chart with ${data.categories.length} data points across ${data.series.length} series.` : void 0);
@@ -891,7 +927,7 @@ AreaChart.displayName = "AreaChart";
891
927
  // src/components/core/ScatterChart.tsx
892
928
  import * as React9 from "react";
893
929
  import * as echarts7 from "echarts/core";
894
- import { ScatterChart as EChartsScatterChart } from "echarts/charts";
930
+ import { ScatterChart as EChartsScatterChart, LineChart as EChartsLineChart3 } from "echarts/charts";
895
931
  import {
896
932
  GridComponent as GridComponent4,
897
933
  TooltipComponent as TooltipComponent5,
@@ -899,10 +935,12 @@ import {
899
935
  TitleComponent as TitleComponent5
900
936
  } from "echarts/components";
901
937
  import { CanvasRenderer as CanvasRenderer5 } from "echarts/renderers";
902
- import { generateScatterChartDescription } from "@classic-homes/charts-core";
938
+ import { escapeHtml, generateScatterChartDescription } from "@classic-homes/charts-core";
903
939
  import { jsx as jsx9 } from "react/jsx-runtime";
904
940
  echarts7.use([
905
941
  EChartsScatterChart,
942
+ // Trend lines are line series
943
+ EChartsLineChart3,
906
944
  GridComponent4,
907
945
  TooltipComponent5,
908
946
  LegendComponent5,
@@ -959,9 +997,12 @@ var ScatterChart = React9.forwardRef(
959
997
  });
960
998
  const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
961
999
  const intercept = (sumY - slope * sumX) / n;
962
- const xValues = s.data.map(([x]) => x);
963
- const minX = Math.min(...xValues);
964
- const maxX = Math.max(...xValues);
1000
+ let minX = Infinity;
1001
+ let maxX = -Infinity;
1002
+ for (const [x] of s.data) {
1003
+ if (x < minX) minX = x;
1004
+ if (x > maxX) maxX = x;
1005
+ }
965
1006
  series.push({
966
1007
  name: `${s.name} Trend`,
967
1008
  type: "line",
@@ -994,7 +1035,7 @@ var ScatterChart = React9.forwardRef(
994
1035
  formatter: (params) => {
995
1036
  const p = params;
996
1037
  if (!p.value) return "";
997
- return `${p.seriesName}<br/>X: ${p.value[0]}<br/>Y: ${p.value[1]}`;
1038
+ return `${escapeHtml(p.seriesName)}<br/>X: ${escapeHtml(p.value[0])}<br/>Y: ${escapeHtml(p.value[1])}`;
998
1039
  }
999
1040
  } : void 0,
1000
1041
  legend: showLegend ? {
@@ -1016,25 +1057,26 @@ var ScatterChart = React9.forwardRef(
1016
1057
  series
1017
1058
  };
1018
1059
  }, [data, isEmpty, showTooltip, showLegend, showGrid, showTrendLine, symbolSize]);
1060
+ const latest = useLatestRef({ onClick });
1019
1061
  const { containerRef } = useChart({
1020
1062
  option,
1021
1063
  theme,
1022
1064
  animation,
1023
1065
  onReady: (chart) => {
1024
- if (onClick) {
1025
- chart.on("click", (params) => {
1026
- onClick({
1027
- type: params.type || "click",
1028
- componentType: params.componentType || "series",
1029
- seriesIndex: params.seriesIndex,
1030
- seriesName: params.seriesName || "",
1031
- dataIndex: params.dataIndex || 0,
1032
- name: params.name || "",
1033
- value: params.value,
1034
- color: params.color
1035
- });
1066
+ chart.on("click", (params) => {
1067
+ const { onClick: onClick2 } = latest.current;
1068
+ if (!onClick2) return;
1069
+ onClick2({
1070
+ type: params.type || "click",
1071
+ componentType: params.componentType || "series",
1072
+ seriesIndex: params.seriesIndex,
1073
+ seriesName: params.seriesName || "",
1074
+ dataIndex: params.dataIndex || 0,
1075
+ name: params.name || "",
1076
+ value: params.value,
1077
+ color: params.color
1036
1078
  });
1037
- }
1079
+ });
1038
1080
  }
1039
1081
  });
1040
1082
  const accessibilityDescription = description || (!isEmpty ? generateScatterChartDescription(title, data) : void 0);
@@ -1064,7 +1106,11 @@ import * as echarts8 from "echarts/core";
1064
1106
  import { PieChart as EChartsPieChart2 } from "echarts/charts";
1065
1107
  import { TooltipComponent as TooltipComponent6, LegendComponent as LegendComponent6, TitleComponent as TitleComponent6 } from "echarts/components";
1066
1108
  import { CanvasRenderer as CanvasRenderer6 } from "echarts/renderers";
1067
- import { generatePieChartDescription as generatePieChartDescription2 } from "@classic-homes/charts-core";
1109
+ import {
1110
+ generatePieChartDescription as generatePieChartDescription2,
1111
+ getChartColors as getChartColors3,
1112
+ THEME_DARK as THEME_DARK4
1113
+ } from "@classic-homes/charts-core";
1068
1114
  import { jsx as jsx10 } from "react/jsx-runtime";
1069
1115
  echarts8.use([EChartsPieChart2, TooltipComponent6, LegendComponent6, TitleComponent6, CanvasRenderer6]);
1070
1116
  var DonutChart = React10.forwardRef(
@@ -1090,6 +1136,8 @@ var DonutChart = React10.forwardRef(
1090
1136
  ...props
1091
1137
  }, ref) => {
1092
1138
  const isEmpty = !data?.length;
1139
+ const currentTheme = useChartTheme(theme);
1140
+ const chartColors = getChartColors3(currentTheme);
1093
1141
  const option = React10.useMemo(() => {
1094
1142
  if (isEmpty) return {};
1095
1143
  const graphicElements = [];
@@ -1106,7 +1154,7 @@ var DonutChart = React10.forwardRef(
1106
1154
  text: String(centerValue),
1107
1155
  fontSize: 28,
1108
1156
  fontWeight: "bold",
1109
- fill: "currentColor",
1157
+ fill: chartColors.foreground,
1110
1158
  textAlign: "center"
1111
1159
  },
1112
1160
  top: 0,
@@ -1119,7 +1167,7 @@ var DonutChart = React10.forwardRef(
1119
1167
  style: {
1120
1168
  text: centerLabel,
1121
1169
  fontSize: 14,
1122
- fill: "#888",
1170
+ fill: chartColors.mutedForeground,
1123
1171
  textAlign: "center"
1124
1172
  },
1125
1173
  top: centerValue !== void 0 ? 35 : 0,
@@ -1182,25 +1230,27 @@ var DonutChart = React10.forwardRef(
1182
1230
  innerRadius,
1183
1231
  centerLabel,
1184
1232
  centerValue,
1185
- title
1233
+ title,
1234
+ chartColors
1186
1235
  ]);
1236
+ const latest = useLatestRef({ onClick });
1187
1237
  const { containerRef } = useChart({
1188
1238
  option,
1189
- theme,
1239
+ theme: currentTheme === THEME_DARK4 ? "dark" : "light",
1190
1240
  animation,
1191
1241
  onReady: (chart) => {
1192
- if (onClick) {
1193
- chart.on("click", (params) => {
1194
- onClick({
1195
- type: params.type || "click",
1196
- componentType: params.componentType || "series",
1197
- name: params.name || "",
1198
- value: params.value,
1199
- percent: params.percent,
1200
- color: params.color
1201
- });
1242
+ chart.on("click", (params) => {
1243
+ const { onClick: onClick2 } = latest.current;
1244
+ if (!onClick2) return;
1245
+ onClick2({
1246
+ type: params.type || "click",
1247
+ componentType: params.componentType || "series",
1248
+ name: params.name || "",
1249
+ value: params.value,
1250
+ percent: params.percent,
1251
+ color: params.color
1202
1252
  });
1203
- }
1253
+ });
1204
1254
  }
1205
1255
  });
1206
1256
  const accessibilityDescription = description || (!isEmpty ? generatePieChartDescription2(title, data, true) : void 0);
@@ -1377,7 +1427,8 @@ var FunnelChart = React12.forwardRef(
1377
1427
  bottom: 60,
1378
1428
  width: "80%",
1379
1429
  min: 0,
1380
- max: Math.max(...data.map((d) => d.value)),
1430
+ // reduce, not Math.max(...): spreading large arrays overflows the stack
1431
+ max: data.reduce((m, d) => d.value > m ? d.value : m, -Infinity),
1381
1432
  minSize: "0%",
1382
1433
  maxSize: "100%",
1383
1434
  sort,
@@ -1407,22 +1458,23 @@ var FunnelChart = React12.forwardRef(
1407
1458
  ]
1408
1459
  };
1409
1460
  }, [data, isEmpty, showTooltip, showLegend, sort, orientation, showLabels, title]);
1461
+ const latest = useLatestRef({ onClick });
1410
1462
  const { containerRef } = useChart({
1411
1463
  option,
1412
1464
  theme,
1413
1465
  animation,
1414
1466
  onReady: (chart) => {
1415
- if (onClick) {
1416
- chart.on("click", (params) => {
1417
- onClick({
1418
- type: params.type || "click",
1419
- componentType: params.componentType || "series",
1420
- name: params.name || "",
1421
- value: params.value,
1422
- color: params.color
1423
- });
1467
+ chart.on("click", (params) => {
1468
+ const { onClick: onClick2 } = latest.current;
1469
+ if (!onClick2) return;
1470
+ onClick2({
1471
+ type: params.type || "click",
1472
+ componentType: params.componentType || "series",
1473
+ name: params.name || "",
1474
+ value: params.value,
1475
+ color: params.color
1424
1476
  });
1425
- }
1477
+ });
1426
1478
  }
1427
1479
  });
1428
1480
  const accessibilityDescription = description || (!isEmpty ? `${title}. Funnel chart with ${data.length} stages.` : void 0);
@@ -1452,6 +1504,7 @@ import * as echarts11 from "echarts/core";
1452
1504
  import { GaugeChart as EChartsGaugeChart } from "echarts/charts";
1453
1505
  import { TitleComponent as TitleComponent9 } from "echarts/components";
1454
1506
  import { CanvasRenderer as CanvasRenderer9 } from "echarts/renderers";
1507
+ import { getChartColors as getChartColors4, THEME_DARK as THEME_DARK5 } from "@classic-homes/charts-core";
1455
1508
  import { jsx as jsx13 } from "react/jsx-runtime";
1456
1509
  echarts11.use([EChartsGaugeChart, TitleComponent9, CanvasRenderer9]);
1457
1510
  var GaugeChart = React13.forwardRef(
@@ -1473,6 +1526,9 @@ var GaugeChart = React13.forwardRef(
1473
1526
  class: className,
1474
1527
  ...props
1475
1528
  }, ref) => {
1529
+ const currentTheme = useChartTheme(theme);
1530
+ const chartColors = getChartColors4(currentTheme);
1531
+ const reducedMotion = useReducedMotion();
1476
1532
  const option = React13.useMemo(() => {
1477
1533
  return {
1478
1534
  series: [
@@ -1497,7 +1553,7 @@ var GaugeChart = React13.forwardRef(
1497
1553
  splitNumber: 5,
1498
1554
  lineStyle: {
1499
1555
  width: 2,
1500
- color: "#999"
1556
+ color: chartColors.mutedForeground
1501
1557
  }
1502
1558
  },
1503
1559
  splitLine: {
@@ -1505,12 +1561,12 @@ var GaugeChart = React13.forwardRef(
1505
1561
  length: 10,
1506
1562
  lineStyle: {
1507
1563
  width: 3,
1508
- color: "#999"
1564
+ color: chartColors.mutedForeground
1509
1565
  }
1510
1566
  },
1511
1567
  axisLabel: {
1512
1568
  distance: -20,
1513
- color: "#999",
1569
+ color: chartColors.mutedForeground,
1514
1570
  fontSize: 12
1515
1571
  },
1516
1572
  anchor: {
@@ -1530,7 +1586,7 @@ var GaugeChart = React13.forwardRef(
1530
1586
  fontSize: 14
1531
1587
  },
1532
1588
  detail: {
1533
- valueAnimation: animation,
1589
+ valueAnimation: animation && !reducedMotion,
1534
1590
  fontSize: 28,
1535
1591
  fontWeight: "bold",
1536
1592
  offsetCenter: [0, "40%"],
@@ -1545,10 +1601,21 @@ var GaugeChart = React13.forwardRef(
1545
1601
  }
1546
1602
  ]
1547
1603
  };
1548
- }, [value, min, max, splitNumber, showProgress, formatValue, title, animation]);
1604
+ }, [
1605
+ value,
1606
+ min,
1607
+ max,
1608
+ splitNumber,
1609
+ showProgress,
1610
+ formatValue,
1611
+ title,
1612
+ animation,
1613
+ reducedMotion,
1614
+ chartColors
1615
+ ]);
1549
1616
  const { containerRef } = useChart({
1550
1617
  option,
1551
- theme,
1618
+ theme: currentTheme === THEME_DARK5 ? "dark" : "light",
1552
1619
  animation
1553
1620
  });
1554
1621
  const accessibilityDescription = description || `${title}. Gauge showing value ${value} on scale from ${min} to ${max}.`;
@@ -1582,6 +1649,7 @@ import {
1582
1649
  VisualMapComponent
1583
1650
  } from "echarts/components";
1584
1651
  import { CanvasRenderer as CanvasRenderer10 } from "echarts/renderers";
1652
+ import { escapeHtml as escapeHtml2 } from "@classic-homes/charts-core";
1585
1653
  import { jsx as jsx14 } from "react/jsx-runtime";
1586
1654
  echarts12.use([
1587
1655
  EChartsHeatmapChart,
@@ -1611,9 +1679,14 @@ var HeatmapChart = React14.forwardRef(
1611
1679
  ...props
1612
1680
  }, ref) => {
1613
1681
  const isEmpty = !data?.data?.length || !xAxis?.length || !yAxis?.length;
1614
- const values = data?.data?.map(([, , value]) => value) || [];
1615
- const minValue = data?.min ?? Math.min(...values);
1616
- const maxValue = data?.max ?? Math.max(...values);
1682
+ let dataMin = Infinity;
1683
+ let dataMax = -Infinity;
1684
+ for (const [, , value] of data?.data ?? []) {
1685
+ if (value < dataMin) dataMin = value;
1686
+ if (value > dataMax) dataMax = value;
1687
+ }
1688
+ const minValue = data?.min ?? dataMin;
1689
+ const maxValue = data?.max ?? dataMax;
1617
1690
  const option = React14.useMemo(() => {
1618
1691
  if (isEmpty) return {};
1619
1692
  return {
@@ -1623,7 +1696,7 @@ var HeatmapChart = React14.forwardRef(
1623
1696
  const p = params;
1624
1697
  if (!p.value) return "";
1625
1698
  const [xIdx, yIdx, value] = p.value;
1626
- return `${xAxis[xIdx]} / ${yAxis[yIdx]}: ${value}`;
1699
+ return `${escapeHtml2(xAxis[xIdx])} / ${escapeHtml2(yAxis[yIdx])}: ${escapeHtml2(value)}`;
1627
1700
  }
1628
1701
  } : void 0,
1629
1702
  grid: {
@@ -1672,25 +1745,26 @@ var HeatmapChart = React14.forwardRef(
1672
1745
  ]
1673
1746
  };
1674
1747
  }, [data, xAxis, yAxis, isEmpty, showTooltip, showValues, minValue, maxValue, title]);
1748
+ const latest = useLatestRef({ onClick, xAxis, yAxis });
1675
1749
  const { containerRef } = useChart({
1676
1750
  option,
1677
1751
  theme,
1678
1752
  animation,
1679
1753
  onReady: (chart) => {
1680
- if (onClick) {
1681
- chart.on("click", (params) => {
1682
- const [xIdx, yIdx, value] = params.value || [0, 0, 0];
1683
- onClick({
1684
- type: params.type || "click",
1685
- componentType: params.componentType || "series",
1686
- xIndex: xIdx,
1687
- yIndex: yIdx,
1688
- xLabel: xAxis[xIdx],
1689
- yLabel: yAxis[yIdx],
1690
- value
1691
- });
1754
+ chart.on("click", (params) => {
1755
+ const { onClick: onClick2, xAxis: xAxis2, yAxis: yAxis2 } = latest.current;
1756
+ if (!onClick2) return;
1757
+ const [xIdx, yIdx, value] = params.value || [0, 0, 0];
1758
+ onClick2({
1759
+ type: params.type || "click",
1760
+ componentType: params.componentType || "series",
1761
+ xIndex: xIdx,
1762
+ yIndex: yIdx,
1763
+ xLabel: xAxis2[xIdx],
1764
+ yLabel: yAxis2[yIdx],
1765
+ value
1692
1766
  });
1693
- }
1767
+ });
1694
1768
  }
1695
1769
  });
1696
1770
  const accessibilityDescription = description || (!isEmpty ? `${title}. Heatmap with ${xAxis.length} columns and ${yAxis.length} rows.` : void 0);
@@ -1720,6 +1794,7 @@ import * as echarts13 from "echarts/core";
1720
1794
  import { TreemapChart as EChartsTreemapChart } from "echarts/charts";
1721
1795
  import { TooltipComponent as TooltipComponent10, TitleComponent as TitleComponent11 } from "echarts/components";
1722
1796
  import { CanvasRenderer as CanvasRenderer11 } from "echarts/renderers";
1797
+ import { escapeHtml as escapeHtml3, getChartColors as getChartColors5, THEME_DARK as THEME_DARK6 } from "@classic-homes/charts-core";
1723
1798
  import { jsx as jsx15 } from "react/jsx-runtime";
1724
1799
  echarts13.use([EChartsTreemapChart, TooltipComponent10, TitleComponent11, CanvasRenderer11]);
1725
1800
  var TreemapChart = React15.forwardRef(
@@ -1740,13 +1815,15 @@ var TreemapChart = React15.forwardRef(
1740
1815
  ...props
1741
1816
  }, ref) => {
1742
1817
  const isEmpty = !data?.length;
1818
+ const currentTheme = useChartTheme(theme);
1819
+ const chartColors = getChartColors5(currentTheme);
1743
1820
  const option = React15.useMemo(() => {
1744
1821
  if (isEmpty) return {};
1745
1822
  return {
1746
1823
  tooltip: showTooltip ? {
1747
1824
  formatter: (params) => {
1748
1825
  const p = params;
1749
- return `${p.name}: ${p.value}`;
1826
+ return `${escapeHtml3(p.name)}: ${escapeHtml3(p.value)}`;
1750
1827
  }
1751
1828
  } : void 0,
1752
1829
  series: [
@@ -1757,7 +1834,7 @@ var TreemapChart = React15.forwardRef(
1757
1834
  levels: levels || [
1758
1835
  {
1759
1836
  itemStyle: {
1760
- borderColor: "#555",
1837
+ borderColor: chartColors.divider,
1761
1838
  borderWidth: 4,
1762
1839
  gapWidth: 4
1763
1840
  }
@@ -1795,25 +1872,26 @@ var TreemapChart = React15.forwardRef(
1795
1872
  }
1796
1873
  ]
1797
1874
  };
1798
- }, [data, levels, isEmpty, showTooltip]);
1875
+ }, [data, levels, isEmpty, showTooltip, chartColors]);
1876
+ const latest = useLatestRef({ onClick });
1799
1877
  const { containerRef } = useChart({
1800
1878
  option,
1801
- theme,
1879
+ theme: currentTheme === THEME_DARK6 ? "dark" : "light",
1802
1880
  animation,
1803
1881
  onReady: (chart) => {
1804
- if (onClick) {
1805
- chart.on("click", (params) => {
1806
- const treePathInfo = params.treePathInfo || [];
1807
- onClick({
1808
- type: params.type || "click",
1809
- componentType: params.componentType || "series",
1810
- name: params.name || "",
1811
- value: params.value,
1812
- path: treePathInfo.map((item) => item.name || ""),
1813
- depth: treePathInfo.length - 1
1814
- });
1882
+ chart.on("click", (params) => {
1883
+ const { onClick: onClick2 } = latest.current;
1884
+ if (!onClick2) return;
1885
+ const treePathInfo = params.treePathInfo || [];
1886
+ onClick2({
1887
+ type: params.type || "click",
1888
+ componentType: params.componentType || "series",
1889
+ name: params.name || "",
1890
+ value: params.value,
1891
+ path: treePathInfo.map((item) => item.name || ""),
1892
+ depth: treePathInfo.length - 1
1815
1893
  });
1816
- }
1894
+ });
1817
1895
  }
1818
1896
  });
1819
1897
  const accessibilityDescription = description || (!isEmpty ? `${title}. Treemap chart showing hierarchical data.` : void 0);
@@ -1851,6 +1929,7 @@ import {
1851
1929
  DataZoomComponent
1852
1930
  } from "echarts/components";
1853
1931
  import { CanvasRenderer as CanvasRenderer12 } from "echarts/renderers";
1932
+ import { getChartColors as getChartColors6, THEME_DARK as THEME_DARK7 } from "@classic-homes/charts-core";
1854
1933
  import { jsx as jsx16 } from "react/jsx-runtime";
1855
1934
  echarts14.use([
1856
1935
  EChartsCandlestickChart,
@@ -1879,6 +1958,8 @@ var CandlestickChart = React16.forwardRef(
1879
1958
  ...props
1880
1959
  }, ref) => {
1881
1960
  const isEmpty = !data?.dates?.length || !data?.data?.length;
1961
+ const currentTheme = useChartTheme(theme);
1962
+ const chartColors = getChartColors6(currentTheme);
1882
1963
  const option = React16.useMemo(() => {
1883
1964
  if (isEmpty) return {};
1884
1965
  const candlestickData = data.data.map((item) => [item.open, item.close, item.low, item.high]);
@@ -1888,12 +1969,12 @@ var CandlestickChart = React16.forwardRef(
1888
1969
  type: "candlestick",
1889
1970
  data: candlestickData,
1890
1971
  itemStyle: {
1891
- color: "#ef4444",
1972
+ color: chartColors.destructive,
1892
1973
  // Up color (red for bearish in some markets)
1893
- color0: "#22c55e",
1974
+ color0: chartColors.success,
1894
1975
  // Down color (green for bullish)
1895
- borderColor: "#ef4444",
1896
- borderColor0: "#22c55e"
1976
+ borderColor: chartColors.destructive,
1977
+ borderColor0: chartColors.success
1897
1978
  }
1898
1979
  }
1899
1980
  ];
@@ -1959,7 +2040,7 @@ var CandlestickChart = React16.forwardRef(
1959
2040
  yAxisIndex: 1,
1960
2041
  data: data.volume,
1961
2042
  itemStyle: {
1962
- color: "#6b7280"
2043
+ color: chartColors.mutedForeground
1963
2044
  }
1964
2045
  });
1965
2046
  }
@@ -1989,28 +2070,30 @@ var CandlestickChart = React16.forwardRef(
1989
2070
  ],
1990
2071
  series: seriesArr
1991
2072
  };
1992
- }, [data, isEmpty, showTooltip, showVolume, title]);
2073
+ }, [data, isEmpty, showTooltip, showVolume, title, chartColors]);
2074
+ const latest = useLatestRef({ onClick, data });
1993
2075
  const { containerRef } = useChart({
1994
2076
  option,
1995
- theme,
2077
+ theme: currentTheme === THEME_DARK7 ? "dark" : "light",
1996
2078
  animation,
1997
2079
  onReady: (chart) => {
1998
- if (onClick) {
1999
- chart.on("click", (params) => {
2000
- const dataIndex = params.dataIndex || 0;
2001
- const item = data.data[dataIndex];
2002
- onClick({
2003
- type: params.type || "click",
2004
- componentType: params.componentType || "series",
2005
- date: data.dates[dataIndex],
2006
- open: item.open,
2007
- close: item.close,
2008
- high: item.high,
2009
- low: item.low,
2010
- volume: data.volume?.[dataIndex]
2011
- });
2080
+ chart.on("click", (params) => {
2081
+ const { onClick: onClick2, data: data2 } = latest.current;
2082
+ if (!onClick2) return;
2083
+ const dataIndex = params.dataIndex || 0;
2084
+ const item = data2.data[dataIndex];
2085
+ if (!item) return;
2086
+ onClick2({
2087
+ type: params.type || "click",
2088
+ componentType: params.componentType || "series",
2089
+ date: data2.dates[dataIndex],
2090
+ open: item.open,
2091
+ close: item.close,
2092
+ high: item.high,
2093
+ low: item.low,
2094
+ volume: data2.volume?.[dataIndex]
2012
2095
  });
2013
- }
2096
+ });
2014
2097
  }
2015
2098
  });
2016
2099
  const accessibilityDescription = description || (!isEmpty ? `${title}. Candlestick chart with ${data.dates.length} data points.` : void 0);
@@ -2094,25 +2177,26 @@ var SankeyChart = React17.forwardRef(
2094
2177
  ]
2095
2178
  };
2096
2179
  }, [data, isEmpty, showTooltip, orient]);
2180
+ const latest = useLatestRef({ onClick });
2097
2181
  const { containerRef } = useChart({
2098
2182
  option,
2099
2183
  theme,
2100
2184
  animation,
2101
2185
  onReady: (chart) => {
2102
- if (onClick) {
2103
- chart.on("click", (params) => {
2104
- const isEdge = params.dataType === "edge";
2105
- onClick({
2106
- type: params.type || "click",
2107
- componentType: params.componentType || "series",
2108
- dataType: isEdge ? "edge" : "node",
2109
- name: params.name || "",
2110
- value: isEdge ? params.value : void 0,
2111
- source: isEdge ? params.data?.source : void 0,
2112
- target: isEdge ? params.data?.target : void 0
2113
- });
2186
+ chart.on("click", (params) => {
2187
+ const { onClick: onClick2 } = latest.current;
2188
+ if (!onClick2) return;
2189
+ const isEdge = params.dataType === "edge";
2190
+ onClick2({
2191
+ type: params.type || "click",
2192
+ componentType: params.componentType || "series",
2193
+ dataType: isEdge ? "edge" : "node",
2194
+ name: params.name || "",
2195
+ value: isEdge ? params.value : void 0,
2196
+ source: isEdge ? params.data?.source : void 0,
2197
+ target: isEdge ? params.data?.target : void 0
2114
2198
  });
2115
- }
2199
+ });
2116
2200
  }
2117
2201
  });
2118
2202
  const accessibilityDescription = description || (!isEmpty ? `${title}. Sankey diagram with ${data.nodes.length} nodes and ${data.links.length} links.` : void 0);