react-klinecharts-ui 0.3.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/README.md CHANGED
@@ -206,6 +206,7 @@ interface KlinechartsUIState {
206
206
  periods: TerminalPeriod[]; // List of available timeframes
207
207
  mainIndicators: string[]; // Active main chart indicators
208
208
  subIndicators: Record<string, string>; // Active sub-indicators: { name → paneId }
209
+ indicatorAxes: Record<string, string>; // Custom Y-axis bindings: { indicatorId → yAxisId }
209
210
  styles: DeepPartial<Styles> | undefined; // Custom klinecharts styles
210
211
  screenshotUrl: string | null; // URL of the last screenshot
211
212
  }
@@ -225,6 +226,7 @@ type KlinechartsUIAction =
225
226
  | { type: "SET_LOADING"; isLoading: boolean }
226
227
  | { type: "SET_MAIN_INDICATORS"; indicators: string[] }
227
228
  | { type: "SET_SUB_INDICATORS"; indicators: Record<string, string> }
229
+ | { type: "SET_INDICATOR_AXES"; axes: Record<string, string> }
228
230
  | { type: "SET_STYLES"; styles: DeepPartial<Styles> | undefined }
229
231
  | { type: "SET_LOCALE"; locale: string }
230
232
  | { type: "SET_SCREENSHOT_URL"; url: string | null };
@@ -459,6 +461,9 @@ const {
459
461
  getIndicatorParams,
460
462
  isMainIndicatorActive,
461
463
  isSubIndicatorActive,
464
+ indicatorAxes,
465
+ getIndicatorAxis,
466
+ bindIndicatorToNewAxis,
462
467
  } = useIndicators();
463
468
  ```
464
469
 
@@ -484,9 +489,9 @@ interface IndicatorInfo {
484
489
 
485
490
  | Method | Description |
486
491
  | --------------------------------------------- | ------------------------------------------------------------- |
487
- | `addMainIndicator(name)` | Creates the indicator on `candle_pane` with id `main_${name}` |
492
+ | `addMainIndicator(name, options?)` | Creates the indicator on `candle_pane` with id `main_${name}`. Pass `{ yAxis }` to bind it to a secondary axis (see below) |
488
493
  | `removeMainIndicator(name)` | Removes the indicator and updates state |
489
- | `addSubIndicator(name)` | Creates the indicator on a new sub-pane with id `sub_${name}` |
494
+ | `addSubIndicator(name, options?)` | Creates the indicator on a new sub-pane with id `sub_${name}`. Also accepts `{ yAxis }` |
490
495
  | `removeSubIndicator(name)` | Removes the sub-indicator and its pane |
491
496
  | `toggleMainIndicator(name)` | `add` if inactive, `remove` if active |
492
497
  | `toggleSubIndicator(name)` | Same for sub-indicators |
@@ -497,6 +502,32 @@ interface IndicatorInfo {
497
502
  | `getIndicatorParams(name)` | Returns `[{ label, defaultValue }]` or `[]` if no parameters |
498
503
  | `isMainIndicatorActive(name)` | Quick active check |
499
504
  | `isSubIndicatorActive(name)` | Quick active check |
505
+ | `getIndicatorAxis(name, isMain)` | Returns the custom `yAxisId` an indicator is bound to, or `undefined` for the default axis |
506
+ | `bindIndicatorToNewAxis(name, isMain, yAxis?)`| Rebinds an existing indicator to a different Y-axis (omit `yAxis` to return it to the default axis) |
507
+
508
+ #### Secondary Y-axis binding (multiple Y-axes)
509
+
510
+ klinecharts v10 supports several independent Y-axes on one pane, so an indicator whose value range differs sharply from price (RSI 0–100, volume, …) can get its own scale instead of distorting the shared price axis or being forced into a separate sub-pane.
511
+
512
+ ```typescript
513
+ // Add RSI on the main pane, on its own left axis (price scale stays intact)
514
+ addMainIndicator("RSI", { yAxis: { id: "rsi_axis", position: "left" } });
515
+
516
+ // Later: move it back to the shared price axis
517
+ bindIndicatorToNewAxis("RSI", true);
518
+
519
+ // Or move an indicator already on the chart onto a dedicated right axis
520
+ bindIndicatorToNewAxis("VOL", true, { id: "vol_axis", position: "right" });
521
+ ```
522
+
523
+ `yAxis` is a klinecharts `YAxisOverride`; the key field is **`id`** — indicators sharing the same `id` share one axis. Useful extras: `position` (`"left" | "right"`), `name` (scale type: `"normal" | "percentage" | "log"`), `inside`, and `needWidget` (set `false` for an invisible axis — own scale, no labels).
524
+
525
+ This binding is a **persistent property, not a one-shot action**. It is tracked in provider state (`indicatorAxes`, keyed by indicator id) and preserved across:
526
+
527
+ - **undo/redo** — toggling an indicator off and back on (`useUndoRedo`) restores it on the same axis, not the price axis;
528
+ - **layout presets** — `useLayoutManager` save/load reproduces each indicator's axis 1:1.
529
+
530
+ > Note: `bindIndicatorToNewAxis` removes and recreates the indicator (v10 `overrideIndicator` cannot rebind an axis), preserving calc params, styles and visibility. The rebind action itself is not pushed onto the undo stack.
500
531
 
501
532
  #### Available indicators
502
533
 
package/dist/index.cjs CHANGED
@@ -58,6 +58,8 @@ function reducer(state, action) {
58
58
  return { ...state, mainIndicators: action.indicators };
59
59
  case "SET_SUB_INDICATORS":
60
60
  return { ...state, subIndicators: action.indicators };
61
+ case "SET_INDICATOR_AXES":
62
+ return { ...state, indicatorAxes: action.axes };
61
63
  case "SET_STYLES":
62
64
  return { ...state, styles: action.styles };
63
65
  case "SET_LOCALE":
@@ -120,6 +122,7 @@ function KlinechartsUIProvider({
120
122
  (acc, name) => ({ ...acc, [name]: "" }),
121
123
  {}
122
124
  ),
125
+ indicatorAxes: {},
123
126
  styles: opts.styles,
124
127
  screenshotUrl: null
125
128
  })
@@ -667,62 +670,95 @@ function useIndicators() {
667
670
  ];
668
671
  }, [state.subIndicators]);
669
672
  const addMainIndicator = react.useCallback(
670
- (name) => {
673
+ (name, options) => {
671
674
  if (state.mainIndicators.includes(name)) return;
675
+ const yAxis = options?.yAxis;
672
676
  state.chart?.createIndicator(
673
- { name, id: `main_${name}`, paneId: "candle_pane" },
674
- true,
675
- { id: "candle_pane" }
677
+ { name, id: `main_${name}` },
678
+ { isStack: true, pane: { id: "candle_pane" }, yAxis }
676
679
  );
677
680
  const newIndicators = [...state.mainIndicators, name];
678
681
  dispatch({ type: "SET_MAIN_INDICATORS", indicators: newIndicators });
682
+ if (yAxis?.id) {
683
+ dispatch({
684
+ type: "SET_INDICATOR_AXES",
685
+ axes: { ...state.indicatorAxes, [`main_${name}`]: yAxis.id }
686
+ });
687
+ }
679
688
  undoRedoListenerRef.current?.({
680
689
  type: "indicator_toggled",
681
- data: { name, wasActive: false, isMain: true, paneId: "candle_pane" }
690
+ data: {
691
+ name,
692
+ wasActive: false,
693
+ isMain: true,
694
+ paneId: "candle_pane",
695
+ yAxisId: yAxis?.id
696
+ }
682
697
  });
683
698
  },
684
- [state.chart, state.mainIndicators, dispatch, undoRedoListenerRef]
699
+ [state.chart, state.mainIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
685
700
  );
686
701
  const removeMainIndicator = react.useCallback(
687
702
  (name) => {
688
- state.chart?.removeIndicator({ id: `main_${name}` });
703
+ const id = `main_${name}`;
704
+ const yAxisId = state.indicatorAxes[id];
705
+ state.chart?.removeIndicator({ id });
689
706
  const newIndicators = state.mainIndicators.filter((n) => n !== name);
690
707
  dispatch({ type: "SET_MAIN_INDICATORS", indicators: newIndicators });
708
+ if (yAxisId) {
709
+ const nextAxes = { ...state.indicatorAxes };
710
+ delete nextAxes[id];
711
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
712
+ }
691
713
  undoRedoListenerRef.current?.({
692
714
  type: "indicator_toggled",
693
- data: { name, wasActive: true, isMain: true, paneId: "candle_pane" }
715
+ data: { name, wasActive: true, isMain: true, paneId: "candle_pane", yAxisId }
694
716
  });
695
717
  },
696
- [state.chart, state.mainIndicators, dispatch, undoRedoListenerRef]
718
+ [state.chart, state.mainIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
697
719
  );
698
720
  const addSubIndicator = react.useCallback(
699
- (name) => {
721
+ (name, options) => {
700
722
  if (name in state.subIndicators) return;
701
723
  const id = `sub_${name}`;
702
- state.chart?.createIndicator({ name, id });
724
+ const yAxis = options?.yAxis;
725
+ state.chart?.createIndicator({ name, id }, { yAxis });
703
726
  const paneId = state.chart?.getIndicators({ id })?.[0]?.paneId ?? "";
704
727
  const newIndicators = { ...state.subIndicators, [name]: paneId };
705
728
  dispatch({ type: "SET_SUB_INDICATORS", indicators: newIndicators });
729
+ if (yAxis?.id) {
730
+ dispatch({
731
+ type: "SET_INDICATOR_AXES",
732
+ axes: { ...state.indicatorAxes, [id]: yAxis.id }
733
+ });
734
+ }
706
735
  undoRedoListenerRef.current?.({
707
736
  type: "indicator_toggled",
708
- data: { name, wasActive: false, isMain: false, paneId }
737
+ data: { name, wasActive: false, isMain: false, paneId, yAxisId: yAxis?.id }
709
738
  });
710
739
  },
711
- [state.chart, state.subIndicators, dispatch, undoRedoListenerRef]
740
+ [state.chart, state.subIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
712
741
  );
713
742
  const removeSubIndicator = react.useCallback(
714
743
  (name) => {
744
+ const id = `sub_${name}`;
715
745
  const paneId = state.subIndicators[name] ?? "";
716
- state.chart?.removeIndicator({ id: `sub_${name}` });
746
+ const yAxisId = state.indicatorAxes[id];
747
+ state.chart?.removeIndicator({ id });
717
748
  const newIndicators = { ...state.subIndicators };
718
749
  delete newIndicators[name];
719
750
  dispatch({ type: "SET_SUB_INDICATORS", indicators: newIndicators });
751
+ if (yAxisId) {
752
+ const nextAxes = { ...state.indicatorAxes };
753
+ delete nextAxes[id];
754
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
755
+ }
720
756
  undoRedoListenerRef.current?.({
721
757
  type: "indicator_toggled",
722
- data: { name, wasActive: true, isMain: false, paneId }
758
+ data: { name, wasActive: true, isMain: false, paneId, yAxisId }
723
759
  });
724
760
  },
725
- [state.chart, state.subIndicators, dispatch, undoRedoListenerRef]
761
+ [state.chart, state.subIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
726
762
  );
727
763
  const toggleMainIndicator = react.useCallback(
728
764
  (name) => {
@@ -752,8 +788,10 @@ function useIndicators() {
752
788
  [state.chart]
753
789
  );
754
790
  const updateIndicatorParams = react.useCallback(
755
- (name, paneId, params) => {
756
- state.chart?.overrideIndicator({ name, paneId, calcParams: params });
791
+ // `paneId` is kept in the signature for API stability; klinecharts v10
792
+ // `overrideIndicator` no longer accepts it and targets by name/id instead.
793
+ (name, _paneId, params) => {
794
+ state.chart?.overrideIndicator({ name, calcParams: params });
757
795
  },
758
796
  [state.chart]
759
797
  );
@@ -774,11 +812,9 @@ function useIndicators() {
774
812
  {
775
813
  name,
776
814
  id: `main_${name}`,
777
- paneId: "candle_pane",
778
815
  ...prevCalcParams ? { calcParams: prevCalcParams } : {}
779
816
  },
780
- true,
781
- { id: "candle_pane" }
817
+ { isStack: true, pane: { id: "candle_pane" } }
782
818
  );
783
819
  const newSub = { ...state.subIndicators };
784
820
  delete newSub[name];
@@ -894,7 +930,7 @@ function useIndicators() {
894
930
  if (sub.styles) {
895
931
  state.chart.overrideIndicator({
896
932
  name: sub.name,
897
- paneId,
933
+ id,
898
934
  styles: sub.styles
899
935
  });
900
936
  }
@@ -903,6 +939,44 @@ function useIndicators() {
903
939
  },
904
940
  [state.chart, state.subIndicators, dispatch]
905
941
  );
942
+ const getIndicatorAxis = react.useCallback(
943
+ (name, isMain) => state.indicatorAxes[isMain ? `main_${name}` : `sub_${name}`],
944
+ [state.indicatorAxes]
945
+ );
946
+ const bindIndicatorToNewAxis = react.useCallback(
947
+ (name, isMain, yAxis) => {
948
+ if (!state.chart) return;
949
+ const id = isMain ? `main_${name}` : `sub_${name}`;
950
+ const current = state.chart.getIndicators({ id })?.[0];
951
+ if (!current) return;
952
+ const calcParams = current.calcParams;
953
+ const styles = current.styles;
954
+ const visible = current.visible ?? true;
955
+ const paneId = isMain ? "candle_pane" : current.paneId ?? state.subIndicators[name] ?? "";
956
+ state.chart.removeIndicator({ id });
957
+ state.chart.createIndicator(
958
+ {
959
+ name,
960
+ id,
961
+ ...calcParams ? { calcParams } : {},
962
+ visible
963
+ },
964
+ {
965
+ isStack: isMain,
966
+ ...paneId ? { pane: { id: paneId } } : {},
967
+ yAxis
968
+ }
969
+ );
970
+ if (styles) {
971
+ state.chart.overrideIndicator({ name, id, styles });
972
+ }
973
+ const nextAxes = { ...state.indicatorAxes };
974
+ if (yAxis?.id) nextAxes[id] = yAxis.id;
975
+ else delete nextAxes[id];
976
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
977
+ },
978
+ [state.chart, state.indicatorAxes, state.subIndicators, dispatch]
979
+ );
906
980
  return {
907
981
  mainIndicators,
908
982
  subIndicators,
@@ -926,7 +1000,10 @@ function useIndicators() {
926
1000
  collapseSubIndicator,
927
1001
  expandSubIndicator,
928
1002
  isSubIndicatorCollapsed,
929
- reorderSubIndicator
1003
+ reorderSubIndicator,
1004
+ indicatorAxes: state.indicatorAxes,
1005
+ getIndicatorAxis,
1006
+ bindIndicatorToNewAxis
930
1007
  };
931
1008
  }
932
1009
 
@@ -1236,20 +1313,23 @@ function useKlinechartsUISettings() {
1236
1313
  () => TOOLTIP_SHOW_RULES.map((r) => ({ key: r, localeKey: r })),
1237
1314
  []
1238
1315
  );
1316
+ const applyPaneAxis = react.useCallback(
1317
+ (axis) => {
1318
+ state.chart?.overrideYAxis?.({ paneId: "candle_pane", ...axis });
1319
+ },
1320
+ [state.chart]
1321
+ );
1239
1322
  const hasAppliedInitial = react.useRef(false);
1240
1323
  react.useEffect(() => {
1241
1324
  if (!state.chart || hasAppliedInitial.current) return;
1242
1325
  hasAppliedInitial.current = true;
1243
- const needsPaneOptions = settings.reverseCoordinate || settings.priceAxisType !== "normal" || settings.yAxisPosition !== "right" || settings.yAxisInside;
1244
- if (needsPaneOptions) {
1245
- state.chart.setPaneOptions({
1246
- id: "candle_pane",
1247
- axis: {
1248
- ...settings.priceAxisType !== "normal" && { name: settings.priceAxisType },
1249
- ...settings.reverseCoordinate && { reverse: true },
1250
- ...settings.yAxisPosition !== "right" && { position: settings.yAxisPosition },
1251
- ...settings.yAxisInside && { inside: true }
1252
- }
1326
+ const needsAxisOverride = settings.reverseCoordinate || settings.priceAxisType !== "normal" || settings.yAxisPosition !== "right" || settings.yAxisInside;
1327
+ if (needsAxisOverride) {
1328
+ applyPaneAxis({
1329
+ ...settings.priceAxisType !== "normal" && { name: settings.priceAxisType },
1330
+ ...settings.reverseCoordinate && { reverse: true },
1331
+ ...settings.yAxisPosition !== "right" && { position: settings.yAxisPosition },
1332
+ ...settings.yAxisInside && { inside: true }
1253
1333
  });
1254
1334
  }
1255
1335
  if (settings.showIndicatorLastValue) {
@@ -1272,12 +1352,6 @@ function useKlinechartsUISettings() {
1272
1352
  },
1273
1353
  [state.chart]
1274
1354
  );
1275
- const applyPaneAxis = react.useCallback(
1276
- (axis) => {
1277
- state.chart?.setPaneOptions({ id: "candle_pane", axis });
1278
- },
1279
- [state.chart]
1280
- );
1281
1355
  const setCandleType = react.useCallback(
1282
1356
  (type) => {
1283
1357
  applyStyle("candle.type", type);
@@ -1425,11 +1499,8 @@ function useKlinechartsUISettings() {
1425
1499
  const resetToDefaults = react.useCallback(() => {
1426
1500
  setSettings(defaultSettings);
1427
1501
  state.chart?.setStyles(state.theme);
1428
- state.chart?.setPaneOptions({
1429
- id: "candle_pane",
1430
- axis: { name: "normal", reverse: false, position: "right", inside: false }
1431
- });
1432
- }, [state.chart, state.theme]);
1502
+ applyPaneAxis({ name: "normal", reverse: false, position: "right", inside: false });
1503
+ }, [state.chart, state.theme, applyPaneAxis]);
1433
1504
  return {
1434
1505
  ...settings,
1435
1506
  candleTypes,
@@ -1686,21 +1757,28 @@ function useUndoRedo() {
1686
1757
  break;
1687
1758
  }
1688
1759
  case "indicator_toggled": {
1689
- const { name, wasActive, isMain, paneId } = action.data;
1760
+ const { name, wasActive, isMain, paneId, yAxisId } = action.data;
1761
+ const id = isMain ? `main_${name}` : `sub_${name}`;
1690
1762
  if (wasActive) {
1691
1763
  if (isMain) {
1692
1764
  state.chart.createIndicator(
1693
- { name, id: `main_${name}`, paneId: "candle_pane" },
1694
- true,
1695
- { id: "candle_pane" }
1765
+ { name, id },
1766
+ {
1767
+ isStack: true,
1768
+ pane: { id: "candle_pane" },
1769
+ ...yAxisId ? { yAxis: { id: yAxisId } } : {}
1770
+ }
1696
1771
  );
1697
1772
  dispatch({
1698
1773
  type: "SET_MAIN_INDICATORS",
1699
1774
  indicators: [...state.mainIndicators, name]
1700
1775
  });
1701
1776
  } else {
1702
- state.chart.createIndicator({ name, id: `sub_${name}` });
1703
- const newPaneId = state.chart.getIndicators({ id: `sub_${name}` })?.[0]?.paneId ?? "";
1777
+ state.chart.createIndicator(
1778
+ { name, id },
1779
+ yAxisId ? { yAxis: { id: yAxisId } } : void 0
1780
+ );
1781
+ const newPaneId = state.chart.getIndicators({ id })?.[0]?.paneId ?? "";
1704
1782
  dispatch({
1705
1783
  type: "SET_SUB_INDICATORS",
1706
1784
  indicators: {
@@ -1709,9 +1787,15 @@ function useUndoRedo() {
1709
1787
  }
1710
1788
  });
1711
1789
  }
1790
+ if (yAxisId) {
1791
+ dispatch({
1792
+ type: "SET_INDICATOR_AXES",
1793
+ axes: { ...state.indicatorAxes, [id]: yAxisId }
1794
+ });
1795
+ }
1712
1796
  } else {
1797
+ state.chart.removeIndicator({ id });
1713
1798
  if (isMain) {
1714
- state.chart.removeIndicator({ id: `main_${name}` });
1715
1799
  dispatch({
1716
1800
  type: "SET_MAIN_INDICATORS",
1717
1801
  indicators: state.mainIndicators.filter(
@@ -1719,7 +1803,6 @@ function useUndoRedo() {
1719
1803
  )
1720
1804
  });
1721
1805
  } else {
1722
- state.chart.removeIndicator({ id: `sub_${name}` });
1723
1806
  const newSub = { ...state.subIndicators };
1724
1807
  delete newSub[name];
1725
1808
  dispatch({
@@ -1727,6 +1810,11 @@ function useUndoRedo() {
1727
1810
  indicators: newSub
1728
1811
  });
1729
1812
  }
1813
+ if (state.indicatorAxes[id]) {
1814
+ const nextAxes = { ...state.indicatorAxes };
1815
+ delete nextAxes[id];
1816
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
1817
+ }
1730
1818
  }
1731
1819
  setRedoStack((prev) => [
1732
1820
  ...prev,
@@ -1736,7 +1824,8 @@ function useUndoRedo() {
1736
1824
  name,
1737
1825
  wasActive: !wasActive,
1738
1826
  isMain,
1739
- paneId
1827
+ paneId,
1828
+ yAxisId
1740
1829
  }
1741
1830
  }
1742
1831
  ]);
@@ -1746,7 +1835,7 @@ function useUndoRedo() {
1746
1835
  } finally {
1747
1836
  isProcessingRef.current = false;
1748
1837
  }
1749
- }, [undoStack, state.chart, state.mainIndicators, state.subIndicators, dispatch]);
1838
+ }, [undoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
1750
1839
  const redo = react.useCallback(() => {
1751
1840
  if (redoStack.length === 0 || !state.chart || isProcessingRef.current)
1752
1841
  return;
@@ -1798,21 +1887,28 @@ function useUndoRedo() {
1798
1887
  break;
1799
1888
  }
1800
1889
  case "indicator_toggled": {
1801
- const { name, wasActive, isMain } = action.data;
1890
+ const { name, wasActive, isMain, paneId, yAxisId } = action.data;
1891
+ const id = isMain ? `main_${name}` : `sub_${name}`;
1802
1892
  if (wasActive) {
1803
1893
  if (isMain) {
1804
1894
  state.chart.createIndicator(
1805
- { name, id: `main_${name}`, paneId: "candle_pane" },
1806
- true,
1807
- { id: "candle_pane" }
1895
+ { name, id },
1896
+ {
1897
+ isStack: true,
1898
+ pane: { id: "candle_pane" },
1899
+ ...yAxisId ? { yAxis: { id: yAxisId } } : {}
1900
+ }
1808
1901
  );
1809
1902
  dispatch({
1810
1903
  type: "SET_MAIN_INDICATORS",
1811
1904
  indicators: [...state.mainIndicators, name]
1812
1905
  });
1813
1906
  } else {
1814
- state.chart.createIndicator({ name, id: `sub_${name}` });
1815
- const newPaneId = state.chart.getIndicators({ id: `sub_${name}` })?.[0]?.paneId ?? "";
1907
+ state.chart.createIndicator(
1908
+ { name, id },
1909
+ yAxisId ? { yAxis: { id: yAxisId } } : void 0
1910
+ );
1911
+ const newPaneId = state.chart.getIndicators({ id })?.[0]?.paneId ?? "";
1816
1912
  dispatch({
1817
1913
  type: "SET_SUB_INDICATORS",
1818
1914
  indicators: {
@@ -1821,9 +1917,15 @@ function useUndoRedo() {
1821
1917
  }
1822
1918
  });
1823
1919
  }
1920
+ if (yAxisId) {
1921
+ dispatch({
1922
+ type: "SET_INDICATOR_AXES",
1923
+ axes: { ...state.indicatorAxes, [id]: yAxisId }
1924
+ });
1925
+ }
1824
1926
  } else {
1927
+ state.chart.removeIndicator({ id });
1825
1928
  if (isMain) {
1826
- state.chart.removeIndicator({ id: `main_${name}` });
1827
1929
  dispatch({
1828
1930
  type: "SET_MAIN_INDICATORS",
1829
1931
  indicators: state.mainIndicators.filter(
@@ -1831,7 +1933,6 @@ function useUndoRedo() {
1831
1933
  )
1832
1934
  });
1833
1935
  } else {
1834
- state.chart.removeIndicator({ id: `sub_${name}` });
1835
1936
  const newSub = { ...state.subIndicators };
1836
1937
  delete newSub[name];
1837
1938
  dispatch({
@@ -1839,6 +1940,11 @@ function useUndoRedo() {
1839
1940
  indicators: newSub
1840
1941
  });
1841
1942
  }
1943
+ if (state.indicatorAxes[id]) {
1944
+ const nextAxes = { ...state.indicatorAxes };
1945
+ delete nextAxes[id];
1946
+ dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
1947
+ }
1842
1948
  }
1843
1949
  setUndoStack((prev) => [
1844
1950
  ...prev,
@@ -1847,7 +1953,9 @@ function useUndoRedo() {
1847
1953
  data: {
1848
1954
  name,
1849
1955
  wasActive: !wasActive,
1850
- isMain
1956
+ isMain,
1957
+ paneId,
1958
+ yAxisId
1851
1959
  }
1852
1960
  }
1853
1961
  ]);
@@ -1857,7 +1965,7 @@ function useUndoRedo() {
1857
1965
  } finally {
1858
1966
  isProcessingRef.current = false;
1859
1967
  }
1860
- }, [redoStack, state.chart, state.mainIndicators, state.subIndicators, dispatch]);
1968
+ }, [redoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
1861
1969
  react.useEffect(() => {
1862
1970
  const handleKeyDown = (e) => {
1863
1971
  const isCtrlOrMeta = e.ctrlKey || e.metaKey;
@@ -1931,12 +2039,14 @@ function useLayoutManager() {
1931
2039
  if (indicatorMap) {
1932
2040
  indicatorMap.forEach((paneIndicators, paneId) => {
1933
2041
  paneIndicators.forEach((indicator) => {
2042
+ const yAxisId = state.indicatorAxes[indicator.id];
1934
2043
  indicators2.push({
1935
2044
  paneId,
1936
2045
  name: indicator.name,
1937
2046
  calcParams: indicator.calcParams,
1938
2047
  visible: indicator.visible,
1939
- ...indicator.styles ? { styles: indicator.styles } : {}
2048
+ ...indicator.styles ? { styles: indicator.styles } : {},
2049
+ ...yAxisId ? { yAxisId } : {}
1940
2050
  });
1941
2051
  });
1942
2052
  });
@@ -1964,7 +2074,7 @@ function useLayoutManager() {
1964
2074
  indicators: indicators2,
1965
2075
  drawings
1966
2076
  };
1967
- }, [state.chart, state.symbol, state.period]);
2077
+ }, [state.chart, state.symbol, state.period, state.indicatorAxes]);
1968
2078
  const saveLayout = react.useCallback(
1969
2079
  (name) => {
1970
2080
  const chartState = serializeState();
@@ -2012,25 +2122,35 @@ function useLayoutManager() {
2012
2122
  }
2013
2123
  const newMainIndicators = [];
2014
2124
  const newSubIndicators = {};
2125
+ const restoredAxes = {};
2015
2126
  if (chartState.indicators) {
2016
2127
  for (const ind of chartState.indicators) {
2128
+ const isMain = ind.paneId === "candle_pane";
2129
+ const id2 = isMain ? `main_${ind.name}` : `sub_${ind.name}`;
2017
2130
  chart.createIndicator(
2018
2131
  {
2019
2132
  name: ind.name,
2133
+ id: id2,
2020
2134
  calcParams: ind.calcParams,
2021
2135
  visible: ind.visible
2022
2136
  },
2023
- ind.paneId !== "candle_pane",
2024
- { id: ind.paneId }
2137
+ {
2138
+ isStack: ind.paneId !== "candle_pane",
2139
+ pane: { id: ind.paneId },
2140
+ ...ind.yAxisId ? { yAxis: { id: ind.yAxisId } } : {}
2141
+ }
2025
2142
  );
2026
2143
  if (ind.styles) {
2027
2144
  chart.overrideIndicator({
2028
2145
  name: ind.name,
2029
- paneId: ind.paneId,
2146
+ id: id2,
2030
2147
  styles: ind.styles
2031
2148
  });
2032
2149
  }
2033
- if (ind.paneId === "candle_pane") {
2150
+ if (ind.yAxisId) {
2151
+ restoredAxes[id2] = ind.yAxisId;
2152
+ }
2153
+ if (isMain) {
2034
2154
  newMainIndicators.push(ind.name);
2035
2155
  } else {
2036
2156
  newSubIndicators[ind.name] = ind.paneId;
@@ -2045,6 +2165,10 @@ function useLayoutManager() {
2045
2165
  type: "SET_SUB_INDICATORS",
2046
2166
  indicators: newSubIndicators
2047
2167
  });
2168
+ dispatch({
2169
+ type: "SET_INDICATOR_AXES",
2170
+ axes: restoredAxes
2171
+ });
2048
2172
  if (chartState.drawings) {
2049
2173
  for (const drawing of chartState.drawings) {
2050
2174
  chart.createOverlay({
@@ -2270,8 +2394,7 @@ ${code}`
2270
2394
  if (placement === "main") {
2271
2395
  paneId = chart.createIndicator(
2272
2396
  { name: indicatorName },
2273
- true,
2274
- { id: "candle_pane" }
2397
+ { isStack: true, pane: { id: "candle_pane" } }
2275
2398
  );
2276
2399
  } else {
2277
2400
  paneId = chart.createIndicator({ name: indicatorName });
@@ -2856,8 +2979,7 @@ function useCompare() {
2856
2979
  });
2857
2980
  const paneId = state.chart.createIndicator(
2858
2981
  { name: indicatorName },
2859
- true,
2860
- { id: "candle_pane" }
2982
+ { isStack: true, pane: { id: "candle_pane" } }
2861
2983
  );
2862
2984
  indicatorsRef.current.set(ticker, {
2863
2985
  name: indicatorName,