@qfo/qfchart 0.5.0
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/LICENSE +222 -0
- package/README.md +271 -0
- package/dist/index.d.ts +465 -0
- package/dist/qfchart.min.browser.js +109 -0
- package/package.json +71 -0
- package/src/QFChart.ts +1198 -0
- package/src/Utils.ts +31 -0
- package/src/components/AbstractPlugin.ts +104 -0
- package/src/components/DrawingEditor.ts +248 -0
- package/src/components/GraphicBuilder.ts +263 -0
- package/src/components/Indicator.ts +104 -0
- package/src/components/LayoutManager.ts +459 -0
- package/src/components/PluginManager.ts +234 -0
- package/src/components/SeriesBuilder.ts +192 -0
- package/src/components/TooltipFormatter.ts +97 -0
- package/src/index.ts +6 -0
- package/src/plugins/FibonacciTool.ts +192 -0
- package/src/plugins/LineTool.ts +190 -0
- package/src/plugins/MeasureTool.ts +344 -0
- package/src/types.ts +160 -0
- package/src/utils/EventBus.ts +67 -0
package/src/Utils.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const imageCache = new Map<string, string>();
|
|
2
|
+
|
|
3
|
+
export function textToBase64Image(
|
|
4
|
+
text: string,
|
|
5
|
+
color: string = "#00da3c",
|
|
6
|
+
fontSize: string = "64px"
|
|
7
|
+
): string {
|
|
8
|
+
if (typeof document === "undefined") return "";
|
|
9
|
+
|
|
10
|
+
const cacheKey = `${text}-${color}-${fontSize}`;
|
|
11
|
+
if (imageCache.has(cacheKey)) {
|
|
12
|
+
return imageCache.get(cacheKey)!;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const canvas = document.createElement("canvas");
|
|
16
|
+
const ctx = canvas.getContext("2d");
|
|
17
|
+
canvas.width = 32;
|
|
18
|
+
canvas.height = 32;
|
|
19
|
+
|
|
20
|
+
if (ctx) {
|
|
21
|
+
ctx.font = "bold " + fontSize + " Arial";
|
|
22
|
+
ctx.fillStyle = color;
|
|
23
|
+
ctx.textAlign = "center";
|
|
24
|
+
ctx.textBaseline = "middle";
|
|
25
|
+
ctx.fillText(text, 16, 16);
|
|
26
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
27
|
+
imageCache.set(cacheKey, dataUrl);
|
|
28
|
+
return dataUrl;
|
|
29
|
+
}
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { ChartContext, Plugin, PluginConfig, OHLCV } from "../types";
|
|
2
|
+
import { EventType, EventHandler } from "../utils/EventBus";
|
|
3
|
+
|
|
4
|
+
export abstract class AbstractPlugin implements Plugin {
|
|
5
|
+
public id: string;
|
|
6
|
+
public name?: string;
|
|
7
|
+
public icon?: string;
|
|
8
|
+
|
|
9
|
+
protected context!: ChartContext;
|
|
10
|
+
private eventListeners: Array<{ event: EventType; handler: EventHandler }> =
|
|
11
|
+
[];
|
|
12
|
+
|
|
13
|
+
constructor(config: PluginConfig) {
|
|
14
|
+
this.id = config.id;
|
|
15
|
+
this.name = config.name;
|
|
16
|
+
this.icon = config.icon;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public init(context: ChartContext): void {
|
|
20
|
+
this.context = context;
|
|
21
|
+
this.onInit();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Lifecycle hook called after context is initialized.
|
|
26
|
+
* Override this instead of init().
|
|
27
|
+
*/
|
|
28
|
+
protected onInit(): void {}
|
|
29
|
+
|
|
30
|
+
public activate(): void {
|
|
31
|
+
this.onActivate();
|
|
32
|
+
this.context.events.emit("plugin:activated", this.id);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Lifecycle hook called when the plugin is activated.
|
|
37
|
+
*/
|
|
38
|
+
protected onActivate(): void {}
|
|
39
|
+
|
|
40
|
+
public deactivate(): void {
|
|
41
|
+
this.onDeactivate();
|
|
42
|
+
this.context.events.emit("plugin:deactivated", this.id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Lifecycle hook called when the plugin is deactivated.
|
|
47
|
+
*/
|
|
48
|
+
protected onDeactivate(): void {}
|
|
49
|
+
|
|
50
|
+
public destroy(): void {
|
|
51
|
+
this.removeAllListeners();
|
|
52
|
+
this.onDestroy();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Lifecycle hook called when the plugin is destroyed.
|
|
57
|
+
*/
|
|
58
|
+
protected onDestroy(): void {}
|
|
59
|
+
|
|
60
|
+
// --- Helper Methods ---
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register an event listener that will be automatically cleaned up on destroy.
|
|
64
|
+
*/
|
|
65
|
+
protected on(event: EventType, handler: EventHandler): void {
|
|
66
|
+
this.context.events.on(event, handler);
|
|
67
|
+
this.eventListeners.push({ event, handler });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Remove a specific event listener.
|
|
72
|
+
*/
|
|
73
|
+
protected off(event: EventType, handler: EventHandler): void {
|
|
74
|
+
this.context.events.off(event, handler);
|
|
75
|
+
this.eventListeners = this.eventListeners.filter(
|
|
76
|
+
(l) => l.event !== event || l.handler !== handler
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Remove all listeners registered by this plugin.
|
|
82
|
+
*/
|
|
83
|
+
protected removeAllListeners(): void {
|
|
84
|
+
this.eventListeners.forEach(({ event, handler }) => {
|
|
85
|
+
this.context.events.off(event, handler);
|
|
86
|
+
});
|
|
87
|
+
this.eventListeners = [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Access to the ECharts instance.
|
|
92
|
+
*/
|
|
93
|
+
protected get chart() {
|
|
94
|
+
return this.context.getChart();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Access to market data.
|
|
99
|
+
*/
|
|
100
|
+
protected get marketData(): OHLCV[] {
|
|
101
|
+
return this.context.getMarketData();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { ChartContext, DrawingElement, DataCoordinate } from "../types";
|
|
2
|
+
import * as echarts from "echarts";
|
|
3
|
+
|
|
4
|
+
export class DrawingEditor {
|
|
5
|
+
private context: ChartContext;
|
|
6
|
+
private isEditing: boolean = false;
|
|
7
|
+
private currentDrawing: DrawingElement | null = null;
|
|
8
|
+
private editingPointIndex: number | null = null;
|
|
9
|
+
private zr: any;
|
|
10
|
+
|
|
11
|
+
// Temporary ZRender elements for visual feedback during drag
|
|
12
|
+
private editGroup: any = null;
|
|
13
|
+
private editLine: any = null;
|
|
14
|
+
private editStartPoint: any = null;
|
|
15
|
+
private editEndPoint: any = null;
|
|
16
|
+
|
|
17
|
+
private isMovingShape: boolean = false;
|
|
18
|
+
private dragStart: { x: number; y: number } | null = null;
|
|
19
|
+
private initialPixelPoints: { x: number; y: number }[] = [];
|
|
20
|
+
|
|
21
|
+
constructor(context: ChartContext) {
|
|
22
|
+
this.context = context;
|
|
23
|
+
this.zr = this.context.getChart().getZr();
|
|
24
|
+
this.bindEvents();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private bindEvents() {
|
|
28
|
+
this.context.events.on("drawing:point:mousedown", this.onPointMouseDown);
|
|
29
|
+
this.context.events.on("drawing:mousedown", this.onDrawingMouseDown);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private onDrawingMouseDown = (payload: {
|
|
33
|
+
id: string;
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
}) => {
|
|
37
|
+
if (this.isEditing) return;
|
|
38
|
+
|
|
39
|
+
const drawing = this.context.getDrawing(payload.id);
|
|
40
|
+
if (!drawing) return;
|
|
41
|
+
|
|
42
|
+
this.isEditing = true;
|
|
43
|
+
this.isMovingShape = true;
|
|
44
|
+
this.currentDrawing = JSON.parse(JSON.stringify(drawing));
|
|
45
|
+
this.dragStart = { x: payload.x, y: payload.y };
|
|
46
|
+
|
|
47
|
+
// Capture initial pixel positions
|
|
48
|
+
this.initialPixelPoints = drawing.points.map((p) => {
|
|
49
|
+
const pixel = this.context.coordinateConversion.dataToPixel(p);
|
|
50
|
+
return pixel ? { x: pixel.x, y: pixel.y } : { x: 0, y: 0 }; // Fallback
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.context.lockChart();
|
|
54
|
+
this.createEditGraphic();
|
|
55
|
+
|
|
56
|
+
this.zr.on("mousemove", this.onMouseMove);
|
|
57
|
+
this.zr.on("mouseup", this.onMouseUp);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
private onPointMouseDown = (payload: { id: string; pointIndex: number }) => {
|
|
61
|
+
if (this.isEditing) return;
|
|
62
|
+
|
|
63
|
+
const drawing = this.context.getDrawing(payload.id);
|
|
64
|
+
if (!drawing) return;
|
|
65
|
+
|
|
66
|
+
// Start Editing
|
|
67
|
+
this.isEditing = true;
|
|
68
|
+
this.currentDrawing = JSON.parse(JSON.stringify(drawing)); // Deep copy
|
|
69
|
+
this.editingPointIndex = payload.pointIndex;
|
|
70
|
+
|
|
71
|
+
this.context.lockChart();
|
|
72
|
+
|
|
73
|
+
// Create visual feedback (overlay)
|
|
74
|
+
this.createEditGraphic();
|
|
75
|
+
|
|
76
|
+
// Hide the actual drawing (optional, but good for UX so we don't see double)
|
|
77
|
+
// Actually, we can just drag the overlay and update the drawing on mouseup.
|
|
78
|
+
// The underlying drawing remains visible but static until updated.
|
|
79
|
+
|
|
80
|
+
// Bind temporary drag listeners
|
|
81
|
+
this.zr.on("mousemove", this.onMouseMove);
|
|
82
|
+
this.zr.on("mouseup", this.onMouseUp);
|
|
83
|
+
// Global mouseup to catch releases outside chart area if needed (window listener better?)
|
|
84
|
+
// ZRender usually handles global mouseup if initiated within.
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
private createEditGraphic() {
|
|
88
|
+
if (!this.currentDrawing) return;
|
|
89
|
+
|
|
90
|
+
this.editGroup = new echarts.graphic.Group();
|
|
91
|
+
|
|
92
|
+
// We need current pixel coordinates
|
|
93
|
+
const p1Data = this.currentDrawing.points[0];
|
|
94
|
+
const p2Data = this.currentDrawing.points[1];
|
|
95
|
+
|
|
96
|
+
const p1 = this.context.coordinateConversion.dataToPixel(p1Data);
|
|
97
|
+
const p2 = this.context.coordinateConversion.dataToPixel(p2Data);
|
|
98
|
+
|
|
99
|
+
if (!p1 || !p2) return;
|
|
100
|
+
|
|
101
|
+
// Create Line
|
|
102
|
+
this.editLine = new echarts.graphic.Line({
|
|
103
|
+
shape: { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y },
|
|
104
|
+
style: {
|
|
105
|
+
stroke: this.currentDrawing.style?.color || "#3b82f6",
|
|
106
|
+
lineWidth: this.currentDrawing.style?.lineWidth || 2,
|
|
107
|
+
lineDash: [4, 4], // Dashed to indicate editing
|
|
108
|
+
},
|
|
109
|
+
silent: true, // Events pass through to handlers
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Create Points (we only really need to visualize the one being dragged, but showing both is fine)
|
|
113
|
+
this.editStartPoint = new echarts.graphic.Circle({
|
|
114
|
+
shape: { cx: p1.x, cy: p1.y, r: 5 },
|
|
115
|
+
style: { fill: "#fff", stroke: "#3b82f6", lineWidth: 2 },
|
|
116
|
+
z: 1000,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
this.editEndPoint = new echarts.graphic.Circle({
|
|
120
|
+
shape: { cx: p2.x, cy: p2.y, r: 5 },
|
|
121
|
+
style: { fill: "#fff", stroke: "#3b82f6", lineWidth: 2 },
|
|
122
|
+
z: 1000,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
this.editGroup.add(this.editLine);
|
|
126
|
+
this.editGroup.add(this.editStartPoint);
|
|
127
|
+
this.editGroup.add(this.editEndPoint);
|
|
128
|
+
|
|
129
|
+
this.zr.add(this.editGroup);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private onMouseMove = (e: any) => {
|
|
133
|
+
if (!this.isEditing || !this.currentDrawing) return;
|
|
134
|
+
|
|
135
|
+
const x = e.offsetX;
|
|
136
|
+
const y = e.offsetY;
|
|
137
|
+
|
|
138
|
+
if (this.isMovingShape && this.dragStart) {
|
|
139
|
+
const dx = x - this.dragStart.x;
|
|
140
|
+
const dy = y - this.dragStart.y;
|
|
141
|
+
|
|
142
|
+
// Apply delta to all points
|
|
143
|
+
const newP1 = {
|
|
144
|
+
x: this.initialPixelPoints[0].x + dx,
|
|
145
|
+
y: this.initialPixelPoints[0].y + dy,
|
|
146
|
+
};
|
|
147
|
+
const newP2 = {
|
|
148
|
+
x: this.initialPixelPoints[1].x + dx,
|
|
149
|
+
y: this.initialPixelPoints[1].y + dy,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
this.editLine.setShape({
|
|
153
|
+
x1: newP1.x,
|
|
154
|
+
y1: newP1.y,
|
|
155
|
+
x2: newP2.x,
|
|
156
|
+
y2: newP2.y,
|
|
157
|
+
});
|
|
158
|
+
this.editStartPoint.setShape({ cx: newP1.x, cy: newP1.y });
|
|
159
|
+
this.editEndPoint.setShape({ cx: newP2.x, cy: newP2.y });
|
|
160
|
+
} else if (this.editingPointIndex !== null) {
|
|
161
|
+
// Update the pixel position of the edited point in the overlay
|
|
162
|
+
if (this.editingPointIndex === 0) {
|
|
163
|
+
this.editLine.setShape({ x1: x, y1: y });
|
|
164
|
+
this.editStartPoint.setShape({ cx: x, cy: y });
|
|
165
|
+
} else {
|
|
166
|
+
this.editLine.setShape({ x2: x, y2: y });
|
|
167
|
+
this.editEndPoint.setShape({ cx: x, cy: y });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
private onMouseUp = (e: any) => {
|
|
173
|
+
if (!this.isEditing) return;
|
|
174
|
+
|
|
175
|
+
// Commit changes
|
|
176
|
+
this.finishEditing(e.offsetX, e.offsetY);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
private finishEditing(finalX: number, finishY: number) {
|
|
180
|
+
if (!this.currentDrawing) return;
|
|
181
|
+
|
|
182
|
+
if (this.isMovingShape && this.dragStart) {
|
|
183
|
+
const dx = finalX - this.dragStart.x;
|
|
184
|
+
const dy = finishY - this.dragStart.y;
|
|
185
|
+
|
|
186
|
+
// Update all points
|
|
187
|
+
const newPoints = this.initialPixelPoints.map((p, i) => {
|
|
188
|
+
const newX = p.x + dx;
|
|
189
|
+
const newY = p.y + dy;
|
|
190
|
+
return this.context.coordinateConversion.pixelToData({
|
|
191
|
+
x: newX,
|
|
192
|
+
y: newY,
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Check if conversion succeeded
|
|
197
|
+
if (newPoints.every((p) => p !== null)) {
|
|
198
|
+
// Update points
|
|
199
|
+
// Assuming 2 points for line tool
|
|
200
|
+
if (newPoints[0] && newPoints[1]) {
|
|
201
|
+
this.currentDrawing.points[0] = newPoints[0];
|
|
202
|
+
this.currentDrawing.points[1] = newPoints[1];
|
|
203
|
+
|
|
204
|
+
// Update pane index if we moved significantly (using start point as ref)
|
|
205
|
+
if (newPoints[0].paneIndex !== undefined) {
|
|
206
|
+
this.currentDrawing.paneIndex = newPoints[0].paneIndex;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.context.updateDrawing(this.currentDrawing);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} else if (this.editingPointIndex !== null) {
|
|
213
|
+
// Convert final pixel to data
|
|
214
|
+
const newData = this.context.coordinateConversion.pixelToData({
|
|
215
|
+
x: finalX,
|
|
216
|
+
y: finishY,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (newData) {
|
|
220
|
+
this.currentDrawing.points[this.editingPointIndex] = newData;
|
|
221
|
+
|
|
222
|
+
if (this.editingPointIndex === 0 && newData.paneIndex !== undefined) {
|
|
223
|
+
this.currentDrawing.paneIndex = newData.paneIndex;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.context.updateDrawing(this.currentDrawing);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Cleanup
|
|
231
|
+
this.isEditing = false;
|
|
232
|
+
this.isMovingShape = false;
|
|
233
|
+
this.dragStart = null;
|
|
234
|
+
this.initialPixelPoints = [];
|
|
235
|
+
this.currentDrawing = null;
|
|
236
|
+
this.editingPointIndex = null;
|
|
237
|
+
|
|
238
|
+
if (this.editGroup) {
|
|
239
|
+
this.zr.remove(this.editGroup);
|
|
240
|
+
this.editGroup = null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.zr.off("mousemove", this.onMouseMove);
|
|
244
|
+
this.zr.off("mouseup", this.onMouseUp);
|
|
245
|
+
|
|
246
|
+
this.context.unlockChart();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { LayoutResult } from './LayoutManager';
|
|
2
|
+
import { QFChartOptions } from '../types';
|
|
3
|
+
|
|
4
|
+
export class GraphicBuilder {
|
|
5
|
+
public static build(
|
|
6
|
+
layout: LayoutResult,
|
|
7
|
+
options: QFChartOptions,
|
|
8
|
+
onToggle: (id: string, action?: 'collapse' | 'maximize' | 'fullscreen') => void,
|
|
9
|
+
isMainCollapsed: boolean = false,
|
|
10
|
+
maximizedPaneId: string | null = null
|
|
11
|
+
): any[] {
|
|
12
|
+
const graphic: any[] = [];
|
|
13
|
+
const pixelToPercent = layout.pixelToPercent;
|
|
14
|
+
const mainPaneTop = layout.mainPaneTop;
|
|
15
|
+
|
|
16
|
+
// Main Chart Title (Only if main chart is visible or maximized)
|
|
17
|
+
// If maximizedPaneId is set and NOT main, main title should be hidden?
|
|
18
|
+
// With current LayoutManager logic, if maximizedPaneId !== main, mainPaneHeight is 0.
|
|
19
|
+
// We should check heights or IDs.
|
|
20
|
+
|
|
21
|
+
const showMain = !maximizedPaneId || maximizedPaneId === 'main';
|
|
22
|
+
|
|
23
|
+
if (showMain) {
|
|
24
|
+
const titleTopMargin = 10 * pixelToPercent;
|
|
25
|
+
graphic.push({
|
|
26
|
+
type: 'text',
|
|
27
|
+
left: '8.5%',
|
|
28
|
+
top: mainPaneTop + titleTopMargin + '%',
|
|
29
|
+
z: 10,
|
|
30
|
+
style: {
|
|
31
|
+
text: options.title || 'Market',
|
|
32
|
+
fill: options.titleColor || '#fff',
|
|
33
|
+
font: `bold 16px ${options.fontFamily || 'sans-serif'}`,
|
|
34
|
+
textVerticalAlign: 'top',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Watermark
|
|
39
|
+
if (options.watermark !== false) {
|
|
40
|
+
const bottomY = layout.mainPaneTop + layout.mainPaneHeight;
|
|
41
|
+
graphic.push({
|
|
42
|
+
type: 'text',
|
|
43
|
+
right: '11%',
|
|
44
|
+
top: bottomY - 3 + '%', // Position 5% from bottom of main chart
|
|
45
|
+
z: 10,
|
|
46
|
+
style: {
|
|
47
|
+
text: 'QFChart',
|
|
48
|
+
fill: options.fontColor || '#cbd5e1',
|
|
49
|
+
font: `bold 16px sans-serif`,
|
|
50
|
+
opacity: 0.1,
|
|
51
|
+
},
|
|
52
|
+
cursor: 'pointer',
|
|
53
|
+
onclick: () => {
|
|
54
|
+
window.open('https://quantforge.org', '_blank');
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Main Controls Group
|
|
60
|
+
const controls: any[] = [];
|
|
61
|
+
|
|
62
|
+
// Collapse Button
|
|
63
|
+
if (options.controls?.collapse) {
|
|
64
|
+
controls.push({
|
|
65
|
+
type: 'group',
|
|
66
|
+
children: [
|
|
67
|
+
{
|
|
68
|
+
type: 'rect',
|
|
69
|
+
shape: { width: 20, height: 20, r: 2 },
|
|
70
|
+
style: { fill: '#334155', stroke: '#475569', lineWidth: 1 },
|
|
71
|
+
onclick: () => onToggle('main', 'collapse'),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
style: {
|
|
76
|
+
text: isMainCollapsed ? '+' : '−',
|
|
77
|
+
fill: '#cbd5e1',
|
|
78
|
+
font: `bold 14px ${options.fontFamily}`,
|
|
79
|
+
x: 10,
|
|
80
|
+
y: 10,
|
|
81
|
+
textAlign: 'center',
|
|
82
|
+
textVerticalAlign: 'middle',
|
|
83
|
+
},
|
|
84
|
+
silent: true,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Maximize Button
|
|
91
|
+
if (options.controls?.maximize) {
|
|
92
|
+
const isMaximized = maximizedPaneId === 'main';
|
|
93
|
+
// Shift x position if collapse button exists
|
|
94
|
+
const xOffset = options.controls?.collapse ? 25 : 0;
|
|
95
|
+
|
|
96
|
+
controls.push({
|
|
97
|
+
type: 'group',
|
|
98
|
+
x: xOffset,
|
|
99
|
+
children: [
|
|
100
|
+
{
|
|
101
|
+
type: 'rect',
|
|
102
|
+
shape: { width: 20, height: 20, r: 2 },
|
|
103
|
+
style: { fill: '#334155', stroke: '#475569', lineWidth: 1 },
|
|
104
|
+
onclick: () => onToggle('main', 'maximize'),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'text',
|
|
108
|
+
style: {
|
|
109
|
+
text: isMaximized ? '❐' : '□', // Simple chars for now
|
|
110
|
+
fill: '#cbd5e1',
|
|
111
|
+
font: `14px ${options.fontFamily}`,
|
|
112
|
+
x: 10,
|
|
113
|
+
y: 10,
|
|
114
|
+
textAlign: 'center',
|
|
115
|
+
textVerticalAlign: 'middle',
|
|
116
|
+
},
|
|
117
|
+
silent: true,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fullscreen Button
|
|
124
|
+
if (options.controls?.fullscreen) {
|
|
125
|
+
let xOffset = 0;
|
|
126
|
+
if (options.controls?.collapse) xOffset += 25;
|
|
127
|
+
if (options.controls?.maximize) xOffset += 25;
|
|
128
|
+
|
|
129
|
+
controls.push({
|
|
130
|
+
type: 'group',
|
|
131
|
+
x: xOffset,
|
|
132
|
+
children: [
|
|
133
|
+
{
|
|
134
|
+
type: 'rect',
|
|
135
|
+
shape: { width: 20, height: 20, r: 2 },
|
|
136
|
+
style: { fill: '#334155', stroke: '#475569', lineWidth: 1 },
|
|
137
|
+
onclick: () => onToggle('main', 'fullscreen'),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'text',
|
|
141
|
+
style: {
|
|
142
|
+
text: '⛶',
|
|
143
|
+
fill: '#cbd5e1',
|
|
144
|
+
font: `14px ${options.fontFamily}`,
|
|
145
|
+
x: 10,
|
|
146
|
+
y: 10,
|
|
147
|
+
textAlign: 'center',
|
|
148
|
+
textVerticalAlign: 'middle',
|
|
149
|
+
},
|
|
150
|
+
silent: true,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (controls.length > 0) {
|
|
157
|
+
graphic.push({
|
|
158
|
+
type: 'group',
|
|
159
|
+
right: '10.5%',
|
|
160
|
+
top: mainPaneTop + '%',
|
|
161
|
+
children: controls,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Indicator Panes
|
|
167
|
+
layout.paneLayout.forEach((pane) => {
|
|
168
|
+
// If maximizedPaneId is set, and this is NOT the maximized pane, skip rendering its controls
|
|
169
|
+
if (maximizedPaneId && pane.indicatorId !== maximizedPaneId) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Title
|
|
174
|
+
graphic.push({
|
|
175
|
+
type: 'text',
|
|
176
|
+
left: '8.5%',
|
|
177
|
+
top: pane.top + 10 * pixelToPercent + '%',
|
|
178
|
+
z: 10,
|
|
179
|
+
style: {
|
|
180
|
+
text: pane.indicatorId || '',
|
|
181
|
+
fill: pane.titleColor || '#fff',
|
|
182
|
+
font: `bold 12px ${options.fontFamily || 'sans-serif'}`,
|
|
183
|
+
textVerticalAlign: 'top',
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Controls
|
|
188
|
+
const controls: any[] = [];
|
|
189
|
+
|
|
190
|
+
// Collapse
|
|
191
|
+
if (pane.controls?.collapse) {
|
|
192
|
+
controls.push({
|
|
193
|
+
type: 'group',
|
|
194
|
+
children: [
|
|
195
|
+
{
|
|
196
|
+
type: 'rect',
|
|
197
|
+
shape: { width: 20, height: 20, r: 2 },
|
|
198
|
+
style: { fill: '#334155', stroke: '#475569', lineWidth: 1 },
|
|
199
|
+
onclick: () => pane.indicatorId && onToggle(pane.indicatorId, 'collapse'),
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
type: 'text',
|
|
203
|
+
style: {
|
|
204
|
+
text: pane.isCollapsed ? '+' : '−',
|
|
205
|
+
fill: '#cbd5e1',
|
|
206
|
+
font: `bold 14px ${options.fontFamily}`,
|
|
207
|
+
x: 10,
|
|
208
|
+
y: 10,
|
|
209
|
+
textAlign: 'center',
|
|
210
|
+
textVerticalAlign: 'middle',
|
|
211
|
+
},
|
|
212
|
+
silent: true,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Maximize
|
|
219
|
+
if (pane.controls?.maximize) {
|
|
220
|
+
// Assuming we add maximize to Indicator controls
|
|
221
|
+
const isMaximized = maximizedPaneId === pane.indicatorId;
|
|
222
|
+
const xOffset = pane.controls?.collapse ? 25 : 0;
|
|
223
|
+
|
|
224
|
+
controls.push({
|
|
225
|
+
type: 'group',
|
|
226
|
+
x: xOffset,
|
|
227
|
+
children: [
|
|
228
|
+
{
|
|
229
|
+
type: 'rect',
|
|
230
|
+
shape: { width: 20, height: 20, r: 2 },
|
|
231
|
+
style: { fill: '#334155', stroke: '#475569', lineWidth: 1 },
|
|
232
|
+
onclick: () => pane.indicatorId && onToggle(pane.indicatorId, 'maximize'),
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: 'text',
|
|
236
|
+
style: {
|
|
237
|
+
text: isMaximized ? '❐' : '□',
|
|
238
|
+
fill: '#cbd5e1',
|
|
239
|
+
font: `14px ${options.fontFamily}`,
|
|
240
|
+
x: 10,
|
|
241
|
+
y: 10,
|
|
242
|
+
textAlign: 'center',
|
|
243
|
+
textVerticalAlign: 'middle',
|
|
244
|
+
},
|
|
245
|
+
silent: true,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (controls.length > 0) {
|
|
252
|
+
graphic.push({
|
|
253
|
+
type: 'group',
|
|
254
|
+
right: '10.5%',
|
|
255
|
+
top: pane.top + '%',
|
|
256
|
+
children: controls,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return graphic;
|
|
262
|
+
}
|
|
263
|
+
}
|