react-klinecharts-ui 0.4.0 → 0.5.0

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
@@ -1,5 +1,5 @@
1
- import { registerExtensions, TA_default } from './chunk-U325AHHX.js';
2
- export { TA_default as TA, abcd_default as abcd, anyWaves_default as anyWaves, arrow_default as arrow, bollTv_default as bollTv, brush_default as brush, cci_default as cci, circle_default as circle, depthOverlay_default as depthOverlay, eightWaves_default as eightWaves, elliottWave_default as elliottWave, fibRetracement_default as fibRetracement, fibonacciCircle_default as fibonacciCircle, fibonacciExtension_default as fibonacciExtension, fibonacciSegment_default as fibonacciSegment, fibonacciSpeedResistanceFan_default as fibonacciSpeedResistanceFan, fibonacciSpiral_default as fibonacciSpiral, fiveWaves_default as fiveWaves, gannBox_default as gannBox, gannFan_default as gannFan, hma_default as hma, ichimoku_default as ichimoku, indicators, longPosition_default as longPosition, maRibbon_default as maRibbon, macdTv_default as macdTv, measure_default as measure, orderLine_default as orderLine, overlays, parallelChannel_default as parallelChannel, parallelogram_default as parallelogram, pivotPoints_default as pivotPoints, ray_default as ray, rect_default as rect, registerExtensions, rsiTv_default as rsiTv, shortPosition_default as shortPosition, stochastic_default as stochastic, superTrend_default as superTrend, threeWaves_default as threeWaves, triangle_default as triangle, vwap_default as vwap, xabcd_default as xabcd } from './chunk-U325AHHX.js';
1
+ import { registerExtensions, TA_default } from './chunk-DTBNTO4M.js';
2
+ export { TA_default as TA, abcd_default as abcd, anyWaves_default as anyWaves, arrow_default as arrow, bollTv_default as bollTv, brush_default as brush, cci_default as cci, circle_default as circle, depthOverlay_default as depthOverlay, eightWaves_default as eightWaves, elliottWave_default as elliottWave, fibRetracement_default as fibRetracement, fibonacciCircle_default as fibonacciCircle, fibonacciExtension_default as fibonacciExtension, fibonacciSegment_default as fibonacciSegment, fibonacciSpeedResistanceFan_default as fibonacciSpeedResistanceFan, fibonacciSpiral_default as fibonacciSpiral, fiveWaves_default as fiveWaves, gannBox_default as gannBox, gannFan_default as gannFan, hma_default as hma, ichimoku_default as ichimoku, indicators, longPosition_default as longPosition, maRibbon_default as maRibbon, macdTv_default as macdTv, measure_default as measure, orderLine_default as orderLine, overlays, parallelChannel_default as parallelChannel, parallelogram_default as parallelogram, pivotPoints_default as pivotPoints, ray_default as ray, rect_default as rect, registerExtensions, rsiTv_default as rsiTv, shortPosition_default as shortPosition, stochastic_default as stochastic, superTrend_default as superTrend, threeWaves_default as threeWaves, triangle_default as triangle, vwap_default as vwap, xabcd_default as xabcd } from './chunk-DTBNTO4M.js';
3
3
  import { createContext, useContext, useReducer, useRef, useCallback, useEffect, useMemo, useState } from 'react';
4
4
  import { registerOverlay, registerIndicator } from 'react-klinecharts';
5
5
  import { jsx } from 'react/jsx-runtime';
@@ -59,6 +59,14 @@ function reducer(state, action) {
59
59
  return { ...state, subIndicators: action.indicators };
60
60
  case "SET_INDICATOR_AXES":
61
61
  return { ...state, indicatorAxes: action.axes };
62
+ case "SET_INDICATOR_VISIBILITY":
63
+ return { ...state, indicatorVisibility: action.visibility };
64
+ case "SET_ALERTS":
65
+ return { ...state, alerts: action.alerts };
66
+ case "SET_MEASURE":
67
+ return { ...state, measure: { ...state.measure, ...action.measure } };
68
+ case "SET_REPLAY":
69
+ return { ...state, replay: { ...state.replay, ...action.replay } };
62
70
  case "SET_STYLES":
63
71
  return { ...state, styles: action.styles };
64
72
  case "SET_LOCALE":
@@ -122,12 +130,27 @@ function KlinechartsUIProvider({
122
130
  {}
123
131
  ),
124
132
  indicatorAxes: {},
133
+ indicatorVisibility: {},
134
+ alerts: [],
135
+ measure: { isActive: false, fromPoint: null, result: null },
136
+ replay: {
137
+ isReplaying: false,
138
+ isPaused: false,
139
+ speed: 1,
140
+ barIndex: 0,
141
+ totalBars: 0
142
+ },
125
143
  styles: opts.styles,
126
144
  screenshotUrl: null
127
145
  })
128
146
  );
129
147
  const fullscreenContainerRef = useRef(null);
130
148
  const undoRedoListenerRef = useRef(null);
149
+ const alertTriggeredListenerRef = useRef(null);
150
+ const replayIntervalRef = useRef(null);
151
+ const replaySavedDataRef = useRef([]);
152
+ const replayIndexRef = useRef(0);
153
+ const alertPrevCloseRef = useRef(null);
131
154
  const stateRef = useRef(state);
132
155
  stateRef.current = state;
133
156
  const callbacksRef = useRef({
@@ -188,13 +211,58 @@ function KlinechartsUIProvider({
188
211
  state.chart.setStyles(state.styles);
189
212
  }
190
213
  }, [state.chart, state.styles]);
214
+ const hasAlerts = state.alerts.length > 0;
215
+ useEffect(() => {
216
+ const chart = state.chart;
217
+ if (!chart || !hasAlerts) return;
218
+ alertPrevCloseRef.current = null;
219
+ const interval = setInterval(() => {
220
+ const dataList = chart.getDataList();
221
+ if (!dataList || dataList.length === 0) return;
222
+ const currentClose = dataList[dataList.length - 1].close;
223
+ const prevClose = alertPrevCloseRef.current;
224
+ alertPrevCloseRef.current = currentClose;
225
+ if (prevClose === null) return;
226
+ const alerts = stateRef.current.alerts;
227
+ let changed = false;
228
+ const next = alerts.map((alert) => {
229
+ if (alert.triggered) return alert;
230
+ const crossedUp = prevClose < alert.price && currentClose >= alert.price;
231
+ const crossedDown = prevClose > alert.price && currentClose <= alert.price;
232
+ const shouldTrigger = alert.condition === "crossing_up" ? crossedUp : alert.condition === "crossing_down" ? crossedDown : crossedUp || crossedDown;
233
+ if (shouldTrigger) {
234
+ changed = true;
235
+ const triggered = { ...alert, triggered: true };
236
+ alertTriggeredListenerRef.current?.(triggered);
237
+ return triggered;
238
+ }
239
+ return alert;
240
+ });
241
+ if (changed) {
242
+ enhancedDispatch({ type: "SET_ALERTS", alerts: next });
243
+ }
244
+ }, 1e3);
245
+ return () => clearInterval(interval);
246
+ }, [state.chart, hasAlerts, enhancedDispatch]);
247
+ useEffect(() => {
248
+ return () => {
249
+ if (replayIntervalRef.current !== null) {
250
+ clearInterval(replayIntervalRef.current);
251
+ replayIntervalRef.current = null;
252
+ }
253
+ };
254
+ }, []);
191
255
  const dispatchValue = useMemo(
192
256
  () => ({
193
257
  dispatch: enhancedDispatch,
194
258
  datafeed,
195
259
  onSettingsChange,
196
260
  fullscreenContainerRef,
197
- undoRedoListenerRef
261
+ undoRedoListenerRef,
262
+ alertTriggeredListenerRef,
263
+ replayIntervalRef,
264
+ replaySavedDataRef,
265
+ replayIndexRef
198
266
  }),
199
267
  [enhancedDispatch, datafeed, onSettingsChange]
200
268
  );
@@ -656,18 +724,26 @@ function useIndicators() {
656
724
  const activeNames = state.mainIndicators;
657
725
  const inactive = MAIN_INDICATORS.filter((n) => !activeNames.includes(n));
658
726
  return [
659
- ...activeNames.map((name) => ({ name, isActive: true })),
660
- ...inactive.map((name) => ({ name, isActive: false }))
727
+ ...activeNames.map((name) => ({
728
+ name,
729
+ isActive: true,
730
+ visible: state.indicatorVisibility[`main_${name}`] ?? true
731
+ })),
732
+ ...inactive.map((name) => ({ name, isActive: false, visible: true }))
661
733
  ];
662
- }, [state.mainIndicators]);
734
+ }, [state.mainIndicators, state.indicatorVisibility]);
663
735
  const subIndicators = useMemo(() => {
664
736
  const activeNames = Object.keys(state.subIndicators);
665
737
  const inactive = SUB_INDICATORS.filter((n) => !activeNames.includes(n));
666
738
  return [
667
- ...activeNames.map((name) => ({ name, isActive: true })),
668
- ...inactive.map((name) => ({ name, isActive: false }))
739
+ ...activeNames.map((name) => ({
740
+ name,
741
+ isActive: true,
742
+ visible: state.indicatorVisibility[`sub_${name}`] ?? true
743
+ })),
744
+ ...inactive.map((name) => ({ name, isActive: false, visible: true }))
669
745
  ];
670
- }, [state.subIndicators]);
746
+ }, [state.subIndicators, state.indicatorVisibility]);
671
747
  const addMainIndicator = useCallback(
672
748
  (name, options) => {
673
749
  if (state.mainIndicators.includes(name)) return;
@@ -709,12 +785,24 @@ function useIndicators() {
709
785
  delete nextAxes[id];
710
786
  dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
711
787
  }
788
+ if (id in state.indicatorVisibility) {
789
+ const nextVisibility = { ...state.indicatorVisibility };
790
+ delete nextVisibility[id];
791
+ dispatch({ type: "SET_INDICATOR_VISIBILITY", visibility: nextVisibility });
792
+ }
712
793
  undoRedoListenerRef.current?.({
713
794
  type: "indicator_toggled",
714
795
  data: { name, wasActive: true, isMain: true, paneId: "candle_pane", yAxisId }
715
796
  });
716
797
  },
717
- [state.chart, state.mainIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
798
+ [
799
+ state.chart,
800
+ state.mainIndicators,
801
+ state.indicatorAxes,
802
+ state.indicatorVisibility,
803
+ dispatch,
804
+ undoRedoListenerRef
805
+ ]
718
806
  );
719
807
  const addSubIndicator = useCallback(
720
808
  (name, options) => {
@@ -752,12 +840,24 @@ function useIndicators() {
752
840
  delete nextAxes[id];
753
841
  dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
754
842
  }
843
+ if (id in state.indicatorVisibility) {
844
+ const nextVisibility = { ...state.indicatorVisibility };
845
+ delete nextVisibility[id];
846
+ dispatch({ type: "SET_INDICATOR_VISIBILITY", visibility: nextVisibility });
847
+ }
755
848
  undoRedoListenerRef.current?.({
756
849
  type: "indicator_toggled",
757
850
  data: { name, wasActive: true, isMain: false, paneId, yAxisId }
758
851
  });
759
852
  },
760
- [state.chart, state.subIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
853
+ [
854
+ state.chart,
855
+ state.subIndicators,
856
+ state.indicatorAxes,
857
+ state.indicatorVisibility,
858
+ dispatch,
859
+ undoRedoListenerRef
860
+ ]
761
861
  );
762
862
  const toggleMainIndicator = useCallback(
763
863
  (name) => {
@@ -783,8 +883,16 @@ function useIndicators() {
783
883
  (name, isMain, visible) => {
784
884
  const id = isMain ? `main_${name}` : `sub_${name}`;
785
885
  state.chart?.overrideIndicator({ name, id, visible });
886
+ const nextVisibility = { ...state.indicatorVisibility };
887
+ if (visible) delete nextVisibility[id];
888
+ else nextVisibility[id] = false;
889
+ dispatch({ type: "SET_INDICATOR_VISIBILITY", visibility: nextVisibility });
786
890
  },
787
- [state.chart]
891
+ [state.chart, state.indicatorVisibility, dispatch]
892
+ );
893
+ const isIndicatorVisible = useCallback(
894
+ (name, isMain) => state.indicatorVisibility[isMain ? `main_${name}` : `sub_${name}`] ?? true,
895
+ [state.indicatorVisibility]
788
896
  );
789
897
  const updateIndicatorParams = useCallback(
790
898
  // `paneId` is kept in the signature for API stability; klinecharts v10
@@ -867,9 +975,14 @@ function useIndicators() {
867
975
  id: paneId,
868
976
  height: COLLAPSED_HEIGHT
869
977
  });
870
- state.chart.overrideIndicator({ name, id: `sub_${name}`, visible: false });
978
+ const id = `sub_${name}`;
979
+ state.chart.overrideIndicator({ name, id, visible: false });
980
+ dispatch({
981
+ type: "SET_INDICATOR_VISIBILITY",
982
+ visibility: { ...state.indicatorVisibility, [id]: false }
983
+ });
871
984
  },
872
- [state.chart, state.subIndicators]
985
+ [state.chart, state.subIndicators, state.indicatorVisibility, dispatch]
873
986
  );
874
987
  const expandSubIndicator = useCallback(
875
988
  (name) => {
@@ -881,9 +994,13 @@ function useIndicators() {
881
994
  id: paneId,
882
995
  height: savedHeight
883
996
  });
884
- state.chart.overrideIndicator({ name, id: `sub_${name}`, visible: true });
997
+ const id = `sub_${name}`;
998
+ state.chart.overrideIndicator({ name, id, visible: true });
999
+ const nextVisibility = { ...state.indicatorVisibility };
1000
+ delete nextVisibility[id];
1001
+ dispatch({ type: "SET_INDICATOR_VISIBILITY", visibility: nextVisibility });
885
1002
  },
886
- [state.chart, state.subIndicators]
1003
+ [state.chart, state.subIndicators, state.indicatorVisibility, dispatch]
887
1004
  );
888
1005
  const isSubIndicatorCollapsed = useCallback(
889
1006
  (name) => {
@@ -992,6 +1109,7 @@ function useIndicators() {
992
1109
  moveToMain,
993
1110
  moveToSub,
994
1111
  setIndicatorVisible,
1112
+ isIndicatorVisible,
995
1113
  updateIndicatorParams,
996
1114
  getIndicatorParams,
997
1115
  isMainIndicatorActive,
@@ -1002,6 +1120,7 @@ function useIndicators() {
1002
1120
  reorderSubIndicator,
1003
1121
  indicatorAxes: state.indicatorAxes,
1004
1122
  getIndicatorAxis,
1123
+ indicatorVisibility: state.indicatorVisibility,
1005
1124
  bindIndicatorToNewAxis
1006
1125
  };
1007
1126
  }
@@ -2122,6 +2241,7 @@ function useLayoutManager() {
2122
2241
  const newMainIndicators = [];
2123
2242
  const newSubIndicators = {};
2124
2243
  const restoredAxes = {};
2244
+ const restoredVisibility = {};
2125
2245
  if (chartState.indicators) {
2126
2246
  for (const ind of chartState.indicators) {
2127
2247
  const isMain = ind.paneId === "candle_pane";
@@ -2149,6 +2269,9 @@ function useLayoutManager() {
2149
2269
  if (ind.yAxisId) {
2150
2270
  restoredAxes[id2] = ind.yAxisId;
2151
2271
  }
2272
+ if (ind.visible === false) {
2273
+ restoredVisibility[id2] = false;
2274
+ }
2152
2275
  if (isMain) {
2153
2276
  newMainIndicators.push(ind.name);
2154
2277
  } else {
@@ -2168,6 +2291,10 @@ function useLayoutManager() {
2168
2291
  type: "SET_INDICATOR_AXES",
2169
2292
  axes: restoredAxes
2170
2293
  });
2294
+ dispatch({
2295
+ type: "SET_INDICATOR_VISIBILITY",
2296
+ visibility: restoredVisibility
2297
+ });
2171
2298
  if (chartState.drawings) {
2172
2299
  for (const drawing of chartState.drawings) {
2173
2300
  chart.createOverlay({
@@ -2534,10 +2661,9 @@ function useCrosshair() {
2534
2661
  }
2535
2662
  var alertCounter = 0;
2536
2663
  function useAlerts() {
2537
- const { state } = useKlinechartsUI();
2538
- const [alerts, setAlerts] = useState([]);
2539
- const callbackRef = useRef(null);
2540
- const prevCloseRef = useRef(null);
2664
+ const { state, dispatch } = useKlinechartsUI();
2665
+ const { alertTriggeredListenerRef } = useKlinechartsUIDispatch();
2666
+ const alerts = state.alerts;
2541
2667
  const addAlert = useCallback(
2542
2668
  (price, condition, message) => {
2543
2669
  const id = `alert_${++alertCounter}`;
@@ -2548,7 +2674,7 @@ function useAlerts() {
2548
2674
  message,
2549
2675
  triggered: false
2550
2676
  };
2551
- setAlerts((prev) => [...prev, alert]);
2677
+ dispatch({ type: "SET_ALERTS", alerts: [...state.alerts, alert] });
2552
2678
  if (state.chart) {
2553
2679
  state.chart.createOverlay({
2554
2680
  name: "horizontalStraightLine",
@@ -2567,67 +2693,30 @@ function useAlerts() {
2567
2693
  }
2568
2694
  return id;
2569
2695
  },
2570
- [state.chart]
2696
+ [state.chart, state.alerts, dispatch]
2571
2697
  );
2572
2698
  const removeAlert = useCallback(
2573
2699
  (id) => {
2574
- setAlerts((prev) => prev.filter((a) => a.id !== id));
2700
+ dispatch({
2701
+ type: "SET_ALERTS",
2702
+ alerts: state.alerts.filter((a) => a.id !== id)
2703
+ });
2575
2704
  state.chart?.removeOverlay({ id });
2576
2705
  },
2577
- [state.chart]
2706
+ [state.chart, state.alerts, dispatch]
2578
2707
  );
2579
2708
  const clearAlerts = useCallback(() => {
2580
- setAlerts((prev) => {
2581
- for (const alert of prev) {
2582
- state.chart?.removeOverlay({ id: alert.id });
2583
- }
2584
- return [];
2585
- });
2586
- }, [state.chart]);
2709
+ for (const alert of state.alerts) {
2710
+ state.chart?.removeOverlay({ id: alert.id });
2711
+ }
2712
+ dispatch({ type: "SET_ALERTS", alerts: [] });
2713
+ }, [state.chart, state.alerts, dispatch]);
2587
2714
  const onAlertTriggered = useCallback(
2588
2715
  (callback) => {
2589
- callbackRef.current = callback;
2716
+ alertTriggeredListenerRef.current = callback;
2590
2717
  },
2591
- []
2718
+ [alertTriggeredListenerRef]
2592
2719
  );
2593
- useEffect(() => {
2594
- if (!state.chart) return;
2595
- const interval = setInterval(() => {
2596
- const dataList = state.chart?.getDataList();
2597
- if (!dataList || dataList.length === 0) return;
2598
- const lastBar = dataList[dataList.length - 1];
2599
- const currentClose = lastBar.close;
2600
- const prevClose = prevCloseRef.current;
2601
- if (prevClose === null) {
2602
- prevCloseRef.current = currentClose;
2603
- return;
2604
- }
2605
- prevCloseRef.current = currentClose;
2606
- setAlerts((prev) => {
2607
- let changed = false;
2608
- const next = prev.map((alert) => {
2609
- if (alert.triggered) return alert;
2610
- let shouldTrigger = false;
2611
- if (alert.condition === "crossing_up") {
2612
- shouldTrigger = prevClose < alert.price && currentClose >= alert.price;
2613
- } else if (alert.condition === "crossing_down") {
2614
- shouldTrigger = prevClose > alert.price && currentClose <= alert.price;
2615
- } else if (alert.condition === "crossing") {
2616
- shouldTrigger = prevClose < alert.price && currentClose >= alert.price || prevClose > alert.price && currentClose <= alert.price;
2617
- }
2618
- if (shouldTrigger) {
2619
- changed = true;
2620
- const triggered = { ...alert, triggered: true };
2621
- callbackRef.current?.(triggered);
2622
- return triggered;
2623
- }
2624
- return alert;
2625
- });
2626
- return changed ? next : prev;
2627
- });
2628
- }, 1e3);
2629
- return () => clearInterval(interval);
2630
- }, [state.chart]);
2631
2720
  return {
2632
2721
  alerts,
2633
2722
  addAlert,
@@ -2773,128 +2862,124 @@ function useWatchlist() {
2773
2862
  };
2774
2863
  }
2775
2864
  function useReplay() {
2776
- const { state } = useKlinechartsUI();
2777
- const [isReplaying, setIsReplaying] = useState(false);
2778
- const [isPaused, setIsPaused] = useState(false);
2779
- const [speed, setSpeedState] = useState(1);
2780
- const [barIndex, setBarIndex] = useState(0);
2781
- const [totalBars, setTotalBars] = useState(0);
2782
- const intervalRef = useRef(null);
2783
- const savedDataRef = useRef([]);
2784
- const currentIndexRef = useRef(0);
2865
+ const { state, dispatch } = useKlinechartsUI();
2866
+ const { replayIntervalRef, replaySavedDataRef, replayIndexRef } = useKlinechartsUIDispatch();
2867
+ const { isReplaying, isPaused, speed, barIndex, totalBars } = state.replay;
2785
2868
  const clearInterval_ = useCallback(() => {
2786
- if (intervalRef.current !== null) {
2787
- clearInterval(intervalRef.current);
2788
- intervalRef.current = null;
2869
+ if (replayIntervalRef.current !== null) {
2870
+ clearInterval(replayIntervalRef.current);
2871
+ replayIntervalRef.current = null;
2789
2872
  }
2790
- }, []);
2873
+ }, [replayIntervalRef]);
2791
2874
  const addNextBar = useCallback(() => {
2792
2875
  if (!state.chart) return;
2793
- const data = savedDataRef.current;
2794
- const idx = currentIndexRef.current;
2876
+ const data = replaySavedDataRef.current;
2877
+ const idx = replayIndexRef.current;
2795
2878
  if (idx >= data.length) {
2796
2879
  clearInterval_();
2797
- setIsPaused(true);
2880
+ dispatch({ type: "SET_REPLAY", replay: { isPaused: true } });
2798
2881
  return;
2799
2882
  }
2800
2883
  const bar = data[idx];
2801
2884
  state.chart.updateData?.(bar);
2802
- currentIndexRef.current = idx + 1;
2803
- setBarIndex(idx + 1);
2804
- }, [state.chart, clearInterval_]);
2885
+ replayIndexRef.current = idx + 1;
2886
+ dispatch({ type: "SET_REPLAY", replay: { barIndex: idx + 1 } });
2887
+ }, [state.chart, clearInterval_, replaySavedDataRef, replayIndexRef, dispatch]);
2805
2888
  const startInterval = useCallback(
2806
2889
  (currentSpeed) => {
2807
2890
  clearInterval_();
2808
- intervalRef.current = setInterval(addNextBar, 1e3 / currentSpeed);
2891
+ replayIntervalRef.current = setInterval(addNextBar, 1e3 / currentSpeed);
2809
2892
  },
2810
- [addNextBar, clearInterval_]
2893
+ [addNextBar, clearInterval_, replayIntervalRef]
2811
2894
  );
2812
2895
  const startReplay = useCallback(() => {
2813
2896
  if (!state.chart) return;
2814
2897
  const dataList = state.chart.getDataList();
2815
2898
  if (!dataList || dataList.length === 0) return;
2816
- savedDataRef.current = [...dataList];
2817
- currentIndexRef.current = 0;
2818
- setTotalBars(dataList.length);
2819
- setBarIndex(0);
2820
- setIsReplaying(true);
2821
- setIsPaused(false);
2899
+ replaySavedDataRef.current = [...dataList];
2900
+ replayIndexRef.current = 0;
2901
+ dispatch({
2902
+ type: "SET_REPLAY",
2903
+ replay: {
2904
+ totalBars: dataList.length,
2905
+ barIndex: 0,
2906
+ isReplaying: true,
2907
+ isPaused: false
2908
+ }
2909
+ });
2822
2910
  state.chart.clearData?.();
2823
2911
  startInterval(speed);
2824
- }, [state.chart, speed, startInterval]);
2912
+ }, [state.chart, speed, startInterval, replaySavedDataRef, replayIndexRef, dispatch]);
2825
2913
  const stopReplay = useCallback(() => {
2826
2914
  if (!state.chart) return;
2827
2915
  clearInterval_();
2828
- const data = savedDataRef.current;
2916
+ const data = replaySavedDataRef.current;
2829
2917
  if (data.length > 0) {
2830
2918
  state.chart.clearData?.();
2831
2919
  for (const bar of data) {
2832
2920
  state.chart.updateData?.(bar);
2833
2921
  }
2834
2922
  }
2835
- savedDataRef.current = [];
2836
- currentIndexRef.current = 0;
2837
- setIsReplaying(false);
2838
- setIsPaused(false);
2839
- setBarIndex(0);
2840
- setTotalBars(0);
2841
- }, [state.chart, clearInterval_]);
2923
+ replaySavedDataRef.current = [];
2924
+ replayIndexRef.current = 0;
2925
+ dispatch({
2926
+ type: "SET_REPLAY",
2927
+ replay: { isReplaying: false, isPaused: false, barIndex: 0, totalBars: 0 }
2928
+ });
2929
+ }, [state.chart, clearInterval_, replaySavedDataRef, replayIndexRef, dispatch]);
2842
2930
  const togglePause = useCallback(() => {
2843
2931
  if (!isReplaying) return;
2844
2932
  if (isPaused) {
2845
2933
  startInterval(speed);
2846
- setIsPaused(false);
2934
+ dispatch({ type: "SET_REPLAY", replay: { isPaused: false } });
2847
2935
  } else {
2848
2936
  clearInterval_();
2849
- setIsPaused(true);
2937
+ dispatch({ type: "SET_REPLAY", replay: { isPaused: true } });
2850
2938
  }
2851
- }, [isReplaying, isPaused, speed, startInterval, clearInterval_]);
2939
+ }, [isReplaying, isPaused, speed, startInterval, clearInterval_, dispatch]);
2852
2940
  const stepForward = useCallback(() => {
2853
2941
  if (!isReplaying || !isPaused) return;
2854
2942
  addNextBar();
2855
2943
  }, [isReplaying, isPaused, addNextBar]);
2856
2944
  const stepBackward = useCallback(() => {
2857
2945
  if (!isReplaying || !isPaused || !state.chart) return;
2858
- const idx = currentIndexRef.current;
2946
+ const idx = replayIndexRef.current;
2859
2947
  if (idx <= 1) return;
2860
- const data = savedDataRef.current;
2948
+ const data = replaySavedDataRef.current;
2861
2949
  state.chart.clearData?.();
2862
2950
  for (let i = 0; i < idx - 1; i++) {
2863
2951
  state.chart.updateData?.(data[i]);
2864
2952
  }
2865
- currentIndexRef.current = idx - 1;
2866
- setBarIndex(idx - 1);
2867
- }, [isReplaying, isPaused, state.chart]);
2953
+ replayIndexRef.current = idx - 1;
2954
+ dispatch({ type: "SET_REPLAY", replay: { barIndex: idx - 1 } });
2955
+ }, [isReplaying, isPaused, state.chart, replaySavedDataRef, replayIndexRef, dispatch]);
2868
2956
  const seekTo = useCallback(
2869
2957
  (targetIndex) => {
2870
2958
  if (!isReplaying || !state.chart) return;
2871
- const data = savedDataRef.current;
2959
+ const data = replaySavedDataRef.current;
2872
2960
  const clamped = Math.max(0, Math.min(targetIndex, data.length));
2873
2961
  clearInterval_();
2874
- setIsPaused(true);
2875
2962
  state.chart.clearData?.();
2876
2963
  for (let i = 0; i < clamped; i++) {
2877
2964
  state.chart.updateData?.(data[i]);
2878
2965
  }
2879
- currentIndexRef.current = clamped;
2880
- setBarIndex(clamped);
2966
+ replayIndexRef.current = clamped;
2967
+ dispatch({
2968
+ type: "SET_REPLAY",
2969
+ replay: { isPaused: true, barIndex: clamped }
2970
+ });
2881
2971
  },
2882
- [isReplaying, state.chart, clearInterval_]
2972
+ [isReplaying, state.chart, clearInterval_, replaySavedDataRef, replayIndexRef, dispatch]
2883
2973
  );
2884
2974
  const setSpeed = useCallback(
2885
2975
  (newSpeed) => {
2886
- setSpeedState(newSpeed);
2976
+ dispatch({ type: "SET_REPLAY", replay: { speed: newSpeed } });
2887
2977
  if (isReplaying && !isPaused) {
2888
2978
  startInterval(newSpeed);
2889
2979
  }
2890
2980
  },
2891
- [isReplaying, isPaused, startInterval]
2981
+ [isReplaying, isPaused, startInterval, dispatch]
2892
2982
  );
2893
- useEffect(() => {
2894
- return () => {
2895
- clearInterval_();
2896
- };
2897
- }, [clearInterval_]);
2898
2983
  return {
2899
2984
  isReplaying,
2900
2985
  isPaused,
@@ -3051,31 +3136,25 @@ function useCompare() {
3051
3136
  return { symbols, addSymbol, removeSymbol, toggleSymbol, clearAll };
3052
3137
  }
3053
3138
  var MEASURE_OVERLAY_ID = "__measure_line__";
3139
+ function computeResult(from, to) {
3140
+ const priceDiff = to.price - from.price;
3141
+ const pricePercent = from.price !== 0 ? priceDiff / from.price * 100 : 0;
3142
+ const bars = Math.abs(to.barIndex - from.barIndex);
3143
+ const timeDiff = Math.abs(to.timestamp - from.timestamp);
3144
+ return { from, to, priceDiff, pricePercent, bars, timeDiff };
3145
+ }
3054
3146
  function useMeasure() {
3055
- const { state } = useKlinechartsUI();
3056
- const [isActive, setIsActive] = useState(false);
3057
- const [fromPoint, setFromPoint] = useState(null);
3058
- const [result, setResult] = useState(null);
3059
- const clickCountRef = useRef(0);
3060
- const computeResult = useCallback(
3061
- (from, to) => {
3062
- const priceDiff = to.price - from.price;
3063
- const pricePercent = from.price !== 0 ? priceDiff / from.price * 100 : 0;
3064
- const bars = Math.abs(to.barIndex - from.barIndex);
3065
- const timeDiff = Math.abs(to.timestamp - from.timestamp);
3066
- return { from, to, priceDiff, pricePercent, bars, timeDiff };
3067
- },
3068
- []
3069
- );
3147
+ const { state, dispatch } = useKlinechartsUI();
3148
+ const { isActive, fromPoint, result } = state.measure;
3070
3149
  const cleanup = useCallback(() => {
3071
3150
  state.chart?.removeOverlay({ id: MEASURE_OVERLAY_ID });
3072
3151
  }, [state.chart]);
3073
3152
  const startMeasure = useCallback(() => {
3074
3153
  cleanup();
3075
- setIsActive(true);
3076
- setFromPoint(null);
3077
- setResult(null);
3078
- clickCountRef.current = 0;
3154
+ dispatch({
3155
+ type: "SET_MEASURE",
3156
+ measure: { isActive: true, fromPoint: null, result: null }
3157
+ });
3079
3158
  if (!state.chart) return;
3080
3159
  state.chart.createOverlay({
3081
3160
  name: "segment",
@@ -3111,23 +3190,25 @@ function useMeasure() {
3111
3190
  };
3112
3191
  const from = makePoint(points[0]);
3113
3192
  const to = makePoint(points[1]);
3114
- setFromPoint(from);
3115
- setResult(computeResult(from, to));
3116
- setIsActive(false);
3193
+ dispatch({
3194
+ type: "SET_MEASURE",
3195
+ measure: {
3196
+ isActive: false,
3197
+ fromPoint: from,
3198
+ result: computeResult(from, to)
3199
+ }
3200
+ });
3117
3201
  }
3118
3202
  });
3119
- }, [state.chart, cleanup, computeResult]);
3203
+ }, [state.chart, cleanup, dispatch]);
3120
3204
  const cancelMeasure = useCallback(() => {
3121
3205
  cleanup();
3122
- setIsActive(false);
3123
- setFromPoint(null);
3124
- clickCountRef.current = 0;
3125
- }, [cleanup]);
3206
+ dispatch({ type: "SET_MEASURE", measure: { isActive: false, fromPoint: null } });
3207
+ }, [cleanup, dispatch]);
3126
3208
  const clearResult = useCallback(() => {
3127
3209
  cleanup();
3128
- setResult(null);
3129
- setFromPoint(null);
3130
- }, [cleanup]);
3210
+ dispatch({ type: "SET_MEASURE", measure: { result: null, fromPoint: null } });
3211
+ }, [cleanup, dispatch]);
3131
3212
  useEffect(() => {
3132
3213
  return () => {
3133
3214
  state.chart?.removeOverlay({ id: MEASURE_OVERLAY_ID });