@qfo/qfchart 0.6.3 → 0.6.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 +4 -2
- package/dist/qfchart.min.browser.js +16 -16
- package/dist/qfchart.min.es.js +16 -16
- package/package.json +1 -1
- package/src/components/Indicator.ts +4 -2
- package/src/components/LayoutManager.ts +27 -36
- package/src/components/SeriesBuilder.ts +67 -620
- package/src/components/SeriesRendererFactory.ts +36 -0
- package/src/components/renderers/BackgroundRenderer.ts +45 -0
- package/src/components/renderers/FillRenderer.ts +99 -0
- package/src/components/renderers/HistogramRenderer.ts +20 -0
- package/src/components/renderers/LineRenderer.ts +44 -0
- package/src/components/renderers/OHLCBarRenderer.ts +161 -0
- package/src/components/renderers/ScatterRenderer.ts +54 -0
- package/src/components/renderers/SeriesRenderer.ts +20 -0
- package/src/components/renderers/ShapeRenderer.ts +121 -0
- package/src/components/renderers/StepRenderer.ts +39 -0
- package/src/types.ts +5 -2
- package/src/utils/AxisUtils.ts +18 -0
- package/src/utils/ColorUtils.ts +32 -0
- package/src/utils/ShapeUtils.ts +140 -0
- /package/src/{Utils.ts → utils/CanvasUtils.ts} +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { OHLCV, Indicator as IndicatorType, QFChartOptions, IndicatorPlot, IndicatorStyle } from '../types';
|
|
2
2
|
import { PaneConfiguration } from './LayoutManager';
|
|
3
|
-
import {
|
|
3
|
+
import { SeriesRendererFactory } from './SeriesRendererFactory';
|
|
4
4
|
|
|
5
5
|
export class SeriesBuilder {
|
|
6
|
+
private static readonly DEFAULT_COLOR = '#2962ff';
|
|
7
|
+
|
|
6
8
|
public static buildCandlestickSeries(marketData: OHLCV[], options: QFChartOptions, totalLength?: number): any {
|
|
7
9
|
const upColor = options.upColor || '#00da3c';
|
|
8
10
|
const downColor = options.downColor || '#ec0000';
|
|
@@ -83,145 +85,6 @@ export class SeriesBuilder {
|
|
|
83
85
|
};
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
private static getShapeSymbol(shape: string): string {
|
|
87
|
-
// SVG Paths need to be:
|
|
88
|
-
// 1. Valid SVG path data strings
|
|
89
|
-
// 2. Ideally centered around the origin or a standard box (e.g., 0 0 24 24)
|
|
90
|
-
// 3. ECharts path:// format expects just the path data usually, but complex shapes might need 'image://' or better paths.
|
|
91
|
-
// For simple shapes, standard ECharts symbols or simple paths work.
|
|
92
|
-
|
|
93
|
-
switch (shape) {
|
|
94
|
-
case 'arrowdown':
|
|
95
|
-
// Blocky arrow down
|
|
96
|
-
return 'path://M12 24l-12-12h8v-12h8v12h8z';
|
|
97
|
-
|
|
98
|
-
case 'arrowup':
|
|
99
|
-
// Blocky arrow up
|
|
100
|
-
return 'path://M12 0l12 12h-8v12h-8v-12h-8z';
|
|
101
|
-
|
|
102
|
-
case 'circle':
|
|
103
|
-
return 'circle';
|
|
104
|
-
|
|
105
|
-
case 'cross':
|
|
106
|
-
// Plus sign (+)
|
|
107
|
-
return 'path://M11 2h2v9h9v2h-9v9h-2v-9h-9v-2h9z';
|
|
108
|
-
|
|
109
|
-
case 'diamond':
|
|
110
|
-
return 'diamond'; // Built-in
|
|
111
|
-
|
|
112
|
-
case 'flag':
|
|
113
|
-
// Flag on a pole
|
|
114
|
-
return 'path://M6 2v20h2v-8h12l-2-6 2-6h-12z';
|
|
115
|
-
|
|
116
|
-
case 'labeldown':
|
|
117
|
-
// Bubble pointing down: Rounded rect with a triangle at bottom
|
|
118
|
-
return 'path://M4 2h16a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-6l-2 4l-2 -4h-6a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2z';
|
|
119
|
-
|
|
120
|
-
case 'labelup':
|
|
121
|
-
// Bubble pointing up: Rounded rect with triangle at top
|
|
122
|
-
return 'path://M12 2l2 4h6a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-16a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h6z';
|
|
123
|
-
|
|
124
|
-
case 'square':
|
|
125
|
-
return 'rect';
|
|
126
|
-
|
|
127
|
-
case 'triangledown':
|
|
128
|
-
// Pointing down
|
|
129
|
-
return 'path://M12 21l-10-18h20z';
|
|
130
|
-
|
|
131
|
-
case 'triangleup':
|
|
132
|
-
// Pointing up
|
|
133
|
-
return 'triangle'; // Built-in is pointing up
|
|
134
|
-
|
|
135
|
-
case 'xcross':
|
|
136
|
-
// 'X' shape
|
|
137
|
-
return 'path://M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z';
|
|
138
|
-
|
|
139
|
-
default:
|
|
140
|
-
return 'circle';
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
private static getShapeRotation(shape: string): number {
|
|
145
|
-
// With custom paths defined above, we might not need rotation unless we reuse shapes.
|
|
146
|
-
// Built-in triangle is UP.
|
|
147
|
-
return 0;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private static getShapeSize(size: string, width?: number, height?: number): number | number[] {
|
|
151
|
-
// If both width and height are specified, use them directly
|
|
152
|
-
if (width !== undefined && height !== undefined) {
|
|
153
|
-
return [width, height];
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Base size from the size parameter
|
|
157
|
-
let baseSize: number;
|
|
158
|
-
switch (size) {
|
|
159
|
-
case 'tiny':
|
|
160
|
-
baseSize = 8;
|
|
161
|
-
break;
|
|
162
|
-
case 'small':
|
|
163
|
-
baseSize = 12;
|
|
164
|
-
break;
|
|
165
|
-
case 'normal':
|
|
166
|
-
case 'auto':
|
|
167
|
-
baseSize = 16;
|
|
168
|
-
break;
|
|
169
|
-
case 'large':
|
|
170
|
-
baseSize = 24;
|
|
171
|
-
break;
|
|
172
|
-
case 'huge':
|
|
173
|
-
baseSize = 32;
|
|
174
|
-
break;
|
|
175
|
-
default:
|
|
176
|
-
baseSize = 16;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// If only width is specified, preserve aspect ratio (assume square default)
|
|
180
|
-
if (width !== undefined) {
|
|
181
|
-
return [width, width];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// If only height is specified, preserve aspect ratio (assume square default)
|
|
185
|
-
if (height !== undefined) {
|
|
186
|
-
return [height, height];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Default uniform size
|
|
190
|
-
return baseSize;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Helper to determine label position and distance relative to shape BASED ON LOCATION
|
|
194
|
-
private static getLabelConfig(shape: string, location: string): { position: string; distance: number } {
|
|
195
|
-
// Text position should be determined by location, not shape direction
|
|
196
|
-
|
|
197
|
-
switch (location) {
|
|
198
|
-
case 'abovebar':
|
|
199
|
-
// Shape is above the candle, text should be above the shape
|
|
200
|
-
return { position: 'top', distance: 5 };
|
|
201
|
-
|
|
202
|
-
case 'belowbar':
|
|
203
|
-
// Shape is below the candle, text should be below the shape
|
|
204
|
-
return { position: 'bottom', distance: 5 };
|
|
205
|
-
|
|
206
|
-
case 'top':
|
|
207
|
-
// Shape at top of chart, text below it
|
|
208
|
-
return { position: 'bottom', distance: 5 };
|
|
209
|
-
|
|
210
|
-
case 'bottom':
|
|
211
|
-
// Shape at bottom of chart, text above it
|
|
212
|
-
return { position: 'top', distance: 5 };
|
|
213
|
-
|
|
214
|
-
case 'absolute':
|
|
215
|
-
default:
|
|
216
|
-
// For labelup/down, text is INSIDE the shape
|
|
217
|
-
if (shape === 'labelup' || shape === 'labeldown') {
|
|
218
|
-
return { position: 'inside', distance: 0 };
|
|
219
|
-
}
|
|
220
|
-
// For other shapes, text above by default
|
|
221
|
-
return { position: 'top', distance: 5 };
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
88
|
public static buildIndicatorSeries(
|
|
226
89
|
indicators: Map<string, IndicatorType>,
|
|
227
90
|
timeToIndex: Map<number, number>,
|
|
@@ -235,10 +98,25 @@ export class SeriesBuilder {
|
|
|
235
98
|
const series: any[] = [];
|
|
236
99
|
const barColors: (string | null)[] = new Array(totalDataLength).fill(null);
|
|
237
100
|
|
|
101
|
+
// Store plot data arrays for fill plots to reference
|
|
102
|
+
const plotDataArrays = new Map<string, number[]>();
|
|
103
|
+
|
|
238
104
|
indicators.forEach((indicator, id) => {
|
|
239
105
|
if (indicator.collapsed) return; // Skip if collapsed
|
|
240
106
|
|
|
241
|
-
|
|
107
|
+
// Sort plots so that 'fill' plots are processed last
|
|
108
|
+
// This ensures that the plots they reference (plot1, plot2) have already been processed and their data stored
|
|
109
|
+
const sortedPlots = Object.keys(indicator.plots).sort((a, b) => {
|
|
110
|
+
const plotA = indicator.plots[a];
|
|
111
|
+
const plotB = indicator.plots[b];
|
|
112
|
+
const isFillA = plotA.options.style === 'fill';
|
|
113
|
+
const isFillB = plotB.options.style === 'fill';
|
|
114
|
+
if (isFillA && !isFillB) return 1;
|
|
115
|
+
if (!isFillA && isFillB) return -1;
|
|
116
|
+
return 0;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
sortedPlots.forEach((plotName) => {
|
|
242
120
|
const plot = indicator.plots[plotName];
|
|
243
121
|
const seriesName = `${id}::${plotName}`;
|
|
244
122
|
|
|
@@ -269,11 +147,13 @@ export class SeriesBuilder {
|
|
|
269
147
|
}
|
|
270
148
|
}
|
|
271
149
|
|
|
150
|
+
// Prepare data arrays
|
|
151
|
+
// For 'fill' style, we don't use plot.data directly in the same way, but we initialize generic arrays
|
|
272
152
|
const dataArray = new Array(totalDataLength).fill(null);
|
|
273
153
|
const colorArray = new Array(totalDataLength).fill(null);
|
|
274
154
|
const optionsArray = new Array(totalDataLength).fill(null); // Store per-point options
|
|
275
155
|
|
|
276
|
-
plot.data
|
|
156
|
+
plot.data?.forEach((point) => {
|
|
277
157
|
const index = timeToIndex.get(point.time);
|
|
278
158
|
if (index !== undefined) {
|
|
279
159
|
const plotOffset = point.options?.offset ?? plot.options.offset ?? 0;
|
|
@@ -295,497 +175,64 @@ export class SeriesBuilder {
|
|
|
295
175
|
}
|
|
296
176
|
|
|
297
177
|
dataArray[offsetIndex] = value;
|
|
298
|
-
colorArray[offsetIndex] = pointColor || plot.options.color;
|
|
178
|
+
colorArray[offsetIndex] = pointColor || plot.options.color || SeriesBuilder.DEFAULT_COLOR;
|
|
299
179
|
optionsArray[offsetIndex] = point.options || {};
|
|
300
180
|
}
|
|
301
181
|
}
|
|
302
182
|
});
|
|
303
183
|
|
|
184
|
+
// Store data array for fill plots to reference
|
|
185
|
+
// Only store for non-fill plots as fill plots don't produce data to be referenced by other fills (usually)
|
|
186
|
+
plotDataArrays.set(`${id}::${plotName}`, dataArray);
|
|
187
|
+
|
|
304
188
|
if (plot.options?.style?.startsWith('style_')) {
|
|
305
189
|
plot.options.style = plot.options.style.replace('style_', '') as IndicatorStyle;
|
|
306
190
|
}
|
|
307
|
-
switch (plot.options.style) {
|
|
308
|
-
case 'histogram':
|
|
309
|
-
case 'columns':
|
|
310
|
-
series.push({
|
|
311
|
-
name: seriesName,
|
|
312
|
-
type: 'bar',
|
|
313
|
-
xAxisIndex: xAxisIndex,
|
|
314
|
-
yAxisIndex: yAxisIndex,
|
|
315
|
-
data: dataArray.map((val, i) => ({
|
|
316
|
-
value: val,
|
|
317
|
-
itemStyle: colorArray[i] ? { color: colorArray[i] } : undefined,
|
|
318
|
-
})),
|
|
319
|
-
itemStyle: { color: plot.options.color },
|
|
320
|
-
});
|
|
321
|
-
break;
|
|
322
|
-
|
|
323
|
-
case 'circles':
|
|
324
|
-
case 'cross':
|
|
325
|
-
// Scatter
|
|
326
|
-
const scatterData = dataArray
|
|
327
|
-
.map((val, i) => {
|
|
328
|
-
if (val === null) return null;
|
|
329
|
-
const pointColor = colorArray[i] || plot.options.color;
|
|
330
|
-
const item: any = {
|
|
331
|
-
value: [i, val],
|
|
332
|
-
itemStyle: { color: pointColor },
|
|
333
|
-
};
|
|
334
191
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
case 'bar':
|
|
356
|
-
case 'candle':
|
|
357
|
-
// OHLC Bar/Candle rendering
|
|
358
|
-
const ohlcData = dataArray
|
|
359
|
-
.map((val, i) => {
|
|
360
|
-
if (val === null || !Array.isArray(val) || val.length !== 4) return null;
|
|
361
|
-
|
|
362
|
-
const [open, high, low, close] = val;
|
|
363
|
-
const pointOpts = optionsArray[i] || {};
|
|
364
|
-
const color = pointOpts.color || colorArray[i] || plot.options.color;
|
|
365
|
-
const wickColor = pointOpts.wickcolor || plot.options.wickcolor || color;
|
|
366
|
-
const borderColor = pointOpts.bordercolor || plot.options.bordercolor || wickColor;
|
|
367
|
-
|
|
368
|
-
// Store colors in value array at positions 5, 6, and 7 for access in renderItem
|
|
369
|
-
return [i, open, close, low, high, color, wickColor, borderColor];
|
|
370
|
-
})
|
|
371
|
-
.filter((item) => item !== null);
|
|
372
|
-
|
|
373
|
-
series.push({
|
|
374
|
-
name: seriesName,
|
|
375
|
-
type: 'custom',
|
|
376
|
-
xAxisIndex: xAxisIndex,
|
|
377
|
-
yAxisIndex: yAxisIndex,
|
|
378
|
-
renderItem: (params: any, api: any) => {
|
|
379
|
-
const xValue = api.value(0);
|
|
380
|
-
const openValue = api.value(1);
|
|
381
|
-
const closeValue = api.value(2);
|
|
382
|
-
const lowValue = api.value(3);
|
|
383
|
-
const highValue = api.value(4);
|
|
384
|
-
const color = api.value(5);
|
|
385
|
-
const wickColor = api.value(6);
|
|
386
|
-
const borderColor = api.value(7);
|
|
387
|
-
|
|
388
|
-
if (isNaN(openValue) || isNaN(closeValue) || isNaN(lowValue) || isNaN(highValue)) {
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const xPos = api.coord([xValue, 0])[0];
|
|
393
|
-
const openPos = api.coord([xValue, openValue])[1];
|
|
394
|
-
const closePos = api.coord([xValue, closeValue])[1];
|
|
395
|
-
const lowPos = api.coord([xValue, lowValue])[1];
|
|
396
|
-
const highPos = api.coord([xValue, highValue])[1];
|
|
397
|
-
|
|
398
|
-
const barWidth = api.size([1, 0])[0] * 0.6;
|
|
399
|
-
|
|
400
|
-
if (plot.options.style === 'candle') {
|
|
401
|
-
// Classic candlestick rendering
|
|
402
|
-
const bodyTop = Math.min(openPos, closePos);
|
|
403
|
-
const bodyBottom = Math.max(openPos, closePos);
|
|
404
|
-
const bodyHeight = Math.abs(closePos - openPos);
|
|
405
|
-
|
|
406
|
-
return {
|
|
407
|
-
type: 'group',
|
|
408
|
-
children: [
|
|
409
|
-
// Upper wick
|
|
410
|
-
{
|
|
411
|
-
type: 'line',
|
|
412
|
-
shape: {
|
|
413
|
-
x1: xPos,
|
|
414
|
-
y1: highPos,
|
|
415
|
-
x2: xPos,
|
|
416
|
-
y2: bodyTop,
|
|
417
|
-
},
|
|
418
|
-
style: {
|
|
419
|
-
stroke: wickColor,
|
|
420
|
-
lineWidth: 1,
|
|
421
|
-
},
|
|
422
|
-
},
|
|
423
|
-
// Lower wick
|
|
424
|
-
{
|
|
425
|
-
type: 'line',
|
|
426
|
-
shape: {
|
|
427
|
-
x1: xPos,
|
|
428
|
-
y1: bodyBottom,
|
|
429
|
-
x2: xPos,
|
|
430
|
-
y2: lowPos,
|
|
431
|
-
},
|
|
432
|
-
style: {
|
|
433
|
-
stroke: wickColor,
|
|
434
|
-
lineWidth: 1,
|
|
435
|
-
},
|
|
436
|
-
},
|
|
437
|
-
// Body
|
|
438
|
-
{
|
|
439
|
-
type: 'rect',
|
|
440
|
-
shape: {
|
|
441
|
-
x: xPos - barWidth / 2,
|
|
442
|
-
y: bodyTop,
|
|
443
|
-
width: barWidth,
|
|
444
|
-
height: bodyHeight || 1, // Minimum height for doji
|
|
445
|
-
},
|
|
446
|
-
style: {
|
|
447
|
-
fill: color,
|
|
448
|
-
stroke: borderColor,
|
|
449
|
-
lineWidth: 1,
|
|
450
|
-
},
|
|
451
|
-
},
|
|
452
|
-
],
|
|
453
|
-
};
|
|
454
|
-
} else {
|
|
455
|
-
// Bar style (OHLC bar)
|
|
456
|
-
const tickWidth = barWidth * 0.5;
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
type: 'group',
|
|
460
|
-
children: [
|
|
461
|
-
// Vertical line (low to high)
|
|
462
|
-
{
|
|
463
|
-
type: 'line',
|
|
464
|
-
shape: {
|
|
465
|
-
x1: xPos,
|
|
466
|
-
y1: lowPos,
|
|
467
|
-
x2: xPos,
|
|
468
|
-
y2: highPos,
|
|
469
|
-
},
|
|
470
|
-
style: {
|
|
471
|
-
stroke: color,
|
|
472
|
-
lineWidth: 1,
|
|
473
|
-
},
|
|
474
|
-
},
|
|
475
|
-
// Open tick (left)
|
|
476
|
-
{
|
|
477
|
-
type: 'line',
|
|
478
|
-
shape: {
|
|
479
|
-
x1: xPos - tickWidth,
|
|
480
|
-
y1: openPos,
|
|
481
|
-
x2: xPos,
|
|
482
|
-
y2: openPos,
|
|
483
|
-
},
|
|
484
|
-
style: {
|
|
485
|
-
stroke: color,
|
|
486
|
-
lineWidth: 1,
|
|
487
|
-
},
|
|
488
|
-
},
|
|
489
|
-
// Close tick (right)
|
|
490
|
-
{
|
|
491
|
-
type: 'line',
|
|
492
|
-
shape: {
|
|
493
|
-
x1: xPos,
|
|
494
|
-
y1: closePos,
|
|
495
|
-
x2: xPos + tickWidth,
|
|
496
|
-
y2: closePos,
|
|
497
|
-
},
|
|
498
|
-
style: {
|
|
499
|
-
stroke: color,
|
|
500
|
-
lineWidth: 1,
|
|
501
|
-
},
|
|
502
|
-
},
|
|
503
|
-
],
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
},
|
|
507
|
-
data: ohlcData,
|
|
508
|
-
});
|
|
509
|
-
break;
|
|
510
|
-
|
|
511
|
-
case 'shape':
|
|
512
|
-
const shapeData = dataArray
|
|
513
|
-
.map((val, i) => {
|
|
514
|
-
// Merge global options with per-point options to get location first
|
|
515
|
-
const pointOpts = optionsArray[i] || {};
|
|
516
|
-
const globalOpts = plot.options;
|
|
517
|
-
const location = pointOpts.location || globalOpts.location || 'absolute';
|
|
518
|
-
|
|
519
|
-
// For location="absolute", always draw the shape (ignore value)
|
|
520
|
-
// For other locations, only draw if value is truthy (TradingView behavior)
|
|
521
|
-
if (location !== 'absolute' && !val) {
|
|
522
|
-
return null;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// If we get here and val is null/undefined, it means location is absolute
|
|
526
|
-
// In that case, we still need a valid value for positioning
|
|
527
|
-
// Use the value if it exists, otherwise we'd need a fallback
|
|
528
|
-
// But in TradingView, absolute location still expects a value for Y position
|
|
529
|
-
if (val === null || val === undefined) {
|
|
530
|
-
return null; // Can't plot without a Y coordinate
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const color = pointOpts.color || globalOpts.color || 'blue';
|
|
534
|
-
const shape = pointOpts.shape || globalOpts.shape || 'circle';
|
|
535
|
-
const size = pointOpts.size || globalOpts.size || 'normal';
|
|
536
|
-
const text = pointOpts.text || globalOpts.text;
|
|
537
|
-
const textColor = pointOpts.textcolor || globalOpts.textcolor || 'white';
|
|
538
|
-
|
|
539
|
-
// NEW: Get width and height
|
|
540
|
-
const width = pointOpts.width || globalOpts.width;
|
|
541
|
-
const height = pointOpts.height || globalOpts.height;
|
|
542
|
-
|
|
543
|
-
// Debug logging (remove after testing)
|
|
544
|
-
// if (width !== undefined || height !== undefined) {
|
|
545
|
-
// console.log('[Shape Debug]', { shape, width, height, pointOpts, globalOpts });
|
|
546
|
-
// }
|
|
547
|
-
|
|
548
|
-
// Positioning based on location
|
|
549
|
-
let yValue = val; // Default to absolute value
|
|
550
|
-
let symbolOffset: (string | number)[] = [0, 0];
|
|
551
|
-
|
|
552
|
-
if (location === 'abovebar') {
|
|
553
|
-
// Shape above the candle
|
|
554
|
-
if (candlestickData && candlestickData[i]) {
|
|
555
|
-
yValue = candlestickData[i].high;
|
|
556
|
-
}
|
|
557
|
-
symbolOffset = [0, '-150%']; // Shift up
|
|
558
|
-
} else if (location === 'belowbar') {
|
|
559
|
-
// Shape below the candle
|
|
560
|
-
if (candlestickData && candlestickData[i]) {
|
|
561
|
-
yValue = candlestickData[i].low;
|
|
562
|
-
}
|
|
563
|
-
symbolOffset = [0, '150%']; // Shift down
|
|
564
|
-
} else if (location === 'top') {
|
|
565
|
-
// Shape at top of chart - we need to use a very high value
|
|
566
|
-
// This would require knowing the y-axis max, which we don't have here easily
|
|
567
|
-
// For now, use a placeholder approach - might need to calculate from data
|
|
568
|
-
// Or we can use a percentage of the viewport? ECharts doesn't support that directly in scatter.
|
|
569
|
-
// Best approach: use a large multiplier of current value or track max
|
|
570
|
-
// Simplified: use coordinate system max (will need enhancement)
|
|
571
|
-
yValue = val; // For now, keep absolute - would need axis max
|
|
572
|
-
symbolOffset = [0, 0];
|
|
573
|
-
} else if (location === 'bottom') {
|
|
574
|
-
// Shape at bottom of chart
|
|
575
|
-
yValue = val; // For now, keep absolute - would need axis min
|
|
576
|
-
symbolOffset = [0, 0];
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
const symbol = SeriesBuilder.getShapeSymbol(shape);
|
|
580
|
-
const symbolSize = SeriesBuilder.getShapeSize(size, width, height);
|
|
581
|
-
const rotate = SeriesBuilder.getShapeRotation(shape);
|
|
582
|
-
|
|
583
|
-
// Debug logging (remove after testing)
|
|
584
|
-
// if (width !== undefined || height !== undefined) {
|
|
585
|
-
// console.log('[Shape Size Debug]', { symbolSize, width, height, size });
|
|
586
|
-
// }
|
|
587
|
-
|
|
588
|
-
// Special handling for labelup/down sizing - they contain text so they should be larger
|
|
589
|
-
let finalSize: number | number[] = symbolSize;
|
|
590
|
-
if (shape.includes('label')) {
|
|
591
|
-
// If custom size, scale it up for labels
|
|
592
|
-
if (Array.isArray(symbolSize)) {
|
|
593
|
-
finalSize = [symbolSize[0] * 2.5, symbolSize[1] * 2.5];
|
|
594
|
-
} else {
|
|
595
|
-
finalSize = symbolSize * 2.5;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Get label configuration based on location
|
|
600
|
-
const labelConfig = SeriesBuilder.getLabelConfig(shape, location);
|
|
601
|
-
|
|
602
|
-
const item: any = {
|
|
603
|
-
value: [i, yValue],
|
|
604
|
-
symbol: symbol,
|
|
605
|
-
symbolSize: finalSize,
|
|
606
|
-
symbolRotate: rotate,
|
|
607
|
-
symbolOffset: symbolOffset,
|
|
608
|
-
itemStyle: {
|
|
609
|
-
color: color,
|
|
610
|
-
},
|
|
611
|
-
label: {
|
|
612
|
-
show: !!text,
|
|
613
|
-
position: labelConfig.position,
|
|
614
|
-
distance: labelConfig.distance,
|
|
615
|
-
formatter: text,
|
|
616
|
-
color: textColor,
|
|
617
|
-
fontSize: 10,
|
|
618
|
-
fontWeight: 'bold',
|
|
619
|
-
},
|
|
620
|
-
};
|
|
621
|
-
|
|
622
|
-
return item;
|
|
623
|
-
})
|
|
624
|
-
.filter((item) => item !== null);
|
|
625
|
-
|
|
626
|
-
series.push({
|
|
627
|
-
name: seriesName,
|
|
628
|
-
type: 'scatter',
|
|
629
|
-
xAxisIndex: xAxisIndex,
|
|
630
|
-
yAxisIndex: yAxisIndex,
|
|
631
|
-
data: shapeData,
|
|
632
|
-
});
|
|
633
|
-
break;
|
|
634
|
-
|
|
635
|
-
case 'background':
|
|
636
|
-
series.push({
|
|
637
|
-
name: seriesName,
|
|
638
|
-
type: 'custom',
|
|
639
|
-
xAxisIndex: xAxisIndex,
|
|
640
|
-
yAxisIndex: yAxisIndex,
|
|
641
|
-
z: -10,
|
|
642
|
-
renderItem: (params: any, api: any) => {
|
|
643
|
-
const xVal = api.value(0);
|
|
644
|
-
if (isNaN(xVal)) return;
|
|
645
|
-
|
|
646
|
-
const start = api.coord([xVal, 0]);
|
|
647
|
-
const size = api.size([1, 0]);
|
|
648
|
-
const width = size[0];
|
|
649
|
-
const sys = params.coordSys;
|
|
650
|
-
const x = start[0] - width / 2;
|
|
651
|
-
const barColor = colorArray[params.dataIndex];
|
|
652
|
-
const val = api.value(1);
|
|
653
|
-
|
|
654
|
-
if (!barColor || val === null || val === undefined || isNaN(val)) return;
|
|
655
|
-
|
|
656
|
-
return {
|
|
657
|
-
type: 'rect',
|
|
658
|
-
shape: {
|
|
659
|
-
x: x,
|
|
660
|
-
y: sys.y,
|
|
661
|
-
width: width,
|
|
662
|
-
height: sys.height,
|
|
663
|
-
},
|
|
664
|
-
style: {
|
|
665
|
-
fill: barColor,
|
|
666
|
-
opacity: 0.3,
|
|
667
|
-
},
|
|
668
|
-
silent: true,
|
|
669
|
-
};
|
|
670
|
-
},
|
|
671
|
-
data: dataArray.map((val, i) => [i, val]),
|
|
672
|
-
});
|
|
673
|
-
break;
|
|
674
|
-
|
|
675
|
-
case 'step':
|
|
676
|
-
series.push({
|
|
677
|
-
name: seriesName,
|
|
678
|
-
type: 'custom',
|
|
679
|
-
xAxisIndex: xAxisIndex,
|
|
680
|
-
yAxisIndex: yAxisIndex,
|
|
681
|
-
renderItem: (params: any, api: any) => {
|
|
682
|
-
const x = api.value(0);
|
|
683
|
-
const y = api.value(1);
|
|
684
|
-
if (isNaN(y) || y === null) return;
|
|
685
|
-
|
|
686
|
-
const coords = api.coord([x, y]);
|
|
687
|
-
const width = api.size([1, 0])[0];
|
|
688
|
-
|
|
689
|
-
return {
|
|
690
|
-
type: 'line',
|
|
691
|
-
shape: {
|
|
692
|
-
x1: coords[0] - width / 2,
|
|
693
|
-
y1: coords[1],
|
|
694
|
-
x2: coords[0] + width / 2,
|
|
695
|
-
y2: coords[1],
|
|
696
|
-
},
|
|
697
|
-
style: {
|
|
698
|
-
stroke: colorArray[params.dataIndex] || plot.options.color,
|
|
699
|
-
lineWidth: plot.options.linewidth || 1,
|
|
700
|
-
},
|
|
701
|
-
silent: true,
|
|
702
|
-
};
|
|
703
|
-
},
|
|
704
|
-
data: dataArray.map((val, i) => [i, val]),
|
|
705
|
-
});
|
|
706
|
-
break;
|
|
707
|
-
|
|
708
|
-
case 'barcolor':
|
|
709
|
-
// Apply colors to main chart candlesticks
|
|
710
|
-
// Don't create a visual series, just store colors in barColors array
|
|
711
|
-
plot.data.forEach((point) => {
|
|
712
|
-
const index = timeToIndex.get(point.time);
|
|
713
|
-
if (index !== undefined) {
|
|
714
|
-
const plotOffset = point.options?.offset ?? plot.options.offset ?? 0;
|
|
715
|
-
const offsetIndex = index + dataIndexOffset + plotOffset;
|
|
716
|
-
|
|
717
|
-
if (offsetIndex >= 0 && offsetIndex < totalDataLength) {
|
|
718
|
-
const pointColor = point.options?.color || plot.options.color;
|
|
719
|
-
// Only apply if color is valid (not 'na')
|
|
720
|
-
const isNaColor =
|
|
721
|
-
pointColor === null ||
|
|
722
|
-
pointColor === 'na' ||
|
|
723
|
-
pointColor === 'NaN' ||
|
|
724
|
-
(typeof pointColor === 'number' && isNaN(pointColor));
|
|
725
|
-
|
|
726
|
-
if (!isNaColor && point.value !== null && point.value !== undefined) {
|
|
727
|
-
// Only apply color if value is defined (allow 0)
|
|
728
|
-
barColors[offsetIndex] = pointColor;
|
|
729
|
-
}
|
|
192
|
+
// Handle barcolor specifically as it modifies shared state (barColors)
|
|
193
|
+
if (plot.options.style === 'barcolor') {
|
|
194
|
+
// Apply colors to main chart candlesticks
|
|
195
|
+
plot.data?.forEach((point) => {
|
|
196
|
+
const index = timeToIndex.get(point.time);
|
|
197
|
+
if (index !== undefined) {
|
|
198
|
+
const plotOffset = point.options?.offset ?? plot.options.offset ?? 0;
|
|
199
|
+
const offsetIndex = index + dataIndexOffset + plotOffset;
|
|
200
|
+
|
|
201
|
+
if (offsetIndex >= 0 && offsetIndex < totalDataLength) {
|
|
202
|
+
const pointColor = point.options?.color || plot.options.color || SeriesBuilder.DEFAULT_COLOR;
|
|
203
|
+
const isNaColor =
|
|
204
|
+
pointColor === null ||
|
|
205
|
+
pointColor === 'na' ||
|
|
206
|
+
pointColor === 'NaN' ||
|
|
207
|
+
(typeof pointColor === 'number' && isNaN(pointColor));
|
|
208
|
+
|
|
209
|
+
if (!isNaColor && point.value !== null && point.value !== undefined) {
|
|
210
|
+
barColors[offsetIndex] = pointColor;
|
|
730
211
|
}
|
|
731
212
|
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
// Invisible series - data only shown in tooltip/sidebar
|
|
737
|
-
series.push({
|
|
738
|
-
name: seriesName,
|
|
739
|
-
type: 'scatter',
|
|
740
|
-
xAxisIndex: xAxisIndex,
|
|
741
|
-
yAxisIndex: yAxisIndex,
|
|
742
|
-
symbolSize: 0, // Invisible
|
|
743
|
-
data: dataArray.map((val, i) => ({
|
|
744
|
-
value: [i, val],
|
|
745
|
-
itemStyle: { opacity: 0 },
|
|
746
|
-
})),
|
|
747
|
-
silent: true, // No interaction
|
|
748
|
-
});
|
|
749
|
-
break;
|
|
750
|
-
|
|
751
|
-
case 'line':
|
|
752
|
-
default:
|
|
753
|
-
series.push({
|
|
754
|
-
name: seriesName,
|
|
755
|
-
type: 'custom',
|
|
756
|
-
xAxisIndex: xAxisIndex,
|
|
757
|
-
yAxisIndex: yAxisIndex,
|
|
758
|
-
renderItem: (params: any, api: any) => {
|
|
759
|
-
const index = params.dataIndex;
|
|
760
|
-
if (index === 0) return; // Need at least two points for a line segment
|
|
761
|
-
|
|
762
|
-
const y2 = api.value(1);
|
|
763
|
-
const y1 = api.value(2); // We'll store prevValue in the data
|
|
764
|
-
|
|
765
|
-
if (y2 === null || isNaN(y2) || y1 === null || isNaN(y1)) return;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
return; // Skip rendering a series for barcolor
|
|
216
|
+
}
|
|
766
217
|
|
|
767
|
-
|
|
768
|
-
|
|
218
|
+
// Use Factory to get appropriate renderer
|
|
219
|
+
const renderer = SeriesRendererFactory.get(plot.options.style);
|
|
220
|
+
const seriesConfig = renderer.render({
|
|
221
|
+
seriesName,
|
|
222
|
+
xAxisIndex,
|
|
223
|
+
yAxisIndex,
|
|
224
|
+
dataArray,
|
|
225
|
+
colorArray,
|
|
226
|
+
optionsArray,
|
|
227
|
+
plotOptions: plot.options,
|
|
228
|
+
candlestickData,
|
|
229
|
+
plotDataArrays,
|
|
230
|
+
indicatorId: id,
|
|
231
|
+
plotName: plotName
|
|
232
|
+
});
|
|
769
233
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
shape: {
|
|
773
|
-
x1: p1[0],
|
|
774
|
-
y1: p1[1],
|
|
775
|
-
x2: p2[0],
|
|
776
|
-
y2: p2[1],
|
|
777
|
-
},
|
|
778
|
-
style: {
|
|
779
|
-
stroke: colorArray[index] || plot.options.color,
|
|
780
|
-
lineWidth: plot.options.linewidth || 1,
|
|
781
|
-
},
|
|
782
|
-
silent: true,
|
|
783
|
-
};
|
|
784
|
-
},
|
|
785
|
-
// Data format: [index, value, prevValue]
|
|
786
|
-
data: dataArray.map((val, i) => [i, val, i > 0 ? dataArray[i - 1] : null]),
|
|
787
|
-
});
|
|
788
|
-
break;
|
|
234
|
+
if (seriesConfig) {
|
|
235
|
+
series.push(seriesConfig);
|
|
789
236
|
}
|
|
790
237
|
});
|
|
791
238
|
});
|