@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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Indicator as IndicatorInterface, IndicatorPlot, IndicatorPoint } from '../types';
|
|
2
|
+
|
|
3
|
+
export class Indicator implements IndicatorInterface {
|
|
4
|
+
public id: string;
|
|
5
|
+
public plots: { [name: string]: IndicatorPlot };
|
|
6
|
+
public paneIndex: number;
|
|
7
|
+
public height?: number;
|
|
8
|
+
public collapsed: boolean;
|
|
9
|
+
public titleColor?: string;
|
|
10
|
+
public controls?: { collapse?: boolean; maximize?: boolean };
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
id: string,
|
|
14
|
+
plots: { [name: string]: IndicatorPlot },
|
|
15
|
+
paneIndex: number,
|
|
16
|
+
options: {
|
|
17
|
+
height?: number;
|
|
18
|
+
collapsed?: boolean;
|
|
19
|
+
titleColor?: string;
|
|
20
|
+
controls?: { collapse?: boolean; maximize?: boolean };
|
|
21
|
+
} = {}
|
|
22
|
+
) {
|
|
23
|
+
this.id = id;
|
|
24
|
+
this.plots = plots;
|
|
25
|
+
this.paneIndex = paneIndex;
|
|
26
|
+
this.height = options.height;
|
|
27
|
+
this.collapsed = options.collapsed || false;
|
|
28
|
+
this.titleColor = options.titleColor;
|
|
29
|
+
this.controls = options.controls;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public toggleCollapse(): void {
|
|
33
|
+
this.collapsed = !this.collapsed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public isVisible(): boolean {
|
|
37
|
+
return !this.collapsed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Update indicator data incrementally by merging new points
|
|
42
|
+
*
|
|
43
|
+
* @param plots - New plots data to merge (same structure as constructor)
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* This method merges new indicator data with existing data by timestamp.
|
|
47
|
+
* - New timestamps are added
|
|
48
|
+
* - Existing timestamps are updated with new values
|
|
49
|
+
* - All data is automatically sorted by time after merge
|
|
50
|
+
*
|
|
51
|
+
* **Important**: This method only updates the indicator's internal data structure.
|
|
52
|
+
* To see the changes reflected in the chart, you MUST call `chart.updateData()`
|
|
53
|
+
* after updating indicator data.
|
|
54
|
+
*
|
|
55
|
+
* **Usage Pattern**:
|
|
56
|
+
* ```typescript
|
|
57
|
+
* // 1. Update indicator data first
|
|
58
|
+
* indicator.updateData({
|
|
59
|
+
* macd: { data: [{ time: 1234567890, value: 150 }], options: { style: 'line', color: '#2962FF' } }
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* // 2. Then update chart data to trigger re-render
|
|
63
|
+
* chart.updateData([
|
|
64
|
+
* { time: 1234567890, open: 100, high: 105, low: 99, close: 103, volume: 1000 }
|
|
65
|
+
* ]);
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* **Note**: If you update indicator data without corresponding market data changes,
|
|
69
|
+
* this typically indicates a recalculation scenario. In normal workflows, indicator
|
|
70
|
+
* values are derived from market data, so indicator updates should correspond to
|
|
71
|
+
* new or modified market bars.
|
|
72
|
+
*/
|
|
73
|
+
public updateData(plots: { [name: string]: IndicatorPlot }): void {
|
|
74
|
+
Object.keys(plots).forEach((plotName) => {
|
|
75
|
+
if (!this.plots[plotName]) {
|
|
76
|
+
// New plot - add it
|
|
77
|
+
this.plots[plotName] = plots[plotName];
|
|
78
|
+
} else {
|
|
79
|
+
// Existing plot - merge data points
|
|
80
|
+
const existingPlot = this.plots[plotName];
|
|
81
|
+
const newPlot = plots[plotName];
|
|
82
|
+
|
|
83
|
+
// Update options if provided
|
|
84
|
+
if (newPlot.options) {
|
|
85
|
+
existingPlot.options = { ...existingPlot.options, ...newPlot.options };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Merge data points by time
|
|
89
|
+
const existingTimeMap = new Map<number, IndicatorPoint>();
|
|
90
|
+
existingPlot.data.forEach((point) => {
|
|
91
|
+
existingTimeMap.set(point.time, point);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Update or add new points
|
|
95
|
+
newPlot.data.forEach((point) => {
|
|
96
|
+
existingTimeMap.set(point.time, point);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Rebuild data array sorted by time
|
|
100
|
+
existingPlot.data = Array.from(existingTimeMap.values()).sort((a, b) => a.time - b.time);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { QFChartOptions, Indicator as IndicatorType } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface PaneConfiguration {
|
|
4
|
+
index: number;
|
|
5
|
+
height: number;
|
|
6
|
+
top: number;
|
|
7
|
+
isCollapsed: boolean;
|
|
8
|
+
indicatorId?: string;
|
|
9
|
+
titleColor?: string;
|
|
10
|
+
controls?: {
|
|
11
|
+
collapse?: boolean;
|
|
12
|
+
maximize?: boolean;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LayoutResult {
|
|
17
|
+
grid: any[];
|
|
18
|
+
xAxis: any[];
|
|
19
|
+
yAxis: any[];
|
|
20
|
+
dataZoom: any[];
|
|
21
|
+
paneLayout: PaneConfiguration[];
|
|
22
|
+
mainPaneHeight: number;
|
|
23
|
+
mainPaneTop: number;
|
|
24
|
+
pixelToPercent: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class LayoutManager {
|
|
28
|
+
public static calculate(
|
|
29
|
+
containerHeight: number,
|
|
30
|
+
indicators: Map<string, IndicatorType>,
|
|
31
|
+
options: QFChartOptions,
|
|
32
|
+
isMainCollapsed: boolean = false,
|
|
33
|
+
maximizedPaneId: string | null = null
|
|
34
|
+
): LayoutResult {
|
|
35
|
+
// Calculate pixelToPercent early for maximized logic
|
|
36
|
+
let pixelToPercent = 0;
|
|
37
|
+
if (containerHeight > 0) {
|
|
38
|
+
pixelToPercent = (1 / containerHeight) * 100;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Identify unique separate panes (indices > 0) and sort them
|
|
42
|
+
const separatePaneIndices = Array.from(indicators.values())
|
|
43
|
+
.map((ind) => ind.paneIndex)
|
|
44
|
+
.filter((idx) => idx > 0)
|
|
45
|
+
.sort((a, b) => a - b)
|
|
46
|
+
.filter((value, index, self) => self.indexOf(value) === index); // Unique
|
|
47
|
+
|
|
48
|
+
const hasSeparatePane = separatePaneIndices.length > 0;
|
|
49
|
+
|
|
50
|
+
// DataZoom Configuration
|
|
51
|
+
const dzVisible = options.dataZoom?.visible ?? true;
|
|
52
|
+
const dzPosition = options.dataZoom?.position ?? 'top';
|
|
53
|
+
const dzHeight = options.dataZoom?.height ?? 6;
|
|
54
|
+
const dzStart = options.dataZoom?.start ?? 0;
|
|
55
|
+
const dzEnd = options.dataZoom?.end ?? 100;
|
|
56
|
+
|
|
57
|
+
// Layout Calculation
|
|
58
|
+
let mainPaneTop = 8;
|
|
59
|
+
let chartAreaBottom = 92; // Default if no dataZoom at bottom
|
|
60
|
+
|
|
61
|
+
// Maximized State Logic
|
|
62
|
+
let maximizeTargetIndex = -1; // -1 = none
|
|
63
|
+
|
|
64
|
+
if (maximizedPaneId) {
|
|
65
|
+
if (maximizedPaneId === 'main') {
|
|
66
|
+
maximizeTargetIndex = 0;
|
|
67
|
+
} else {
|
|
68
|
+
const ind = indicators.get(maximizedPaneId);
|
|
69
|
+
if (ind) {
|
|
70
|
+
maximizeTargetIndex = ind.paneIndex;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (maximizeTargetIndex !== -1) {
|
|
76
|
+
// Special Layout for Maximize
|
|
77
|
+
// We must generate grid/axis definitions for ALL indices to maintain series mapping,
|
|
78
|
+
// but hide the non-maximized ones.
|
|
79
|
+
|
|
80
|
+
const grid: any[] = [];
|
|
81
|
+
const xAxis: any[] = [];
|
|
82
|
+
const yAxis: any[] = [];
|
|
83
|
+
const dataZoom: any[] = []; // Hide slider, keep inside?
|
|
84
|
+
|
|
85
|
+
// DataZoom: keep inside, maybe slider if main?
|
|
86
|
+
// Let's keep strict maximize: Full container.
|
|
87
|
+
// Use defaults for maximize if not available, or preserve logic?
|
|
88
|
+
// The calculateMaximized doesn't use LayoutManager.calculate directly but inline logic.
|
|
89
|
+
// It should probably respect the same zoom?
|
|
90
|
+
// But here we are inside LayoutManager.calculate.
|
|
91
|
+
|
|
92
|
+
const dzStart = options.dataZoom?.start ?? 50;
|
|
93
|
+
const dzEnd = options.dataZoom?.end ?? 100;
|
|
94
|
+
|
|
95
|
+
dataZoom.push({ type: 'inside', xAxisIndex: 'all', start: dzStart, end: dzEnd });
|
|
96
|
+
|
|
97
|
+
// Need to know total panes to iterate
|
|
98
|
+
const maxPaneIndex = hasSeparatePane ? Math.max(...separatePaneIndices) : 0;
|
|
99
|
+
|
|
100
|
+
const paneConfigs: PaneConfiguration[] = []; // For GraphicBuilder title placement
|
|
101
|
+
|
|
102
|
+
// Iterate 0 to maxPaneIndex
|
|
103
|
+
for (let i = 0; i <= maxPaneIndex; i++) {
|
|
104
|
+
const isTarget = i === maximizeTargetIndex;
|
|
105
|
+
|
|
106
|
+
// Grid
|
|
107
|
+
grid.push({
|
|
108
|
+
left: '10%',
|
|
109
|
+
right: '10%',
|
|
110
|
+
top: isTarget ? '5%' : '0%',
|
|
111
|
+
height: isTarget ? '90%' : '0%',
|
|
112
|
+
show: isTarget,
|
|
113
|
+
containLabel: false,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// X-Axis
|
|
117
|
+
xAxis.push({
|
|
118
|
+
type: 'category',
|
|
119
|
+
gridIndex: i,
|
|
120
|
+
data: [],
|
|
121
|
+
show: isTarget,
|
|
122
|
+
axisLabel: {
|
|
123
|
+
show: isTarget,
|
|
124
|
+
color: '#94a3b8',
|
|
125
|
+
fontFamily: options.fontFamily,
|
|
126
|
+
},
|
|
127
|
+
axisLine: { show: isTarget, lineStyle: { color: '#334155' } },
|
|
128
|
+
splitLine: {
|
|
129
|
+
show: isTarget,
|
|
130
|
+
lineStyle: { color: '#334155', opacity: 0.5 },
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Y-Axis
|
|
135
|
+
yAxis.push({
|
|
136
|
+
position: 'right',
|
|
137
|
+
gridIndex: i,
|
|
138
|
+
show: isTarget,
|
|
139
|
+
scale: true,
|
|
140
|
+
axisLabel: {
|
|
141
|
+
show: isTarget,
|
|
142
|
+
color: '#94a3b8',
|
|
143
|
+
fontFamily: options.fontFamily,
|
|
144
|
+
},
|
|
145
|
+
splitLine: {
|
|
146
|
+
show: isTarget,
|
|
147
|
+
lineStyle: { color: '#334155', opacity: 0.5 },
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Reconstruct Pane Config for GraphicBuilder
|
|
152
|
+
// We need to return `paneLayout` so GraphicBuilder can draw the Restore button
|
|
153
|
+
if (i > 0) {
|
|
154
|
+
// Find indicator for this pane
|
|
155
|
+
const ind = Array.from(indicators.values()).find((ind) => ind.paneIndex === i);
|
|
156
|
+
if (ind) {
|
|
157
|
+
paneConfigs.push({
|
|
158
|
+
index: i,
|
|
159
|
+
height: isTarget ? 90 : 0,
|
|
160
|
+
top: isTarget ? 5 : 0,
|
|
161
|
+
isCollapsed: false,
|
|
162
|
+
indicatorId: ind.id,
|
|
163
|
+
titleColor: ind.titleColor,
|
|
164
|
+
controls: ind.controls,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
grid,
|
|
172
|
+
xAxis,
|
|
173
|
+
yAxis,
|
|
174
|
+
dataZoom,
|
|
175
|
+
paneLayout: paneConfigs,
|
|
176
|
+
mainPaneHeight: maximizeTargetIndex === 0 ? 90 : 0,
|
|
177
|
+
mainPaneTop: maximizeTargetIndex === 0 ? 5 : 0,
|
|
178
|
+
pixelToPercent,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (dzVisible) {
|
|
183
|
+
if (dzPosition === 'top') {
|
|
184
|
+
// DataZoom takes top 0% to dzHeight%
|
|
185
|
+
// Main chart starts below it with a small gap
|
|
186
|
+
mainPaneTop = dzHeight + 4; // dzHeight + 4% gap
|
|
187
|
+
chartAreaBottom = 95; // Use more space at bottom since slider is gone
|
|
188
|
+
} else {
|
|
189
|
+
// DataZoom takes bottom
|
|
190
|
+
// Chart ends at 100 - dzHeight - margin
|
|
191
|
+
chartAreaBottom = 100 - dzHeight - 2;
|
|
192
|
+
mainPaneTop = 8;
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
// No data zoom
|
|
196
|
+
mainPaneTop = 5;
|
|
197
|
+
chartAreaBottom = 95;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// We need to calculate height distribution dynamically to avoid overlap.
|
|
201
|
+
// Calculate gap in percent
|
|
202
|
+
let gapPercent = 5;
|
|
203
|
+
if (containerHeight > 0) {
|
|
204
|
+
gapPercent = (20 / containerHeight) * 100;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let mainHeightVal = 75; // Default if no separate pane
|
|
208
|
+
|
|
209
|
+
// Prepare separate panes configuration
|
|
210
|
+
let paneConfigs: PaneConfiguration[] = [];
|
|
211
|
+
|
|
212
|
+
if (hasSeparatePane) {
|
|
213
|
+
// Resolve heights for all separate panes
|
|
214
|
+
// 1. Identify panes and their requested heights
|
|
215
|
+
const panes = separatePaneIndices.map((idx) => {
|
|
216
|
+
const ind = Array.from(indicators.values()).find((i) => i.paneIndex === idx);
|
|
217
|
+
return {
|
|
218
|
+
index: idx,
|
|
219
|
+
requestedHeight: ind?.height,
|
|
220
|
+
isCollapsed: ind?.collapsed ?? false,
|
|
221
|
+
indicatorId: ind?.id,
|
|
222
|
+
titleColor: ind?.titleColor,
|
|
223
|
+
controls: ind?.controls,
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// 2. Assign actual heights
|
|
228
|
+
// If collapsed, use small fixed height (e.g. 3%)
|
|
229
|
+
const resolvedPanes = panes.map((p) => ({
|
|
230
|
+
...p,
|
|
231
|
+
height: p.isCollapsed ? 3 : p.requestedHeight !== undefined ? p.requestedHeight : 15,
|
|
232
|
+
}));
|
|
233
|
+
|
|
234
|
+
// 3. Calculate total space needed for indicators
|
|
235
|
+
const totalIndicatorHeight = resolvedPanes.reduce((sum, p) => sum + p.height, 0);
|
|
236
|
+
const totalGaps = resolvedPanes.length * gapPercent;
|
|
237
|
+
const totalBottomSpace = totalIndicatorHeight + totalGaps;
|
|
238
|
+
|
|
239
|
+
// 4. Calculate Main Chart Height
|
|
240
|
+
// Available space = chartAreaBottom - mainPaneTop;
|
|
241
|
+
const totalAvailable = chartAreaBottom - mainPaneTop;
|
|
242
|
+
mainHeightVal = totalAvailable - totalBottomSpace;
|
|
243
|
+
|
|
244
|
+
if (isMainCollapsed) {
|
|
245
|
+
mainHeightVal = 3;
|
|
246
|
+
} else {
|
|
247
|
+
// Safety check: ensure main chart has at least some space (e.g. 20%)
|
|
248
|
+
if (mainHeightVal < 20) {
|
|
249
|
+
mainHeightVal = Math.max(mainHeightVal, 10);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 5. Calculate positions
|
|
254
|
+
let currentTop = mainPaneTop + mainHeightVal + gapPercent;
|
|
255
|
+
|
|
256
|
+
paneConfigs = resolvedPanes.map((p) => {
|
|
257
|
+
const config = {
|
|
258
|
+
index: p.index,
|
|
259
|
+
height: p.height,
|
|
260
|
+
top: currentTop,
|
|
261
|
+
isCollapsed: p.isCollapsed,
|
|
262
|
+
indicatorId: p.indicatorId,
|
|
263
|
+
titleColor: p.titleColor,
|
|
264
|
+
controls: p.controls,
|
|
265
|
+
};
|
|
266
|
+
currentTop += p.height + gapPercent;
|
|
267
|
+
return config;
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
mainHeightVal = chartAreaBottom - mainPaneTop;
|
|
271
|
+
if (isMainCollapsed) {
|
|
272
|
+
mainHeightVal = 3;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// --- Generate Grids ---
|
|
277
|
+
const grid: any[] = [];
|
|
278
|
+
// Main Grid (index 0)
|
|
279
|
+
grid.push({
|
|
280
|
+
left: '10%',
|
|
281
|
+
right: '10%',
|
|
282
|
+
top: mainPaneTop + '%',
|
|
283
|
+
height: mainHeightVal + '%',
|
|
284
|
+
containLabel: false, // We handle margins explicitly
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Separate Panes Grids
|
|
288
|
+
paneConfigs.forEach((pane) => {
|
|
289
|
+
grid.push({
|
|
290
|
+
left: '10%',
|
|
291
|
+
right: '10%',
|
|
292
|
+
top: pane.top + '%',
|
|
293
|
+
height: pane.height + '%',
|
|
294
|
+
containLabel: false,
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// --- Generate X-Axes ---
|
|
299
|
+
const allXAxisIndices = [0, ...paneConfigs.map((_, i) => i + 1)];
|
|
300
|
+
const xAxis: any[] = [];
|
|
301
|
+
|
|
302
|
+
// Main X-Axis
|
|
303
|
+
const isMainBottom = paneConfigs.length === 0;
|
|
304
|
+
xAxis.push({
|
|
305
|
+
type: 'category',
|
|
306
|
+
data: [], // Will be filled by SeriesBuilder or QFChart
|
|
307
|
+
gridIndex: 0,
|
|
308
|
+
scale: true,
|
|
309
|
+
// boundaryGap will be set in QFChart.ts based on padding option
|
|
310
|
+
axisLine: {
|
|
311
|
+
onZero: false,
|
|
312
|
+
show: !isMainCollapsed,
|
|
313
|
+
lineStyle: { color: '#334155' },
|
|
314
|
+
},
|
|
315
|
+
splitLine: {
|
|
316
|
+
show: !isMainCollapsed,
|
|
317
|
+
lineStyle: { color: '#334155', opacity: 0.5 },
|
|
318
|
+
},
|
|
319
|
+
axisLabel: {
|
|
320
|
+
show: !isMainCollapsed,
|
|
321
|
+
color: '#94a3b8',
|
|
322
|
+
fontFamily: options.fontFamily || 'sans-serif',
|
|
323
|
+
},
|
|
324
|
+
axisTick: { show: !isMainCollapsed },
|
|
325
|
+
axisPointer: {
|
|
326
|
+
label: {
|
|
327
|
+
show: isMainBottom,
|
|
328
|
+
fontSize: 11,
|
|
329
|
+
backgroundColor: '#475569',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Separate Panes X-Axes
|
|
335
|
+
paneConfigs.forEach((pane, i) => {
|
|
336
|
+
const isBottom = i === paneConfigs.length - 1;
|
|
337
|
+
xAxis.push({
|
|
338
|
+
type: 'category',
|
|
339
|
+
gridIndex: i + 1, // 0 is main
|
|
340
|
+
data: [], // Shared data
|
|
341
|
+
axisLabel: { show: false }, // Hide labels on indicator panes
|
|
342
|
+
axisLine: { show: !pane.isCollapsed, lineStyle: { color: '#334155' } },
|
|
343
|
+
axisTick: { show: false },
|
|
344
|
+
splitLine: { show: false },
|
|
345
|
+
axisPointer: {
|
|
346
|
+
label: {
|
|
347
|
+
show: isBottom,
|
|
348
|
+
fontSize: 11,
|
|
349
|
+
backgroundColor: '#475569',
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// --- Generate Y-Axes ---
|
|
356
|
+
const yAxis: any[] = [];
|
|
357
|
+
// Main Y-Axis
|
|
358
|
+
yAxis.push({
|
|
359
|
+
position: 'right',
|
|
360
|
+
scale: true,
|
|
361
|
+
gridIndex: 0,
|
|
362
|
+
splitLine: {
|
|
363
|
+
show: !isMainCollapsed,
|
|
364
|
+
lineStyle: { color: '#334155', opacity: 0.5 },
|
|
365
|
+
},
|
|
366
|
+
axisLine: { show: !isMainCollapsed, lineStyle: { color: '#334155' } },
|
|
367
|
+
axisLabel: {
|
|
368
|
+
show: !isMainCollapsed,
|
|
369
|
+
color: '#94a3b8',
|
|
370
|
+
fontFamily: options.fontFamily || 'sans-serif',
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Separate Panes Y-Axes
|
|
375
|
+
paneConfigs.forEach((pane, i) => {
|
|
376
|
+
yAxis.push({
|
|
377
|
+
position: 'right',
|
|
378
|
+
scale: true,
|
|
379
|
+
gridIndex: i + 1,
|
|
380
|
+
splitLine: {
|
|
381
|
+
show: !pane.isCollapsed,
|
|
382
|
+
lineStyle: { color: '#334155', opacity: 0.3 },
|
|
383
|
+
},
|
|
384
|
+
axisLabel: {
|
|
385
|
+
show: !pane.isCollapsed,
|
|
386
|
+
color: '#94a3b8',
|
|
387
|
+
fontFamily: options.fontFamily || 'sans-serif',
|
|
388
|
+
fontSize: 10,
|
|
389
|
+
},
|
|
390
|
+
axisLine: { show: !pane.isCollapsed, lineStyle: { color: '#334155' } },
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// --- Generate DataZoom ---
|
|
395
|
+
const dataZoom: any[] = [];
|
|
396
|
+
if (dzVisible) {
|
|
397
|
+
dataZoom.push({
|
|
398
|
+
type: 'inside',
|
|
399
|
+
xAxisIndex: allXAxisIndices,
|
|
400
|
+
start: dzStart,
|
|
401
|
+
end: dzEnd,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (dzPosition === 'top') {
|
|
405
|
+
dataZoom.push({
|
|
406
|
+
type: 'slider',
|
|
407
|
+
xAxisIndex: allXAxisIndices,
|
|
408
|
+
top: '1%',
|
|
409
|
+
height: dzHeight + '%',
|
|
410
|
+
start: dzStart,
|
|
411
|
+
end: dzEnd,
|
|
412
|
+
borderColor: '#334155',
|
|
413
|
+
textStyle: { color: '#cbd5e1' },
|
|
414
|
+
brushSelect: false,
|
|
415
|
+
});
|
|
416
|
+
} else {
|
|
417
|
+
dataZoom.push({
|
|
418
|
+
type: 'slider',
|
|
419
|
+
xAxisIndex: allXAxisIndices,
|
|
420
|
+
bottom: '1%',
|
|
421
|
+
height: dzHeight + '%',
|
|
422
|
+
start: dzStart,
|
|
423
|
+
end: dzEnd,
|
|
424
|
+
borderColor: '#334155',
|
|
425
|
+
textStyle: { color: '#cbd5e1' },
|
|
426
|
+
brushSelect: false,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
grid,
|
|
433
|
+
xAxis,
|
|
434
|
+
yAxis,
|
|
435
|
+
dataZoom,
|
|
436
|
+
paneLayout: paneConfigs,
|
|
437
|
+
mainPaneHeight: mainHeightVal,
|
|
438
|
+
mainPaneTop,
|
|
439
|
+
pixelToPercent,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private static calculateMaximized(
|
|
444
|
+
containerHeight: number,
|
|
445
|
+
options: QFChartOptions,
|
|
446
|
+
targetPaneIndex: number // 0 for main, 1+ for indicators
|
|
447
|
+
): LayoutResult {
|
|
448
|
+
return {
|
|
449
|
+
grid: [],
|
|
450
|
+
xAxis: [],
|
|
451
|
+
yAxis: [],
|
|
452
|
+
dataZoom: [],
|
|
453
|
+
paneLayout: [],
|
|
454
|
+
mainPaneHeight: 0,
|
|
455
|
+
mainPaneTop: 0,
|
|
456
|
+
pixelToPercent: 0,
|
|
457
|
+
} as any;
|
|
458
|
+
}
|
|
459
|
+
}
|