@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.
Files changed (34) hide show
  1. package/dist/index.d.ts +206 -1
  2. package/dist/qfchart.min.browser.js +16 -16
  3. package/dist/qfchart.min.es.js +16 -16
  4. package/package.json +1 -1
  5. package/src/QFChart.ts +11 -10
  6. package/src/components/LayoutManager.ts +51 -17
  7. package/src/index.ts +8 -0
  8. package/src/plugins/CrossLineTool/CrossLineDrawingRenderer.ts +49 -0
  9. package/src/plugins/CrossLineTool/CrossLineTool.ts +52 -0
  10. package/src/plugins/CrossLineTool/index.ts +2 -0
  11. package/src/plugins/ExtendedLineTool/ExtendedLineDrawingRenderer.ts +73 -0
  12. package/src/plugins/ExtendedLineTool/ExtendedLineTool.ts +173 -0
  13. package/src/plugins/ExtendedLineTool/index.ts +2 -0
  14. package/src/plugins/HorizontalLineTool/HorizontalLineDrawingRenderer.ts +54 -0
  15. package/src/plugins/HorizontalLineTool/HorizontalLineTool.ts +52 -0
  16. package/src/plugins/HorizontalLineTool/index.ts +2 -0
  17. package/src/plugins/HorizontalRayTool/HorizontalRayDrawingRenderer.ts +34 -0
  18. package/src/plugins/HorizontalRayTool/HorizontalRayTool.ts +52 -0
  19. package/src/plugins/HorizontalRayTool/index.ts +2 -0
  20. package/src/plugins/InfoLineTool/InfoLineDrawingRenderer.ts +72 -0
  21. package/src/plugins/InfoLineTool/InfoLineTool.ts +130 -0
  22. package/src/plugins/InfoLineTool/index.ts +2 -0
  23. package/src/plugins/LineTool/LineDrawingRenderer.ts +2 -2
  24. package/src/plugins/LineTool/LineTool.ts +5 -5
  25. package/src/plugins/RayTool/RayDrawingRenderer.ts +69 -0
  26. package/src/plugins/RayTool/RayTool.ts +162 -0
  27. package/src/plugins/RayTool/index.ts +2 -0
  28. package/src/plugins/TrendAngleTool/TrendAngleDrawingRenderer.ts +87 -0
  29. package/src/plugins/TrendAngleTool/TrendAngleTool.ts +176 -0
  30. package/src/plugins/TrendAngleTool/index.ts +2 -0
  31. package/src/plugins/VerticalLineTool/VerticalLineDrawingRenderer.ts +35 -0
  32. package/src/plugins/VerticalLineTool/VerticalLineTool.ts +52 -0
  33. package/src/plugins/VerticalLineTool/index.ts +2 -0
  34. package/src/types.ts +2 -0
@@ -0,0 +1,52 @@
1
+ import { AbstractPlugin } from '../../components/AbstractPlugin';
2
+ import { HorizontalRayDrawingRenderer } from './HorizontalRayDrawingRenderer';
3
+
4
+ export class HorizontalRayTool extends AbstractPlugin {
5
+ private zr!: any;
6
+
7
+ constructor(options: { name?: string; icon?: string } = {}) {
8
+ super({
9
+ id: 'horizontal-ray-tool',
10
+ name: options?.name || 'Horizontal Ray',
11
+ icon: options?.icon || `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="12" x2="22" y2="12"/><circle cx="4" 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 HorizontalRayDrawingRenderer());
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: `hray-${Date.now()}`,
43
+ type: 'horizontal-ray',
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,2 @@
1
+ export { HorizontalRayTool } from './HorizontalRayTool';
2
+ export { HorizontalRayDrawingRenderer } from './HorizontalRayDrawingRenderer';
@@ -0,0 +1,72 @@
1
+ import { DrawingRenderer, DrawingRenderContext } from '../../types';
2
+
3
+ export class InfoLineDrawingRenderer implements DrawingRenderer {
4
+ type = 'info-line';
5
+
6
+ render(ctx: DrawingRenderContext): any {
7
+ const { drawing, pixelPoints, isSelected } = ctx;
8
+ const [x1, y1] = pixelPoints[0];
9
+ const [x2, y2] = pixelPoints[1];
10
+ const color = drawing.style?.color || '#d1d4dc';
11
+
12
+ const p0 = drawing.points[0];
13
+ const p1 = drawing.points[1];
14
+
15
+ const priceChange = p1.value - p0.value;
16
+ const pctChange = p0.value !== 0 ? (priceChange / p0.value) * 100 : 0;
17
+ const bars = Math.abs(p1.timeIndex - p0.timeIndex);
18
+
19
+ const sign = priceChange >= 0 ? '+' : '';
20
+ const infoText = `${sign}${priceChange.toFixed(2)} (${sign}${pctChange.toFixed(2)}%) ${bars} bars`;
21
+
22
+ // Position info box at midpoint
23
+ const mx = (x1 + x2) / 2;
24
+ const my = (y1 + y2) / 2;
25
+ const isUp = priceChange >= 0;
26
+ const textColor = isUp ? '#26a69a' : '#ef5350';
27
+
28
+ return {
29
+ type: 'group',
30
+ children: [
31
+ {
32
+ type: 'line',
33
+ name: 'line',
34
+ shape: { x1, y1, x2, y2 },
35
+ style: { stroke: color, lineWidth: drawing.style?.lineWidth || 1 },
36
+ },
37
+ // Info box background
38
+ {
39
+ type: 'rect',
40
+ shape: { x: mx - 2, y: my - 22, width: infoText.length * 6.5 + 12, height: 18, r: 3 },
41
+ style: { fill: '#1e293b', stroke: '#475569', lineWidth: 1, opacity: 0.9 },
42
+ z2: 10,
43
+ },
44
+ // Info text
45
+ {
46
+ type: 'text',
47
+ x: mx + 4,
48
+ y: my - 20,
49
+ style: {
50
+ text: infoText,
51
+ fill: textColor,
52
+ fontSize: 11,
53
+ fontFamily: 'monospace',
54
+ },
55
+ z2: 11,
56
+ },
57
+ {
58
+ type: 'circle',
59
+ name: 'point-0',
60
+ shape: { cx: x1, cy: y1, r: 4 },
61
+ style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
62
+ },
63
+ {
64
+ type: 'circle',
65
+ name: 'point-1',
66
+ shape: { cx: x2, cy: y2, r: 4 },
67
+ style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
68
+ },
69
+ ],
70
+ };
71
+ }
72
+ }
@@ -0,0 +1,130 @@
1
+ import { AbstractPlugin } from '../../components/AbstractPlugin';
2
+ import { InfoLineDrawingRenderer } from './InfoLineDrawingRenderer';
3
+ import * as echarts from 'echarts';
4
+
5
+ type PluginState = 'idle' | 'drawing' | 'finished';
6
+
7
+ export class InfoLineTool extends AbstractPlugin {
8
+ private zr!: any;
9
+ private state: PluginState = 'idle';
10
+ private startPoint: number[] | null = null;
11
+ private endPoint: number[] | null = null;
12
+ private group: any = null;
13
+ private line: any = null;
14
+ private startCircle: any = null;
15
+ private endCircle: any = null;
16
+
17
+ constructor(options: { name?: string; icon?: string } = {}) {
18
+ super({
19
+ id: 'info-line-tool',
20
+ name: options?.name || 'Info Line',
21
+ 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"/><rect x="12" y="8" width="8" height="5" rx="1" fill="none" stroke-width="1.5"/></svg>`,
22
+ });
23
+ }
24
+
25
+ protected onInit(): void {
26
+ this.zr = this.chart.getZr();
27
+ this.context.registerDrawingRenderer(new InfoLineDrawingRenderer());
28
+ }
29
+
30
+ protected onActivate(): void {
31
+ this.state = 'idle';
32
+ this.chart.getZr().setCursorStyle('crosshair');
33
+ this.zr.on('click', this.onClick);
34
+ this.zr.on('mousemove', this.onMouseMove);
35
+ }
36
+
37
+ protected onDeactivate(): void {
38
+ this.state = 'idle';
39
+ this.chart.getZr().setCursorStyle('default');
40
+ this.zr.off('click', this.onClick);
41
+ this.zr.off('mousemove', this.onMouseMove);
42
+ this.removeGraphic();
43
+ }
44
+
45
+ protected onDestroy(): void {
46
+ this.removeGraphic();
47
+ }
48
+
49
+ private onClick = (params: any) => {
50
+ if (this.state === 'idle') {
51
+ this.state = 'drawing';
52
+ this.startPoint = this.getPoint(params);
53
+ this.endPoint = this.getPoint(params);
54
+ this.initGraphic();
55
+ this.updateGraphic();
56
+ } else if (this.state === 'drawing') {
57
+ this.state = 'finished';
58
+ this.endPoint = this.getPoint(params);
59
+ this.updateGraphic();
60
+
61
+ if (this.startPoint && this.endPoint) {
62
+ const start = this.context.coordinateConversion.pixelToData({
63
+ x: this.startPoint[0], y: this.startPoint[1],
64
+ });
65
+ const end = this.context.coordinateConversion.pixelToData({
66
+ x: this.endPoint[0], y: this.endPoint[1],
67
+ });
68
+
69
+ if (start && end) {
70
+ this.context.addDrawing({
71
+ id: `info-line-${Date.now()}`,
72
+ type: 'info-line',
73
+ points: [start, end],
74
+ paneIndex: start.paneIndex || 0,
75
+ style: { color: '#d1d4dc', lineWidth: 1 },
76
+ });
77
+ }
78
+ }
79
+
80
+ this.removeGraphic();
81
+ this.context.disableTools();
82
+ }
83
+ };
84
+
85
+ private onMouseMove = (params: any) => {
86
+ if (this.state !== 'drawing') return;
87
+ this.endPoint = this.getPoint(params);
88
+ this.updateGraphic();
89
+ };
90
+
91
+ private initGraphic(): void {
92
+ if (this.group) return;
93
+ this.group = new echarts.graphic.Group();
94
+ this.line = new echarts.graphic.Line({
95
+ shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
96
+ style: { stroke: '#d1d4dc', lineWidth: 1 },
97
+ z: 100,
98
+ });
99
+ this.startCircle = new echarts.graphic.Circle({
100
+ shape: { cx: 0, cy: 0, r: 4 },
101
+ style: { fill: '#fff', stroke: '#d1d4dc', lineWidth: 1 },
102
+ z: 101,
103
+ });
104
+ this.endCircle = new echarts.graphic.Circle({
105
+ shape: { cx: 0, cy: 0, r: 4 },
106
+ style: { fill: '#fff', stroke: '#d1d4dc', lineWidth: 1 },
107
+ z: 101,
108
+ });
109
+ this.group.add(this.line);
110
+ this.group.add(this.startCircle);
111
+ this.group.add(this.endCircle);
112
+ this.zr.add(this.group);
113
+ }
114
+
115
+ private removeGraphic(): void {
116
+ if (this.group) {
117
+ this.zr.remove(this.group);
118
+ this.group = null;
119
+ }
120
+ }
121
+
122
+ private updateGraphic(): void {
123
+ if (!this.startPoint || !this.endPoint || !this.group) return;
124
+ const [x1, y1] = this.startPoint;
125
+ const [x2, y2] = this.endPoint;
126
+ this.line.setShape({ x1, y1, x2, y2 });
127
+ this.startCircle.setShape({ cx: x1, cy: y1 });
128
+ this.endCircle.setShape({ cx: x2, cy: y2 });
129
+ }
130
+ }
@@ -0,0 +1,2 @@
1
+ export { InfoLineTool } from './InfoLineTool';
2
+ export { InfoLineDrawingRenderer } from './InfoLineDrawingRenderer';
@@ -7,7 +7,7 @@ export class LineDrawingRenderer implements DrawingRenderer {
7
7
  const { drawing, pixelPoints, isSelected } = ctx;
8
8
  const [x1, y1] = pixelPoints[0];
9
9
  const [x2, y2] = pixelPoints[1];
10
- const color = drawing.style?.color || '#3b82f6';
10
+ const color = drawing.style?.color || '#d1d4dc';
11
11
 
12
12
  return {
13
13
  type: 'group',
@@ -18,7 +18,7 @@ export class LineDrawingRenderer implements DrawingRenderer {
18
18
  shape: { x1, y1, x2, y2 },
19
19
  style: {
20
20
  stroke: color,
21
- lineWidth: drawing.style?.lineWidth || 2,
21
+ lineWidth: drawing.style?.lineWidth || 1,
22
22
  },
23
23
  },
24
24
  {
@@ -90,8 +90,8 @@ export class LineTool extends AbstractPlugin {
90
90
  points: [start, end],
91
91
  paneIndex: paneIndex,
92
92
  style: {
93
- color: '#3b82f6',
94
- lineWidth: 2,
93
+ color: '#d1d4dc',
94
+ lineWidth: 1,
95
95
  },
96
96
  });
97
97
  }
@@ -118,19 +118,19 @@ export class LineTool extends AbstractPlugin {
118
118
 
119
119
  this.line = new echarts.graphic.Line({
120
120
  shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
121
- style: { stroke: '#3b82f6', lineWidth: 2 },
121
+ style: { stroke: '#d1d4dc', lineWidth: 1 },
122
122
  z: 100,
123
123
  });
124
124
 
125
125
  this.startCircle = new echarts.graphic.Circle({
126
126
  shape: { cx: 0, cy: 0, r: 4 },
127
- style: { fill: '#fff', stroke: '#3b82f6', lineWidth: 1 },
127
+ style: { fill: '#fff', stroke: '#d1d4dc', lineWidth: 1 },
128
128
  z: 101,
129
129
  });
130
130
 
131
131
  this.endCircle = new echarts.graphic.Circle({
132
132
  shape: { cx: 0, cy: 0, r: 4 },
133
- style: { fill: '#fff', stroke: '#3b82f6', lineWidth: 1 },
133
+ style: { fill: '#fff', stroke: '#d1d4dc', lineWidth: 1 },
134
134
  z: 101,
135
135
  });
136
136
 
@@ -0,0 +1,69 @@
1
+ import { DrawingRenderer, DrawingRenderContext } from '../../types';
2
+
3
+ export class RayDrawingRenderer implements DrawingRenderer {
4
+ type = 'ray';
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
+ // Extend the ray from p1 through p2 to the chart boundary
13
+ const [ex, ey] = this.extendToEdge(x1, y1, x2, y2, coordSys);
14
+
15
+ return {
16
+ type: 'group',
17
+ children: [
18
+ {
19
+ type: 'line',
20
+ name: 'line',
21
+ shape: { x1, y1, x2: ex, y2: ey },
22
+ style: {
23
+ stroke: color,
24
+ lineWidth: drawing.style?.lineWidth || 1,
25
+ },
26
+ },
27
+ {
28
+ type: 'circle',
29
+ name: 'point-0',
30
+ shape: { cx: x1, cy: y1, r: 4 },
31
+ style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
32
+ },
33
+ {
34
+ type: 'circle',
35
+ name: 'point-1',
36
+ shape: { cx: x2, cy: y2, r: 4 },
37
+ style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
38
+ },
39
+ ],
40
+ };
41
+ }
42
+
43
+ private extendToEdge(
44
+ x1: number, y1: number, x2: number, y2: number,
45
+ cs: { x: number; y: number; width: number; height: number },
46
+ ): [number, number] {
47
+ const dx = x2 - x1;
48
+ const dy = y2 - y1;
49
+ if (dx === 0 && dy === 0) return [x2, y2];
50
+
51
+ const left = cs.x;
52
+ const right = cs.x + cs.width;
53
+ const top = cs.y;
54
+ const bottom = cs.y + cs.height;
55
+
56
+ let tMax = Infinity;
57
+ if (dx !== 0) {
58
+ const tx = dx > 0 ? (right - x1) / dx : (left - x1) / dx;
59
+ if (tx > 0) tMax = Math.min(tMax, tx);
60
+ }
61
+ if (dy !== 0) {
62
+ const ty = dy > 0 ? (bottom - y1) / dy : (top - y1) / dy;
63
+ if (ty > 0) tMax = Math.min(tMax, ty);
64
+ }
65
+ if (!isFinite(tMax)) tMax = 1;
66
+
67
+ return [x1 + tMax * dx, y1 + tMax * dy];
68
+ }
69
+ }
@@ -0,0 +1,162 @@
1
+ import { AbstractPlugin } from '../../components/AbstractPlugin';
2
+ import { RayDrawingRenderer } from './RayDrawingRenderer';
3
+ import * as echarts from 'echarts';
4
+
5
+ const COLOR = '#d1d4dc';
6
+
7
+ type PluginState = 'idle' | 'drawing' | 'finished';
8
+
9
+ export class RayTool 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 dashLine: any = null;
17
+ private startCircle: any = null;
18
+ private endCircle: any = null;
19
+
20
+ constructor(options: { name?: string; icon?: string } = {}) {
21
+ super({
22
+ id: 'ray-tool',
23
+ name: options?.name || 'Ray',
24
+ icon: options?.icon || `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="20" x2="21" y2="4"/><circle cx="21" cy="4" r="0" fill="currentColor"/><polyline points="16,4 21,4 21,9" stroke-width="1.5"/></svg>`,
25
+ });
26
+ }
27
+
28
+ protected onInit(): void {
29
+ this.zr = this.chart.getZr();
30
+ this.context.registerDrawingRenderer(new RayDrawingRenderer());
31
+ }
32
+
33
+ protected onActivate(): void {
34
+ this.state = 'idle';
35
+ this.chart.getZr().setCursorStyle('crosshair');
36
+ this.zr.on('click', this.onClick);
37
+ this.zr.on('mousemove', this.onMouseMove);
38
+ }
39
+
40
+ protected onDeactivate(): void {
41
+ this.state = 'idle';
42
+ this.chart.getZr().setCursorStyle('default');
43
+ this.zr.off('click', this.onClick);
44
+ this.zr.off('mousemove', this.onMouseMove);
45
+ this.removeGraphic();
46
+ }
47
+
48
+ protected onDestroy(): void {
49
+ this.removeGraphic();
50
+ }
51
+
52
+ private onClick = (params: any) => {
53
+ if (this.state === 'idle') {
54
+ this.state = 'drawing';
55
+ this.startPoint = this.getPoint(params);
56
+ this.endPoint = this.getPoint(params);
57
+ this.initGraphic();
58
+ this.updateGraphic();
59
+ } else if (this.state === 'drawing') {
60
+ this.state = 'finished';
61
+ this.endPoint = this.getPoint(params);
62
+
63
+ if (this.startPoint && this.endPoint) {
64
+ const start = this.context.coordinateConversion.pixelToData({
65
+ x: this.startPoint[0], y: this.startPoint[1],
66
+ });
67
+ const end = this.context.coordinateConversion.pixelToData({
68
+ x: this.endPoint[0], y: this.endPoint[1],
69
+ });
70
+
71
+ if (start && end) {
72
+ this.context.addDrawing({
73
+ id: `ray-${Date.now()}`,
74
+ type: 'ray',
75
+ points: [start, end],
76
+ paneIndex: start.paneIndex || 0,
77
+ style: { color: COLOR, lineWidth: 1 },
78
+ });
79
+ }
80
+ }
81
+
82
+ this.removeGraphic();
83
+ this.context.disableTools();
84
+ }
85
+ };
86
+
87
+ private onMouseMove = (params: any) => {
88
+ if (this.state !== 'drawing') return;
89
+ this.endPoint = this.getPoint(params);
90
+ this.updateGraphic();
91
+ };
92
+
93
+ private initGraphic(): void {
94
+ if (this.group) return;
95
+ this.group = new echarts.graphic.Group();
96
+ this.line = new echarts.graphic.Line({
97
+ shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
98
+ style: { stroke: COLOR, lineWidth: 1 },
99
+ z: 100,
100
+ });
101
+ this.dashLine = new echarts.graphic.Line({
102
+ shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
103
+ style: { stroke: COLOR, lineWidth: 1, lineDash: [4, 4], opacity: 0.5 },
104
+ z: 99,
105
+ });
106
+ this.startCircle = new echarts.graphic.Circle({
107
+ shape: { cx: 0, cy: 0, r: 4 },
108
+ style: { fill: '#fff', stroke: COLOR, lineWidth: 1 },
109
+ z: 101,
110
+ });
111
+ this.endCircle = new echarts.graphic.Circle({
112
+ shape: { cx: 0, cy: 0, r: 4 },
113
+ style: { fill: '#fff', stroke: COLOR, lineWidth: 1 },
114
+ z: 101,
115
+ });
116
+ this.group.add(this.dashLine);
117
+ this.group.add(this.line);
118
+ this.group.add(this.startCircle);
119
+ this.group.add(this.endCircle);
120
+ this.zr.add(this.group);
121
+ }
122
+
123
+ private removeGraphic(): void {
124
+ if (this.group) {
125
+ this.zr.remove(this.group);
126
+ this.group = null;
127
+ }
128
+ }
129
+
130
+ private updateGraphic(): void {
131
+ if (!this.startPoint || !this.endPoint || !this.group) return;
132
+ const [x1, y1] = this.startPoint;
133
+ const [x2, y2] = this.endPoint;
134
+ this.line.setShape({ x1, y1, x2, y2 });
135
+ this.startCircle.setShape({ cx: x1, cy: y1 });
136
+ this.endCircle.setShape({ cx: x2, cy: y2 });
137
+
138
+ // Dashed extension from p2 to chart edge
139
+ const [ex, ey] = this.extendToEdge(x1, y1, x2, y2);
140
+ this.dashLine.setShape({ x1: x2, y1: y2, x2: ex, y2: ey });
141
+ }
142
+
143
+ private extendToEdge(x1: number, y1: number, x2: number, y2: number): [number, number] {
144
+ const dx = x2 - x1;
145
+ const dy = y2 - y1;
146
+ if (dx === 0 && dy === 0) return [x2, y2];
147
+
148
+ const w = this.chart.getWidth();
149
+ const h = this.chart.getHeight();
150
+ let tMax = Infinity;
151
+ if (dx !== 0) {
152
+ const tx = dx > 0 ? (w - x1) / dx : -x1 / dx;
153
+ if (tx > 0) tMax = Math.min(tMax, tx);
154
+ }
155
+ if (dy !== 0) {
156
+ const ty = dy > 0 ? (h - y1) / dy : -y1 / dy;
157
+ if (ty > 0) tMax = Math.min(tMax, ty);
158
+ }
159
+ if (!isFinite(tMax)) tMax = 1;
160
+ return [x1 + tMax * dx, y1 + tMax * dy];
161
+ }
162
+ }
@@ -0,0 +1,2 @@
1
+ export { RayTool } from './RayTool';
2
+ export { RayDrawingRenderer } from './RayDrawingRenderer';
@@ -0,0 +1,87 @@
1
+ import { DrawingRenderer, DrawingRenderContext } from '../../types';
2
+
3
+ export class TrendAngleDrawingRenderer implements DrawingRenderer {
4
+ type = 'trend-angle';
5
+
6
+ render(ctx: DrawingRenderContext): any {
7
+ const { drawing, pixelPoints, isSelected } = 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
+ // Angle in degrees (screen Y is inverted, so negate dy for natural angle)
16
+ const angleRad = Math.atan2(-dy, dx);
17
+ const angleDeg = angleRad * (180 / Math.PI);
18
+ const displayAngle = angleDeg.toFixed(1);
19
+
20
+ // Arc radius
21
+ const arcR = Math.min(30, Math.sqrt(dx * dx + dy * dy) * 0.3);
22
+
23
+ // Horizontal reference line from p1 extending right
24
+ const hLineEndX = x1 + Math.max(Math.abs(dx), arcR + 20);
25
+
26
+ // Arc path: from 0 degrees (horizontal right) to the angle
27
+ // In screen coords, positive angle goes CCW (since Y is inverted)
28
+ const startAngle = 0;
29
+ const endAngle = -angleRad; // Convert back to screen angle
30
+
31
+ const children: any[] = [
32
+ // Main trend line
33
+ {
34
+ type: 'line',
35
+ name: 'line',
36
+ shape: { x1, y1, x2, y2 },
37
+ style: { stroke: color, lineWidth: drawing.style?.lineWidth || 1 },
38
+ },
39
+ // Horizontal reference line
40
+ {
41
+ type: 'line',
42
+ shape: { x1, y1, x2: hLineEndX, y2: y1 },
43
+ style: { stroke: color, lineWidth: 1, opacity: 0.4, lineDash: [4, 4] },
44
+ },
45
+ // Arc
46
+ {
47
+ type: 'arc',
48
+ shape: {
49
+ cx: x1,
50
+ cy: y1,
51
+ r: arcR,
52
+ startAngle: Math.min(startAngle, endAngle),
53
+ endAngle: Math.max(startAngle, endAngle),
54
+ },
55
+ style: { stroke: color, lineWidth: 1.5, fill: 'none' },
56
+ },
57
+ // Angle label
58
+ {
59
+ type: 'text',
60
+ x: x1 + arcR + 6,
61
+ y: y1 + (dy < 0 ? -14 : 2),
62
+ style: {
63
+ text: `${displayAngle}\u00B0`,
64
+ fill: color,
65
+ fontSize: 11,
66
+ fontFamily: 'sans-serif',
67
+ },
68
+ z2: 10,
69
+ },
70
+ // Control points
71
+ {
72
+ type: 'circle',
73
+ name: 'point-0',
74
+ shape: { cx: x1, cy: y1, r: 4 },
75
+ style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
76
+ },
77
+ {
78
+ type: 'circle',
79
+ name: 'point-1',
80
+ shape: { cx: x2, cy: y2, r: 4 },
81
+ style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
82
+ },
83
+ ];
84
+
85
+ return { type: 'group', children };
86
+ }
87
+ }