viainti-chart 1.0.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 +127 -0
- package/dist/index.cjs +3811 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.mjs +3808 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +81 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3808 @@
|
|
|
1
|
+
import React, { useRef, useEffect, useSyncExternalStore, useState, memo, useCallback, useMemo } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
const Chart = ({ data, width = 800, height = 400 }) => {
|
|
5
|
+
const canvasRef = useRef(null);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const canvas = canvasRef.current;
|
|
8
|
+
if (!canvas)
|
|
9
|
+
return;
|
|
10
|
+
const ctx = canvas.getContext('2d');
|
|
11
|
+
if (!ctx)
|
|
12
|
+
return;
|
|
13
|
+
// Clear canvas
|
|
14
|
+
ctx.clearRect(0, 0, width, height);
|
|
15
|
+
if (data.length === 0)
|
|
16
|
+
return;
|
|
17
|
+
// Find min and max values
|
|
18
|
+
const highs = data.map((d) => d.high);
|
|
19
|
+
const lows = data.map((d) => d.low);
|
|
20
|
+
const minPrice = Math.min(...lows);
|
|
21
|
+
const maxPrice = Math.max(...highs);
|
|
22
|
+
const priceRange = maxPrice - minPrice;
|
|
23
|
+
const candleWidth = width / data.length;
|
|
24
|
+
data.forEach((candle, index) => {
|
|
25
|
+
const x = index * candleWidth;
|
|
26
|
+
const yHigh = ((maxPrice - candle.high) / priceRange) * height;
|
|
27
|
+
const yLow = ((maxPrice - candle.low) / priceRange) * height;
|
|
28
|
+
const yOpen = ((maxPrice - candle.open) / priceRange) * height;
|
|
29
|
+
const yClose = ((maxPrice - candle.close) / priceRange) * height;
|
|
30
|
+
// Draw high-low line
|
|
31
|
+
ctx.strokeStyle = 'black';
|
|
32
|
+
ctx.beginPath();
|
|
33
|
+
ctx.moveTo(x + candleWidth / 2, yHigh);
|
|
34
|
+
ctx.lineTo(x + candleWidth / 2, yLow);
|
|
35
|
+
ctx.stroke();
|
|
36
|
+
// Draw open-close body
|
|
37
|
+
const bodyHeight = Math.abs(yClose - yOpen);
|
|
38
|
+
const bodyY = Math.min(yOpen, yClose);
|
|
39
|
+
const isGreen = candle.close > candle.open;
|
|
40
|
+
ctx.fillStyle = isGreen ? 'green' : 'red';
|
|
41
|
+
ctx.fillRect(x + candleWidth * 0.1, bodyY, candleWidth * 0.8, bodyHeight);
|
|
42
|
+
// Draw open/close wicks if body is small
|
|
43
|
+
if (bodyHeight < 1) {
|
|
44
|
+
ctx.strokeStyle = isGreen ? 'green' : 'red';
|
|
45
|
+
ctx.beginPath();
|
|
46
|
+
ctx.moveTo(x + candleWidth / 2, yOpen);
|
|
47
|
+
ctx.lineTo(x + candleWidth / 2, yClose);
|
|
48
|
+
ctx.stroke();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}, [data, width, height]);
|
|
52
|
+
return React.createElement("canvas", { ref: canvasRef, width: width, height: height });
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
var DefaultContext = {
|
|
56
|
+
color: undefined,
|
|
57
|
+
size: undefined,
|
|
58
|
+
className: undefined,
|
|
59
|
+
style: undefined,
|
|
60
|
+
attr: undefined
|
|
61
|
+
};
|
|
62
|
+
var IconContext = React.createContext && /*#__PURE__*/React.createContext(DefaultContext);
|
|
63
|
+
|
|
64
|
+
var _excluded = ["attr", "size", "title"];
|
|
65
|
+
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
|
|
66
|
+
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } } return target; }
|
|
67
|
+
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
68
|
+
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
69
|
+
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), true).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
70
|
+
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
71
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
72
|
+
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
73
|
+
function Tree2Element(tree) {
|
|
74
|
+
return tree && tree.map((node, i) => /*#__PURE__*/React.createElement(node.tag, _objectSpread({
|
|
75
|
+
key: i
|
|
76
|
+
}, node.attr), Tree2Element(node.child)));
|
|
77
|
+
}
|
|
78
|
+
function GenIcon(data) {
|
|
79
|
+
return props => /*#__PURE__*/React.createElement(IconBase, _extends({
|
|
80
|
+
attr: _objectSpread({}, data.attr)
|
|
81
|
+
}, props), Tree2Element(data.child));
|
|
82
|
+
}
|
|
83
|
+
function IconBase(props) {
|
|
84
|
+
var elem = conf => {
|
|
85
|
+
var {
|
|
86
|
+
attr,
|
|
87
|
+
size,
|
|
88
|
+
title
|
|
89
|
+
} = props,
|
|
90
|
+
svgProps = _objectWithoutProperties(props, _excluded);
|
|
91
|
+
var computedSize = size || conf.size || "1em";
|
|
92
|
+
var className;
|
|
93
|
+
if (conf.className) className = conf.className;
|
|
94
|
+
if (props.className) className = (className ? className + " " : "") + props.className;
|
|
95
|
+
return /*#__PURE__*/React.createElement("svg", _extends({
|
|
96
|
+
stroke: "currentColor",
|
|
97
|
+
fill: "currentColor",
|
|
98
|
+
strokeWidth: "0"
|
|
99
|
+
}, conf.attr, attr, svgProps, {
|
|
100
|
+
className: className,
|
|
101
|
+
style: _objectSpread(_objectSpread({
|
|
102
|
+
color: props.color || conf.color
|
|
103
|
+
}, conf.style), props.style),
|
|
104
|
+
height: computedSize,
|
|
105
|
+
width: computedSize,
|
|
106
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
107
|
+
}), title && /*#__PURE__*/React.createElement("title", null, title), props.children);
|
|
108
|
+
};
|
|
109
|
+
return IconContext !== undefined ? /*#__PURE__*/React.createElement(IconContext.Consumer, null, conf => elem(conf)) : elem(DefaultContext);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// THIS FILE IS AUTO GENERATED
|
|
113
|
+
function BsArrowRepeat (props) {
|
|
114
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41m-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9"},"child":[]},{"tag":"path","attr":{"fillRule":"evenodd","d":"M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5 5 0 0 0 8 3M3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9z"},"child":[]}]})(props);
|
|
115
|
+
}function BsArrowsAngleExpand (props) {
|
|
116
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707m4.344-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707"},"child":[]}]})(props);
|
|
117
|
+
}function BsArrowsFullscreen (props) {
|
|
118
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707m4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707m0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707m-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707"},"child":[]}]})(props);
|
|
119
|
+
}function BsBarChartSteps (props) {
|
|
120
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M.5 0a.5.5 0 0 1 .5.5v15a.5.5 0 0 1-1 0V.5A.5.5 0 0 1 .5 0M2 1.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-4a.5.5 0 0 1-.5-.5zm2 4a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5zm2 4a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5zm2 4a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5z"},"child":[]}]})(props);
|
|
121
|
+
}function BsBarChart (props) {
|
|
122
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M4 11H2v3h2zm5-4H7v7h2zm5-5v12h-2V2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1z"},"child":[]}]})(props);
|
|
123
|
+
}function BsBoundingBoxCircles (props) {
|
|
124
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M2 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2M0 2a2 2 0 0 1 3.937-.5h8.126A2 2 0 1 1 14.5 3.937v8.126a2 2 0 1 1-2.437 2.437H3.937A2 2 0 1 1 1.5 12.063V3.937A2 2 0 0 1 0 2m2.5 1.937v8.126c.703.18 1.256.734 1.437 1.437h8.126a2 2 0 0 1 1.437-1.437V3.937A2 2 0 0 1 12.063 2.5H3.937A2 2 0 0 1 2.5 3.937M14 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2M2 13a1 1 0 1 0 0 2 1 1 0 0 0 0-2m12 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2"},"child":[]}]})(props);
|
|
125
|
+
}function BsCamera (props) {
|
|
126
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M15 12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.172a3 3 0 0 0 2.12-.879l.83-.828A1 1 0 0 1 6.827 3h2.344a1 1 0 0 1 .707.293l.828.828A3 3 0 0 0 12.828 5H14a1 1 0 0 1 1 1zM2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4z"},"child":[]},{"tag":"path","attr":{"d":"M8 11a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5m0 1a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7M3 6.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0"},"child":[]}]})(props);
|
|
127
|
+
}function BsCheck2 (props) {
|
|
128
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0"},"child":[]}]})(props);
|
|
129
|
+
}function BsChevronBarExpand (props) {
|
|
130
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M3.646 10.146a.5.5 0 0 1 .708 0L8 13.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708m0-4.292a.5.5 0 0 0 .708 0L8 2.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708M1 8a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13A.5.5 0 0 1 1 8"},"child":[]}]})(props);
|
|
131
|
+
}function BsClockHistory (props) {
|
|
132
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8.515 1.019A7 7 0 0 0 8 1V0a8 8 0 0 1 .589.022zm2.004.45a7 7 0 0 0-.985-.299l.219-.976q.576.129 1.126.342zm1.37.71a7 7 0 0 0-.439-.27l.493-.87a8 8 0 0 1 .979.654l-.615.789a7 7 0 0 0-.418-.302zm1.834 1.79a7 7 0 0 0-.653-.796l.724-.69q.406.429.747.91zm.744 1.352a7 7 0 0 0-.214-.468l.893-.45a8 8 0 0 1 .45 1.088l-.95.313a7 7 0 0 0-.179-.483m.53 2.507a7 7 0 0 0-.1-1.025l.985-.17q.1.58.116 1.17zm-.131 1.538q.05-.254.081-.51l.993.123a8 8 0 0 1-.23 1.155l-.964-.267q.069-.247.12-.501m-.952 2.379q.276-.436.486-.908l.914.405q-.24.54-.555 1.038zm-.964 1.205q.183-.183.35-.378l.758.653a8 8 0 0 1-.401.432z"},"child":[]},{"tag":"path","attr":{"d":"M8 1a7 7 0 1 0 4.95 11.95l.707.707A8.001 8.001 0 1 1 8 0z"},"child":[]},{"tag":"path","attr":{"d":"M7.5 3a.5.5 0 0 1 .5.5v5.21l3.248 1.856a.5.5 0 0 1-.496.868l-3.5-2A.5.5 0 0 1 7 9V3.5a.5.5 0 0 1 .5-.5"},"child":[]}]})(props);
|
|
133
|
+
}function BsCollection (props) {
|
|
134
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M2.5 3.5a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1zm2-2a.5.5 0 0 1 0-1h7a.5.5 0 0 1 0 1zM0 13a1.5 1.5 0 0 0 1.5 1.5h13A1.5 1.5 0 0 0 16 13V6a1.5 1.5 0 0 0-1.5-1.5h-13A1.5 1.5 0 0 0 0 6zm1.5.5A.5.5 0 0 1 1 13V6a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5z"},"child":[]}]})(props);
|
|
135
|
+
}function BsCursor (props) {
|
|
136
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M14.082 2.182a.5.5 0 0 1 .103.557L8.528 15.467a.5.5 0 0 1-.917-.007L5.57 10.694.803 8.652a.5.5 0 0 1-.006-.916l12.728-5.657a.5.5 0 0 1 .556.103zM2.25 8.184l3.897 1.67a.5.5 0 0 1 .262.263l1.67 3.897L12.743 3.52z"},"child":[]}]})(props);
|
|
137
|
+
}function BsDash (props) {
|
|
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
|
+
}function BsDiagram3 (props) {
|
|
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 BsEmojiSmile (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 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);
|
|
143
|
+
}function BsEyeSlash (props) {
|
|
144
|
+
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
|
+
}function BsGear (props) {
|
|
146
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0"},"child":[]},{"tag":"path","attr":{"d":"M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115z"},"child":[]}]})(props);
|
|
147
|
+
}function BsGraphDown (props) {
|
|
148
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M0 0h1v15h15v1H0zm14.817 11.887a.5.5 0 0 0 .07-.704l-4.5-5.5a.5.5 0 0 0-.74-.037L7.06 8.233 3.404 3.206a.5.5 0 0 0-.808.588l4 5.5a.5.5 0 0 0 .758.06l2.609-2.61 4.15 5.073a.5.5 0 0 0 .704.07"},"child":[]}]})(props);
|
|
149
|
+
}function BsGraphUp (props) {
|
|
150
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M0 0h1v15h15v1H0zm14.817 3.113a.5.5 0 0 1 .07.704l-4.5 5.5a.5.5 0 0 1-.74.037L7.06 6.767l-3.656 5.027a.5.5 0 0 1-.808-.588l4-5.5a.5.5 0 0 1 .758-.06l2.609 2.61 4.15-5.073a.5.5 0 0 1 .704-.07"},"child":[]}]})(props);
|
|
151
|
+
}function BsInfoCircle (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":"m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"},"child":[]}]})(props);
|
|
153
|
+
}function BsLock (props) {
|
|
154
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2m3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2M5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1"},"child":[]}]})(props);
|
|
155
|
+
}function BsMagnet (props) {
|
|
156
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M8 1a7 7 0 0 0-7 7v3h4V8a3 3 0 0 1 6 0v3h4V8a7 7 0 0 0-7-7m7 11h-4v3h4zM5 12H1v3h4zM0 8a8 8 0 1 1 16 0v8h-6V8a2 2 0 1 0-4 0v8H0z"},"child":[]}]})(props);
|
|
157
|
+
}function BsPencil (props) {
|
|
158
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"},"child":[]}]})(props);
|
|
159
|
+
}function BsQuestionCircle (props) {
|
|
160
|
+
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":"M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286m1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94"},"child":[]}]})(props);
|
|
161
|
+
}function BsSlash (props) {
|
|
162
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M11.354 4.646a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708l6-6a.5.5 0 0 1 .708 0"},"child":[]}]})(props);
|
|
163
|
+
}function BsSquare (props) {
|
|
164
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"},"child":[]}]})(props);
|
|
165
|
+
}function BsTrash (props) {
|
|
166
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"},"child":[]},{"tag":"path","attr":{"d":"M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"},"child":[]}]})(props);
|
|
167
|
+
}function BsTriangle (props) {
|
|
168
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.15.15 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.2.2 0 0 1-.054.06.1.1 0 0 1-.066.017H1.146a.1.1 0 0 1-.066-.017.2.2 0 0 1-.054-.06.18.18 0 0 1 .002-.183L7.884 2.073a.15.15 0 0 1 .054-.057m1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767z"},"child":[]}]})(props);
|
|
169
|
+
}function BsType (props) {
|
|
170
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"d":"m2.244 13.081.943-2.803H6.66l.944 2.803H8.86L5.54 3.75H4.322L1 13.081zm2.7-7.923L6.34 9.314H3.51l1.4-4.156zm9.146 7.027h.035v.896h1.128V8.125c0-1.51-1.114-2.345-2.646-2.345-1.736 0-2.59.916-2.666 2.174h1.108c.068-.718.595-1.19 1.517-1.19.971 0 1.518.52 1.518 1.464v.731H12.19c-1.647.007-2.522.8-2.522 2.058 0 1.319.957 2.18 2.345 2.18 1.06 0 1.716-.43 2.078-1.011zm-1.763.035c-.752 0-1.456-.397-1.456-1.244 0-.65.424-1.115 1.408-1.115h1.805v.834c0 .896-.752 1.525-1.757 1.525"},"child":[]}]})(props);
|
|
171
|
+
}function BsZoomIn (props) {
|
|
172
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11M13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0"},"child":[]},{"tag":"path","attr":{"d":"M10.344 11.742q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1 6.5 6.5 0 0 1-1.398 1.4z"},"child":[]},{"tag":"path","attr":{"fillRule":"evenodd","d":"M6.5 3a.5.5 0 0 1 .5.5V6h2.5a.5.5 0 0 1 0 1H7v2.5a.5.5 0 0 1-1 0V7H3.5a.5.5 0 0 1 0-1H6V3.5a.5.5 0 0 1 .5-.5"},"child":[]}]})(props);
|
|
173
|
+
}function BsZoomOut (props) {
|
|
174
|
+
return GenIcon({"attr":{"fill":"currentColor","viewBox":"0 0 16 16"},"child":[{"tag":"path","attr":{"fillRule":"evenodd","d":"M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11M13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0"},"child":[]},{"tag":"path","attr":{"d":"M10.344 11.742q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1 6.5 6.5 0 0 1-1.398 1.4z"},"child":[]},{"tag":"path","attr":{"fillRule":"evenodd","d":"M3 6.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5"},"child":[]}]})(props);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const createCoordinateSystem = () => ({
|
|
178
|
+
pixelToPrice: (pixelY, chartHeight, minPrice, maxPrice) => {
|
|
179
|
+
const priceRange = maxPrice - minPrice;
|
|
180
|
+
return maxPrice - (pixelY / chartHeight) * priceRange;
|
|
181
|
+
},
|
|
182
|
+
priceToPixel: (price, chartHeight, minPrice, maxPrice) => {
|
|
183
|
+
const priceRange = maxPrice - minPrice;
|
|
184
|
+
return ((maxPrice - price) / priceRange) * chartHeight;
|
|
185
|
+
},
|
|
186
|
+
pixelToTime: (pixelX, chartWidth, dataLength) => {
|
|
187
|
+
return Math.floor((pixelX / chartWidth) * dataLength);
|
|
188
|
+
},
|
|
189
|
+
timeToPixel: (timeIndex, chartWidth, dataLength) => {
|
|
190
|
+
return (timeIndex / dataLength) * chartWidth;
|
|
191
|
+
},
|
|
192
|
+
snapToCandle: (pixelX, chartWidth, dataLength) => {
|
|
193
|
+
const candleIndex = Math.round((pixelX / chartWidth) * dataLength);
|
|
194
|
+
return Math.max(0, Math.min(dataLength - 1, candleIndex));
|
|
195
|
+
},
|
|
196
|
+
snapToPrice: (pixelY, chartHeight, minPrice, maxPrice, step = 0.01) => {
|
|
197
|
+
const price = maxPrice - (pixelY / chartHeight) * (maxPrice - minPrice);
|
|
198
|
+
return Math.round(price / step) * step;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
const getDataBounds = (data) => {
|
|
202
|
+
if (!data.length)
|
|
203
|
+
return { minPrice: 0, maxPrice: 0, minVolume: 0, maxVolume: 0 };
|
|
204
|
+
const prices = data.flatMap(d => [d.open, d.high, d.low, d.close]);
|
|
205
|
+
const volumes = data.map(d => d.volume || 0);
|
|
206
|
+
return {
|
|
207
|
+
minPrice: Math.min(...prices),
|
|
208
|
+
maxPrice: Math.max(...prices),
|
|
209
|
+
minVolume: Math.min(...volumes),
|
|
210
|
+
maxVolume: Math.max(...volumes)
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
class Item {
|
|
215
|
+
constructor(data, prev, next) {
|
|
216
|
+
this.next = next;
|
|
217
|
+
if (next)
|
|
218
|
+
next.prev = this;
|
|
219
|
+
this.prev = prev;
|
|
220
|
+
if (prev)
|
|
221
|
+
prev.next = this;
|
|
222
|
+
this.data = data;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
class LinkedList {
|
|
226
|
+
constructor() {
|
|
227
|
+
this._length = 0;
|
|
228
|
+
}
|
|
229
|
+
get head() {
|
|
230
|
+
return this._head && this._head.data;
|
|
231
|
+
}
|
|
232
|
+
get tail() {
|
|
233
|
+
return this._tail && this._tail.data;
|
|
234
|
+
}
|
|
235
|
+
get current() {
|
|
236
|
+
return this._current && this._current.data;
|
|
237
|
+
}
|
|
238
|
+
get length() {
|
|
239
|
+
return this._length;
|
|
240
|
+
}
|
|
241
|
+
push(data) {
|
|
242
|
+
this._tail = new Item(data, this._tail);
|
|
243
|
+
if (this._length === 0) {
|
|
244
|
+
this._head = this._tail;
|
|
245
|
+
this._current = this._head;
|
|
246
|
+
this._next = this._head;
|
|
247
|
+
}
|
|
248
|
+
this._length++;
|
|
249
|
+
}
|
|
250
|
+
pop() {
|
|
251
|
+
var tail = this._tail;
|
|
252
|
+
if (this._length === 0) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this._length--;
|
|
256
|
+
if (this._length === 0) {
|
|
257
|
+
this._head = this._tail = this._current = this._next = undefined;
|
|
258
|
+
return tail.data;
|
|
259
|
+
}
|
|
260
|
+
this._tail = tail.prev;
|
|
261
|
+
this._tail.next = undefined;
|
|
262
|
+
if (this._current === tail) {
|
|
263
|
+
this._current = this._tail;
|
|
264
|
+
this._next = undefined;
|
|
265
|
+
}
|
|
266
|
+
return tail.data;
|
|
267
|
+
}
|
|
268
|
+
shift() {
|
|
269
|
+
var head = this._head;
|
|
270
|
+
if (this._length === 0) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
this._length--;
|
|
274
|
+
if (this._length === 0) {
|
|
275
|
+
this._head = this._tail = this._current = this._next = undefined;
|
|
276
|
+
return head.data;
|
|
277
|
+
}
|
|
278
|
+
this._head = this._head.next;
|
|
279
|
+
if (this._current === head) {
|
|
280
|
+
this._current = this._head;
|
|
281
|
+
this._next = this._current.next;
|
|
282
|
+
}
|
|
283
|
+
return head.data;
|
|
284
|
+
}
|
|
285
|
+
unshift(data) {
|
|
286
|
+
this._head = new Item(data, undefined, this._head);
|
|
287
|
+
if (this._length === 0) {
|
|
288
|
+
this._tail = this._head;
|
|
289
|
+
this._next = this._head;
|
|
290
|
+
}
|
|
291
|
+
this._length++;
|
|
292
|
+
}
|
|
293
|
+
unshiftCurrent() {
|
|
294
|
+
var current = this._current;
|
|
295
|
+
if (current === this._head || this._length < 2) {
|
|
296
|
+
return current && current.data;
|
|
297
|
+
}
|
|
298
|
+
// remove
|
|
299
|
+
if (current === this._tail) {
|
|
300
|
+
this._tail = current.prev;
|
|
301
|
+
this._tail.next = undefined;
|
|
302
|
+
this._current = this._tail;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
current.next.prev = current.prev;
|
|
306
|
+
current.prev.next = current.next;
|
|
307
|
+
this._current = current.prev;
|
|
308
|
+
}
|
|
309
|
+
this._next = this._current.next;
|
|
310
|
+
// unshift
|
|
311
|
+
current.next = this._head;
|
|
312
|
+
current.prev = undefined;
|
|
313
|
+
this._head.prev = current;
|
|
314
|
+
this._head = current;
|
|
315
|
+
return current.data;
|
|
316
|
+
}
|
|
317
|
+
removeCurrent() {
|
|
318
|
+
var current = this._current;
|
|
319
|
+
if (this._length === 0) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
this._length--;
|
|
323
|
+
if (this._length === 0) {
|
|
324
|
+
this._head = this._tail = this._current = this._next = undefined;
|
|
325
|
+
return current.data;
|
|
326
|
+
}
|
|
327
|
+
if (current === this._tail) {
|
|
328
|
+
this._tail = current.prev;
|
|
329
|
+
this._tail.next = undefined;
|
|
330
|
+
this._current = this._tail;
|
|
331
|
+
}
|
|
332
|
+
else if (current === this._head) {
|
|
333
|
+
this._head = current.next;
|
|
334
|
+
this._head.prev = undefined;
|
|
335
|
+
this._current = this._head;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
current.next.prev = current.prev;
|
|
339
|
+
current.prev.next = current.next;
|
|
340
|
+
this._current = current.prev;
|
|
341
|
+
}
|
|
342
|
+
this._next = this._current.next;
|
|
343
|
+
return current.data;
|
|
344
|
+
}
|
|
345
|
+
resetCursor() {
|
|
346
|
+
this._current = this._next = this._head;
|
|
347
|
+
return this;
|
|
348
|
+
}
|
|
349
|
+
next() {
|
|
350
|
+
var next = this._next;
|
|
351
|
+
if (next !== undefined) {
|
|
352
|
+
this._next = next.next;
|
|
353
|
+
this._current = next;
|
|
354
|
+
return next.data;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let config = {};
|
|
360
|
+
function getConfig(key) {
|
|
361
|
+
return config[key];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function format(v) {
|
|
365
|
+
let precision = getConfig('precision');
|
|
366
|
+
if (precision) {
|
|
367
|
+
return parseFloat(v.toPrecision(precision));
|
|
368
|
+
}
|
|
369
|
+
return v;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
class Indicator {
|
|
373
|
+
constructor(input) {
|
|
374
|
+
this.format = input.format || format;
|
|
375
|
+
}
|
|
376
|
+
static reverseInputs(input) {
|
|
377
|
+
if (input.reversedInput) {
|
|
378
|
+
input.values ? input.values.reverse() : undefined;
|
|
379
|
+
input.open ? input.open.reverse() : undefined;
|
|
380
|
+
input.high ? input.high.reverse() : undefined;
|
|
381
|
+
input.low ? input.low.reverse() : undefined;
|
|
382
|
+
input.close ? input.close.reverse() : undefined;
|
|
383
|
+
input.volume ? input.volume.reverse() : undefined;
|
|
384
|
+
input.timestamp ? input.timestamp.reverse() : undefined;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
getResult() {
|
|
388
|
+
return this.result;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
//STEP 1. Import Necessary indicator or rather last step
|
|
393
|
+
//STEP3. Add class based syntax with export
|
|
394
|
+
class SMA extends Indicator {
|
|
395
|
+
constructor(input) {
|
|
396
|
+
super(input);
|
|
397
|
+
this.period = input.period;
|
|
398
|
+
this.price = input.values;
|
|
399
|
+
var genFn = (function* (period) {
|
|
400
|
+
var list = new LinkedList();
|
|
401
|
+
var sum = 0;
|
|
402
|
+
var counter = 1;
|
|
403
|
+
var current = yield;
|
|
404
|
+
var result;
|
|
405
|
+
list.push(0);
|
|
406
|
+
while (true) {
|
|
407
|
+
if (counter < period) {
|
|
408
|
+
counter++;
|
|
409
|
+
list.push(current);
|
|
410
|
+
sum = sum + current;
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
sum = sum - list.shift() + current;
|
|
414
|
+
result = ((sum) / period);
|
|
415
|
+
list.push(current);
|
|
416
|
+
}
|
|
417
|
+
current = yield result;
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
this.generator = genFn(this.period);
|
|
421
|
+
this.generator.next();
|
|
422
|
+
this.result = [];
|
|
423
|
+
this.price.forEach((tick) => {
|
|
424
|
+
var result = this.generator.next(tick);
|
|
425
|
+
if (result.value !== undefined) {
|
|
426
|
+
this.result.push(this.format(result.value));
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
nextValue(price) {
|
|
431
|
+
var result = this.generator.next(price).value;
|
|
432
|
+
if (result != undefined)
|
|
433
|
+
return this.format(result);
|
|
434
|
+
}
|
|
435
|
+
;
|
|
436
|
+
}
|
|
437
|
+
SMA.calculate = sma;
|
|
438
|
+
function sma(input) {
|
|
439
|
+
Indicator.reverseInputs(input);
|
|
440
|
+
var result = new SMA(input).result;
|
|
441
|
+
if (input.reversedInput) {
|
|
442
|
+
result.reverse();
|
|
443
|
+
}
|
|
444
|
+
Indicator.reverseInputs(input);
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
//STEP 6. Run the tests
|
|
448
|
+
|
|
449
|
+
class EMA extends Indicator {
|
|
450
|
+
constructor(input) {
|
|
451
|
+
super(input);
|
|
452
|
+
var period = input.period;
|
|
453
|
+
var priceArray = input.values;
|
|
454
|
+
var exponent = (2 / (period + 1));
|
|
455
|
+
var sma;
|
|
456
|
+
this.result = [];
|
|
457
|
+
sma = new SMA({ period: period, values: [] });
|
|
458
|
+
var genFn = (function* () {
|
|
459
|
+
var tick = yield;
|
|
460
|
+
var prevEma;
|
|
461
|
+
while (true) {
|
|
462
|
+
if (prevEma !== undefined && tick !== undefined) {
|
|
463
|
+
prevEma = ((tick - prevEma) * exponent) + prevEma;
|
|
464
|
+
tick = yield prevEma;
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
tick = yield;
|
|
468
|
+
prevEma = sma.nextValue(tick);
|
|
469
|
+
if (prevEma)
|
|
470
|
+
tick = yield prevEma;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
this.generator = genFn();
|
|
475
|
+
this.generator.next();
|
|
476
|
+
this.generator.next();
|
|
477
|
+
priceArray.forEach((tick) => {
|
|
478
|
+
var result = this.generator.next(tick);
|
|
479
|
+
if (result.value != undefined) {
|
|
480
|
+
this.result.push(this.format(result.value));
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
nextValue(price) {
|
|
485
|
+
var result = this.generator.next(price).value;
|
|
486
|
+
if (result != undefined)
|
|
487
|
+
return this.format(result);
|
|
488
|
+
}
|
|
489
|
+
;
|
|
490
|
+
}
|
|
491
|
+
EMA.calculate = ema;
|
|
492
|
+
function ema(input) {
|
|
493
|
+
Indicator.reverseInputs(input);
|
|
494
|
+
var result = new EMA(input).result;
|
|
495
|
+
if (input.reversedInput) {
|
|
496
|
+
result.reverse();
|
|
497
|
+
}
|
|
498
|
+
Indicator.reverseInputs(input);
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Created by AAravindan on 5/4/16.
|
|
504
|
+
*/
|
|
505
|
+
class MACD extends Indicator {
|
|
506
|
+
constructor(input) {
|
|
507
|
+
super(input);
|
|
508
|
+
var oscillatorMAtype = input.SimpleMAOscillator ? SMA : EMA;
|
|
509
|
+
var signalMAtype = input.SimpleMASignal ? SMA : EMA;
|
|
510
|
+
var fastMAProducer = new oscillatorMAtype({ period: input.fastPeriod, values: [], format: (v) => { return v; } });
|
|
511
|
+
var slowMAProducer = new oscillatorMAtype({ period: input.slowPeriod, values: [], format: (v) => { return v; } });
|
|
512
|
+
var signalMAProducer = new signalMAtype({ period: input.signalPeriod, values: [], format: (v) => { return v; } });
|
|
513
|
+
var format = this.format;
|
|
514
|
+
this.result = [];
|
|
515
|
+
this.generator = (function* () {
|
|
516
|
+
var index = 0;
|
|
517
|
+
var tick;
|
|
518
|
+
var MACD, signal, histogram, fast, slow;
|
|
519
|
+
while (true) {
|
|
520
|
+
if (index < input.slowPeriod) {
|
|
521
|
+
tick = yield;
|
|
522
|
+
fast = fastMAProducer.nextValue(tick);
|
|
523
|
+
slow = slowMAProducer.nextValue(tick);
|
|
524
|
+
index++;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (fast && slow) { //Just for typescript to be happy
|
|
528
|
+
MACD = fast - slow;
|
|
529
|
+
signal = signalMAProducer.nextValue(MACD);
|
|
530
|
+
}
|
|
531
|
+
histogram = MACD - signal;
|
|
532
|
+
tick = yield ({
|
|
533
|
+
//fast : fast,
|
|
534
|
+
//slow : slow,
|
|
535
|
+
MACD: format(MACD),
|
|
536
|
+
signal: signal ? format(signal) : undefined,
|
|
537
|
+
histogram: isNaN(histogram) ? undefined : format(histogram)
|
|
538
|
+
});
|
|
539
|
+
fast = fastMAProducer.nextValue(tick);
|
|
540
|
+
slow = slowMAProducer.nextValue(tick);
|
|
541
|
+
}
|
|
542
|
+
})();
|
|
543
|
+
this.generator.next();
|
|
544
|
+
input.values.forEach((tick) => {
|
|
545
|
+
var result = this.generator.next(tick);
|
|
546
|
+
if (result.value != undefined) {
|
|
547
|
+
this.result.push(result.value);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
nextValue(price) {
|
|
552
|
+
var result = this.generator.next(price).value;
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
555
|
+
;
|
|
556
|
+
}
|
|
557
|
+
MACD.calculate = macd;
|
|
558
|
+
function macd(input) {
|
|
559
|
+
Indicator.reverseInputs(input);
|
|
560
|
+
var result = new MACD(input).result;
|
|
561
|
+
if (input.reversedInput) {
|
|
562
|
+
result.reverse();
|
|
563
|
+
}
|
|
564
|
+
Indicator.reverseInputs(input);
|
|
565
|
+
return result;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
class AverageGain extends Indicator {
|
|
569
|
+
constructor(input) {
|
|
570
|
+
super(input);
|
|
571
|
+
let values = input.values;
|
|
572
|
+
let period = input.period;
|
|
573
|
+
let format = this.format;
|
|
574
|
+
this.generator = (function* (period) {
|
|
575
|
+
var currentValue = yield;
|
|
576
|
+
var counter = 1;
|
|
577
|
+
var gainSum = 0;
|
|
578
|
+
var avgGain;
|
|
579
|
+
var gain;
|
|
580
|
+
var lastValue = currentValue;
|
|
581
|
+
currentValue = yield;
|
|
582
|
+
while (true) {
|
|
583
|
+
gain = currentValue - lastValue;
|
|
584
|
+
gain = gain > 0 ? gain : 0;
|
|
585
|
+
if (gain > 0) {
|
|
586
|
+
gainSum = gainSum + gain;
|
|
587
|
+
}
|
|
588
|
+
if (counter < period) {
|
|
589
|
+
counter++;
|
|
590
|
+
}
|
|
591
|
+
else if (avgGain === undefined) {
|
|
592
|
+
avgGain = gainSum / period;
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
avgGain = ((avgGain * (period - 1)) + gain) / period;
|
|
596
|
+
}
|
|
597
|
+
lastValue = currentValue;
|
|
598
|
+
avgGain = (avgGain !== undefined) ? format(avgGain) : undefined;
|
|
599
|
+
currentValue = yield avgGain;
|
|
600
|
+
}
|
|
601
|
+
})(period);
|
|
602
|
+
this.generator.next();
|
|
603
|
+
this.result = [];
|
|
604
|
+
values.forEach((tick) => {
|
|
605
|
+
var result = this.generator.next(tick);
|
|
606
|
+
if (result.value !== undefined) {
|
|
607
|
+
this.result.push(result.value);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
nextValue(price) {
|
|
612
|
+
return this.generator.next(price).value;
|
|
613
|
+
}
|
|
614
|
+
;
|
|
615
|
+
}
|
|
616
|
+
AverageGain.calculate = averagegain;
|
|
617
|
+
function averagegain(input) {
|
|
618
|
+
Indicator.reverseInputs(input);
|
|
619
|
+
var result = new AverageGain(input).result;
|
|
620
|
+
if (input.reversedInput) {
|
|
621
|
+
result.reverse();
|
|
622
|
+
}
|
|
623
|
+
Indicator.reverseInputs(input);
|
|
624
|
+
return result;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
class AverageLoss extends Indicator {
|
|
628
|
+
constructor(input) {
|
|
629
|
+
super(input);
|
|
630
|
+
let values = input.values;
|
|
631
|
+
let period = input.period;
|
|
632
|
+
let format = this.format;
|
|
633
|
+
this.generator = (function* (period) {
|
|
634
|
+
var currentValue = yield;
|
|
635
|
+
var counter = 1;
|
|
636
|
+
var lossSum = 0;
|
|
637
|
+
var avgLoss;
|
|
638
|
+
var loss;
|
|
639
|
+
var lastValue = currentValue;
|
|
640
|
+
currentValue = yield;
|
|
641
|
+
while (true) {
|
|
642
|
+
loss = lastValue - currentValue;
|
|
643
|
+
loss = loss > 0 ? loss : 0;
|
|
644
|
+
if (loss > 0) {
|
|
645
|
+
lossSum = lossSum + loss;
|
|
646
|
+
}
|
|
647
|
+
if (counter < period) {
|
|
648
|
+
counter++;
|
|
649
|
+
}
|
|
650
|
+
else if (avgLoss === undefined) {
|
|
651
|
+
avgLoss = lossSum / period;
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
avgLoss = ((avgLoss * (period - 1)) + loss) / period;
|
|
655
|
+
}
|
|
656
|
+
lastValue = currentValue;
|
|
657
|
+
avgLoss = (avgLoss !== undefined) ? format(avgLoss) : undefined;
|
|
658
|
+
currentValue = yield avgLoss;
|
|
659
|
+
}
|
|
660
|
+
})(period);
|
|
661
|
+
this.generator.next();
|
|
662
|
+
this.result = [];
|
|
663
|
+
values.forEach((tick) => {
|
|
664
|
+
var result = this.generator.next(tick);
|
|
665
|
+
if (result.value !== undefined) {
|
|
666
|
+
this.result.push(result.value);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
nextValue(price) {
|
|
671
|
+
return this.generator.next(price).value;
|
|
672
|
+
}
|
|
673
|
+
;
|
|
674
|
+
}
|
|
675
|
+
AverageLoss.calculate = averageloss;
|
|
676
|
+
function averageloss(input) {
|
|
677
|
+
Indicator.reverseInputs(input);
|
|
678
|
+
var result = new AverageLoss(input).result;
|
|
679
|
+
if (input.reversedInput) {
|
|
680
|
+
result.reverse();
|
|
681
|
+
}
|
|
682
|
+
Indicator.reverseInputs(input);
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Created by AAravindan on 5/5/16.
|
|
688
|
+
*/
|
|
689
|
+
class RSI extends Indicator {
|
|
690
|
+
constructor(input) {
|
|
691
|
+
super(input);
|
|
692
|
+
var period = input.period;
|
|
693
|
+
var values = input.values;
|
|
694
|
+
var GainProvider = new AverageGain({ period: period, values: [] });
|
|
695
|
+
var LossProvider = new AverageLoss({ period: period, values: [] });
|
|
696
|
+
this.generator = (function* (period) {
|
|
697
|
+
var current = yield;
|
|
698
|
+
var lastAvgGain, lastAvgLoss, RS, currentRSI;
|
|
699
|
+
while (true) {
|
|
700
|
+
lastAvgGain = GainProvider.nextValue(current);
|
|
701
|
+
lastAvgLoss = LossProvider.nextValue(current);
|
|
702
|
+
if ((lastAvgGain !== undefined) && (lastAvgLoss !== undefined)) {
|
|
703
|
+
if (lastAvgLoss === 0) {
|
|
704
|
+
currentRSI = 100;
|
|
705
|
+
}
|
|
706
|
+
else if (lastAvgGain === 0) {
|
|
707
|
+
currentRSI = 0;
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
RS = lastAvgGain / lastAvgLoss;
|
|
711
|
+
RS = isNaN(RS) ? 0 : RS;
|
|
712
|
+
currentRSI = parseFloat((100 - (100 / (1 + RS))).toFixed(2));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
current = yield currentRSI;
|
|
716
|
+
}
|
|
717
|
+
})();
|
|
718
|
+
this.generator.next();
|
|
719
|
+
this.result = [];
|
|
720
|
+
values.forEach((tick) => {
|
|
721
|
+
var result = this.generator.next(tick);
|
|
722
|
+
if (result.value !== undefined) {
|
|
723
|
+
this.result.push(result.value);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
;
|
|
728
|
+
nextValue(price) {
|
|
729
|
+
return this.generator.next(price).value;
|
|
730
|
+
}
|
|
731
|
+
;
|
|
732
|
+
}
|
|
733
|
+
RSI.calculate = rsi;
|
|
734
|
+
function rsi(input) {
|
|
735
|
+
Indicator.reverseInputs(input);
|
|
736
|
+
var result = new RSI(input).result;
|
|
737
|
+
if (input.reversedInput) {
|
|
738
|
+
result.reverse();
|
|
739
|
+
}
|
|
740
|
+
Indicator.reverseInputs(input);
|
|
741
|
+
return result;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
class IndicatorCalculator {
|
|
745
|
+
static calculateSMA(data, period) {
|
|
746
|
+
const closes = data.map(d => d.close);
|
|
747
|
+
const smaValues = SMA.calculate({ period, values: closes });
|
|
748
|
+
return {
|
|
749
|
+
name: `SMA(${period})`,
|
|
750
|
+
type: 'line',
|
|
751
|
+
data: smaValues,
|
|
752
|
+
color: '#2962ff',
|
|
753
|
+
style: 'solid',
|
|
754
|
+
width: 2
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
static calculateEMA(data, period) {
|
|
758
|
+
const closes = data.map(d => d.close);
|
|
759
|
+
const emaValues = EMA.calculate({ period, values: closes });
|
|
760
|
+
return {
|
|
761
|
+
name: `EMA(${period})`,
|
|
762
|
+
type: 'line',
|
|
763
|
+
data: emaValues,
|
|
764
|
+
color: '#ff6b35',
|
|
765
|
+
style: 'solid',
|
|
766
|
+
width: 2
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
static calculateRSI(data, period = 14) {
|
|
770
|
+
const closes = data.map(d => d.close);
|
|
771
|
+
const rsiValues = RSI.calculate({ period, values: closes });
|
|
772
|
+
return {
|
|
773
|
+
name: `RSI(${period})`,
|
|
774
|
+
type: 'line',
|
|
775
|
+
data: rsiValues,
|
|
776
|
+
color: '#00d4aa',
|
|
777
|
+
style: 'solid',
|
|
778
|
+
width: 1
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
static calculateMACD(data, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) {
|
|
782
|
+
const closes = data.map(d => d.close);
|
|
783
|
+
const macdResult = MACD.calculate({
|
|
784
|
+
fastPeriod,
|
|
785
|
+
slowPeriod,
|
|
786
|
+
signalPeriod,
|
|
787
|
+
values: closes,
|
|
788
|
+
SimpleMAOscillator: false,
|
|
789
|
+
SimpleMASignal: false
|
|
790
|
+
});
|
|
791
|
+
return {
|
|
792
|
+
macd: {
|
|
793
|
+
name: `MACD(${fastPeriod},${slowPeriod},${signalPeriod})`,
|
|
794
|
+
type: 'line',
|
|
795
|
+
data: macdResult.map(d => d.MACD || 0),
|
|
796
|
+
color: '#2962ff',
|
|
797
|
+
style: 'solid',
|
|
798
|
+
width: 1
|
|
799
|
+
},
|
|
800
|
+
signal: {
|
|
801
|
+
name: 'Signal',
|
|
802
|
+
type: 'line',
|
|
803
|
+
data: macdResult.map(d => d.signal || 0),
|
|
804
|
+
color: '#ff6b35',
|
|
805
|
+
style: 'solid',
|
|
806
|
+
width: 1
|
|
807
|
+
},
|
|
808
|
+
histogram: {
|
|
809
|
+
name: 'MACD Histogram',
|
|
810
|
+
type: 'histogram',
|
|
811
|
+
data: macdResult.map(d => d.histogram || 0),
|
|
812
|
+
color: '#00d4aa',
|
|
813
|
+
width: 2
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function createStore(creator) {
|
|
820
|
+
let state;
|
|
821
|
+
const listeners = new Set();
|
|
822
|
+
const setState = (partial) => {
|
|
823
|
+
const partialState = typeof partial === 'function' ? partial(state) : partial;
|
|
824
|
+
state = { ...state, ...partialState };
|
|
825
|
+
listeners.forEach((listener) => listener());
|
|
826
|
+
};
|
|
827
|
+
const getState = () => state;
|
|
828
|
+
state = creator(setState, getState);
|
|
829
|
+
const subscribe = (listener) => {
|
|
830
|
+
listeners.add(listener);
|
|
831
|
+
return () => {
|
|
832
|
+
listeners.delete(listener);
|
|
833
|
+
};
|
|
834
|
+
};
|
|
835
|
+
const useStore = () => useSyncExternalStore(subscribe, getState, getState);
|
|
836
|
+
useStore.getState = getState;
|
|
837
|
+
useStore.setState = setState;
|
|
838
|
+
return useStore;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const useChartStore = createStore((set) => ({
|
|
842
|
+
data: [],
|
|
843
|
+
visibleData: [],
|
|
844
|
+
timeframe: '1h',
|
|
845
|
+
zoomLevel: 1,
|
|
846
|
+
panOffset: 0,
|
|
847
|
+
indicators: [],
|
|
848
|
+
showIndicatorsPanel: false,
|
|
849
|
+
activeTool: 'cursor',
|
|
850
|
+
cursorType: 'cross',
|
|
851
|
+
strokeColor: '#8ab4ff',
|
|
852
|
+
strokeWidth: 1.5,
|
|
853
|
+
magnetEnabled: true,
|
|
854
|
+
drawings: [],
|
|
855
|
+
selectedDrawing: null,
|
|
856
|
+
isDrawing: false,
|
|
857
|
+
currentDrawing: null,
|
|
858
|
+
selectedEmoji: null,
|
|
859
|
+
interactionsLocked: false,
|
|
860
|
+
drawingsHidden: false,
|
|
861
|
+
currentPrice: 0,
|
|
862
|
+
priceChange: 0,
|
|
863
|
+
priceChangePercent: 0,
|
|
864
|
+
isLoading: false,
|
|
865
|
+
setData: (data) => {
|
|
866
|
+
const lastPrice = data[data.length - 1]?.close || 0;
|
|
867
|
+
const firstPrice = data[0]?.close || 0;
|
|
868
|
+
const priceChange = lastPrice - firstPrice;
|
|
869
|
+
const priceChangePercent = firstPrice > 0 ? (priceChange / firstPrice) * 100 : 0;
|
|
870
|
+
set({
|
|
871
|
+
data,
|
|
872
|
+
currentPrice: lastPrice,
|
|
873
|
+
priceChange,
|
|
874
|
+
priceChangePercent
|
|
875
|
+
});
|
|
876
|
+
},
|
|
877
|
+
setVisibleData: (visibleData) => set({ visibleData }),
|
|
878
|
+
setTimeframe: (timeframe) => set({ timeframe }),
|
|
879
|
+
setZoomLevel: (zoomLevel) => set({ zoomLevel }),
|
|
880
|
+
setPanOffset: (panOffset) => set({ panOffset }),
|
|
881
|
+
addIndicator: (indicator) => set((state) => ({
|
|
882
|
+
indicators: [...state.indicators, indicator]
|
|
883
|
+
})),
|
|
884
|
+
removeIndicator: (name) => set((state) => ({
|
|
885
|
+
indicators: state.indicators.filter((ind) => ind.name !== name)
|
|
886
|
+
})),
|
|
887
|
+
toggleIndicatorsPanel: () => set((state) => ({
|
|
888
|
+
showIndicatorsPanel: !state.showIndicatorsPanel
|
|
889
|
+
})),
|
|
890
|
+
updateCurrentPrice: (currentPrice) => set({ currentPrice }),
|
|
891
|
+
setLoading: (isLoading) => set({ isLoading }),
|
|
892
|
+
setActiveTool: (activeTool) => set({ activeTool }),
|
|
893
|
+
setCursorType: (cursorType) => set({ cursorType }),
|
|
894
|
+
setStrokeColor: (strokeColor) => set({ strokeColor }),
|
|
895
|
+
setStrokeWidth: (strokeWidth) => set({ strokeWidth }),
|
|
896
|
+
toggleMagnet: () => set((state) => ({ magnetEnabled: !state.magnetEnabled })),
|
|
897
|
+
addDrawing: (drawing) => set((state) => ({
|
|
898
|
+
drawings: [...state.drawings, drawing]
|
|
899
|
+
})),
|
|
900
|
+
updateDrawing: (id, updates) => set((state) => ({
|
|
901
|
+
drawings: state.drawings.map((d) => (d.id === id ? { ...d, ...updates } : d))
|
|
902
|
+
})),
|
|
903
|
+
deleteDrawing: (id) => set((state) => ({
|
|
904
|
+
drawings: state.drawings.filter((d) => d.id !== id),
|
|
905
|
+
selectedDrawing: state.selectedDrawing === id ? null : state.selectedDrawing
|
|
906
|
+
})),
|
|
907
|
+
clearDrawings: () => set({ drawings: [], selectedDrawing: null }),
|
|
908
|
+
selectDrawing: (selectedDrawing) => set({ selectedDrawing }),
|
|
909
|
+
setIsDrawing: (isDrawing) => set({ isDrawing }),
|
|
910
|
+
setCurrentDrawing: (currentDrawing) => set({ currentDrawing }),
|
|
911
|
+
setSelectedEmoji: (selectedEmoji) => set({ selectedEmoji }),
|
|
912
|
+
toggleInteractionsLock: () => set((state) => {
|
|
913
|
+
const nextLocked = !state.interactionsLocked;
|
|
914
|
+
return {
|
|
915
|
+
interactionsLocked: nextLocked,
|
|
916
|
+
activeTool: nextLocked ? 'cursor' : state.activeTool,
|
|
917
|
+
isDrawing: nextLocked ? false : state.isDrawing
|
|
918
|
+
};
|
|
919
|
+
}),
|
|
920
|
+
toggleDrawingsHidden: () => set((state) => ({ drawingsHidden: !state.drawingsHidden }))
|
|
921
|
+
}));
|
|
922
|
+
|
|
923
|
+
const EMOJIS = ['⭐', '🔥', '🚀', '✅', '❌', '💎', '🐂', '🐻', '📈', '📉'];
|
|
924
|
+
const cursorOptions = [
|
|
925
|
+
{ type: 'cross', icon: React.createElement(BsCursor, null), label: 'Cross' },
|
|
926
|
+
{ type: 'dot', icon: React.createElement("div", { style: { width: '6px', height: '6px', borderRadius: '50%', background: 'currentColor' } }), label: 'Dot' },
|
|
927
|
+
{ type: 'arrow', icon: React.createElement(BsCursor, { style: { transform: 'rotate(45deg)' } }), label: 'Arrow' },
|
|
928
|
+
{ type: 'eraser', icon: React.createElement(BsTrash, { style: { fontSize: '14px' } }), label: 'Eraser' }
|
|
929
|
+
];
|
|
930
|
+
const lineOptions = [
|
|
931
|
+
{ tool: 'trendline', icon: React.createElement(BsSlash, null), label: 'Trend line' },
|
|
932
|
+
{ tool: 'ray', icon: React.createElement(BsSlash, { style: { transform: 'translateX(-3px)' } }), label: 'Ray' },
|
|
933
|
+
{ tool: 'info-line', icon: React.createElement(BsGraphUp, null), label: 'Info line' },
|
|
934
|
+
{ tool: 'extended-line', icon: React.createElement(BsSlash, { style: { width: '24px' } }), label: 'Extended line' },
|
|
935
|
+
{ tool: 'trend-angle', icon: React.createElement(BsSlash, { style: { transform: 'rotate(15deg)' } }), label: 'Trend angle' },
|
|
936
|
+
{ tool: 'horizontal', icon: React.createElement(BsDash, { style: { width: '22px' } }), label: 'Horizontal segment' },
|
|
937
|
+
{ tool: 'horizontal-line', icon: React.createElement(BsDash, null), label: 'Horizontal line' },
|
|
938
|
+
{ tool: 'horizontal-ray', icon: React.createElement(BsDash, { style: { transform: 'translateX(4px)' } }), label: 'Horizontal ray' },
|
|
939
|
+
{ tool: 'vertical-line', icon: React.createElement(BsDash, { style: { transform: 'rotate(90deg)' } }), label: 'Vertical line' },
|
|
940
|
+
{ tool: 'cross-line', icon: React.createElement("span", { style: { fontWeight: 700 } }, "\u271A"), label: 'Cross line' }
|
|
941
|
+
];
|
|
942
|
+
const channelOptions = [
|
|
943
|
+
{ tool: 'parallel', icon: React.createElement(BsArrowsAngleExpand, null), label: 'Parallel channel' },
|
|
944
|
+
{ tool: 'channel', icon: React.createElement(BsChevronBarExpand, null), label: 'Standard channel' },
|
|
945
|
+
{ tool: 'regression-trend', icon: React.createElement(BsGraphUp, null), label: 'Regression trend' },
|
|
946
|
+
{ tool: 'flat-top-bottom', icon: React.createElement(BsDash, { style: { width: '24px' } }), label: 'Flat top/bottom' },
|
|
947
|
+
{ tool: 'disjoint-channel', icon: React.createElement(BsChevronBarExpand, { style: { transform: 'rotate(90deg)' } }), label: 'Disjoint channel' }
|
|
948
|
+
];
|
|
949
|
+
const pitchforkOptions = [
|
|
950
|
+
{ tool: 'pitchfork', icon: React.createElement(BsDiagram3, null), label: 'Pitchfork' },
|
|
951
|
+
{ tool: 'schiff-pitchfork', icon: React.createElement(BsDiagram3, null), label: 'Schiff pitchfork' },
|
|
952
|
+
{ tool: 'modified-schiff-pitchfork', icon: React.createElement(BsDiagram3, null), label: 'Modified Schiff' },
|
|
953
|
+
{ tool: 'inside-pitchfork', icon: React.createElement(BsDiagram3, null), label: 'Inside pitchfork' }
|
|
954
|
+
];
|
|
955
|
+
const shapeOptions = [
|
|
956
|
+
{ tool: 'rectangle', icon: React.createElement(BsSquare, null), label: 'Rectángulo' },
|
|
957
|
+
{ tool: 'triangle', icon: React.createElement(BsTriangle, null), label: 'Triángulo' }
|
|
958
|
+
];
|
|
959
|
+
const measurementOptions = [
|
|
960
|
+
{ tool: 'fibonacci', icon: React.createElement(BsGraphUp, null), label: 'Fibonacci' },
|
|
961
|
+
{ tool: 'ruler', icon: React.createElement(BsGraphDown, null), label: 'Regla' }
|
|
962
|
+
];
|
|
963
|
+
const popoverStyle = {
|
|
964
|
+
position: 'absolute',
|
|
965
|
+
left: '64px',
|
|
966
|
+
top: '50%',
|
|
967
|
+
transform: 'translateY(-50%)',
|
|
968
|
+
background: '#020617',
|
|
969
|
+
border: '1px solid #1f2937',
|
|
970
|
+
borderRadius: '14px',
|
|
971
|
+
padding: '10px',
|
|
972
|
+
boxShadow: '0 12px 30px rgba(0,0,0,0.5)',
|
|
973
|
+
display: 'grid',
|
|
974
|
+
gap: '8px',
|
|
975
|
+
zIndex: 1100
|
|
976
|
+
};
|
|
977
|
+
const DrawingToolbar = () => {
|
|
978
|
+
const { activeTool, cursorType, strokeColor, strokeWidth, setActiveTool, setCursorType, setStrokeColor, setStrokeWidth, clearDrawings, setSelectedEmoji, setIsDrawing } = useChartStore();
|
|
979
|
+
const [openMenu, setOpenMenu] = useState(null);
|
|
980
|
+
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
|
981
|
+
const LINE_COLORS = ['#8ab4ff', '#f472b6', '#34d399', '#facc15', '#f87171', '#e2e8f0'];
|
|
982
|
+
const WIDTH_OPTIONS = [1, 1.5, 2.5, 3.5];
|
|
983
|
+
const handleSelectTool = (tool) => {
|
|
984
|
+
setActiveTool(tool);
|
|
985
|
+
setIsDrawing(false);
|
|
986
|
+
setOpenMenu(null);
|
|
987
|
+
setShowEmojiPicker(false);
|
|
988
|
+
};
|
|
989
|
+
const handleCursorSelect = (type) => {
|
|
990
|
+
setCursorType(type);
|
|
991
|
+
setOpenMenu(null);
|
|
992
|
+
};
|
|
993
|
+
const toggleMenu = (menu) => {
|
|
994
|
+
setShowEmojiPicker(false);
|
|
995
|
+
setOpenMenu(prev => (prev === menu ? null : menu));
|
|
996
|
+
};
|
|
997
|
+
const buttonConfig = [
|
|
998
|
+
{
|
|
999
|
+
key: 'cursor',
|
|
1000
|
+
icon: React.createElement(BsCursor, null),
|
|
1001
|
+
active: openMenu === 'cursor',
|
|
1002
|
+
onClick: () => toggleMenu('cursor'),
|
|
1003
|
+
title: 'Cursores'
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
key: 'lines',
|
|
1007
|
+
icon: React.createElement(BsSlash, null),
|
|
1008
|
+
active: lineOptions.some(option => option.tool === activeTool) || openMenu === 'lines',
|
|
1009
|
+
onClick: () => toggleMenu('lines'),
|
|
1010
|
+
title: 'Lines'
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
key: 'shapes',
|
|
1014
|
+
icon: React.createElement(BsSquare, null),
|
|
1015
|
+
active: shapeOptions.some(option => option.tool === activeTool) || openMenu === 'shapes',
|
|
1016
|
+
onClick: () => toggleMenu('shapes'),
|
|
1017
|
+
title: 'Shapes'
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
key: 'channels',
|
|
1021
|
+
icon: React.createElement(BsChevronBarExpand, null),
|
|
1022
|
+
active: channelOptions.some(option => option.tool === activeTool) || openMenu === 'channels',
|
|
1023
|
+
onClick: () => toggleMenu('channels'),
|
|
1024
|
+
title: 'Channels'
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
key: 'pitchforks',
|
|
1028
|
+
icon: React.createElement(BsDiagram3, null),
|
|
1029
|
+
active: pitchforkOptions.some(option => option.tool === activeTool) || openMenu === 'pitchforks',
|
|
1030
|
+
onClick: () => toggleMenu('pitchforks'),
|
|
1031
|
+
title: 'Pitchforks'
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
key: 'measurements',
|
|
1035
|
+
icon: React.createElement(BsGraphUp, null),
|
|
1036
|
+
active: measurementOptions.some(option => option.tool === activeTool) || openMenu === 'measurements',
|
|
1037
|
+
onClick: () => toggleMenu('measurements'),
|
|
1038
|
+
title: 'Mediciones'
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
key: 'brush',
|
|
1042
|
+
icon: React.createElement(BsPencil, null),
|
|
1043
|
+
active: activeTool === 'freehand',
|
|
1044
|
+
onClick: () => handleSelectTool('freehand'),
|
|
1045
|
+
title: 'Brush'
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
key: 'text',
|
|
1049
|
+
icon: React.createElement(BsType, null),
|
|
1050
|
+
active: activeTool === 'text',
|
|
1051
|
+
onClick: () => handleSelectTool('text'),
|
|
1052
|
+
title: 'Texto y notas'
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
key: 'emoji',
|
|
1056
|
+
icon: React.createElement(BsEmojiSmile, null),
|
|
1057
|
+
active: showEmojiPicker,
|
|
1058
|
+
onClick: () => {
|
|
1059
|
+
setOpenMenu(null);
|
|
1060
|
+
setShowEmojiPicker(prev => !prev);
|
|
1061
|
+
},
|
|
1062
|
+
title: 'Emojis'
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
key: 'clear',
|
|
1066
|
+
icon: React.createElement(BsTrash, null),
|
|
1067
|
+
active: false,
|
|
1068
|
+
onClick: () => {
|
|
1069
|
+
clearDrawings();
|
|
1070
|
+
setOpenMenu(null);
|
|
1071
|
+
setShowEmojiPicker(false);
|
|
1072
|
+
},
|
|
1073
|
+
title: 'Limpiar todo'
|
|
1074
|
+
}
|
|
1075
|
+
];
|
|
1076
|
+
return (React.createElement("div", { style: { position: 'relative', height: '100%' } },
|
|
1077
|
+
React.createElement("div", { style: {
|
|
1078
|
+
height: '100%',
|
|
1079
|
+
display: 'flex',
|
|
1080
|
+
flexDirection: 'column',
|
|
1081
|
+
alignItems: 'center',
|
|
1082
|
+
gap: '12px'
|
|
1083
|
+
} }, buttonConfig.map((btn) => (React.createElement("div", { key: btn.key, style: { position: 'relative' } },
|
|
1084
|
+
React.createElement("button", { onClick: btn.onClick, title: btn.title, "aria-label": btn.title, style: {
|
|
1085
|
+
width: '46px',
|
|
1086
|
+
height: '46px',
|
|
1087
|
+
borderRadius: '14px',
|
|
1088
|
+
border: `1px solid ${btn.active ? '#2563eb' : '#1f2937'}`,
|
|
1089
|
+
background: btn.active ? 'rgba(37,99,235,0.25)' : 'rgba(15,23,42,0.4)',
|
|
1090
|
+
color: btn.active ? '#f8fafc' : '#b2b5be',
|
|
1091
|
+
cursor: 'pointer',
|
|
1092
|
+
display: 'flex',
|
|
1093
|
+
alignItems: 'center',
|
|
1094
|
+
justifyContent: 'center',
|
|
1095
|
+
fontSize: '18px',
|
|
1096
|
+
transition: 'all 0.2s'
|
|
1097
|
+
} }, btn.icon),
|
|
1098
|
+
btn.key === 'cursor' && openMenu === 'cursor' && (React.createElement("div", { style: { ...popoverStyle, gridTemplateColumns: 'repeat(2, 1fr)', width: '200px' } }, cursorOptions.map(option => (React.createElement("button", { key: option.type, onClick: () => handleCursorSelect(option.type), style: {
|
|
1099
|
+
width: '84px',
|
|
1100
|
+
height: '60px',
|
|
1101
|
+
borderRadius: '12px',
|
|
1102
|
+
border: `1px solid ${cursorType === option.type ? '#2563eb' : '#1f2937'}`,
|
|
1103
|
+
background: cursorType === option.type ? 'rgba(37,99,235,0.25)' : 'rgba(2,6,23,0.6)',
|
|
1104
|
+
color: '#e2e8f0',
|
|
1105
|
+
display: 'flex',
|
|
1106
|
+
flexDirection: 'column',
|
|
1107
|
+
alignItems: 'center',
|
|
1108
|
+
justifyContent: 'center',
|
|
1109
|
+
gap: '4px',
|
|
1110
|
+
cursor: 'pointer'
|
|
1111
|
+
}, title: option.label },
|
|
1112
|
+
React.createElement("span", { style: { fontSize: '18px', display: 'flex', alignItems: 'center', justifyContent: 'center' } }, option.icon),
|
|
1113
|
+
React.createElement("span", { style: { fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase' } }, option.label)))))),
|
|
1114
|
+
btn.key === 'lines' && openMenu === 'lines' && (React.createElement("div", { style: { ...popoverStyle, width: '280px' } },
|
|
1115
|
+
React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Lines"),
|
|
1116
|
+
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
|
+
borderRadius: '12px',
|
|
1118
|
+
border: `1px solid ${activeTool === option.tool ? '#2563eb' : '#1f2937'}`,
|
|
1119
|
+
background: activeTool === option.tool ? 'rgba(37,99,235,0.2)' : 'rgba(2,6,23,0.6)',
|
|
1120
|
+
color: '#e2e8f0',
|
|
1121
|
+
display: 'flex',
|
|
1122
|
+
flexDirection: 'column',
|
|
1123
|
+
alignItems: 'center',
|
|
1124
|
+
justifyContent: 'center',
|
|
1125
|
+
padding: '10px',
|
|
1126
|
+
gap: '6px',
|
|
1127
|
+
cursor: 'pointer'
|
|
1128
|
+
}, title: option.label },
|
|
1129
|
+
React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
|
|
1130
|
+
React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em' } }, option.label))))),
|
|
1131
|
+
React.createElement("div", { style: { marginTop: '12px' } },
|
|
1132
|
+
React.createElement("p", { style: { margin: '0 0 6px', fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Stroke color"),
|
|
1133
|
+
React.createElement("div", { style: { display: 'flex', flexWrap: 'wrap', gap: '6px' } }, LINE_COLORS.map(color => (React.createElement("button", { key: color, onClick: () => setStrokeColor(color), style: {
|
|
1134
|
+
width: '26px',
|
|
1135
|
+
height: '26px',
|
|
1136
|
+
borderRadius: '50%',
|
|
1137
|
+
border: strokeColor === color ? '2px solid #f8fafc' : '2px solid transparent',
|
|
1138
|
+
background: color,
|
|
1139
|
+
cursor: 'pointer'
|
|
1140
|
+
}, "aria-label": `Stroke color ${color}` }))))),
|
|
1141
|
+
React.createElement("div", { style: { marginTop: '12px' } },
|
|
1142
|
+
React.createElement("p", { style: { margin: '0 0 6px', fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Stroke width"),
|
|
1143
|
+
React.createElement("div", { style: { display: 'flex', gap: '6px', flexWrap: 'wrap' } }, WIDTH_OPTIONS.map(width => (React.createElement("button", { key: width, onClick: () => setStrokeWidth(width), style: {
|
|
1144
|
+
padding: '4px 10px',
|
|
1145
|
+
borderRadius: '999px',
|
|
1146
|
+
border: strokeWidth === width ? '1px solid #2563eb' : '1px solid #1f2937',
|
|
1147
|
+
background: strokeWidth === width ? 'rgba(37,99,235,0.2)' : 'rgba(2,6,23,0.6)',
|
|
1148
|
+
color: '#e2e8f0',
|
|
1149
|
+
fontSize: '11px',
|
|
1150
|
+
cursor: 'pointer'
|
|
1151
|
+
} },
|
|
1152
|
+
width,
|
|
1153
|
+
"px"))))))),
|
|
1154
|
+
btn.key === 'shapes' && openMenu === 'shapes' && (React.createElement("div", { style: { ...popoverStyle, width: '200px' } },
|
|
1155
|
+
React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Shapes"),
|
|
1156
|
+
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
|
+
borderRadius: '12px',
|
|
1158
|
+
border: `1px solid ${activeTool === option.tool ? '#fcd34d' : '#1f2937'}`,
|
|
1159
|
+
background: activeTool === option.tool ? 'rgba(252,211,77,0.18)' : 'rgba(2,6,23,0.6)',
|
|
1160
|
+
color: '#e2e8f0',
|
|
1161
|
+
display: 'flex',
|
|
1162
|
+
flexDirection: 'column',
|
|
1163
|
+
alignItems: 'center',
|
|
1164
|
+
justifyContent: 'center',
|
|
1165
|
+
padding: '10px',
|
|
1166
|
+
gap: '6px',
|
|
1167
|
+
cursor: 'pointer'
|
|
1168
|
+
}, title: option.label },
|
|
1169
|
+
React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
|
|
1170
|
+
React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em' } }, option.label))))))),
|
|
1171
|
+
btn.key === 'channels' && openMenu === 'channels' && (React.createElement("div", { style: { ...popoverStyle, width: '220px' } },
|
|
1172
|
+
React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Channels"),
|
|
1173
|
+
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
|
+
borderRadius: '12px',
|
|
1175
|
+
border: `1px solid ${activeTool === option.tool ? '#22d3ee' : '#1f2937'}`,
|
|
1176
|
+
background: activeTool === option.tool ? 'rgba(34,211,238,0.15)' : 'rgba(2,6,23,0.6)',
|
|
1177
|
+
color: '#e2e8f0',
|
|
1178
|
+
display: 'flex',
|
|
1179
|
+
flexDirection: 'column',
|
|
1180
|
+
alignItems: 'center',
|
|
1181
|
+
justifyContent: 'center',
|
|
1182
|
+
padding: '10px',
|
|
1183
|
+
gap: '6px',
|
|
1184
|
+
cursor: 'pointer'
|
|
1185
|
+
}, title: option.label },
|
|
1186
|
+
React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
|
|
1187
|
+
React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em' } }, option.label))))))),
|
|
1188
|
+
btn.key === 'pitchforks' && openMenu === 'pitchforks' && (React.createElement("div", { style: { ...popoverStyle, width: '200px' } },
|
|
1189
|
+
React.createElement("p", { style: { margin: 0, fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, "Pitchforks"),
|
|
1190
|
+
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
|
+
borderRadius: '12px',
|
|
1192
|
+
border: `1px solid ${activeTool === option.tool ? '#f472b6' : '#1f2937'}`,
|
|
1193
|
+
background: activeTool === option.tool ? 'rgba(244,114,182,0.15)' : 'rgba(2,6,23,0.6)',
|
|
1194
|
+
color: '#e2e8f0',
|
|
1195
|
+
display: 'flex',
|
|
1196
|
+
flexDirection: 'column',
|
|
1197
|
+
alignItems: 'center',
|
|
1198
|
+
justifyContent: 'center',
|
|
1199
|
+
padding: '10px',
|
|
1200
|
+
gap: '6px',
|
|
1201
|
+
cursor: 'pointer'
|
|
1202
|
+
}, title: option.label },
|
|
1203
|
+
React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
|
|
1204
|
+
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: { ...popoverStyle, gridTemplateColumns: 'repeat(2, 1fr)', width: '160px' } }, measurementOptions.map(option => (React.createElement("button", { key: option.tool, onClick: () => handleSelectTool(option.tool), style: {
|
|
1206
|
+
width: '64px',
|
|
1207
|
+
height: '54px',
|
|
1208
|
+
borderRadius: '12px',
|
|
1209
|
+
border: `1px solid ${activeTool === option.tool ? '#fbbf24' : '#1f2937'}`,
|
|
1210
|
+
background: activeTool === option.tool ? 'rgba(251,191,36,0.18)' : 'rgba(2,6,23,0.6)',
|
|
1211
|
+
color: '#fbbf24',
|
|
1212
|
+
display: 'flex',
|
|
1213
|
+
flexDirection: 'column',
|
|
1214
|
+
alignItems: 'center',
|
|
1215
|
+
justifyContent: 'center',
|
|
1216
|
+
gap: '4px',
|
|
1217
|
+
fontSize: '10px',
|
|
1218
|
+
cursor: 'pointer'
|
|
1219
|
+
}, title: option.label },
|
|
1220
|
+
React.createElement("span", { style: { fontSize: '16px' } }, option.icon),
|
|
1221
|
+
React.createElement("span", null, option.label)))))),
|
|
1222
|
+
btn.key === 'emoji' && showEmojiPicker && (React.createElement("div", { style: {
|
|
1223
|
+
position: 'absolute',
|
|
1224
|
+
left: '64px',
|
|
1225
|
+
top: '50%',
|
|
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);
|
|
1241
|
+
setActiveTool('icon');
|
|
1242
|
+
setShowEmojiPicker(false);
|
|
1243
|
+
}, style: {
|
|
1244
|
+
width: '40px',
|
|
1245
|
+
height: '40px',
|
|
1246
|
+
background: '#111827',
|
|
1247
|
+
border: '1px solid #1f2937',
|
|
1248
|
+
borderRadius: '10px',
|
|
1249
|
+
cursor: 'pointer',
|
|
1250
|
+
display: 'flex',
|
|
1251
|
+
alignItems: 'center',
|
|
1252
|
+
justifyContent: 'center',
|
|
1253
|
+
fontSize: '18px',
|
|
1254
|
+
color: '#f8fafc'
|
|
1255
|
+
} },
|
|
1256
|
+
React.createElement("span", null, emoji)))))))))))));
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
const drawRoundedRect = (ctx, x, y, width, height, radius = 8) => {
|
|
1260
|
+
const r = Math.min(radius, width / 2, height / 2);
|
|
1261
|
+
ctx.beginPath();
|
|
1262
|
+
ctx.moveTo(x + r, y);
|
|
1263
|
+
ctx.lineTo(x + width - r, y);
|
|
1264
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
|
1265
|
+
ctx.lineTo(x + width, y + height - r);
|
|
1266
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
|
1267
|
+
ctx.lineTo(x + r, y + height);
|
|
1268
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
|
1269
|
+
ctx.lineTo(x, y + r);
|
|
1270
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
1271
|
+
ctx.closePath();
|
|
1272
|
+
};
|
|
1273
|
+
class DrawingRenderer {
|
|
1274
|
+
static draw(drawing, ctx) {
|
|
1275
|
+
if (!drawing.visible)
|
|
1276
|
+
return;
|
|
1277
|
+
ctx.save();
|
|
1278
|
+
ctx.strokeStyle = drawing.color;
|
|
1279
|
+
ctx.lineWidth = drawing.width;
|
|
1280
|
+
ctx.setLineDash(drawing.style === 'dashed' ? [5, 5] : drawing.style === 'dotted' ? [2, 2] : []);
|
|
1281
|
+
switch (drawing.type) {
|
|
1282
|
+
case 'trendline':
|
|
1283
|
+
this.drawLine(drawing, ctx);
|
|
1284
|
+
break;
|
|
1285
|
+
case 'ray':
|
|
1286
|
+
this.drawRay(drawing, ctx);
|
|
1287
|
+
break;
|
|
1288
|
+
case 'extended-line':
|
|
1289
|
+
this.drawExtendedLine(drawing, ctx);
|
|
1290
|
+
break;
|
|
1291
|
+
case 'info-line':
|
|
1292
|
+
this.drawInfoLine(drawing, ctx);
|
|
1293
|
+
break;
|
|
1294
|
+
case 'trend-angle':
|
|
1295
|
+
this.drawTrendAngle(drawing, ctx);
|
|
1296
|
+
break;
|
|
1297
|
+
case 'horizontal':
|
|
1298
|
+
this.drawLine(drawing, ctx);
|
|
1299
|
+
break;
|
|
1300
|
+
case 'horizontal-line':
|
|
1301
|
+
this.drawInfiniteHorizontal(drawing, ctx);
|
|
1302
|
+
break;
|
|
1303
|
+
case 'horizontal-ray':
|
|
1304
|
+
this.drawHorizontalRay(drawing, ctx);
|
|
1305
|
+
break;
|
|
1306
|
+
case 'vertical-line':
|
|
1307
|
+
this.drawVerticalLine(drawing, ctx);
|
|
1308
|
+
break;
|
|
1309
|
+
case 'parallel':
|
|
1310
|
+
case 'channel':
|
|
1311
|
+
case 'regression-trend':
|
|
1312
|
+
case 'flat-top-bottom':
|
|
1313
|
+
case 'disjoint-channel':
|
|
1314
|
+
case 'pitchfork':
|
|
1315
|
+
case 'schiff-pitchfork':
|
|
1316
|
+
case 'modified-schiff-pitchfork':
|
|
1317
|
+
case 'inside-pitchfork':
|
|
1318
|
+
this.drawChannel(drawing, ctx);
|
|
1319
|
+
break;
|
|
1320
|
+
case 'rectangle':
|
|
1321
|
+
this.drawRectangle(drawing, ctx);
|
|
1322
|
+
break;
|
|
1323
|
+
case 'triangle':
|
|
1324
|
+
this.drawTriangle(drawing, ctx);
|
|
1325
|
+
break;
|
|
1326
|
+
case 'text':
|
|
1327
|
+
this.drawText(drawing, ctx);
|
|
1328
|
+
break;
|
|
1329
|
+
case 'icon':
|
|
1330
|
+
this.drawIcon(drawing, ctx);
|
|
1331
|
+
break;
|
|
1332
|
+
case 'ruler':
|
|
1333
|
+
this.drawRuler(drawing, ctx);
|
|
1334
|
+
break;
|
|
1335
|
+
case 'freehand':
|
|
1336
|
+
this.drawFreehand(drawing, ctx);
|
|
1337
|
+
break;
|
|
1338
|
+
case 'fibonacci':
|
|
1339
|
+
this.drawFibonacci(drawing, ctx);
|
|
1340
|
+
break;
|
|
1341
|
+
case 'cross-line':
|
|
1342
|
+
this.drawCrossLine(drawing, ctx);
|
|
1343
|
+
break;
|
|
1344
|
+
default:
|
|
1345
|
+
this.drawLine(drawing, ctx);
|
|
1346
|
+
}
|
|
1347
|
+
ctx.setLineDash([]);
|
|
1348
|
+
ctx.restore();
|
|
1349
|
+
}
|
|
1350
|
+
static drawLine(drawing, ctx) {
|
|
1351
|
+
if (drawing.points.length < 2)
|
|
1352
|
+
return;
|
|
1353
|
+
const start = drawing.points[0];
|
|
1354
|
+
const end = drawing.points[1];
|
|
1355
|
+
if (!start || !end)
|
|
1356
|
+
return;
|
|
1357
|
+
ctx.beginPath();
|
|
1358
|
+
ctx.moveTo(start.x, start.y);
|
|
1359
|
+
ctx.lineTo(end.x, end.y);
|
|
1360
|
+
ctx.stroke();
|
|
1361
|
+
if (drawing.type === 'parallel' && drawing.points.length >= 4) {
|
|
1362
|
+
const start2 = drawing.points[2];
|
|
1363
|
+
const end2 = drawing.points[3];
|
|
1364
|
+
if (start2 && end2) {
|
|
1365
|
+
ctx.beginPath();
|
|
1366
|
+
ctx.moveTo(start2.x, start2.y);
|
|
1367
|
+
ctx.lineTo(end2.x, end2.y);
|
|
1368
|
+
ctx.stroke();
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
static drawRay(drawing, ctx) {
|
|
1373
|
+
if (drawing.points.length < 2)
|
|
1374
|
+
return;
|
|
1375
|
+
const start = drawing.points[0];
|
|
1376
|
+
const end = drawing.points[1];
|
|
1377
|
+
if (!start || !end)
|
|
1378
|
+
return;
|
|
1379
|
+
const intersections = this.computeBoundaryIntersections(start, end, ctx.canvas.width, ctx.canvas.height)
|
|
1380
|
+
.filter(point => point.t >= 0)
|
|
1381
|
+
.sort((a, b) => a.t - b.t);
|
|
1382
|
+
const target = intersections[0];
|
|
1383
|
+
if (!target) {
|
|
1384
|
+
this.drawLine(drawing, ctx);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
ctx.beginPath();
|
|
1388
|
+
ctx.moveTo(start.x, start.y);
|
|
1389
|
+
ctx.lineTo(target.x, target.y);
|
|
1390
|
+
ctx.stroke();
|
|
1391
|
+
}
|
|
1392
|
+
static drawExtendedLine(drawing, ctx) {
|
|
1393
|
+
if (drawing.points.length < 2)
|
|
1394
|
+
return;
|
|
1395
|
+
const start = drawing.points[0];
|
|
1396
|
+
const end = drawing.points[1];
|
|
1397
|
+
if (!start || !end)
|
|
1398
|
+
return;
|
|
1399
|
+
const intersections = this.computeBoundaryIntersections(start, end, ctx.canvas.width, ctx.canvas.height);
|
|
1400
|
+
const forward = intersections.filter(point => point.t >= 0).sort((a, b) => a.t - b.t)[0];
|
|
1401
|
+
const backward = intersections.filter(point => point.t <= 0).sort((a, b) => b.t - a.t)[0];
|
|
1402
|
+
if (!forward || !backward) {
|
|
1403
|
+
this.drawLine(drawing, ctx);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
ctx.beginPath();
|
|
1407
|
+
ctx.moveTo(backward.x, backward.y);
|
|
1408
|
+
ctx.lineTo(forward.x, forward.y);
|
|
1409
|
+
ctx.stroke();
|
|
1410
|
+
}
|
|
1411
|
+
static drawInfoLine(drawing, ctx) {
|
|
1412
|
+
this.drawLine(drawing, ctx);
|
|
1413
|
+
if (drawing.points.length < 2)
|
|
1414
|
+
return;
|
|
1415
|
+
const start = drawing.points[0];
|
|
1416
|
+
const end = drawing.points[1];
|
|
1417
|
+
if (!start || !end)
|
|
1418
|
+
return;
|
|
1419
|
+
const priceDelta = (end.price ?? 0) - (start.price ?? 0);
|
|
1420
|
+
const timeDelta = (end.time ?? 0) - (start.time ?? 0);
|
|
1421
|
+
const label = `ΔP ${priceDelta >= 0 ? '+' : ''}${priceDelta.toFixed(2)} | ΔT ${timeDelta >= 0 ? '+' : ''}${timeDelta.toFixed(0)}`;
|
|
1422
|
+
const midX = (start.x + end.x) / 2;
|
|
1423
|
+
const midY = (start.y + end.y) / 2;
|
|
1424
|
+
const fontSize = 10;
|
|
1425
|
+
const paddingX = 10;
|
|
1426
|
+
const paddingY = 6;
|
|
1427
|
+
ctx.font = `${fontSize}px Inter, sans-serif`;
|
|
1428
|
+
const textWidth = ctx.measureText(label).width;
|
|
1429
|
+
const boxWidth = textWidth + paddingX * 2;
|
|
1430
|
+
const boxHeight = fontSize + paddingY * 2;
|
|
1431
|
+
const boxX = midX - boxWidth / 2;
|
|
1432
|
+
const boxY = midY - boxHeight - 8;
|
|
1433
|
+
ctx.fillStyle = 'rgba(15,23,42,0.9)';
|
|
1434
|
+
ctx.strokeStyle = '#38bdf8';
|
|
1435
|
+
drawRoundedRect(ctx, boxX, boxY, boxWidth, boxHeight, 10);
|
|
1436
|
+
ctx.fill();
|
|
1437
|
+
ctx.stroke();
|
|
1438
|
+
ctx.fillStyle = '#e2e8f0';
|
|
1439
|
+
ctx.textAlign = 'center';
|
|
1440
|
+
ctx.textBaseline = 'middle';
|
|
1441
|
+
ctx.fillText(label, midX, boxY + boxHeight / 2);
|
|
1442
|
+
}
|
|
1443
|
+
static drawTrendAngle(drawing, ctx) {
|
|
1444
|
+
this.drawLine(drawing, ctx);
|
|
1445
|
+
if (drawing.points.length < 2)
|
|
1446
|
+
return;
|
|
1447
|
+
const start = drawing.points[0];
|
|
1448
|
+
const end = drawing.points[1];
|
|
1449
|
+
if (!start || !end)
|
|
1450
|
+
return;
|
|
1451
|
+
const angleRad = Math.atan2(start.y - end.y, end.x - start.x);
|
|
1452
|
+
const angleDeg = ((angleRad * 180) / Math.PI + 360) % 360;
|
|
1453
|
+
const label = `${angleDeg.toFixed(1)}°`;
|
|
1454
|
+
ctx.font = '11px Inter, sans-serif';
|
|
1455
|
+
ctx.fillStyle = '#f8fafc';
|
|
1456
|
+
ctx.strokeStyle = '#2563eb';
|
|
1457
|
+
const paddingX = 8;
|
|
1458
|
+
const textWidth = ctx.measureText(label).width;
|
|
1459
|
+
const boxWidth = textWidth + paddingX * 2;
|
|
1460
|
+
const boxHeight = 18;
|
|
1461
|
+
const boxX = end.x + 10;
|
|
1462
|
+
const boxY = end.y - boxHeight - 4;
|
|
1463
|
+
ctx.beginPath();
|
|
1464
|
+
drawRoundedRect(ctx, boxX, boxY, boxWidth, boxHeight, 8);
|
|
1465
|
+
ctx.fillStyle = 'rgba(2,6,23,0.85)';
|
|
1466
|
+
ctx.fill();
|
|
1467
|
+
ctx.stroke();
|
|
1468
|
+
ctx.fillStyle = '#f8fafc';
|
|
1469
|
+
ctx.textAlign = 'center';
|
|
1470
|
+
ctx.textBaseline = 'middle';
|
|
1471
|
+
ctx.fillText(label, boxX + boxWidth / 2, boxY + boxHeight / 2);
|
|
1472
|
+
}
|
|
1473
|
+
static drawHorizontalRay(drawing, ctx) {
|
|
1474
|
+
if (drawing.points.length < 1)
|
|
1475
|
+
return;
|
|
1476
|
+
const start = drawing.points[0];
|
|
1477
|
+
const guide = drawing.points[1] ?? start;
|
|
1478
|
+
if (!start || !guide)
|
|
1479
|
+
return;
|
|
1480
|
+
const direction = guide.x >= start.x ? 1 : -1;
|
|
1481
|
+
const targetX = direction > 0 ? ctx.canvas.width : 0;
|
|
1482
|
+
ctx.beginPath();
|
|
1483
|
+
ctx.moveTo(start.x, start.y);
|
|
1484
|
+
ctx.lineTo(targetX, start.y);
|
|
1485
|
+
ctx.stroke();
|
|
1486
|
+
}
|
|
1487
|
+
static drawInfiniteHorizontal(drawing, ctx) {
|
|
1488
|
+
if (drawing.points.length < 1)
|
|
1489
|
+
return;
|
|
1490
|
+
const start = drawing.points[0];
|
|
1491
|
+
if (!start)
|
|
1492
|
+
return;
|
|
1493
|
+
ctx.beginPath();
|
|
1494
|
+
ctx.moveTo(0, start.y);
|
|
1495
|
+
ctx.lineTo(ctx.canvas.width, start.y);
|
|
1496
|
+
ctx.stroke();
|
|
1497
|
+
}
|
|
1498
|
+
static drawVerticalLine(drawing, ctx) {
|
|
1499
|
+
if (drawing.points.length < 1)
|
|
1500
|
+
return;
|
|
1501
|
+
const start = drawing.points[0];
|
|
1502
|
+
if (!start)
|
|
1503
|
+
return;
|
|
1504
|
+
ctx.beginPath();
|
|
1505
|
+
ctx.moveTo(start.x, 0);
|
|
1506
|
+
ctx.lineTo(start.x, ctx.canvas.height);
|
|
1507
|
+
ctx.stroke();
|
|
1508
|
+
}
|
|
1509
|
+
static computeBoundaryIntersections(start, end, width, height) {
|
|
1510
|
+
const intersections = [];
|
|
1511
|
+
const dx = end.x - start.x;
|
|
1512
|
+
const dy = end.y - start.y;
|
|
1513
|
+
if (dx === 0 && dy === 0) {
|
|
1514
|
+
return intersections;
|
|
1515
|
+
}
|
|
1516
|
+
if (dx !== 0) {
|
|
1517
|
+
const tLeft = (0 - start.x) / dx;
|
|
1518
|
+
const yLeft = start.y + tLeft * dy;
|
|
1519
|
+
if (!Number.isNaN(yLeft) && yLeft >= 0 && yLeft <= height) {
|
|
1520
|
+
intersections.push({ x: 0, y: yLeft, t: tLeft });
|
|
1521
|
+
}
|
|
1522
|
+
const tRight = (width - start.x) / dx;
|
|
1523
|
+
const yRight = start.y + tRight * dy;
|
|
1524
|
+
if (!Number.isNaN(yRight) && yRight >= 0 && yRight <= height) {
|
|
1525
|
+
intersections.push({ x: width, y: yRight, t: tRight });
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (dy !== 0) {
|
|
1529
|
+
const tTop = (0 - start.y) / dy;
|
|
1530
|
+
const xTop = start.x + tTop * dx;
|
|
1531
|
+
if (!Number.isNaN(xTop) && xTop >= 0 && xTop <= width) {
|
|
1532
|
+
intersections.push({ x: xTop, y: 0, t: tTop });
|
|
1533
|
+
}
|
|
1534
|
+
const tBottom = (height - start.y) / dy;
|
|
1535
|
+
const xBottom = start.x + tBottom * dx;
|
|
1536
|
+
if (!Number.isNaN(xBottom) && xBottom >= 0 && xBottom <= width) {
|
|
1537
|
+
intersections.push({ x: xBottom, y: height, t: tBottom });
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return intersections;
|
|
1541
|
+
}
|
|
1542
|
+
static drawRectangle(drawing, ctx) {
|
|
1543
|
+
if (drawing.points.length < 2)
|
|
1544
|
+
return;
|
|
1545
|
+
const start = drawing.points[0];
|
|
1546
|
+
const end = drawing.points[1];
|
|
1547
|
+
if (!start || !end)
|
|
1548
|
+
return;
|
|
1549
|
+
const rectX = Math.min(start.x, end.x);
|
|
1550
|
+
const rectY = Math.min(start.y, end.y);
|
|
1551
|
+
const rectWidth = Math.abs(end.x - start.x);
|
|
1552
|
+
const rectHeight = Math.abs(end.y - start.y);
|
|
1553
|
+
if (drawing.backgroundColor) {
|
|
1554
|
+
ctx.fillStyle = drawing.backgroundColor;
|
|
1555
|
+
ctx.fillRect(rectX, rectY, rectWidth, rectHeight);
|
|
1556
|
+
}
|
|
1557
|
+
ctx.strokeRect(rectX, rectY, rectWidth, rectHeight);
|
|
1558
|
+
}
|
|
1559
|
+
static drawTriangle(drawing, ctx) {
|
|
1560
|
+
if (drawing.points.length < 3)
|
|
1561
|
+
return;
|
|
1562
|
+
const p1 = drawing.points[0];
|
|
1563
|
+
const p2 = drawing.points[1];
|
|
1564
|
+
const p3 = drawing.points[2];
|
|
1565
|
+
if (!p1 || !p2 || !p3)
|
|
1566
|
+
return;
|
|
1567
|
+
ctx.beginPath();
|
|
1568
|
+
ctx.moveTo(p1.x, p1.y);
|
|
1569
|
+
ctx.lineTo(p2.x, p2.y);
|
|
1570
|
+
ctx.lineTo(p3.x, p3.y);
|
|
1571
|
+
ctx.closePath();
|
|
1572
|
+
if (drawing.backgroundColor) {
|
|
1573
|
+
ctx.fillStyle = drawing.backgroundColor;
|
|
1574
|
+
ctx.fill();
|
|
1575
|
+
}
|
|
1576
|
+
ctx.stroke();
|
|
1577
|
+
}
|
|
1578
|
+
static drawChannel(drawing, ctx) {
|
|
1579
|
+
if (drawing.points.length < 4) {
|
|
1580
|
+
this.drawLine(drawing, ctx);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
const [start1, end1, start2, end2] = drawing.points;
|
|
1584
|
+
if (!start1 || !end1 || !start2 || !end2)
|
|
1585
|
+
return;
|
|
1586
|
+
if (drawing.backgroundColor) {
|
|
1587
|
+
ctx.fillStyle = drawing.backgroundColor;
|
|
1588
|
+
ctx.beginPath();
|
|
1589
|
+
ctx.moveTo(start1.x, start1.y);
|
|
1590
|
+
ctx.lineTo(end1.x, end1.y);
|
|
1591
|
+
ctx.lineTo(end2.x, end2.y);
|
|
1592
|
+
ctx.lineTo(start2.x, start2.y);
|
|
1593
|
+
ctx.closePath();
|
|
1594
|
+
ctx.fill();
|
|
1595
|
+
}
|
|
1596
|
+
ctx.beginPath();
|
|
1597
|
+
ctx.moveTo(start1.x, start1.y);
|
|
1598
|
+
ctx.lineTo(end1.x, end1.y);
|
|
1599
|
+
ctx.stroke();
|
|
1600
|
+
ctx.beginPath();
|
|
1601
|
+
ctx.moveTo(start2.x, start2.y);
|
|
1602
|
+
ctx.lineTo(end2.x, end2.y);
|
|
1603
|
+
ctx.stroke();
|
|
1604
|
+
}
|
|
1605
|
+
static drawText(drawing, ctx) {
|
|
1606
|
+
if (drawing.points.length < 1 || !drawing.text)
|
|
1607
|
+
return;
|
|
1608
|
+
const point = drawing.points[0];
|
|
1609
|
+
if (!point)
|
|
1610
|
+
return;
|
|
1611
|
+
const fontSize = drawing.fontSize ?? 14;
|
|
1612
|
+
ctx.font = `${fontSize}px Inter, sans-serif`;
|
|
1613
|
+
const textMetrics = ctx.measureText(drawing.text);
|
|
1614
|
+
const textWidth = textMetrics.width;
|
|
1615
|
+
const paddingX = 12;
|
|
1616
|
+
const paddingY = 6;
|
|
1617
|
+
const boxWidth = textWidth + paddingX * 2;
|
|
1618
|
+
const boxHeight = fontSize + paddingY * 2;
|
|
1619
|
+
const originX = point.x - boxWidth / 2;
|
|
1620
|
+
const originY = point.y - boxHeight - 6;
|
|
1621
|
+
ctx.fillStyle = drawing.backgroundColor ?? 'rgba(15,23,42,0.85)';
|
|
1622
|
+
drawRoundedRect(ctx, originX, originY, boxWidth, boxHeight, 10);
|
|
1623
|
+
ctx.fill();
|
|
1624
|
+
ctx.fillStyle = drawing.color;
|
|
1625
|
+
ctx.textAlign = 'center';
|
|
1626
|
+
ctx.textBaseline = 'middle';
|
|
1627
|
+
ctx.fillText(drawing.text, point.x, originY + boxHeight / 2);
|
|
1628
|
+
}
|
|
1629
|
+
static drawIcon(drawing, ctx) {
|
|
1630
|
+
if (drawing.points.length < 1 || !drawing.icon)
|
|
1631
|
+
return;
|
|
1632
|
+
const point = drawing.points[0];
|
|
1633
|
+
if (!point)
|
|
1634
|
+
return;
|
|
1635
|
+
const fontSize = drawing.fontSize ?? 20;
|
|
1636
|
+
ctx.font = `${fontSize}px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI Emoji", "Noto Color Emoji", "Android Emoji", "EmojiSymbols"`;
|
|
1637
|
+
ctx.textAlign = 'center';
|
|
1638
|
+
ctx.textBaseline = 'middle';
|
|
1639
|
+
ctx.fillStyle = drawing.color;
|
|
1640
|
+
ctx.fillText(drawing.icon, point.x, point.y);
|
|
1641
|
+
}
|
|
1642
|
+
static drawRuler(drawing, ctx) {
|
|
1643
|
+
this.drawLine(drawing, ctx);
|
|
1644
|
+
if (drawing.points.length < 2)
|
|
1645
|
+
return;
|
|
1646
|
+
const start = drawing.points[0];
|
|
1647
|
+
const end = drawing.points[1];
|
|
1648
|
+
const measurement = drawing.meta?.measurement;
|
|
1649
|
+
if (!start || !end || !measurement)
|
|
1650
|
+
return;
|
|
1651
|
+
const midX = (start.x + end.x) / 2;
|
|
1652
|
+
const midY = (start.y + end.y) / 2;
|
|
1653
|
+
const label = `${measurement.priceDelta >= 0 ? '+' : ''}${measurement.priceDelta.toFixed(2)} | ${measurement.percentDelta.toFixed(2)}%`;
|
|
1654
|
+
ctx.font = '11px Inter, sans-serif';
|
|
1655
|
+
const textWidth = ctx.measureText(label).width;
|
|
1656
|
+
const boxWidth = textWidth + 16;
|
|
1657
|
+
const boxHeight = 20;
|
|
1658
|
+
const boxX = midX - boxWidth / 2;
|
|
1659
|
+
const boxY = midY - boxHeight - 8;
|
|
1660
|
+
ctx.fillStyle = 'rgba(15,23,42,0.9)';
|
|
1661
|
+
ctx.strokeStyle = drawing.color;
|
|
1662
|
+
drawRoundedRect(ctx, boxX, boxY, boxWidth, boxHeight, 8);
|
|
1663
|
+
ctx.fill();
|
|
1664
|
+
ctx.stroke();
|
|
1665
|
+
ctx.fillStyle = '#e2e8f0';
|
|
1666
|
+
ctx.textAlign = 'center';
|
|
1667
|
+
ctx.textBaseline = 'middle';
|
|
1668
|
+
ctx.fillText(label, midX, boxY + boxHeight / 2);
|
|
1669
|
+
}
|
|
1670
|
+
static drawFreehand(drawing, ctx) {
|
|
1671
|
+
if (drawing.points.length < 2)
|
|
1672
|
+
return;
|
|
1673
|
+
const firstPoint = drawing.points[0];
|
|
1674
|
+
if (!firstPoint)
|
|
1675
|
+
return;
|
|
1676
|
+
ctx.beginPath();
|
|
1677
|
+
ctx.moveTo(firstPoint.x, firstPoint.y);
|
|
1678
|
+
for (let i = 1; i < drawing.points.length; i++) {
|
|
1679
|
+
const point = drawing.points[i];
|
|
1680
|
+
if (point) {
|
|
1681
|
+
ctx.lineTo(point.x, point.y);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
ctx.stroke();
|
|
1685
|
+
}
|
|
1686
|
+
static drawFibonacci(drawing, ctx) {
|
|
1687
|
+
if (drawing.points.length < 2)
|
|
1688
|
+
return;
|
|
1689
|
+
const start = drawing.points[0];
|
|
1690
|
+
const end = drawing.points[1];
|
|
1691
|
+
if (!start || !end)
|
|
1692
|
+
return;
|
|
1693
|
+
const levels = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
|
|
1694
|
+
const height = end.y - start.y;
|
|
1695
|
+
levels.forEach(level => {
|
|
1696
|
+
const y = start.y + height * level;
|
|
1697
|
+
ctx.beginPath();
|
|
1698
|
+
ctx.moveTo(start.x, y);
|
|
1699
|
+
ctx.lineTo(end.x, y);
|
|
1700
|
+
ctx.stroke();
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
static drawCrossLine(drawing, ctx) {
|
|
1704
|
+
if (drawing.points.length < 1)
|
|
1705
|
+
return;
|
|
1706
|
+
const center = drawing.points[0];
|
|
1707
|
+
if (!center)
|
|
1708
|
+
return;
|
|
1709
|
+
// Draw horizontal line
|
|
1710
|
+
ctx.beginPath();
|
|
1711
|
+
ctx.moveTo(0, center.y);
|
|
1712
|
+
ctx.lineTo(ctx.canvas.width, center.y);
|
|
1713
|
+
ctx.stroke();
|
|
1714
|
+
// Draw vertical line
|
|
1715
|
+
ctx.beginPath();
|
|
1716
|
+
ctx.moveTo(center.x, 0);
|
|
1717
|
+
ctx.lineTo(center.x, ctx.canvas.height);
|
|
1718
|
+
ctx.stroke();
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
const UI_TEXT = {
|
|
1723
|
+
en: {
|
|
1724
|
+
symbolLabel: 'Symbol',
|
|
1725
|
+
quickTipsTitle: 'Quick start',
|
|
1726
|
+
quickTipsButton: 'Got it',
|
|
1727
|
+
quickTips: [
|
|
1728
|
+
'Drag anywhere on the canvas to pan the chart.',
|
|
1729
|
+
'Use your mouse wheel or trackpad to zoom smoothly.',
|
|
1730
|
+
'Pick any tool on the left rail and draw on the canvas.'
|
|
1731
|
+
],
|
|
1732
|
+
timeframeTitle: 'Quick timeframes',
|
|
1733
|
+
axis: {
|
|
1734
|
+
price: 'Price',
|
|
1735
|
+
time: 'Time'
|
|
1736
|
+
},
|
|
1737
|
+
buttons: {
|
|
1738
|
+
series: 'Change series',
|
|
1739
|
+
indicators: 'Indicators',
|
|
1740
|
+
config: 'Settings',
|
|
1741
|
+
fullscreenEnter: 'Enter fullscreen',
|
|
1742
|
+
fullscreenExit: 'Exit fullscreen',
|
|
1743
|
+
screenshot: 'Save screenshot',
|
|
1744
|
+
help: 'Quick tips'
|
|
1745
|
+
},
|
|
1746
|
+
ohlcLabels: {
|
|
1747
|
+
open: 'Open',
|
|
1748
|
+
high: 'High',
|
|
1749
|
+
low: 'Low',
|
|
1750
|
+
close: 'Close',
|
|
1751
|
+
volume: 'Volume'
|
|
1752
|
+
},
|
|
1753
|
+
priceChangeLabel: 'Change',
|
|
1754
|
+
seriesMenuTitle: 'Series type',
|
|
1755
|
+
indicatorsPanelTitle: 'Indicators',
|
|
1756
|
+
config: {
|
|
1757
|
+
title: 'Settings',
|
|
1758
|
+
language: 'Language',
|
|
1759
|
+
colors: 'Color theme',
|
|
1760
|
+
colorOptions: {
|
|
1761
|
+
green: 'Green / Red',
|
|
1762
|
+
red: 'Red / Green'
|
|
1763
|
+
},
|
|
1764
|
+
soon: 'More features coming soon...'
|
|
1765
|
+
},
|
|
1766
|
+
actions: {
|
|
1767
|
+
zoomIn: 'Zoom in',
|
|
1768
|
+
zoomOut: 'Zoom out',
|
|
1769
|
+
lock: 'Lock interactions',
|
|
1770
|
+
unlock: 'Unlock canvas',
|
|
1771
|
+
hide: 'Hide drawings',
|
|
1772
|
+
show: 'Show drawings',
|
|
1773
|
+
reset: 'Reset view',
|
|
1774
|
+
magnetOn: 'Magnet on',
|
|
1775
|
+
magnetOff: 'Magnet off',
|
|
1776
|
+
capture: 'Screenshot',
|
|
1777
|
+
indicators: 'Indicators'
|
|
1778
|
+
},
|
|
1779
|
+
toasts: {
|
|
1780
|
+
screenshot: 'Screenshot saved'
|
|
1781
|
+
},
|
|
1782
|
+
novice: {
|
|
1783
|
+
title: 'Novice mode',
|
|
1784
|
+
magnetOn: 'Magnet enabled for snapping.',
|
|
1785
|
+
magnetOff: 'Enable the magnet for precise snapping.',
|
|
1786
|
+
lockOn: 'Unlock the canvas to interact.',
|
|
1787
|
+
lockOff: 'You can lock the canvas from Actions.',
|
|
1788
|
+
hidden: 'Your drawings are hidden.',
|
|
1789
|
+
visible: 'Use “Hide” if you need a clean canvas.',
|
|
1790
|
+
reopen: 'Novice mode',
|
|
1791
|
+
checklistTitle: 'Starter checklist',
|
|
1792
|
+
legendTitle: 'Action legend',
|
|
1793
|
+
legendHint: 'Try every icon and see what it changes.',
|
|
1794
|
+
steps: {
|
|
1795
|
+
pan: {
|
|
1796
|
+
title: 'Move the chart',
|
|
1797
|
+
helper: 'Click and drag anywhere until the crosshair follows you.'
|
|
1798
|
+
},
|
|
1799
|
+
zoom: {
|
|
1800
|
+
title: 'Zoom smoothly',
|
|
1801
|
+
helper: 'Use the wheel or the zoom buttons to focus a zone.'
|
|
1802
|
+
},
|
|
1803
|
+
draw: {
|
|
1804
|
+
title: 'Place a drawing',
|
|
1805
|
+
helper: 'Pick any tool and drop at least one point on the canvas.'
|
|
1806
|
+
},
|
|
1807
|
+
capture: {
|
|
1808
|
+
title: 'Save a capture',
|
|
1809
|
+
helper: 'Use the camera icon to export a PNG.'
|
|
1810
|
+
}
|
|
1811
|
+
},
|
|
1812
|
+
labels: {
|
|
1813
|
+
magnet: 'Magnet',
|
|
1814
|
+
lock: 'Lock',
|
|
1815
|
+
drawings: 'Drawings'
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
},
|
|
1819
|
+
es: {
|
|
1820
|
+
symbolLabel: 'Símbolo',
|
|
1821
|
+
quickTipsTitle: 'Guía rápida',
|
|
1822
|
+
quickTipsButton: 'Entendido',
|
|
1823
|
+
quickTips: [
|
|
1824
|
+
'Haz clic y arrastra para desplazar el gráfico.',
|
|
1825
|
+
'Usa la rueda o el trackpad para hacer zoom suave.',
|
|
1826
|
+
'Selecciona una herramienta y dibuja sobre el lienzo.'
|
|
1827
|
+
],
|
|
1828
|
+
timeframeTitle: 'Frecuencias rápidas',
|
|
1829
|
+
axis: {
|
|
1830
|
+
price: 'Precio',
|
|
1831
|
+
time: 'Tiempo'
|
|
1832
|
+
},
|
|
1833
|
+
buttons: {
|
|
1834
|
+
series: 'Cambiar serie',
|
|
1835
|
+
indicators: 'Indicadores',
|
|
1836
|
+
config: 'Configuración',
|
|
1837
|
+
fullscreenEnter: 'Pantalla completa',
|
|
1838
|
+
fullscreenExit: 'Salir de pantalla completa',
|
|
1839
|
+
screenshot: 'Guardar captura',
|
|
1840
|
+
help: 'Tips rápidos'
|
|
1841
|
+
},
|
|
1842
|
+
ohlcLabels: {
|
|
1843
|
+
open: 'Apertura',
|
|
1844
|
+
high: 'Máximo',
|
|
1845
|
+
low: 'Mínimo',
|
|
1846
|
+
close: 'Cierre',
|
|
1847
|
+
volume: 'Volumen'
|
|
1848
|
+
},
|
|
1849
|
+
priceChangeLabel: 'Cambio',
|
|
1850
|
+
seriesMenuTitle: 'Tipos de series',
|
|
1851
|
+
indicatorsPanelTitle: 'Indicadores',
|
|
1852
|
+
config: {
|
|
1853
|
+
title: 'Configuración',
|
|
1854
|
+
language: 'Idioma',
|
|
1855
|
+
colors: 'Tema de color',
|
|
1856
|
+
colorOptions: {
|
|
1857
|
+
green: 'Verde / Rojo',
|
|
1858
|
+
red: 'Rojo / Verde'
|
|
1859
|
+
},
|
|
1860
|
+
soon: 'Más funciones próximamente...'
|
|
1861
|
+
},
|
|
1862
|
+
actions: {
|
|
1863
|
+
zoomIn: 'Acercar',
|
|
1864
|
+
zoomOut: 'Alejar',
|
|
1865
|
+
lock: 'Bloquear interacciones',
|
|
1866
|
+
unlock: 'Desbloquear',
|
|
1867
|
+
hide: 'Ocultar dibujos',
|
|
1868
|
+
show: 'Mostrar dibujos',
|
|
1869
|
+
reset: 'Restablecer vista',
|
|
1870
|
+
magnetOn: 'Imán activado',
|
|
1871
|
+
magnetOff: 'Imán desactivado',
|
|
1872
|
+
capture: 'Captura',
|
|
1873
|
+
indicators: 'Indicadores'
|
|
1874
|
+
},
|
|
1875
|
+
toasts: {
|
|
1876
|
+
screenshot: 'Captura descargada'
|
|
1877
|
+
},
|
|
1878
|
+
novice: {
|
|
1879
|
+
title: 'Modo novato',
|
|
1880
|
+
magnetOn: 'Imán activo para ajustar precios.',
|
|
1881
|
+
magnetOff: 'Activa el imán para más precisión.',
|
|
1882
|
+
lockOn: 'Desbloquea el lienzo para interactuar.',
|
|
1883
|
+
lockOff: 'Puedes bloquear el lienzo en Acciones.',
|
|
1884
|
+
hidden: 'Tus dibujos están ocultos.',
|
|
1885
|
+
visible: 'Usa “Ocultar” si quieres un lienzo limpio.',
|
|
1886
|
+
reopen: 'Modo novato',
|
|
1887
|
+
checklistTitle: 'Checklist inicial',
|
|
1888
|
+
legendTitle: 'Leyenda de acciones',
|
|
1889
|
+
legendHint: 'Prueba cada icono y observa el cambio.',
|
|
1890
|
+
steps: {
|
|
1891
|
+
pan: {
|
|
1892
|
+
title: 'Mueve el gráfico',
|
|
1893
|
+
helper: 'Haz clic y arrastra hasta que el cursor te siga.'
|
|
1894
|
+
},
|
|
1895
|
+
zoom: {
|
|
1896
|
+
title: 'Zoom suave',
|
|
1897
|
+
helper: 'Usa la rueda o los botones de zoom para enfocar una zona.'
|
|
1898
|
+
},
|
|
1899
|
+
draw: {
|
|
1900
|
+
title: 'Dibuja algo',
|
|
1901
|
+
helper: 'Elige una herramienta y deja al menos un punto en el lienzo.'
|
|
1902
|
+
},
|
|
1903
|
+
capture: {
|
|
1904
|
+
title: 'Guarda una captura',
|
|
1905
|
+
helper: 'Toca la cámara para exportar un PNG.'
|
|
1906
|
+
}
|
|
1907
|
+
},
|
|
1908
|
+
labels: {
|
|
1909
|
+
magnet: 'Imán',
|
|
1910
|
+
lock: 'Bloqueo',
|
|
1911
|
+
drawings: 'Dibujos'
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
};
|
|
1916
|
+
const TOOL_COLOR_MAP = {
|
|
1917
|
+
trendline: '#8ab4ff',
|
|
1918
|
+
ray: '#8ab4ff',
|
|
1919
|
+
'info-line': '#a5b4fc',
|
|
1920
|
+
'extended-line': '#93c5fd',
|
|
1921
|
+
'trend-angle': '#7dd3fc',
|
|
1922
|
+
horizontal: '#fbbf24',
|
|
1923
|
+
'horizontal-line': '#fbbf24',
|
|
1924
|
+
'horizontal-ray': '#facc15',
|
|
1925
|
+
'vertical-line': '#fdba74',
|
|
1926
|
+
'cross-line': '#fda4af',
|
|
1927
|
+
parallel: '#a78bfa',
|
|
1928
|
+
rectangle: '#60a5fa',
|
|
1929
|
+
triangle: '#f472b6',
|
|
1930
|
+
channel: '#34d399',
|
|
1931
|
+
'regression-trend': '#4ade80',
|
|
1932
|
+
'flat-top-bottom': '#2dd4bf',
|
|
1933
|
+
'disjoint-channel': '#22d3ee',
|
|
1934
|
+
pitchfork: '#f472b6',
|
|
1935
|
+
'schiff-pitchfork': '#f472b6',
|
|
1936
|
+
'modified-schiff-pitchfork': '#fb7185',
|
|
1937
|
+
'inside-pitchfork': '#fb7185',
|
|
1938
|
+
fibonacci: '#c4b5fd',
|
|
1939
|
+
text: '#e5e7eb',
|
|
1940
|
+
icon: '#fcd34d',
|
|
1941
|
+
ruler: '#f87171',
|
|
1942
|
+
freehand: '#c084fc'
|
|
1943
|
+
};
|
|
1944
|
+
const metricCardStyle = {
|
|
1945
|
+
background: 'rgba(15,23,42,0.85)',
|
|
1946
|
+
border: '1px solid #111827',
|
|
1947
|
+
borderRadius: '18px',
|
|
1948
|
+
padding: '14px 16px',
|
|
1949
|
+
display: 'flex',
|
|
1950
|
+
flexDirection: 'column',
|
|
1951
|
+
gap: '6px',
|
|
1952
|
+
minHeight: '90px'
|
|
1953
|
+
};
|
|
1954
|
+
const metricLabelStyle = {
|
|
1955
|
+
fontSize: '10px',
|
|
1956
|
+
textTransform: 'uppercase',
|
|
1957
|
+
letterSpacing: '0.08em',
|
|
1958
|
+
color: '#94a3b8'
|
|
1959
|
+
};
|
|
1960
|
+
const metricValueStyle = {
|
|
1961
|
+
fontSize: '18px',
|
|
1962
|
+
fontWeight: 700,
|
|
1963
|
+
color: '#f8fafc'
|
|
1964
|
+
};
|
|
1965
|
+
const metricSubValueStyle = {
|
|
1966
|
+
fontSize: '12px',
|
|
1967
|
+
color: '#8b9cc2'
|
|
1968
|
+
};
|
|
1969
|
+
const LINE_TOOL_TYPES = [
|
|
1970
|
+
'trendline',
|
|
1971
|
+
'ray',
|
|
1972
|
+
'info-line',
|
|
1973
|
+
'extended-line',
|
|
1974
|
+
'trend-angle',
|
|
1975
|
+
'horizontal',
|
|
1976
|
+
'horizontal-line',
|
|
1977
|
+
'horizontal-ray',
|
|
1978
|
+
'vertical-line',
|
|
1979
|
+
'cross-line',
|
|
1980
|
+
'parallel',
|
|
1981
|
+
'channel',
|
|
1982
|
+
'regression-trend',
|
|
1983
|
+
'flat-top-bottom',
|
|
1984
|
+
'disjoint-channel',
|
|
1985
|
+
'pitchfork',
|
|
1986
|
+
'schiff-pitchfork',
|
|
1987
|
+
'modified-schiff-pitchfork',
|
|
1988
|
+
'inside-pitchfork'
|
|
1989
|
+
];
|
|
1990
|
+
const LINE_TOOL_SET = new Set(LINE_TOOL_TYPES);
|
|
1991
|
+
const THEME_PRESETS = {
|
|
1992
|
+
dark: {
|
|
1993
|
+
pageBg: '#050910',
|
|
1994
|
+
heroFrom: '#111827',
|
|
1995
|
+
heroTo: '#0b1120',
|
|
1996
|
+
panelBg: 'rgba(15,23,42,0.9)',
|
|
1997
|
+
panelBorder: '#1f2937',
|
|
1998
|
+
cardBg: 'rgba(15,23,42,0.85)',
|
|
1999
|
+
cardBorder: '#111827',
|
|
2000
|
+
textPrimary: '#f8fafc',
|
|
2001
|
+
textSecondary: '#94a3b8',
|
|
2002
|
+
accent: '#2563eb',
|
|
2003
|
+
accentSoft: 'rgba(37,99,235,0.2)',
|
|
2004
|
+
railBg: 'rgba(2,6,23,0.85)',
|
|
2005
|
+
railBorder: '#111827',
|
|
2006
|
+
plotBg: '#020617',
|
|
2007
|
+
plotBorder: '#111827',
|
|
2008
|
+
scaleBg: 'rgba(15,23,42,0.9)',
|
|
2009
|
+
overlayBg: 'rgba(5,7,15,0.85)'
|
|
2010
|
+
},
|
|
2011
|
+
blue: {
|
|
2012
|
+
pageBg: '#060e1f',
|
|
2013
|
+
heroFrom: '#0f172a',
|
|
2014
|
+
heroTo: '#1d4ed8',
|
|
2015
|
+
panelBg: 'rgba(6,11,25,0.9)',
|
|
2016
|
+
panelBorder: '#1e3a8a',
|
|
2017
|
+
cardBg: 'rgba(13,23,42,0.92)',
|
|
2018
|
+
cardBorder: '#1d4ed8',
|
|
2019
|
+
textPrimary: '#e2e8f0',
|
|
2020
|
+
textSecondary: '#a5b4fc',
|
|
2021
|
+
accent: '#38bdf8',
|
|
2022
|
+
accentSoft: 'rgba(56,189,248,0.2)',
|
|
2023
|
+
railBg: 'rgba(4,8,20,0.85)',
|
|
2024
|
+
railBorder: '#1e3a8a',
|
|
2025
|
+
plotBg: '#020b1f',
|
|
2026
|
+
plotBorder: '#1d4ed8',
|
|
2027
|
+
scaleBg: 'rgba(6,11,25,0.95)',
|
|
2028
|
+
overlayBg: 'rgba(4,8,20,0.8)'
|
|
2029
|
+
},
|
|
2030
|
+
light: {
|
|
2031
|
+
pageBg: '#f8fafc',
|
|
2032
|
+
heroFrom: '#e3e8ff',
|
|
2033
|
+
heroTo: '#fdfdff',
|
|
2034
|
+
panelBg: '#ffffff',
|
|
2035
|
+
panelBorder: '#d7def1',
|
|
2036
|
+
cardBg: '#ffffff',
|
|
2037
|
+
cardBorder: '#dde3f7',
|
|
2038
|
+
textPrimary: '#0f172a',
|
|
2039
|
+
textSecondary: '#5b6473',
|
|
2040
|
+
accent: '#2563eb',
|
|
2041
|
+
accentSoft: 'rgba(37,99,235,0.18)',
|
|
2042
|
+
railBg: '#ffffff',
|
|
2043
|
+
railBorder: '#dbe4fb',
|
|
2044
|
+
plotBg: '#ffffff',
|
|
2045
|
+
plotBorder: '#dbe4fb',
|
|
2046
|
+
scaleBg: '#f3f6ff',
|
|
2047
|
+
overlayBg: 'rgba(15,23,42,0.18)'
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
const DEFAULT_CUSTOM_THEME = { ...THEME_PRESETS.dark };
|
|
2051
|
+
const CUSTOM_THEME_FIELDS = [
|
|
2052
|
+
{ key: 'pageBg', label: 'Page background' },
|
|
2053
|
+
{ key: 'heroFrom', label: 'Header gradient (from)' },
|
|
2054
|
+
{ key: 'heroTo', label: 'Header gradient (to)' },
|
|
2055
|
+
{ key: 'accent', label: 'Accent color' },
|
|
2056
|
+
{ key: 'plotBg', label: 'Plot background' }
|
|
2057
|
+
];
|
|
2058
|
+
const MIN_CANDLE_PX = 4;
|
|
2059
|
+
const MAX_CANDLE_PX = 28;
|
|
2060
|
+
const BUFFER_FRACTION = 0.18;
|
|
2061
|
+
const INERTIA_DURATION_MS = 900;
|
|
2062
|
+
const ZOOM_MIN = 0.2;
|
|
2063
|
+
const ZOOM_MAX = 6;
|
|
2064
|
+
const easeOutQuad = (t) => 1 - (1 - t) * (1 - t);
|
|
2065
|
+
const easeInOutCubic = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
2066
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
2067
|
+
const alignStroke = (value) => Math.round(value) + 0.5;
|
|
2068
|
+
const createParallelPoints = (start, end, offset) => {
|
|
2069
|
+
const dx = end.x - start.x;
|
|
2070
|
+
const dy = end.y - start.y;
|
|
2071
|
+
const length = Math.hypot(dx, dy) || 1;
|
|
2072
|
+
const offsetX = (-dy / length) * offset;
|
|
2073
|
+
const offsetY = (dx / length) * offset;
|
|
2074
|
+
const start2 = { ...start, x: start.x + offsetX, y: start.y + offsetY };
|
|
2075
|
+
const end2 = { ...end, x: end.x + offsetX, y: end.y + offsetY };
|
|
2076
|
+
return [start, end, start2, end2];
|
|
2077
|
+
};
|
|
2078
|
+
const getChannelOffset = (tool) => {
|
|
2079
|
+
switch (tool) {
|
|
2080
|
+
case 'channel':
|
|
2081
|
+
return 40;
|
|
2082
|
+
case 'regression-trend':
|
|
2083
|
+
return 36;
|
|
2084
|
+
case 'flat-top-bottom':
|
|
2085
|
+
return 28;
|
|
2086
|
+
case 'disjoint-channel':
|
|
2087
|
+
return 48;
|
|
2088
|
+
case 'pitchfork':
|
|
2089
|
+
case 'schiff-pitchfork':
|
|
2090
|
+
case 'modified-schiff-pitchfork':
|
|
2091
|
+
case 'inside-pitchfork':
|
|
2092
|
+
return 34;
|
|
2093
|
+
default:
|
|
2094
|
+
return 28;
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
const createTrianglePoints = (start, end) => {
|
|
2098
|
+
const apexDirection = start.y < end.y ? -1 : 1;
|
|
2099
|
+
const apexHeight = Math.max(24, Math.abs(end.y - start.y));
|
|
2100
|
+
const apex = {
|
|
2101
|
+
...start,
|
|
2102
|
+
x: (start.x + end.x) / 2,
|
|
2103
|
+
y: start.y + apexDirection * apexHeight,
|
|
2104
|
+
time: (start.time + end.time) / 2
|
|
2105
|
+
};
|
|
2106
|
+
return [start, end, apex];
|
|
2107
|
+
};
|
|
2108
|
+
const buildRulerMeta = (start, end) => ({
|
|
2109
|
+
distancePx: Math.hypot(end.x - start.x, end.y - start.y),
|
|
2110
|
+
priceDelta: end.price - start.price,
|
|
2111
|
+
percentDelta: start.price !== 0 ? ((end.price - start.price) / start.price) * 100 : 0
|
|
2112
|
+
});
|
|
2113
|
+
const computeViewportCount = (chartWidth, timeframe, zoomLevel, getVisibleCountFn) => {
|
|
2114
|
+
if (chartWidth <= 0)
|
|
2115
|
+
return 0;
|
|
2116
|
+
const timeframeHint = getVisibleCountFn(timeframe);
|
|
2117
|
+
const pxPerCandle = clamp(12 / Math.max(zoomLevel, 0.1), MIN_CANDLE_PX, MAX_CANDLE_PX);
|
|
2118
|
+
const widthBased = Math.max(10, Math.floor(chartWidth / pxPerCandle));
|
|
2119
|
+
const blended = (timeframeHint + widthBased) / 2;
|
|
2120
|
+
return Math.max(10, Math.round(blended / Math.max(zoomLevel, 0.1)));
|
|
2121
|
+
};
|
|
2122
|
+
const computeVisibleWindow = (dataset, chartWidth, timeframe, zoomLevel, panOffset, getVisibleCountFn) => {
|
|
2123
|
+
const datasetLength = dataset.length;
|
|
2124
|
+
if (!datasetLength || chartWidth <= 0) {
|
|
2125
|
+
return {
|
|
2126
|
+
visibleSlice: [],
|
|
2127
|
+
meta: {
|
|
2128
|
+
start: 0,
|
|
2129
|
+
end: 0,
|
|
2130
|
+
bufferStart: 0,
|
|
2131
|
+
bufferEnd: 0,
|
|
2132
|
+
visibleCount: 0,
|
|
2133
|
+
buffer: 0
|
|
2134
|
+
}
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
const visibleCount = Math.min(datasetLength, computeViewportCount(chartWidth, timeframe, zoomLevel, getVisibleCountFn));
|
|
2138
|
+
const buffer = Math.min(200, Math.max(10, Math.round(visibleCount * BUFFER_FRACTION)));
|
|
2139
|
+
const unclampedStart = datasetLength - visibleCount - panOffset;
|
|
2140
|
+
const start = clamp(Math.round(unclampedStart), 0, datasetLength);
|
|
2141
|
+
const end = Math.min(datasetLength, start + visibleCount);
|
|
2142
|
+
const bufferStart = clamp(start - buffer, 0, datasetLength);
|
|
2143
|
+
const bufferEnd = clamp(end + buffer, bufferStart, datasetLength);
|
|
2144
|
+
const visibleSlice = dataset.slice(start, end);
|
|
2145
|
+
return {
|
|
2146
|
+
visibleSlice,
|
|
2147
|
+
meta: {
|
|
2148
|
+
start,
|
|
2149
|
+
end,
|
|
2150
|
+
bufferStart,
|
|
2151
|
+
bufferEnd,
|
|
2152
|
+
visibleCount: visibleSlice.length,
|
|
2153
|
+
buffer
|
|
2154
|
+
}
|
|
2155
|
+
};
|
|
2156
|
+
};
|
|
2157
|
+
const TradingViewChart = ({ data, symbol = 'BTC/USDT', onTimeframeChange }) => {
|
|
2158
|
+
const chartRef = useRef(null);
|
|
2159
|
+
const volumeRef = useRef(null);
|
|
2160
|
+
const gridRef = useRef(null);
|
|
2161
|
+
const overlayRef = useRef(null);
|
|
2162
|
+
const drawingsRef = useRef(null);
|
|
2163
|
+
const mousePosRef = useRef({ x: 0, y: 0 });
|
|
2164
|
+
const showCrosshairRef = useRef(false);
|
|
2165
|
+
const containerRef = useRef(null);
|
|
2166
|
+
const plotAreaRef = useRef(null);
|
|
2167
|
+
const priceWindowRef = useRef({ min: 0, max: 1 });
|
|
2168
|
+
const [dimensions, setDimensions] = useState({ width: 800, height: 400, cssWidth: 800, cssHeight: 400 });
|
|
2169
|
+
const isDraggingRef = useRef(false);
|
|
2170
|
+
const lastMouseXRef = useRef(0);
|
|
2171
|
+
const coordsRef = useRef(createCoordinateSystem());
|
|
2172
|
+
const [calculatedIndicators, setCalculatedIndicators] = useState([]);
|
|
2173
|
+
const visibleWindowRef = useRef({ start: 0, end: 0, bufferStart: 0, bufferEnd: 0, visibleCount: 0, buffer: 0 });
|
|
2174
|
+
const visibleDataRef = useRef([]);
|
|
2175
|
+
const panOffsetRef = useRef(0);
|
|
2176
|
+
const zoomLevelRef = useRef(1);
|
|
2177
|
+
const dragSampleRef = useRef({ velocity: 0, lastTs: 0 });
|
|
2178
|
+
const inertiaRef = useRef({
|
|
2179
|
+
rafId: null,
|
|
2180
|
+
startTime: 0,
|
|
2181
|
+
lastTime: 0,
|
|
2182
|
+
initialVelocity: 0
|
|
2183
|
+
});
|
|
2184
|
+
const zoomAnimationRef = useRef(null);
|
|
2185
|
+
const zoomAnimationStateRef = useRef(null);
|
|
2186
|
+
// Zustand store
|
|
2187
|
+
const { data: storeData, visibleData, timeframe, zoomLevel, panOffset, indicators, showIndicatorsPanel, drawings, magnetEnabled, currentPrice, priceChange, priceChangePercent, activeTool, cursorType, strokeColor, strokeWidth, isDrawing, currentDrawing, selectedEmoji, interactionsLocked, drawingsHidden, toggleInteractionsLock, toggleDrawingsHidden, setData, setVisibleData, setTimeframe, setZoomLevel, setPanOffset, addIndicator, removeIndicator, toggleIndicatorsPanel, toggleMagnet, setIsDrawing, setCurrentDrawing, addDrawing, setSelectedEmoji, deleteDrawing, setCursorType } = useChartStore();
|
|
2188
|
+
const handleTimeframeChange = useCallback((nextTimeframe) => {
|
|
2189
|
+
setTimeframe(nextTimeframe);
|
|
2190
|
+
if (onTimeframeChange) {
|
|
2191
|
+
onTimeframeChange(nextTimeframe);
|
|
2192
|
+
}
|
|
2193
|
+
}, [setTimeframe, onTimeframeChange]);
|
|
2194
|
+
const getVisibleCount = useCallback((tf) => {
|
|
2195
|
+
switch (tf) {
|
|
2196
|
+
case '1m': return 200;
|
|
2197
|
+
case '3m': return 134;
|
|
2198
|
+
case '5m': return 100;
|
|
2199
|
+
case '15m': return 50;
|
|
2200
|
+
case '30m': return 25;
|
|
2201
|
+
case '1h': return 50;
|
|
2202
|
+
case '4h': return 25;
|
|
2203
|
+
case '12h': return 15;
|
|
2204
|
+
case '1D': return 10;
|
|
2205
|
+
case '3D': return 8;
|
|
2206
|
+
case '1W': return 5;
|
|
2207
|
+
case '1M': return 3;
|
|
2208
|
+
default: return 25;
|
|
2209
|
+
}
|
|
2210
|
+
}, []);
|
|
2211
|
+
useEffect(() => {
|
|
2212
|
+
setData(data);
|
|
2213
|
+
}, [data, setData]);
|
|
2214
|
+
useEffect(() => {
|
|
2215
|
+
const newIndicators = [];
|
|
2216
|
+
indicators.forEach(config => {
|
|
2217
|
+
if (!config.visible)
|
|
2218
|
+
return;
|
|
2219
|
+
try {
|
|
2220
|
+
switch (config.name) {
|
|
2221
|
+
case 'SMA':
|
|
2222
|
+
newIndicators.push(IndicatorCalculator.calculateSMA(storeData, config.params.period));
|
|
2223
|
+
break;
|
|
2224
|
+
case 'EMA':
|
|
2225
|
+
newIndicators.push(IndicatorCalculator.calculateEMA(storeData, config.params.period));
|
|
2226
|
+
break;
|
|
2227
|
+
case 'RSI':
|
|
2228
|
+
newIndicators.push(IndicatorCalculator.calculateRSI(storeData, config.params.period));
|
|
2229
|
+
break;
|
|
2230
|
+
case 'MACD':
|
|
2231
|
+
const macdResult = IndicatorCalculator.calculateMACD(storeData, config.params.fastPeriod, config.params.slowPeriod, config.params.signalPeriod);
|
|
2232
|
+
newIndicators.push(macdResult.macd, macdResult.signal, macdResult.histogram);
|
|
2233
|
+
break;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
catch (error) {
|
|
2237
|
+
console.error(`Error calculating ${config.name}:`, error);
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
setCalculatedIndicators(newIndicators);
|
|
2241
|
+
}, [storeData, indicators]);
|
|
2242
|
+
useEffect(() => {
|
|
2243
|
+
const updateDimensions = () => {
|
|
2244
|
+
if (plotAreaRef.current) {
|
|
2245
|
+
const rect = plotAreaRef.current.getBoundingClientRect();
|
|
2246
|
+
const dpr = window.devicePixelRatio || 1;
|
|
2247
|
+
setDimensions({ width: rect.width * dpr, height: rect.height * dpr, cssWidth: rect.width, cssHeight: rect.height });
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
updateDimensions();
|
|
2251
|
+
window.addEventListener('resize', updateDimensions);
|
|
2252
|
+
return () => window.removeEventListener('resize', updateDimensions);
|
|
2253
|
+
}, []);
|
|
2254
|
+
const { width, height, cssWidth, cssHeight } = dimensions;
|
|
2255
|
+
const priceScaleWidth = 60;
|
|
2256
|
+
const timeScaleHeight = 48;
|
|
2257
|
+
const volumeHeight = 120;
|
|
2258
|
+
const chartWidth = Math.max(1, cssWidth - priceScaleWidth);
|
|
2259
|
+
const chartHeight = Math.max(1, cssHeight - timeScaleHeight - volumeHeight);
|
|
2260
|
+
const overlayHeight = Math.max(1, cssHeight - timeScaleHeight);
|
|
2261
|
+
useEffect(() => {
|
|
2262
|
+
const { visibleSlice, meta } = computeVisibleWindow(storeData, chartWidth, timeframe, zoomLevel, panOffset, getVisibleCount);
|
|
2263
|
+
setVisibleData(visibleSlice);
|
|
2264
|
+
visibleWindowRef.current = meta;
|
|
2265
|
+
}, [storeData, chartWidth, timeframe, zoomLevel, panOffset, setVisibleData]);
|
|
2266
|
+
useEffect(() => {
|
|
2267
|
+
visibleDataRef.current = visibleData;
|
|
2268
|
+
}, [visibleData]);
|
|
2269
|
+
useEffect(() => {
|
|
2270
|
+
panOffsetRef.current = panOffset;
|
|
2271
|
+
}, [panOffset]);
|
|
2272
|
+
useEffect(() => {
|
|
2273
|
+
zoomLevelRef.current = zoomLevel;
|
|
2274
|
+
}, [zoomLevel]);
|
|
2275
|
+
const getVisibleCandles = useCallback(() => visibleDataRef.current, []);
|
|
2276
|
+
useCallback(() => visibleWindowRef.current, []);
|
|
2277
|
+
const clampPanOffset = useCallback((value) => clamp(value, -storeData.length, storeData.length), [storeData.length]);
|
|
2278
|
+
const setPanOffsetSafe = useCallback((value) => {
|
|
2279
|
+
const next = clampPanOffset(value);
|
|
2280
|
+
panOffsetRef.current = next;
|
|
2281
|
+
setPanOffset(next);
|
|
2282
|
+
}, [clampPanOffset, setPanOffset]);
|
|
2283
|
+
const clampZoomLevel = useCallback((value) => clamp(value, ZOOM_MIN, ZOOM_MAX), []);
|
|
2284
|
+
const setZoomLevelSafe = useCallback((value) => {
|
|
2285
|
+
const next = clampZoomLevel(value);
|
|
2286
|
+
zoomLevelRef.current = next;
|
|
2287
|
+
setZoomLevel(next);
|
|
2288
|
+
}, [clampZoomLevel, setZoomLevel]);
|
|
2289
|
+
const handleCustomThemeChange = useCallback((key, value) => {
|
|
2290
|
+
setCustomTheme((prev) => ({ ...prev, [key]: value }));
|
|
2291
|
+
}, []);
|
|
2292
|
+
const applyPanDelta = useCallback((deltaPx) => {
|
|
2293
|
+
if (!chartWidth)
|
|
2294
|
+
return;
|
|
2295
|
+
const candles = getVisibleCandles();
|
|
2296
|
+
const pxPerCandle = chartWidth / Math.max(candles.length || 1, 1);
|
|
2297
|
+
if (!isFinite(pxPerCandle) || pxPerCandle === 0)
|
|
2298
|
+
return;
|
|
2299
|
+
const deltaOffset = deltaPx / Math.max(pxPerCandle, 1e-3);
|
|
2300
|
+
setPanOffsetSafe(panOffsetRef.current + deltaOffset);
|
|
2301
|
+
}, [chartWidth, getVisibleCandles, setPanOffsetSafe]);
|
|
2302
|
+
const cancelInertia = useCallback(() => {
|
|
2303
|
+
if (inertiaRef.current.rafId !== null) {
|
|
2304
|
+
cancelAnimationFrame(inertiaRef.current.rafId);
|
|
2305
|
+
}
|
|
2306
|
+
inertiaRef.current = {
|
|
2307
|
+
rafId: null,
|
|
2308
|
+
startTime: 0,
|
|
2309
|
+
lastTime: 0,
|
|
2310
|
+
initialVelocity: 0
|
|
2311
|
+
};
|
|
2312
|
+
}, []);
|
|
2313
|
+
const startInertia = useCallback((initialVelocity) => {
|
|
2314
|
+
if (Math.abs(initialVelocity) < 0.002)
|
|
2315
|
+
return;
|
|
2316
|
+
cancelInertia();
|
|
2317
|
+
inertiaRef.current.initialVelocity = initialVelocity;
|
|
2318
|
+
inertiaRef.current.startTime = performance.now();
|
|
2319
|
+
inertiaRef.current.lastTime = inertiaRef.current.startTime;
|
|
2320
|
+
const step = (timestamp) => {
|
|
2321
|
+
const elapsed = timestamp - inertiaRef.current.startTime;
|
|
2322
|
+
const progress = Math.min(1, elapsed / INERTIA_DURATION_MS);
|
|
2323
|
+
const eased = 1 - easeOutQuad(progress);
|
|
2324
|
+
const currentVelocity = inertiaRef.current.initialVelocity * eased;
|
|
2325
|
+
const deltaTime = timestamp - inertiaRef.current.lastTime;
|
|
2326
|
+
inertiaRef.current.lastTime = timestamp;
|
|
2327
|
+
if (Math.abs(currentVelocity) < 0.001 || progress >= 1) {
|
|
2328
|
+
cancelInertia();
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
applyPanDelta(currentVelocity * deltaTime);
|
|
2332
|
+
inertiaRef.current.rafId = requestAnimationFrame(step);
|
|
2333
|
+
};
|
|
2334
|
+
inertiaRef.current.rafId = requestAnimationFrame(step);
|
|
2335
|
+
}, [applyPanDelta, cancelInertia]);
|
|
2336
|
+
const cancelZoomAnimation = useCallback(() => {
|
|
2337
|
+
if (zoomAnimationRef.current) {
|
|
2338
|
+
cancelAnimationFrame(zoomAnimationRef.current);
|
|
2339
|
+
zoomAnimationRef.current = null;
|
|
2340
|
+
}
|
|
2341
|
+
zoomAnimationStateRef.current = null;
|
|
2342
|
+
}, []);
|
|
2343
|
+
const applyZoomAtRatio = useCallback((nextZoom, anchorRatio) => {
|
|
2344
|
+
const datasetLength = storeData.length;
|
|
2345
|
+
if (!datasetLength) {
|
|
2346
|
+
setZoomLevelSafe(nextZoom);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
const prevCount = computeViewportCount(chartWidth, timeframe, zoomLevelRef.current, getVisibleCount);
|
|
2350
|
+
const nextCount = computeViewportCount(chartWidth, timeframe, nextZoom, getVisibleCount);
|
|
2351
|
+
if (!prevCount || !nextCount) {
|
|
2352
|
+
setZoomLevelSafe(nextZoom);
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
const startBefore = clamp(datasetLength - prevCount - panOffsetRef.current, 0, Math.max(0, datasetLength - prevCount));
|
|
2356
|
+
const anchorIndex = startBefore + anchorRatio * prevCount;
|
|
2357
|
+
const nextStart = clamp(anchorIndex - anchorRatio * nextCount, 0, Math.max(0, datasetLength - nextCount));
|
|
2358
|
+
const nextPan = datasetLength - nextCount - nextStart;
|
|
2359
|
+
setZoomLevelSafe(nextZoom);
|
|
2360
|
+
setPanOffsetSafe(nextPan);
|
|
2361
|
+
}, [chartWidth, timeframe, storeData.length, setZoomLevelSafe, setPanOffsetSafe, getVisibleCount]);
|
|
2362
|
+
const startZoomAnimation = useCallback((targetZoom, anchorRatio) => {
|
|
2363
|
+
const clampedTarget = clampZoomLevel(targetZoom);
|
|
2364
|
+
const currentZoom = zoomLevelRef.current;
|
|
2365
|
+
if (Math.abs(clampedTarget - currentZoom) < 0.001) {
|
|
2366
|
+
applyZoomAtRatio(clampedTarget, anchorRatio);
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
cancelZoomAnimation();
|
|
2370
|
+
zoomAnimationStateRef.current = {
|
|
2371
|
+
from: currentZoom,
|
|
2372
|
+
to: clampedTarget,
|
|
2373
|
+
startTime: performance.now(),
|
|
2374
|
+
duration: 220,
|
|
2375
|
+
anchorRatio
|
|
2376
|
+
};
|
|
2377
|
+
const animate = (timestamp) => {
|
|
2378
|
+
const state = zoomAnimationStateRef.current;
|
|
2379
|
+
if (!state)
|
|
2380
|
+
return;
|
|
2381
|
+
const progress = Math.min(1, (timestamp - state.startTime) / state.duration);
|
|
2382
|
+
const eased = easeInOutCubic(progress);
|
|
2383
|
+
const nextZoom = state.from + (state.to - state.from) * eased;
|
|
2384
|
+
applyZoomAtRatio(nextZoom, state.anchorRatio);
|
|
2385
|
+
if (progress < 1) {
|
|
2386
|
+
zoomAnimationRef.current = requestAnimationFrame(animate);
|
|
2387
|
+
}
|
|
2388
|
+
else {
|
|
2389
|
+
zoomAnimationRef.current = null;
|
|
2390
|
+
zoomAnimationStateRef.current = null;
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
zoomAnimationRef.current = requestAnimationFrame(animate);
|
|
2394
|
+
}, [applyZoomAtRatio, cancelZoomAnimation, clampZoomLevel]);
|
|
2395
|
+
useEffect(() => {
|
|
2396
|
+
return () => {
|
|
2397
|
+
cancelInertia();
|
|
2398
|
+
cancelZoomAnimation();
|
|
2399
|
+
};
|
|
2400
|
+
}, [cancelInertia, cancelZoomAnimation]);
|
|
2401
|
+
// Draw drawings
|
|
2402
|
+
useEffect(() => {
|
|
2403
|
+
const drawingsCtx = drawingsRef.current?.getContext('2d');
|
|
2404
|
+
if (drawingsCtx) {
|
|
2405
|
+
drawingsCtx.clearRect(0, 0, chartWidth, overlayHeight);
|
|
2406
|
+
if (drawingsHidden) {
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
drawings.forEach(drawing => {
|
|
2410
|
+
DrawingRenderer.draw(drawing, drawingsCtx);
|
|
2411
|
+
});
|
|
2412
|
+
if (currentDrawing) {
|
|
2413
|
+
DrawingRenderer.draw(currentDrawing, drawingsCtx);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}, [drawings, currentDrawing, chartWidth, overlayHeight, drawingsHidden]);
|
|
2417
|
+
const [priceLabels, setPriceLabels] = useState([]);
|
|
2418
|
+
const [timeLabels, setTimeLabels] = useState([]);
|
|
2419
|
+
const [clickedPrice, setClickedPrice] = useState(null);
|
|
2420
|
+
const [showConfigPanel, setShowConfigPanel] = useState(false);
|
|
2421
|
+
const [language, setLanguage] = useState('en');
|
|
2422
|
+
const [colorScheme, setColorScheme] = useState('green');
|
|
2423
|
+
const [themePreset, setThemePreset] = useState('dark');
|
|
2424
|
+
const [customTheme, setCustomTheme] = useState(DEFAULT_CUSTOM_THEME);
|
|
2425
|
+
const resolvedPreset = themePreset === 'custom' ? 'dark' : themePreset;
|
|
2426
|
+
const activeTheme = themePreset === 'custom' ? customTheme : THEME_PRESETS[resolvedPreset];
|
|
2427
|
+
const themeOptions = [
|
|
2428
|
+
{ key: 'dark', label: 'Dark' },
|
|
2429
|
+
{ key: 'blue', label: 'Blue' },
|
|
2430
|
+
{ key: 'light', label: 'Light' },
|
|
2431
|
+
{ key: 'custom', label: 'Custom' }
|
|
2432
|
+
];
|
|
2433
|
+
const elevatedShadow = themePreset === 'light' ? '0 15px 30px rgba(15,23,42,0.12)' : '0 15px 40px rgba(0,0,0,0.35)';
|
|
2434
|
+
const surfaceShadow = themePreset === 'light' ? '0 30px 50px rgba(15,23,42,0.12)' : '0 30px 60px rgba(0,0,0,0.35)';
|
|
2435
|
+
const iconBaseBg = themePreset === 'light' ? '#0f172a' : 'rgba(15,23,42,0.6)';
|
|
2436
|
+
const iconBaseColor = '#f8fafc';
|
|
2437
|
+
const leftRailBaseBg = themePreset === 'light' ? '#0f172a' : activeTheme.panelBg;
|
|
2438
|
+
const strings = useMemo(() => UI_TEXT[language] ?? UI_TEXT.en, [language]);
|
|
2439
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
2440
|
+
const [selectedCandleIndex, setSelectedCandleIndex] = useState(null);
|
|
2441
|
+
const [seriesType, setSeriesType] = useState('candles');
|
|
2442
|
+
const [showSeriesMenu, setShowSeriesMenu] = useState(false);
|
|
2443
|
+
const [isSeriesLoading, setIsSeriesLoading] = useState(false);
|
|
2444
|
+
const [showQuickTips, setShowQuickTips] = useState(true);
|
|
2445
|
+
const [toastMessage, setToastMessage] = useState(null);
|
|
2446
|
+
const [showNoteModal, setShowNoteModal] = useState(false);
|
|
2447
|
+
const [pendingNotePoint, setPendingNotePoint] = useState(null);
|
|
2448
|
+
const [noteDraft, setNoteDraft] = useState('');
|
|
2449
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
2450
|
+
useEffect(() => {
|
|
2451
|
+
if (!plotAreaRef.current)
|
|
2452
|
+
return;
|
|
2453
|
+
const rect = plotAreaRef.current.getBoundingClientRect();
|
|
2454
|
+
const dpr = window.devicePixelRatio || 1;
|
|
2455
|
+
setDimensions({ width: rect.width * dpr, height: rect.height * dpr, cssWidth: rect.width, cssHeight: rect.height });
|
|
2456
|
+
}, [isFullscreen, showIndicatorsPanel, showConfigPanel, showSeriesMenu]);
|
|
2457
|
+
useEffect(() => {
|
|
2458
|
+
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
|
2459
|
+
checkMobile();
|
|
2460
|
+
window.addEventListener('resize', checkMobile);
|
|
2461
|
+
return () => window.removeEventListener('resize', checkMobile);
|
|
2462
|
+
}, []);
|
|
2463
|
+
useEffect(() => {
|
|
2464
|
+
if (!visibleData.length)
|
|
2465
|
+
return;
|
|
2466
|
+
setIsSeriesLoading(true);
|
|
2467
|
+
const id = window.setTimeout(() => setIsSeriesLoading(false), 250);
|
|
2468
|
+
return () => window.clearTimeout(id);
|
|
2469
|
+
}, [seriesType, visibleData.length]);
|
|
2470
|
+
const locale = language === 'es' ? 'es-ES' : 'en-US';
|
|
2471
|
+
const numberFormatter = useMemo(() => new Intl.NumberFormat(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }), [locale]);
|
|
2472
|
+
const shortNumberFormatter = useMemo(() => new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }), [locale]);
|
|
2473
|
+
const timeFormatter = useMemo(() => new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit' }), [locale]);
|
|
2474
|
+
const latencyMs = useMemo(() => Math.round(20 + Math.random() * 15), []);
|
|
2475
|
+
const derivedStats = useMemo(() => {
|
|
2476
|
+
if (!storeData.length) {
|
|
2477
|
+
return null;
|
|
2478
|
+
}
|
|
2479
|
+
const highs = storeData.map((d) => d.high);
|
|
2480
|
+
const lows = storeData.map((d) => d.low);
|
|
2481
|
+
const closes = storeData.map((d) => d.close);
|
|
2482
|
+
const volumes = storeData.map((d) => d.volume ?? 0);
|
|
2483
|
+
const maxHigh = Math.max(...highs);
|
|
2484
|
+
const minLow = Math.min(...lows);
|
|
2485
|
+
const range = maxHigh - minLow;
|
|
2486
|
+
const lastClose = closes[closes.length - 1] ?? 0;
|
|
2487
|
+
const rangePct = lastClose ? (range / lastClose) * 100 : 0;
|
|
2488
|
+
const avgVolume = volumes.reduce((acc, value) => acc + value, 0) / Math.max(1, volumes.length);
|
|
2489
|
+
let volatility = 0;
|
|
2490
|
+
if (closes.length > 1) {
|
|
2491
|
+
let diffSum = 0;
|
|
2492
|
+
for (let i = 1; i < closes.length; i++) {
|
|
2493
|
+
const current = closes[i] ?? closes[i - 1] ?? 0;
|
|
2494
|
+
const prevValue = closes[i - 1] ?? current;
|
|
2495
|
+
if (prevValue !== 0) {
|
|
2496
|
+
diffSum += Math.abs(current - prevValue) / prevValue;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
volatility = (diffSum / (closes.length - 1)) * 100;
|
|
2500
|
+
}
|
|
2501
|
+
const hour = new Date().getUTCHours();
|
|
2502
|
+
const session = hour < 7 ? 'Asia' : hour < 14 ? 'Europe' : hour < 21 ? 'New York' : 'Pacific';
|
|
2503
|
+
return {
|
|
2504
|
+
range,
|
|
2505
|
+
rangePct,
|
|
2506
|
+
avgVolume,
|
|
2507
|
+
volatility,
|
|
2508
|
+
session,
|
|
2509
|
+
samples: storeData.length
|
|
2510
|
+
};
|
|
2511
|
+
}, [storeData]);
|
|
2512
|
+
const showToast = useCallback((message) => {
|
|
2513
|
+
setToastMessage(message);
|
|
2514
|
+
}, []);
|
|
2515
|
+
useEffect(() => {
|
|
2516
|
+
if (!toastMessage)
|
|
2517
|
+
return;
|
|
2518
|
+
const id = window.setTimeout(() => setToastMessage(null), 2400);
|
|
2519
|
+
return () => window.clearTimeout(id);
|
|
2520
|
+
}, [toastMessage]);
|
|
2521
|
+
useEffect(() => {
|
|
2522
|
+
const candles = getVisibleCandles();
|
|
2523
|
+
if (!candles.length)
|
|
2524
|
+
return;
|
|
2525
|
+
const bounds = getDataBounds(candles);
|
|
2526
|
+
const rawRange = bounds.maxPrice - bounds.minPrice;
|
|
2527
|
+
const padding = rawRange === 0 ? Math.max(1, bounds.maxPrice * 0.01 || 1) : rawRange * 0.05;
|
|
2528
|
+
const minPrice = Math.max(0, bounds.minPrice - padding);
|
|
2529
|
+
const maxPrice = bounds.maxPrice + padding;
|
|
2530
|
+
const priceRange = Math.max(1e-6, maxPrice - minPrice);
|
|
2531
|
+
const maxVolume = Math.max(bounds.maxVolume, 1);
|
|
2532
|
+
priceWindowRef.current = { min: minPrice, max: maxPrice };
|
|
2533
|
+
const chartWidth = Math.max(1, cssWidth - priceScaleWidth);
|
|
2534
|
+
const chartHeight = Math.max(1, cssHeight - timeScaleHeight - volumeHeight);
|
|
2535
|
+
const candleWidth = chartWidth / Math.max(candles.length, 1);
|
|
2536
|
+
// Generate price labels
|
|
2537
|
+
const priceLabelsArray = [];
|
|
2538
|
+
for (let i = 0; i <= 10; i++) {
|
|
2539
|
+
const price = minPrice + (priceRange * i) / 10;
|
|
2540
|
+
priceLabelsArray.push(numberFormatter.format(price));
|
|
2541
|
+
}
|
|
2542
|
+
setPriceLabels(priceLabelsArray);
|
|
2543
|
+
// Generate time labels
|
|
2544
|
+
const timeLabelsArray = [];
|
|
2545
|
+
const step = Math.max(1, Math.floor(candles.length / 10));
|
|
2546
|
+
for (let i = 0; i < candles.length; i += step) {
|
|
2547
|
+
const timestamp = candles[i]?.timestamp;
|
|
2548
|
+
if (timestamp !== undefined) {
|
|
2549
|
+
const date = new Date(timestamp);
|
|
2550
|
+
timeLabelsArray.push(timeFormatter.format(date));
|
|
2551
|
+
}
|
|
2552
|
+
else {
|
|
2553
|
+
timeLabelsArray.push(`Vela ${i} / Candle ${i}`);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
setTimeLabels(timeLabelsArray);
|
|
2557
|
+
// Draw grid
|
|
2558
|
+
const gridCtx = gridRef.current?.getContext('2d');
|
|
2559
|
+
if (gridCtx) {
|
|
2560
|
+
gridCtx.clearRect(0, 0, chartWidth, chartHeight);
|
|
2561
|
+
gridCtx.strokeStyle = '#363a45';
|
|
2562
|
+
gridCtx.lineWidth = 0.5;
|
|
2563
|
+
for (let i = 0; i <= 10; i++) {
|
|
2564
|
+
const y = alignStroke((i / 10) * chartHeight);
|
|
2565
|
+
gridCtx.beginPath();
|
|
2566
|
+
gridCtx.moveTo(0, y);
|
|
2567
|
+
gridCtx.lineTo(chartWidth, y);
|
|
2568
|
+
gridCtx.stroke();
|
|
2569
|
+
}
|
|
2570
|
+
const timeStep = Math.max(1, Math.floor(candles.length / 10));
|
|
2571
|
+
for (let i = 0; i <= candles.length; i += timeStep) {
|
|
2572
|
+
const x = alignStroke(i * candleWidth);
|
|
2573
|
+
gridCtx.beginPath();
|
|
2574
|
+
gridCtx.moveTo(x, 0);
|
|
2575
|
+
gridCtx.lineTo(x, chartHeight);
|
|
2576
|
+
gridCtx.stroke();
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
const bullishColor = colorScheme === 'green' ? '#089981' : '#f23645';
|
|
2580
|
+
const bearishColor = colorScheme === 'green' ? '#f23645' : '#089981';
|
|
2581
|
+
// Draw price series
|
|
2582
|
+
const chartCtx = chartRef.current?.getContext('2d');
|
|
2583
|
+
if (chartCtx) {
|
|
2584
|
+
chartCtx.clearRect(0, 0, chartWidth, chartHeight);
|
|
2585
|
+
const isLineLike = seriesType === 'line' ||
|
|
2586
|
+
seriesType === 'line-markers' ||
|
|
2587
|
+
seriesType === 'step' ||
|
|
2588
|
+
seriesType === 'area' ||
|
|
2589
|
+
seriesType === 'hlc-area' ||
|
|
2590
|
+
seriesType === 'baseline';
|
|
2591
|
+
if (isLineLike) {
|
|
2592
|
+
// ----- LINE-BASED SERIES -----
|
|
2593
|
+
const baselinePrice = seriesType === 'baseline'
|
|
2594
|
+
? (candles[0]?.close ?? minPrice)
|
|
2595
|
+
: minPrice;
|
|
2596
|
+
chartCtx.beginPath();
|
|
2597
|
+
candles.forEach((candle, index) => {
|
|
2598
|
+
const x = index * candleWidth + candleWidth / 2;
|
|
2599
|
+
const yClose = ((maxPrice - candle.close) / priceRange) * chartHeight;
|
|
2600
|
+
if (index === 0) {
|
|
2601
|
+
chartCtx.moveTo(x, yClose);
|
|
2602
|
+
}
|
|
2603
|
+
else if (seriesType === 'step') {
|
|
2604
|
+
const prev = candles[index - 1];
|
|
2605
|
+
if (prev) {
|
|
2606
|
+
const prevY = ((maxPrice - prev.close) / priceRange) * chartHeight;
|
|
2607
|
+
chartCtx.lineTo(x, prevY);
|
|
2608
|
+
chartCtx.lineTo(x, yClose);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
else {
|
|
2612
|
+
chartCtx.lineTo(x, yClose);
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
chartCtx.strokeStyle = '#2962ff';
|
|
2616
|
+
chartCtx.lineWidth = seriesType === 'baseline' ? 2 : 1;
|
|
2617
|
+
chartCtx.setLineDash([]);
|
|
2618
|
+
chartCtx.stroke();
|
|
2619
|
+
if (seriesType === 'line-markers') {
|
|
2620
|
+
candles.forEach((candle, index) => {
|
|
2621
|
+
const x = index * candleWidth + candleWidth / 2;
|
|
2622
|
+
const yClose = ((maxPrice - candle.close) / priceRange) * chartHeight;
|
|
2623
|
+
chartCtx.beginPath();
|
|
2624
|
+
chartCtx.arc(x, yClose, 2.5, 0, 2 * Math.PI);
|
|
2625
|
+
chartCtx.fillStyle = '#2962ff';
|
|
2626
|
+
chartCtx.fill();
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
if (seriesType === 'area' || seriesType === 'hlc-area' || seriesType === 'baseline') {
|
|
2630
|
+
chartCtx.lineTo((candles.length - 1) * candleWidth + candleWidth / 2, ((maxPrice - baselinePrice) / priceRange) * chartHeight);
|
|
2631
|
+
chartCtx.lineTo(0 * candleWidth + candleWidth / 2, ((maxPrice - baselinePrice) / priceRange) * chartHeight);
|
|
2632
|
+
chartCtx.closePath();
|
|
2633
|
+
chartCtx.fillStyle =
|
|
2634
|
+
seriesType === 'baseline' ? 'rgba(41,98,255,0.25)' : 'rgba(41,98,255,0.15)';
|
|
2635
|
+
chartCtx.fill();
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
else {
|
|
2639
|
+
// ----- CANDLE / BAR-BASED SERIES -----
|
|
2640
|
+
candles.forEach((candle, index) => {
|
|
2641
|
+
const xCenter = index * candleWidth + candleWidth / 2;
|
|
2642
|
+
const strokeX = alignStroke(xCenter);
|
|
2643
|
+
const yHigh = ((maxPrice - candle.high) / priceRange) * chartHeight;
|
|
2644
|
+
const yLow = ((maxPrice - candle.low) / priceRange) * chartHeight;
|
|
2645
|
+
const yOpen = ((maxPrice - candle.open) / priceRange) * chartHeight;
|
|
2646
|
+
const yClose = ((maxPrice - candle.close) / priceRange) * chartHeight;
|
|
2647
|
+
const alignedYOpen = alignStroke(yOpen);
|
|
2648
|
+
const alignedYClose = alignStroke(yClose);
|
|
2649
|
+
const alignedYHigh = alignStroke(yHigh);
|
|
2650
|
+
const alignedYLow = alignStroke(yLow);
|
|
2651
|
+
const isBullish = candle.close > candle.open;
|
|
2652
|
+
const bodyHeight = Math.abs(yClose - yOpen);
|
|
2653
|
+
const bodyY = Math.min(yOpen, yClose);
|
|
2654
|
+
const colorUp = bullishColor;
|
|
2655
|
+
const colorDown = bearishColor;
|
|
2656
|
+
const color = isBullish ? colorUp : colorDown;
|
|
2657
|
+
// Common wick for styles that use full OHLC
|
|
2658
|
+
const usesWick = seriesType === 'candles' ||
|
|
2659
|
+
seriesType === 'hollow' ||
|
|
2660
|
+
seriesType === 'bars' ||
|
|
2661
|
+
seriesType === 'columns' ||
|
|
2662
|
+
seriesType === 'high-low';
|
|
2663
|
+
if (usesWick) {
|
|
2664
|
+
chartCtx.strokeStyle = color;
|
|
2665
|
+
chartCtx.lineWidth = 1;
|
|
2666
|
+
chartCtx.beginPath();
|
|
2667
|
+
chartCtx.moveTo(strokeX, alignedYHigh);
|
|
2668
|
+
chartCtx.lineTo(strokeX, alignedYLow);
|
|
2669
|
+
chartCtx.stroke();
|
|
2670
|
+
}
|
|
2671
|
+
if (seriesType === 'candles' || seriesType === 'hollow') {
|
|
2672
|
+
chartCtx.lineWidth = 1;
|
|
2673
|
+
chartCtx.strokeStyle = color;
|
|
2674
|
+
if (seriesType === 'hollow' && isBullish) {
|
|
2675
|
+
// Hollow candle: sólo borde cuando es alcista
|
|
2676
|
+
chartCtx.fillStyle = 'transparent';
|
|
2677
|
+
}
|
|
2678
|
+
else {
|
|
2679
|
+
chartCtx.fillStyle = color;
|
|
2680
|
+
}
|
|
2681
|
+
if (bodyHeight > 1) {
|
|
2682
|
+
if (seriesType === 'hollow' && isBullish) {
|
|
2683
|
+
chartCtx.strokeRect(xCenter - candleWidth * 0.4, bodyY, candleWidth * 0.8, bodyHeight);
|
|
2684
|
+
}
|
|
2685
|
+
else {
|
|
2686
|
+
chartCtx.fillRect(xCenter - candleWidth * 0.4, bodyY, candleWidth * 0.8, bodyHeight);
|
|
2687
|
+
chartCtx.strokeRect(xCenter - candleWidth * 0.4, bodyY, candleWidth * 0.8, bodyHeight);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
else {
|
|
2691
|
+
chartCtx.beginPath();
|
|
2692
|
+
const bodyYStroke = alignStroke(yOpen);
|
|
2693
|
+
chartCtx.moveTo(xCenter - candleWidth * 0.4, bodyYStroke);
|
|
2694
|
+
chartCtx.lineTo(xCenter + candleWidth * 0.4, bodyYStroke);
|
|
2695
|
+
chartCtx.stroke();
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
else if (seriesType === 'bars') {
|
|
2699
|
+
// OHLC bar style
|
|
2700
|
+
chartCtx.strokeStyle = color;
|
|
2701
|
+
chartCtx.lineWidth = 1;
|
|
2702
|
+
// wick already drawn; add open/close ticks
|
|
2703
|
+
chartCtx.beginPath();
|
|
2704
|
+
// open tick (left)
|
|
2705
|
+
chartCtx.moveTo(xCenter - candleWidth * 0.4, alignedYOpen);
|
|
2706
|
+
chartCtx.lineTo(xCenter, alignedYOpen);
|
|
2707
|
+
// close tick (right)
|
|
2708
|
+
chartCtx.moveTo(xCenter, alignedYClose);
|
|
2709
|
+
chartCtx.lineTo(xCenter + candleWidth * 0.4, alignedYClose);
|
|
2710
|
+
chartCtx.stroke();
|
|
2711
|
+
}
|
|
2712
|
+
else if (seriesType === 'columns') {
|
|
2713
|
+
// Column style (vertical bar from low to high)
|
|
2714
|
+
chartCtx.fillStyle = color;
|
|
2715
|
+
chartCtx.fillRect(xCenter - candleWidth * 0.3, yLow, candleWidth * 0.6, yHigh - yLow);
|
|
2716
|
+
}
|
|
2717
|
+
else ;
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
// Draw volume
|
|
2722
|
+
const volumeCtx = volumeRef.current?.getContext('2d');
|
|
2723
|
+
if (volumeCtx) {
|
|
2724
|
+
volumeCtx.clearRect(0, 0, chartWidth, volumeHeight);
|
|
2725
|
+
candles.forEach((candle, index) => {
|
|
2726
|
+
const x = index * candleWidth;
|
|
2727
|
+
const barHeight = ((candle.volume || 0) / maxVolume) * volumeHeight;
|
|
2728
|
+
const y = volumeHeight - barHeight;
|
|
2729
|
+
const isBullish = candle.close > candle.open;
|
|
2730
|
+
volumeCtx.fillStyle = isBullish ? bullishColor : bearishColor;
|
|
2731
|
+
volumeCtx.fillRect(x + 1, y, candleWidth - 2, barHeight);
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
// Draw indicators
|
|
2735
|
+
const indicatorCtx = chartRef.current?.getContext('2d');
|
|
2736
|
+
if (indicatorCtx) {
|
|
2737
|
+
calculatedIndicators.forEach(indicator => {
|
|
2738
|
+
if (indicator.type === 'line') {
|
|
2739
|
+
indicatorCtx.strokeStyle = indicator.color;
|
|
2740
|
+
indicatorCtx.lineWidth = indicator.width || 1;
|
|
2741
|
+
indicatorCtx.setLineDash(indicator.style === 'dashed' ? [5, 5] : indicator.style === 'dotted' ? [2, 2] : []);
|
|
2742
|
+
indicatorCtx.beginPath();
|
|
2743
|
+
indicator.data.forEach((value, index) => {
|
|
2744
|
+
if (value !== null && value !== undefined) {
|
|
2745
|
+
const x = index * candleWidth + candleWidth / 2;
|
|
2746
|
+
const y = ((maxPrice - value) / priceRange) * chartHeight;
|
|
2747
|
+
if (index === 0) {
|
|
2748
|
+
indicatorCtx.moveTo(x, y);
|
|
2749
|
+
}
|
|
2750
|
+
else {
|
|
2751
|
+
indicatorCtx.lineTo(x, y);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
});
|
|
2755
|
+
indicatorCtx.stroke();
|
|
2756
|
+
}
|
|
2757
|
+
else if (indicator.type === 'histogram') {
|
|
2758
|
+
indicator.data.forEach((value, index) => {
|
|
2759
|
+
if (value !== null && value !== undefined) {
|
|
2760
|
+
const x = index * candleWidth;
|
|
2761
|
+
const barHeight = Math.abs(value) * chartHeight * 0.1; // Scale histogram
|
|
2762
|
+
const y = value >= 0 ? chartHeight / 2 - barHeight : chartHeight / 2;
|
|
2763
|
+
indicatorCtx.fillStyle = value >= 0 ? '#089981' : '#f23645';
|
|
2764
|
+
indicatorCtx.fillRect(x + 2, y, candleWidth - 4, barHeight);
|
|
2765
|
+
}
|
|
2766
|
+
});
|
|
2767
|
+
}
|
|
2768
|
+
});
|
|
2769
|
+
indicatorCtx.setLineDash([]); // Reset line dash
|
|
2770
|
+
}
|
|
2771
|
+
}, [visibleData, cssWidth, cssHeight, colorScheme, calculatedIndicators, seriesType, getVisibleCandles]);
|
|
2772
|
+
const drawCrosshair = useCallback(() => {
|
|
2773
|
+
const overlayCtx = overlayRef.current?.getContext('2d');
|
|
2774
|
+
if (overlayCtx && showCrosshairRef.current) {
|
|
2775
|
+
const { x, y } = mousePosRef.current;
|
|
2776
|
+
const alignedX = alignStroke(x);
|
|
2777
|
+
const alignedY = alignStroke(y);
|
|
2778
|
+
overlayCtx.clearRect(0, 0, chartWidth, overlayHeight);
|
|
2779
|
+
overlayCtx.strokeStyle = '#9598a1';
|
|
2780
|
+
overlayCtx.lineWidth = 1;
|
|
2781
|
+
overlayCtx.setLineDash([2, 2]);
|
|
2782
|
+
overlayCtx.beginPath();
|
|
2783
|
+
overlayCtx.moveTo(alignedX, 0);
|
|
2784
|
+
overlayCtx.lineTo(alignedX, chartHeight);
|
|
2785
|
+
overlayCtx.stroke();
|
|
2786
|
+
overlayCtx.beginPath();
|
|
2787
|
+
overlayCtx.moveTo(0, alignedY);
|
|
2788
|
+
overlayCtx.lineTo(chartWidth, alignedY);
|
|
2789
|
+
overlayCtx.stroke();
|
|
2790
|
+
overlayCtx.setLineDash([]);
|
|
2791
|
+
}
|
|
2792
|
+
}, [chartWidth, chartHeight, overlayHeight]);
|
|
2793
|
+
const handlePointerMove = useCallback((e) => {
|
|
2794
|
+
if (interactionsLocked || !visibleData.length)
|
|
2795
|
+
return;
|
|
2796
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
2797
|
+
const x = e.clientX - rect.left;
|
|
2798
|
+
const y = e.clientY - rect.top;
|
|
2799
|
+
setClickedPrice(null);
|
|
2800
|
+
if (cursorType !== 'cross') {
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
if (!isDraggingRef.current) {
|
|
2804
|
+
const { min, max } = priceWindowRef.current;
|
|
2805
|
+
const snappedX = coordsRef.current.snapToCandle(x, chartWidth, visibleData.length);
|
|
2806
|
+
const snappedY = coordsRef.current.snapToPrice(y, chartHeight, min, max);
|
|
2807
|
+
mousePosRef.current = {
|
|
2808
|
+
x: coordsRef.current.timeToPixel(snappedX, chartWidth, visibleData.length),
|
|
2809
|
+
y: coordsRef.current.priceToPixel(snappedY, chartHeight, min, max)
|
|
2810
|
+
};
|
|
2811
|
+
showCrosshairRef.current = true;
|
|
2812
|
+
requestAnimationFrame(drawCrosshair);
|
|
2813
|
+
}
|
|
2814
|
+
}, [visibleData, cursorType, chartWidth, chartHeight, drawCrosshair, interactionsLocked]);
|
|
2815
|
+
const handleMouseLeave = useCallback(() => {
|
|
2816
|
+
showCrosshairRef.current = false;
|
|
2817
|
+
isDraggingRef.current = false;
|
|
2818
|
+
dragSampleRef.current.velocity = 0;
|
|
2819
|
+
dragSampleRef.current.lastTs = 0;
|
|
2820
|
+
requestAnimationFrame(() => {
|
|
2821
|
+
const overlayCtx = overlayRef.current?.getContext('2d');
|
|
2822
|
+
if (overlayCtx) {
|
|
2823
|
+
overlayCtx.clearRect(0, 0, chartWidth, overlayHeight);
|
|
2824
|
+
}
|
|
2825
|
+
});
|
|
2826
|
+
}, [chartWidth, overlayHeight]);
|
|
2827
|
+
const handleWheel = useCallback((e) => {
|
|
2828
|
+
if (interactionsLocked)
|
|
2829
|
+
return;
|
|
2830
|
+
e.preventDefault();
|
|
2831
|
+
applyPanDelta(e.deltaX);
|
|
2832
|
+
}, [interactionsLocked, applyPanDelta]);
|
|
2833
|
+
const handleQuickZoom = useCallback((direction) => {
|
|
2834
|
+
if (interactionsLocked)
|
|
2835
|
+
return;
|
|
2836
|
+
const factor = direction === 'in' ? 1.2 : 0.8;
|
|
2837
|
+
const targetZoom = clampZoomLevel(zoomLevelRef.current * factor);
|
|
2838
|
+
startZoomAnimation(targetZoom, 0.5);
|
|
2839
|
+
}, [interactionsLocked, startZoomAnimation, clampZoomLevel]);
|
|
2840
|
+
const handleResetView = useCallback(() => {
|
|
2841
|
+
if (interactionsLocked)
|
|
2842
|
+
return;
|
|
2843
|
+
cancelInertia();
|
|
2844
|
+
cancelZoomAnimation();
|
|
2845
|
+
setPanOffsetSafe(0);
|
|
2846
|
+
startZoomAnimation(1, 0.5);
|
|
2847
|
+
}, [interactionsLocked, cancelInertia, cancelZoomAnimation, setPanOffsetSafe, startZoomAnimation]);
|
|
2848
|
+
const getPointerPoint = useCallback((event) => {
|
|
2849
|
+
if (interactionsLocked)
|
|
2850
|
+
return null;
|
|
2851
|
+
const targetData = visibleData.length ? visibleData : storeData;
|
|
2852
|
+
if (!event.currentTarget || !targetData.length) {
|
|
2853
|
+
return null;
|
|
2854
|
+
}
|
|
2855
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
2856
|
+
let x = event.clientX - rect.left;
|
|
2857
|
+
let y = event.clientY - rect.top;
|
|
2858
|
+
const usingVisible = visibleData.length > 0;
|
|
2859
|
+
const bounds = usingVisible
|
|
2860
|
+
? { minPrice: priceWindowRef.current.min, maxPrice: priceWindowRef.current.max }
|
|
2861
|
+
: getDataBounds(targetData);
|
|
2862
|
+
const dataLength = Math.max(targetData.length, 1);
|
|
2863
|
+
if (magnetEnabled && visibleData.length) {
|
|
2864
|
+
const snappedIndex = coordsRef.current.snapToCandle(x, chartWidth, dataLength);
|
|
2865
|
+
const snappedPrice = coordsRef.current.snapToPrice(y, chartHeight, bounds.minPrice, bounds.maxPrice, 0.5);
|
|
2866
|
+
x = coordsRef.current.timeToPixel(snappedIndex, chartWidth, dataLength);
|
|
2867
|
+
y = coordsRef.current.priceToPixel(snappedPrice, chartHeight, bounds.minPrice, bounds.maxPrice);
|
|
2868
|
+
}
|
|
2869
|
+
const price = coordsRef.current.pixelToPrice(y, chartHeight, bounds.minPrice, bounds.maxPrice);
|
|
2870
|
+
const time = coordsRef.current.pixelToTime(x, chartWidth, dataLength);
|
|
2871
|
+
return { x, y, price, time };
|
|
2872
|
+
}, [visibleData, storeData, magnetEnabled, chartWidth, chartHeight, interactionsLocked]);
|
|
2873
|
+
const tryEraseDrawing = useCallback((x, y) => {
|
|
2874
|
+
for (let i = drawings.length - 1; i >= 0; i--) {
|
|
2875
|
+
const drawing = drawings[i];
|
|
2876
|
+
if (!drawing)
|
|
2877
|
+
continue;
|
|
2878
|
+
if (drawing.points.some(point => Math.hypot(point.x - x, point.y - y) < 14)) {
|
|
2879
|
+
deleteDrawing(drawing.id);
|
|
2880
|
+
showToast('Dibujo eliminado');
|
|
2881
|
+
return true;
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
return false;
|
|
2885
|
+
}, [drawings, deleteDrawing, showToast]);
|
|
2886
|
+
const finalizeDrawing = useCallback((draft) => {
|
|
2887
|
+
const normalizedPoints = draft.points.length ? [...draft.points] : [];
|
|
2888
|
+
if (normalizedPoints.length === 1 &&
|
|
2889
|
+
draft.type !== 'text' &&
|
|
2890
|
+
draft.type !== 'icon' &&
|
|
2891
|
+
draft.type !== 'freehand') {
|
|
2892
|
+
const firstPoint = normalizedPoints[0];
|
|
2893
|
+
if (firstPoint) {
|
|
2894
|
+
normalizedPoints.push({ ...firstPoint });
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
let normalized = {
|
|
2898
|
+
...draft,
|
|
2899
|
+
points: normalizedPoints
|
|
2900
|
+
};
|
|
2901
|
+
const start = normalized.points[0];
|
|
2902
|
+
const end = normalized.points[1] ?? normalized.points[0];
|
|
2903
|
+
if (!start) {
|
|
2904
|
+
return normalized;
|
|
2905
|
+
}
|
|
2906
|
+
switch (draft.type) {
|
|
2907
|
+
case 'horizontal':
|
|
2908
|
+
case 'horizontal-line':
|
|
2909
|
+
case 'horizontal-ray': {
|
|
2910
|
+
if (end) {
|
|
2911
|
+
normalized.points = [start, { ...end, y: start.y, price: start.price }];
|
|
2912
|
+
}
|
|
2913
|
+
break;
|
|
2914
|
+
}
|
|
2915
|
+
case 'vertical-line': {
|
|
2916
|
+
if (end) {
|
|
2917
|
+
normalized.points = [start, { ...end, x: start.x, time: start.time }];
|
|
2918
|
+
}
|
|
2919
|
+
break;
|
|
2920
|
+
}
|
|
2921
|
+
case 'rectangle': {
|
|
2922
|
+
normalized.backgroundColor = draft.backgroundColor ?? 'rgba(96,165,250,0.15)';
|
|
2923
|
+
break;
|
|
2924
|
+
}
|
|
2925
|
+
case 'triangle': {
|
|
2926
|
+
if (end) {
|
|
2927
|
+
normalized.points = createTrianglePoints(start, end);
|
|
2928
|
+
}
|
|
2929
|
+
normalized.backgroundColor = draft.backgroundColor ?? 'rgba(244,114,182,0.12)';
|
|
2930
|
+
break;
|
|
2931
|
+
}
|
|
2932
|
+
case 'channel':
|
|
2933
|
+
case 'parallel':
|
|
2934
|
+
case 'regression-trend':
|
|
2935
|
+
case 'flat-top-bottom':
|
|
2936
|
+
case 'disjoint-channel':
|
|
2937
|
+
case 'pitchfork':
|
|
2938
|
+
case 'schiff-pitchfork':
|
|
2939
|
+
case 'modified-schiff-pitchfork':
|
|
2940
|
+
case 'inside-pitchfork': {
|
|
2941
|
+
if (end) {
|
|
2942
|
+
const offset = getChannelOffset(draft.type);
|
|
2943
|
+
normalized.points = createParallelPoints(start, end, offset);
|
|
2944
|
+
}
|
|
2945
|
+
if (['channel', 'regression-trend', 'flat-top-bottom', 'disjoint-channel'].includes(draft.type)) {
|
|
2946
|
+
normalized.backgroundColor = draft.backgroundColor ?? 'rgba(52,211,153,0.08)';
|
|
2947
|
+
}
|
|
2948
|
+
if (['pitchfork', 'schiff-pitchfork', 'modified-schiff-pitchfork', 'inside-pitchfork'].includes(draft.type)) {
|
|
2949
|
+
normalized.backgroundColor = draft.backgroundColor ?? 'rgba(244,114,182,0.08)';
|
|
2950
|
+
}
|
|
2951
|
+
break;
|
|
2952
|
+
}
|
|
2953
|
+
case 'cross-line': {
|
|
2954
|
+
if (end) {
|
|
2955
|
+
const horizontal = Math.max(20, Math.abs(end.x - start.x));
|
|
2956
|
+
const vertical = Math.max(20, Math.abs(end.y - start.y));
|
|
2957
|
+
normalized.meta = {
|
|
2958
|
+
...normalized.meta,
|
|
2959
|
+
crossSize: { horizontal, vertical }
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
break;
|
|
2963
|
+
}
|
|
2964
|
+
case 'ruler': {
|
|
2965
|
+
if (end) {
|
|
2966
|
+
normalized.meta = {
|
|
2967
|
+
...normalized.meta,
|
|
2968
|
+
measurement: buildRulerMeta(start, end)
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
break;
|
|
2972
|
+
}
|
|
2973
|
+
case 'icon': {
|
|
2974
|
+
normalized.icon = draft.icon ?? selectedEmoji ?? '⭐';
|
|
2975
|
+
normalized.fontSize = draft.fontSize ?? 22;
|
|
2976
|
+
normalized.color = draft.color ?? '#fcd34d';
|
|
2977
|
+
break;
|
|
2978
|
+
}
|
|
2979
|
+
case 'text': {
|
|
2980
|
+
normalized.fontSize = draft.fontSize ?? 14;
|
|
2981
|
+
normalized.backgroundColor = draft.backgroundColor ?? 'rgba(15,23,42,0.65)';
|
|
2982
|
+
normalized.color = draft.color ?? '#e5e7eb';
|
|
2983
|
+
break;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
return normalized;
|
|
2987
|
+
}, [selectedEmoji]);
|
|
2988
|
+
const handleSaveNote = useCallback(() => {
|
|
2989
|
+
if (!pendingNotePoint || !noteDraft.trim()) {
|
|
2990
|
+
setShowNoteModal(false);
|
|
2991
|
+
setPendingNotePoint(null);
|
|
2992
|
+
setNoteDraft('');
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
const textDrawing = {
|
|
2996
|
+
id: `drawing-${Date.now()}`,
|
|
2997
|
+
type: 'text',
|
|
2998
|
+
points: [pendingNotePoint],
|
|
2999
|
+
color: '#e5e7eb',
|
|
3000
|
+
width: 1.2,
|
|
3001
|
+
style: 'solid',
|
|
3002
|
+
locked: false,
|
|
3003
|
+
visible: true,
|
|
3004
|
+
text: noteDraft.trim(),
|
|
3005
|
+
backgroundColor: 'rgba(15,23,42,0.65)'
|
|
3006
|
+
};
|
|
3007
|
+
addDrawing(finalizeDrawing(textDrawing));
|
|
3008
|
+
setShowNoteModal(false);
|
|
3009
|
+
setPendingNotePoint(null);
|
|
3010
|
+
setNoteDraft('');
|
|
3011
|
+
showToast('Note added');
|
|
3012
|
+
}, [pendingNotePoint, noteDraft, addDrawing, finalizeDrawing, showToast]);
|
|
3013
|
+
const handleCancelNote = useCallback(() => {
|
|
3014
|
+
setShowNoteModal(false);
|
|
3015
|
+
setPendingNotePoint(null);
|
|
3016
|
+
setNoteDraft('');
|
|
3017
|
+
}, []);
|
|
3018
|
+
const handlePointerDown = useCallback((e) => {
|
|
3019
|
+
if (interactionsLocked)
|
|
3020
|
+
return;
|
|
3021
|
+
cancelInertia();
|
|
3022
|
+
cancelZoomAnimation();
|
|
3023
|
+
dragSampleRef.current.velocity = 0;
|
|
3024
|
+
dragSampleRef.current.lastTs = performance.now();
|
|
3025
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
3026
|
+
const rawX = e.clientX - rect.left;
|
|
3027
|
+
const rawY = e.clientY - rect.top;
|
|
3028
|
+
if (activeTool !== 'cursor') {
|
|
3029
|
+
const point = getPointerPoint(e);
|
|
3030
|
+
if (!point)
|
|
3031
|
+
return;
|
|
3032
|
+
if (activeTool === 'text') {
|
|
3033
|
+
if (!point)
|
|
3034
|
+
return;
|
|
3035
|
+
setPendingNotePoint(point);
|
|
3036
|
+
setNoteDraft('');
|
|
3037
|
+
setShowNoteModal(true);
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
if (activeTool === 'icon') {
|
|
3041
|
+
const emoji = selectedEmoji ?? '⭐';
|
|
3042
|
+
if (!selectedEmoji) {
|
|
3043
|
+
setSelectedEmoji(emoji);
|
|
3044
|
+
}
|
|
3045
|
+
const iconDrawing = {
|
|
3046
|
+
id: `drawing-${Date.now()}`,
|
|
3047
|
+
type: 'icon',
|
|
3048
|
+
points: [point],
|
|
3049
|
+
color: '#fcd34d',
|
|
3050
|
+
width: 1,
|
|
3051
|
+
style: 'solid',
|
|
3052
|
+
locked: false,
|
|
3053
|
+
visible: true,
|
|
3054
|
+
icon: emoji
|
|
3055
|
+
};
|
|
3056
|
+
addDrawing(finalizeDrawing(iconDrawing));
|
|
3057
|
+
showToast('Icono añadido');
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
const isLineTool = LINE_TOOL_SET.has(activeTool);
|
|
3061
|
+
const baseColor = isLineTool ? strokeColor : (TOOL_COLOR_MAP[activeTool] ?? strokeColor);
|
|
3062
|
+
const baseWidth = isLineTool ? strokeWidth : (activeTool === 'freehand' ? 2.2 : 1.5);
|
|
3063
|
+
setIsDrawing(true);
|
|
3064
|
+
setCurrentDrawing({
|
|
3065
|
+
id: `drawing-${Date.now()}`,
|
|
3066
|
+
type: activeTool,
|
|
3067
|
+
points: [point],
|
|
3068
|
+
color: baseColor,
|
|
3069
|
+
width: baseWidth,
|
|
3070
|
+
style: 'solid',
|
|
3071
|
+
locked: false,
|
|
3072
|
+
visible: true,
|
|
3073
|
+
backgroundColor: activeTool === 'rectangle' ? 'rgba(96,165,250,0.15)' : undefined
|
|
3074
|
+
});
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
if (cursorType === 'eraser' && tryEraseDrawing(rawX, rawY)) {
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
if (visibleData.length) {
|
|
3081
|
+
const { min, max } = priceWindowRef.current;
|
|
3082
|
+
const snappedIndex = coordsRef.current.snapToCandle(rawX, chartWidth, visibleData.length);
|
|
3083
|
+
setSelectedCandleIndex(snappedIndex);
|
|
3084
|
+
const price = coordsRef.current.pixelToPrice(rawY, chartHeight, min, max);
|
|
3085
|
+
setClickedPrice({ x: rawX, y: rawY, price });
|
|
3086
|
+
}
|
|
3087
|
+
isDraggingRef.current = true;
|
|
3088
|
+
dragSampleRef.current.lastTs = performance.now();
|
|
3089
|
+
lastMouseXRef.current = e.clientX;
|
|
3090
|
+
}, [activeTool, cursorType, getPointerPoint, visibleData, chartWidth, chartHeight, addDrawing, finalizeDrawing, showToast, selectedEmoji, setSelectedEmoji, setIsDrawing, setCurrentDrawing, tryEraseDrawing, interactionsLocked, strokeColor, strokeWidth, cancelInertia, cancelZoomAnimation]);
|
|
3091
|
+
const handlePointerUp = useCallback(() => {
|
|
3092
|
+
if (isDrawing && currentDrawing) {
|
|
3093
|
+
const normalized = finalizeDrawing(currentDrawing);
|
|
3094
|
+
addDrawing(normalized);
|
|
3095
|
+
setIsDrawing(false);
|
|
3096
|
+
setCurrentDrawing(null);
|
|
3097
|
+
}
|
|
3098
|
+
const wasDragging = isDraggingRef.current;
|
|
3099
|
+
isDraggingRef.current = false;
|
|
3100
|
+
if (!isDrawing && !currentDrawing && wasDragging) {
|
|
3101
|
+
startInertia(dragSampleRef.current.velocity);
|
|
3102
|
+
}
|
|
3103
|
+
}, [isDrawing, currentDrawing, finalizeDrawing, addDrawing, setIsDrawing, setCurrentDrawing, startInertia]);
|
|
3104
|
+
const takeScreenshot = useCallback(() => {
|
|
3105
|
+
const priceCanvas = chartRef.current;
|
|
3106
|
+
if (!priceCanvas) {
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
const gridCanvas = gridRef.current;
|
|
3110
|
+
const volumeCanvas = volumeRef.current;
|
|
3111
|
+
const drawingsCanvas = drawingsRef.current;
|
|
3112
|
+
const overlayCanvas = overlayRef.current;
|
|
3113
|
+
const priceHeight = priceCanvas.height || 0;
|
|
3114
|
+
const volumeHeightPx = volumeCanvas?.height ?? 0;
|
|
3115
|
+
const overlayHeightPx = overlayCanvas?.height ?? priceHeight + volumeHeightPx;
|
|
3116
|
+
const exportWidth = priceCanvas.width || gridCanvas?.width || 0;
|
|
3117
|
+
const exportHeight = Math.max(priceHeight + volumeHeightPx, overlayHeightPx);
|
|
3118
|
+
if (!exportWidth || !exportHeight) {
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
const exportCanvas = document.createElement('canvas');
|
|
3122
|
+
exportCanvas.width = exportWidth;
|
|
3123
|
+
exportCanvas.height = exportHeight;
|
|
3124
|
+
const ctx = exportCanvas.getContext('2d');
|
|
3125
|
+
if (!ctx) {
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
ctx.fillStyle = '#020617';
|
|
3129
|
+
ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
|
|
3130
|
+
if (gridCanvas) {
|
|
3131
|
+
ctx.drawImage(gridCanvas, 0, 0, gridCanvas.width, gridCanvas.height);
|
|
3132
|
+
}
|
|
3133
|
+
ctx.drawImage(priceCanvas, 0, 0, priceCanvas.width, priceCanvas.height);
|
|
3134
|
+
if (volumeCanvas) {
|
|
3135
|
+
ctx.drawImage(volumeCanvas, 0, priceHeight, volumeCanvas.width, volumeCanvas.height);
|
|
3136
|
+
}
|
|
3137
|
+
if (drawingsCanvas) {
|
|
3138
|
+
ctx.drawImage(drawingsCanvas, 0, 0, drawingsCanvas.width, drawingsCanvas.height);
|
|
3139
|
+
}
|
|
3140
|
+
if (overlayCanvas) {
|
|
3141
|
+
ctx.drawImage(overlayCanvas, 0, 0, overlayCanvas.width, overlayCanvas.height);
|
|
3142
|
+
}
|
|
3143
|
+
const link = document.createElement('a');
|
|
3144
|
+
const normalizedSymbol = symbol.replace(/[^a-z0-9]+/gi, '-');
|
|
3145
|
+
link.download = `${normalizedSymbol}-${timeframe}-viainti.png`;
|
|
3146
|
+
link.href = exportCanvas.toDataURL('image/png');
|
|
3147
|
+
link.click();
|
|
3148
|
+
showToast(strings.toasts.screenshot);
|
|
3149
|
+
}, [showToast, strings.toasts.screenshot, symbol, timeframe]);
|
|
3150
|
+
const handlePointerMoveDrag = useCallback((e) => {
|
|
3151
|
+
if (interactionsLocked)
|
|
3152
|
+
return;
|
|
3153
|
+
if (isDrawing && currentDrawing) {
|
|
3154
|
+
const point = getPointerPoint(e);
|
|
3155
|
+
if (!point)
|
|
3156
|
+
return;
|
|
3157
|
+
if (!currentDrawing.points)
|
|
3158
|
+
return;
|
|
3159
|
+
const updatedPoints = [...currentDrawing.points];
|
|
3160
|
+
const isFreehand = currentDrawing.type === 'freehand';
|
|
3161
|
+
if (isFreehand) {
|
|
3162
|
+
updatedPoints.push(point);
|
|
3163
|
+
}
|
|
3164
|
+
else {
|
|
3165
|
+
updatedPoints[1] = point;
|
|
3166
|
+
}
|
|
3167
|
+
setCurrentDrawing({ ...currentDrawing, points: updatedPoints });
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
if (isDraggingRef.current) {
|
|
3171
|
+
const deltaX = e.clientX - lastMouseXRef.current;
|
|
3172
|
+
const now = performance.now();
|
|
3173
|
+
const lastTs = dragSampleRef.current.lastTs || now;
|
|
3174
|
+
const deltaTime = Math.max(1, now - lastTs);
|
|
3175
|
+
dragSampleRef.current.velocity = deltaX / deltaTime;
|
|
3176
|
+
dragSampleRef.current.lastTs = now;
|
|
3177
|
+
applyPanDelta(deltaX);
|
|
3178
|
+
lastMouseXRef.current = e.clientX;
|
|
3179
|
+
}
|
|
3180
|
+
}, [isDrawing, currentDrawing, getPointerPoint, setCurrentDrawing, interactionsLocked, applyPanDelta]);
|
|
3181
|
+
const referenceCandle = selectedCandleIndex !== null && visibleData[selectedCandleIndex]
|
|
3182
|
+
? visibleData[selectedCandleIndex]
|
|
3183
|
+
: visibleData[visibleData.length - 1];
|
|
3184
|
+
const referenceTimestamp = referenceCandle?.timestamp ?? null;
|
|
3185
|
+
const currentTimeLabel = useMemo(() => {
|
|
3186
|
+
const source = referenceTimestamp ?? Date.now();
|
|
3187
|
+
return timeFormatter.format(new Date(source));
|
|
3188
|
+
}, [referenceTimestamp, timeFormatter]);
|
|
3189
|
+
const leftRailActions = useMemo(() => ([
|
|
3190
|
+
{ key: 'zoomIn', icon: React.createElement(BsZoomIn, null), onClick: () => handleQuickZoom('in'), active: false, label: strings.actions.zoomIn },
|
|
3191
|
+
{ key: 'zoomOut', icon: React.createElement(BsZoomOut, null), onClick: () => handleQuickZoom('out'), active: false, label: strings.actions.zoomOut },
|
|
3192
|
+
{ key: 'lock', icon: React.createElement(BsLock, null), onClick: toggleInteractionsLock, active: interactionsLocked, label: interactionsLocked ? strings.actions.unlock : strings.actions.lock },
|
|
3193
|
+
{ key: 'hide', icon: React.createElement(BsEyeSlash, null), onClick: toggleDrawingsHidden, active: drawingsHidden, label: drawingsHidden ? strings.actions.show : strings.actions.hide },
|
|
3194
|
+
{ key: 'reset', icon: React.createElement(BsArrowRepeat, null), onClick: handleResetView, active: false, label: strings.actions.reset },
|
|
3195
|
+
{ key: 'magnet', icon: React.createElement(BsMagnet, null), onClick: toggleMagnet, active: magnetEnabled, label: magnetEnabled ? strings.actions.magnetOn : strings.actions.magnetOff },
|
|
3196
|
+
{ key: 'snapshot', icon: React.createElement(BsCamera, null), onClick: takeScreenshot, active: false, label: strings.actions.capture },
|
|
3197
|
+
{ key: 'indicators', icon: React.createElement(BsBarChartSteps, null), onClick: toggleIndicatorsPanel, active: showIndicatorsPanel, label: strings.actions.indicators }
|
|
3198
|
+
]), [handleQuickZoom, handleResetView, toggleInteractionsLock, toggleDrawingsHidden, toggleMagnet, toggleIndicatorsPanel, interactionsLocked, drawingsHidden, magnetEnabled, showIndicatorsPanel, takeScreenshot, strings.actions]);
|
|
3199
|
+
const seriesIconMap = {
|
|
3200
|
+
candles: React.createElement(BsBarChart, null),
|
|
3201
|
+
bars: React.createElement(BsBarChartSteps, null),
|
|
3202
|
+
hollow: React.createElement(BsBoundingBoxCircles, null),
|
|
3203
|
+
line: React.createElement(BsGraphUp, null),
|
|
3204
|
+
'line-markers': React.createElement(BsGraphUp, null),
|
|
3205
|
+
step: React.createElement(BsBarChartSteps, null),
|
|
3206
|
+
area: React.createElement(BsCollection, null),
|
|
3207
|
+
'hlc-area': React.createElement(BsCollection, null),
|
|
3208
|
+
baseline: React.createElement(BsGraphDown, null),
|
|
3209
|
+
columns: React.createElement(BsBarChartSteps, null),
|
|
3210
|
+
'high-low': React.createElement(BsGraphDown, null)
|
|
3211
|
+
};
|
|
3212
|
+
const seriesTypeOptions = [
|
|
3213
|
+
{ value: 'candles', label: 'Velas', icon: seriesIconMap.candles },
|
|
3214
|
+
{ value: 'bars', label: 'Barras', icon: seriesIconMap.bars },
|
|
3215
|
+
{ value: 'hollow', label: 'Hollow', icon: seriesIconMap.hollow },
|
|
3216
|
+
{ value: 'line', label: 'Línea', icon: seriesIconMap.line },
|
|
3217
|
+
{ value: 'line-markers', label: 'Línea + puntos', icon: seriesIconMap['line-markers'] },
|
|
3218
|
+
{ value: 'step', label: 'Step', icon: seriesIconMap.step },
|
|
3219
|
+
{ value: 'area', label: 'Área', icon: seriesIconMap.area },
|
|
3220
|
+
{ value: 'hlc-area', label: 'HLC Área', icon: seriesIconMap['hlc-area'] },
|
|
3221
|
+
{ value: 'baseline', label: 'Baseline', icon: seriesIconMap.baseline },
|
|
3222
|
+
{ value: 'columns', label: 'Columnas', icon: seriesIconMap.columns },
|
|
3223
|
+
{ value: 'high-low', label: 'High/Low', icon: seriesIconMap['high-low'] }
|
|
3224
|
+
];
|
|
3225
|
+
const cursorCss = isDraggingRef.current
|
|
3226
|
+
? 'grabbing'
|
|
3227
|
+
: cursorType === 'cross'
|
|
3228
|
+
? 'crosshair'
|
|
3229
|
+
: cursorType === 'dot'
|
|
3230
|
+
? 'crosshair'
|
|
3231
|
+
: cursorType === 'arrow'
|
|
3232
|
+
? 'default'
|
|
3233
|
+
: cursorType === 'eraser'
|
|
3234
|
+
? 'crosshair'
|
|
3235
|
+
: 'default';
|
|
3236
|
+
return (React.createElement("div", { ref: containerRef, style: {
|
|
3237
|
+
width: isFullscreen ? '100vw' : '100%',
|
|
3238
|
+
height: isFullscreen ? '100vh' : '100%',
|
|
3239
|
+
minHeight: isFullscreen ? '100vh' : '640px',
|
|
3240
|
+
background: activeTheme.pageBg,
|
|
3241
|
+
color: activeTheme.textPrimary,
|
|
3242
|
+
fontFamily: 'Inter, sans-serif',
|
|
3243
|
+
display: 'flex',
|
|
3244
|
+
flexDirection: 'column',
|
|
3245
|
+
position: isFullscreen ? 'fixed' : 'relative',
|
|
3246
|
+
inset: isFullscreen ? 0 : undefined,
|
|
3247
|
+
zIndex: isFullscreen ? 9999 : 'auto'
|
|
3248
|
+
} },
|
|
3249
|
+
React.createElement(motion.div, { style: {
|
|
3250
|
+
background: `linear-gradient(120deg, ${activeTheme.heroFrom} 0%, ${activeTheme.heroTo} 100%)`,
|
|
3251
|
+
borderBottom: `1px solid ${activeTheme.panelBorder}`,
|
|
3252
|
+
display: 'flex',
|
|
3253
|
+
flexWrap: 'wrap',
|
|
3254
|
+
gap: isMobile ? '12px' : '16px',
|
|
3255
|
+
alignItems: 'center',
|
|
3256
|
+
padding: isMobile ? '12px 16px' : '16px 20px',
|
|
3257
|
+
boxShadow: elevatedShadow
|
|
3258
|
+
}, initial: { opacity: 0, y: -15 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.3 } },
|
|
3259
|
+
React.createElement("div", { style: { display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center', minWidth: 0 } },
|
|
3260
|
+
React.createElement("div", { style: { background: activeTheme.cardBg, border: `1px solid ${activeTheme.cardBorder}`, borderRadius: '14px', padding: '10px 16px', minWidth: '160px', color: activeTheme.textPrimary } },
|
|
3261
|
+
React.createElement("div", { style: { fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.08em', color: activeTheme.textSecondary, marginBottom: '4px' } }, strings.symbolLabel),
|
|
3262
|
+
React.createElement("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', color: activeTheme.textPrimary, fontWeight: 600, gap: '10px' } },
|
|
3263
|
+
React.createElement("span", { style: { fontSize: '15px' } }, symbol),
|
|
3264
|
+
React.createElement("span", { style: { fontSize: '12px', background: activeTheme.accent, color: '#fff', padding: '2px 10px', borderRadius: '999px' } }, timeframe))),
|
|
3265
|
+
React.createElement(motion.div, { key: currentPrice, style: { background: activeTheme.cardBg, border: `1px solid ${priceChange >= 0 ? '#16a34a' : '#f43f5e'}`, borderRadius: '14px', padding: '10px 20px', display: 'flex', alignItems: 'center', gap: '16px', minWidth: '210px', color: activeTheme.textPrimary }, initial: { scale: 1.05, opacity: 0 }, animate: { scale: 1, opacity: 1 }, transition: { duration: 0.2 } },
|
|
3266
|
+
React.createElement("div", { style: { fontSize: '20px', fontWeight: 700, color: activeTheme.textPrimary } }, numberFormatter.format(currentPrice)),
|
|
3267
|
+
React.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
|
|
3268
|
+
React.createElement("span", { style: { fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: activeTheme.textSecondary } }, strings.priceChangeLabel),
|
|
3269
|
+
React.createElement(motion.span, { key: priceChange, style: {
|
|
3270
|
+
fontSize: '12px',
|
|
3271
|
+
fontWeight: 600,
|
|
3272
|
+
color: priceChange >= 0 ? '#16a34a' : '#f43f5e',
|
|
3273
|
+
background: themePreset === 'light' ? 'rgba(15,23,42,0.08)' : 'rgba(15,23,42,0.5)',
|
|
3274
|
+
padding: '4px 10px',
|
|
3275
|
+
borderRadius: '999px'
|
|
3276
|
+
}, initial: { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.2 } },
|
|
3277
|
+
priceChange >= 0 ? '+' : '',
|
|
3278
|
+
numberFormatter.format(priceChange),
|
|
3279
|
+
" (",
|
|
3280
|
+
priceChangePercent >= 0 ? '+' : '',
|
|
3281
|
+
shortNumberFormatter.format(priceChangePercent),
|
|
3282
|
+
"%)"))),
|
|
3283
|
+
referenceCandle && (React.createElement("div", { style: { display: 'flex', gap: '10px', flexWrap: 'wrap', background: 'rgba(15,23,42,0.6)', borderRadius: '12px', padding: '10px 14px', border: '1px solid #1f2937', color: '#94a3b8', fontSize: '12px' } }, [
|
|
3284
|
+
{ label: 'O', value: referenceCandle.open },
|
|
3285
|
+
{ label: 'H', value: referenceCandle.high },
|
|
3286
|
+
{ label: 'L', value: referenceCandle.low },
|
|
3287
|
+
{ label: 'C', value: referenceCandle.close },
|
|
3288
|
+
{ label: 'V', value: referenceCandle.volume ?? 0 }
|
|
3289
|
+
].map(item => (React.createElement("div", { key: item.label, style: { display: 'flex', flexDirection: 'column', minWidth: '50px' } },
|
|
3290
|
+
React.createElement("span", { style: { fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase' } }, item.label),
|
|
3291
|
+
React.createElement("span", { style: { color: '#e2e8f0', fontWeight: 600 } }, item.value.toFixed(2)))))))),
|
|
3292
|
+
React.createElement("div", { style: {
|
|
3293
|
+
display: 'flex',
|
|
3294
|
+
alignItems: 'center',
|
|
3295
|
+
gap: '12px',
|
|
3296
|
+
padding: '10px 14px',
|
|
3297
|
+
background: activeTheme.panelBg,
|
|
3298
|
+
borderRadius: '16px',
|
|
3299
|
+
border: `1px solid ${activeTheme.panelBorder}`,
|
|
3300
|
+
overflowX: 'auto'
|
|
3301
|
+
} },
|
|
3302
|
+
React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap' } },
|
|
3303
|
+
React.createElement("span", { style: { fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8' } }, strings.timeframeTitle),
|
|
3304
|
+
React.createElement("span", { style: { fontSize: '11px', color: '#f8fafc', fontWeight: 600, padding: '2px 8px', borderRadius: '999px', background: 'rgba(37,99,235,0.25)', border: '1px solid rgba(37,99,235,0.6)' } }, timeframe)),
|
|
3305
|
+
React.createElement("div", { style: { display: 'flex', gap: '6px', flex: 1, minWidth: 0, overflowX: 'auto', paddingBottom: '4px' } }, ['1m', '3m', '5m', '15m', '30m', '1h', '4h'].map(tf => (React.createElement("button", { key: tf, onClick: () => handleTimeframeChange(tf), style: {
|
|
3306
|
+
background: timeframe === tf ? activeTheme.accent : activeTheme.panelBg,
|
|
3307
|
+
color: timeframe === tf ? '#f8fafc' : activeTheme.textSecondary,
|
|
3308
|
+
border: `1px solid ${timeframe === tf ? activeTheme.accent : activeTheme.panelBorder}`,
|
|
3309
|
+
borderRadius: '10px',
|
|
3310
|
+
padding: '6px 12px',
|
|
3311
|
+
fontSize: '12px',
|
|
3312
|
+
fontWeight: 600,
|
|
3313
|
+
cursor: 'pointer',
|
|
3314
|
+
transition: 'all 0.2s',
|
|
3315
|
+
flex: '0 0 auto',
|
|
3316
|
+
whiteSpace: 'nowrap'
|
|
3317
|
+
} }, tf)))),
|
|
3318
|
+
React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px', color: '#cbd5f5', whiteSpace: 'nowrap', flexShrink: 0 } },
|
|
3319
|
+
React.createElement(BsClockHistory, null),
|
|
3320
|
+
React.createElement("span", null,
|
|
3321
|
+
strings.axis.time,
|
|
3322
|
+
": ",
|
|
3323
|
+
currentTimeLabel)),
|
|
3324
|
+
React.createElement("select", { value: timeframe, onChange: (e) => handleTimeframeChange(e.target.value), style: {
|
|
3325
|
+
background: 'rgba(15,23,42,0.8)',
|
|
3326
|
+
color: '#e2e8f0',
|
|
3327
|
+
border: '1px solid #1f2937',
|
|
3328
|
+
padding: '10px 36px 10px 12px',
|
|
3329
|
+
borderRadius: '12px',
|
|
3330
|
+
cursor: 'pointer',
|
|
3331
|
+
fontSize: '12px',
|
|
3332
|
+
fontWeight: 600,
|
|
3333
|
+
appearance: 'none',
|
|
3334
|
+
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2394a3b8' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e")`,
|
|
3335
|
+
backgroundPosition: 'right 10px center',
|
|
3336
|
+
backgroundRepeat: 'no-repeat',
|
|
3337
|
+
backgroundSize: '14px',
|
|
3338
|
+
minWidth: '120px',
|
|
3339
|
+
flexShrink: 0
|
|
3340
|
+
} },
|
|
3341
|
+
React.createElement("option", { value: "1m" }, "1m"),
|
|
3342
|
+
React.createElement("option", { value: "3m" }, "3m"),
|
|
3343
|
+
React.createElement("option", { value: "5m" }, "5m"),
|
|
3344
|
+
React.createElement("option", { value: "15m" }, "15m"),
|
|
3345
|
+
React.createElement("option", { value: "30m" }, "30m"),
|
|
3346
|
+
React.createElement("option", { value: "1h" }, "1h"),
|
|
3347
|
+
React.createElement("option", { value: "4h" }, "4h"),
|
|
3348
|
+
React.createElement("option", { value: "12h" }, "12h"),
|
|
3349
|
+
React.createElement("option", { value: "1D" }, "1D"),
|
|
3350
|
+
React.createElement("option", { value: "3D" }, "3D"),
|
|
3351
|
+
React.createElement("option", { value: "1W" }, "1W"),
|
|
3352
|
+
React.createElement("option", { value: "1M" }, "1M"))),
|
|
3353
|
+
React.createElement("div", { style: { marginLeft: 'auto', display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' } },
|
|
3354
|
+
React.createElement("button", { onClick: () => setShowSeriesMenu(!showSeriesMenu), style: {
|
|
3355
|
+
width: '38px',
|
|
3356
|
+
height: '38px',
|
|
3357
|
+
borderRadius: '999px',
|
|
3358
|
+
border: `1px solid ${showSeriesMenu ? activeTheme.accent : iconBaseBg}`,
|
|
3359
|
+
background: showSeriesMenu ? activeTheme.accent : iconBaseBg,
|
|
3360
|
+
color: iconBaseColor,
|
|
3361
|
+
display: 'flex',
|
|
3362
|
+
alignItems: 'center',
|
|
3363
|
+
justifyContent: 'center',
|
|
3364
|
+
cursor: 'pointer',
|
|
3365
|
+
transition: 'all 0.2s'
|
|
3366
|
+
}, title: strings.buttons.series, "aria-label": strings.buttons.series }, seriesIconMap[seriesType] ?? React.createElement(BsGraphUp, null)),
|
|
3367
|
+
React.createElement("button", { onClick: toggleIndicatorsPanel, style: {
|
|
3368
|
+
background: showIndicatorsPanel ? '#2563eb' : 'rgba(15,23,42,0.6)',
|
|
3369
|
+
color: '#e2e8f0',
|
|
3370
|
+
border: '1px solid #1f2937',
|
|
3371
|
+
borderRadius: '999px',
|
|
3372
|
+
padding: '8px 14px',
|
|
3373
|
+
fontSize: '12px',
|
|
3374
|
+
fontWeight: 600,
|
|
3375
|
+
cursor: 'pointer',
|
|
3376
|
+
transition: 'all 0.2s'
|
|
3377
|
+
} }, strings.buttons.indicators),
|
|
3378
|
+
React.createElement("button", { onClick: () => setShowConfigPanel(!showConfigPanel), style: {
|
|
3379
|
+
width: '38px',
|
|
3380
|
+
height: '38px',
|
|
3381
|
+
borderRadius: '999px',
|
|
3382
|
+
border: `1px solid ${showConfigPanel ? activeTheme.accent : iconBaseBg}`,
|
|
3383
|
+
background: showConfigPanel ? activeTheme.accent : iconBaseBg,
|
|
3384
|
+
color: iconBaseColor,
|
|
3385
|
+
display: 'flex',
|
|
3386
|
+
alignItems: 'center',
|
|
3387
|
+
justifyContent: 'center',
|
|
3388
|
+
cursor: 'pointer',
|
|
3389
|
+
transition: 'all 0.2s'
|
|
3390
|
+
}, title: strings.buttons.config, "aria-label": strings.buttons.config },
|
|
3391
|
+
React.createElement(BsGear, null)),
|
|
3392
|
+
React.createElement("button", { onClick: () => setIsFullscreen(prev => !prev), style: {
|
|
3393
|
+
width: '38px',
|
|
3394
|
+
height: '38px',
|
|
3395
|
+
borderRadius: '999px',
|
|
3396
|
+
border: `1px solid ${isFullscreen ? activeTheme.accent : iconBaseBg}`,
|
|
3397
|
+
background: isFullscreen ? activeTheme.accent : iconBaseBg,
|
|
3398
|
+
color: iconBaseColor,
|
|
3399
|
+
display: 'flex',
|
|
3400
|
+
alignItems: 'center',
|
|
3401
|
+
justifyContent: 'center',
|
|
3402
|
+
cursor: 'pointer',
|
|
3403
|
+
transition: 'all 0.2s'
|
|
3404
|
+
}, title: isFullscreen ? strings.buttons.fullscreenExit : strings.buttons.fullscreenEnter, "aria-label": isFullscreen ? strings.buttons.fullscreenExit : strings.buttons.fullscreenEnter },
|
|
3405
|
+
React.createElement(BsArrowsFullscreen, null)),
|
|
3406
|
+
React.createElement("button", { onClick: takeScreenshot, style: {
|
|
3407
|
+
width: '38px',
|
|
3408
|
+
height: '38px',
|
|
3409
|
+
borderRadius: '999px',
|
|
3410
|
+
border: `1px solid ${iconBaseBg}`,
|
|
3411
|
+
background: iconBaseBg,
|
|
3412
|
+
color: iconBaseColor,
|
|
3413
|
+
display: 'flex',
|
|
3414
|
+
alignItems: 'center',
|
|
3415
|
+
justifyContent: 'center',
|
|
3416
|
+
cursor: 'pointer',
|
|
3417
|
+
transition: 'all 0.2s'
|
|
3418
|
+
}, title: strings.buttons.screenshot, "aria-label": strings.buttons.screenshot },
|
|
3419
|
+
React.createElement(BsCamera, null)),
|
|
3420
|
+
React.createElement("button", { onClick: () => setShowQuickTips(prev => !prev), style: {
|
|
3421
|
+
width: '38px',
|
|
3422
|
+
height: '38px',
|
|
3423
|
+
borderRadius: '999px',
|
|
3424
|
+
border: `1px solid ${showQuickTips ? activeTheme.accent : iconBaseBg}`,
|
|
3425
|
+
background: showQuickTips ? activeTheme.accent : iconBaseBg,
|
|
3426
|
+
color: iconBaseColor,
|
|
3427
|
+
display: 'flex',
|
|
3428
|
+
alignItems: 'center',
|
|
3429
|
+
justifyContent: 'center',
|
|
3430
|
+
cursor: 'pointer',
|
|
3431
|
+
transition: 'all 0.2s'
|
|
3432
|
+
}, title: strings.buttons.help, "aria-label": strings.buttons.help },
|
|
3433
|
+
React.createElement(BsQuestionCircle, null)))),
|
|
3434
|
+
derivedStats && (React.createElement("div", { style: { padding: '16px 20px 0', background: activeTheme.panelBg, borderBottom: `1px solid ${activeTheme.panelBorder}` } },
|
|
3435
|
+
React.createElement("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '12px' } },
|
|
3436
|
+
React.createElement("div", { style: { ...metricCardStyle, background: activeTheme.cardBg, border: `1px solid ${activeTheme.cardBorder}` } },
|
|
3437
|
+
React.createElement("span", { style: { ...metricLabelStyle, color: activeTheme.textSecondary } }, "Trading range"),
|
|
3438
|
+
React.createElement("span", { style: { ...metricValueStyle, color: activeTheme.textPrimary } }, numberFormatter.format(derivedStats.range)),
|
|
3439
|
+
React.createElement("span", { style: { ...metricSubValueStyle, color: activeTheme.textSecondary } },
|
|
3440
|
+
derivedStats.rangePct.toFixed(2),
|
|
3441
|
+
"%")),
|
|
3442
|
+
React.createElement("div", { style: { ...metricCardStyle, background: activeTheme.cardBg, border: `1px solid ${activeTheme.cardBorder}` } },
|
|
3443
|
+
React.createElement("span", { style: { ...metricLabelStyle, color: activeTheme.textSecondary } }, "Avg volume"),
|
|
3444
|
+
React.createElement("span", { style: { ...metricValueStyle, color: activeTheme.textPrimary } }, shortNumberFormatter.format(derivedStats.avgVolume)),
|
|
3445
|
+
React.createElement("span", { style: { ...metricSubValueStyle, color: activeTheme.textSecondary } },
|
|
3446
|
+
"Last ",
|
|
3447
|
+
derivedStats.samples,
|
|
3448
|
+
" samples")),
|
|
3449
|
+
React.createElement("div", { style: { ...metricCardStyle, background: activeTheme.cardBg, border: `1px solid ${activeTheme.cardBorder}` } },
|
|
3450
|
+
React.createElement("span", { style: { ...metricLabelStyle, color: activeTheme.textSecondary } }, "Volatility"),
|
|
3451
|
+
React.createElement("span", { style: { ...metricValueStyle, color: activeTheme.textPrimary } },
|
|
3452
|
+
derivedStats.volatility.toFixed(2),
|
|
3453
|
+
"%"),
|
|
3454
|
+
React.createElement("span", { style: { ...metricSubValueStyle, color: activeTheme.textSecondary } }, "Abs move mean")),
|
|
3455
|
+
React.createElement("div", { style: { ...metricCardStyle, background: activeTheme.cardBg, border: `1px solid ${activeTheme.cardBorder}` } },
|
|
3456
|
+
React.createElement("span", { style: { ...metricLabelStyle, color: activeTheme.textSecondary } }, "Session"),
|
|
3457
|
+
React.createElement("span", { style: { ...metricValueStyle, color: activeTheme.textPrimary } }, derivedStats.session),
|
|
3458
|
+
React.createElement("span", { style: { ...metricSubValueStyle, color: activeTheme.textSecondary } },
|
|
3459
|
+
"Latency ~",
|
|
3460
|
+
latencyMs,
|
|
3461
|
+
"ms"))))),
|
|
3462
|
+
React.createElement(AnimatePresence, null, showQuickTips && (React.createElement(motion.div, { initial: { opacity: 0, y: -10 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -10 }, transition: { duration: 0.2 }, style: {
|
|
3463
|
+
position: (isFullscreen ? 'fixed' : 'absolute'),
|
|
3464
|
+
top: isFullscreen ? 80 : 140,
|
|
3465
|
+
left: isFullscreen ? 80 : 140,
|
|
3466
|
+
maxWidth: '300px',
|
|
3467
|
+
background: activeTheme.overlayBg,
|
|
3468
|
+
borderRadius: '16px',
|
|
3469
|
+
padding: '16px',
|
|
3470
|
+
border: `1px solid ${activeTheme.accent}`,
|
|
3471
|
+
boxShadow: '0 25px 60px rgba(0,0,0,0.55)',
|
|
3472
|
+
zIndex: 1200
|
|
3473
|
+
} },
|
|
3474
|
+
React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '10px', color: activeTheme.textPrimary, fontWeight: 600 } },
|
|
3475
|
+
React.createElement(BsInfoCircle, null),
|
|
3476
|
+
strings.quickTipsTitle),
|
|
3477
|
+
React.createElement("ul", { style: { listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '8px', color: activeTheme.textSecondary, fontSize: '13px' } }, strings.quickTips.map(tip => (React.createElement("li", { key: tip, style: { display: 'flex', alignItems: 'center', gap: '6px' } },
|
|
3478
|
+
React.createElement(BsCheck2, { style: { color: activeTheme.accent } }),
|
|
3479
|
+
React.createElement("span", null, tip))))),
|
|
3480
|
+
React.createElement("button", { onClick: () => setShowQuickTips(false), style: {
|
|
3481
|
+
marginTop: '12px',
|
|
3482
|
+
width: '100%',
|
|
3483
|
+
background: activeTheme.accent,
|
|
3484
|
+
color: '#f8fafc',
|
|
3485
|
+
border: 'none',
|
|
3486
|
+
borderRadius: '10px',
|
|
3487
|
+
padding: '8px 0',
|
|
3488
|
+
fontSize: '12px',
|
|
3489
|
+
fontWeight: 600,
|
|
3490
|
+
cursor: 'pointer'
|
|
3491
|
+
} }, strings.quickTipsButton)))),
|
|
3492
|
+
showNoteModal && (React.createElement("div", { style: {
|
|
3493
|
+
position: (isFullscreen ? 'fixed' : 'absolute'),
|
|
3494
|
+
inset: isFullscreen ? 0 : undefined,
|
|
3495
|
+
top: isFullscreen ? 0 : 0,
|
|
3496
|
+
left: isFullscreen ? 0 : 0,
|
|
3497
|
+
right: 0,
|
|
3498
|
+
bottom: 0,
|
|
3499
|
+
background: activeTheme.overlayBg,
|
|
3500
|
+
display: 'flex',
|
|
3501
|
+
alignItems: 'center',
|
|
3502
|
+
justifyContent: 'center',
|
|
3503
|
+
zIndex: 1600,
|
|
3504
|
+
padding: '20px'
|
|
3505
|
+
} },
|
|
3506
|
+
React.createElement("div", { style: {
|
|
3507
|
+
width: 'min(420px, 90vw)',
|
|
3508
|
+
background: activeTheme.panelBg,
|
|
3509
|
+
borderRadius: '20px',
|
|
3510
|
+
border: `1px solid ${activeTheme.panelBorder}`,
|
|
3511
|
+
padding: '24px',
|
|
3512
|
+
boxShadow: '0 30px 80px rgba(0,0,0,0.6)',
|
|
3513
|
+
display: 'flex',
|
|
3514
|
+
flexDirection: 'column',
|
|
3515
|
+
gap: '12px'
|
|
3516
|
+
} },
|
|
3517
|
+
React.createElement("div", null,
|
|
3518
|
+
React.createElement("p", { style: { margin: 0, fontSize: '12px', letterSpacing: '0.08em', textTransform: 'uppercase', color: activeTheme.textSecondary } }, "Quick note"),
|
|
3519
|
+
React.createElement("h3", { style: { margin: '4px 0 0', color: activeTheme.textPrimary, fontSize: '18px' } }, "Write a quick note in English")),
|
|
3520
|
+
React.createElement("textarea", { value: noteDraft, onChange: (event) => setNoteDraft(event.target.value), placeholder: "Describe your idea...", rows: 4, style: {
|
|
3521
|
+
width: '100%',
|
|
3522
|
+
background: activeTheme.plotBg,
|
|
3523
|
+
border: `1px solid ${activeTheme.panelBorder}`,
|
|
3524
|
+
borderRadius: '12px',
|
|
3525
|
+
padding: '12px',
|
|
3526
|
+
color: activeTheme.textPrimary,
|
|
3527
|
+
fontFamily: 'Inter, sans-serif',
|
|
3528
|
+
fontSize: '14px',
|
|
3529
|
+
resize: 'none'
|
|
3530
|
+
} }),
|
|
3531
|
+
React.createElement("div", { style: { display: 'flex', justifyContent: 'flex-end', gap: '10px' } },
|
|
3532
|
+
React.createElement("button", { onClick: handleCancelNote, style: {
|
|
3533
|
+
padding: '8px 16px',
|
|
3534
|
+
borderRadius: '10px',
|
|
3535
|
+
border: `1px solid ${activeTheme.panelBorder}`,
|
|
3536
|
+
background: 'transparent',
|
|
3537
|
+
color: activeTheme.textPrimary,
|
|
3538
|
+
cursor: 'pointer',
|
|
3539
|
+
fontSize: '13px',
|
|
3540
|
+
fontWeight: 600
|
|
3541
|
+
} }, "Cancel"),
|
|
3542
|
+
React.createElement("button", { onClick: handleSaveNote, disabled: !noteDraft.trim(), style: {
|
|
3543
|
+
padding: '8px 16px',
|
|
3544
|
+
borderRadius: '10px',
|
|
3545
|
+
border: 'none',
|
|
3546
|
+
background: noteDraft.trim() ? activeTheme.accent : activeTheme.accentSoft,
|
|
3547
|
+
color: '#f8fafc',
|
|
3548
|
+
cursor: noteDraft.trim() ? 'pointer' : 'not-allowed',
|
|
3549
|
+
fontSize: '13px',
|
|
3550
|
+
fontWeight: 600
|
|
3551
|
+
} }, "Save note"))))),
|
|
3552
|
+
React.createElement("div", { style: {
|
|
3553
|
+
flex: 1,
|
|
3554
|
+
display: 'flex',
|
|
3555
|
+
gap: isMobile ? '12px' : '16px',
|
|
3556
|
+
padding: isFullscreen ? '16px' : isMobile ? '12px 16px 20px' : '16px 20px 24px',
|
|
3557
|
+
minHeight: 0,
|
|
3558
|
+
alignItems: 'stretch'
|
|
3559
|
+
} },
|
|
3560
|
+
React.createElement("div", { style: { width: isMobile ? '60px' : '88px', flexShrink: 0, height: '100%' } },
|
|
3561
|
+
React.createElement("div", { style: {
|
|
3562
|
+
height: '100%',
|
|
3563
|
+
borderRadius: '24px',
|
|
3564
|
+
border: `1px solid ${themePreset === 'light' ? '#0f172a' : activeTheme.railBorder}`,
|
|
3565
|
+
background: themePreset === 'light' ? '#0f172a' : activeTheme.railBg,
|
|
3566
|
+
display: 'flex',
|
|
3567
|
+
justifyContent: 'center',
|
|
3568
|
+
alignItems: 'center',
|
|
3569
|
+
padding: isMobile ? '12px 0' : '16px 0'
|
|
3570
|
+
} },
|
|
3571
|
+
React.createElement(DrawingToolbar, null))),
|
|
3572
|
+
React.createElement("div", { ref: plotAreaRef, style: {
|
|
3573
|
+
flex: 1,
|
|
3574
|
+
position: 'relative',
|
|
3575
|
+
background: activeTheme.plotBg,
|
|
3576
|
+
cursor: cursorCss,
|
|
3577
|
+
borderRadius: '24px',
|
|
3578
|
+
border: `1px solid ${activeTheme.plotBorder}`,
|
|
3579
|
+
overflow: 'hidden',
|
|
3580
|
+
minHeight: isMobile ? '400px' : '520px',
|
|
3581
|
+
boxShadow: surfaceShadow
|
|
3582
|
+
}, onPointerMove: (e) => { handlePointerMove(e); handlePointerMoveDrag(e); }, onPointerLeave: handleMouseLeave, onWheel: handleWheel, onPointerDown: handlePointerDown, onPointerUp: handlePointerUp },
|
|
3583
|
+
React.createElement("div", { style: { position: 'absolute', left: 20, bottom: 90, display: 'flex', flexDirection: 'column', gap: '10px', zIndex: 35 } }, leftRailActions.map(action => {
|
|
3584
|
+
const isDisabled = interactionsLocked && ['zoomIn', 'zoomOut', 'reset'].includes(action.key);
|
|
3585
|
+
return (React.createElement("button", { key: action.key, onClick: action.onClick, style: {
|
|
3586
|
+
width: isMobile ? '36px' : '42px',
|
|
3587
|
+
height: isMobile ? '36px' : '42px',
|
|
3588
|
+
borderRadius: '12px',
|
|
3589
|
+
border: `1px solid ${action.active ? activeTheme.accent : leftRailBaseBg}`,
|
|
3590
|
+
background: action.active ? activeTheme.accent : leftRailBaseBg,
|
|
3591
|
+
color: action.active ? '#f8fafc' : (themePreset === 'light' ? '#f8fafc' : activeTheme.textPrimary),
|
|
3592
|
+
display: 'flex',
|
|
3593
|
+
alignItems: 'center',
|
|
3594
|
+
justifyContent: 'center',
|
|
3595
|
+
fontSize: isMobile ? '16px' : '18px',
|
|
3596
|
+
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
3597
|
+
opacity: isDisabled ? 0.4 : 1,
|
|
3598
|
+
transition: 'all 0.2s'
|
|
3599
|
+
}, title: action.label, disabled: isDisabled }, action.icon));
|
|
3600
|
+
})),
|
|
3601
|
+
React.createElement("div", { style: { position: 'absolute', right: 0, top: 0, width: `${priceScaleWidth}px`, height: '100%', background: activeTheme.scaleBg, borderLeft: `1px solid ${activeTheme.plotBorder}`, fontSize: '11px', color: activeTheme.textSecondary, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', padding: '8px 0' } },
|
|
3602
|
+
React.createElement("div", { style: { textTransform: 'uppercase', fontSize: '10px', letterSpacing: '0.08em', paddingLeft: '5px', marginBottom: '6px' } }, strings.axis.price),
|
|
3603
|
+
priceLabels.map((label, i) => (React.createElement("div", { key: i, style: { textAlign: 'left', paddingLeft: '5px', color: activeTheme.textPrimary } }, label)))),
|
|
3604
|
+
React.createElement("canvas", { ref: gridRef, width: chartWidth, height: chartHeight, style: { position: 'absolute', left: 0, top: 0, width: chartWidth, height: chartHeight } }),
|
|
3605
|
+
React.createElement("canvas", { ref: chartRef, width: chartWidth, height: chartHeight, style: { position: 'absolute', left: 0, top: 0, width: chartWidth, height: chartHeight } }),
|
|
3606
|
+
React.createElement("canvas", { ref: volumeRef, width: chartWidth, height: volumeHeight, style: { position: 'absolute', left: 0, top: chartHeight, width: chartWidth, height: volumeHeight } }),
|
|
3607
|
+
React.createElement("canvas", { ref: drawingsRef, width: chartWidth, height: overlayHeight, style: { position: 'absolute', left: 0, top: 0, pointerEvents: 'none', width: chartWidth, height: overlayHeight } }),
|
|
3608
|
+
React.createElement("canvas", { ref: overlayRef, width: chartWidth, height: overlayHeight, style: { position: 'absolute', left: 0, top: 0, pointerEvents: 'none', width: chartWidth, height: overlayHeight } }),
|
|
3609
|
+
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' } },
|
|
3610
|
+
React.createElement("span", { style: { textTransform: 'uppercase', fontSize: '10px', letterSpacing: '0.08em' } }, strings.axis.time),
|
|
3611
|
+
React.createElement("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', color: activeTheme.textPrimary } }, timeLabels.map((label, i) => (React.createElement("div", { key: i }, label))))),
|
|
3612
|
+
clickedPrice && (React.createElement("div", { style: {
|
|
3613
|
+
position: 'absolute',
|
|
3614
|
+
background: '#1e222d',
|
|
3615
|
+
color: '#d1d4dc',
|
|
3616
|
+
padding: '8px 12px',
|
|
3617
|
+
borderRadius: '6px',
|
|
3618
|
+
fontSize: '14px',
|
|
3619
|
+
fontWeight: '600',
|
|
3620
|
+
pointerEvents: 'none',
|
|
3621
|
+
zIndex: 10,
|
|
3622
|
+
left: clickedPrice.x + 10,
|
|
3623
|
+
top: clickedPrice.y - 10,
|
|
3624
|
+
border: '1px solid #2a2e39',
|
|
3625
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.6)',
|
|
3626
|
+
backdropFilter: 'blur(8px)',
|
|
3627
|
+
WebkitBackdropFilter: 'blur(8px)'
|
|
3628
|
+
} },
|
|
3629
|
+
"$",
|
|
3630
|
+
clickedPrice.price.toFixed(2))),
|
|
3631
|
+
isSeriesLoading && (React.createElement(motion.div, { initial: { opacity: 0, scale: 0.95 }, animate: { opacity: 1, scale: 1 }, style: {
|
|
3632
|
+
position: 'absolute',
|
|
3633
|
+
inset: 0,
|
|
3634
|
+
display: 'flex',
|
|
3635
|
+
alignItems: 'center',
|
|
3636
|
+
justifyContent: 'center',
|
|
3637
|
+
background: 'rgba(0,0,0,0.35)',
|
|
3638
|
+
zIndex: 50
|
|
3639
|
+
} },
|
|
3640
|
+
React.createElement("div", { style: {
|
|
3641
|
+
padding: '10px 16px',
|
|
3642
|
+
borderRadius: '999px',
|
|
3643
|
+
background: '#111827',
|
|
3644
|
+
border: '1px solid #2962ff',
|
|
3645
|
+
display: 'flex',
|
|
3646
|
+
alignItems: 'center',
|
|
3647
|
+
gap: '8px',
|
|
3648
|
+
boxShadow: '0 10px 30px rgba(0,0,0,0.8)'
|
|
3649
|
+
} },
|
|
3650
|
+
React.createElement("div", { style: {
|
|
3651
|
+
width: '10px',
|
|
3652
|
+
height: '10px',
|
|
3653
|
+
borderRadius: '999px',
|
|
3654
|
+
background: '#2962ff',
|
|
3655
|
+
boxShadow: '0 0 12px #2962ff'
|
|
3656
|
+
} }),
|
|
3657
|
+
React.createElement("span", { style: { fontSize: '12px', color: '#e5e7eb', fontWeight: 500 } }, "Updating series...")))))),
|
|
3658
|
+
React.createElement(AnimatePresence, null, toastMessage && (React.createElement(motion.div, { key: toastMessage, initial: { opacity: 0, y: 12 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: 12 }, transition: { duration: 0.2 }, style: {
|
|
3659
|
+
position: (isFullscreen ? 'fixed' : 'absolute'),
|
|
3660
|
+
bottom: isFullscreen ? 24 : 16,
|
|
3661
|
+
right: isFullscreen ? 24 : 16,
|
|
3662
|
+
background: 'rgba(15,23,42,0.9)',
|
|
3663
|
+
border: '1px solid #1d4ed8',
|
|
3664
|
+
borderRadius: '999px',
|
|
3665
|
+
padding: '10px 18px',
|
|
3666
|
+
color: '#e2e8f0',
|
|
3667
|
+
fontSize: '12px',
|
|
3668
|
+
display: 'flex',
|
|
3669
|
+
alignItems: 'center',
|
|
3670
|
+
gap: '8px',
|
|
3671
|
+
boxShadow: '0 20px 45px rgba(0,0,0,0.5)',
|
|
3672
|
+
pointerEvents: 'none',
|
|
3673
|
+
zIndex: 1200
|
|
3674
|
+
} },
|
|
3675
|
+
React.createElement(BsInfoCircle, { style: { color: '#60a5fa' } }),
|
|
3676
|
+
React.createElement("span", null, toastMessage)))),
|
|
3677
|
+
showSeriesMenu && (React.createElement("div", { style: {
|
|
3678
|
+
position: 'absolute',
|
|
3679
|
+
top: '50px',
|
|
3680
|
+
right: '140px',
|
|
3681
|
+
background: activeTheme.panelBg,
|
|
3682
|
+
border: `1px solid ${activeTheme.panelBorder}`,
|
|
3683
|
+
borderRadius: '12px',
|
|
3684
|
+
padding: '16px',
|
|
3685
|
+
minWidth: '240px',
|
|
3686
|
+
maxHeight: '60vh',
|
|
3687
|
+
overflowY: 'auto',
|
|
3688
|
+
zIndex: 1000,
|
|
3689
|
+
boxShadow: '0 12px 30px rgba(0,0,0,0.6)'
|
|
3690
|
+
} },
|
|
3691
|
+
React.createElement("div", { style: { fontSize: '13px', fontWeight: '600', marginBottom: '12px', color: '#d1d4dc', display: 'flex', alignItems: 'center', gap: '6px' } },
|
|
3692
|
+
React.createElement(BsGraphUp, null),
|
|
3693
|
+
"Series Type"),
|
|
3694
|
+
React.createElement("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px' } }, seriesTypeOptions.map(({ value, label, icon }) => (React.createElement("button", { key: value, onClick: () => {
|
|
3695
|
+
setSeriesType(value);
|
|
3696
|
+
setShowSeriesMenu(false);
|
|
3697
|
+
}, style: {
|
|
3698
|
+
display: 'flex',
|
|
3699
|
+
alignItems: 'center',
|
|
3700
|
+
gap: '8px',
|
|
3701
|
+
padding: '8px',
|
|
3702
|
+
borderRadius: '10px',
|
|
3703
|
+
border: seriesType === value ? '1px solid #2563eb' : '1px solid #2a2e39',
|
|
3704
|
+
background: seriesType === value ? 'rgba(37,99,235,0.15)' : 'rgba(2,6,23,0.5)',
|
|
3705
|
+
color: '#e2e8f0',
|
|
3706
|
+
cursor: 'pointer',
|
|
3707
|
+
fontSize: '12px'
|
|
3708
|
+
} },
|
|
3709
|
+
React.createElement("span", { style: { fontSize: '18px' } }, icon),
|
|
3710
|
+
React.createElement("span", null, label))))))),
|
|
3711
|
+
showIndicatorsPanel && (React.createElement("div", { style: {
|
|
3712
|
+
position: 'absolute',
|
|
3713
|
+
top: '50px',
|
|
3714
|
+
right: '10px',
|
|
3715
|
+
background: activeTheme.panelBg,
|
|
3716
|
+
border: `1px solid ${activeTheme.panelBorder}`,
|
|
3717
|
+
borderRadius: '12px',
|
|
3718
|
+
padding: '16px',
|
|
3719
|
+
minWidth: '250px',
|
|
3720
|
+
maxHeight: '60vh',
|
|
3721
|
+
overflowY: 'auto',
|
|
3722
|
+
zIndex: 1000,
|
|
3723
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.6)'
|
|
3724
|
+
} },
|
|
3725
|
+
React.createElement("div", { style: { fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: '#d1d4dc', display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
|
|
3726
|
+
"Indicators",
|
|
3727
|
+
React.createElement("div", { style: { display: 'flex', gap: '8px' } },
|
|
3728
|
+
React.createElement("button", { onClick: () => setShowConfigPanel(!showConfigPanel), style: { background: 'transparent', border: 'none', color: '#b2b5be', cursor: 'pointer', fontSize: '16px' } },
|
|
3729
|
+
React.createElement(BsGear, null)),
|
|
3730
|
+
React.createElement("button", { onClick: takeScreenshot, style: { background: 'transparent', border: 'none', color: '#b2b5be', cursor: 'pointer', fontSize: '16px' } },
|
|
3731
|
+
React.createElement(BsCamera, null)))),
|
|
3732
|
+
['SMA', 'EMA', 'RSI', 'MACD'].map(indicatorName => {
|
|
3733
|
+
const isActive = indicators.some(config => config.name === indicatorName && config.visible);
|
|
3734
|
+
return (React.createElement("div", { key: indicatorName, style: { display: 'flex', alignItems: 'center', marginBottom: '8px' } },
|
|
3735
|
+
React.createElement("input", { type: "checkbox", checked: isActive, onChange: (e) => {
|
|
3736
|
+
if (e.target.checked) {
|
|
3737
|
+
const newConfig = {
|
|
3738
|
+
name: indicatorName,
|
|
3739
|
+
params: indicatorName === 'SMA' ? { period: 20 } :
|
|
3740
|
+
indicatorName === 'EMA' ? { period: 20 } :
|
|
3741
|
+
indicatorName === 'RSI' ? { period: 14 } :
|
|
3742
|
+
{ fastPeriod: 12, slowPeriod: 26, signalPeriod: 9 },
|
|
3743
|
+
color: '#2962ff',
|
|
3744
|
+
visible: true
|
|
3745
|
+
};
|
|
3746
|
+
addIndicator(newConfig);
|
|
3747
|
+
}
|
|
3748
|
+
else {
|
|
3749
|
+
removeIndicator(indicatorName);
|
|
3750
|
+
}
|
|
3751
|
+
}, style: { marginRight: '8px' } }),
|
|
3752
|
+
React.createElement("span", { style: { fontSize: '12px', color: '#b2b5be' } }, indicatorName)));
|
|
3753
|
+
}))),
|
|
3754
|
+
showConfigPanel && (React.createElement("div", { style: {
|
|
3755
|
+
position: 'absolute',
|
|
3756
|
+
top: '50px',
|
|
3757
|
+
right: '280px',
|
|
3758
|
+
background: activeTheme.panelBg,
|
|
3759
|
+
border: `1px solid ${activeTheme.panelBorder}`,
|
|
3760
|
+
borderRadius: '12px',
|
|
3761
|
+
padding: '16px',
|
|
3762
|
+
minWidth: '240px',
|
|
3763
|
+
maxHeight: '60vh',
|
|
3764
|
+
overflowY: 'auto',
|
|
3765
|
+
zIndex: 1000,
|
|
3766
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.6)'
|
|
3767
|
+
} },
|
|
3768
|
+
React.createElement("div", { style: { fontSize: '14px', fontWeight: 600, marginBottom: '12px', color: activeTheme.textPrimary } }, strings.config.title),
|
|
3769
|
+
React.createElement("div", { style: { marginBottom: '12px' } },
|
|
3770
|
+
React.createElement("label", { style: { fontSize: '12px', color: activeTheme.textSecondary, display: 'block', marginBottom: '4px' } }, strings.config.language),
|
|
3771
|
+
React.createElement("select", { value: language, onChange: (e) => setLanguage(e.target.value), style: { width: '100%', background: activeTheme.plotBg, color: activeTheme.textPrimary, border: `1px solid ${activeTheme.panelBorder}`, padding: '6px', borderRadius: '8px' } },
|
|
3772
|
+
React.createElement("option", { value: "es" }, "Espa\u00F1ol"),
|
|
3773
|
+
React.createElement("option", { value: "en" }, "English"))),
|
|
3774
|
+
React.createElement("div", { style: { marginBottom: '12px' } },
|
|
3775
|
+
React.createElement("label", { style: { fontSize: '12px', color: activeTheme.textSecondary, display: 'block', marginBottom: '4px' } }, strings.config.colors),
|
|
3776
|
+
React.createElement("select", { value: colorScheme, onChange: (e) => setColorScheme(e.target.value), style: { width: '100%', background: activeTheme.plotBg, color: activeTheme.textPrimary, border: `1px solid ${activeTheme.panelBorder}`, padding: '6px', borderRadius: '8px' } },
|
|
3777
|
+
React.createElement("option", { value: "green" }, strings.config.colorOptions.green),
|
|
3778
|
+
React.createElement("option", { value: "red" }, strings.config.colorOptions.red))),
|
|
3779
|
+
React.createElement("div", { style: { marginBottom: '12px' } },
|
|
3780
|
+
React.createElement("label", { style: { fontSize: '12px', color: activeTheme.textSecondary, display: 'block', marginBottom: '6px' } }, "Theme preset"),
|
|
3781
|
+
React.createElement("div", { style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } }, themeOptions.map(option => (React.createElement("button", { key: option.key, onClick: () => setThemePreset(option.key), style: {
|
|
3782
|
+
padding: '6px 10px',
|
|
3783
|
+
borderRadius: '999px',
|
|
3784
|
+
border: `1px solid ${themePreset === option.key ? activeTheme.accent : activeTheme.panelBorder}`,
|
|
3785
|
+
background: themePreset === option.key ? activeTheme.accentSoft : 'transparent',
|
|
3786
|
+
color: activeTheme.textPrimary,
|
|
3787
|
+
fontSize: '11px',
|
|
3788
|
+
cursor: 'pointer'
|
|
3789
|
+
} }, option.label))))),
|
|
3790
|
+
themePreset === 'custom' && (React.createElement("div", { style: { marginBottom: '12px', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: '10px' } }, CUSTOM_THEME_FIELDS.map(field => (React.createElement("label", { key: field.key, style: { fontSize: '11px', color: activeTheme.textSecondary, display: 'flex', flexDirection: 'column', gap: '4px' } },
|
|
3791
|
+
React.createElement("span", null, field.label),
|
|
3792
|
+
React.createElement("input", { type: "color", value: customTheme[field.key], onChange: (event) => handleCustomThemeChange(field.key, event.target.value), style: { width: '100%', height: '34px', border: 'none', cursor: 'pointer', borderRadius: '8px', background: 'transparent' } })))))),
|
|
3793
|
+
React.createElement("div", { style: { fontSize: '12px', color: activeTheme.textSecondary } }, strings.config.soon))),
|
|
3794
|
+
React.createElement("div", { style: { padding: isMobile ? '8px 16px' : '10px 20px', background: activeTheme.panelBg, borderTop: `1px solid ${activeTheme.panelBorder}`, display: 'flex', flexWrap: 'wrap', gap: isMobile ? '12px' : '18px', fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase', color: activeTheme.textSecondary } },
|
|
3795
|
+
React.createElement("span", null,
|
|
3796
|
+
"Latency ",
|
|
3797
|
+
latencyMs,
|
|
3798
|
+
"ms"),
|
|
3799
|
+
React.createElement("span", null,
|
|
3800
|
+
"Session ",
|
|
3801
|
+
derivedStats?.session ?? 'Global'),
|
|
3802
|
+
React.createElement("span", null, "Feed Binance Composite"),
|
|
3803
|
+
React.createElement("span", null, "Security AES-256"))));
|
|
3804
|
+
};
|
|
3805
|
+
var TradingViewChart_default = memo(TradingViewChart);
|
|
3806
|
+
|
|
3807
|
+
export { Chart, TradingViewChart_default as TradingViewChart };
|
|
3808
|
+
//# sourceMappingURL=index.mjs.map
|