react-klinecharts-ui 0.3.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';
@@ -57,6 +57,16 @@ function reducer(state, action) {
57
57
  return { ...state, mainIndicators: action.indicators };
58
58
  case "SET_SUB_INDICATORS":
59
59
  return { ...state, subIndicators: action.indicators };
60
+ case "SET_INDICATOR_AXES":
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 } };
60
70
  case "SET_STYLES":
61
71
  return { ...state, styles: action.styles };
62
72
  case "SET_LOCALE":
@@ -119,12 +129,28 @@ function KlinechartsUIProvider({
119
129
  (acc, name) => ({ ...acc, [name]: "" }),
120
130
  {}
121
131
  ),
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
+ },
122
143
  styles: opts.styles,
123
144
  screenshotUrl: null
124
145
  })
125
146
  );
126
147
  const fullscreenContainerRef = useRef(null);
127
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);
128
154
  const stateRef = useRef(state);
129
155
  stateRef.current = state;
130
156
  const callbacksRef = useRef({
@@ -185,13 +211,58 @@ function KlinechartsUIProvider({
185
211
  state.chart.setStyles(state.styles);
186
212
  }
187
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
+ }, []);
188
255
  const dispatchValue = useMemo(
189
256
  () => ({
190
257
  dispatch: enhancedDispatch,
191
258
  datafeed,
192
259
  onSettingsChange,
193
260
  fullscreenContainerRef,
194
- undoRedoListenerRef
261
+ undoRedoListenerRef,
262
+ alertTriggeredListenerRef,
263
+ replayIntervalRef,
264
+ replaySavedDataRef,
265
+ replayIndexRef
195
266
  }),
196
267
  [enhancedDispatch, datafeed, onSettingsChange]
197
268
  );
@@ -653,75 +724,140 @@ function useIndicators() {
653
724
  const activeNames = state.mainIndicators;
654
725
  const inactive = MAIN_INDICATORS.filter((n) => !activeNames.includes(n));
655
726
  return [
656
- ...activeNames.map((name) => ({ name, isActive: true })),
657
- ...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 }))
658
733
  ];
659
- }, [state.mainIndicators]);
734
+ }, [state.mainIndicators, state.indicatorVisibility]);
660
735
  const subIndicators = useMemo(() => {
661
736
  const activeNames = Object.keys(state.subIndicators);
662
737
  const inactive = SUB_INDICATORS.filter((n) => !activeNames.includes(n));
663
738
  return [
664
- ...activeNames.map((name) => ({ name, isActive: true })),
665
- ...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 }))
666
745
  ];
667
- }, [state.subIndicators]);
746
+ }, [state.subIndicators, state.indicatorVisibility]);
668
747
  const addMainIndicator = useCallback(
669
- (name) => {
748
+ (name, options) => {
670
749
  if (state.mainIndicators.includes(name)) return;
750
+ const yAxis = options?.yAxis;
671
751
  state.chart?.createIndicator(
672
- { name, id: `main_${name}`, paneId: "candle_pane" },
673
- true,
674
- { id: "candle_pane" }
752
+ { name, id: `main_${name}` },
753
+ { isStack: true, pane: { id: "candle_pane" }, yAxis }
675
754
  );
676
755
  const newIndicators = [...state.mainIndicators, name];
677
756
  dispatch({ type: "SET_MAIN_INDICATORS", indicators: newIndicators });
757
+ if (yAxis?.id) {
758
+ dispatch({
759
+ type: "SET_INDICATOR_AXES",
760
+ axes: { ...state.indicatorAxes, [`main_${name}`]: yAxis.id }
761
+ });
762
+ }
678
763
  undoRedoListenerRef.current?.({
679
764
  type: "indicator_toggled",
680
- data: { name, wasActive: false, isMain: true, paneId: "candle_pane" }
765
+ data: {
766
+ name,
767
+ wasActive: false,
768
+ isMain: true,
769
+ paneId: "candle_pane",
770
+ yAxisId: yAxis?.id
771
+ }
681
772
  });
682
773
  },
683
- [state.chart, state.mainIndicators, dispatch, undoRedoListenerRef]
774
+ [state.chart, state.mainIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
684
775
  );
685
776
  const removeMainIndicator = useCallback(
686
777
  (name) => {
687
- state.chart?.removeIndicator({ id: `main_${name}` });
778
+ const id = `main_${name}`;
779
+ const yAxisId = state.indicatorAxes[id];
780
+ state.chart?.removeIndicator({ id });
688
781
  const newIndicators = state.mainIndicators.filter((n) => n !== name);
689
782
  dispatch({ type: "SET_MAIN_INDICATORS", indicators: newIndicators });
783
+ if (yAxisId) {
784
+ const nextAxes = { ...state.indicatorAxes };
785
+ delete nextAxes[id];
786
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
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
+ }
690
793
  undoRedoListenerRef.current?.({
691
794
  type: "indicator_toggled",
692
- data: { name, wasActive: true, isMain: true, paneId: "candle_pane" }
795
+ data: { name, wasActive: true, isMain: true, paneId: "candle_pane", yAxisId }
693
796
  });
694
797
  },
695
- [state.chart, state.mainIndicators, dispatch, undoRedoListenerRef]
798
+ [
799
+ state.chart,
800
+ state.mainIndicators,
801
+ state.indicatorAxes,
802
+ state.indicatorVisibility,
803
+ dispatch,
804
+ undoRedoListenerRef
805
+ ]
696
806
  );
697
807
  const addSubIndicator = useCallback(
698
- (name) => {
808
+ (name, options) => {
699
809
  if (name in state.subIndicators) return;
700
810
  const id = `sub_${name}`;
701
- state.chart?.createIndicator({ name, id });
811
+ const yAxis = options?.yAxis;
812
+ state.chart?.createIndicator({ name, id }, { yAxis });
702
813
  const paneId = state.chart?.getIndicators({ id })?.[0]?.paneId ?? "";
703
814
  const newIndicators = { ...state.subIndicators, [name]: paneId };
704
815
  dispatch({ type: "SET_SUB_INDICATORS", indicators: newIndicators });
816
+ if (yAxis?.id) {
817
+ dispatch({
818
+ type: "SET_INDICATOR_AXES",
819
+ axes: { ...state.indicatorAxes, [id]: yAxis.id }
820
+ });
821
+ }
705
822
  undoRedoListenerRef.current?.({
706
823
  type: "indicator_toggled",
707
- data: { name, wasActive: false, isMain: false, paneId }
824
+ data: { name, wasActive: false, isMain: false, paneId, yAxisId: yAxis?.id }
708
825
  });
709
826
  },
710
- [state.chart, state.subIndicators, dispatch, undoRedoListenerRef]
827
+ [state.chart, state.subIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
711
828
  );
712
829
  const removeSubIndicator = useCallback(
713
830
  (name) => {
831
+ const id = `sub_${name}`;
714
832
  const paneId = state.subIndicators[name] ?? "";
715
- state.chart?.removeIndicator({ id: `sub_${name}` });
833
+ const yAxisId = state.indicatorAxes[id];
834
+ state.chart?.removeIndicator({ id });
716
835
  const newIndicators = { ...state.subIndicators };
717
836
  delete newIndicators[name];
718
837
  dispatch({ type: "SET_SUB_INDICATORS", indicators: newIndicators });
838
+ if (yAxisId) {
839
+ const nextAxes = { ...state.indicatorAxes };
840
+ delete nextAxes[id];
841
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
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
+ }
719
848
  undoRedoListenerRef.current?.({
720
849
  type: "indicator_toggled",
721
- data: { name, wasActive: true, isMain: false, paneId }
850
+ data: { name, wasActive: true, isMain: false, paneId, yAxisId }
722
851
  });
723
852
  },
724
- [state.chart, state.subIndicators, dispatch, undoRedoListenerRef]
853
+ [
854
+ state.chart,
855
+ state.subIndicators,
856
+ state.indicatorAxes,
857
+ state.indicatorVisibility,
858
+ dispatch,
859
+ undoRedoListenerRef
860
+ ]
725
861
  );
726
862
  const toggleMainIndicator = useCallback(
727
863
  (name) => {
@@ -747,12 +883,22 @@ function useIndicators() {
747
883
  (name, isMain, visible) => {
748
884
  const id = isMain ? `main_${name}` : `sub_${name}`;
749
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 });
750
890
  },
751
- [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]
752
896
  );
753
897
  const updateIndicatorParams = useCallback(
754
- (name, paneId, params) => {
755
- state.chart?.overrideIndicator({ name, paneId, calcParams: params });
898
+ // `paneId` is kept in the signature for API stability; klinecharts v10
899
+ // `overrideIndicator` no longer accepts it and targets by name/id instead.
900
+ (name, _paneId, params) => {
901
+ state.chart?.overrideIndicator({ name, calcParams: params });
756
902
  },
757
903
  [state.chart]
758
904
  );
@@ -773,11 +919,9 @@ function useIndicators() {
773
919
  {
774
920
  name,
775
921
  id: `main_${name}`,
776
- paneId: "candle_pane",
777
922
  ...prevCalcParams ? { calcParams: prevCalcParams } : {}
778
923
  },
779
- true,
780
- { id: "candle_pane" }
924
+ { isStack: true, pane: { id: "candle_pane" } }
781
925
  );
782
926
  const newSub = { ...state.subIndicators };
783
927
  delete newSub[name];
@@ -831,9 +975,14 @@ function useIndicators() {
831
975
  id: paneId,
832
976
  height: COLLAPSED_HEIGHT
833
977
  });
834
- 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
+ });
835
984
  },
836
- [state.chart, state.subIndicators]
985
+ [state.chart, state.subIndicators, state.indicatorVisibility, dispatch]
837
986
  );
838
987
  const expandSubIndicator = useCallback(
839
988
  (name) => {
@@ -845,9 +994,13 @@ function useIndicators() {
845
994
  id: paneId,
846
995
  height: savedHeight
847
996
  });
848
- 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 });
849
1002
  },
850
- [state.chart, state.subIndicators]
1003
+ [state.chart, state.subIndicators, state.indicatorVisibility, dispatch]
851
1004
  );
852
1005
  const isSubIndicatorCollapsed = useCallback(
853
1006
  (name) => {
@@ -893,7 +1046,7 @@ function useIndicators() {
893
1046
  if (sub.styles) {
894
1047
  state.chart.overrideIndicator({
895
1048
  name: sub.name,
896
- paneId,
1049
+ id,
897
1050
  styles: sub.styles
898
1051
  });
899
1052
  }
@@ -902,6 +1055,44 @@ function useIndicators() {
902
1055
  },
903
1056
  [state.chart, state.subIndicators, dispatch]
904
1057
  );
1058
+ const getIndicatorAxis = useCallback(
1059
+ (name, isMain) => state.indicatorAxes[isMain ? `main_${name}` : `sub_${name}`],
1060
+ [state.indicatorAxes]
1061
+ );
1062
+ const bindIndicatorToNewAxis = useCallback(
1063
+ (name, isMain, yAxis) => {
1064
+ if (!state.chart) return;
1065
+ const id = isMain ? `main_${name}` : `sub_${name}`;
1066
+ const current = state.chart.getIndicators({ id })?.[0];
1067
+ if (!current) return;
1068
+ const calcParams = current.calcParams;
1069
+ const styles = current.styles;
1070
+ const visible = current.visible ?? true;
1071
+ const paneId = isMain ? "candle_pane" : current.paneId ?? state.subIndicators[name] ?? "";
1072
+ state.chart.removeIndicator({ id });
1073
+ state.chart.createIndicator(
1074
+ {
1075
+ name,
1076
+ id,
1077
+ ...calcParams ? { calcParams } : {},
1078
+ visible
1079
+ },
1080
+ {
1081
+ isStack: isMain,
1082
+ ...paneId ? { pane: { id: paneId } } : {},
1083
+ yAxis
1084
+ }
1085
+ );
1086
+ if (styles) {
1087
+ state.chart.overrideIndicator({ name, id, styles });
1088
+ }
1089
+ const nextAxes = { ...state.indicatorAxes };
1090
+ if (yAxis?.id) nextAxes[id] = yAxis.id;
1091
+ else delete nextAxes[id];
1092
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
1093
+ },
1094
+ [state.chart, state.indicatorAxes, state.subIndicators, dispatch]
1095
+ );
905
1096
  return {
906
1097
  mainIndicators,
907
1098
  subIndicators,
@@ -918,6 +1109,7 @@ function useIndicators() {
918
1109
  moveToMain,
919
1110
  moveToSub,
920
1111
  setIndicatorVisible,
1112
+ isIndicatorVisible,
921
1113
  updateIndicatorParams,
922
1114
  getIndicatorParams,
923
1115
  isMainIndicatorActive,
@@ -925,7 +1117,11 @@ function useIndicators() {
925
1117
  collapseSubIndicator,
926
1118
  expandSubIndicator,
927
1119
  isSubIndicatorCollapsed,
928
- reorderSubIndicator
1120
+ reorderSubIndicator,
1121
+ indicatorAxes: state.indicatorAxes,
1122
+ getIndicatorAxis,
1123
+ indicatorVisibility: state.indicatorVisibility,
1124
+ bindIndicatorToNewAxis
929
1125
  };
930
1126
  }
931
1127
 
@@ -1235,20 +1431,23 @@ function useKlinechartsUISettings() {
1235
1431
  () => TOOLTIP_SHOW_RULES.map((r) => ({ key: r, localeKey: r })),
1236
1432
  []
1237
1433
  );
1434
+ const applyPaneAxis = useCallback(
1435
+ (axis) => {
1436
+ state.chart?.overrideYAxis?.({ paneId: "candle_pane", ...axis });
1437
+ },
1438
+ [state.chart]
1439
+ );
1238
1440
  const hasAppliedInitial = useRef(false);
1239
1441
  useEffect(() => {
1240
1442
  if (!state.chart || hasAppliedInitial.current) return;
1241
1443
  hasAppliedInitial.current = true;
1242
- const needsPaneOptions = settings.reverseCoordinate || settings.priceAxisType !== "normal" || settings.yAxisPosition !== "right" || settings.yAxisInside;
1243
- if (needsPaneOptions) {
1244
- state.chart.setPaneOptions({
1245
- id: "candle_pane",
1246
- axis: {
1247
- ...settings.priceAxisType !== "normal" && { name: settings.priceAxisType },
1248
- ...settings.reverseCoordinate && { reverse: true },
1249
- ...settings.yAxisPosition !== "right" && { position: settings.yAxisPosition },
1250
- ...settings.yAxisInside && { inside: true }
1251
- }
1444
+ const needsAxisOverride = settings.reverseCoordinate || settings.priceAxisType !== "normal" || settings.yAxisPosition !== "right" || settings.yAxisInside;
1445
+ if (needsAxisOverride) {
1446
+ applyPaneAxis({
1447
+ ...settings.priceAxisType !== "normal" && { name: settings.priceAxisType },
1448
+ ...settings.reverseCoordinate && { reverse: true },
1449
+ ...settings.yAxisPosition !== "right" && { position: settings.yAxisPosition },
1450
+ ...settings.yAxisInside && { inside: true }
1252
1451
  });
1253
1452
  }
1254
1453
  if (settings.showIndicatorLastValue) {
@@ -1271,12 +1470,6 @@ function useKlinechartsUISettings() {
1271
1470
  },
1272
1471
  [state.chart]
1273
1472
  );
1274
- const applyPaneAxis = useCallback(
1275
- (axis) => {
1276
- state.chart?.setPaneOptions({ id: "candle_pane", axis });
1277
- },
1278
- [state.chart]
1279
- );
1280
1473
  const setCandleType = useCallback(
1281
1474
  (type) => {
1282
1475
  applyStyle("candle.type", type);
@@ -1424,11 +1617,8 @@ function useKlinechartsUISettings() {
1424
1617
  const resetToDefaults = useCallback(() => {
1425
1618
  setSettings(defaultSettings);
1426
1619
  state.chart?.setStyles(state.theme);
1427
- state.chart?.setPaneOptions({
1428
- id: "candle_pane",
1429
- axis: { name: "normal", reverse: false, position: "right", inside: false }
1430
- });
1431
- }, [state.chart, state.theme]);
1620
+ applyPaneAxis({ name: "normal", reverse: false, position: "right", inside: false });
1621
+ }, [state.chart, state.theme, applyPaneAxis]);
1432
1622
  return {
1433
1623
  ...settings,
1434
1624
  candleTypes,
@@ -1685,21 +1875,28 @@ function useUndoRedo() {
1685
1875
  break;
1686
1876
  }
1687
1877
  case "indicator_toggled": {
1688
- const { name, wasActive, isMain, paneId } = action.data;
1878
+ const { name, wasActive, isMain, paneId, yAxisId } = action.data;
1879
+ const id = isMain ? `main_${name}` : `sub_${name}`;
1689
1880
  if (wasActive) {
1690
1881
  if (isMain) {
1691
1882
  state.chart.createIndicator(
1692
- { name, id: `main_${name}`, paneId: "candle_pane" },
1693
- true,
1694
- { id: "candle_pane" }
1883
+ { name, id },
1884
+ {
1885
+ isStack: true,
1886
+ pane: { id: "candle_pane" },
1887
+ ...yAxisId ? { yAxis: { id: yAxisId } } : {}
1888
+ }
1695
1889
  );
1696
1890
  dispatch({
1697
1891
  type: "SET_MAIN_INDICATORS",
1698
1892
  indicators: [...state.mainIndicators, name]
1699
1893
  });
1700
1894
  } else {
1701
- state.chart.createIndicator({ name, id: `sub_${name}` });
1702
- const newPaneId = state.chart.getIndicators({ id: `sub_${name}` })?.[0]?.paneId ?? "";
1895
+ state.chart.createIndicator(
1896
+ { name, id },
1897
+ yAxisId ? { yAxis: { id: yAxisId } } : void 0
1898
+ );
1899
+ const newPaneId = state.chart.getIndicators({ id })?.[0]?.paneId ?? "";
1703
1900
  dispatch({
1704
1901
  type: "SET_SUB_INDICATORS",
1705
1902
  indicators: {
@@ -1708,9 +1905,15 @@ function useUndoRedo() {
1708
1905
  }
1709
1906
  });
1710
1907
  }
1908
+ if (yAxisId) {
1909
+ dispatch({
1910
+ type: "SET_INDICATOR_AXES",
1911
+ axes: { ...state.indicatorAxes, [id]: yAxisId }
1912
+ });
1913
+ }
1711
1914
  } else {
1915
+ state.chart.removeIndicator({ id });
1712
1916
  if (isMain) {
1713
- state.chart.removeIndicator({ id: `main_${name}` });
1714
1917
  dispatch({
1715
1918
  type: "SET_MAIN_INDICATORS",
1716
1919
  indicators: state.mainIndicators.filter(
@@ -1718,7 +1921,6 @@ function useUndoRedo() {
1718
1921
  )
1719
1922
  });
1720
1923
  } else {
1721
- state.chart.removeIndicator({ id: `sub_${name}` });
1722
1924
  const newSub = { ...state.subIndicators };
1723
1925
  delete newSub[name];
1724
1926
  dispatch({
@@ -1726,6 +1928,11 @@ function useUndoRedo() {
1726
1928
  indicators: newSub
1727
1929
  });
1728
1930
  }
1931
+ if (state.indicatorAxes[id]) {
1932
+ const nextAxes = { ...state.indicatorAxes };
1933
+ delete nextAxes[id];
1934
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
1935
+ }
1729
1936
  }
1730
1937
  setRedoStack((prev) => [
1731
1938
  ...prev,
@@ -1735,7 +1942,8 @@ function useUndoRedo() {
1735
1942
  name,
1736
1943
  wasActive: !wasActive,
1737
1944
  isMain,
1738
- paneId
1945
+ paneId,
1946
+ yAxisId
1739
1947
  }
1740
1948
  }
1741
1949
  ]);
@@ -1745,7 +1953,7 @@ function useUndoRedo() {
1745
1953
  } finally {
1746
1954
  isProcessingRef.current = false;
1747
1955
  }
1748
- }, [undoStack, state.chart, state.mainIndicators, state.subIndicators, dispatch]);
1956
+ }, [undoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
1749
1957
  const redo = useCallback(() => {
1750
1958
  if (redoStack.length === 0 || !state.chart || isProcessingRef.current)
1751
1959
  return;
@@ -1797,21 +2005,28 @@ function useUndoRedo() {
1797
2005
  break;
1798
2006
  }
1799
2007
  case "indicator_toggled": {
1800
- const { name, wasActive, isMain } = action.data;
2008
+ const { name, wasActive, isMain, paneId, yAxisId } = action.data;
2009
+ const id = isMain ? `main_${name}` : `sub_${name}`;
1801
2010
  if (wasActive) {
1802
2011
  if (isMain) {
1803
2012
  state.chart.createIndicator(
1804
- { name, id: `main_${name}`, paneId: "candle_pane" },
1805
- true,
1806
- { id: "candle_pane" }
2013
+ { name, id },
2014
+ {
2015
+ isStack: true,
2016
+ pane: { id: "candle_pane" },
2017
+ ...yAxisId ? { yAxis: { id: yAxisId } } : {}
2018
+ }
1807
2019
  );
1808
2020
  dispatch({
1809
2021
  type: "SET_MAIN_INDICATORS",
1810
2022
  indicators: [...state.mainIndicators, name]
1811
2023
  });
1812
2024
  } else {
1813
- state.chart.createIndicator({ name, id: `sub_${name}` });
1814
- const newPaneId = state.chart.getIndicators({ id: `sub_${name}` })?.[0]?.paneId ?? "";
2025
+ state.chart.createIndicator(
2026
+ { name, id },
2027
+ yAxisId ? { yAxis: { id: yAxisId } } : void 0
2028
+ );
2029
+ const newPaneId = state.chart.getIndicators({ id })?.[0]?.paneId ?? "";
1815
2030
  dispatch({
1816
2031
  type: "SET_SUB_INDICATORS",
1817
2032
  indicators: {
@@ -1820,9 +2035,15 @@ function useUndoRedo() {
1820
2035
  }
1821
2036
  });
1822
2037
  }
2038
+ if (yAxisId) {
2039
+ dispatch({
2040
+ type: "SET_INDICATOR_AXES",
2041
+ axes: { ...state.indicatorAxes, [id]: yAxisId }
2042
+ });
2043
+ }
1823
2044
  } else {
2045
+ state.chart.removeIndicator({ id });
1824
2046
  if (isMain) {
1825
- state.chart.removeIndicator({ id: `main_${name}` });
1826
2047
  dispatch({
1827
2048
  type: "SET_MAIN_INDICATORS",
1828
2049
  indicators: state.mainIndicators.filter(
@@ -1830,7 +2051,6 @@ function useUndoRedo() {
1830
2051
  )
1831
2052
  });
1832
2053
  } else {
1833
- state.chart.removeIndicator({ id: `sub_${name}` });
1834
2054
  const newSub = { ...state.subIndicators };
1835
2055
  delete newSub[name];
1836
2056
  dispatch({
@@ -1838,6 +2058,11 @@ function useUndoRedo() {
1838
2058
  indicators: newSub
1839
2059
  });
1840
2060
  }
2061
+ if (state.indicatorAxes[id]) {
2062
+ const nextAxes = { ...state.indicatorAxes };
2063
+ delete nextAxes[id];
2064
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
2065
+ }
1841
2066
  }
1842
2067
  setUndoStack((prev) => [
1843
2068
  ...prev,
@@ -1846,7 +2071,9 @@ function useUndoRedo() {
1846
2071
  data: {
1847
2072
  name,
1848
2073
  wasActive: !wasActive,
1849
- isMain
2074
+ isMain,
2075
+ paneId,
2076
+ yAxisId
1850
2077
  }
1851
2078
  }
1852
2079
  ]);
@@ -1856,7 +2083,7 @@ function useUndoRedo() {
1856
2083
  } finally {
1857
2084
  isProcessingRef.current = false;
1858
2085
  }
1859
- }, [redoStack, state.chart, state.mainIndicators, state.subIndicators, dispatch]);
2086
+ }, [redoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
1860
2087
  useEffect(() => {
1861
2088
  const handleKeyDown = (e) => {
1862
2089
  const isCtrlOrMeta = e.ctrlKey || e.metaKey;
@@ -1930,12 +2157,14 @@ function useLayoutManager() {
1930
2157
  if (indicatorMap) {
1931
2158
  indicatorMap.forEach((paneIndicators, paneId) => {
1932
2159
  paneIndicators.forEach((indicator) => {
2160
+ const yAxisId = state.indicatorAxes[indicator.id];
1933
2161
  indicators2.push({
1934
2162
  paneId,
1935
2163
  name: indicator.name,
1936
2164
  calcParams: indicator.calcParams,
1937
2165
  visible: indicator.visible,
1938
- ...indicator.styles ? { styles: indicator.styles } : {}
2166
+ ...indicator.styles ? { styles: indicator.styles } : {},
2167
+ ...yAxisId ? { yAxisId } : {}
1939
2168
  });
1940
2169
  });
1941
2170
  });
@@ -1963,7 +2192,7 @@ function useLayoutManager() {
1963
2192
  indicators: indicators2,
1964
2193
  drawings
1965
2194
  };
1966
- }, [state.chart, state.symbol, state.period]);
2195
+ }, [state.chart, state.symbol, state.period, state.indicatorAxes]);
1967
2196
  const saveLayout = useCallback(
1968
2197
  (name) => {
1969
2198
  const chartState = serializeState();
@@ -2011,25 +2240,39 @@ function useLayoutManager() {
2011
2240
  }
2012
2241
  const newMainIndicators = [];
2013
2242
  const newSubIndicators = {};
2243
+ const restoredAxes = {};
2244
+ const restoredVisibility = {};
2014
2245
  if (chartState.indicators) {
2015
2246
  for (const ind of chartState.indicators) {
2247
+ const isMain = ind.paneId === "candle_pane";
2248
+ const id2 = isMain ? `main_${ind.name}` : `sub_${ind.name}`;
2016
2249
  chart.createIndicator(
2017
2250
  {
2018
2251
  name: ind.name,
2252
+ id: id2,
2019
2253
  calcParams: ind.calcParams,
2020
2254
  visible: ind.visible
2021
2255
  },
2022
- ind.paneId !== "candle_pane",
2023
- { id: ind.paneId }
2256
+ {
2257
+ isStack: ind.paneId !== "candle_pane",
2258
+ pane: { id: ind.paneId },
2259
+ ...ind.yAxisId ? { yAxis: { id: ind.yAxisId } } : {}
2260
+ }
2024
2261
  );
2025
2262
  if (ind.styles) {
2026
2263
  chart.overrideIndicator({
2027
2264
  name: ind.name,
2028
- paneId: ind.paneId,
2265
+ id: id2,
2029
2266
  styles: ind.styles
2030
2267
  });
2031
2268
  }
2032
- if (ind.paneId === "candle_pane") {
2269
+ if (ind.yAxisId) {
2270
+ restoredAxes[id2] = ind.yAxisId;
2271
+ }
2272
+ if (ind.visible === false) {
2273
+ restoredVisibility[id2] = false;
2274
+ }
2275
+ if (isMain) {
2033
2276
  newMainIndicators.push(ind.name);
2034
2277
  } else {
2035
2278
  newSubIndicators[ind.name] = ind.paneId;
@@ -2044,6 +2287,14 @@ function useLayoutManager() {
2044
2287
  type: "SET_SUB_INDICATORS",
2045
2288
  indicators: newSubIndicators
2046
2289
  });
2290
+ dispatch({
2291
+ type: "SET_INDICATOR_AXES",
2292
+ axes: restoredAxes
2293
+ });
2294
+ dispatch({
2295
+ type: "SET_INDICATOR_VISIBILITY",
2296
+ visibility: restoredVisibility
2297
+ });
2047
2298
  if (chartState.drawings) {
2048
2299
  for (const drawing of chartState.drawings) {
2049
2300
  chart.createOverlay({
@@ -2269,8 +2520,7 @@ ${code}`
2269
2520
  if (placement === "main") {
2270
2521
  paneId = chart.createIndicator(
2271
2522
  { name: indicatorName },
2272
- true,
2273
- { id: "candle_pane" }
2523
+ { isStack: true, pane: { id: "candle_pane" } }
2274
2524
  );
2275
2525
  } else {
2276
2526
  paneId = chart.createIndicator({ name: indicatorName });
@@ -2411,10 +2661,9 @@ function useCrosshair() {
2411
2661
  }
2412
2662
  var alertCounter = 0;
2413
2663
  function useAlerts() {
2414
- const { state } = useKlinechartsUI();
2415
- const [alerts, setAlerts] = useState([]);
2416
- const callbackRef = useRef(null);
2417
- const prevCloseRef = useRef(null);
2664
+ const { state, dispatch } = useKlinechartsUI();
2665
+ const { alertTriggeredListenerRef } = useKlinechartsUIDispatch();
2666
+ const alerts = state.alerts;
2418
2667
  const addAlert = useCallback(
2419
2668
  (price, condition, message) => {
2420
2669
  const id = `alert_${++alertCounter}`;
@@ -2425,7 +2674,7 @@ function useAlerts() {
2425
2674
  message,
2426
2675
  triggered: false
2427
2676
  };
2428
- setAlerts((prev) => [...prev, alert]);
2677
+ dispatch({ type: "SET_ALERTS", alerts: [...state.alerts, alert] });
2429
2678
  if (state.chart) {
2430
2679
  state.chart.createOverlay({
2431
2680
  name: "horizontalStraightLine",
@@ -2444,67 +2693,30 @@ function useAlerts() {
2444
2693
  }
2445
2694
  return id;
2446
2695
  },
2447
- [state.chart]
2696
+ [state.chart, state.alerts, dispatch]
2448
2697
  );
2449
2698
  const removeAlert = useCallback(
2450
2699
  (id) => {
2451
- setAlerts((prev) => prev.filter((a) => a.id !== id));
2700
+ dispatch({
2701
+ type: "SET_ALERTS",
2702
+ alerts: state.alerts.filter((a) => a.id !== id)
2703
+ });
2452
2704
  state.chart?.removeOverlay({ id });
2453
2705
  },
2454
- [state.chart]
2706
+ [state.chart, state.alerts, dispatch]
2455
2707
  );
2456
2708
  const clearAlerts = useCallback(() => {
2457
- setAlerts((prev) => {
2458
- for (const alert of prev) {
2459
- state.chart?.removeOverlay({ id: alert.id });
2460
- }
2461
- return [];
2462
- });
2463
- }, [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]);
2464
2714
  const onAlertTriggered = useCallback(
2465
2715
  (callback) => {
2466
- callbackRef.current = callback;
2716
+ alertTriggeredListenerRef.current = callback;
2467
2717
  },
2468
- []
2718
+ [alertTriggeredListenerRef]
2469
2719
  );
2470
- useEffect(() => {
2471
- if (!state.chart) return;
2472
- const interval = setInterval(() => {
2473
- const dataList = state.chart?.getDataList();
2474
- if (!dataList || dataList.length === 0) return;
2475
- const lastBar = dataList[dataList.length - 1];
2476
- const currentClose = lastBar.close;
2477
- const prevClose = prevCloseRef.current;
2478
- if (prevClose === null) {
2479
- prevCloseRef.current = currentClose;
2480
- return;
2481
- }
2482
- prevCloseRef.current = currentClose;
2483
- setAlerts((prev) => {
2484
- let changed = false;
2485
- const next = prev.map((alert) => {
2486
- if (alert.triggered) return alert;
2487
- let shouldTrigger = false;
2488
- if (alert.condition === "crossing_up") {
2489
- shouldTrigger = prevClose < alert.price && currentClose >= alert.price;
2490
- } else if (alert.condition === "crossing_down") {
2491
- shouldTrigger = prevClose > alert.price && currentClose <= alert.price;
2492
- } else if (alert.condition === "crossing") {
2493
- shouldTrigger = prevClose < alert.price && currentClose >= alert.price || prevClose > alert.price && currentClose <= alert.price;
2494
- }
2495
- if (shouldTrigger) {
2496
- changed = true;
2497
- const triggered = { ...alert, triggered: true };
2498
- callbackRef.current?.(triggered);
2499
- return triggered;
2500
- }
2501
- return alert;
2502
- });
2503
- return changed ? next : prev;
2504
- });
2505
- }, 1e3);
2506
- return () => clearInterval(interval);
2507
- }, [state.chart]);
2508
2720
  return {
2509
2721
  alerts,
2510
2722
  addAlert,
@@ -2650,128 +2862,124 @@ function useWatchlist() {
2650
2862
  };
2651
2863
  }
2652
2864
  function useReplay() {
2653
- const { state } = useKlinechartsUI();
2654
- const [isReplaying, setIsReplaying] = useState(false);
2655
- const [isPaused, setIsPaused] = useState(false);
2656
- const [speed, setSpeedState] = useState(1);
2657
- const [barIndex, setBarIndex] = useState(0);
2658
- const [totalBars, setTotalBars] = useState(0);
2659
- const intervalRef = useRef(null);
2660
- const savedDataRef = useRef([]);
2661
- const currentIndexRef = useRef(0);
2865
+ const { state, dispatch } = useKlinechartsUI();
2866
+ const { replayIntervalRef, replaySavedDataRef, replayIndexRef } = useKlinechartsUIDispatch();
2867
+ const { isReplaying, isPaused, speed, barIndex, totalBars } = state.replay;
2662
2868
  const clearInterval_ = useCallback(() => {
2663
- if (intervalRef.current !== null) {
2664
- clearInterval(intervalRef.current);
2665
- intervalRef.current = null;
2869
+ if (replayIntervalRef.current !== null) {
2870
+ clearInterval(replayIntervalRef.current);
2871
+ replayIntervalRef.current = null;
2666
2872
  }
2667
- }, []);
2873
+ }, [replayIntervalRef]);
2668
2874
  const addNextBar = useCallback(() => {
2669
2875
  if (!state.chart) return;
2670
- const data = savedDataRef.current;
2671
- const idx = currentIndexRef.current;
2876
+ const data = replaySavedDataRef.current;
2877
+ const idx = replayIndexRef.current;
2672
2878
  if (idx >= data.length) {
2673
2879
  clearInterval_();
2674
- setIsPaused(true);
2880
+ dispatch({ type: "SET_REPLAY", replay: { isPaused: true } });
2675
2881
  return;
2676
2882
  }
2677
2883
  const bar = data[idx];
2678
2884
  state.chart.updateData?.(bar);
2679
- currentIndexRef.current = idx + 1;
2680
- setBarIndex(idx + 1);
2681
- }, [state.chart, clearInterval_]);
2885
+ replayIndexRef.current = idx + 1;
2886
+ dispatch({ type: "SET_REPLAY", replay: { barIndex: idx + 1 } });
2887
+ }, [state.chart, clearInterval_, replaySavedDataRef, replayIndexRef, dispatch]);
2682
2888
  const startInterval = useCallback(
2683
2889
  (currentSpeed) => {
2684
2890
  clearInterval_();
2685
- intervalRef.current = setInterval(addNextBar, 1e3 / currentSpeed);
2891
+ replayIntervalRef.current = setInterval(addNextBar, 1e3 / currentSpeed);
2686
2892
  },
2687
- [addNextBar, clearInterval_]
2893
+ [addNextBar, clearInterval_, replayIntervalRef]
2688
2894
  );
2689
2895
  const startReplay = useCallback(() => {
2690
2896
  if (!state.chart) return;
2691
2897
  const dataList = state.chart.getDataList();
2692
2898
  if (!dataList || dataList.length === 0) return;
2693
- savedDataRef.current = [...dataList];
2694
- currentIndexRef.current = 0;
2695
- setTotalBars(dataList.length);
2696
- setBarIndex(0);
2697
- setIsReplaying(true);
2698
- 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
+ });
2699
2910
  state.chart.clearData?.();
2700
2911
  startInterval(speed);
2701
- }, [state.chart, speed, startInterval]);
2912
+ }, [state.chart, speed, startInterval, replaySavedDataRef, replayIndexRef, dispatch]);
2702
2913
  const stopReplay = useCallback(() => {
2703
2914
  if (!state.chart) return;
2704
2915
  clearInterval_();
2705
- const data = savedDataRef.current;
2916
+ const data = replaySavedDataRef.current;
2706
2917
  if (data.length > 0) {
2707
2918
  state.chart.clearData?.();
2708
2919
  for (const bar of data) {
2709
2920
  state.chart.updateData?.(bar);
2710
2921
  }
2711
2922
  }
2712
- savedDataRef.current = [];
2713
- currentIndexRef.current = 0;
2714
- setIsReplaying(false);
2715
- setIsPaused(false);
2716
- setBarIndex(0);
2717
- setTotalBars(0);
2718
- }, [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]);
2719
2930
  const togglePause = useCallback(() => {
2720
2931
  if (!isReplaying) return;
2721
2932
  if (isPaused) {
2722
2933
  startInterval(speed);
2723
- setIsPaused(false);
2934
+ dispatch({ type: "SET_REPLAY", replay: { isPaused: false } });
2724
2935
  } else {
2725
2936
  clearInterval_();
2726
- setIsPaused(true);
2937
+ dispatch({ type: "SET_REPLAY", replay: { isPaused: true } });
2727
2938
  }
2728
- }, [isReplaying, isPaused, speed, startInterval, clearInterval_]);
2939
+ }, [isReplaying, isPaused, speed, startInterval, clearInterval_, dispatch]);
2729
2940
  const stepForward = useCallback(() => {
2730
2941
  if (!isReplaying || !isPaused) return;
2731
2942
  addNextBar();
2732
2943
  }, [isReplaying, isPaused, addNextBar]);
2733
2944
  const stepBackward = useCallback(() => {
2734
2945
  if (!isReplaying || !isPaused || !state.chart) return;
2735
- const idx = currentIndexRef.current;
2946
+ const idx = replayIndexRef.current;
2736
2947
  if (idx <= 1) return;
2737
- const data = savedDataRef.current;
2948
+ const data = replaySavedDataRef.current;
2738
2949
  state.chart.clearData?.();
2739
2950
  for (let i = 0; i < idx - 1; i++) {
2740
2951
  state.chart.updateData?.(data[i]);
2741
2952
  }
2742
- currentIndexRef.current = idx - 1;
2743
- setBarIndex(idx - 1);
2744
- }, [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]);
2745
2956
  const seekTo = useCallback(
2746
2957
  (targetIndex) => {
2747
2958
  if (!isReplaying || !state.chart) return;
2748
- const data = savedDataRef.current;
2959
+ const data = replaySavedDataRef.current;
2749
2960
  const clamped = Math.max(0, Math.min(targetIndex, data.length));
2750
2961
  clearInterval_();
2751
- setIsPaused(true);
2752
2962
  state.chart.clearData?.();
2753
2963
  for (let i = 0; i < clamped; i++) {
2754
2964
  state.chart.updateData?.(data[i]);
2755
2965
  }
2756
- currentIndexRef.current = clamped;
2757
- setBarIndex(clamped);
2966
+ replayIndexRef.current = clamped;
2967
+ dispatch({
2968
+ type: "SET_REPLAY",
2969
+ replay: { isPaused: true, barIndex: clamped }
2970
+ });
2758
2971
  },
2759
- [isReplaying, state.chart, clearInterval_]
2972
+ [isReplaying, state.chart, clearInterval_, replaySavedDataRef, replayIndexRef, dispatch]
2760
2973
  );
2761
2974
  const setSpeed = useCallback(
2762
2975
  (newSpeed) => {
2763
- setSpeedState(newSpeed);
2976
+ dispatch({ type: "SET_REPLAY", replay: { speed: newSpeed } });
2764
2977
  if (isReplaying && !isPaused) {
2765
2978
  startInterval(newSpeed);
2766
2979
  }
2767
2980
  },
2768
- [isReplaying, isPaused, startInterval]
2981
+ [isReplaying, isPaused, startInterval, dispatch]
2769
2982
  );
2770
- useEffect(() => {
2771
- return () => {
2772
- clearInterval_();
2773
- };
2774
- }, [clearInterval_]);
2775
2983
  return {
2776
2984
  isReplaying,
2777
2985
  isPaused,
@@ -2855,8 +3063,7 @@ function useCompare() {
2855
3063
  });
2856
3064
  const paneId = state.chart.createIndicator(
2857
3065
  { name: indicatorName },
2858
- true,
2859
- { id: "candle_pane" }
3066
+ { isStack: true, pane: { id: "candle_pane" } }
2860
3067
  );
2861
3068
  indicatorsRef.current.set(ticker, {
2862
3069
  name: indicatorName,
@@ -2929,31 +3136,25 @@ function useCompare() {
2929
3136
  return { symbols, addSymbol, removeSymbol, toggleSymbol, clearAll };
2930
3137
  }
2931
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
+ }
2932
3146
  function useMeasure() {
2933
- const { state } = useKlinechartsUI();
2934
- const [isActive, setIsActive] = useState(false);
2935
- const [fromPoint, setFromPoint] = useState(null);
2936
- const [result, setResult] = useState(null);
2937
- const clickCountRef = useRef(0);
2938
- const computeResult = useCallback(
2939
- (from, to) => {
2940
- const priceDiff = to.price - from.price;
2941
- const pricePercent = from.price !== 0 ? priceDiff / from.price * 100 : 0;
2942
- const bars = Math.abs(to.barIndex - from.barIndex);
2943
- const timeDiff = Math.abs(to.timestamp - from.timestamp);
2944
- return { from, to, priceDiff, pricePercent, bars, timeDiff };
2945
- },
2946
- []
2947
- );
3147
+ const { state, dispatch } = useKlinechartsUI();
3148
+ const { isActive, fromPoint, result } = state.measure;
2948
3149
  const cleanup = useCallback(() => {
2949
3150
  state.chart?.removeOverlay({ id: MEASURE_OVERLAY_ID });
2950
3151
  }, [state.chart]);
2951
3152
  const startMeasure = useCallback(() => {
2952
3153
  cleanup();
2953
- setIsActive(true);
2954
- setFromPoint(null);
2955
- setResult(null);
2956
- clickCountRef.current = 0;
3154
+ dispatch({
3155
+ type: "SET_MEASURE",
3156
+ measure: { isActive: true, fromPoint: null, result: null }
3157
+ });
2957
3158
  if (!state.chart) return;
2958
3159
  state.chart.createOverlay({
2959
3160
  name: "segment",
@@ -2989,23 +3190,25 @@ function useMeasure() {
2989
3190
  };
2990
3191
  const from = makePoint(points[0]);
2991
3192
  const to = makePoint(points[1]);
2992
- setFromPoint(from);
2993
- setResult(computeResult(from, to));
2994
- setIsActive(false);
3193
+ dispatch({
3194
+ type: "SET_MEASURE",
3195
+ measure: {
3196
+ isActive: false,
3197
+ fromPoint: from,
3198
+ result: computeResult(from, to)
3199
+ }
3200
+ });
2995
3201
  }
2996
3202
  });
2997
- }, [state.chart, cleanup, computeResult]);
3203
+ }, [state.chart, cleanup, dispatch]);
2998
3204
  const cancelMeasure = useCallback(() => {
2999
3205
  cleanup();
3000
- setIsActive(false);
3001
- setFromPoint(null);
3002
- clickCountRef.current = 0;
3003
- }, [cleanup]);
3206
+ dispatch({ type: "SET_MEASURE", measure: { isActive: false, fromPoint: null } });
3207
+ }, [cleanup, dispatch]);
3004
3208
  const clearResult = useCallback(() => {
3005
3209
  cleanup();
3006
- setResult(null);
3007
- setFromPoint(null);
3008
- }, [cleanup]);
3210
+ dispatch({ type: "SET_MEASURE", measure: { result: null, fromPoint: null } });
3211
+ }, [cleanup, dispatch]);
3009
3212
  useEffect(() => {
3010
3213
  return () => {
3011
3214
  state.chart?.removeOverlay({ id: MEASURE_OVERLAY_ID });