@qfo/qfchart 0.6.7 → 0.7.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 +4 -1
- package/dist/qfchart.min.browser.js +14 -14
- package/dist/qfchart.min.es.js +14 -14
- package/package.json +81 -81
- package/src/QFChart.ts +52 -0
- package/src/components/LayoutManager.ts +682 -679
- package/src/components/SeriesBuilder.ts +260 -250
- package/src/components/SeriesRendererFactory.ts +8 -0
- package/src/components/TableOverlayRenderer.ts +322 -0
- package/src/components/renderers/BoxRenderer.ts +258 -0
- package/src/components/renderers/DrawingLineRenderer.ts +194 -0
- package/src/components/renderers/FillRenderer.ts +99 -99
- package/src/components/renderers/LabelRenderer.ts +85 -41
- package/src/components/renderers/LinefillRenderer.ts +155 -0
- package/src/components/renderers/PolylineRenderer.ts +197 -0
- package/src/components/renderers/SeriesRenderer.ts +21 -20
- package/src/components/renderers/ShapeRenderer.ts +121 -121
- package/src/types.ts +2 -1
- package/src/utils/ShapeUtils.ts +156 -140
|
@@ -1,679 +1,682 @@
|
|
|
1
|
-
import { QFChartOptions, Indicator as IndicatorType, OHLCV } from '../types';
|
|
2
|
-
import { AxisUtils } from '../utils/AxisUtils';
|
|
3
|
-
|
|
4
|
-
export interface PaneConfiguration {
|
|
5
|
-
index: number;
|
|
6
|
-
height: number;
|
|
7
|
-
top: number;
|
|
8
|
-
isCollapsed: boolean;
|
|
9
|
-
indicatorId?: string;
|
|
10
|
-
titleColor?: string;
|
|
11
|
-
controls?: {
|
|
12
|
-
collapse?: boolean;
|
|
13
|
-
maximize?: boolean;
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface LayoutResult {
|
|
18
|
-
grid: any[];
|
|
19
|
-
xAxis: any[];
|
|
20
|
-
yAxis: any[];
|
|
21
|
-
dataZoom: any[];
|
|
22
|
-
paneLayout: PaneConfiguration[];
|
|
23
|
-
mainPaneHeight: number;
|
|
24
|
-
mainPaneTop: number;
|
|
25
|
-
pixelToPercent: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class LayoutManager {
|
|
29
|
-
public static calculate(
|
|
30
|
-
containerHeight: number,
|
|
31
|
-
indicators: Map<string, IndicatorType>,
|
|
32
|
-
options: QFChartOptions,
|
|
33
|
-
isMainCollapsed: boolean = false,
|
|
34
|
-
maximizedPaneId: string | null = null,
|
|
35
|
-
marketData?: import('../types').OHLCV[]
|
|
36
|
-
): LayoutResult & { overlayYAxisMap: Map<string, number>; separatePaneYAxisOffset: number } {
|
|
37
|
-
// Calculate pixelToPercent early for maximized logic
|
|
38
|
-
let pixelToPercent = 0;
|
|
39
|
-
if (containerHeight > 0) {
|
|
40
|
-
pixelToPercent = (1 / containerHeight) * 100;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Get Y-axis padding percentage (default 5%)
|
|
44
|
-
const yAxisPaddingPercent = options.yAxisPadding !== undefined ? options.yAxisPadding : 5;
|
|
45
|
-
|
|
46
|
-
// Identify unique separate panes (indices > 0) and sort them
|
|
47
|
-
const separatePaneIndices = Array.from(indicators.values())
|
|
48
|
-
.map((ind) => ind.paneIndex)
|
|
49
|
-
.filter((idx) => idx > 0)
|
|
50
|
-
.sort((a, b) => a - b)
|
|
51
|
-
.filter((value, index, self) => self.indexOf(value) === index); // Unique
|
|
52
|
-
|
|
53
|
-
const hasSeparatePane = separatePaneIndices.length > 0;
|
|
54
|
-
|
|
55
|
-
// DataZoom Configuration
|
|
56
|
-
const dzVisible = options.dataZoom?.visible ?? true;
|
|
57
|
-
const dzPosition = options.dataZoom?.position ?? 'top';
|
|
58
|
-
const dzHeight = options.dataZoom?.height ?? 6;
|
|
59
|
-
const dzStart = options.dataZoom?.start ?? 0;
|
|
60
|
-
const dzEnd = options.dataZoom?.end ?? 100;
|
|
61
|
-
|
|
62
|
-
// Layout Calculation
|
|
63
|
-
let mainPaneTop = 8;
|
|
64
|
-
let chartAreaBottom = 92; // Default if no dataZoom at bottom
|
|
65
|
-
|
|
66
|
-
// Maximized State Logic
|
|
67
|
-
let maximizeTargetIndex = -1; // -1 = none
|
|
68
|
-
|
|
69
|
-
if (maximizedPaneId) {
|
|
70
|
-
if (maximizedPaneId === 'main') {
|
|
71
|
-
maximizeTargetIndex = 0;
|
|
72
|
-
} else {
|
|
73
|
-
const ind = indicators.get(maximizedPaneId);
|
|
74
|
-
if (ind) {
|
|
75
|
-
maximizeTargetIndex = ind.paneIndex;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (maximizeTargetIndex !== -1) {
|
|
81
|
-
// Special Layout for Maximize
|
|
82
|
-
// We must generate grid/axis definitions for ALL indices to maintain series mapping,
|
|
83
|
-
// but hide the non-maximized ones.
|
|
84
|
-
|
|
85
|
-
const grid: any[] = [];
|
|
86
|
-
const xAxis: any[] = [];
|
|
87
|
-
const yAxis: any[] = [];
|
|
88
|
-
const dataZoom: any[] = []; // Hide slider, keep inside?
|
|
89
|
-
|
|
90
|
-
// DataZoom: keep inside, maybe slider if main?
|
|
91
|
-
// Let's keep strict maximize: Full container.
|
|
92
|
-
// Use defaults for maximize if not available, or preserve logic?
|
|
93
|
-
// The calculateMaximized doesn't use LayoutManager.calculate directly but inline logic.
|
|
94
|
-
// It should probably respect the same zoom?
|
|
95
|
-
// But here we are inside LayoutManager.calculate.
|
|
96
|
-
|
|
97
|
-
const dzStart = options.dataZoom?.start ?? 50;
|
|
98
|
-
const dzEnd = options.dataZoom?.end ?? 100;
|
|
99
|
-
|
|
100
|
-
// Add 'inside' zoom only if zoomOnTouch is enabled (default true)
|
|
101
|
-
const zoomOnTouch = options.dataZoom?.zoomOnTouch ?? true;
|
|
102
|
-
if (zoomOnTouch) {
|
|
103
|
-
dataZoom.push({ type: 'inside', xAxisIndex: 'all', start: dzStart, end: dzEnd });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Need to know total panes to iterate
|
|
107
|
-
const maxPaneIndex = hasSeparatePane ? Math.max(...separatePaneIndices) : 0;
|
|
108
|
-
|
|
109
|
-
const paneConfigs: PaneConfiguration[] = []; // For GraphicBuilder title placement
|
|
110
|
-
|
|
111
|
-
// Iterate 0 to maxPaneIndex
|
|
112
|
-
for (let i = 0; i <= maxPaneIndex; i++) {
|
|
113
|
-
const isTarget = i === maximizeTargetIndex;
|
|
114
|
-
|
|
115
|
-
// Grid
|
|
116
|
-
grid.push({
|
|
117
|
-
left: '10%',
|
|
118
|
-
right: '10%',
|
|
119
|
-
top: isTarget ? '5%' : '0%',
|
|
120
|
-
height: isTarget ? '90%' : '0%',
|
|
121
|
-
show: isTarget,
|
|
122
|
-
containLabel: false,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// X-Axis
|
|
126
|
-
xAxis.push({
|
|
127
|
-
type: 'category',
|
|
128
|
-
gridIndex: i,
|
|
129
|
-
data: [],
|
|
130
|
-
show: isTarget,
|
|
131
|
-
axisLabel: {
|
|
132
|
-
show: isTarget,
|
|
133
|
-
color: '#94a3b8',
|
|
134
|
-
fontFamily: options.fontFamily,
|
|
135
|
-
},
|
|
136
|
-
axisLine: { show: isTarget, lineStyle: { color: '#334155' } },
|
|
137
|
-
splitLine: {
|
|
138
|
-
show: isTarget,
|
|
139
|
-
lineStyle: { color: '#334155', opacity: 0.5 },
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// Y-Axis
|
|
144
|
-
// For maximized pane 0 (main), respect custom min/max if provided
|
|
145
|
-
let yMin: any;
|
|
146
|
-
let yMax: any;
|
|
147
|
-
|
|
148
|
-
if (i === 0 && maximizeTargetIndex === 0) {
|
|
149
|
-
// Main pane is maximized, use custom values if provided
|
|
150
|
-
yMin =
|
|
151
|
-
options.yAxisMin !== undefined && options.yAxisMin !== 'auto'
|
|
152
|
-
? options.yAxisMin
|
|
153
|
-
: AxisUtils.createMinFunction(yAxisPaddingPercent);
|
|
154
|
-
yMax =
|
|
155
|
-
options.yAxisMax !== undefined && options.yAxisMax !== 'auto'
|
|
156
|
-
? options.yAxisMax
|
|
157
|
-
: AxisUtils.createMaxFunction(yAxisPaddingPercent);
|
|
158
|
-
} else {
|
|
159
|
-
// Separate panes always use dynamic scaling
|
|
160
|
-
yMin = AxisUtils.createMinFunction(yAxisPaddingPercent);
|
|
161
|
-
yMax = AxisUtils.createMaxFunction(yAxisPaddingPercent);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
yAxis.push({
|
|
165
|
-
position: 'right',
|
|
166
|
-
gridIndex: i,
|
|
167
|
-
show: isTarget,
|
|
168
|
-
scale: true,
|
|
169
|
-
min: yMin,
|
|
170
|
-
max: yMax,
|
|
171
|
-
axisLabel: {
|
|
172
|
-
show: isTarget,
|
|
173
|
-
color: '#94a3b8',
|
|
174
|
-
fontFamily: options.fontFamily,
|
|
175
|
-
formatter: (value: number) => {
|
|
176
|
-
if (options.yAxisLabelFormatter) {
|
|
177
|
-
return options.yAxisLabelFormatter(value);
|
|
178
|
-
}
|
|
179
|
-
const decimals = options.yAxisDecimalPlaces !== undefined
|
|
180
|
-
? options.yAxisDecimalPlaces
|
|
181
|
-
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
182
|
-
return AxisUtils.formatValue(value, decimals);
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
splitLine: {
|
|
186
|
-
show: isTarget,
|
|
187
|
-
lineStyle: { color: '#334155', opacity: 0.5 },
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// Reconstruct Pane Config for GraphicBuilder
|
|
192
|
-
// We need to return `paneLayout` so GraphicBuilder can draw the Restore button
|
|
193
|
-
if (i > 0) {
|
|
194
|
-
// Find indicator for this pane
|
|
195
|
-
const ind = Array.from(indicators.values()).find((ind) => ind.paneIndex === i);
|
|
196
|
-
if (ind) {
|
|
197
|
-
paneConfigs.push({
|
|
198
|
-
index: i,
|
|
199
|
-
height: isTarget ? 90 : 0,
|
|
200
|
-
top: isTarget ? 5 : 0,
|
|
201
|
-
isCollapsed: false,
|
|
202
|
-
indicatorId: ind.id,
|
|
203
|
-
titleColor: ind.titleColor,
|
|
204
|
-
controls: ind.controls,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
grid,
|
|
212
|
-
xAxis,
|
|
213
|
-
yAxis,
|
|
214
|
-
dataZoom,
|
|
215
|
-
paneLayout: paneConfigs,
|
|
216
|
-
mainPaneHeight: maximizeTargetIndex === 0 ? 90 : 0,
|
|
217
|
-
mainPaneTop: maximizeTargetIndex === 0 ? 5 : 0,
|
|
218
|
-
pixelToPercent,
|
|
219
|
-
overlayYAxisMap: new Map(), // No overlays in maximized view
|
|
220
|
-
separatePaneYAxisOffset: 1, // In maximized view, no overlays, so separate panes start at 1
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (dzVisible) {
|
|
225
|
-
if (dzPosition === 'top') {
|
|
226
|
-
// DataZoom takes top 0% to dzHeight%
|
|
227
|
-
// Main chart starts below it with a small gap
|
|
228
|
-
mainPaneTop = dzHeight + 4; // dzHeight + 4% gap
|
|
229
|
-
chartAreaBottom = 95; // Use more space at bottom since slider is gone
|
|
230
|
-
} else {
|
|
231
|
-
// DataZoom takes bottom
|
|
232
|
-
// Chart ends at 100 - dzHeight - margin
|
|
233
|
-
chartAreaBottom = 100 - dzHeight - 2;
|
|
234
|
-
mainPaneTop = 8;
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
// No data zoom
|
|
238
|
-
mainPaneTop = 5;
|
|
239
|
-
chartAreaBottom = 95;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// We need to calculate height distribution dynamically to avoid overlap.
|
|
243
|
-
// Calculate gap in percent
|
|
244
|
-
let gapPercent = 5;
|
|
245
|
-
if (containerHeight > 0) {
|
|
246
|
-
gapPercent = (20 / containerHeight) * 100;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
let mainHeightVal = 75; // Default if no separate pane
|
|
250
|
-
|
|
251
|
-
// Prepare separate panes configuration
|
|
252
|
-
let paneConfigs: PaneConfiguration[] = [];
|
|
253
|
-
|
|
254
|
-
if (hasSeparatePane) {
|
|
255
|
-
// Resolve heights for all separate panes
|
|
256
|
-
// 1. Identify panes and their requested heights
|
|
257
|
-
const panes = separatePaneIndices.map((idx) => {
|
|
258
|
-
const ind = Array.from(indicators.values()).find((i) => i.paneIndex === idx);
|
|
259
|
-
return {
|
|
260
|
-
index: idx,
|
|
261
|
-
requestedHeight: ind?.height,
|
|
262
|
-
isCollapsed: ind?.collapsed ?? false,
|
|
263
|
-
indicatorId: ind?.id,
|
|
264
|
-
titleColor: ind?.titleColor,
|
|
265
|
-
controls: ind?.controls,
|
|
266
|
-
};
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// 2. Assign actual heights
|
|
270
|
-
// If collapsed, use small fixed height (e.g. 3%)
|
|
271
|
-
const resolvedPanes = panes.map((p) => ({
|
|
272
|
-
...p,
|
|
273
|
-
height: p.isCollapsed ? 3 : p.requestedHeight !== undefined ? p.requestedHeight : 15,
|
|
274
|
-
}));
|
|
275
|
-
|
|
276
|
-
// 3. Calculate total space needed for indicators
|
|
277
|
-
const totalIndicatorHeight = resolvedPanes.reduce((sum, p) => sum + p.height, 0);
|
|
278
|
-
const totalGaps = resolvedPanes.length * gapPercent;
|
|
279
|
-
const totalBottomSpace = totalIndicatorHeight + totalGaps;
|
|
280
|
-
|
|
281
|
-
// 4. Calculate Main Chart Height
|
|
282
|
-
// Available space = chartAreaBottom - mainPaneTop;
|
|
283
|
-
const totalAvailable = chartAreaBottom - mainPaneTop;
|
|
284
|
-
mainHeightVal = totalAvailable - totalBottomSpace;
|
|
285
|
-
|
|
286
|
-
if (isMainCollapsed) {
|
|
287
|
-
mainHeightVal = 3;
|
|
288
|
-
} else {
|
|
289
|
-
// Safety check: ensure main chart has at least some space (e.g. 20%)
|
|
290
|
-
if (mainHeightVal < 20) {
|
|
291
|
-
mainHeightVal = Math.max(mainHeightVal, 10);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// 5. Calculate positions
|
|
296
|
-
let currentTop = mainPaneTop + mainHeightVal + gapPercent;
|
|
297
|
-
|
|
298
|
-
paneConfigs = resolvedPanes.map((p) => {
|
|
299
|
-
const config = {
|
|
300
|
-
index: p.index,
|
|
301
|
-
height: p.height,
|
|
302
|
-
top: currentTop,
|
|
303
|
-
isCollapsed: p.isCollapsed,
|
|
304
|
-
indicatorId: p.indicatorId,
|
|
305
|
-
titleColor: p.titleColor,
|
|
306
|
-
controls: p.controls,
|
|
307
|
-
};
|
|
308
|
-
currentTop += p.height + gapPercent;
|
|
309
|
-
return config;
|
|
310
|
-
});
|
|
311
|
-
} else {
|
|
312
|
-
mainHeightVal = chartAreaBottom - mainPaneTop;
|
|
313
|
-
if (isMainCollapsed) {
|
|
314
|
-
mainHeightVal = 3;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// --- Generate Grids ---
|
|
319
|
-
const grid: any[] = [];
|
|
320
|
-
// Main Grid (index 0)
|
|
321
|
-
grid.push({
|
|
322
|
-
left: '10%',
|
|
323
|
-
right: '10%',
|
|
324
|
-
top: mainPaneTop + '%',
|
|
325
|
-
height: mainHeightVal + '%',
|
|
326
|
-
containLabel: false, // We handle margins explicitly
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
// Separate Panes Grids
|
|
330
|
-
paneConfigs.forEach((pane) => {
|
|
331
|
-
grid.push({
|
|
332
|
-
left: '10%',
|
|
333
|
-
right: '10%',
|
|
334
|
-
top: pane.top + '%',
|
|
335
|
-
height: pane.height + '%',
|
|
336
|
-
containLabel: false,
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// --- Generate X-Axes ---
|
|
341
|
-
const allXAxisIndices = [0, ...paneConfigs.map((_, i) => i + 1)];
|
|
342
|
-
const xAxis: any[] = [];
|
|
343
|
-
|
|
344
|
-
// Main X-Axis
|
|
345
|
-
const isMainBottom = paneConfigs.length === 0;
|
|
346
|
-
xAxis.push({
|
|
347
|
-
type: 'category',
|
|
348
|
-
data: [], // Will be filled by SeriesBuilder or QFChart
|
|
349
|
-
gridIndex: 0,
|
|
350
|
-
scale: true,
|
|
351
|
-
// boundaryGap will be set in QFChart.ts based on padding option
|
|
352
|
-
axisLine: {
|
|
353
|
-
onZero: false,
|
|
354
|
-
show: !isMainCollapsed,
|
|
355
|
-
lineStyle: { color: '#334155' },
|
|
356
|
-
},
|
|
357
|
-
splitLine: {
|
|
358
|
-
show: !isMainCollapsed,
|
|
359
|
-
lineStyle: { color: '#334155', opacity: 0.5 },
|
|
360
|
-
},
|
|
361
|
-
axisLabel: {
|
|
362
|
-
show: !isMainCollapsed,
|
|
363
|
-
color: '#94a3b8',
|
|
364
|
-
fontFamily: options.fontFamily || 'sans-serif',
|
|
365
|
-
formatter: (value: number) => {
|
|
366
|
-
if (options.yAxisLabelFormatter) {
|
|
367
|
-
return options.yAxisLabelFormatter(value);
|
|
368
|
-
}
|
|
369
|
-
const decimals = options.yAxisDecimalPlaces !== undefined
|
|
370
|
-
? options.yAxisDecimalPlaces
|
|
371
|
-
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
372
|
-
return AxisUtils.formatValue(value, decimals);
|
|
373
|
-
},
|
|
374
|
-
},
|
|
375
|
-
axisTick: { show: !isMainCollapsed },
|
|
376
|
-
axisPointer: {
|
|
377
|
-
label: {
|
|
378
|
-
show: isMainBottom,
|
|
379
|
-
fontSize: 11,
|
|
380
|
-
backgroundColor: '#475569',
|
|
381
|
-
},
|
|
382
|
-
},
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
// Separate Panes X-Axes
|
|
386
|
-
paneConfigs.forEach((pane, i) => {
|
|
387
|
-
const isBottom = i === paneConfigs.length - 1;
|
|
388
|
-
xAxis.push({
|
|
389
|
-
type: 'category',
|
|
390
|
-
gridIndex: i + 1, // 0 is main
|
|
391
|
-
data: [], // Shared data
|
|
392
|
-
axisLabel: { show: false }, // Hide labels on indicator panes
|
|
393
|
-
axisLine: { show: !pane.isCollapsed, lineStyle: { color: '#334155' } },
|
|
394
|
-
axisTick: { show: false },
|
|
395
|
-
splitLine: { show: false },
|
|
396
|
-
axisPointer: {
|
|
397
|
-
label: {
|
|
398
|
-
show: isBottom,
|
|
399
|
-
fontSize: 11,
|
|
400
|
-
backgroundColor: '#475569',
|
|
401
|
-
},
|
|
402
|
-
},
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// --- Generate Y-Axes ---
|
|
407
|
-
const yAxis: any[] = [];
|
|
408
|
-
|
|
409
|
-
// Determine min/max for main Y-axis (respect custom values if provided)
|
|
410
|
-
let mainYAxisMin: any;
|
|
411
|
-
let mainYAxisMax: any;
|
|
412
|
-
|
|
413
|
-
if (options.yAxisMin !== undefined && options.yAxisMin !== 'auto') {
|
|
414
|
-
mainYAxisMin = options.yAxisMin;
|
|
415
|
-
} else {
|
|
416
|
-
mainYAxisMin = AxisUtils.createMinFunction(yAxisPaddingPercent);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (options.yAxisMax !== undefined && options.yAxisMax !== 'auto') {
|
|
420
|
-
mainYAxisMax = options.yAxisMax;
|
|
421
|
-
} else {
|
|
422
|
-
mainYAxisMax = AxisUtils.createMaxFunction(yAxisPaddingPercent);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Main Y-Axis (for candlesticks)
|
|
426
|
-
yAxis.push({
|
|
427
|
-
position: 'right',
|
|
428
|
-
scale: true,
|
|
429
|
-
min: mainYAxisMin,
|
|
430
|
-
max: mainYAxisMax,
|
|
431
|
-
gridIndex: 0,
|
|
432
|
-
splitLine: {
|
|
433
|
-
show: !isMainCollapsed,
|
|
434
|
-
lineStyle: { color: '#334155', opacity: 0.5 },
|
|
435
|
-
},
|
|
436
|
-
axisLine: { show: !isMainCollapsed, lineStyle: { color: '#334155' } },
|
|
437
|
-
axisLabel: {
|
|
438
|
-
show: !isMainCollapsed,
|
|
439
|
-
color: '#94a3b8',
|
|
440
|
-
fontFamily: options.fontFamily || 'sans-serif',
|
|
441
|
-
formatter: (value: number) => {
|
|
442
|
-
if (options.yAxisLabelFormatter) {
|
|
443
|
-
return options.yAxisLabelFormatter(value);
|
|
444
|
-
}
|
|
445
|
-
const decimals = options.yAxisDecimalPlaces !== undefined
|
|
446
|
-
? options.yAxisDecimalPlaces
|
|
447
|
-
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
448
|
-
return AxisUtils.formatValue(value, decimals);
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
// Create separate Y-axes for overlay plots that are incompatible with price range
|
|
454
|
-
// Analyze each PLOT separately, not entire indicators
|
|
455
|
-
let nextYAxisIndex = 1;
|
|
456
|
-
|
|
457
|
-
// Calculate price range if market data is available
|
|
458
|
-
let priceMin = -Infinity;
|
|
459
|
-
let priceMax = Infinity;
|
|
460
|
-
if (marketData && marketData.length > 0) {
|
|
461
|
-
priceMin = Math.min(...marketData.map((d) => d.low));
|
|
462
|
-
priceMax = Math.max(...marketData.map((d) => d.high));
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Map to store plot-specific Y-axis assignments (key: "indicatorId::plotName")
|
|
466
|
-
const overlayYAxisMap: Map<string, number> = new Map();
|
|
467
|
-
|
|
468
|
-
indicators.forEach((indicator, id) => {
|
|
469
|
-
if (indicator.paneIndex === 0 && !indicator.collapsed) {
|
|
470
|
-
// This is an overlay on the main pane
|
|
471
|
-
// Analyze EACH PLOT separately
|
|
472
|
-
|
|
473
|
-
if (marketData && marketData.length > 0) {
|
|
474
|
-
Object.entries(indicator.plots).forEach(([plotName, plot]) => {
|
|
475
|
-
const plotKey = `${id}::${plotName}`;
|
|
476
|
-
|
|
477
|
-
// Skip visual-only plot types that should never affect Y-axis scaling
|
|
478
|
-
// EXCEPTION: shapes with abovebar/belowbar must stay on main Y-axis
|
|
479
|
-
const visualOnlyStyles = ['background', 'barcolor', 'char'];
|
|
480
|
-
|
|
481
|
-
// Check if this is a shape with price-relative positioning
|
|
482
|
-
const isShapeWithPriceLocation =
|
|
483
|
-
plot.options.style === 'shape' && (plot.options.location === 'abovebar' || plot.options.location === 'belowbar');
|
|
484
|
-
|
|
485
|
-
if (visualOnlyStyles.includes(plot.options.style)) {
|
|
486
|
-
// Assign these to a separate Y-axis so they don't affect price scale
|
|
487
|
-
if (!overlayYAxisMap.has(plotKey)) {
|
|
488
|
-
overlayYAxisMap.set(plotKey, nextYAxisIndex);
|
|
489
|
-
nextYAxisIndex++;
|
|
490
|
-
}
|
|
491
|
-
return; // Skip further processing for this plot
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// If it's a shape but NOT with price-relative positioning, treat as visual-only
|
|
495
|
-
if (plot.options.style === 'shape' && !isShapeWithPriceLocation) {
|
|
496
|
-
if (!overlayYAxisMap.has(plotKey)) {
|
|
497
|
-
overlayYAxisMap.set(plotKey, nextYAxisIndex);
|
|
498
|
-
nextYAxisIndex++;
|
|
499
|
-
}
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const values: number[] = [];
|
|
504
|
-
|
|
505
|
-
// Extract values for this specific plot
|
|
506
|
-
if (plot.data) {
|
|
507
|
-
Object.values(plot.data).forEach((value) => {
|
|
508
|
-
if (typeof value === 'number' && !isNaN(value) && isFinite(value)) {
|
|
509
|
-
values.push(value);
|
|
510
|
-
}
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (values.length > 0) {
|
|
515
|
-
const plotMin = Math.min(...values);
|
|
516
|
-
const plotMax = Math.max(...values);
|
|
517
|
-
const plotRange = plotMax - plotMin;
|
|
518
|
-
const priceRange = priceMax - priceMin;
|
|
519
|
-
|
|
520
|
-
// Check if this plot's range is compatible with price range
|
|
521
|
-
// Compatible = within price bounds with similar magnitude
|
|
522
|
-
const isWithinBounds = plotMin >= priceMin * 0.5 && plotMax <= priceMax * 1.5;
|
|
523
|
-
const hasSimilarMagnitude = plotRange > priceRange * 0.01; // At least 1% of price range
|
|
524
|
-
|
|
525
|
-
const isCompatible = isWithinBounds && hasSimilarMagnitude;
|
|
526
|
-
|
|
527
|
-
if (!isCompatible) {
|
|
528
|
-
// This plot needs its own Y-axis - check if we already assigned one
|
|
529
|
-
if (!overlayYAxisMap.has(plotKey)) {
|
|
530
|
-
overlayYAxisMap.set(plotKey, nextYAxisIndex);
|
|
531
|
-
nextYAxisIndex++;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
// Compatible plots stay on yAxisIndex: 0 (not added to map)
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
// Create Y-axes for incompatible plots
|
|
542
|
-
// nextYAxisIndex already incremented in the loop above, so we know how many axes we need
|
|
543
|
-
const numOverlayAxes = overlayYAxisMap.size > 0 ? nextYAxisIndex - 1 : 0;
|
|
544
|
-
|
|
545
|
-
// Track which overlay axes are for visual-only plots (background, barcolor, etc.)
|
|
546
|
-
const visualOnlyAxes = new Set<number>();
|
|
547
|
-
overlayYAxisMap.forEach((yAxisIdx, plotKey) => {
|
|
548
|
-
// Check if this plot is visual-only by looking at the original indicator
|
|
549
|
-
indicators.forEach((indicator) => {
|
|
550
|
-
Object.entries(indicator.plots).forEach(([plotName, plot]) => {
|
|
551
|
-
const key = `${indicator.id}::${plotName}`;
|
|
552
|
-
if (key === plotKey && ['background', 'barcolor', 'char'].includes(plot.options.style)) {
|
|
553
|
-
visualOnlyAxes.add(yAxisIdx);
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
});
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
for (let i = 0; i < numOverlayAxes; i++) {
|
|
560
|
-
const yAxisIndex = i + 1; // Y-axis indices start at 1 for overlays
|
|
561
|
-
const isVisualOnly = visualOnlyAxes.has(yAxisIndex);
|
|
562
|
-
|
|
563
|
-
yAxis.push({
|
|
564
|
-
position: 'left',
|
|
565
|
-
scale: !isVisualOnly, // Disable scaling for visual-only plots
|
|
566
|
-
min: isVisualOnly ? 0 : AxisUtils.createMinFunction(yAxisPaddingPercent), // Fixed range for visual plots
|
|
567
|
-
max: isVisualOnly ? 1 : AxisUtils.createMaxFunction(yAxisPaddingPercent), // Fixed range for visual plots
|
|
568
|
-
gridIndex: 0,
|
|
569
|
-
show: false, // Hide the axis visual elements
|
|
570
|
-
splitLine: { show: false },
|
|
571
|
-
axisLine: { show: false },
|
|
572
|
-
axisLabel: { show: false },
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// Separate Panes Y-Axes (start after overlay axes)
|
|
577
|
-
const separatePaneYAxisOffset = nextYAxisIndex;
|
|
578
|
-
paneConfigs.forEach((pane, i) => {
|
|
579
|
-
yAxis.push({
|
|
580
|
-
position: 'right',
|
|
581
|
-
scale: true,
|
|
582
|
-
min: AxisUtils.createMinFunction(yAxisPaddingPercent),
|
|
583
|
-
max: AxisUtils.createMaxFunction(yAxisPaddingPercent),
|
|
584
|
-
gridIndex: i + 1,
|
|
585
|
-
splitLine: {
|
|
586
|
-
show: !pane.isCollapsed,
|
|
587
|
-
lineStyle: { color: '#334155', opacity: 0.3 },
|
|
588
|
-
},
|
|
589
|
-
axisLabel: {
|
|
590
|
-
show: !pane.isCollapsed,
|
|
591
|
-
color: '#94a3b8',
|
|
592
|
-
fontFamily: options.fontFamily || 'sans-serif',
|
|
593
|
-
fontSize: 10,
|
|
594
|
-
formatter: (value: number) => {
|
|
595
|
-
if (options.yAxisLabelFormatter) {
|
|
596
|
-
return options.yAxisLabelFormatter(value);
|
|
597
|
-
}
|
|
598
|
-
const decimals = options.yAxisDecimalPlaces !== undefined
|
|
599
|
-
? options.yAxisDecimalPlaces
|
|
600
|
-
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
601
|
-
return AxisUtils.formatValue(value, decimals);
|
|
602
|
-
},
|
|
603
|
-
},
|
|
604
|
-
axisLine: { show: !pane.isCollapsed, lineStyle: { color: '#334155' } },
|
|
605
|
-
});
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
// --- Generate DataZoom ---
|
|
609
|
-
const dataZoom: any[] = [];
|
|
610
|
-
if (dzVisible) {
|
|
611
|
-
// Add 'inside' zoom (pan/drag) only if zoomOnTouch is enabled (default true)
|
|
612
|
-
const zoomOnTouch = options.dataZoom?.zoomOnTouch ?? true;
|
|
613
|
-
if (zoomOnTouch) {
|
|
614
|
-
dataZoom.push({
|
|
615
|
-
type: 'inside',
|
|
616
|
-
xAxisIndex: allXAxisIndices,
|
|
617
|
-
start: dzStart,
|
|
618
|
-
end: dzEnd,
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1
|
+
import { QFChartOptions, Indicator as IndicatorType, OHLCV } from '../types';
|
|
2
|
+
import { AxisUtils } from '../utils/AxisUtils';
|
|
3
|
+
|
|
4
|
+
export interface PaneConfiguration {
|
|
5
|
+
index: number;
|
|
6
|
+
height: number;
|
|
7
|
+
top: number;
|
|
8
|
+
isCollapsed: boolean;
|
|
9
|
+
indicatorId?: string;
|
|
10
|
+
titleColor?: string;
|
|
11
|
+
controls?: {
|
|
12
|
+
collapse?: boolean;
|
|
13
|
+
maximize?: boolean;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LayoutResult {
|
|
18
|
+
grid: any[];
|
|
19
|
+
xAxis: any[];
|
|
20
|
+
yAxis: any[];
|
|
21
|
+
dataZoom: any[];
|
|
22
|
+
paneLayout: PaneConfiguration[];
|
|
23
|
+
mainPaneHeight: number;
|
|
24
|
+
mainPaneTop: number;
|
|
25
|
+
pixelToPercent: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class LayoutManager {
|
|
29
|
+
public static calculate(
|
|
30
|
+
containerHeight: number,
|
|
31
|
+
indicators: Map<string, IndicatorType>,
|
|
32
|
+
options: QFChartOptions,
|
|
33
|
+
isMainCollapsed: boolean = false,
|
|
34
|
+
maximizedPaneId: string | null = null,
|
|
35
|
+
marketData?: import('../types').OHLCV[]
|
|
36
|
+
): LayoutResult & { overlayYAxisMap: Map<string, number>; separatePaneYAxisOffset: number } {
|
|
37
|
+
// Calculate pixelToPercent early for maximized logic
|
|
38
|
+
let pixelToPercent = 0;
|
|
39
|
+
if (containerHeight > 0) {
|
|
40
|
+
pixelToPercent = (1 / containerHeight) * 100;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Get Y-axis padding percentage (default 5%)
|
|
44
|
+
const yAxisPaddingPercent = options.yAxisPadding !== undefined ? options.yAxisPadding : 5;
|
|
45
|
+
|
|
46
|
+
// Identify unique separate panes (indices > 0) and sort them
|
|
47
|
+
const separatePaneIndices = Array.from(indicators.values())
|
|
48
|
+
.map((ind) => ind.paneIndex)
|
|
49
|
+
.filter((idx) => idx > 0)
|
|
50
|
+
.sort((a, b) => a - b)
|
|
51
|
+
.filter((value, index, self) => self.indexOf(value) === index); // Unique
|
|
52
|
+
|
|
53
|
+
const hasSeparatePane = separatePaneIndices.length > 0;
|
|
54
|
+
|
|
55
|
+
// DataZoom Configuration
|
|
56
|
+
const dzVisible = options.dataZoom?.visible ?? true;
|
|
57
|
+
const dzPosition = options.dataZoom?.position ?? 'top';
|
|
58
|
+
const dzHeight = options.dataZoom?.height ?? 6;
|
|
59
|
+
const dzStart = options.dataZoom?.start ?? 0;
|
|
60
|
+
const dzEnd = options.dataZoom?.end ?? 100;
|
|
61
|
+
|
|
62
|
+
// Layout Calculation
|
|
63
|
+
let mainPaneTop = 8;
|
|
64
|
+
let chartAreaBottom = 92; // Default if no dataZoom at bottom
|
|
65
|
+
|
|
66
|
+
// Maximized State Logic
|
|
67
|
+
let maximizeTargetIndex = -1; // -1 = none
|
|
68
|
+
|
|
69
|
+
if (maximizedPaneId) {
|
|
70
|
+
if (maximizedPaneId === 'main') {
|
|
71
|
+
maximizeTargetIndex = 0;
|
|
72
|
+
} else {
|
|
73
|
+
const ind = indicators.get(maximizedPaneId);
|
|
74
|
+
if (ind) {
|
|
75
|
+
maximizeTargetIndex = ind.paneIndex;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (maximizeTargetIndex !== -1) {
|
|
81
|
+
// Special Layout for Maximize
|
|
82
|
+
// We must generate grid/axis definitions for ALL indices to maintain series mapping,
|
|
83
|
+
// but hide the non-maximized ones.
|
|
84
|
+
|
|
85
|
+
const grid: any[] = [];
|
|
86
|
+
const xAxis: any[] = [];
|
|
87
|
+
const yAxis: any[] = [];
|
|
88
|
+
const dataZoom: any[] = []; // Hide slider, keep inside?
|
|
89
|
+
|
|
90
|
+
// DataZoom: keep inside, maybe slider if main?
|
|
91
|
+
// Let's keep strict maximize: Full container.
|
|
92
|
+
// Use defaults for maximize if not available, or preserve logic?
|
|
93
|
+
// The calculateMaximized doesn't use LayoutManager.calculate directly but inline logic.
|
|
94
|
+
// It should probably respect the same zoom?
|
|
95
|
+
// But here we are inside LayoutManager.calculate.
|
|
96
|
+
|
|
97
|
+
const dzStart = options.dataZoom?.start ?? 50;
|
|
98
|
+
const dzEnd = options.dataZoom?.end ?? 100;
|
|
99
|
+
|
|
100
|
+
// Add 'inside' zoom only if zoomOnTouch is enabled (default true)
|
|
101
|
+
const zoomOnTouch = options.dataZoom?.zoomOnTouch ?? true;
|
|
102
|
+
if (zoomOnTouch) {
|
|
103
|
+
dataZoom.push({ type: 'inside', xAxisIndex: 'all', start: dzStart, end: dzEnd, filterMode: 'weakFilter' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Need to know total panes to iterate
|
|
107
|
+
const maxPaneIndex = hasSeparatePane ? Math.max(...separatePaneIndices) : 0;
|
|
108
|
+
|
|
109
|
+
const paneConfigs: PaneConfiguration[] = []; // For GraphicBuilder title placement
|
|
110
|
+
|
|
111
|
+
// Iterate 0 to maxPaneIndex
|
|
112
|
+
for (let i = 0; i <= maxPaneIndex; i++) {
|
|
113
|
+
const isTarget = i === maximizeTargetIndex;
|
|
114
|
+
|
|
115
|
+
// Grid
|
|
116
|
+
grid.push({
|
|
117
|
+
left: '10%',
|
|
118
|
+
right: '10%',
|
|
119
|
+
top: isTarget ? '5%' : '0%',
|
|
120
|
+
height: isTarget ? '90%' : '0%',
|
|
121
|
+
show: isTarget,
|
|
122
|
+
containLabel: false,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// X-Axis
|
|
126
|
+
xAxis.push({
|
|
127
|
+
type: 'category',
|
|
128
|
+
gridIndex: i,
|
|
129
|
+
data: [],
|
|
130
|
+
show: isTarget,
|
|
131
|
+
axisLabel: {
|
|
132
|
+
show: isTarget,
|
|
133
|
+
color: '#94a3b8',
|
|
134
|
+
fontFamily: options.fontFamily,
|
|
135
|
+
},
|
|
136
|
+
axisLine: { show: isTarget, lineStyle: { color: '#334155' } },
|
|
137
|
+
splitLine: {
|
|
138
|
+
show: isTarget,
|
|
139
|
+
lineStyle: { color: '#334155', opacity: 0.5 },
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Y-Axis
|
|
144
|
+
// For maximized pane 0 (main), respect custom min/max if provided
|
|
145
|
+
let yMin: any;
|
|
146
|
+
let yMax: any;
|
|
147
|
+
|
|
148
|
+
if (i === 0 && maximizeTargetIndex === 0) {
|
|
149
|
+
// Main pane is maximized, use custom values if provided
|
|
150
|
+
yMin =
|
|
151
|
+
options.yAxisMin !== undefined && options.yAxisMin !== 'auto'
|
|
152
|
+
? options.yAxisMin
|
|
153
|
+
: AxisUtils.createMinFunction(yAxisPaddingPercent);
|
|
154
|
+
yMax =
|
|
155
|
+
options.yAxisMax !== undefined && options.yAxisMax !== 'auto'
|
|
156
|
+
? options.yAxisMax
|
|
157
|
+
: AxisUtils.createMaxFunction(yAxisPaddingPercent);
|
|
158
|
+
} else {
|
|
159
|
+
// Separate panes always use dynamic scaling
|
|
160
|
+
yMin = AxisUtils.createMinFunction(yAxisPaddingPercent);
|
|
161
|
+
yMax = AxisUtils.createMaxFunction(yAxisPaddingPercent);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
yAxis.push({
|
|
165
|
+
position: 'right',
|
|
166
|
+
gridIndex: i,
|
|
167
|
+
show: isTarget,
|
|
168
|
+
scale: true,
|
|
169
|
+
min: yMin,
|
|
170
|
+
max: yMax,
|
|
171
|
+
axisLabel: {
|
|
172
|
+
show: isTarget,
|
|
173
|
+
color: '#94a3b8',
|
|
174
|
+
fontFamily: options.fontFamily,
|
|
175
|
+
formatter: (value: number) => {
|
|
176
|
+
if (options.yAxisLabelFormatter) {
|
|
177
|
+
return options.yAxisLabelFormatter(value);
|
|
178
|
+
}
|
|
179
|
+
const decimals = options.yAxisDecimalPlaces !== undefined
|
|
180
|
+
? options.yAxisDecimalPlaces
|
|
181
|
+
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
182
|
+
return AxisUtils.formatValue(value, decimals);
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
splitLine: {
|
|
186
|
+
show: isTarget,
|
|
187
|
+
lineStyle: { color: '#334155', opacity: 0.5 },
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Reconstruct Pane Config for GraphicBuilder
|
|
192
|
+
// We need to return `paneLayout` so GraphicBuilder can draw the Restore button
|
|
193
|
+
if (i > 0) {
|
|
194
|
+
// Find indicator for this pane
|
|
195
|
+
const ind = Array.from(indicators.values()).find((ind) => ind.paneIndex === i);
|
|
196
|
+
if (ind) {
|
|
197
|
+
paneConfigs.push({
|
|
198
|
+
index: i,
|
|
199
|
+
height: isTarget ? 90 : 0,
|
|
200
|
+
top: isTarget ? 5 : 0,
|
|
201
|
+
isCollapsed: false,
|
|
202
|
+
indicatorId: ind.id,
|
|
203
|
+
titleColor: ind.titleColor,
|
|
204
|
+
controls: ind.controls,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
grid,
|
|
212
|
+
xAxis,
|
|
213
|
+
yAxis,
|
|
214
|
+
dataZoom,
|
|
215
|
+
paneLayout: paneConfigs,
|
|
216
|
+
mainPaneHeight: maximizeTargetIndex === 0 ? 90 : 0,
|
|
217
|
+
mainPaneTop: maximizeTargetIndex === 0 ? 5 : 0,
|
|
218
|
+
pixelToPercent,
|
|
219
|
+
overlayYAxisMap: new Map(), // No overlays in maximized view
|
|
220
|
+
separatePaneYAxisOffset: 1, // In maximized view, no overlays, so separate panes start at 1
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (dzVisible) {
|
|
225
|
+
if (dzPosition === 'top') {
|
|
226
|
+
// DataZoom takes top 0% to dzHeight%
|
|
227
|
+
// Main chart starts below it with a small gap
|
|
228
|
+
mainPaneTop = dzHeight + 4; // dzHeight + 4% gap
|
|
229
|
+
chartAreaBottom = 95; // Use more space at bottom since slider is gone
|
|
230
|
+
} else {
|
|
231
|
+
// DataZoom takes bottom
|
|
232
|
+
// Chart ends at 100 - dzHeight - margin
|
|
233
|
+
chartAreaBottom = 100 - dzHeight - 2;
|
|
234
|
+
mainPaneTop = 8;
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
// No data zoom
|
|
238
|
+
mainPaneTop = 5;
|
|
239
|
+
chartAreaBottom = 95;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// We need to calculate height distribution dynamically to avoid overlap.
|
|
243
|
+
// Calculate gap in percent
|
|
244
|
+
let gapPercent = 5;
|
|
245
|
+
if (containerHeight > 0) {
|
|
246
|
+
gapPercent = (20 / containerHeight) * 100;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let mainHeightVal = 75; // Default if no separate pane
|
|
250
|
+
|
|
251
|
+
// Prepare separate panes configuration
|
|
252
|
+
let paneConfigs: PaneConfiguration[] = [];
|
|
253
|
+
|
|
254
|
+
if (hasSeparatePane) {
|
|
255
|
+
// Resolve heights for all separate panes
|
|
256
|
+
// 1. Identify panes and their requested heights
|
|
257
|
+
const panes = separatePaneIndices.map((idx) => {
|
|
258
|
+
const ind = Array.from(indicators.values()).find((i) => i.paneIndex === idx);
|
|
259
|
+
return {
|
|
260
|
+
index: idx,
|
|
261
|
+
requestedHeight: ind?.height,
|
|
262
|
+
isCollapsed: ind?.collapsed ?? false,
|
|
263
|
+
indicatorId: ind?.id,
|
|
264
|
+
titleColor: ind?.titleColor,
|
|
265
|
+
controls: ind?.controls,
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// 2. Assign actual heights
|
|
270
|
+
// If collapsed, use small fixed height (e.g. 3%)
|
|
271
|
+
const resolvedPanes = panes.map((p) => ({
|
|
272
|
+
...p,
|
|
273
|
+
height: p.isCollapsed ? 3 : p.requestedHeight !== undefined ? p.requestedHeight : 15,
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
// 3. Calculate total space needed for indicators
|
|
277
|
+
const totalIndicatorHeight = resolvedPanes.reduce((sum, p) => sum + p.height, 0);
|
|
278
|
+
const totalGaps = resolvedPanes.length * gapPercent;
|
|
279
|
+
const totalBottomSpace = totalIndicatorHeight + totalGaps;
|
|
280
|
+
|
|
281
|
+
// 4. Calculate Main Chart Height
|
|
282
|
+
// Available space = chartAreaBottom - mainPaneTop;
|
|
283
|
+
const totalAvailable = chartAreaBottom - mainPaneTop;
|
|
284
|
+
mainHeightVal = totalAvailable - totalBottomSpace;
|
|
285
|
+
|
|
286
|
+
if (isMainCollapsed) {
|
|
287
|
+
mainHeightVal = 3;
|
|
288
|
+
} else {
|
|
289
|
+
// Safety check: ensure main chart has at least some space (e.g. 20%)
|
|
290
|
+
if (mainHeightVal < 20) {
|
|
291
|
+
mainHeightVal = Math.max(mainHeightVal, 10);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 5. Calculate positions
|
|
296
|
+
let currentTop = mainPaneTop + mainHeightVal + gapPercent;
|
|
297
|
+
|
|
298
|
+
paneConfigs = resolvedPanes.map((p) => {
|
|
299
|
+
const config = {
|
|
300
|
+
index: p.index,
|
|
301
|
+
height: p.height,
|
|
302
|
+
top: currentTop,
|
|
303
|
+
isCollapsed: p.isCollapsed,
|
|
304
|
+
indicatorId: p.indicatorId,
|
|
305
|
+
titleColor: p.titleColor,
|
|
306
|
+
controls: p.controls,
|
|
307
|
+
};
|
|
308
|
+
currentTop += p.height + gapPercent;
|
|
309
|
+
return config;
|
|
310
|
+
});
|
|
311
|
+
} else {
|
|
312
|
+
mainHeightVal = chartAreaBottom - mainPaneTop;
|
|
313
|
+
if (isMainCollapsed) {
|
|
314
|
+
mainHeightVal = 3;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// --- Generate Grids ---
|
|
319
|
+
const grid: any[] = [];
|
|
320
|
+
// Main Grid (index 0)
|
|
321
|
+
grid.push({
|
|
322
|
+
left: '10%',
|
|
323
|
+
right: '10%',
|
|
324
|
+
top: mainPaneTop + '%',
|
|
325
|
+
height: mainHeightVal + '%',
|
|
326
|
+
containLabel: false, // We handle margins explicitly
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Separate Panes Grids
|
|
330
|
+
paneConfigs.forEach((pane) => {
|
|
331
|
+
grid.push({
|
|
332
|
+
left: '10%',
|
|
333
|
+
right: '10%',
|
|
334
|
+
top: pane.top + '%',
|
|
335
|
+
height: pane.height + '%',
|
|
336
|
+
containLabel: false,
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// --- Generate X-Axes ---
|
|
341
|
+
const allXAxisIndices = [0, ...paneConfigs.map((_, i) => i + 1)];
|
|
342
|
+
const xAxis: any[] = [];
|
|
343
|
+
|
|
344
|
+
// Main X-Axis
|
|
345
|
+
const isMainBottom = paneConfigs.length === 0;
|
|
346
|
+
xAxis.push({
|
|
347
|
+
type: 'category',
|
|
348
|
+
data: [], // Will be filled by SeriesBuilder or QFChart
|
|
349
|
+
gridIndex: 0,
|
|
350
|
+
scale: true,
|
|
351
|
+
// boundaryGap will be set in QFChart.ts based on padding option
|
|
352
|
+
axisLine: {
|
|
353
|
+
onZero: false,
|
|
354
|
+
show: !isMainCollapsed,
|
|
355
|
+
lineStyle: { color: '#334155' },
|
|
356
|
+
},
|
|
357
|
+
splitLine: {
|
|
358
|
+
show: !isMainCollapsed,
|
|
359
|
+
lineStyle: { color: '#334155', opacity: 0.5 },
|
|
360
|
+
},
|
|
361
|
+
axisLabel: {
|
|
362
|
+
show: !isMainCollapsed,
|
|
363
|
+
color: '#94a3b8',
|
|
364
|
+
fontFamily: options.fontFamily || 'sans-serif',
|
|
365
|
+
formatter: (value: number) => {
|
|
366
|
+
if (options.yAxisLabelFormatter) {
|
|
367
|
+
return options.yAxisLabelFormatter(value);
|
|
368
|
+
}
|
|
369
|
+
const decimals = options.yAxisDecimalPlaces !== undefined
|
|
370
|
+
? options.yAxisDecimalPlaces
|
|
371
|
+
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
372
|
+
return AxisUtils.formatValue(value, decimals);
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
axisTick: { show: !isMainCollapsed },
|
|
376
|
+
axisPointer: {
|
|
377
|
+
label: {
|
|
378
|
+
show: isMainBottom,
|
|
379
|
+
fontSize: 11,
|
|
380
|
+
backgroundColor: '#475569',
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Separate Panes X-Axes
|
|
386
|
+
paneConfigs.forEach((pane, i) => {
|
|
387
|
+
const isBottom = i === paneConfigs.length - 1;
|
|
388
|
+
xAxis.push({
|
|
389
|
+
type: 'category',
|
|
390
|
+
gridIndex: i + 1, // 0 is main
|
|
391
|
+
data: [], // Shared data
|
|
392
|
+
axisLabel: { show: false }, // Hide labels on indicator panes
|
|
393
|
+
axisLine: { show: !pane.isCollapsed, lineStyle: { color: '#334155' } },
|
|
394
|
+
axisTick: { show: false },
|
|
395
|
+
splitLine: { show: false },
|
|
396
|
+
axisPointer: {
|
|
397
|
+
label: {
|
|
398
|
+
show: isBottom,
|
|
399
|
+
fontSize: 11,
|
|
400
|
+
backgroundColor: '#475569',
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// --- Generate Y-Axes ---
|
|
407
|
+
const yAxis: any[] = [];
|
|
408
|
+
|
|
409
|
+
// Determine min/max for main Y-axis (respect custom values if provided)
|
|
410
|
+
let mainYAxisMin: any;
|
|
411
|
+
let mainYAxisMax: any;
|
|
412
|
+
|
|
413
|
+
if (options.yAxisMin !== undefined && options.yAxisMin !== 'auto') {
|
|
414
|
+
mainYAxisMin = options.yAxisMin;
|
|
415
|
+
} else {
|
|
416
|
+
mainYAxisMin = AxisUtils.createMinFunction(yAxisPaddingPercent);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (options.yAxisMax !== undefined && options.yAxisMax !== 'auto') {
|
|
420
|
+
mainYAxisMax = options.yAxisMax;
|
|
421
|
+
} else {
|
|
422
|
+
mainYAxisMax = AxisUtils.createMaxFunction(yAxisPaddingPercent);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Main Y-Axis (for candlesticks)
|
|
426
|
+
yAxis.push({
|
|
427
|
+
position: 'right',
|
|
428
|
+
scale: true,
|
|
429
|
+
min: mainYAxisMin,
|
|
430
|
+
max: mainYAxisMax,
|
|
431
|
+
gridIndex: 0,
|
|
432
|
+
splitLine: {
|
|
433
|
+
show: !isMainCollapsed,
|
|
434
|
+
lineStyle: { color: '#334155', opacity: 0.5 },
|
|
435
|
+
},
|
|
436
|
+
axisLine: { show: !isMainCollapsed, lineStyle: { color: '#334155' } },
|
|
437
|
+
axisLabel: {
|
|
438
|
+
show: !isMainCollapsed,
|
|
439
|
+
color: '#94a3b8',
|
|
440
|
+
fontFamily: options.fontFamily || 'sans-serif',
|
|
441
|
+
formatter: (value: number) => {
|
|
442
|
+
if (options.yAxisLabelFormatter) {
|
|
443
|
+
return options.yAxisLabelFormatter(value);
|
|
444
|
+
}
|
|
445
|
+
const decimals = options.yAxisDecimalPlaces !== undefined
|
|
446
|
+
? options.yAxisDecimalPlaces
|
|
447
|
+
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
448
|
+
return AxisUtils.formatValue(value, decimals);
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Create separate Y-axes for overlay plots that are incompatible with price range
|
|
454
|
+
// Analyze each PLOT separately, not entire indicators
|
|
455
|
+
let nextYAxisIndex = 1;
|
|
456
|
+
|
|
457
|
+
// Calculate price range if market data is available
|
|
458
|
+
let priceMin = -Infinity;
|
|
459
|
+
let priceMax = Infinity;
|
|
460
|
+
if (marketData && marketData.length > 0) {
|
|
461
|
+
priceMin = Math.min(...marketData.map((d) => d.low));
|
|
462
|
+
priceMax = Math.max(...marketData.map((d) => d.high));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Map to store plot-specific Y-axis assignments (key: "indicatorId::plotName")
|
|
466
|
+
const overlayYAxisMap: Map<string, number> = new Map();
|
|
467
|
+
|
|
468
|
+
indicators.forEach((indicator, id) => {
|
|
469
|
+
if (indicator.paneIndex === 0 && !indicator.collapsed) {
|
|
470
|
+
// This is an overlay on the main pane
|
|
471
|
+
// Analyze EACH PLOT separately
|
|
472
|
+
|
|
473
|
+
if (marketData && marketData.length > 0) {
|
|
474
|
+
Object.entries(indicator.plots).forEach(([plotName, plot]) => {
|
|
475
|
+
const plotKey = `${id}::${plotName}`;
|
|
476
|
+
|
|
477
|
+
// Skip visual-only plot types that should never affect Y-axis scaling
|
|
478
|
+
// EXCEPTION: shapes with abovebar/belowbar must stay on main Y-axis
|
|
479
|
+
const visualOnlyStyles = ['background', 'barcolor', 'char'];
|
|
480
|
+
|
|
481
|
+
// Check if this is a shape with price-relative positioning
|
|
482
|
+
const isShapeWithPriceLocation =
|
|
483
|
+
plot.options.style === 'shape' && (plot.options.location === 'abovebar' || plot.options.location === 'AboveBar' || plot.options.location === 'belowbar' || plot.options.location === 'BelowBar');
|
|
484
|
+
|
|
485
|
+
if (visualOnlyStyles.includes(plot.options.style)) {
|
|
486
|
+
// Assign these to a separate Y-axis so they don't affect price scale
|
|
487
|
+
if (!overlayYAxisMap.has(plotKey)) {
|
|
488
|
+
overlayYAxisMap.set(plotKey, nextYAxisIndex);
|
|
489
|
+
nextYAxisIndex++;
|
|
490
|
+
}
|
|
491
|
+
return; // Skip further processing for this plot
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// If it's a shape but NOT with price-relative positioning, treat as visual-only
|
|
495
|
+
if (plot.options.style === 'shape' && !isShapeWithPriceLocation) {
|
|
496
|
+
if (!overlayYAxisMap.has(plotKey)) {
|
|
497
|
+
overlayYAxisMap.set(plotKey, nextYAxisIndex);
|
|
498
|
+
nextYAxisIndex++;
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const values: number[] = [];
|
|
504
|
+
|
|
505
|
+
// Extract values for this specific plot
|
|
506
|
+
if (plot.data) {
|
|
507
|
+
Object.values(plot.data).forEach((value) => {
|
|
508
|
+
if (typeof value === 'number' && !isNaN(value) && isFinite(value)) {
|
|
509
|
+
values.push(value);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (values.length > 0) {
|
|
515
|
+
const plotMin = Math.min(...values);
|
|
516
|
+
const plotMax = Math.max(...values);
|
|
517
|
+
const plotRange = plotMax - plotMin;
|
|
518
|
+
const priceRange = priceMax - priceMin;
|
|
519
|
+
|
|
520
|
+
// Check if this plot's range is compatible with price range
|
|
521
|
+
// Compatible = within price bounds with similar magnitude
|
|
522
|
+
const isWithinBounds = plotMin >= priceMin * 0.5 && plotMax <= priceMax * 1.5;
|
|
523
|
+
const hasSimilarMagnitude = plotRange > priceRange * 0.01; // At least 1% of price range
|
|
524
|
+
|
|
525
|
+
const isCompatible = isWithinBounds && hasSimilarMagnitude;
|
|
526
|
+
|
|
527
|
+
if (!isCompatible) {
|
|
528
|
+
// This plot needs its own Y-axis - check if we already assigned one
|
|
529
|
+
if (!overlayYAxisMap.has(plotKey)) {
|
|
530
|
+
overlayYAxisMap.set(plotKey, nextYAxisIndex);
|
|
531
|
+
nextYAxisIndex++;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// Compatible plots stay on yAxisIndex: 0 (not added to map)
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Create Y-axes for incompatible plots
|
|
542
|
+
// nextYAxisIndex already incremented in the loop above, so we know how many axes we need
|
|
543
|
+
const numOverlayAxes = overlayYAxisMap.size > 0 ? nextYAxisIndex - 1 : 0;
|
|
544
|
+
|
|
545
|
+
// Track which overlay axes are for visual-only plots (background, barcolor, etc.)
|
|
546
|
+
const visualOnlyAxes = new Set<number>();
|
|
547
|
+
overlayYAxisMap.forEach((yAxisIdx, plotKey) => {
|
|
548
|
+
// Check if this plot is visual-only by looking at the original indicator
|
|
549
|
+
indicators.forEach((indicator) => {
|
|
550
|
+
Object.entries(indicator.plots).forEach(([plotName, plot]) => {
|
|
551
|
+
const key = `${indicator.id}::${plotName}`;
|
|
552
|
+
if (key === plotKey && ['background', 'barcolor', 'char'].includes(plot.options.style)) {
|
|
553
|
+
visualOnlyAxes.add(yAxisIdx);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
for (let i = 0; i < numOverlayAxes; i++) {
|
|
560
|
+
const yAxisIndex = i + 1; // Y-axis indices start at 1 for overlays
|
|
561
|
+
const isVisualOnly = visualOnlyAxes.has(yAxisIndex);
|
|
562
|
+
|
|
563
|
+
yAxis.push({
|
|
564
|
+
position: 'left',
|
|
565
|
+
scale: !isVisualOnly, // Disable scaling for visual-only plots
|
|
566
|
+
min: isVisualOnly ? 0 : AxisUtils.createMinFunction(yAxisPaddingPercent), // Fixed range for visual plots
|
|
567
|
+
max: isVisualOnly ? 1 : AxisUtils.createMaxFunction(yAxisPaddingPercent), // Fixed range for visual plots
|
|
568
|
+
gridIndex: 0,
|
|
569
|
+
show: false, // Hide the axis visual elements
|
|
570
|
+
splitLine: { show: false },
|
|
571
|
+
axisLine: { show: false },
|
|
572
|
+
axisLabel: { show: false },
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Separate Panes Y-Axes (start after overlay axes)
|
|
577
|
+
const separatePaneYAxisOffset = nextYAxisIndex;
|
|
578
|
+
paneConfigs.forEach((pane, i) => {
|
|
579
|
+
yAxis.push({
|
|
580
|
+
position: 'right',
|
|
581
|
+
scale: true,
|
|
582
|
+
min: AxisUtils.createMinFunction(yAxisPaddingPercent),
|
|
583
|
+
max: AxisUtils.createMaxFunction(yAxisPaddingPercent),
|
|
584
|
+
gridIndex: i + 1,
|
|
585
|
+
splitLine: {
|
|
586
|
+
show: !pane.isCollapsed,
|
|
587
|
+
lineStyle: { color: '#334155', opacity: 0.3 },
|
|
588
|
+
},
|
|
589
|
+
axisLabel: {
|
|
590
|
+
show: !pane.isCollapsed,
|
|
591
|
+
color: '#94a3b8',
|
|
592
|
+
fontFamily: options.fontFamily || 'sans-serif',
|
|
593
|
+
fontSize: 10,
|
|
594
|
+
formatter: (value: number) => {
|
|
595
|
+
if (options.yAxisLabelFormatter) {
|
|
596
|
+
return options.yAxisLabelFormatter(value);
|
|
597
|
+
}
|
|
598
|
+
const decimals = options.yAxisDecimalPlaces !== undefined
|
|
599
|
+
? options.yAxisDecimalPlaces
|
|
600
|
+
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
601
|
+
return AxisUtils.formatValue(value, decimals);
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
axisLine: { show: !pane.isCollapsed, lineStyle: { color: '#334155' } },
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// --- Generate DataZoom ---
|
|
609
|
+
const dataZoom: any[] = [];
|
|
610
|
+
if (dzVisible) {
|
|
611
|
+
// Add 'inside' zoom (pan/drag) only if zoomOnTouch is enabled (default true)
|
|
612
|
+
const zoomOnTouch = options.dataZoom?.zoomOnTouch ?? true;
|
|
613
|
+
if (zoomOnTouch) {
|
|
614
|
+
dataZoom.push({
|
|
615
|
+
type: 'inside',
|
|
616
|
+
xAxisIndex: allXAxisIndices,
|
|
617
|
+
start: dzStart,
|
|
618
|
+
end: dzEnd,
|
|
619
|
+
filterMode: 'weakFilter',
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (dzPosition === 'top') {
|
|
624
|
+
dataZoom.push({
|
|
625
|
+
type: 'slider',
|
|
626
|
+
xAxisIndex: allXAxisIndices,
|
|
627
|
+
top: '1%',
|
|
628
|
+
height: dzHeight + '%',
|
|
629
|
+
start: dzStart,
|
|
630
|
+
end: dzEnd,
|
|
631
|
+
borderColor: '#334155',
|
|
632
|
+
textStyle: { color: '#cbd5e1' },
|
|
633
|
+
brushSelect: false,
|
|
634
|
+
filterMode: 'weakFilter',
|
|
635
|
+
});
|
|
636
|
+
} else {
|
|
637
|
+
dataZoom.push({
|
|
638
|
+
type: 'slider',
|
|
639
|
+
xAxisIndex: allXAxisIndices,
|
|
640
|
+
bottom: '1%',
|
|
641
|
+
height: dzHeight + '%',
|
|
642
|
+
start: dzStart,
|
|
643
|
+
end: dzEnd,
|
|
644
|
+
borderColor: '#334155',
|
|
645
|
+
textStyle: { color: '#cbd5e1' },
|
|
646
|
+
brushSelect: false,
|
|
647
|
+
filterMode: 'weakFilter',
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return {
|
|
653
|
+
grid,
|
|
654
|
+
xAxis,
|
|
655
|
+
yAxis,
|
|
656
|
+
dataZoom,
|
|
657
|
+
paneLayout: paneConfigs,
|
|
658
|
+
mainPaneHeight: mainHeightVal,
|
|
659
|
+
mainPaneTop,
|
|
660
|
+
pixelToPercent,
|
|
661
|
+
overlayYAxisMap,
|
|
662
|
+
separatePaneYAxisOffset,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
private static calculateMaximized(
|
|
667
|
+
containerHeight: number,
|
|
668
|
+
options: QFChartOptions,
|
|
669
|
+
targetPaneIndex: number // 0 for main, 1+ for indicators
|
|
670
|
+
): LayoutResult {
|
|
671
|
+
return {
|
|
672
|
+
grid: [],
|
|
673
|
+
xAxis: [],
|
|
674
|
+
yAxis: [],
|
|
675
|
+
dataZoom: [],
|
|
676
|
+
paneLayout: [],
|
|
677
|
+
mainPaneHeight: 0,
|
|
678
|
+
mainPaneTop: 0,
|
|
679
|
+
pixelToPercent: 0,
|
|
680
|
+
} as any;
|
|
681
|
+
}
|
|
682
|
+
}
|