react-klinecharts-ui 0.1.0 → 0.2.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 +284 -24
- package/dist/chunk-47JXSAIT.cjs +2467 -0
- package/dist/chunk-47JXSAIT.cjs.map +1 -0
- package/dist/chunk-B3NZ66YA.js +2424 -0
- package/dist/chunk-B3NZ66YA.js.map +1 -0
- package/dist/extensions.cjs +8 -4
- package/dist/extensions.d.cts +5 -1
- package/dist/extensions.d.ts +5 -1
- package/dist/extensions.js +1 -1
- package/dist/index.cjs +1058 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +253 -3
- package/dist/index.d.ts +253 -3
- package/dist/index.js +949 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-PUULIBFN.js +0 -911
- package/dist/chunk-PUULIBFN.js.map +0 -1
- package/dist/chunk-SLTXNLKN.cjs +0 -932
- package/dist/chunk-SLTXNLKN.cjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { registerExtensions } from './chunk-
|
|
2
|
-
export { abcd_default as abcd, anyWaves_default as anyWaves, arrow_default as arrow, circle_default as circle, eightWaves_default as eightWaves, 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, orderLine_default as orderLine, overlays, parallelogram_default as parallelogram, rect_default as rect, registerExtensions, threeWaves_default as threeWaves, triangle_default as triangle, xabcd_default as xabcd } from './chunk-
|
|
1
|
+
import { registerExtensions, TA_default } from './chunk-B3NZ66YA.js';
|
|
2
|
+
export { TA_default as TA, abcd_default as abcd, anyWaves_default as anyWaves, arrow_default as arrow, bollTv_default as bollTv, brush_default as brush, cci_default as cci, circle_default as circle, eightWaves_default as eightWaves, elliottWave_default as elliottWave, fibRetracement_default as fibRetracement, fibonacciCircle_default as fibonacciCircle, fibonacciExtension_default as fibonacciExtension, fibonacciSegment_default as fibonacciSegment, fibonacciSpeedResistanceFan_default as fibonacciSpeedResistanceFan, fibonacciSpiral_default as fibonacciSpiral, fiveWaves_default as fiveWaves, gannBox_default as gannBox, gannFan_default as gannFan, hma_default as hma, ichimoku_default as ichimoku, indicators, longPosition_default as longPosition, maRibbon_default as maRibbon, macdTv_default as macdTv, measure_default as measure, orderLine_default as orderLine, overlays, parallelChannel_default as parallelChannel, parallelogram_default as parallelogram, pivotPoints_default as pivotPoints, ray_default as ray, rect_default as rect, registerExtensions, rsiTv_default as rsiTv, shortPosition_default as shortPosition, stochastic_default as stochastic, superTrend_default as superTrend, threeWaves_default as threeWaves, triangle_default as triangle, vwap_default as vwap, xabcd_default as xabcd } from './chunk-B3NZ66YA.js';
|
|
3
3
|
import { createContext, useContext, useReducer, useRef, useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
|
-
import { registerOverlay } from 'react-klinecharts';
|
|
4
|
+
import { registerOverlay, registerIndicator } from 'react-klinecharts';
|
|
5
5
|
import { jsx } from 'react/jsx-runtime';
|
|
6
6
|
|
|
7
7
|
var KlinechartsUIStateContext = createContext(null);
|
|
@@ -124,6 +124,7 @@ function KlinechartsUIProvider({
|
|
|
124
124
|
})
|
|
125
125
|
);
|
|
126
126
|
const fullscreenContainerRef = useRef(null);
|
|
127
|
+
const undoRedoListenerRef = useRef(null);
|
|
127
128
|
const stateRef = useRef(state);
|
|
128
129
|
stateRef.current = state;
|
|
129
130
|
const callbacksRef = useRef({
|
|
@@ -189,7 +190,8 @@ function KlinechartsUIProvider({
|
|
|
189
190
|
dispatch: enhancedDispatch,
|
|
190
191
|
datafeed,
|
|
191
192
|
onSettingsChange,
|
|
192
|
-
fullscreenContainerRef
|
|
193
|
+
fullscreenContainerRef,
|
|
194
|
+
undoRedoListenerRef
|
|
193
195
|
}),
|
|
194
196
|
[enhancedDispatch, datafeed, onSettingsChange]
|
|
195
197
|
);
|
|
@@ -357,7 +359,14 @@ var MAIN_INDICATORS = [
|
|
|
357
359
|
"SMA",
|
|
358
360
|
"BOLL",
|
|
359
361
|
"SAR",
|
|
360
|
-
"BBI"
|
|
362
|
+
"BBI",
|
|
363
|
+
"BOLL_TV",
|
|
364
|
+
"Ichimoku",
|
|
365
|
+
"SuperTrend",
|
|
366
|
+
"HMA",
|
|
367
|
+
"VWAP",
|
|
368
|
+
"PivotPoints",
|
|
369
|
+
"MA_Ribbon"
|
|
361
370
|
];
|
|
362
371
|
var SUB_INDICATORS = [
|
|
363
372
|
"MA",
|
|
@@ -385,7 +394,11 @@ var SUB_INDICATORS = [
|
|
|
385
394
|
"ROC",
|
|
386
395
|
"PVT",
|
|
387
396
|
"BBI",
|
|
388
|
-
"AO"
|
|
397
|
+
"AO",
|
|
398
|
+
"MACD_TV",
|
|
399
|
+
"RSI_TV",
|
|
400
|
+
"CCI_TV",
|
|
401
|
+
"Stochastic"
|
|
389
402
|
];
|
|
390
403
|
var INDICATOR_PARAMS = {
|
|
391
404
|
SMA: {
|
|
@@ -544,12 +557,95 @@ var INDICATOR_PARAMS = {
|
|
|
544
557
|
{ label: "params_1", defaultValue: 5 },
|
|
545
558
|
{ label: "params_2", defaultValue: 34 }
|
|
546
559
|
]
|
|
560
|
+
},
|
|
561
|
+
BOLL_TV: {
|
|
562
|
+
name: "BOLL_TV",
|
|
563
|
+
localeKey: "boll_tv",
|
|
564
|
+
params: [
|
|
565
|
+
{ label: "period", defaultValue: 20 },
|
|
566
|
+
{ label: "multiplier", defaultValue: 2 }
|
|
567
|
+
]
|
|
568
|
+
},
|
|
569
|
+
Ichimoku: {
|
|
570
|
+
name: "Ichimoku",
|
|
571
|
+
localeKey: "ichimoku",
|
|
572
|
+
params: [
|
|
573
|
+
{ label: "tenkan", defaultValue: 9 },
|
|
574
|
+
{ label: "kijun", defaultValue: 26 },
|
|
575
|
+
{ label: "senkou_b", defaultValue: 52 },
|
|
576
|
+
{ label: "offset", defaultValue: 26 }
|
|
577
|
+
]
|
|
578
|
+
},
|
|
579
|
+
SuperTrend: {
|
|
580
|
+
name: "SuperTrend",
|
|
581
|
+
localeKey: "super_trend",
|
|
582
|
+
params: [
|
|
583
|
+
{ label: "period", defaultValue: 10 },
|
|
584
|
+
{ label: "factor", defaultValue: 3 }
|
|
585
|
+
]
|
|
586
|
+
},
|
|
587
|
+
HMA: {
|
|
588
|
+
name: "HMA",
|
|
589
|
+
localeKey: "hma",
|
|
590
|
+
params: [{ label: "period", defaultValue: 9 }]
|
|
591
|
+
},
|
|
592
|
+
VWAP: {
|
|
593
|
+
name: "VWAP",
|
|
594
|
+
localeKey: "vwap",
|
|
595
|
+
params: []
|
|
596
|
+
},
|
|
597
|
+
PivotPoints: {
|
|
598
|
+
name: "PivotPoints",
|
|
599
|
+
localeKey: "pivot_points",
|
|
600
|
+
params: []
|
|
601
|
+
},
|
|
602
|
+
MA_Ribbon: {
|
|
603
|
+
name: "MA_Ribbon",
|
|
604
|
+
localeKey: "ma_ribbon",
|
|
605
|
+
params: [
|
|
606
|
+
{ label: "period_1", defaultValue: 10 },
|
|
607
|
+
{ label: "period_2", defaultValue: 20 },
|
|
608
|
+
{ label: "period_3", defaultValue: 30 },
|
|
609
|
+
{ label: "period_4", defaultValue: 40 }
|
|
610
|
+
]
|
|
611
|
+
},
|
|
612
|
+
MACD_TV: {
|
|
613
|
+
name: "MACD_TV",
|
|
614
|
+
localeKey: "macd_tv",
|
|
615
|
+
params: [
|
|
616
|
+
{ label: "fast", defaultValue: 12 },
|
|
617
|
+
{ label: "slow", defaultValue: 26 },
|
|
618
|
+
{ label: "signal", defaultValue: 9 }
|
|
619
|
+
]
|
|
620
|
+
},
|
|
621
|
+
RSI_TV: {
|
|
622
|
+
name: "RSI_TV",
|
|
623
|
+
localeKey: "rsi_tv",
|
|
624
|
+
params: [
|
|
625
|
+
{ label: "rsi_period", defaultValue: 14 },
|
|
626
|
+
{ label: "ma_period", defaultValue: 14 }
|
|
627
|
+
]
|
|
628
|
+
},
|
|
629
|
+
CCI_TV: {
|
|
630
|
+
name: "CCI_TV",
|
|
631
|
+
localeKey: "cci_tv",
|
|
632
|
+
params: [{ label: "period", defaultValue: 20 }]
|
|
633
|
+
},
|
|
634
|
+
Stochastic: {
|
|
635
|
+
name: "Stochastic",
|
|
636
|
+
localeKey: "stochastic",
|
|
637
|
+
params: [
|
|
638
|
+
{ label: "period", defaultValue: 14 },
|
|
639
|
+
{ label: "smooth_k", defaultValue: 3 },
|
|
640
|
+
{ label: "smooth_d", defaultValue: 3 }
|
|
641
|
+
]
|
|
547
642
|
}
|
|
548
643
|
};
|
|
549
644
|
|
|
550
645
|
// src/hooks/useIndicators.ts
|
|
551
646
|
function useIndicators() {
|
|
552
647
|
const { state, dispatch } = useKlinechartsUI();
|
|
648
|
+
const { undoRedoListenerRef } = useKlinechartsUIDispatch();
|
|
553
649
|
const mainIndicators = useMemo(() => {
|
|
554
650
|
const activeNames = state.mainIndicators;
|
|
555
651
|
const inactive = MAIN_INDICATORS.filter((n) => !activeNames.includes(n));
|
|
@@ -576,16 +672,24 @@ function useIndicators() {
|
|
|
576
672
|
);
|
|
577
673
|
const newIndicators = [...state.mainIndicators, name];
|
|
578
674
|
dispatch({ type: "SET_MAIN_INDICATORS", indicators: newIndicators });
|
|
675
|
+
undoRedoListenerRef.current?.({
|
|
676
|
+
type: "indicator_toggled",
|
|
677
|
+
data: { name, wasActive: false, isMain: true, paneId: "candle_pane" }
|
|
678
|
+
});
|
|
579
679
|
},
|
|
580
|
-
[state.chart, state.mainIndicators, dispatch]
|
|
680
|
+
[state.chart, state.mainIndicators, dispatch, undoRedoListenerRef]
|
|
581
681
|
);
|
|
582
682
|
const removeMainIndicator = useCallback(
|
|
583
683
|
(name) => {
|
|
584
684
|
state.chart?.removeIndicator({ id: `main_${name}` });
|
|
585
685
|
const newIndicators = state.mainIndicators.filter((n) => n !== name);
|
|
586
686
|
dispatch({ type: "SET_MAIN_INDICATORS", indicators: newIndicators });
|
|
687
|
+
undoRedoListenerRef.current?.({
|
|
688
|
+
type: "indicator_toggled",
|
|
689
|
+
data: { name, wasActive: true, isMain: true, paneId: "candle_pane" }
|
|
690
|
+
});
|
|
587
691
|
},
|
|
588
|
-
[state.chart, state.mainIndicators, dispatch]
|
|
692
|
+
[state.chart, state.mainIndicators, dispatch, undoRedoListenerRef]
|
|
589
693
|
);
|
|
590
694
|
const addSubIndicator = useCallback(
|
|
591
695
|
(name) => {
|
|
@@ -595,17 +699,26 @@ function useIndicators() {
|
|
|
595
699
|
const paneId = state.chart?.getIndicators({ id })?.[0]?.paneId ?? "";
|
|
596
700
|
const newIndicators = { ...state.subIndicators, [name]: paneId };
|
|
597
701
|
dispatch({ type: "SET_SUB_INDICATORS", indicators: newIndicators });
|
|
702
|
+
undoRedoListenerRef.current?.({
|
|
703
|
+
type: "indicator_toggled",
|
|
704
|
+
data: { name, wasActive: false, isMain: false, paneId }
|
|
705
|
+
});
|
|
598
706
|
},
|
|
599
|
-
[state.chart, state.subIndicators, dispatch]
|
|
707
|
+
[state.chart, state.subIndicators, dispatch, undoRedoListenerRef]
|
|
600
708
|
);
|
|
601
709
|
const removeSubIndicator = useCallback(
|
|
602
710
|
(name) => {
|
|
711
|
+
const paneId = state.subIndicators[name] ?? "";
|
|
603
712
|
state.chart?.removeIndicator({ id: `sub_${name}` });
|
|
604
713
|
const newIndicators = { ...state.subIndicators };
|
|
605
714
|
delete newIndicators[name];
|
|
606
715
|
dispatch({ type: "SET_SUB_INDICATORS", indicators: newIndicators });
|
|
716
|
+
undoRedoListenerRef.current?.({
|
|
717
|
+
type: "indicator_toggled",
|
|
718
|
+
data: { name, wasActive: true, isMain: false, paneId }
|
|
719
|
+
});
|
|
607
720
|
},
|
|
608
|
-
[state.chart, state.subIndicators, dispatch]
|
|
721
|
+
[state.chart, state.subIndicators, dispatch, undoRedoListenerRef]
|
|
609
722
|
);
|
|
610
723
|
const toggleMainIndicator = useCallback(
|
|
611
724
|
(name) => {
|
|
@@ -651,9 +764,15 @@ function useIndicators() {
|
|
|
651
764
|
const moveToMain = useCallback(
|
|
652
765
|
(name) => {
|
|
653
766
|
if (!state.chart) return;
|
|
767
|
+
const prevCalcParams = state.chart.getIndicators({ id: `sub_${name}` })?.[0]?.calcParams;
|
|
654
768
|
state.chart.removeIndicator({ id: `sub_${name}` });
|
|
655
769
|
state.chart.createIndicator(
|
|
656
|
-
{
|
|
770
|
+
{
|
|
771
|
+
name,
|
|
772
|
+
id: `main_${name}`,
|
|
773
|
+
paneId: "candle_pane",
|
|
774
|
+
...prevCalcParams ? { calcParams: prevCalcParams } : {}
|
|
775
|
+
},
|
|
657
776
|
true,
|
|
658
777
|
{ id: "candle_pane" }
|
|
659
778
|
);
|
|
@@ -670,9 +789,14 @@ function useIndicators() {
|
|
|
670
789
|
const moveToSub = useCallback(
|
|
671
790
|
(name) => {
|
|
672
791
|
if (!state.chart) return;
|
|
792
|
+
const prevCalcParams = state.chart.getIndicators({ id: `main_${name}` })?.[0]?.calcParams;
|
|
673
793
|
state.chart.removeIndicator({ id: `main_${name}` });
|
|
674
794
|
const subId = `sub_${name}`;
|
|
675
|
-
state.chart.createIndicator({
|
|
795
|
+
state.chart.createIndicator({
|
|
796
|
+
name,
|
|
797
|
+
id: subId,
|
|
798
|
+
...prevCalcParams ? { calcParams: prevCalcParams } : {}
|
|
799
|
+
});
|
|
676
800
|
const paneId = state.chart.getIndicators({ id: subId })?.[0]?.paneId ?? "";
|
|
677
801
|
const newMain = state.mainIndicators.filter((n) => n !== name);
|
|
678
802
|
dispatch({ type: "SET_MAIN_INDICATORS", indicators: newMain });
|
|
@@ -736,7 +860,9 @@ var DRAWING_CATEGORIES = [
|
|
|
736
860
|
key: "moreLine",
|
|
737
861
|
tools: [
|
|
738
862
|
{ name: "priceChannelLine", localeKey: "price_channel_line" },
|
|
739
|
-
{ name: "parallelStraightLine", localeKey: "parallel_straight_line" }
|
|
863
|
+
{ name: "parallelStraightLine", localeKey: "parallel_straight_line" },
|
|
864
|
+
{ name: "ray", localeKey: "ray" },
|
|
865
|
+
{ name: "parallelChannel", localeKey: "parallel_channel" }
|
|
740
866
|
]
|
|
741
867
|
},
|
|
742
868
|
{
|
|
@@ -760,7 +886,9 @@ var DRAWING_CATEGORIES = [
|
|
|
760
886
|
localeKey: "fibonacci_speed_resistance_fan"
|
|
761
887
|
},
|
|
762
888
|
{ name: "fibonacciExtension", localeKey: "fibonacci_extension" },
|
|
763
|
-
{ name: "
|
|
889
|
+
{ name: "fibRetracement", localeKey: "fib_retracement" },
|
|
890
|
+
{ name: "gannBox", localeKey: "gann_box" },
|
|
891
|
+
{ name: "gannFan", localeKey: "gann_fan" }
|
|
764
892
|
]
|
|
765
893
|
},
|
|
766
894
|
{
|
|
@@ -771,7 +899,27 @@ var DRAWING_CATEGORIES = [
|
|
|
771
899
|
{ name: "threeWaves", localeKey: "three_waves" },
|
|
772
900
|
{ name: "fiveWaves", localeKey: "five_waves" },
|
|
773
901
|
{ name: "eightWaves", localeKey: "eight_waves" },
|
|
774
|
-
{ name: "anyWaves", localeKey: "any_waves" }
|
|
902
|
+
{ name: "anyWaves", localeKey: "any_waves" },
|
|
903
|
+
{ name: "elliottWave", localeKey: "elliott_wave" }
|
|
904
|
+
]
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
key: "measure",
|
|
908
|
+
tools: [
|
|
909
|
+
{ name: "measure", localeKey: "measure" }
|
|
910
|
+
]
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
key: "position",
|
|
914
|
+
tools: [
|
|
915
|
+
{ name: "longPosition", localeKey: "long_position" },
|
|
916
|
+
{ name: "shortPosition", localeKey: "short_position" }
|
|
917
|
+
]
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
key: "annotation",
|
|
921
|
+
tools: [
|
|
922
|
+
{ name: "brush", localeKey: "brush" }
|
|
775
923
|
]
|
|
776
924
|
}
|
|
777
925
|
];
|
|
@@ -780,6 +928,7 @@ var DRAWING_CATEGORIES = [
|
|
|
780
928
|
var DRAWING_GROUP_ID = "drawing_tools";
|
|
781
929
|
function useDrawingTools() {
|
|
782
930
|
const { state } = useKlinechartsUI();
|
|
931
|
+
const { undoRedoListenerRef } = useKlinechartsUIDispatch();
|
|
783
932
|
const [activeTool, setActiveTool] = useState(null);
|
|
784
933
|
const [magnetMode, setMagnetModeState] = useState("normal");
|
|
785
934
|
const [isLocked, setIsLocked] = useState(false);
|
|
@@ -802,10 +951,25 @@ function useDrawingTools() {
|
|
|
802
951
|
groupId: DRAWING_GROUP_ID,
|
|
803
952
|
lock: isLocked,
|
|
804
953
|
visible: isVisible,
|
|
805
|
-
mode: magnetMode === "strong" ? "strong_magnet" : magnetMode === "weak" ? "weak_magnet" : "normal"
|
|
954
|
+
mode: magnetMode === "strong" ? "strong_magnet" : magnetMode === "weak" ? "weak_magnet" : "normal",
|
|
955
|
+
onDrawEnd: (event) => {
|
|
956
|
+
const o = event.overlay;
|
|
957
|
+
undoRedoListenerRef.current?.({
|
|
958
|
+
type: "overlay_added",
|
|
959
|
+
data: {
|
|
960
|
+
id: o.id,
|
|
961
|
+
overlayData: {
|
|
962
|
+
name: o.name,
|
|
963
|
+
points: o.points,
|
|
964
|
+
styles: o.styles,
|
|
965
|
+
extendData: o.extendData
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
}
|
|
806
970
|
});
|
|
807
971
|
},
|
|
808
|
-
[state.chart, isLocked, isVisible, magnetMode]
|
|
972
|
+
[state.chart, isLocked, isVisible, magnetMode, undoRedoListenerRef]
|
|
809
973
|
);
|
|
810
974
|
const clearActiveTool = useCallback(() => {
|
|
811
975
|
setActiveTool(null);
|
|
@@ -853,9 +1017,23 @@ function useDrawingTools() {
|
|
|
853
1017
|
}
|
|
854
1018
|
}, [state.chart, isVisible]);
|
|
855
1019
|
const removeAllDrawings = useCallback(() => {
|
|
1020
|
+
const overlays2 = state.chart?.getOverlays({ groupId: DRAWING_GROUP_ID });
|
|
1021
|
+
if (overlays2 && overlays2.length > 0) {
|
|
1022
|
+
const snapshot = overlays2.map((o) => ({
|
|
1023
|
+
name: o.name,
|
|
1024
|
+
id: o.id,
|
|
1025
|
+
points: o.points,
|
|
1026
|
+
styles: o.styles,
|
|
1027
|
+
extendData: o.extendData
|
|
1028
|
+
}));
|
|
1029
|
+
undoRedoListenerRef.current?.({
|
|
1030
|
+
type: "overlays_removed",
|
|
1031
|
+
data: { overlays: snapshot }
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
856
1034
|
state.chart?.removeOverlay({ groupId: DRAWING_GROUP_ID });
|
|
857
1035
|
setActiveTool(null);
|
|
858
|
-
}, [state.chart]);
|
|
1036
|
+
}, [state.chart, undoRedoListenerRef]);
|
|
859
1037
|
return {
|
|
860
1038
|
categories,
|
|
861
1039
|
activeTool,
|
|
@@ -1259,12 +1437,12 @@ function useOrderLines() {
|
|
|
1259
1437
|
if (onPriceChange) {
|
|
1260
1438
|
callbacksRef.current.set(id, onPriceChange);
|
|
1261
1439
|
}
|
|
1262
|
-
const
|
|
1263
|
-
const
|
|
1440
|
+
const dataList = state.chart.getDataList();
|
|
1441
|
+
const anchorTimestamp = dataList.length > 0 ? dataList[dataList.length - 1].timestamp : Date.now();
|
|
1264
1442
|
state.chart.createOverlay({
|
|
1265
1443
|
name: "orderLine",
|
|
1266
1444
|
id,
|
|
1267
|
-
points: [{
|
|
1445
|
+
points: [{ timestamp: anchorTimestamp, value: price }],
|
|
1268
1446
|
extendData,
|
|
1269
1447
|
lock: !draggable,
|
|
1270
1448
|
mode: "normal",
|
|
@@ -1288,11 +1466,11 @@ function useOrderLines() {
|
|
|
1288
1466
|
if (onPriceChange !== void 0) {
|
|
1289
1467
|
callbacksRef.current.set(id, onPriceChange);
|
|
1290
1468
|
}
|
|
1291
|
-
const
|
|
1292
|
-
const
|
|
1469
|
+
const dataList = state.chart.getDataList();
|
|
1470
|
+
const anchorTimestamp = dataList.length > 0 ? dataList[dataList.length - 1].timestamp : Date.now();
|
|
1293
1471
|
state.chart.overrideOverlay({
|
|
1294
1472
|
id,
|
|
1295
|
-
...price != null ? { points: [{
|
|
1473
|
+
...price != null ? { points: [{ timestamp: anchorTimestamp, value: price }] } : {},
|
|
1296
1474
|
...draggable != null ? { lock: !draggable } : {},
|
|
1297
1475
|
extendData: {
|
|
1298
1476
|
...existing.extendData,
|
|
@@ -1320,6 +1498,752 @@ function useOrderLines() {
|
|
|
1320
1498
|
removeAllOrderLines
|
|
1321
1499
|
};
|
|
1322
1500
|
}
|
|
1501
|
+
var DRAWING_GROUP_ID2 = "drawing_tools";
|
|
1502
|
+
function useUndoRedo() {
|
|
1503
|
+
const { state, dispatch } = useKlinechartsUI();
|
|
1504
|
+
const { undoRedoListenerRef } = useKlinechartsUIDispatch();
|
|
1505
|
+
const [undoStack, setUndoStack] = useState([]);
|
|
1506
|
+
const [redoStack, setRedoStack] = useState([]);
|
|
1507
|
+
const isProcessingRef = useRef(false);
|
|
1508
|
+
const canUndo = undoStack.length > 0;
|
|
1509
|
+
const canRedo = redoStack.length > 0;
|
|
1510
|
+
const pushAction = useCallback((action) => {
|
|
1511
|
+
if (isProcessingRef.current) return;
|
|
1512
|
+
setUndoStack((prev) => [...prev, action]);
|
|
1513
|
+
setRedoStack([]);
|
|
1514
|
+
}, []);
|
|
1515
|
+
useEffect(() => {
|
|
1516
|
+
undoRedoListenerRef.current = pushAction;
|
|
1517
|
+
return () => {
|
|
1518
|
+
undoRedoListenerRef.current = null;
|
|
1519
|
+
};
|
|
1520
|
+
}, [pushAction, undoRedoListenerRef]);
|
|
1521
|
+
const clear = useCallback(() => {
|
|
1522
|
+
setUndoStack([]);
|
|
1523
|
+
setRedoStack([]);
|
|
1524
|
+
}, []);
|
|
1525
|
+
const undo = useCallback(() => {
|
|
1526
|
+
if (undoStack.length === 0 || !state.chart || isProcessingRef.current)
|
|
1527
|
+
return;
|
|
1528
|
+
isProcessingRef.current = true;
|
|
1529
|
+
const action = undoStack[undoStack.length - 1];
|
|
1530
|
+
setUndoStack((prev) => prev.slice(0, -1));
|
|
1531
|
+
try {
|
|
1532
|
+
switch (action.type) {
|
|
1533
|
+
case "overlay_added": {
|
|
1534
|
+
const { id, overlayData } = action.data;
|
|
1535
|
+
state.chart.removeOverlay({ id });
|
|
1536
|
+
setRedoStack((prev) => [
|
|
1537
|
+
...prev,
|
|
1538
|
+
{
|
|
1539
|
+
type: "overlay_added",
|
|
1540
|
+
data: { id, overlayData }
|
|
1541
|
+
}
|
|
1542
|
+
]);
|
|
1543
|
+
break;
|
|
1544
|
+
}
|
|
1545
|
+
case "overlays_removed": {
|
|
1546
|
+
const { overlays: overlays2 } = action.data;
|
|
1547
|
+
const restoredIds = [];
|
|
1548
|
+
for (const overlay of overlays2) {
|
|
1549
|
+
const newId = state.chart.createOverlay({
|
|
1550
|
+
...overlay,
|
|
1551
|
+
groupId: DRAWING_GROUP_ID2
|
|
1552
|
+
});
|
|
1553
|
+
if (typeof newId === "string") {
|
|
1554
|
+
restoredIds.push(newId);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
setRedoStack((prev) => [
|
|
1558
|
+
...prev,
|
|
1559
|
+
{
|
|
1560
|
+
type: "overlays_removed",
|
|
1561
|
+
data: {
|
|
1562
|
+
overlays: overlays2.map((o, i) => ({
|
|
1563
|
+
...o,
|
|
1564
|
+
id: restoredIds[i]
|
|
1565
|
+
}))
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
]);
|
|
1569
|
+
break;
|
|
1570
|
+
}
|
|
1571
|
+
case "indicator_toggled": {
|
|
1572
|
+
const { name, wasActive, isMain, paneId } = action.data;
|
|
1573
|
+
if (wasActive) {
|
|
1574
|
+
if (isMain) {
|
|
1575
|
+
state.chart.createIndicator(
|
|
1576
|
+
{ name, id: `main_${name}`, paneId: "candle_pane" },
|
|
1577
|
+
true,
|
|
1578
|
+
{ id: "candle_pane" }
|
|
1579
|
+
);
|
|
1580
|
+
dispatch({
|
|
1581
|
+
type: "SET_MAIN_INDICATORS",
|
|
1582
|
+
indicators: [...state.mainIndicators, name]
|
|
1583
|
+
});
|
|
1584
|
+
} else {
|
|
1585
|
+
state.chart.createIndicator({ name, id: `sub_${name}` });
|
|
1586
|
+
const newPaneId = state.chart.getIndicators({ id: `sub_${name}` })?.[0]?.paneId ?? "";
|
|
1587
|
+
dispatch({
|
|
1588
|
+
type: "SET_SUB_INDICATORS",
|
|
1589
|
+
indicators: {
|
|
1590
|
+
...state.subIndicators,
|
|
1591
|
+
[name]: newPaneId
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
} else {
|
|
1596
|
+
if (isMain) {
|
|
1597
|
+
state.chart.removeIndicator({ id: `main_${name}` });
|
|
1598
|
+
dispatch({
|
|
1599
|
+
type: "SET_MAIN_INDICATORS",
|
|
1600
|
+
indicators: state.mainIndicators.filter(
|
|
1601
|
+
(n) => n !== name
|
|
1602
|
+
)
|
|
1603
|
+
});
|
|
1604
|
+
} else {
|
|
1605
|
+
state.chart.removeIndicator({ id: `sub_${name}` });
|
|
1606
|
+
const newSub = { ...state.subIndicators };
|
|
1607
|
+
delete newSub[name];
|
|
1608
|
+
dispatch({
|
|
1609
|
+
type: "SET_SUB_INDICATORS",
|
|
1610
|
+
indicators: newSub
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
setRedoStack((prev) => [
|
|
1615
|
+
...prev,
|
|
1616
|
+
{
|
|
1617
|
+
type: "indicator_toggled",
|
|
1618
|
+
data: {
|
|
1619
|
+
name,
|
|
1620
|
+
wasActive: !wasActive,
|
|
1621
|
+
isMain,
|
|
1622
|
+
paneId
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
]);
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
} finally {
|
|
1630
|
+
isProcessingRef.current = false;
|
|
1631
|
+
}
|
|
1632
|
+
}, [undoStack, state.chart, state.mainIndicators, state.subIndicators, dispatch]);
|
|
1633
|
+
const redo = useCallback(() => {
|
|
1634
|
+
if (redoStack.length === 0 || !state.chart || isProcessingRef.current)
|
|
1635
|
+
return;
|
|
1636
|
+
isProcessingRef.current = true;
|
|
1637
|
+
const action = redoStack[redoStack.length - 1];
|
|
1638
|
+
setRedoStack((prev) => prev.slice(0, -1));
|
|
1639
|
+
try {
|
|
1640
|
+
switch (action.type) {
|
|
1641
|
+
case "overlay_added": {
|
|
1642
|
+
const { overlayData } = action.data;
|
|
1643
|
+
const newId = state.chart.createOverlay({
|
|
1644
|
+
...overlayData,
|
|
1645
|
+
groupId: DRAWING_GROUP_ID2
|
|
1646
|
+
});
|
|
1647
|
+
setUndoStack((prev) => [
|
|
1648
|
+
...prev,
|
|
1649
|
+
{
|
|
1650
|
+
type: "overlay_added",
|
|
1651
|
+
data: {
|
|
1652
|
+
id: typeof newId === "string" ? newId : action.data.id,
|
|
1653
|
+
overlayData
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
]);
|
|
1657
|
+
break;
|
|
1658
|
+
}
|
|
1659
|
+
case "overlays_removed": {
|
|
1660
|
+
const { overlays: overlays2 } = action.data;
|
|
1661
|
+
const snapshotOverlays = overlays2.map((o) => {
|
|
1662
|
+
const allOverlays = state.chart?.getOverlays({ id: o.id });
|
|
1663
|
+
const actual = allOverlays?.[0];
|
|
1664
|
+
return actual ? {
|
|
1665
|
+
name: actual.name,
|
|
1666
|
+
points: actual.points,
|
|
1667
|
+
styles: actual.styles,
|
|
1668
|
+
extendData: actual.extendData
|
|
1669
|
+
} : o;
|
|
1670
|
+
});
|
|
1671
|
+
for (const overlay of overlays2) {
|
|
1672
|
+
state.chart.removeOverlay({ id: overlay.id });
|
|
1673
|
+
}
|
|
1674
|
+
setUndoStack((prev) => [
|
|
1675
|
+
...prev,
|
|
1676
|
+
{
|
|
1677
|
+
type: "overlays_removed",
|
|
1678
|
+
data: { overlays: snapshotOverlays }
|
|
1679
|
+
}
|
|
1680
|
+
]);
|
|
1681
|
+
break;
|
|
1682
|
+
}
|
|
1683
|
+
case "indicator_toggled": {
|
|
1684
|
+
const { name, wasActive, isMain } = action.data;
|
|
1685
|
+
if (wasActive) {
|
|
1686
|
+
if (isMain) {
|
|
1687
|
+
state.chart.createIndicator(
|
|
1688
|
+
{ name, id: `main_${name}`, paneId: "candle_pane" },
|
|
1689
|
+
true,
|
|
1690
|
+
{ id: "candle_pane" }
|
|
1691
|
+
);
|
|
1692
|
+
dispatch({
|
|
1693
|
+
type: "SET_MAIN_INDICATORS",
|
|
1694
|
+
indicators: [...state.mainIndicators, name]
|
|
1695
|
+
});
|
|
1696
|
+
} else {
|
|
1697
|
+
state.chart.createIndicator({ name, id: `sub_${name}` });
|
|
1698
|
+
const newPaneId = state.chart.getIndicators({ id: `sub_${name}` })?.[0]?.paneId ?? "";
|
|
1699
|
+
dispatch({
|
|
1700
|
+
type: "SET_SUB_INDICATORS",
|
|
1701
|
+
indicators: {
|
|
1702
|
+
...state.subIndicators,
|
|
1703
|
+
[name]: newPaneId
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
} else {
|
|
1708
|
+
if (isMain) {
|
|
1709
|
+
state.chart.removeIndicator({ id: `main_${name}` });
|
|
1710
|
+
dispatch({
|
|
1711
|
+
type: "SET_MAIN_INDICATORS",
|
|
1712
|
+
indicators: state.mainIndicators.filter(
|
|
1713
|
+
(n) => n !== name
|
|
1714
|
+
)
|
|
1715
|
+
});
|
|
1716
|
+
} else {
|
|
1717
|
+
state.chart.removeIndicator({ id: `sub_${name}` });
|
|
1718
|
+
const newSub = { ...state.subIndicators };
|
|
1719
|
+
delete newSub[name];
|
|
1720
|
+
dispatch({
|
|
1721
|
+
type: "SET_SUB_INDICATORS",
|
|
1722
|
+
indicators: newSub
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
setUndoStack((prev) => [
|
|
1727
|
+
...prev,
|
|
1728
|
+
{
|
|
1729
|
+
type: "indicator_toggled",
|
|
1730
|
+
data: {
|
|
1731
|
+
name,
|
|
1732
|
+
wasActive: !wasActive,
|
|
1733
|
+
isMain
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
]);
|
|
1737
|
+
break;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
} finally {
|
|
1741
|
+
isProcessingRef.current = false;
|
|
1742
|
+
}
|
|
1743
|
+
}, [redoStack, state.chart, state.mainIndicators, state.subIndicators, dispatch]);
|
|
1744
|
+
useEffect(() => {
|
|
1745
|
+
const handleKeyDown = (e) => {
|
|
1746
|
+
const isCtrlOrMeta = e.ctrlKey || e.metaKey;
|
|
1747
|
+
if (!isCtrlOrMeta) return;
|
|
1748
|
+
if (e.key === "z" && !e.shiftKey) {
|
|
1749
|
+
e.preventDefault();
|
|
1750
|
+
undo();
|
|
1751
|
+
} else if (e.key === "y" || e.key === "z" && e.shiftKey) {
|
|
1752
|
+
e.preventDefault();
|
|
1753
|
+
redo();
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
1757
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
1758
|
+
}, [undo, redo]);
|
|
1759
|
+
return {
|
|
1760
|
+
canUndo,
|
|
1761
|
+
canRedo,
|
|
1762
|
+
undo,
|
|
1763
|
+
redo,
|
|
1764
|
+
pushAction,
|
|
1765
|
+
clear
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
var STORAGE_KEY_PREFIX = "klinecharts_layout:";
|
|
1769
|
+
var INDEX_KEY = "klinecharts_layout_index";
|
|
1770
|
+
var STATE_VERSION = "1.0";
|
|
1771
|
+
function generateId() {
|
|
1772
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
|
|
1773
|
+
/[xy]/g,
|
|
1774
|
+
(c) => {
|
|
1775
|
+
const r = Math.random() * 16 | 0;
|
|
1776
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
1777
|
+
}
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
function getLayoutIds() {
|
|
1781
|
+
try {
|
|
1782
|
+
const raw = localStorage.getItem(INDEX_KEY);
|
|
1783
|
+
return raw ? JSON.parse(raw) : [];
|
|
1784
|
+
} catch {
|
|
1785
|
+
return [];
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
function getEntry(id) {
|
|
1789
|
+
try {
|
|
1790
|
+
const raw = localStorage.getItem(STORAGE_KEY_PREFIX + id);
|
|
1791
|
+
return raw ? JSON.parse(raw) : null;
|
|
1792
|
+
} catch {
|
|
1793
|
+
return null;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function useLayoutManager() {
|
|
1797
|
+
const { state, dispatch } = useKlinechartsUI();
|
|
1798
|
+
const [layouts, setLayouts] = useState([]);
|
|
1799
|
+
const [autoSaveEnabled, setAutoSaveEnabled] = useState(false);
|
|
1800
|
+
const autoSaveTimerRef = useRef(null);
|
|
1801
|
+
const autoSaveIdRef = useRef(null);
|
|
1802
|
+
const refreshLayouts = useCallback(() => {
|
|
1803
|
+
const entries = getLayoutIds().map((id) => getEntry(id)).filter((e) => e !== null);
|
|
1804
|
+
setLayouts(entries);
|
|
1805
|
+
}, []);
|
|
1806
|
+
useEffect(() => {
|
|
1807
|
+
refreshLayouts();
|
|
1808
|
+
}, [refreshLayouts]);
|
|
1809
|
+
const serializeState = useCallback(() => {
|
|
1810
|
+
const chart = state.chart;
|
|
1811
|
+
if (!chart) return null;
|
|
1812
|
+
const indicators2 = [];
|
|
1813
|
+
const indicatorMap = chart.getIndicatorByPaneId?.();
|
|
1814
|
+
if (indicatorMap) {
|
|
1815
|
+
indicatorMap.forEach((paneIndicators, paneId) => {
|
|
1816
|
+
paneIndicators.forEach((indicator) => {
|
|
1817
|
+
indicators2.push({
|
|
1818
|
+
paneId,
|
|
1819
|
+
name: indicator.name,
|
|
1820
|
+
calcParams: indicator.calcParams,
|
|
1821
|
+
visible: indicator.visible,
|
|
1822
|
+
...indicator.styles ? { styles: indicator.styles } : {}
|
|
1823
|
+
});
|
|
1824
|
+
});
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
const drawings = [];
|
|
1828
|
+
const allOverlays = chart.getOverlays({});
|
|
1829
|
+
if (allOverlays) {
|
|
1830
|
+
for (const overlay of allOverlays) {
|
|
1831
|
+
drawings.push({
|
|
1832
|
+
name: overlay.name,
|
|
1833
|
+
points: overlay.points,
|
|
1834
|
+
styles: overlay.styles,
|
|
1835
|
+
extendData: overlay.extendData
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
return {
|
|
1840
|
+
version: STATE_VERSION,
|
|
1841
|
+
meta: {
|
|
1842
|
+
symbol: state.symbol?.ticker ?? "",
|
|
1843
|
+
period: state.period?.label ?? "",
|
|
1844
|
+
timestamp: Date.now(),
|
|
1845
|
+
lastModified: Date.now()
|
|
1846
|
+
},
|
|
1847
|
+
indicators: indicators2,
|
|
1848
|
+
drawings
|
|
1849
|
+
};
|
|
1850
|
+
}, [state.chart, state.symbol, state.period]);
|
|
1851
|
+
const saveLayout = useCallback(
|
|
1852
|
+
(name) => {
|
|
1853
|
+
const chartState = serializeState();
|
|
1854
|
+
if (!chartState) return null;
|
|
1855
|
+
const id = generateId();
|
|
1856
|
+
const now = Date.now();
|
|
1857
|
+
const entry = {
|
|
1858
|
+
id,
|
|
1859
|
+
name: name.trim() || `Layout ${new Date(now).toLocaleString()}`,
|
|
1860
|
+
symbol: chartState.meta.symbol,
|
|
1861
|
+
period: chartState.meta.period,
|
|
1862
|
+
timestamp: now,
|
|
1863
|
+
lastModified: now,
|
|
1864
|
+
state: chartState
|
|
1865
|
+
};
|
|
1866
|
+
localStorage.setItem(
|
|
1867
|
+
STORAGE_KEY_PREFIX + id,
|
|
1868
|
+
JSON.stringify(entry)
|
|
1869
|
+
);
|
|
1870
|
+
const ids = getLayoutIds();
|
|
1871
|
+
if (!ids.includes(id)) {
|
|
1872
|
+
ids.push(id);
|
|
1873
|
+
localStorage.setItem(INDEX_KEY, JSON.stringify(ids));
|
|
1874
|
+
}
|
|
1875
|
+
refreshLayouts();
|
|
1876
|
+
return id;
|
|
1877
|
+
},
|
|
1878
|
+
[serializeState, refreshLayouts]
|
|
1879
|
+
);
|
|
1880
|
+
const loadLayout = useCallback(
|
|
1881
|
+
(id) => {
|
|
1882
|
+
const entry = getEntry(id);
|
|
1883
|
+
if (!entry || !state.chart) return false;
|
|
1884
|
+
const chartState = entry.state;
|
|
1885
|
+
if (chartState.version !== STATE_VERSION) return false;
|
|
1886
|
+
const chart = state.chart;
|
|
1887
|
+
chart.removeOverlay();
|
|
1888
|
+
const indicatorMap = chart.getIndicatorByPaneId?.();
|
|
1889
|
+
if (indicatorMap) {
|
|
1890
|
+
indicatorMap.forEach((_paneIndicators, paneId) => {
|
|
1891
|
+
_paneIndicators.forEach((_, indicatorName) => {
|
|
1892
|
+
chart.removeIndicator({ paneId, name: indicatorName });
|
|
1893
|
+
});
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
const newMainIndicators = [];
|
|
1897
|
+
const newSubIndicators = {};
|
|
1898
|
+
if (chartState.indicators) {
|
|
1899
|
+
for (const ind of chartState.indicators) {
|
|
1900
|
+
chart.createIndicator(
|
|
1901
|
+
{
|
|
1902
|
+
name: ind.name,
|
|
1903
|
+
calcParams: ind.calcParams,
|
|
1904
|
+
visible: ind.visible
|
|
1905
|
+
},
|
|
1906
|
+
ind.paneId !== "candle_pane",
|
|
1907
|
+
{ id: ind.paneId }
|
|
1908
|
+
);
|
|
1909
|
+
if (ind.styles) {
|
|
1910
|
+
chart.overrideIndicator({
|
|
1911
|
+
name: ind.name,
|
|
1912
|
+
paneId: ind.paneId,
|
|
1913
|
+
styles: ind.styles
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
if (ind.paneId === "candle_pane") {
|
|
1917
|
+
newMainIndicators.push(ind.name);
|
|
1918
|
+
} else {
|
|
1919
|
+
newSubIndicators[ind.name] = ind.paneId;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
dispatch({
|
|
1924
|
+
type: "SET_MAIN_INDICATORS",
|
|
1925
|
+
indicators: newMainIndicators
|
|
1926
|
+
});
|
|
1927
|
+
dispatch({
|
|
1928
|
+
type: "SET_SUB_INDICATORS",
|
|
1929
|
+
indicators: newSubIndicators
|
|
1930
|
+
});
|
|
1931
|
+
if (chartState.drawings) {
|
|
1932
|
+
for (const drawing of chartState.drawings) {
|
|
1933
|
+
chart.createOverlay({
|
|
1934
|
+
name: drawing.name,
|
|
1935
|
+
points: drawing.points,
|
|
1936
|
+
styles: drawing.styles,
|
|
1937
|
+
extendData: drawing.extendData
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
return true;
|
|
1942
|
+
},
|
|
1943
|
+
[state.chart, dispatch]
|
|
1944
|
+
);
|
|
1945
|
+
const deleteLayout = useCallback(
|
|
1946
|
+
(id) => {
|
|
1947
|
+
localStorage.removeItem(STORAGE_KEY_PREFIX + id);
|
|
1948
|
+
const ids = getLayoutIds().filter((i) => i !== id);
|
|
1949
|
+
localStorage.setItem(INDEX_KEY, JSON.stringify(ids));
|
|
1950
|
+
refreshLayouts();
|
|
1951
|
+
},
|
|
1952
|
+
[refreshLayouts]
|
|
1953
|
+
);
|
|
1954
|
+
const renameLayout = useCallback(
|
|
1955
|
+
(id, name) => {
|
|
1956
|
+
const entry = getEntry(id);
|
|
1957
|
+
if (!entry) return false;
|
|
1958
|
+
const updated = {
|
|
1959
|
+
...entry,
|
|
1960
|
+
name: name.trim(),
|
|
1961
|
+
lastModified: Date.now()
|
|
1962
|
+
};
|
|
1963
|
+
localStorage.setItem(
|
|
1964
|
+
STORAGE_KEY_PREFIX + id,
|
|
1965
|
+
JSON.stringify(updated)
|
|
1966
|
+
);
|
|
1967
|
+
refreshLayouts();
|
|
1968
|
+
return true;
|
|
1969
|
+
},
|
|
1970
|
+
[refreshLayouts]
|
|
1971
|
+
);
|
|
1972
|
+
useEffect(() => {
|
|
1973
|
+
if (!autoSaveEnabled || !state.chart) return;
|
|
1974
|
+
if (autoSaveTimerRef.current) {
|
|
1975
|
+
clearTimeout(autoSaveTimerRef.current);
|
|
1976
|
+
}
|
|
1977
|
+
autoSaveTimerRef.current = setTimeout(() => {
|
|
1978
|
+
const chartState = serializeState();
|
|
1979
|
+
if (!chartState) return;
|
|
1980
|
+
if (autoSaveIdRef.current) {
|
|
1981
|
+
const entry = getEntry(autoSaveIdRef.current);
|
|
1982
|
+
if (entry) {
|
|
1983
|
+
const updated = {
|
|
1984
|
+
...entry,
|
|
1985
|
+
lastModified: Date.now(),
|
|
1986
|
+
state: chartState
|
|
1987
|
+
};
|
|
1988
|
+
localStorage.setItem(
|
|
1989
|
+
STORAGE_KEY_PREFIX + autoSaveIdRef.current,
|
|
1990
|
+
JSON.stringify(updated)
|
|
1991
|
+
);
|
|
1992
|
+
refreshLayouts();
|
|
1993
|
+
}
|
|
1994
|
+
} else {
|
|
1995
|
+
const id = generateId();
|
|
1996
|
+
const now = Date.now();
|
|
1997
|
+
const entry = {
|
|
1998
|
+
id,
|
|
1999
|
+
name: "Auto-save",
|
|
2000
|
+
symbol: chartState.meta.symbol,
|
|
2001
|
+
period: chartState.meta.period,
|
|
2002
|
+
timestamp: now,
|
|
2003
|
+
lastModified: now,
|
|
2004
|
+
state: chartState
|
|
2005
|
+
};
|
|
2006
|
+
localStorage.setItem(
|
|
2007
|
+
STORAGE_KEY_PREFIX + id,
|
|
2008
|
+
JSON.stringify(entry)
|
|
2009
|
+
);
|
|
2010
|
+
const ids = getLayoutIds();
|
|
2011
|
+
ids.push(id);
|
|
2012
|
+
localStorage.setItem(INDEX_KEY, JSON.stringify(ids));
|
|
2013
|
+
autoSaveIdRef.current = id;
|
|
2014
|
+
refreshLayouts();
|
|
2015
|
+
}
|
|
2016
|
+
}, 5e3);
|
|
2017
|
+
return () => {
|
|
2018
|
+
if (autoSaveTimerRef.current) {
|
|
2019
|
+
clearTimeout(autoSaveTimerRef.current);
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
}, [
|
|
2023
|
+
autoSaveEnabled,
|
|
2024
|
+
state.chart,
|
|
2025
|
+
state.mainIndicators,
|
|
2026
|
+
state.subIndicators,
|
|
2027
|
+
serializeState,
|
|
2028
|
+
refreshLayouts
|
|
2029
|
+
]);
|
|
2030
|
+
return {
|
|
2031
|
+
layouts,
|
|
2032
|
+
saveLayout,
|
|
2033
|
+
loadLayout,
|
|
2034
|
+
deleteLayout,
|
|
2035
|
+
renameLayout,
|
|
2036
|
+
refreshLayouts,
|
|
2037
|
+
autoSaveEnabled,
|
|
2038
|
+
setAutoSaveEnabled
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
var DEFAULT_SCRIPT = `// Custom Indicator \u2014 Pine Script Style
|
|
2042
|
+
// Available: TA, dataList, params
|
|
2043
|
+
|
|
2044
|
+
const period = params[0] ?? 14
|
|
2045
|
+
const closes = dataList.map(d => d.close)
|
|
2046
|
+
const highs = dataList.map(d => d.high)
|
|
2047
|
+
const lows = dataList.map(d => d.low)
|
|
2048
|
+
|
|
2049
|
+
// Example: RSI + Bollinger Bands
|
|
2050
|
+
const rsi = TA.rsi(closes, period)
|
|
2051
|
+
const boll = TA.bollinger(closes, period, 2)
|
|
2052
|
+
|
|
2053
|
+
// Return one object per candle \u2014 each key = one line on chart
|
|
2054
|
+
return rsi.map((v, i) => ({
|
|
2055
|
+
rsi: v,
|
|
2056
|
+
upper: boll.upper[i],
|
|
2057
|
+
mid: boll.mid[i],
|
|
2058
|
+
lower: boll.lower[i],
|
|
2059
|
+
}))`;
|
|
2060
|
+
var SERIES_COLORS = [
|
|
2061
|
+
"#2962FF",
|
|
2062
|
+
"#FF6D00",
|
|
2063
|
+
"#00BCD4",
|
|
2064
|
+
"#E91E63",
|
|
2065
|
+
"#76FF03",
|
|
2066
|
+
"#FFD600"
|
|
2067
|
+
];
|
|
2068
|
+
var SHADOW_KEYS = [
|
|
2069
|
+
"fetch",
|
|
2070
|
+
"XMLHttpRequest",
|
|
2071
|
+
"WebSocket",
|
|
2072
|
+
"Worker",
|
|
2073
|
+
"SharedWorker",
|
|
2074
|
+
"importScripts",
|
|
2075
|
+
"self",
|
|
2076
|
+
"caches",
|
|
2077
|
+
"indexedDB"
|
|
2078
|
+
];
|
|
2079
|
+
var scriptCounter = 0;
|
|
2080
|
+
function useScriptEditor() {
|
|
2081
|
+
const { state } = useKlinechartsUI();
|
|
2082
|
+
const [code, setCode] = useState(DEFAULT_SCRIPT);
|
|
2083
|
+
const [scriptName, setScriptName] = useState("");
|
|
2084
|
+
const [params, setParams] = useState("14");
|
|
2085
|
+
const [placement, setPlacement] = useState("sub");
|
|
2086
|
+
const [error, setError] = useState("");
|
|
2087
|
+
const [status, setStatus] = useState("");
|
|
2088
|
+
const [isRunning, setIsRunning] = useState(false);
|
|
2089
|
+
const activeIdRef = useRef(null);
|
|
2090
|
+
const activeNameRef = useRef(null);
|
|
2091
|
+
const hasActiveScript = activeNameRef.current !== null;
|
|
2092
|
+
const runScript = useCallback(() => {
|
|
2093
|
+
const chart = state.chart;
|
|
2094
|
+
if (!chart) {
|
|
2095
|
+
setError("Chart not initialized");
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
setError("");
|
|
2099
|
+
setStatus("Running...");
|
|
2100
|
+
setIsRunning(true);
|
|
2101
|
+
try {
|
|
2102
|
+
const parsedParams = params.split(",").map((s) => parseFloat(s.trim())).filter((n) => !isNaN(n));
|
|
2103
|
+
const dataList = chart.getDataList?.() ?? [];
|
|
2104
|
+
const allArgs = [
|
|
2105
|
+
...SHADOW_KEYS,
|
|
2106
|
+
"TA",
|
|
2107
|
+
"dataList",
|
|
2108
|
+
"params",
|
|
2109
|
+
`"use strict";
|
|
2110
|
+
${code}`
|
|
2111
|
+
];
|
|
2112
|
+
const fn = new Function(...allArgs);
|
|
2113
|
+
const shadowValues = SHADOW_KEYS.map(() => void 0);
|
|
2114
|
+
const result = fn(...shadowValues, TA_default, dataList, parsedParams);
|
|
2115
|
+
if (!Array.isArray(result)) {
|
|
2116
|
+
throw new Error("Script must return an Array.");
|
|
2117
|
+
}
|
|
2118
|
+
const sample = result.find((v) => v !== null && v !== void 0);
|
|
2119
|
+
const seriesKeys = sample ? Object.keys(sample) : ["value"];
|
|
2120
|
+
if (seriesKeys.length === 0) {
|
|
2121
|
+
throw new Error("Returned objects have no keys.");
|
|
2122
|
+
}
|
|
2123
|
+
scriptCounter++;
|
|
2124
|
+
const indicatorName = `_custom_script_${scriptCounter}`;
|
|
2125
|
+
const figures = seriesKeys.map((key, i) => ({
|
|
2126
|
+
key,
|
|
2127
|
+
title: `${key}: `,
|
|
2128
|
+
type: "line",
|
|
2129
|
+
styles: () => ({ color: SERIES_COLORS[i % SERIES_COLORS.length] })
|
|
2130
|
+
}));
|
|
2131
|
+
registerIndicator({
|
|
2132
|
+
name: indicatorName,
|
|
2133
|
+
shortName: (scriptName.trim() || `Script #${scriptCounter}`) + (placement === "main" ? " (Main)" : " (Sub)"),
|
|
2134
|
+
calcParams: parsedParams,
|
|
2135
|
+
figures,
|
|
2136
|
+
calc: () => result,
|
|
2137
|
+
extendData: {
|
|
2138
|
+
isCustomScript: true,
|
|
2139
|
+
code,
|
|
2140
|
+
placement
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
if (activeNameRef.current) {
|
|
2144
|
+
try {
|
|
2145
|
+
chart.removeIndicator({
|
|
2146
|
+
id: activeIdRef.current === "candle_pane" ? "candle_pane" : activeIdRef.current,
|
|
2147
|
+
name: activeNameRef.current
|
|
2148
|
+
});
|
|
2149
|
+
} catch {
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
let paneId = null;
|
|
2153
|
+
if (placement === "main") {
|
|
2154
|
+
paneId = chart.createIndicator(
|
|
2155
|
+
{ name: indicatorName },
|
|
2156
|
+
true,
|
|
2157
|
+
{ id: "candle_pane" }
|
|
2158
|
+
);
|
|
2159
|
+
} else {
|
|
2160
|
+
paneId = chart.createIndicator({ name: indicatorName });
|
|
2161
|
+
}
|
|
2162
|
+
activeIdRef.current = typeof paneId === "string" ? paneId : null;
|
|
2163
|
+
activeNameRef.current = indicatorName;
|
|
2164
|
+
const title = scriptName.trim() || `Script #${scriptCounter}`;
|
|
2165
|
+
setStatus(
|
|
2166
|
+
`${title} applied to ${placement === "main" ? "Main Chart" : "Sub Pane"} \u2014 ${seriesKeys.length} series: ${seriesKeys.join(", ")}`
|
|
2167
|
+
);
|
|
2168
|
+
} catch (e) {
|
|
2169
|
+
setError(e?.message ?? String(e));
|
|
2170
|
+
} finally {
|
|
2171
|
+
setIsRunning(false);
|
|
2172
|
+
}
|
|
2173
|
+
}, [state.chart, code, scriptName, params, placement]);
|
|
2174
|
+
const removeScript = useCallback(() => {
|
|
2175
|
+
const chart = state.chart;
|
|
2176
|
+
if (chart && activeNameRef.current && activeIdRef.current) {
|
|
2177
|
+
try {
|
|
2178
|
+
chart.removeIndicator({
|
|
2179
|
+
id: activeIdRef.current,
|
|
2180
|
+
name: activeNameRef.current
|
|
2181
|
+
});
|
|
2182
|
+
} catch {
|
|
2183
|
+
}
|
|
2184
|
+
activeIdRef.current = null;
|
|
2185
|
+
activeNameRef.current = null;
|
|
2186
|
+
setStatus("Indicator removed.");
|
|
2187
|
+
}
|
|
2188
|
+
}, [state.chart]);
|
|
2189
|
+
const resetCode = useCallback(() => {
|
|
2190
|
+
setCode(DEFAULT_SCRIPT);
|
|
2191
|
+
setError("");
|
|
2192
|
+
setStatus("");
|
|
2193
|
+
}, []);
|
|
2194
|
+
const exportScript = useCallback(() => {
|
|
2195
|
+
const blob = new Blob([code], {
|
|
2196
|
+
type: "text/javascript;charset=utf-8"
|
|
2197
|
+
});
|
|
2198
|
+
const url = URL.createObjectURL(blob);
|
|
2199
|
+
const a = document.createElement("a");
|
|
2200
|
+
a.href = url;
|
|
2201
|
+
a.download = `${scriptName.trim() || "custom_indicator"}.js`;
|
|
2202
|
+
document.body.appendChild(a);
|
|
2203
|
+
a.click();
|
|
2204
|
+
document.body.removeChild(a);
|
|
2205
|
+
URL.revokeObjectURL(url);
|
|
2206
|
+
setStatus("Script exported.");
|
|
2207
|
+
}, [code, scriptName]);
|
|
2208
|
+
const importScript = useCallback(
|
|
2209
|
+
(file) => {
|
|
2210
|
+
const reader = new FileReader();
|
|
2211
|
+
reader.onload = (e) => {
|
|
2212
|
+
const content = e.target?.result;
|
|
2213
|
+
if (content) {
|
|
2214
|
+
setCode(content);
|
|
2215
|
+
if (!scriptName) {
|
|
2216
|
+
setScriptName(file.name.replace(/\.(js|ts|txt)$/i, ""));
|
|
2217
|
+
}
|
|
2218
|
+
setError("");
|
|
2219
|
+
setStatus(`Loaded ${file.name}`);
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
reader.readAsText(file);
|
|
2223
|
+
},
|
|
2224
|
+
[scriptName]
|
|
2225
|
+
);
|
|
2226
|
+
return {
|
|
2227
|
+
code,
|
|
2228
|
+
setCode,
|
|
2229
|
+
scriptName,
|
|
2230
|
+
setScriptName,
|
|
2231
|
+
params,
|
|
2232
|
+
setParams,
|
|
2233
|
+
placement,
|
|
2234
|
+
setPlacement,
|
|
2235
|
+
error,
|
|
2236
|
+
status,
|
|
2237
|
+
isRunning,
|
|
2238
|
+
hasActiveScript,
|
|
2239
|
+
runScript,
|
|
2240
|
+
removeScript,
|
|
2241
|
+
resetCode,
|
|
2242
|
+
exportScript,
|
|
2243
|
+
importScript,
|
|
2244
|
+
defaultScript: DEFAULT_SCRIPT
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
1323
2247
|
|
|
1324
2248
|
// src/utils/createDataLoader.ts
|
|
1325
2249
|
function createDataLoader(datafeed, dispatch) {
|
|
@@ -1385,6 +2309,6 @@ function createDataLoader(datafeed, dispatch) {
|
|
|
1385
2309
|
};
|
|
1386
2310
|
}
|
|
1387
2311
|
|
|
1388
|
-
export { CANDLE_TYPES, COMPARE_RULES, DEFAULT_PERIODS, DRAWING_CATEGORIES, INDICATOR_PARAMS, KlinechartsUIDispatchContext, KlinechartsUIProvider, KlinechartsUIStateContext, MAIN_INDICATORS, PRICE_AXIS_TYPES, SUB_INDICATORS, TIMEZONES, TOOLTIP_SHOW_RULES, YAXIS_POSITIONS, createDataLoader, useDrawingTools, useFullscreen, useIndicators, useKlinechartsUI, useKlinechartsUILoading, useKlinechartsUISettings, useKlinechartsUITheme, useOrderLines, usePeriods, useScreenshot, useSymbolSearch, useTimezone };
|
|
2312
|
+
export { CANDLE_TYPES, COMPARE_RULES, DEFAULT_PERIODS, DRAWING_CATEGORIES, INDICATOR_PARAMS, KlinechartsUIDispatchContext, KlinechartsUIProvider, KlinechartsUIStateContext, MAIN_INDICATORS, PRICE_AXIS_TYPES, SUB_INDICATORS, TIMEZONES, TOOLTIP_SHOW_RULES, YAXIS_POSITIONS, createDataLoader, useDrawingTools, useFullscreen, useIndicators, useKlinechartsUI, useKlinechartsUILoading, useKlinechartsUISettings, useKlinechartsUITheme, useLayoutManager, useOrderLines, usePeriods, useScreenshot, useScriptEditor, useSymbolSearch, useTimezone, useUndoRedo };
|
|
1389
2313
|
//# sourceMappingURL=index.js.map
|
|
1390
2314
|
//# sourceMappingURL=index.js.map
|