react-klinecharts-ui 0.2.0 → 0.4.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-B3NZ66YA.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, 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-B3NZ66YA.js';
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';
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,8 @@ 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 };
60
62
  case "SET_STYLES":
61
63
  return { ...state, styles: action.styles };
62
64
  case "SET_LOCALE":
@@ -119,6 +121,7 @@ function KlinechartsUIProvider({
119
121
  (acc, name) => ({ ...acc, [name]: "" }),
120
122
  {}
121
123
  ),
124
+ indicatorAxes: {},
122
125
  styles: opts.styles,
123
126
  screenshotUrl: null
124
127
  })
@@ -643,9 +646,12 @@ var INDICATOR_PARAMS = {
643
646
  };
644
647
 
645
648
  // src/hooks/useIndicators.ts
649
+ var COLLAPSED_HEIGHT = 30;
646
650
  function useIndicators() {
647
651
  const { state, dispatch } = useKlinechartsUI();
648
652
  const { undoRedoListenerRef } = useKlinechartsUIDispatch();
653
+ const paneHeightsRef = useRef({});
654
+ const collapsedPanesRef = useRef(/* @__PURE__ */ new Set());
649
655
  const mainIndicators = useMemo(() => {
650
656
  const activeNames = state.mainIndicators;
651
657
  const inactive = MAIN_INDICATORS.filter((n) => !activeNames.includes(n));
@@ -663,62 +669,95 @@ function useIndicators() {
663
669
  ];
664
670
  }, [state.subIndicators]);
665
671
  const addMainIndicator = useCallback(
666
- (name) => {
672
+ (name, options) => {
667
673
  if (state.mainIndicators.includes(name)) return;
674
+ const yAxis = options?.yAxis;
668
675
  state.chart?.createIndicator(
669
- { name, id: `main_${name}`, paneId: "candle_pane" },
670
- true,
671
- { id: "candle_pane" }
676
+ { name, id: `main_${name}` },
677
+ { isStack: true, pane: { id: "candle_pane" }, yAxis }
672
678
  );
673
679
  const newIndicators = [...state.mainIndicators, name];
674
680
  dispatch({ type: "SET_MAIN_INDICATORS", indicators: newIndicators });
681
+ if (yAxis?.id) {
682
+ dispatch({
683
+ type: "SET_INDICATOR_AXES",
684
+ axes: { ...state.indicatorAxes, [`main_${name}`]: yAxis.id }
685
+ });
686
+ }
675
687
  undoRedoListenerRef.current?.({
676
688
  type: "indicator_toggled",
677
- data: { name, wasActive: false, isMain: true, paneId: "candle_pane" }
689
+ data: {
690
+ name,
691
+ wasActive: false,
692
+ isMain: true,
693
+ paneId: "candle_pane",
694
+ yAxisId: yAxis?.id
695
+ }
678
696
  });
679
697
  },
680
- [state.chart, state.mainIndicators, dispatch, undoRedoListenerRef]
698
+ [state.chart, state.mainIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
681
699
  );
682
700
  const removeMainIndicator = useCallback(
683
701
  (name) => {
684
- state.chart?.removeIndicator({ id: `main_${name}` });
702
+ const id = `main_${name}`;
703
+ const yAxisId = state.indicatorAxes[id];
704
+ state.chart?.removeIndicator({ id });
685
705
  const newIndicators = state.mainIndicators.filter((n) => n !== name);
686
706
  dispatch({ type: "SET_MAIN_INDICATORS", indicators: newIndicators });
707
+ if (yAxisId) {
708
+ const nextAxes = { ...state.indicatorAxes };
709
+ delete nextAxes[id];
710
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
711
+ }
687
712
  undoRedoListenerRef.current?.({
688
713
  type: "indicator_toggled",
689
- data: { name, wasActive: true, isMain: true, paneId: "candle_pane" }
714
+ data: { name, wasActive: true, isMain: true, paneId: "candle_pane", yAxisId }
690
715
  });
691
716
  },
692
- [state.chart, state.mainIndicators, dispatch, undoRedoListenerRef]
717
+ [state.chart, state.mainIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
693
718
  );
694
719
  const addSubIndicator = useCallback(
695
- (name) => {
720
+ (name, options) => {
696
721
  if (name in state.subIndicators) return;
697
722
  const id = `sub_${name}`;
698
- state.chart?.createIndicator({ name, id });
723
+ const yAxis = options?.yAxis;
724
+ state.chart?.createIndicator({ name, id }, { yAxis });
699
725
  const paneId = state.chart?.getIndicators({ id })?.[0]?.paneId ?? "";
700
726
  const newIndicators = { ...state.subIndicators, [name]: paneId };
701
727
  dispatch({ type: "SET_SUB_INDICATORS", indicators: newIndicators });
728
+ if (yAxis?.id) {
729
+ dispatch({
730
+ type: "SET_INDICATOR_AXES",
731
+ axes: { ...state.indicatorAxes, [id]: yAxis.id }
732
+ });
733
+ }
702
734
  undoRedoListenerRef.current?.({
703
735
  type: "indicator_toggled",
704
- data: { name, wasActive: false, isMain: false, paneId }
736
+ data: { name, wasActive: false, isMain: false, paneId, yAxisId: yAxis?.id }
705
737
  });
706
738
  },
707
- [state.chart, state.subIndicators, dispatch, undoRedoListenerRef]
739
+ [state.chart, state.subIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
708
740
  );
709
741
  const removeSubIndicator = useCallback(
710
742
  (name) => {
743
+ const id = `sub_${name}`;
711
744
  const paneId = state.subIndicators[name] ?? "";
712
- state.chart?.removeIndicator({ id: `sub_${name}` });
745
+ const yAxisId = state.indicatorAxes[id];
746
+ state.chart?.removeIndicator({ id });
713
747
  const newIndicators = { ...state.subIndicators };
714
748
  delete newIndicators[name];
715
749
  dispatch({ type: "SET_SUB_INDICATORS", indicators: newIndicators });
750
+ if (yAxisId) {
751
+ const nextAxes = { ...state.indicatorAxes };
752
+ delete nextAxes[id];
753
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
754
+ }
716
755
  undoRedoListenerRef.current?.({
717
756
  type: "indicator_toggled",
718
- data: { name, wasActive: true, isMain: false, paneId }
757
+ data: { name, wasActive: true, isMain: false, paneId, yAxisId }
719
758
  });
720
759
  },
721
- [state.chart, state.subIndicators, dispatch, undoRedoListenerRef]
760
+ [state.chart, state.subIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
722
761
  );
723
762
  const toggleMainIndicator = useCallback(
724
763
  (name) => {
@@ -748,8 +787,10 @@ function useIndicators() {
748
787
  [state.chart]
749
788
  );
750
789
  const updateIndicatorParams = useCallback(
751
- (name, paneId, params) => {
752
- state.chart?.overrideIndicator({ name, paneId, calcParams: params });
790
+ // `paneId` is kept in the signature for API stability; klinecharts v10
791
+ // `overrideIndicator` no longer accepts it and targets by name/id instead.
792
+ (name, _paneId, params) => {
793
+ state.chart?.overrideIndicator({ name, calcParams: params });
753
794
  },
754
795
  [state.chart]
755
796
  );
@@ -770,11 +811,9 @@ function useIndicators() {
770
811
  {
771
812
  name,
772
813
  id: `main_${name}`,
773
- paneId: "candle_pane",
774
814
  ...prevCalcParams ? { calcParams: prevCalcParams } : {}
775
815
  },
776
- true,
777
- { id: "candle_pane" }
816
+ { isStack: true, pane: { id: "candle_pane" } }
778
817
  );
779
818
  const newSub = { ...state.subIndicators };
780
819
  delete newSub[name];
@@ -815,6 +854,128 @@ function useIndicators() {
815
854
  (name) => name in state.subIndicators,
816
855
  [state.subIndicators]
817
856
  );
857
+ const collapseSubIndicator = useCallback(
858
+ (name) => {
859
+ const paneId = state.subIndicators[name];
860
+ if (!state.chart || !paneId) return;
861
+ const currentSize = state.chart.getSize?.(paneId);
862
+ if (currentSize?.height && currentSize.height > COLLAPSED_HEIGHT) {
863
+ paneHeightsRef.current[paneId] = currentSize.height;
864
+ }
865
+ collapsedPanesRef.current.add(paneId);
866
+ state.chart.setPaneOptions?.({
867
+ id: paneId,
868
+ height: COLLAPSED_HEIGHT
869
+ });
870
+ state.chart.overrideIndicator({ name, id: `sub_${name}`, visible: false });
871
+ },
872
+ [state.chart, state.subIndicators]
873
+ );
874
+ const expandSubIndicator = useCallback(
875
+ (name) => {
876
+ const paneId = state.subIndicators[name];
877
+ if (!state.chart || !paneId) return;
878
+ const savedHeight = paneHeightsRef.current[paneId] ?? 100;
879
+ collapsedPanesRef.current.delete(paneId);
880
+ state.chart.setPaneOptions?.({
881
+ id: paneId,
882
+ height: savedHeight
883
+ });
884
+ state.chart.overrideIndicator({ name, id: `sub_${name}`, visible: true });
885
+ },
886
+ [state.chart, state.subIndicators]
887
+ );
888
+ const isSubIndicatorCollapsed = useCallback(
889
+ (name) => {
890
+ const paneId = state.subIndicators[name];
891
+ return paneId ? collapsedPanesRef.current.has(paneId) : false;
892
+ },
893
+ [state.subIndicators]
894
+ );
895
+ const reorderSubIndicator = useCallback(
896
+ (name, direction) => {
897
+ if (!state.chart) return;
898
+ const subNames = Object.keys(state.subIndicators);
899
+ const idx = subNames.indexOf(name);
900
+ if (idx === -1) return;
901
+ const swapIdx = direction === "up" ? idx - 1 : idx + 1;
902
+ if (swapIdx < 0 || swapIdx >= subNames.length) return;
903
+ const subStates = subNames.map((n) => {
904
+ const id = `sub_${n}`;
905
+ const indicator = state.chart.getIndicators({ id })?.[0];
906
+ return {
907
+ name: n,
908
+ calcParams: indicator?.calcParams,
909
+ visible: indicator?.visible ?? true,
910
+ styles: indicator?.styles,
911
+ paneId: state.subIndicators[n]
912
+ };
913
+ });
914
+ [subStates[idx], subStates[swapIdx]] = [subStates[swapIdx], subStates[idx]];
915
+ for (const n of subNames) {
916
+ state.chart.removeIndicator({ id: `sub_${n}` });
917
+ }
918
+ const newSubIndicators = {};
919
+ for (const sub of subStates) {
920
+ const id = `sub_${sub.name}`;
921
+ state.chart.createIndicator({
922
+ name: sub.name,
923
+ id,
924
+ ...sub.calcParams ? { calcParams: sub.calcParams } : {},
925
+ visible: sub.visible
926
+ });
927
+ const paneId = state.chart.getIndicators({ id })?.[0]?.paneId ?? "";
928
+ newSubIndicators[sub.name] = paneId;
929
+ if (sub.styles) {
930
+ state.chart.overrideIndicator({
931
+ name: sub.name,
932
+ id,
933
+ styles: sub.styles
934
+ });
935
+ }
936
+ }
937
+ dispatch({ type: "SET_SUB_INDICATORS", indicators: newSubIndicators });
938
+ },
939
+ [state.chart, state.subIndicators, dispatch]
940
+ );
941
+ const getIndicatorAxis = useCallback(
942
+ (name, isMain) => state.indicatorAxes[isMain ? `main_${name}` : `sub_${name}`],
943
+ [state.indicatorAxes]
944
+ );
945
+ const bindIndicatorToNewAxis = useCallback(
946
+ (name, isMain, yAxis) => {
947
+ if (!state.chart) return;
948
+ const id = isMain ? `main_${name}` : `sub_${name}`;
949
+ const current = state.chart.getIndicators({ id })?.[0];
950
+ if (!current) return;
951
+ const calcParams = current.calcParams;
952
+ const styles = current.styles;
953
+ const visible = current.visible ?? true;
954
+ const paneId = isMain ? "candle_pane" : current.paneId ?? state.subIndicators[name] ?? "";
955
+ state.chart.removeIndicator({ id });
956
+ state.chart.createIndicator(
957
+ {
958
+ name,
959
+ id,
960
+ ...calcParams ? { calcParams } : {},
961
+ visible
962
+ },
963
+ {
964
+ isStack: isMain,
965
+ ...paneId ? { pane: { id: paneId } } : {},
966
+ yAxis
967
+ }
968
+ );
969
+ if (styles) {
970
+ state.chart.overrideIndicator({ name, id, styles });
971
+ }
972
+ const nextAxes = { ...state.indicatorAxes };
973
+ if (yAxis?.id) nextAxes[id] = yAxis.id;
974
+ else delete nextAxes[id];
975
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
976
+ },
977
+ [state.chart, state.indicatorAxes, state.subIndicators, dispatch]
978
+ );
818
979
  return {
819
980
  mainIndicators,
820
981
  subIndicators,
@@ -834,7 +995,14 @@ function useIndicators() {
834
995
  updateIndicatorParams,
835
996
  getIndicatorParams,
836
997
  isMainIndicatorActive,
837
- isSubIndicatorActive
998
+ isSubIndicatorActive,
999
+ collapseSubIndicator,
1000
+ expandSubIndicator,
1001
+ isSubIndicatorCollapsed,
1002
+ reorderSubIndicator,
1003
+ indicatorAxes: state.indicatorAxes,
1004
+ getIndicatorAxis,
1005
+ bindIndicatorToNewAxis
838
1006
  };
839
1007
  }
840
1008
 
@@ -933,6 +1101,17 @@ function useDrawingTools() {
933
1101
  const [magnetMode, setMagnetModeState] = useState("normal");
934
1102
  const [isLocked, setIsLocked] = useState(false);
935
1103
  const [isVisible, setIsVisible] = useState(true);
1104
+ const [autoRetrigger, setAutoRetrigger] = useState(true);
1105
+ const activeToolRef = useRef(activeTool);
1106
+ activeToolRef.current = activeTool;
1107
+ const autoRetriggerRef = useRef(autoRetrigger);
1108
+ autoRetriggerRef.current = autoRetrigger;
1109
+ const isLockedRef = useRef(isLocked);
1110
+ isLockedRef.current = isLocked;
1111
+ const isVisibleRef = useRef(isVisible);
1112
+ isVisibleRef.current = isVisible;
1113
+ const magnetModeRef = useRef(magnetMode);
1114
+ magnetModeRef.current = magnetMode;
936
1115
  const categories = useMemo(
937
1116
  () => DRAWING_CATEGORIES.map((cat) => ({
938
1117
  key: cat.key,
@@ -943,15 +1122,15 @@ function useDrawingTools() {
943
1122
  })),
944
1123
  []
945
1124
  );
946
- const selectTool = useCallback(
1125
+ const createOverlayForTool = useCallback(
947
1126
  (name) => {
948
- setActiveTool(name);
1127
+ const mode = magnetModeRef.current === "strong" ? "strong_magnet" : magnetModeRef.current === "weak" ? "weak_magnet" : "normal";
949
1128
  state.chart?.createOverlay({
950
1129
  name,
951
1130
  groupId: DRAWING_GROUP_ID,
952
- lock: isLocked,
953
- visible: isVisible,
954
- mode: magnetMode === "strong" ? "strong_magnet" : magnetMode === "weak" ? "weak_magnet" : "normal",
1131
+ lock: isLockedRef.current,
1132
+ visible: isVisibleRef.current,
1133
+ mode,
955
1134
  onDrawEnd: (event) => {
956
1135
  const o = event.overlay;
957
1136
  undoRedoListenerRef.current?.({
@@ -966,10 +1145,22 @@ function useDrawingTools() {
966
1145
  }
967
1146
  }
968
1147
  });
1148
+ if (autoRetriggerRef.current && activeToolRef.current === name) {
1149
+ requestAnimationFrame(() => {
1150
+ createOverlayForTool(name);
1151
+ });
1152
+ }
969
1153
  }
970
1154
  });
971
1155
  },
972
- [state.chart, isLocked, isVisible, magnetMode, undoRedoListenerRef]
1156
+ [state.chart, undoRedoListenerRef]
1157
+ );
1158
+ const selectTool = useCallback(
1159
+ (name) => {
1160
+ setActiveTool(name);
1161
+ createOverlayForTool(name);
1162
+ },
1163
+ [createOverlayForTool]
973
1164
  );
974
1165
  const clearActiveTool = useCallback(() => {
975
1166
  setActiveTool(null);
@@ -1040,12 +1231,14 @@ function useDrawingTools() {
1040
1231
  magnetMode,
1041
1232
  isLocked,
1042
1233
  isVisible,
1234
+ autoRetrigger,
1043
1235
  selectTool,
1044
1236
  clearActiveTool,
1045
1237
  setMagnetMode,
1046
1238
  toggleLock,
1047
1239
  toggleVisibility,
1048
- removeAllDrawings
1240
+ removeAllDrawings,
1241
+ setAutoRetrigger
1049
1242
  };
1050
1243
  }
1051
1244
 
@@ -1119,20 +1312,23 @@ function useKlinechartsUISettings() {
1119
1312
  () => TOOLTIP_SHOW_RULES.map((r) => ({ key: r, localeKey: r })),
1120
1313
  []
1121
1314
  );
1315
+ const applyPaneAxis = useCallback(
1316
+ (axis) => {
1317
+ state.chart?.overrideYAxis?.({ paneId: "candle_pane", ...axis });
1318
+ },
1319
+ [state.chart]
1320
+ );
1122
1321
  const hasAppliedInitial = useRef(false);
1123
1322
  useEffect(() => {
1124
1323
  if (!state.chart || hasAppliedInitial.current) return;
1125
1324
  hasAppliedInitial.current = true;
1126
- const needsPaneOptions = settings.reverseCoordinate || settings.priceAxisType !== "normal" || settings.yAxisPosition !== "right" || settings.yAxisInside;
1127
- if (needsPaneOptions) {
1128
- state.chart.setPaneOptions({
1129
- id: "candle_pane",
1130
- axis: {
1131
- ...settings.priceAxisType !== "normal" && { name: settings.priceAxisType },
1132
- ...settings.reverseCoordinate && { reverse: true },
1133
- ...settings.yAxisPosition !== "right" && { position: settings.yAxisPosition },
1134
- ...settings.yAxisInside && { inside: true }
1135
- }
1325
+ const needsAxisOverride = settings.reverseCoordinate || settings.priceAxisType !== "normal" || settings.yAxisPosition !== "right" || settings.yAxisInside;
1326
+ if (needsAxisOverride) {
1327
+ applyPaneAxis({
1328
+ ...settings.priceAxisType !== "normal" && { name: settings.priceAxisType },
1329
+ ...settings.reverseCoordinate && { reverse: true },
1330
+ ...settings.yAxisPosition !== "right" && { position: settings.yAxisPosition },
1331
+ ...settings.yAxisInside && { inside: true }
1136
1332
  });
1137
1333
  }
1138
1334
  if (settings.showIndicatorLastValue) {
@@ -1155,12 +1351,6 @@ function useKlinechartsUISettings() {
1155
1351
  },
1156
1352
  [state.chart]
1157
1353
  );
1158
- const applyPaneAxis = useCallback(
1159
- (axis) => {
1160
- state.chart?.setPaneOptions({ id: "candle_pane", axis });
1161
- },
1162
- [state.chart]
1163
- );
1164
1354
  const setCandleType = useCallback(
1165
1355
  (type) => {
1166
1356
  applyStyle("candle.type", type);
@@ -1308,11 +1498,8 @@ function useKlinechartsUISettings() {
1308
1498
  const resetToDefaults = useCallback(() => {
1309
1499
  setSettings(defaultSettings);
1310
1500
  state.chart?.setStyles(state.theme);
1311
- state.chart?.setPaneOptions({
1312
- id: "candle_pane",
1313
- axis: { name: "normal", reverse: false, position: "right", inside: false }
1314
- });
1315
- }, [state.chart, state.theme]);
1501
+ applyPaneAxis({ name: "normal", reverse: false, position: "right", inside: false });
1502
+ }, [state.chart, state.theme, applyPaneAxis]);
1316
1503
  return {
1317
1504
  ...settings,
1318
1505
  candleTypes,
@@ -1569,21 +1756,28 @@ function useUndoRedo() {
1569
1756
  break;
1570
1757
  }
1571
1758
  case "indicator_toggled": {
1572
- const { name, wasActive, isMain, paneId } = action.data;
1759
+ const { name, wasActive, isMain, paneId, yAxisId } = action.data;
1760
+ const id = isMain ? `main_${name}` : `sub_${name}`;
1573
1761
  if (wasActive) {
1574
1762
  if (isMain) {
1575
1763
  state.chart.createIndicator(
1576
- { name, id: `main_${name}`, paneId: "candle_pane" },
1577
- true,
1578
- { id: "candle_pane" }
1764
+ { name, id },
1765
+ {
1766
+ isStack: true,
1767
+ pane: { id: "candle_pane" },
1768
+ ...yAxisId ? { yAxis: { id: yAxisId } } : {}
1769
+ }
1579
1770
  );
1580
1771
  dispatch({
1581
1772
  type: "SET_MAIN_INDICATORS",
1582
1773
  indicators: [...state.mainIndicators, name]
1583
1774
  });
1584
1775
  } else {
1585
- state.chart.createIndicator({ name, id: `sub_${name}` });
1586
- const newPaneId = state.chart.getIndicators({ id: `sub_${name}` })?.[0]?.paneId ?? "";
1776
+ state.chart.createIndicator(
1777
+ { name, id },
1778
+ yAxisId ? { yAxis: { id: yAxisId } } : void 0
1779
+ );
1780
+ const newPaneId = state.chart.getIndicators({ id })?.[0]?.paneId ?? "";
1587
1781
  dispatch({
1588
1782
  type: "SET_SUB_INDICATORS",
1589
1783
  indicators: {
@@ -1592,9 +1786,15 @@ function useUndoRedo() {
1592
1786
  }
1593
1787
  });
1594
1788
  }
1789
+ if (yAxisId) {
1790
+ dispatch({
1791
+ type: "SET_INDICATOR_AXES",
1792
+ axes: { ...state.indicatorAxes, [id]: yAxisId }
1793
+ });
1794
+ }
1595
1795
  } else {
1796
+ state.chart.removeIndicator({ id });
1596
1797
  if (isMain) {
1597
- state.chart.removeIndicator({ id: `main_${name}` });
1598
1798
  dispatch({
1599
1799
  type: "SET_MAIN_INDICATORS",
1600
1800
  indicators: state.mainIndicators.filter(
@@ -1602,7 +1802,6 @@ function useUndoRedo() {
1602
1802
  )
1603
1803
  });
1604
1804
  } else {
1605
- state.chart.removeIndicator({ id: `sub_${name}` });
1606
1805
  const newSub = { ...state.subIndicators };
1607
1806
  delete newSub[name];
1608
1807
  dispatch({
@@ -1610,6 +1809,11 @@ function useUndoRedo() {
1610
1809
  indicators: newSub
1611
1810
  });
1612
1811
  }
1812
+ if (state.indicatorAxes[id]) {
1813
+ const nextAxes = { ...state.indicatorAxes };
1814
+ delete nextAxes[id];
1815
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
1816
+ }
1613
1817
  }
1614
1818
  setRedoStack((prev) => [
1615
1819
  ...prev,
@@ -1619,7 +1823,8 @@ function useUndoRedo() {
1619
1823
  name,
1620
1824
  wasActive: !wasActive,
1621
1825
  isMain,
1622
- paneId
1826
+ paneId,
1827
+ yAxisId
1623
1828
  }
1624
1829
  }
1625
1830
  ]);
@@ -1629,7 +1834,7 @@ function useUndoRedo() {
1629
1834
  } finally {
1630
1835
  isProcessingRef.current = false;
1631
1836
  }
1632
- }, [undoStack, state.chart, state.mainIndicators, state.subIndicators, dispatch]);
1837
+ }, [undoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
1633
1838
  const redo = useCallback(() => {
1634
1839
  if (redoStack.length === 0 || !state.chart || isProcessingRef.current)
1635
1840
  return;
@@ -1681,21 +1886,28 @@ function useUndoRedo() {
1681
1886
  break;
1682
1887
  }
1683
1888
  case "indicator_toggled": {
1684
- const { name, wasActive, isMain } = action.data;
1889
+ const { name, wasActive, isMain, paneId, yAxisId } = action.data;
1890
+ const id = isMain ? `main_${name}` : `sub_${name}`;
1685
1891
  if (wasActive) {
1686
1892
  if (isMain) {
1687
1893
  state.chart.createIndicator(
1688
- { name, id: `main_${name}`, paneId: "candle_pane" },
1689
- true,
1690
- { id: "candle_pane" }
1894
+ { name, id },
1895
+ {
1896
+ isStack: true,
1897
+ pane: { id: "candle_pane" },
1898
+ ...yAxisId ? { yAxis: { id: yAxisId } } : {}
1899
+ }
1691
1900
  );
1692
1901
  dispatch({
1693
1902
  type: "SET_MAIN_INDICATORS",
1694
1903
  indicators: [...state.mainIndicators, name]
1695
1904
  });
1696
1905
  } else {
1697
- state.chart.createIndicator({ name, id: `sub_${name}` });
1698
- const newPaneId = state.chart.getIndicators({ id: `sub_${name}` })?.[0]?.paneId ?? "";
1906
+ state.chart.createIndicator(
1907
+ { name, id },
1908
+ yAxisId ? { yAxis: { id: yAxisId } } : void 0
1909
+ );
1910
+ const newPaneId = state.chart.getIndicators({ id })?.[0]?.paneId ?? "";
1699
1911
  dispatch({
1700
1912
  type: "SET_SUB_INDICATORS",
1701
1913
  indicators: {
@@ -1704,9 +1916,15 @@ function useUndoRedo() {
1704
1916
  }
1705
1917
  });
1706
1918
  }
1919
+ if (yAxisId) {
1920
+ dispatch({
1921
+ type: "SET_INDICATOR_AXES",
1922
+ axes: { ...state.indicatorAxes, [id]: yAxisId }
1923
+ });
1924
+ }
1707
1925
  } else {
1926
+ state.chart.removeIndicator({ id });
1708
1927
  if (isMain) {
1709
- state.chart.removeIndicator({ id: `main_${name}` });
1710
1928
  dispatch({
1711
1929
  type: "SET_MAIN_INDICATORS",
1712
1930
  indicators: state.mainIndicators.filter(
@@ -1714,7 +1932,6 @@ function useUndoRedo() {
1714
1932
  )
1715
1933
  });
1716
1934
  } else {
1717
- state.chart.removeIndicator({ id: `sub_${name}` });
1718
1935
  const newSub = { ...state.subIndicators };
1719
1936
  delete newSub[name];
1720
1937
  dispatch({
@@ -1722,6 +1939,11 @@ function useUndoRedo() {
1722
1939
  indicators: newSub
1723
1940
  });
1724
1941
  }
1942
+ if (state.indicatorAxes[id]) {
1943
+ const nextAxes = { ...state.indicatorAxes };
1944
+ delete nextAxes[id];
1945
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
1946
+ }
1725
1947
  }
1726
1948
  setUndoStack((prev) => [
1727
1949
  ...prev,
@@ -1730,7 +1952,9 @@ function useUndoRedo() {
1730
1952
  data: {
1731
1953
  name,
1732
1954
  wasActive: !wasActive,
1733
- isMain
1955
+ isMain,
1956
+ paneId,
1957
+ yAxisId
1734
1958
  }
1735
1959
  }
1736
1960
  ]);
@@ -1740,7 +1964,7 @@ function useUndoRedo() {
1740
1964
  } finally {
1741
1965
  isProcessingRef.current = false;
1742
1966
  }
1743
- }, [redoStack, state.chart, state.mainIndicators, state.subIndicators, dispatch]);
1967
+ }, [redoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
1744
1968
  useEffect(() => {
1745
1969
  const handleKeyDown = (e) => {
1746
1970
  const isCtrlOrMeta = e.ctrlKey || e.metaKey;
@@ -1814,12 +2038,14 @@ function useLayoutManager() {
1814
2038
  if (indicatorMap) {
1815
2039
  indicatorMap.forEach((paneIndicators, paneId) => {
1816
2040
  paneIndicators.forEach((indicator) => {
2041
+ const yAxisId = state.indicatorAxes[indicator.id];
1817
2042
  indicators2.push({
1818
2043
  paneId,
1819
2044
  name: indicator.name,
1820
2045
  calcParams: indicator.calcParams,
1821
2046
  visible: indicator.visible,
1822
- ...indicator.styles ? { styles: indicator.styles } : {}
2047
+ ...indicator.styles ? { styles: indicator.styles } : {},
2048
+ ...yAxisId ? { yAxisId } : {}
1823
2049
  });
1824
2050
  });
1825
2051
  });
@@ -1847,7 +2073,7 @@ function useLayoutManager() {
1847
2073
  indicators: indicators2,
1848
2074
  drawings
1849
2075
  };
1850
- }, [state.chart, state.symbol, state.period]);
2076
+ }, [state.chart, state.symbol, state.period, state.indicatorAxes]);
1851
2077
  const saveLayout = useCallback(
1852
2078
  (name) => {
1853
2079
  const chartState = serializeState();
@@ -1895,25 +2121,35 @@ function useLayoutManager() {
1895
2121
  }
1896
2122
  const newMainIndicators = [];
1897
2123
  const newSubIndicators = {};
2124
+ const restoredAxes = {};
1898
2125
  if (chartState.indicators) {
1899
2126
  for (const ind of chartState.indicators) {
2127
+ const isMain = ind.paneId === "candle_pane";
2128
+ const id2 = isMain ? `main_${ind.name}` : `sub_${ind.name}`;
1900
2129
  chart.createIndicator(
1901
2130
  {
1902
2131
  name: ind.name,
2132
+ id: id2,
1903
2133
  calcParams: ind.calcParams,
1904
2134
  visible: ind.visible
1905
2135
  },
1906
- ind.paneId !== "candle_pane",
1907
- { id: ind.paneId }
2136
+ {
2137
+ isStack: ind.paneId !== "candle_pane",
2138
+ pane: { id: ind.paneId },
2139
+ ...ind.yAxisId ? { yAxis: { id: ind.yAxisId } } : {}
2140
+ }
1908
2141
  );
1909
2142
  if (ind.styles) {
1910
2143
  chart.overrideIndicator({
1911
2144
  name: ind.name,
1912
- paneId: ind.paneId,
2145
+ id: id2,
1913
2146
  styles: ind.styles
1914
2147
  });
1915
2148
  }
1916
- if (ind.paneId === "candle_pane") {
2149
+ if (ind.yAxisId) {
2150
+ restoredAxes[id2] = ind.yAxisId;
2151
+ }
2152
+ if (isMain) {
1917
2153
  newMainIndicators.push(ind.name);
1918
2154
  } else {
1919
2155
  newSubIndicators[ind.name] = ind.paneId;
@@ -1928,6 +2164,10 @@ function useLayoutManager() {
1928
2164
  type: "SET_SUB_INDICATORS",
1929
2165
  indicators: newSubIndicators
1930
2166
  });
2167
+ dispatch({
2168
+ type: "SET_INDICATOR_AXES",
2169
+ axes: restoredAxes
2170
+ });
1931
2171
  if (chartState.drawings) {
1932
2172
  for (const drawing of chartState.drawings) {
1933
2173
  chart.createOverlay({
@@ -2153,8 +2393,7 @@ ${code}`
2153
2393
  if (placement === "main") {
2154
2394
  paneId = chart.createIndicator(
2155
2395
  { name: indicatorName },
2156
- true,
2157
- { id: "candle_pane" }
2396
+ { isStack: true, pane: { id: "candle_pane" } }
2158
2397
  );
2159
2398
  } else {
2160
2399
  paneId = chart.createIndicator({ name: indicatorName });
@@ -2244,6 +2483,749 @@ ${code}`
2244
2483
  defaultScript: DEFAULT_SCRIPT
2245
2484
  };
2246
2485
  }
2486
+ function useCrosshair() {
2487
+ const { state } = useKlinechartsUI();
2488
+ const [barData, setBarData] = useState(null);
2489
+ const rafRef = useRef(0);
2490
+ const handler = useCallback(
2491
+ (event) => {
2492
+ if (rafRef.current) {
2493
+ cancelAnimationFrame(rafRef.current);
2494
+ }
2495
+ rafRef.current = requestAnimationFrame(() => {
2496
+ rafRef.current = 0;
2497
+ const klineData = event?.kLineData;
2498
+ if (!klineData) {
2499
+ setBarData(null);
2500
+ return;
2501
+ }
2502
+ const pricePrecision = state.symbol?.pricePrecision ?? 2;
2503
+ const open = klineData.open ?? 0;
2504
+ const close = klineData.close ?? 0;
2505
+ const change = parseFloat((close - open).toFixed(pricePrecision));
2506
+ const changePercent = open !== 0 ? parseFloat((change / open * 100).toFixed(2)) : 0;
2507
+ setBarData({
2508
+ open,
2509
+ high: klineData.high ?? 0,
2510
+ low: klineData.low ?? 0,
2511
+ close,
2512
+ volume: klineData.volume ?? 0,
2513
+ timestamp: klineData.timestamp ?? 0,
2514
+ change,
2515
+ changePercent
2516
+ });
2517
+ });
2518
+ },
2519
+ [state.symbol?.pricePrecision]
2520
+ );
2521
+ useEffect(() => {
2522
+ const chart = state.chart;
2523
+ if (!chart) return;
2524
+ chart.subscribeAction?.("onCrosshairChange", handler);
2525
+ return () => {
2526
+ if (rafRef.current) {
2527
+ cancelAnimationFrame(rafRef.current);
2528
+ rafRef.current = 0;
2529
+ }
2530
+ chart.unsubscribeAction?.("onCrosshairChange", handler);
2531
+ };
2532
+ }, [state.chart, handler]);
2533
+ return { barData };
2534
+ }
2535
+ var alertCounter = 0;
2536
+ function useAlerts() {
2537
+ const { state } = useKlinechartsUI();
2538
+ const [alerts, setAlerts] = useState([]);
2539
+ const callbackRef = useRef(null);
2540
+ const prevCloseRef = useRef(null);
2541
+ const addAlert = useCallback(
2542
+ (price, condition, message) => {
2543
+ const id = `alert_${++alertCounter}`;
2544
+ const alert = {
2545
+ id,
2546
+ price,
2547
+ condition,
2548
+ message,
2549
+ triggered: false
2550
+ };
2551
+ setAlerts((prev) => [...prev, alert]);
2552
+ if (state.chart) {
2553
+ state.chart.createOverlay({
2554
+ name: "horizontalStraightLine",
2555
+ id,
2556
+ groupId: "price_alerts",
2557
+ points: [{ value: price }],
2558
+ styles: {
2559
+ line: {
2560
+ style: "dashed",
2561
+ color: "#ff9800",
2562
+ size: 1
2563
+ }
2564
+ },
2565
+ lock: true
2566
+ });
2567
+ }
2568
+ return id;
2569
+ },
2570
+ [state.chart]
2571
+ );
2572
+ const removeAlert = useCallback(
2573
+ (id) => {
2574
+ setAlerts((prev) => prev.filter((a) => a.id !== id));
2575
+ state.chart?.removeOverlay({ id });
2576
+ },
2577
+ [state.chart]
2578
+ );
2579
+ 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]);
2587
+ const onAlertTriggered = useCallback(
2588
+ (callback) => {
2589
+ callbackRef.current = callback;
2590
+ },
2591
+ []
2592
+ );
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
+ return {
2632
+ alerts,
2633
+ addAlert,
2634
+ removeAlert,
2635
+ clearAlerts,
2636
+ onAlertTriggered
2637
+ };
2638
+ }
2639
+ function useDataExport() {
2640
+ const { state } = useKlinechartsUI();
2641
+ const exportAll = useCallback(
2642
+ (format) => {
2643
+ if (!state.chart) return;
2644
+ const dataList = state.chart.getDataList();
2645
+ if (!dataList || dataList.length === 0) return;
2646
+ downloadData(dataList, format, state.symbol?.ticker);
2647
+ },
2648
+ [state.chart, state.symbol?.ticker]
2649
+ );
2650
+ const exportVisible = useCallback(
2651
+ (format) => {
2652
+ if (!state.chart) return;
2653
+ const dataList = state.chart.getDataList();
2654
+ if (!dataList || dataList.length === 0) return;
2655
+ const visibleRange = state.chart.getVisibleRange?.();
2656
+ if (!visibleRange) {
2657
+ downloadData(dataList, format, state.symbol?.ticker);
2658
+ return;
2659
+ }
2660
+ const { from, to } = visibleRange;
2661
+ const visibleData = dataList.slice(from, to);
2662
+ downloadData(visibleData, format, state.symbol?.ticker);
2663
+ },
2664
+ [state.chart, state.symbol?.ticker]
2665
+ );
2666
+ return { exportAll, exportVisible };
2667
+ }
2668
+ function formatRow(bar) {
2669
+ return {
2670
+ date: new Date(bar.timestamp).toISOString(),
2671
+ open: bar.open,
2672
+ high: bar.high,
2673
+ low: bar.low,
2674
+ close: bar.close,
2675
+ volume: bar.volume ?? 0
2676
+ };
2677
+ }
2678
+ function buildCsv(dataList) {
2679
+ const header = "Date,Open,High,Low,Close,Volume\n";
2680
+ const rows = dataList.map((bar) => {
2681
+ const r = formatRow(bar);
2682
+ return `${r.date},${r.open},${r.high},${r.low},${r.close},${r.volume}`;
2683
+ });
2684
+ return header + rows.join("\n");
2685
+ }
2686
+ function buildJson(dataList) {
2687
+ return JSON.stringify(dataList.map(formatRow), null, 2);
2688
+ }
2689
+ function downloadData(dataList, format, ticker) {
2690
+ const content = format === "csv" ? buildCsv(dataList) : buildJson(dataList);
2691
+ const mimeType = format === "csv" ? "text/csv" : "application/json";
2692
+ const blob = new Blob([content], { type: mimeType });
2693
+ const url = URL.createObjectURL(blob);
2694
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2695
+ const prefix = ticker ?? "chart";
2696
+ const filename = `${prefix}_${date}.${format}`;
2697
+ const link = document.createElement("a");
2698
+ link.href = url;
2699
+ link.download = filename;
2700
+ document.body.appendChild(link);
2701
+ link.click();
2702
+ document.body.removeChild(link);
2703
+ URL.revokeObjectURL(url);
2704
+ }
2705
+ function useWatchlist() {
2706
+ const { state } = useKlinechartsUI();
2707
+ const { dispatch, datafeed } = useKlinechartsUIDispatch();
2708
+ const [items, setItems] = useState([]);
2709
+ const subscriptionsRef = useRef(/* @__PURE__ */ new Map());
2710
+ const activeSymbol = state.symbol?.ticker ?? null;
2711
+ const addSymbol = useCallback(
2712
+ (ticker) => {
2713
+ setItems((prev) => {
2714
+ if (prev.some((item) => item.ticker === ticker)) return prev;
2715
+ return [
2716
+ ...prev,
2717
+ { ticker, lastPrice: null, change: null, changePercent: null }
2718
+ ];
2719
+ });
2720
+ if (subscriptionsRef.current.has(ticker)) return;
2721
+ const symbolInfo = { ticker };
2722
+ const period = state.period;
2723
+ subscriptionsRef.current.set(ticker, { symbolInfo, period });
2724
+ datafeed.subscribe(symbolInfo, period, (bar) => {
2725
+ setItems(
2726
+ (prev) => prev.map((item) => {
2727
+ if (item.ticker !== ticker) return item;
2728
+ const change = bar.close - bar.open;
2729
+ const changePercent = bar.open !== 0 ? change / bar.open * 100 : null;
2730
+ return {
2731
+ ...item,
2732
+ lastPrice: bar.close,
2733
+ change,
2734
+ changePercent
2735
+ };
2736
+ })
2737
+ );
2738
+ });
2739
+ },
2740
+ [datafeed, state.period]
2741
+ );
2742
+ const removeSymbol = useCallback(
2743
+ (ticker) => {
2744
+ const sub = subscriptionsRef.current.get(ticker);
2745
+ if (sub) {
2746
+ datafeed.unsubscribe(sub.symbolInfo, sub.period);
2747
+ subscriptionsRef.current.delete(ticker);
2748
+ }
2749
+ setItems((prev) => prev.filter((item) => item.ticker !== ticker));
2750
+ },
2751
+ [datafeed]
2752
+ );
2753
+ const switchSymbol = useCallback(
2754
+ (ticker) => {
2755
+ dispatch({ type: "SET_SYMBOL", symbol: { ticker } });
2756
+ },
2757
+ [dispatch]
2758
+ );
2759
+ useEffect(() => {
2760
+ return () => {
2761
+ subscriptionsRef.current.forEach((sub) => {
2762
+ datafeed.unsubscribe(sub.symbolInfo, sub.period);
2763
+ });
2764
+ subscriptionsRef.current.clear();
2765
+ };
2766
+ }, [datafeed]);
2767
+ return {
2768
+ items,
2769
+ addSymbol,
2770
+ removeSymbol,
2771
+ switchSymbol,
2772
+ activeSymbol
2773
+ };
2774
+ }
2775
+ 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);
2785
+ const clearInterval_ = useCallback(() => {
2786
+ if (intervalRef.current !== null) {
2787
+ clearInterval(intervalRef.current);
2788
+ intervalRef.current = null;
2789
+ }
2790
+ }, []);
2791
+ const addNextBar = useCallback(() => {
2792
+ if (!state.chart) return;
2793
+ const data = savedDataRef.current;
2794
+ const idx = currentIndexRef.current;
2795
+ if (idx >= data.length) {
2796
+ clearInterval_();
2797
+ setIsPaused(true);
2798
+ return;
2799
+ }
2800
+ const bar = data[idx];
2801
+ state.chart.updateData?.(bar);
2802
+ currentIndexRef.current = idx + 1;
2803
+ setBarIndex(idx + 1);
2804
+ }, [state.chart, clearInterval_]);
2805
+ const startInterval = useCallback(
2806
+ (currentSpeed) => {
2807
+ clearInterval_();
2808
+ intervalRef.current = setInterval(addNextBar, 1e3 / currentSpeed);
2809
+ },
2810
+ [addNextBar, clearInterval_]
2811
+ );
2812
+ const startReplay = useCallback(() => {
2813
+ if (!state.chart) return;
2814
+ const dataList = state.chart.getDataList();
2815
+ 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);
2822
+ state.chart.clearData?.();
2823
+ startInterval(speed);
2824
+ }, [state.chart, speed, startInterval]);
2825
+ const stopReplay = useCallback(() => {
2826
+ if (!state.chart) return;
2827
+ clearInterval_();
2828
+ const data = savedDataRef.current;
2829
+ if (data.length > 0) {
2830
+ state.chart.clearData?.();
2831
+ for (const bar of data) {
2832
+ state.chart.updateData?.(bar);
2833
+ }
2834
+ }
2835
+ savedDataRef.current = [];
2836
+ currentIndexRef.current = 0;
2837
+ setIsReplaying(false);
2838
+ setIsPaused(false);
2839
+ setBarIndex(0);
2840
+ setTotalBars(0);
2841
+ }, [state.chart, clearInterval_]);
2842
+ const togglePause = useCallback(() => {
2843
+ if (!isReplaying) return;
2844
+ if (isPaused) {
2845
+ startInterval(speed);
2846
+ setIsPaused(false);
2847
+ } else {
2848
+ clearInterval_();
2849
+ setIsPaused(true);
2850
+ }
2851
+ }, [isReplaying, isPaused, speed, startInterval, clearInterval_]);
2852
+ const stepForward = useCallback(() => {
2853
+ if (!isReplaying || !isPaused) return;
2854
+ addNextBar();
2855
+ }, [isReplaying, isPaused, addNextBar]);
2856
+ const stepBackward = useCallback(() => {
2857
+ if (!isReplaying || !isPaused || !state.chart) return;
2858
+ const idx = currentIndexRef.current;
2859
+ if (idx <= 1) return;
2860
+ const data = savedDataRef.current;
2861
+ state.chart.clearData?.();
2862
+ for (let i = 0; i < idx - 1; i++) {
2863
+ state.chart.updateData?.(data[i]);
2864
+ }
2865
+ currentIndexRef.current = idx - 1;
2866
+ setBarIndex(idx - 1);
2867
+ }, [isReplaying, isPaused, state.chart]);
2868
+ const seekTo = useCallback(
2869
+ (targetIndex) => {
2870
+ if (!isReplaying || !state.chart) return;
2871
+ const data = savedDataRef.current;
2872
+ const clamped = Math.max(0, Math.min(targetIndex, data.length));
2873
+ clearInterval_();
2874
+ setIsPaused(true);
2875
+ state.chart.clearData?.();
2876
+ for (let i = 0; i < clamped; i++) {
2877
+ state.chart.updateData?.(data[i]);
2878
+ }
2879
+ currentIndexRef.current = clamped;
2880
+ setBarIndex(clamped);
2881
+ },
2882
+ [isReplaying, state.chart, clearInterval_]
2883
+ );
2884
+ const setSpeed = useCallback(
2885
+ (newSpeed) => {
2886
+ setSpeedState(newSpeed);
2887
+ if (isReplaying && !isPaused) {
2888
+ startInterval(newSpeed);
2889
+ }
2890
+ },
2891
+ [isReplaying, isPaused, startInterval]
2892
+ );
2893
+ useEffect(() => {
2894
+ return () => {
2895
+ clearInterval_();
2896
+ };
2897
+ }, [clearInterval_]);
2898
+ return {
2899
+ isReplaying,
2900
+ isPaused,
2901
+ speed,
2902
+ barIndex,
2903
+ totalBars,
2904
+ startReplay,
2905
+ stopReplay,
2906
+ togglePause,
2907
+ stepForward,
2908
+ stepBackward,
2909
+ seekTo,
2910
+ setSpeed
2911
+ };
2912
+ }
2913
+ var DEFAULT_COLORS = [
2914
+ "#2196f3",
2915
+ "#ff9800",
2916
+ "#4caf50",
2917
+ "#e91e63",
2918
+ "#9c27b0",
2919
+ "#00bcd4",
2920
+ "#ff5722",
2921
+ "#8bc34a"
2922
+ ];
2923
+ var colorIndex = 0;
2924
+ var compareCounter = 0;
2925
+ function useCompare() {
2926
+ const { state, datafeed } = useKlinechartsUI();
2927
+ const [symbols, setSymbols] = useState([]);
2928
+ const indicatorsRef = useRef(/* @__PURE__ */ new Map());
2929
+ const addSymbol = useCallback(
2930
+ async (ticker, color) => {
2931
+ if (!state.chart || !datafeed) return;
2932
+ if (indicatorsRef.current.has(ticker)) return;
2933
+ const assignedColor = color ?? DEFAULT_COLORS[colorIndex++ % DEFAULT_COLORS.length];
2934
+ const mainDataList = state.chart.getDataList();
2935
+ if (!mainDataList || mainDataList.length === 0) return;
2936
+ const from = mainDataList[0].timestamp;
2937
+ const to = mainDataList[mainDataList.length - 1].timestamp;
2938
+ const compareData = await datafeed.getHistoryKLineData(
2939
+ { ticker },
2940
+ { ...state.period, label: "" },
2941
+ from,
2942
+ to
2943
+ );
2944
+ if (!compareData || compareData.length === 0) return;
2945
+ const compareMap = /* @__PURE__ */ new Map();
2946
+ for (const bar of compareData) {
2947
+ compareMap.set(bar.timestamp, bar.close);
2948
+ }
2949
+ let basePrice = null;
2950
+ for (const bar of mainDataList) {
2951
+ const close = compareMap.get(bar.timestamp);
2952
+ if (close != null) {
2953
+ basePrice = close;
2954
+ break;
2955
+ }
2956
+ }
2957
+ if (basePrice === null || basePrice === 0) return;
2958
+ const bp = basePrice;
2959
+ const normalizedData = mainDataList.map((mainBar) => {
2960
+ const close = compareMap.get(mainBar.timestamp);
2961
+ return {
2962
+ pct: close != null ? (close - bp) / bp * 100 : NaN
2963
+ };
2964
+ });
2965
+ const indicatorName = `__cmp_${++compareCounter}_${ticker}`;
2966
+ registerIndicator({
2967
+ name: indicatorName,
2968
+ shortName: ticker,
2969
+ figures: [
2970
+ {
2971
+ key: "pct",
2972
+ title: `${ticker} %: `,
2973
+ type: "line",
2974
+ styles: () => ({ color: assignedColor })
2975
+ }
2976
+ ],
2977
+ calc: () => normalizedData
2978
+ });
2979
+ const paneId = state.chart.createIndicator(
2980
+ { name: indicatorName },
2981
+ { isStack: true, pane: { id: "candle_pane" } }
2982
+ );
2983
+ indicatorsRef.current.set(ticker, {
2984
+ name: indicatorName,
2985
+ paneId: typeof paneId === "string" ? paneId : "candle_pane"
2986
+ });
2987
+ setSymbols((prev) => {
2988
+ if (prev.some((s) => s.ticker === ticker)) return prev;
2989
+ return [
2990
+ ...prev,
2991
+ { ticker, basePrice: bp, color: assignedColor, visible: true }
2992
+ ];
2993
+ });
2994
+ },
2995
+ [state.chart, state.period, datafeed]
2996
+ );
2997
+ const removeSymbol = useCallback(
2998
+ (ticker) => {
2999
+ const info = indicatorsRef.current.get(ticker);
3000
+ if (info && state.chart) {
3001
+ try {
3002
+ state.chart.removeIndicator({ name: info.name });
3003
+ } catch {
3004
+ }
3005
+ indicatorsRef.current.delete(ticker);
3006
+ }
3007
+ setSymbols((prev) => prev.filter((s) => s.ticker !== ticker));
3008
+ },
3009
+ [state.chart]
3010
+ );
3011
+ const toggleSymbol = useCallback(
3012
+ (ticker) => {
3013
+ const info = indicatorsRef.current.get(ticker);
3014
+ if (!info || !state.chart) return;
3015
+ setSymbols((prev) => {
3016
+ const sym = prev.find((s) => s.ticker === ticker);
3017
+ if (!sym) return prev;
3018
+ const newVisible = !sym.visible;
3019
+ state.chart?.overrideIndicator({
3020
+ name: info.name,
3021
+ visible: newVisible
3022
+ });
3023
+ return prev.map(
3024
+ (s) => s.ticker === ticker ? { ...s, visible: newVisible } : s
3025
+ );
3026
+ });
3027
+ },
3028
+ [state.chart]
3029
+ );
3030
+ const clearAll = useCallback(() => {
3031
+ indicatorsRef.current.forEach((info) => {
3032
+ try {
3033
+ state.chart?.removeIndicator({ name: info.name });
3034
+ } catch {
3035
+ }
3036
+ });
3037
+ indicatorsRef.current.clear();
3038
+ setSymbols([]);
3039
+ }, [state.chart]);
3040
+ useEffect(() => {
3041
+ return () => {
3042
+ indicatorsRef.current.forEach((info) => {
3043
+ try {
3044
+ state.chart?.removeIndicator({ name: info.name });
3045
+ } catch {
3046
+ }
3047
+ });
3048
+ indicatorsRef.current.clear();
3049
+ };
3050
+ }, [state.chart]);
3051
+ return { symbols, addSymbol, removeSymbol, toggleSymbol, clearAll };
3052
+ }
3053
+ var MEASURE_OVERLAY_ID = "__measure_line__";
3054
+ 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
+ );
3070
+ const cleanup = useCallback(() => {
3071
+ state.chart?.removeOverlay({ id: MEASURE_OVERLAY_ID });
3072
+ }, [state.chart]);
3073
+ const startMeasure = useCallback(() => {
3074
+ cleanup();
3075
+ setIsActive(true);
3076
+ setFromPoint(null);
3077
+ setResult(null);
3078
+ clickCountRef.current = 0;
3079
+ if (!state.chart) return;
3080
+ state.chart.createOverlay({
3081
+ name: "segment",
3082
+ id: MEASURE_OVERLAY_ID,
3083
+ groupId: "measure",
3084
+ styles: {
3085
+ line: {
3086
+ style: "dashed",
3087
+ color: "#FFD600",
3088
+ size: 1
3089
+ }
3090
+ },
3091
+ lock: true,
3092
+ onDrawEnd: (event) => {
3093
+ const overlay = event?.overlay;
3094
+ const points = overlay?.points;
3095
+ if (!points || points.length < 2) return;
3096
+ const chart = state.chart;
3097
+ if (!chart) return;
3098
+ const dataList = chart.getDataList();
3099
+ if (!dataList || dataList.length === 0) return;
3100
+ const makePoint = (p) => {
3101
+ const idx = Math.max(
3102
+ 0,
3103
+ Math.min(p.dataIndex ?? 0, dataList.length - 1)
3104
+ );
3105
+ const bar = dataList[idx];
3106
+ return {
3107
+ price: p.value ?? bar?.close ?? 0,
3108
+ timestamp: bar?.timestamp ?? 0,
3109
+ barIndex: idx
3110
+ };
3111
+ };
3112
+ const from = makePoint(points[0]);
3113
+ const to = makePoint(points[1]);
3114
+ setFromPoint(from);
3115
+ setResult(computeResult(from, to));
3116
+ setIsActive(false);
3117
+ }
3118
+ });
3119
+ }, [state.chart, cleanup, computeResult]);
3120
+ const cancelMeasure = useCallback(() => {
3121
+ cleanup();
3122
+ setIsActive(false);
3123
+ setFromPoint(null);
3124
+ clickCountRef.current = 0;
3125
+ }, [cleanup]);
3126
+ const clearResult = useCallback(() => {
3127
+ cleanup();
3128
+ setResult(null);
3129
+ setFromPoint(null);
3130
+ }, [cleanup]);
3131
+ useEffect(() => {
3132
+ return () => {
3133
+ state.chart?.removeOverlay({ id: MEASURE_OVERLAY_ID });
3134
+ };
3135
+ }, [state.chart]);
3136
+ return {
3137
+ isActive,
3138
+ startMeasure,
3139
+ cancelMeasure,
3140
+ result,
3141
+ clearResult,
3142
+ fromPoint
3143
+ };
3144
+ }
3145
+ var annotationCounter = 0;
3146
+ function useAnnotations() {
3147
+ const { state } = useKlinechartsUI();
3148
+ const [annotations, setAnnotations] = useState([]);
3149
+ const addAnnotation = useCallback(
3150
+ (text, price, timestamp, color) => {
3151
+ const id = `annotation_${++annotationCounter}`;
3152
+ const annotation = {
3153
+ id,
3154
+ text,
3155
+ price,
3156
+ timestamp,
3157
+ color
3158
+ };
3159
+ setAnnotations((prev) => [...prev, annotation]);
3160
+ if (state.chart) {
3161
+ state.chart.createOverlay({
3162
+ name: "simpleAnnotation",
3163
+ id,
3164
+ groupId: "annotations",
3165
+ points: [{ timestamp, value: price }],
3166
+ extendData: text,
3167
+ styles: color ? {
3168
+ text: {
3169
+ color
3170
+ }
3171
+ } : void 0,
3172
+ lock: true
3173
+ });
3174
+ }
3175
+ return id;
3176
+ },
3177
+ [state.chart]
3178
+ );
3179
+ const removeAnnotation = useCallback(
3180
+ (id) => {
3181
+ setAnnotations((prev) => prev.filter((a) => a.id !== id));
3182
+ state.chart?.removeOverlay({ id });
3183
+ },
3184
+ [state.chart]
3185
+ );
3186
+ const updateAnnotation = useCallback(
3187
+ (id, updates) => {
3188
+ setAnnotations(
3189
+ (prev) => prev.map((a) => a.id === id ? { ...a, ...updates } : a)
3190
+ );
3191
+ if (state.chart) {
3192
+ const overrideData = { id };
3193
+ if (updates.text !== void 0) {
3194
+ overrideData.extendData = updates.text;
3195
+ }
3196
+ if (updates.color !== void 0) {
3197
+ overrideData.styles = {
3198
+ text: {
3199
+ color: updates.color
3200
+ }
3201
+ };
3202
+ }
3203
+ state.chart.overrideOverlay(overrideData);
3204
+ }
3205
+ },
3206
+ [state.chart]
3207
+ );
3208
+ const clearAnnotations = useCallback(() => {
3209
+ setAnnotations((prev) => {
3210
+ for (const annotation of prev) {
3211
+ state.chart?.removeOverlay({ id: annotation.id });
3212
+ }
3213
+ return [];
3214
+ });
3215
+ }, [state.chart]);
3216
+ useEffect(() => {
3217
+ return () => {
3218
+ state.chart?.removeOverlay({ groupId: "annotations" });
3219
+ };
3220
+ }, [state.chart]);
3221
+ return {
3222
+ annotations,
3223
+ addAnnotation,
3224
+ removeAnnotation,
3225
+ updateAnnotation,
3226
+ clearAnnotations
3227
+ };
3228
+ }
2247
3229
 
2248
3230
  // src/utils/createDataLoader.ts
2249
3231
  function createDataLoader(datafeed, dispatch) {
@@ -2309,6 +3291,6 @@ function createDataLoader(datafeed, dispatch) {
2309
3291
  };
2310
3292
  }
2311
3293
 
2312
- export { CANDLE_TYPES, COMPARE_RULES, DEFAULT_PERIODS, DRAWING_CATEGORIES, INDICATOR_PARAMS, KlinechartsUIDispatchContext, KlinechartsUIProvider, KlinechartsUIStateContext, MAIN_INDICATORS, PRICE_AXIS_TYPES, SUB_INDICATORS, TIMEZONES, TOOLTIP_SHOW_RULES, YAXIS_POSITIONS, createDataLoader, useDrawingTools, useFullscreen, useIndicators, useKlinechartsUI, useKlinechartsUILoading, useKlinechartsUISettings, useKlinechartsUITheme, useLayoutManager, useOrderLines, usePeriods, useScreenshot, useScriptEditor, useSymbolSearch, useTimezone, useUndoRedo };
3294
+ export { CANDLE_TYPES, COMPARE_RULES, DEFAULT_PERIODS, DRAWING_CATEGORIES, INDICATOR_PARAMS, KlinechartsUIDispatchContext, KlinechartsUIProvider, KlinechartsUIStateContext, MAIN_INDICATORS, PRICE_AXIS_TYPES, SUB_INDICATORS, TIMEZONES, TOOLTIP_SHOW_RULES, YAXIS_POSITIONS, createDataLoader, useAlerts, useAnnotations, useCompare, useCrosshair, useDataExport, useDrawingTools, useFullscreen, useIndicators, useKlinechartsUI, useKlinechartsUILoading, useKlinechartsUISettings, useKlinechartsUITheme, useLayoutManager, useMeasure, useOrderLines, usePeriods, useReplay, useScreenshot, useScriptEditor, useSymbolSearch, useTimezone, useUndoRedo, useWatchlist };
2313
3295
  //# sourceMappingURL=index.js.map
2314
3296
  //# sourceMappingURL=index.js.map