react-klinecharts-ui 0.5.0 → 1.0.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,7 +1,7 @@
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
- import { createContext, useContext, useReducer, useRef, useCallback, useEffect, useMemo, useState } from 'react';
4
- import { registerOverlay, registerIndicator } from 'react-klinecharts';
1
+ import { registerExtensions, TA_default, ensureAlertLineRegistered } from './chunk-JD4NBJ5F.js';
2
+ export { TA_default as TA, abcd_default as abcd, alertLine_default as alertLine, 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-JD4NBJ5F.js';
3
+ import { createContext, useContext, useReducer, useRef, useEffect, useCallback, useMemo, useState, useId } from 'react';
4
+ import { registerOverlay, registerHotkey, getHotkey, getSupportedHotkeys, registerIndicator } from 'klinecharts';
5
5
  import { jsx } from 'react/jsx-runtime';
6
6
 
7
7
  var KlinechartsUIStateContext = createContext(null);
@@ -63,6 +63,25 @@ function reducer(state, action) {
63
63
  return { ...state, indicatorVisibility: action.visibility };
64
64
  case "SET_ALERTS":
65
65
  return { ...state, alerts: action.alerts };
66
+ case "ADD_ALERT":
67
+ return { ...state, alerts: [...state.alerts, action.alert] };
68
+ case "REMOVE_ALERT":
69
+ return {
70
+ ...state,
71
+ alerts: state.alerts.filter((a) => a.id !== action.id)
72
+ };
73
+ case "CLEAR_ALERTS":
74
+ return { ...state, alerts: [] };
75
+ case "MARK_ALERT_TRIGGERED": {
76
+ if (action.ids.length === 0) return state;
77
+ const triggered = new Set(action.ids);
78
+ return {
79
+ ...state,
80
+ alerts: state.alerts.map(
81
+ (a) => triggered.has(a.id) ? { ...a, triggered: true } : a
82
+ )
83
+ };
84
+ }
66
85
  case "SET_MEASURE":
67
86
  return { ...state, measure: { ...state.measure, ...action.measure } };
68
87
  case "SET_REPLAY":
@@ -152,7 +171,6 @@ function KlinechartsUIProvider({
152
171
  const replayIndexRef = useRef(0);
153
172
  const alertPrevCloseRef = useRef(null);
154
173
  const stateRef = useRef(state);
155
- stateRef.current = state;
156
174
  const callbacksRef = useRef({
157
175
  onStateChange,
158
176
  onSymbolChange,
@@ -162,15 +180,18 @@ function KlinechartsUIProvider({
162
180
  onMainIndicatorsChange,
163
181
  onSubIndicatorsChange
164
182
  });
165
- callbacksRef.current = {
166
- onStateChange,
167
- onSymbolChange,
168
- onPeriodChange,
169
- onThemeChange,
170
- onTimezoneChange,
171
- onMainIndicatorsChange,
172
- onSubIndicatorsChange
173
- };
183
+ useEffect(() => {
184
+ stateRef.current = state;
185
+ callbacksRef.current = {
186
+ onStateChange,
187
+ onSymbolChange,
188
+ onPeriodChange,
189
+ onThemeChange,
190
+ onTimezoneChange,
191
+ onMainIndicatorsChange,
192
+ onSubIndicatorsChange
193
+ };
194
+ });
174
195
  const enhancedDispatch = useCallback((action) => {
175
196
  const prevState = stateRef.current;
176
197
  const newState = reducer(prevState, action);
@@ -200,6 +221,12 @@ function KlinechartsUIProvider({
200
221
  }
201
222
  }, []);
202
223
  const extraOverlaysRef = useRef(extraOverlays);
224
+ const datafeedRef = useRef(datafeed);
225
+ const onSettingsChangeRef = useRef(onSettingsChange);
226
+ useEffect(() => {
227
+ datafeedRef.current = datafeed;
228
+ onSettingsChangeRef.current = onSettingsChange;
229
+ });
203
230
  useEffect(() => {
204
231
  if (shouldRegister) {
205
232
  registerExtensions();
@@ -224,26 +251,29 @@ function KlinechartsUIProvider({
224
251
  alertPrevCloseRef.current = currentClose;
225
252
  if (prevClose === null) return;
226
253
  const alerts = stateRef.current.alerts;
227
- let changed = false;
228
- const next = alerts.map((alert) => {
229
- if (alert.triggered) return alert;
254
+ const triggeredIds = [];
255
+ for (const alert of alerts) {
256
+ if (alert.triggered) continue;
230
257
  const crossedUp = prevClose < alert.price && currentClose >= alert.price;
231
258
  const crossedDown = prevClose > alert.price && currentClose <= alert.price;
232
259
  const shouldTrigger = alert.condition === "crossing_up" ? crossedUp : alert.condition === "crossing_down" ? crossedDown : crossedUp || crossedDown;
233
260
  if (shouldTrigger) {
234
- changed = true;
235
- const triggered = { ...alert, triggered: true };
236
- alertTriggeredListenerRef.current?.(triggered);
237
- return triggered;
261
+ triggeredIds.push(alert.id);
262
+ alertTriggeredListenerRef.current?.({ ...alert, triggered: true });
238
263
  }
239
- return alert;
240
- });
241
- if (changed) {
242
- enhancedDispatch({ type: "SET_ALERTS", alerts: next });
264
+ }
265
+ if (triggeredIds.length > 0) {
266
+ enhancedDispatch({
267
+ type: "MARK_ALERT_TRIGGERED",
268
+ ids: triggeredIds
269
+ });
243
270
  }
244
271
  }, 1e3);
245
272
  return () => clearInterval(interval);
246
273
  }, [state.chart, hasAlerts, enhancedDispatch]);
274
+ useEffect(() => {
275
+ alertPrevCloseRef.current = null;
276
+ }, [state.symbol, state.period]);
247
277
  useEffect(() => {
248
278
  return () => {
249
279
  if (replayIntervalRef.current !== null) {
@@ -255,8 +285,8 @@ function KlinechartsUIProvider({
255
285
  const dispatchValue = useMemo(
256
286
  () => ({
257
287
  dispatch: enhancedDispatch,
258
- datafeed,
259
- onSettingsChange,
288
+ datafeed: datafeedRef.current,
289
+ onSettingsChange: onSettingsChangeRef.current,
260
290
  fullscreenContainerRef,
261
291
  undoRedoListenerRef,
262
292
  alertTriggeredListenerRef,
@@ -264,7 +294,8 @@ function KlinechartsUIProvider({
264
294
  replaySavedDataRef,
265
295
  replayIndexRef
266
296
  }),
267
- [enhancedDispatch, datafeed, onSettingsChange]
297
+ // eslint-disable-next-line react-hooks/exhaustive-deps
298
+ [enhancedDispatch]
268
299
  );
269
300
  return /* @__PURE__ */ jsx(KlinechartsUIStateContext.Provider, { value: state, children: /* @__PURE__ */ jsx(KlinechartsUIDispatchContext.Provider, { value: dispatchValue, children }) });
270
301
  }
@@ -1222,15 +1253,17 @@ function useDrawingTools() {
1222
1253
  const [isVisible, setIsVisible] = useState(true);
1223
1254
  const [autoRetrigger, setAutoRetrigger] = useState(true);
1224
1255
  const activeToolRef = useRef(activeTool);
1225
- activeToolRef.current = activeTool;
1226
1256
  const autoRetriggerRef = useRef(autoRetrigger);
1227
- autoRetriggerRef.current = autoRetrigger;
1228
1257
  const isLockedRef = useRef(isLocked);
1229
- isLockedRef.current = isLocked;
1230
1258
  const isVisibleRef = useRef(isVisible);
1231
- isVisibleRef.current = isVisible;
1232
1259
  const magnetModeRef = useRef(magnetMode);
1233
- magnetModeRef.current = magnetMode;
1260
+ useEffect(() => {
1261
+ activeToolRef.current = activeTool;
1262
+ autoRetriggerRef.current = autoRetrigger;
1263
+ isLockedRef.current = isLocked;
1264
+ isVisibleRef.current = isVisible;
1265
+ magnetModeRef.current = magnetMode;
1266
+ });
1234
1267
  const categories = useMemo(
1235
1268
  () => DRAWING_CATEGORIES.map((cat) => ({
1236
1269
  key: cat.key,
@@ -1241,6 +1274,8 @@ function useDrawingTools() {
1241
1274
  })),
1242
1275
  []
1243
1276
  );
1277
+ const createOverlayForToolRef = useRef(() => {
1278
+ });
1244
1279
  const createOverlayForTool = useCallback(
1245
1280
  (name) => {
1246
1281
  const mode = magnetModeRef.current === "strong" ? "strong_magnet" : magnetModeRef.current === "weak" ? "weak_magnet" : "normal";
@@ -1266,7 +1301,7 @@ function useDrawingTools() {
1266
1301
  });
1267
1302
  if (autoRetriggerRef.current && activeToolRef.current === name) {
1268
1303
  requestAnimationFrame(() => {
1269
- createOverlayForTool(name);
1304
+ createOverlayForToolRef.current(name);
1270
1305
  });
1271
1306
  }
1272
1307
  }
@@ -1274,6 +1309,9 @@ function useDrawingTools() {
1274
1309
  },
1275
1310
  [state.chart, undoRedoListenerRef]
1276
1311
  );
1312
+ useEffect(() => {
1313
+ createOverlayForToolRef.current = createOverlayForTool;
1314
+ });
1277
1315
  const selectTool = useCallback(
1278
1316
  (name) => {
1279
1317
  setActiveTool(name);
@@ -1677,28 +1715,81 @@ function useScreenshot() {
1677
1715
  clear
1678
1716
  };
1679
1717
  }
1718
+ function useHotkeys() {
1719
+ const { state } = useKlinechartsUI();
1720
+ const registerHotkey$1 = useCallback((template) => {
1721
+ registerHotkey(template);
1722
+ }, []);
1723
+ const getHotkey$1 = useCallback(
1724
+ (name) => getHotkey(name) ?? null,
1725
+ []
1726
+ );
1727
+ const setHotkeysEnabled = useCallback(
1728
+ (enabled, exclude) => {
1729
+ state.chart?.setHotkey(
1730
+ exclude ? { enabled, exclude } : { enabled }
1731
+ );
1732
+ },
1733
+ [state.chart]
1734
+ );
1735
+ const getHotkeysConfig = useCallback(
1736
+ () => state.chart?.getHotkey() ?? null,
1737
+ [state.chart]
1738
+ );
1739
+ return {
1740
+ registerHotkey: registerHotkey$1,
1741
+ getHotkey: getHotkey$1,
1742
+ supportedHotkeys: getSupportedHotkeys(),
1743
+ setHotkeysEnabled,
1744
+ getHotkeysConfig
1745
+ };
1746
+ }
1747
+ function useChartAxes() {
1748
+ const { state } = useKlinechartsUI();
1749
+ const overrideXAxis = useCallback(
1750
+ (override) => {
1751
+ if (!state.chart) return;
1752
+ state.chart.overrideXAxis(override);
1753
+ },
1754
+ [state.chart]
1755
+ );
1756
+ const overrideYAxis = useCallback(
1757
+ (override) => {
1758
+ if (!state.chart) return;
1759
+ state.chart.overrideYAxis(override);
1760
+ },
1761
+ [state.chart]
1762
+ );
1763
+ return { overrideXAxis, overrideYAxis };
1764
+ }
1680
1765
  function useFullscreen() {
1681
1766
  const { fullscreenContainerRef } = useKlinechartsUIDispatch();
1682
1767
  const [isFullscreen, setIsFullscreen] = useState(false);
1683
1768
  const enter = useCallback(() => {
1684
1769
  const el = fullscreenContainerRef.current;
1685
1770
  if (!el) return;
1771
+ let result;
1686
1772
  if (el.requestFullscreen) {
1687
- el.requestFullscreen();
1773
+ result = el.requestFullscreen();
1688
1774
  } else if ("webkitRequestFullscreen" in el) {
1689
1775
  el.webkitRequestFullscreen();
1690
1776
  } else if ("msRequestFullscreen" in el) {
1691
1777
  el.msRequestFullscreen();
1692
1778
  }
1779
+ result?.catch(() => {
1780
+ });
1693
1781
  }, [fullscreenContainerRef]);
1694
1782
  const exit = useCallback(() => {
1783
+ let result;
1695
1784
  if (document.exitFullscreen) {
1696
- document.exitFullscreen();
1785
+ result = document.exitFullscreen();
1697
1786
  } else if ("webkitExitFullscreen" in document) {
1698
1787
  document.webkitExitFullscreen();
1699
1788
  } else if ("msExitFullscreen" in document) {
1700
1789
  document.msExitFullscreen();
1701
1790
  }
1791
+ result?.catch(() => {
1792
+ });
1702
1793
  }, []);
1703
1794
  const toggle = useCallback(() => {
1704
1795
  if (isFullscreen) {
@@ -1729,6 +1820,7 @@ function useFullscreen() {
1729
1820
  function useOrderLines() {
1730
1821
  const { state } = useKlinechartsUI();
1731
1822
  const callbacksRef = useRef(/* @__PURE__ */ new Map());
1823
+ const ownedIdsRef = useRef(/* @__PURE__ */ new Set());
1732
1824
  const createOrderLine = useCallback(
1733
1825
  (options) => {
1734
1826
  if (!state.chart) return null;
@@ -1759,6 +1851,7 @@ function useOrderLines() {
1759
1851
  }
1760
1852
  }
1761
1853
  });
1854
+ ownedIdsRef.current.add(id);
1762
1855
  return id;
1763
1856
  },
1764
1857
  [state.chart]
@@ -1790,12 +1883,28 @@ function useOrderLines() {
1790
1883
  (id) => {
1791
1884
  state.chart?.removeOverlay({ id });
1792
1885
  callbacksRef.current.delete(id);
1886
+ ownedIdsRef.current.delete(id);
1793
1887
  },
1794
1888
  [state.chart]
1795
1889
  );
1796
1890
  const removeAllOrderLines = useCallback(() => {
1797
1891
  state.chart?.removeOverlay({ name: "orderLine" });
1798
1892
  callbacksRef.current.clear();
1893
+ ownedIdsRef.current.clear();
1894
+ }, [state.chart]);
1895
+ useEffect(() => {
1896
+ const chart = state.chart;
1897
+ const owned = ownedIdsRef.current;
1898
+ return () => {
1899
+ owned.forEach((id) => {
1900
+ try {
1901
+ chart?.removeOverlay({ id });
1902
+ } catch {
1903
+ }
1904
+ });
1905
+ owned.clear();
1906
+ callbacksRef.current.clear();
1907
+ };
1799
1908
  }, [state.chart]);
1800
1909
  return {
1801
1910
  createOrderLine,
@@ -1813,6 +1922,14 @@ function useUndoRedo() {
1813
1922
  const isProcessingRef = useRef(false);
1814
1923
  const canUndo = undoStack.length > 0;
1815
1924
  const canRedo = redoStack.length > 0;
1925
+ const undoStackRef = useRef([]);
1926
+ const redoStackRef = useRef([]);
1927
+ useEffect(() => {
1928
+ undoStackRef.current = undoStack;
1929
+ }, [undoStack]);
1930
+ useEffect(() => {
1931
+ redoStackRef.current = redoStack;
1932
+ }, [redoStack]);
1816
1933
  const pushAction = useCallback((action) => {
1817
1934
  if (isProcessingRef.current) return;
1818
1935
  setUndoStack((prev) => [...prev, action]);
@@ -1829,10 +1946,11 @@ function useUndoRedo() {
1829
1946
  setRedoStack([]);
1830
1947
  }, []);
1831
1948
  const undo = useCallback(() => {
1832
- if (undoStack.length === 0 || !state.chart || isProcessingRef.current)
1949
+ const stack = undoStackRef.current;
1950
+ if (stack.length === 0 || !state.chart || isProcessingRef.current)
1833
1951
  return;
1834
1952
  isProcessingRef.current = true;
1835
- const action = undoStack[undoStack.length - 1];
1953
+ const action = stack[stack.length - 1];
1836
1954
  setUndoStack((prev) => prev.slice(0, -1));
1837
1955
  try {
1838
1956
  switch (action.type) {
@@ -1953,12 +2071,13 @@ function useUndoRedo() {
1953
2071
  } finally {
1954
2072
  isProcessingRef.current = false;
1955
2073
  }
1956
- }, [undoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
2074
+ }, [state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
1957
2075
  const redo = useCallback(() => {
1958
- if (redoStack.length === 0 || !state.chart || isProcessingRef.current)
2076
+ const stack = redoStackRef.current;
2077
+ if (stack.length === 0 || !state.chart || isProcessingRef.current)
1959
2078
  return;
1960
2079
  isProcessingRef.current = true;
1961
- const action = redoStack[redoStack.length - 1];
2080
+ const action = stack[stack.length - 1];
1962
2081
  setRedoStack((prev) => prev.slice(0, -1));
1963
2082
  try {
1964
2083
  switch (action.type) {
@@ -2083,7 +2202,7 @@ function useUndoRedo() {
2083
2202
  } finally {
2084
2203
  isProcessingRef.current = false;
2085
2204
  }
2086
- }, [redoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
2205
+ }, [state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
2087
2206
  useEffect(() => {
2088
2207
  const handleKeyDown = (e) => {
2089
2208
  const isCtrlOrMeta = e.ctrlKey || e.metaKey;
@@ -2121,6 +2240,7 @@ function generateId() {
2121
2240
  );
2122
2241
  }
2123
2242
  function getLayoutIds() {
2243
+ if (typeof localStorage === "undefined") return [];
2124
2244
  try {
2125
2245
  const raw = localStorage.getItem(INDEX_KEY);
2126
2246
  return raw ? JSON.parse(raw) : [];
@@ -2129,6 +2249,7 @@ function getLayoutIds() {
2129
2249
  }
2130
2250
  }
2131
2251
  function getEntry(id) {
2252
+ if (typeof localStorage === "undefined") return null;
2132
2253
  try {
2133
2254
  const raw = localStorage.getItem(STORAGE_KEY_PREFIX + id);
2134
2255
  return raw ? JSON.parse(raw) : null;
@@ -2138,35 +2259,31 @@ function getEntry(id) {
2138
2259
  }
2139
2260
  function useLayoutManager() {
2140
2261
  const { state, dispatch } = useKlinechartsUI();
2141
- const [layouts, setLayouts] = useState([]);
2262
+ const [layouts, setLayouts] = useState(
2263
+ () => getLayoutIds().map((id) => getEntry(id)).filter((e) => e !== null)
2264
+ );
2142
2265
  const [autoSaveEnabled, setAutoSaveEnabled] = useState(false);
2143
2266
  const autoSaveTimerRef = useRef(null);
2144
2267
  const autoSaveIdRef = useRef(null);
2145
2268
  const refreshLayouts = useCallback(() => {
2146
- const entries = getLayoutIds().map((id) => getEntry(id)).filter((e) => e !== null);
2147
- setLayouts(entries);
2269
+ setLayouts(
2270
+ getLayoutIds().map((id) => getEntry(id)).filter((e) => e !== null)
2271
+ );
2148
2272
  }, []);
2149
- useEffect(() => {
2150
- refreshLayouts();
2151
- }, [refreshLayouts]);
2152
2273
  const serializeState = useCallback(() => {
2153
2274
  const chart = state.chart;
2154
2275
  if (!chart) return null;
2155
2276
  const indicators2 = [];
2156
- const indicatorMap = chart.getIndicatorByPaneId?.();
2157
- if (indicatorMap) {
2158
- indicatorMap.forEach((paneIndicators, paneId) => {
2159
- paneIndicators.forEach((indicator) => {
2160
- const yAxisId = state.indicatorAxes[indicator.id];
2161
- indicators2.push({
2162
- paneId,
2163
- name: indicator.name,
2164
- calcParams: indicator.calcParams,
2165
- visible: indicator.visible,
2166
- ...indicator.styles ? { styles: indicator.styles } : {},
2167
- ...yAxisId ? { yAxisId } : {}
2168
- });
2169
- });
2277
+ const allIndicators = chart.getIndicators();
2278
+ for (const indicator of allIndicators) {
2279
+ const yAxisId = state.indicatorAxes[indicator.id];
2280
+ indicators2.push({
2281
+ paneId: indicator.paneId,
2282
+ name: indicator.name,
2283
+ calcParams: indicator.calcParams,
2284
+ visible: indicator.visible,
2285
+ ...indicator.styles ? { styles: indicator.styles } : {},
2286
+ ...yAxisId ? { yAxisId } : {}
2170
2287
  });
2171
2288
  }
2172
2289
  const drawings = [];
@@ -2230,13 +2347,8 @@ function useLayoutManager() {
2230
2347
  if (chartState.version !== STATE_VERSION) return false;
2231
2348
  const chart = state.chart;
2232
2349
  chart.removeOverlay();
2233
- const indicatorMap = chart.getIndicatorByPaneId?.();
2234
- if (indicatorMap) {
2235
- indicatorMap.forEach((_paneIndicators, paneId) => {
2236
- _paneIndicators.forEach((_, indicatorName) => {
2237
- chart.removeIndicator({ paneId, name: indicatorName });
2238
- });
2239
- });
2350
+ for (const indicator of chart.getIndicators()) {
2351
+ chart.removeIndicator({ id: indicator.id });
2240
2352
  }
2241
2353
  const newMainIndicators = [];
2242
2354
  const newSubIndicators = {};
@@ -2254,7 +2366,12 @@ function useLayoutManager() {
2254
2366
  visible: ind.visible
2255
2367
  },
2256
2368
  {
2257
- isStack: ind.paneId !== "candle_pane",
2369
+ // Main indicators stack OVER the candle series on the candle
2370
+ // pane (isStack: true), exactly as useIndicators does on the
2371
+ // add path. The previous `ind.paneId !== "candle_pane"` inverted
2372
+ // this, so loading a layout with main indicators replaced the
2373
+ // candles instead of overlaying them.
2374
+ isStack: isMain,
2258
2375
  pane: { id: ind.paneId },
2259
2376
  ...ind.yAxisId ? { yAxis: { id: ind.yAxisId } } : {}
2260
2377
  }
@@ -2454,8 +2571,10 @@ function useScriptEditor() {
2454
2571
  const [status, setStatus] = useState("");
2455
2572
  const [isRunning, setIsRunning] = useState(false);
2456
2573
  const activeIdRef = useRef(null);
2574
+ const [activeName, setActiveName] = useState(null);
2457
2575
  const activeNameRef = useRef(null);
2458
- const hasActiveScript = activeNameRef.current !== null;
2576
+ activeNameRef.current = activeName;
2577
+ const hasActiveScript = activeName !== null;
2459
2578
  const runScript = useCallback(() => {
2460
2579
  const chart = state.chart;
2461
2580
  if (!chart) {
@@ -2476,19 +2595,26 @@ function useScriptEditor() {
2476
2595
  `"use strict";
2477
2596
  ${code}`
2478
2597
  ];
2479
- const fn = new Function(...allArgs);
2598
+ const compiledFn = new Function(...allArgs);
2480
2599
  const shadowValues = SHADOW_KEYS.map(() => void 0);
2481
- const result = fn(...shadowValues, TA_default, dataList, parsedParams);
2482
- if (!Array.isArray(result)) {
2600
+ const sampleResult = compiledFn(
2601
+ ...shadowValues,
2602
+ TA_default,
2603
+ dataList,
2604
+ parsedParams
2605
+ );
2606
+ if (!Array.isArray(sampleResult)) {
2483
2607
  throw new Error("Script must return an Array.");
2484
2608
  }
2485
- const sample = result.find((v) => v !== null && v !== void 0);
2609
+ const sample = sampleResult.find(
2610
+ (v) => v !== null && v !== void 0
2611
+ );
2486
2612
  const seriesKeys = sample ? Object.keys(sample) : ["value"];
2487
2613
  if (seriesKeys.length === 0) {
2488
2614
  throw new Error("Returned objects have no keys.");
2489
2615
  }
2490
2616
  scriptCounter++;
2491
- const indicatorName = `_custom_script_${scriptCounter}`;
2617
+ const indicatorName = `_custom_script_active`;
2492
2618
  const figures = seriesKeys.map((key, i) => ({
2493
2619
  key,
2494
2620
  title: `${key}: `,
@@ -2500,7 +2626,7 @@ ${code}`
2500
2626
  shortName: (scriptName.trim() || `Script #${scriptCounter}`) + (placement === "main" ? " (Main)" : " (Sub)"),
2501
2627
  calcParams: parsedParams,
2502
2628
  figures,
2503
- calc: () => result,
2629
+ calc: (liveDataList) => compiledFn(...shadowValues, TA_default, liveDataList, parsedParams),
2504
2630
  extendData: {
2505
2631
  isCustomScript: true,
2506
2632
  code,
@@ -2526,7 +2652,7 @@ ${code}`
2526
2652
  paneId = chart.createIndicator({ name: indicatorName });
2527
2653
  }
2528
2654
  activeIdRef.current = typeof paneId === "string" ? paneId : null;
2529
- activeNameRef.current = indicatorName;
2655
+ setActiveName(indicatorName);
2530
2656
  const title = scriptName.trim() || `Script #${scriptCounter}`;
2531
2657
  setStatus(
2532
2658
  `${title} applied to ${placement === "main" ? "Main Chart" : "Sub Pane"} \u2014 ${seriesKeys.length} series: ${seriesKeys.join(", ")}`
@@ -2548,7 +2674,7 @@ ${code}`
2548
2674
  } catch {
2549
2675
  }
2550
2676
  activeIdRef.current = null;
2551
- activeNameRef.current = null;
2677
+ setActiveName(null);
2552
2678
  setStatus("Indicator removed.");
2553
2679
  }
2554
2680
  }, [state.chart]);
@@ -2665,52 +2791,48 @@ function useAlerts() {
2665
2791
  const { alertTriggeredListenerRef } = useKlinechartsUIDispatch();
2666
2792
  const alerts = state.alerts;
2667
2793
  const addAlert = useCallback(
2668
- (price, condition, message) => {
2794
+ (price, condition, message, extendData) => {
2669
2795
  const id = `alert_${++alertCounter}`;
2796
+ const precision = state.chart?.getSymbol()?.pricePrecision ?? 2;
2797
+ const resolvedExtendData = {
2798
+ ...extendData,
2799
+ text: extendData?.text ?? message ?? price.toFixed(precision)
2800
+ };
2670
2801
  const alert = {
2671
2802
  id,
2672
2803
  price,
2673
2804
  condition,
2674
2805
  message,
2675
- triggered: false
2806
+ triggered: false,
2807
+ extendData: resolvedExtendData
2676
2808
  };
2677
- dispatch({ type: "SET_ALERTS", alerts: [...state.alerts, alert] });
2809
+ dispatch({ type: "ADD_ALERT", alert });
2678
2810
  if (state.chart) {
2811
+ ensureAlertLineRegistered();
2679
2812
  state.chart.createOverlay({
2680
- name: "horizontalStraightLine",
2813
+ name: "alertLine",
2681
2814
  id,
2682
2815
  groupId: "price_alerts",
2683
2816
  points: [{ value: price }],
2684
- styles: {
2685
- line: {
2686
- style: "dashed",
2687
- color: "#ff9800",
2688
- size: 1
2689
- }
2690
- },
2817
+ extendData: resolvedExtendData,
2691
2818
  lock: true
2692
2819
  });
2693
2820
  }
2694
2821
  return id;
2695
2822
  },
2696
- [state.chart, state.alerts, dispatch]
2823
+ [state.chart, dispatch]
2697
2824
  );
2698
2825
  const removeAlert = useCallback(
2699
2826
  (id) => {
2700
- dispatch({
2701
- type: "SET_ALERTS",
2702
- alerts: state.alerts.filter((a) => a.id !== id)
2703
- });
2827
+ dispatch({ type: "REMOVE_ALERT", id });
2704
2828
  state.chart?.removeOverlay({ id });
2705
2829
  },
2706
- [state.chart, state.alerts, dispatch]
2830
+ [state.chart, dispatch]
2707
2831
  );
2708
2832
  const clearAlerts = useCallback(() => {
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]);
2833
+ state.chart?.removeOverlay({ groupId: "price_alerts" });
2834
+ dispatch({ type: "CLEAR_ALERTS" });
2835
+ }, [state.chart, dispatch]);
2714
2836
  const onAlertTriggered = useCallback(
2715
2837
  (callback) => {
2716
2838
  alertTriggeredListenerRef.current = callback;
@@ -2894,6 +3016,7 @@ function useReplay() {
2894
3016
  );
2895
3017
  const startReplay = useCallback(() => {
2896
3018
  if (!state.chart) return;
3019
+ if (isReplaying) return;
2897
3020
  const dataList = state.chart.getDataList();
2898
3021
  if (!dataList || dataList.length === 0) return;
2899
3022
  replaySavedDataRef.current = [...dataList];
@@ -2909,7 +3032,12 @@ function useReplay() {
2909
3032
  });
2910
3033
  state.chart.clearData?.();
2911
3034
  startInterval(speed);
2912
- }, [state.chart, speed, startInterval, replaySavedDataRef, replayIndexRef, dispatch]);
3035
+ }, [state.chart, state.symbol, state.period, speed, startInterval, isReplaying, replaySavedDataRef, replayIndexRef, dispatch]);
3036
+ useEffect(() => {
3037
+ if (isReplaying) {
3038
+ stopReplay();
3039
+ }
3040
+ }, [state.symbol, state.period]);
2913
3041
  const stopReplay = useCallback(() => {
2914
3042
  if (!state.chart) return;
2915
3043
  clearInterval_();
@@ -3005,24 +3133,29 @@ var DEFAULT_COLORS = [
3005
3133
  "#ff5722",
3006
3134
  "#8bc34a"
3007
3135
  ];
3008
- var colorIndex = 0;
3009
- var compareCounter = 0;
3010
3136
  function useCompare() {
3011
3137
  const { state, datafeed } = useKlinechartsUI();
3012
3138
  const [symbols, setSymbols] = useState([]);
3013
3139
  const indicatorsRef = useRef(/* @__PURE__ */ new Map());
3140
+ const instanceSalt = useId().replace(/[^a-zA-Z0-9]/g, "");
3014
3141
  const addSymbol = useCallback(
3015
3142
  async (ticker, color) => {
3016
3143
  if (!state.chart || !datafeed) return;
3017
3144
  if (indicatorsRef.current.has(ticker)) return;
3018
- const assignedColor = color ?? DEFAULT_COLORS[colorIndex++ % DEFAULT_COLORS.length];
3145
+ const assignedColor = color ?? DEFAULT_COLORS[symbols.length % DEFAULT_COLORS.length];
3019
3146
  const mainDataList = state.chart.getDataList();
3020
3147
  if (!mainDataList || mainDataList.length === 0) return;
3021
3148
  const from = mainDataList[0].timestamp;
3022
3149
  const to = mainDataList[mainDataList.length - 1].timestamp;
3150
+ const mainSymbol = state.symbol;
3151
+ const symbol = {
3152
+ ticker,
3153
+ pricePrecision: mainSymbol?.pricePrecision ?? 2,
3154
+ volumePrecision: mainSymbol?.volumePrecision ?? 8
3155
+ };
3023
3156
  const compareData = await datafeed.getHistoryKLineData(
3024
- { ticker },
3025
- { ...state.period, label: "" },
3157
+ symbol,
3158
+ state.period,
3026
3159
  from,
3027
3160
  to
3028
3161
  );
@@ -3041,13 +3174,7 @@ function useCompare() {
3041
3174
  }
3042
3175
  if (basePrice === null || basePrice === 0) return;
3043
3176
  const bp = basePrice;
3044
- const normalizedData = mainDataList.map((mainBar) => {
3045
- const close = compareMap.get(mainBar.timestamp);
3046
- return {
3047
- pct: close != null ? (close - bp) / bp * 100 : NaN
3048
- };
3049
- });
3050
- const indicatorName = `__cmp_${++compareCounter}_${ticker}`;
3177
+ const indicatorName = `__cmp_${instanceSalt}_${ticker}`;
3051
3178
  registerIndicator({
3052
3179
  name: indicatorName,
3053
3180
  shortName: ticker,
@@ -3059,7 +3186,16 @@ function useCompare() {
3059
3186
  styles: () => ({ color: assignedColor })
3060
3187
  }
3061
3188
  ],
3062
- calc: () => normalizedData
3189
+ calc: (dataList) => {
3190
+ let lastPct = null;
3191
+ return dataList.map((bar) => {
3192
+ const close = compareMap.get(bar.timestamp);
3193
+ if (close != null) {
3194
+ lastPct = (close - bp) / bp * 100;
3195
+ }
3196
+ return { pct: lastPct };
3197
+ });
3198
+ }
3063
3199
  });
3064
3200
  const paneId = state.chart.createIndicator(
3065
3201
  { name: indicatorName },
@@ -3077,7 +3213,7 @@ function useCompare() {
3077
3213
  ];
3078
3214
  });
3079
3215
  },
3080
- [state.chart, state.period, datafeed]
3216
+ [state.chart, state.symbol, state.period, datafeed, symbols.length, instanceSalt]
3081
3217
  );
3082
3218
  const removeSymbol = useCallback(
3083
3219
  (ticker) => {
@@ -3287,13 +3423,11 @@ function useAnnotations() {
3287
3423
  [state.chart]
3288
3424
  );
3289
3425
  const clearAnnotations = useCallback(() => {
3290
- setAnnotations((prev) => {
3291
- for (const annotation of prev) {
3292
- state.chart?.removeOverlay({ id: annotation.id });
3293
- }
3294
- return [];
3295
- });
3296
- }, [state.chart]);
3426
+ for (const annotation of annotations) {
3427
+ state.chart?.removeOverlay({ id: annotation.id });
3428
+ }
3429
+ setAnnotations([]);
3430
+ }, [state.chart, annotations]);
3297
3431
  useEffect(() => {
3298
3432
  return () => {
3299
3433
  state.chart?.removeOverlay({ groupId: "annotations" });
@@ -3372,6 +3506,6 @@ function createDataLoader(datafeed, dispatch) {
3372
3506
  };
3373
3507
  }
3374
3508
 
3375
- 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 };
3509
+ 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, useChartAxes, useCompare, useCrosshair, useDataExport, useDrawingTools, useFullscreen, useHotkeys, useIndicators, useKlinechartsUI, useKlinechartsUILoading, useKlinechartsUISettings, useKlinechartsUITheme, useLayoutManager, useMeasure, useOrderLines, usePeriods, useReplay, useScreenshot, useScriptEditor, useSymbolSearch, useTimezone, useUndoRedo, useWatchlist };
3376
3510
  //# sourceMappingURL=index.js.map
3377
3511
  //# sourceMappingURL=index.js.map