@qfo/qfchart 0.6.6 → 0.6.8
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 +1 -1
- package/dist/qfchart.min.browser.js +16 -16
- package/dist/qfchart.min.es.js +16 -16
- package/package.json +81 -81
- package/src/QFChart.ts +1434 -1434
- package/src/components/SeriesBuilder.ts +251 -250
- package/src/components/SeriesRendererFactory.ts +42 -36
- package/src/components/renderers/DrawingLineRenderer.ts +188 -0
- package/src/components/renderers/FillRenderer.ts +99 -99
- package/src/components/renderers/LabelRenderer.ts +274 -0
- package/src/components/renderers/LinefillRenderer.ts +167 -0
- package/src/components/renderers/SeriesRenderer.ts +21 -20
- package/src/types.ts +207 -205
- package/src/utils/ShapeUtils.ts +148 -140
|
@@ -1,250 +1,251 @@
|
|
|
1
|
-
import { OHLCV, Indicator as IndicatorType, QFChartOptions, IndicatorPlot, IndicatorStyle } from '../types';
|
|
2
|
-
import { PaneConfiguration } from './LayoutManager';
|
|
3
|
-
import { SeriesRendererFactory } from './SeriesRendererFactory';
|
|
4
|
-
import { AxisUtils } from '../utils/AxisUtils';
|
|
5
|
-
|
|
6
|
-
export class SeriesBuilder {
|
|
7
|
-
private static readonly DEFAULT_COLOR = '#2962ff';
|
|
8
|
-
|
|
9
|
-
public static buildCandlestickSeries(marketData: OHLCV[], options: QFChartOptions, totalLength?: number): any {
|
|
10
|
-
const upColor = options.upColor || '#00da3c';
|
|
11
|
-
const downColor = options.downColor || '#ec0000';
|
|
12
|
-
|
|
13
|
-
const data = marketData.map((d) => [d.open, d.close, d.low, d.high]);
|
|
14
|
-
|
|
15
|
-
// Pad with nulls if totalLength is provided and greater than current data length
|
|
16
|
-
if (totalLength && totalLength > data.length) {
|
|
17
|
-
const padding = totalLength - data.length;
|
|
18
|
-
for (let i = 0; i < padding; i++) {
|
|
19
|
-
data.push(null as any);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Build markLine for last price if enabled
|
|
24
|
-
let markLine = undefined;
|
|
25
|
-
if (options.lastPriceLine?.visible !== false && marketData.length > 0) {
|
|
26
|
-
const lastBar = marketData[marketData.length - 1];
|
|
27
|
-
const lastClose = lastBar.close;
|
|
28
|
-
const isUp = lastBar.close >= lastBar.open;
|
|
29
|
-
// Use configured color, or dynamic color based on candle direction
|
|
30
|
-
const lineColor = options.lastPriceLine?.color || (isUp ? upColor : downColor);
|
|
31
|
-
let lineStyleType = options.lastPriceLine?.lineStyle || 'dashed';
|
|
32
|
-
|
|
33
|
-
if (lineStyleType.startsWith('linestyle_')) {
|
|
34
|
-
lineStyleType = lineStyleType.replace('linestyle_', '') as any;
|
|
35
|
-
}
|
|
36
|
-
const decimals = options.yAxisDecimalPlaces !== undefined
|
|
37
|
-
? options.yAxisDecimalPlaces
|
|
38
|
-
: AxisUtils.autoDetectDecimals(marketData);
|
|
39
|
-
|
|
40
|
-
markLine = {
|
|
41
|
-
symbol: ['none', 'none'],
|
|
42
|
-
precision: decimals, // Ensure line position is precise enough for small values
|
|
43
|
-
data: [
|
|
44
|
-
{
|
|
45
|
-
yAxis: lastClose,
|
|
46
|
-
label: {
|
|
47
|
-
show: true,
|
|
48
|
-
position: 'end', // Right side
|
|
49
|
-
formatter: (params: any) => {
|
|
50
|
-
// Respect Y-axis formatting options
|
|
51
|
-
if (options.yAxisLabelFormatter) {
|
|
52
|
-
return options.yAxisLabelFormatter(params.value);
|
|
53
|
-
}
|
|
54
|
-
return AxisUtils.formatValue(params.value, decimals);
|
|
55
|
-
},
|
|
56
|
-
color: '#fff',
|
|
57
|
-
backgroundColor: lineColor,
|
|
58
|
-
padding: [2, 4],
|
|
59
|
-
borderRadius: 2,
|
|
60
|
-
fontSize: 11,
|
|
61
|
-
fontWeight: 'bold',
|
|
62
|
-
},
|
|
63
|
-
lineStyle: {
|
|
64
|
-
color: lineColor,
|
|
65
|
-
type: lineStyleType,
|
|
66
|
-
width: 1,
|
|
67
|
-
opacity: 0.8,
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
animation: false,
|
|
72
|
-
silent: true, // Disable interaction
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
type: 'candlestick',
|
|
78
|
-
name: options.title || 'Market',
|
|
79
|
-
data: data,
|
|
80
|
-
itemStyle: {
|
|
81
|
-
color: upColor,
|
|
82
|
-
color0: downColor,
|
|
83
|
-
borderColor: upColor,
|
|
84
|
-
borderColor0: downColor,
|
|
85
|
-
},
|
|
86
|
-
markLine: markLine,
|
|
87
|
-
xAxisIndex: 0,
|
|
88
|
-
yAxisIndex: 0,
|
|
89
|
-
z: 5,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
public static buildIndicatorSeries(
|
|
94
|
-
indicators: Map<string, IndicatorType>,
|
|
95
|
-
timeToIndex: Map<number, number>,
|
|
96
|
-
paneLayout: PaneConfiguration[],
|
|
97
|
-
totalDataLength: number,
|
|
98
|
-
dataIndexOffset: number = 0,
|
|
99
|
-
candlestickData?: OHLCV[], // Add candlestick data to access High/Low for positioning
|
|
100
|
-
overlayYAxisMap?: Map<string, number>, // Map of overlay indicator IDs to their Y-axis indices
|
|
101
|
-
separatePaneYAxisOffset: number = 1 // Offset for separate pane Y-axes (accounts for overlay axes)
|
|
102
|
-
): { series: any[]; barColors: (string | null)[] } {
|
|
103
|
-
const series: any[] = [];
|
|
104
|
-
const barColors: (string | null)[] = new Array(totalDataLength).fill(null);
|
|
105
|
-
|
|
106
|
-
// Store plot data arrays for fill plots to reference
|
|
107
|
-
const plotDataArrays = new Map<string, number[]>();
|
|
108
|
-
|
|
109
|
-
indicators.forEach((indicator, id) => {
|
|
110
|
-
if (indicator.collapsed) return; // Skip if collapsed
|
|
111
|
-
|
|
112
|
-
// Sort plots so that 'fill' plots are processed last
|
|
113
|
-
// This ensures that the plots they reference (plot1, plot2) have already been processed and their data stored
|
|
114
|
-
const sortedPlots = Object.keys(indicator.plots).sort((a, b) => {
|
|
115
|
-
const plotA = indicator.plots[a];
|
|
116
|
-
const plotB = indicator.plots[b];
|
|
117
|
-
const isFillA = plotA.options.style === 'fill';
|
|
118
|
-
const isFillB = plotB.options.style === 'fill';
|
|
119
|
-
if (isFillA && !isFillB) return 1;
|
|
120
|
-
if (!isFillA && isFillB) return -1;
|
|
121
|
-
return 0;
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
sortedPlots.forEach((plotName) => {
|
|
125
|
-
const plot = indicator.plots[plotName];
|
|
126
|
-
const seriesName = `${id}::${plotName}`;
|
|
127
|
-
|
|
128
|
-
// Find axis index for THIS SPECIFIC PLOT
|
|
129
|
-
let xAxisIndex = 0;
|
|
130
|
-
let yAxisIndex = 0;
|
|
131
|
-
|
|
132
|
-
// Check plot-level overlay setting (overrides indicator-level setting)
|
|
133
|
-
// IMPORTANT: If indicator is overlay (paneIndex === 0), treat all plots as overlays
|
|
134
|
-
// This allows visual-only plots (background, barcolor) to have separate Y-axes while
|
|
135
|
-
// still being on the main chart pane
|
|
136
|
-
const plotOverlay = plot.options.overlay;
|
|
137
|
-
const isPlotOverlay = indicator.paneIndex === 0 || plotOverlay === true;
|
|
138
|
-
|
|
139
|
-
if (isPlotOverlay) {
|
|
140
|
-
// Plot should be on main chart (overlay)
|
|
141
|
-
xAxisIndex = 0;
|
|
142
|
-
if (overlayYAxisMap && overlayYAxisMap.has(seriesName)) {
|
|
143
|
-
// This specific plot has its own Y-axis (incompatible with price range)
|
|
144
|
-
yAxisIndex = overlayYAxisMap.get(seriesName)!;
|
|
145
|
-
} else {
|
|
146
|
-
// Shares main Y-axis with candlesticks
|
|
147
|
-
yAxisIndex = 0;
|
|
148
|
-
}
|
|
149
|
-
} else {
|
|
150
|
-
// Plot should be in indicator's separate pane
|
|
151
|
-
const confIndex = paneLayout.findIndex((p) => p.index === indicator.paneIndex);
|
|
152
|
-
if (confIndex !== -1) {
|
|
153
|
-
xAxisIndex = confIndex + 1;
|
|
154
|
-
yAxisIndex = separatePaneYAxisOffset + confIndex;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Prepare data arrays
|
|
159
|
-
// For 'fill' style, we don't use plot.data directly in the same way, but we initialize generic arrays
|
|
160
|
-
const dataArray = new Array(totalDataLength).fill(null);
|
|
161
|
-
const colorArray = new Array(totalDataLength).fill(null);
|
|
162
|
-
const optionsArray = new Array(totalDataLength).fill(null); // Store per-point options
|
|
163
|
-
|
|
164
|
-
plot.data?.forEach((point) => {
|
|
165
|
-
const index = timeToIndex.get(point.time);
|
|
166
|
-
if (index !== undefined) {
|
|
167
|
-
const plotOffset = point.options?.offset ?? plot.options.offset ?? 0;
|
|
168
|
-
const offsetIndex = index + dataIndexOffset + plotOffset;
|
|
169
|
-
|
|
170
|
-
if (offsetIndex >= 0 && offsetIndex < totalDataLength) {
|
|
171
|
-
let value = point.value;
|
|
172
|
-
const pointColor = point.options?.color;
|
|
173
|
-
|
|
174
|
-
// TradingView compatibility: if color is 'na' (NaN, null, or "na"), break the line
|
|
175
|
-
const isNaColor =
|
|
176
|
-
pointColor === null ||
|
|
177
|
-
pointColor === 'na' ||
|
|
178
|
-
pointColor === 'NaN' ||
|
|
179
|
-
(typeof pointColor === 'number' && isNaN(pointColor));
|
|
180
|
-
|
|
181
|
-
if (isNaColor) {
|
|
182
|
-
value = null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
dataArray[offsetIndex] = value;
|
|
186
|
-
colorArray[offsetIndex] = pointColor || plot.options.color || SeriesBuilder.DEFAULT_COLOR;
|
|
187
|
-
optionsArray[offsetIndex] = point.options || {};
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// Store data array for fill plots to reference
|
|
193
|
-
// Only store for non-fill plots as fill plots don't produce data to be referenced by other fills (usually)
|
|
194
|
-
plotDataArrays.set(`${id}::${plotName}`, dataArray);
|
|
195
|
-
|
|
196
|
-
if (plot.options?.style?.startsWith('style_')) {
|
|
197
|
-
plot.options.style = plot.options.style.replace('style_', '') as IndicatorStyle;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Handle barcolor specifically as it modifies shared state (barColors)
|
|
201
|
-
if (plot.options.style === 'barcolor') {
|
|
202
|
-
// Apply colors to main chart candlesticks
|
|
203
|
-
plot.data?.forEach((point) => {
|
|
204
|
-
const index = timeToIndex.get(point.time);
|
|
205
|
-
if (index !== undefined) {
|
|
206
|
-
const plotOffset = point.options?.offset ?? plot.options.offset ?? 0;
|
|
207
|
-
const offsetIndex = index + dataIndexOffset + plotOffset;
|
|
208
|
-
|
|
209
|
-
if (offsetIndex >= 0 && offsetIndex < totalDataLength) {
|
|
210
|
-
const pointColor = point.options?.color || plot.options.color || SeriesBuilder.DEFAULT_COLOR;
|
|
211
|
-
const isNaColor =
|
|
212
|
-
pointColor === null ||
|
|
213
|
-
pointColor === 'na' ||
|
|
214
|
-
pointColor === 'NaN' ||
|
|
215
|
-
(typeof pointColor === 'number' && isNaN(pointColor));
|
|
216
|
-
|
|
217
|
-
if (!isNaColor && point.value !== null && point.value !== undefined) {
|
|
218
|
-
barColors[offsetIndex] = pointColor;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
return; // Skip rendering a series for barcolor
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Use Factory to get appropriate renderer
|
|
227
|
-
const renderer = SeriesRendererFactory.get(plot.options.style);
|
|
228
|
-
const seriesConfig = renderer.render({
|
|
229
|
-
seriesName,
|
|
230
|
-
xAxisIndex,
|
|
231
|
-
yAxisIndex,
|
|
232
|
-
dataArray,
|
|
233
|
-
colorArray,
|
|
234
|
-
optionsArray,
|
|
235
|
-
plotOptions: plot.options,
|
|
236
|
-
candlestickData,
|
|
237
|
-
plotDataArrays,
|
|
238
|
-
indicatorId: id,
|
|
239
|
-
plotName: plotName
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
1
|
+
import { OHLCV, Indicator as IndicatorType, QFChartOptions, IndicatorPlot, IndicatorStyle } from '../types';
|
|
2
|
+
import { PaneConfiguration } from './LayoutManager';
|
|
3
|
+
import { SeriesRendererFactory } from './SeriesRendererFactory';
|
|
4
|
+
import { AxisUtils } from '../utils/AxisUtils';
|
|
5
|
+
|
|
6
|
+
export class SeriesBuilder {
|
|
7
|
+
private static readonly DEFAULT_COLOR = '#2962ff';
|
|
8
|
+
|
|
9
|
+
public static buildCandlestickSeries(marketData: OHLCV[], options: QFChartOptions, totalLength?: number): any {
|
|
10
|
+
const upColor = options.upColor || '#00da3c';
|
|
11
|
+
const downColor = options.downColor || '#ec0000';
|
|
12
|
+
|
|
13
|
+
const data = marketData.map((d) => [d.open, d.close, d.low, d.high]);
|
|
14
|
+
|
|
15
|
+
// Pad with nulls if totalLength is provided and greater than current data length
|
|
16
|
+
if (totalLength && totalLength > data.length) {
|
|
17
|
+
const padding = totalLength - data.length;
|
|
18
|
+
for (let i = 0; i < padding; i++) {
|
|
19
|
+
data.push(null as any);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Build markLine for last price if enabled
|
|
24
|
+
let markLine = undefined;
|
|
25
|
+
if (options.lastPriceLine?.visible !== false && marketData.length > 0) {
|
|
26
|
+
const lastBar = marketData[marketData.length - 1];
|
|
27
|
+
const lastClose = lastBar.close;
|
|
28
|
+
const isUp = lastBar.close >= lastBar.open;
|
|
29
|
+
// Use configured color, or dynamic color based on candle direction
|
|
30
|
+
const lineColor = options.lastPriceLine?.color || (isUp ? upColor : downColor);
|
|
31
|
+
let lineStyleType = options.lastPriceLine?.lineStyle || 'dashed';
|
|
32
|
+
|
|
33
|
+
if (lineStyleType.startsWith('linestyle_')) {
|
|
34
|
+
lineStyleType = lineStyleType.replace('linestyle_', '') as any;
|
|
35
|
+
}
|
|
36
|
+
const decimals = options.yAxisDecimalPlaces !== undefined
|
|
37
|
+
? options.yAxisDecimalPlaces
|
|
38
|
+
: AxisUtils.autoDetectDecimals(marketData);
|
|
39
|
+
|
|
40
|
+
markLine = {
|
|
41
|
+
symbol: ['none', 'none'],
|
|
42
|
+
precision: decimals, // Ensure line position is precise enough for small values
|
|
43
|
+
data: [
|
|
44
|
+
{
|
|
45
|
+
yAxis: lastClose,
|
|
46
|
+
label: {
|
|
47
|
+
show: true,
|
|
48
|
+
position: 'end', // Right side
|
|
49
|
+
formatter: (params: any) => {
|
|
50
|
+
// Respect Y-axis formatting options
|
|
51
|
+
if (options.yAxisLabelFormatter) {
|
|
52
|
+
return options.yAxisLabelFormatter(params.value);
|
|
53
|
+
}
|
|
54
|
+
return AxisUtils.formatValue(params.value, decimals);
|
|
55
|
+
},
|
|
56
|
+
color: '#fff',
|
|
57
|
+
backgroundColor: lineColor,
|
|
58
|
+
padding: [2, 4],
|
|
59
|
+
borderRadius: 2,
|
|
60
|
+
fontSize: 11,
|
|
61
|
+
fontWeight: 'bold',
|
|
62
|
+
},
|
|
63
|
+
lineStyle: {
|
|
64
|
+
color: lineColor,
|
|
65
|
+
type: lineStyleType,
|
|
66
|
+
width: 1,
|
|
67
|
+
opacity: 0.8,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
animation: false,
|
|
72
|
+
silent: true, // Disable interaction
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
type: 'candlestick',
|
|
78
|
+
name: options.title || 'Market',
|
|
79
|
+
data: data,
|
|
80
|
+
itemStyle: {
|
|
81
|
+
color: upColor,
|
|
82
|
+
color0: downColor,
|
|
83
|
+
borderColor: upColor,
|
|
84
|
+
borderColor0: downColor,
|
|
85
|
+
},
|
|
86
|
+
markLine: markLine,
|
|
87
|
+
xAxisIndex: 0,
|
|
88
|
+
yAxisIndex: 0,
|
|
89
|
+
z: 5,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public static buildIndicatorSeries(
|
|
94
|
+
indicators: Map<string, IndicatorType>,
|
|
95
|
+
timeToIndex: Map<number, number>,
|
|
96
|
+
paneLayout: PaneConfiguration[],
|
|
97
|
+
totalDataLength: number,
|
|
98
|
+
dataIndexOffset: number = 0,
|
|
99
|
+
candlestickData?: OHLCV[], // Add candlestick data to access High/Low for positioning
|
|
100
|
+
overlayYAxisMap?: Map<string, number>, // Map of overlay indicator IDs to their Y-axis indices
|
|
101
|
+
separatePaneYAxisOffset: number = 1 // Offset for separate pane Y-axes (accounts for overlay axes)
|
|
102
|
+
): { series: any[]; barColors: (string | null)[] } {
|
|
103
|
+
const series: any[] = [];
|
|
104
|
+
const barColors: (string | null)[] = new Array(totalDataLength).fill(null);
|
|
105
|
+
|
|
106
|
+
// Store plot data arrays for fill plots to reference
|
|
107
|
+
const plotDataArrays = new Map<string, number[]>();
|
|
108
|
+
|
|
109
|
+
indicators.forEach((indicator, id) => {
|
|
110
|
+
if (indicator.collapsed) return; // Skip if collapsed
|
|
111
|
+
|
|
112
|
+
// Sort plots so that 'fill' plots are processed last
|
|
113
|
+
// This ensures that the plots they reference (plot1, plot2) have already been processed and their data stored
|
|
114
|
+
const sortedPlots = Object.keys(indicator.plots).sort((a, b) => {
|
|
115
|
+
const plotA = indicator.plots[a];
|
|
116
|
+
const plotB = indicator.plots[b];
|
|
117
|
+
const isFillA = plotA.options.style === 'fill';
|
|
118
|
+
const isFillB = plotB.options.style === 'fill';
|
|
119
|
+
if (isFillA && !isFillB) return 1;
|
|
120
|
+
if (!isFillA && isFillB) return -1;
|
|
121
|
+
return 0;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
sortedPlots.forEach((plotName) => {
|
|
125
|
+
const plot = indicator.plots[plotName];
|
|
126
|
+
const seriesName = `${id}::${plotName}`;
|
|
127
|
+
|
|
128
|
+
// Find axis index for THIS SPECIFIC PLOT
|
|
129
|
+
let xAxisIndex = 0;
|
|
130
|
+
let yAxisIndex = 0;
|
|
131
|
+
|
|
132
|
+
// Check plot-level overlay setting (overrides indicator-level setting)
|
|
133
|
+
// IMPORTANT: If indicator is overlay (paneIndex === 0), treat all plots as overlays
|
|
134
|
+
// This allows visual-only plots (background, barcolor) to have separate Y-axes while
|
|
135
|
+
// still being on the main chart pane
|
|
136
|
+
const plotOverlay = plot.options.overlay;
|
|
137
|
+
const isPlotOverlay = indicator.paneIndex === 0 || plotOverlay === true;
|
|
138
|
+
|
|
139
|
+
if (isPlotOverlay) {
|
|
140
|
+
// Plot should be on main chart (overlay)
|
|
141
|
+
xAxisIndex = 0;
|
|
142
|
+
if (overlayYAxisMap && overlayYAxisMap.has(seriesName)) {
|
|
143
|
+
// This specific plot has its own Y-axis (incompatible with price range)
|
|
144
|
+
yAxisIndex = overlayYAxisMap.get(seriesName)!;
|
|
145
|
+
} else {
|
|
146
|
+
// Shares main Y-axis with candlesticks
|
|
147
|
+
yAxisIndex = 0;
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
// Plot should be in indicator's separate pane
|
|
151
|
+
const confIndex = paneLayout.findIndex((p) => p.index === indicator.paneIndex);
|
|
152
|
+
if (confIndex !== -1) {
|
|
153
|
+
xAxisIndex = confIndex + 1;
|
|
154
|
+
yAxisIndex = separatePaneYAxisOffset + confIndex;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Prepare data arrays
|
|
159
|
+
// For 'fill' style, we don't use plot.data directly in the same way, but we initialize generic arrays
|
|
160
|
+
const dataArray = new Array(totalDataLength).fill(null);
|
|
161
|
+
const colorArray = new Array(totalDataLength).fill(null);
|
|
162
|
+
const optionsArray = new Array(totalDataLength).fill(null); // Store per-point options
|
|
163
|
+
|
|
164
|
+
plot.data?.forEach((point) => {
|
|
165
|
+
const index = timeToIndex.get(point.time);
|
|
166
|
+
if (index !== undefined) {
|
|
167
|
+
const plotOffset = point.options?.offset ?? plot.options.offset ?? 0;
|
|
168
|
+
const offsetIndex = index + dataIndexOffset + plotOffset;
|
|
169
|
+
|
|
170
|
+
if (offsetIndex >= 0 && offsetIndex < totalDataLength) {
|
|
171
|
+
let value = point.value;
|
|
172
|
+
const pointColor = point.options?.color;
|
|
173
|
+
|
|
174
|
+
// TradingView compatibility: if color is 'na' (NaN, null, or "na"), break the line
|
|
175
|
+
const isNaColor =
|
|
176
|
+
pointColor === null ||
|
|
177
|
+
pointColor === 'na' ||
|
|
178
|
+
pointColor === 'NaN' ||
|
|
179
|
+
(typeof pointColor === 'number' && isNaN(pointColor));
|
|
180
|
+
|
|
181
|
+
if (isNaColor) {
|
|
182
|
+
value = null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
dataArray[offsetIndex] = value;
|
|
186
|
+
colorArray[offsetIndex] = pointColor || plot.options.color || SeriesBuilder.DEFAULT_COLOR;
|
|
187
|
+
optionsArray[offsetIndex] = point.options || {};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Store data array for fill plots to reference
|
|
193
|
+
// Only store for non-fill plots as fill plots don't produce data to be referenced by other fills (usually)
|
|
194
|
+
plotDataArrays.set(`${id}::${plotName}`, dataArray);
|
|
195
|
+
|
|
196
|
+
if (plot.options?.style?.startsWith('style_')) {
|
|
197
|
+
plot.options.style = plot.options.style.replace('style_', '') as IndicatorStyle;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Handle barcolor specifically as it modifies shared state (barColors)
|
|
201
|
+
if (plot.options.style === 'barcolor') {
|
|
202
|
+
// Apply colors to main chart candlesticks
|
|
203
|
+
plot.data?.forEach((point) => {
|
|
204
|
+
const index = timeToIndex.get(point.time);
|
|
205
|
+
if (index !== undefined) {
|
|
206
|
+
const plotOffset = point.options?.offset ?? plot.options.offset ?? 0;
|
|
207
|
+
const offsetIndex = index + dataIndexOffset + plotOffset;
|
|
208
|
+
|
|
209
|
+
if (offsetIndex >= 0 && offsetIndex < totalDataLength) {
|
|
210
|
+
const pointColor = point.options?.color || plot.options.color || SeriesBuilder.DEFAULT_COLOR;
|
|
211
|
+
const isNaColor =
|
|
212
|
+
pointColor === null ||
|
|
213
|
+
pointColor === 'na' ||
|
|
214
|
+
pointColor === 'NaN' ||
|
|
215
|
+
(typeof pointColor === 'number' && isNaN(pointColor));
|
|
216
|
+
|
|
217
|
+
if (!isNaColor && point.value !== null && point.value !== undefined) {
|
|
218
|
+
barColors[offsetIndex] = pointColor;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return; // Skip rendering a series for barcolor
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Use Factory to get appropriate renderer
|
|
227
|
+
const renderer = SeriesRendererFactory.get(plot.options.style);
|
|
228
|
+
const seriesConfig = renderer.render({
|
|
229
|
+
seriesName,
|
|
230
|
+
xAxisIndex,
|
|
231
|
+
yAxisIndex,
|
|
232
|
+
dataArray,
|
|
233
|
+
colorArray,
|
|
234
|
+
optionsArray,
|
|
235
|
+
plotOptions: plot.options,
|
|
236
|
+
candlestickData,
|
|
237
|
+
plotDataArrays,
|
|
238
|
+
indicatorId: id,
|
|
239
|
+
plotName: plotName,
|
|
240
|
+
dataIndexOffset,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (seriesConfig) {
|
|
244
|
+
series.push(seriesConfig);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return { series, barColors };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -1,36 +1,42 @@
|
|
|
1
|
-
import { SeriesRenderer } from './renderers/SeriesRenderer';
|
|
2
|
-
import { LineRenderer } from './renderers/LineRenderer';
|
|
3
|
-
import { StepRenderer } from './renderers/StepRenderer';
|
|
4
|
-
import { HistogramRenderer } from './renderers/HistogramRenderer';
|
|
5
|
-
import { ScatterRenderer } from './renderers/ScatterRenderer';
|
|
6
|
-
import { OHLCBarRenderer } from './renderers/OHLCBarRenderer';
|
|
7
|
-
import { ShapeRenderer } from './renderers/ShapeRenderer';
|
|
8
|
-
import { BackgroundRenderer } from './renderers/BackgroundRenderer';
|
|
9
|
-
import { FillRenderer } from './renderers/FillRenderer';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
this.register('
|
|
19
|
-
this.register('
|
|
20
|
-
this.register('
|
|
21
|
-
this.register('
|
|
22
|
-
this.register('
|
|
23
|
-
this.register('
|
|
24
|
-
this.register('
|
|
25
|
-
this.register('
|
|
26
|
-
this.register('
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
import { SeriesRenderer } from './renderers/SeriesRenderer';
|
|
2
|
+
import { LineRenderer } from './renderers/LineRenderer';
|
|
3
|
+
import { StepRenderer } from './renderers/StepRenderer';
|
|
4
|
+
import { HistogramRenderer } from './renderers/HistogramRenderer';
|
|
5
|
+
import { ScatterRenderer } from './renderers/ScatterRenderer';
|
|
6
|
+
import { OHLCBarRenderer } from './renderers/OHLCBarRenderer';
|
|
7
|
+
import { ShapeRenderer } from './renderers/ShapeRenderer';
|
|
8
|
+
import { BackgroundRenderer } from './renderers/BackgroundRenderer';
|
|
9
|
+
import { FillRenderer } from './renderers/FillRenderer';
|
|
10
|
+
import { LabelRenderer } from './renderers/LabelRenderer';
|
|
11
|
+
import { DrawingLineRenderer } from './renderers/DrawingLineRenderer';
|
|
12
|
+
import { LinefillRenderer } from './renderers/LinefillRenderer';
|
|
13
|
+
|
|
14
|
+
export class SeriesRendererFactory {
|
|
15
|
+
private static renderers: Map<string, SeriesRenderer> = new Map();
|
|
16
|
+
|
|
17
|
+
static {
|
|
18
|
+
this.register('line', new LineRenderer());
|
|
19
|
+
this.register('step', new StepRenderer());
|
|
20
|
+
this.register('histogram', new HistogramRenderer());
|
|
21
|
+
this.register('columns', new HistogramRenderer());
|
|
22
|
+
this.register('circles', new ScatterRenderer());
|
|
23
|
+
this.register('cross', new ScatterRenderer());
|
|
24
|
+
this.register('char', new ScatterRenderer());
|
|
25
|
+
this.register('bar', new OHLCBarRenderer());
|
|
26
|
+
this.register('candle', new OHLCBarRenderer());
|
|
27
|
+
this.register('shape', new ShapeRenderer());
|
|
28
|
+
this.register('background', new BackgroundRenderer());
|
|
29
|
+
this.register('fill', new FillRenderer());
|
|
30
|
+
this.register('label', new LabelRenderer());
|
|
31
|
+
this.register('drawing_line', new DrawingLineRenderer());
|
|
32
|
+
this.register('linefill', new LinefillRenderer());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public static register(style: string, renderer: SeriesRenderer) {
|
|
36
|
+
this.renderers.set(style, renderer);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public static get(style: string): SeriesRenderer {
|
|
40
|
+
return this.renderers.get(style) || this.renderers.get('line')!; // Default to line
|
|
41
|
+
}
|
|
42
|
+
}
|