react-klinecharts-ui 0.6.0 → 1.1.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 +186 -6
- package/dist/chart.cjs +70 -0
- package/dist/chart.cjs.map +1 -0
- package/dist/chart.d.cts +45 -0
- package/dist/chart.d.ts +45 -0
- package/dist/chart.js +68 -0
- package/dist/chart.js.map +1 -0
- package/dist/chunk-F24D6PWY.cjs +98 -0
- package/dist/chunk-F24D6PWY.cjs.map +1 -0
- package/dist/chunk-O7E57I66.js +92 -0
- package/dist/chunk-O7E57I66.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +9 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-Q7SFCCGT.cjs +11 -0
- package/dist/chunk-Q7SFCCGT.cjs.map +1 -0
- package/dist/{chunk-MVD3ILJT.js → chunk-RIT2LJQT.js} +35 -22
- package/dist/chunk-RIT2LJQT.js.map +1 -0
- package/dist/{chunk-FSHI2ZDB.cjs → chunk-SZDU2D3D.cjs} +46 -33
- package/dist/chunk-SZDU2D3D.cjs.map +1 -0
- package/dist/extensions.cjs +10 -9
- package/dist/extensions.d.cts +10 -7
- package/dist/extensions.d.ts +10 -7
- package/dist/extensions.js +2 -1
- package/dist/index.cjs +657 -323
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +246 -9
- package/dist/index.d.ts +246 -9
- package/dist/index.js +555 -240
- package/dist/index.js.map +1 -1
- package/package.json +31 -5
- package/dist/chunk-FSHI2ZDB.cjs.map +0 -1
- package/dist/chunk-MVD3ILJT.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,31 +1,12 @@
|
|
|
1
|
-
import { registerExtensions, TA_default, ensureAlertLineRegistered } from './chunk-
|
|
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-
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { registerExtensions, TA_default, ensureAlertLineRegistered } from './chunk-RIT2LJQT.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-RIT2LJQT.js';
|
|
3
|
+
import { KlinechartsUIStateContext, KlinechartsUIDispatchContext, useKlinechartsUI, useKlinechartsUIDispatch } from './chunk-O7E57I66.js';
|
|
4
|
+
export { KlinechartsUIDispatchContext, KlinechartsUIStateContext, createDataLoader, useKlinechartsUI } from './chunk-O7E57I66.js';
|
|
5
|
+
import './chunk-PZ5AY32C.js';
|
|
6
|
+
import { createContext, useMemo, useReducer, useRef, useEffect, useCallback, useState, useId, useContext } from 'react';
|
|
7
|
+
import { registerOverlay, registerHotkey, getHotkey, getSupportedHotkeys, registerIndicator } from 'klinecharts';
|
|
5
8
|
import { jsx } from 'react/jsx-runtime';
|
|
6
9
|
|
|
7
|
-
var KlinechartsUIStateContext = createContext(null);
|
|
8
|
-
var KlinechartsUIDispatchContext = createContext(null);
|
|
9
|
-
function useKlinechartsUIDispatch() {
|
|
10
|
-
const ctx = useContext(KlinechartsUIDispatchContext);
|
|
11
|
-
if (!ctx) {
|
|
12
|
-
throw new Error(
|
|
13
|
-
"useKlinechartsUI must be used within a <KlinechartsUIProvider>"
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
return ctx;
|
|
17
|
-
}
|
|
18
|
-
function useKlinechartsUI() {
|
|
19
|
-
const state = useContext(KlinechartsUIStateContext);
|
|
20
|
-
const dispatch = useContext(KlinechartsUIDispatchContext);
|
|
21
|
-
if (!state || !dispatch) {
|
|
22
|
-
throw new Error(
|
|
23
|
-
"useKlinechartsUI must be used within a <KlinechartsUIProvider>"
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
return { state, ...dispatch };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
10
|
// src/data/periods.ts
|
|
30
11
|
var DEFAULT_PERIODS = [
|
|
31
12
|
{ span: 1, type: "minute", label: "1m" },
|
|
@@ -39,6 +20,50 @@ var DEFAULT_PERIODS = [
|
|
|
39
20
|
{ span: 1, type: "month", label: "M" },
|
|
40
21
|
{ span: 1, type: "year", label: "Y" }
|
|
41
22
|
];
|
|
23
|
+
|
|
24
|
+
// src/storage/types.ts
|
|
25
|
+
var DEFAULT_STORAGE_NAMESPACES = [
|
|
26
|
+
"alerts",
|
|
27
|
+
"settings",
|
|
28
|
+
"indicators"
|
|
29
|
+
];
|
|
30
|
+
var DEFAULT_STORAGE_KEY_PREFIX = "rkui:";
|
|
31
|
+
|
|
32
|
+
// src/storage/createDefaultStorage.ts
|
|
33
|
+
var NOOP_ADAPTER = {
|
|
34
|
+
getItem: () => null,
|
|
35
|
+
setItem: () => {
|
|
36
|
+
},
|
|
37
|
+
removeItem: () => {
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function createDefaultStorage() {
|
|
41
|
+
if (typeof localStorage === "undefined") return NOOP_ADAPTER;
|
|
42
|
+
try {
|
|
43
|
+
const probe = localStorage.getItem("__rkui_probe__");
|
|
44
|
+
return probe === null ? localStorage : localStorage;
|
|
45
|
+
} catch {
|
|
46
|
+
return NOOP_ADAPTER;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function resolveStorage(options) {
|
|
50
|
+
const adapter = options?.adapter ?? createDefaultStorage();
|
|
51
|
+
const keyPrefix = options?.keyPrefix ?? DEFAULT_STORAGE_KEY_PREFIX;
|
|
52
|
+
const namespaces = new Set(
|
|
53
|
+
options?.namespaces ?? DEFAULT_STORAGE_NAMESPACES
|
|
54
|
+
);
|
|
55
|
+
return {
|
|
56
|
+
adapter,
|
|
57
|
+
namespaces,
|
|
58
|
+
keyPrefix,
|
|
59
|
+
key(namespace) {
|
|
60
|
+
return `${keyPrefix}${namespace}`;
|
|
61
|
+
},
|
|
62
|
+
persists(namespace) {
|
|
63
|
+
return namespaces.has(namespace);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
42
67
|
function reducer(state, action) {
|
|
43
68
|
switch (action.type) {
|
|
44
69
|
case "SET_CHART":
|
|
@@ -63,6 +88,25 @@ function reducer(state, action) {
|
|
|
63
88
|
return { ...state, indicatorVisibility: action.visibility };
|
|
64
89
|
case "SET_ALERTS":
|
|
65
90
|
return { ...state, alerts: action.alerts };
|
|
91
|
+
case "ADD_ALERT":
|
|
92
|
+
return { ...state, alerts: [...state.alerts, action.alert] };
|
|
93
|
+
case "REMOVE_ALERT":
|
|
94
|
+
return {
|
|
95
|
+
...state,
|
|
96
|
+
alerts: state.alerts.filter((a) => a.id !== action.id)
|
|
97
|
+
};
|
|
98
|
+
case "CLEAR_ALERTS":
|
|
99
|
+
return { ...state, alerts: [] };
|
|
100
|
+
case "MARK_ALERT_TRIGGERED": {
|
|
101
|
+
if (action.ids.length === 0) return state;
|
|
102
|
+
const triggered = new Set(action.ids);
|
|
103
|
+
return {
|
|
104
|
+
...state,
|
|
105
|
+
alerts: state.alerts.map(
|
|
106
|
+
(a) => triggered.has(a.id) ? { ...a, triggered: true } : a
|
|
107
|
+
)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
66
110
|
case "SET_MEASURE":
|
|
67
111
|
return { ...state, measure: { ...state.measure, ...action.measure } };
|
|
68
112
|
case "SET_REPLAY":
|
|
@@ -83,13 +127,16 @@ function KlinechartsUIProvider({
|
|
|
83
127
|
defaultPeriod,
|
|
84
128
|
defaultTheme = "light",
|
|
85
129
|
defaultTimezone = "Asia/Shanghai",
|
|
86
|
-
|
|
87
|
-
|
|
130
|
+
// NOTE: no destructuring default here — the init function must distinguish
|
|
131
|
+
// "consumer passed default*" (wins) from "use stored value" (hydrate).
|
|
132
|
+
defaultMainIndicators,
|
|
133
|
+
defaultSubIndicators,
|
|
88
134
|
defaultLocale = "en-US",
|
|
89
135
|
periods,
|
|
90
136
|
styles,
|
|
91
137
|
registerExtensions: shouldRegister = true,
|
|
92
138
|
overlays: extraOverlays,
|
|
139
|
+
storage: storageOptions,
|
|
93
140
|
children,
|
|
94
141
|
onStateChange,
|
|
95
142
|
onSymbolChange,
|
|
@@ -100,6 +147,11 @@ function KlinechartsUIProvider({
|
|
|
100
147
|
onSubIndicatorsChange,
|
|
101
148
|
onSettingsChange
|
|
102
149
|
}) {
|
|
150
|
+
const resolvedStorage = useMemo(
|
|
151
|
+
() => storageOptions ? resolveStorage(storageOptions) : null,
|
|
152
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
153
|
+
[]
|
|
154
|
+
);
|
|
103
155
|
const [state, dispatch] = useReducer(
|
|
104
156
|
reducer,
|
|
105
157
|
{
|
|
@@ -112,47 +164,66 @@ function KlinechartsUIProvider({
|
|
|
112
164
|
defaultSubIndicators,
|
|
113
165
|
defaultLocale,
|
|
114
166
|
periods,
|
|
115
|
-
styles
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
167
|
+
styles,
|
|
168
|
+
storage: resolvedStorage
|
|
169
|
+
},
|
|
170
|
+
(opts) => {
|
|
171
|
+
const s = opts.storage;
|
|
172
|
+
const read = (ns, fallback) => {
|
|
173
|
+
if (!s || !s.persists(ns)) return fallback;
|
|
174
|
+
try {
|
|
175
|
+
const raw = s.adapter.getItem(s.key(ns));
|
|
176
|
+
return raw ? JSON.parse(raw) : fallback;
|
|
177
|
+
} catch {
|
|
178
|
+
return fallback;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const storedIndicators = read("indicators", {});
|
|
182
|
+
return {
|
|
183
|
+
chart: null,
|
|
184
|
+
datafeed: opts.datafeed,
|
|
185
|
+
symbol: opts.defaultSymbol ?? null,
|
|
186
|
+
period: opts.defaultPeriod ?? (opts.periods ?? DEFAULT_PERIODS)[0],
|
|
187
|
+
theme: opts.defaultTheme ?? "light",
|
|
188
|
+
timezone: opts.defaultTimezone ?? "Asia/Shanghai",
|
|
189
|
+
isLoading: false,
|
|
190
|
+
locale: opts.defaultLocale ?? "en-US",
|
|
191
|
+
periods: opts.periods ?? DEFAULT_PERIODS,
|
|
192
|
+
// Persistence semantics: STORED values win over `default*` props —
|
|
193
|
+
// the user's saved configuration should survive reload even when the
|
|
194
|
+
// app passes defaults. Only when storage is empty/unconfigured do the
|
|
195
|
+
// `default*` props apply, falling back to the built-ins last.
|
|
196
|
+
// (Settings live in useKlinechartsUISettings useState and hydrate
|
|
197
|
+
// separately via the dispatch-context `storage`.)
|
|
198
|
+
mainIndicators: storedIndicators.main ?? opts.defaultMainIndicators ?? ["MA"],
|
|
199
|
+
subIndicators: storedIndicators.sub ?? (opts.defaultSubIndicators ? opts.defaultSubIndicators.reduce(
|
|
200
|
+
(acc, name) => ({ ...acc, [name]: "" }),
|
|
201
|
+
{}
|
|
202
|
+
) : { VOL: "" }),
|
|
203
|
+
indicatorAxes: storedIndicators.axes ?? {},
|
|
204
|
+
indicatorVisibility: storedIndicators.visibility ?? {},
|
|
205
|
+
alerts: read("alerts", []),
|
|
206
|
+
measure: { isActive: false, fromPoint: null, result: null },
|
|
207
|
+
replay: {
|
|
208
|
+
isReplaying: false,
|
|
209
|
+
isPaused: false,
|
|
210
|
+
speed: 1,
|
|
211
|
+
barIndex: 0,
|
|
212
|
+
totalBars: 0
|
|
213
|
+
},
|
|
214
|
+
styles: opts.styles,
|
|
215
|
+
screenshotUrl: null
|
|
216
|
+
};
|
|
217
|
+
}
|
|
146
218
|
);
|
|
147
219
|
const fullscreenContainerRef = useRef(null);
|
|
148
220
|
const undoRedoListenerRef = useRef(null);
|
|
149
|
-
const
|
|
221
|
+
const alertTriggeredListenersRef = useRef(/* @__PURE__ */ new Set());
|
|
150
222
|
const replayIntervalRef = useRef(null);
|
|
151
223
|
const replaySavedDataRef = useRef([]);
|
|
152
224
|
const replayIndexRef = useRef(0);
|
|
153
225
|
const alertPrevCloseRef = useRef(null);
|
|
154
226
|
const stateRef = useRef(state);
|
|
155
|
-
stateRef.current = state;
|
|
156
227
|
const callbacksRef = useRef({
|
|
157
228
|
onStateChange,
|
|
158
229
|
onSymbolChange,
|
|
@@ -162,15 +233,18 @@ function KlinechartsUIProvider({
|
|
|
162
233
|
onMainIndicatorsChange,
|
|
163
234
|
onSubIndicatorsChange
|
|
164
235
|
});
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
stateRef.current = state;
|
|
238
|
+
callbacksRef.current = {
|
|
239
|
+
onStateChange,
|
|
240
|
+
onSymbolChange,
|
|
241
|
+
onPeriodChange,
|
|
242
|
+
onThemeChange,
|
|
243
|
+
onTimezoneChange,
|
|
244
|
+
onMainIndicatorsChange,
|
|
245
|
+
onSubIndicatorsChange
|
|
246
|
+
};
|
|
247
|
+
});
|
|
174
248
|
const enhancedDispatch = useCallback((action) => {
|
|
175
249
|
const prevState = stateRef.current;
|
|
176
250
|
const newState = reducer(prevState, action);
|
|
@@ -200,6 +274,12 @@ function KlinechartsUIProvider({
|
|
|
200
274
|
}
|
|
201
275
|
}, []);
|
|
202
276
|
const extraOverlaysRef = useRef(extraOverlays);
|
|
277
|
+
const datafeedRef = useRef(datafeed);
|
|
278
|
+
const onSettingsChangeRef = useRef(onSettingsChange);
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
datafeedRef.current = datafeed;
|
|
281
|
+
onSettingsChangeRef.current = onSettingsChange;
|
|
282
|
+
});
|
|
203
283
|
useEffect(() => {
|
|
204
284
|
if (shouldRegister) {
|
|
205
285
|
registerExtensions();
|
|
@@ -216,34 +296,97 @@ function KlinechartsUIProvider({
|
|
|
216
296
|
const chart = state.chart;
|
|
217
297
|
if (!chart || !hasAlerts) return;
|
|
218
298
|
alertPrevCloseRef.current = null;
|
|
299
|
+
const prevValueByAlert = /* @__PURE__ */ new Map();
|
|
219
300
|
const interval = setInterval(() => {
|
|
220
301
|
const dataList = chart.getDataList();
|
|
221
302
|
if (!dataList || dataList.length === 0) return;
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
303
|
+
const lastIdx = dataList.length - 1;
|
|
304
|
+
const indicatorCache = /* @__PURE__ */ new Map();
|
|
305
|
+
const readIndicatorValue = (indicatorId, figureKey) => {
|
|
306
|
+
const cacheKey = `${indicatorId}:${figureKey}`;
|
|
307
|
+
if (indicatorCache.has(cacheKey)) return indicatorCache.get(cacheKey);
|
|
308
|
+
let value = null;
|
|
309
|
+
try {
|
|
310
|
+
const indicators2 = chart.getIndicators({ id: indicatorId });
|
|
311
|
+
const ind = indicators2?.[0];
|
|
312
|
+
const result = ind?.result;
|
|
313
|
+
const last = Array.isArray(result) ? result[lastIdx] : null;
|
|
314
|
+
if (last && typeof last === "object") {
|
|
315
|
+
const v = last[figureKey];
|
|
316
|
+
value = typeof v === "number" ? v : null;
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
value = null;
|
|
320
|
+
}
|
|
321
|
+
indicatorCache.set(cacheKey, value);
|
|
322
|
+
return value;
|
|
323
|
+
};
|
|
226
324
|
const alerts = stateRef.current.alerts;
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
if (alert.triggered)
|
|
230
|
-
const
|
|
231
|
-
|
|
325
|
+
const triggeredIds = [];
|
|
326
|
+
for (const alert of alerts) {
|
|
327
|
+
if (alert.triggered) continue;
|
|
328
|
+
const target = alert.target ?? { type: "price" };
|
|
329
|
+
let currentValue;
|
|
330
|
+
if (target.type === "indicator") {
|
|
331
|
+
currentValue = readIndicatorValue(target.indicatorId, target.figureKey);
|
|
332
|
+
} else {
|
|
333
|
+
currentValue = dataList[lastIdx].close;
|
|
334
|
+
}
|
|
335
|
+
if (currentValue === null || !Number.isFinite(currentValue)) continue;
|
|
336
|
+
const prevValue = prevValueByAlert.get(alert.id) ?? null;
|
|
337
|
+
prevValueByAlert.set(alert.id, currentValue);
|
|
338
|
+
if (prevValue === null) continue;
|
|
339
|
+
const crossedUp = prevValue < alert.price && currentValue >= alert.price;
|
|
340
|
+
const crossedDown = prevValue > alert.price && currentValue <= alert.price;
|
|
232
341
|
const shouldTrigger = alert.condition === "crossing_up" ? crossedUp : alert.condition === "crossing_down" ? crossedDown : crossedUp || crossedDown;
|
|
233
342
|
if (shouldTrigger) {
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
return triggered;
|
|
343
|
+
triggeredIds.push(alert.id);
|
|
344
|
+
const fired = { ...alert, triggered: true };
|
|
345
|
+
alertTriggeredListenersRef.current.forEach((cb) => cb(fired));
|
|
238
346
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
347
|
+
}
|
|
348
|
+
if (triggeredIds.length > 0) {
|
|
349
|
+
enhancedDispatch({
|
|
350
|
+
type: "MARK_ALERT_TRIGGERED",
|
|
351
|
+
ids: triggeredIds
|
|
352
|
+
});
|
|
243
353
|
}
|
|
244
354
|
}, 1e3);
|
|
245
355
|
return () => clearInterval(interval);
|
|
246
356
|
}, [state.chart, hasAlerts, enhancedDispatch]);
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
alertPrevCloseRef.current = null;
|
|
359
|
+
}, [state.symbol, state.period]);
|
|
360
|
+
const writeNs = useCallback(
|
|
361
|
+
(ns, value) => {
|
|
362
|
+
if (!resolvedStorage || !resolvedStorage.persists(ns)) return;
|
|
363
|
+
try {
|
|
364
|
+
resolvedStorage.adapter.setItem(
|
|
365
|
+
resolvedStorage.key(ns),
|
|
366
|
+
JSON.stringify(value)
|
|
367
|
+
);
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
[resolvedStorage]
|
|
372
|
+
);
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
writeNs("alerts", state.alerts);
|
|
375
|
+
}, [state.alerts, writeNs]);
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
writeNs("indicators", {
|
|
378
|
+
main: state.mainIndicators,
|
|
379
|
+
sub: state.subIndicators,
|
|
380
|
+
axes: state.indicatorAxes,
|
|
381
|
+
visibility: state.indicatorVisibility
|
|
382
|
+
});
|
|
383
|
+
}, [
|
|
384
|
+
state.mainIndicators,
|
|
385
|
+
state.subIndicators,
|
|
386
|
+
state.indicatorAxes,
|
|
387
|
+
state.indicatorVisibility,
|
|
388
|
+
writeNs
|
|
389
|
+
]);
|
|
247
390
|
useEffect(() => {
|
|
248
391
|
return () => {
|
|
249
392
|
if (replayIntervalRef.current !== null) {
|
|
@@ -255,16 +398,18 @@ function KlinechartsUIProvider({
|
|
|
255
398
|
const dispatchValue = useMemo(
|
|
256
399
|
() => ({
|
|
257
400
|
dispatch: enhancedDispatch,
|
|
258
|
-
datafeed,
|
|
259
|
-
onSettingsChange,
|
|
401
|
+
datafeed: datafeedRef.current,
|
|
402
|
+
onSettingsChange: onSettingsChangeRef.current,
|
|
260
403
|
fullscreenContainerRef,
|
|
261
404
|
undoRedoListenerRef,
|
|
262
|
-
|
|
405
|
+
alertTriggeredListenersRef,
|
|
263
406
|
replayIntervalRef,
|
|
264
407
|
replaySavedDataRef,
|
|
265
|
-
replayIndexRef
|
|
408
|
+
replayIndexRef,
|
|
409
|
+
storage: resolvedStorage
|
|
266
410
|
}),
|
|
267
|
-
|
|
411
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
412
|
+
[enhancedDispatch, resolvedStorage]
|
|
268
413
|
);
|
|
269
414
|
return /* @__PURE__ */ jsx(KlinechartsUIStateContext.Provider, { value: state, children: /* @__PURE__ */ jsx(KlinechartsUIDispatchContext.Provider, { value: dispatchValue, children }) });
|
|
270
415
|
}
|
|
@@ -1222,15 +1367,17 @@ function useDrawingTools() {
|
|
|
1222
1367
|
const [isVisible, setIsVisible] = useState(true);
|
|
1223
1368
|
const [autoRetrigger, setAutoRetrigger] = useState(true);
|
|
1224
1369
|
const activeToolRef = useRef(activeTool);
|
|
1225
|
-
activeToolRef.current = activeTool;
|
|
1226
1370
|
const autoRetriggerRef = useRef(autoRetrigger);
|
|
1227
|
-
autoRetriggerRef.current = autoRetrigger;
|
|
1228
1371
|
const isLockedRef = useRef(isLocked);
|
|
1229
|
-
isLockedRef.current = isLocked;
|
|
1230
1372
|
const isVisibleRef = useRef(isVisible);
|
|
1231
|
-
isVisibleRef.current = isVisible;
|
|
1232
1373
|
const magnetModeRef = useRef(magnetMode);
|
|
1233
|
-
|
|
1374
|
+
useEffect(() => {
|
|
1375
|
+
activeToolRef.current = activeTool;
|
|
1376
|
+
autoRetriggerRef.current = autoRetrigger;
|
|
1377
|
+
isLockedRef.current = isLocked;
|
|
1378
|
+
isVisibleRef.current = isVisible;
|
|
1379
|
+
magnetModeRef.current = magnetMode;
|
|
1380
|
+
});
|
|
1234
1381
|
const categories = useMemo(
|
|
1235
1382
|
() => DRAWING_CATEGORIES.map((cat) => ({
|
|
1236
1383
|
key: cat.key,
|
|
@@ -1241,6 +1388,8 @@ function useDrawingTools() {
|
|
|
1241
1388
|
})),
|
|
1242
1389
|
[]
|
|
1243
1390
|
);
|
|
1391
|
+
const createOverlayForToolRef = useRef(() => {
|
|
1392
|
+
});
|
|
1244
1393
|
const createOverlayForTool = useCallback(
|
|
1245
1394
|
(name) => {
|
|
1246
1395
|
const mode = magnetModeRef.current === "strong" ? "strong_magnet" : magnetModeRef.current === "weak" ? "weak_magnet" : "normal";
|
|
@@ -1266,7 +1415,7 @@ function useDrawingTools() {
|
|
|
1266
1415
|
});
|
|
1267
1416
|
if (autoRetriggerRef.current && activeToolRef.current === name) {
|
|
1268
1417
|
requestAnimationFrame(() => {
|
|
1269
|
-
|
|
1418
|
+
createOverlayForToolRef.current(name);
|
|
1270
1419
|
});
|
|
1271
1420
|
}
|
|
1272
1421
|
}
|
|
@@ -1274,6 +1423,9 @@ function useDrawingTools() {
|
|
|
1274
1423
|
},
|
|
1275
1424
|
[state.chart, undoRedoListenerRef]
|
|
1276
1425
|
);
|
|
1426
|
+
useEffect(() => {
|
|
1427
|
+
createOverlayForToolRef.current = createOverlayForTool;
|
|
1428
|
+
});
|
|
1277
1429
|
const selectTool = useCallback(
|
|
1278
1430
|
(name) => {
|
|
1279
1431
|
setActiveTool(name);
|
|
@@ -1399,7 +1551,17 @@ var defaultSettings = {
|
|
|
1399
1551
|
};
|
|
1400
1552
|
function useKlinechartsUISettings() {
|
|
1401
1553
|
const { state, onSettingsChange } = useKlinechartsUI();
|
|
1402
|
-
const
|
|
1554
|
+
const { storage } = useKlinechartsUIDispatch();
|
|
1555
|
+
const [settings, setSettings] = useState(() => {
|
|
1556
|
+
if (storage && storage.persists("settings")) {
|
|
1557
|
+
try {
|
|
1558
|
+
const raw = storage.adapter.getItem(storage.key("settings"));
|
|
1559
|
+
if (raw) return { ...defaultSettings, ...JSON.parse(raw) };
|
|
1560
|
+
} catch {
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return defaultSettings;
|
|
1564
|
+
});
|
|
1403
1565
|
const isInitialMount = useRef(true);
|
|
1404
1566
|
useEffect(() => {
|
|
1405
1567
|
if (isInitialMount.current) {
|
|
@@ -1408,6 +1570,14 @@ function useKlinechartsUISettings() {
|
|
|
1408
1570
|
}
|
|
1409
1571
|
onSettingsChange?.({ ...settings });
|
|
1410
1572
|
}, [settings, onSettingsChange]);
|
|
1573
|
+
useEffect(() => {
|
|
1574
|
+
if (!storage || !storage.persists("settings")) return;
|
|
1575
|
+
if (isInitialMount.current) return;
|
|
1576
|
+
try {
|
|
1577
|
+
storage.adapter.setItem(storage.key("settings"), JSON.stringify(settings));
|
|
1578
|
+
} catch {
|
|
1579
|
+
}
|
|
1580
|
+
}, [settings, storage]);
|
|
1411
1581
|
const candleTypes = useMemo(
|
|
1412
1582
|
() => CANDLE_TYPES.map((ct) => ({
|
|
1413
1583
|
key: ct.key,
|
|
@@ -1730,22 +1900,28 @@ function useFullscreen() {
|
|
|
1730
1900
|
const enter = useCallback(() => {
|
|
1731
1901
|
const el = fullscreenContainerRef.current;
|
|
1732
1902
|
if (!el) return;
|
|
1903
|
+
let result;
|
|
1733
1904
|
if (el.requestFullscreen) {
|
|
1734
|
-
el.requestFullscreen();
|
|
1905
|
+
result = el.requestFullscreen();
|
|
1735
1906
|
} else if ("webkitRequestFullscreen" in el) {
|
|
1736
1907
|
el.webkitRequestFullscreen();
|
|
1737
1908
|
} else if ("msRequestFullscreen" in el) {
|
|
1738
1909
|
el.msRequestFullscreen();
|
|
1739
1910
|
}
|
|
1911
|
+
result?.catch(() => {
|
|
1912
|
+
});
|
|
1740
1913
|
}, [fullscreenContainerRef]);
|
|
1741
1914
|
const exit = useCallback(() => {
|
|
1915
|
+
let result;
|
|
1742
1916
|
if (document.exitFullscreen) {
|
|
1743
|
-
document.exitFullscreen();
|
|
1917
|
+
result = document.exitFullscreen();
|
|
1744
1918
|
} else if ("webkitExitFullscreen" in document) {
|
|
1745
1919
|
document.webkitExitFullscreen();
|
|
1746
1920
|
} else if ("msExitFullscreen" in document) {
|
|
1747
1921
|
document.msExitFullscreen();
|
|
1748
1922
|
}
|
|
1923
|
+
result?.catch(() => {
|
|
1924
|
+
});
|
|
1749
1925
|
}, []);
|
|
1750
1926
|
const toggle = useCallback(() => {
|
|
1751
1927
|
if (isFullscreen) {
|
|
@@ -1776,6 +1952,7 @@ function useFullscreen() {
|
|
|
1776
1952
|
function useOrderLines() {
|
|
1777
1953
|
const { state } = useKlinechartsUI();
|
|
1778
1954
|
const callbacksRef = useRef(/* @__PURE__ */ new Map());
|
|
1955
|
+
const ownedIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
1779
1956
|
const createOrderLine = useCallback(
|
|
1780
1957
|
(options) => {
|
|
1781
1958
|
if (!state.chart) return null;
|
|
@@ -1806,6 +1983,7 @@ function useOrderLines() {
|
|
|
1806
1983
|
}
|
|
1807
1984
|
}
|
|
1808
1985
|
});
|
|
1986
|
+
ownedIdsRef.current.add(id);
|
|
1809
1987
|
return id;
|
|
1810
1988
|
},
|
|
1811
1989
|
[state.chart]
|
|
@@ -1837,12 +2015,28 @@ function useOrderLines() {
|
|
|
1837
2015
|
(id) => {
|
|
1838
2016
|
state.chart?.removeOverlay({ id });
|
|
1839
2017
|
callbacksRef.current.delete(id);
|
|
2018
|
+
ownedIdsRef.current.delete(id);
|
|
1840
2019
|
},
|
|
1841
2020
|
[state.chart]
|
|
1842
2021
|
);
|
|
1843
2022
|
const removeAllOrderLines = useCallback(() => {
|
|
1844
2023
|
state.chart?.removeOverlay({ name: "orderLine" });
|
|
1845
2024
|
callbacksRef.current.clear();
|
|
2025
|
+
ownedIdsRef.current.clear();
|
|
2026
|
+
}, [state.chart]);
|
|
2027
|
+
useEffect(() => {
|
|
2028
|
+
const chart = state.chart;
|
|
2029
|
+
const owned = ownedIdsRef.current;
|
|
2030
|
+
return () => {
|
|
2031
|
+
owned.forEach((id) => {
|
|
2032
|
+
try {
|
|
2033
|
+
chart?.removeOverlay({ id });
|
|
2034
|
+
} catch {
|
|
2035
|
+
}
|
|
2036
|
+
});
|
|
2037
|
+
owned.clear();
|
|
2038
|
+
callbacksRef.current.clear();
|
|
2039
|
+
};
|
|
1846
2040
|
}, [state.chart]);
|
|
1847
2041
|
return {
|
|
1848
2042
|
createOrderLine,
|
|
@@ -1860,6 +2054,14 @@ function useUndoRedo() {
|
|
|
1860
2054
|
const isProcessingRef = useRef(false);
|
|
1861
2055
|
const canUndo = undoStack.length > 0;
|
|
1862
2056
|
const canRedo = redoStack.length > 0;
|
|
2057
|
+
const undoStackRef = useRef([]);
|
|
2058
|
+
const redoStackRef = useRef([]);
|
|
2059
|
+
useEffect(() => {
|
|
2060
|
+
undoStackRef.current = undoStack;
|
|
2061
|
+
}, [undoStack]);
|
|
2062
|
+
useEffect(() => {
|
|
2063
|
+
redoStackRef.current = redoStack;
|
|
2064
|
+
}, [redoStack]);
|
|
1863
2065
|
const pushAction = useCallback((action) => {
|
|
1864
2066
|
if (isProcessingRef.current) return;
|
|
1865
2067
|
setUndoStack((prev) => [...prev, action]);
|
|
@@ -1876,10 +2078,11 @@ function useUndoRedo() {
|
|
|
1876
2078
|
setRedoStack([]);
|
|
1877
2079
|
}, []);
|
|
1878
2080
|
const undo = useCallback(() => {
|
|
1879
|
-
|
|
2081
|
+
const stack = undoStackRef.current;
|
|
2082
|
+
if (stack.length === 0 || !state.chart || isProcessingRef.current)
|
|
1880
2083
|
return;
|
|
1881
2084
|
isProcessingRef.current = true;
|
|
1882
|
-
const action =
|
|
2085
|
+
const action = stack[stack.length - 1];
|
|
1883
2086
|
setUndoStack((prev) => prev.slice(0, -1));
|
|
1884
2087
|
try {
|
|
1885
2088
|
switch (action.type) {
|
|
@@ -2000,12 +2203,13 @@ function useUndoRedo() {
|
|
|
2000
2203
|
} finally {
|
|
2001
2204
|
isProcessingRef.current = false;
|
|
2002
2205
|
}
|
|
2003
|
-
}, [
|
|
2206
|
+
}, [state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
|
|
2004
2207
|
const redo = useCallback(() => {
|
|
2005
|
-
|
|
2208
|
+
const stack = redoStackRef.current;
|
|
2209
|
+
if (stack.length === 0 || !state.chart || isProcessingRef.current)
|
|
2006
2210
|
return;
|
|
2007
2211
|
isProcessingRef.current = true;
|
|
2008
|
-
const action =
|
|
2212
|
+
const action = stack[stack.length - 1];
|
|
2009
2213
|
setRedoStack((prev) => prev.slice(0, -1));
|
|
2010
2214
|
try {
|
|
2011
2215
|
switch (action.type) {
|
|
@@ -2130,7 +2334,7 @@ function useUndoRedo() {
|
|
|
2130
2334
|
} finally {
|
|
2131
2335
|
isProcessingRef.current = false;
|
|
2132
2336
|
}
|
|
2133
|
-
}, [
|
|
2337
|
+
}, [state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
|
|
2134
2338
|
useEffect(() => {
|
|
2135
2339
|
const handleKeyDown = (e) => {
|
|
2136
2340
|
const isCtrlOrMeta = e.ctrlKey || e.metaKey;
|
|
@@ -2168,6 +2372,7 @@ function generateId() {
|
|
|
2168
2372
|
);
|
|
2169
2373
|
}
|
|
2170
2374
|
function getLayoutIds() {
|
|
2375
|
+
if (typeof localStorage === "undefined") return [];
|
|
2171
2376
|
try {
|
|
2172
2377
|
const raw = localStorage.getItem(INDEX_KEY);
|
|
2173
2378
|
return raw ? JSON.parse(raw) : [];
|
|
@@ -2176,6 +2381,7 @@ function getLayoutIds() {
|
|
|
2176
2381
|
}
|
|
2177
2382
|
}
|
|
2178
2383
|
function getEntry(id) {
|
|
2384
|
+
if (typeof localStorage === "undefined") return null;
|
|
2179
2385
|
try {
|
|
2180
2386
|
const raw = localStorage.getItem(STORAGE_KEY_PREFIX + id);
|
|
2181
2387
|
return raw ? JSON.parse(raw) : null;
|
|
@@ -2185,35 +2391,31 @@ function getEntry(id) {
|
|
|
2185
2391
|
}
|
|
2186
2392
|
function useLayoutManager() {
|
|
2187
2393
|
const { state, dispatch } = useKlinechartsUI();
|
|
2188
|
-
const [layouts, setLayouts] = useState(
|
|
2394
|
+
const [layouts, setLayouts] = useState(
|
|
2395
|
+
() => getLayoutIds().map((id) => getEntry(id)).filter((e) => e !== null)
|
|
2396
|
+
);
|
|
2189
2397
|
const [autoSaveEnabled, setAutoSaveEnabled] = useState(false);
|
|
2190
2398
|
const autoSaveTimerRef = useRef(null);
|
|
2191
2399
|
const autoSaveIdRef = useRef(null);
|
|
2192
2400
|
const refreshLayouts = useCallback(() => {
|
|
2193
|
-
|
|
2194
|
-
|
|
2401
|
+
setLayouts(
|
|
2402
|
+
getLayoutIds().map((id) => getEntry(id)).filter((e) => e !== null)
|
|
2403
|
+
);
|
|
2195
2404
|
}, []);
|
|
2196
|
-
useEffect(() => {
|
|
2197
|
-
refreshLayouts();
|
|
2198
|
-
}, [refreshLayouts]);
|
|
2199
2405
|
const serializeState = useCallback(() => {
|
|
2200
2406
|
const chart = state.chart;
|
|
2201
2407
|
if (!chart) return null;
|
|
2202
2408
|
const indicators2 = [];
|
|
2203
|
-
const
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
...indicator.styles ? { styles: indicator.styles } : {},
|
|
2214
|
-
...yAxisId ? { yAxisId } : {}
|
|
2215
|
-
});
|
|
2216
|
-
});
|
|
2409
|
+
const allIndicators = chart.getIndicators();
|
|
2410
|
+
for (const indicator of allIndicators) {
|
|
2411
|
+
const yAxisId = state.indicatorAxes[indicator.id];
|
|
2412
|
+
indicators2.push({
|
|
2413
|
+
paneId: indicator.paneId,
|
|
2414
|
+
name: indicator.name,
|
|
2415
|
+
calcParams: indicator.calcParams,
|
|
2416
|
+
visible: indicator.visible,
|
|
2417
|
+
...indicator.styles ? { styles: indicator.styles } : {},
|
|
2418
|
+
...yAxisId ? { yAxisId } : {}
|
|
2217
2419
|
});
|
|
2218
2420
|
}
|
|
2219
2421
|
const drawings = [];
|
|
@@ -2277,13 +2479,8 @@ function useLayoutManager() {
|
|
|
2277
2479
|
if (chartState.version !== STATE_VERSION) return false;
|
|
2278
2480
|
const chart = state.chart;
|
|
2279
2481
|
chart.removeOverlay();
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
indicatorMap.forEach((_paneIndicators, paneId) => {
|
|
2283
|
-
_paneIndicators.forEach((_, indicatorName) => {
|
|
2284
|
-
chart.removeIndicator({ paneId, name: indicatorName });
|
|
2285
|
-
});
|
|
2286
|
-
});
|
|
2482
|
+
for (const indicator of chart.getIndicators()) {
|
|
2483
|
+
chart.removeIndicator({ id: indicator.id });
|
|
2287
2484
|
}
|
|
2288
2485
|
const newMainIndicators = [];
|
|
2289
2486
|
const newSubIndicators = {};
|
|
@@ -2301,7 +2498,12 @@ function useLayoutManager() {
|
|
|
2301
2498
|
visible: ind.visible
|
|
2302
2499
|
},
|
|
2303
2500
|
{
|
|
2304
|
-
|
|
2501
|
+
// Main indicators stack OVER the candle series on the candle
|
|
2502
|
+
// pane (isStack: true), exactly as useIndicators does on the
|
|
2503
|
+
// add path. The previous `ind.paneId !== "candle_pane"` inverted
|
|
2504
|
+
// this, so loading a layout with main indicators replaced the
|
|
2505
|
+
// candles instead of overlaying them.
|
|
2506
|
+
isStack: isMain,
|
|
2305
2507
|
pane: { id: ind.paneId },
|
|
2306
2508
|
...ind.yAxisId ? { yAxis: { id: ind.yAxisId } } : {}
|
|
2307
2509
|
}
|
|
@@ -2501,8 +2703,10 @@ function useScriptEditor() {
|
|
|
2501
2703
|
const [status, setStatus] = useState("");
|
|
2502
2704
|
const [isRunning, setIsRunning] = useState(false);
|
|
2503
2705
|
const activeIdRef = useRef(null);
|
|
2706
|
+
const [activeName, setActiveName] = useState(null);
|
|
2504
2707
|
const activeNameRef = useRef(null);
|
|
2505
|
-
|
|
2708
|
+
activeNameRef.current = activeName;
|
|
2709
|
+
const hasActiveScript = activeName !== null;
|
|
2506
2710
|
const runScript = useCallback(() => {
|
|
2507
2711
|
const chart = state.chart;
|
|
2508
2712
|
if (!chart) {
|
|
@@ -2523,19 +2727,26 @@ function useScriptEditor() {
|
|
|
2523
2727
|
`"use strict";
|
|
2524
2728
|
${code}`
|
|
2525
2729
|
];
|
|
2526
|
-
const
|
|
2730
|
+
const compiledFn = new Function(...allArgs);
|
|
2527
2731
|
const shadowValues = SHADOW_KEYS.map(() => void 0);
|
|
2528
|
-
const
|
|
2529
|
-
|
|
2732
|
+
const sampleResult = compiledFn(
|
|
2733
|
+
...shadowValues,
|
|
2734
|
+
TA_default,
|
|
2735
|
+
dataList,
|
|
2736
|
+
parsedParams
|
|
2737
|
+
);
|
|
2738
|
+
if (!Array.isArray(sampleResult)) {
|
|
2530
2739
|
throw new Error("Script must return an Array.");
|
|
2531
2740
|
}
|
|
2532
|
-
const sample =
|
|
2741
|
+
const sample = sampleResult.find(
|
|
2742
|
+
(v) => v !== null && v !== void 0
|
|
2743
|
+
);
|
|
2533
2744
|
const seriesKeys = sample ? Object.keys(sample) : ["value"];
|
|
2534
2745
|
if (seriesKeys.length === 0) {
|
|
2535
2746
|
throw new Error("Returned objects have no keys.");
|
|
2536
2747
|
}
|
|
2537
2748
|
scriptCounter++;
|
|
2538
|
-
const indicatorName = `
|
|
2749
|
+
const indicatorName = `_custom_script_active`;
|
|
2539
2750
|
const figures = seriesKeys.map((key, i) => ({
|
|
2540
2751
|
key,
|
|
2541
2752
|
title: `${key}: `,
|
|
@@ -2547,7 +2758,7 @@ ${code}`
|
|
|
2547
2758
|
shortName: (scriptName.trim() || `Script #${scriptCounter}`) + (placement === "main" ? " (Main)" : " (Sub)"),
|
|
2548
2759
|
calcParams: parsedParams,
|
|
2549
2760
|
figures,
|
|
2550
|
-
calc: () =>
|
|
2761
|
+
calc: (liveDataList) => compiledFn(...shadowValues, TA_default, liveDataList, parsedParams),
|
|
2551
2762
|
extendData: {
|
|
2552
2763
|
isCustomScript: true,
|
|
2553
2764
|
code,
|
|
@@ -2573,7 +2784,7 @@ ${code}`
|
|
|
2573
2784
|
paneId = chart.createIndicator({ name: indicatorName });
|
|
2574
2785
|
}
|
|
2575
2786
|
activeIdRef.current = typeof paneId === "string" ? paneId : null;
|
|
2576
|
-
|
|
2787
|
+
setActiveName(indicatorName);
|
|
2577
2788
|
const title = scriptName.trim() || `Script #${scriptCounter}`;
|
|
2578
2789
|
setStatus(
|
|
2579
2790
|
`${title} applied to ${placement === "main" ? "Main Chart" : "Sub Pane"} \u2014 ${seriesKeys.length} series: ${seriesKeys.join(", ")}`
|
|
@@ -2595,7 +2806,7 @@ ${code}`
|
|
|
2595
2806
|
} catch {
|
|
2596
2807
|
}
|
|
2597
2808
|
activeIdRef.current = null;
|
|
2598
|
-
|
|
2809
|
+
setActiveName(null);
|
|
2599
2810
|
setStatus("Indicator removed.");
|
|
2600
2811
|
}
|
|
2601
2812
|
}, [state.chart]);
|
|
@@ -2709,10 +2920,10 @@ function useCrosshair() {
|
|
|
2709
2920
|
var alertCounter = 0;
|
|
2710
2921
|
function useAlerts() {
|
|
2711
2922
|
const { state, dispatch } = useKlinechartsUI();
|
|
2712
|
-
const {
|
|
2923
|
+
const { alertTriggeredListenersRef } = useKlinechartsUIDispatch();
|
|
2713
2924
|
const alerts = state.alerts;
|
|
2714
2925
|
const addAlert = useCallback(
|
|
2715
|
-
(price, condition, message, extendData) => {
|
|
2926
|
+
(price, condition, message, extendData, target) => {
|
|
2716
2927
|
const id = `alert_${++alertCounter}`;
|
|
2717
2928
|
const precision = state.chart?.getSymbol()?.pricePrecision ?? 2;
|
|
2718
2929
|
const resolvedExtendData = {
|
|
@@ -2725,9 +2936,12 @@ function useAlerts() {
|
|
|
2725
2936
|
condition,
|
|
2726
2937
|
message,
|
|
2727
2938
|
triggered: false,
|
|
2728
|
-
extendData: resolvedExtendData
|
|
2939
|
+
extendData: resolvedExtendData,
|
|
2940
|
+
// Persist the target only when it's an indicator alert; omit for the
|
|
2941
|
+
// default price target so existing serialized alerts stay compatible.
|
|
2942
|
+
...target && target.type === "indicator" ? { target } : {}
|
|
2729
2943
|
};
|
|
2730
|
-
dispatch({ type: "
|
|
2944
|
+
dispatch({ type: "ADD_ALERT", alert });
|
|
2731
2945
|
if (state.chart) {
|
|
2732
2946
|
ensureAlertLineRegistered();
|
|
2733
2947
|
state.chart.createOverlay({
|
|
@@ -2741,29 +2955,28 @@ function useAlerts() {
|
|
|
2741
2955
|
}
|
|
2742
2956
|
return id;
|
|
2743
2957
|
},
|
|
2744
|
-
[state.chart,
|
|
2958
|
+
[state.chart, dispatch]
|
|
2745
2959
|
);
|
|
2746
2960
|
const removeAlert = useCallback(
|
|
2747
2961
|
(id) => {
|
|
2748
|
-
dispatch({
|
|
2749
|
-
type: "SET_ALERTS",
|
|
2750
|
-
alerts: state.alerts.filter((a) => a.id !== id)
|
|
2751
|
-
});
|
|
2962
|
+
dispatch({ type: "REMOVE_ALERT", id });
|
|
2752
2963
|
state.chart?.removeOverlay({ id });
|
|
2753
2964
|
},
|
|
2754
|
-
[state.chart,
|
|
2965
|
+
[state.chart, dispatch]
|
|
2755
2966
|
);
|
|
2756
2967
|
const clearAlerts = useCallback(() => {
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
dispatch({ type: "SET_ALERTS", alerts: [] });
|
|
2761
|
-
}, [state.chart, state.alerts, dispatch]);
|
|
2968
|
+
state.chart?.removeOverlay({ groupId: "price_alerts" });
|
|
2969
|
+
dispatch({ type: "CLEAR_ALERTS" });
|
|
2970
|
+
}, [state.chart, dispatch]);
|
|
2762
2971
|
const onAlertTriggered = useCallback(
|
|
2763
2972
|
(callback) => {
|
|
2764
|
-
|
|
2973
|
+
const set = alertTriggeredListenersRef.current;
|
|
2974
|
+
set.add(callback);
|
|
2975
|
+
return () => {
|
|
2976
|
+
set.delete(callback);
|
|
2977
|
+
};
|
|
2765
2978
|
},
|
|
2766
|
-
[
|
|
2979
|
+
[alertTriggeredListenersRef]
|
|
2767
2980
|
);
|
|
2768
2981
|
return {
|
|
2769
2982
|
alerts,
|
|
@@ -2942,6 +3155,7 @@ function useReplay() {
|
|
|
2942
3155
|
);
|
|
2943
3156
|
const startReplay = useCallback(() => {
|
|
2944
3157
|
if (!state.chart) return;
|
|
3158
|
+
if (isReplaying) return;
|
|
2945
3159
|
const dataList = state.chart.getDataList();
|
|
2946
3160
|
if (!dataList || dataList.length === 0) return;
|
|
2947
3161
|
replaySavedDataRef.current = [...dataList];
|
|
@@ -2957,7 +3171,12 @@ function useReplay() {
|
|
|
2957
3171
|
});
|
|
2958
3172
|
state.chart.clearData?.();
|
|
2959
3173
|
startInterval(speed);
|
|
2960
|
-
}, [state.chart, speed, startInterval, replaySavedDataRef, replayIndexRef, dispatch]);
|
|
3174
|
+
}, [state.chart, state.symbol, state.period, speed, startInterval, isReplaying, replaySavedDataRef, replayIndexRef, dispatch]);
|
|
3175
|
+
useEffect(() => {
|
|
3176
|
+
if (isReplaying) {
|
|
3177
|
+
stopReplay();
|
|
3178
|
+
}
|
|
3179
|
+
}, [state.symbol, state.period]);
|
|
2961
3180
|
const stopReplay = useCallback(() => {
|
|
2962
3181
|
if (!state.chart) return;
|
|
2963
3182
|
clearInterval_();
|
|
@@ -3053,24 +3272,29 @@ var DEFAULT_COLORS = [
|
|
|
3053
3272
|
"#ff5722",
|
|
3054
3273
|
"#8bc34a"
|
|
3055
3274
|
];
|
|
3056
|
-
var colorIndex = 0;
|
|
3057
|
-
var compareCounter = 0;
|
|
3058
3275
|
function useCompare() {
|
|
3059
3276
|
const { state, datafeed } = useKlinechartsUI();
|
|
3060
3277
|
const [symbols, setSymbols] = useState([]);
|
|
3061
3278
|
const indicatorsRef = useRef(/* @__PURE__ */ new Map());
|
|
3279
|
+
const instanceSalt = useId().replace(/[^a-zA-Z0-9]/g, "");
|
|
3062
3280
|
const addSymbol = useCallback(
|
|
3063
3281
|
async (ticker, color) => {
|
|
3064
3282
|
if (!state.chart || !datafeed) return;
|
|
3065
3283
|
if (indicatorsRef.current.has(ticker)) return;
|
|
3066
|
-
const assignedColor = color ?? DEFAULT_COLORS[
|
|
3284
|
+
const assignedColor = color ?? DEFAULT_COLORS[symbols.length % DEFAULT_COLORS.length];
|
|
3067
3285
|
const mainDataList = state.chart.getDataList();
|
|
3068
3286
|
if (!mainDataList || mainDataList.length === 0) return;
|
|
3069
3287
|
const from = mainDataList[0].timestamp;
|
|
3070
3288
|
const to = mainDataList[mainDataList.length - 1].timestamp;
|
|
3289
|
+
const mainSymbol = state.symbol;
|
|
3290
|
+
const symbol = {
|
|
3291
|
+
ticker,
|
|
3292
|
+
pricePrecision: mainSymbol?.pricePrecision ?? 2,
|
|
3293
|
+
volumePrecision: mainSymbol?.volumePrecision ?? 8
|
|
3294
|
+
};
|
|
3071
3295
|
const compareData = await datafeed.getHistoryKLineData(
|
|
3072
|
-
|
|
3073
|
-
|
|
3296
|
+
symbol,
|
|
3297
|
+
state.period,
|
|
3074
3298
|
from,
|
|
3075
3299
|
to
|
|
3076
3300
|
);
|
|
@@ -3089,13 +3313,7 @@ function useCompare() {
|
|
|
3089
3313
|
}
|
|
3090
3314
|
if (basePrice === null || basePrice === 0) return;
|
|
3091
3315
|
const bp = basePrice;
|
|
3092
|
-
const
|
|
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}`;
|
|
3316
|
+
const indicatorName = `__cmp_${instanceSalt}_${ticker}`;
|
|
3099
3317
|
registerIndicator({
|
|
3100
3318
|
name: indicatorName,
|
|
3101
3319
|
shortName: ticker,
|
|
@@ -3107,7 +3325,16 @@ function useCompare() {
|
|
|
3107
3325
|
styles: () => ({ color: assignedColor })
|
|
3108
3326
|
}
|
|
3109
3327
|
],
|
|
3110
|
-
calc: () =>
|
|
3328
|
+
calc: (dataList) => {
|
|
3329
|
+
let lastPct = null;
|
|
3330
|
+
return dataList.map((bar) => {
|
|
3331
|
+
const close = compareMap.get(bar.timestamp);
|
|
3332
|
+
if (close != null) {
|
|
3333
|
+
lastPct = (close - bp) / bp * 100;
|
|
3334
|
+
}
|
|
3335
|
+
return { pct: lastPct };
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3111
3338
|
});
|
|
3112
3339
|
const paneId = state.chart.createIndicator(
|
|
3113
3340
|
{ name: indicatorName },
|
|
@@ -3125,7 +3352,7 @@ function useCompare() {
|
|
|
3125
3352
|
];
|
|
3126
3353
|
});
|
|
3127
3354
|
},
|
|
3128
|
-
[state.chart, state.period, datafeed]
|
|
3355
|
+
[state.chart, state.symbol, state.period, datafeed, symbols.length, instanceSalt]
|
|
3129
3356
|
);
|
|
3130
3357
|
const removeSymbol = useCallback(
|
|
3131
3358
|
(ticker) => {
|
|
@@ -3335,13 +3562,11 @@ function useAnnotations() {
|
|
|
3335
3562
|
[state.chart]
|
|
3336
3563
|
);
|
|
3337
3564
|
const clearAnnotations = useCallback(() => {
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
});
|
|
3344
|
-
}, [state.chart]);
|
|
3565
|
+
for (const annotation of annotations) {
|
|
3566
|
+
state.chart?.removeOverlay({ id: annotation.id });
|
|
3567
|
+
}
|
|
3568
|
+
setAnnotations([]);
|
|
3569
|
+
}, [state.chart, annotations]);
|
|
3345
3570
|
useEffect(() => {
|
|
3346
3571
|
return () => {
|
|
3347
3572
|
state.chart?.removeOverlay({ groupId: "annotations" });
|
|
@@ -3355,71 +3580,161 @@ function useAnnotations() {
|
|
|
3355
3580
|
clearAnnotations
|
|
3356
3581
|
};
|
|
3357
3582
|
}
|
|
3583
|
+
var WorkspaceContext = createContext(null);
|
|
3584
|
+
function useWorkspace() {
|
|
3585
|
+
const ctx = useContext(WorkspaceContext);
|
|
3586
|
+
if (!ctx) {
|
|
3587
|
+
throw new Error("useWorkspace must be used within a <WorkspaceProvider>");
|
|
3588
|
+
}
|
|
3589
|
+
return ctx;
|
|
3590
|
+
}
|
|
3358
3591
|
|
|
3359
|
-
// src/
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3592
|
+
// src/workspace/types.ts
|
|
3593
|
+
var DEFAULT_SYNC_CONFIG = {
|
|
3594
|
+
crosshair: true,
|
|
3595
|
+
scroll: true,
|
|
3596
|
+
zoom: true,
|
|
3597
|
+
symbol: true,
|
|
3598
|
+
period: true
|
|
3599
|
+
};
|
|
3600
|
+
function reducer2(state, action) {
|
|
3601
|
+
switch (action.type) {
|
|
3602
|
+
case "SET_CELLS":
|
|
3603
|
+
return { ...state, cells: action.cells };
|
|
3604
|
+
case "ADD_CELL":
|
|
3605
|
+
return { ...state, cells: [...state.cells, action.cell] };
|
|
3606
|
+
case "REMOVE_CELL": {
|
|
3607
|
+
const cells = state.cells.filter((c) => c.id !== action.id);
|
|
3608
|
+
return {
|
|
3609
|
+
cells,
|
|
3610
|
+
activeCellId: state.activeCellId === action.id ? cells[0]?.id ?? null : state.activeCellId
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
case "SET_CELL_SYMBOL":
|
|
3614
|
+
return {
|
|
3615
|
+
...state,
|
|
3616
|
+
cells: state.cells.map(
|
|
3617
|
+
(c) => c.id === action.id ? { ...c, symbol: action.symbol } : c
|
|
3618
|
+
)
|
|
3619
|
+
};
|
|
3620
|
+
case "SET_CELL_PERIOD":
|
|
3621
|
+
return {
|
|
3622
|
+
...state,
|
|
3623
|
+
cells: state.cells.map(
|
|
3624
|
+
(c) => c.id === action.id ? { ...c, period: action.period } : c
|
|
3625
|
+
)
|
|
3626
|
+
};
|
|
3627
|
+
case "SET_ACTIVE_CELL":
|
|
3628
|
+
return { ...state, activeCellId: action.id };
|
|
3629
|
+
default:
|
|
3630
|
+
return state;
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
function WorkspaceProvider({
|
|
3634
|
+
defaultCells,
|
|
3635
|
+
sync,
|
|
3636
|
+
children
|
|
3637
|
+
}) {
|
|
3638
|
+
const [state, dispatch] = useReducer(reducer2, defaultCells, (cells) => ({
|
|
3639
|
+
cells,
|
|
3640
|
+
activeCellId: cells[0]?.id ?? null
|
|
3641
|
+
}));
|
|
3642
|
+
const chartsRef = useRef(/* @__PURE__ */ new Map());
|
|
3643
|
+
const broadcastingRef = useRef(false);
|
|
3644
|
+
const resolvedSync = useMemo(
|
|
3645
|
+
() => ({ ...DEFAULT_SYNC_CONFIG, ...sync }),
|
|
3646
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3647
|
+
[]
|
|
3648
|
+
);
|
|
3649
|
+
const value = useMemo(
|
|
3650
|
+
() => ({
|
|
3651
|
+
state,
|
|
3652
|
+
dispatch,
|
|
3653
|
+
chartsRef,
|
|
3654
|
+
broadcastingRef,
|
|
3655
|
+
sync: resolvedSync
|
|
3656
|
+
}),
|
|
3657
|
+
[state, resolvedSync]
|
|
3658
|
+
);
|
|
3659
|
+
return /* @__PURE__ */ jsx(WorkspaceContext.Provider, { value, children });
|
|
3660
|
+
}
|
|
3661
|
+
function useChartSync({ cellId }) {
|
|
3662
|
+
const { chartsRef, broadcastingRef, sync, dispatch: workspaceDispatch } = useWorkspace();
|
|
3663
|
+
const { state } = useKlinechartsUI();
|
|
3664
|
+
const chart = state.chart;
|
|
3665
|
+
useEffect(() => {
|
|
3666
|
+
if (!chart) return;
|
|
3667
|
+
chartsRef.current.set(cellId, chart);
|
|
3668
|
+
return () => {
|
|
3669
|
+
chartsRef.current.delete(cellId);
|
|
3670
|
+
};
|
|
3671
|
+
}, [chart, cellId, chartsRef]);
|
|
3672
|
+
useEffect(() => {
|
|
3673
|
+
if (!chart) return;
|
|
3674
|
+
const broadcast = (fn) => {
|
|
3675
|
+
if (broadcastingRef.current) return;
|
|
3676
|
+
broadcastingRef.current = true;
|
|
3365
3677
|
try {
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
{ ...params.period, label: "" },
|
|
3373
|
-
0,
|
|
3374
|
-
Date.now()
|
|
3375
|
-
);
|
|
3376
|
-
if (gen !== currentGen) return;
|
|
3377
|
-
if (data.length > 0) {
|
|
3378
|
-
oldestTimestamp = data[0].timestamp;
|
|
3379
|
-
}
|
|
3380
|
-
params.callback(data, {
|
|
3381
|
-
forward: data.length > 0,
|
|
3382
|
-
backward: false
|
|
3383
|
-
});
|
|
3384
|
-
} else if (params.type === "forward" && oldestTimestamp !== null) {
|
|
3385
|
-
const gen = currentGen;
|
|
3386
|
-
const data = await datafeed.getHistoryKLineData(
|
|
3387
|
-
params.symbol,
|
|
3388
|
-
{ ...params.period, label: "" },
|
|
3389
|
-
0,
|
|
3390
|
-
oldestTimestamp - 1
|
|
3391
|
-
);
|
|
3392
|
-
if (gen !== currentGen) return;
|
|
3393
|
-
if (data.length > 0) {
|
|
3394
|
-
oldestTimestamp = data[0].timestamp;
|
|
3678
|
+
chartsRef.current.forEach((target, id) => {
|
|
3679
|
+
if (id !== cellId) {
|
|
3680
|
+
try {
|
|
3681
|
+
fn(target);
|
|
3682
|
+
} catch {
|
|
3683
|
+
}
|
|
3395
3684
|
}
|
|
3396
|
-
|
|
3397
|
-
forward: data.length > 0,
|
|
3398
|
-
backward: false
|
|
3399
|
-
});
|
|
3400
|
-
} else if (params.type === "backward") {
|
|
3401
|
-
params.callback([], { forward: false, backward: false });
|
|
3402
|
-
}
|
|
3403
|
-
} catch (error) {
|
|
3404
|
-
console.error("Failed to load chart data:", error);
|
|
3405
|
-
params.callback([], { forward: false, backward: false });
|
|
3685
|
+
});
|
|
3406
3686
|
} finally {
|
|
3407
|
-
|
|
3687
|
+
broadcastingRef.current = false;
|
|
3408
3688
|
}
|
|
3409
|
-
}
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3689
|
+
};
|
|
3690
|
+
const onCrosshairChange = (data) => {
|
|
3691
|
+
if (!sync.crosshair) return;
|
|
3692
|
+
const crosshair = data;
|
|
3693
|
+
if (!crosshair) return;
|
|
3694
|
+
broadcast((target) => target.executeAction("onCrosshairChange", crosshair));
|
|
3695
|
+
};
|
|
3696
|
+
const onScroll = () => {
|
|
3697
|
+
if (!sync.scroll) return;
|
|
3698
|
+
let range = null;
|
|
3699
|
+
try {
|
|
3700
|
+
range = chart.getVisibleRange();
|
|
3701
|
+
} catch {
|
|
3702
|
+
return;
|
|
3703
|
+
}
|
|
3704
|
+
if (!range || range.realTo == null) return;
|
|
3705
|
+
broadcast(
|
|
3706
|
+
(target) => target.scrollToTimestamp(range.realTo, 0)
|
|
3415
3707
|
);
|
|
3416
|
-
}
|
|
3417
|
-
|
|
3418
|
-
|
|
3708
|
+
};
|
|
3709
|
+
const onZoom = () => {
|
|
3710
|
+
if (!sync.zoom) return;
|
|
3711
|
+
let bar = 0;
|
|
3712
|
+
try {
|
|
3713
|
+
bar = chart.getBarSpace().bar;
|
|
3714
|
+
} catch {
|
|
3715
|
+
return;
|
|
3716
|
+
}
|
|
3717
|
+
broadcast((target) => target.setBarSpace(bar));
|
|
3718
|
+
};
|
|
3719
|
+
chart.subscribeAction("onCrosshairChange", onCrosshairChange);
|
|
3720
|
+
chart.subscribeAction("onScroll", onScroll);
|
|
3721
|
+
chart.subscribeAction("onZoom", onZoom);
|
|
3722
|
+
return () => {
|
|
3723
|
+
chart.unsubscribeAction("onCrosshairChange", onCrosshairChange);
|
|
3724
|
+
chart.unsubscribeAction("onScroll", onScroll);
|
|
3725
|
+
chart.unsubscribeAction("onZoom", onZoom);
|
|
3726
|
+
};
|
|
3727
|
+
}, [chart, cellId, chartsRef, broadcastingRef, sync.crosshair, sync.scroll, sync.zoom]);
|
|
3728
|
+
useEffect(() => {
|
|
3729
|
+
if (state.symbol) {
|
|
3730
|
+
workspaceDispatch({ type: "SET_CELL_SYMBOL", id: cellId, symbol: state.symbol });
|
|
3419
3731
|
}
|
|
3420
|
-
};
|
|
3732
|
+
}, [state.symbol]);
|
|
3733
|
+
useEffect(() => {
|
|
3734
|
+
workspaceDispatch({ type: "SET_CELL_PERIOD", id: cellId, period: state.period });
|
|
3735
|
+
}, [state.period]);
|
|
3421
3736
|
}
|
|
3422
3737
|
|
|
3423
|
-
export { CANDLE_TYPES, COMPARE_RULES, DEFAULT_PERIODS,
|
|
3738
|
+
export { CANDLE_TYPES, COMPARE_RULES, DEFAULT_PERIODS, DEFAULT_STORAGE_KEY_PREFIX, DEFAULT_STORAGE_NAMESPACES, DEFAULT_SYNC_CONFIG, DRAWING_CATEGORIES, INDICATOR_PARAMS, KlinechartsUIProvider, MAIN_INDICATORS, PRICE_AXIS_TYPES, SUB_INDICATORS, TIMEZONES, TOOLTIP_SHOW_RULES, WorkspaceProvider, YAXIS_POSITIONS, createDefaultStorage, resolveStorage, useAlerts, useAnnotations, useChartAxes, useChartSync, useCompare, useCrosshair, useDataExport, useDrawingTools, useFullscreen, useHotkeys, useIndicators, useKlinechartsUILoading, useKlinechartsUISettings, useKlinechartsUITheme, useLayoutManager, useMeasure, useOrderLines, usePeriods, useReplay, useScreenshot, useScriptEditor, useSymbolSearch, useTimezone, useUndoRedo, useWatchlist, useWorkspace };
|
|
3424
3739
|
//# sourceMappingURL=index.js.map
|
|
3425
3740
|
//# sourceMappingURL=index.js.map
|