@qfo/qfchart 0.8.1 → 0.8.2
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/dist/index.d.ts +206 -1
- package/dist/qfchart.min.browser.js +16 -16
- package/dist/qfchart.min.es.js +16 -16
- package/package.json +1 -1
- package/src/QFChart.ts +11 -10
- package/src/components/LayoutManager.ts +51 -17
- package/src/index.ts +8 -0
- package/src/plugins/CrossLineTool/CrossLineDrawingRenderer.ts +49 -0
- package/src/plugins/CrossLineTool/CrossLineTool.ts +52 -0
- package/src/plugins/CrossLineTool/index.ts +2 -0
- package/src/plugins/ExtendedLineTool/ExtendedLineDrawingRenderer.ts +73 -0
- package/src/plugins/ExtendedLineTool/ExtendedLineTool.ts +173 -0
- package/src/plugins/ExtendedLineTool/index.ts +2 -0
- package/src/plugins/HorizontalLineTool/HorizontalLineDrawingRenderer.ts +54 -0
- package/src/plugins/HorizontalLineTool/HorizontalLineTool.ts +52 -0
- package/src/plugins/HorizontalLineTool/index.ts +2 -0
- package/src/plugins/HorizontalRayTool/HorizontalRayDrawingRenderer.ts +34 -0
- package/src/plugins/HorizontalRayTool/HorizontalRayTool.ts +52 -0
- package/src/plugins/HorizontalRayTool/index.ts +2 -0
- package/src/plugins/InfoLineTool/InfoLineDrawingRenderer.ts +72 -0
- package/src/plugins/InfoLineTool/InfoLineTool.ts +130 -0
- package/src/plugins/InfoLineTool/index.ts +2 -0
- package/src/plugins/LineTool/LineDrawingRenderer.ts +2 -2
- package/src/plugins/LineTool/LineTool.ts +5 -5
- package/src/plugins/RayTool/RayDrawingRenderer.ts +69 -0
- package/src/plugins/RayTool/RayTool.ts +162 -0
- package/src/plugins/RayTool/index.ts +2 -0
- package/src/plugins/TrendAngleTool/TrendAngleDrawingRenderer.ts +87 -0
- package/src/plugins/TrendAngleTool/TrendAngleTool.ts +176 -0
- package/src/plugins/TrendAngleTool/index.ts +2 -0
- package/src/plugins/VerticalLineTool/VerticalLineDrawingRenderer.ts +35 -0
- package/src/plugins/VerticalLineTool/VerticalLineTool.ts +52 -0
- package/src/plugins/VerticalLineTool/index.ts +2 -0
- package/src/types.ts +2 -0
package/package.json
CHANGED
package/src/QFChart.ts
CHANGED
|
@@ -1397,22 +1397,22 @@ export class QFChart implements ChartContext {
|
|
|
1397
1397
|
});
|
|
1398
1398
|
drawingsByPane.forEach((paneDrawings) => {
|
|
1399
1399
|
drawingSeriesUpdates.push({
|
|
1400
|
-
data: paneDrawings.map((d) =>
|
|
1401
|
-
|
|
1402
|
-
d.points
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1400
|
+
data: paneDrawings.map((d) => {
|
|
1401
|
+
const row: number[] = [];
|
|
1402
|
+
d.points.forEach((p) => {
|
|
1403
|
+
row.push(p.timeIndex + this.dataIndexOffset, p.value);
|
|
1404
|
+
});
|
|
1405
|
+
return row;
|
|
1406
|
+
}),
|
|
1406
1407
|
});
|
|
1407
1408
|
});
|
|
1408
1409
|
|
|
1409
1410
|
// 6. Merge update — preserves drag/interaction state
|
|
1410
1411
|
const updateOption: any = {
|
|
1411
1412
|
xAxis: currentOption.xAxis.map(() => ({ data: categoryData })),
|
|
1412
|
-
dataZoom: [
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
],
|
|
1413
|
+
dataZoom: (currentOption.dataZoom || []).map(() => ({
|
|
1414
|
+
start: newStart, end: newEnd,
|
|
1415
|
+
})),
|
|
1416
1416
|
series: [
|
|
1417
1417
|
{ data: coloredCandlestickData, markLine: candlestickSeries.markLine },
|
|
1418
1418
|
...indicatorSeries.map((s) => {
|
|
@@ -1668,6 +1668,7 @@ export class QFChart implements ChartContext {
|
|
|
1668
1668
|
pixelPoints,
|
|
1669
1669
|
isSelected: drawing.id === this.selectedDrawingId,
|
|
1670
1670
|
api,
|
|
1671
|
+
coordSys: params.coordSys,
|
|
1671
1672
|
});
|
|
1672
1673
|
},
|
|
1673
1674
|
data: drawings.map((d) => {
|
|
@@ -268,6 +268,18 @@ export class LayoutManager {
|
|
|
268
268
|
|
|
269
269
|
let mainHeightVal = 75; // Default if no separate pane
|
|
270
270
|
|
|
271
|
+
// Parse layout.mainPaneHeight option (e.g. '40%' or 40)
|
|
272
|
+
let configuredMainHeight: number | undefined;
|
|
273
|
+
if (options.layout?.mainPaneHeight !== undefined) {
|
|
274
|
+
const raw = options.layout.mainPaneHeight;
|
|
275
|
+
if (typeof raw === 'string') {
|
|
276
|
+
const parsed = parseFloat(raw);
|
|
277
|
+
if (!isNaN(parsed)) configuredMainHeight = parsed;
|
|
278
|
+
} else if (typeof raw === 'number') {
|
|
279
|
+
configuredMainHeight = raw as unknown as number;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
271
283
|
// Prepare separate panes configuration
|
|
272
284
|
let paneConfigs: PaneConfiguration[] = [];
|
|
273
285
|
|
|
@@ -286,33 +298,54 @@ export class LayoutManager {
|
|
|
286
298
|
};
|
|
287
299
|
});
|
|
288
300
|
|
|
289
|
-
// 2. Assign
|
|
290
|
-
|
|
291
|
-
const resolvedPanes = panes.map((p) => ({
|
|
301
|
+
// 2. Assign raw heights (collapsed = 3%, otherwise use requested or default 15)
|
|
302
|
+
const rawPanes = panes.map((p) => ({
|
|
292
303
|
...p,
|
|
293
|
-
|
|
304
|
+
rawHeight: p.isCollapsed ? 3 : p.requestedHeight !== undefined ? p.requestedHeight : 15,
|
|
294
305
|
}));
|
|
295
306
|
|
|
296
|
-
// 3. Calculate total space needed for indicators
|
|
297
|
-
const totalIndicatorHeight = resolvedPanes.reduce((sum, p) => sum + p.height, 0);
|
|
298
|
-
const totalGaps = resolvedPanes.length * gapPercent;
|
|
299
|
-
const totalBottomSpace = totalIndicatorHeight + totalGaps;
|
|
300
|
-
|
|
301
|
-
// 4. Calculate Main Chart Height
|
|
302
|
-
// Available space = chartAreaBottom - mainPaneTop;
|
|
303
307
|
const totalAvailable = chartAreaBottom - mainPaneTop;
|
|
304
|
-
|
|
308
|
+
const totalGaps = rawPanes.length * gapPercent;
|
|
305
309
|
|
|
306
|
-
//
|
|
310
|
+
// 4. Determine main chart height
|
|
307
311
|
if (mainHeightOverride !== undefined && mainHeightOverride > 0 && !isMainCollapsed) {
|
|
312
|
+
// Drag-resize takes absolute priority
|
|
308
313
|
mainHeightVal = mainHeightOverride;
|
|
309
314
|
} else if (isMainCollapsed) {
|
|
310
315
|
mainHeightVal = 3;
|
|
316
|
+
} else if (configuredMainHeight !== undefined && configuredMainHeight > 0) {
|
|
317
|
+
// User set mainPaneHeight — indicators fill remaining space proportionally
|
|
318
|
+
mainHeightVal = configuredMainHeight;
|
|
311
319
|
} else {
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
320
|
+
// Auto: subtract indicator heights from available space
|
|
321
|
+
const totalIndicatorHeight = rawPanes.reduce((sum, p) => sum + p.rawHeight, 0);
|
|
322
|
+
mainHeightVal = totalAvailable - totalIndicatorHeight - totalGaps;
|
|
323
|
+
if (mainHeightVal < 20) mainHeightVal = Math.max(mainHeightVal, 10);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 3. Resolve indicator heights
|
|
327
|
+
// When mainPaneHeight is configured (or drag override active), distribute remaining space
|
|
328
|
+
// proportionally among non-collapsed panes using their rawHeight as weights.
|
|
329
|
+
const isMainHeightFixed = (mainHeightOverride !== undefined && mainHeightOverride > 0 && !isMainCollapsed)
|
|
330
|
+
|| (configuredMainHeight !== undefined && configuredMainHeight > 0 && !isMainCollapsed);
|
|
331
|
+
|
|
332
|
+
type ResolvedPane = (typeof rawPanes)[number] & { height: number };
|
|
333
|
+
let resolvedPanes: ResolvedPane[];
|
|
334
|
+
if (isMainHeightFixed) {
|
|
335
|
+
const remainingForIndicators = totalAvailable - mainHeightVal - totalGaps;
|
|
336
|
+
const totalWeights = rawPanes
|
|
337
|
+
.filter((p) => !p.isCollapsed)
|
|
338
|
+
.reduce((sum, p) => sum + p.rawHeight, 0);
|
|
339
|
+
resolvedPanes = rawPanes.map((p) => ({
|
|
340
|
+
...p,
|
|
341
|
+
height: p.isCollapsed
|
|
342
|
+
? 3
|
|
343
|
+
: totalWeights > 0
|
|
344
|
+
? Math.max(5, (p.rawHeight / totalWeights) * remainingForIndicators)
|
|
345
|
+
: remainingForIndicators / rawPanes.filter((x) => !x.isCollapsed).length,
|
|
346
|
+
}));
|
|
347
|
+
} else {
|
|
348
|
+
resolvedPanes = rawPanes.map((p) => ({ ...p, height: p.rawHeight }));
|
|
316
349
|
}
|
|
317
350
|
|
|
318
351
|
// 5. Calculate positions
|
|
@@ -332,6 +365,7 @@ export class LayoutManager {
|
|
|
332
365
|
return config;
|
|
333
366
|
});
|
|
334
367
|
} else {
|
|
368
|
+
// No secondary panes — mainPaneHeight is ignored, fill all available space
|
|
335
369
|
mainHeightVal = chartAreaBottom - mainPaneTop;
|
|
336
370
|
if (isMainCollapsed) {
|
|
337
371
|
mainHeightVal = 3;
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,14 @@ export * from "./types";
|
|
|
2
2
|
export * from "./QFChart";
|
|
3
3
|
export * from "./plugins/MeasureTool";
|
|
4
4
|
export * from "./plugins/LineTool";
|
|
5
|
+
export * from "./plugins/RayTool";
|
|
6
|
+
export * from "./plugins/InfoLineTool";
|
|
7
|
+
export * from "./plugins/ExtendedLineTool";
|
|
8
|
+
export * from "./plugins/TrendAngleTool";
|
|
9
|
+
export * from "./plugins/HorizontalLineTool";
|
|
10
|
+
export * from "./plugins/HorizontalRayTool";
|
|
11
|
+
export * from "./plugins/VerticalLineTool";
|
|
12
|
+
export * from "./plugins/CrossLineTool";
|
|
5
13
|
export * from "./plugins/FibonacciTool";
|
|
6
14
|
export * from "./plugins/FibonacciChannelTool";
|
|
7
15
|
export * from "./plugins/FibSpeedResistanceFanTool";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
export class CrossLineDrawingRenderer implements DrawingRenderer {
|
|
4
|
+
type = 'cross-line';
|
|
5
|
+
|
|
6
|
+
render(ctx: DrawingRenderContext): any {
|
|
7
|
+
const { drawing, pixelPoints, isSelected, coordSys } = ctx;
|
|
8
|
+
const [px, py] = pixelPoints[0];
|
|
9
|
+
const color = drawing.style?.color || '#d1d4dc';
|
|
10
|
+
|
|
11
|
+
const left = coordSys.x;
|
|
12
|
+
const right = coordSys.x + coordSys.width;
|
|
13
|
+
const top = coordSys.y;
|
|
14
|
+
const bottom = coordSys.y + coordSys.height;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
type: 'group',
|
|
18
|
+
children: [
|
|
19
|
+
// Horizontal line
|
|
20
|
+
{
|
|
21
|
+
type: 'line',
|
|
22
|
+
name: 'line-h',
|
|
23
|
+
shape: { x1: left, y1: py, x2: right, y2: py },
|
|
24
|
+
style: {
|
|
25
|
+
stroke: color,
|
|
26
|
+
lineWidth: drawing.style?.lineWidth || 1,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
// Vertical line
|
|
30
|
+
{
|
|
31
|
+
type: 'line',
|
|
32
|
+
name: 'line-v',
|
|
33
|
+
shape: { x1: px, y1: top, x2: px, y2: bottom },
|
|
34
|
+
style: {
|
|
35
|
+
stroke: color,
|
|
36
|
+
lineWidth: drawing.style?.lineWidth || 1,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
// Center point
|
|
40
|
+
{
|
|
41
|
+
type: 'circle',
|
|
42
|
+
name: 'point-0',
|
|
43
|
+
shape: { cx: px, cy: py, r: 4 },
|
|
44
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AbstractPlugin } from '../../components/AbstractPlugin';
|
|
2
|
+
import { CrossLineDrawingRenderer } from './CrossLineDrawingRenderer';
|
|
3
|
+
|
|
4
|
+
export class CrossLineTool extends AbstractPlugin {
|
|
5
|
+
private zr!: any;
|
|
6
|
+
|
|
7
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
8
|
+
super({
|
|
9
|
+
id: 'cross-line-tool',
|
|
10
|
+
name: options?.name || 'Cross Line',
|
|
11
|
+
icon: options?.icon || `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="2" x2="12" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/></svg>`,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
protected onInit(): void {
|
|
16
|
+
this.zr = this.chart.getZr();
|
|
17
|
+
this.context.registerDrawingRenderer(new CrossLineDrawingRenderer());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected onActivate(): void {
|
|
21
|
+
this.chart.getZr().setCursorStyle('crosshair');
|
|
22
|
+
this.zr.on('click', this.onClick);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected onDeactivate(): void {
|
|
26
|
+
this.chart.getZr().setCursorStyle('default');
|
|
27
|
+
this.zr.off('click', this.onClick);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected onDestroy(): void {}
|
|
31
|
+
|
|
32
|
+
private onClick = (params: any) => {
|
|
33
|
+
const point = this.getPoint(params);
|
|
34
|
+
if (!point) return;
|
|
35
|
+
|
|
36
|
+
const data = this.context.coordinateConversion.pixelToData({
|
|
37
|
+
x: point[0], y: point[1],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (data) {
|
|
41
|
+
this.context.addDrawing({
|
|
42
|
+
id: `crossline-${Date.now()}`,
|
|
43
|
+
type: 'cross-line',
|
|
44
|
+
points: [data],
|
|
45
|
+
paneIndex: data.paneIndex || 0,
|
|
46
|
+
style: { color: '#d1d4dc', lineWidth: 1 },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.context.disableTools();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
export class ExtendedLineDrawingRenderer implements DrawingRenderer {
|
|
4
|
+
type = 'extended-line';
|
|
5
|
+
|
|
6
|
+
render(ctx: DrawingRenderContext): any {
|
|
7
|
+
const { drawing, pixelPoints, isSelected, coordSys } = ctx;
|
|
8
|
+
const [x1, y1] = pixelPoints[0];
|
|
9
|
+
const [x2, y2] = pixelPoints[1];
|
|
10
|
+
const color = drawing.style?.color || '#d1d4dc';
|
|
11
|
+
|
|
12
|
+
const dx = x2 - x1;
|
|
13
|
+
const dy = y2 - y1;
|
|
14
|
+
|
|
15
|
+
let ex1 = x1, ey1 = y1, ex2 = x2, ey2 = y2;
|
|
16
|
+
|
|
17
|
+
if (dx !== 0 || dy !== 0) {
|
|
18
|
+
const left = coordSys.x;
|
|
19
|
+
const right = coordSys.x + coordSys.width;
|
|
20
|
+
const top = coordSys.y;
|
|
21
|
+
const bottom = coordSys.y + coordSys.height;
|
|
22
|
+
|
|
23
|
+
// Extend forward (past p2)
|
|
24
|
+
[ex2, ey2] = this.extendToEdge(x1, y1, dx, dy, left, right, top, bottom);
|
|
25
|
+
// Extend backward (past p1)
|
|
26
|
+
[ex1, ey1] = this.extendToEdge(x2, y2, -dx, -dy, left, right, top, bottom);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
type: 'group',
|
|
31
|
+
children: [
|
|
32
|
+
{
|
|
33
|
+
type: 'line',
|
|
34
|
+
name: 'line',
|
|
35
|
+
shape: { x1: ex1, y1: ey1, x2: ex2, y2: ey2 },
|
|
36
|
+
style: {
|
|
37
|
+
stroke: color,
|
|
38
|
+
lineWidth: drawing.style?.lineWidth || 1,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: 'circle',
|
|
43
|
+
name: 'point-0',
|
|
44
|
+
shape: { cx: x1, cy: y1, r: 4 },
|
|
45
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'circle',
|
|
49
|
+
name: 'point-1',
|
|
50
|
+
shape: { cx: x2, cy: y2, r: 4 },
|
|
51
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private extendToEdge(
|
|
58
|
+
ox: number, oy: number, dx: number, dy: number,
|
|
59
|
+
left: number, right: number, top: number, bottom: number,
|
|
60
|
+
): [number, number] {
|
|
61
|
+
let tMax = Infinity;
|
|
62
|
+
if (dx !== 0) {
|
|
63
|
+
const tx = dx > 0 ? (right - ox) / dx : (left - ox) / dx;
|
|
64
|
+
if (tx > 0) tMax = Math.min(tMax, tx);
|
|
65
|
+
}
|
|
66
|
+
if (dy !== 0) {
|
|
67
|
+
const ty = dy > 0 ? (bottom - oy) / dy : (top - oy) / dy;
|
|
68
|
+
if (ty > 0) tMax = Math.min(tMax, ty);
|
|
69
|
+
}
|
|
70
|
+
if (!isFinite(tMax)) tMax = 1;
|
|
71
|
+
return [ox + tMax * dx, oy + tMax * dy];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { AbstractPlugin } from '../../components/AbstractPlugin';
|
|
2
|
+
import { ExtendedLineDrawingRenderer } from './ExtendedLineDrawingRenderer';
|
|
3
|
+
import * as echarts from 'echarts';
|
|
4
|
+
|
|
5
|
+
const COLOR = '#d1d4dc';
|
|
6
|
+
|
|
7
|
+
type PluginState = 'idle' | 'drawing' | 'finished';
|
|
8
|
+
|
|
9
|
+
export class ExtendedLineTool extends AbstractPlugin {
|
|
10
|
+
private zr!: any;
|
|
11
|
+
private state: PluginState = 'idle';
|
|
12
|
+
private startPoint: number[] | null = null;
|
|
13
|
+
private endPoint: number[] | null = null;
|
|
14
|
+
private group: any = null;
|
|
15
|
+
private line: any = null;
|
|
16
|
+
private dashLineForward: any = null;
|
|
17
|
+
private dashLineBackward: any = null;
|
|
18
|
+
private startCircle: any = null;
|
|
19
|
+
private endCircle: any = null;
|
|
20
|
+
|
|
21
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
22
|
+
super({
|
|
23
|
+
id: 'extended-line-tool',
|
|
24
|
+
name: options?.name || 'Extended Line',
|
|
25
|
+
icon: options?.icon || `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="1" y1="23" x2="23" y2="1" stroke-dasharray="2,2" opacity="0.4"/><line x1="6" y1="18" x2="18" y2="6"/></svg>`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected onInit(): void {
|
|
30
|
+
this.zr = this.chart.getZr();
|
|
31
|
+
this.context.registerDrawingRenderer(new ExtendedLineDrawingRenderer());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected onActivate(): void {
|
|
35
|
+
this.state = 'idle';
|
|
36
|
+
this.chart.getZr().setCursorStyle('crosshair');
|
|
37
|
+
this.zr.on('click', this.onClick);
|
|
38
|
+
this.zr.on('mousemove', this.onMouseMove);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected onDeactivate(): void {
|
|
42
|
+
this.state = 'idle';
|
|
43
|
+
this.chart.getZr().setCursorStyle('default');
|
|
44
|
+
this.zr.off('click', this.onClick);
|
|
45
|
+
this.zr.off('mousemove', this.onMouseMove);
|
|
46
|
+
this.removeGraphic();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
protected onDestroy(): void {
|
|
50
|
+
this.removeGraphic();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private onClick = (params: any) => {
|
|
54
|
+
if (this.state === 'idle') {
|
|
55
|
+
this.state = 'drawing';
|
|
56
|
+
this.startPoint = this.getPoint(params);
|
|
57
|
+
this.endPoint = this.getPoint(params);
|
|
58
|
+
this.initGraphic();
|
|
59
|
+
this.updateGraphic();
|
|
60
|
+
} else if (this.state === 'drawing') {
|
|
61
|
+
this.state = 'finished';
|
|
62
|
+
this.endPoint = this.getPoint(params);
|
|
63
|
+
|
|
64
|
+
if (this.startPoint && this.endPoint) {
|
|
65
|
+
const start = this.context.coordinateConversion.pixelToData({
|
|
66
|
+
x: this.startPoint[0], y: this.startPoint[1],
|
|
67
|
+
});
|
|
68
|
+
const end = this.context.coordinateConversion.pixelToData({
|
|
69
|
+
x: this.endPoint[0], y: this.endPoint[1],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (start && end) {
|
|
73
|
+
this.context.addDrawing({
|
|
74
|
+
id: `extended-line-${Date.now()}`,
|
|
75
|
+
type: 'extended-line',
|
|
76
|
+
points: [start, end],
|
|
77
|
+
paneIndex: start.paneIndex || 0,
|
|
78
|
+
style: { color: COLOR, lineWidth: 1 },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.removeGraphic();
|
|
84
|
+
this.context.disableTools();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
private onMouseMove = (params: any) => {
|
|
89
|
+
if (this.state !== 'drawing') return;
|
|
90
|
+
this.endPoint = this.getPoint(params);
|
|
91
|
+
this.updateGraphic();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
private initGraphic(): void {
|
|
95
|
+
if (this.group) return;
|
|
96
|
+
this.group = new echarts.graphic.Group();
|
|
97
|
+
this.line = new echarts.graphic.Line({
|
|
98
|
+
shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
99
|
+
style: { stroke: COLOR, lineWidth: 1 },
|
|
100
|
+
z: 100,
|
|
101
|
+
});
|
|
102
|
+
this.dashLineForward = new echarts.graphic.Line({
|
|
103
|
+
shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
104
|
+
style: { stroke: COLOR, lineWidth: 1, lineDash: [4, 4], opacity: 0.5 },
|
|
105
|
+
z: 99,
|
|
106
|
+
});
|
|
107
|
+
this.dashLineBackward = new echarts.graphic.Line({
|
|
108
|
+
shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
109
|
+
style: { stroke: COLOR, lineWidth: 1, lineDash: [4, 4], opacity: 0.5 },
|
|
110
|
+
z: 99,
|
|
111
|
+
});
|
|
112
|
+
this.startCircle = new echarts.graphic.Circle({
|
|
113
|
+
shape: { cx: 0, cy: 0, r: 4 },
|
|
114
|
+
style: { fill: '#fff', stroke: COLOR, lineWidth: 1 },
|
|
115
|
+
z: 101,
|
|
116
|
+
});
|
|
117
|
+
this.endCircle = new echarts.graphic.Circle({
|
|
118
|
+
shape: { cx: 0, cy: 0, r: 4 },
|
|
119
|
+
style: { fill: '#fff', stroke: COLOR, lineWidth: 1 },
|
|
120
|
+
z: 101,
|
|
121
|
+
});
|
|
122
|
+
this.group.add(this.dashLineBackward);
|
|
123
|
+
this.group.add(this.dashLineForward);
|
|
124
|
+
this.group.add(this.line);
|
|
125
|
+
this.group.add(this.startCircle);
|
|
126
|
+
this.group.add(this.endCircle);
|
|
127
|
+
this.zr.add(this.group);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private removeGraphic(): void {
|
|
131
|
+
if (this.group) {
|
|
132
|
+
this.zr.remove(this.group);
|
|
133
|
+
this.group = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private updateGraphic(): void {
|
|
138
|
+
if (!this.startPoint || !this.endPoint || !this.group) return;
|
|
139
|
+
const [x1, y1] = this.startPoint;
|
|
140
|
+
const [x2, y2] = this.endPoint;
|
|
141
|
+
this.line.setShape({ x1, y1, x2, y2 });
|
|
142
|
+
this.startCircle.setShape({ cx: x1, cy: y1 });
|
|
143
|
+
this.endCircle.setShape({ cx: x2, cy: y2 });
|
|
144
|
+
|
|
145
|
+
const dx = x2 - x1;
|
|
146
|
+
const dy = y2 - y1;
|
|
147
|
+
if (dx === 0 && dy === 0) return;
|
|
148
|
+
|
|
149
|
+
// Dashed extension forward (past p2)
|
|
150
|
+
const [fwX, fwY] = this.extendToEdge(x1, y1, dx, dy);
|
|
151
|
+
this.dashLineForward.setShape({ x1: x2, y1: y2, x2: fwX, y2: fwY });
|
|
152
|
+
|
|
153
|
+
// Dashed extension backward (past p1)
|
|
154
|
+
const [bwX, bwY] = this.extendToEdge(x2, y2, -dx, -dy);
|
|
155
|
+
this.dashLineBackward.setShape({ x1: x1, y1: y1, x2: bwX, y2: bwY });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private extendToEdge(ox: number, oy: number, dx: number, dy: number): [number, number] {
|
|
159
|
+
const w = this.chart.getWidth();
|
|
160
|
+
const h = this.chart.getHeight();
|
|
161
|
+
let tMax = Infinity;
|
|
162
|
+
if (dx !== 0) {
|
|
163
|
+
const tx = dx > 0 ? (w - ox) / dx : -ox / dx;
|
|
164
|
+
if (tx > 0) tMax = Math.min(tMax, tx);
|
|
165
|
+
}
|
|
166
|
+
if (dy !== 0) {
|
|
167
|
+
const ty = dy > 0 ? (h - oy) / dy : -oy / dy;
|
|
168
|
+
if (ty > 0) tMax = Math.min(tMax, ty);
|
|
169
|
+
}
|
|
170
|
+
if (!isFinite(tMax)) tMax = 1;
|
|
171
|
+
return [ox + tMax * dx, oy + tMax * dy];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
export class HorizontalLineDrawingRenderer implements DrawingRenderer {
|
|
4
|
+
type = 'horizontal-line';
|
|
5
|
+
|
|
6
|
+
render(ctx: DrawingRenderContext): any {
|
|
7
|
+
const { drawing, pixelPoints, isSelected, coordSys } = ctx;
|
|
8
|
+
const [px, py] = pixelPoints[0];
|
|
9
|
+
const color = drawing.style?.color || '#d1d4dc';
|
|
10
|
+
|
|
11
|
+
const left = coordSys.x;
|
|
12
|
+
const right = coordSys.x + coordSys.width;
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
type: 'group',
|
|
16
|
+
children: [
|
|
17
|
+
{
|
|
18
|
+
type: 'line',
|
|
19
|
+
name: 'line',
|
|
20
|
+
shape: { x1: left, y1: py, x2: right, y2: py },
|
|
21
|
+
style: {
|
|
22
|
+
stroke: color,
|
|
23
|
+
lineWidth: drawing.style?.lineWidth || 1,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
// Price label on the right
|
|
27
|
+
{
|
|
28
|
+
type: 'rect',
|
|
29
|
+
shape: { x: right - 70, y: py - 10, width: 65, height: 18, r: 2 },
|
|
30
|
+
style: { fill: color, opacity: 0.9 },
|
|
31
|
+
z2: 10,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'text',
|
|
35
|
+
x: right - 67,
|
|
36
|
+
y: py - 8,
|
|
37
|
+
style: {
|
|
38
|
+
text: drawing.points[0].value.toFixed(2),
|
|
39
|
+
fill: '#fff',
|
|
40
|
+
fontSize: 10,
|
|
41
|
+
fontFamily: 'monospace',
|
|
42
|
+
},
|
|
43
|
+
z2: 11,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: 'circle',
|
|
47
|
+
name: 'point-0',
|
|
48
|
+
shape: { cx: px, cy: py, r: 4 },
|
|
49
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AbstractPlugin } from '../../components/AbstractPlugin';
|
|
2
|
+
import { HorizontalLineDrawingRenderer } from './HorizontalLineDrawingRenderer';
|
|
3
|
+
|
|
4
|
+
export class HorizontalLineTool extends AbstractPlugin {
|
|
5
|
+
private zr!: any;
|
|
6
|
+
|
|
7
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
8
|
+
super({
|
|
9
|
+
id: 'horizontal-line-tool',
|
|
10
|
+
name: options?.name || 'Horizontal Line',
|
|
11
|
+
icon: options?.icon || `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="2" y1="12" x2="22" y2="12"/><circle cx="12" cy="12" r="2" fill="currentColor"/></svg>`,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
protected onInit(): void {
|
|
16
|
+
this.zr = this.chart.getZr();
|
|
17
|
+
this.context.registerDrawingRenderer(new HorizontalLineDrawingRenderer());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected onActivate(): void {
|
|
21
|
+
this.chart.getZr().setCursorStyle('crosshair');
|
|
22
|
+
this.zr.on('click', this.onClick);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected onDeactivate(): void {
|
|
26
|
+
this.chart.getZr().setCursorStyle('default');
|
|
27
|
+
this.zr.off('click', this.onClick);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected onDestroy(): void {}
|
|
31
|
+
|
|
32
|
+
private onClick = (params: any) => {
|
|
33
|
+
const point = this.getPoint(params);
|
|
34
|
+
if (!point) return;
|
|
35
|
+
|
|
36
|
+
const data = this.context.coordinateConversion.pixelToData({
|
|
37
|
+
x: point[0], y: point[1],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (data) {
|
|
41
|
+
this.context.addDrawing({
|
|
42
|
+
id: `hline-${Date.now()}`,
|
|
43
|
+
type: 'horizontal-line',
|
|
44
|
+
points: [data],
|
|
45
|
+
paneIndex: data.paneIndex || 0,
|
|
46
|
+
style: { color: '#d1d4dc', lineWidth: 1 },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.context.disableTools();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
export class HorizontalRayDrawingRenderer implements DrawingRenderer {
|
|
4
|
+
type = 'horizontal-ray';
|
|
5
|
+
|
|
6
|
+
render(ctx: DrawingRenderContext): any {
|
|
7
|
+
const { drawing, pixelPoints, isSelected, coordSys } = ctx;
|
|
8
|
+
const [px, py] = pixelPoints[0];
|
|
9
|
+
const color = drawing.style?.color || '#d1d4dc';
|
|
10
|
+
|
|
11
|
+
const right = coordSys.x + coordSys.width;
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
type: 'group',
|
|
15
|
+
children: [
|
|
16
|
+
{
|
|
17
|
+
type: 'line',
|
|
18
|
+
name: 'line',
|
|
19
|
+
shape: { x1: px, y1: py, x2: right, y2: py },
|
|
20
|
+
style: {
|
|
21
|
+
stroke: color,
|
|
22
|
+
lineWidth: drawing.style?.lineWidth || 1,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: 'circle',
|
|
27
|
+
name: 'point-0',
|
|
28
|
+
shape: { cx: px, cy: py, r: 4 },
|
|
29
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|