@qfo/qfchart 0.8.1 → 0.8.4
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 +216 -1
- package/dist/qfchart.min.browser.js +20 -19
- package/dist/qfchart.min.es.js +18 -17
- package/package.json +1 -1
- package/src/QFChart.ts +146 -11
- package/src/components/LayoutManager.ts +76 -28
- package/src/components/PluginManager.ts +229 -229
- package/src/components/SeriesBuilder.ts +21 -14
- package/src/components/renderers/LabelRenderer.ts +6 -3
- package/src/components/renderers/ScatterRenderer.ts +92 -54
- package/src/components/renderers/ShapeRenderer.ts +12 -0
- 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
|
@@ -1,54 +1,92 @@
|
|
|
1
|
-
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
-
import { textToBase64Image } from '../../utils/CanvasUtils';
|
|
3
|
-
|
|
4
|
-
export class ScatterRenderer implements SeriesRenderer {
|
|
5
|
-
render(context: RenderContext): any {
|
|
6
|
-
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, plotOptions } = context;
|
|
7
|
-
const defaultColor = '#2962ff';
|
|
8
|
-
const style = plotOptions.style; // 'circles', 'cross', 'char'
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
if (style === 'char') {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
+
import { textToBase64Image } from '../../utils/CanvasUtils';
|
|
3
|
+
|
|
4
|
+
export class ScatterRenderer implements SeriesRenderer {
|
|
5
|
+
render(context: RenderContext): any {
|
|
6
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, plotOptions } = context;
|
|
7
|
+
const defaultColor = '#2962ff';
|
|
8
|
+
const style = plotOptions.style; // 'circles', 'cross', 'char'
|
|
9
|
+
|
|
10
|
+
// plotchar: render the Unicode character at the data point
|
|
11
|
+
if (style === 'char') {
|
|
12
|
+
const { optionsArray, candlestickData } = context;
|
|
13
|
+
const defaultChar = plotOptions.char || '•';
|
|
14
|
+
const defaultLocation = plotOptions.location || 'abovebar';
|
|
15
|
+
|
|
16
|
+
const charData = dataArray
|
|
17
|
+
.map((val, i) => {
|
|
18
|
+
if (val === null || val === undefined || (typeof val === 'number' && isNaN(val))) return null;
|
|
19
|
+
|
|
20
|
+
const pointOpts = optionsArray?.[i] || {};
|
|
21
|
+
const char = pointOpts.char || defaultChar;
|
|
22
|
+
const color = pointOpts.color || colorArray[i] || plotOptions.color || defaultColor;
|
|
23
|
+
const location = pointOpts.location || defaultLocation;
|
|
24
|
+
const size = pointOpts.size || plotOptions.size || 'normal';
|
|
25
|
+
|
|
26
|
+
// Positioning based on location
|
|
27
|
+
let yValue = val;
|
|
28
|
+
let symbolOffset: (string | number)[] = [0, 0];
|
|
29
|
+
|
|
30
|
+
if (location === 'abovebar' || location === 'AboveBar' || location === 'ab') {
|
|
31
|
+
if (candlestickData && candlestickData[i]) yValue = candlestickData[i].high;
|
|
32
|
+
symbolOffset = [0, '-150%'];
|
|
33
|
+
} else if (location === 'belowbar' || location === 'BelowBar' || location === 'bl') {
|
|
34
|
+
if (candlestickData && candlestickData[i]) yValue = candlestickData[i].low;
|
|
35
|
+
symbolOffset = [0, '150%'];
|
|
36
|
+
}
|
|
37
|
+
// absolute / top / bottom: yValue stays as-is
|
|
38
|
+
|
|
39
|
+
// Size mapping — matches TradingView's plotchar sizing
|
|
40
|
+
const sizeMap: Record<string, string> = {
|
|
41
|
+
tiny: '18px', small: '26px', normal: '34px', large: '42px', huge: '54px', auto: '28px',
|
|
42
|
+
};
|
|
43
|
+
const fontSize = sizeMap[size] || '34px';
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
value: [i, yValue],
|
|
47
|
+
symbol: `image://${textToBase64Image(char, color, fontSize)}`,
|
|
48
|
+
symbolSize: parseInt(fontSize) + 8,
|
|
49
|
+
symbolOffset: symbolOffset,
|
|
50
|
+
};
|
|
51
|
+
})
|
|
52
|
+
.filter((item) => item !== null);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name: seriesName,
|
|
56
|
+
type: 'scatter',
|
|
57
|
+
xAxisIndex: xAxisIndex,
|
|
58
|
+
yAxisIndex: yAxisIndex,
|
|
59
|
+
z: 10, // Render in front of candles
|
|
60
|
+
data: charData,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const scatterData = dataArray
|
|
65
|
+
.map((val, i) => {
|
|
66
|
+
if (val === null) return null;
|
|
67
|
+
const pointColor = colorArray[i] || plotOptions.color || defaultColor;
|
|
68
|
+
const item: any = {
|
|
69
|
+
value: [i, val],
|
|
70
|
+
itemStyle: { color: pointColor },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (style === 'cross') {
|
|
74
|
+
item.symbol = `image://${textToBase64Image('+', pointColor, '24px')}`;
|
|
75
|
+
item.symbolSize = 16;
|
|
76
|
+
} else {
|
|
77
|
+
item.symbol = 'circle';
|
|
78
|
+
item.symbolSize = 6;
|
|
79
|
+
}
|
|
80
|
+
return item;
|
|
81
|
+
})
|
|
82
|
+
.filter((item) => item !== null);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
name: seriesName,
|
|
86
|
+
type: 'scatter',
|
|
87
|
+
xAxisIndex: xAxisIndex,
|
|
88
|
+
yAxisIndex: yAxisIndex,
|
|
89
|
+
data: scatterData,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -40,6 +40,8 @@ export class ShapeRenderer implements SeriesRenderer {
|
|
|
40
40
|
// Positioning based on location
|
|
41
41
|
let yValue = val; // Default to absolute value
|
|
42
42
|
let symbolOffset: (string | number)[] = [0, 0];
|
|
43
|
+
const isLabelUp = shape.includes('label_up') || shape === 'labelup';
|
|
44
|
+
const isLabelDown = shape.includes('label_down') || shape === 'labeldown';
|
|
43
45
|
|
|
44
46
|
if (location === 'abovebar' || location === 'AboveBar' || location === 'ab') {
|
|
45
47
|
// Shape above the candle
|
|
@@ -83,6 +85,15 @@ export class ShapeRenderer implements SeriesRenderer {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
// Anchor labelup/labeldown so the arrow tip sits at the data point.
|
|
89
|
+
// labelup: arrow tip points up (at top of shape) → shift shape down
|
|
90
|
+
// labeldown: arrow tip points down (at bottom of shape) → shift shape up
|
|
91
|
+
if (isLabelUp) {
|
|
92
|
+
symbolOffset = [symbolOffset[0], '50%'];
|
|
93
|
+
} else if (isLabelDown) {
|
|
94
|
+
symbolOffset = [symbolOffset[0], '-50%'];
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
// Get label configuration based on location
|
|
87
98
|
const labelConfig = ShapeUtils.getLabelConfig(shape, location);
|
|
88
99
|
|
|
@@ -115,6 +126,7 @@ export class ShapeRenderer implements SeriesRenderer {
|
|
|
115
126
|
type: 'scatter',
|
|
116
127
|
xAxisIndex: xAxisIndex,
|
|
117
128
|
yAxisIndex: yAxisIndex,
|
|
129
|
+
z: 10, // Render shapes in front of candles (z: 5)
|
|
118
130
|
data: shapeData,
|
|
119
131
|
};
|
|
120
132
|
}
|
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
|
+
}
|