@qfo/qfchart 0.8.2 → 0.8.5
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 +10 -0
- package/dist/qfchart.min.browser.js +21 -19
- package/dist/qfchart.min.es.js +19 -17
- package/package.json +1 -1
- package/src/QFChart.ts +135 -1
- package/src/components/GraphicBuilder.ts +44 -0
- package/src/components/Indicator.ts +106 -106
- package/src/components/LayoutManager.ts +25 -11
- package/src/components/PluginManager.ts +229 -229
- package/src/components/SeriesBuilder.ts +26 -14
- package/src/components/renderers/BoxRenderer.ts +7 -5
- package/src/components/renderers/DrawingLineRenderer.ts +7 -5
- package/src/components/renderers/FillRenderer.ts +54 -45
- package/src/components/renderers/LabelRenderer.ts +28 -12
- package/src/components/renderers/LineRenderer.ts +44 -44
- package/src/components/renderers/LinefillRenderer.ts +11 -8
- package/src/components/renderers/PolylineRenderer.ts +11 -4
- package/src/components/renderers/ScatterRenderer.ts +92 -54
- package/src/components/renderers/SeriesRenderer.ts +78 -0
- package/src/components/renderers/ShapeRenderer.ts +12 -0
- package/src/components/renderers/StepRenderer.ts +39 -39
- package/src/utils/ShapeUtils.ts +5 -0
package/package.json
CHANGED
package/src/QFChart.ts
CHANGED
|
@@ -984,6 +984,9 @@ export class QFChart implements ChartContext {
|
|
|
984
984
|
return candle;
|
|
985
985
|
});
|
|
986
986
|
|
|
987
|
+
// Build drawing range hints for Y-axis scaling
|
|
988
|
+
const updateDrawingRangeHints = this._buildDrawingRangeHints(layout, paddingPoints);
|
|
989
|
+
|
|
987
990
|
// Update only the data arrays in the option, not the full config
|
|
988
991
|
const updateOption: any = {
|
|
989
992
|
xAxis: currentOption.xAxis.map((axis: any, index: number) => ({
|
|
@@ -995,6 +998,7 @@ export class QFChart implements ChartContext {
|
|
|
995
998
|
markLine: candlestickSeries.markLine, // Ensure markLine is updated
|
|
996
999
|
},
|
|
997
1000
|
...indicatorSeries,
|
|
1001
|
+
...updateDrawingRangeHints,
|
|
998
1002
|
],
|
|
999
1003
|
};
|
|
1000
1004
|
|
|
@@ -1232,6 +1236,132 @@ export class QFChart implements ChartContext {
|
|
|
1232
1236
|
this._renderTableOverlays();
|
|
1233
1237
|
}
|
|
1234
1238
|
|
|
1239
|
+
/**
|
|
1240
|
+
* Build invisible "scatter" series that carry the min/max Y values of Pine
|
|
1241
|
+
* Script drawing objects (lines, boxes, labels, polylines). ECharts includes
|
|
1242
|
+
* these points in its automatic Y-axis range calculation so drawings below
|
|
1243
|
+
* or above the candlestick range are no longer clipped.
|
|
1244
|
+
*
|
|
1245
|
+
* Returns one hidden series per pane that has drawing objects with Y-values
|
|
1246
|
+
* outside the default data range.
|
|
1247
|
+
*/
|
|
1248
|
+
private _buildDrawingRangeHints(layout: any, paddingPoints: number): any[] {
|
|
1249
|
+
const hintSeries: any[] = [];
|
|
1250
|
+
|
|
1251
|
+
// Collect Y-value bounds per pane from all indicator drawing objects
|
|
1252
|
+
const boundsPerPane = new Map<number, { yMin: number; yMax: number }>();
|
|
1253
|
+
|
|
1254
|
+
for (const indicator of this.indicators) {
|
|
1255
|
+
if (!indicator.plots) continue;
|
|
1256
|
+
const paneIndex = indicator.paneIndex ?? 0;
|
|
1257
|
+
if (!boundsPerPane.has(paneIndex)) {
|
|
1258
|
+
boundsPerPane.set(paneIndex, { yMin: Infinity, yMax: -Infinity });
|
|
1259
|
+
}
|
|
1260
|
+
const bounds = boundsPerPane.get(paneIndex)!;
|
|
1261
|
+
|
|
1262
|
+
for (const [plotName, plot] of Object.entries(indicator.plots as Record<string, any>)) {
|
|
1263
|
+
if (!plot || !plot.options) continue;
|
|
1264
|
+
const style = plot.options?.style;
|
|
1265
|
+
|
|
1266
|
+
// Lines: y1, y2
|
|
1267
|
+
if (style === 'drawing_line' && plot.data) {
|
|
1268
|
+
for (const entry of plot.data) {
|
|
1269
|
+
const items = entry?.value ? (Array.isArray(entry.value) ? entry.value : [entry.value]) : [];
|
|
1270
|
+
for (const ln of items) {
|
|
1271
|
+
if (!ln || ln._deleted) continue;
|
|
1272
|
+
if (typeof ln.y1 === 'number' && isFinite(ln.y1)) {
|
|
1273
|
+
bounds.yMin = Math.min(bounds.yMin, ln.y1);
|
|
1274
|
+
bounds.yMax = Math.max(bounds.yMax, ln.y1);
|
|
1275
|
+
}
|
|
1276
|
+
if (typeof ln.y2 === 'number' && isFinite(ln.y2)) {
|
|
1277
|
+
bounds.yMin = Math.min(bounds.yMin, ln.y2);
|
|
1278
|
+
bounds.yMax = Math.max(bounds.yMax, ln.y2);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Boxes: top, bottom
|
|
1285
|
+
if (style === 'drawing_box' && plot.data) {
|
|
1286
|
+
for (const entry of plot.data) {
|
|
1287
|
+
const items = entry?.value ? (Array.isArray(entry.value) ? entry.value : [entry.value]) : [];
|
|
1288
|
+
for (const bx of items) {
|
|
1289
|
+
if (!bx || bx._deleted) continue;
|
|
1290
|
+
if (typeof bx.top === 'number' && isFinite(bx.top)) {
|
|
1291
|
+
bounds.yMin = Math.min(bounds.yMin, bx.top);
|
|
1292
|
+
bounds.yMax = Math.max(bounds.yMax, bx.top);
|
|
1293
|
+
}
|
|
1294
|
+
if (typeof bx.bottom === 'number' && isFinite(bx.bottom)) {
|
|
1295
|
+
bounds.yMin = Math.min(bounds.yMin, bx.bottom);
|
|
1296
|
+
bounds.yMax = Math.max(bounds.yMax, bx.bottom);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Labels: y
|
|
1303
|
+
if (style === 'label' && plot.data) {
|
|
1304
|
+
for (const entry of plot.data) {
|
|
1305
|
+
const items = entry?.value ? (Array.isArray(entry.value) ? entry.value : [entry.value]) : [];
|
|
1306
|
+
for (const lbl of items) {
|
|
1307
|
+
if (!lbl || lbl._deleted) continue;
|
|
1308
|
+
if (typeof lbl.y === 'number' && isFinite(lbl.y)) {
|
|
1309
|
+
bounds.yMin = Math.min(bounds.yMin, lbl.y);
|
|
1310
|
+
bounds.yMax = Math.max(bounds.yMax, lbl.y);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Polylines: points[].price
|
|
1317
|
+
if (style === 'drawing_polyline' && plot.data) {
|
|
1318
|
+
for (const entry of plot.data) {
|
|
1319
|
+
const items = entry?.value ? (Array.isArray(entry.value) ? entry.value : [entry.value]) : [];
|
|
1320
|
+
for (const pl of items) {
|
|
1321
|
+
if (!pl || pl._deleted || !pl._points) continue;
|
|
1322
|
+
for (const pt of pl._points) {
|
|
1323
|
+
if (typeof pt?.price === 'number' && isFinite(pt.price)) {
|
|
1324
|
+
bounds.yMin = Math.min(bounds.yMin, pt.price);
|
|
1325
|
+
bounds.yMax = Math.max(bounds.yMax, pt.price);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// Create a hidden scatter series per pane with min/max Y values
|
|
1335
|
+
const midIndex = paddingPoints + Math.floor((this.marketData?.length || 0) / 2);
|
|
1336
|
+
boundsPerPane.forEach((bounds, paneIndex) => {
|
|
1337
|
+
if (!isFinite(bounds.yMin) || !isFinite(bounds.yMax)) return;
|
|
1338
|
+
|
|
1339
|
+
// Determine Y-axis index for this pane
|
|
1340
|
+
const yAxisIndex = paneIndex === 0
|
|
1341
|
+
? 0
|
|
1342
|
+
: (layout.separatePaneYAxisOffset || 1) + (paneIndex - 1);
|
|
1343
|
+
|
|
1344
|
+
hintSeries.push({
|
|
1345
|
+
name: `_drawingRange_pane${paneIndex}`,
|
|
1346
|
+
type: 'scatter',
|
|
1347
|
+
xAxisIndex: paneIndex,
|
|
1348
|
+
yAxisIndex,
|
|
1349
|
+
symbol: 'none',
|
|
1350
|
+
symbolSize: 0,
|
|
1351
|
+
silent: true,
|
|
1352
|
+
animation: false,
|
|
1353
|
+
// Two invisible points at min and max Y — ECharts includes them in axis scaling
|
|
1354
|
+
data: [
|
|
1355
|
+
[midIndex, bounds.yMin],
|
|
1356
|
+
[midIndex, bounds.yMax],
|
|
1357
|
+
],
|
|
1358
|
+
tooltip: { show: false },
|
|
1359
|
+
});
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
return hintSeries;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1235
1365
|
/**
|
|
1236
1366
|
* Build table canvas graphic elements from the current _lastTables.
|
|
1237
1367
|
* Must be called AFTER setOption so grid rects are available from ECharts.
|
|
@@ -1602,6 +1732,10 @@ export class QFChart implements ChartContext {
|
|
|
1602
1732
|
layout.separatePaneYAxisOffset, // Pass Y-axis offset for separate panes
|
|
1603
1733
|
);
|
|
1604
1734
|
|
|
1735
|
+
// Create hidden range-hint series so Pine Script drawing objects
|
|
1736
|
+
// (lines, boxes, labels, polylines) contribute to Y-axis auto-scaling.
|
|
1737
|
+
const drawingRangeHints = this._buildDrawingRangeHints(layout, paddingPoints);
|
|
1738
|
+
|
|
1605
1739
|
// Apply barColors (TradingView: barcolor() only changes body fill, borders/wicks stay default)
|
|
1606
1740
|
candlestickSeries.data = candlestickSeries.data.map((candle: any, i: number) => {
|
|
1607
1741
|
if (barColors[i]) {
|
|
@@ -1770,7 +1904,7 @@ export class QFChart implements ChartContext {
|
|
|
1770
1904
|
xAxis: layout.xAxis,
|
|
1771
1905
|
yAxis: layout.yAxis,
|
|
1772
1906
|
dataZoom: layout.dataZoom,
|
|
1773
|
-
series: [candlestickSeries, ...indicatorSeries, ...drawingSeriesList],
|
|
1907
|
+
series: [candlestickSeries, ...indicatorSeries, ...drawingRangeHints, ...drawingSeriesList],
|
|
1774
1908
|
};
|
|
1775
1909
|
|
|
1776
1910
|
this.chart.setOption(option, true); // true = not merge, replace.
|
|
@@ -184,6 +184,50 @@ export class GraphicBuilder {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// Pane Separator Lines (between main chart and indicator panes, and between indicators)
|
|
188
|
+
// Offset upward from center so the line doesn't overlap the lower pane's y-axis labels
|
|
189
|
+
if (!maximizedPaneId && layout.paneBoundaries.length > 0) {
|
|
190
|
+
const sepOffset = -8 * pixelToPercent; // shift 8px up from gap center
|
|
191
|
+
for (const boundary of layout.paneBoundaries) {
|
|
192
|
+
graphic.push({
|
|
193
|
+
type: 'group',
|
|
194
|
+
left: '10%',
|
|
195
|
+
top: (boundary.yPercent + sepOffset) + '%',
|
|
196
|
+
children: [
|
|
197
|
+
// Invisible wide hit target for easier hover/drag
|
|
198
|
+
{
|
|
199
|
+
type: 'rect',
|
|
200
|
+
shape: { width: 5000, height: 12, y: -6 },
|
|
201
|
+
style: { fill: 'transparent' },
|
|
202
|
+
cursor: 'row-resize',
|
|
203
|
+
},
|
|
204
|
+
// Visible line — moderately visible default, bright on hover
|
|
205
|
+
{
|
|
206
|
+
type: 'rect',
|
|
207
|
+
shape: { width: 5000, height: 2, y: -1 },
|
|
208
|
+
style: { fill: '#475569', opacity: 0.7 },
|
|
209
|
+
cursor: 'row-resize',
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
z: 50,
|
|
213
|
+
onmouseover: function () {
|
|
214
|
+
const line = this.children()[1];
|
|
215
|
+
if (line) {
|
|
216
|
+
line.setStyle({ fill: '#94a3b8', opacity: 1.0 });
|
|
217
|
+
line.setShape({ height: 3, y: -1.5 });
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
onmouseout: function () {
|
|
221
|
+
const line = this.children()[1];
|
|
222
|
+
if (line) {
|
|
223
|
+
line.setStyle({ fill: '#475569', opacity: 0.7 });
|
|
224
|
+
line.setShape({ height: 2, y: -1 });
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
187
231
|
// Indicator Panes
|
|
188
232
|
layout.paneLayout.forEach((pane) => {
|
|
189
233
|
// If maximizedPaneId is set, and this is NOT the maximized pane, skip rendering its controls
|
|
@@ -1,106 +1,106 @@
|
|
|
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
|
-
if (!existingPlot.data) return;
|
|
84
|
-
|
|
85
|
-
// Update options if provided
|
|
86
|
-
if (newPlot.options) {
|
|
87
|
-
existingPlot.options = { ...existingPlot.options, ...newPlot.options };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Merge data points by time
|
|
91
|
-
const existingTimeMap = new Map<number, IndicatorPoint>();
|
|
92
|
-
existingPlot.data?.forEach((point) => {
|
|
93
|
-
existingTimeMap.set(point.time, point);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Update or add new points
|
|
97
|
-
newPlot.data?.forEach((point) => {
|
|
98
|
-
existingTimeMap.set(point.time, point);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// Rebuild data array sorted by time
|
|
102
|
-
existingPlot.data = Array.from(existingTimeMap.values()).sort((a, b) => a.time - b.time);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
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
|
+
if (!existingPlot.data) return;
|
|
84
|
+
|
|
85
|
+
// Update options if provided
|
|
86
|
+
if (newPlot.options) {
|
|
87
|
+
existingPlot.options = { ...existingPlot.options, ...newPlot.options };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Merge data points by time
|
|
91
|
+
const existingTimeMap = new Map<number, IndicatorPoint>();
|
|
92
|
+
existingPlot.data?.forEach((point) => {
|
|
93
|
+
existingTimeMap.set(point.time, point);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Update or add new points
|
|
97
|
+
newPlot.data?.forEach((point) => {
|
|
98
|
+
existingTimeMap.set(point.time, point);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Rebuild data array sorted by time
|
|
102
|
+
existingPlot.data = Array.from(existingTimeMap.values()).sort((a, b) => a.time - b.time);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -418,7 +418,10 @@ export class LayoutManager {
|
|
|
418
418
|
const xAxis: any[] = [];
|
|
419
419
|
|
|
420
420
|
// Main X-Axis
|
|
421
|
+
// Hide date labels on the main chart when indicator panes exist below —
|
|
422
|
+
// the bottom-most pane's x-axis will show them instead.
|
|
421
423
|
const isMainBottom = paneConfigs.length === 0;
|
|
424
|
+
const showMainXLabels = !isMainCollapsed && isMainBottom;
|
|
422
425
|
xAxis.push({
|
|
423
426
|
type: 'category',
|
|
424
427
|
data: [], // Will be filled by SeriesBuilder or QFChart
|
|
@@ -435,7 +438,7 @@ export class LayoutManager {
|
|
|
435
438
|
lineStyle: { color: gridLineColor, opacity: gridLineOpacity },
|
|
436
439
|
},
|
|
437
440
|
axisLabel: {
|
|
438
|
-
show:
|
|
441
|
+
show: showMainXLabels,
|
|
439
442
|
color: '#94a3b8',
|
|
440
443
|
fontFamily: options.fontFamily || 'sans-serif',
|
|
441
444
|
formatter: (value: number) => {
|
|
@@ -447,7 +450,7 @@ export class LayoutManager {
|
|
|
447
450
|
return AxisUtils.formatValue(value, decimals);
|
|
448
451
|
},
|
|
449
452
|
},
|
|
450
|
-
axisTick: { show:
|
|
453
|
+
axisTick: { show: showMainXLabels },
|
|
451
454
|
axisPointer: {
|
|
452
455
|
label: {
|
|
453
456
|
show: isMainBottom,
|
|
@@ -458,15 +461,21 @@ export class LayoutManager {
|
|
|
458
461
|
});
|
|
459
462
|
|
|
460
463
|
// Separate Panes X-Axes
|
|
464
|
+
// Show date labels only on the bottom-most pane
|
|
461
465
|
paneConfigs.forEach((pane, i) => {
|
|
462
466
|
const isBottom = i === paneConfigs.length - 1;
|
|
467
|
+
const showLabels = isBottom && !pane.isCollapsed;
|
|
463
468
|
xAxis.push({
|
|
464
469
|
type: 'category',
|
|
465
470
|
gridIndex: i + 1, // 0 is main
|
|
466
471
|
data: [], // Shared data
|
|
467
|
-
axisLabel: {
|
|
472
|
+
axisLabel: {
|
|
473
|
+
show: showLabels,
|
|
474
|
+
color: '#94a3b8',
|
|
475
|
+
fontFamily: options.fontFamily || 'sans-serif',
|
|
476
|
+
},
|
|
468
477
|
axisLine: { show: !pane.isCollapsed && gridBorderShow, lineStyle: { color: gridBorderColor } },
|
|
469
|
-
axisTick: { show:
|
|
478
|
+
axisTick: { show: showLabels },
|
|
470
479
|
splitLine: { show: false },
|
|
471
480
|
axisPointer: {
|
|
472
481
|
label: {
|
|
@@ -549,16 +558,21 @@ export class LayoutManager {
|
|
|
549
558
|
const plotKey = `${id}::${plotName}`;
|
|
550
559
|
|
|
551
560
|
// Skip visual-only plot types that should never affect Y-axis scaling
|
|
552
|
-
// EXCEPTION: shapes with
|
|
553
|
-
const visualOnlyStyles = ['background', 'barcolor'
|
|
561
|
+
// EXCEPTION: shapes/chars with price-relative locations must stay on main Y-axis
|
|
562
|
+
const visualOnlyStyles = ['background', 'barcolor'];
|
|
554
563
|
|
|
555
|
-
// Check if this is a shape with price-relative positioning
|
|
564
|
+
// Check if this is a shape/char with price-relative positioning
|
|
565
|
+
// Includes abovebar/belowbar (relative to candle) and absolute (exact Y value)
|
|
556
566
|
const isShapeWithPriceLocation =
|
|
557
|
-
plot.options.style === 'shape' &&
|
|
567
|
+
(plot.options.style === 'shape' || plot.options.style === 'char') &&
|
|
558
568
|
(plot.options.location === 'abovebar' ||
|
|
559
569
|
plot.options.location === 'AboveBar' ||
|
|
570
|
+
plot.options.location === 'ab' ||
|
|
560
571
|
plot.options.location === 'belowbar' ||
|
|
561
|
-
plot.options.location === 'BelowBar'
|
|
572
|
+
plot.options.location === 'BelowBar' ||
|
|
573
|
+
plot.options.location === 'bl' ||
|
|
574
|
+
plot.options.location === 'absolute' ||
|
|
575
|
+
plot.options.location === 'Absolute');
|
|
562
576
|
|
|
563
577
|
if (visualOnlyStyles.includes(plot.options.style)) {
|
|
564
578
|
// Assign these to a separate Y-axis so they don't affect price scale
|
|
@@ -569,8 +583,8 @@ export class LayoutManager {
|
|
|
569
583
|
return; // Skip further processing for this plot
|
|
570
584
|
}
|
|
571
585
|
|
|
572
|
-
// If it's a shape but NOT with price-relative positioning, treat as visual-only
|
|
573
|
-
if (plot.options.style === 'shape' && !isShapeWithPriceLocation) {
|
|
586
|
+
// If it's a shape/char but NOT with price-relative positioning, treat as visual-only
|
|
587
|
+
if ((plot.options.style === 'shape' || plot.options.style === 'char') && !isShapeWithPriceLocation) {
|
|
574
588
|
if (!overlayYAxisMap.has(plotKey)) {
|
|
575
589
|
overlayYAxisMap.set(plotKey, nextYAxisIndex);
|
|
576
590
|
nextYAxisIndex++;
|