@qfo/qfchart 0.8.0 → 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.
- package/dist/index.d.ts +319 -12
- package/dist/qfchart.min.browser.js +32 -16
- package/dist/qfchart.min.es.js +32 -16
- package/package.json +1 -1
- package/src/QFChart.ts +98 -262
- package/src/components/AbstractPlugin.ts +234 -104
- package/src/components/DrawingEditor.ts +297 -248
- package/src/components/DrawingRendererRegistry.ts +13 -0
- package/src/components/GraphicBuilder.ts +2 -2
- package/src/components/LayoutManager.ts +41 -35
- package/src/components/SeriesBuilder.ts +10 -10
- package/src/components/TooltipFormatter.ts +1 -1
- package/src/index.ts +17 -6
- package/src/plugins/ABCDPatternTool/ABCDPatternDrawingRenderer.ts +112 -0
- package/src/plugins/ABCDPatternTool/ABCDPatternTool.ts +136 -0
- package/src/plugins/ABCDPatternTool/index.ts +2 -0
- package/src/plugins/CypherPatternTool/CypherPatternDrawingRenderer.ts +80 -0
- package/src/plugins/CypherPatternTool/CypherPatternTool.ts +84 -0
- package/src/plugins/CypherPatternTool/index.ts +2 -0
- package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanDrawingRenderer.ts +163 -0
- package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanTool.ts +210 -0
- package/src/plugins/FibSpeedResistanceFanTool/index.ts +2 -0
- package/src/plugins/FibTrendExtensionTool/FibTrendExtensionDrawingRenderer.ts +141 -0
- package/src/plugins/FibTrendExtensionTool/FibTrendExtensionTool.ts +188 -0
- package/src/plugins/FibTrendExtensionTool/index.ts +2 -0
- package/src/plugins/FibonacciChannelTool/FibonacciChannelDrawingRenderer.ts +128 -0
- package/src/plugins/FibonacciChannelTool/FibonacciChannelTool.ts +231 -0
- package/src/plugins/FibonacciChannelTool/index.ts +2 -0
- package/src/plugins/FibonacciTool/FibonacciDrawingRenderer.ts +107 -0
- package/src/plugins/{FibonacciTool.ts → FibonacciTool/FibonacciTool.ts} +195 -192
- package/src/plugins/FibonacciTool/index.ts +2 -0
- package/src/plugins/HeadAndShouldersTool/HeadAndShouldersDrawingRenderer.ts +95 -0
- package/src/plugins/HeadAndShouldersTool/HeadAndShouldersTool.ts +97 -0
- package/src/plugins/HeadAndShouldersTool/index.ts +2 -0
- package/src/plugins/LineTool/LineDrawingRenderer.ts +49 -0
- package/src/plugins/{LineTool.ts → LineTool/LineTool.ts} +161 -190
- package/src/plugins/LineTool/index.ts +2 -0
- package/src/plugins/{MeasureTool.ts → MeasureTool/MeasureTool.ts} +324 -344
- package/src/plugins/MeasureTool/index.ts +1 -0
- package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternDrawingRenderer.ts +106 -0
- package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternTool.ts +98 -0
- package/src/plugins/ThreeDrivesPatternTool/index.ts +2 -0
- package/src/plugins/ToolGroup.ts +211 -0
- package/src/plugins/TrianglePatternTool/TrianglePatternDrawingRenderer.ts +107 -0
- package/src/plugins/TrianglePatternTool/TrianglePatternTool.ts +98 -0
- package/src/plugins/TrianglePatternTool/index.ts +2 -0
- package/src/plugins/XABCDPatternTool/XABCDPatternDrawingRenderer.ts +178 -0
- package/src/plugins/XABCDPatternTool/XABCDPatternTool.ts +213 -0
- package/src/plugins/XABCDPatternTool/index.ts +2 -0
- package/src/types.ts +37 -11
|
@@ -15,9 +15,9 @@ export interface PaneConfiguration {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface PaneBoundary {
|
|
18
|
-
yPercent: number;
|
|
19
|
-
aboveId: string | 'main';
|
|
20
|
-
belowId: string;
|
|
18
|
+
yPercent: number; // Y position in %, center of the gap between panes
|
|
19
|
+
aboveId: string | 'main'; // pane above (main chart or indicator id)
|
|
20
|
+
belowId: string; // indicator id below
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface LayoutResult {
|
|
@@ -40,7 +40,7 @@ export class LayoutManager {
|
|
|
40
40
|
isMainCollapsed: boolean = false,
|
|
41
41
|
maximizedPaneId: string | null = null,
|
|
42
42
|
marketData?: import('../types').OHLCV[],
|
|
43
|
-
mainHeightOverride?: number
|
|
43
|
+
mainHeightOverride?: number,
|
|
44
44
|
): LayoutResult & { overlayYAxisMap: Map<string, number>; separatePaneYAxisOffset: number } {
|
|
45
45
|
// Calculate pixelToPercent early for maximized logic
|
|
46
46
|
let pixelToPercent = 0;
|
|
@@ -52,11 +52,11 @@ export class LayoutManager {
|
|
|
52
52
|
const yAxisPaddingPercent = options.yAxisPadding !== undefined ? options.yAxisPadding : 5;
|
|
53
53
|
|
|
54
54
|
// Grid styling options
|
|
55
|
-
const gridShow = options.grid?.show === true;
|
|
55
|
+
const gridShow = options.grid?.show === true; // default false
|
|
56
56
|
const gridLineColor = options.grid?.lineColor ?? '#334155';
|
|
57
57
|
const gridLineOpacity = options.grid?.lineOpacity ?? 0.5;
|
|
58
58
|
const gridBorderColor = options.grid?.borderColor ?? '#334155';
|
|
59
|
-
const gridBorderShow = options.grid?.borderShow === true;
|
|
59
|
+
const gridBorderShow = options.grid?.borderShow === true; // default false
|
|
60
60
|
|
|
61
61
|
// Layout margin options
|
|
62
62
|
const layoutLeft = options.layout?.left ?? '10%';
|
|
@@ -195,9 +195,10 @@ export class LayoutManager {
|
|
|
195
195
|
if (options.yAxisLabelFormatter) {
|
|
196
196
|
return options.yAxisLabelFormatter(value);
|
|
197
197
|
}
|
|
198
|
-
const decimals =
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
const decimals =
|
|
199
|
+
options.yAxisDecimalPlaces !== undefined
|
|
200
|
+
? options.yAxisDecimalPlaces
|
|
201
|
+
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
201
202
|
return AxisUtils.formatValue(value, decimals);
|
|
202
203
|
},
|
|
203
204
|
},
|
|
@@ -407,9 +408,8 @@ export class LayoutManager {
|
|
|
407
408
|
if (options.yAxisLabelFormatter) {
|
|
408
409
|
return options.yAxisLabelFormatter(value);
|
|
409
410
|
}
|
|
410
|
-
const decimals =
|
|
411
|
-
? options.yAxisDecimalPlaces
|
|
412
|
-
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
411
|
+
const decimals =
|
|
412
|
+
options.yAxisDecimalPlaces !== undefined ? options.yAxisDecimalPlaces : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
413
413
|
return AxisUtils.formatValue(value, decimals);
|
|
414
414
|
},
|
|
415
415
|
},
|
|
@@ -483,9 +483,8 @@ export class LayoutManager {
|
|
|
483
483
|
if (options.yAxisLabelFormatter) {
|
|
484
484
|
return options.yAxisLabelFormatter(value);
|
|
485
485
|
}
|
|
486
|
-
const decimals =
|
|
487
|
-
? options.yAxisDecimalPlaces
|
|
488
|
-
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
486
|
+
const decimals =
|
|
487
|
+
options.yAxisDecimalPlaces !== undefined ? options.yAxisDecimalPlaces : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
489
488
|
return AxisUtils.formatValue(value, decimals);
|
|
490
489
|
},
|
|
491
490
|
},
|
|
@@ -521,7 +520,11 @@ export class LayoutManager {
|
|
|
521
520
|
|
|
522
521
|
// Check if this is a shape with price-relative positioning
|
|
523
522
|
const isShapeWithPriceLocation =
|
|
524
|
-
plot.options.style === 'shape' &&
|
|
523
|
+
plot.options.style === 'shape' &&
|
|
524
|
+
(plot.options.location === 'abovebar' ||
|
|
525
|
+
plot.options.location === 'AboveBar' ||
|
|
526
|
+
plot.options.location === 'belowbar' ||
|
|
527
|
+
plot.options.location === 'BelowBar');
|
|
525
528
|
|
|
526
529
|
if (visualOnlyStyles.includes(plot.options.style)) {
|
|
527
530
|
// Assign these to a separate Y-axis so they don't affect price scale
|
|
@@ -582,7 +585,7 @@ export class LayoutManager {
|
|
|
582
585
|
// Create Y-axes for incompatible plots
|
|
583
586
|
// nextYAxisIndex already incremented in the loop above, so we know how many axes we need
|
|
584
587
|
const numOverlayAxes = overlayYAxisMap.size > 0 ? nextYAxisIndex - 1 : 0;
|
|
585
|
-
|
|
588
|
+
|
|
586
589
|
// Track which overlay axes are for visual-only plots (background, barcolor, etc.)
|
|
587
590
|
const visualOnlyAxes = new Set<number>();
|
|
588
591
|
overlayYAxisMap.forEach((yAxisIdx, plotKey) => {
|
|
@@ -596,11 +599,11 @@ export class LayoutManager {
|
|
|
596
599
|
});
|
|
597
600
|
});
|
|
598
601
|
});
|
|
599
|
-
|
|
602
|
+
|
|
600
603
|
for (let i = 0; i < numOverlayAxes; i++) {
|
|
601
604
|
const yAxisIndex = i + 1; // Y-axis indices start at 1 for overlays
|
|
602
605
|
const isVisualOnly = visualOnlyAxes.has(yAxisIndex);
|
|
603
|
-
|
|
606
|
+
|
|
604
607
|
yAxis.push({
|
|
605
608
|
position: 'left',
|
|
606
609
|
scale: !isVisualOnly, // Disable scaling for visual-only plots
|
|
@@ -636,9 +639,10 @@ export class LayoutManager {
|
|
|
636
639
|
if (options.yAxisLabelFormatter) {
|
|
637
640
|
return options.yAxisLabelFormatter(value);
|
|
638
641
|
}
|
|
639
|
-
const decimals =
|
|
640
|
-
|
|
641
|
-
|
|
642
|
+
const decimals =
|
|
643
|
+
options.yAxisDecimalPlaces !== undefined
|
|
644
|
+
? options.yAxisDecimalPlaces
|
|
645
|
+
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
642
646
|
return AxisUtils.formatValue(value, decimals);
|
|
643
647
|
},
|
|
644
648
|
},
|
|
@@ -648,19 +652,21 @@ export class LayoutManager {
|
|
|
648
652
|
|
|
649
653
|
// --- Generate DataZoom ---
|
|
650
654
|
const dataZoom: any[] = [];
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
655
|
+
const zoomOnTouch = options.dataZoom?.zoomOnTouch ?? true;
|
|
656
|
+
const pannable = options.dataZoom?.pannable ?? true;
|
|
657
|
+
|
|
658
|
+
// 'inside' zoom provides pan/drag — enabled independently of slider visibility
|
|
659
|
+
if (zoomOnTouch && pannable) {
|
|
660
|
+
dataZoom.push({
|
|
661
|
+
type: 'inside',
|
|
662
|
+
xAxisIndex: allXAxisIndices,
|
|
663
|
+
start: dzStart,
|
|
664
|
+
end: dzEnd,
|
|
665
|
+
filterMode: 'weakFilter',
|
|
666
|
+
});
|
|
667
|
+
}
|
|
663
668
|
|
|
669
|
+
if (dzVisible) {
|
|
664
670
|
if (dzPosition === 'top') {
|
|
665
671
|
dataZoom.push({
|
|
666
672
|
type: 'slider',
|
|
@@ -708,7 +714,7 @@ export class LayoutManager {
|
|
|
708
714
|
private static calculateMaximized(
|
|
709
715
|
containerHeight: number,
|
|
710
716
|
options: QFChartOptions,
|
|
711
|
-
targetPaneIndex: number // 0 for main, 1+ for indicators
|
|
717
|
+
targetPaneIndex: number, // 0 for main, 1+ for indicators
|
|
712
718
|
): LayoutResult {
|
|
713
719
|
return {
|
|
714
720
|
grid: [],
|
|
@@ -35,9 +35,7 @@ export class SeriesBuilder {
|
|
|
35
35
|
if (lineStyleType.startsWith('linestyle_')) {
|
|
36
36
|
lineStyleType = lineStyleType.replace('linestyle_', '') as any;
|
|
37
37
|
}
|
|
38
|
-
const decimals = options.yAxisDecimalPlaces !== undefined
|
|
39
|
-
? options.yAxisDecimalPlaces
|
|
40
|
-
: AxisUtils.autoDetectDecimals(marketData);
|
|
38
|
+
const decimals = options.yAxisDecimalPlaces !== undefined ? options.yAxisDecimalPlaces : AxisUtils.autoDetectDecimals(marketData);
|
|
41
39
|
|
|
42
40
|
markLine = {
|
|
43
41
|
symbol: ['none', 'none'],
|
|
@@ -77,7 +75,8 @@ export class SeriesBuilder {
|
|
|
77
75
|
|
|
78
76
|
return {
|
|
79
77
|
type: 'candlestick',
|
|
80
|
-
|
|
78
|
+
id: '__candlestick__',
|
|
79
|
+
name: options.title,
|
|
81
80
|
data: data,
|
|
82
81
|
itemStyle: {
|
|
83
82
|
color: upColor,
|
|
@@ -100,7 +99,7 @@ export class SeriesBuilder {
|
|
|
100
99
|
dataIndexOffset: number = 0,
|
|
101
100
|
candlestickData?: OHLCV[], // Add candlestick data to access High/Low for positioning
|
|
102
101
|
overlayYAxisMap?: Map<string, number>, // Map of overlay indicator IDs to their Y-axis indices
|
|
103
|
-
separatePaneYAxisOffset: number = 1 // Offset for separate pane Y-axes (accounts for overlay axes)
|
|
102
|
+
separatePaneYAxisOffset: number = 1, // Offset for separate pane Y-axes (accounts for overlay axes)
|
|
104
103
|
): { series: any[]; barColors: (string | null)[] } {
|
|
105
104
|
const series: any[] = [];
|
|
106
105
|
const barColors: (string | null)[] = new Array(totalDataLength).fill(null);
|
|
@@ -220,7 +219,7 @@ export class SeriesBuilder {
|
|
|
220
219
|
}
|
|
221
220
|
|
|
222
221
|
dataArray[offsetIndex] = value;
|
|
223
|
-
colorArray[offsetIndex] = isNaColor ? null :
|
|
222
|
+
colorArray[offsetIndex] = isNaColor ? null : pointColor || plot.options.color || SeriesBuilder.DEFAULT_COLOR;
|
|
224
223
|
optionsArray[offsetIndex] = point.options || {};
|
|
225
224
|
}
|
|
226
225
|
}
|
|
@@ -281,8 +280,9 @@ export class SeriesBuilder {
|
|
|
281
280
|
|
|
282
281
|
if (plot1Data && plot2Data) {
|
|
283
282
|
// Parse per-bar colors
|
|
284
|
-
const { color: defaultColor, opacity: defaultOpacity } =
|
|
285
|
-
|
|
283
|
+
const { color: defaultColor, opacity: defaultOpacity } = ColorUtils.parseColor(
|
|
284
|
+
plot.options.color || 'rgba(128, 128, 128, 0.2)',
|
|
285
|
+
);
|
|
286
286
|
const hasPerBarColor = optionsArray.some((o: any) => o && o.color !== undefined);
|
|
287
287
|
|
|
288
288
|
const fillBarColors: { color: string; opacity: number }[] = [];
|
|
@@ -359,7 +359,7 @@ export class SeriesBuilder {
|
|
|
359
359
|
xAxisIndex,
|
|
360
360
|
yAxisIndex,
|
|
361
361
|
totalDataLength,
|
|
362
|
-
entries
|
|
362
|
+
entries,
|
|
363
363
|
);
|
|
364
364
|
if (batchedConfig) {
|
|
365
365
|
series.push(batchedConfig);
|
|
@@ -371,7 +371,7 @@ export class SeriesBuilder {
|
|
|
371
371
|
xAxisIndex,
|
|
372
372
|
yAxisIndex,
|
|
373
373
|
totalDataLength,
|
|
374
|
-
entries
|
|
374
|
+
entries,
|
|
375
375
|
);
|
|
376
376
|
if (batchedConfig) {
|
|
377
377
|
series.push(batchedConfig);
|
|
@@ -4,7 +4,7 @@ export class TooltipFormatter {
|
|
|
4
4
|
public static format(params: any[], options: QFChartOptions): string {
|
|
5
5
|
if (!params || params.length === 0) return "";
|
|
6
6
|
|
|
7
|
-
const marketName = options.title || "
|
|
7
|
+
const marketName = options.title || "";
|
|
8
8
|
const upColor = options.upColor || "#00da3c";
|
|
9
9
|
const downColor = options.downColor || "#ec0000";
|
|
10
10
|
const fontFamily = options.fontFamily || "sans-serif";
|
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 "./
|
|
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
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as echarts from 'echarts';
|
|
2
|
+
import { AbstractPlugin } from '../../components/AbstractPlugin';
|
|
3
|
+
import { ABCDPatternDrawingRenderer } from './ABCDPatternDrawingRenderer';
|
|
4
|
+
|
|
5
|
+
const LABELS = ['A', 'B', 'C', 'D'];
|
|
6
|
+
const LEG_COLORS = ['#2196f3', '#ff9800', '#4caf50'];
|
|
7
|
+
const TOTAL_POINTS = 4;
|
|
8
|
+
|
|
9
|
+
export class ABCDPatternTool extends AbstractPlugin {
|
|
10
|
+
private points: number[][] = [];
|
|
11
|
+
private state: 'idle' | 'drawing' | 'finished' = 'idle';
|
|
12
|
+
private graphicGroup: any = null;
|
|
13
|
+
|
|
14
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
15
|
+
super({
|
|
16
|
+
id: 'abcd-pattern-tool',
|
|
17
|
+
name: options.name || 'ABCD Pattern',
|
|
18
|
+
icon: options.icon || `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#e3e3e3" stroke-width="1.5"><polyline points="3,18 8,5 15,15 21,3"/><circle cx="3" cy="18" r="1.5" fill="#e3e3e3"/><circle cx="8" cy="5" r="1.5" fill="#e3e3e3"/><circle cx="15" cy="15" r="1.5" fill="#e3e3e3"/><circle cx="21" cy="3" r="1.5" fill="#e3e3e3"/></svg>`,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected onInit(): void {
|
|
23
|
+
this.context.registerDrawingRenderer(new ABCDPatternDrawingRenderer());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
protected onActivate(): void {
|
|
27
|
+
this.state = 'idle';
|
|
28
|
+
this.points = [];
|
|
29
|
+
this.context.getChart().getZr().setCursorStyle('crosshair');
|
|
30
|
+
const zr = this.context.getChart().getZr();
|
|
31
|
+
zr.on('click', this.onClick);
|
|
32
|
+
zr.on('mousemove', this.onMouseMove);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected onDeactivate(): void {
|
|
36
|
+
this.state = 'idle';
|
|
37
|
+
this.points = [];
|
|
38
|
+
this.removeGraphic();
|
|
39
|
+
const zr = this.context.getChart().getZr();
|
|
40
|
+
zr.off('click', this.onClick);
|
|
41
|
+
zr.off('mousemove', this.onMouseMove);
|
|
42
|
+
this.context.getChart().getZr().setCursorStyle('default');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private onClick = (params: any) => {
|
|
46
|
+
const pt = this.getPoint(params);
|
|
47
|
+
if (this.state === 'idle') {
|
|
48
|
+
this.state = 'drawing';
|
|
49
|
+
this.points = [pt, [...pt]];
|
|
50
|
+
this.initGraphic();
|
|
51
|
+
this.updateGraphic();
|
|
52
|
+
} else if (this.state === 'drawing') {
|
|
53
|
+
this.points[this.points.length - 1] = pt;
|
|
54
|
+
if (this.points.length >= TOTAL_POINTS) {
|
|
55
|
+
this.state = 'finished';
|
|
56
|
+
this.updateGraphic();
|
|
57
|
+
this.saveDrawing();
|
|
58
|
+
this.removeGraphic();
|
|
59
|
+
this.context.disableTools();
|
|
60
|
+
} else {
|
|
61
|
+
this.points.push([...pt]);
|
|
62
|
+
this.updateGraphic();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
private onMouseMove = (params: any) => {
|
|
68
|
+
if (this.state !== 'drawing' || this.points.length < 2) return;
|
|
69
|
+
this.points[this.points.length - 1] = this.getPoint(params);
|
|
70
|
+
this.updateGraphic();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
private initGraphic() {
|
|
74
|
+
this.graphicGroup = new echarts.graphic.Group();
|
|
75
|
+
this.context.getChart().getZr().add(this.graphicGroup);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private removeGraphic() {
|
|
79
|
+
if (this.graphicGroup) {
|
|
80
|
+
this.context.getChart().getZr().remove(this.graphicGroup);
|
|
81
|
+
this.graphicGroup = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private updateGraphic() {
|
|
86
|
+
if (!this.graphicGroup) return;
|
|
87
|
+
this.graphicGroup.removeAll();
|
|
88
|
+
const pts = this.points;
|
|
89
|
+
|
|
90
|
+
// Fills
|
|
91
|
+
if (pts.length >= 3) {
|
|
92
|
+
this.graphicGroup.add(new echarts.graphic.Polygon({ shape: { points: pts.slice(0, 3) }, style: { fill: 'rgba(33,150,243,0.08)' }, silent: true }));
|
|
93
|
+
}
|
|
94
|
+
if (pts.length >= 4) {
|
|
95
|
+
this.graphicGroup.add(new echarts.graphic.Polygon({ shape: { points: pts.slice(1, 4) }, style: { fill: 'rgba(244,67,54,0.08)' }, silent: true }));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Legs
|
|
99
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
100
|
+
this.graphicGroup.add(new echarts.graphic.Line({
|
|
101
|
+
shape: { x1: pts[i][0], y1: pts[i][1], x2: pts[i + 1][0], y2: pts[i + 1][1] },
|
|
102
|
+
style: { stroke: LEG_COLORS[i % LEG_COLORS.length], lineWidth: 2 },
|
|
103
|
+
silent: true,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Dashed connectors
|
|
108
|
+
if (pts.length >= 3) {
|
|
109
|
+
this.graphicGroup.add(new echarts.graphic.Line({ shape: { x1: pts[0][0], y1: pts[0][1], x2: pts[2][0], y2: pts[2][1] }, style: { stroke: '#555', lineWidth: 1, lineDash: [4, 4] }, silent: true }));
|
|
110
|
+
}
|
|
111
|
+
if (pts.length >= 4) {
|
|
112
|
+
this.graphicGroup.add(new echarts.graphic.Line({ shape: { x1: pts[1][0], y1: pts[1][1], x2: pts[3][0], y2: pts[3][1] }, style: { stroke: '#555', lineWidth: 1, lineDash: [4, 4] }, silent: true }));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Labels & circles
|
|
116
|
+
for (let i = 0; i < pts.length && i < LABELS.length; i++) {
|
|
117
|
+
const [px, py] = pts[i];
|
|
118
|
+
const isHigh = (i === 0 || py <= pts[i - 1][1]) && (i === pts.length - 1 || py <= pts[i + 1]?.[1]);
|
|
119
|
+
this.graphicGroup.add(new echarts.graphic.Text({ style: { text: LABELS[i], x: px, y: isHigh ? py - 14 : py + 16, fill: '#e2e8f0', fontSize: 12, fontWeight: 'bold', align: 'center', verticalAlign: 'middle' }, silent: true }));
|
|
120
|
+
this.graphicGroup.add(new echarts.graphic.Circle({ shape: { cx: px, cy: py, r: 4 }, style: { fill: '#fff', stroke: '#3b82f6', lineWidth: 1.5 }, z: 101, silent: true }));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private saveDrawing() {
|
|
125
|
+
const dataPoints = this.points.map((pt) => this.context.coordinateConversion.pixelToData({ x: pt[0], y: pt[1] }));
|
|
126
|
+
if (dataPoints.every((p) => p !== null)) {
|
|
127
|
+
this.context.addDrawing({
|
|
128
|
+
id: `abcd-${Date.now()}`,
|
|
129
|
+
type: 'abcd_pattern',
|
|
130
|
+
points: dataPoints as any[],
|
|
131
|
+
paneIndex: dataPoints[0]!.paneIndex || 0,
|
|
132
|
+
style: { color: '#3b82f6', lineWidth: 2 },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
const LABELS = ['X', 'A', 'B', 'C', 'D'];
|
|
4
|
+
const LEG_COLORS = ['#00bcd4', '#e91e63', '#8bc34a', '#ff5722'];
|
|
5
|
+
|
|
6
|
+
export class CypherPatternDrawingRenderer implements DrawingRenderer {
|
|
7
|
+
type = 'cypher_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 triangles XAB and BCD
|
|
17
|
+
if (pixelPoints.length >= 3) {
|
|
18
|
+
children.push({ type: 'polygon', name: 'line', shape: { points: pixelPoints.slice(0, 3).map(([x, y]) => [x, y]) }, style: { fill: 'rgba(0, 188, 212, 0.08)' } });
|
|
19
|
+
}
|
|
20
|
+
if (pixelPoints.length >= 5) {
|
|
21
|
+
children.push({ type: 'polygon', name: 'line', shape: { points: pixelPoints.slice(2, 5).map(([x, y]) => [x, y]) }, style: { fill: 'rgba(233, 30, 99, 0.08)' } });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Legs
|
|
25
|
+
for (let i = 0; i < pixelPoints.length - 1; i++) {
|
|
26
|
+
const [x1, y1] = pixelPoints[i];
|
|
27
|
+
const [x2, y2] = pixelPoints[i + 1];
|
|
28
|
+
children.push({ type: 'line', name: 'line', shape: { x1, y1, x2, y2 }, style: { stroke: LEG_COLORS[i % LEG_COLORS.length], lineWidth: drawing.style?.lineWidth || 2 } });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Dashed connectors X→B, X→C, A→D
|
|
32
|
+
const connectors: [number, number][] = [[0, 2], [0, 3], [1, 4]];
|
|
33
|
+
for (const [from, to] of connectors) {
|
|
34
|
+
if (from < pixelPoints.length && to < pixelPoints.length) {
|
|
35
|
+
children.push({ type: 'line', shape: { x1: pixelPoints[from][0], y1: pixelPoints[from][1], x2: pixelPoints[to][0], y2: pixelPoints[to][1] }, style: { stroke: '#555', lineWidth: 1, lineDash: [4, 4] }, silent: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Ratios
|
|
40
|
+
const pts = drawing.points;
|
|
41
|
+
if (pts.length >= 3) {
|
|
42
|
+
const xa = Math.abs(pts[1].value - pts[0].value);
|
|
43
|
+
const ab = Math.abs(pts[2].value - pts[1].value);
|
|
44
|
+
if (xa !== 0) {
|
|
45
|
+
const r = (ab / xa).toFixed(3);
|
|
46
|
+
children.push({ type: 'text', style: { text: r, x: (pixelPoints[1][0] + pixelPoints[2][0]) / 2 + 8, y: (pixelPoints[1][1] + pixelPoints[2][1]) / 2, fill: '#e91e63', fontSize: 10 }, silent: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (pts.length >= 4) {
|
|
50
|
+
const xa = Math.abs(pts[1].value - pts[0].value);
|
|
51
|
+
const xc = Math.abs(pts[3].value - pts[0].value);
|
|
52
|
+
if (xa !== 0) {
|
|
53
|
+
const r = (xc / xa).toFixed(3);
|
|
54
|
+
children.push({ type: 'text', style: { text: `XC/XA: ${r}`, x: (pixelPoints[0][0] + pixelPoints[3][0]) / 2 + 8, y: (pixelPoints[0][1] + pixelPoints[3][1]) / 2, fill: '#8bc34a', fontSize: 10 }, silent: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (pts.length >= 5) {
|
|
58
|
+
const xc = Math.abs(pts[3].value - pts[0].value);
|
|
59
|
+
const cd = Math.abs(pts[4].value - pts[3].value);
|
|
60
|
+
if (xc !== 0) {
|
|
61
|
+
const r = (cd / xc).toFixed(3);
|
|
62
|
+
children.push({ type: 'text', style: { text: r, x: (pixelPoints[3][0] + pixelPoints[4][0]) / 2 + 8, y: (pixelPoints[3][1] + pixelPoints[4][1]) / 2, fill: '#ff5722', fontSize: 10 }, silent: true });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Labels
|
|
67
|
+
for (let i = 0; i < pixelPoints.length && i < LABELS.length; i++) {
|
|
68
|
+
const [px, py] = pixelPoints[i];
|
|
69
|
+
const isHigh = (i === 0 || py <= pixelPoints[i - 1][1]) && (i === pixelPoints.length - 1 || py <= pixelPoints[i + 1]?.[1]);
|
|
70
|
+
children.push({ type: 'text', style: { text: LABELS[i], x: px, y: isHigh ? py - 14 : py + 16, fill: '#e2e8f0', fontSize: 12, fontWeight: 'bold', align: 'center', verticalAlign: 'middle' }, silent: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Control points
|
|
74
|
+
for (let i = 0; i < pixelPoints.length; i++) {
|
|
75
|
+
children.push({ type: 'circle', name: `point-${i}`, shape: { cx: pixelPoints[i][0], cy: pixelPoints[i][1], r: 4 }, style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 }, z: 100 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { type: 'group', children };
|
|
79
|
+
}
|
|
80
|
+
}
|