@backtest-kit/ui 6.15.0 → 6.16.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/build/index.cjs +108 -14
- package/build/index.mjs +109 -15
- package/build/modules/frontend/build/3rdparty/qfchart_0.8.7/echarts.min.js +45 -0
- package/build/modules/frontend/build/3rdparty/qfchart_0.8.7/pinets.js +21534 -0
- package/build/modules/frontend/build/3rdparty/qfchart_0.8.7/qfchart.js +3997 -0
- package/build/modules/frontend/build/assets/Article-DYhWfFAi.js +1 -0
- package/build/modules/frontend/build/assets/Background-COpkJTP-.js +1 -0
- package/build/modules/frontend/build/assets/{IconPhoto-CNX0t203.js → IconPhoto-DUgsqsYP.js} +1 -1
- package/build/modules/frontend/build/assets/{KeyboardArrowLeft-5ogjRzwF.js → KeyboardArrowLeft-BlwcBPHn.js} +1 -1
- package/build/modules/frontend/build/assets/Refresh-aWduZclR.js +1 -0
- package/build/modules/frontend/build/assets/emitters-CSLNlyHG.js +1 -0
- package/build/modules/frontend/build/assets/hasRouteMatch-CImjW4Jf.js +1 -0
- package/build/modules/frontend/build/assets/{html2canvas-BnarC1H-.js → html2canvas-Do8zi4tS.js} +1 -1
- package/build/modules/frontend/build/assets/index-7wQuV9F_.js +1 -0
- package/build/modules/frontend/build/assets/index-B_46G-d8.js +1 -0
- package/build/modules/frontend/build/assets/index-BfiWEbdc.js +1 -0
- package/build/modules/frontend/build/assets/index-Bmqo1dsM.js +1 -0
- package/build/modules/frontend/build/assets/index-CETLG42q.js +1 -0
- package/build/modules/frontend/build/assets/index-Cb4CVVwH.js +1 -0
- package/build/modules/frontend/build/assets/{index-BMzg4t4L.js → index-Ce2sQITf.js} +7 -7
- package/build/modules/frontend/build/assets/{index-CYHbLqvS.js → index-CjymMd2D.js} +1 -1
- package/build/modules/frontend/build/assets/{index-DLH5bivm.js → index-D6YeBcAB.js} +1 -1
- package/build/modules/frontend/build/assets/index-WhalaOXd.js +1 -0
- package/build/modules/frontend/build/assets/{index-eYCKhqo-.js → index-kQ_oUmTQ.js} +1 -1
- package/build/modules/frontend/build/assets/{index-BY7xx8Qo.js → index-lZGMPvrT.js} +1 -1
- package/build/modules/frontend/build/assets/{index.es-XkghWdRJ.js → index.es-Cel98f_u.js} +1 -1
- package/build/modules/frontend/build/assets/{markdownit-CFUj-lOk.js → markdownit-22RgFULT.js} +1 -1
- package/build/modules/frontend/build/index.html +4 -1
- package/package.json +3 -3
- package/types.d.ts +11 -2
- package/build/modules/frontend/build/assets/Article-BFiJ1grS.js +0 -1
- package/build/modules/frontend/build/assets/Background-C4tJEFCD.js +0 -1
- package/build/modules/frontend/build/assets/Refresh-DPMpKmUf.js +0 -1
- package/build/modules/frontend/build/assets/emitters-BxM0853t.js +0 -1
- package/build/modules/frontend/build/assets/hasRouteMatch-DvMTRSzL.js +0 -1
- package/build/modules/frontend/build/assets/index-2cnF0R3L.js +0 -1
- package/build/modules/frontend/build/assets/index-B9jcFvZN.js +0 -1
- package/build/modules/frontend/build/assets/index-C-XjS3CF.js +0 -1
- package/build/modules/frontend/build/assets/index-C1SBiwRD.js +0 -1
- package/build/modules/frontend/build/assets/index-C5kEItX6.js +0 -1
- package/build/modules/frontend/build/assets/index-Dl9D9lt4.js +0 -1
|
@@ -0,0 +1,3997 @@
|
|
|
1
|
+
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (C) 2025 Alaa-eddine KADDOURI
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
(function (global, factory) {
|
|
18
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('echarts')) :
|
|
19
|
+
typeof define === 'function' && define.amd ? define(['exports', 'echarts'], factory) :
|
|
20
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.QFChart = {}, global.echarts));
|
|
21
|
+
})(this, (function (exports, echarts) { 'use strict';
|
|
22
|
+
|
|
23
|
+
function _interopNamespaceDefault(e) {
|
|
24
|
+
var n = Object.create(null);
|
|
25
|
+
if (e) {
|
|
26
|
+
Object.keys(e).forEach(function (k) {
|
|
27
|
+
if (k !== 'default') {
|
|
28
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
29
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
get: function () { return e[k]; }
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
n.default = e;
|
|
37
|
+
return Object.freeze(n);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
var echarts__namespace = /*#__PURE__*/_interopNamespaceDefault(echarts);
|
|
41
|
+
|
|
42
|
+
var __defProp$a = Object.defineProperty;
|
|
43
|
+
var __defNormalProp$a = (obj, key, value) => key in obj ? __defProp$a(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
44
|
+
var __publicField$a = (obj, key, value) => {
|
|
45
|
+
__defNormalProp$a(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
class Indicator {
|
|
49
|
+
constructor(id, plots, paneIndex, options = {}) {
|
|
50
|
+
__publicField$a(this, "id");
|
|
51
|
+
__publicField$a(this, "plots");
|
|
52
|
+
__publicField$a(this, "paneIndex");
|
|
53
|
+
__publicField$a(this, "height");
|
|
54
|
+
__publicField$a(this, "collapsed");
|
|
55
|
+
__publicField$a(this, "titleColor");
|
|
56
|
+
__publicField$a(this, "controls");
|
|
57
|
+
this.id = id;
|
|
58
|
+
this.plots = plots;
|
|
59
|
+
this.paneIndex = paneIndex;
|
|
60
|
+
this.height = options.height;
|
|
61
|
+
this.collapsed = options.collapsed || false;
|
|
62
|
+
this.titleColor = options.titleColor;
|
|
63
|
+
this.controls = options.controls;
|
|
64
|
+
}
|
|
65
|
+
toggleCollapse() {
|
|
66
|
+
this.collapsed = !this.collapsed;
|
|
67
|
+
}
|
|
68
|
+
isVisible() {
|
|
69
|
+
return !this.collapsed;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Update indicator data incrementally by merging new points
|
|
73
|
+
*
|
|
74
|
+
* @param plots - New plots data to merge (same structure as constructor)
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* This method merges new indicator data with existing data by timestamp.
|
|
78
|
+
* - New timestamps are added
|
|
79
|
+
* - Existing timestamps are updated with new values
|
|
80
|
+
* - All data is automatically sorted by time after merge
|
|
81
|
+
*
|
|
82
|
+
* **Important**: This method only updates the indicator's internal data structure.
|
|
83
|
+
* To see the changes reflected in the chart, you MUST call `chart.updateData()`
|
|
84
|
+
* after updating indicator data.
|
|
85
|
+
*
|
|
86
|
+
* **Usage Pattern**:
|
|
87
|
+
* ```typescript
|
|
88
|
+
* // 1. Update indicator data first
|
|
89
|
+
* indicator.updateData({
|
|
90
|
+
* macd: { data: [{ time: 1234567890, value: 150 }], options: { style: 'line', color: '#2962FF' } }
|
|
91
|
+
* });
|
|
92
|
+
*
|
|
93
|
+
* // 2. Then update chart data to trigger re-render
|
|
94
|
+
* chart.updateData([
|
|
95
|
+
* { time: 1234567890, open: 100, high: 105, low: 99, close: 103, volume: 1000 }
|
|
96
|
+
* ]);
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* **Note**: If you update indicator data without corresponding market data changes,
|
|
100
|
+
* this typically indicates a recalculation scenario. In normal workflows, indicator
|
|
101
|
+
* values are derived from market data, so indicator updates should correspond to
|
|
102
|
+
* new or modified market bars.
|
|
103
|
+
*/
|
|
104
|
+
updateData(plots) {
|
|
105
|
+
Object.keys(plots).forEach((plotName) => {
|
|
106
|
+
if (!this.plots[plotName]) {
|
|
107
|
+
this.plots[plotName] = plots[plotName];
|
|
108
|
+
} else {
|
|
109
|
+
const existingPlot = this.plots[plotName];
|
|
110
|
+
const newPlot = plots[plotName];
|
|
111
|
+
if (!existingPlot.data)
|
|
112
|
+
return;
|
|
113
|
+
if (newPlot.options) {
|
|
114
|
+
existingPlot.options = { ...existingPlot.options, ...newPlot.options };
|
|
115
|
+
}
|
|
116
|
+
const existingTimeMap = /* @__PURE__ */ new Map();
|
|
117
|
+
existingPlot.data?.forEach((point) => {
|
|
118
|
+
existingTimeMap.set(point.time, point);
|
|
119
|
+
});
|
|
120
|
+
newPlot.data?.forEach((point) => {
|
|
121
|
+
existingTimeMap.set(point.time, point);
|
|
122
|
+
});
|
|
123
|
+
existingPlot.data = Array.from(existingTimeMap.values()).sort((a, b) => a.time - b.time);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
class AxisUtils {
|
|
130
|
+
// Create min/max functions that apply padding
|
|
131
|
+
static createMinFunction(paddingPercent) {
|
|
132
|
+
return (value) => {
|
|
133
|
+
const range = value.max - value.min;
|
|
134
|
+
const padding = range * (paddingPercent / 100);
|
|
135
|
+
return value.min - padding;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
static createMaxFunction(paddingPercent) {
|
|
139
|
+
return (value) => {
|
|
140
|
+
const range = value.max - value.min;
|
|
141
|
+
const padding = range * (paddingPercent / 100);
|
|
142
|
+
return value.max + padding;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
class LayoutManager {
|
|
148
|
+
static calculate(containerHeight, indicators, options, isMainCollapsed = false, maximizedPaneId = null, marketData) {
|
|
149
|
+
let pixelToPercent = 0;
|
|
150
|
+
if (containerHeight > 0) {
|
|
151
|
+
pixelToPercent = 1 / containerHeight * 100;
|
|
152
|
+
}
|
|
153
|
+
const yAxisPaddingPercent = options.yAxisPadding !== void 0 ? options.yAxisPadding : 5;
|
|
154
|
+
const separatePaneIndices = Array.from(indicators.values()).map((ind) => ind.paneIndex).filter((idx) => idx > 0).sort((a, b) => a - b).filter((value, index, self) => self.indexOf(value) === index);
|
|
155
|
+
const hasSeparatePane = separatePaneIndices.length > 0;
|
|
156
|
+
const dzVisible = options.dataZoom?.visible ?? true;
|
|
157
|
+
const dzPosition = options.dataZoom?.position ?? "top";
|
|
158
|
+
const dzHeight = options.dataZoom?.height ?? 6;
|
|
159
|
+
const dzStart = options.dataZoom?.start ?? 0;
|
|
160
|
+
const dzEnd = options.dataZoom?.end ?? 100;
|
|
161
|
+
let mainPaneTop = 8;
|
|
162
|
+
let chartAreaBottom = 92;
|
|
163
|
+
let maximizeTargetIndex = -1;
|
|
164
|
+
if (maximizedPaneId) {
|
|
165
|
+
if (maximizedPaneId === "main") {
|
|
166
|
+
maximizeTargetIndex = 0;
|
|
167
|
+
} else {
|
|
168
|
+
const ind = indicators.get(maximizedPaneId);
|
|
169
|
+
if (ind) {
|
|
170
|
+
maximizeTargetIndex = ind.paneIndex;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (maximizeTargetIndex !== -1) {
|
|
175
|
+
const grid2 = [];
|
|
176
|
+
const xAxis2 = [];
|
|
177
|
+
const yAxis2 = [];
|
|
178
|
+
const dataZoom2 = [];
|
|
179
|
+
const dzStart2 = options.dataZoom?.start ?? 50;
|
|
180
|
+
const dzEnd2 = options.dataZoom?.end ?? 100;
|
|
181
|
+
const zoomOnTouch = options.dataZoom?.zoomOnTouch ?? true;
|
|
182
|
+
if (zoomOnTouch) {
|
|
183
|
+
dataZoom2.push({ type: "inside", xAxisIndex: "all", start: dzStart2, end: dzEnd2 });
|
|
184
|
+
}
|
|
185
|
+
const maxPaneIndex = hasSeparatePane ? Math.max(...separatePaneIndices) : 0;
|
|
186
|
+
const paneConfigs2 = [];
|
|
187
|
+
for (let i = 0; i <= maxPaneIndex; i++) {
|
|
188
|
+
const isTarget = i === maximizeTargetIndex;
|
|
189
|
+
grid2.push({
|
|
190
|
+
left: "10%",
|
|
191
|
+
right: "10%",
|
|
192
|
+
top: isTarget ? "5%" : "0%",
|
|
193
|
+
height: isTarget ? "90%" : "0%",
|
|
194
|
+
show: isTarget,
|
|
195
|
+
containLabel: false
|
|
196
|
+
});
|
|
197
|
+
xAxis2.push({
|
|
198
|
+
type: "category",
|
|
199
|
+
gridIndex: i,
|
|
200
|
+
data: [],
|
|
201
|
+
show: isTarget,
|
|
202
|
+
axisLabel: {
|
|
203
|
+
show: isTarget,
|
|
204
|
+
color: "#94a3b8",
|
|
205
|
+
fontFamily: options.fontFamily
|
|
206
|
+
},
|
|
207
|
+
axisLine: { show: isTarget, lineStyle: { color: "#334155" } },
|
|
208
|
+
splitLine: {
|
|
209
|
+
show: isTarget,
|
|
210
|
+
lineStyle: { color: "#334155", opacity: 0.5 }
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
let yMin;
|
|
214
|
+
let yMax;
|
|
215
|
+
if (i === 0 && maximizeTargetIndex === 0) {
|
|
216
|
+
yMin = options.yAxisMin !== void 0 && options.yAxisMin !== "auto" ? options.yAxisMin : AxisUtils.createMinFunction(yAxisPaddingPercent);
|
|
217
|
+
yMax = options.yAxisMax !== void 0 && options.yAxisMax !== "auto" ? options.yAxisMax : AxisUtils.createMaxFunction(yAxisPaddingPercent);
|
|
218
|
+
} else {
|
|
219
|
+
yMin = AxisUtils.createMinFunction(yAxisPaddingPercent);
|
|
220
|
+
yMax = AxisUtils.createMaxFunction(yAxisPaddingPercent);
|
|
221
|
+
}
|
|
222
|
+
yAxis2.push({
|
|
223
|
+
position: "right",
|
|
224
|
+
gridIndex: i,
|
|
225
|
+
show: isTarget,
|
|
226
|
+
scale: true,
|
|
227
|
+
min: yMin,
|
|
228
|
+
max: yMax,
|
|
229
|
+
axisLabel: {
|
|
230
|
+
show: isTarget,
|
|
231
|
+
color: "#94a3b8",
|
|
232
|
+
fontFamily: options.fontFamily,
|
|
233
|
+
formatter: (value) => {
|
|
234
|
+
if (options.yAxisLabelFormatter) {
|
|
235
|
+
return options.yAxisLabelFormatter(value);
|
|
236
|
+
}
|
|
237
|
+
const decimals = options.yAxisDecimalPlaces !== void 0 ? options.yAxisDecimalPlaces : 2;
|
|
238
|
+
if (typeof value === "number") {
|
|
239
|
+
return value.toFixed(decimals);
|
|
240
|
+
}
|
|
241
|
+
return String(value);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
splitLine: {
|
|
245
|
+
show: isTarget,
|
|
246
|
+
lineStyle: { color: "#334155", opacity: 0.5 }
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
if (i > 0) {
|
|
250
|
+
const ind = Array.from(indicators.values()).find((ind2) => ind2.paneIndex === i);
|
|
251
|
+
if (ind) {
|
|
252
|
+
paneConfigs2.push({
|
|
253
|
+
index: i,
|
|
254
|
+
height: isTarget ? 90 : 0,
|
|
255
|
+
top: isTarget ? 5 : 0,
|
|
256
|
+
isCollapsed: false,
|
|
257
|
+
indicatorId: ind.id,
|
|
258
|
+
titleColor: ind.titleColor,
|
|
259
|
+
controls: ind.controls
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
grid: grid2,
|
|
266
|
+
xAxis: xAxis2,
|
|
267
|
+
yAxis: yAxis2,
|
|
268
|
+
dataZoom: dataZoom2,
|
|
269
|
+
paneLayout: paneConfigs2,
|
|
270
|
+
mainPaneHeight: maximizeTargetIndex === 0 ? 90 : 0,
|
|
271
|
+
mainPaneTop: maximizeTargetIndex === 0 ? 5 : 0,
|
|
272
|
+
pixelToPercent,
|
|
273
|
+
overlayYAxisMap: /* @__PURE__ */ new Map(),
|
|
274
|
+
// No overlays in maximized view
|
|
275
|
+
separatePaneYAxisOffset: 1
|
|
276
|
+
// In maximized view, no overlays, so separate panes start at 1
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (dzVisible) {
|
|
280
|
+
if (dzPosition === "top") {
|
|
281
|
+
mainPaneTop = dzHeight + 4;
|
|
282
|
+
chartAreaBottom = 95;
|
|
283
|
+
} else {
|
|
284
|
+
chartAreaBottom = 100 - dzHeight - 2;
|
|
285
|
+
mainPaneTop = 8;
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
mainPaneTop = 5;
|
|
289
|
+
chartAreaBottom = 95;
|
|
290
|
+
}
|
|
291
|
+
let gapPercent = 5;
|
|
292
|
+
if (containerHeight > 0) {
|
|
293
|
+
gapPercent = 20 / containerHeight * 100;
|
|
294
|
+
}
|
|
295
|
+
let mainHeightVal = 75;
|
|
296
|
+
let paneConfigs = [];
|
|
297
|
+
if (hasSeparatePane) {
|
|
298
|
+
const panes = separatePaneIndices.map((idx) => {
|
|
299
|
+
const ind = Array.from(indicators.values()).find((i) => i.paneIndex === idx);
|
|
300
|
+
return {
|
|
301
|
+
index: idx,
|
|
302
|
+
requestedHeight: ind?.height,
|
|
303
|
+
isCollapsed: ind?.collapsed ?? false,
|
|
304
|
+
indicatorId: ind?.id,
|
|
305
|
+
titleColor: ind?.titleColor,
|
|
306
|
+
controls: ind?.controls
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
const resolvedPanes = panes.map((p) => ({
|
|
310
|
+
...p,
|
|
311
|
+
height: p.isCollapsed ? 3 : p.requestedHeight !== void 0 ? p.requestedHeight : 15
|
|
312
|
+
}));
|
|
313
|
+
const totalIndicatorHeight = resolvedPanes.reduce((sum, p) => sum + p.height, 0);
|
|
314
|
+
const totalGaps = resolvedPanes.length * gapPercent;
|
|
315
|
+
const totalBottomSpace = totalIndicatorHeight + totalGaps;
|
|
316
|
+
const totalAvailable = chartAreaBottom - mainPaneTop;
|
|
317
|
+
mainHeightVal = totalAvailable - totalBottomSpace;
|
|
318
|
+
if (isMainCollapsed) {
|
|
319
|
+
mainHeightVal = 3;
|
|
320
|
+
} else {
|
|
321
|
+
if (mainHeightVal < 20) {
|
|
322
|
+
mainHeightVal = Math.max(mainHeightVal, 10);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
let currentTop = mainPaneTop + mainHeightVal + gapPercent;
|
|
326
|
+
paneConfigs = resolvedPanes.map((p) => {
|
|
327
|
+
const config = {
|
|
328
|
+
index: p.index,
|
|
329
|
+
height: p.height,
|
|
330
|
+
top: currentTop,
|
|
331
|
+
isCollapsed: p.isCollapsed,
|
|
332
|
+
indicatorId: p.indicatorId,
|
|
333
|
+
titleColor: p.titleColor,
|
|
334
|
+
controls: p.controls
|
|
335
|
+
};
|
|
336
|
+
currentTop += p.height + gapPercent;
|
|
337
|
+
return config;
|
|
338
|
+
});
|
|
339
|
+
} else {
|
|
340
|
+
mainHeightVal = chartAreaBottom - mainPaneTop;
|
|
341
|
+
if (isMainCollapsed) {
|
|
342
|
+
mainHeightVal = 3;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const grid = [];
|
|
346
|
+
grid.push({
|
|
347
|
+
left: "10%",
|
|
348
|
+
right: "10%",
|
|
349
|
+
top: mainPaneTop + "%",
|
|
350
|
+
height: mainHeightVal + "%",
|
|
351
|
+
containLabel: false
|
|
352
|
+
// We handle margins explicitly
|
|
353
|
+
});
|
|
354
|
+
paneConfigs.forEach((pane) => {
|
|
355
|
+
grid.push({
|
|
356
|
+
left: "10%",
|
|
357
|
+
right: "10%",
|
|
358
|
+
top: pane.top + "%",
|
|
359
|
+
height: pane.height + "%",
|
|
360
|
+
containLabel: false
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
const allXAxisIndices = [0, ...paneConfigs.map((_, i) => i + 1)];
|
|
364
|
+
const xAxis = [];
|
|
365
|
+
const isMainBottom = paneConfigs.length === 0;
|
|
366
|
+
xAxis.push({
|
|
367
|
+
type: "category",
|
|
368
|
+
data: [],
|
|
369
|
+
// Will be filled by SeriesBuilder or QFChart
|
|
370
|
+
gridIndex: 0,
|
|
371
|
+
scale: true,
|
|
372
|
+
// boundaryGap will be set in QFChart.ts based on padding option
|
|
373
|
+
axisLine: {
|
|
374
|
+
onZero: false,
|
|
375
|
+
show: !isMainCollapsed,
|
|
376
|
+
lineStyle: { color: "#334155" }
|
|
377
|
+
},
|
|
378
|
+
splitLine: {
|
|
379
|
+
show: !isMainCollapsed,
|
|
380
|
+
lineStyle: { color: "#334155", opacity: 0.5 }
|
|
381
|
+
},
|
|
382
|
+
axisLabel: {
|
|
383
|
+
show: !isMainCollapsed,
|
|
384
|
+
color: "#94a3b8",
|
|
385
|
+
fontFamily: options.fontFamily || "sans-serif",
|
|
386
|
+
formatter: (value) => {
|
|
387
|
+
if (options.yAxisLabelFormatter) {
|
|
388
|
+
return options.yAxisLabelFormatter(value);
|
|
389
|
+
}
|
|
390
|
+
const decimals = options.yAxisDecimalPlaces !== void 0 ? options.yAxisDecimalPlaces : 2;
|
|
391
|
+
if (typeof value === "number") {
|
|
392
|
+
return value.toFixed(decimals);
|
|
393
|
+
}
|
|
394
|
+
return String(value);
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
axisTick: { show: !isMainCollapsed },
|
|
398
|
+
axisPointer: {
|
|
399
|
+
label: {
|
|
400
|
+
show: isMainBottom,
|
|
401
|
+
fontSize: 11,
|
|
402
|
+
backgroundColor: "#475569"
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
paneConfigs.forEach((pane, i) => {
|
|
407
|
+
const isBottom = i === paneConfigs.length - 1;
|
|
408
|
+
xAxis.push({
|
|
409
|
+
type: "category",
|
|
410
|
+
gridIndex: i + 1,
|
|
411
|
+
// 0 is main
|
|
412
|
+
data: [],
|
|
413
|
+
// Shared data
|
|
414
|
+
axisLabel: { show: false },
|
|
415
|
+
// Hide labels on indicator panes
|
|
416
|
+
axisLine: { show: !pane.isCollapsed, lineStyle: { color: "#334155" } },
|
|
417
|
+
axisTick: { show: false },
|
|
418
|
+
splitLine: { show: false },
|
|
419
|
+
axisPointer: {
|
|
420
|
+
label: {
|
|
421
|
+
show: isBottom,
|
|
422
|
+
fontSize: 11,
|
|
423
|
+
backgroundColor: "#475569"
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
const yAxis = [];
|
|
429
|
+
let mainYAxisMin;
|
|
430
|
+
let mainYAxisMax;
|
|
431
|
+
if (options.yAxisMin !== void 0 && options.yAxisMin !== "auto") {
|
|
432
|
+
mainYAxisMin = options.yAxisMin;
|
|
433
|
+
} else {
|
|
434
|
+
mainYAxisMin = AxisUtils.createMinFunction(yAxisPaddingPercent);
|
|
435
|
+
}
|
|
436
|
+
if (options.yAxisMax !== void 0 && options.yAxisMax !== "auto") {
|
|
437
|
+
mainYAxisMax = options.yAxisMax;
|
|
438
|
+
} else {
|
|
439
|
+
mainYAxisMax = AxisUtils.createMaxFunction(yAxisPaddingPercent);
|
|
440
|
+
}
|
|
441
|
+
yAxis.push({
|
|
442
|
+
position: "right",
|
|
443
|
+
scale: true,
|
|
444
|
+
min: mainYAxisMin,
|
|
445
|
+
max: mainYAxisMax,
|
|
446
|
+
gridIndex: 0,
|
|
447
|
+
splitLine: {
|
|
448
|
+
show: !isMainCollapsed,
|
|
449
|
+
lineStyle: { color: "#334155", opacity: 0.5 }
|
|
450
|
+
},
|
|
451
|
+
axisLine: { show: !isMainCollapsed, lineStyle: { color: "#334155" } },
|
|
452
|
+
axisLabel: {
|
|
453
|
+
show: !isMainCollapsed,
|
|
454
|
+
color: "#94a3b8",
|
|
455
|
+
fontFamily: options.fontFamily || "sans-serif",
|
|
456
|
+
formatter: (value) => {
|
|
457
|
+
if (options.yAxisLabelFormatter) {
|
|
458
|
+
return options.yAxisLabelFormatter(value);
|
|
459
|
+
}
|
|
460
|
+
const decimals = options.yAxisDecimalPlaces !== void 0 ? options.yAxisDecimalPlaces : 2;
|
|
461
|
+
if (typeof value === "number") {
|
|
462
|
+
return value.toFixed(decimals);
|
|
463
|
+
}
|
|
464
|
+
return String(value);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
let nextYAxisIndex = 1;
|
|
469
|
+
let priceMin = -Infinity;
|
|
470
|
+
let priceMax = Infinity;
|
|
471
|
+
if (marketData && marketData.length > 0) {
|
|
472
|
+
priceMin = Math.min(...marketData.map((d) => d.low));
|
|
473
|
+
priceMax = Math.max(...marketData.map((d) => d.high));
|
|
474
|
+
}
|
|
475
|
+
const overlayYAxisMap = /* @__PURE__ */ new Map();
|
|
476
|
+
indicators.forEach((indicator, id) => {
|
|
477
|
+
if (indicator.paneIndex === 0 && !indicator.collapsed) {
|
|
478
|
+
if (marketData && marketData.length > 0) {
|
|
479
|
+
Object.entries(indicator.plots).forEach(([plotName, plot]) => {
|
|
480
|
+
const plotKey = `${id}::${plotName}`;
|
|
481
|
+
const visualOnlyStyles = ["background", "barcolor", "char"];
|
|
482
|
+
const isShapeWithPriceLocation = plot.options.style === "shape" && (plot.options.location === "abovebar" || plot.options.location === "belowbar");
|
|
483
|
+
if (visualOnlyStyles.includes(plot.options.style)) {
|
|
484
|
+
if (!overlayYAxisMap.has(plotKey)) {
|
|
485
|
+
overlayYAxisMap.set(plotKey, nextYAxisIndex);
|
|
486
|
+
nextYAxisIndex++;
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (plot.options.style === "shape" && !isShapeWithPriceLocation) {
|
|
491
|
+
if (!overlayYAxisMap.has(plotKey)) {
|
|
492
|
+
overlayYAxisMap.set(plotKey, nextYAxisIndex);
|
|
493
|
+
nextYAxisIndex++;
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const values = [];
|
|
498
|
+
if (plot.data) {
|
|
499
|
+
Object.values(plot.data).forEach((value) => {
|
|
500
|
+
if (typeof value === "number" && !isNaN(value) && isFinite(value)) {
|
|
501
|
+
values.push(value);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
if (values.length > 0) {
|
|
506
|
+
const plotMin = Math.min(...values);
|
|
507
|
+
const plotMax = Math.max(...values);
|
|
508
|
+
const plotRange = plotMax - plotMin;
|
|
509
|
+
const priceRange = priceMax - priceMin;
|
|
510
|
+
const isWithinBounds = plotMin >= priceMin * 0.5 && plotMax <= priceMax * 1.5;
|
|
511
|
+
const hasSimilarMagnitude = plotRange > priceRange * 0.01;
|
|
512
|
+
const isCompatible = isWithinBounds && hasSimilarMagnitude;
|
|
513
|
+
if (!isCompatible) {
|
|
514
|
+
if (!overlayYAxisMap.has(plotKey)) {
|
|
515
|
+
overlayYAxisMap.set(plotKey, nextYAxisIndex);
|
|
516
|
+
nextYAxisIndex++;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
const numOverlayAxes = overlayYAxisMap.size > 0 ? nextYAxisIndex - 1 : 0;
|
|
525
|
+
for (let i = 0; i < numOverlayAxes; i++) {
|
|
526
|
+
yAxis.push({
|
|
527
|
+
position: "left",
|
|
528
|
+
scale: true,
|
|
529
|
+
min: AxisUtils.createMinFunction(yAxisPaddingPercent),
|
|
530
|
+
max: AxisUtils.createMaxFunction(yAxisPaddingPercent),
|
|
531
|
+
gridIndex: 0,
|
|
532
|
+
show: false,
|
|
533
|
+
// Hide the axis visual elements
|
|
534
|
+
splitLine: { show: false },
|
|
535
|
+
axisLine: { show: false },
|
|
536
|
+
axisLabel: { show: false }
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
const separatePaneYAxisOffset = nextYAxisIndex;
|
|
540
|
+
paneConfigs.forEach((pane, i) => {
|
|
541
|
+
yAxis.push({
|
|
542
|
+
position: "right",
|
|
543
|
+
scale: true,
|
|
544
|
+
min: AxisUtils.createMinFunction(yAxisPaddingPercent),
|
|
545
|
+
max: AxisUtils.createMaxFunction(yAxisPaddingPercent),
|
|
546
|
+
gridIndex: i + 1,
|
|
547
|
+
splitLine: {
|
|
548
|
+
show: !pane.isCollapsed,
|
|
549
|
+
lineStyle: { color: "#334155", opacity: 0.3 }
|
|
550
|
+
},
|
|
551
|
+
axisLabel: {
|
|
552
|
+
show: !pane.isCollapsed,
|
|
553
|
+
color: "#94a3b8",
|
|
554
|
+
fontFamily: options.fontFamily || "sans-serif",
|
|
555
|
+
fontSize: 10,
|
|
556
|
+
formatter: (value) => {
|
|
557
|
+
if (options.yAxisLabelFormatter) {
|
|
558
|
+
return options.yAxisLabelFormatter(value);
|
|
559
|
+
}
|
|
560
|
+
const decimals = options.yAxisDecimalPlaces !== void 0 ? options.yAxisDecimalPlaces : 2;
|
|
561
|
+
if (typeof value === "number") {
|
|
562
|
+
return value.toFixed(decimals);
|
|
563
|
+
}
|
|
564
|
+
return String(value);
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
axisLine: { show: !pane.isCollapsed, lineStyle: { color: "#334155" } }
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
const dataZoom = [];
|
|
571
|
+
if (dzVisible) {
|
|
572
|
+
const zoomOnTouch = options.dataZoom?.zoomOnTouch ?? true;
|
|
573
|
+
if (zoomOnTouch) {
|
|
574
|
+
dataZoom.push({
|
|
575
|
+
type: "inside",
|
|
576
|
+
xAxisIndex: allXAxisIndices,
|
|
577
|
+
start: dzStart,
|
|
578
|
+
end: dzEnd
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
if (dzPosition === "top") {
|
|
582
|
+
dataZoom.push({
|
|
583
|
+
type: "slider",
|
|
584
|
+
xAxisIndex: allXAxisIndices,
|
|
585
|
+
top: "1%",
|
|
586
|
+
height: dzHeight + "%",
|
|
587
|
+
start: dzStart,
|
|
588
|
+
end: dzEnd,
|
|
589
|
+
borderColor: "#334155",
|
|
590
|
+
textStyle: { color: "#cbd5e1" },
|
|
591
|
+
brushSelect: false
|
|
592
|
+
});
|
|
593
|
+
} else {
|
|
594
|
+
dataZoom.push({
|
|
595
|
+
type: "slider",
|
|
596
|
+
xAxisIndex: allXAxisIndices,
|
|
597
|
+
bottom: "1%",
|
|
598
|
+
height: dzHeight + "%",
|
|
599
|
+
start: dzStart,
|
|
600
|
+
end: dzEnd,
|
|
601
|
+
borderColor: "#334155",
|
|
602
|
+
textStyle: { color: "#cbd5e1" },
|
|
603
|
+
brushSelect: false
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
grid,
|
|
609
|
+
xAxis,
|
|
610
|
+
yAxis,
|
|
611
|
+
dataZoom,
|
|
612
|
+
paneLayout: paneConfigs,
|
|
613
|
+
mainPaneHeight: mainHeightVal,
|
|
614
|
+
mainPaneTop,
|
|
615
|
+
pixelToPercent,
|
|
616
|
+
overlayYAxisMap,
|
|
617
|
+
separatePaneYAxisOffset
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
static calculateMaximized(containerHeight, options, targetPaneIndex) {
|
|
621
|
+
return {
|
|
622
|
+
grid: [],
|
|
623
|
+
xAxis: [],
|
|
624
|
+
yAxis: [],
|
|
625
|
+
dataZoom: [],
|
|
626
|
+
paneLayout: [],
|
|
627
|
+
mainPaneHeight: 0,
|
|
628
|
+
mainPaneTop: 0,
|
|
629
|
+
pixelToPercent: 0
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
class LineRenderer {
|
|
635
|
+
render(context) {
|
|
636
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, plotOptions } = context;
|
|
637
|
+
const defaultColor = "#2962ff";
|
|
638
|
+
return {
|
|
639
|
+
name: seriesName,
|
|
640
|
+
type: "custom",
|
|
641
|
+
xAxisIndex,
|
|
642
|
+
yAxisIndex,
|
|
643
|
+
renderItem: (params, api) => {
|
|
644
|
+
const index = params.dataIndex;
|
|
645
|
+
if (index === 0)
|
|
646
|
+
return;
|
|
647
|
+
const y2 = api.value(1);
|
|
648
|
+
const y1 = api.value(2);
|
|
649
|
+
if (y2 === null || isNaN(y2) || y1 === null || isNaN(y1))
|
|
650
|
+
return;
|
|
651
|
+
const p1 = api.coord([index - 1, y1]);
|
|
652
|
+
const p2 = api.coord([index, y2]);
|
|
653
|
+
return {
|
|
654
|
+
type: "line",
|
|
655
|
+
shape: {
|
|
656
|
+
x1: p1[0],
|
|
657
|
+
y1: p1[1],
|
|
658
|
+
x2: p2[0],
|
|
659
|
+
y2: p2[1]
|
|
660
|
+
},
|
|
661
|
+
style: {
|
|
662
|
+
stroke: colorArray[index] || plotOptions.color || defaultColor,
|
|
663
|
+
lineWidth: plotOptions.linewidth || 1
|
|
664
|
+
},
|
|
665
|
+
silent: true
|
|
666
|
+
};
|
|
667
|
+
},
|
|
668
|
+
// Data format: [index, value, prevValue]
|
|
669
|
+
data: dataArray.map((val, i) => [i, val, i > 0 ? dataArray[i - 1] : null])
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
class StepRenderer {
|
|
675
|
+
render(context) {
|
|
676
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, plotOptions } = context;
|
|
677
|
+
const defaultColor = "#2962ff";
|
|
678
|
+
return {
|
|
679
|
+
name: seriesName,
|
|
680
|
+
type: "custom",
|
|
681
|
+
xAxisIndex,
|
|
682
|
+
yAxisIndex,
|
|
683
|
+
renderItem: (params, api) => {
|
|
684
|
+
const x = api.value(0);
|
|
685
|
+
const y = api.value(1);
|
|
686
|
+
if (isNaN(y) || y === null)
|
|
687
|
+
return;
|
|
688
|
+
const coords = api.coord([x, y]);
|
|
689
|
+
const width = api.size([1, 0])[0];
|
|
690
|
+
return {
|
|
691
|
+
type: "line",
|
|
692
|
+
shape: {
|
|
693
|
+
x1: coords[0] - width / 2,
|
|
694
|
+
y1: coords[1],
|
|
695
|
+
x2: coords[0] + width / 2,
|
|
696
|
+
y2: coords[1]
|
|
697
|
+
},
|
|
698
|
+
style: {
|
|
699
|
+
stroke: colorArray[params.dataIndex] || plotOptions.color || defaultColor,
|
|
700
|
+
lineWidth: plotOptions.linewidth || 1
|
|
701
|
+
},
|
|
702
|
+
silent: true
|
|
703
|
+
};
|
|
704
|
+
},
|
|
705
|
+
data: dataArray.map((val, i) => [i, val])
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
class HistogramRenderer {
|
|
711
|
+
render(context) {
|
|
712
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, plotOptions } = context;
|
|
713
|
+
const defaultColor = "#2962ff";
|
|
714
|
+
return {
|
|
715
|
+
name: seriesName,
|
|
716
|
+
type: "bar",
|
|
717
|
+
xAxisIndex,
|
|
718
|
+
yAxisIndex,
|
|
719
|
+
data: dataArray.map((val, i) => ({
|
|
720
|
+
value: val,
|
|
721
|
+
itemStyle: colorArray[i] ? { color: colorArray[i] } : void 0
|
|
722
|
+
})),
|
|
723
|
+
itemStyle: { color: plotOptions.color || defaultColor }
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const imageCache = /* @__PURE__ */ new Map();
|
|
729
|
+
function textToBase64Image(text, color = "#00da3c", fontSize = "64px") {
|
|
730
|
+
if (typeof document === "undefined")
|
|
731
|
+
return "";
|
|
732
|
+
const cacheKey = `${text}-${color}-${fontSize}`;
|
|
733
|
+
if (imageCache.has(cacheKey)) {
|
|
734
|
+
return imageCache.get(cacheKey);
|
|
735
|
+
}
|
|
736
|
+
const canvas = document.createElement("canvas");
|
|
737
|
+
const ctx = canvas.getContext("2d");
|
|
738
|
+
canvas.width = 32;
|
|
739
|
+
canvas.height = 32;
|
|
740
|
+
if (ctx) {
|
|
741
|
+
ctx.font = "bold " + fontSize + " Arial";
|
|
742
|
+
ctx.fillStyle = color;
|
|
743
|
+
ctx.textAlign = "center";
|
|
744
|
+
ctx.textBaseline = "middle";
|
|
745
|
+
ctx.fillText(text, 16, 16);
|
|
746
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
747
|
+
imageCache.set(cacheKey, dataUrl);
|
|
748
|
+
return dataUrl;
|
|
749
|
+
}
|
|
750
|
+
return "";
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
class ScatterRenderer {
|
|
754
|
+
render(context) {
|
|
755
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, plotOptions } = context;
|
|
756
|
+
const defaultColor = "#2962ff";
|
|
757
|
+
const style = plotOptions.style;
|
|
758
|
+
if (style === "char") {
|
|
759
|
+
return {
|
|
760
|
+
name: seriesName,
|
|
761
|
+
type: "scatter",
|
|
762
|
+
xAxisIndex,
|
|
763
|
+
yAxisIndex,
|
|
764
|
+
symbolSize: 0,
|
|
765
|
+
// Invisible
|
|
766
|
+
data: dataArray.map((val, i) => ({
|
|
767
|
+
value: [i, val],
|
|
768
|
+
itemStyle: { opacity: 0 }
|
|
769
|
+
})),
|
|
770
|
+
silent: true
|
|
771
|
+
// No interaction
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const scatterData = dataArray.map((val, i) => {
|
|
775
|
+
if (val === null)
|
|
776
|
+
return null;
|
|
777
|
+
const pointColor = colorArray[i] || plotOptions.color || defaultColor;
|
|
778
|
+
const item = {
|
|
779
|
+
value: [i, val],
|
|
780
|
+
itemStyle: { color: pointColor }
|
|
781
|
+
};
|
|
782
|
+
if (style === "cross") {
|
|
783
|
+
item.symbol = `image://${textToBase64Image("+", pointColor, "24px")}`;
|
|
784
|
+
item.symbolSize = 16;
|
|
785
|
+
} else {
|
|
786
|
+
item.symbol = "circle";
|
|
787
|
+
item.symbolSize = 6;
|
|
788
|
+
}
|
|
789
|
+
return item;
|
|
790
|
+
}).filter((item) => item !== null);
|
|
791
|
+
return {
|
|
792
|
+
name: seriesName,
|
|
793
|
+
type: "scatter",
|
|
794
|
+
xAxisIndex,
|
|
795
|
+
yAxisIndex,
|
|
796
|
+
data: scatterData
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
class OHLCBarRenderer {
|
|
802
|
+
render(context) {
|
|
803
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, optionsArray, plotOptions } = context;
|
|
804
|
+
const defaultColor = "#2962ff";
|
|
805
|
+
const isCandle = plotOptions.style === "candle";
|
|
806
|
+
const ohlcData = dataArray.map((val, i) => {
|
|
807
|
+
if (val === null || !Array.isArray(val) || val.length !== 4)
|
|
808
|
+
return null;
|
|
809
|
+
const [open, high, low, close] = val;
|
|
810
|
+
const pointOpts = optionsArray[i] || {};
|
|
811
|
+
const color = pointOpts.color || colorArray[i] || plotOptions.color || defaultColor;
|
|
812
|
+
const wickColor = pointOpts.wickcolor || plotOptions.wickcolor || color;
|
|
813
|
+
const borderColor = pointOpts.bordercolor || plotOptions.bordercolor || wickColor;
|
|
814
|
+
return [i, open, close, low, high, color, wickColor, borderColor];
|
|
815
|
+
}).filter((item) => item !== null);
|
|
816
|
+
return {
|
|
817
|
+
name: seriesName,
|
|
818
|
+
type: "custom",
|
|
819
|
+
xAxisIndex,
|
|
820
|
+
yAxisIndex,
|
|
821
|
+
renderItem: (params, api) => {
|
|
822
|
+
const xValue = api.value(0);
|
|
823
|
+
const openValue = api.value(1);
|
|
824
|
+
const closeValue = api.value(2);
|
|
825
|
+
const lowValue = api.value(3);
|
|
826
|
+
const highValue = api.value(4);
|
|
827
|
+
const color = api.value(5);
|
|
828
|
+
const wickColor = api.value(6);
|
|
829
|
+
const borderColor = api.value(7);
|
|
830
|
+
if (isNaN(openValue) || isNaN(closeValue) || isNaN(lowValue) || isNaN(highValue)) {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
const xPos = api.coord([xValue, 0])[0];
|
|
834
|
+
const openPos = api.coord([xValue, openValue])[1];
|
|
835
|
+
const closePos = api.coord([xValue, closeValue])[1];
|
|
836
|
+
const lowPos = api.coord([xValue, lowValue])[1];
|
|
837
|
+
const highPos = api.coord([xValue, highValue])[1];
|
|
838
|
+
const barWidth = api.size([1, 0])[0] * 0.6;
|
|
839
|
+
if (isCandle) {
|
|
840
|
+
const bodyTop = Math.min(openPos, closePos);
|
|
841
|
+
const bodyBottom = Math.max(openPos, closePos);
|
|
842
|
+
const bodyHeight = Math.abs(closePos - openPos);
|
|
843
|
+
return {
|
|
844
|
+
type: "group",
|
|
845
|
+
children: [
|
|
846
|
+
// Upper wick
|
|
847
|
+
{
|
|
848
|
+
type: "line",
|
|
849
|
+
shape: {
|
|
850
|
+
x1: xPos,
|
|
851
|
+
y1: highPos,
|
|
852
|
+
x2: xPos,
|
|
853
|
+
y2: bodyTop
|
|
854
|
+
},
|
|
855
|
+
style: {
|
|
856
|
+
stroke: wickColor,
|
|
857
|
+
lineWidth: 1
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
// Lower wick
|
|
861
|
+
{
|
|
862
|
+
type: "line",
|
|
863
|
+
shape: {
|
|
864
|
+
x1: xPos,
|
|
865
|
+
y1: bodyBottom,
|
|
866
|
+
x2: xPos,
|
|
867
|
+
y2: lowPos
|
|
868
|
+
},
|
|
869
|
+
style: {
|
|
870
|
+
stroke: wickColor,
|
|
871
|
+
lineWidth: 1
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
// Body
|
|
875
|
+
{
|
|
876
|
+
type: "rect",
|
|
877
|
+
shape: {
|
|
878
|
+
x: xPos - barWidth / 2,
|
|
879
|
+
y: bodyTop,
|
|
880
|
+
width: barWidth,
|
|
881
|
+
height: bodyHeight || 1
|
|
882
|
+
// Minimum height for doji
|
|
883
|
+
},
|
|
884
|
+
style: {
|
|
885
|
+
fill: color,
|
|
886
|
+
stroke: borderColor,
|
|
887
|
+
lineWidth: 1
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
]
|
|
891
|
+
};
|
|
892
|
+
} else {
|
|
893
|
+
const tickWidth = barWidth * 0.5;
|
|
894
|
+
return {
|
|
895
|
+
type: "group",
|
|
896
|
+
children: [
|
|
897
|
+
// Vertical line (low to high)
|
|
898
|
+
{
|
|
899
|
+
type: "line",
|
|
900
|
+
shape: {
|
|
901
|
+
x1: xPos,
|
|
902
|
+
y1: lowPos,
|
|
903
|
+
x2: xPos,
|
|
904
|
+
y2: highPos
|
|
905
|
+
},
|
|
906
|
+
style: {
|
|
907
|
+
stroke: color,
|
|
908
|
+
lineWidth: 1
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
// Open tick (left)
|
|
912
|
+
{
|
|
913
|
+
type: "line",
|
|
914
|
+
shape: {
|
|
915
|
+
x1: xPos - tickWidth,
|
|
916
|
+
y1: openPos,
|
|
917
|
+
x2: xPos,
|
|
918
|
+
y2: openPos
|
|
919
|
+
},
|
|
920
|
+
style: {
|
|
921
|
+
stroke: color,
|
|
922
|
+
lineWidth: 1
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
// Close tick (right)
|
|
926
|
+
{
|
|
927
|
+
type: "line",
|
|
928
|
+
shape: {
|
|
929
|
+
x1: xPos,
|
|
930
|
+
y1: closePos,
|
|
931
|
+
x2: xPos + tickWidth,
|
|
932
|
+
y2: closePos
|
|
933
|
+
},
|
|
934
|
+
style: {
|
|
935
|
+
stroke: color,
|
|
936
|
+
lineWidth: 1
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
]
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
data: ohlcData
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
class ShapeUtils {
|
|
949
|
+
static getShapeSymbol(shape) {
|
|
950
|
+
switch (shape) {
|
|
951
|
+
case "arrowdown":
|
|
952
|
+
return "path://M12 24l-12-12h8v-12h8v12h8z";
|
|
953
|
+
case "arrowup":
|
|
954
|
+
return "path://M12 0l12 12h-8v12h-8v-12h-8z";
|
|
955
|
+
case "circle":
|
|
956
|
+
return "circle";
|
|
957
|
+
case "cross":
|
|
958
|
+
return "path://M11 2h2v9h9v2h-9v9h-2v-9h-9v-2h9z";
|
|
959
|
+
case "diamond":
|
|
960
|
+
return "diamond";
|
|
961
|
+
case "flag":
|
|
962
|
+
return "path://M6 2v20h2v-8h12l-2-6 2-6h-12z";
|
|
963
|
+
case "labeldown":
|
|
964
|
+
return "path://M4 2h16a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-6l-2 4l-2 -4h-6a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2z";
|
|
965
|
+
case "labelup":
|
|
966
|
+
return "path://M12 2l2 4h6a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-16a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h6z";
|
|
967
|
+
case "square":
|
|
968
|
+
return "rect";
|
|
969
|
+
case "triangledown":
|
|
970
|
+
return "path://M12 21l-10-18h20z";
|
|
971
|
+
case "triangleup":
|
|
972
|
+
return "triangle";
|
|
973
|
+
case "xcross":
|
|
974
|
+
return "path://M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z";
|
|
975
|
+
default:
|
|
976
|
+
return "circle";
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
static getShapeRotation(shape) {
|
|
980
|
+
return 0;
|
|
981
|
+
}
|
|
982
|
+
static getShapeSize(size, width, height) {
|
|
983
|
+
if (width !== void 0 && height !== void 0) {
|
|
984
|
+
return [width, height];
|
|
985
|
+
}
|
|
986
|
+
let baseSize;
|
|
987
|
+
switch (size) {
|
|
988
|
+
case "tiny":
|
|
989
|
+
baseSize = 8;
|
|
990
|
+
break;
|
|
991
|
+
case "small":
|
|
992
|
+
baseSize = 12;
|
|
993
|
+
break;
|
|
994
|
+
case "normal":
|
|
995
|
+
case "auto":
|
|
996
|
+
baseSize = 16;
|
|
997
|
+
break;
|
|
998
|
+
case "large":
|
|
999
|
+
baseSize = 24;
|
|
1000
|
+
break;
|
|
1001
|
+
case "huge":
|
|
1002
|
+
baseSize = 32;
|
|
1003
|
+
break;
|
|
1004
|
+
default:
|
|
1005
|
+
baseSize = 16;
|
|
1006
|
+
}
|
|
1007
|
+
if (width !== void 0) {
|
|
1008
|
+
return [width, width];
|
|
1009
|
+
}
|
|
1010
|
+
if (height !== void 0) {
|
|
1011
|
+
return [height, height];
|
|
1012
|
+
}
|
|
1013
|
+
return baseSize;
|
|
1014
|
+
}
|
|
1015
|
+
// Helper to determine label position and distance relative to shape BASED ON LOCATION
|
|
1016
|
+
static getLabelConfig(shape, location) {
|
|
1017
|
+
switch (location) {
|
|
1018
|
+
case "abovebar":
|
|
1019
|
+
return { position: "top", distance: 5 };
|
|
1020
|
+
case "belowbar":
|
|
1021
|
+
return { position: "bottom", distance: 5 };
|
|
1022
|
+
case "top":
|
|
1023
|
+
return { position: "bottom", distance: 5 };
|
|
1024
|
+
case "bottom":
|
|
1025
|
+
return { position: "top", distance: 5 };
|
|
1026
|
+
case "absolute":
|
|
1027
|
+
default:
|
|
1028
|
+
if (shape === "labelup" || shape === "labeldown") {
|
|
1029
|
+
return { position: "inside", distance: 0 };
|
|
1030
|
+
}
|
|
1031
|
+
return { position: "top", distance: 5 };
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
class ShapeRenderer {
|
|
1037
|
+
render(context) {
|
|
1038
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, optionsArray, plotOptions, candlestickData } = context;
|
|
1039
|
+
const defaultColor = "#2962ff";
|
|
1040
|
+
const shapeData = dataArray.map((val, i) => {
|
|
1041
|
+
const pointOpts = optionsArray[i] || {};
|
|
1042
|
+
const globalOpts = plotOptions;
|
|
1043
|
+
const location = pointOpts.location || globalOpts.location || "absolute";
|
|
1044
|
+
if (location !== "absolute" && !val) {
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
if (val === null || val === void 0) {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
const color = pointOpts.color || globalOpts.color || defaultColor;
|
|
1051
|
+
const shape = pointOpts.shape || globalOpts.shape || "circle";
|
|
1052
|
+
const size = pointOpts.size || globalOpts.size || "normal";
|
|
1053
|
+
const text = pointOpts.text || globalOpts.text;
|
|
1054
|
+
const textColor = pointOpts.textcolor || globalOpts.textcolor || "white";
|
|
1055
|
+
const width = pointOpts.width || globalOpts.width;
|
|
1056
|
+
const height = pointOpts.height || globalOpts.height;
|
|
1057
|
+
let yValue = val;
|
|
1058
|
+
let symbolOffset = [0, 0];
|
|
1059
|
+
if (location === "abovebar") {
|
|
1060
|
+
if (candlestickData && candlestickData[i]) {
|
|
1061
|
+
yValue = candlestickData[i].high;
|
|
1062
|
+
}
|
|
1063
|
+
symbolOffset = [0, "-150%"];
|
|
1064
|
+
} else if (location === "belowbar") {
|
|
1065
|
+
if (candlestickData && candlestickData[i]) {
|
|
1066
|
+
yValue = candlestickData[i].low;
|
|
1067
|
+
}
|
|
1068
|
+
symbolOffset = [0, "150%"];
|
|
1069
|
+
} else if (location === "top") {
|
|
1070
|
+
yValue = val;
|
|
1071
|
+
symbolOffset = [0, 0];
|
|
1072
|
+
} else if (location === "bottom") {
|
|
1073
|
+
yValue = val;
|
|
1074
|
+
symbolOffset = [0, 0];
|
|
1075
|
+
}
|
|
1076
|
+
const symbol = ShapeUtils.getShapeSymbol(shape);
|
|
1077
|
+
const symbolSize = ShapeUtils.getShapeSize(size, width, height);
|
|
1078
|
+
const rotate = ShapeUtils.getShapeRotation(shape);
|
|
1079
|
+
let finalSize = symbolSize;
|
|
1080
|
+
if (shape.includes("label")) {
|
|
1081
|
+
if (Array.isArray(symbolSize)) {
|
|
1082
|
+
finalSize = [symbolSize[0] * 2.5, symbolSize[1] * 2.5];
|
|
1083
|
+
} else {
|
|
1084
|
+
finalSize = symbolSize * 2.5;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const labelConfig = ShapeUtils.getLabelConfig(shape, location);
|
|
1088
|
+
const item = {
|
|
1089
|
+
value: [i, yValue],
|
|
1090
|
+
symbol,
|
|
1091
|
+
symbolSize: finalSize,
|
|
1092
|
+
symbolRotate: rotate,
|
|
1093
|
+
symbolOffset,
|
|
1094
|
+
itemStyle: {
|
|
1095
|
+
color
|
|
1096
|
+
},
|
|
1097
|
+
label: {
|
|
1098
|
+
show: !!text,
|
|
1099
|
+
position: labelConfig.position,
|
|
1100
|
+
distance: labelConfig.distance,
|
|
1101
|
+
formatter: text,
|
|
1102
|
+
color: textColor,
|
|
1103
|
+
fontSize: 10,
|
|
1104
|
+
fontWeight: "bold"
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
return item;
|
|
1108
|
+
}).filter((item) => item !== null);
|
|
1109
|
+
return {
|
|
1110
|
+
name: seriesName,
|
|
1111
|
+
type: "scatter",
|
|
1112
|
+
xAxisIndex,
|
|
1113
|
+
yAxisIndex,
|
|
1114
|
+
data: shapeData
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
class BackgroundRenderer {
|
|
1120
|
+
render(context) {
|
|
1121
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray } = context;
|
|
1122
|
+
return {
|
|
1123
|
+
name: seriesName,
|
|
1124
|
+
type: "custom",
|
|
1125
|
+
xAxisIndex,
|
|
1126
|
+
yAxisIndex,
|
|
1127
|
+
z: -10,
|
|
1128
|
+
renderItem: (params, api) => {
|
|
1129
|
+
const xVal = api.value(0);
|
|
1130
|
+
if (isNaN(xVal))
|
|
1131
|
+
return;
|
|
1132
|
+
const start = api.coord([xVal, 0]);
|
|
1133
|
+
const size = api.size([1, 0]);
|
|
1134
|
+
const width = size[0];
|
|
1135
|
+
const sys = params.coordSys;
|
|
1136
|
+
const x = start[0] - width / 2;
|
|
1137
|
+
const barColor = colorArray[params.dataIndex];
|
|
1138
|
+
const val = api.value(1);
|
|
1139
|
+
if (!barColor || val === null || val === void 0 || isNaN(val))
|
|
1140
|
+
return;
|
|
1141
|
+
return {
|
|
1142
|
+
type: "rect",
|
|
1143
|
+
shape: {
|
|
1144
|
+
x,
|
|
1145
|
+
y: sys.y,
|
|
1146
|
+
width,
|
|
1147
|
+
height: sys.height
|
|
1148
|
+
},
|
|
1149
|
+
style: {
|
|
1150
|
+
fill: barColor,
|
|
1151
|
+
opacity: 0.3
|
|
1152
|
+
},
|
|
1153
|
+
silent: true
|
|
1154
|
+
};
|
|
1155
|
+
},
|
|
1156
|
+
data: dataArray.map((val, i) => [i, val])
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
class ColorUtils {
|
|
1162
|
+
/**
|
|
1163
|
+
* Parse color string and extract opacity
|
|
1164
|
+
* Supports: hex (#RRGGBB), named colors (green, red), rgba(r,g,b,a), rgb(r,g,b)
|
|
1165
|
+
*/
|
|
1166
|
+
static parseColor(colorStr) {
|
|
1167
|
+
if (!colorStr) {
|
|
1168
|
+
return { color: "#888888", opacity: 0.2 };
|
|
1169
|
+
}
|
|
1170
|
+
const rgbaMatch = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
1171
|
+
if (rgbaMatch) {
|
|
1172
|
+
const r = rgbaMatch[1];
|
|
1173
|
+
const g = rgbaMatch[2];
|
|
1174
|
+
const b = rgbaMatch[3];
|
|
1175
|
+
const a = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1;
|
|
1176
|
+
return {
|
|
1177
|
+
color: `rgb(${r},${g},${b})`,
|
|
1178
|
+
opacity: a
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
color: colorStr,
|
|
1183
|
+
opacity: 0.3
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
class FillRenderer {
|
|
1189
|
+
render(context) {
|
|
1190
|
+
const { seriesName, xAxisIndex, yAxisIndex, plotOptions, plotDataArrays, indicatorId, plotName } = context;
|
|
1191
|
+
const totalDataLength = context.dataArray.length;
|
|
1192
|
+
const plot1Key = plotOptions.plot1 ? `${indicatorId}::${plotOptions.plot1}` : null;
|
|
1193
|
+
const plot2Key = plotOptions.plot2 ? `${indicatorId}::${plotOptions.plot2}` : null;
|
|
1194
|
+
if (!plot1Key || !plot2Key) {
|
|
1195
|
+
console.warn(`Fill plot "${plotName}" missing plot1 or plot2 reference`);
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
const plot1Data = plotDataArrays?.get(plot1Key);
|
|
1199
|
+
const plot2Data = plotDataArrays?.get(plot2Key);
|
|
1200
|
+
if (!plot1Data || !plot2Data) {
|
|
1201
|
+
console.warn(`Fill plot "${plotName}" references non-existent plots: ${plotOptions.plot1}, ${plotOptions.plot2}`);
|
|
1202
|
+
return null;
|
|
1203
|
+
}
|
|
1204
|
+
const { color: fillColor, opacity: fillOpacity } = ColorUtils.parseColor(plotOptions.color || "rgba(128, 128, 128, 0.2)");
|
|
1205
|
+
const fillDataWithPrev = [];
|
|
1206
|
+
for (let i = 0; i < totalDataLength; i++) {
|
|
1207
|
+
const y1 = plot1Data[i];
|
|
1208
|
+
const y2 = plot2Data[i];
|
|
1209
|
+
const prevY1 = i > 0 ? plot1Data[i - 1] : null;
|
|
1210
|
+
const prevY2 = i > 0 ? plot2Data[i - 1] : null;
|
|
1211
|
+
fillDataWithPrev.push([i, y1, y2, prevY1, prevY2]);
|
|
1212
|
+
}
|
|
1213
|
+
return {
|
|
1214
|
+
name: seriesName,
|
|
1215
|
+
type: "custom",
|
|
1216
|
+
xAxisIndex,
|
|
1217
|
+
yAxisIndex,
|
|
1218
|
+
z: -5,
|
|
1219
|
+
// Render behind lines but above background
|
|
1220
|
+
renderItem: (params, api) => {
|
|
1221
|
+
const index = params.dataIndex;
|
|
1222
|
+
if (index === 0)
|
|
1223
|
+
return null;
|
|
1224
|
+
const y1 = api.value(1);
|
|
1225
|
+
const y2 = api.value(2);
|
|
1226
|
+
const prevY1 = api.value(3);
|
|
1227
|
+
const prevY2 = api.value(4);
|
|
1228
|
+
if (y1 === null || y2 === null || prevY1 === null || prevY2 === null || isNaN(y1) || isNaN(y2) || isNaN(prevY1) || isNaN(prevY2)) {
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
const p1Prev = api.coord([index - 1, prevY1]);
|
|
1232
|
+
const p1Curr = api.coord([index, y1]);
|
|
1233
|
+
const p2Curr = api.coord([index, y2]);
|
|
1234
|
+
const p2Prev = api.coord([index - 1, prevY2]);
|
|
1235
|
+
return {
|
|
1236
|
+
type: "polygon",
|
|
1237
|
+
shape: {
|
|
1238
|
+
points: [
|
|
1239
|
+
p1Prev,
|
|
1240
|
+
// Top-left
|
|
1241
|
+
p1Curr,
|
|
1242
|
+
// Top-right
|
|
1243
|
+
p2Curr,
|
|
1244
|
+
// Bottom-right
|
|
1245
|
+
p2Prev
|
|
1246
|
+
// Bottom-left
|
|
1247
|
+
]
|
|
1248
|
+
},
|
|
1249
|
+
style: {
|
|
1250
|
+
fill: fillColor,
|
|
1251
|
+
opacity: fillOpacity
|
|
1252
|
+
},
|
|
1253
|
+
silent: true
|
|
1254
|
+
};
|
|
1255
|
+
},
|
|
1256
|
+
data: fillDataWithPrev
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
var __defProp$9 = Object.defineProperty;
|
|
1262
|
+
var __defNormalProp$9 = (obj, key, value) => key in obj ? __defProp$9(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1263
|
+
var __publicField$9 = (obj, key, value) => {
|
|
1264
|
+
__defNormalProp$9(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1265
|
+
return value;
|
|
1266
|
+
};
|
|
1267
|
+
const _SeriesRendererFactory = class _SeriesRendererFactory {
|
|
1268
|
+
static register(style, renderer) {
|
|
1269
|
+
this.renderers.set(style, renderer);
|
|
1270
|
+
}
|
|
1271
|
+
static get(style) {
|
|
1272
|
+
return this.renderers.get(style) || this.renderers.get("line");
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
__publicField$9(_SeriesRendererFactory, "renderers", /* @__PURE__ */ new Map());
|
|
1276
|
+
_SeriesRendererFactory.register("line", new LineRenderer());
|
|
1277
|
+
_SeriesRendererFactory.register("step", new StepRenderer());
|
|
1278
|
+
_SeriesRendererFactory.register("histogram", new HistogramRenderer());
|
|
1279
|
+
_SeriesRendererFactory.register("columns", new HistogramRenderer());
|
|
1280
|
+
_SeriesRendererFactory.register("circles", new ScatterRenderer());
|
|
1281
|
+
_SeriesRendererFactory.register("cross", new ScatterRenderer());
|
|
1282
|
+
_SeriesRendererFactory.register("char", new ScatterRenderer());
|
|
1283
|
+
_SeriesRendererFactory.register("bar", new OHLCBarRenderer());
|
|
1284
|
+
_SeriesRendererFactory.register("candle", new OHLCBarRenderer());
|
|
1285
|
+
_SeriesRendererFactory.register("shape", new ShapeRenderer());
|
|
1286
|
+
_SeriesRendererFactory.register("background", new BackgroundRenderer());
|
|
1287
|
+
_SeriesRendererFactory.register("fill", new FillRenderer());
|
|
1288
|
+
let SeriesRendererFactory = _SeriesRendererFactory;
|
|
1289
|
+
|
|
1290
|
+
var __defProp$8 = Object.defineProperty;
|
|
1291
|
+
var __defNormalProp$8 = (obj, key, value) => key in obj ? __defProp$8(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1292
|
+
var __publicField$8 = (obj, key, value) => {
|
|
1293
|
+
__defNormalProp$8(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1294
|
+
return value;
|
|
1295
|
+
};
|
|
1296
|
+
const _SeriesBuilder = class _SeriesBuilder {
|
|
1297
|
+
static buildCandlestickSeries(marketData, options, totalLength) {
|
|
1298
|
+
const upColor = options.upColor || "#00da3c";
|
|
1299
|
+
const downColor = options.downColor || "#ec0000";
|
|
1300
|
+
const data = marketData.map((d) => [d.open, d.close, d.low, d.high]);
|
|
1301
|
+
if (totalLength && totalLength > data.length) {
|
|
1302
|
+
const padding = totalLength - data.length;
|
|
1303
|
+
for (let i = 0; i < padding; i++) {
|
|
1304
|
+
data.push(null);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
let markLine = void 0;
|
|
1308
|
+
if (options.lastPriceLine?.visible !== false && marketData.length > 0) {
|
|
1309
|
+
const lastBar = marketData[marketData.length - 1];
|
|
1310
|
+
const lastClose = lastBar.close;
|
|
1311
|
+
const isUp = lastBar.close >= lastBar.open;
|
|
1312
|
+
const lineColor = options.lastPriceLine?.color || (isUp ? upColor : downColor);
|
|
1313
|
+
let lineStyleType = options.lastPriceLine?.lineStyle || "dashed";
|
|
1314
|
+
if (lineStyleType.startsWith("linestyle_")) {
|
|
1315
|
+
lineStyleType = lineStyleType.replace("linestyle_", "");
|
|
1316
|
+
}
|
|
1317
|
+
markLine = {
|
|
1318
|
+
symbol: ["none", "none"],
|
|
1319
|
+
data: [
|
|
1320
|
+
{
|
|
1321
|
+
yAxis: lastClose,
|
|
1322
|
+
label: {
|
|
1323
|
+
show: true,
|
|
1324
|
+
position: "end",
|
|
1325
|
+
// Right side
|
|
1326
|
+
formatter: (params) => {
|
|
1327
|
+
if (options.yAxisLabelFormatter) {
|
|
1328
|
+
return options.yAxisLabelFormatter(params.value);
|
|
1329
|
+
}
|
|
1330
|
+
const decimals = options.yAxisDecimalPlaces !== void 0 ? options.yAxisDecimalPlaces : 2;
|
|
1331
|
+
return typeof params.value === "number" ? params.value.toFixed(decimals) : params.value;
|
|
1332
|
+
},
|
|
1333
|
+
color: "#fff",
|
|
1334
|
+
backgroundColor: lineColor,
|
|
1335
|
+
padding: [2, 4],
|
|
1336
|
+
borderRadius: 2,
|
|
1337
|
+
fontSize: 11,
|
|
1338
|
+
fontWeight: "bold"
|
|
1339
|
+
},
|
|
1340
|
+
lineStyle: {
|
|
1341
|
+
color: lineColor,
|
|
1342
|
+
type: lineStyleType,
|
|
1343
|
+
width: 1,
|
|
1344
|
+
opacity: 0.8
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
],
|
|
1348
|
+
animation: false,
|
|
1349
|
+
silent: true
|
|
1350
|
+
// Disable interaction
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
type: "candlestick",
|
|
1355
|
+
name: options.title || "Market",
|
|
1356
|
+
data,
|
|
1357
|
+
itemStyle: {
|
|
1358
|
+
color: upColor,
|
|
1359
|
+
color0: downColor,
|
|
1360
|
+
borderColor: upColor,
|
|
1361
|
+
borderColor0: downColor
|
|
1362
|
+
},
|
|
1363
|
+
markLine,
|
|
1364
|
+
xAxisIndex: 0,
|
|
1365
|
+
yAxisIndex: 0,
|
|
1366
|
+
z: 5
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
static buildIndicatorSeries(indicators, timeToIndex, paneLayout, totalDataLength, dataIndexOffset = 0, candlestickData, overlayYAxisMap, separatePaneYAxisOffset = 1) {
|
|
1370
|
+
const series = [];
|
|
1371
|
+
const barColors = new Array(totalDataLength).fill(null);
|
|
1372
|
+
const plotDataArrays = /* @__PURE__ */ new Map();
|
|
1373
|
+
indicators.forEach((indicator, id) => {
|
|
1374
|
+
if (indicator.collapsed)
|
|
1375
|
+
return;
|
|
1376
|
+
const sortedPlots = Object.keys(indicator.plots).sort((a, b) => {
|
|
1377
|
+
const plotA = indicator.plots[a];
|
|
1378
|
+
const plotB = indicator.plots[b];
|
|
1379
|
+
const isFillA = plotA.options.style === "fill";
|
|
1380
|
+
const isFillB = plotB.options.style === "fill";
|
|
1381
|
+
if (isFillA && !isFillB)
|
|
1382
|
+
return 1;
|
|
1383
|
+
if (!isFillA && isFillB)
|
|
1384
|
+
return -1;
|
|
1385
|
+
return 0;
|
|
1386
|
+
});
|
|
1387
|
+
sortedPlots.forEach((plotName) => {
|
|
1388
|
+
const plot = indicator.plots[plotName];
|
|
1389
|
+
const seriesName = `${id}::${plotName}`;
|
|
1390
|
+
let xAxisIndex = 0;
|
|
1391
|
+
let yAxisIndex = 0;
|
|
1392
|
+
const plotOverlay = plot.options.overlay;
|
|
1393
|
+
const isPlotOverlay = plotOverlay !== void 0 ? plotOverlay : indicator.paneIndex === 0;
|
|
1394
|
+
if (isPlotOverlay) {
|
|
1395
|
+
xAxisIndex = 0;
|
|
1396
|
+
if (overlayYAxisMap && overlayYAxisMap.has(seriesName)) {
|
|
1397
|
+
yAxisIndex = overlayYAxisMap.get(seriesName);
|
|
1398
|
+
} else {
|
|
1399
|
+
yAxisIndex = 0;
|
|
1400
|
+
}
|
|
1401
|
+
} else {
|
|
1402
|
+
const confIndex = paneLayout.findIndex((p) => p.index === indicator.paneIndex);
|
|
1403
|
+
if (confIndex !== -1) {
|
|
1404
|
+
xAxisIndex = confIndex + 1;
|
|
1405
|
+
yAxisIndex = separatePaneYAxisOffset + confIndex;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
const dataArray = new Array(totalDataLength).fill(null);
|
|
1409
|
+
const colorArray = new Array(totalDataLength).fill(null);
|
|
1410
|
+
const optionsArray = new Array(totalDataLength).fill(null);
|
|
1411
|
+
plot.data?.forEach((point) => {
|
|
1412
|
+
const index = timeToIndex.get(point.time);
|
|
1413
|
+
if (index !== void 0) {
|
|
1414
|
+
const plotOffset = point.options?.offset ?? plot.options.offset ?? 0;
|
|
1415
|
+
const offsetIndex = index + dataIndexOffset + plotOffset;
|
|
1416
|
+
if (offsetIndex >= 0 && offsetIndex < totalDataLength) {
|
|
1417
|
+
let value = point.value;
|
|
1418
|
+
const pointColor = point.options?.color;
|
|
1419
|
+
const isNaColor = pointColor === null || pointColor === "na" || pointColor === "NaN" || typeof pointColor === "number" && isNaN(pointColor);
|
|
1420
|
+
if (isNaColor) {
|
|
1421
|
+
value = null;
|
|
1422
|
+
}
|
|
1423
|
+
dataArray[offsetIndex] = value;
|
|
1424
|
+
colorArray[offsetIndex] = pointColor || plot.options.color || _SeriesBuilder.DEFAULT_COLOR;
|
|
1425
|
+
optionsArray[offsetIndex] = point.options || {};
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
plotDataArrays.set(`${id}::${plotName}`, dataArray);
|
|
1430
|
+
if (plot.options?.style?.startsWith("style_")) {
|
|
1431
|
+
plot.options.style = plot.options.style.replace("style_", "");
|
|
1432
|
+
}
|
|
1433
|
+
if (plot.options.style === "barcolor") {
|
|
1434
|
+
plot.data?.forEach((point) => {
|
|
1435
|
+
const index = timeToIndex.get(point.time);
|
|
1436
|
+
if (index !== void 0) {
|
|
1437
|
+
const plotOffset = point.options?.offset ?? plot.options.offset ?? 0;
|
|
1438
|
+
const offsetIndex = index + dataIndexOffset + plotOffset;
|
|
1439
|
+
if (offsetIndex >= 0 && offsetIndex < totalDataLength) {
|
|
1440
|
+
const pointColor = point.options?.color || plot.options.color || _SeriesBuilder.DEFAULT_COLOR;
|
|
1441
|
+
const isNaColor = pointColor === null || pointColor === "na" || pointColor === "NaN" || typeof pointColor === "number" && isNaN(pointColor);
|
|
1442
|
+
if (!isNaColor && point.value !== null && point.value !== void 0) {
|
|
1443
|
+
barColors[offsetIndex] = pointColor;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
const renderer = SeriesRendererFactory.get(plot.options.style);
|
|
1451
|
+
const seriesConfig = renderer.render({
|
|
1452
|
+
seriesName,
|
|
1453
|
+
xAxisIndex,
|
|
1454
|
+
yAxisIndex,
|
|
1455
|
+
dataArray,
|
|
1456
|
+
colorArray,
|
|
1457
|
+
optionsArray,
|
|
1458
|
+
plotOptions: plot.options,
|
|
1459
|
+
candlestickData,
|
|
1460
|
+
plotDataArrays,
|
|
1461
|
+
indicatorId: id,
|
|
1462
|
+
plotName
|
|
1463
|
+
});
|
|
1464
|
+
if (seriesConfig) {
|
|
1465
|
+
series.push(seriesConfig);
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
});
|
|
1469
|
+
return { series, barColors };
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
__publicField$8(_SeriesBuilder, "DEFAULT_COLOR", "#2962ff");
|
|
1473
|
+
let SeriesBuilder = _SeriesBuilder;
|
|
1474
|
+
|
|
1475
|
+
class GraphicBuilder {
|
|
1476
|
+
static build(layout, options, onToggle, isMainCollapsed = false, maximizedPaneId = null) {
|
|
1477
|
+
const graphic = [];
|
|
1478
|
+
const pixelToPercent = layout.pixelToPercent;
|
|
1479
|
+
const mainPaneTop = layout.mainPaneTop;
|
|
1480
|
+
const showMain = !maximizedPaneId || maximizedPaneId === "main";
|
|
1481
|
+
if (showMain) {
|
|
1482
|
+
const titleTopMargin = 10 * pixelToPercent;
|
|
1483
|
+
graphic.push({
|
|
1484
|
+
type: "text",
|
|
1485
|
+
left: "8.5%",
|
|
1486
|
+
top: mainPaneTop + titleTopMargin + "%",
|
|
1487
|
+
z: 10,
|
|
1488
|
+
style: {
|
|
1489
|
+
text: options.title || "Market",
|
|
1490
|
+
fill: options.titleColor || "#fff",
|
|
1491
|
+
font: `bold 16px ${options.fontFamily || "sans-serif"}`,
|
|
1492
|
+
textVerticalAlign: "top"
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
if (options.watermark !== false) {
|
|
1496
|
+
const bottomY = layout.mainPaneTop + layout.mainPaneHeight;
|
|
1497
|
+
graphic.push({
|
|
1498
|
+
type: "text",
|
|
1499
|
+
right: "11%",
|
|
1500
|
+
top: bottomY - 3 + "%",
|
|
1501
|
+
// Position 5% from bottom of main chart
|
|
1502
|
+
z: 10,
|
|
1503
|
+
style: {
|
|
1504
|
+
text: "QFChart",
|
|
1505
|
+
fill: options.fontColor || "#cbd5e1",
|
|
1506
|
+
font: `bold 16px sans-serif`,
|
|
1507
|
+
opacity: 0.1
|
|
1508
|
+
},
|
|
1509
|
+
cursor: "pointer",
|
|
1510
|
+
onclick: () => {
|
|
1511
|
+
window.open("https://quantforge.org", "_blank");
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
const controls = [];
|
|
1516
|
+
if (options.controls?.collapse) {
|
|
1517
|
+
controls.push({
|
|
1518
|
+
type: "group",
|
|
1519
|
+
children: [
|
|
1520
|
+
{
|
|
1521
|
+
type: "rect",
|
|
1522
|
+
shape: { width: 20, height: 20, r: 2 },
|
|
1523
|
+
style: { fill: "#334155", stroke: "#475569", lineWidth: 1 },
|
|
1524
|
+
onclick: () => onToggle("main", "collapse")
|
|
1525
|
+
},
|
|
1526
|
+
{
|
|
1527
|
+
type: "text",
|
|
1528
|
+
style: {
|
|
1529
|
+
text: isMainCollapsed ? "+" : "\u2212",
|
|
1530
|
+
fill: "#cbd5e1",
|
|
1531
|
+
font: `bold 14px ${options.fontFamily}`,
|
|
1532
|
+
x: 10,
|
|
1533
|
+
y: 10,
|
|
1534
|
+
textAlign: "center",
|
|
1535
|
+
textVerticalAlign: "middle"
|
|
1536
|
+
},
|
|
1537
|
+
silent: true
|
|
1538
|
+
}
|
|
1539
|
+
]
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
if (options.controls?.maximize) {
|
|
1543
|
+
const isMaximized = maximizedPaneId === "main";
|
|
1544
|
+
const xOffset = options.controls?.collapse ? 25 : 0;
|
|
1545
|
+
controls.push({
|
|
1546
|
+
type: "group",
|
|
1547
|
+
x: xOffset,
|
|
1548
|
+
children: [
|
|
1549
|
+
{
|
|
1550
|
+
type: "rect",
|
|
1551
|
+
shape: { width: 20, height: 20, r: 2 },
|
|
1552
|
+
style: { fill: "#334155", stroke: "#475569", lineWidth: 1 },
|
|
1553
|
+
onclick: () => onToggle("main", "maximize")
|
|
1554
|
+
},
|
|
1555
|
+
{
|
|
1556
|
+
type: "text",
|
|
1557
|
+
style: {
|
|
1558
|
+
text: isMaximized ? "\u2750" : "\u25A1",
|
|
1559
|
+
// Simple chars for now
|
|
1560
|
+
fill: "#cbd5e1",
|
|
1561
|
+
font: `14px ${options.fontFamily}`,
|
|
1562
|
+
x: 10,
|
|
1563
|
+
y: 10,
|
|
1564
|
+
textAlign: "center",
|
|
1565
|
+
textVerticalAlign: "middle"
|
|
1566
|
+
},
|
|
1567
|
+
silent: true
|
|
1568
|
+
}
|
|
1569
|
+
]
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
if (options.controls?.fullscreen) {
|
|
1573
|
+
let xOffset = 0;
|
|
1574
|
+
if (options.controls?.collapse)
|
|
1575
|
+
xOffset += 25;
|
|
1576
|
+
if (options.controls?.maximize)
|
|
1577
|
+
xOffset += 25;
|
|
1578
|
+
controls.push({
|
|
1579
|
+
type: "group",
|
|
1580
|
+
x: xOffset,
|
|
1581
|
+
children: [
|
|
1582
|
+
{
|
|
1583
|
+
type: "rect",
|
|
1584
|
+
shape: { width: 20, height: 20, r: 2 },
|
|
1585
|
+
style: { fill: "#334155", stroke: "#475569", lineWidth: 1 },
|
|
1586
|
+
onclick: () => onToggle("main", "fullscreen")
|
|
1587
|
+
},
|
|
1588
|
+
{
|
|
1589
|
+
type: "text",
|
|
1590
|
+
style: {
|
|
1591
|
+
text: "\u26F6",
|
|
1592
|
+
fill: "#cbd5e1",
|
|
1593
|
+
font: `14px ${options.fontFamily}`,
|
|
1594
|
+
x: 10,
|
|
1595
|
+
y: 10,
|
|
1596
|
+
textAlign: "center",
|
|
1597
|
+
textVerticalAlign: "middle"
|
|
1598
|
+
},
|
|
1599
|
+
silent: true
|
|
1600
|
+
}
|
|
1601
|
+
]
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
if (controls.length > 0) {
|
|
1605
|
+
graphic.push({
|
|
1606
|
+
type: "group",
|
|
1607
|
+
right: "10.5%",
|
|
1608
|
+
top: mainPaneTop + "%",
|
|
1609
|
+
children: controls
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
layout.paneLayout.forEach((pane) => {
|
|
1614
|
+
if (maximizedPaneId && pane.indicatorId !== maximizedPaneId) {
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
graphic.push({
|
|
1618
|
+
type: "text",
|
|
1619
|
+
left: "8.5%",
|
|
1620
|
+
top: pane.top + 10 * pixelToPercent + "%",
|
|
1621
|
+
z: 10,
|
|
1622
|
+
style: {
|
|
1623
|
+
text: pane.indicatorId || "",
|
|
1624
|
+
fill: pane.titleColor || "#fff",
|
|
1625
|
+
font: `bold 12px ${options.fontFamily || "sans-serif"}`,
|
|
1626
|
+
textVerticalAlign: "top"
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
const controls = [];
|
|
1630
|
+
if (pane.controls?.collapse) {
|
|
1631
|
+
controls.push({
|
|
1632
|
+
type: "group",
|
|
1633
|
+
children: [
|
|
1634
|
+
{
|
|
1635
|
+
type: "rect",
|
|
1636
|
+
shape: { width: 20, height: 20, r: 2 },
|
|
1637
|
+
style: { fill: "#334155", stroke: "#475569", lineWidth: 1 },
|
|
1638
|
+
onclick: () => pane.indicatorId && onToggle(pane.indicatorId, "collapse")
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
type: "text",
|
|
1642
|
+
style: {
|
|
1643
|
+
text: pane.isCollapsed ? "+" : "\u2212",
|
|
1644
|
+
fill: "#cbd5e1",
|
|
1645
|
+
font: `bold 14px ${options.fontFamily}`,
|
|
1646
|
+
x: 10,
|
|
1647
|
+
y: 10,
|
|
1648
|
+
textAlign: "center",
|
|
1649
|
+
textVerticalAlign: "middle"
|
|
1650
|
+
},
|
|
1651
|
+
silent: true
|
|
1652
|
+
}
|
|
1653
|
+
]
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
if (pane.controls?.maximize) {
|
|
1657
|
+
const isMaximized = maximizedPaneId === pane.indicatorId;
|
|
1658
|
+
const xOffset = pane.controls?.collapse ? 25 : 0;
|
|
1659
|
+
controls.push({
|
|
1660
|
+
type: "group",
|
|
1661
|
+
x: xOffset,
|
|
1662
|
+
children: [
|
|
1663
|
+
{
|
|
1664
|
+
type: "rect",
|
|
1665
|
+
shape: { width: 20, height: 20, r: 2 },
|
|
1666
|
+
style: { fill: "#334155", stroke: "#475569", lineWidth: 1 },
|
|
1667
|
+
onclick: () => pane.indicatorId && onToggle(pane.indicatorId, "maximize")
|
|
1668
|
+
},
|
|
1669
|
+
{
|
|
1670
|
+
type: "text",
|
|
1671
|
+
style: {
|
|
1672
|
+
text: isMaximized ? "\u2750" : "\u25A1",
|
|
1673
|
+
fill: "#cbd5e1",
|
|
1674
|
+
font: `14px ${options.fontFamily}`,
|
|
1675
|
+
x: 10,
|
|
1676
|
+
y: 10,
|
|
1677
|
+
textAlign: "center",
|
|
1678
|
+
textVerticalAlign: "middle"
|
|
1679
|
+
},
|
|
1680
|
+
silent: true
|
|
1681
|
+
}
|
|
1682
|
+
]
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
if (controls.length > 0) {
|
|
1686
|
+
graphic.push({
|
|
1687
|
+
type: "group",
|
|
1688
|
+
right: "10.5%",
|
|
1689
|
+
top: pane.top + "%",
|
|
1690
|
+
children: controls
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
return graphic;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
class TooltipFormatter {
|
|
1699
|
+
static format(params, options) {
|
|
1700
|
+
if (!params || params.length === 0)
|
|
1701
|
+
return "";
|
|
1702
|
+
const marketName = options.title || "Market";
|
|
1703
|
+
const upColor = options.upColor || "#00da3c";
|
|
1704
|
+
const downColor = options.downColor || "#ec0000";
|
|
1705
|
+
const fontFamily = options.fontFamily || "sans-serif";
|
|
1706
|
+
const date = params[0].axisValue;
|
|
1707
|
+
let html = `<div style="font-weight: bold; margin-bottom: 5px; color: #cbd5e1; font-family: ${fontFamily};">${date}</div>`;
|
|
1708
|
+
const marketSeries = params.find(
|
|
1709
|
+
(p) => p.seriesType === "candlestick"
|
|
1710
|
+
);
|
|
1711
|
+
const indicatorParams = params.filter(
|
|
1712
|
+
(p) => p.seriesType !== "candlestick"
|
|
1713
|
+
);
|
|
1714
|
+
if (marketSeries) {
|
|
1715
|
+
const [_, open, close, low, high] = marketSeries.value;
|
|
1716
|
+
const color = close >= open ? upColor : downColor;
|
|
1717
|
+
html += `
|
|
1718
|
+
<div style="margin-bottom: 8px; font-family: ${fontFamily};">
|
|
1719
|
+
<div style="display:flex; justify-content:space-between; color:${color}; font-weight:bold;">
|
|
1720
|
+
<span>${marketName}</span>
|
|
1721
|
+
</div>
|
|
1722
|
+
<div style="display: grid; grid-template-columns: auto auto; gap: 2px 15px; font-size: 0.9em; color: #cbd5e1;">
|
|
1723
|
+
<span>Open:</span> <span style="text-align: right; color: ${close >= open ? upColor : downColor}">${open}</span>
|
|
1724
|
+
<span>High:</span> <span style="text-align: right; color: ${upColor}">${high}</span>
|
|
1725
|
+
<span>Low:</span> <span style="text-align: right; color: ${downColor}">${low}</span>
|
|
1726
|
+
<span>Close:</span> <span style="text-align: right; color: ${close >= open ? upColor : downColor}">${close}</span>
|
|
1727
|
+
</div>
|
|
1728
|
+
</div>
|
|
1729
|
+
`;
|
|
1730
|
+
}
|
|
1731
|
+
if (indicatorParams.length > 0) {
|
|
1732
|
+
html += `<div style="border-top: 1px solid #334155; margin: 5px 0; padding-top: 5px;"></div>`;
|
|
1733
|
+
const indicators = {};
|
|
1734
|
+
indicatorParams.forEach((p) => {
|
|
1735
|
+
const parts = p.seriesName.split("::");
|
|
1736
|
+
const indId = parts.length > 1 ? parts[0] : "Unknown";
|
|
1737
|
+
const plotName = parts.length > 1 ? parts[1] : p.seriesName;
|
|
1738
|
+
if (!indicators[indId])
|
|
1739
|
+
indicators[indId] = [];
|
|
1740
|
+
indicators[indId].push({ ...p, displayName: plotName });
|
|
1741
|
+
});
|
|
1742
|
+
Object.keys(indicators).forEach((indId) => {
|
|
1743
|
+
html += `
|
|
1744
|
+
<div style="margin-top: 8px; font-family: ${fontFamily};">
|
|
1745
|
+
<div style="font-weight:bold; color: #fff; margin-bottom: 2px;">${indId}</div>
|
|
1746
|
+
`;
|
|
1747
|
+
indicators[indId].forEach((p) => {
|
|
1748
|
+
let val = p.value;
|
|
1749
|
+
if (Array.isArray(val)) {
|
|
1750
|
+
val = val[1];
|
|
1751
|
+
}
|
|
1752
|
+
if (val === null || val === void 0)
|
|
1753
|
+
return;
|
|
1754
|
+
const valStr = typeof val === "number" ? val.toLocaleString(void 0, { maximumFractionDigits: 4 }) : val;
|
|
1755
|
+
html += `
|
|
1756
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px; padding-left: 8px;">
|
|
1757
|
+
<div>${p.marker} <span style="color: #cbd5e1;">${p.displayName}</span></div>
|
|
1758
|
+
<div style="font-size: 10px; color: #fff;padding-left:10px;">${valStr}</div>
|
|
1759
|
+
</div>`;
|
|
1760
|
+
});
|
|
1761
|
+
html += `</div>`;
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
return html;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
var __defProp$7 = Object.defineProperty;
|
|
1769
|
+
var __defNormalProp$7 = (obj, key, value) => key in obj ? __defProp$7(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1770
|
+
var __publicField$7 = (obj, key, value) => {
|
|
1771
|
+
__defNormalProp$7(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1772
|
+
return value;
|
|
1773
|
+
};
|
|
1774
|
+
class PluginManager {
|
|
1775
|
+
constructor(context, toolbarContainer) {
|
|
1776
|
+
__publicField$7(this, "plugins", /* @__PURE__ */ new Map());
|
|
1777
|
+
__publicField$7(this, "activePluginId", null);
|
|
1778
|
+
__publicField$7(this, "context");
|
|
1779
|
+
__publicField$7(this, "toolbarContainer");
|
|
1780
|
+
__publicField$7(this, "tooltipElement", null);
|
|
1781
|
+
__publicField$7(this, "hideTimeout", null);
|
|
1782
|
+
this.context = context;
|
|
1783
|
+
this.toolbarContainer = toolbarContainer;
|
|
1784
|
+
this.createTooltip();
|
|
1785
|
+
this.renderToolbar();
|
|
1786
|
+
}
|
|
1787
|
+
createTooltip() {
|
|
1788
|
+
this.tooltipElement = document.createElement("div");
|
|
1789
|
+
Object.assign(this.tooltipElement.style, {
|
|
1790
|
+
position: "fixed",
|
|
1791
|
+
display: "none",
|
|
1792
|
+
backgroundColor: "#1e293b",
|
|
1793
|
+
color: "#e2e8f0",
|
|
1794
|
+
padding: "6px 10px",
|
|
1795
|
+
borderRadius: "6px",
|
|
1796
|
+
fontSize: "13px",
|
|
1797
|
+
lineHeight: "1.4",
|
|
1798
|
+
fontWeight: "500",
|
|
1799
|
+
border: "1px solid #334155",
|
|
1800
|
+
zIndex: "9999",
|
|
1801
|
+
pointerEvents: "none",
|
|
1802
|
+
whiteSpace: "nowrap",
|
|
1803
|
+
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.15)",
|
|
1804
|
+
fontFamily: this.context.getOptions().fontFamily || "sans-serif",
|
|
1805
|
+
transition: "opacity 0.15s ease-in-out, transform 0.15s ease-in-out",
|
|
1806
|
+
opacity: "0",
|
|
1807
|
+
transform: "translateX(-5px)"
|
|
1808
|
+
});
|
|
1809
|
+
document.body.appendChild(this.tooltipElement);
|
|
1810
|
+
}
|
|
1811
|
+
destroy() {
|
|
1812
|
+
if (this.tooltipElement && this.tooltipElement.parentNode) {
|
|
1813
|
+
this.tooltipElement.parentNode.removeChild(this.tooltipElement);
|
|
1814
|
+
}
|
|
1815
|
+
this.tooltipElement = null;
|
|
1816
|
+
}
|
|
1817
|
+
showTooltip(target, text) {
|
|
1818
|
+
if (!this.tooltipElement)
|
|
1819
|
+
return;
|
|
1820
|
+
if (this.hideTimeout) {
|
|
1821
|
+
clearTimeout(this.hideTimeout);
|
|
1822
|
+
this.hideTimeout = null;
|
|
1823
|
+
}
|
|
1824
|
+
const rect = target.getBoundingClientRect();
|
|
1825
|
+
this.tooltipElement.textContent = text;
|
|
1826
|
+
this.tooltipElement.style.display = "block";
|
|
1827
|
+
const tooltipRect = this.tooltipElement.getBoundingClientRect();
|
|
1828
|
+
const top = rect.top + (rect.height - tooltipRect.height) / 2;
|
|
1829
|
+
const left = rect.right + 10;
|
|
1830
|
+
this.tooltipElement.style.top = `${top}px`;
|
|
1831
|
+
this.tooltipElement.style.left = `${left}px`;
|
|
1832
|
+
requestAnimationFrame(() => {
|
|
1833
|
+
if (this.tooltipElement) {
|
|
1834
|
+
this.tooltipElement.style.opacity = "1";
|
|
1835
|
+
this.tooltipElement.style.transform = "translateX(0)";
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
hideTooltip() {
|
|
1840
|
+
if (!this.tooltipElement)
|
|
1841
|
+
return;
|
|
1842
|
+
this.tooltipElement.style.opacity = "0";
|
|
1843
|
+
this.tooltipElement.style.transform = "translateX(-5px)";
|
|
1844
|
+
if (this.hideTimeout) {
|
|
1845
|
+
clearTimeout(this.hideTimeout);
|
|
1846
|
+
}
|
|
1847
|
+
this.hideTimeout = setTimeout(() => {
|
|
1848
|
+
if (this.tooltipElement) {
|
|
1849
|
+
this.tooltipElement.style.display = "none";
|
|
1850
|
+
}
|
|
1851
|
+
this.hideTimeout = null;
|
|
1852
|
+
}, 150);
|
|
1853
|
+
}
|
|
1854
|
+
register(plugin) {
|
|
1855
|
+
if (this.plugins.has(plugin.id)) {
|
|
1856
|
+
console.warn(`Plugin with id ${plugin.id} is already registered.`);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
this.plugins.set(plugin.id, plugin);
|
|
1860
|
+
plugin.init(this.context);
|
|
1861
|
+
this.addButton(plugin);
|
|
1862
|
+
}
|
|
1863
|
+
unregister(pluginId) {
|
|
1864
|
+
const plugin = this.plugins.get(pluginId);
|
|
1865
|
+
if (plugin) {
|
|
1866
|
+
if (this.activePluginId === pluginId) {
|
|
1867
|
+
this.deactivatePlugin();
|
|
1868
|
+
}
|
|
1869
|
+
plugin.destroy?.();
|
|
1870
|
+
this.plugins.delete(pluginId);
|
|
1871
|
+
this.removeButton(pluginId);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
activatePlugin(pluginId) {
|
|
1875
|
+
if (this.activePluginId === pluginId) {
|
|
1876
|
+
this.deactivatePlugin();
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
if (this.activePluginId) {
|
|
1880
|
+
this.deactivatePlugin();
|
|
1881
|
+
}
|
|
1882
|
+
const plugin = this.plugins.get(pluginId);
|
|
1883
|
+
if (plugin) {
|
|
1884
|
+
this.activePluginId = pluginId;
|
|
1885
|
+
this.setButtonActive(pluginId, true);
|
|
1886
|
+
plugin.activate?.();
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
deactivatePlugin() {
|
|
1890
|
+
if (this.activePluginId) {
|
|
1891
|
+
const plugin = this.plugins.get(this.activePluginId);
|
|
1892
|
+
plugin?.deactivate?.();
|
|
1893
|
+
this.setButtonActive(this.activePluginId, false);
|
|
1894
|
+
this.activePluginId = null;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
// --- UI Handling ---
|
|
1898
|
+
renderToolbar() {
|
|
1899
|
+
this.toolbarContainer.innerHTML = "";
|
|
1900
|
+
this.toolbarContainer.classList.add("qfchart-toolbar");
|
|
1901
|
+
this.toolbarContainer.style.display = "flex";
|
|
1902
|
+
this.toolbarContainer.style.flexDirection = "column";
|
|
1903
|
+
this.toolbarContainer.style.width = "40px";
|
|
1904
|
+
this.toolbarContainer.style.backgroundColor = this.context.getOptions().backgroundColor || "#1e293b";
|
|
1905
|
+
this.toolbarContainer.style.borderRight = "1px solid #334155";
|
|
1906
|
+
this.toolbarContainer.style.padding = "5px";
|
|
1907
|
+
this.toolbarContainer.style.boxSizing = "border-box";
|
|
1908
|
+
this.toolbarContainer.style.gap = "5px";
|
|
1909
|
+
this.toolbarContainer.style.flexShrink = "0";
|
|
1910
|
+
}
|
|
1911
|
+
addButton(plugin) {
|
|
1912
|
+
const btn = document.createElement("button");
|
|
1913
|
+
btn.id = `qfchart-plugin-btn-${plugin.id}`;
|
|
1914
|
+
btn.style.width = "30px";
|
|
1915
|
+
btn.style.height = "30px";
|
|
1916
|
+
btn.style.padding = "4px";
|
|
1917
|
+
btn.style.border = "1px solid transparent";
|
|
1918
|
+
btn.style.borderRadius = "4px";
|
|
1919
|
+
btn.style.backgroundColor = "transparent";
|
|
1920
|
+
btn.style.cursor = "pointer";
|
|
1921
|
+
btn.style.color = this.context.getOptions().fontColor || "#cbd5e1";
|
|
1922
|
+
btn.style.display = "flex";
|
|
1923
|
+
btn.style.alignItems = "center";
|
|
1924
|
+
btn.style.justifyContent = "center";
|
|
1925
|
+
if (plugin.icon) {
|
|
1926
|
+
btn.innerHTML = plugin.icon;
|
|
1927
|
+
} else {
|
|
1928
|
+
btn.innerText = (plugin.name || plugin.id).substring(0, 2).toUpperCase();
|
|
1929
|
+
}
|
|
1930
|
+
btn.addEventListener("mouseenter", () => {
|
|
1931
|
+
if (this.activePluginId !== plugin.id) {
|
|
1932
|
+
btn.style.backgroundColor = "rgba(255, 255, 255, 0.1)";
|
|
1933
|
+
}
|
|
1934
|
+
this.showTooltip(btn, plugin.name || plugin.id);
|
|
1935
|
+
});
|
|
1936
|
+
btn.addEventListener("mouseleave", () => {
|
|
1937
|
+
if (this.activePluginId !== plugin.id) {
|
|
1938
|
+
btn.style.backgroundColor = "transparent";
|
|
1939
|
+
}
|
|
1940
|
+
this.hideTooltip();
|
|
1941
|
+
});
|
|
1942
|
+
btn.onclick = () => this.activatePlugin(plugin.id);
|
|
1943
|
+
this.toolbarContainer.appendChild(btn);
|
|
1944
|
+
}
|
|
1945
|
+
removeButton(pluginId) {
|
|
1946
|
+
const btn = this.toolbarContainer.querySelector(`#qfchart-plugin-btn-${pluginId}`);
|
|
1947
|
+
if (btn) {
|
|
1948
|
+
btn.remove();
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
setButtonActive(pluginId, active) {
|
|
1952
|
+
const btn = this.toolbarContainer.querySelector(`#qfchart-plugin-btn-${pluginId}`);
|
|
1953
|
+
if (btn) {
|
|
1954
|
+
if (active) {
|
|
1955
|
+
btn.style.backgroundColor = "#2563eb";
|
|
1956
|
+
btn.style.color = "#ffffff";
|
|
1957
|
+
} else {
|
|
1958
|
+
btn.style.backgroundColor = "transparent";
|
|
1959
|
+
btn.style.color = this.context.getOptions().fontColor || "#cbd5e1";
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
var __defProp$6 = Object.defineProperty;
|
|
1966
|
+
var __defNormalProp$6 = (obj, key, value) => key in obj ? __defProp$6(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1967
|
+
var __publicField$6 = (obj, key, value) => {
|
|
1968
|
+
__defNormalProp$6(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1969
|
+
return value;
|
|
1970
|
+
};
|
|
1971
|
+
class DrawingEditor {
|
|
1972
|
+
constructor(context) {
|
|
1973
|
+
__publicField$6(this, "context");
|
|
1974
|
+
__publicField$6(this, "isEditing", false);
|
|
1975
|
+
__publicField$6(this, "currentDrawing", null);
|
|
1976
|
+
__publicField$6(this, "editingPointIndex", null);
|
|
1977
|
+
__publicField$6(this, "zr");
|
|
1978
|
+
// Temporary ZRender elements for visual feedback during drag
|
|
1979
|
+
__publicField$6(this, "editGroup", null);
|
|
1980
|
+
__publicField$6(this, "editLine", null);
|
|
1981
|
+
__publicField$6(this, "editStartPoint", null);
|
|
1982
|
+
__publicField$6(this, "editEndPoint", null);
|
|
1983
|
+
__publicField$6(this, "isMovingShape", false);
|
|
1984
|
+
__publicField$6(this, "dragStart", null);
|
|
1985
|
+
__publicField$6(this, "initialPixelPoints", []);
|
|
1986
|
+
__publicField$6(this, "onDrawingMouseDown", (payload) => {
|
|
1987
|
+
if (this.isEditing)
|
|
1988
|
+
return;
|
|
1989
|
+
const drawing = this.context.getDrawing(payload.id);
|
|
1990
|
+
if (!drawing)
|
|
1991
|
+
return;
|
|
1992
|
+
this.isEditing = true;
|
|
1993
|
+
this.isMovingShape = true;
|
|
1994
|
+
this.currentDrawing = JSON.parse(JSON.stringify(drawing));
|
|
1995
|
+
this.dragStart = { x: payload.x, y: payload.y };
|
|
1996
|
+
this.initialPixelPoints = drawing.points.map((p) => {
|
|
1997
|
+
const pixel = this.context.coordinateConversion.dataToPixel(p);
|
|
1998
|
+
return pixel ? { x: pixel.x, y: pixel.y } : { x: 0, y: 0 };
|
|
1999
|
+
});
|
|
2000
|
+
this.context.lockChart();
|
|
2001
|
+
this.createEditGraphic();
|
|
2002
|
+
this.zr.on("mousemove", this.onMouseMove);
|
|
2003
|
+
this.zr.on("mouseup", this.onMouseUp);
|
|
2004
|
+
});
|
|
2005
|
+
__publicField$6(this, "onPointMouseDown", (payload) => {
|
|
2006
|
+
if (this.isEditing)
|
|
2007
|
+
return;
|
|
2008
|
+
const drawing = this.context.getDrawing(payload.id);
|
|
2009
|
+
if (!drawing)
|
|
2010
|
+
return;
|
|
2011
|
+
this.isEditing = true;
|
|
2012
|
+
this.currentDrawing = JSON.parse(JSON.stringify(drawing));
|
|
2013
|
+
this.editingPointIndex = payload.pointIndex;
|
|
2014
|
+
this.context.lockChart();
|
|
2015
|
+
this.createEditGraphic();
|
|
2016
|
+
this.zr.on("mousemove", this.onMouseMove);
|
|
2017
|
+
this.zr.on("mouseup", this.onMouseUp);
|
|
2018
|
+
});
|
|
2019
|
+
__publicField$6(this, "onMouseMove", (e) => {
|
|
2020
|
+
if (!this.isEditing || !this.currentDrawing)
|
|
2021
|
+
return;
|
|
2022
|
+
const x = e.offsetX;
|
|
2023
|
+
const y = e.offsetY;
|
|
2024
|
+
if (this.isMovingShape && this.dragStart) {
|
|
2025
|
+
const dx = x - this.dragStart.x;
|
|
2026
|
+
const dy = y - this.dragStart.y;
|
|
2027
|
+
const newP1 = {
|
|
2028
|
+
x: this.initialPixelPoints[0].x + dx,
|
|
2029
|
+
y: this.initialPixelPoints[0].y + dy
|
|
2030
|
+
};
|
|
2031
|
+
const newP2 = {
|
|
2032
|
+
x: this.initialPixelPoints[1].x + dx,
|
|
2033
|
+
y: this.initialPixelPoints[1].y + dy
|
|
2034
|
+
};
|
|
2035
|
+
this.editLine.setShape({
|
|
2036
|
+
x1: newP1.x,
|
|
2037
|
+
y1: newP1.y,
|
|
2038
|
+
x2: newP2.x,
|
|
2039
|
+
y2: newP2.y
|
|
2040
|
+
});
|
|
2041
|
+
this.editStartPoint.setShape({ cx: newP1.x, cy: newP1.y });
|
|
2042
|
+
this.editEndPoint.setShape({ cx: newP2.x, cy: newP2.y });
|
|
2043
|
+
} else if (this.editingPointIndex !== null) {
|
|
2044
|
+
if (this.editingPointIndex === 0) {
|
|
2045
|
+
this.editLine.setShape({ x1: x, y1: y });
|
|
2046
|
+
this.editStartPoint.setShape({ cx: x, cy: y });
|
|
2047
|
+
} else {
|
|
2048
|
+
this.editLine.setShape({ x2: x, y2: y });
|
|
2049
|
+
this.editEndPoint.setShape({ cx: x, cy: y });
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
});
|
|
2053
|
+
__publicField$6(this, "onMouseUp", (e) => {
|
|
2054
|
+
if (!this.isEditing)
|
|
2055
|
+
return;
|
|
2056
|
+
this.finishEditing(e.offsetX, e.offsetY);
|
|
2057
|
+
});
|
|
2058
|
+
this.context = context;
|
|
2059
|
+
this.zr = this.context.getChart().getZr();
|
|
2060
|
+
this.bindEvents();
|
|
2061
|
+
}
|
|
2062
|
+
bindEvents() {
|
|
2063
|
+
this.context.events.on("drawing:point:mousedown", this.onPointMouseDown);
|
|
2064
|
+
this.context.events.on("drawing:mousedown", this.onDrawingMouseDown);
|
|
2065
|
+
}
|
|
2066
|
+
createEditGraphic() {
|
|
2067
|
+
if (!this.currentDrawing)
|
|
2068
|
+
return;
|
|
2069
|
+
this.editGroup = new echarts__namespace.graphic.Group();
|
|
2070
|
+
const p1Data = this.currentDrawing.points[0];
|
|
2071
|
+
const p2Data = this.currentDrawing.points[1];
|
|
2072
|
+
const p1 = this.context.coordinateConversion.dataToPixel(p1Data);
|
|
2073
|
+
const p2 = this.context.coordinateConversion.dataToPixel(p2Data);
|
|
2074
|
+
if (!p1 || !p2)
|
|
2075
|
+
return;
|
|
2076
|
+
this.editLine = new echarts__namespace.graphic.Line({
|
|
2077
|
+
shape: { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y },
|
|
2078
|
+
style: {
|
|
2079
|
+
stroke: this.currentDrawing.style?.color || "#3b82f6",
|
|
2080
|
+
lineWidth: this.currentDrawing.style?.lineWidth || 2,
|
|
2081
|
+
lineDash: [4, 4]
|
|
2082
|
+
// Dashed to indicate editing
|
|
2083
|
+
},
|
|
2084
|
+
silent: true
|
|
2085
|
+
// Events pass through to handlers
|
|
2086
|
+
});
|
|
2087
|
+
this.editStartPoint = new echarts__namespace.graphic.Circle({
|
|
2088
|
+
shape: { cx: p1.x, cy: p1.y, r: 5 },
|
|
2089
|
+
style: { fill: "#fff", stroke: "#3b82f6", lineWidth: 2 },
|
|
2090
|
+
z: 1e3
|
|
2091
|
+
});
|
|
2092
|
+
this.editEndPoint = new echarts__namespace.graphic.Circle({
|
|
2093
|
+
shape: { cx: p2.x, cy: p2.y, r: 5 },
|
|
2094
|
+
style: { fill: "#fff", stroke: "#3b82f6", lineWidth: 2 },
|
|
2095
|
+
z: 1e3
|
|
2096
|
+
});
|
|
2097
|
+
this.editGroup.add(this.editLine);
|
|
2098
|
+
this.editGroup.add(this.editStartPoint);
|
|
2099
|
+
this.editGroup.add(this.editEndPoint);
|
|
2100
|
+
this.zr.add(this.editGroup);
|
|
2101
|
+
}
|
|
2102
|
+
finishEditing(finalX, finishY) {
|
|
2103
|
+
if (!this.currentDrawing)
|
|
2104
|
+
return;
|
|
2105
|
+
if (this.isMovingShape && this.dragStart) {
|
|
2106
|
+
const dx = finalX - this.dragStart.x;
|
|
2107
|
+
const dy = finishY - this.dragStart.y;
|
|
2108
|
+
const newPoints = this.initialPixelPoints.map((p, i) => {
|
|
2109
|
+
const newX = p.x + dx;
|
|
2110
|
+
const newY = p.y + dy;
|
|
2111
|
+
return this.context.coordinateConversion.pixelToData({
|
|
2112
|
+
x: newX,
|
|
2113
|
+
y: newY
|
|
2114
|
+
});
|
|
2115
|
+
});
|
|
2116
|
+
if (newPoints.every((p) => p !== null)) {
|
|
2117
|
+
if (newPoints[0] && newPoints[1]) {
|
|
2118
|
+
this.currentDrawing.points[0] = newPoints[0];
|
|
2119
|
+
this.currentDrawing.points[1] = newPoints[1];
|
|
2120
|
+
if (newPoints[0].paneIndex !== void 0) {
|
|
2121
|
+
this.currentDrawing.paneIndex = newPoints[0].paneIndex;
|
|
2122
|
+
}
|
|
2123
|
+
this.context.updateDrawing(this.currentDrawing);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
} else if (this.editingPointIndex !== null) {
|
|
2127
|
+
const newData = this.context.coordinateConversion.pixelToData({
|
|
2128
|
+
x: finalX,
|
|
2129
|
+
y: finishY
|
|
2130
|
+
});
|
|
2131
|
+
if (newData) {
|
|
2132
|
+
this.currentDrawing.points[this.editingPointIndex] = newData;
|
|
2133
|
+
if (this.editingPointIndex === 0 && newData.paneIndex !== void 0) {
|
|
2134
|
+
this.currentDrawing.paneIndex = newData.paneIndex;
|
|
2135
|
+
}
|
|
2136
|
+
this.context.updateDrawing(this.currentDrawing);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
this.isEditing = false;
|
|
2140
|
+
this.isMovingShape = false;
|
|
2141
|
+
this.dragStart = null;
|
|
2142
|
+
this.initialPixelPoints = [];
|
|
2143
|
+
this.currentDrawing = null;
|
|
2144
|
+
this.editingPointIndex = null;
|
|
2145
|
+
if (this.editGroup) {
|
|
2146
|
+
this.zr.remove(this.editGroup);
|
|
2147
|
+
this.editGroup = null;
|
|
2148
|
+
}
|
|
2149
|
+
this.zr.off("mousemove", this.onMouseMove);
|
|
2150
|
+
this.zr.off("mouseup", this.onMouseUp);
|
|
2151
|
+
this.context.unlockChart();
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
var __defProp$5 = Object.defineProperty;
|
|
2156
|
+
var __defNormalProp$5 = (obj, key, value) => key in obj ? __defProp$5(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
2157
|
+
var __publicField$5 = (obj, key, value) => {
|
|
2158
|
+
__defNormalProp$5(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
2159
|
+
return value;
|
|
2160
|
+
};
|
|
2161
|
+
class EventBus {
|
|
2162
|
+
constructor() {
|
|
2163
|
+
__publicField$5(this, "handlers", /* @__PURE__ */ new Map());
|
|
2164
|
+
}
|
|
2165
|
+
on(event, handler) {
|
|
2166
|
+
if (!this.handlers.has(event)) {
|
|
2167
|
+
this.handlers.set(event, /* @__PURE__ */ new Set());
|
|
2168
|
+
}
|
|
2169
|
+
this.handlers.get(event).add(handler);
|
|
2170
|
+
}
|
|
2171
|
+
off(event, handler) {
|
|
2172
|
+
const handlers = this.handlers.get(event);
|
|
2173
|
+
if (handlers) {
|
|
2174
|
+
handlers.delete(handler);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
emit(event, payload) {
|
|
2178
|
+
const handlers = this.handlers.get(event);
|
|
2179
|
+
if (handlers) {
|
|
2180
|
+
handlers.forEach((handler) => {
|
|
2181
|
+
try {
|
|
2182
|
+
handler(payload);
|
|
2183
|
+
} catch (e) {
|
|
2184
|
+
console.error(`Error in EventBus handler for ${event}:`, e);
|
|
2185
|
+
}
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
clear() {
|
|
2190
|
+
this.handlers.clear();
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
var __defProp$4 = Object.defineProperty;
|
|
2195
|
+
var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
2196
|
+
var __publicField$4 = (obj, key, value) => {
|
|
2197
|
+
__defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
2198
|
+
return value;
|
|
2199
|
+
};
|
|
2200
|
+
class QFChart {
|
|
2201
|
+
constructor(container, options = {}) {
|
|
2202
|
+
__publicField$4(this, "chart");
|
|
2203
|
+
__publicField$4(this, "options");
|
|
2204
|
+
__publicField$4(this, "marketData", []);
|
|
2205
|
+
__publicField$4(this, "indicators", /* @__PURE__ */ new Map());
|
|
2206
|
+
__publicField$4(this, "timeToIndex", /* @__PURE__ */ new Map());
|
|
2207
|
+
__publicField$4(this, "pluginManager");
|
|
2208
|
+
__publicField$4(this, "drawingEditor");
|
|
2209
|
+
__publicField$4(this, "events", new EventBus());
|
|
2210
|
+
__publicField$4(this, "isMainCollapsed", false);
|
|
2211
|
+
__publicField$4(this, "maximizedPaneId", null);
|
|
2212
|
+
__publicField$4(this, "countdownInterval", null);
|
|
2213
|
+
__publicField$4(this, "selectedDrawingId", null);
|
|
2214
|
+
// Track selected drawing
|
|
2215
|
+
// Drawing System
|
|
2216
|
+
__publicField$4(this, "drawings", []);
|
|
2217
|
+
__publicField$4(this, "coordinateConversion", {
|
|
2218
|
+
pixelToData: (point) => {
|
|
2219
|
+
const option = this.chart.getOption();
|
|
2220
|
+
if (!option || !option.grid)
|
|
2221
|
+
return null;
|
|
2222
|
+
const gridCount = option.grid.length;
|
|
2223
|
+
for (let i = 0; i < gridCount; i++) {
|
|
2224
|
+
if (this.chart.containPixel({ gridIndex: i }, [point.x, point.y])) {
|
|
2225
|
+
this.chart.convertFromPixel({ seriesIndex: i }, [point.x, point.y]);
|
|
2226
|
+
const pGrid = this.chart.convertFromPixel({ gridIndex: i }, [point.x, point.y]);
|
|
2227
|
+
if (pGrid) {
|
|
2228
|
+
return { timeIndex: Math.round(pGrid[0]), value: pGrid[1], paneIndex: i };
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
return null;
|
|
2233
|
+
},
|
|
2234
|
+
dataToPixel: (point) => {
|
|
2235
|
+
const paneIdx = point.paneIndex || 0;
|
|
2236
|
+
const p = this.chart.convertToPixel({ gridIndex: paneIdx }, [point.timeIndex, point.value]);
|
|
2237
|
+
if (p) {
|
|
2238
|
+
return { x: p[0], y: p[1] };
|
|
2239
|
+
}
|
|
2240
|
+
return null;
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
// Default colors and constants
|
|
2244
|
+
__publicField$4(this, "upColor", "#00da3c");
|
|
2245
|
+
__publicField$4(this, "downColor", "#ec0000");
|
|
2246
|
+
__publicField$4(this, "defaultPadding", 0);
|
|
2247
|
+
__publicField$4(this, "padding");
|
|
2248
|
+
__publicField$4(this, "dataIndexOffset", 0);
|
|
2249
|
+
// Offset for phantom padding data
|
|
2250
|
+
// DOM Elements for Layout
|
|
2251
|
+
__publicField$4(this, "rootContainer");
|
|
2252
|
+
__publicField$4(this, "layoutContainer");
|
|
2253
|
+
__publicField$4(this, "toolbarContainer");
|
|
2254
|
+
// New Toolbar
|
|
2255
|
+
__publicField$4(this, "leftSidebar");
|
|
2256
|
+
__publicField$4(this, "rightSidebar");
|
|
2257
|
+
__publicField$4(this, "chartContainer");
|
|
2258
|
+
__publicField$4(this, "onKeyDown", (e) => {
|
|
2259
|
+
if ((e.key === "Delete" || e.key === "Backspace") && this.selectedDrawingId) {
|
|
2260
|
+
this.removeDrawing(this.selectedDrawingId);
|
|
2261
|
+
this.selectedDrawingId = null;
|
|
2262
|
+
this.render();
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
|
+
__publicField$4(this, "onFullscreenChange", () => {
|
|
2266
|
+
this.render();
|
|
2267
|
+
});
|
|
2268
|
+
// --- Interaction Locking ---
|
|
2269
|
+
__publicField$4(this, "isLocked", false);
|
|
2270
|
+
__publicField$4(this, "lockedState", null);
|
|
2271
|
+
this.rootContainer = container;
|
|
2272
|
+
this.options = {
|
|
2273
|
+
title: "Market",
|
|
2274
|
+
height: "600px",
|
|
2275
|
+
backgroundColor: "#1e293b",
|
|
2276
|
+
upColor: "#00da3c",
|
|
2277
|
+
downColor: "#ec0000",
|
|
2278
|
+
fontColor: "#cbd5e1",
|
|
2279
|
+
fontFamily: "sans-serif",
|
|
2280
|
+
padding: 0.01,
|
|
2281
|
+
dataZoom: {
|
|
2282
|
+
visible: true,
|
|
2283
|
+
position: "top",
|
|
2284
|
+
height: 6
|
|
2285
|
+
},
|
|
2286
|
+
layout: {
|
|
2287
|
+
mainPaneHeight: "50%",
|
|
2288
|
+
gap: 13
|
|
2289
|
+
},
|
|
2290
|
+
watermark: true,
|
|
2291
|
+
...options
|
|
2292
|
+
};
|
|
2293
|
+
if (this.options.upColor)
|
|
2294
|
+
this.upColor = this.options.upColor;
|
|
2295
|
+
if (this.options.downColor)
|
|
2296
|
+
this.downColor = this.options.downColor;
|
|
2297
|
+
this.padding = this.options.padding !== void 0 ? this.options.padding : this.defaultPadding;
|
|
2298
|
+
if (this.options.height) {
|
|
2299
|
+
if (typeof this.options.height === "number") {
|
|
2300
|
+
this.rootContainer.style.height = `${this.options.height}px`;
|
|
2301
|
+
} else {
|
|
2302
|
+
this.rootContainer.style.height = this.options.height;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
this.rootContainer.innerHTML = "";
|
|
2306
|
+
this.layoutContainer = document.createElement("div");
|
|
2307
|
+
this.layoutContainer.style.display = "flex";
|
|
2308
|
+
this.layoutContainer.style.width = "100%";
|
|
2309
|
+
this.layoutContainer.style.height = "100%";
|
|
2310
|
+
this.layoutContainer.style.overflow = "hidden";
|
|
2311
|
+
this.rootContainer.appendChild(this.layoutContainer);
|
|
2312
|
+
this.leftSidebar = document.createElement("div");
|
|
2313
|
+
this.leftSidebar.style.display = "none";
|
|
2314
|
+
this.leftSidebar.style.width = "250px";
|
|
2315
|
+
this.leftSidebar.style.flexShrink = "0";
|
|
2316
|
+
this.leftSidebar.style.overflowY = "auto";
|
|
2317
|
+
this.leftSidebar.style.backgroundColor = this.options.backgroundColor || "#1e293b";
|
|
2318
|
+
this.leftSidebar.style.borderRight = "1px solid #334155";
|
|
2319
|
+
this.leftSidebar.style.padding = "10px";
|
|
2320
|
+
this.leftSidebar.style.boxSizing = "border-box";
|
|
2321
|
+
this.leftSidebar.style.color = "#cbd5e1";
|
|
2322
|
+
this.leftSidebar.style.fontSize = "12px";
|
|
2323
|
+
this.leftSidebar.style.fontFamily = this.options.fontFamily || "sans-serif";
|
|
2324
|
+
this.layoutContainer.appendChild(this.leftSidebar);
|
|
2325
|
+
this.toolbarContainer = document.createElement("div");
|
|
2326
|
+
this.layoutContainer.appendChild(this.toolbarContainer);
|
|
2327
|
+
this.chartContainer = document.createElement("div");
|
|
2328
|
+
this.chartContainer.style.flexGrow = "1";
|
|
2329
|
+
this.chartContainer.style.height = "100%";
|
|
2330
|
+
this.chartContainer.style.overflow = "hidden";
|
|
2331
|
+
this.layoutContainer.appendChild(this.chartContainer);
|
|
2332
|
+
this.rightSidebar = document.createElement("div");
|
|
2333
|
+
this.rightSidebar.style.display = "none";
|
|
2334
|
+
this.rightSidebar.style.width = "250px";
|
|
2335
|
+
this.rightSidebar.style.flexShrink = "0";
|
|
2336
|
+
this.rightSidebar.style.overflowY = "auto";
|
|
2337
|
+
this.rightSidebar.style.backgroundColor = this.options.backgroundColor || "#1e293b";
|
|
2338
|
+
this.rightSidebar.style.borderLeft = "1px solid #334155";
|
|
2339
|
+
this.rightSidebar.style.padding = "10px";
|
|
2340
|
+
this.rightSidebar.style.boxSizing = "border-box";
|
|
2341
|
+
this.rightSidebar.style.color = "#cbd5e1";
|
|
2342
|
+
this.rightSidebar.style.fontSize = "12px";
|
|
2343
|
+
this.rightSidebar.style.fontFamily = this.options.fontFamily || "sans-serif";
|
|
2344
|
+
this.layoutContainer.appendChild(this.rightSidebar);
|
|
2345
|
+
this.chart = echarts__namespace.init(this.chartContainer);
|
|
2346
|
+
this.pluginManager = new PluginManager(this, this.toolbarContainer);
|
|
2347
|
+
this.drawingEditor = new DrawingEditor(this);
|
|
2348
|
+
this.chart.on("dataZoom", (params) => {
|
|
2349
|
+
this.events.emit("chart:dataZoom", params);
|
|
2350
|
+
const triggerOn = this.options.databox?.triggerOn;
|
|
2351
|
+
const position = this.options.databox?.position;
|
|
2352
|
+
if (triggerOn === "click" && position === "floating") {
|
|
2353
|
+
this.chart.dispatchAction({
|
|
2354
|
+
type: "hideTip"
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
});
|
|
2358
|
+
this.chart.on("finished", (params) => this.events.emit("chart:updated", params));
|
|
2359
|
+
this.chart.getZr().on("mousedown", (params) => this.events.emit("mouse:down", params));
|
|
2360
|
+
this.chart.getZr().on("mousemove", (params) => this.events.emit("mouse:move", params));
|
|
2361
|
+
this.chart.getZr().on("mouseup", (params) => this.events.emit("mouse:up", params));
|
|
2362
|
+
this.chart.getZr().on("click", (params) => this.events.emit("mouse:click", params));
|
|
2363
|
+
const zr = this.chart.getZr();
|
|
2364
|
+
const originalSetCursorStyle = zr.setCursorStyle;
|
|
2365
|
+
zr.setCursorStyle = function(cursorStyle) {
|
|
2366
|
+
if (cursorStyle === "grab") {
|
|
2367
|
+
cursorStyle = "crosshair";
|
|
2368
|
+
}
|
|
2369
|
+
originalSetCursorStyle.call(this, cursorStyle);
|
|
2370
|
+
};
|
|
2371
|
+
this.bindDrawingEvents();
|
|
2372
|
+
window.addEventListener("resize", this.resize.bind(this));
|
|
2373
|
+
document.addEventListener("fullscreenchange", this.onFullscreenChange);
|
|
2374
|
+
document.addEventListener("keydown", this.onKeyDown);
|
|
2375
|
+
}
|
|
2376
|
+
bindDrawingEvents() {
|
|
2377
|
+
let hideTimeout = null;
|
|
2378
|
+
const getDrawingInfo = (params) => {
|
|
2379
|
+
if (!params || params.componentType !== "series" || !params.seriesName?.startsWith("drawings")) {
|
|
2380
|
+
return null;
|
|
2381
|
+
}
|
|
2382
|
+
params.seriesIndex;
|
|
2383
|
+
const match = params.seriesName.match(/drawings-pane-(\d+)/);
|
|
2384
|
+
if (!match)
|
|
2385
|
+
return null;
|
|
2386
|
+
const paneIdx = parseInt(match[1]);
|
|
2387
|
+
const paneDrawings = this.drawings.filter((d) => (d.paneIndex || 0) === paneIdx);
|
|
2388
|
+
const drawing = paneDrawings[params.dataIndex];
|
|
2389
|
+
if (!drawing)
|
|
2390
|
+
return null;
|
|
2391
|
+
const targetName = params.event?.target?.name;
|
|
2392
|
+
return { drawing, targetName, paneIdx };
|
|
2393
|
+
};
|
|
2394
|
+
this.chart.on("mouseover", (params) => {
|
|
2395
|
+
const info = getDrawingInfo(params);
|
|
2396
|
+
if (!info)
|
|
2397
|
+
return;
|
|
2398
|
+
const group = params.event?.target?.parent;
|
|
2399
|
+
if (group) {
|
|
2400
|
+
const isSelected = info.drawing.id === this.selectedDrawingId;
|
|
2401
|
+
if (hideTimeout) {
|
|
2402
|
+
clearTimeout(hideTimeout);
|
|
2403
|
+
hideTimeout = null;
|
|
2404
|
+
}
|
|
2405
|
+
if (!isSelected) {
|
|
2406
|
+
group.children().forEach((child) => {
|
|
2407
|
+
if (child.name && child.name.startsWith("point")) {
|
|
2408
|
+
child.attr("style", { opacity: 1 });
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
if (info.targetName === "line") {
|
|
2414
|
+
this.events.emit("drawing:hover", {
|
|
2415
|
+
id: info.drawing.id,
|
|
2416
|
+
type: info.drawing.type
|
|
2417
|
+
});
|
|
2418
|
+
this.chart.getZr().setCursorStyle("move");
|
|
2419
|
+
} else if (info.targetName?.startsWith("point")) {
|
|
2420
|
+
const pointIdx = info.targetName === "point-start" ? 0 : 1;
|
|
2421
|
+
this.events.emit("drawing:point:hover", {
|
|
2422
|
+
id: info.drawing.id,
|
|
2423
|
+
pointIndex: pointIdx
|
|
2424
|
+
});
|
|
2425
|
+
this.chart.getZr().setCursorStyle("pointer");
|
|
2426
|
+
}
|
|
2427
|
+
});
|
|
2428
|
+
this.chart.on("mouseout", (params) => {
|
|
2429
|
+
const info = getDrawingInfo(params);
|
|
2430
|
+
if (!info)
|
|
2431
|
+
return;
|
|
2432
|
+
const group = params.event?.target?.parent;
|
|
2433
|
+
if (info.drawing.id === this.selectedDrawingId) {
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
hideTimeout = setTimeout(() => {
|
|
2437
|
+
if (group) {
|
|
2438
|
+
if (this.selectedDrawingId === info.drawing.id)
|
|
2439
|
+
return;
|
|
2440
|
+
group.children().forEach((child) => {
|
|
2441
|
+
if (child.name && child.name.startsWith("point")) {
|
|
2442
|
+
child.attr("style", { opacity: 0 });
|
|
2443
|
+
}
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
}, 50);
|
|
2447
|
+
if (info.targetName === "line") {
|
|
2448
|
+
this.events.emit("drawing:mouseout", { id: info.drawing.id });
|
|
2449
|
+
} else if (info.targetName?.startsWith("point")) {
|
|
2450
|
+
const pointIdx = info.targetName === "point-start" ? 0 : 1;
|
|
2451
|
+
this.events.emit("drawing:point:mouseout", {
|
|
2452
|
+
id: info.drawing.id,
|
|
2453
|
+
pointIndex: pointIdx
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
this.chart.getZr().setCursorStyle("default");
|
|
2457
|
+
});
|
|
2458
|
+
this.chart.on("mousedown", (params) => {
|
|
2459
|
+
const info = getDrawingInfo(params);
|
|
2460
|
+
if (!info)
|
|
2461
|
+
return;
|
|
2462
|
+
const event = params.event?.event || params.event;
|
|
2463
|
+
const x = event?.offsetX;
|
|
2464
|
+
const y = event?.offsetY;
|
|
2465
|
+
if (info.targetName === "line") {
|
|
2466
|
+
this.events.emit("drawing:mousedown", {
|
|
2467
|
+
id: info.drawing.id,
|
|
2468
|
+
x,
|
|
2469
|
+
y
|
|
2470
|
+
});
|
|
2471
|
+
} else if (info.targetName?.startsWith("point")) {
|
|
2472
|
+
const pointIdx = info.targetName === "point-start" ? 0 : 1;
|
|
2473
|
+
this.events.emit("drawing:point:mousedown", {
|
|
2474
|
+
id: info.drawing.id,
|
|
2475
|
+
pointIndex: pointIdx,
|
|
2476
|
+
x,
|
|
2477
|
+
y
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
});
|
|
2481
|
+
this.chart.on("click", (params) => {
|
|
2482
|
+
const info = getDrawingInfo(params);
|
|
2483
|
+
if (!info)
|
|
2484
|
+
return;
|
|
2485
|
+
if (this.selectedDrawingId !== info.drawing.id) {
|
|
2486
|
+
this.selectedDrawingId = info.drawing.id;
|
|
2487
|
+
this.events.emit("drawing:selected", { id: info.drawing.id });
|
|
2488
|
+
this.render();
|
|
2489
|
+
}
|
|
2490
|
+
if (info.targetName === "line") {
|
|
2491
|
+
this.events.emit("drawing:click", { id: info.drawing.id });
|
|
2492
|
+
} else if (info.targetName?.startsWith("point")) {
|
|
2493
|
+
const pointIdx = info.targetName === "point-start" ? 0 : 1;
|
|
2494
|
+
this.events.emit("drawing:point:click", {
|
|
2495
|
+
id: info.drawing.id,
|
|
2496
|
+
pointIndex: pointIdx
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
this.chart.getZr().on("click", (params) => {
|
|
2501
|
+
if (!params.target) {
|
|
2502
|
+
if (this.selectedDrawingId) {
|
|
2503
|
+
this.events.emit("drawing:deselected", { id: this.selectedDrawingId });
|
|
2504
|
+
this.selectedDrawingId = null;
|
|
2505
|
+
this.render();
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
// --- Plugin System Integration ---
|
|
2511
|
+
getChart() {
|
|
2512
|
+
return this.chart;
|
|
2513
|
+
}
|
|
2514
|
+
getMarketData() {
|
|
2515
|
+
return this.marketData;
|
|
2516
|
+
}
|
|
2517
|
+
getTimeToIndex() {
|
|
2518
|
+
return this.timeToIndex;
|
|
2519
|
+
}
|
|
2520
|
+
getOptions() {
|
|
2521
|
+
return this.options;
|
|
2522
|
+
}
|
|
2523
|
+
disableTools() {
|
|
2524
|
+
this.pluginManager.deactivatePlugin();
|
|
2525
|
+
}
|
|
2526
|
+
registerPlugin(plugin) {
|
|
2527
|
+
this.pluginManager.register(plugin);
|
|
2528
|
+
}
|
|
2529
|
+
// --- Drawing System ---
|
|
2530
|
+
addDrawing(drawing) {
|
|
2531
|
+
this.drawings.push(drawing);
|
|
2532
|
+
this.render();
|
|
2533
|
+
}
|
|
2534
|
+
removeDrawing(id) {
|
|
2535
|
+
const index = this.drawings.findIndex((d) => d.id === id);
|
|
2536
|
+
if (index !== -1) {
|
|
2537
|
+
const drawing = this.drawings[index];
|
|
2538
|
+
this.drawings.splice(index, 1);
|
|
2539
|
+
this.events.emit("drawing:deleted", { id: drawing.id });
|
|
2540
|
+
this.render();
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
getDrawing(id) {
|
|
2544
|
+
return this.drawings.find((d) => d.id === id);
|
|
2545
|
+
}
|
|
2546
|
+
updateDrawing(drawing) {
|
|
2547
|
+
const index = this.drawings.findIndex((d) => d.id === drawing.id);
|
|
2548
|
+
if (index !== -1) {
|
|
2549
|
+
this.drawings[index] = drawing;
|
|
2550
|
+
this.render();
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
lockChart() {
|
|
2554
|
+
if (this.isLocked)
|
|
2555
|
+
return;
|
|
2556
|
+
this.isLocked = true;
|
|
2557
|
+
const option = this.chart.getOption();
|
|
2558
|
+
this.chart.setOption({
|
|
2559
|
+
dataZoom: option.dataZoom.map((dz) => ({ ...dz, disabled: true })),
|
|
2560
|
+
tooltip: { show: false }
|
|
2561
|
+
// Hide tooltip during drag
|
|
2562
|
+
// We can also disable series interaction if needed, but custom series is handled by us.
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
unlockChart() {
|
|
2566
|
+
if (!this.isLocked)
|
|
2567
|
+
return;
|
|
2568
|
+
this.isLocked = false;
|
|
2569
|
+
const option = this.chart.getOption();
|
|
2570
|
+
const dzConfig = this.options.dataZoom || {};
|
|
2571
|
+
dzConfig.visible ?? true;
|
|
2572
|
+
if (option.dataZoom) {
|
|
2573
|
+
this.chart.setOption({
|
|
2574
|
+
dataZoom: option.dataZoom.map((dz) => ({
|
|
2575
|
+
...dz,
|
|
2576
|
+
disabled: false
|
|
2577
|
+
})),
|
|
2578
|
+
tooltip: { show: true }
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
// --------------------------------
|
|
2583
|
+
setZoom(start, end) {
|
|
2584
|
+
this.chart.dispatchAction({
|
|
2585
|
+
type: "dataZoom",
|
|
2586
|
+
start,
|
|
2587
|
+
end
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
setMarketData(data) {
|
|
2591
|
+
this.marketData = data;
|
|
2592
|
+
this.rebuildTimeIndex();
|
|
2593
|
+
this.render();
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
2596
|
+
* Update market data incrementally without full re-render
|
|
2597
|
+
* Merges new/updated OHLCV data with existing data by timestamp
|
|
2598
|
+
*
|
|
2599
|
+
* @param data - Array of OHLCV data to merge
|
|
2600
|
+
*
|
|
2601
|
+
* @remarks
|
|
2602
|
+
* **Performance Optimization**: This method only triggers a chart update if the data array contains
|
|
2603
|
+
* new or modified bars. If an empty array is passed, no update occurs (expected behavior).
|
|
2604
|
+
*
|
|
2605
|
+
* **Usage Pattern for Updating Indicators**:
|
|
2606
|
+
* When updating both market data and indicators, follow this order:
|
|
2607
|
+
*
|
|
2608
|
+
* 1. Update indicator data first using `indicator.updateData(plots)`
|
|
2609
|
+
* 2. Then call `chart.updateData(newBars)` with the new/modified market data
|
|
2610
|
+
*
|
|
2611
|
+
* The chart update will trigger a re-render that includes the updated indicator data.
|
|
2612
|
+
*
|
|
2613
|
+
* **Important**: If you update indicator data without updating market data (e.g., recalculation
|
|
2614
|
+
* with same bars), you must still call `chart.updateData([...])` with at least one bar
|
|
2615
|
+
* to trigger the re-render. Calling with an empty array will NOT trigger an update.
|
|
2616
|
+
*
|
|
2617
|
+
* @example
|
|
2618
|
+
* ```typescript
|
|
2619
|
+
* // Step 1: Update indicator data
|
|
2620
|
+
* macdIndicator.updateData({
|
|
2621
|
+
* macd: { data: [{ time: 1234567890, value: 150 }], options: { style: 'line', color: '#2962FF' } }
|
|
2622
|
+
* });
|
|
2623
|
+
*
|
|
2624
|
+
* // Step 2: Update market data (triggers re-render with new indicator data)
|
|
2625
|
+
* chart.updateData([
|
|
2626
|
+
* { time: 1234567890, open: 100, high: 105, low: 99, close: 103, volume: 1000 }
|
|
2627
|
+
* ]);
|
|
2628
|
+
* ```
|
|
2629
|
+
*
|
|
2630
|
+
* @example
|
|
2631
|
+
* ```typescript
|
|
2632
|
+
* // If only updating existing bar (e.g., real-time tick updates):
|
|
2633
|
+
* const lastBar = { ...existingBar, close: newPrice, high: Math.max(existingBar.high, newPrice) };
|
|
2634
|
+
* chart.updateData([lastBar]); // Updates by timestamp
|
|
2635
|
+
* ```
|
|
2636
|
+
*/
|
|
2637
|
+
updateData(data) {
|
|
2638
|
+
if (data.length === 0)
|
|
2639
|
+
return;
|
|
2640
|
+
const existingTimeMap = /* @__PURE__ */ new Map();
|
|
2641
|
+
this.marketData.forEach((bar) => {
|
|
2642
|
+
existingTimeMap.set(bar.time, bar);
|
|
2643
|
+
});
|
|
2644
|
+
data.forEach((bar) => {
|
|
2645
|
+
if (!existingTimeMap.has(bar.time)) ;
|
|
2646
|
+
existingTimeMap.set(bar.time, bar);
|
|
2647
|
+
});
|
|
2648
|
+
this.marketData = Array.from(existingTimeMap.values()).sort((a, b) => a.time - b.time);
|
|
2649
|
+
this.rebuildTimeIndex();
|
|
2650
|
+
const paddingPoints = this.dataIndexOffset;
|
|
2651
|
+
const candlestickSeries = SeriesBuilder.buildCandlestickSeries(this.marketData, this.options);
|
|
2652
|
+
const emptyCandle = { value: [NaN, NaN, NaN, NaN], itemStyle: { opacity: 0 } };
|
|
2653
|
+
const paddedCandlestickData = [
|
|
2654
|
+
...Array(paddingPoints).fill(emptyCandle),
|
|
2655
|
+
...candlestickSeries.data,
|
|
2656
|
+
...Array(paddingPoints).fill(emptyCandle)
|
|
2657
|
+
];
|
|
2658
|
+
const categoryData = [
|
|
2659
|
+
...Array(paddingPoints).fill(""),
|
|
2660
|
+
...this.marketData.map((k) => new Date(k.time).toLocaleString()),
|
|
2661
|
+
...Array(paddingPoints).fill("")
|
|
2662
|
+
];
|
|
2663
|
+
const currentOption = this.chart.getOption();
|
|
2664
|
+
const layout = LayoutManager.calculate(
|
|
2665
|
+
this.chart.getHeight(),
|
|
2666
|
+
this.indicators,
|
|
2667
|
+
this.options,
|
|
2668
|
+
this.isMainCollapsed,
|
|
2669
|
+
this.maximizedPaneId,
|
|
2670
|
+
this.marketData
|
|
2671
|
+
);
|
|
2672
|
+
const paddedOHLCVForShapes = [...Array(paddingPoints).fill(null), ...this.marketData, ...Array(paddingPoints).fill(null)];
|
|
2673
|
+
const { series: indicatorSeries, barColors } = SeriesBuilder.buildIndicatorSeries(
|
|
2674
|
+
this.indicators,
|
|
2675
|
+
this.timeToIndex,
|
|
2676
|
+
layout.paneLayout,
|
|
2677
|
+
categoryData.length,
|
|
2678
|
+
paddingPoints,
|
|
2679
|
+
paddedOHLCVForShapes,
|
|
2680
|
+
// Pass padded OHLCV data
|
|
2681
|
+
layout.overlayYAxisMap,
|
|
2682
|
+
// Pass overlay Y-axis mapping
|
|
2683
|
+
layout.separatePaneYAxisOffset
|
|
2684
|
+
// Pass Y-axis offset for separate panes
|
|
2685
|
+
);
|
|
2686
|
+
const coloredCandlestickData = paddedCandlestickData.map((candle, i) => {
|
|
2687
|
+
if (barColors[i]) {
|
|
2688
|
+
return {
|
|
2689
|
+
value: candle.value || candle,
|
|
2690
|
+
itemStyle: {
|
|
2691
|
+
color: barColors[i],
|
|
2692
|
+
color0: barColors[i],
|
|
2693
|
+
borderColor: barColors[i],
|
|
2694
|
+
borderColor0: barColors[i]
|
|
2695
|
+
}
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
return candle;
|
|
2699
|
+
});
|
|
2700
|
+
const updateOption = {
|
|
2701
|
+
xAxis: currentOption.xAxis.map((axis, index) => ({
|
|
2702
|
+
data: categoryData
|
|
2703
|
+
})),
|
|
2704
|
+
series: [
|
|
2705
|
+
{
|
|
2706
|
+
data: coloredCandlestickData,
|
|
2707
|
+
markLine: candlestickSeries.markLine
|
|
2708
|
+
// Ensure markLine is updated
|
|
2709
|
+
},
|
|
2710
|
+
...indicatorSeries.map((s) => {
|
|
2711
|
+
const update = { data: s.data };
|
|
2712
|
+
if (s.renderItem) {
|
|
2713
|
+
update.renderItem = s.renderItem;
|
|
2714
|
+
}
|
|
2715
|
+
return update;
|
|
2716
|
+
})
|
|
2717
|
+
]
|
|
2718
|
+
};
|
|
2719
|
+
this.chart.setOption(updateOption, { notMerge: false });
|
|
2720
|
+
this.startCountdown();
|
|
2721
|
+
}
|
|
2722
|
+
startCountdown() {
|
|
2723
|
+
this.stopCountdown();
|
|
2724
|
+
if (!this.options.lastPriceLine?.showCountdown || !this.options.interval || this.marketData.length === 0) {
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
const updateLabel = () => {
|
|
2728
|
+
if (this.marketData.length === 0)
|
|
2729
|
+
return;
|
|
2730
|
+
const lastBar = this.marketData[this.marketData.length - 1];
|
|
2731
|
+
const nextCloseTime = lastBar.time + (this.options.interval || 0);
|
|
2732
|
+
const now = Date.now();
|
|
2733
|
+
const diff = nextCloseTime - now;
|
|
2734
|
+
if (diff <= 0) {
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
const absDiff = Math.abs(diff);
|
|
2738
|
+
const hours = Math.floor(absDiff / 36e5);
|
|
2739
|
+
const minutes = Math.floor(absDiff % 36e5 / 6e4);
|
|
2740
|
+
const seconds = Math.floor(absDiff % 6e4 / 1e3);
|
|
2741
|
+
const timeString = `${hours > 0 ? hours.toString().padStart(2, "0") + ":" : ""}${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
2742
|
+
const currentOption = this.chart.getOption();
|
|
2743
|
+
if (!currentOption || !currentOption.series)
|
|
2744
|
+
return;
|
|
2745
|
+
const candleSeriesIndex = currentOption.series.findIndex((s) => s.type === "candlestick");
|
|
2746
|
+
if (candleSeriesIndex === -1)
|
|
2747
|
+
return;
|
|
2748
|
+
const candleSeries = currentOption.series[candleSeriesIndex];
|
|
2749
|
+
if (!candleSeries.markLine || !candleSeries.markLine.data || !candleSeries.markLine.data[0])
|
|
2750
|
+
return;
|
|
2751
|
+
const markLineData = candleSeries.markLine.data[0];
|
|
2752
|
+
markLineData.label.formatter;
|
|
2753
|
+
const price = markLineData.yAxis;
|
|
2754
|
+
let priceStr = "";
|
|
2755
|
+
if (this.options.yAxisLabelFormatter) {
|
|
2756
|
+
priceStr = this.options.yAxisLabelFormatter(price);
|
|
2757
|
+
} else {
|
|
2758
|
+
const decimals = this.options.yAxisDecimalPlaces !== void 0 ? this.options.yAxisDecimalPlaces : 2;
|
|
2759
|
+
priceStr = typeof price === "number" ? price.toFixed(decimals) : price;
|
|
2760
|
+
}
|
|
2761
|
+
const labelText = `${priceStr}
|
|
2762
|
+
${timeString}`;
|
|
2763
|
+
this.chart.setOption({
|
|
2764
|
+
series: [
|
|
2765
|
+
{
|
|
2766
|
+
name: this.options.title || "Market",
|
|
2767
|
+
markLine: {
|
|
2768
|
+
data: [
|
|
2769
|
+
{
|
|
2770
|
+
...markLineData,
|
|
2771
|
+
// Preserve lineStyle (color), symbol, yAxis, etc.
|
|
2772
|
+
label: {
|
|
2773
|
+
...markLineData.label,
|
|
2774
|
+
// Preserve existing label styles including backgroundColor
|
|
2775
|
+
formatter: labelText
|
|
2776
|
+
// Update only the text
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
]
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
]
|
|
2783
|
+
});
|
|
2784
|
+
};
|
|
2785
|
+
updateLabel();
|
|
2786
|
+
this.countdownInterval = setInterval(updateLabel, 1e3);
|
|
2787
|
+
}
|
|
2788
|
+
stopCountdown() {
|
|
2789
|
+
if (this.countdownInterval) {
|
|
2790
|
+
clearInterval(this.countdownInterval);
|
|
2791
|
+
this.countdownInterval = null;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
addIndicator(id, plots, options = {}) {
|
|
2795
|
+
const isOverlay = options.overlay !== void 0 ? options.overlay : options.isOverlay ?? false;
|
|
2796
|
+
let paneIndex = 0;
|
|
2797
|
+
if (!isOverlay) {
|
|
2798
|
+
let maxPaneIndex = 0;
|
|
2799
|
+
this.indicators.forEach((ind) => {
|
|
2800
|
+
if (ind.paneIndex > maxPaneIndex) {
|
|
2801
|
+
maxPaneIndex = ind.paneIndex;
|
|
2802
|
+
}
|
|
2803
|
+
});
|
|
2804
|
+
paneIndex = maxPaneIndex + 1;
|
|
2805
|
+
}
|
|
2806
|
+
const indicator = new Indicator(id, plots, paneIndex, {
|
|
2807
|
+
height: options.height,
|
|
2808
|
+
collapsed: false,
|
|
2809
|
+
titleColor: options.titleColor,
|
|
2810
|
+
controls: options.controls
|
|
2811
|
+
});
|
|
2812
|
+
this.indicators.set(id, indicator);
|
|
2813
|
+
this.render();
|
|
2814
|
+
return indicator;
|
|
2815
|
+
}
|
|
2816
|
+
/** @deprecated Use addIndicator instead */
|
|
2817
|
+
setIndicator(id, plot, isOverlay = false) {
|
|
2818
|
+
this.addIndicator(id, { [id]: plot }, { overlay: isOverlay });
|
|
2819
|
+
}
|
|
2820
|
+
removeIndicator(id) {
|
|
2821
|
+
this.indicators.delete(id);
|
|
2822
|
+
this.render();
|
|
2823
|
+
}
|
|
2824
|
+
toggleIndicator(id, action = "collapse") {
|
|
2825
|
+
if (action === "fullscreen") {
|
|
2826
|
+
if (document.fullscreenElement) {
|
|
2827
|
+
document.exitFullscreen();
|
|
2828
|
+
} else {
|
|
2829
|
+
this.rootContainer.requestFullscreen();
|
|
2830
|
+
}
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
if (action === "maximize") {
|
|
2834
|
+
if (this.maximizedPaneId === id) {
|
|
2835
|
+
this.maximizedPaneId = null;
|
|
2836
|
+
} else {
|
|
2837
|
+
this.maximizedPaneId = id;
|
|
2838
|
+
}
|
|
2839
|
+
this.render();
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
if (id === "main") {
|
|
2843
|
+
this.isMainCollapsed = !this.isMainCollapsed;
|
|
2844
|
+
this.render();
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
const indicator = this.indicators.get(id);
|
|
2848
|
+
if (indicator) {
|
|
2849
|
+
indicator.toggleCollapse();
|
|
2850
|
+
this.render();
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
resize() {
|
|
2854
|
+
this.chart.resize();
|
|
2855
|
+
}
|
|
2856
|
+
destroy() {
|
|
2857
|
+
this.stopCountdown();
|
|
2858
|
+
window.removeEventListener("resize", this.resize.bind(this));
|
|
2859
|
+
document.removeEventListener("fullscreenchange", this.onFullscreenChange);
|
|
2860
|
+
document.removeEventListener("keydown", this.onKeyDown);
|
|
2861
|
+
this.pluginManager.deactivatePlugin();
|
|
2862
|
+
this.pluginManager.destroy();
|
|
2863
|
+
this.chart.dispose();
|
|
2864
|
+
}
|
|
2865
|
+
rebuildTimeIndex() {
|
|
2866
|
+
this.timeToIndex.clear();
|
|
2867
|
+
this.marketData.forEach((k, index) => {
|
|
2868
|
+
this.timeToIndex.set(k.time, index);
|
|
2869
|
+
});
|
|
2870
|
+
const dataLength = this.marketData.length;
|
|
2871
|
+
const paddingPoints = Math.ceil(dataLength * this.padding);
|
|
2872
|
+
this.dataIndexOffset = paddingPoints;
|
|
2873
|
+
}
|
|
2874
|
+
render() {
|
|
2875
|
+
if (this.marketData.length === 0)
|
|
2876
|
+
return;
|
|
2877
|
+
let currentZoomState = null;
|
|
2878
|
+
try {
|
|
2879
|
+
const currentOption = this.chart.getOption();
|
|
2880
|
+
if (currentOption && currentOption.dataZoom && currentOption.dataZoom.length > 0) {
|
|
2881
|
+
const zoomComponent = currentOption.dataZoom.find((dz) => dz.type === "slider" || dz.type === "inside");
|
|
2882
|
+
if (zoomComponent) {
|
|
2883
|
+
currentZoomState = {
|
|
2884
|
+
start: zoomComponent.start,
|
|
2885
|
+
end: zoomComponent.end
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
} catch (e) {
|
|
2890
|
+
}
|
|
2891
|
+
const tooltipPos = this.options.databox?.position;
|
|
2892
|
+
const prevLeftDisplay = this.leftSidebar.style.display;
|
|
2893
|
+
const prevRightDisplay = this.rightSidebar.style.display;
|
|
2894
|
+
const newLeftDisplay = tooltipPos === "left" ? "block" : "none";
|
|
2895
|
+
const newRightDisplay = tooltipPos === "right" ? "block" : "none";
|
|
2896
|
+
if (prevLeftDisplay !== newLeftDisplay || prevRightDisplay !== newRightDisplay) {
|
|
2897
|
+
this.leftSidebar.style.display = newLeftDisplay;
|
|
2898
|
+
this.rightSidebar.style.display = newRightDisplay;
|
|
2899
|
+
this.chart.resize();
|
|
2900
|
+
}
|
|
2901
|
+
const paddingPoints = this.dataIndexOffset;
|
|
2902
|
+
const categoryData = [
|
|
2903
|
+
...Array(paddingPoints).fill(""),
|
|
2904
|
+
// Left padding
|
|
2905
|
+
...this.marketData.map((k) => new Date(k.time).toLocaleString()),
|
|
2906
|
+
...Array(paddingPoints).fill("")
|
|
2907
|
+
// Right padding
|
|
2908
|
+
];
|
|
2909
|
+
const layout = LayoutManager.calculate(
|
|
2910
|
+
this.chart.getHeight(),
|
|
2911
|
+
this.indicators,
|
|
2912
|
+
this.options,
|
|
2913
|
+
this.isMainCollapsed,
|
|
2914
|
+
this.maximizedPaneId,
|
|
2915
|
+
this.marketData
|
|
2916
|
+
);
|
|
2917
|
+
if (!currentZoomState && layout.dataZoom && this.marketData.length > 0) {
|
|
2918
|
+
const realDataLength = this.marketData.length;
|
|
2919
|
+
const totalLength = categoryData.length;
|
|
2920
|
+
const paddingRatio = paddingPoints / totalLength;
|
|
2921
|
+
const dataRatio = realDataLength / totalLength;
|
|
2922
|
+
layout.dataZoom.forEach((dz) => {
|
|
2923
|
+
if (dz.start !== void 0) {
|
|
2924
|
+
const userStartFraction = dz.start / 100;
|
|
2925
|
+
const actualStartFraction = paddingRatio + userStartFraction * dataRatio;
|
|
2926
|
+
dz.start = actualStartFraction * 100;
|
|
2927
|
+
}
|
|
2928
|
+
if (dz.end !== void 0) {
|
|
2929
|
+
const userEndFraction = dz.end / 100;
|
|
2930
|
+
const actualEndFraction = paddingRatio + userEndFraction * dataRatio;
|
|
2931
|
+
dz.end = actualEndFraction * 100;
|
|
2932
|
+
}
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
if (currentZoomState && layout.dataZoom) {
|
|
2936
|
+
layout.dataZoom.forEach((dz) => {
|
|
2937
|
+
dz.start = currentZoomState.start;
|
|
2938
|
+
dz.end = currentZoomState.end;
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
layout.xAxis.forEach((axis) => {
|
|
2942
|
+
axis.data = categoryData;
|
|
2943
|
+
axis.boundaryGap = false;
|
|
2944
|
+
});
|
|
2945
|
+
const candlestickSeries = SeriesBuilder.buildCandlestickSeries(this.marketData, this.options);
|
|
2946
|
+
const emptyCandle = { value: [NaN, NaN, NaN, NaN], itemStyle: { opacity: 0 } };
|
|
2947
|
+
candlestickSeries.data = [...Array(paddingPoints).fill(emptyCandle), ...candlestickSeries.data, ...Array(paddingPoints).fill(emptyCandle)];
|
|
2948
|
+
const paddedOHLCVForShapes = [...Array(paddingPoints).fill(null), ...this.marketData, ...Array(paddingPoints).fill(null)];
|
|
2949
|
+
const { series: indicatorSeries, barColors } = SeriesBuilder.buildIndicatorSeries(
|
|
2950
|
+
this.indicators,
|
|
2951
|
+
this.timeToIndex,
|
|
2952
|
+
layout.paneLayout,
|
|
2953
|
+
categoryData.length,
|
|
2954
|
+
paddingPoints,
|
|
2955
|
+
paddedOHLCVForShapes,
|
|
2956
|
+
// Pass padded OHLCV
|
|
2957
|
+
layout.overlayYAxisMap,
|
|
2958
|
+
// Pass overlay Y-axis mapping
|
|
2959
|
+
layout.separatePaneYAxisOffset
|
|
2960
|
+
// Pass Y-axis offset for separate panes
|
|
2961
|
+
);
|
|
2962
|
+
candlestickSeries.data = candlestickSeries.data.map((candle, i) => {
|
|
2963
|
+
if (barColors[i]) {
|
|
2964
|
+
return {
|
|
2965
|
+
value: candle.value || candle,
|
|
2966
|
+
itemStyle: {
|
|
2967
|
+
color: barColors[i],
|
|
2968
|
+
color0: barColors[i],
|
|
2969
|
+
borderColor: barColors[i],
|
|
2970
|
+
borderColor0: barColors[i]
|
|
2971
|
+
}
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
return candle;
|
|
2975
|
+
});
|
|
2976
|
+
const graphic = GraphicBuilder.build(layout, this.options, this.toggleIndicator.bind(this), this.isMainCollapsed, this.maximizedPaneId);
|
|
2977
|
+
const drawingsByPane = /* @__PURE__ */ new Map();
|
|
2978
|
+
this.drawings.forEach((d) => {
|
|
2979
|
+
const paneIdx = d.paneIndex || 0;
|
|
2980
|
+
if (!drawingsByPane.has(paneIdx)) {
|
|
2981
|
+
drawingsByPane.set(paneIdx, []);
|
|
2982
|
+
}
|
|
2983
|
+
drawingsByPane.get(paneIdx).push(d);
|
|
2984
|
+
});
|
|
2985
|
+
const drawingSeriesList = [];
|
|
2986
|
+
drawingsByPane.forEach((drawings, paneIndex) => {
|
|
2987
|
+
drawingSeriesList.push({
|
|
2988
|
+
type: "custom",
|
|
2989
|
+
name: `drawings-pane-${paneIndex}`,
|
|
2990
|
+
xAxisIndex: paneIndex,
|
|
2991
|
+
yAxisIndex: paneIndex,
|
|
2992
|
+
clip: true,
|
|
2993
|
+
renderItem: (params, api) => {
|
|
2994
|
+
const drawing = drawings[params.dataIndex];
|
|
2995
|
+
if (!drawing)
|
|
2996
|
+
return;
|
|
2997
|
+
const start = drawing.points[0];
|
|
2998
|
+
const end = drawing.points[1];
|
|
2999
|
+
if (!start || !end)
|
|
3000
|
+
return;
|
|
3001
|
+
const p1 = api.coord([start.timeIndex, start.value]);
|
|
3002
|
+
const p2 = api.coord([end.timeIndex, end.value]);
|
|
3003
|
+
const isSelected = drawing.id === this.selectedDrawingId;
|
|
3004
|
+
if (drawing.type === "line") {
|
|
3005
|
+
return {
|
|
3006
|
+
type: "group",
|
|
3007
|
+
children: [
|
|
3008
|
+
{
|
|
3009
|
+
type: "line",
|
|
3010
|
+
name: "line",
|
|
3011
|
+
shape: {
|
|
3012
|
+
x1: p1[0],
|
|
3013
|
+
y1: p1[1],
|
|
3014
|
+
x2: p2[0],
|
|
3015
|
+
y2: p2[1]
|
|
3016
|
+
},
|
|
3017
|
+
style: {
|
|
3018
|
+
stroke: drawing.style?.color || "#3b82f6",
|
|
3019
|
+
lineWidth: drawing.style?.lineWidth || 2
|
|
3020
|
+
}
|
|
3021
|
+
},
|
|
3022
|
+
{
|
|
3023
|
+
type: "circle",
|
|
3024
|
+
name: "point-start",
|
|
3025
|
+
shape: { cx: p1[0], cy: p1[1], r: 4 },
|
|
3026
|
+
style: {
|
|
3027
|
+
fill: "#fff",
|
|
3028
|
+
stroke: drawing.style?.color || "#3b82f6",
|
|
3029
|
+
lineWidth: 1,
|
|
3030
|
+
opacity: isSelected ? 1 : 0
|
|
3031
|
+
// Show if selected
|
|
3032
|
+
}
|
|
3033
|
+
},
|
|
3034
|
+
{
|
|
3035
|
+
type: "circle",
|
|
3036
|
+
name: "point-end",
|
|
3037
|
+
shape: { cx: p2[0], cy: p2[1], r: 4 },
|
|
3038
|
+
style: {
|
|
3039
|
+
fill: "#fff",
|
|
3040
|
+
stroke: drawing.style?.color || "#3b82f6",
|
|
3041
|
+
lineWidth: 1,
|
|
3042
|
+
opacity: isSelected ? 1 : 0
|
|
3043
|
+
// Show if selected
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
]
|
|
3047
|
+
};
|
|
3048
|
+
} else if (drawing.type === "fibonacci") {
|
|
3049
|
+
const x1 = p1[0];
|
|
3050
|
+
const y1 = p1[1];
|
|
3051
|
+
const x2 = p2[0];
|
|
3052
|
+
const y2 = p2[1];
|
|
3053
|
+
const startX = Math.min(x1, x2);
|
|
3054
|
+
const endX = Math.max(x1, x2);
|
|
3055
|
+
const width = endX - startX;
|
|
3056
|
+
const diffY = y2 - y1;
|
|
3057
|
+
const levels = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
|
|
3058
|
+
const colors = ["#787b86", "#f44336", "#ff9800", "#4caf50", "#2196f3", "#00bcd4", "#787b86"];
|
|
3059
|
+
const children = [];
|
|
3060
|
+
children.push({
|
|
3061
|
+
type: "line",
|
|
3062
|
+
name: "line",
|
|
3063
|
+
// Use 'line' name to enable dragging logic in DrawingEditor
|
|
3064
|
+
shape: { x1, y1, x2, y2 },
|
|
3065
|
+
style: {
|
|
3066
|
+
stroke: "#999",
|
|
3067
|
+
lineWidth: 1,
|
|
3068
|
+
lineDash: [4, 4]
|
|
3069
|
+
}
|
|
3070
|
+
});
|
|
3071
|
+
children.push({
|
|
3072
|
+
type: "circle",
|
|
3073
|
+
name: "point-start",
|
|
3074
|
+
shape: { cx: x1, cy: y1, r: 4 },
|
|
3075
|
+
style: {
|
|
3076
|
+
fill: "#fff",
|
|
3077
|
+
stroke: drawing.style?.color || "#3b82f6",
|
|
3078
|
+
lineWidth: 1,
|
|
3079
|
+
opacity: isSelected ? 1 : 0
|
|
3080
|
+
},
|
|
3081
|
+
z: 100
|
|
3082
|
+
// Ensure on top
|
|
3083
|
+
});
|
|
3084
|
+
children.push({
|
|
3085
|
+
type: "circle",
|
|
3086
|
+
name: "point-end",
|
|
3087
|
+
shape: { cx: x2, cy: y2, r: 4 },
|
|
3088
|
+
style: {
|
|
3089
|
+
fill: "#fff",
|
|
3090
|
+
stroke: drawing.style?.color || "#3b82f6",
|
|
3091
|
+
lineWidth: 1,
|
|
3092
|
+
opacity: isSelected ? 1 : 0
|
|
3093
|
+
},
|
|
3094
|
+
z: 100
|
|
3095
|
+
});
|
|
3096
|
+
levels.forEach((level, index) => {
|
|
3097
|
+
const levelY = y2 - diffY * level;
|
|
3098
|
+
const color = colors[index % colors.length];
|
|
3099
|
+
children.push({
|
|
3100
|
+
type: "line",
|
|
3101
|
+
name: "fib-line",
|
|
3102
|
+
// distinct name, maybe we don't want to drag by clicking these lines? or yes? 'line' triggers drag. 'fib-line' won't unless we update logic.
|
|
3103
|
+
// The user asked for "fib levels between start and end".
|
|
3104
|
+
shape: { x1: startX, y1: levelY, x2: endX, y2: levelY },
|
|
3105
|
+
style: { stroke: color, lineWidth: 1 },
|
|
3106
|
+
silent: true
|
|
3107
|
+
// Make internal lines silent so clicks pass to background/diagonal?
|
|
3108
|
+
});
|
|
3109
|
+
const startVal = drawing.points[0].value;
|
|
3110
|
+
const endVal = drawing.points[1].value;
|
|
3111
|
+
const valDiff = endVal - startVal;
|
|
3112
|
+
const price = endVal - valDiff * level;
|
|
3113
|
+
children.push({
|
|
3114
|
+
type: "text",
|
|
3115
|
+
style: {
|
|
3116
|
+
text: `${level} (${price.toFixed(2)})`,
|
|
3117
|
+
x: startX + 5,
|
|
3118
|
+
y: levelY - 10,
|
|
3119
|
+
fill: color,
|
|
3120
|
+
fontSize: 10
|
|
3121
|
+
},
|
|
3122
|
+
silent: true
|
|
3123
|
+
});
|
|
3124
|
+
if (index < levels.length - 1) {
|
|
3125
|
+
const nextLevel = levels[index + 1];
|
|
3126
|
+
const nextY = y2 - diffY * nextLevel;
|
|
3127
|
+
const rectH = Math.abs(nextY - levelY);
|
|
3128
|
+
const rectY = Math.min(levelY, nextY);
|
|
3129
|
+
children.push({
|
|
3130
|
+
type: "rect",
|
|
3131
|
+
shape: { x: startX, y: rectY, width, height: rectH },
|
|
3132
|
+
style: {
|
|
3133
|
+
fill: colors[(index + 1) % colors.length],
|
|
3134
|
+
opacity: 0.1
|
|
3135
|
+
},
|
|
3136
|
+
silent: true
|
|
3137
|
+
// Let clicks pass through?
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
});
|
|
3141
|
+
const backgrounds = [];
|
|
3142
|
+
const linesAndText = [];
|
|
3143
|
+
levels.forEach((level, index) => {
|
|
3144
|
+
const levelY = y2 - diffY * level;
|
|
3145
|
+
const color = colors[index % colors.length];
|
|
3146
|
+
linesAndText.push({
|
|
3147
|
+
type: "line",
|
|
3148
|
+
shape: { x1: startX, y1: levelY, x2: endX, y2: levelY },
|
|
3149
|
+
style: { stroke: color, lineWidth: 1 },
|
|
3150
|
+
silent: true
|
|
3151
|
+
});
|
|
3152
|
+
const startVal = drawing.points[0].value;
|
|
3153
|
+
const endVal = drawing.points[1].value;
|
|
3154
|
+
const valDiff = endVal - startVal;
|
|
3155
|
+
const price = endVal - valDiff * level;
|
|
3156
|
+
linesAndText.push({
|
|
3157
|
+
type: "text",
|
|
3158
|
+
style: {
|
|
3159
|
+
text: `${level} (${price.toFixed(2)})`,
|
|
3160
|
+
x: startX + 5,
|
|
3161
|
+
y: levelY - 10,
|
|
3162
|
+
fill: color,
|
|
3163
|
+
fontSize: 10
|
|
3164
|
+
},
|
|
3165
|
+
silent: true
|
|
3166
|
+
});
|
|
3167
|
+
if (index < levels.length - 1) {
|
|
3168
|
+
const nextLevel = levels[index + 1];
|
|
3169
|
+
const nextY = y2 - diffY * nextLevel;
|
|
3170
|
+
const rectH = Math.abs(nextY - levelY);
|
|
3171
|
+
const rectY = Math.min(levelY, nextY);
|
|
3172
|
+
backgrounds.push({
|
|
3173
|
+
type: "rect",
|
|
3174
|
+
name: "line",
|
|
3175
|
+
// Enable dragging by clicking background!
|
|
3176
|
+
shape: { x: startX, y: rectY, width, height: rectH },
|
|
3177
|
+
style: {
|
|
3178
|
+
fill: colors[(index + 1) % colors.length],
|
|
3179
|
+
opacity: 0.1
|
|
3180
|
+
}
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
});
|
|
3184
|
+
return {
|
|
3185
|
+
type: "group",
|
|
3186
|
+
children: [
|
|
3187
|
+
...backgrounds,
|
|
3188
|
+
...linesAndText,
|
|
3189
|
+
{
|
|
3190
|
+
type: "line",
|
|
3191
|
+
name: "line",
|
|
3192
|
+
shape: { x1, y1, x2, y2 },
|
|
3193
|
+
style: { stroke: "#999", lineWidth: 1, lineDash: [4, 4] }
|
|
3194
|
+
},
|
|
3195
|
+
{
|
|
3196
|
+
type: "circle",
|
|
3197
|
+
name: "point-start",
|
|
3198
|
+
shape: { cx: x1, cy: y1, r: 4 },
|
|
3199
|
+
style: {
|
|
3200
|
+
fill: "#fff",
|
|
3201
|
+
stroke: drawing.style?.color || "#3b82f6",
|
|
3202
|
+
lineWidth: 1,
|
|
3203
|
+
opacity: isSelected ? 1 : 0
|
|
3204
|
+
},
|
|
3205
|
+
z: 100
|
|
3206
|
+
},
|
|
3207
|
+
{
|
|
3208
|
+
type: "circle",
|
|
3209
|
+
name: "point-end",
|
|
3210
|
+
shape: { cx: x2, cy: y2, r: 4 },
|
|
3211
|
+
style: {
|
|
3212
|
+
fill: "#fff",
|
|
3213
|
+
stroke: drawing.style?.color || "#3b82f6",
|
|
3214
|
+
lineWidth: 1,
|
|
3215
|
+
opacity: isSelected ? 1 : 0
|
|
3216
|
+
},
|
|
3217
|
+
z: 100
|
|
3218
|
+
}
|
|
3219
|
+
]
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
},
|
|
3223
|
+
data: drawings.map((d) => [d.points[0].timeIndex, d.points[0].value, d.points[1].timeIndex, d.points[1].value]),
|
|
3224
|
+
z: 100,
|
|
3225
|
+
silent: false
|
|
3226
|
+
});
|
|
3227
|
+
});
|
|
3228
|
+
const tooltipFormatter = (params) => {
|
|
3229
|
+
const html = TooltipFormatter.format(params, this.options);
|
|
3230
|
+
const mode = this.options.databox?.position;
|
|
3231
|
+
if (mode === "left") {
|
|
3232
|
+
this.leftSidebar.innerHTML = html;
|
|
3233
|
+
return "";
|
|
3234
|
+
}
|
|
3235
|
+
if (mode === "right") {
|
|
3236
|
+
this.rightSidebar.innerHTML = html;
|
|
3237
|
+
return "";
|
|
3238
|
+
}
|
|
3239
|
+
if (!this.options.databox) {
|
|
3240
|
+
return "";
|
|
3241
|
+
}
|
|
3242
|
+
return `<div style="min-width: 200px;">${html}</div>`;
|
|
3243
|
+
};
|
|
3244
|
+
const option = {
|
|
3245
|
+
backgroundColor: this.options.backgroundColor,
|
|
3246
|
+
animation: false,
|
|
3247
|
+
legend: {
|
|
3248
|
+
show: false
|
|
3249
|
+
// Hide default legend as we use tooltip
|
|
3250
|
+
},
|
|
3251
|
+
tooltip: {
|
|
3252
|
+
show: true,
|
|
3253
|
+
showContent: !!this.options.databox,
|
|
3254
|
+
// Show content only if databox is present
|
|
3255
|
+
trigger: "axis",
|
|
3256
|
+
triggerOn: this.options.databox?.triggerOn ?? "mousemove",
|
|
3257
|
+
// Control when to show tooltip/crosshair
|
|
3258
|
+
axisPointer: { type: "cross", label: { backgroundColor: "#475569" } },
|
|
3259
|
+
backgroundColor: "rgba(30, 41, 59, 0.9)",
|
|
3260
|
+
borderWidth: 1,
|
|
3261
|
+
borderColor: "#334155",
|
|
3262
|
+
padding: 10,
|
|
3263
|
+
textStyle: {
|
|
3264
|
+
color: "#fff",
|
|
3265
|
+
fontFamily: this.options.fontFamily || "sans-serif"
|
|
3266
|
+
},
|
|
3267
|
+
formatter: tooltipFormatter,
|
|
3268
|
+
extraCssText: tooltipPos !== "floating" && tooltipPos !== void 0 ? "display: none !important;" : void 0,
|
|
3269
|
+
position: (pos, params, el, elRect, size) => {
|
|
3270
|
+
const mode = this.options.databox?.position;
|
|
3271
|
+
if (mode === "floating") {
|
|
3272
|
+
const obj = { top: 10 };
|
|
3273
|
+
obj[["left", "right"][+(pos[0] < size.viewSize[0] / 2)]] = 30;
|
|
3274
|
+
return obj;
|
|
3275
|
+
}
|
|
3276
|
+
return null;
|
|
3277
|
+
}
|
|
3278
|
+
},
|
|
3279
|
+
axisPointer: {
|
|
3280
|
+
link: { xAxisIndex: "all" },
|
|
3281
|
+
label: { backgroundColor: "#475569" }
|
|
3282
|
+
},
|
|
3283
|
+
graphic,
|
|
3284
|
+
grid: layout.grid,
|
|
3285
|
+
xAxis: layout.xAxis,
|
|
3286
|
+
yAxis: layout.yAxis,
|
|
3287
|
+
dataZoom: layout.dataZoom,
|
|
3288
|
+
series: [candlestickSeries, ...indicatorSeries, ...drawingSeriesList]
|
|
3289
|
+
};
|
|
3290
|
+
this.chart.setOption(option, true);
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
var __defProp$3 = Object.defineProperty;
|
|
3295
|
+
var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3296
|
+
var __publicField$3 = (obj, key, value) => {
|
|
3297
|
+
__defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
3298
|
+
return value;
|
|
3299
|
+
};
|
|
3300
|
+
class AbstractPlugin {
|
|
3301
|
+
constructor(config) {
|
|
3302
|
+
__publicField$3(this, "id");
|
|
3303
|
+
__publicField$3(this, "name");
|
|
3304
|
+
__publicField$3(this, "icon");
|
|
3305
|
+
__publicField$3(this, "context");
|
|
3306
|
+
__publicField$3(this, "eventListeners", []);
|
|
3307
|
+
this.id = config.id;
|
|
3308
|
+
this.name = config.name;
|
|
3309
|
+
this.icon = config.icon;
|
|
3310
|
+
}
|
|
3311
|
+
init(context) {
|
|
3312
|
+
this.context = context;
|
|
3313
|
+
this.onInit();
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3316
|
+
* Lifecycle hook called after context is initialized.
|
|
3317
|
+
* Override this instead of init().
|
|
3318
|
+
*/
|
|
3319
|
+
onInit() {
|
|
3320
|
+
}
|
|
3321
|
+
activate() {
|
|
3322
|
+
this.onActivate();
|
|
3323
|
+
this.context.events.emit("plugin:activated", this.id);
|
|
3324
|
+
}
|
|
3325
|
+
/**
|
|
3326
|
+
* Lifecycle hook called when the plugin is activated.
|
|
3327
|
+
*/
|
|
3328
|
+
onActivate() {
|
|
3329
|
+
}
|
|
3330
|
+
deactivate() {
|
|
3331
|
+
this.onDeactivate();
|
|
3332
|
+
this.context.events.emit("plugin:deactivated", this.id);
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3335
|
+
* Lifecycle hook called when the plugin is deactivated.
|
|
3336
|
+
*/
|
|
3337
|
+
onDeactivate() {
|
|
3338
|
+
}
|
|
3339
|
+
destroy() {
|
|
3340
|
+
this.removeAllListeners();
|
|
3341
|
+
this.onDestroy();
|
|
3342
|
+
}
|
|
3343
|
+
/**
|
|
3344
|
+
* Lifecycle hook called when the plugin is destroyed.
|
|
3345
|
+
*/
|
|
3346
|
+
onDestroy() {
|
|
3347
|
+
}
|
|
3348
|
+
// --- Helper Methods ---
|
|
3349
|
+
/**
|
|
3350
|
+
* Register an event listener that will be automatically cleaned up on destroy.
|
|
3351
|
+
*/
|
|
3352
|
+
on(event, handler) {
|
|
3353
|
+
this.context.events.on(event, handler);
|
|
3354
|
+
this.eventListeners.push({ event, handler });
|
|
3355
|
+
}
|
|
3356
|
+
/**
|
|
3357
|
+
* Remove a specific event listener.
|
|
3358
|
+
*/
|
|
3359
|
+
off(event, handler) {
|
|
3360
|
+
this.context.events.off(event, handler);
|
|
3361
|
+
this.eventListeners = this.eventListeners.filter(
|
|
3362
|
+
(l) => l.event !== event || l.handler !== handler
|
|
3363
|
+
);
|
|
3364
|
+
}
|
|
3365
|
+
/**
|
|
3366
|
+
* Remove all listeners registered by this plugin.
|
|
3367
|
+
*/
|
|
3368
|
+
removeAllListeners() {
|
|
3369
|
+
this.eventListeners.forEach(({ event, handler }) => {
|
|
3370
|
+
this.context.events.off(event, handler);
|
|
3371
|
+
});
|
|
3372
|
+
this.eventListeners = [];
|
|
3373
|
+
}
|
|
3374
|
+
/**
|
|
3375
|
+
* Access to the ECharts instance.
|
|
3376
|
+
*/
|
|
3377
|
+
get chart() {
|
|
3378
|
+
return this.context.getChart();
|
|
3379
|
+
}
|
|
3380
|
+
/**
|
|
3381
|
+
* Access to market data.
|
|
3382
|
+
*/
|
|
3383
|
+
get marketData() {
|
|
3384
|
+
return this.context.getMarketData();
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
var __defProp$2 = Object.defineProperty;
|
|
3389
|
+
var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3390
|
+
var __publicField$2 = (obj, key, value) => {
|
|
3391
|
+
__defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
3392
|
+
return value;
|
|
3393
|
+
};
|
|
3394
|
+
class MeasureTool extends AbstractPlugin {
|
|
3395
|
+
// End Arrow
|
|
3396
|
+
constructor(options) {
|
|
3397
|
+
super({
|
|
3398
|
+
id: "measure",
|
|
3399
|
+
name: options?.name || "Measure",
|
|
3400
|
+
icon: options?.icon || `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M160-240q-33 0-56.5-23.5T80-320v-320q0-33 23.5-56.5T160-720h640q33 0 56.5 23.5T880-640v320q0 33-23.5 56.5T800-240H160Zm0-80h640v-320H680v160h-80v-160h-80v160h-80v-160h-80v160h-80v-160H160v320Zm120-160h80-80Zm160 0h80-80Zm160 0h80-80Zm-120 0Z"/></svg>`
|
|
3401
|
+
});
|
|
3402
|
+
__publicField$2(this, "zr");
|
|
3403
|
+
__publicField$2(this, "state", "idle");
|
|
3404
|
+
__publicField$2(this, "startPoint", null);
|
|
3405
|
+
__publicField$2(this, "endPoint", null);
|
|
3406
|
+
// ZRender Elements
|
|
3407
|
+
__publicField$2(this, "group", null);
|
|
3408
|
+
__publicField$2(this, "rect", null);
|
|
3409
|
+
// Measurement Box
|
|
3410
|
+
__publicField$2(this, "labelRect", null);
|
|
3411
|
+
// Label Background
|
|
3412
|
+
__publicField$2(this, "labelText", null);
|
|
3413
|
+
// Label Text
|
|
3414
|
+
__publicField$2(this, "lineV", null);
|
|
3415
|
+
// Vertical Arrow Line
|
|
3416
|
+
__publicField$2(this, "lineH", null);
|
|
3417
|
+
// Horizontal Arrow Line
|
|
3418
|
+
__publicField$2(this, "arrowStart", null);
|
|
3419
|
+
// Start Arrow
|
|
3420
|
+
__publicField$2(this, "arrowEnd", null);
|
|
3421
|
+
// --- Interaction Handlers ---
|
|
3422
|
+
__publicField$2(this, "onMouseDown", () => {
|
|
3423
|
+
if (this.state === "finished") {
|
|
3424
|
+
this.removeGraphic();
|
|
3425
|
+
}
|
|
3426
|
+
});
|
|
3427
|
+
__publicField$2(this, "onChartInteraction", () => {
|
|
3428
|
+
if (this.group) {
|
|
3429
|
+
this.removeGraphic();
|
|
3430
|
+
}
|
|
3431
|
+
});
|
|
3432
|
+
__publicField$2(this, "onClick", (params) => {
|
|
3433
|
+
if (this.state === "idle") {
|
|
3434
|
+
this.state = "drawing";
|
|
3435
|
+
this.startPoint = [params.offsetX, params.offsetY];
|
|
3436
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
3437
|
+
this.initGraphic();
|
|
3438
|
+
this.updateGraphic();
|
|
3439
|
+
} else if (this.state === "drawing") {
|
|
3440
|
+
this.state = "finished";
|
|
3441
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
3442
|
+
this.updateGraphic();
|
|
3443
|
+
this.context.disableTools();
|
|
3444
|
+
this.enableClearListeners();
|
|
3445
|
+
}
|
|
3446
|
+
});
|
|
3447
|
+
__publicField$2(this, "clearHandlers", {});
|
|
3448
|
+
__publicField$2(this, "onMouseMove", (params) => {
|
|
3449
|
+
if (this.state !== "drawing")
|
|
3450
|
+
return;
|
|
3451
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
3452
|
+
this.updateGraphic();
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
onInit() {
|
|
3456
|
+
this.zr = this.chart.getZr();
|
|
3457
|
+
}
|
|
3458
|
+
onActivate() {
|
|
3459
|
+
this.state = "idle";
|
|
3460
|
+
this.chart.getZr().setCursorStyle("crosshair");
|
|
3461
|
+
this.zr.on("click", this.onClick);
|
|
3462
|
+
this.zr.on("mousemove", this.onMouseMove);
|
|
3463
|
+
}
|
|
3464
|
+
onDeactivate() {
|
|
3465
|
+
this.state = "idle";
|
|
3466
|
+
this.chart.getZr().setCursorStyle("default");
|
|
3467
|
+
this.zr.off("click", this.onClick);
|
|
3468
|
+
this.zr.off("mousemove", this.onMouseMove);
|
|
3469
|
+
this.disableClearListeners();
|
|
3470
|
+
if (this.state === "drawing") {
|
|
3471
|
+
this.removeGraphic();
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
onDestroy() {
|
|
3475
|
+
this.removeGraphic();
|
|
3476
|
+
}
|
|
3477
|
+
enableClearListeners() {
|
|
3478
|
+
const clickHandler = () => {
|
|
3479
|
+
this.removeGraphic();
|
|
3480
|
+
};
|
|
3481
|
+
setTimeout(() => {
|
|
3482
|
+
this.zr.on("click", clickHandler);
|
|
3483
|
+
}, 10);
|
|
3484
|
+
this.zr.on("mousedown", this.onMouseDown);
|
|
3485
|
+
this.context.events.on("chart:dataZoom", this.onChartInteraction);
|
|
3486
|
+
this.clearHandlers = {
|
|
3487
|
+
click: clickHandler,
|
|
3488
|
+
mousedown: this.onMouseDown,
|
|
3489
|
+
dataZoom: this.onChartInteraction
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
disableClearListeners() {
|
|
3493
|
+
if (this.clearHandlers.click)
|
|
3494
|
+
this.zr.off("click", this.clearHandlers.click);
|
|
3495
|
+
if (this.clearHandlers.mousedown)
|
|
3496
|
+
this.zr.off("mousedown", this.clearHandlers.mousedown);
|
|
3497
|
+
if (this.clearHandlers.dataZoom) {
|
|
3498
|
+
this.context.events.off("chart:dataZoom", this.clearHandlers.dataZoom);
|
|
3499
|
+
}
|
|
3500
|
+
this.clearHandlers = {};
|
|
3501
|
+
}
|
|
3502
|
+
// --- Graphics ---
|
|
3503
|
+
initGraphic() {
|
|
3504
|
+
if (this.group)
|
|
3505
|
+
return;
|
|
3506
|
+
this.group = new echarts__namespace.graphic.Group();
|
|
3507
|
+
this.rect = new echarts__namespace.graphic.Rect({
|
|
3508
|
+
shape: { x: 0, y: 0, width: 0, height: 0 },
|
|
3509
|
+
style: { fill: "rgba(0,0,0,0)", stroke: "transparent", lineWidth: 0 },
|
|
3510
|
+
z: 100
|
|
3511
|
+
});
|
|
3512
|
+
this.lineV = new echarts__namespace.graphic.Line({
|
|
3513
|
+
shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
3514
|
+
style: { stroke: "#fff", lineWidth: 1, lineDash: [4, 4] },
|
|
3515
|
+
z: 101
|
|
3516
|
+
});
|
|
3517
|
+
this.lineH = new echarts__namespace.graphic.Line({
|
|
3518
|
+
shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
3519
|
+
style: { stroke: "#fff", lineWidth: 1, lineDash: [4, 4] },
|
|
3520
|
+
z: 101
|
|
3521
|
+
});
|
|
3522
|
+
this.arrowStart = new echarts__namespace.graphic.Polygon({
|
|
3523
|
+
shape: {
|
|
3524
|
+
points: [
|
|
3525
|
+
[0, 0],
|
|
3526
|
+
[-5, 10],
|
|
3527
|
+
[5, 10]
|
|
3528
|
+
]
|
|
3529
|
+
},
|
|
3530
|
+
style: { fill: "#fff" },
|
|
3531
|
+
z: 102
|
|
3532
|
+
});
|
|
3533
|
+
this.arrowEnd = new echarts__namespace.graphic.Polygon({
|
|
3534
|
+
shape: {
|
|
3535
|
+
points: [
|
|
3536
|
+
[0, 0],
|
|
3537
|
+
[-5, -10],
|
|
3538
|
+
[5, -10]
|
|
3539
|
+
]
|
|
3540
|
+
},
|
|
3541
|
+
style: { fill: "#fff" },
|
|
3542
|
+
z: 102
|
|
3543
|
+
});
|
|
3544
|
+
this.labelRect = new echarts__namespace.graphic.Rect({
|
|
3545
|
+
shape: { x: 0, y: 0, width: 0, height: 0, r: 4 },
|
|
3546
|
+
style: {
|
|
3547
|
+
fill: "transparent",
|
|
3548
|
+
stroke: "transparent",
|
|
3549
|
+
lineWidth: 0,
|
|
3550
|
+
shadowBlur: 5,
|
|
3551
|
+
shadowColor: "rgba(0,0,0,0.3)"
|
|
3552
|
+
},
|
|
3553
|
+
z: 102
|
|
3554
|
+
});
|
|
3555
|
+
this.labelText = new echarts__namespace.graphic.Text({
|
|
3556
|
+
style: {
|
|
3557
|
+
x: 0,
|
|
3558
|
+
y: 0,
|
|
3559
|
+
text: "",
|
|
3560
|
+
fill: "#fff",
|
|
3561
|
+
font: "12px sans-serif",
|
|
3562
|
+
align: "center",
|
|
3563
|
+
verticalAlign: "middle"
|
|
3564
|
+
},
|
|
3565
|
+
z: 103
|
|
3566
|
+
});
|
|
3567
|
+
this.group.add(this.rect);
|
|
3568
|
+
this.group.add(this.lineV);
|
|
3569
|
+
this.group.add(this.lineH);
|
|
3570
|
+
this.group.add(this.arrowStart);
|
|
3571
|
+
this.group.add(this.arrowEnd);
|
|
3572
|
+
this.group.add(this.labelRect);
|
|
3573
|
+
this.group.add(this.labelText);
|
|
3574
|
+
this.zr.add(this.group);
|
|
3575
|
+
}
|
|
3576
|
+
removeGraphic() {
|
|
3577
|
+
if (this.group) {
|
|
3578
|
+
this.zr.remove(this.group);
|
|
3579
|
+
this.group = null;
|
|
3580
|
+
this.disableClearListeners();
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
updateGraphic() {
|
|
3584
|
+
if (!this.startPoint || !this.endPoint || !this.group)
|
|
3585
|
+
return;
|
|
3586
|
+
const [x1, y1] = this.startPoint;
|
|
3587
|
+
const [x2, y2] = this.endPoint;
|
|
3588
|
+
const p1 = this.context.coordinateConversion.pixelToData({ x: x1, y: y1 });
|
|
3589
|
+
const p2 = this.context.coordinateConversion.pixelToData({ x: x2, y: y2 });
|
|
3590
|
+
if (!p1 || !p2)
|
|
3591
|
+
return;
|
|
3592
|
+
const idx1 = Math.round(p1.timeIndex);
|
|
3593
|
+
const idx2 = Math.round(p2.timeIndex);
|
|
3594
|
+
const val1 = p1.value;
|
|
3595
|
+
const val2 = p2.value;
|
|
3596
|
+
const bars = idx2 - idx1;
|
|
3597
|
+
const priceDiff = val2 - val1;
|
|
3598
|
+
const priceChangePercent = priceDiff / val1 * 100;
|
|
3599
|
+
const isUp = priceDiff >= 0;
|
|
3600
|
+
const color = isUp ? "rgba(33, 150, 243, 0.2)" : "rgba(236, 0, 0, 0.2)";
|
|
3601
|
+
const strokeColor = isUp ? "#2196F3" : "#ec0000";
|
|
3602
|
+
this.rect.setShape({
|
|
3603
|
+
x: Math.min(x1, x2),
|
|
3604
|
+
y: Math.min(y1, y2),
|
|
3605
|
+
width: Math.abs(x2 - x1),
|
|
3606
|
+
height: Math.abs(y2 - y1)
|
|
3607
|
+
});
|
|
3608
|
+
this.rect.setStyle({ fill: color });
|
|
3609
|
+
const midX = (x1 + x2) / 2;
|
|
3610
|
+
const midY = (y1 + y2) / 2;
|
|
3611
|
+
this.lineV.setShape({ x1: midX, y1, x2: midX, y2 });
|
|
3612
|
+
this.lineV.setStyle({ stroke: strokeColor });
|
|
3613
|
+
this.lineH.setShape({ x1, y1: midY, x2, y2: midY });
|
|
3614
|
+
this.lineH.setStyle({ stroke: strokeColor });
|
|
3615
|
+
const topY = Math.min(y1, y2);
|
|
3616
|
+
const bottomY = Math.max(y1, y2);
|
|
3617
|
+
this.arrowStart.setStyle({ fill: "none" });
|
|
3618
|
+
this.arrowEnd.setStyle({ fill: "none" });
|
|
3619
|
+
if (isUp) {
|
|
3620
|
+
this.arrowStart.setShape({
|
|
3621
|
+
points: [
|
|
3622
|
+
[midX, topY],
|
|
3623
|
+
[midX - 4, topY + 6],
|
|
3624
|
+
[midX + 4, topY + 6]
|
|
3625
|
+
]
|
|
3626
|
+
});
|
|
3627
|
+
this.arrowStart.setStyle({ fill: strokeColor });
|
|
3628
|
+
} else {
|
|
3629
|
+
this.arrowEnd.setShape({
|
|
3630
|
+
points: [
|
|
3631
|
+
[midX, bottomY],
|
|
3632
|
+
[midX - 4, bottomY - 6],
|
|
3633
|
+
[midX + 4, bottomY - 6]
|
|
3634
|
+
]
|
|
3635
|
+
});
|
|
3636
|
+
this.arrowEnd.setStyle({ fill: strokeColor });
|
|
3637
|
+
}
|
|
3638
|
+
const textContent = [`${priceDiff.toFixed(2)} (${priceChangePercent.toFixed(2)}%)`, `${bars} bars, ${(bars * 0).toFixed(0)}d`].join("\n");
|
|
3639
|
+
const labelW = 140;
|
|
3640
|
+
const labelH = 40;
|
|
3641
|
+
const rectBottomY = Math.max(y1, y2);
|
|
3642
|
+
const rectTopY = Math.min(y1, y2);
|
|
3643
|
+
const rectCenterX = (x1 + x2) / 2;
|
|
3644
|
+
let labelX = rectCenterX - labelW / 2;
|
|
3645
|
+
let labelY = rectBottomY + 10;
|
|
3646
|
+
const canvasHeight = this.chart.getHeight();
|
|
3647
|
+
if (labelY + labelH > canvasHeight) {
|
|
3648
|
+
labelY = rectTopY - labelH - 10;
|
|
3649
|
+
}
|
|
3650
|
+
this.labelRect.setShape({
|
|
3651
|
+
x: labelX,
|
|
3652
|
+
y: labelY,
|
|
3653
|
+
width: labelW,
|
|
3654
|
+
height: labelH
|
|
3655
|
+
});
|
|
3656
|
+
this.labelRect.setStyle({
|
|
3657
|
+
fill: "#1e293b",
|
|
3658
|
+
stroke: strokeColor,
|
|
3659
|
+
lineWidth: 1
|
|
3660
|
+
});
|
|
3661
|
+
this.labelText.setStyle({
|
|
3662
|
+
x: labelX + labelW / 2,
|
|
3663
|
+
y: labelY + labelH / 2,
|
|
3664
|
+
text: textContent,
|
|
3665
|
+
fill: "#fff"
|
|
3666
|
+
});
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
var __defProp$1 = Object.defineProperty;
|
|
3671
|
+
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3672
|
+
var __publicField$1 = (obj, key, value) => {
|
|
3673
|
+
__defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
3674
|
+
return value;
|
|
3675
|
+
};
|
|
3676
|
+
class LineTool extends AbstractPlugin {
|
|
3677
|
+
constructor(options) {
|
|
3678
|
+
super({
|
|
3679
|
+
id: "trend-line",
|
|
3680
|
+
name: options?.name || "Trend Line",
|
|
3681
|
+
icon: options?.icon || `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="2" y1="22" x2="22" y2="2" /></svg>`
|
|
3682
|
+
});
|
|
3683
|
+
__publicField$1(this, "zr");
|
|
3684
|
+
__publicField$1(this, "state", "idle");
|
|
3685
|
+
__publicField$1(this, "startPoint", null);
|
|
3686
|
+
__publicField$1(this, "endPoint", null);
|
|
3687
|
+
// ZRender Elements
|
|
3688
|
+
__publicField$1(this, "group", null);
|
|
3689
|
+
__publicField$1(this, "line", null);
|
|
3690
|
+
__publicField$1(this, "startCircle", null);
|
|
3691
|
+
__publicField$1(this, "endCircle", null);
|
|
3692
|
+
// --- Interaction Handlers ---
|
|
3693
|
+
__publicField$1(this, "onMouseDown", () => {
|
|
3694
|
+
});
|
|
3695
|
+
__publicField$1(this, "onChartInteraction", () => {
|
|
3696
|
+
});
|
|
3697
|
+
__publicField$1(this, "onClick", (params) => {
|
|
3698
|
+
if (this.state === "idle") {
|
|
3699
|
+
this.state = "drawing";
|
|
3700
|
+
this.startPoint = [params.offsetX, params.offsetY];
|
|
3701
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
3702
|
+
this.initGraphic();
|
|
3703
|
+
this.updateGraphic();
|
|
3704
|
+
} else if (this.state === "drawing") {
|
|
3705
|
+
this.state = "finished";
|
|
3706
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
3707
|
+
this.updateGraphic();
|
|
3708
|
+
if (this.startPoint && this.endPoint) {
|
|
3709
|
+
const start = this.context.coordinateConversion.pixelToData({
|
|
3710
|
+
x: this.startPoint[0],
|
|
3711
|
+
y: this.startPoint[1]
|
|
3712
|
+
});
|
|
3713
|
+
const end = this.context.coordinateConversion.pixelToData({
|
|
3714
|
+
x: this.endPoint[0],
|
|
3715
|
+
y: this.endPoint[1]
|
|
3716
|
+
});
|
|
3717
|
+
if (start && end) {
|
|
3718
|
+
const paneIndex = start.paneIndex || 0;
|
|
3719
|
+
this.context.addDrawing({
|
|
3720
|
+
id: `line-${Date.now()}`,
|
|
3721
|
+
type: "line",
|
|
3722
|
+
points: [start, end],
|
|
3723
|
+
paneIndex,
|
|
3724
|
+
style: {
|
|
3725
|
+
color: "#3b82f6",
|
|
3726
|
+
lineWidth: 2
|
|
3727
|
+
}
|
|
3728
|
+
});
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
this.removeGraphic();
|
|
3732
|
+
this.context.disableTools();
|
|
3733
|
+
}
|
|
3734
|
+
});
|
|
3735
|
+
__publicField$1(this, "clearHandlers", {});
|
|
3736
|
+
__publicField$1(this, "onMouseMove", (params) => {
|
|
3737
|
+
if (this.state !== "drawing")
|
|
3738
|
+
return;
|
|
3739
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
3740
|
+
this.updateGraphic();
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
onInit() {
|
|
3744
|
+
this.zr = this.chart.getZr();
|
|
3745
|
+
}
|
|
3746
|
+
onActivate() {
|
|
3747
|
+
this.state = "idle";
|
|
3748
|
+
this.chart.getZr().setCursorStyle("crosshair");
|
|
3749
|
+
this.zr.on("click", this.onClick);
|
|
3750
|
+
this.zr.on("mousemove", this.onMouseMove);
|
|
3751
|
+
}
|
|
3752
|
+
onDeactivate() {
|
|
3753
|
+
this.state = "idle";
|
|
3754
|
+
this.chart.getZr().setCursorStyle("default");
|
|
3755
|
+
this.zr.off("click", this.onClick);
|
|
3756
|
+
this.zr.off("mousemove", this.onMouseMove);
|
|
3757
|
+
this.disableClearListeners();
|
|
3758
|
+
if (this.state === "drawing") {
|
|
3759
|
+
this.removeGraphic();
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
onDestroy() {
|
|
3763
|
+
this.removeGraphic();
|
|
3764
|
+
}
|
|
3765
|
+
saveDataCoordinates() {
|
|
3766
|
+
}
|
|
3767
|
+
updateGraphicFromData() {
|
|
3768
|
+
}
|
|
3769
|
+
enableClearListeners() {
|
|
3770
|
+
}
|
|
3771
|
+
disableClearListeners() {
|
|
3772
|
+
}
|
|
3773
|
+
// --- Graphics ---
|
|
3774
|
+
initGraphic() {
|
|
3775
|
+
if (this.group)
|
|
3776
|
+
return;
|
|
3777
|
+
this.group = new echarts__namespace.graphic.Group();
|
|
3778
|
+
this.line = new echarts__namespace.graphic.Line({
|
|
3779
|
+
shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
3780
|
+
style: { stroke: "#3b82f6", lineWidth: 2 },
|
|
3781
|
+
z: 100
|
|
3782
|
+
});
|
|
3783
|
+
this.startCircle = new echarts__namespace.graphic.Circle({
|
|
3784
|
+
shape: { cx: 0, cy: 0, r: 4 },
|
|
3785
|
+
style: { fill: "#fff", stroke: "#3b82f6", lineWidth: 1 },
|
|
3786
|
+
z: 101
|
|
3787
|
+
});
|
|
3788
|
+
this.endCircle = new echarts__namespace.graphic.Circle({
|
|
3789
|
+
shape: { cx: 0, cy: 0, r: 4 },
|
|
3790
|
+
style: { fill: "#fff", stroke: "#3b82f6", lineWidth: 1 },
|
|
3791
|
+
z: 101
|
|
3792
|
+
});
|
|
3793
|
+
this.group.add(this.line);
|
|
3794
|
+
this.group.add(this.startCircle);
|
|
3795
|
+
this.group.add(this.endCircle);
|
|
3796
|
+
this.zr.add(this.group);
|
|
3797
|
+
}
|
|
3798
|
+
removeGraphic() {
|
|
3799
|
+
if (this.group) {
|
|
3800
|
+
this.zr.remove(this.group);
|
|
3801
|
+
this.group = null;
|
|
3802
|
+
this.disableClearListeners();
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
updateGraphic() {
|
|
3806
|
+
if (!this.startPoint || !this.endPoint || !this.group)
|
|
3807
|
+
return;
|
|
3808
|
+
const [x1, y1] = this.startPoint;
|
|
3809
|
+
const [x2, y2] = this.endPoint;
|
|
3810
|
+
this.line.setShape({ x1, y1, x2, y2 });
|
|
3811
|
+
this.startCircle.setShape({ cx: x1, cy: y1 });
|
|
3812
|
+
this.endCircle.setShape({ cx: x2, cy: y2 });
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
var __defProp = Object.defineProperty;
|
|
3817
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3818
|
+
var __publicField = (obj, key, value) => {
|
|
3819
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
3820
|
+
return value;
|
|
3821
|
+
};
|
|
3822
|
+
class FibonacciTool extends AbstractPlugin {
|
|
3823
|
+
constructor(options = {}) {
|
|
3824
|
+
super({
|
|
3825
|
+
id: "fibonacci-tool",
|
|
3826
|
+
name: options.name || "Fibonacci Retracement",
|
|
3827
|
+
icon: options.icon || `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M120-80v-80h720v80H120Zm0-240v-80h720v80H120Zm0-240v-80h720v80H120Zm0-240v-80h720v80H120Z"/></svg>`
|
|
3828
|
+
});
|
|
3829
|
+
__publicField(this, "startPoint", null);
|
|
3830
|
+
__publicField(this, "endPoint", null);
|
|
3831
|
+
__publicField(this, "state", "idle");
|
|
3832
|
+
// Temporary ZRender elements
|
|
3833
|
+
__publicField(this, "graphicGroup", null);
|
|
3834
|
+
// Fib levels config
|
|
3835
|
+
__publicField(this, "levels", [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]);
|
|
3836
|
+
__publicField(this, "colors", [
|
|
3837
|
+
"#787b86",
|
|
3838
|
+
// 0
|
|
3839
|
+
"#f44336",
|
|
3840
|
+
// 0.236
|
|
3841
|
+
"#ff9800",
|
|
3842
|
+
// 0.382
|
|
3843
|
+
"#4caf50",
|
|
3844
|
+
// 0.5
|
|
3845
|
+
"#2196f3",
|
|
3846
|
+
// 0.618
|
|
3847
|
+
"#00bcd4",
|
|
3848
|
+
// 0.786
|
|
3849
|
+
"#787b86"
|
|
3850
|
+
// 1
|
|
3851
|
+
]);
|
|
3852
|
+
__publicField(this, "onClick", (params) => {
|
|
3853
|
+
if (this.state === "idle") {
|
|
3854
|
+
this.state = "drawing";
|
|
3855
|
+
this.startPoint = [params.offsetX, params.offsetY];
|
|
3856
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
3857
|
+
this.initGraphic();
|
|
3858
|
+
this.updateGraphic();
|
|
3859
|
+
} else if (this.state === "drawing") {
|
|
3860
|
+
this.state = "finished";
|
|
3861
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
3862
|
+
this.updateGraphic();
|
|
3863
|
+
this.saveDrawing();
|
|
3864
|
+
this.removeGraphic();
|
|
3865
|
+
this.context.disableTools();
|
|
3866
|
+
}
|
|
3867
|
+
});
|
|
3868
|
+
__publicField(this, "onMouseMove", (params) => {
|
|
3869
|
+
if (this.state === "drawing") {
|
|
3870
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
3871
|
+
this.updateGraphic();
|
|
3872
|
+
}
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
onActivate() {
|
|
3876
|
+
this.state = "idle";
|
|
3877
|
+
this.startPoint = null;
|
|
3878
|
+
this.endPoint = null;
|
|
3879
|
+
this.context.getChart().getZr().setCursorStyle("crosshair");
|
|
3880
|
+
this.bindEvents();
|
|
3881
|
+
}
|
|
3882
|
+
onDeactivate() {
|
|
3883
|
+
this.state = "idle";
|
|
3884
|
+
this.startPoint = null;
|
|
3885
|
+
this.endPoint = null;
|
|
3886
|
+
this.removeGraphic();
|
|
3887
|
+
this.unbindEvents();
|
|
3888
|
+
this.context.getChart().getZr().setCursorStyle("default");
|
|
3889
|
+
}
|
|
3890
|
+
bindEvents() {
|
|
3891
|
+
const zr = this.context.getChart().getZr();
|
|
3892
|
+
zr.on("click", this.onClick);
|
|
3893
|
+
zr.on("mousemove", this.onMouseMove);
|
|
3894
|
+
}
|
|
3895
|
+
unbindEvents() {
|
|
3896
|
+
const zr = this.context.getChart().getZr();
|
|
3897
|
+
zr.off("click", this.onClick);
|
|
3898
|
+
zr.off("mousemove", this.onMouseMove);
|
|
3899
|
+
}
|
|
3900
|
+
initGraphic() {
|
|
3901
|
+
this.graphicGroup = new echarts__namespace.graphic.Group();
|
|
3902
|
+
this.context.getChart().getZr().add(this.graphicGroup);
|
|
3903
|
+
}
|
|
3904
|
+
removeGraphic() {
|
|
3905
|
+
if (this.graphicGroup) {
|
|
3906
|
+
this.context.getChart().getZr().remove(this.graphicGroup);
|
|
3907
|
+
this.graphicGroup = null;
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
updateGraphic() {
|
|
3911
|
+
if (!this.graphicGroup || !this.startPoint || !this.endPoint)
|
|
3912
|
+
return;
|
|
3913
|
+
this.graphicGroup.removeAll();
|
|
3914
|
+
const x1 = this.startPoint[0];
|
|
3915
|
+
const y1 = this.startPoint[1];
|
|
3916
|
+
const x2 = this.endPoint[0];
|
|
3917
|
+
const y2 = this.endPoint[1];
|
|
3918
|
+
const trendLine = new echarts__namespace.graphic.Line({
|
|
3919
|
+
shape: { x1, y1, x2, y2 },
|
|
3920
|
+
style: {
|
|
3921
|
+
stroke: "#999",
|
|
3922
|
+
lineWidth: 1,
|
|
3923
|
+
lineDash: [4, 4]
|
|
3924
|
+
},
|
|
3925
|
+
silent: true
|
|
3926
|
+
});
|
|
3927
|
+
this.graphicGroup.add(trendLine);
|
|
3928
|
+
const startX = Math.min(x1, x2);
|
|
3929
|
+
const endX = Math.max(x1, x2);
|
|
3930
|
+
const width = endX - startX;
|
|
3931
|
+
const diffY = y2 - y1;
|
|
3932
|
+
this.levels.forEach((level, index) => {
|
|
3933
|
+
const levelY = y2 - diffY * level;
|
|
3934
|
+
const color = this.colors[index % this.colors.length];
|
|
3935
|
+
const line = new echarts__namespace.graphic.Line({
|
|
3936
|
+
shape: { x1: startX, y1: levelY, x2: endX, y2: levelY },
|
|
3937
|
+
style: {
|
|
3938
|
+
stroke: color,
|
|
3939
|
+
lineWidth: 1
|
|
3940
|
+
},
|
|
3941
|
+
silent: true
|
|
3942
|
+
});
|
|
3943
|
+
this.graphicGroup.add(line);
|
|
3944
|
+
if (index < this.levels.length - 1) {
|
|
3945
|
+
const nextLevel = this.levels[index + 1];
|
|
3946
|
+
const nextY = y2 - diffY * nextLevel;
|
|
3947
|
+
const rectH = Math.abs(nextY - levelY);
|
|
3948
|
+
const rectY = Math.min(levelY, nextY);
|
|
3949
|
+
const rect = new echarts__namespace.graphic.Rect({
|
|
3950
|
+
shape: { x: startX, y: rectY, width, height: rectH },
|
|
3951
|
+
style: {
|
|
3952
|
+
fill: this.colors[(index + 1) % this.colors.length],
|
|
3953
|
+
// Use next level's color
|
|
3954
|
+
opacity: 0.1
|
|
3955
|
+
},
|
|
3956
|
+
silent: true
|
|
3957
|
+
});
|
|
3958
|
+
this.graphicGroup.add(rect);
|
|
3959
|
+
}
|
|
3960
|
+
});
|
|
3961
|
+
}
|
|
3962
|
+
saveDrawing() {
|
|
3963
|
+
if (!this.startPoint || !this.endPoint)
|
|
3964
|
+
return;
|
|
3965
|
+
const start = this.context.coordinateConversion.pixelToData({
|
|
3966
|
+
x: this.startPoint[0],
|
|
3967
|
+
y: this.startPoint[1]
|
|
3968
|
+
});
|
|
3969
|
+
const end = this.context.coordinateConversion.pixelToData({
|
|
3970
|
+
x: this.endPoint[0],
|
|
3971
|
+
y: this.endPoint[1]
|
|
3972
|
+
});
|
|
3973
|
+
if (start && end) {
|
|
3974
|
+
const paneIndex = start.paneIndex || 0;
|
|
3975
|
+
this.context.addDrawing({
|
|
3976
|
+
id: `fib-${Date.now()}`,
|
|
3977
|
+
type: "fibonacci",
|
|
3978
|
+
points: [start, end],
|
|
3979
|
+
paneIndex,
|
|
3980
|
+
style: {
|
|
3981
|
+
color: "#3b82f6",
|
|
3982
|
+
// Default color, though individual lines use specific colors
|
|
3983
|
+
lineWidth: 1
|
|
3984
|
+
}
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
exports.AbstractPlugin = AbstractPlugin;
|
|
3991
|
+
exports.FibonacciTool = FibonacciTool;
|
|
3992
|
+
exports.LineTool = LineTool;
|
|
3993
|
+
exports.MeasureTool = MeasureTool;
|
|
3994
|
+
exports.QFChart = QFChart;
|
|
3995
|
+
|
|
3996
|
+
}));
|
|
3997
|
+
//# sourceMappingURL=qfchart.dev.browser.js.map
|