@classic-homes/charts-react 0.1.36 → 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,44 +253,49 @@ 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
- const chart = chartRef.current;
267
- if (!container || !chart) return;
276
+ if (!container) return;
268
277
  let resizeTimeout;
269
- resizeObserverRef.current = new ResizeObserver(() => {
278
+ const observer = new ResizeObserver(() => {
270
279
  clearTimeout(resizeTimeout);
271
280
  resizeTimeout = setTimeout(() => {
272
- chart.resize();
281
+ const chart = chartRef.current;
282
+ if (chart && !chart.isDisposed()) {
283
+ chart.resize();
284
+ }
273
285
  }, 100);
274
286
  });
275
- resizeObserverRef.current.observe(container);
287
+ observer.observe(container);
276
288
  return () => {
277
289
  clearTimeout(resizeTimeout);
278
- resizeObserverRef.current?.disconnect();
290
+ observer.disconnect();
279
291
  };
280
- }, [chartRef, containerRef]);
292
+ }, [chartRef, container]);
281
293
  }
282
294
 
283
295
  // src/hooks/useReducedMotion.ts
284
- import { useState as useState2, useEffect as useEffect3 } from "react";
296
+ import { useState as useState3, useEffect as useEffect3 } from "react";
285
297
  function useReducedMotion() {
286
- const [reducedMotion, setReducedMotion] = useState2(false);
298
+ const [reducedMotion, setReducedMotion] = useState3(false);
287
299
  useEffect3(() => {
288
300
  const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
289
301
  setReducedMotion(mediaQuery.matches);
@@ -299,51 +311,53 @@ function useReducedMotion() {
299
311
  }
300
312
 
301
313
  // src/hooks/useChart.ts
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
+ }
302
321
  function useChart({
303
322
  option,
304
323
  theme = "auto",
305
324
  animation,
306
325
  onReady
307
326
  }) {
308
- const containerRef = useRef2(null);
309
- const chartRef = useRef2(null);
327
+ const containerRef = useRef(null);
328
+ const container = useRefElement(containerRef);
329
+ const chartRef = useRef(null);
310
330
  const currentTheme = useChartTheme(theme);
311
331
  const reducedMotion = useReducedMotion();
312
- const onReadyRef = useRef2(onReady);
332
+ const onReadyRef = useRef(onReady);
313
333
  onReadyRef.current = onReady;
334
+ const optionRef = useRef(option);
335
+ optionRef.current = {
336
+ ...option,
337
+ // Reduced motion always wins over the animation prop
338
+ animation: (animation ?? true) && !reducedMotion
339
+ };
340
+ const appliedRef = useRef(null);
314
341
  useEffect4(() => {
315
- if (!containerRef.current) return;
316
- if (chartRef.current) {
317
- chartRef.current.dispose();
318
- }
319
- chartRef.current = echarts2.init(containerRef.current, currentTheme);
320
- if (onReadyRef.current && chartRef.current) {
321
- onReadyRef.current(chartRef.current);
322
- }
342
+ if (!container) return;
343
+ const chart = echarts2.init(container, currentTheme);
344
+ chartRef.current = chart;
345
+ applyOption(chart, null, optionRef.current);
346
+ appliedRef.current = optionRef.current;
347
+ onReadyRef.current?.(chart);
323
348
  return () => {
324
- chartRef.current?.dispose();
325
- chartRef.current = null;
349
+ chart.dispose();
350
+ if (chartRef.current === chart) {
351
+ chartRef.current = null;
352
+ }
326
353
  };
327
- }, [currentTheme]);
354
+ }, [container, currentTheme]);
328
355
  useEffect4(() => {
329
- if (!chartRef.current) return;
330
- const shouldAnimate = animation ?? !reducedMotion;
331
- const optionWithAnimation = {
332
- ...option,
333
- animation: shouldAnimate
334
- };
335
- chartRef.current.setOption(optionWithAnimation, {
336
- notMerge: false,
337
- lazyUpdate: true
338
- });
356
+ const chart = chartRef.current;
357
+ if (!chart || chart.isDisposed()) return;
358
+ applyOption(chart, appliedRef.current, optionRef.current);
359
+ appliedRef.current = optionRef.current;
339
360
  }, [option, animation, reducedMotion]);
340
- useEffect4(() => {
341
- if (!chartRef.current || !containerRef.current) return;
342
- const currentOption = chartRef.current.getOption();
343
- chartRef.current.dispose();
344
- chartRef.current = echarts2.init(containerRef.current, currentTheme);
345
- chartRef.current.setOption(currentOption);
346
- }, [currentTheme]);
347
361
  useChartResize(chartRef, containerRef);
348
362
  return {
349
363
  chartRef,
@@ -352,6 +366,14 @@ function useChart({
352
366
  };
353
367
  }
354
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
+
355
377
  // src/components/core/LineChart.tsx
356
378
  import { jsx as jsx5 } from "react/jsx-runtime";
357
379
  echarts3.use([
@@ -386,6 +408,8 @@ var LineChart = React5.forwardRef(
386
408
  ...props
387
409
  }, ref) => {
388
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);
389
413
  const option = React5.useMemo(() => {
390
414
  if (isEmpty) return {};
391
415
  return {
@@ -400,7 +424,7 @@ var LineChart = React5.forwardRef(
400
424
  axisPointer: {
401
425
  type: "cross",
402
426
  label: {
403
- backgroundColor: "#6a7985"
427
+ backgroundColor: chartColors.axisPointerLabel
404
428
  }
405
429
  }
406
430
  } : void 0,
@@ -446,41 +470,43 @@ var LineChart = React5.forwardRef(
446
470
  smooth,
447
471
  areaFilled,
448
472
  showDataPoints,
449
- stacked
473
+ stacked,
474
+ chartColors
450
475
  ]);
476
+ const latest = useLatestRef({ onClick, onHover });
451
477
  const { containerRef } = useChart({
452
478
  option,
453
- theme,
479
+ theme: currentTheme === THEME_DARK2 ? "dark" : "light",
454
480
  animation,
455
481
  onReady: (chart) => {
456
- if (onClick) {
457
- chart.on("click", (params) => {
458
- onClick({
459
- type: params.type || "click",
460
- componentType: params.componentType || "series",
461
- seriesIndex: params.seriesIndex,
462
- seriesName: params.seriesName || "",
463
- dataIndex: params.dataIndex || 0,
464
- name: params.name || "",
465
- value: params.value,
466
- color: params.color
467
- });
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
468
494
  });
469
- }
470
- if (onHover) {
471
- chart.on("mouseover", (params) => {
472
- onHover({
473
- type: params.type || "mouseover",
474
- componentType: params.componentType || "series",
475
- seriesIndex: params.seriesIndex,
476
- seriesName: params.seriesName || "",
477
- dataIndex: params.dataIndex || 0,
478
- name: params.name || "",
479
- value: params.value,
480
- color: params.color
481
- });
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
482
508
  });
483
- }
509
+ });
484
510
  }
485
511
  });
486
512
  const accessibilityDescription = description || (!isEmpty ? generateLineChartDescription(title, data) : void 0);
@@ -598,25 +624,26 @@ var BarChart = React6.forwardRef(
598
624
  }))
599
625
  };
600
626
  }, [data, isEmpty, showTooltip, showLegend, showGrid, isHorizontal, stacked, showValues]);
627
+ const latest = useLatestRef({ onClick });
601
628
  const { containerRef } = useChart({
602
629
  option,
603
630
  theme,
604
631
  animation,
605
632
  onReady: (chart) => {
606
- if (onClick) {
607
- chart.on("click", (params) => {
608
- onClick({
609
- type: params.type || "click",
610
- componentType: params.componentType || "series",
611
- seriesIndex: params.seriesIndex,
612
- seriesName: params.seriesName || "",
613
- dataIndex: params.dataIndex || 0,
614
- name: params.name || "",
615
- value: params.value,
616
- color: params.color
617
- });
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
618
645
  });
619
- }
646
+ });
620
647
  }
621
648
  });
622
649
  const accessibilityDescription = description || (!isEmpty ? generateBarChartDescription(title, data) : void 0);
@@ -686,7 +713,7 @@ var PieChart = React7.forwardRef(
686
713
  {
687
714
  name: title,
688
715
  type: "pie",
689
- radius: innerRadius === 0 ? "70%" : ["50%", "70%"],
716
+ radius: innerRadius ? [innerRadius, "70%"] : "70%",
690
717
  center: ["50%", "45%"],
691
718
  data: data.map((item) => ({
692
719
  name: item.name,
@@ -711,23 +738,24 @@ var PieChart = React7.forwardRef(
711
738
  ]
712
739
  };
713
740
  }, [data, isEmpty, showTooltip, showLegend, showLabels, labelPosition, innerRadius, title]);
741
+ const latest = useLatestRef({ onClick });
714
742
  const { containerRef } = useChart({
715
743
  option,
716
744
  theme,
717
745
  animation,
718
746
  onReady: (chart) => {
719
- if (onClick) {
720
- chart.on("click", (params) => {
721
- onClick({
722
- type: params.type || "click",
723
- componentType: params.componentType || "series",
724
- name: params.name || "",
725
- value: params.value,
726
- percent: params.percent,
727
- color: params.color
728
- });
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
729
757
  });
730
- }
758
+ });
731
759
  }
732
760
  });
733
761
  const accessibilityDescription = description || (!isEmpty ? generatePieChartDescription(title, data, false) : void 0);
@@ -762,6 +790,7 @@ import {
762
790
  TitleComponent as TitleComponent4
763
791
  } from "echarts/components";
764
792
  import { CanvasRenderer as CanvasRenderer4 } from "echarts/renderers";
793
+ import { colorWithAlpha, getChartColors as getChartColors2, THEME_DARK as THEME_DARK3 } from "@classic-homes/charts-core";
765
794
  import { jsx as jsx8 } from "react/jsx-runtime";
766
795
  echarts6.use([
767
796
  EChartsLineChart2,
@@ -793,6 +822,8 @@ var AreaChart = React8.forwardRef(
793
822
  ...props
794
823
  }, ref) => {
795
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);
796
827
  const option = React8.useMemo(() => {
797
828
  if (isEmpty) return {};
798
829
  return {
@@ -807,7 +838,7 @@ var AreaChart = React8.forwardRef(
807
838
  axisPointer: {
808
839
  type: "cross",
809
840
  label: {
810
- backgroundColor: "#6a7985"
841
+ backgroundColor: chartColors.axisPointerLabel
811
842
  }
812
843
  }
813
844
  } : void 0,
@@ -826,52 +857,50 @@ var AreaChart = React8.forwardRef(
826
857
  show: showGrid
827
858
  }
828
859
  },
829
- series: data.series.map((series, index) => ({
830
- name: series.name,
831
- type: "line",
832
- data: series.data,
833
- smooth,
834
- stack: stacked ? "Total" : void 0,
835
- areaStyle: gradient ? {
836
- opacity: 0.8,
837
- color: new echarts6.graphic.LinearGradient(0, 0, 0, 1, [
838
- {
839
- offset: 0,
840
- color: series.color || `rgba(${60 + index * 40}, ${160 - index * 20}, ${180 - index * 30}, 0.8)`
841
- },
842
- {
843
- offset: 1,
844
- color: series.color || `rgba(${60 + index * 40}, ${160 - index * 20}, ${180 - index * 30}, 0.1)`
845
- }
846
- ])
847
- } : { opacity: 0.5 },
848
- itemStyle: series.color ? { color: series.color } : void 0,
849
- emphasis: {
850
- focus: "series"
851
- },
852
- showSymbol: false
853
- }))
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
+ })
854
882
  };
855
- }, [data, isEmpty, showTooltip, showLegend, showGrid, smooth, stacked, gradient]);
883
+ }, [data, isEmpty, showTooltip, showLegend, showGrid, smooth, stacked, gradient, chartColors]);
884
+ const latest = useLatestRef({ onClick });
856
885
  const { containerRef } = useChart({
857
886
  option,
858
- theme,
887
+ theme: currentTheme === THEME_DARK3 ? "dark" : "light",
859
888
  animation,
860
889
  onReady: (chart) => {
861
- if (onClick) {
862
- chart.on("click", (params) => {
863
- onClick({
864
- type: params.type || "click",
865
- componentType: params.componentType || "series",
866
- seriesIndex: params.seriesIndex,
867
- seriesName: params.seriesName || "",
868
- dataIndex: params.dataIndex || 0,
869
- name: params.name || "",
870
- value: params.value,
871
- color: params.color
872
- });
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
873
902
  });
874
- }
903
+ });
875
904
  }
876
905
  });
877
906
  const accessibilityDescription = description || (!isEmpty ? `${title}. Area chart with ${data.categories.length} data points across ${data.series.length} series.` : void 0);
@@ -898,7 +927,7 @@ AreaChart.displayName = "AreaChart";
898
927
  // src/components/core/ScatterChart.tsx
899
928
  import * as React9 from "react";
900
929
  import * as echarts7 from "echarts/core";
901
- import { ScatterChart as EChartsScatterChart } from "echarts/charts";
930
+ import { ScatterChart as EChartsScatterChart, LineChart as EChartsLineChart3 } from "echarts/charts";
902
931
  import {
903
932
  GridComponent as GridComponent4,
904
933
  TooltipComponent as TooltipComponent5,
@@ -906,10 +935,12 @@ import {
906
935
  TitleComponent as TitleComponent5
907
936
  } from "echarts/components";
908
937
  import { CanvasRenderer as CanvasRenderer5 } from "echarts/renderers";
909
- import { generateScatterChartDescription } from "@classic-homes/charts-core";
938
+ import { escapeHtml, generateScatterChartDescription } from "@classic-homes/charts-core";
910
939
  import { jsx as jsx9 } from "react/jsx-runtime";
911
940
  echarts7.use([
912
941
  EChartsScatterChart,
942
+ // Trend lines are line series
943
+ EChartsLineChart3,
913
944
  GridComponent4,
914
945
  TooltipComponent5,
915
946
  LegendComponent5,
@@ -966,9 +997,12 @@ var ScatterChart = React9.forwardRef(
966
997
  });
967
998
  const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
968
999
  const intercept = (sumY - slope * sumX) / n;
969
- const xValues = s.data.map(([x]) => x);
970
- const minX = Math.min(...xValues);
971
- 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
+ }
972
1006
  series.push({
973
1007
  name: `${s.name} Trend`,
974
1008
  type: "line",
@@ -1001,7 +1035,7 @@ var ScatterChart = React9.forwardRef(
1001
1035
  formatter: (params) => {
1002
1036
  const p = params;
1003
1037
  if (!p.value) return "";
1004
- 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])}`;
1005
1039
  }
1006
1040
  } : void 0,
1007
1041
  legend: showLegend ? {
@@ -1023,25 +1057,26 @@ var ScatterChart = React9.forwardRef(
1023
1057
  series
1024
1058
  };
1025
1059
  }, [data, isEmpty, showTooltip, showLegend, showGrid, showTrendLine, symbolSize]);
1060
+ const latest = useLatestRef({ onClick });
1026
1061
  const { containerRef } = useChart({
1027
1062
  option,
1028
1063
  theme,
1029
1064
  animation,
1030
1065
  onReady: (chart) => {
1031
- if (onClick) {
1032
- chart.on("click", (params) => {
1033
- onClick({
1034
- type: params.type || "click",
1035
- componentType: params.componentType || "series",
1036
- seriesIndex: params.seriesIndex,
1037
- seriesName: params.seriesName || "",
1038
- dataIndex: params.dataIndex || 0,
1039
- name: params.name || "",
1040
- value: params.value,
1041
- color: params.color
1042
- });
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
1043
1078
  });
1044
- }
1079
+ });
1045
1080
  }
1046
1081
  });
1047
1082
  const accessibilityDescription = description || (!isEmpty ? generateScatterChartDescription(title, data) : void 0);
@@ -1071,7 +1106,11 @@ import * as echarts8 from "echarts/core";
1071
1106
  import { PieChart as EChartsPieChart2 } from "echarts/charts";
1072
1107
  import { TooltipComponent as TooltipComponent6, LegendComponent as LegendComponent6, TitleComponent as TitleComponent6 } from "echarts/components";
1073
1108
  import { CanvasRenderer as CanvasRenderer6 } from "echarts/renderers";
1074
- 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";
1075
1114
  import { jsx as jsx10 } from "react/jsx-runtime";
1076
1115
  echarts8.use([EChartsPieChart2, TooltipComponent6, LegendComponent6, TitleComponent6, CanvasRenderer6]);
1077
1116
  var DonutChart = React10.forwardRef(
@@ -1097,6 +1136,8 @@ var DonutChart = React10.forwardRef(
1097
1136
  ...props
1098
1137
  }, ref) => {
1099
1138
  const isEmpty = !data?.length;
1139
+ const currentTheme = useChartTheme(theme);
1140
+ const chartColors = getChartColors3(currentTheme);
1100
1141
  const option = React10.useMemo(() => {
1101
1142
  if (isEmpty) return {};
1102
1143
  const graphicElements = [];
@@ -1113,7 +1154,7 @@ var DonutChart = React10.forwardRef(
1113
1154
  text: String(centerValue),
1114
1155
  fontSize: 28,
1115
1156
  fontWeight: "bold",
1116
- fill: "currentColor",
1157
+ fill: chartColors.foreground,
1117
1158
  textAlign: "center"
1118
1159
  },
1119
1160
  top: 0,
@@ -1126,7 +1167,7 @@ var DonutChart = React10.forwardRef(
1126
1167
  style: {
1127
1168
  text: centerLabel,
1128
1169
  fontSize: 14,
1129
- fill: "#888",
1170
+ fill: chartColors.mutedForeground,
1130
1171
  textAlign: "center"
1131
1172
  },
1132
1173
  top: centerValue !== void 0 ? 35 : 0,
@@ -1189,25 +1230,27 @@ var DonutChart = React10.forwardRef(
1189
1230
  innerRadius,
1190
1231
  centerLabel,
1191
1232
  centerValue,
1192
- title
1233
+ title,
1234
+ chartColors
1193
1235
  ]);
1236
+ const latest = useLatestRef({ onClick });
1194
1237
  const { containerRef } = useChart({
1195
1238
  option,
1196
- theme,
1239
+ theme: currentTheme === THEME_DARK4 ? "dark" : "light",
1197
1240
  animation,
1198
1241
  onReady: (chart) => {
1199
- if (onClick) {
1200
- chart.on("click", (params) => {
1201
- onClick({
1202
- type: params.type || "click",
1203
- componentType: params.componentType || "series",
1204
- name: params.name || "",
1205
- value: params.value,
1206
- percent: params.percent,
1207
- color: params.color
1208
- });
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
1209
1252
  });
1210
- }
1253
+ });
1211
1254
  }
1212
1255
  });
1213
1256
  const accessibilityDescription = description || (!isEmpty ? generatePieChartDescription2(title, data, true) : void 0);
@@ -1384,7 +1427,8 @@ var FunnelChart = React12.forwardRef(
1384
1427
  bottom: 60,
1385
1428
  width: "80%",
1386
1429
  min: 0,
1387
- 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),
1388
1432
  minSize: "0%",
1389
1433
  maxSize: "100%",
1390
1434
  sort,
@@ -1414,22 +1458,23 @@ var FunnelChart = React12.forwardRef(
1414
1458
  ]
1415
1459
  };
1416
1460
  }, [data, isEmpty, showTooltip, showLegend, sort, orientation, showLabels, title]);
1461
+ const latest = useLatestRef({ onClick });
1417
1462
  const { containerRef } = useChart({
1418
1463
  option,
1419
1464
  theme,
1420
1465
  animation,
1421
1466
  onReady: (chart) => {
1422
- if (onClick) {
1423
- chart.on("click", (params) => {
1424
- onClick({
1425
- type: params.type || "click",
1426
- componentType: params.componentType || "series",
1427
- name: params.name || "",
1428
- value: params.value,
1429
- color: params.color
1430
- });
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
1431
1476
  });
1432
- }
1477
+ });
1433
1478
  }
1434
1479
  });
1435
1480
  const accessibilityDescription = description || (!isEmpty ? `${title}. Funnel chart with ${data.length} stages.` : void 0);
@@ -1459,6 +1504,7 @@ import * as echarts11 from "echarts/core";
1459
1504
  import { GaugeChart as EChartsGaugeChart } from "echarts/charts";
1460
1505
  import { TitleComponent as TitleComponent9 } from "echarts/components";
1461
1506
  import { CanvasRenderer as CanvasRenderer9 } from "echarts/renderers";
1507
+ import { getChartColors as getChartColors4, THEME_DARK as THEME_DARK5 } from "@classic-homes/charts-core";
1462
1508
  import { jsx as jsx13 } from "react/jsx-runtime";
1463
1509
  echarts11.use([EChartsGaugeChart, TitleComponent9, CanvasRenderer9]);
1464
1510
  var GaugeChart = React13.forwardRef(
@@ -1480,6 +1526,9 @@ var GaugeChart = React13.forwardRef(
1480
1526
  class: className,
1481
1527
  ...props
1482
1528
  }, ref) => {
1529
+ const currentTheme = useChartTheme(theme);
1530
+ const chartColors = getChartColors4(currentTheme);
1531
+ const reducedMotion = useReducedMotion();
1483
1532
  const option = React13.useMemo(() => {
1484
1533
  return {
1485
1534
  series: [
@@ -1504,7 +1553,7 @@ var GaugeChart = React13.forwardRef(
1504
1553
  splitNumber: 5,
1505
1554
  lineStyle: {
1506
1555
  width: 2,
1507
- color: "#999"
1556
+ color: chartColors.mutedForeground
1508
1557
  }
1509
1558
  },
1510
1559
  splitLine: {
@@ -1512,12 +1561,12 @@ var GaugeChart = React13.forwardRef(
1512
1561
  length: 10,
1513
1562
  lineStyle: {
1514
1563
  width: 3,
1515
- color: "#999"
1564
+ color: chartColors.mutedForeground
1516
1565
  }
1517
1566
  },
1518
1567
  axisLabel: {
1519
1568
  distance: -20,
1520
- color: "#999",
1569
+ color: chartColors.mutedForeground,
1521
1570
  fontSize: 12
1522
1571
  },
1523
1572
  anchor: {
@@ -1537,7 +1586,7 @@ var GaugeChart = React13.forwardRef(
1537
1586
  fontSize: 14
1538
1587
  },
1539
1588
  detail: {
1540
- valueAnimation: animation,
1589
+ valueAnimation: animation && !reducedMotion,
1541
1590
  fontSize: 28,
1542
1591
  fontWeight: "bold",
1543
1592
  offsetCenter: [0, "40%"],
@@ -1552,10 +1601,21 @@ var GaugeChart = React13.forwardRef(
1552
1601
  }
1553
1602
  ]
1554
1603
  };
1555
- }, [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
+ ]);
1556
1616
  const { containerRef } = useChart({
1557
1617
  option,
1558
- theme,
1618
+ theme: currentTheme === THEME_DARK5 ? "dark" : "light",
1559
1619
  animation
1560
1620
  });
1561
1621
  const accessibilityDescription = description || `${title}. Gauge showing value ${value} on scale from ${min} to ${max}.`;
@@ -1589,6 +1649,7 @@ import {
1589
1649
  VisualMapComponent
1590
1650
  } from "echarts/components";
1591
1651
  import { CanvasRenderer as CanvasRenderer10 } from "echarts/renderers";
1652
+ import { escapeHtml as escapeHtml2 } from "@classic-homes/charts-core";
1592
1653
  import { jsx as jsx14 } from "react/jsx-runtime";
1593
1654
  echarts12.use([
1594
1655
  EChartsHeatmapChart,
@@ -1618,9 +1679,14 @@ var HeatmapChart = React14.forwardRef(
1618
1679
  ...props
1619
1680
  }, ref) => {
1620
1681
  const isEmpty = !data?.data?.length || !xAxis?.length || !yAxis?.length;
1621
- const values = data?.data?.map(([, , value]) => value) || [];
1622
- const minValue = data?.min ?? Math.min(...values);
1623
- 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;
1624
1690
  const option = React14.useMemo(() => {
1625
1691
  if (isEmpty) return {};
1626
1692
  return {
@@ -1630,7 +1696,7 @@ var HeatmapChart = React14.forwardRef(
1630
1696
  const p = params;
1631
1697
  if (!p.value) return "";
1632
1698
  const [xIdx, yIdx, value] = p.value;
1633
- return `${xAxis[xIdx]} / ${yAxis[yIdx]}: ${value}`;
1699
+ return `${escapeHtml2(xAxis[xIdx])} / ${escapeHtml2(yAxis[yIdx])}: ${escapeHtml2(value)}`;
1634
1700
  }
1635
1701
  } : void 0,
1636
1702
  grid: {
@@ -1679,25 +1745,26 @@ var HeatmapChart = React14.forwardRef(
1679
1745
  ]
1680
1746
  };
1681
1747
  }, [data, xAxis, yAxis, isEmpty, showTooltip, showValues, minValue, maxValue, title]);
1748
+ const latest = useLatestRef({ onClick, xAxis, yAxis });
1682
1749
  const { containerRef } = useChart({
1683
1750
  option,
1684
1751
  theme,
1685
1752
  animation,
1686
1753
  onReady: (chart) => {
1687
- if (onClick) {
1688
- chart.on("click", (params) => {
1689
- const [xIdx, yIdx, value] = params.value || [0, 0, 0];
1690
- onClick({
1691
- type: params.type || "click",
1692
- componentType: params.componentType || "series",
1693
- xIndex: xIdx,
1694
- yIndex: yIdx,
1695
- xLabel: xAxis[xIdx],
1696
- yLabel: yAxis[yIdx],
1697
- value
1698
- });
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
1699
1766
  });
1700
- }
1767
+ });
1701
1768
  }
1702
1769
  });
1703
1770
  const accessibilityDescription = description || (!isEmpty ? `${title}. Heatmap with ${xAxis.length} columns and ${yAxis.length} rows.` : void 0);
@@ -1727,6 +1794,7 @@ import * as echarts13 from "echarts/core";
1727
1794
  import { TreemapChart as EChartsTreemapChart } from "echarts/charts";
1728
1795
  import { TooltipComponent as TooltipComponent10, TitleComponent as TitleComponent11 } from "echarts/components";
1729
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";
1730
1798
  import { jsx as jsx15 } from "react/jsx-runtime";
1731
1799
  echarts13.use([EChartsTreemapChart, TooltipComponent10, TitleComponent11, CanvasRenderer11]);
1732
1800
  var TreemapChart = React15.forwardRef(
@@ -1747,13 +1815,15 @@ var TreemapChart = React15.forwardRef(
1747
1815
  ...props
1748
1816
  }, ref) => {
1749
1817
  const isEmpty = !data?.length;
1818
+ const currentTheme = useChartTheme(theme);
1819
+ const chartColors = getChartColors5(currentTheme);
1750
1820
  const option = React15.useMemo(() => {
1751
1821
  if (isEmpty) return {};
1752
1822
  return {
1753
1823
  tooltip: showTooltip ? {
1754
1824
  formatter: (params) => {
1755
1825
  const p = params;
1756
- return `${p.name}: ${p.value}`;
1826
+ return `${escapeHtml3(p.name)}: ${escapeHtml3(p.value)}`;
1757
1827
  }
1758
1828
  } : void 0,
1759
1829
  series: [
@@ -1764,7 +1834,7 @@ var TreemapChart = React15.forwardRef(
1764
1834
  levels: levels || [
1765
1835
  {
1766
1836
  itemStyle: {
1767
- borderColor: "#555",
1837
+ borderColor: chartColors.divider,
1768
1838
  borderWidth: 4,
1769
1839
  gapWidth: 4
1770
1840
  }
@@ -1802,25 +1872,26 @@ var TreemapChart = React15.forwardRef(
1802
1872
  }
1803
1873
  ]
1804
1874
  };
1805
- }, [data, levels, isEmpty, showTooltip]);
1875
+ }, [data, levels, isEmpty, showTooltip, chartColors]);
1876
+ const latest = useLatestRef({ onClick });
1806
1877
  const { containerRef } = useChart({
1807
1878
  option,
1808
- theme,
1879
+ theme: currentTheme === THEME_DARK6 ? "dark" : "light",
1809
1880
  animation,
1810
1881
  onReady: (chart) => {
1811
- if (onClick) {
1812
- chart.on("click", (params) => {
1813
- const treePathInfo = params.treePathInfo || [];
1814
- onClick({
1815
- type: params.type || "click",
1816
- componentType: params.componentType || "series",
1817
- name: params.name || "",
1818
- value: params.value,
1819
- path: treePathInfo.map((item) => item.name || ""),
1820
- depth: treePathInfo.length - 1
1821
- });
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
1822
1893
  });
1823
- }
1894
+ });
1824
1895
  }
1825
1896
  });
1826
1897
  const accessibilityDescription = description || (!isEmpty ? `${title}. Treemap chart showing hierarchical data.` : void 0);
@@ -1858,6 +1929,7 @@ import {
1858
1929
  DataZoomComponent
1859
1930
  } from "echarts/components";
1860
1931
  import { CanvasRenderer as CanvasRenderer12 } from "echarts/renderers";
1932
+ import { getChartColors as getChartColors6, THEME_DARK as THEME_DARK7 } from "@classic-homes/charts-core";
1861
1933
  import { jsx as jsx16 } from "react/jsx-runtime";
1862
1934
  echarts14.use([
1863
1935
  EChartsCandlestickChart,
@@ -1886,6 +1958,8 @@ var CandlestickChart = React16.forwardRef(
1886
1958
  ...props
1887
1959
  }, ref) => {
1888
1960
  const isEmpty = !data?.dates?.length || !data?.data?.length;
1961
+ const currentTheme = useChartTheme(theme);
1962
+ const chartColors = getChartColors6(currentTheme);
1889
1963
  const option = React16.useMemo(() => {
1890
1964
  if (isEmpty) return {};
1891
1965
  const candlestickData = data.data.map((item) => [item.open, item.close, item.low, item.high]);
@@ -1895,12 +1969,12 @@ var CandlestickChart = React16.forwardRef(
1895
1969
  type: "candlestick",
1896
1970
  data: candlestickData,
1897
1971
  itemStyle: {
1898
- color: "#ef4444",
1972
+ color: chartColors.destructive,
1899
1973
  // Up color (red for bearish in some markets)
1900
- color0: "#22c55e",
1974
+ color0: chartColors.success,
1901
1975
  // Down color (green for bullish)
1902
- borderColor: "#ef4444",
1903
- borderColor0: "#22c55e"
1976
+ borderColor: chartColors.destructive,
1977
+ borderColor0: chartColors.success
1904
1978
  }
1905
1979
  }
1906
1980
  ];
@@ -1966,7 +2040,7 @@ var CandlestickChart = React16.forwardRef(
1966
2040
  yAxisIndex: 1,
1967
2041
  data: data.volume,
1968
2042
  itemStyle: {
1969
- color: "#6b7280"
2043
+ color: chartColors.mutedForeground
1970
2044
  }
1971
2045
  });
1972
2046
  }
@@ -1996,28 +2070,30 @@ var CandlestickChart = React16.forwardRef(
1996
2070
  ],
1997
2071
  series: seriesArr
1998
2072
  };
1999
- }, [data, isEmpty, showTooltip, showVolume, title]);
2073
+ }, [data, isEmpty, showTooltip, showVolume, title, chartColors]);
2074
+ const latest = useLatestRef({ onClick, data });
2000
2075
  const { containerRef } = useChart({
2001
2076
  option,
2002
- theme,
2077
+ theme: currentTheme === THEME_DARK7 ? "dark" : "light",
2003
2078
  animation,
2004
2079
  onReady: (chart) => {
2005
- if (onClick) {
2006
- chart.on("click", (params) => {
2007
- const dataIndex = params.dataIndex || 0;
2008
- const item = data.data[dataIndex];
2009
- onClick({
2010
- type: params.type || "click",
2011
- componentType: params.componentType || "series",
2012
- date: data.dates[dataIndex],
2013
- open: item.open,
2014
- close: item.close,
2015
- high: item.high,
2016
- low: item.low,
2017
- volume: data.volume?.[dataIndex]
2018
- });
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]
2019
2095
  });
2020
- }
2096
+ });
2021
2097
  }
2022
2098
  });
2023
2099
  const accessibilityDescription = description || (!isEmpty ? `${title}. Candlestick chart with ${data.dates.length} data points.` : void 0);
@@ -2101,25 +2177,26 @@ var SankeyChart = React17.forwardRef(
2101
2177
  ]
2102
2178
  };
2103
2179
  }, [data, isEmpty, showTooltip, orient]);
2180
+ const latest = useLatestRef({ onClick });
2104
2181
  const { containerRef } = useChart({
2105
2182
  option,
2106
2183
  theme,
2107
2184
  animation,
2108
2185
  onReady: (chart) => {
2109
- if (onClick) {
2110
- chart.on("click", (params) => {
2111
- const isEdge = params.dataType === "edge";
2112
- onClick({
2113
- type: params.type || "click",
2114
- componentType: params.componentType || "series",
2115
- dataType: isEdge ? "edge" : "node",
2116
- name: params.name || "",
2117
- value: isEdge ? params.value : void 0,
2118
- source: isEdge ? params.data?.source : void 0,
2119
- target: isEdge ? params.data?.target : void 0
2120
- });
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
2121
2198
  });
2122
- }
2199
+ });
2123
2200
  }
2124
2201
  });
2125
2202
  const accessibilityDescription = description || (!isEmpty ? `${title}. Sankey diagram with ${data.nodes.length} nodes and ${data.links.length} links.` : void 0);