react-klinecharts-ui 0.6.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, ensureAlertLineRegistered } from './chunk-MVD3ILJT.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-MVD3ILJT.js';
3
- import { createContext, useContext, useReducer, useRef, useCallback, useEffect, useMemo, useState } from 'react';
4
- import { registerOverlay, registerHotkey, getHotkey, getSupportedHotkeys, 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);
@@ -1730,22 +1768,28 @@ function useFullscreen() {
1730
1768
  const enter = useCallback(() => {
1731
1769
  const el = fullscreenContainerRef.current;
1732
1770
  if (!el) return;
1771
+ let result;
1733
1772
  if (el.requestFullscreen) {
1734
- el.requestFullscreen();
1773
+ result = el.requestFullscreen();
1735
1774
  } else if ("webkitRequestFullscreen" in el) {
1736
1775
  el.webkitRequestFullscreen();
1737
1776
  } else if ("msRequestFullscreen" in el) {
1738
1777
  el.msRequestFullscreen();
1739
1778
  }
1779
+ result?.catch(() => {
1780
+ });
1740
1781
  }, [fullscreenContainerRef]);
1741
1782
  const exit = useCallback(() => {
1783
+ let result;
1742
1784
  if (document.exitFullscreen) {
1743
- document.exitFullscreen();
1785
+ result = document.exitFullscreen();
1744
1786
  } else if ("webkitExitFullscreen" in document) {
1745
1787
  document.webkitExitFullscreen();
1746
1788
  } else if ("msExitFullscreen" in document) {
1747
1789
  document.msExitFullscreen();
1748
1790
  }
1791
+ result?.catch(() => {
1792
+ });
1749
1793
  }, []);
1750
1794
  const toggle = useCallback(() => {
1751
1795
  if (isFullscreen) {
@@ -1776,6 +1820,7 @@ function useFullscreen() {
1776
1820
  function useOrderLines() {
1777
1821
  const { state } = useKlinechartsUI();
1778
1822
  const callbacksRef = useRef(/* @__PURE__ */ new Map());
1823
+ const ownedIdsRef = useRef(/* @__PURE__ */ new Set());
1779
1824
  const createOrderLine = useCallback(
1780
1825
  (options) => {
1781
1826
  if (!state.chart) return null;
@@ -1806,6 +1851,7 @@ function useOrderLines() {
1806
1851
  }
1807
1852
  }
1808
1853
  });
1854
+ ownedIdsRef.current.add(id);
1809
1855
  return id;
1810
1856
  },
1811
1857
  [state.chart]
@@ -1837,12 +1883,28 @@ function useOrderLines() {
1837
1883
  (id) => {
1838
1884
  state.chart?.removeOverlay({ id });
1839
1885
  callbacksRef.current.delete(id);
1886
+ ownedIdsRef.current.delete(id);
1840
1887
  },
1841
1888
  [state.chart]
1842
1889
  );
1843
1890
  const removeAllOrderLines = useCallback(() => {
1844
1891
  state.chart?.removeOverlay({ name: "orderLine" });
1845
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
+ };
1846
1908
  }, [state.chart]);
1847
1909
  return {
1848
1910
  createOrderLine,
@@ -1860,6 +1922,14 @@ function useUndoRedo() {
1860
1922
  const isProcessingRef = useRef(false);
1861
1923
  const canUndo = undoStack.length > 0;
1862
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]);
1863
1933
  const pushAction = useCallback((action) => {
1864
1934
  if (isProcessingRef.current) return;
1865
1935
  setUndoStack((prev) => [...prev, action]);
@@ -1876,10 +1946,11 @@ function useUndoRedo() {
1876
1946
  setRedoStack([]);
1877
1947
  }, []);
1878
1948
  const undo = useCallback(() => {
1879
- if (undoStack.length === 0 || !state.chart || isProcessingRef.current)
1949
+ const stack = undoStackRef.current;
1950
+ if (stack.length === 0 || !state.chart || isProcessingRef.current)
1880
1951
  return;
1881
1952
  isProcessingRef.current = true;
1882
- const action = undoStack[undoStack.length - 1];
1953
+ const action = stack[stack.length - 1];
1883
1954
  setUndoStack((prev) => prev.slice(0, -1));
1884
1955
  try {
1885
1956
  switch (action.type) {
@@ -2000,12 +2071,13 @@ function useUndoRedo() {
2000
2071
  } finally {
2001
2072
  isProcessingRef.current = false;
2002
2073
  }
2003
- }, [undoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
2074
+ }, [state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
2004
2075
  const redo = useCallback(() => {
2005
- if (redoStack.length === 0 || !state.chart || isProcessingRef.current)
2076
+ const stack = redoStackRef.current;
2077
+ if (stack.length === 0 || !state.chart || isProcessingRef.current)
2006
2078
  return;
2007
2079
  isProcessingRef.current = true;
2008
- const action = redoStack[redoStack.length - 1];
2080
+ const action = stack[stack.length - 1];
2009
2081
  setRedoStack((prev) => prev.slice(0, -1));
2010
2082
  try {
2011
2083
  switch (action.type) {
@@ -2130,7 +2202,7 @@ function useUndoRedo() {
2130
2202
  } finally {
2131
2203
  isProcessingRef.current = false;
2132
2204
  }
2133
- }, [redoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
2205
+ }, [state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
2134
2206
  useEffect(() => {
2135
2207
  const handleKeyDown = (e) => {
2136
2208
  const isCtrlOrMeta = e.ctrlKey || e.metaKey;
@@ -2168,6 +2240,7 @@ function generateId() {
2168
2240
  );
2169
2241
  }
2170
2242
  function getLayoutIds() {
2243
+ if (typeof localStorage === "undefined") return [];
2171
2244
  try {
2172
2245
  const raw = localStorage.getItem(INDEX_KEY);
2173
2246
  return raw ? JSON.parse(raw) : [];
@@ -2176,6 +2249,7 @@ function getLayoutIds() {
2176
2249
  }
2177
2250
  }
2178
2251
  function getEntry(id) {
2252
+ if (typeof localStorage === "undefined") return null;
2179
2253
  try {
2180
2254
  const raw = localStorage.getItem(STORAGE_KEY_PREFIX + id);
2181
2255
  return raw ? JSON.parse(raw) : null;
@@ -2185,35 +2259,31 @@ function getEntry(id) {
2185
2259
  }
2186
2260
  function useLayoutManager() {
2187
2261
  const { state, dispatch } = useKlinechartsUI();
2188
- const [layouts, setLayouts] = useState([]);
2262
+ const [layouts, setLayouts] = useState(
2263
+ () => getLayoutIds().map((id) => getEntry(id)).filter((e) => e !== null)
2264
+ );
2189
2265
  const [autoSaveEnabled, setAutoSaveEnabled] = useState(false);
2190
2266
  const autoSaveTimerRef = useRef(null);
2191
2267
  const autoSaveIdRef = useRef(null);
2192
2268
  const refreshLayouts = useCallback(() => {
2193
- const entries = getLayoutIds().map((id) => getEntry(id)).filter((e) => e !== null);
2194
- setLayouts(entries);
2269
+ setLayouts(
2270
+ getLayoutIds().map((id) => getEntry(id)).filter((e) => e !== null)
2271
+ );
2195
2272
  }, []);
2196
- useEffect(() => {
2197
- refreshLayouts();
2198
- }, [refreshLayouts]);
2199
2273
  const serializeState = useCallback(() => {
2200
2274
  const chart = state.chart;
2201
2275
  if (!chart) return null;
2202
2276
  const indicators2 = [];
2203
- const indicatorMap = chart.getIndicatorByPaneId?.();
2204
- if (indicatorMap) {
2205
- indicatorMap.forEach((paneIndicators, paneId) => {
2206
- paneIndicators.forEach((indicator) => {
2207
- const yAxisId = state.indicatorAxes[indicator.id];
2208
- indicators2.push({
2209
- paneId,
2210
- name: indicator.name,
2211
- calcParams: indicator.calcParams,
2212
- visible: indicator.visible,
2213
- ...indicator.styles ? { styles: indicator.styles } : {},
2214
- ...yAxisId ? { yAxisId } : {}
2215
- });
2216
- });
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 } : {}
2217
2287
  });
2218
2288
  }
2219
2289
  const drawings = [];
@@ -2277,13 +2347,8 @@ function useLayoutManager() {
2277
2347
  if (chartState.version !== STATE_VERSION) return false;
2278
2348
  const chart = state.chart;
2279
2349
  chart.removeOverlay();
2280
- const indicatorMap = chart.getIndicatorByPaneId?.();
2281
- if (indicatorMap) {
2282
- indicatorMap.forEach((_paneIndicators, paneId) => {
2283
- _paneIndicators.forEach((_, indicatorName) => {
2284
- chart.removeIndicator({ paneId, name: indicatorName });
2285
- });
2286
- });
2350
+ for (const indicator of chart.getIndicators()) {
2351
+ chart.removeIndicator({ id: indicator.id });
2287
2352
  }
2288
2353
  const newMainIndicators = [];
2289
2354
  const newSubIndicators = {};
@@ -2301,7 +2366,12 @@ function useLayoutManager() {
2301
2366
  visible: ind.visible
2302
2367
  },
2303
2368
  {
2304
- 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,
2305
2375
  pane: { id: ind.paneId },
2306
2376
  ...ind.yAxisId ? { yAxis: { id: ind.yAxisId } } : {}
2307
2377
  }
@@ -2501,8 +2571,10 @@ function useScriptEditor() {
2501
2571
  const [status, setStatus] = useState("");
2502
2572
  const [isRunning, setIsRunning] = useState(false);
2503
2573
  const activeIdRef = useRef(null);
2574
+ const [activeName, setActiveName] = useState(null);
2504
2575
  const activeNameRef = useRef(null);
2505
- const hasActiveScript = activeNameRef.current !== null;
2576
+ activeNameRef.current = activeName;
2577
+ const hasActiveScript = activeName !== null;
2506
2578
  const runScript = useCallback(() => {
2507
2579
  const chart = state.chart;
2508
2580
  if (!chart) {
@@ -2523,19 +2595,26 @@ function useScriptEditor() {
2523
2595
  `"use strict";
2524
2596
  ${code}`
2525
2597
  ];
2526
- const fn = new Function(...allArgs);
2598
+ const compiledFn = new Function(...allArgs);
2527
2599
  const shadowValues = SHADOW_KEYS.map(() => void 0);
2528
- const result = fn(...shadowValues, TA_default, dataList, parsedParams);
2529
- if (!Array.isArray(result)) {
2600
+ const sampleResult = compiledFn(
2601
+ ...shadowValues,
2602
+ TA_default,
2603
+ dataList,
2604
+ parsedParams
2605
+ );
2606
+ if (!Array.isArray(sampleResult)) {
2530
2607
  throw new Error("Script must return an Array.");
2531
2608
  }
2532
- const sample = result.find((v) => v !== null && v !== void 0);
2609
+ const sample = sampleResult.find(
2610
+ (v) => v !== null && v !== void 0
2611
+ );
2533
2612
  const seriesKeys = sample ? Object.keys(sample) : ["value"];
2534
2613
  if (seriesKeys.length === 0) {
2535
2614
  throw new Error("Returned objects have no keys.");
2536
2615
  }
2537
2616
  scriptCounter++;
2538
- const indicatorName = `_custom_script_${scriptCounter}`;
2617
+ const indicatorName = `_custom_script_active`;
2539
2618
  const figures = seriesKeys.map((key, i) => ({
2540
2619
  key,
2541
2620
  title: `${key}: `,
@@ -2547,7 +2626,7 @@ ${code}`
2547
2626
  shortName: (scriptName.trim() || `Script #${scriptCounter}`) + (placement === "main" ? " (Main)" : " (Sub)"),
2548
2627
  calcParams: parsedParams,
2549
2628
  figures,
2550
- calc: () => result,
2629
+ calc: (liveDataList) => compiledFn(...shadowValues, TA_default, liveDataList, parsedParams),
2551
2630
  extendData: {
2552
2631
  isCustomScript: true,
2553
2632
  code,
@@ -2573,7 +2652,7 @@ ${code}`
2573
2652
  paneId = chart.createIndicator({ name: indicatorName });
2574
2653
  }
2575
2654
  activeIdRef.current = typeof paneId === "string" ? paneId : null;
2576
- activeNameRef.current = indicatorName;
2655
+ setActiveName(indicatorName);
2577
2656
  const title = scriptName.trim() || `Script #${scriptCounter}`;
2578
2657
  setStatus(
2579
2658
  `${title} applied to ${placement === "main" ? "Main Chart" : "Sub Pane"} \u2014 ${seriesKeys.length} series: ${seriesKeys.join(", ")}`
@@ -2595,7 +2674,7 @@ ${code}`
2595
2674
  } catch {
2596
2675
  }
2597
2676
  activeIdRef.current = null;
2598
- activeNameRef.current = null;
2677
+ setActiveName(null);
2599
2678
  setStatus("Indicator removed.");
2600
2679
  }
2601
2680
  }, [state.chart]);
@@ -2727,7 +2806,7 @@ function useAlerts() {
2727
2806
  triggered: false,
2728
2807
  extendData: resolvedExtendData
2729
2808
  };
2730
- dispatch({ type: "SET_ALERTS", alerts: [...state.alerts, alert] });
2809
+ dispatch({ type: "ADD_ALERT", alert });
2731
2810
  if (state.chart) {
2732
2811
  ensureAlertLineRegistered();
2733
2812
  state.chart.createOverlay({
@@ -2741,24 +2820,19 @@ function useAlerts() {
2741
2820
  }
2742
2821
  return id;
2743
2822
  },
2744
- [state.chart, state.alerts, dispatch]
2823
+ [state.chart, dispatch]
2745
2824
  );
2746
2825
  const removeAlert = useCallback(
2747
2826
  (id) => {
2748
- dispatch({
2749
- type: "SET_ALERTS",
2750
- alerts: state.alerts.filter((a) => a.id !== id)
2751
- });
2827
+ dispatch({ type: "REMOVE_ALERT", id });
2752
2828
  state.chart?.removeOverlay({ id });
2753
2829
  },
2754
- [state.chart, state.alerts, dispatch]
2830
+ [state.chart, dispatch]
2755
2831
  );
2756
2832
  const clearAlerts = useCallback(() => {
2757
- for (const alert of state.alerts) {
2758
- state.chart?.removeOverlay({ id: alert.id });
2759
- }
2760
- dispatch({ type: "SET_ALERTS", alerts: [] });
2761
- }, [state.chart, state.alerts, dispatch]);
2833
+ state.chart?.removeOverlay({ groupId: "price_alerts" });
2834
+ dispatch({ type: "CLEAR_ALERTS" });
2835
+ }, [state.chart, dispatch]);
2762
2836
  const onAlertTriggered = useCallback(
2763
2837
  (callback) => {
2764
2838
  alertTriggeredListenerRef.current = callback;
@@ -2942,6 +3016,7 @@ function useReplay() {
2942
3016
  );
2943
3017
  const startReplay = useCallback(() => {
2944
3018
  if (!state.chart) return;
3019
+ if (isReplaying) return;
2945
3020
  const dataList = state.chart.getDataList();
2946
3021
  if (!dataList || dataList.length === 0) return;
2947
3022
  replaySavedDataRef.current = [...dataList];
@@ -2957,7 +3032,12 @@ function useReplay() {
2957
3032
  });
2958
3033
  state.chart.clearData?.();
2959
3034
  startInterval(speed);
2960
- }, [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]);
2961
3041
  const stopReplay = useCallback(() => {
2962
3042
  if (!state.chart) return;
2963
3043
  clearInterval_();
@@ -3053,24 +3133,29 @@ var DEFAULT_COLORS = [
3053
3133
  "#ff5722",
3054
3134
  "#8bc34a"
3055
3135
  ];
3056
- var colorIndex = 0;
3057
- var compareCounter = 0;
3058
3136
  function useCompare() {
3059
3137
  const { state, datafeed } = useKlinechartsUI();
3060
3138
  const [symbols, setSymbols] = useState([]);
3061
3139
  const indicatorsRef = useRef(/* @__PURE__ */ new Map());
3140
+ const instanceSalt = useId().replace(/[^a-zA-Z0-9]/g, "");
3062
3141
  const addSymbol = useCallback(
3063
3142
  async (ticker, color) => {
3064
3143
  if (!state.chart || !datafeed) return;
3065
3144
  if (indicatorsRef.current.has(ticker)) return;
3066
- const assignedColor = color ?? DEFAULT_COLORS[colorIndex++ % DEFAULT_COLORS.length];
3145
+ const assignedColor = color ?? DEFAULT_COLORS[symbols.length % DEFAULT_COLORS.length];
3067
3146
  const mainDataList = state.chart.getDataList();
3068
3147
  if (!mainDataList || mainDataList.length === 0) return;
3069
3148
  const from = mainDataList[0].timestamp;
3070
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
+ };
3071
3156
  const compareData = await datafeed.getHistoryKLineData(
3072
- { ticker },
3073
- { ...state.period, label: "" },
3157
+ symbol,
3158
+ state.period,
3074
3159
  from,
3075
3160
  to
3076
3161
  );
@@ -3089,13 +3174,7 @@ function useCompare() {
3089
3174
  }
3090
3175
  if (basePrice === null || basePrice === 0) return;
3091
3176
  const bp = basePrice;
3092
- const normalizedData = mainDataList.map((mainBar) => {
3093
- const close = compareMap.get(mainBar.timestamp);
3094
- return {
3095
- pct: close != null ? (close - bp) / bp * 100 : NaN
3096
- };
3097
- });
3098
- const indicatorName = `__cmp_${++compareCounter}_${ticker}`;
3177
+ const indicatorName = `__cmp_${instanceSalt}_${ticker}`;
3099
3178
  registerIndicator({
3100
3179
  name: indicatorName,
3101
3180
  shortName: ticker,
@@ -3107,7 +3186,16 @@ function useCompare() {
3107
3186
  styles: () => ({ color: assignedColor })
3108
3187
  }
3109
3188
  ],
3110
- 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
+ }
3111
3199
  });
3112
3200
  const paneId = state.chart.createIndicator(
3113
3201
  { name: indicatorName },
@@ -3125,7 +3213,7 @@ function useCompare() {
3125
3213
  ];
3126
3214
  });
3127
3215
  },
3128
- [state.chart, state.period, datafeed]
3216
+ [state.chart, state.symbol, state.period, datafeed, symbols.length, instanceSalt]
3129
3217
  );
3130
3218
  const removeSymbol = useCallback(
3131
3219
  (ticker) => {
@@ -3335,13 +3423,11 @@ function useAnnotations() {
3335
3423
  [state.chart]
3336
3424
  );
3337
3425
  const clearAnnotations = useCallback(() => {
3338
- setAnnotations((prev) => {
3339
- for (const annotation of prev) {
3340
- state.chart?.removeOverlay({ id: annotation.id });
3341
- }
3342
- return [];
3343
- });
3344
- }, [state.chart]);
3426
+ for (const annotation of annotations) {
3427
+ state.chart?.removeOverlay({ id: annotation.id });
3428
+ }
3429
+ setAnnotations([]);
3430
+ }, [state.chart, annotations]);
3345
3431
  useEffect(() => {
3346
3432
  return () => {
3347
3433
  state.chart?.removeOverlay({ groupId: "annotations" });