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.mjs
CHANGED
|
@@ -138,8 +138,20 @@ function BsArrowRepeat (props) {
|
|
|
138
138
|
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);
|
|
139
139
|
}function BsDiagram3 (props) {
|
|
140
140
|
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);
|
|
141
|
+
}function BsEmojiAngry (props) {
|
|
142
|
+
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);
|
|
143
|
+
}function BsEmojiDizzy (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":"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);
|
|
145
|
+
}function BsEmojiHeartEyes (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":"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);
|
|
147
|
+
}function BsEmojiLaughing (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":"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);
|
|
149
|
+
}function BsEmojiNeutral (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":"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);
|
|
141
151
|
}function BsEmojiSmile (props) {
|
|
142
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.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);
|
|
153
|
+
}function BsEmojiSunglasses (props) {
|
|
154
|
+
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);
|
|
143
155
|
}function BsEyeSlash (props) {
|
|
144
156
|
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);
|
|
145
157
|
}function BsGear (props) {
|
|
@@ -841,7 +853,7 @@ function createStore(creator) {
|
|
|
841
853
|
const useChartStore = createStore((set) => ({
|
|
842
854
|
data: [],
|
|
843
855
|
visibleData: [],
|
|
844
|
-
timeframe: '
|
|
856
|
+
timeframe: '12h',
|
|
845
857
|
zoomLevel: 1,
|
|
846
858
|
panOffset: 0,
|
|
847
859
|
indicators: [],
|
|
@@ -920,7 +932,6 @@ const useChartStore = createStore((set) => ({
|
|
|
920
932
|
toggleDrawingsHidden: () => set((state) => ({ drawingsHidden: !state.drawingsHidden }))
|
|
921
933
|
}));
|
|
922
934
|
|
|
923
|
-
const EMOJIS = ['⭐', '🔥', '🚀', '✅', '❌', '💎', '🐂', '🐻', '📈', '📉'];
|
|
924
935
|
const cursorOptions = [
|
|
925
936
|
{ type: 'cross', icon: React.createElement(BsCursor, null), label: 'Cross' },
|
|
926
937
|
{ type: 'dot', icon: React.createElement("div", { style: { width: '6px', height: '6px', borderRadius: '50%', background: 'currentColor' } }), label: 'Dot' },
|
|
@@ -960,6 +971,15 @@ const measurementOptions = [
|
|
|
960
971
|
{ tool: 'fibonacci', icon: React.createElement(BsGraphUp, null), label: 'Fibonacci' },
|
|
961
972
|
{ tool: 'ruler', icon: React.createElement(BsGraphDown, null), label: 'Regla' }
|
|
962
973
|
];
|
|
974
|
+
const emojiOptions = [
|
|
975
|
+
{ label: 'Smile', icon: React.createElement(BsEmojiSmile, null), value: '🙂' },
|
|
976
|
+
{ label: 'Laugh', icon: React.createElement(BsEmojiLaughing, null), value: '😂' },
|
|
977
|
+
{ label: 'Heart Eyes', icon: React.createElement(BsEmojiHeartEyes, null), value: '😍' },
|
|
978
|
+
{ label: 'Sunglasses', icon: React.createElement(BsEmojiSunglasses, null), value: '😎' },
|
|
979
|
+
{ label: 'Dizzy', icon: React.createElement(BsEmojiDizzy, null), value: '😵' },
|
|
980
|
+
{ label: 'Neutral', icon: React.createElement(BsEmojiNeutral, null), value: '😐' },
|
|
981
|
+
{ label: 'Angry', icon: React.createElement(BsEmojiAngry, null), value: '😠' }
|
|
982
|
+
];
|
|
963
983
|
const popoverStyle = {
|
|
964
984
|
position: 'absolute',
|
|
965
985
|
left: '64px',
|
|
@@ -978,6 +998,15 @@ const DrawingToolbar = () => {
|
|
|
978
998
|
const { activeTool, cursorType, strokeColor, strokeWidth, setActiveTool, setCursorType, setStrokeColor, setStrokeWidth, clearDrawings, setSelectedEmoji, setIsDrawing } = useChartStore();
|
|
979
999
|
const [openMenu, setOpenMenu] = useState(null);
|
|
980
1000
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
|
1001
|
+
const [isCompact, setIsCompact] = useState(false);
|
|
1002
|
+
useEffect(() => {
|
|
1003
|
+
const handleResize = () => {
|
|
1004
|
+
setIsCompact(window.innerWidth < 768);
|
|
1005
|
+
};
|
|
1006
|
+
handleResize();
|
|
1007
|
+
window.addEventListener('resize', handleResize);
|
|
1008
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
1009
|
+
}, []);
|
|
981
1010
|
const LINE_COLORS = ['#8ab4ff', '#f472b6', '#34d399', '#facc15', '#f87171', '#e2e8f0'];
|
|
982
1011
|
const WIDTH_OPTIONS = [1, 1.5, 2.5, 3.5];
|
|
983
1012
|
const handleSelectTool = (tool) => {
|
|
@@ -1073,14 +1102,27 @@ const DrawingToolbar = () => {
|
|
|
1073
1102
|
title: 'Limpiar todo'
|
|
1074
1103
|
}
|
|
1075
1104
|
];
|
|
1076
|
-
|
|
1105
|
+
const filteredButtons = isCompact
|
|
1106
|
+
? buttonConfig.filter(btn => !['pitchforks', 'measurements'].includes(btn.key))
|
|
1107
|
+
: buttonConfig;
|
|
1108
|
+
const buildPopoverStyle = (custom) => ({
|
|
1109
|
+
...popoverStyle,
|
|
1110
|
+
...(isCompact
|
|
1111
|
+
? { left: '0px', top: 'calc(100% + 12px)', transform: 'none', width: 'min(360px, 92vw)' }
|
|
1112
|
+
: {}),
|
|
1113
|
+
...custom
|
|
1114
|
+
});
|
|
1115
|
+
return (React.createElement("div", { style: { position: 'relative', height: '100%', width: '100%' } },
|
|
1077
1116
|
React.createElement("div", { style: {
|
|
1078
|
-
height: '100%',
|
|
1117
|
+
height: isCompact ? 'auto' : '100%',
|
|
1118
|
+
width: '100%',
|
|
1079
1119
|
display: 'flex',
|
|
1080
|
-
flexDirection: 'column',
|
|
1120
|
+
flexDirection: isCompact ? 'row' : 'column',
|
|
1081
1121
|
alignItems: 'center',
|
|
1082
|
-
gap: '12px'
|
|
1083
|
-
|
|
1122
|
+
gap: '12px',
|
|
1123
|
+
overflowX: isCompact ? 'auto' : 'visible',
|
|
1124
|
+
padding: isCompact ? '12px 6px' : 0
|
|
1125
|
+
} }, filteredButtons.map((btn) => (React.createElement("div", { key: btn.key, style: { position: 'relative' } },
|
|
1084
1126
|
React.createElement("button", { onClick: btn.onClick, title: btn.title, "aria-label": btn.title, style: {
|
|
1085
1127
|
width: '46px',
|
|
1086
1128
|
height: '46px',
|
|
@@ -1095,7 +1137,7 @@ const DrawingToolbar = () => {
|
|
|
1095
1137
|
fontSize: '18px',
|
|
1096
1138
|
transition: 'all 0.2s'
|
|
1097
1139
|
} }, btn.icon),
|
|
1098
|
-
btn.key === 'cursor' && openMenu === 'cursor' && (React.createElement("div", { style: {
|
|
1140
|
+
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: {
|
|
1099
1141
|
width: '84px',
|
|
1100
1142
|
height: '60px',
|
|
1101
1143
|
borderRadius: '12px',
|
|
@@ -1111,7 +1153,7 @@ const DrawingToolbar = () => {
|
|
|
1111
1153
|
}, title: option.label },
|
|
1112
1154
|
React.createElement("span", { style: { fontSize: '18px', display: 'flex', alignItems: 'center', justifyContent: 'center' } }, option.icon),
|
|
1113
1155
|
React.createElement("span", { style: { fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase' } }, option.label)))))),
|
|
1114
|
-
btn.key === 'lines' && openMenu === 'lines' && (React.createElement("div", { style: {
|
|
1156
|
+
btn.key === 'lines' && openMenu === 'lines' && (React.createElement("div", { style: buildPopoverStyle({ width: isCompact ? 'min(360px, 92vw)' : '280px' }) },
|
|
1115
1157
|
React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Lines"),
|
|
1116
1158
|
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: {
|
|
1117
1159
|
borderRadius: '12px',
|
|
@@ -1151,7 +1193,7 @@ const DrawingToolbar = () => {
|
|
|
1151
1193
|
} },
|
|
1152
1194
|
width,
|
|
1153
1195
|
"px"))))))),
|
|
1154
|
-
btn.key === 'shapes' && openMenu === 'shapes' && (React.createElement("div", { style: {
|
|
1196
|
+
btn.key === 'shapes' && openMenu === 'shapes' && (React.createElement("div", { style: buildPopoverStyle({ width: isCompact ? 'min(320px, 92vw)' : '200px' }) },
|
|
1155
1197
|
React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Shapes"),
|
|
1156
1198
|
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: {
|
|
1157
1199
|
borderRadius: '12px',
|
|
@@ -1168,7 +1210,7 @@ const DrawingToolbar = () => {
|
|
|
1168
1210
|
}, title: option.label },
|
|
1169
1211
|
React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
|
|
1170
1212
|
React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em' } }, option.label))))))),
|
|
1171
|
-
btn.key === 'channels' && openMenu === 'channels' && (React.createElement("div", { style: {
|
|
1213
|
+
btn.key === 'channels' && openMenu === 'channels' && (React.createElement("div", { style: buildPopoverStyle({ width: isCompact ? 'min(340px, 92vw)' : '220px' }) },
|
|
1172
1214
|
React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Channels"),
|
|
1173
1215
|
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: {
|
|
1174
1216
|
borderRadius: '12px',
|
|
@@ -1185,7 +1227,7 @@ const DrawingToolbar = () => {
|
|
|
1185
1227
|
}, title: option.label },
|
|
1186
1228
|
React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
|
|
1187
1229
|
React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em' } }, option.label))))))),
|
|
1188
|
-
btn.key === 'pitchforks' && openMenu === 'pitchforks' && (React.createElement("div", { style: {
|
|
1230
|
+
btn.key === 'pitchforks' && openMenu === 'pitchforks' && (React.createElement("div", { style: buildPopoverStyle({ width: isCompact ? 'min(320px, 92vw)' : '200px' }) },
|
|
1189
1231
|
React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Pitchforks"),
|
|
1190
1232
|
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: {
|
|
1191
1233
|
borderRadius: '12px',
|
|
@@ -1202,7 +1244,7 @@ const DrawingToolbar = () => {
|
|
|
1202
1244
|
}, title: option.label },
|
|
1203
1245
|
React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
|
|
1204
1246
|
React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', textAlign: 'center' } }, option.label))))))),
|
|
1205
|
-
btn.key === 'measurements' && openMenu === 'measurements' && (React.createElement("div", { style: {
|
|
1247
|
+
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: {
|
|
1206
1248
|
width: '64px',
|
|
1207
1249
|
height: '54px',
|
|
1208
1250
|
borderRadius: '12px',
|
|
@@ -1219,41 +1261,24 @@ const DrawingToolbar = () => {
|
|
|
1219
1261
|
}, title: option.label },
|
|
1220
1262
|
React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
|
|
1221
1263
|
React.createElement("span", null, option.label)))))),
|
|
1222
|
-
btn.key === 'emoji' && showEmojiPicker && (React.createElement("div", { style: {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
transform: 'translateY(-50%)',
|
|
1227
|
-
zIndex: 1100,
|
|
1228
|
-
background: '#020617',
|
|
1229
|
-
borderRadius: '14px',
|
|
1230
|
-
overflow: 'hidden',
|
|
1231
|
-
boxShadow: '0 18px 40px rgba(0,0,0,0.65)',
|
|
1232
|
-
padding: '10px',
|
|
1233
|
-
border: '1px solid #1f2937'
|
|
1234
|
-
} },
|
|
1235
|
-
React.createElement("div", { style: {
|
|
1236
|
-
display: 'grid',
|
|
1237
|
-
gridTemplateColumns: 'repeat(3, 1fr)',
|
|
1238
|
-
gap: '6px'
|
|
1239
|
-
} }, EMOJIS.map((emoji) => (React.createElement("button", { key: emoji, onClick: () => {
|
|
1240
|
-
setSelectedEmoji(emoji);
|
|
1264
|
+
btn.key === 'emoji' && showEmojiPicker && (React.createElement("div", { style: buildPopoverStyle({ width: isCompact ? 'min(320px, 92vw)' : '220px' }) },
|
|
1265
|
+
React.createElement("p", { style: { margin: '0 0 6px', fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Emojis"),
|
|
1266
|
+
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: () => {
|
|
1267
|
+
setSelectedEmoji(option.value);
|
|
1241
1268
|
setActiveTool('icon');
|
|
1242
1269
|
setShowEmojiPicker(false);
|
|
1243
1270
|
}, style: {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
background: '#111827',
|
|
1271
|
+
height: '48px',
|
|
1272
|
+
borderRadius: '12px',
|
|
1247
1273
|
border: '1px solid #1f2937',
|
|
1248
|
-
|
|
1249
|
-
|
|
1274
|
+
background: 'rgba(2,6,23,0.65)',
|
|
1275
|
+
color: '#f8fafc',
|
|
1250
1276
|
display: 'flex',
|
|
1251
1277
|
alignItems: 'center',
|
|
1252
1278
|
justifyContent: 'center',
|
|
1253
|
-
fontSize: '
|
|
1254
|
-
|
|
1255
|
-
} },
|
|
1256
|
-
React.createElement("span", null, emoji)))))))))))));
|
|
1279
|
+
fontSize: '22px',
|
|
1280
|
+
cursor: 'pointer'
|
|
1281
|
+
}, title: option.label, "aria-label": option.label }, option.icon))))))))))));
|
|
1257
1282
|
};
|
|
1258
1283
|
|
|
1259
1284
|
const drawRoundedRect = (ctx, x, y, width, height, radius = 8) => {
|
|
@@ -1990,23 +2015,23 @@ const LINE_TOOL_TYPES = [
|
|
|
1990
2015
|
const LINE_TOOL_SET = new Set(LINE_TOOL_TYPES);
|
|
1991
2016
|
const THEME_PRESETS = {
|
|
1992
2017
|
dark: {
|
|
1993
|
-
pageBg: '#
|
|
1994
|
-
heroFrom: '#
|
|
1995
|
-
heroTo: '#
|
|
1996
|
-
panelBg: 'rgba(
|
|
1997
|
-
panelBorder: '#
|
|
1998
|
-
cardBg: 'rgba(
|
|
1999
|
-
cardBorder: '#
|
|
2018
|
+
pageBg: '#000000',
|
|
2019
|
+
heroFrom: '#050505',
|
|
2020
|
+
heroTo: '#000000',
|
|
2021
|
+
panelBg: 'rgba(0,0,0,0.92)',
|
|
2022
|
+
panelBorder: '#0f0f0f',
|
|
2023
|
+
cardBg: 'rgba(0,0,0,0.88)',
|
|
2024
|
+
cardBorder: '#050505',
|
|
2000
2025
|
textPrimary: '#f8fafc',
|
|
2001
|
-
textSecondary: '#
|
|
2026
|
+
textSecondary: '#9ca3af',
|
|
2002
2027
|
accent: '#2563eb',
|
|
2003
|
-
accentSoft: 'rgba(37,99,235,0.
|
|
2004
|
-
railBg: 'rgba(
|
|
2005
|
-
railBorder: '#
|
|
2006
|
-
plotBg: '#
|
|
2007
|
-
plotBorder: '#
|
|
2008
|
-
scaleBg: 'rgba(
|
|
2009
|
-
overlayBg: 'rgba(
|
|
2028
|
+
accentSoft: 'rgba(37,99,235,0.25)',
|
|
2029
|
+
railBg: 'rgba(0,0,0,0.9)',
|
|
2030
|
+
railBorder: '#080808',
|
|
2031
|
+
plotBg: '#010101',
|
|
2032
|
+
plotBorder: '#0d0d0d',
|
|
2033
|
+
scaleBg: 'rgba(0,0,0,0.92)',
|
|
2034
|
+
overlayBg: 'rgba(0,0,0,0.85)'
|
|
2010
2035
|
},
|
|
2011
2036
|
blue: {
|
|
2012
2037
|
pageBg: '#060e1f',
|
|
@@ -2058,6 +2083,10 @@ const CUSTOM_THEME_FIELDS = [
|
|
|
2058
2083
|
const MIN_CANDLE_PX = 4;
|
|
2059
2084
|
const MAX_CANDLE_PX = 28;
|
|
2060
2085
|
const BUFFER_FRACTION = 0.18;
|
|
2086
|
+
const LEFT_OFFSET_BARS = 2;
|
|
2087
|
+
const RIGHT_OFFSET_BARS = 18;
|
|
2088
|
+
const BAR_BODY_RATIO = 0.78;
|
|
2089
|
+
const GRID_DIVISIONS = 10;
|
|
2061
2090
|
const INERTIA_DURATION_MS = 900;
|
|
2062
2091
|
const ZOOM_MIN = 0.2;
|
|
2063
2092
|
const ZOOM_MAX = 6;
|
|
@@ -2065,6 +2094,42 @@ const easeOutQuad = (t) => 1 - (1 - t) * (1 - t);
|
|
|
2065
2094
|
const easeInOutCubic = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
2066
2095
|
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
2067
2096
|
const alignStroke = (value) => Math.round(value) + 0.5;
|
|
2097
|
+
const determinePriceFormat = (reference) => {
|
|
2098
|
+
const value = Math.abs(reference);
|
|
2099
|
+
if (!Number.isFinite(value) || value === 0)
|
|
2100
|
+
return { min: 2, max: 6 };
|
|
2101
|
+
if (value < 0.0001)
|
|
2102
|
+
return { min: 6, max: 8 };
|
|
2103
|
+
if (value < 0.001)
|
|
2104
|
+
return { min: 5, max: 7 };
|
|
2105
|
+
if (value < 0.01)
|
|
2106
|
+
return { min: 4, max: 6 };
|
|
2107
|
+
if (value < 0.1)
|
|
2108
|
+
return { min: 4, max: 5 };
|
|
2109
|
+
if (value < 1)
|
|
2110
|
+
return { min: 3, max: 4 };
|
|
2111
|
+
if (value < 100)
|
|
2112
|
+
return { min: 2, max: 3 };
|
|
2113
|
+
return { min: 2, max: 2 };
|
|
2114
|
+
};
|
|
2115
|
+
const getEffectiveBarCount = (count) => Math.max(count + LEFT_OFFSET_BARS + RIGHT_OFFSET_BARS, 1);
|
|
2116
|
+
const getCandleStep = (width, count) => (width <= 0 ? 0 : width / getEffectiveBarCount(count));
|
|
2117
|
+
const getCandleCenter = (index, width, count) => {
|
|
2118
|
+
const step = getCandleStep(width, count);
|
|
2119
|
+
return (index + LEFT_OFFSET_BARS + 0.5) * step;
|
|
2120
|
+
};
|
|
2121
|
+
const clampPixelToChart = (pixel, width) => Math.max(0, Math.min(width, pixel));
|
|
2122
|
+
const pixelToCandleIndex = (pixel, width, count) => {
|
|
2123
|
+
const step = getCandleStep(width, count);
|
|
2124
|
+
if (step === 0)
|
|
2125
|
+
return 0;
|
|
2126
|
+
return pixel / step - LEFT_OFFSET_BARS - 0.5;
|
|
2127
|
+
};
|
|
2128
|
+
const clampCandleIndex = (value, length) => {
|
|
2129
|
+
if (length <= 0)
|
|
2130
|
+
return 0;
|
|
2131
|
+
return clamp(Math.round(value), 0, Math.max(length - 1, 0));
|
|
2132
|
+
};
|
|
2068
2133
|
const createParallelPoints = (start, end, offset) => {
|
|
2069
2134
|
const dx = end.x - start.x;
|
|
2070
2135
|
const dy = end.y - start.y;
|
|
@@ -2154,7 +2219,7 @@ const computeVisibleWindow = (dataset, chartWidth, timeframe, zoomLevel, panOffs
|
|
|
2154
2219
|
}
|
|
2155
2220
|
};
|
|
2156
2221
|
};
|
|
2157
|
-
const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showStats = true, showHeaderStats = true }) => {
|
|
2222
|
+
const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showStats = true, showHeaderStats = true, showDrawingToolbar = true }) => {
|
|
2158
2223
|
const chartRef = useRef(null);
|
|
2159
2224
|
const volumeRef = useRef(null);
|
|
2160
2225
|
const gridRef = useRef(null);
|
|
@@ -2293,7 +2358,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2293
2358
|
if (!chartWidth)
|
|
2294
2359
|
return;
|
|
2295
2360
|
const candles = getVisibleCandles();
|
|
2296
|
-
const pxPerCandle = chartWidth / Math.max(candles.length || 1, 1);
|
|
2361
|
+
const pxPerCandle = chartWidth / Math.max(getEffectiveBarCount(candles.length || 1), 1);
|
|
2297
2362
|
if (!isFinite(pxPerCandle) || pxPerCandle === 0)
|
|
2298
2363
|
return;
|
|
2299
2364
|
const deltaOffset = deltaPx / Math.max(pxPerCandle, 1e-3);
|
|
@@ -2417,6 +2482,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2417
2482
|
const [priceLabels, setPriceLabels] = useState([]);
|
|
2418
2483
|
const [timeLabels, setTimeLabels] = useState([]);
|
|
2419
2484
|
const [clickedPrice, setClickedPrice] = useState(null);
|
|
2485
|
+
const [crosshairMeta, setCrosshairMeta] = useState(null);
|
|
2420
2486
|
const [showConfigPanel, setShowConfigPanel] = useState(false);
|
|
2421
2487
|
const [language, setLanguage] = useState('en');
|
|
2422
2488
|
const [colorScheme, setColorScheme] = useState('green');
|
|
@@ -2447,6 +2513,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2447
2513
|
const [showNoteModal, setShowNoteModal] = useState(false);
|
|
2448
2514
|
const [pendingNotePoint, setPendingNotePoint] = useState(null);
|
|
2449
2515
|
const [noteDraft, setNoteDraft] = useState('');
|
|
2516
|
+
const [priceFormat, setPriceFormat] = useState({ min: 2, max: 4 });
|
|
2450
2517
|
const [isMobile, setIsMobile] = useState(false);
|
|
2451
2518
|
useEffect(() => {
|
|
2452
2519
|
if (!plotAreaRef.current)
|
|
@@ -2469,7 +2536,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2469
2536
|
return () => window.clearTimeout(id);
|
|
2470
2537
|
}, [seriesType, visibleData.length]);
|
|
2471
2538
|
const locale = language === 'es' ? 'es-ES' : 'en-US';
|
|
2472
|
-
const numberFormatter = useMemo(() => new Intl.NumberFormat(locale, { minimumFractionDigits:
|
|
2539
|
+
const numberFormatter = useMemo(() => new Intl.NumberFormat(locale, { minimumFractionDigits: priceFormat.min, maximumFractionDigits: priceFormat.max }), [locale, priceFormat]);
|
|
2473
2540
|
const shortNumberFormatter = useMemo(() => new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }), [locale]);
|
|
2474
2541
|
const timeFormatter = useMemo(() => new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit' }), [locale]);
|
|
2475
2542
|
const latencyMs = useMemo(() => Math.round(20 + Math.random() * 15), []);
|
|
@@ -2531,9 +2598,14 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2531
2598
|
const priceRange = Math.max(1e-6, maxPrice - minPrice);
|
|
2532
2599
|
const maxVolume = Math.max(bounds.maxVolume, 1);
|
|
2533
2600
|
priceWindowRef.current = { min: minPrice, max: maxPrice };
|
|
2601
|
+
const referenceMagnitude = Math.min(Math.abs(minPrice), Math.abs(maxPrice)) || Math.max(Math.abs(minPrice), Math.abs(maxPrice));
|
|
2602
|
+
const suggestedFormat = determinePriceFormat(referenceMagnitude);
|
|
2603
|
+
setPriceFormat((prev) => (prev.min === suggestedFormat.min && prev.max === suggestedFormat.max ? prev : suggestedFormat));
|
|
2534
2604
|
const chartWidth = Math.max(1, cssWidth - priceScaleWidth);
|
|
2535
2605
|
const chartHeight = Math.max(1, cssHeight - timeScaleHeight - volumeHeight);
|
|
2536
|
-
const
|
|
2606
|
+
const candleStep = getCandleStep(chartWidth, candles.length);
|
|
2607
|
+
const candleWidth = Math.max(1, candleStep * BAR_BODY_RATIO);
|
|
2608
|
+
const centerX = (index) => getCandleCenter(index, chartWidth, candles.length);
|
|
2537
2609
|
// Generate price labels
|
|
2538
2610
|
const priceLabelsArray = [];
|
|
2539
2611
|
for (let i = 0; i <= 10; i++) {
|
|
@@ -2568,9 +2640,11 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2568
2640
|
gridCtx.lineTo(chartWidth, y);
|
|
2569
2641
|
gridCtx.stroke();
|
|
2570
2642
|
}
|
|
2571
|
-
const timeStep = Math.max(1, Math.floor(candles.length /
|
|
2643
|
+
const timeStep = Math.max(1, Math.floor(Math.max(candles.length, 1) / GRID_DIVISIONS));
|
|
2572
2644
|
for (let i = 0; i <= candles.length; i += timeStep) {
|
|
2573
|
-
const x = alignStroke(i *
|
|
2645
|
+
const x = alignStroke((i + LEFT_OFFSET_BARS) * candleStep);
|
|
2646
|
+
if (x < 0 || x > chartWidth)
|
|
2647
|
+
continue;
|
|
2574
2648
|
gridCtx.beginPath();
|
|
2575
2649
|
gridCtx.moveTo(x, 0);
|
|
2576
2650
|
gridCtx.lineTo(x, chartHeight);
|
|
@@ -2596,7 +2670,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2596
2670
|
: minPrice;
|
|
2597
2671
|
chartCtx.beginPath();
|
|
2598
2672
|
candles.forEach((candle, index) => {
|
|
2599
|
-
const x = index
|
|
2673
|
+
const x = centerX(index);
|
|
2600
2674
|
const yClose = ((maxPrice - candle.close) / priceRange) * chartHeight;
|
|
2601
2675
|
if (index === 0) {
|
|
2602
2676
|
chartCtx.moveTo(x, yClose);
|
|
@@ -2604,6 +2678,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2604
2678
|
else if (seriesType === 'step') {
|
|
2605
2679
|
const prev = candles[index - 1];
|
|
2606
2680
|
if (prev) {
|
|
2681
|
+
centerX(index - 1);
|
|
2607
2682
|
const prevY = ((maxPrice - prev.close) / priceRange) * chartHeight;
|
|
2608
2683
|
chartCtx.lineTo(x, prevY);
|
|
2609
2684
|
chartCtx.lineTo(x, yClose);
|
|
@@ -2619,7 +2694,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2619
2694
|
chartCtx.stroke();
|
|
2620
2695
|
if (seriesType === 'line-markers') {
|
|
2621
2696
|
candles.forEach((candle, index) => {
|
|
2622
|
-
const x = index
|
|
2697
|
+
const x = centerX(index);
|
|
2623
2698
|
const yClose = ((maxPrice - candle.close) / priceRange) * chartHeight;
|
|
2624
2699
|
chartCtx.beginPath();
|
|
2625
2700
|
chartCtx.arc(x, yClose, 2.5, 0, 2 * Math.PI);
|
|
@@ -2628,8 +2703,8 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2628
2703
|
});
|
|
2629
2704
|
}
|
|
2630
2705
|
if (seriesType === 'area' || seriesType === 'hlc-area' || seriesType === 'baseline') {
|
|
2631
|
-
chartCtx.lineTo((candles.length - 1)
|
|
2632
|
-
chartCtx.lineTo(0
|
|
2706
|
+
chartCtx.lineTo(centerX(candles.length - 1), ((maxPrice - baselinePrice) / priceRange) * chartHeight);
|
|
2707
|
+
chartCtx.lineTo(centerX(0), ((maxPrice - baselinePrice) / priceRange) * chartHeight);
|
|
2633
2708
|
chartCtx.closePath();
|
|
2634
2709
|
chartCtx.fillStyle =
|
|
2635
2710
|
seriesType === 'baseline' ? 'rgba(41,98,255,0.25)' : 'rgba(41,98,255,0.15)';
|
|
@@ -2639,7 +2714,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2639
2714
|
else {
|
|
2640
2715
|
// ----- CANDLE / BAR-BASED SERIES -----
|
|
2641
2716
|
candles.forEach((candle, index) => {
|
|
2642
|
-
const xCenter = index
|
|
2717
|
+
const xCenter = centerX(index);
|
|
2643
2718
|
const strokeX = alignStroke(xCenter);
|
|
2644
2719
|
const yHigh = ((maxPrice - candle.high) / priceRange) * chartHeight;
|
|
2645
2720
|
const yLow = ((maxPrice - candle.low) / priceRange) * chartHeight;
|
|
@@ -2724,12 +2799,12 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2724
2799
|
if (volumeCtx) {
|
|
2725
2800
|
volumeCtx.clearRect(0, 0, chartWidth, volumeHeight);
|
|
2726
2801
|
candles.forEach((candle, index) => {
|
|
2727
|
-
const
|
|
2802
|
+
const xLeft = centerX(index) - candleWidth / 2;
|
|
2728
2803
|
const barHeight = ((candle.volume || 0) / maxVolume) * volumeHeight;
|
|
2729
2804
|
const y = volumeHeight - barHeight;
|
|
2730
2805
|
const isBullish = candle.close > candle.open;
|
|
2731
2806
|
volumeCtx.fillStyle = isBullish ? bullishColor : bearishColor;
|
|
2732
|
-
volumeCtx.fillRect(
|
|
2807
|
+
volumeCtx.fillRect(xLeft + 1, y, Math.max(1, candleWidth - 2), barHeight);
|
|
2733
2808
|
});
|
|
2734
2809
|
}
|
|
2735
2810
|
// Draw indicators
|
|
@@ -2743,7 +2818,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2743
2818
|
indicatorCtx.beginPath();
|
|
2744
2819
|
indicator.data.forEach((value, index) => {
|
|
2745
2820
|
if (value !== null && value !== undefined) {
|
|
2746
|
-
const x = index
|
|
2821
|
+
const x = centerX(index);
|
|
2747
2822
|
const y = ((maxPrice - value) / priceRange) * chartHeight;
|
|
2748
2823
|
if (index === 0) {
|
|
2749
2824
|
indicatorCtx.moveTo(x, y);
|
|
@@ -2758,11 +2833,11 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2758
2833
|
else if (indicator.type === 'histogram') {
|
|
2759
2834
|
indicator.data.forEach((value, index) => {
|
|
2760
2835
|
if (value !== null && value !== undefined) {
|
|
2761
|
-
const
|
|
2836
|
+
const xLeft = centerX(index) - candleWidth / 2;
|
|
2762
2837
|
const barHeight = Math.abs(value) * chartHeight * 0.1; // Scale histogram
|
|
2763
2838
|
const y = value >= 0 ? chartHeight / 2 - barHeight : chartHeight / 2;
|
|
2764
2839
|
indicatorCtx.fillStyle = value >= 0 ? '#089981' : '#f23645';
|
|
2765
|
-
indicatorCtx.fillRect(
|
|
2840
|
+
indicatorCtx.fillRect(xLeft + 2, y, Math.max(1, candleWidth - 4), barHeight);
|
|
2766
2841
|
}
|
|
2767
2842
|
});
|
|
2768
2843
|
}
|
|
@@ -2792,32 +2867,46 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2792
2867
|
}
|
|
2793
2868
|
}, [chartWidth, chartHeight, overlayHeight]);
|
|
2794
2869
|
const handlePointerMove = useCallback((e) => {
|
|
2795
|
-
if (interactionsLocked || !visibleData.length)
|
|
2870
|
+
if (interactionsLocked || !visibleData.length) {
|
|
2871
|
+
setCrosshairMeta(null);
|
|
2796
2872
|
return;
|
|
2873
|
+
}
|
|
2797
2874
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
2798
2875
|
const x = e.clientX - rect.left;
|
|
2799
2876
|
const y = e.clientY - rect.top;
|
|
2800
2877
|
setClickedPrice(null);
|
|
2801
2878
|
if (cursorType !== 'cross') {
|
|
2879
|
+
setCrosshairMeta(null);
|
|
2802
2880
|
return;
|
|
2803
2881
|
}
|
|
2804
2882
|
if (!isDraggingRef.current) {
|
|
2805
2883
|
const { min, max } = priceWindowRef.current;
|
|
2806
|
-
const
|
|
2807
|
-
const
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2884
|
+
const rawIndex = pixelToCandleIndex(x, chartWidth, visibleData.length);
|
|
2885
|
+
const hoveredIndex = clampCandleIndex(rawIndex, visibleData.length);
|
|
2886
|
+
const hoveredCandle = visibleData[hoveredIndex];
|
|
2887
|
+
const snappedPrice = coordsRef.current.snapToPrice(y, chartHeight, min, max);
|
|
2888
|
+
const yPixel = coordsRef.current.priceToPixel(snappedPrice, chartHeight, min, max);
|
|
2889
|
+
const xPixel = magnetEnabled
|
|
2890
|
+
? getCandleCenter(hoveredIndex, chartWidth, visibleData.length)
|
|
2891
|
+
: clampPixelToChart(x, chartWidth);
|
|
2892
|
+
mousePosRef.current = { x: xPixel, y: yPixel };
|
|
2893
|
+
setCrosshairMeta({
|
|
2894
|
+
price: snappedPrice,
|
|
2895
|
+
timestamp: hoveredCandle?.timestamp ?? null,
|
|
2896
|
+
x: xPixel,
|
|
2897
|
+
y: yPixel,
|
|
2898
|
+
candle: hoveredCandle ?? null
|
|
2899
|
+
});
|
|
2812
2900
|
showCrosshairRef.current = true;
|
|
2813
2901
|
requestAnimationFrame(drawCrosshair);
|
|
2814
2902
|
}
|
|
2815
|
-
}, [visibleData, cursorType, chartWidth, chartHeight, drawCrosshair, interactionsLocked]);
|
|
2903
|
+
}, [visibleData, cursorType, chartWidth, chartHeight, drawCrosshair, interactionsLocked, magnetEnabled]);
|
|
2816
2904
|
const handleMouseLeave = useCallback(() => {
|
|
2817
2905
|
showCrosshairRef.current = false;
|
|
2818
2906
|
isDraggingRef.current = false;
|
|
2819
2907
|
dragSampleRef.current.velocity = 0;
|
|
2820
2908
|
dragSampleRef.current.lastTs = 0;
|
|
2909
|
+
setCrosshairMeta(null);
|
|
2821
2910
|
requestAnimationFrame(() => {
|
|
2822
2911
|
const overlayCtx = overlayRef.current?.getContext('2d');
|
|
2823
2912
|
if (overlayCtx) {
|
|
@@ -2861,15 +2950,21 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
2861
2950
|
? { minPrice: priceWindowRef.current.min, maxPrice: priceWindowRef.current.max }
|
|
2862
2951
|
: getDataBounds(targetData);
|
|
2863
2952
|
const dataLength = Math.max(targetData.length, 1);
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2953
|
+
let snappedPrice = coordsRef.current.snapToPrice(y, chartHeight, bounds.minPrice, bounds.maxPrice, 0.5);
|
|
2954
|
+
let snappedY = y;
|
|
2955
|
+
if (magnetEnabled && targetData.length) {
|
|
2956
|
+
const snappedIndex = clampCandleIndex(pixelToCandleIndex(x, chartWidth, dataLength), dataLength);
|
|
2957
|
+
x = getCandleCenter(snappedIndex, chartWidth, dataLength);
|
|
2958
|
+
snappedY = coordsRef.current.priceToPixel(snappedPrice, chartHeight, bounds.minPrice, bounds.maxPrice);
|
|
2959
|
+
y = snappedY;
|
|
2960
|
+
}
|
|
2961
|
+
else {
|
|
2962
|
+
x = clampPixelToChart(x, chartWidth);
|
|
2963
|
+
}
|
|
2964
|
+
const price = coordsRef.current.pixelToPrice(snappedY, chartHeight, bounds.minPrice, bounds.maxPrice);
|
|
2965
|
+
const hoveredIndex = clampCandleIndex(pixelToCandleIndex(x, chartWidth, dataLength), dataLength);
|
|
2966
|
+
const time = targetData[hoveredIndex]?.timestamp ?? hoveredIndex;
|
|
2967
|
+
return { x, y: snappedY, price, time };
|
|
2873
2968
|
}, [visibleData, storeData, magnetEnabled, chartWidth, chartHeight, interactionsLocked]);
|
|
2874
2969
|
const tryEraseDrawing = useCallback((x, y) => {
|
|
2875
2970
|
for (let i = drawings.length - 1; i >= 0; i--) {
|
|
@@ -3080,7 +3175,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
3080
3175
|
}
|
|
3081
3176
|
if (visibleData.length) {
|
|
3082
3177
|
const { min, max } = priceWindowRef.current;
|
|
3083
|
-
const snappedIndex =
|
|
3178
|
+
const snappedIndex = clampCandleIndex(pixelToCandleIndex(rawX, chartWidth, visibleData.length), visibleData.length);
|
|
3084
3179
|
setSelectedCandleIndex(snappedIndex);
|
|
3085
3180
|
const price = coordsRef.current.pixelToPrice(rawY, chartHeight, min, max);
|
|
3086
3181
|
setClickedPrice({ x: rawX, y: rawY, price });
|
|
@@ -3606,7 +3701,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
3606
3701
|
minHeight: 0,
|
|
3607
3702
|
alignItems: 'stretch'
|
|
3608
3703
|
} },
|
|
3609
|
-
React.createElement("div", { style: { width: isMobile ? '60px' : '88px', flexShrink: 0, height: '100%' } },
|
|
3704
|
+
showDrawingToolbar && (React.createElement("div", { style: { width: isMobile ? '60px' : '88px', flexShrink: 0, height: '100%' } },
|
|
3610
3705
|
React.createElement("div", { style: {
|
|
3611
3706
|
height: '100%',
|
|
3612
3707
|
borderRadius: '24px',
|
|
@@ -3617,7 +3712,7 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
3617
3712
|
alignItems: 'center',
|
|
3618
3713
|
padding: isMobile ? '12px 0' : '16px 0'
|
|
3619
3714
|
} },
|
|
3620
|
-
React.createElement(DrawingToolbar, null))),
|
|
3715
|
+
React.createElement(DrawingToolbar, null)))),
|
|
3621
3716
|
React.createElement("div", { ref: plotAreaRef, style: {
|
|
3622
3717
|
flex: 1,
|
|
3623
3718
|
position: 'relative',
|
|
@@ -3658,6 +3753,65 @@ const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange, showSt
|
|
|
3658
3753
|
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' } },
|
|
3659
3754
|
React.createElement("span", { style: { textTransform: 'uppercase', fontSize: '10px', letterSpacing: '0.08em' } }, strings.axis.time),
|
|
3660
3755
|
React.createElement("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', color: activeTheme.textPrimary } }, timeLabels.map((label, i) => (React.createElement("div", { key: i }, label))))),
|
|
3756
|
+
crosshairMeta?.candle && (React.createElement("div", { style: {
|
|
3757
|
+
position: 'absolute',
|
|
3758
|
+
top: 16,
|
|
3759
|
+
left: 16,
|
|
3760
|
+
padding: '10px 14px',
|
|
3761
|
+
borderRadius: '14px',
|
|
3762
|
+
background: activeTheme.overlayBg,
|
|
3763
|
+
border: `1px solid ${activeTheme.panelBorder}`,
|
|
3764
|
+
boxShadow: '0 12px 30px rgba(0,0,0,0.45)',
|
|
3765
|
+
color: activeTheme.textPrimary,
|
|
3766
|
+
fontSize: '11px',
|
|
3767
|
+
letterSpacing: '0.04em',
|
|
3768
|
+
display: 'flex',
|
|
3769
|
+
gap: '14px',
|
|
3770
|
+
pointerEvents: 'none',
|
|
3771
|
+
zIndex: 60
|
|
3772
|
+
} },
|
|
3773
|
+
([
|
|
3774
|
+
{ label: 'O', value: crosshairMeta.candle.open },
|
|
3775
|
+
{ label: 'H', value: crosshairMeta.candle.high },
|
|
3776
|
+
{ label: 'L', value: crosshairMeta.candle.low },
|
|
3777
|
+
{ label: 'C', value: crosshairMeta.candle.close }
|
|
3778
|
+
]).map((item) => (React.createElement("div", { key: item.label, style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
3779
|
+
React.createElement("span", { style: { color: activeTheme.textSecondary } }, item.label),
|
|
3780
|
+
React.createElement("span", { style: { fontWeight: 600 } }, numberFormatter.format(item.value))))),
|
|
3781
|
+
React.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
3782
|
+
React.createElement("span", { style: { color: activeTheme.textSecondary } }, "V"),
|
|
3783
|
+
React.createElement("span", { style: { fontWeight: 600 } }, shortNumberFormatter.format(crosshairMeta.candle.volume ?? 0))))),
|
|
3784
|
+
crosshairMeta && (React.createElement(React.Fragment, null,
|
|
3785
|
+
React.createElement("div", { style: {
|
|
3786
|
+
position: 'absolute',
|
|
3787
|
+
right: `${priceScaleWidth}px`,
|
|
3788
|
+
top: `${Math.min(Math.max(crosshairMeta.y, 12), overlayHeight - 12)}px`,
|
|
3789
|
+
transform: 'translate(0, -50%)',
|
|
3790
|
+
background: activeTheme.scaleBg,
|
|
3791
|
+
border: `1px solid ${activeTheme.panelBorder}`,
|
|
3792
|
+
borderRadius: '6px 0 0 6px',
|
|
3793
|
+
padding: '2px 8px',
|
|
3794
|
+
fontSize: '11px',
|
|
3795
|
+
fontWeight: 600,
|
|
3796
|
+
color: activeTheme.textPrimary,
|
|
3797
|
+
pointerEvents: 'none',
|
|
3798
|
+
boxShadow: '0 8px 20px rgba(0,0,0,0.4)'
|
|
3799
|
+
} }, numberFormatter.format(crosshairMeta.price)),
|
|
3800
|
+
crosshairMeta.timestamp && (React.createElement("div", { style: {
|
|
3801
|
+
position: 'absolute',
|
|
3802
|
+
left: `${Math.min(Math.max(crosshairMeta.x, 40), chartWidth - 40)}px`,
|
|
3803
|
+
bottom: `${timeScaleHeight}px`,
|
|
3804
|
+
transform: 'translate(-50%, 50%)',
|
|
3805
|
+
background: activeTheme.scaleBg,
|
|
3806
|
+
border: `1px solid ${activeTheme.panelBorder}`,
|
|
3807
|
+
borderRadius: '6px',
|
|
3808
|
+
padding: '2px 8px',
|
|
3809
|
+
fontSize: '10px',
|
|
3810
|
+
letterSpacing: '0.05em',
|
|
3811
|
+
color: activeTheme.textPrimary,
|
|
3812
|
+
pointerEvents: 'none',
|
|
3813
|
+
boxShadow: '0 8px 20px rgba(0,0,0,0.35)'
|
|
3814
|
+
} }, timeFormatter.format(new Date(crosshairMeta.timestamp)))))),
|
|
3661
3815
|
clickedPrice && (React.createElement("div", { style: {
|
|
3662
3816
|
position: 'absolute',
|
|
3663
3817
|
background: '#1e222d',
|