@qfo/qfchart 0.7.3 → 0.8.1

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 (59) hide show
  1. package/dist/index.d.ts +368 -14
  2. package/dist/qfchart.min.browser.js +34 -16
  3. package/dist/qfchart.min.es.js +34 -16
  4. package/package.json +1 -1
  5. package/src/QFChart.ts +460 -311
  6. package/src/components/AbstractPlugin.ts +234 -104
  7. package/src/components/DrawingEditor.ts +297 -248
  8. package/src/components/DrawingRendererRegistry.ts +13 -0
  9. package/src/components/GraphicBuilder.ts +284 -263
  10. package/src/components/LayoutManager.ts +72 -55
  11. package/src/components/SeriesBuilder.ts +110 -6
  12. package/src/components/TableCanvasRenderer.ts +467 -0
  13. package/src/components/TableOverlayRenderer.ts +38 -9
  14. package/src/components/TooltipFormatter.ts +97 -97
  15. package/src/components/renderers/BackgroundRenderer.ts +59 -47
  16. package/src/components/renderers/BoxRenderer.ts +113 -17
  17. package/src/components/renderers/FillRenderer.ts +118 -3
  18. package/src/components/renderers/LabelRenderer.ts +35 -9
  19. package/src/components/renderers/OHLCBarRenderer.ts +171 -161
  20. package/src/components/renderers/PolylineRenderer.ts +26 -19
  21. package/src/index.ts +17 -6
  22. package/src/plugins/ABCDPatternTool/ABCDPatternDrawingRenderer.ts +112 -0
  23. package/src/plugins/ABCDPatternTool/ABCDPatternTool.ts +136 -0
  24. package/src/plugins/ABCDPatternTool/index.ts +2 -0
  25. package/src/plugins/CypherPatternTool/CypherPatternDrawingRenderer.ts +80 -0
  26. package/src/plugins/CypherPatternTool/CypherPatternTool.ts +84 -0
  27. package/src/plugins/CypherPatternTool/index.ts +2 -0
  28. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanDrawingRenderer.ts +163 -0
  29. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanTool.ts +210 -0
  30. package/src/plugins/FibSpeedResistanceFanTool/index.ts +2 -0
  31. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionDrawingRenderer.ts +141 -0
  32. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionTool.ts +188 -0
  33. package/src/plugins/FibTrendExtensionTool/index.ts +2 -0
  34. package/src/plugins/FibonacciChannelTool/FibonacciChannelDrawingRenderer.ts +128 -0
  35. package/src/plugins/FibonacciChannelTool/FibonacciChannelTool.ts +231 -0
  36. package/src/plugins/FibonacciChannelTool/index.ts +2 -0
  37. package/src/plugins/FibonacciTool/FibonacciDrawingRenderer.ts +107 -0
  38. package/src/plugins/{FibonacciTool.ts → FibonacciTool/FibonacciTool.ts} +195 -192
  39. package/src/plugins/FibonacciTool/index.ts +2 -0
  40. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersDrawingRenderer.ts +95 -0
  41. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersTool.ts +97 -0
  42. package/src/plugins/HeadAndShouldersTool/index.ts +2 -0
  43. package/src/plugins/LineTool/LineDrawingRenderer.ts +49 -0
  44. package/src/plugins/{LineTool.ts → LineTool/LineTool.ts} +161 -190
  45. package/src/plugins/LineTool/index.ts +2 -0
  46. package/src/plugins/{MeasureTool.ts → MeasureTool/MeasureTool.ts} +324 -344
  47. package/src/plugins/MeasureTool/index.ts +1 -0
  48. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternDrawingRenderer.ts +106 -0
  49. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternTool.ts +98 -0
  50. package/src/plugins/ThreeDrivesPatternTool/index.ts +2 -0
  51. package/src/plugins/ToolGroup.ts +211 -0
  52. package/src/plugins/TrianglePatternTool/TrianglePatternDrawingRenderer.ts +107 -0
  53. package/src/plugins/TrianglePatternTool/TrianglePatternTool.ts +98 -0
  54. package/src/plugins/TrianglePatternTool/index.ts +2 -0
  55. package/src/plugins/XABCDPatternTool/XABCDPatternDrawingRenderer.ts +178 -0
  56. package/src/plugins/XABCDPatternTool/XABCDPatternTool.ts +213 -0
  57. package/src/plugins/XABCDPatternTool/index.ts +2 -0
  58. package/src/types.ts +39 -4
  59. package/src/utils/ColorUtils.ts +1 -1
@@ -24,14 +24,19 @@ export class LabelRenderer implements SeriesRenderer {
24
24
 
25
25
  const labelData = labelObjects
26
26
  .map((lbl) => {
27
- const text = lbl.text || '';
28
- const color = lbl.color || '#2962ff';
29
- const textcolor = lbl.textcolor || '#ffffff';
30
- const yloc = lbl.yloc || 'price';
31
- const styleRaw = lbl.style || 'style_label_down';
32
- const size = lbl.size || 'normal';
33
- const textalign = lbl.textalign || 'align_center';
34
- const tooltip = lbl.tooltip || '';
27
+ // Resolve any function/Series values that may not have been
28
+ // resolved at PineTS level (e.g. setters that skip _resolve()).
29
+ const resolve = (v: any) => typeof v === 'function' ? v() : v;
30
+
31
+ const text = resolve(lbl.text) || '';
32
+ const rawColor = resolve(lbl.color);
33
+ const color = (rawColor != null && rawColor !== '') ? rawColor : 'transparent';
34
+ const textcolor = resolve(lbl.textcolor) || '#ffffff';
35
+ const yloc = resolve(lbl.yloc) || 'price';
36
+ const styleRaw = resolve(lbl.style) || 'style_label_down';
37
+ const size = resolve(lbl.size) || 'normal';
38
+ const textalign = resolve(lbl.textalign) || 'align_center';
39
+ const tooltip = resolve(lbl.tooltip) || '';
35
40
 
36
41
  // Map Pine style string to shape name for ShapeUtils
37
42
  const shape = this.styleToShape(styleRaw);
@@ -160,7 +165,24 @@ export class LabelRenderer implements SeriesRenderer {
160
165
  };
161
166
 
162
167
  if (tooltip) {
163
- item.tooltip = { formatter: tooltip };
168
+ // Store tooltip text for the custom tooltip overlay in QFChart.ts.
169
+ // ECharts mouseover event can read this from params.data._tooltipText.
170
+ item._tooltipText = tooltip;
171
+ // Enable emphasis for this item so ECharts fires mouseover/mouseout
172
+ // events, but prevent any visual change by mirroring normal styles.
173
+ item.emphasis = {
174
+ scale: false,
175
+ itemStyle: { color: color },
176
+ label: {
177
+ show: item.label.show,
178
+ color: textcolor,
179
+ fontSize: fontSize,
180
+ fontWeight: 'bold',
181
+ },
182
+ };
183
+ } else {
184
+ // No tooltip: fully disable emphasis (no hover interaction)
185
+ item.emphasis = { disabled: true };
164
186
  }
165
187
 
166
188
  return item;
@@ -174,6 +196,10 @@ export class LabelRenderer implements SeriesRenderer {
174
196
  yAxisIndex: yAxisIndex,
175
197
  data: labelData,
176
198
  z: 20,
199
+ // Per-item emphasis: disabled for labels without tooltips,
200
+ // scale:false for labels with tooltips (allows hover for custom tooltip).
201
+ animation: false, // Prevent labels disappearing on zoom
202
+ clip: false, // Keep labels visible when partially outside viewport
177
203
  };
178
204
  }
179
205
 
@@ -1,161 +1,171 @@
1
- import { SeriesRenderer, RenderContext } from './SeriesRenderer';
2
-
3
- export class OHLCBarRenderer implements SeriesRenderer {
4
- render(context: RenderContext): any {
5
- const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, optionsArray, plotOptions } = context;
6
- const defaultColor = '#2962ff';
7
- const isCandle = plotOptions.style === 'candle';
8
-
9
- const ohlcData = dataArray
10
- .map((val, i) => {
11
- if (val === null || !Array.isArray(val) || val.length !== 4) return null;
12
-
13
- const [open, high, low, close] = val;
14
- const pointOpts = optionsArray[i] || {};
15
- const color = pointOpts.color || colorArray[i] || plotOptions.color || defaultColor;
16
- const wickColor = pointOpts.wickcolor || plotOptions.wickcolor || color;
17
- const borderColor = pointOpts.bordercolor || plotOptions.bordercolor || wickColor;
18
-
19
- // Store colors in value array at positions 5, 6, and 7 for access in renderItem
20
- return [i, open, close, low, high, color, wickColor, borderColor];
21
- })
22
- .filter((item) => item !== null);
23
-
24
- return {
25
- name: seriesName,
26
- type: 'custom',
27
- xAxisIndex: xAxisIndex,
28
- yAxisIndex: yAxisIndex,
29
- renderItem: (params: any, api: any) => {
30
- const xValue = api.value(0);
31
- const openValue = api.value(1);
32
- const closeValue = api.value(2);
33
- const lowValue = api.value(3);
34
- const highValue = api.value(4);
35
- const color = api.value(5);
36
- const wickColor = api.value(6);
37
- const borderColor = api.value(7);
38
-
39
- if (isNaN(openValue) || isNaN(closeValue) || isNaN(lowValue) || isNaN(highValue)) {
40
- return null;
41
- }
42
-
43
- const xPos = api.coord([xValue, 0])[0];
44
- const openPos = api.coord([xValue, openValue])[1];
45
- const closePos = api.coord([xValue, closeValue])[1];
46
- const lowPos = api.coord([xValue, lowValue])[1];
47
- const highPos = api.coord([xValue, highValue])[1];
48
-
49
- const barWidth = api.size([1, 0])[0] * 0.6;
50
-
51
- if (isCandle) {
52
- // Classic candlestick rendering
53
- const bodyTop = Math.min(openPos, closePos);
54
- const bodyBottom = Math.max(openPos, closePos);
55
- const bodyHeight = Math.abs(closePos - openPos);
56
-
57
- return {
58
- type: 'group',
59
- children: [
60
- // Upper wick
61
- {
62
- type: 'line',
63
- shape: {
64
- x1: xPos,
65
- y1: highPos,
66
- x2: xPos,
67
- y2: bodyTop,
68
- },
69
- style: {
70
- stroke: wickColor,
71
- lineWidth: 1,
72
- },
73
- },
74
- // Lower wick
75
- {
76
- type: 'line',
77
- shape: {
78
- x1: xPos,
79
- y1: bodyBottom,
80
- x2: xPos,
81
- y2: lowPos,
82
- },
83
- style: {
84
- stroke: wickColor,
85
- lineWidth: 1,
86
- },
87
- },
88
- // Body
89
- {
90
- type: 'rect',
91
- shape: {
92
- x: xPos - barWidth / 2,
93
- y: bodyTop,
94
- width: barWidth,
95
- height: bodyHeight || 1, // Minimum height for doji
96
- },
97
- style: {
98
- fill: color,
99
- stroke: borderColor,
100
- lineWidth: 1,
101
- },
102
- },
103
- ],
104
- };
105
- } else {
106
- // Bar style (OHLC bar)
107
- const tickWidth = barWidth * 0.5;
108
-
109
- return {
110
- type: 'group',
111
- children: [
112
- // Vertical line (low to high)
113
- {
114
- type: 'line',
115
- shape: {
116
- x1: xPos,
117
- y1: lowPos,
118
- x2: xPos,
119
- y2: highPos,
120
- },
121
- style: {
122
- stroke: color,
123
- lineWidth: 1,
124
- },
125
- },
126
- // Open tick (left)
127
- {
128
- type: 'line',
129
- shape: {
130
- x1: xPos - tickWidth,
131
- y1: openPos,
132
- x2: xPos,
133
- y2: openPos,
134
- },
135
- style: {
136
- stroke: color,
137
- lineWidth: 1,
138
- },
139
- },
140
- // Close tick (right)
141
- {
142
- type: 'line',
143
- shape: {
144
- x1: xPos,
145
- y1: closePos,
146
- x2: xPos + tickWidth,
147
- y2: closePos,
148
- },
149
- style: {
150
- stroke: color,
151
- lineWidth: 1,
152
- },
153
- },
154
- ],
155
- };
156
- }
157
- },
158
- data: ohlcData,
159
- };
160
- }
161
- }
1
+ import { SeriesRenderer, RenderContext } from './SeriesRenderer';
2
+
3
+ export class OHLCBarRenderer implements SeriesRenderer {
4
+ render(context: RenderContext): any {
5
+ const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, optionsArray, plotOptions } = context;
6
+ const defaultColor = '#2962ff';
7
+ const isCandle = plotOptions.style === 'candle';
8
+
9
+ // Build a separate color lookup — ECharts custom series coerces data values to numbers,
10
+ // so string colors stored in the data array would become NaN via api.value().
11
+ const colorLookup: { color: string; wickColor: string; borderColor: string }[] = [];
12
+
13
+ const ohlcData = dataArray
14
+ .map((val, i) => {
15
+ if (val === null || !Array.isArray(val) || val.length !== 4) return null;
16
+
17
+ const [open, high, low, close] = val;
18
+ const pointOpts = optionsArray[i] || {};
19
+ const color = pointOpts.color || colorArray[i] || plotOptions.color || defaultColor;
20
+ const wickColor = pointOpts.wickcolor || plotOptions.wickcolor || color;
21
+ const borderColor = pointOpts.bordercolor || plotOptions.bordercolor || wickColor;
22
+
23
+ // Store colors in a closure-accessible lookup keyed by the data index
24
+ colorLookup[i] = { color, wickColor, borderColor };
25
+
26
+ // Data array contains only numeric values for ECharts
27
+ return [i, open, close, low, high];
28
+ })
29
+ .filter((item) => item !== null);
30
+
31
+ return {
32
+ name: seriesName,
33
+ type: 'custom',
34
+ xAxisIndex: xAxisIndex,
35
+ yAxisIndex: yAxisIndex,
36
+ renderItem: (params: any, api: any) => {
37
+ const xValue = api.value(0);
38
+ const openValue = api.value(1);
39
+ const closeValue = api.value(2);
40
+ const lowValue = api.value(3);
41
+ const highValue = api.value(4);
42
+
43
+ if (isNaN(openValue) || isNaN(closeValue) || isNaN(lowValue) || isNaN(highValue)) {
44
+ return null;
45
+ }
46
+
47
+ // Retrieve colors from the closure-based lookup using the original data index
48
+ const colors = colorLookup[xValue] || { color: defaultColor, wickColor: defaultColor, borderColor: defaultColor };
49
+ const color = colors.color;
50
+ const wickColor = colors.wickColor;
51
+ const borderColor = colors.borderColor;
52
+
53
+ const xPos = api.coord([xValue, 0])[0];
54
+ const openPos = api.coord([xValue, openValue])[1];
55
+ const closePos = api.coord([xValue, closeValue])[1];
56
+ const lowPos = api.coord([xValue, lowValue])[1];
57
+ const highPos = api.coord([xValue, highValue])[1];
58
+
59
+ const barWidth = api.size([1, 0])[0] * 0.6;
60
+
61
+ if (isCandle) {
62
+ // Classic candlestick rendering
63
+ const bodyTop = Math.min(openPos, closePos);
64
+ const bodyBottom = Math.max(openPos, closePos);
65
+ const bodyHeight = Math.abs(closePos - openPos);
66
+
67
+ return {
68
+ type: 'group',
69
+ children: [
70
+ // Upper wick
71
+ {
72
+ type: 'line',
73
+ shape: {
74
+ x1: xPos,
75
+ y1: highPos,
76
+ x2: xPos,
77
+ y2: bodyTop,
78
+ },
79
+ style: {
80
+ stroke: wickColor,
81
+ lineWidth: 1,
82
+ },
83
+ },
84
+ // Lower wick
85
+ {
86
+ type: 'line',
87
+ shape: {
88
+ x1: xPos,
89
+ y1: bodyBottom,
90
+ x2: xPos,
91
+ y2: lowPos,
92
+ },
93
+ style: {
94
+ stroke: wickColor,
95
+ lineWidth: 1,
96
+ },
97
+ },
98
+ // Body
99
+ {
100
+ type: 'rect',
101
+ shape: {
102
+ x: xPos - barWidth / 2,
103
+ y: bodyTop,
104
+ width: barWidth,
105
+ height: bodyHeight || 1, // Minimum height for doji
106
+ },
107
+ style: {
108
+ fill: color,
109
+ stroke: borderColor,
110
+ lineWidth: 1,
111
+ },
112
+ },
113
+ ],
114
+ };
115
+ } else {
116
+ // Bar style (OHLC bar)
117
+ const tickWidth = barWidth * 0.5;
118
+
119
+ return {
120
+ type: 'group',
121
+ children: [
122
+ // Vertical line (low to high)
123
+ {
124
+ type: 'line',
125
+ shape: {
126
+ x1: xPos,
127
+ y1: lowPos,
128
+ x2: xPos,
129
+ y2: highPos,
130
+ },
131
+ style: {
132
+ stroke: color,
133
+ lineWidth: 1,
134
+ },
135
+ },
136
+ // Open tick (left)
137
+ {
138
+ type: 'line',
139
+ shape: {
140
+ x1: xPos - tickWidth,
141
+ y1: openPos,
142
+ x2: xPos,
143
+ y2: openPos,
144
+ },
145
+ style: {
146
+ stroke: color,
147
+ lineWidth: 1,
148
+ },
149
+ },
150
+ // Close tick (right)
151
+ {
152
+ type: 'line',
153
+ shape: {
154
+ x1: xPos,
155
+ y1: closePos,
156
+ x2: xPos + tickWidth,
157
+ y2: closePos,
158
+ },
159
+ style: {
160
+ stroke: color,
161
+ lineWidth: 1,
162
+ },
163
+ },
164
+ ],
165
+ };
166
+ }
167
+ },
168
+ data: ohlcData,
169
+ };
170
+ }
171
+ }
@@ -69,7 +69,12 @@ export class PolylineRenderer implements SeriesRenderer {
69
69
 
70
70
  if (pixelPoints.length < 2) continue;
71
71
 
72
- const lineColor = pl.line_color || '#2962ff';
72
+ // Detect na/NaN line_color (means no stroke)
73
+ const rawLineColor = pl.line_color;
74
+ const isNaLineColor = rawLineColor === null || rawLineColor === undefined ||
75
+ (typeof rawLineColor === 'number' && isNaN(rawLineColor)) ||
76
+ rawLineColor === 'na' || rawLineColor === 'NaN';
77
+ const lineColor = isNaLineColor ? null : (rawLineColor || '#2962ff');
73
78
  const lineWidth = pl.line_width || 1;
74
79
  const dashPattern = this.getDashPattern(pl.line_style);
75
80
 
@@ -95,23 +100,25 @@ export class PolylineRenderer implements SeriesRenderer {
95
100
  }
96
101
  }
97
102
 
98
- // Stroke (line segments)
99
- if (pl.curved) {
100
- const pathData = this.buildCurvedPath(pixelPoints, pl.closed);
101
- children.push({
102
- type: 'path',
103
- shape: { pathData },
104
- style: { fill: 'none', stroke: lineColor, lineWidth, lineDash: dashPattern },
105
- silent: true,
106
- });
107
- } else {
108
- const allPoints = pl.closed ? [...pixelPoints, pixelPoints[0]] : pixelPoints;
109
- children.push({
110
- type: 'polyline',
111
- shape: { points: allPoints },
112
- style: { fill: 'none', stroke: lineColor, lineWidth, lineDash: dashPattern },
113
- silent: true,
114
- });
103
+ // Stroke (line segments) — skip entirely if line_color is na
104
+ if (lineColor && lineWidth > 0) {
105
+ if (pl.curved) {
106
+ const pathData = this.buildCurvedPath(pixelPoints, pl.closed);
107
+ children.push({
108
+ type: 'path',
109
+ shape: { pathData },
110
+ style: { fill: 'none', stroke: lineColor, lineWidth, lineDash: dashPattern },
111
+ silent: true,
112
+ });
113
+ } else {
114
+ const allPoints = pl.closed ? [...pixelPoints, pixelPoints[0]] : pixelPoints;
115
+ children.push({
116
+ type: 'polyline',
117
+ shape: { points: allPoints },
118
+ style: { fill: 'none', stroke: lineColor, lineWidth, lineDash: dashPattern },
119
+ silent: true,
120
+ });
121
+ }
115
122
  }
116
123
  }
117
124
 
@@ -122,7 +129,7 @@ export class PolylineRenderer implements SeriesRenderer {
122
129
  encode: { x: [0, 1] },
123
130
  // Prevent ECharts visual system from overriding element colors with palette
124
131
  itemStyle: { color: 'transparent', borderColor: 'transparent' },
125
- z: 12,
132
+ z: 15,
126
133
  silent: true,
127
134
  emphasis: { disabled: true },
128
135
  };
package/src/index.ts CHANGED
@@ -1,6 +1,17 @@
1
- export * from "./types";
2
- export * from "./QFChart";
3
- export * from "./plugins/MeasureTool";
4
- export * from "./plugins/LineTool";
5
- export * from "./plugins/FibonacciTool";
6
- export * from "./components/AbstractPlugin";
1
+ export * from "./types";
2
+ export * from "./QFChart";
3
+ export * from "./plugins/MeasureTool";
4
+ export * from "./plugins/LineTool";
5
+ export * from "./plugins/FibonacciTool";
6
+ export * from "./plugins/FibonacciChannelTool";
7
+ export * from "./plugins/FibSpeedResistanceFanTool";
8
+ export * from "./plugins/FibTrendExtensionTool";
9
+ export * from "./plugins/XABCDPatternTool";
10
+ export * from "./plugins/ABCDPatternTool";
11
+ export * from "./plugins/CypherPatternTool";
12
+ export * from "./plugins/HeadAndShouldersTool";
13
+ export * from "./plugins/TrianglePatternTool";
14
+ export * from "./plugins/ThreeDrivesPatternTool";
15
+ export * from "./plugins/ToolGroup";
16
+ export * from "./components/AbstractPlugin";
17
+ export * from "./components/DrawingRendererRegistry";
@@ -0,0 +1,112 @@
1
+ import { DrawingRenderer, DrawingRenderContext } from '../../types';
2
+
3
+ const LABELS = ['A', 'B', 'C', 'D'];
4
+ const LEG_COLORS = ['#2196f3', '#ff9800', '#4caf50'];
5
+
6
+ export class ABCDPatternDrawingRenderer implements DrawingRenderer {
7
+ type = 'abcd_pattern';
8
+
9
+ render(ctx: DrawingRenderContext): any {
10
+ const { drawing, pixelPoints, isSelected } = ctx;
11
+ const color = drawing.style?.color || '#3b82f6';
12
+ if (pixelPoints.length < 2) return;
13
+
14
+ const children: any[] = [];
15
+
16
+ // Fill triangle ABC
17
+ if (pixelPoints.length >= 3) {
18
+ children.push({
19
+ type: 'polygon',
20
+ name: 'line',
21
+ shape: { points: pixelPoints.slice(0, 3).map(([x, y]) => [x, y]) },
22
+ style: { fill: 'rgba(33, 150, 243, 0.08)' },
23
+ });
24
+ }
25
+ // Fill triangle BCD
26
+ if (pixelPoints.length >= 4) {
27
+ children.push({
28
+ type: 'polygon',
29
+ name: 'line',
30
+ shape: { points: pixelPoints.slice(1, 4).map(([x, y]) => [x, y]) },
31
+ style: { fill: 'rgba(244, 67, 54, 0.08)' },
32
+ });
33
+ }
34
+
35
+ // Leg lines
36
+ for (let i = 0; i < pixelPoints.length - 1; i++) {
37
+ const [x1, y1] = pixelPoints[i];
38
+ const [x2, y2] = pixelPoints[i + 1];
39
+ children.push({
40
+ type: 'line',
41
+ name: 'line',
42
+ shape: { x1, y1, x2, y2 },
43
+ style: { stroke: LEG_COLORS[i % LEG_COLORS.length], lineWidth: drawing.style?.lineWidth || 2 },
44
+ });
45
+ }
46
+
47
+ // Dashed connector A→C
48
+ if (pixelPoints.length >= 3) {
49
+ children.push({
50
+ type: 'line',
51
+ shape: { x1: pixelPoints[0][0], y1: pixelPoints[0][1], x2: pixelPoints[2][0], y2: pixelPoints[2][1] },
52
+ style: { stroke: '#555', lineWidth: 1, lineDash: [4, 4] },
53
+ silent: true,
54
+ });
55
+ }
56
+ // Dashed connector B→D
57
+ if (pixelPoints.length >= 4) {
58
+ children.push({
59
+ type: 'line',
60
+ shape: { x1: pixelPoints[1][0], y1: pixelPoints[1][1], x2: pixelPoints[3][0], y2: pixelPoints[3][1] },
61
+ style: { stroke: '#555', lineWidth: 1, lineDash: [4, 4] },
62
+ silent: true,
63
+ });
64
+ }
65
+
66
+ // Fibonacci ratios
67
+ if (drawing.points.length >= 3) {
68
+ const ab = Math.abs(drawing.points[1].value - drawing.points[0].value);
69
+ const bc = Math.abs(drawing.points[2].value - drawing.points[1].value);
70
+ if (ab !== 0) {
71
+ const ratio = (bc / ab).toFixed(3);
72
+ const mx = (pixelPoints[1][0] + pixelPoints[2][0]) / 2;
73
+ const my = (pixelPoints[1][1] + pixelPoints[2][1]) / 2;
74
+ children.push({ type: 'text', style: { text: ratio, x: mx + 8, y: my, fill: '#ff9800', fontSize: 10 }, silent: true });
75
+ }
76
+ }
77
+ if (drawing.points.length >= 4) {
78
+ const bc = Math.abs(drawing.points[2].value - drawing.points[1].value);
79
+ const cd = Math.abs(drawing.points[3].value - drawing.points[2].value);
80
+ if (bc !== 0) {
81
+ const ratio = (cd / bc).toFixed(3);
82
+ const mx = (pixelPoints[2][0] + pixelPoints[3][0]) / 2;
83
+ const my = (pixelPoints[2][1] + pixelPoints[3][1]) / 2;
84
+ children.push({ type: 'text', style: { text: ratio, x: mx + 8, y: my, fill: '#4caf50', fontSize: 10 }, silent: true });
85
+ }
86
+ }
87
+
88
+ // Vertex labels
89
+ for (let i = 0; i < pixelPoints.length && i < LABELS.length; i++) {
90
+ const [px, py] = pixelPoints[i];
91
+ const isHigh = (i === 0 || py <= pixelPoints[i - 1][1]) && (i === pixelPoints.length - 1 || py <= pixelPoints[i + 1]?.[1]);
92
+ children.push({
93
+ type: 'text',
94
+ style: { text: LABELS[i], x: px, y: isHigh ? py - 14 : py + 16, fill: '#e2e8f0', fontSize: 12, fontWeight: 'bold', align: 'center', verticalAlign: 'middle' },
95
+ silent: true,
96
+ });
97
+ }
98
+
99
+ // Control points
100
+ for (let i = 0; i < pixelPoints.length; i++) {
101
+ children.push({
102
+ type: 'circle',
103
+ name: `point-${i}`,
104
+ shape: { cx: pixelPoints[i][0], cy: pixelPoints[i][1], r: 4 },
105
+ style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
106
+ z: 100,
107
+ });
108
+ }
109
+
110
+ return { type: 'group', children };
111
+ }
112
+ }