@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.
@@ -1,8 +1,10 @@
1
1
  import { OHLCV, Indicator as IndicatorType, QFChartOptions, IndicatorPlot, IndicatorStyle } from '../types';
2
2
  import { PaneConfiguration } from './LayoutManager';
3
- import { textToBase64Image } from '../Utils';
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
- Object.keys(indicator.plots).forEach((plotName) => {
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.forEach((point) => {
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
- if (plot.options.style === 'cross') {
336
- item.symbol = `image://${textToBase64Image('+', pointColor, '24px')}`;
337
- item.symbolSize = 16;
338
- } else {
339
- item.symbol = 'circle';
340
- item.symbolSize = 6;
341
- }
342
- return item;
343
- })
344
- .filter((item) => item !== null);
345
-
346
- series.push({
347
- name: seriesName,
348
- type: 'scatter',
349
- xAxisIndex: xAxisIndex,
350
- yAxisIndex: yAxisIndex,
351
- data: scatterData,
352
- });
353
- break;
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
- break;
734
-
735
- case 'char':
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
- const p1 = api.coord([index - 1, y1]);
768
- const p2 = api.coord([index, y2]);
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
- return {
771
- type: 'line',
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
  });