react-klinecharts-ui 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +277 -7
- package/dist/{chunk-47JXSAIT.cjs → chunk-PIFLJKYF.cjs} +47 -2
- package/dist/chunk-PIFLJKYF.cjs.map +1 -0
- package/dist/{chunk-B3NZ66YA.js → chunk-U325AHHX.js} +47 -3
- package/dist/chunk-U325AHHX.js.map +1 -0
- package/dist/extensions.cjs +9 -5
- package/dist/extensions.d.cts +18 -1
- package/dist/extensions.d.ts +18 -1
- package/dist/extensions.js +1 -1
- package/dist/index.cjs +1117 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +232 -5
- package/dist/index.d.ts +232 -5
- package/dist/index.js +1063 -81
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/dist/chunk-47JXSAIT.cjs.map +0 -1
- package/dist/chunk-B3NZ66YA.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { registerExtensions, TA_default } from './chunk-
|
|
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-
|
|
1
|
+
import { registerExtensions, TA_default } from './chunk-U325AHHX.js';
|
|
2
|
+
export { TA_default as TA, abcd_default as abcd, anyWaves_default as anyWaves, arrow_default as arrow, bollTv_default as bollTv, brush_default as brush, cci_default as cci, circle_default as circle, depthOverlay_default as depthOverlay, eightWaves_default as eightWaves, elliottWave_default as elliottWave, fibRetracement_default as fibRetracement, fibonacciCircle_default as fibonacciCircle, fibonacciExtension_default as fibonacciExtension, fibonacciSegment_default as fibonacciSegment, fibonacciSpeedResistanceFan_default as fibonacciSpeedResistanceFan, fibonacciSpiral_default as fibonacciSpiral, fiveWaves_default as fiveWaves, gannBox_default as gannBox, gannFan_default as gannFan, hma_default as hma, ichimoku_default as ichimoku, indicators, longPosition_default as longPosition, maRibbon_default as maRibbon, macdTv_default as macdTv, measure_default as measure, orderLine_default as orderLine, overlays, parallelChannel_default as parallelChannel, parallelogram_default as parallelogram, pivotPoints_default as pivotPoints, ray_default as ray, rect_default as rect, registerExtensions, rsiTv_default as rsiTv, shortPosition_default as shortPosition, stochastic_default as stochastic, superTrend_default as superTrend, threeWaves_default as threeWaves, triangle_default as triangle, vwap_default as vwap, xabcd_default as xabcd } from './chunk-U325AHHX.js';
|
|
3
3
|
import { createContext, useContext, useReducer, useRef, useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import { registerOverlay, registerIndicator } from 'react-klinecharts';
|
|
5
5
|
import { jsx } from 'react/jsx-runtime';
|
|
@@ -57,6 +57,8 @@ function reducer(state, action) {
|
|
|
57
57
|
return { ...state, mainIndicators: action.indicators };
|
|
58
58
|
case "SET_SUB_INDICATORS":
|
|
59
59
|
return { ...state, subIndicators: action.indicators };
|
|
60
|
+
case "SET_INDICATOR_AXES":
|
|
61
|
+
return { ...state, indicatorAxes: action.axes };
|
|
60
62
|
case "SET_STYLES":
|
|
61
63
|
return { ...state, styles: action.styles };
|
|
62
64
|
case "SET_LOCALE":
|
|
@@ -119,6 +121,7 @@ function KlinechartsUIProvider({
|
|
|
119
121
|
(acc, name) => ({ ...acc, [name]: "" }),
|
|
120
122
|
{}
|
|
121
123
|
),
|
|
124
|
+
indicatorAxes: {},
|
|
122
125
|
styles: opts.styles,
|
|
123
126
|
screenshotUrl: null
|
|
124
127
|
})
|
|
@@ -643,9 +646,12 @@ var INDICATOR_PARAMS = {
|
|
|
643
646
|
};
|
|
644
647
|
|
|
645
648
|
// src/hooks/useIndicators.ts
|
|
649
|
+
var COLLAPSED_HEIGHT = 30;
|
|
646
650
|
function useIndicators() {
|
|
647
651
|
const { state, dispatch } = useKlinechartsUI();
|
|
648
652
|
const { undoRedoListenerRef } = useKlinechartsUIDispatch();
|
|
653
|
+
const paneHeightsRef = useRef({});
|
|
654
|
+
const collapsedPanesRef = useRef(/* @__PURE__ */ new Set());
|
|
649
655
|
const mainIndicators = useMemo(() => {
|
|
650
656
|
const activeNames = state.mainIndicators;
|
|
651
657
|
const inactive = MAIN_INDICATORS.filter((n) => !activeNames.includes(n));
|
|
@@ -663,62 +669,95 @@ function useIndicators() {
|
|
|
663
669
|
];
|
|
664
670
|
}, [state.subIndicators]);
|
|
665
671
|
const addMainIndicator = useCallback(
|
|
666
|
-
(name) => {
|
|
672
|
+
(name, options) => {
|
|
667
673
|
if (state.mainIndicators.includes(name)) return;
|
|
674
|
+
const yAxis = options?.yAxis;
|
|
668
675
|
state.chart?.createIndicator(
|
|
669
|
-
{ name, id: `main_${name}
|
|
670
|
-
true,
|
|
671
|
-
{ id: "candle_pane" }
|
|
676
|
+
{ name, id: `main_${name}` },
|
|
677
|
+
{ isStack: true, pane: { id: "candle_pane" }, yAxis }
|
|
672
678
|
);
|
|
673
679
|
const newIndicators = [...state.mainIndicators, name];
|
|
674
680
|
dispatch({ type: "SET_MAIN_INDICATORS", indicators: newIndicators });
|
|
681
|
+
if (yAxis?.id) {
|
|
682
|
+
dispatch({
|
|
683
|
+
type: "SET_INDICATOR_AXES",
|
|
684
|
+
axes: { ...state.indicatorAxes, [`main_${name}`]: yAxis.id }
|
|
685
|
+
});
|
|
686
|
+
}
|
|
675
687
|
undoRedoListenerRef.current?.({
|
|
676
688
|
type: "indicator_toggled",
|
|
677
|
-
data: {
|
|
689
|
+
data: {
|
|
690
|
+
name,
|
|
691
|
+
wasActive: false,
|
|
692
|
+
isMain: true,
|
|
693
|
+
paneId: "candle_pane",
|
|
694
|
+
yAxisId: yAxis?.id
|
|
695
|
+
}
|
|
678
696
|
});
|
|
679
697
|
},
|
|
680
|
-
[state.chart, state.mainIndicators, dispatch, undoRedoListenerRef]
|
|
698
|
+
[state.chart, state.mainIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
|
|
681
699
|
);
|
|
682
700
|
const removeMainIndicator = useCallback(
|
|
683
701
|
(name) => {
|
|
684
|
-
|
|
702
|
+
const id = `main_${name}`;
|
|
703
|
+
const yAxisId = state.indicatorAxes[id];
|
|
704
|
+
state.chart?.removeIndicator({ id });
|
|
685
705
|
const newIndicators = state.mainIndicators.filter((n) => n !== name);
|
|
686
706
|
dispatch({ type: "SET_MAIN_INDICATORS", indicators: newIndicators });
|
|
707
|
+
if (yAxisId) {
|
|
708
|
+
const nextAxes = { ...state.indicatorAxes };
|
|
709
|
+
delete nextAxes[id];
|
|
710
|
+
dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
|
|
711
|
+
}
|
|
687
712
|
undoRedoListenerRef.current?.({
|
|
688
713
|
type: "indicator_toggled",
|
|
689
|
-
data: { name, wasActive: true, isMain: true, paneId: "candle_pane" }
|
|
714
|
+
data: { name, wasActive: true, isMain: true, paneId: "candle_pane", yAxisId }
|
|
690
715
|
});
|
|
691
716
|
},
|
|
692
|
-
[state.chart, state.mainIndicators, dispatch, undoRedoListenerRef]
|
|
717
|
+
[state.chart, state.mainIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
|
|
693
718
|
);
|
|
694
719
|
const addSubIndicator = useCallback(
|
|
695
|
-
(name) => {
|
|
720
|
+
(name, options) => {
|
|
696
721
|
if (name in state.subIndicators) return;
|
|
697
722
|
const id = `sub_${name}`;
|
|
698
|
-
|
|
723
|
+
const yAxis = options?.yAxis;
|
|
724
|
+
state.chart?.createIndicator({ name, id }, { yAxis });
|
|
699
725
|
const paneId = state.chart?.getIndicators({ id })?.[0]?.paneId ?? "";
|
|
700
726
|
const newIndicators = { ...state.subIndicators, [name]: paneId };
|
|
701
727
|
dispatch({ type: "SET_SUB_INDICATORS", indicators: newIndicators });
|
|
728
|
+
if (yAxis?.id) {
|
|
729
|
+
dispatch({
|
|
730
|
+
type: "SET_INDICATOR_AXES",
|
|
731
|
+
axes: { ...state.indicatorAxes, [id]: yAxis.id }
|
|
732
|
+
});
|
|
733
|
+
}
|
|
702
734
|
undoRedoListenerRef.current?.({
|
|
703
735
|
type: "indicator_toggled",
|
|
704
|
-
data: { name, wasActive: false, isMain: false, paneId }
|
|
736
|
+
data: { name, wasActive: false, isMain: false, paneId, yAxisId: yAxis?.id }
|
|
705
737
|
});
|
|
706
738
|
},
|
|
707
|
-
[state.chart, state.subIndicators, dispatch, undoRedoListenerRef]
|
|
739
|
+
[state.chart, state.subIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
|
|
708
740
|
);
|
|
709
741
|
const removeSubIndicator = useCallback(
|
|
710
742
|
(name) => {
|
|
743
|
+
const id = `sub_${name}`;
|
|
711
744
|
const paneId = state.subIndicators[name] ?? "";
|
|
712
|
-
state.
|
|
745
|
+
const yAxisId = state.indicatorAxes[id];
|
|
746
|
+
state.chart?.removeIndicator({ id });
|
|
713
747
|
const newIndicators = { ...state.subIndicators };
|
|
714
748
|
delete newIndicators[name];
|
|
715
749
|
dispatch({ type: "SET_SUB_INDICATORS", indicators: newIndicators });
|
|
750
|
+
if (yAxisId) {
|
|
751
|
+
const nextAxes = { ...state.indicatorAxes };
|
|
752
|
+
delete nextAxes[id];
|
|
753
|
+
dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
|
|
754
|
+
}
|
|
716
755
|
undoRedoListenerRef.current?.({
|
|
717
756
|
type: "indicator_toggled",
|
|
718
|
-
data: { name, wasActive: true, isMain: false, paneId }
|
|
757
|
+
data: { name, wasActive: true, isMain: false, paneId, yAxisId }
|
|
719
758
|
});
|
|
720
759
|
},
|
|
721
|
-
[state.chart, state.subIndicators, dispatch, undoRedoListenerRef]
|
|
760
|
+
[state.chart, state.subIndicators, state.indicatorAxes, dispatch, undoRedoListenerRef]
|
|
722
761
|
);
|
|
723
762
|
const toggleMainIndicator = useCallback(
|
|
724
763
|
(name) => {
|
|
@@ -748,8 +787,10 @@ function useIndicators() {
|
|
|
748
787
|
[state.chart]
|
|
749
788
|
);
|
|
750
789
|
const updateIndicatorParams = useCallback(
|
|
751
|
-
|
|
752
|
-
|
|
790
|
+
// `paneId` is kept in the signature for API stability; klinecharts v10
|
|
791
|
+
// `overrideIndicator` no longer accepts it and targets by name/id instead.
|
|
792
|
+
(name, _paneId, params) => {
|
|
793
|
+
state.chart?.overrideIndicator({ name, calcParams: params });
|
|
753
794
|
},
|
|
754
795
|
[state.chart]
|
|
755
796
|
);
|
|
@@ -770,11 +811,9 @@ function useIndicators() {
|
|
|
770
811
|
{
|
|
771
812
|
name,
|
|
772
813
|
id: `main_${name}`,
|
|
773
|
-
paneId: "candle_pane",
|
|
774
814
|
...prevCalcParams ? { calcParams: prevCalcParams } : {}
|
|
775
815
|
},
|
|
776
|
-
true,
|
|
777
|
-
{ id: "candle_pane" }
|
|
816
|
+
{ isStack: true, pane: { id: "candle_pane" } }
|
|
778
817
|
);
|
|
779
818
|
const newSub = { ...state.subIndicators };
|
|
780
819
|
delete newSub[name];
|
|
@@ -815,6 +854,128 @@ function useIndicators() {
|
|
|
815
854
|
(name) => name in state.subIndicators,
|
|
816
855
|
[state.subIndicators]
|
|
817
856
|
);
|
|
857
|
+
const collapseSubIndicator = useCallback(
|
|
858
|
+
(name) => {
|
|
859
|
+
const paneId = state.subIndicators[name];
|
|
860
|
+
if (!state.chart || !paneId) return;
|
|
861
|
+
const currentSize = state.chart.getSize?.(paneId);
|
|
862
|
+
if (currentSize?.height && currentSize.height > COLLAPSED_HEIGHT) {
|
|
863
|
+
paneHeightsRef.current[paneId] = currentSize.height;
|
|
864
|
+
}
|
|
865
|
+
collapsedPanesRef.current.add(paneId);
|
|
866
|
+
state.chart.setPaneOptions?.({
|
|
867
|
+
id: paneId,
|
|
868
|
+
height: COLLAPSED_HEIGHT
|
|
869
|
+
});
|
|
870
|
+
state.chart.overrideIndicator({ name, id: `sub_${name}`, visible: false });
|
|
871
|
+
},
|
|
872
|
+
[state.chart, state.subIndicators]
|
|
873
|
+
);
|
|
874
|
+
const expandSubIndicator = useCallback(
|
|
875
|
+
(name) => {
|
|
876
|
+
const paneId = state.subIndicators[name];
|
|
877
|
+
if (!state.chart || !paneId) return;
|
|
878
|
+
const savedHeight = paneHeightsRef.current[paneId] ?? 100;
|
|
879
|
+
collapsedPanesRef.current.delete(paneId);
|
|
880
|
+
state.chart.setPaneOptions?.({
|
|
881
|
+
id: paneId,
|
|
882
|
+
height: savedHeight
|
|
883
|
+
});
|
|
884
|
+
state.chart.overrideIndicator({ name, id: `sub_${name}`, visible: true });
|
|
885
|
+
},
|
|
886
|
+
[state.chart, state.subIndicators]
|
|
887
|
+
);
|
|
888
|
+
const isSubIndicatorCollapsed = useCallback(
|
|
889
|
+
(name) => {
|
|
890
|
+
const paneId = state.subIndicators[name];
|
|
891
|
+
return paneId ? collapsedPanesRef.current.has(paneId) : false;
|
|
892
|
+
},
|
|
893
|
+
[state.subIndicators]
|
|
894
|
+
);
|
|
895
|
+
const reorderSubIndicator = useCallback(
|
|
896
|
+
(name, direction) => {
|
|
897
|
+
if (!state.chart) return;
|
|
898
|
+
const subNames = Object.keys(state.subIndicators);
|
|
899
|
+
const idx = subNames.indexOf(name);
|
|
900
|
+
if (idx === -1) return;
|
|
901
|
+
const swapIdx = direction === "up" ? idx - 1 : idx + 1;
|
|
902
|
+
if (swapIdx < 0 || swapIdx >= subNames.length) return;
|
|
903
|
+
const subStates = subNames.map((n) => {
|
|
904
|
+
const id = `sub_${n}`;
|
|
905
|
+
const indicator = state.chart.getIndicators({ id })?.[0];
|
|
906
|
+
return {
|
|
907
|
+
name: n,
|
|
908
|
+
calcParams: indicator?.calcParams,
|
|
909
|
+
visible: indicator?.visible ?? true,
|
|
910
|
+
styles: indicator?.styles,
|
|
911
|
+
paneId: state.subIndicators[n]
|
|
912
|
+
};
|
|
913
|
+
});
|
|
914
|
+
[subStates[idx], subStates[swapIdx]] = [subStates[swapIdx], subStates[idx]];
|
|
915
|
+
for (const n of subNames) {
|
|
916
|
+
state.chart.removeIndicator({ id: `sub_${n}` });
|
|
917
|
+
}
|
|
918
|
+
const newSubIndicators = {};
|
|
919
|
+
for (const sub of subStates) {
|
|
920
|
+
const id = `sub_${sub.name}`;
|
|
921
|
+
state.chart.createIndicator({
|
|
922
|
+
name: sub.name,
|
|
923
|
+
id,
|
|
924
|
+
...sub.calcParams ? { calcParams: sub.calcParams } : {},
|
|
925
|
+
visible: sub.visible
|
|
926
|
+
});
|
|
927
|
+
const paneId = state.chart.getIndicators({ id })?.[0]?.paneId ?? "";
|
|
928
|
+
newSubIndicators[sub.name] = paneId;
|
|
929
|
+
if (sub.styles) {
|
|
930
|
+
state.chart.overrideIndicator({
|
|
931
|
+
name: sub.name,
|
|
932
|
+
id,
|
|
933
|
+
styles: sub.styles
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
dispatch({ type: "SET_SUB_INDICATORS", indicators: newSubIndicators });
|
|
938
|
+
},
|
|
939
|
+
[state.chart, state.subIndicators, dispatch]
|
|
940
|
+
);
|
|
941
|
+
const getIndicatorAxis = useCallback(
|
|
942
|
+
(name, isMain) => state.indicatorAxes[isMain ? `main_${name}` : `sub_${name}`],
|
|
943
|
+
[state.indicatorAxes]
|
|
944
|
+
);
|
|
945
|
+
const bindIndicatorToNewAxis = useCallback(
|
|
946
|
+
(name, isMain, yAxis) => {
|
|
947
|
+
if (!state.chart) return;
|
|
948
|
+
const id = isMain ? `main_${name}` : `sub_${name}`;
|
|
949
|
+
const current = state.chart.getIndicators({ id })?.[0];
|
|
950
|
+
if (!current) return;
|
|
951
|
+
const calcParams = current.calcParams;
|
|
952
|
+
const styles = current.styles;
|
|
953
|
+
const visible = current.visible ?? true;
|
|
954
|
+
const paneId = isMain ? "candle_pane" : current.paneId ?? state.subIndicators[name] ?? "";
|
|
955
|
+
state.chart.removeIndicator({ id });
|
|
956
|
+
state.chart.createIndicator(
|
|
957
|
+
{
|
|
958
|
+
name,
|
|
959
|
+
id,
|
|
960
|
+
...calcParams ? { calcParams } : {},
|
|
961
|
+
visible
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
isStack: isMain,
|
|
965
|
+
...paneId ? { pane: { id: paneId } } : {},
|
|
966
|
+
yAxis
|
|
967
|
+
}
|
|
968
|
+
);
|
|
969
|
+
if (styles) {
|
|
970
|
+
state.chart.overrideIndicator({ name, id, styles });
|
|
971
|
+
}
|
|
972
|
+
const nextAxes = { ...state.indicatorAxes };
|
|
973
|
+
if (yAxis?.id) nextAxes[id] = yAxis.id;
|
|
974
|
+
else delete nextAxes[id];
|
|
975
|
+
dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
|
|
976
|
+
},
|
|
977
|
+
[state.chart, state.indicatorAxes, state.subIndicators, dispatch]
|
|
978
|
+
);
|
|
818
979
|
return {
|
|
819
980
|
mainIndicators,
|
|
820
981
|
subIndicators,
|
|
@@ -834,7 +995,14 @@ function useIndicators() {
|
|
|
834
995
|
updateIndicatorParams,
|
|
835
996
|
getIndicatorParams,
|
|
836
997
|
isMainIndicatorActive,
|
|
837
|
-
isSubIndicatorActive
|
|
998
|
+
isSubIndicatorActive,
|
|
999
|
+
collapseSubIndicator,
|
|
1000
|
+
expandSubIndicator,
|
|
1001
|
+
isSubIndicatorCollapsed,
|
|
1002
|
+
reorderSubIndicator,
|
|
1003
|
+
indicatorAxes: state.indicatorAxes,
|
|
1004
|
+
getIndicatorAxis,
|
|
1005
|
+
bindIndicatorToNewAxis
|
|
838
1006
|
};
|
|
839
1007
|
}
|
|
840
1008
|
|
|
@@ -933,6 +1101,17 @@ function useDrawingTools() {
|
|
|
933
1101
|
const [magnetMode, setMagnetModeState] = useState("normal");
|
|
934
1102
|
const [isLocked, setIsLocked] = useState(false);
|
|
935
1103
|
const [isVisible, setIsVisible] = useState(true);
|
|
1104
|
+
const [autoRetrigger, setAutoRetrigger] = useState(true);
|
|
1105
|
+
const activeToolRef = useRef(activeTool);
|
|
1106
|
+
activeToolRef.current = activeTool;
|
|
1107
|
+
const autoRetriggerRef = useRef(autoRetrigger);
|
|
1108
|
+
autoRetriggerRef.current = autoRetrigger;
|
|
1109
|
+
const isLockedRef = useRef(isLocked);
|
|
1110
|
+
isLockedRef.current = isLocked;
|
|
1111
|
+
const isVisibleRef = useRef(isVisible);
|
|
1112
|
+
isVisibleRef.current = isVisible;
|
|
1113
|
+
const magnetModeRef = useRef(magnetMode);
|
|
1114
|
+
magnetModeRef.current = magnetMode;
|
|
936
1115
|
const categories = useMemo(
|
|
937
1116
|
() => DRAWING_CATEGORIES.map((cat) => ({
|
|
938
1117
|
key: cat.key,
|
|
@@ -943,15 +1122,15 @@ function useDrawingTools() {
|
|
|
943
1122
|
})),
|
|
944
1123
|
[]
|
|
945
1124
|
);
|
|
946
|
-
const
|
|
1125
|
+
const createOverlayForTool = useCallback(
|
|
947
1126
|
(name) => {
|
|
948
|
-
|
|
1127
|
+
const mode = magnetModeRef.current === "strong" ? "strong_magnet" : magnetModeRef.current === "weak" ? "weak_magnet" : "normal";
|
|
949
1128
|
state.chart?.createOverlay({
|
|
950
1129
|
name,
|
|
951
1130
|
groupId: DRAWING_GROUP_ID,
|
|
952
|
-
lock:
|
|
953
|
-
visible:
|
|
954
|
-
mode
|
|
1131
|
+
lock: isLockedRef.current,
|
|
1132
|
+
visible: isVisibleRef.current,
|
|
1133
|
+
mode,
|
|
955
1134
|
onDrawEnd: (event) => {
|
|
956
1135
|
const o = event.overlay;
|
|
957
1136
|
undoRedoListenerRef.current?.({
|
|
@@ -966,10 +1145,22 @@ function useDrawingTools() {
|
|
|
966
1145
|
}
|
|
967
1146
|
}
|
|
968
1147
|
});
|
|
1148
|
+
if (autoRetriggerRef.current && activeToolRef.current === name) {
|
|
1149
|
+
requestAnimationFrame(() => {
|
|
1150
|
+
createOverlayForTool(name);
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
969
1153
|
}
|
|
970
1154
|
});
|
|
971
1155
|
},
|
|
972
|
-
[state.chart,
|
|
1156
|
+
[state.chart, undoRedoListenerRef]
|
|
1157
|
+
);
|
|
1158
|
+
const selectTool = useCallback(
|
|
1159
|
+
(name) => {
|
|
1160
|
+
setActiveTool(name);
|
|
1161
|
+
createOverlayForTool(name);
|
|
1162
|
+
},
|
|
1163
|
+
[createOverlayForTool]
|
|
973
1164
|
);
|
|
974
1165
|
const clearActiveTool = useCallback(() => {
|
|
975
1166
|
setActiveTool(null);
|
|
@@ -1040,12 +1231,14 @@ function useDrawingTools() {
|
|
|
1040
1231
|
magnetMode,
|
|
1041
1232
|
isLocked,
|
|
1042
1233
|
isVisible,
|
|
1234
|
+
autoRetrigger,
|
|
1043
1235
|
selectTool,
|
|
1044
1236
|
clearActiveTool,
|
|
1045
1237
|
setMagnetMode,
|
|
1046
1238
|
toggleLock,
|
|
1047
1239
|
toggleVisibility,
|
|
1048
|
-
removeAllDrawings
|
|
1240
|
+
removeAllDrawings,
|
|
1241
|
+
setAutoRetrigger
|
|
1049
1242
|
};
|
|
1050
1243
|
}
|
|
1051
1244
|
|
|
@@ -1119,20 +1312,23 @@ function useKlinechartsUISettings() {
|
|
|
1119
1312
|
() => TOOLTIP_SHOW_RULES.map((r) => ({ key: r, localeKey: r })),
|
|
1120
1313
|
[]
|
|
1121
1314
|
);
|
|
1315
|
+
const applyPaneAxis = useCallback(
|
|
1316
|
+
(axis) => {
|
|
1317
|
+
state.chart?.overrideYAxis?.({ paneId: "candle_pane", ...axis });
|
|
1318
|
+
},
|
|
1319
|
+
[state.chart]
|
|
1320
|
+
);
|
|
1122
1321
|
const hasAppliedInitial = useRef(false);
|
|
1123
1322
|
useEffect(() => {
|
|
1124
1323
|
if (!state.chart || hasAppliedInitial.current) return;
|
|
1125
1324
|
hasAppliedInitial.current = true;
|
|
1126
|
-
const
|
|
1127
|
-
if (
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
...settings.yAxisPosition !== "right" && { position: settings.yAxisPosition },
|
|
1134
|
-
...settings.yAxisInside && { inside: true }
|
|
1135
|
-
}
|
|
1325
|
+
const needsAxisOverride = settings.reverseCoordinate || settings.priceAxisType !== "normal" || settings.yAxisPosition !== "right" || settings.yAxisInside;
|
|
1326
|
+
if (needsAxisOverride) {
|
|
1327
|
+
applyPaneAxis({
|
|
1328
|
+
...settings.priceAxisType !== "normal" && { name: settings.priceAxisType },
|
|
1329
|
+
...settings.reverseCoordinate && { reverse: true },
|
|
1330
|
+
...settings.yAxisPosition !== "right" && { position: settings.yAxisPosition },
|
|
1331
|
+
...settings.yAxisInside && { inside: true }
|
|
1136
1332
|
});
|
|
1137
1333
|
}
|
|
1138
1334
|
if (settings.showIndicatorLastValue) {
|
|
@@ -1155,12 +1351,6 @@ function useKlinechartsUISettings() {
|
|
|
1155
1351
|
},
|
|
1156
1352
|
[state.chart]
|
|
1157
1353
|
);
|
|
1158
|
-
const applyPaneAxis = useCallback(
|
|
1159
|
-
(axis) => {
|
|
1160
|
-
state.chart?.setPaneOptions({ id: "candle_pane", axis });
|
|
1161
|
-
},
|
|
1162
|
-
[state.chart]
|
|
1163
|
-
);
|
|
1164
1354
|
const setCandleType = useCallback(
|
|
1165
1355
|
(type) => {
|
|
1166
1356
|
applyStyle("candle.type", type);
|
|
@@ -1308,11 +1498,8 @@ function useKlinechartsUISettings() {
|
|
|
1308
1498
|
const resetToDefaults = useCallback(() => {
|
|
1309
1499
|
setSettings(defaultSettings);
|
|
1310
1500
|
state.chart?.setStyles(state.theme);
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
axis: { name: "normal", reverse: false, position: "right", inside: false }
|
|
1314
|
-
});
|
|
1315
|
-
}, [state.chart, state.theme]);
|
|
1501
|
+
applyPaneAxis({ name: "normal", reverse: false, position: "right", inside: false });
|
|
1502
|
+
}, [state.chart, state.theme, applyPaneAxis]);
|
|
1316
1503
|
return {
|
|
1317
1504
|
...settings,
|
|
1318
1505
|
candleTypes,
|
|
@@ -1569,21 +1756,28 @@ function useUndoRedo() {
|
|
|
1569
1756
|
break;
|
|
1570
1757
|
}
|
|
1571
1758
|
case "indicator_toggled": {
|
|
1572
|
-
const { name, wasActive, isMain, paneId } = action.data;
|
|
1759
|
+
const { name, wasActive, isMain, paneId, yAxisId } = action.data;
|
|
1760
|
+
const id = isMain ? `main_${name}` : `sub_${name}`;
|
|
1573
1761
|
if (wasActive) {
|
|
1574
1762
|
if (isMain) {
|
|
1575
1763
|
state.chart.createIndicator(
|
|
1576
|
-
{ name, id
|
|
1577
|
-
|
|
1578
|
-
|
|
1764
|
+
{ name, id },
|
|
1765
|
+
{
|
|
1766
|
+
isStack: true,
|
|
1767
|
+
pane: { id: "candle_pane" },
|
|
1768
|
+
...yAxisId ? { yAxis: { id: yAxisId } } : {}
|
|
1769
|
+
}
|
|
1579
1770
|
);
|
|
1580
1771
|
dispatch({
|
|
1581
1772
|
type: "SET_MAIN_INDICATORS",
|
|
1582
1773
|
indicators: [...state.mainIndicators, name]
|
|
1583
1774
|
});
|
|
1584
1775
|
} else {
|
|
1585
|
-
state.chart.createIndicator(
|
|
1586
|
-
|
|
1776
|
+
state.chart.createIndicator(
|
|
1777
|
+
{ name, id },
|
|
1778
|
+
yAxisId ? { yAxis: { id: yAxisId } } : void 0
|
|
1779
|
+
);
|
|
1780
|
+
const newPaneId = state.chart.getIndicators({ id })?.[0]?.paneId ?? "";
|
|
1587
1781
|
dispatch({
|
|
1588
1782
|
type: "SET_SUB_INDICATORS",
|
|
1589
1783
|
indicators: {
|
|
@@ -1592,9 +1786,15 @@ function useUndoRedo() {
|
|
|
1592
1786
|
}
|
|
1593
1787
|
});
|
|
1594
1788
|
}
|
|
1789
|
+
if (yAxisId) {
|
|
1790
|
+
dispatch({
|
|
1791
|
+
type: "SET_INDICATOR_AXES",
|
|
1792
|
+
axes: { ...state.indicatorAxes, [id]: yAxisId }
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1595
1795
|
} else {
|
|
1796
|
+
state.chart.removeIndicator({ id });
|
|
1596
1797
|
if (isMain) {
|
|
1597
|
-
state.chart.removeIndicator({ id: `main_${name}` });
|
|
1598
1798
|
dispatch({
|
|
1599
1799
|
type: "SET_MAIN_INDICATORS",
|
|
1600
1800
|
indicators: state.mainIndicators.filter(
|
|
@@ -1602,7 +1802,6 @@ function useUndoRedo() {
|
|
|
1602
1802
|
)
|
|
1603
1803
|
});
|
|
1604
1804
|
} else {
|
|
1605
|
-
state.chart.removeIndicator({ id: `sub_${name}` });
|
|
1606
1805
|
const newSub = { ...state.subIndicators };
|
|
1607
1806
|
delete newSub[name];
|
|
1608
1807
|
dispatch({
|
|
@@ -1610,6 +1809,11 @@ function useUndoRedo() {
|
|
|
1610
1809
|
indicators: newSub
|
|
1611
1810
|
});
|
|
1612
1811
|
}
|
|
1812
|
+
if (state.indicatorAxes[id]) {
|
|
1813
|
+
const nextAxes = { ...state.indicatorAxes };
|
|
1814
|
+
delete nextAxes[id];
|
|
1815
|
+
dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
|
|
1816
|
+
}
|
|
1613
1817
|
}
|
|
1614
1818
|
setRedoStack((prev) => [
|
|
1615
1819
|
...prev,
|
|
@@ -1619,7 +1823,8 @@ function useUndoRedo() {
|
|
|
1619
1823
|
name,
|
|
1620
1824
|
wasActive: !wasActive,
|
|
1621
1825
|
isMain,
|
|
1622
|
-
paneId
|
|
1826
|
+
paneId,
|
|
1827
|
+
yAxisId
|
|
1623
1828
|
}
|
|
1624
1829
|
}
|
|
1625
1830
|
]);
|
|
@@ -1629,7 +1834,7 @@ function useUndoRedo() {
|
|
|
1629
1834
|
} finally {
|
|
1630
1835
|
isProcessingRef.current = false;
|
|
1631
1836
|
}
|
|
1632
|
-
}, [undoStack, state.chart, state.mainIndicators, state.subIndicators, dispatch]);
|
|
1837
|
+
}, [undoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
|
|
1633
1838
|
const redo = useCallback(() => {
|
|
1634
1839
|
if (redoStack.length === 0 || !state.chart || isProcessingRef.current)
|
|
1635
1840
|
return;
|
|
@@ -1681,21 +1886,28 @@ function useUndoRedo() {
|
|
|
1681
1886
|
break;
|
|
1682
1887
|
}
|
|
1683
1888
|
case "indicator_toggled": {
|
|
1684
|
-
const { name, wasActive, isMain } = action.data;
|
|
1889
|
+
const { name, wasActive, isMain, paneId, yAxisId } = action.data;
|
|
1890
|
+
const id = isMain ? `main_${name}` : `sub_${name}`;
|
|
1685
1891
|
if (wasActive) {
|
|
1686
1892
|
if (isMain) {
|
|
1687
1893
|
state.chart.createIndicator(
|
|
1688
|
-
{ name, id
|
|
1689
|
-
|
|
1690
|
-
|
|
1894
|
+
{ name, id },
|
|
1895
|
+
{
|
|
1896
|
+
isStack: true,
|
|
1897
|
+
pane: { id: "candle_pane" },
|
|
1898
|
+
...yAxisId ? { yAxis: { id: yAxisId } } : {}
|
|
1899
|
+
}
|
|
1691
1900
|
);
|
|
1692
1901
|
dispatch({
|
|
1693
1902
|
type: "SET_MAIN_INDICATORS",
|
|
1694
1903
|
indicators: [...state.mainIndicators, name]
|
|
1695
1904
|
});
|
|
1696
1905
|
} else {
|
|
1697
|
-
state.chart.createIndicator(
|
|
1698
|
-
|
|
1906
|
+
state.chart.createIndicator(
|
|
1907
|
+
{ name, id },
|
|
1908
|
+
yAxisId ? { yAxis: { id: yAxisId } } : void 0
|
|
1909
|
+
);
|
|
1910
|
+
const newPaneId = state.chart.getIndicators({ id })?.[0]?.paneId ?? "";
|
|
1699
1911
|
dispatch({
|
|
1700
1912
|
type: "SET_SUB_INDICATORS",
|
|
1701
1913
|
indicators: {
|
|
@@ -1704,9 +1916,15 @@ function useUndoRedo() {
|
|
|
1704
1916
|
}
|
|
1705
1917
|
});
|
|
1706
1918
|
}
|
|
1919
|
+
if (yAxisId) {
|
|
1920
|
+
dispatch({
|
|
1921
|
+
type: "SET_INDICATOR_AXES",
|
|
1922
|
+
axes: { ...state.indicatorAxes, [id]: yAxisId }
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1707
1925
|
} else {
|
|
1926
|
+
state.chart.removeIndicator({ id });
|
|
1708
1927
|
if (isMain) {
|
|
1709
|
-
state.chart.removeIndicator({ id: `main_${name}` });
|
|
1710
1928
|
dispatch({
|
|
1711
1929
|
type: "SET_MAIN_INDICATORS",
|
|
1712
1930
|
indicators: state.mainIndicators.filter(
|
|
@@ -1714,7 +1932,6 @@ function useUndoRedo() {
|
|
|
1714
1932
|
)
|
|
1715
1933
|
});
|
|
1716
1934
|
} else {
|
|
1717
|
-
state.chart.removeIndicator({ id: `sub_${name}` });
|
|
1718
1935
|
const newSub = { ...state.subIndicators };
|
|
1719
1936
|
delete newSub[name];
|
|
1720
1937
|
dispatch({
|
|
@@ -1722,6 +1939,11 @@ function useUndoRedo() {
|
|
|
1722
1939
|
indicators: newSub
|
|
1723
1940
|
});
|
|
1724
1941
|
}
|
|
1942
|
+
if (state.indicatorAxes[id]) {
|
|
1943
|
+
const nextAxes = { ...state.indicatorAxes };
|
|
1944
|
+
delete nextAxes[id];
|
|
1945
|
+
dispatch({ type: "SET_INDICATOR_AXES", axes: nextAxes });
|
|
1946
|
+
}
|
|
1725
1947
|
}
|
|
1726
1948
|
setUndoStack((prev) => [
|
|
1727
1949
|
...prev,
|
|
@@ -1730,7 +1952,9 @@ function useUndoRedo() {
|
|
|
1730
1952
|
data: {
|
|
1731
1953
|
name,
|
|
1732
1954
|
wasActive: !wasActive,
|
|
1733
|
-
isMain
|
|
1955
|
+
isMain,
|
|
1956
|
+
paneId,
|
|
1957
|
+
yAxisId
|
|
1734
1958
|
}
|
|
1735
1959
|
}
|
|
1736
1960
|
]);
|
|
@@ -1740,7 +1964,7 @@ function useUndoRedo() {
|
|
|
1740
1964
|
} finally {
|
|
1741
1965
|
isProcessingRef.current = false;
|
|
1742
1966
|
}
|
|
1743
|
-
}, [redoStack, state.chart, state.mainIndicators, state.subIndicators, dispatch]);
|
|
1967
|
+
}, [redoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
|
|
1744
1968
|
useEffect(() => {
|
|
1745
1969
|
const handleKeyDown = (e) => {
|
|
1746
1970
|
const isCtrlOrMeta = e.ctrlKey || e.metaKey;
|
|
@@ -1814,12 +2038,14 @@ function useLayoutManager() {
|
|
|
1814
2038
|
if (indicatorMap) {
|
|
1815
2039
|
indicatorMap.forEach((paneIndicators, paneId) => {
|
|
1816
2040
|
paneIndicators.forEach((indicator) => {
|
|
2041
|
+
const yAxisId = state.indicatorAxes[indicator.id];
|
|
1817
2042
|
indicators2.push({
|
|
1818
2043
|
paneId,
|
|
1819
2044
|
name: indicator.name,
|
|
1820
2045
|
calcParams: indicator.calcParams,
|
|
1821
2046
|
visible: indicator.visible,
|
|
1822
|
-
...indicator.styles ? { styles: indicator.styles } : {}
|
|
2047
|
+
...indicator.styles ? { styles: indicator.styles } : {},
|
|
2048
|
+
...yAxisId ? { yAxisId } : {}
|
|
1823
2049
|
});
|
|
1824
2050
|
});
|
|
1825
2051
|
});
|
|
@@ -1847,7 +2073,7 @@ function useLayoutManager() {
|
|
|
1847
2073
|
indicators: indicators2,
|
|
1848
2074
|
drawings
|
|
1849
2075
|
};
|
|
1850
|
-
}, [state.chart, state.symbol, state.period]);
|
|
2076
|
+
}, [state.chart, state.symbol, state.period, state.indicatorAxes]);
|
|
1851
2077
|
const saveLayout = useCallback(
|
|
1852
2078
|
(name) => {
|
|
1853
2079
|
const chartState = serializeState();
|
|
@@ -1895,25 +2121,35 @@ function useLayoutManager() {
|
|
|
1895
2121
|
}
|
|
1896
2122
|
const newMainIndicators = [];
|
|
1897
2123
|
const newSubIndicators = {};
|
|
2124
|
+
const restoredAxes = {};
|
|
1898
2125
|
if (chartState.indicators) {
|
|
1899
2126
|
for (const ind of chartState.indicators) {
|
|
2127
|
+
const isMain = ind.paneId === "candle_pane";
|
|
2128
|
+
const id2 = isMain ? `main_${ind.name}` : `sub_${ind.name}`;
|
|
1900
2129
|
chart.createIndicator(
|
|
1901
2130
|
{
|
|
1902
2131
|
name: ind.name,
|
|
2132
|
+
id: id2,
|
|
1903
2133
|
calcParams: ind.calcParams,
|
|
1904
2134
|
visible: ind.visible
|
|
1905
2135
|
},
|
|
1906
|
-
|
|
1907
|
-
|
|
2136
|
+
{
|
|
2137
|
+
isStack: ind.paneId !== "candle_pane",
|
|
2138
|
+
pane: { id: ind.paneId },
|
|
2139
|
+
...ind.yAxisId ? { yAxis: { id: ind.yAxisId } } : {}
|
|
2140
|
+
}
|
|
1908
2141
|
);
|
|
1909
2142
|
if (ind.styles) {
|
|
1910
2143
|
chart.overrideIndicator({
|
|
1911
2144
|
name: ind.name,
|
|
1912
|
-
|
|
2145
|
+
id: id2,
|
|
1913
2146
|
styles: ind.styles
|
|
1914
2147
|
});
|
|
1915
2148
|
}
|
|
1916
|
-
if (ind.
|
|
2149
|
+
if (ind.yAxisId) {
|
|
2150
|
+
restoredAxes[id2] = ind.yAxisId;
|
|
2151
|
+
}
|
|
2152
|
+
if (isMain) {
|
|
1917
2153
|
newMainIndicators.push(ind.name);
|
|
1918
2154
|
} else {
|
|
1919
2155
|
newSubIndicators[ind.name] = ind.paneId;
|
|
@@ -1928,6 +2164,10 @@ function useLayoutManager() {
|
|
|
1928
2164
|
type: "SET_SUB_INDICATORS",
|
|
1929
2165
|
indicators: newSubIndicators
|
|
1930
2166
|
});
|
|
2167
|
+
dispatch({
|
|
2168
|
+
type: "SET_INDICATOR_AXES",
|
|
2169
|
+
axes: restoredAxes
|
|
2170
|
+
});
|
|
1931
2171
|
if (chartState.drawings) {
|
|
1932
2172
|
for (const drawing of chartState.drawings) {
|
|
1933
2173
|
chart.createOverlay({
|
|
@@ -2153,8 +2393,7 @@ ${code}`
|
|
|
2153
2393
|
if (placement === "main") {
|
|
2154
2394
|
paneId = chart.createIndicator(
|
|
2155
2395
|
{ name: indicatorName },
|
|
2156
|
-
true,
|
|
2157
|
-
{ id: "candle_pane" }
|
|
2396
|
+
{ isStack: true, pane: { id: "candle_pane" } }
|
|
2158
2397
|
);
|
|
2159
2398
|
} else {
|
|
2160
2399
|
paneId = chart.createIndicator({ name: indicatorName });
|
|
@@ -2244,6 +2483,749 @@ ${code}`
|
|
|
2244
2483
|
defaultScript: DEFAULT_SCRIPT
|
|
2245
2484
|
};
|
|
2246
2485
|
}
|
|
2486
|
+
function useCrosshair() {
|
|
2487
|
+
const { state } = useKlinechartsUI();
|
|
2488
|
+
const [barData, setBarData] = useState(null);
|
|
2489
|
+
const rafRef = useRef(0);
|
|
2490
|
+
const handler = useCallback(
|
|
2491
|
+
(event) => {
|
|
2492
|
+
if (rafRef.current) {
|
|
2493
|
+
cancelAnimationFrame(rafRef.current);
|
|
2494
|
+
}
|
|
2495
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
2496
|
+
rafRef.current = 0;
|
|
2497
|
+
const klineData = event?.kLineData;
|
|
2498
|
+
if (!klineData) {
|
|
2499
|
+
setBarData(null);
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
const pricePrecision = state.symbol?.pricePrecision ?? 2;
|
|
2503
|
+
const open = klineData.open ?? 0;
|
|
2504
|
+
const close = klineData.close ?? 0;
|
|
2505
|
+
const change = parseFloat((close - open).toFixed(pricePrecision));
|
|
2506
|
+
const changePercent = open !== 0 ? parseFloat((change / open * 100).toFixed(2)) : 0;
|
|
2507
|
+
setBarData({
|
|
2508
|
+
open,
|
|
2509
|
+
high: klineData.high ?? 0,
|
|
2510
|
+
low: klineData.low ?? 0,
|
|
2511
|
+
close,
|
|
2512
|
+
volume: klineData.volume ?? 0,
|
|
2513
|
+
timestamp: klineData.timestamp ?? 0,
|
|
2514
|
+
change,
|
|
2515
|
+
changePercent
|
|
2516
|
+
});
|
|
2517
|
+
});
|
|
2518
|
+
},
|
|
2519
|
+
[state.symbol?.pricePrecision]
|
|
2520
|
+
);
|
|
2521
|
+
useEffect(() => {
|
|
2522
|
+
const chart = state.chart;
|
|
2523
|
+
if (!chart) return;
|
|
2524
|
+
chart.subscribeAction?.("onCrosshairChange", handler);
|
|
2525
|
+
return () => {
|
|
2526
|
+
if (rafRef.current) {
|
|
2527
|
+
cancelAnimationFrame(rafRef.current);
|
|
2528
|
+
rafRef.current = 0;
|
|
2529
|
+
}
|
|
2530
|
+
chart.unsubscribeAction?.("onCrosshairChange", handler);
|
|
2531
|
+
};
|
|
2532
|
+
}, [state.chart, handler]);
|
|
2533
|
+
return { barData };
|
|
2534
|
+
}
|
|
2535
|
+
var alertCounter = 0;
|
|
2536
|
+
function useAlerts() {
|
|
2537
|
+
const { state } = useKlinechartsUI();
|
|
2538
|
+
const [alerts, setAlerts] = useState([]);
|
|
2539
|
+
const callbackRef = useRef(null);
|
|
2540
|
+
const prevCloseRef = useRef(null);
|
|
2541
|
+
const addAlert = useCallback(
|
|
2542
|
+
(price, condition, message) => {
|
|
2543
|
+
const id = `alert_${++alertCounter}`;
|
|
2544
|
+
const alert = {
|
|
2545
|
+
id,
|
|
2546
|
+
price,
|
|
2547
|
+
condition,
|
|
2548
|
+
message,
|
|
2549
|
+
triggered: false
|
|
2550
|
+
};
|
|
2551
|
+
setAlerts((prev) => [...prev, alert]);
|
|
2552
|
+
if (state.chart) {
|
|
2553
|
+
state.chart.createOverlay({
|
|
2554
|
+
name: "horizontalStraightLine",
|
|
2555
|
+
id,
|
|
2556
|
+
groupId: "price_alerts",
|
|
2557
|
+
points: [{ value: price }],
|
|
2558
|
+
styles: {
|
|
2559
|
+
line: {
|
|
2560
|
+
style: "dashed",
|
|
2561
|
+
color: "#ff9800",
|
|
2562
|
+
size: 1
|
|
2563
|
+
}
|
|
2564
|
+
},
|
|
2565
|
+
lock: true
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
return id;
|
|
2569
|
+
},
|
|
2570
|
+
[state.chart]
|
|
2571
|
+
);
|
|
2572
|
+
const removeAlert = useCallback(
|
|
2573
|
+
(id) => {
|
|
2574
|
+
setAlerts((prev) => prev.filter((a) => a.id !== id));
|
|
2575
|
+
state.chart?.removeOverlay({ id });
|
|
2576
|
+
},
|
|
2577
|
+
[state.chart]
|
|
2578
|
+
);
|
|
2579
|
+
const clearAlerts = useCallback(() => {
|
|
2580
|
+
setAlerts((prev) => {
|
|
2581
|
+
for (const alert of prev) {
|
|
2582
|
+
state.chart?.removeOverlay({ id: alert.id });
|
|
2583
|
+
}
|
|
2584
|
+
return [];
|
|
2585
|
+
});
|
|
2586
|
+
}, [state.chart]);
|
|
2587
|
+
const onAlertTriggered = useCallback(
|
|
2588
|
+
(callback) => {
|
|
2589
|
+
callbackRef.current = callback;
|
|
2590
|
+
},
|
|
2591
|
+
[]
|
|
2592
|
+
);
|
|
2593
|
+
useEffect(() => {
|
|
2594
|
+
if (!state.chart) return;
|
|
2595
|
+
const interval = setInterval(() => {
|
|
2596
|
+
const dataList = state.chart?.getDataList();
|
|
2597
|
+
if (!dataList || dataList.length === 0) return;
|
|
2598
|
+
const lastBar = dataList[dataList.length - 1];
|
|
2599
|
+
const currentClose = lastBar.close;
|
|
2600
|
+
const prevClose = prevCloseRef.current;
|
|
2601
|
+
if (prevClose === null) {
|
|
2602
|
+
prevCloseRef.current = currentClose;
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
prevCloseRef.current = currentClose;
|
|
2606
|
+
setAlerts((prev) => {
|
|
2607
|
+
let changed = false;
|
|
2608
|
+
const next = prev.map((alert) => {
|
|
2609
|
+
if (alert.triggered) return alert;
|
|
2610
|
+
let shouldTrigger = false;
|
|
2611
|
+
if (alert.condition === "crossing_up") {
|
|
2612
|
+
shouldTrigger = prevClose < alert.price && currentClose >= alert.price;
|
|
2613
|
+
} else if (alert.condition === "crossing_down") {
|
|
2614
|
+
shouldTrigger = prevClose > alert.price && currentClose <= alert.price;
|
|
2615
|
+
} else if (alert.condition === "crossing") {
|
|
2616
|
+
shouldTrigger = prevClose < alert.price && currentClose >= alert.price || prevClose > alert.price && currentClose <= alert.price;
|
|
2617
|
+
}
|
|
2618
|
+
if (shouldTrigger) {
|
|
2619
|
+
changed = true;
|
|
2620
|
+
const triggered = { ...alert, triggered: true };
|
|
2621
|
+
callbackRef.current?.(triggered);
|
|
2622
|
+
return triggered;
|
|
2623
|
+
}
|
|
2624
|
+
return alert;
|
|
2625
|
+
});
|
|
2626
|
+
return changed ? next : prev;
|
|
2627
|
+
});
|
|
2628
|
+
}, 1e3);
|
|
2629
|
+
return () => clearInterval(interval);
|
|
2630
|
+
}, [state.chart]);
|
|
2631
|
+
return {
|
|
2632
|
+
alerts,
|
|
2633
|
+
addAlert,
|
|
2634
|
+
removeAlert,
|
|
2635
|
+
clearAlerts,
|
|
2636
|
+
onAlertTriggered
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
function useDataExport() {
|
|
2640
|
+
const { state } = useKlinechartsUI();
|
|
2641
|
+
const exportAll = useCallback(
|
|
2642
|
+
(format) => {
|
|
2643
|
+
if (!state.chart) return;
|
|
2644
|
+
const dataList = state.chart.getDataList();
|
|
2645
|
+
if (!dataList || dataList.length === 0) return;
|
|
2646
|
+
downloadData(dataList, format, state.symbol?.ticker);
|
|
2647
|
+
},
|
|
2648
|
+
[state.chart, state.symbol?.ticker]
|
|
2649
|
+
);
|
|
2650
|
+
const exportVisible = useCallback(
|
|
2651
|
+
(format) => {
|
|
2652
|
+
if (!state.chart) return;
|
|
2653
|
+
const dataList = state.chart.getDataList();
|
|
2654
|
+
if (!dataList || dataList.length === 0) return;
|
|
2655
|
+
const visibleRange = state.chart.getVisibleRange?.();
|
|
2656
|
+
if (!visibleRange) {
|
|
2657
|
+
downloadData(dataList, format, state.symbol?.ticker);
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
const { from, to } = visibleRange;
|
|
2661
|
+
const visibleData = dataList.slice(from, to);
|
|
2662
|
+
downloadData(visibleData, format, state.symbol?.ticker);
|
|
2663
|
+
},
|
|
2664
|
+
[state.chart, state.symbol?.ticker]
|
|
2665
|
+
);
|
|
2666
|
+
return { exportAll, exportVisible };
|
|
2667
|
+
}
|
|
2668
|
+
function formatRow(bar) {
|
|
2669
|
+
return {
|
|
2670
|
+
date: new Date(bar.timestamp).toISOString(),
|
|
2671
|
+
open: bar.open,
|
|
2672
|
+
high: bar.high,
|
|
2673
|
+
low: bar.low,
|
|
2674
|
+
close: bar.close,
|
|
2675
|
+
volume: bar.volume ?? 0
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
function buildCsv(dataList) {
|
|
2679
|
+
const header = "Date,Open,High,Low,Close,Volume\n";
|
|
2680
|
+
const rows = dataList.map((bar) => {
|
|
2681
|
+
const r = formatRow(bar);
|
|
2682
|
+
return `${r.date},${r.open},${r.high},${r.low},${r.close},${r.volume}`;
|
|
2683
|
+
});
|
|
2684
|
+
return header + rows.join("\n");
|
|
2685
|
+
}
|
|
2686
|
+
function buildJson(dataList) {
|
|
2687
|
+
return JSON.stringify(dataList.map(formatRow), null, 2);
|
|
2688
|
+
}
|
|
2689
|
+
function downloadData(dataList, format, ticker) {
|
|
2690
|
+
const content = format === "csv" ? buildCsv(dataList) : buildJson(dataList);
|
|
2691
|
+
const mimeType = format === "csv" ? "text/csv" : "application/json";
|
|
2692
|
+
const blob = new Blob([content], { type: mimeType });
|
|
2693
|
+
const url = URL.createObjectURL(blob);
|
|
2694
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2695
|
+
const prefix = ticker ?? "chart";
|
|
2696
|
+
const filename = `${prefix}_${date}.${format}`;
|
|
2697
|
+
const link = document.createElement("a");
|
|
2698
|
+
link.href = url;
|
|
2699
|
+
link.download = filename;
|
|
2700
|
+
document.body.appendChild(link);
|
|
2701
|
+
link.click();
|
|
2702
|
+
document.body.removeChild(link);
|
|
2703
|
+
URL.revokeObjectURL(url);
|
|
2704
|
+
}
|
|
2705
|
+
function useWatchlist() {
|
|
2706
|
+
const { state } = useKlinechartsUI();
|
|
2707
|
+
const { dispatch, datafeed } = useKlinechartsUIDispatch();
|
|
2708
|
+
const [items, setItems] = useState([]);
|
|
2709
|
+
const subscriptionsRef = useRef(/* @__PURE__ */ new Map());
|
|
2710
|
+
const activeSymbol = state.symbol?.ticker ?? null;
|
|
2711
|
+
const addSymbol = useCallback(
|
|
2712
|
+
(ticker) => {
|
|
2713
|
+
setItems((prev) => {
|
|
2714
|
+
if (prev.some((item) => item.ticker === ticker)) return prev;
|
|
2715
|
+
return [
|
|
2716
|
+
...prev,
|
|
2717
|
+
{ ticker, lastPrice: null, change: null, changePercent: null }
|
|
2718
|
+
];
|
|
2719
|
+
});
|
|
2720
|
+
if (subscriptionsRef.current.has(ticker)) return;
|
|
2721
|
+
const symbolInfo = { ticker };
|
|
2722
|
+
const period = state.period;
|
|
2723
|
+
subscriptionsRef.current.set(ticker, { symbolInfo, period });
|
|
2724
|
+
datafeed.subscribe(symbolInfo, period, (bar) => {
|
|
2725
|
+
setItems(
|
|
2726
|
+
(prev) => prev.map((item) => {
|
|
2727
|
+
if (item.ticker !== ticker) return item;
|
|
2728
|
+
const change = bar.close - bar.open;
|
|
2729
|
+
const changePercent = bar.open !== 0 ? change / bar.open * 100 : null;
|
|
2730
|
+
return {
|
|
2731
|
+
...item,
|
|
2732
|
+
lastPrice: bar.close,
|
|
2733
|
+
change,
|
|
2734
|
+
changePercent
|
|
2735
|
+
};
|
|
2736
|
+
})
|
|
2737
|
+
);
|
|
2738
|
+
});
|
|
2739
|
+
},
|
|
2740
|
+
[datafeed, state.period]
|
|
2741
|
+
);
|
|
2742
|
+
const removeSymbol = useCallback(
|
|
2743
|
+
(ticker) => {
|
|
2744
|
+
const sub = subscriptionsRef.current.get(ticker);
|
|
2745
|
+
if (sub) {
|
|
2746
|
+
datafeed.unsubscribe(sub.symbolInfo, sub.period);
|
|
2747
|
+
subscriptionsRef.current.delete(ticker);
|
|
2748
|
+
}
|
|
2749
|
+
setItems((prev) => prev.filter((item) => item.ticker !== ticker));
|
|
2750
|
+
},
|
|
2751
|
+
[datafeed]
|
|
2752
|
+
);
|
|
2753
|
+
const switchSymbol = useCallback(
|
|
2754
|
+
(ticker) => {
|
|
2755
|
+
dispatch({ type: "SET_SYMBOL", symbol: { ticker } });
|
|
2756
|
+
},
|
|
2757
|
+
[dispatch]
|
|
2758
|
+
);
|
|
2759
|
+
useEffect(() => {
|
|
2760
|
+
return () => {
|
|
2761
|
+
subscriptionsRef.current.forEach((sub) => {
|
|
2762
|
+
datafeed.unsubscribe(sub.symbolInfo, sub.period);
|
|
2763
|
+
});
|
|
2764
|
+
subscriptionsRef.current.clear();
|
|
2765
|
+
};
|
|
2766
|
+
}, [datafeed]);
|
|
2767
|
+
return {
|
|
2768
|
+
items,
|
|
2769
|
+
addSymbol,
|
|
2770
|
+
removeSymbol,
|
|
2771
|
+
switchSymbol,
|
|
2772
|
+
activeSymbol
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
function useReplay() {
|
|
2776
|
+
const { state } = useKlinechartsUI();
|
|
2777
|
+
const [isReplaying, setIsReplaying] = useState(false);
|
|
2778
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
2779
|
+
const [speed, setSpeedState] = useState(1);
|
|
2780
|
+
const [barIndex, setBarIndex] = useState(0);
|
|
2781
|
+
const [totalBars, setTotalBars] = useState(0);
|
|
2782
|
+
const intervalRef = useRef(null);
|
|
2783
|
+
const savedDataRef = useRef([]);
|
|
2784
|
+
const currentIndexRef = useRef(0);
|
|
2785
|
+
const clearInterval_ = useCallback(() => {
|
|
2786
|
+
if (intervalRef.current !== null) {
|
|
2787
|
+
clearInterval(intervalRef.current);
|
|
2788
|
+
intervalRef.current = null;
|
|
2789
|
+
}
|
|
2790
|
+
}, []);
|
|
2791
|
+
const addNextBar = useCallback(() => {
|
|
2792
|
+
if (!state.chart) return;
|
|
2793
|
+
const data = savedDataRef.current;
|
|
2794
|
+
const idx = currentIndexRef.current;
|
|
2795
|
+
if (idx >= data.length) {
|
|
2796
|
+
clearInterval_();
|
|
2797
|
+
setIsPaused(true);
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
const bar = data[idx];
|
|
2801
|
+
state.chart.updateData?.(bar);
|
|
2802
|
+
currentIndexRef.current = idx + 1;
|
|
2803
|
+
setBarIndex(idx + 1);
|
|
2804
|
+
}, [state.chart, clearInterval_]);
|
|
2805
|
+
const startInterval = useCallback(
|
|
2806
|
+
(currentSpeed) => {
|
|
2807
|
+
clearInterval_();
|
|
2808
|
+
intervalRef.current = setInterval(addNextBar, 1e3 / currentSpeed);
|
|
2809
|
+
},
|
|
2810
|
+
[addNextBar, clearInterval_]
|
|
2811
|
+
);
|
|
2812
|
+
const startReplay = useCallback(() => {
|
|
2813
|
+
if (!state.chart) return;
|
|
2814
|
+
const dataList = state.chart.getDataList();
|
|
2815
|
+
if (!dataList || dataList.length === 0) return;
|
|
2816
|
+
savedDataRef.current = [...dataList];
|
|
2817
|
+
currentIndexRef.current = 0;
|
|
2818
|
+
setTotalBars(dataList.length);
|
|
2819
|
+
setBarIndex(0);
|
|
2820
|
+
setIsReplaying(true);
|
|
2821
|
+
setIsPaused(false);
|
|
2822
|
+
state.chart.clearData?.();
|
|
2823
|
+
startInterval(speed);
|
|
2824
|
+
}, [state.chart, speed, startInterval]);
|
|
2825
|
+
const stopReplay = useCallback(() => {
|
|
2826
|
+
if (!state.chart) return;
|
|
2827
|
+
clearInterval_();
|
|
2828
|
+
const data = savedDataRef.current;
|
|
2829
|
+
if (data.length > 0) {
|
|
2830
|
+
state.chart.clearData?.();
|
|
2831
|
+
for (const bar of data) {
|
|
2832
|
+
state.chart.updateData?.(bar);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
savedDataRef.current = [];
|
|
2836
|
+
currentIndexRef.current = 0;
|
|
2837
|
+
setIsReplaying(false);
|
|
2838
|
+
setIsPaused(false);
|
|
2839
|
+
setBarIndex(0);
|
|
2840
|
+
setTotalBars(0);
|
|
2841
|
+
}, [state.chart, clearInterval_]);
|
|
2842
|
+
const togglePause = useCallback(() => {
|
|
2843
|
+
if (!isReplaying) return;
|
|
2844
|
+
if (isPaused) {
|
|
2845
|
+
startInterval(speed);
|
|
2846
|
+
setIsPaused(false);
|
|
2847
|
+
} else {
|
|
2848
|
+
clearInterval_();
|
|
2849
|
+
setIsPaused(true);
|
|
2850
|
+
}
|
|
2851
|
+
}, [isReplaying, isPaused, speed, startInterval, clearInterval_]);
|
|
2852
|
+
const stepForward = useCallback(() => {
|
|
2853
|
+
if (!isReplaying || !isPaused) return;
|
|
2854
|
+
addNextBar();
|
|
2855
|
+
}, [isReplaying, isPaused, addNextBar]);
|
|
2856
|
+
const stepBackward = useCallback(() => {
|
|
2857
|
+
if (!isReplaying || !isPaused || !state.chart) return;
|
|
2858
|
+
const idx = currentIndexRef.current;
|
|
2859
|
+
if (idx <= 1) return;
|
|
2860
|
+
const data = savedDataRef.current;
|
|
2861
|
+
state.chart.clearData?.();
|
|
2862
|
+
for (let i = 0; i < idx - 1; i++) {
|
|
2863
|
+
state.chart.updateData?.(data[i]);
|
|
2864
|
+
}
|
|
2865
|
+
currentIndexRef.current = idx - 1;
|
|
2866
|
+
setBarIndex(idx - 1);
|
|
2867
|
+
}, [isReplaying, isPaused, state.chart]);
|
|
2868
|
+
const seekTo = useCallback(
|
|
2869
|
+
(targetIndex) => {
|
|
2870
|
+
if (!isReplaying || !state.chart) return;
|
|
2871
|
+
const data = savedDataRef.current;
|
|
2872
|
+
const clamped = Math.max(0, Math.min(targetIndex, data.length));
|
|
2873
|
+
clearInterval_();
|
|
2874
|
+
setIsPaused(true);
|
|
2875
|
+
state.chart.clearData?.();
|
|
2876
|
+
for (let i = 0; i < clamped; i++) {
|
|
2877
|
+
state.chart.updateData?.(data[i]);
|
|
2878
|
+
}
|
|
2879
|
+
currentIndexRef.current = clamped;
|
|
2880
|
+
setBarIndex(clamped);
|
|
2881
|
+
},
|
|
2882
|
+
[isReplaying, state.chart, clearInterval_]
|
|
2883
|
+
);
|
|
2884
|
+
const setSpeed = useCallback(
|
|
2885
|
+
(newSpeed) => {
|
|
2886
|
+
setSpeedState(newSpeed);
|
|
2887
|
+
if (isReplaying && !isPaused) {
|
|
2888
|
+
startInterval(newSpeed);
|
|
2889
|
+
}
|
|
2890
|
+
},
|
|
2891
|
+
[isReplaying, isPaused, startInterval]
|
|
2892
|
+
);
|
|
2893
|
+
useEffect(() => {
|
|
2894
|
+
return () => {
|
|
2895
|
+
clearInterval_();
|
|
2896
|
+
};
|
|
2897
|
+
}, [clearInterval_]);
|
|
2898
|
+
return {
|
|
2899
|
+
isReplaying,
|
|
2900
|
+
isPaused,
|
|
2901
|
+
speed,
|
|
2902
|
+
barIndex,
|
|
2903
|
+
totalBars,
|
|
2904
|
+
startReplay,
|
|
2905
|
+
stopReplay,
|
|
2906
|
+
togglePause,
|
|
2907
|
+
stepForward,
|
|
2908
|
+
stepBackward,
|
|
2909
|
+
seekTo,
|
|
2910
|
+
setSpeed
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
var DEFAULT_COLORS = [
|
|
2914
|
+
"#2196f3",
|
|
2915
|
+
"#ff9800",
|
|
2916
|
+
"#4caf50",
|
|
2917
|
+
"#e91e63",
|
|
2918
|
+
"#9c27b0",
|
|
2919
|
+
"#00bcd4",
|
|
2920
|
+
"#ff5722",
|
|
2921
|
+
"#8bc34a"
|
|
2922
|
+
];
|
|
2923
|
+
var colorIndex = 0;
|
|
2924
|
+
var compareCounter = 0;
|
|
2925
|
+
function useCompare() {
|
|
2926
|
+
const { state, datafeed } = useKlinechartsUI();
|
|
2927
|
+
const [symbols, setSymbols] = useState([]);
|
|
2928
|
+
const indicatorsRef = useRef(/* @__PURE__ */ new Map());
|
|
2929
|
+
const addSymbol = useCallback(
|
|
2930
|
+
async (ticker, color) => {
|
|
2931
|
+
if (!state.chart || !datafeed) return;
|
|
2932
|
+
if (indicatorsRef.current.has(ticker)) return;
|
|
2933
|
+
const assignedColor = color ?? DEFAULT_COLORS[colorIndex++ % DEFAULT_COLORS.length];
|
|
2934
|
+
const mainDataList = state.chart.getDataList();
|
|
2935
|
+
if (!mainDataList || mainDataList.length === 0) return;
|
|
2936
|
+
const from = mainDataList[0].timestamp;
|
|
2937
|
+
const to = mainDataList[mainDataList.length - 1].timestamp;
|
|
2938
|
+
const compareData = await datafeed.getHistoryKLineData(
|
|
2939
|
+
{ ticker },
|
|
2940
|
+
{ ...state.period, label: "" },
|
|
2941
|
+
from,
|
|
2942
|
+
to
|
|
2943
|
+
);
|
|
2944
|
+
if (!compareData || compareData.length === 0) return;
|
|
2945
|
+
const compareMap = /* @__PURE__ */ new Map();
|
|
2946
|
+
for (const bar of compareData) {
|
|
2947
|
+
compareMap.set(bar.timestamp, bar.close);
|
|
2948
|
+
}
|
|
2949
|
+
let basePrice = null;
|
|
2950
|
+
for (const bar of mainDataList) {
|
|
2951
|
+
const close = compareMap.get(bar.timestamp);
|
|
2952
|
+
if (close != null) {
|
|
2953
|
+
basePrice = close;
|
|
2954
|
+
break;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
if (basePrice === null || basePrice === 0) return;
|
|
2958
|
+
const bp = basePrice;
|
|
2959
|
+
const normalizedData = mainDataList.map((mainBar) => {
|
|
2960
|
+
const close = compareMap.get(mainBar.timestamp);
|
|
2961
|
+
return {
|
|
2962
|
+
pct: close != null ? (close - bp) / bp * 100 : NaN
|
|
2963
|
+
};
|
|
2964
|
+
});
|
|
2965
|
+
const indicatorName = `__cmp_${++compareCounter}_${ticker}`;
|
|
2966
|
+
registerIndicator({
|
|
2967
|
+
name: indicatorName,
|
|
2968
|
+
shortName: ticker,
|
|
2969
|
+
figures: [
|
|
2970
|
+
{
|
|
2971
|
+
key: "pct",
|
|
2972
|
+
title: `${ticker} %: `,
|
|
2973
|
+
type: "line",
|
|
2974
|
+
styles: () => ({ color: assignedColor })
|
|
2975
|
+
}
|
|
2976
|
+
],
|
|
2977
|
+
calc: () => normalizedData
|
|
2978
|
+
});
|
|
2979
|
+
const paneId = state.chart.createIndicator(
|
|
2980
|
+
{ name: indicatorName },
|
|
2981
|
+
{ isStack: true, pane: { id: "candle_pane" } }
|
|
2982
|
+
);
|
|
2983
|
+
indicatorsRef.current.set(ticker, {
|
|
2984
|
+
name: indicatorName,
|
|
2985
|
+
paneId: typeof paneId === "string" ? paneId : "candle_pane"
|
|
2986
|
+
});
|
|
2987
|
+
setSymbols((prev) => {
|
|
2988
|
+
if (prev.some((s) => s.ticker === ticker)) return prev;
|
|
2989
|
+
return [
|
|
2990
|
+
...prev,
|
|
2991
|
+
{ ticker, basePrice: bp, color: assignedColor, visible: true }
|
|
2992
|
+
];
|
|
2993
|
+
});
|
|
2994
|
+
},
|
|
2995
|
+
[state.chart, state.period, datafeed]
|
|
2996
|
+
);
|
|
2997
|
+
const removeSymbol = useCallback(
|
|
2998
|
+
(ticker) => {
|
|
2999
|
+
const info = indicatorsRef.current.get(ticker);
|
|
3000
|
+
if (info && state.chart) {
|
|
3001
|
+
try {
|
|
3002
|
+
state.chart.removeIndicator({ name: info.name });
|
|
3003
|
+
} catch {
|
|
3004
|
+
}
|
|
3005
|
+
indicatorsRef.current.delete(ticker);
|
|
3006
|
+
}
|
|
3007
|
+
setSymbols((prev) => prev.filter((s) => s.ticker !== ticker));
|
|
3008
|
+
},
|
|
3009
|
+
[state.chart]
|
|
3010
|
+
);
|
|
3011
|
+
const toggleSymbol = useCallback(
|
|
3012
|
+
(ticker) => {
|
|
3013
|
+
const info = indicatorsRef.current.get(ticker);
|
|
3014
|
+
if (!info || !state.chart) return;
|
|
3015
|
+
setSymbols((prev) => {
|
|
3016
|
+
const sym = prev.find((s) => s.ticker === ticker);
|
|
3017
|
+
if (!sym) return prev;
|
|
3018
|
+
const newVisible = !sym.visible;
|
|
3019
|
+
state.chart?.overrideIndicator({
|
|
3020
|
+
name: info.name,
|
|
3021
|
+
visible: newVisible
|
|
3022
|
+
});
|
|
3023
|
+
return prev.map(
|
|
3024
|
+
(s) => s.ticker === ticker ? { ...s, visible: newVisible } : s
|
|
3025
|
+
);
|
|
3026
|
+
});
|
|
3027
|
+
},
|
|
3028
|
+
[state.chart]
|
|
3029
|
+
);
|
|
3030
|
+
const clearAll = useCallback(() => {
|
|
3031
|
+
indicatorsRef.current.forEach((info) => {
|
|
3032
|
+
try {
|
|
3033
|
+
state.chart?.removeIndicator({ name: info.name });
|
|
3034
|
+
} catch {
|
|
3035
|
+
}
|
|
3036
|
+
});
|
|
3037
|
+
indicatorsRef.current.clear();
|
|
3038
|
+
setSymbols([]);
|
|
3039
|
+
}, [state.chart]);
|
|
3040
|
+
useEffect(() => {
|
|
3041
|
+
return () => {
|
|
3042
|
+
indicatorsRef.current.forEach((info) => {
|
|
3043
|
+
try {
|
|
3044
|
+
state.chart?.removeIndicator({ name: info.name });
|
|
3045
|
+
} catch {
|
|
3046
|
+
}
|
|
3047
|
+
});
|
|
3048
|
+
indicatorsRef.current.clear();
|
|
3049
|
+
};
|
|
3050
|
+
}, [state.chart]);
|
|
3051
|
+
return { symbols, addSymbol, removeSymbol, toggleSymbol, clearAll };
|
|
3052
|
+
}
|
|
3053
|
+
var MEASURE_OVERLAY_ID = "__measure_line__";
|
|
3054
|
+
function useMeasure() {
|
|
3055
|
+
const { state } = useKlinechartsUI();
|
|
3056
|
+
const [isActive, setIsActive] = useState(false);
|
|
3057
|
+
const [fromPoint, setFromPoint] = useState(null);
|
|
3058
|
+
const [result, setResult] = useState(null);
|
|
3059
|
+
const clickCountRef = useRef(0);
|
|
3060
|
+
const computeResult = useCallback(
|
|
3061
|
+
(from, to) => {
|
|
3062
|
+
const priceDiff = to.price - from.price;
|
|
3063
|
+
const pricePercent = from.price !== 0 ? priceDiff / from.price * 100 : 0;
|
|
3064
|
+
const bars = Math.abs(to.barIndex - from.barIndex);
|
|
3065
|
+
const timeDiff = Math.abs(to.timestamp - from.timestamp);
|
|
3066
|
+
return { from, to, priceDiff, pricePercent, bars, timeDiff };
|
|
3067
|
+
},
|
|
3068
|
+
[]
|
|
3069
|
+
);
|
|
3070
|
+
const cleanup = useCallback(() => {
|
|
3071
|
+
state.chart?.removeOverlay({ id: MEASURE_OVERLAY_ID });
|
|
3072
|
+
}, [state.chart]);
|
|
3073
|
+
const startMeasure = useCallback(() => {
|
|
3074
|
+
cleanup();
|
|
3075
|
+
setIsActive(true);
|
|
3076
|
+
setFromPoint(null);
|
|
3077
|
+
setResult(null);
|
|
3078
|
+
clickCountRef.current = 0;
|
|
3079
|
+
if (!state.chart) return;
|
|
3080
|
+
state.chart.createOverlay({
|
|
3081
|
+
name: "segment",
|
|
3082
|
+
id: MEASURE_OVERLAY_ID,
|
|
3083
|
+
groupId: "measure",
|
|
3084
|
+
styles: {
|
|
3085
|
+
line: {
|
|
3086
|
+
style: "dashed",
|
|
3087
|
+
color: "#FFD600",
|
|
3088
|
+
size: 1
|
|
3089
|
+
}
|
|
3090
|
+
},
|
|
3091
|
+
lock: true,
|
|
3092
|
+
onDrawEnd: (event) => {
|
|
3093
|
+
const overlay = event?.overlay;
|
|
3094
|
+
const points = overlay?.points;
|
|
3095
|
+
if (!points || points.length < 2) return;
|
|
3096
|
+
const chart = state.chart;
|
|
3097
|
+
if (!chart) return;
|
|
3098
|
+
const dataList = chart.getDataList();
|
|
3099
|
+
if (!dataList || dataList.length === 0) return;
|
|
3100
|
+
const makePoint = (p) => {
|
|
3101
|
+
const idx = Math.max(
|
|
3102
|
+
0,
|
|
3103
|
+
Math.min(p.dataIndex ?? 0, dataList.length - 1)
|
|
3104
|
+
);
|
|
3105
|
+
const bar = dataList[idx];
|
|
3106
|
+
return {
|
|
3107
|
+
price: p.value ?? bar?.close ?? 0,
|
|
3108
|
+
timestamp: bar?.timestamp ?? 0,
|
|
3109
|
+
barIndex: idx
|
|
3110
|
+
};
|
|
3111
|
+
};
|
|
3112
|
+
const from = makePoint(points[0]);
|
|
3113
|
+
const to = makePoint(points[1]);
|
|
3114
|
+
setFromPoint(from);
|
|
3115
|
+
setResult(computeResult(from, to));
|
|
3116
|
+
setIsActive(false);
|
|
3117
|
+
}
|
|
3118
|
+
});
|
|
3119
|
+
}, [state.chart, cleanup, computeResult]);
|
|
3120
|
+
const cancelMeasure = useCallback(() => {
|
|
3121
|
+
cleanup();
|
|
3122
|
+
setIsActive(false);
|
|
3123
|
+
setFromPoint(null);
|
|
3124
|
+
clickCountRef.current = 0;
|
|
3125
|
+
}, [cleanup]);
|
|
3126
|
+
const clearResult = useCallback(() => {
|
|
3127
|
+
cleanup();
|
|
3128
|
+
setResult(null);
|
|
3129
|
+
setFromPoint(null);
|
|
3130
|
+
}, [cleanup]);
|
|
3131
|
+
useEffect(() => {
|
|
3132
|
+
return () => {
|
|
3133
|
+
state.chart?.removeOverlay({ id: MEASURE_OVERLAY_ID });
|
|
3134
|
+
};
|
|
3135
|
+
}, [state.chart]);
|
|
3136
|
+
return {
|
|
3137
|
+
isActive,
|
|
3138
|
+
startMeasure,
|
|
3139
|
+
cancelMeasure,
|
|
3140
|
+
result,
|
|
3141
|
+
clearResult,
|
|
3142
|
+
fromPoint
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
var annotationCounter = 0;
|
|
3146
|
+
function useAnnotations() {
|
|
3147
|
+
const { state } = useKlinechartsUI();
|
|
3148
|
+
const [annotations, setAnnotations] = useState([]);
|
|
3149
|
+
const addAnnotation = useCallback(
|
|
3150
|
+
(text, price, timestamp, color) => {
|
|
3151
|
+
const id = `annotation_${++annotationCounter}`;
|
|
3152
|
+
const annotation = {
|
|
3153
|
+
id,
|
|
3154
|
+
text,
|
|
3155
|
+
price,
|
|
3156
|
+
timestamp,
|
|
3157
|
+
color
|
|
3158
|
+
};
|
|
3159
|
+
setAnnotations((prev) => [...prev, annotation]);
|
|
3160
|
+
if (state.chart) {
|
|
3161
|
+
state.chart.createOverlay({
|
|
3162
|
+
name: "simpleAnnotation",
|
|
3163
|
+
id,
|
|
3164
|
+
groupId: "annotations",
|
|
3165
|
+
points: [{ timestamp, value: price }],
|
|
3166
|
+
extendData: text,
|
|
3167
|
+
styles: color ? {
|
|
3168
|
+
text: {
|
|
3169
|
+
color
|
|
3170
|
+
}
|
|
3171
|
+
} : void 0,
|
|
3172
|
+
lock: true
|
|
3173
|
+
});
|
|
3174
|
+
}
|
|
3175
|
+
return id;
|
|
3176
|
+
},
|
|
3177
|
+
[state.chart]
|
|
3178
|
+
);
|
|
3179
|
+
const removeAnnotation = useCallback(
|
|
3180
|
+
(id) => {
|
|
3181
|
+
setAnnotations((prev) => prev.filter((a) => a.id !== id));
|
|
3182
|
+
state.chart?.removeOverlay({ id });
|
|
3183
|
+
},
|
|
3184
|
+
[state.chart]
|
|
3185
|
+
);
|
|
3186
|
+
const updateAnnotation = useCallback(
|
|
3187
|
+
(id, updates) => {
|
|
3188
|
+
setAnnotations(
|
|
3189
|
+
(prev) => prev.map((a) => a.id === id ? { ...a, ...updates } : a)
|
|
3190
|
+
);
|
|
3191
|
+
if (state.chart) {
|
|
3192
|
+
const overrideData = { id };
|
|
3193
|
+
if (updates.text !== void 0) {
|
|
3194
|
+
overrideData.extendData = updates.text;
|
|
3195
|
+
}
|
|
3196
|
+
if (updates.color !== void 0) {
|
|
3197
|
+
overrideData.styles = {
|
|
3198
|
+
text: {
|
|
3199
|
+
color: updates.color
|
|
3200
|
+
}
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
state.chart.overrideOverlay(overrideData);
|
|
3204
|
+
}
|
|
3205
|
+
},
|
|
3206
|
+
[state.chart]
|
|
3207
|
+
);
|
|
3208
|
+
const clearAnnotations = useCallback(() => {
|
|
3209
|
+
setAnnotations((prev) => {
|
|
3210
|
+
for (const annotation of prev) {
|
|
3211
|
+
state.chart?.removeOverlay({ id: annotation.id });
|
|
3212
|
+
}
|
|
3213
|
+
return [];
|
|
3214
|
+
});
|
|
3215
|
+
}, [state.chart]);
|
|
3216
|
+
useEffect(() => {
|
|
3217
|
+
return () => {
|
|
3218
|
+
state.chart?.removeOverlay({ groupId: "annotations" });
|
|
3219
|
+
};
|
|
3220
|
+
}, [state.chart]);
|
|
3221
|
+
return {
|
|
3222
|
+
annotations,
|
|
3223
|
+
addAnnotation,
|
|
3224
|
+
removeAnnotation,
|
|
3225
|
+
updateAnnotation,
|
|
3226
|
+
clearAnnotations
|
|
3227
|
+
};
|
|
3228
|
+
}
|
|
2247
3229
|
|
|
2248
3230
|
// src/utils/createDataLoader.ts
|
|
2249
3231
|
function createDataLoader(datafeed, dispatch) {
|
|
@@ -2309,6 +3291,6 @@ function createDataLoader(datafeed, dispatch) {
|
|
|
2309
3291
|
};
|
|
2310
3292
|
}
|
|
2311
3293
|
|
|
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 };
|
|
3294
|
+
export { CANDLE_TYPES, COMPARE_RULES, DEFAULT_PERIODS, DRAWING_CATEGORIES, INDICATOR_PARAMS, KlinechartsUIDispatchContext, KlinechartsUIProvider, KlinechartsUIStateContext, MAIN_INDICATORS, PRICE_AXIS_TYPES, SUB_INDICATORS, TIMEZONES, TOOLTIP_SHOW_RULES, YAXIS_POSITIONS, createDataLoader, useAlerts, useAnnotations, useCompare, useCrosshair, useDataExport, useDrawingTools, useFullscreen, useIndicators, useKlinechartsUI, useKlinechartsUILoading, useKlinechartsUISettings, useKlinechartsUITheme, useLayoutManager, useMeasure, useOrderLines, usePeriods, useReplay, useScreenshot, useScriptEditor, useSymbolSearch, useTimezone, useUndoRedo, useWatchlist };
|
|
2313
3295
|
//# sourceMappingURL=index.js.map
|
|
2314
3296
|
//# sourceMappingURL=index.js.map
|