@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.
@@ -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
- if (seriesConfig) {
243
- series.push(seriesConfig);
244
- }
245
- });
246
- });
247
-
248
- return { series, barColors };
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
- export class SeriesRendererFactory {
12
- private static renderers: Map<string, SeriesRenderer> = new Map();
13
-
14
- static {
15
- this.register('line', new LineRenderer());
16
- this.register('step', new StepRenderer());
17
- this.register('histogram', new HistogramRenderer());
18
- this.register('columns', new HistogramRenderer());
19
- this.register('circles', new ScatterRenderer());
20
- this.register('cross', new ScatterRenderer());
21
- this.register('char', new ScatterRenderer());
22
- this.register('bar', new OHLCBarRenderer());
23
- this.register('candle', new OHLCBarRenderer());
24
- this.register('shape', new ShapeRenderer());
25
- this.register('background', new BackgroundRenderer());
26
- this.register('fill', new FillRenderer());
27
- }
28
-
29
- public static register(style: string, renderer: SeriesRenderer) {
30
- this.renderers.set(style, renderer);
31
- }
32
-
33
- public static get(style: string): SeriesRenderer {
34
- return this.renderers.get(style) || this.renderers.get('line')!; // Default to line
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
+ }