viainti-chart 1.0.3 → 1.0.6
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 +6 -2
- package/dist/index.cjs +246 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +246 -92
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -140,8 +140,20 @@ function BsArrowRepeat (props) {
|
|
|
140
140
|
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8"},"child":[]}]})(props);
|
|
141
141
|
}function BsDiagram3 (props) {
|
|
142
142
|
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M6 3.5A1.5 1.5 0 0 1 7.5 2h1A1.5 1.5 0 0 1 10 3.5v1A1.5 1.5 0 0 1 8.5 6v1H14a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0v-1A.5.5 0 0 1 2 7h5.5V6A1.5 1.5 0 0 1 6 4.5zM8.5 5a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5zM0 11.5A1.5 1.5 0 0 1 1.5 10h1A1.5 1.5 0 0 1 4 11.5v1A1.5 1.5 0 0 1 2.5 14h-1A1.5 1.5 0 0 1 0 12.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm4.5.5A1.5 1.5 0 0 1 7.5 10h1a1.5 1.5 0 0 1 1.5 1.5v1A1.5 1.5 0 0 1 8.5 14h-1A1.5 1.5 0 0 1 6 12.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm4.5.5a1.5 1.5 0 0 1 1.5-1.5h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5z"},"child":[]}]})(props);
|
|
143
|
+
}function BsEmojiAngry (props) {
|
|
144
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"},"child":[]},{"tag":"path","attr":{"d":"M4.285 12.433a.5.5 0 0 0 .683-.183A3.5 3.5 0 0 1 8 10.5c1.295 0 2.426.703 3.032 1.75a.5.5 0 0 0 .866-.5A4.5 4.5 0 0 0 8 9.5a4.5 4.5 0 0 0-3.898 2.25.5.5 0 0 0 .183.683m6.991-8.38a.5.5 0 1 1 .448.894l-1.009.504c.176.27.285.64.285 1.049 0 .828-.448 1.5-1 1.5s-1-.672-1-1.5c0-.247.04-.48.11-.686a.502.502 0 0 1 .166-.761zm-6.552 0a.5.5 0 0 0-.448.894l1.009.504A1.94 1.94 0 0 0 5 6.5C5 7.328 5.448 8 6 8s1-.672 1-1.5c0-.247-.04-.48-.11-.686a.502.502 0 0 0-.166-.761z"},"child":[]}]})(props);
|
|
145
|
+
}function BsEmojiDizzy (props) {
|
|
146
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"},"child":[]},{"tag":"path","attr":{"d":"M9.146 5.146a.5.5 0 0 1 .708 0l.646.647.646-.647a.5.5 0 0 1 .708.708l-.647.646.647.646a.5.5 0 0 1-.708.708l-.646-.647-.646.647a.5.5 0 1 1-.708-.708l.647-.646-.647-.646a.5.5 0 0 1 0-.708m-5 0a.5.5 0 0 1 .708 0l.646.647.646-.647a.5.5 0 1 1 .708.708l-.647.646.647.646a.5.5 0 1 1-.708.708L5.5 7.207l-.646.647a.5.5 0 1 1-.708-.708l.647-.646-.647-.646a.5.5 0 0 1 0-.708M10 11a2 2 0 1 1-4 0 2 2 0 0 1 4 0"},"child":[]}]})(props);
|
|
147
|
+
}function BsEmojiHeartEyes (props) {
|
|
148
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"},"child":[]},{"tag":"path","attr":{"d":"M11.315 10.014a.5.5 0 0 1 .548.736A4.5 4.5 0 0 1 7.965 13a4.5 4.5 0 0 1-3.898-2.25.5.5 0 0 1 .548-.736h.005l.017.005.067.015.252.055c.215.046.515.108.857.169.693.124 1.522.242 2.152.242s1.46-.118 2.152-.242a27 27 0 0 0 1.109-.224l.067-.015.017-.004.005-.002zM4.756 4.566c.763-1.424 4.02-.12.952 3.434-4.496-1.596-2.35-4.298-.952-3.434m6.488 0c1.398-.864 3.544 1.838-.952 3.434-3.067-3.554.19-4.858.952-3.434"},"child":[]}]})(props);
|
|
149
|
+
}function BsEmojiLaughing (props) {
|
|
150
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"},"child":[]},{"tag":"path","attr":{"d":"M12.331 9.5a1 1 0 0 1 0 1A5 5 0 0 1 8 13a5 5 0 0 1-4.33-2.5A1 1 0 0 1 4.535 9h6.93a1 1 0 0 1 .866.5M7 6.5c0 .828-.448 0-1 0s-1 .828-1 0S5.448 5 6 5s1 .672 1 1.5m4 0c0 .828-.448 0-1 0s-1 .828-1 0S9.448 5 10 5s1 .672 1 1.5"},"child":[]}]})(props);
|
|
151
|
+
}function BsEmojiNeutral (props) {
|
|
152
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"},"child":[]},{"tag":"path","attr":{"d":"M4 10.5a.5.5 0 0 0 .5.5h7a.5.5 0 0 0 0-1h-7a.5.5 0 0 0-.5.5m3-4C7 5.672 6.552 5 6 5s-1 .672-1 1.5S5.448 8 6 8s1-.672 1-1.5m4 0c0-.828-.448-1.5-1-1.5s-1 .672-1 1.5S9.448 8 10 8s1-.672 1-1.5"},"child":[]}]})(props);
|
|
143
153
|
}function BsEmojiSmile (props) {
|
|
144
154
|
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"},"child":[]},{"tag":"path","attr":{"d":"M4.285 9.567a.5.5 0 0 1 .683.183A3.5 3.5 0 0 0 8 11.5a3.5 3.5 0 0 0 3.032-1.75.5.5 0 1 1 .866.5A4.5 4.5 0 0 1 8 12.5a4.5 4.5 0 0 1-3.898-2.25.5.5 0 0 1 .183-.683M7 6.5C7 7.328 6.552 8 6 8s-1-.672-1-1.5S5.448 5 6 5s1 .672 1 1.5m4 0c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5"},"child":[]}]})(props);
|
|
155
|
+
}function BsEmojiSunglasses (props) {
|
|
156
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M4.968 9.75a.5.5 0 1 0-.866.5A4.5 4.5 0 0 0 8 12.5a4.5 4.5 0 0 0 3.898-2.25.5.5 0 1 0-.866-.5A3.5 3.5 0 0 1 8 11.5a3.5 3.5 0 0 1-3.032-1.75M7 5.116V5a1 1 0 0 0-1-1H3.28a1 1 0 0 0-.97 1.243l.311 1.242A2 2 0 0 0 4.561 8H5a2 2 0 0 0 1.994-1.839A3 3 0 0 1 8 6c.393 0 .74.064 1.006.161A2 2 0 0 0 11 8h.438a2 2 0 0 0 1.94-1.515l.311-1.242A1 1 0 0 0 12.72 4H10a1 1 0 0 0-1 1v.116A4.2 4.2 0 0 0 8 5c-.35 0-.69.04-1 .116"},"child":[]},{"tag":"path","attr":{"d":"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-1 0A7 7 0 1 0 1 8a7 7 0 0 0 14 0"},"child":[]}]})(props);
|
|
145
157
|
}function BsEyeSlash (props) {
|
|
146
158
|
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7 7 0 0 0-2.79.588l.77.771A6 6 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755q-.247.248-.517.486z"},"child":[]},{"tag":"path","attr":{"d":"M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829"},"child":[]},{"tag":"path","attr":{"d":"M3.35 5.47q-.27.24-.518.487A13 13 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7 7 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884-12-12 .708-.708 12 12z"},"child":[]}]})(props);
|
|
147
159
|
}function BsGear (props) {
|
|
@@ -843,7 +855,7 @@ function createStore(creator) {
|
|
|
843
855
|
const useChartStore = createStore((set) => ({
|
|
844
856
|
data: [],
|
|
845
857
|
visibleData: [],
|
|
846
|
-
timeframe: '
|
|
858
|
+
timeframe: '12h',
|
|
847
859
|
zoomLevel: 1,
|
|
848
860
|
panOffset: 0,
|
|
849
861
|
indicators: [],
|
|
@@ -922,7 +934,6 @@ const useChartStore = createStore((set) => ({
|
|
|
922
934
|
toggleDrawingsHidden: () => set((state) => ({ drawingsHidden: !state.drawingsHidden }))
|
|
923
935
|
}));
|
|
924
936
|
|
|
925
|
-
const EMOJIS = ['⭐', '🔥', '🚀', '✅', '❌', '💎', '🐂', '🐻', '📈', '📉'];
|
|
926
937
|
const cursorOptions = [
|
|
927
938
|
{ type: 'cross', icon: React.createElement(BsCursor, null), label: 'Cross' },
|
|
928
939
|
{ type: 'dot', icon: React.createElement("div", { style: { width: '6px', height: '6px', borderRadius: '50%', background: 'currentColor' } }), label: 'Dot' },
|
|
@@ -962,6 +973,15 @@ const measurementOptions = [
|
|
|
962
973
|
{ tool: 'fibonacci', icon: React.createElement(BsGraphUp, null), label: 'Fibonacci' },
|
|
963
974
|
{ tool: 'ruler', icon: React.createElement(BsGraphDown, null), label: 'Regla' }
|
|
964
975
|
];
|
|
976
|
+
const emojiOptions = [
|
|
977
|
+
{ label: 'Smile', icon: React.createElement(BsEmojiSmile, null), value: '🙂' },
|
|
978
|
+
{ label: 'Laugh', icon: React.createElement(BsEmojiLaughing, null), value: '😂' },
|
|
979
|
+
{ label: 'Heart Eyes', icon: React.createElement(BsEmojiHeartEyes, null), value: '😍' },
|
|
980
|
+
{ label: 'Sunglasses', icon: React.createElement(BsEmojiSunglasses, null), value: '😎' },
|
|
981
|
+
{ label: 'Dizzy', icon: React.createElement(BsEmojiDizzy, null), value: '😵' },
|
|
982
|
+
{ label: 'Neutral', icon: React.createElement(BsEmojiNeutral, null), value: '😐' },
|
|
983
|
+
{ label: 'Angry', icon: React.createElement(BsEmojiAngry, null), value: '😠' }
|
|
984
|
+
];
|
|
965
985
|
const popoverStyle = {
|
|
966
986
|
position: 'absolute',
|
|
967
987
|
left: '64px',
|
|
@@ -980,6 +1000,15 @@ const DrawingToolbar = () => {
|
|
|
980
1000
|
const { activeTool, cursorType, strokeColor, strokeWidth, setActiveTool, setCursorType, setStrokeColor, setStrokeWidth, clearDrawings, setSelectedEmoji, setIsDrawing } = useChartStore();
|
|
981
1001
|
const [openMenu, setOpenMenu] = React.useState(null);
|
|
982
1002
|
const [showEmojiPicker, setShowEmojiPicker] = React.useState(false);
|
|
1003
|
+
const [isCompact, setIsCompact] = React.useState(false);
|
|
1004
|
+
React.useEffect(() => {
|
|
1005
|
+
const handleResize = () => {
|
|
1006
|
+
setIsCompact(window.innerWidth < 768);
|
|
1007
|
+
};
|
|
1008
|
+
handleResize();
|
|
1009
|
+
window.addEventListener('resize', handleResize);
|
|
1010
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
1011
|
+
}, []);
|
|
983
1012
|
const LINE_COLORS = ['#8ab4ff', '#f472b6', '#34d399', '#facc15', '#f87171', '#e2e8f0'];
|
|
984
1013
|
const WIDTH_OPTIONS = [1, 1.5, 2.5, 3.5];
|
|
985
1014
|
const handleSelectTool = (tool) => {
|
|
@@ -1075,14 +1104,27 @@ const DrawingToolbar = () => {
|
|
|
1075
1104
|
title: 'Limpiar todo'
|
|
1076
1105
|
}
|
|
1077
1106
|
];
|
|
1078
|
-
|
|
1107
|
+
const filteredButtons = isCompact
|
|
1108
|
+
? buttonConfig.filter(btn => !['pitchforks', 'measurements'].includes(btn.key))
|
|
1109
|
+
: buttonConfig;
|
|
1110
|
+
const buildPopoverStyle = (custom) => ({
|
|
1111
|
+
...popoverStyle,
|
|
1112
|
+
...(isCompact
|
|
1113
|
+
? { left: '0px', top: 'calc(100% + 12px)', transform: 'none', width: 'min(360px, 92vw)' }
|
|
1114
|
+
: {}),
|
|
1115
|
+
...custom
|
|
1116
|
+
});
|
|
1117
|
+
return (React.createElement("div", { style: { position: 'relative', height: '100%', width: '100%' } },
|
|
1079
1118
|
React.createElement("div", { style: {
|
|
1080
|
-
height: '100%',
|
|
1119
|
+
height: isCompact ? 'auto' : '100%',
|
|
1120
|
+
width: '100%',
|
|
1081
1121
|
display: 'flex',
|
|
1082
|
-
flexDirection: 'column',
|
|
1122
|
+
flexDirection: isCompact ? 'row' : 'column',
|
|
1083
1123
|
alignItems: 'center',
|
|
1084
|
-
gap: '12px'
|
|
1085
|
-
|
|
1124
|
+
gap: '12px',
|
|
1125
|
+
overflowX: isCompact ? 'auto' : 'visible',
|
|
1126
|
+
padding: isCompact ? '12px 6px' : 0
|
|
1127
|
+
} }, filteredButtons.map((btn) => (React.createElement("div", { key: btn.key, style: { position: 'relative' } },
|
|
1086
1128
|
React.createElement("button", { onClick: btn.onClick, title: btn.title, "aria-label": btn.title, style: {
|
|
1087
1129
|
width: '46px',
|
|
1088
1130
|
height: '46px',
|
|
@@ -1097,7 +1139,7 @@ const DrawingToolbar = () => {
|
|
|
1097
1139
|
fontSize: '18px',
|
|
1098
1140
|
transition: 'all 0.2s'
|
|
1099
1141
|
} }, btn.icon),
|
|
1100
|
-
btn.key === 'cursor' && openMenu === 'cursor' && (React.createElement("div", { style: {
|
|
1142
|
+
btn.key === 'cursor' && openMenu === 'cursor' && (React.createElement("div", { style: buildPopoverStyle({ gridTemplateColumns: 'repeat(2, 1fr)', width: isCompact ? 'min(320px, 92vw)' : '200px' }) }, cursorOptions.map(option => (React.createElement("button", { key: option.type, onClick: () => handleCursorSelect(option.type), style: {
|
|
1101
1143
|
width: '84px',
|
|
1102
1144
|
height: '60px',
|
|
1103
1145
|
borderRadius: '12px',
|
|
@@ -1113,7 +1155,7 @@ const DrawingToolbar = () => {
|
|
|
1113
1155
|
}, title: option.label },
|
|
1114
1156
|
React.createElement("span", { style: { fontSize: '18px', display: 'flex', alignItems: 'center', justifyContent: 'center' } }, option.icon),
|
|
1115
1157
|
React.createElement("span", { style: { fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase' } }, option.label)))))),
|
|
1116
|
-
btn.key === 'lines' && openMenu === 'lines' && (React.createElement("div", { style: {
|
|
1158
|
+
btn.key === 'lines' && openMenu === 'lines' && (React.createElement("div", { style: buildPopoverStyle({ width: isCompact ? 'min(360px, 92vw)' : '280px' }) },
|
|
1117
1159
|
React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Lines"),
|
|
1118
1160
|
React.createElement("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px' } }, lineOptions.map(option => (React.createElement("button", { key: option.tool, onClick: () => handleSelectTool(option.tool), style: {
|
|
1119
1161
|
borderRadius: '12px',
|
|
@@ -1153,7 +1195,7 @@ const DrawingToolbar = () => {
|
|
|
1153
1195
|
} },
|
|
1154
1196
|
width,
|
|
1155
1197
|
"px"))))))),
|
|
1156
|
-
btn.key === 'shapes' && openMenu === 'shapes' && (React.createElement("div", { style: {
|
|
1198
|
+
btn.key === 'shapes' && openMenu === 'shapes' && (React.createElement("div", { style: buildPopoverStyle({ width: isCompact ? 'min(320px, 92vw)' : '200px' }) },
|
|
1157
1199
|
React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Shapes"),
|
|
1158
1200
|
React.createElement("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px' } }, shapeOptions.map(option => (React.createElement("button", { key: option.tool, onClick: () => handleSelectTool(option.tool), style: {
|
|
1159
1201
|
borderRadius: '12px',
|
|
@@ -1170,7 +1212,7 @@ const DrawingToolbar = () => {
|
|
|
1170
1212
|
}, title: option.label },
|
|
1171
1213
|
React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
|
|
1172
1214
|
React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em' } }, option.label))))))),
|
|
1173
|
-
btn.key === 'channels' && openMenu === 'channels' && (React.createElement("div", { style: {
|
|
1215
|
+
btn.key === 'channels' && openMenu === 'channels' && (React.createElement("div", { style: buildPopoverStyle({ width: isCompact ? 'min(340px, 92vw)' : '220px' }) },
|
|
1174
1216
|
React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Channels"),
|
|
1175
1217
|
React.createElement("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px' } }, channelOptions.map(option => (React.createElement("button", { key: option.tool, onClick: () => handleSelectTool(option.tool), style: {
|
|
1176
1218
|
borderRadius: '12px',
|
|
@@ -1187,7 +1229,7 @@ const DrawingToolbar = () => {
|
|
|
1187
1229
|
}, title: option.label },
|
|
1188
1230
|
React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
|
|
1189
1231
|
React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em' } }, option.label))))))),
|
|
1190
|
-
btn.key === 'pitchforks' && openMenu === 'pitchforks' && (React.createElement("div", { style: {
|
|
1232
|
+
btn.key === 'pitchforks' && openMenu === 'pitchforks' && (React.createElement("div", { style: buildPopoverStyle({ width: isCompact ? 'min(320px, 92vw)' : '200px' }) },
|
|
1191
1233
|
React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Pitchforks"),
|
|
1192
1234
|
React.createElement("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px' } }, pitchforkOptions.map(option => (React.createElement("button", { key: option.tool, onClick: () => handleSelectTool(option.tool), style: {
|
|
1193
1235
|
borderRadius: '12px',
|
|
@@ -1204,7 +1246,7 @@ const DrawingToolbar = () => {
|
|
|
1204
1246
|
}, title: option.label },
|
|
1205
1247
|
React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
|
|
1206
1248
|
React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', textAlign: 'center' } }, option.label))))))),
|
|
1207
|
-
btn.key === 'measurements' && openMenu === 'measurements' && (React.createElement("div", { style: {
|
|
1249
|
+
btn.key === 'measurements' && openMenu === 'measurements' && (React.createElement("div", { style: buildPopoverStyle({ gridTemplateColumns: 'repeat(2, 1fr)', width: isCompact ? 'min(280px, 80vw)' : '160px' }) }, measurementOptions.map(option => (React.createElement("button", { key: option.tool, onClick: () => handleSelectTool(option.tool), style: {
|
|
1208
1250
|
width: '64px',
|
|
1209
1251
|
height: '54px',
|
|
1210
1252
|
borderRadius: '12px',
|
|
@@ -1221,41 +1263,24 @@ const DrawingToolbar = () => {
|
|
|
1221
1263
|
}, title: option.label },
|
|
1222
1264
|
React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
|
|
1223
1265
|
React.createElement("span", null, option.label)))))),
|
|
1224
|
-
btn.key === 'emoji' && showEmojiPicker && (React.createElement("div", { style: {
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
transform: 'translateY(-50%)',
|
|
1229
|
-
zIndex: 1100,
|
|
1230
|
-
background: '#020617',
|
|
1231
|
-
borderRadius: '14px',
|
|
1232
|
-
overflow: 'hidden',
|
|
1233
|
-
boxShadow: '0 18px 40px rgba(0,0,0,0.65)',
|
|
1234
|
-
padding: '10px',
|
|
1235
|
-
border: '1px solid #1f2937'
|
|
1236
|
-
} },
|
|
1237
|
-
React.createElement("div", { style: {
|
|
1238
|
-
display: 'grid',
|
|
1239
|
-
gridTemplateColumns: 'repeat(3, 1fr)',
|
|
1240
|
-
gap: '6px'
|
|
1241
|
-
} }, EMOJIS.map((emoji) => (React.createElement("button", { key: emoji, onClick: () => {
|
|
1242
|
-
setSelectedEmoji(emoji);
|
|
1266
|
+
btn.key === 'emoji' && showEmojiPicker && (React.createElement("div", { style: buildPopoverStyle({ width: isCompact ? 'min(320px, 92vw)' : '220px' }) },
|
|
1267
|
+
React.createElement("p", { style: { margin: '0 0 6px', fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Emojis"),
|
|
1268
|
+
React.createElement("div", { style: { display: 'grid', gridTemplateColumns: isCompact ? 'repeat(4, minmax(0, 1fr))' : 'repeat(3, minmax(0, 1fr))', gap: '8px' } }, emojiOptions.map(option => (React.createElement("button", { key: option.label, onClick: () => {
|
|
1269
|
+
setSelectedEmoji(option.value);
|
|
1243
1270
|
setActiveTool('icon');
|
|
1244
1271
|
setShowEmojiPicker(false);
|
|
1245
1272
|
}, style: {
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
background: '#111827',
|
|
1273
|
+
height: '48px',
|
|
1274
|
+
borderRadius: '12px',
|
|
1249
1275
|
border: '1px solid #1f2937',
|
|
1250
|
-
|
|
1251
|
-
|
|
1276
|
+
background: 'rgba(2,6,23,0.65)',
|
|
1277
|
+
color: '#f8fafc',
|
|
1252
1278
|
display: 'flex',
|
|
1253
1279
|
alignItems: 'center',
|
|
1254
1280
|
justifyContent: 'center',
|
|
1255
|
-
fontSize: '
|
|
1256
|
-
|
|
1257
|
-
} },
|
|
1258
|
-
React.createElement("span", null, emoji)))))))))))));
|
|
1281
|
+
fontSize: '22px',
|
|
1282
|
+
cursor: 'pointer'
|
|
1283
|
+
}, title: option.label, "aria-label": option.label }, option.icon))))))))))));
|
|
1259
1284
|
};
|
|
1260
1285
|
|
|
1261
1286
|
const drawRoundedRect = (ctx, x, y, width, height, radius = 8) => {
|
|
@@ -1992,23 +2017,23 @@ const LINE_TOOL_TYPES = [
|
|
|
1992
2017
|
const LINE_TOOL_SET = new Set(LINE_TOOL_TYPES);
|
|
1993
2018
|
const THEME_PRESETS = {
|
|
1994
2019
|
dark: {
|
|
1995
|
-
pageBg: '#
|
|
1996
|
-
heroFrom: '#
|
|
1997
|
-
heroTo: '#
|
|
1998
|
-
panelBg: 'rgba(
|
|
1999
|
-
panelBorder: '#
|
|
2000
|
-
cardBg: 'rgba(
|
|
2001
|
-
cardBorder: '#
|
|
2020
|
+
pageBg: '#000000',
|
|
2021
|
+
heroFrom: '#050505',
|
|
2022
|
+
heroTo: '#000000',
|
|
2023
|
+
panelBg: 'rgba(0,0,0,0.92)',
|
|
2024
|
+
panelBorder: '#0f0f0f',
|
|
2025
|
+
cardBg: 'rgba(0,0,0,0.88)',
|
|
2026
|
+
cardBorder: '#050505',
|
|
2002
2027
|
textPrimary: '#f8fafc',
|
|
2003
|
-
textSecondary: '#
|
|
2028
|
+
textSecondary: '#9ca3af',
|
|
2004
2029
|
accent: '#2563eb',
|
|
2005
|
-
accentSoft: 'rgba(37,99,235,0.
|
|
2006
|
-
railBg: 'rgba(
|
|
2007
|
-
railBorder: '#
|
|
2008
|
-
plotBg: '#
|
|
2009
|
-
plotBorder: '#
|
|
2010
|
-
scaleBg: 'rgba(
|
|
2011
|
-
overlayBg: 'rgba(
|
|
2030
|
+
accentSoft: 'rgba(37,99,235,0.25)',
|
|
2031
|
+
railBg: 'rgba(0,0,0,0.9)',
|
|
2032
|
+
railBorder: '#080808',
|
|
2033
|
+
plotBg: '#010101',
|
|
2034
|
+
plotBorder: '#0d0d0d',
|
|
2035
|
+
scaleBg: 'rgba(0,0,0,0.92)',
|
|
2036
|
+
overlayBg: 'rgba(0,0,0,0.85)'
|
|
2012
2037
|
},
|
|
2013
2038
|
blue: {
|
|
2014
2039
|
pageBg: '#060e1f',
|
|
@@ -2060,6 +2085,10 @@ const CUSTOM_THEME_FIELDS = [
|
|
|
2060
2085
|
const MIN_CANDLE_PX = 4;
|
|
2061
2086
|
const MAX_CANDLE_PX = 28;
|
|
2062
2087
|
const BUFFER_FRACTION = 0.18;
|
|
2088
|
+
const LEFT_OFFSET_BARS = 2;
|
|
2089
|
+
const RIGHT_OFFSET_BARS = 18;
|
|
2090
|
+
const BAR_BODY_RATIO = 0.78;
|
|
2091
|
+
const GRID_DIVISIONS = 10;
|
|
2063
2092
|
const INERTIA_DURATION_MS = 900;
|
|
2064
2093
|
const ZOOM_MIN = 0.2;
|
|
2065
2094
|
const ZOOM_MAX = 6;
|
|
@@ -2067,6 +2096,42 @@ const easeOutQuad = (t) => 1 - (1 - t) * (1 - t);
|
|
|
2067
2096
|
const easeInOutCubic = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
2068
2097
|
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
2069
2098
|
const alignStroke = (value) => Math.round(value) + 0.5;
|
|
2099
|
+
const determinePriceFormat = (reference) => {
|
|
2100
|
+
const value = Math.abs(reference);
|
|
2101
|
+
if (!Number.isFinite(value) || value === 0)
|
|
2102
|
+
return { min: 2, max: 6 };
|
|
2103
|
+
if (value < 0.0001)
|
|
2104
|
+
return { min: 6, max: 8 };
|
|
2105
|
+
if (value < 0.001)
|
|
2106
|
+
return { min: 5, max: 7 };
|
|
2107
|
+
if (value < 0.01)
|
|
2108
|
+
return { min: 4, max: 6 };
|
|
2109
|
+
if (value < 0.1)
|
|
2110
|
+
return { min: 4, max: 5 };
|
|
2111
|
+
if (value < 1)
|
|
2112
|
+
return { min: 3, max: 4 };
|
|
2113
|
+
if (value < 100)
|
|
2114
|
+
return { min: 2, max: 3 };
|
|
2115
|
+
return { min: 2, max: 2 };
|
|
2116
|
+
};
|
|
2117
|
+
const getEffectiveBarCount = (count) => Math.max(count + LEFT_OFFSET_BARS + RIGHT_OFFSET_BARS, 1);
|
|
2118
|
+
const getCandleStep = (width, count) => (width <= 0 ? 0 : width / getEffectiveBarCount(count));
|
|
2119
|
+
const getCandleCenter = (index, width, count) => {
|
|
2120
|
+
const step = getCandleStep(width, count);
|
|
2121
|
+
return (index + LEFT_OFFSET_BARS + 0.5) * step;
|
|
2122
|
+
};
|
|
2123
|
+
const clampPixelToChart = (pixel, width) => Math.max(0, Math.min(width, pixel));
|
|
2124
|
+
const pixelToCandleIndex = (pixel, width, count) => {
|
|
2125
|
+
const step = getCandleStep(width, count);
|
|
2126
|
+
if (step === 0)
|
|
2127
|
+
return 0;
|
|
2128
|
+
return pixel / step - LEFT_OFFSET_BARS - 0.5;
|
|
2129
|
+
};
|
|
2130
|
+
const clampCandleIndex = (value, length) => {
|
|
2131
|
+
if (length <= 0)
|
|
2132
|
+
return 0;
|
|
2133
|
+
return clamp(Math.round(value), 0, Math.max(length - 1, 0));
|
|
2134
|
+
};
|
|
2070
2135
|
const createParallelPoints = (start, end, offset) => {
|
|
2071
2136
|
const dx = end.x - start.x;
|
|
2072
2137
|
const dy = end.y - start.y;
|
|
@@ -2156,7 +2221,7 @@ const computeVisibleWindow = (dataset, chartWidth, timeframe, zoomLevel, panOffs
|
|
|
2156
2221
|
}
|
|
2157
2222
|
};
|
|
2158
2223
|
};
|
|
2159
|
-
const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showStats = true, showHeaderStats = true }) => {
|
|
2224
|
+
const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showStats = true, showHeaderStats = true, showDrawingToolbar = true }) => {
|
|
2160
2225
|
const chartRef = React.useRef(null);
|
|
2161
2226
|
const volumeRef = React.useRef(null);
|
|
2162
2227
|
const gridRef = React.useRef(null);
|
|
@@ -2295,7 +2360,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2295
2360
|
if (!chartWidth)
|
|
2296
2361
|
return;
|
|
2297
2362
|
const candles = getVisibleCandles();
|
|
2298
|
-
const pxPerCandle = chartWidth / Math.max(candles.length || 1, 1);
|
|
2363
|
+
const pxPerCandle = chartWidth / Math.max(getEffectiveBarCount(candles.length || 1), 1);
|
|
2299
2364
|
if (!isFinite(pxPerCandle) || pxPerCandle === 0)
|
|
2300
2365
|
return;
|
|
2301
2366
|
const deltaOffset = deltaPx / Math.max(pxPerCandle, 1e-3);
|
|
@@ -2419,6 +2484,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2419
2484
|
const [priceLabels, setPriceLabels] = React.useState([]);
|
|
2420
2485
|
const [timeLabels, setTimeLabels] = React.useState([]);
|
|
2421
2486
|
const [clickedPrice, setClickedPrice] = React.useState(null);
|
|
2487
|
+
const [crosshairMeta, setCrosshairMeta] = React.useState(null);
|
|
2422
2488
|
const [showConfigPanel, setShowConfigPanel] = React.useState(false);
|
|
2423
2489
|
const [language, setLanguage] = React.useState('en');
|
|
2424
2490
|
const [colorScheme, setColorScheme] = React.useState('green');
|
|
@@ -2449,6 +2515,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2449
2515
|
const [showNoteModal, setShowNoteModal] = React.useState(false);
|
|
2450
2516
|
const [pendingNotePoint, setPendingNotePoint] = React.useState(null);
|
|
2451
2517
|
const [noteDraft, setNoteDraft] = React.useState('');
|
|
2518
|
+
const [priceFormat, setPriceFormat] = React.useState({ min: 2, max: 4 });
|
|
2452
2519
|
const [isMobile, setIsMobile] = React.useState(false);
|
|
2453
2520
|
React.useEffect(() => {
|
|
2454
2521
|
if (!plotAreaRef.current)
|
|
@@ -2471,7 +2538,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2471
2538
|
return () => window.clearTimeout(id);
|
|
2472
2539
|
}, [seriesType, visibleData.length]);
|
|
2473
2540
|
const locale = language === 'es' ? 'es-ES' : 'en-US';
|
|
2474
|
-
const numberFormatter = React.useMemo(() => new Intl.NumberFormat(locale, { minimumFractionDigits:
|
|
2541
|
+
const numberFormatter = React.useMemo(() => new Intl.NumberFormat(locale, { minimumFractionDigits: priceFormat.min, maximumFractionDigits: priceFormat.max }), [locale, priceFormat]);
|
|
2475
2542
|
const shortNumberFormatter = React.useMemo(() => new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }), [locale]);
|
|
2476
2543
|
const timeFormatter = React.useMemo(() => new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit' }), [locale]);
|
|
2477
2544
|
const latencyMs = React.useMemo(() => Math.round(20 + Math.random() * 15), []);
|
|
@@ -2533,9 +2600,14 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2533
2600
|
const priceRange = Math.max(1e-6, maxPrice - minPrice);
|
|
2534
2601
|
const maxVolume = Math.max(bounds.maxVolume, 1);
|
|
2535
2602
|
priceWindowRef.current = { min: minPrice, max: maxPrice };
|
|
2603
|
+
const referenceMagnitude = Math.min(Math.abs(minPrice), Math.abs(maxPrice)) || Math.max(Math.abs(minPrice), Math.abs(maxPrice));
|
|
2604
|
+
const suggestedFormat = determinePriceFormat(referenceMagnitude);
|
|
2605
|
+
setPriceFormat((prev) => (prev.min === suggestedFormat.min && prev.max === suggestedFormat.max ? prev : suggestedFormat));
|
|
2536
2606
|
const chartWidth = Math.max(1, cssWidth - priceScaleWidth);
|
|
2537
2607
|
const chartHeight = Math.max(1, cssHeight - timeScaleHeight - volumeHeight);
|
|
2538
|
-
const
|
|
2608
|
+
const candleStep = getCandleStep(chartWidth, candles.length);
|
|
2609
|
+
const candleWidth = Math.max(1, candleStep * BAR_BODY_RATIO);
|
|
2610
|
+
const centerX = (index) => getCandleCenter(index, chartWidth, candles.length);
|
|
2539
2611
|
// Generate price labels
|
|
2540
2612
|
const priceLabelsArray = [];
|
|
2541
2613
|
for (let i = 0; i <= 10; i++) {
|
|
@@ -2570,9 +2642,11 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2570
2642
|
gridCtx.lineTo(chartWidth, y);
|
|
2571
2643
|
gridCtx.stroke();
|
|
2572
2644
|
}
|
|
2573
|
-
const timeStep = Math.max(1, Math.floor(candles.length /
|
|
2645
|
+
const timeStep = Math.max(1, Math.floor(Math.max(candles.length, 1) / GRID_DIVISIONS));
|
|
2574
2646
|
for (let i = 0; i <= candles.length; i += timeStep) {
|
|
2575
|
-
const x = alignStroke(i *
|
|
2647
|
+
const x = alignStroke((i + LEFT_OFFSET_BARS) * candleStep);
|
|
2648
|
+
if (x < 0 || x > chartWidth)
|
|
2649
|
+
continue;
|
|
2576
2650
|
gridCtx.beginPath();
|
|
2577
2651
|
gridCtx.moveTo(x, 0);
|
|
2578
2652
|
gridCtx.lineTo(x, chartHeight);
|
|
@@ -2598,7 +2672,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2598
2672
|
: minPrice;
|
|
2599
2673
|
chartCtx.beginPath();
|
|
2600
2674
|
candles.forEach((candle, index) => {
|
|
2601
|
-
const x = index
|
|
2675
|
+
const x = centerX(index);
|
|
2602
2676
|
const yClose = ((maxPrice - candle.close) / priceRange) * chartHeight;
|
|
2603
2677
|
if (index === 0) {
|
|
2604
2678
|
chartCtx.moveTo(x, yClose);
|
|
@@ -2606,6 +2680,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2606
2680
|
else if (seriesType === 'step') {
|
|
2607
2681
|
const prev = candles[index - 1];
|
|
2608
2682
|
if (prev) {
|
|
2683
|
+
centerX(index - 1);
|
|
2609
2684
|
const prevY = ((maxPrice - prev.close) / priceRange) * chartHeight;
|
|
2610
2685
|
chartCtx.lineTo(x, prevY);
|
|
2611
2686
|
chartCtx.lineTo(x, yClose);
|
|
@@ -2621,7 +2696,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2621
2696
|
chartCtx.stroke();
|
|
2622
2697
|
if (seriesType === 'line-markers') {
|
|
2623
2698
|
candles.forEach((candle, index) => {
|
|
2624
|
-
const x = index
|
|
2699
|
+
const x = centerX(index);
|
|
2625
2700
|
const yClose = ((maxPrice - candle.close) / priceRange) * chartHeight;
|
|
2626
2701
|
chartCtx.beginPath();
|
|
2627
2702
|
chartCtx.arc(x, yClose, 2.5, 0, 2 * Math.PI);
|
|
@@ -2630,8 +2705,8 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2630
2705
|
});
|
|
2631
2706
|
}
|
|
2632
2707
|
if (seriesType === 'area' || seriesType === 'hlc-area' || seriesType === 'baseline') {
|
|
2633
|
-
chartCtx.lineTo((candles.length - 1)
|
|
2634
|
-
chartCtx.lineTo(0
|
|
2708
|
+
chartCtx.lineTo(centerX(candles.length - 1), ((maxPrice - baselinePrice) / priceRange) * chartHeight);
|
|
2709
|
+
chartCtx.lineTo(centerX(0), ((maxPrice - baselinePrice) / priceRange) * chartHeight);
|
|
2635
2710
|
chartCtx.closePath();
|
|
2636
2711
|
chartCtx.fillStyle =
|
|
2637
2712
|
seriesType === 'baseline' ? 'rgba(41,98,255,0.25)' : 'rgba(41,98,255,0.15)';
|
|
@@ -2641,7 +2716,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2641
2716
|
else {
|
|
2642
2717
|
// ----- CANDLE / BAR-BASED SERIES -----
|
|
2643
2718
|
candles.forEach((candle, index) => {
|
|
2644
|
-
const xCenter = index
|
|
2719
|
+
const xCenter = centerX(index);
|
|
2645
2720
|
const strokeX = alignStroke(xCenter);
|
|
2646
2721
|
const yHigh = ((maxPrice - candle.high) / priceRange) * chartHeight;
|
|
2647
2722
|
const yLow = ((maxPrice - candle.low) / priceRange) * chartHeight;
|
|
@@ -2726,12 +2801,12 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2726
2801
|
if (volumeCtx) {
|
|
2727
2802
|
volumeCtx.clearRect(0, 0, chartWidth, volumeHeight);
|
|
2728
2803
|
candles.forEach((candle, index) => {
|
|
2729
|
-
const
|
|
2804
|
+
const xLeft = centerX(index) - candleWidth / 2;
|
|
2730
2805
|
const barHeight = ((candle.volume || 0) / maxVolume) * volumeHeight;
|
|
2731
2806
|
const y = volumeHeight - barHeight;
|
|
2732
2807
|
const isBullish = candle.close > candle.open;
|
|
2733
2808
|
volumeCtx.fillStyle = isBullish ? bullishColor : bearishColor;
|
|
2734
|
-
volumeCtx.fillRect(
|
|
2809
|
+
volumeCtx.fillRect(xLeft + 1, y, Math.max(1, candleWidth - 2), barHeight);
|
|
2735
2810
|
});
|
|
2736
2811
|
}
|
|
2737
2812
|
// Draw indicators
|
|
@@ -2745,7 +2820,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2745
2820
|
indicatorCtx.beginPath();
|
|
2746
2821
|
indicator.data.forEach((value, index) => {
|
|
2747
2822
|
if (value !== null && value !== undefined) {
|
|
2748
|
-
const x = index
|
|
2823
|
+
const x = centerX(index);
|
|
2749
2824
|
const y = ((maxPrice - value) / priceRange) * chartHeight;
|
|
2750
2825
|
if (index === 0) {
|
|
2751
2826
|
indicatorCtx.moveTo(x, y);
|
|
@@ -2760,11 +2835,11 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2760
2835
|
else if (indicator.type === 'histogram') {
|
|
2761
2836
|
indicator.data.forEach((value, index) => {
|
|
2762
2837
|
if (value !== null && value !== undefined) {
|
|
2763
|
-
const
|
|
2838
|
+
const xLeft = centerX(index) - candleWidth / 2;
|
|
2764
2839
|
const barHeight = Math.abs(value) * chartHeight * 0.1; // Scale histogram
|
|
2765
2840
|
const y = value >= 0 ? chartHeight / 2 - barHeight : chartHeight / 2;
|
|
2766
2841
|
indicatorCtx.fillStyle = value >= 0 ? '#089981' : '#f23645';
|
|
2767
|
-
indicatorCtx.fillRect(
|
|
2842
|
+
indicatorCtx.fillRect(xLeft + 2, y, Math.max(1, candleWidth - 4), barHeight);
|
|
2768
2843
|
}
|
|
2769
2844
|
});
|
|
2770
2845
|
}
|
|
@@ -2794,32 +2869,46 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2794
2869
|
}
|
|
2795
2870
|
}, [chartWidth, chartHeight, overlayHeight]);
|
|
2796
2871
|
const handlePointerMove = React.useCallback((e) => {
|
|
2797
|
-
if (interactionsLocked || !visibleData.length)
|
|
2872
|
+
if (interactionsLocked || !visibleData.length) {
|
|
2873
|
+
setCrosshairMeta(null);
|
|
2798
2874
|
return;
|
|
2875
|
+
}
|
|
2799
2876
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
2800
2877
|
const x = e.clientX - rect.left;
|
|
2801
2878
|
const y = e.clientY - rect.top;
|
|
2802
2879
|
setClickedPrice(null);
|
|
2803
2880
|
if (cursorType !== 'cross') {
|
|
2881
|
+
setCrosshairMeta(null);
|
|
2804
2882
|
return;
|
|
2805
2883
|
}
|
|
2806
2884
|
if (!isDraggingRef.current) {
|
|
2807
2885
|
const { min, max } = priceWindowRef.current;
|
|
2808
|
-
const
|
|
2809
|
-
const
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2886
|
+
const rawIndex = pixelToCandleIndex(x, chartWidth, visibleData.length);
|
|
2887
|
+
const hoveredIndex = clampCandleIndex(rawIndex, visibleData.length);
|
|
2888
|
+
const hoveredCandle = visibleData[hoveredIndex];
|
|
2889
|
+
const snappedPrice = coordsRef.current.snapToPrice(y, chartHeight, min, max);
|
|
2890
|
+
const yPixel = coordsRef.current.priceToPixel(snappedPrice, chartHeight, min, max);
|
|
2891
|
+
const xPixel = magnetEnabled
|
|
2892
|
+
? getCandleCenter(hoveredIndex, chartWidth, visibleData.length)
|
|
2893
|
+
: clampPixelToChart(x, chartWidth);
|
|
2894
|
+
mousePosRef.current = { x: xPixel, y: yPixel };
|
|
2895
|
+
setCrosshairMeta({
|
|
2896
|
+
price: snappedPrice,
|
|
2897
|
+
timestamp: hoveredCandle?.timestamp ?? null,
|
|
2898
|
+
x: xPixel,
|
|
2899
|
+
y: yPixel,
|
|
2900
|
+
candle: hoveredCandle ?? null
|
|
2901
|
+
});
|
|
2814
2902
|
showCrosshairRef.current = true;
|
|
2815
2903
|
requestAnimationFrame(drawCrosshair);
|
|
2816
2904
|
}
|
|
2817
|
-
}, [visibleData, cursorType, chartWidth, chartHeight, drawCrosshair, interactionsLocked]);
|
|
2905
|
+
}, [visibleData, cursorType, chartWidth, chartHeight, drawCrosshair, interactionsLocked, magnetEnabled]);
|
|
2818
2906
|
const handleMouseLeave = React.useCallback(() => {
|
|
2819
2907
|
showCrosshairRef.current = false;
|
|
2820
2908
|
isDraggingRef.current = false;
|
|
2821
2909
|
dragSampleRef.current.velocity = 0;
|
|
2822
2910
|
dragSampleRef.current.lastTs = 0;
|
|
2911
|
+
setCrosshairMeta(null);
|
|
2823
2912
|
requestAnimationFrame(() => {
|
|
2824
2913
|
const overlayCtx = overlayRef.current?.getContext('2d');
|
|
2825
2914
|
if (overlayCtx) {
|
|
@@ -2863,15 +2952,21 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2863
2952
|
? { minPrice: priceWindowRef.current.min, maxPrice: priceWindowRef.current.max }
|
|
2864
2953
|
: getDataBounds(targetData);
|
|
2865
2954
|
const dataLength = Math.max(targetData.length, 1);
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2955
|
+
let snappedPrice = coordsRef.current.snapToPrice(y, chartHeight, bounds.minPrice, bounds.maxPrice, 0.5);
|
|
2956
|
+
let snappedY = y;
|
|
2957
|
+
if (magnetEnabled && targetData.length) {
|
|
2958
|
+
const snappedIndex = clampCandleIndex(pixelToCandleIndex(x, chartWidth, dataLength), dataLength);
|
|
2959
|
+
x = getCandleCenter(snappedIndex, chartWidth, dataLength);
|
|
2960
|
+
snappedY = coordsRef.current.priceToPixel(snappedPrice, chartHeight, bounds.minPrice, bounds.maxPrice);
|
|
2961
|
+
y = snappedY;
|
|
2962
|
+
}
|
|
2963
|
+
else {
|
|
2964
|
+
x = clampPixelToChart(x, chartWidth);
|
|
2965
|
+
}
|
|
2966
|
+
const price = coordsRef.current.pixelToPrice(snappedY, chartHeight, bounds.minPrice, bounds.maxPrice);
|
|
2967
|
+
const hoveredIndex = clampCandleIndex(pixelToCandleIndex(x, chartWidth, dataLength), dataLength);
|
|
2968
|
+
const time = targetData[hoveredIndex]?.timestamp ?? hoveredIndex;
|
|
2969
|
+
return { x, y: snappedY, price, time };
|
|
2875
2970
|
}, [visibleData, storeData, magnetEnabled, chartWidth, chartHeight, interactionsLocked]);
|
|
2876
2971
|
const tryEraseDrawing = React.useCallback((x, y) => {
|
|
2877
2972
|
for (let i = drawings.length - 1; i >= 0; i--) {
|
|
@@ -3082,7 +3177,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
3082
3177
|
}
|
|
3083
3178
|
if (visibleData.length) {
|
|
3084
3179
|
const { min, max } = priceWindowRef.current;
|
|
3085
|
-
const snappedIndex =
|
|
3180
|
+
const snappedIndex = clampCandleIndex(pixelToCandleIndex(rawX, chartWidth, visibleData.length), visibleData.length);
|
|
3086
3181
|
setSelectedCandleIndex(snappedIndex);
|
|
3087
3182
|
const price = coordsRef.current.pixelToPrice(rawY, chartHeight, min, max);
|
|
3088
3183
|
setClickedPrice({ x: rawX, y: rawY, price });
|
|
@@ -3608,7 +3703,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
3608
3703
|
minHeight: 0,
|
|
3609
3704
|
alignItems: 'stretch'
|
|
3610
3705
|
} },
|
|
3611
|
-
React.createElement("div", { style: { width: isMobile ? '60px' : '88px', flexShrink: 0, height: '100%' } },
|
|
3706
|
+
showDrawingToolbar && (React.createElement("div", { style: { width: isMobile ? '60px' : '88px', flexShrink: 0, height: '100%' } },
|
|
3612
3707
|
React.createElement("div", { style: {
|
|
3613
3708
|
height: '100%',
|
|
3614
3709
|
borderRadius: '24px',
|
|
@@ -3619,7 +3714,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
3619
3714
|
alignItems: 'center',
|
|
3620
3715
|
padding: isMobile ? '12px 0' : '16px 0'
|
|
3621
3716
|
} },
|
|
3622
|
-
React.createElement(DrawingToolbar, null))),
|
|
3717
|
+
React.createElement(DrawingToolbar, null)))),
|
|
3623
3718
|
React.createElement("div", { ref: plotAreaRef, style: {
|
|
3624
3719
|
flex: 1,
|
|
3625
3720
|
position: 'relative',
|
|
@@ -3660,6 +3755,65 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
3660
3755
|
React.createElement("div", { style: { position: 'absolute', bottom: 0, left: 0, right: priceScaleWidth, height: `${timeScaleHeight}px`, background: activeTheme.scaleBg, borderTop: `1px solid ${activeTheme.plotBorder}`, fontSize: '11px', color: activeTheme.textSecondary, display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: '4px', padding: '4px 10px' } },
|
|
3661
3756
|
React.createElement("span", { style: { textTransform: 'uppercase', fontSize: '10px', letterSpacing: '0.08em' } }, strings.axis.time),
|
|
3662
3757
|
React.createElement("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', color: activeTheme.textPrimary } }, timeLabels.map((label, i) => (React.createElement("div", { key: i }, label))))),
|
|
3758
|
+
crosshairMeta?.candle && (React.createElement("div", { style: {
|
|
3759
|
+
position: 'absolute',
|
|
3760
|
+
top: 16,
|
|
3761
|
+
left: 16,
|
|
3762
|
+
padding: '10px 14px',
|
|
3763
|
+
borderRadius: '14px',
|
|
3764
|
+
background: activeTheme.overlayBg,
|
|
3765
|
+
border: `1px solid ${activeTheme.panelBorder}`,
|
|
3766
|
+
boxShadow: '0 12px 30px rgba(0,0,0,0.45)',
|
|
3767
|
+
color: activeTheme.textPrimary,
|
|
3768
|
+
fontSize: '11px',
|
|
3769
|
+
letterSpacing: '0.04em',
|
|
3770
|
+
display: 'flex',
|
|
3771
|
+
gap: '14px',
|
|
3772
|
+
pointerEvents: 'none',
|
|
3773
|
+
zIndex: 60
|
|
3774
|
+
} },
|
|
3775
|
+
([
|
|
3776
|
+
{ label: 'O', value: crosshairMeta.candle.open },
|
|
3777
|
+
{ label: 'H', value: crosshairMeta.candle.high },
|
|
3778
|
+
{ label: 'L', value: crosshairMeta.candle.low },
|
|
3779
|
+
{ label: 'C', value: crosshairMeta.candle.close }
|
|
3780
|
+
]).map((item) => (React.createElement("div", { key: item.label, style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
3781
|
+
React.createElement("span", { style: { color: activeTheme.textSecondary } }, item.label),
|
|
3782
|
+
React.createElement("span", { style: { fontWeight: 600 } }, numberFormatter.format(item.value))))),
|
|
3783
|
+
React.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
3784
|
+
React.createElement("span", { style: { color: activeTheme.textSecondary } }, "V"),
|
|
3785
|
+
React.createElement("span", { style: { fontWeight: 600 } }, shortNumberFormatter.format(crosshairMeta.candle.volume ?? 0))))),
|
|
3786
|
+
crosshairMeta && (React.createElement(React.Fragment, null,
|
|
3787
|
+
React.createElement("div", { style: {
|
|
3788
|
+
position: 'absolute',
|
|
3789
|
+
right: `${priceScaleWidth}px`,
|
|
3790
|
+
top: `${Math.min(Math.max(crosshairMeta.y, 12), overlayHeight - 12)}px`,
|
|
3791
|
+
transform: 'translate(0, -50%)',
|
|
3792
|
+
background: activeTheme.scaleBg,
|
|
3793
|
+
border: `1px solid ${activeTheme.panelBorder}`,
|
|
3794
|
+
borderRadius: '6px 0 0 6px',
|
|
3795
|
+
padding: '2px 8px',
|
|
3796
|
+
fontSize: '11px',
|
|
3797
|
+
fontWeight: 600,
|
|
3798
|
+
color: activeTheme.textPrimary,
|
|
3799
|
+
pointerEvents: 'none',
|
|
3800
|
+
boxShadow: '0 8px 20px rgba(0,0,0,0.4)'
|
|
3801
|
+
} }, numberFormatter.format(crosshairMeta.price)),
|
|
3802
|
+
crosshairMeta.timestamp && (React.createElement("div", { style: {
|
|
3803
|
+
position: 'absolute',
|
|
3804
|
+
left: `${Math.min(Math.max(crosshairMeta.x, 40), chartWidth - 40)}px`,
|
|
3805
|
+
bottom: `${timeScaleHeight}px`,
|
|
3806
|
+
transform: 'translate(-50%, 50%)',
|
|
3807
|
+
background: activeTheme.scaleBg,
|
|
3808
|
+
border: `1px solid ${activeTheme.panelBorder}`,
|
|
3809
|
+
borderRadius: '6px',
|
|
3810
|
+
padding: '2px 8px',
|
|
3811
|
+
fontSize: '10px',
|
|
3812
|
+
letterSpacing: '0.05em',
|
|
3813
|
+
color: activeTheme.textPrimary,
|
|
3814
|
+
pointerEvents: 'none',
|
|
3815
|
+
boxShadow: '0 8px 20px rgba(0,0,0,0.35)'
|
|
3816
|
+
} }, timeFormatter.format(new Date(crosshairMeta.timestamp)))))),
|
|
3663
3817
|
clickedPrice && (React.createElement("div", { style: {
|
|
3664
3818
|
position: 'absolute',
|
|
3665
3819
|
background: '#1e222d',
|