@qfo/qfchart 0.7.2 → 0.8.0
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 +54 -2
- package/dist/qfchart.min.browser.js +16 -14
- package/dist/qfchart.min.es.js +16 -14
- package/package.json +1 -1
- package/src/QFChart.ts +533 -58
- package/src/components/GraphicBuilder.ts +284 -263
- package/src/components/LayoutManager.ts +67 -24
- package/src/components/SeriesBuilder.ts +122 -1
- package/src/components/TableCanvasRenderer.ts +467 -0
- package/src/components/TableOverlayRenderer.ts +76 -24
- package/src/components/TooltipFormatter.ts +97 -97
- package/src/components/renderers/BackgroundRenderer.ts +59 -47
- package/src/components/renderers/BoxRenderer.ts +133 -37
- package/src/components/renderers/DrawingLineRenderer.ts +12 -16
- package/src/components/renderers/FillRenderer.ts +118 -3
- package/src/components/renderers/HistogramRenderer.ts +67 -20
- package/src/components/renderers/LabelRenderer.ts +35 -9
- package/src/components/renderers/LinefillRenderer.ts +4 -12
- package/src/components/renderers/OHLCBarRenderer.ts +171 -161
- package/src/components/renderers/PolylineRenderer.ts +32 -32
- package/src/types.ts +11 -2
- package/src/utils/ColorUtils.ts +1 -1
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
2
|
import { ColorUtils } from '../../utils/ColorUtils';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for a single fill band within a batched render.
|
|
6
|
+
*/
|
|
7
|
+
export interface BatchedFillEntry {
|
|
8
|
+
plot1Data: (number | null)[];
|
|
9
|
+
plot2Data: (number | null)[];
|
|
10
|
+
barColors: { color: string; opacity: number }[];
|
|
11
|
+
}
|
|
12
|
+
|
|
4
13
|
export class FillRenderer implements SeriesRenderer {
|
|
5
14
|
render(context: RenderContext): any {
|
|
6
15
|
const { seriesName, xAxisIndex, yAxisIndex, plotOptions, plotDataArrays, indicatorId, plotName, optionsArray } = context;
|
|
@@ -34,8 +43,25 @@ export class FillRenderer implements SeriesRenderer {
|
|
|
34
43
|
);
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
// --- Simple (
|
|
38
|
-
const { color:
|
|
46
|
+
// --- Simple fill (supports per-bar color when color is a series) ---
|
|
47
|
+
const { color: defaultFillColor, opacity: defaultFillOpacity } = ColorUtils.parseColor(plotOptions.color || 'rgba(128, 128, 128, 0.2)');
|
|
48
|
+
|
|
49
|
+
// Check if we have per-bar color data in optionsArray
|
|
50
|
+
const hasPerBarColor = optionsArray?.some((o: any) => o && o.color !== undefined);
|
|
51
|
+
|
|
52
|
+
// Pre-parse per-bar colors for efficiency
|
|
53
|
+
let barColors: { color: string; opacity: number }[] | null = null;
|
|
54
|
+
if (hasPerBarColor) {
|
|
55
|
+
barColors = [];
|
|
56
|
+
for (let i = 0; i < totalDataLength; i++) {
|
|
57
|
+
const opts = optionsArray?.[i];
|
|
58
|
+
if (opts && opts.color !== undefined) {
|
|
59
|
+
barColors[i] = ColorUtils.parseColor(opts.color);
|
|
60
|
+
} else {
|
|
61
|
+
barColors[i] = { color: defaultFillColor, opacity: defaultFillOpacity };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
39
65
|
|
|
40
66
|
// Create fill data with previous values for smooth polygon rendering
|
|
41
67
|
const fillDataWithPrev: any[] = [];
|
|
@@ -54,6 +80,9 @@ export class FillRenderer implements SeriesRenderer {
|
|
|
54
80
|
xAxisIndex: xAxisIndex,
|
|
55
81
|
yAxisIndex: yAxisIndex,
|
|
56
82
|
z: 1,
|
|
83
|
+
clip: true,
|
|
84
|
+
encode: { x: 0 },
|
|
85
|
+
animation: false,
|
|
57
86
|
renderItem: (params: any, api: any) => {
|
|
58
87
|
const index = params.dataIndex;
|
|
59
88
|
if (index === 0) return null;
|
|
@@ -70,6 +99,12 @@ export class FillRenderer implements SeriesRenderer {
|
|
|
70
99
|
return null;
|
|
71
100
|
}
|
|
72
101
|
|
|
102
|
+
const fc = barColors ? barColors[index] : null;
|
|
103
|
+
|
|
104
|
+
// Skip fully transparent fills
|
|
105
|
+
const fillOpacity = fc ? fc.opacity : defaultFillOpacity;
|
|
106
|
+
if (fillOpacity < 0.01) return null;
|
|
107
|
+
|
|
73
108
|
const p1Prev = api.coord([index - 1, prevY1]);
|
|
74
109
|
const p1Curr = api.coord([index, y1]);
|
|
75
110
|
const p2Curr = api.coord([index, y2]);
|
|
@@ -81,13 +116,86 @@ export class FillRenderer implements SeriesRenderer {
|
|
|
81
116
|
points: [p1Prev, p1Curr, p2Curr, p2Prev],
|
|
82
117
|
},
|
|
83
118
|
style: {
|
|
84
|
-
fill:
|
|
119
|
+
fill: fc ? fc.color : defaultFillColor,
|
|
85
120
|
opacity: fillOpacity,
|
|
86
121
|
},
|
|
87
122
|
silent: true,
|
|
88
123
|
};
|
|
89
124
|
},
|
|
90
125
|
data: fillDataWithPrev,
|
|
126
|
+
silent: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Batch-render multiple fill bands as a single ECharts custom series.
|
|
132
|
+
* Instead of N separate series (one per fill), this creates ONE series
|
|
133
|
+
* where each renderItem call draws all fill bands as a group of children.
|
|
134
|
+
*
|
|
135
|
+
* Performance: reduces series count from N to 1, eliminates per-series
|
|
136
|
+
* ECharts overhead, and enables viewport culling via clip + encode.
|
|
137
|
+
*/
|
|
138
|
+
renderBatched(
|
|
139
|
+
seriesName: string,
|
|
140
|
+
xAxisIndex: number,
|
|
141
|
+
yAxisIndex: number,
|
|
142
|
+
totalDataLength: number,
|
|
143
|
+
fills: BatchedFillEntry[]
|
|
144
|
+
): any {
|
|
145
|
+
// Simple index-only data for ECharts — encode: {x:0} enables dataZoom filtering
|
|
146
|
+
const data = Array.from({ length: totalDataLength }, (_, i) => [i]);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
name: seriesName,
|
|
150
|
+
type: 'custom',
|
|
151
|
+
xAxisIndex,
|
|
152
|
+
yAxisIndex,
|
|
153
|
+
z: 1,
|
|
154
|
+
clip: true,
|
|
155
|
+
encode: { x: 0 },
|
|
156
|
+
animation: false,
|
|
157
|
+
renderItem: (params: any, api: any) => {
|
|
158
|
+
const index = params.dataIndex;
|
|
159
|
+
if (index === 0) return null;
|
|
160
|
+
|
|
161
|
+
const children: any[] = [];
|
|
162
|
+
|
|
163
|
+
for (let f = 0; f < fills.length; f++) {
|
|
164
|
+
const fill = fills[f];
|
|
165
|
+
const y1 = fill.plot1Data[index];
|
|
166
|
+
const y2 = fill.plot2Data[index];
|
|
167
|
+
const prevY1 = fill.plot1Data[index - 1];
|
|
168
|
+
const prevY2 = fill.plot2Data[index - 1];
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
y1 == null || y2 == null || prevY1 == null || prevY2 == null ||
|
|
172
|
+
isNaN(y1 as number) || isNaN(y2 as number) ||
|
|
173
|
+
isNaN(prevY1 as number) || isNaN(prevY2 as number)
|
|
174
|
+
) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Skip fully transparent fills
|
|
179
|
+
const fc = fill.barColors[index];
|
|
180
|
+
if (!fc || fc.opacity < 0.01) continue;
|
|
181
|
+
|
|
182
|
+
const p1Prev = api.coord([index - 1, prevY1]);
|
|
183
|
+
const p1Curr = api.coord([index, y1]);
|
|
184
|
+
const p2Curr = api.coord([index, y2]);
|
|
185
|
+
const p2Prev = api.coord([index - 1, prevY2]);
|
|
186
|
+
|
|
187
|
+
children.push({
|
|
188
|
+
type: 'polygon',
|
|
189
|
+
shape: { points: [p1Prev, p1Curr, p2Curr, p2Prev] },
|
|
190
|
+
style: { fill: fc.color, opacity: fc.opacity },
|
|
191
|
+
silent: true,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return children.length > 0 ? { type: 'group', children, silent: true } : null;
|
|
196
|
+
},
|
|
197
|
+
data,
|
|
198
|
+
silent: true,
|
|
91
199
|
};
|
|
92
200
|
}
|
|
93
201
|
|
|
@@ -148,6 +256,9 @@ export class FillRenderer implements SeriesRenderer {
|
|
|
148
256
|
xAxisIndex: xAxisIndex,
|
|
149
257
|
yAxisIndex: yAxisIndex,
|
|
150
258
|
z: 1,
|
|
259
|
+
clip: true,
|
|
260
|
+
encode: { x: 0 },
|
|
261
|
+
animation: false,
|
|
151
262
|
renderItem: (params: any, api: any) => {
|
|
152
263
|
const index = params.dataIndex;
|
|
153
264
|
if (index === 0) return null;
|
|
@@ -173,6 +284,9 @@ export class FillRenderer implements SeriesRenderer {
|
|
|
173
284
|
const gc = gradientColors[index] || gradientColors[index - 1];
|
|
174
285
|
if (!gc) return null;
|
|
175
286
|
|
|
287
|
+
// Skip fully transparent gradient fills
|
|
288
|
+
if (gc.topOpacity < 0.01 && gc.bottomOpacity < 0.01) return null;
|
|
289
|
+
|
|
176
290
|
// Convert colors to rgba strings with their opacities
|
|
177
291
|
const topRgba = ColorUtils.toRgba(gc.topColor, gc.topOpacity);
|
|
178
292
|
const bottomRgba = ColorUtils.toRgba(gc.bottomColor, gc.bottomOpacity);
|
|
@@ -200,6 +314,7 @@ export class FillRenderer implements SeriesRenderer {
|
|
|
200
314
|
};
|
|
201
315
|
},
|
|
202
316
|
data: fillDataWithPrev,
|
|
317
|
+
silent: true,
|
|
203
318
|
};
|
|
204
319
|
}
|
|
205
320
|
|
|
@@ -1,20 +1,67 @@
|
|
|
1
|
-
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
-
|
|
3
|
-
export class HistogramRenderer implements SeriesRenderer {
|
|
4
|
-
render(context: RenderContext): any {
|
|
5
|
-
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, plotOptions } = context;
|
|
6
|
-
const defaultColor = '#2962ff';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
+
|
|
3
|
+
export class HistogramRenderer implements SeriesRenderer {
|
|
4
|
+
render(context: RenderContext): any {
|
|
5
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, plotOptions } = context;
|
|
6
|
+
const defaultColor = '#2962ff';
|
|
7
|
+
const histbase: number = plotOptions.histbase ?? 0;
|
|
8
|
+
const isColumns = plotOptions.style === 'columns';
|
|
9
|
+
const linewidth: number = plotOptions.linewidth ?? 1;
|
|
10
|
+
|
|
11
|
+
// Build data array: [index, value, color]
|
|
12
|
+
const customData = dataArray.map((val: number | null, i: number) => {
|
|
13
|
+
if (val === null || val === undefined || (typeof val === 'number' && isNaN(val))) return null;
|
|
14
|
+
return [i, val, colorArray[i] || plotOptions.color || defaultColor];
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
name: seriesName,
|
|
19
|
+
type: 'custom',
|
|
20
|
+
xAxisIndex: xAxisIndex,
|
|
21
|
+
yAxisIndex: yAxisIndex,
|
|
22
|
+
renderItem: (params: any, api: any) => {
|
|
23
|
+
const idx = api.value(0);
|
|
24
|
+
const value = api.value(1);
|
|
25
|
+
const color = api.value(2);
|
|
26
|
+
|
|
27
|
+
if (value === null || value === undefined || isNaN(value)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const basePos = api.coord([idx, histbase]);
|
|
32
|
+
const valuePos = api.coord([idx, value]);
|
|
33
|
+
const candleWidth = api.size([1, 0])[0];
|
|
34
|
+
|
|
35
|
+
// Columns: thick bars (60% of candle width)
|
|
36
|
+
// Histogram: thin bars — scale with linewidth (like TradingView)
|
|
37
|
+
let barWidth: number;
|
|
38
|
+
if (isColumns) {
|
|
39
|
+
barWidth = candleWidth * 0.6;
|
|
40
|
+
} else {
|
|
41
|
+
// Thin line-like bars: linewidth controls pixel width (min 1px)
|
|
42
|
+
barWidth = Math.max(1, linewidth);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const x = basePos[0];
|
|
46
|
+
const yBase = basePos[1];
|
|
47
|
+
const yValue = valuePos[1];
|
|
48
|
+
const top = Math.min(yBase, yValue);
|
|
49
|
+
const height = Math.abs(yValue - yBase);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
type: 'rect',
|
|
53
|
+
shape: {
|
|
54
|
+
x: x - barWidth / 2,
|
|
55
|
+
y: top,
|
|
56
|
+
width: barWidth,
|
|
57
|
+
height: height || 1, // Minimum 1px for zero-height bars
|
|
58
|
+
},
|
|
59
|
+
style: {
|
|
60
|
+
fill: color,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
data: customData.filter((d: any) => d !== null),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -24,14 +24,19 @@ export class LabelRenderer implements SeriesRenderer {
|
|
|
24
24
|
|
|
25
25
|
const labelData = labelObjects
|
|
26
26
|
.map((lbl) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
27
|
+
// Resolve any function/Series values that may not have been
|
|
28
|
+
// resolved at PineTS level (e.g. setters that skip _resolve()).
|
|
29
|
+
const resolve = (v: any) => typeof v === 'function' ? v() : v;
|
|
30
|
+
|
|
31
|
+
const text = resolve(lbl.text) || '';
|
|
32
|
+
const rawColor = resolve(lbl.color);
|
|
33
|
+
const color = (rawColor != null && rawColor !== '') ? rawColor : 'transparent';
|
|
34
|
+
const textcolor = resolve(lbl.textcolor) || '#ffffff';
|
|
35
|
+
const yloc = resolve(lbl.yloc) || 'price';
|
|
36
|
+
const styleRaw = resolve(lbl.style) || 'style_label_down';
|
|
37
|
+
const size = resolve(lbl.size) || 'normal';
|
|
38
|
+
const textalign = resolve(lbl.textalign) || 'align_center';
|
|
39
|
+
const tooltip = resolve(lbl.tooltip) || '';
|
|
35
40
|
|
|
36
41
|
// Map Pine style string to shape name for ShapeUtils
|
|
37
42
|
const shape = this.styleToShape(styleRaw);
|
|
@@ -160,7 +165,24 @@ export class LabelRenderer implements SeriesRenderer {
|
|
|
160
165
|
};
|
|
161
166
|
|
|
162
167
|
if (tooltip) {
|
|
163
|
-
|
|
168
|
+
// Store tooltip text for the custom tooltip overlay in QFChart.ts.
|
|
169
|
+
// ECharts mouseover event can read this from params.data._tooltipText.
|
|
170
|
+
item._tooltipText = tooltip;
|
|
171
|
+
// Enable emphasis for this item so ECharts fires mouseover/mouseout
|
|
172
|
+
// events, but prevent any visual change by mirroring normal styles.
|
|
173
|
+
item.emphasis = {
|
|
174
|
+
scale: false,
|
|
175
|
+
itemStyle: { color: color },
|
|
176
|
+
label: {
|
|
177
|
+
show: item.label.show,
|
|
178
|
+
color: textcolor,
|
|
179
|
+
fontSize: fontSize,
|
|
180
|
+
fontWeight: 'bold',
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
} else {
|
|
184
|
+
// No tooltip: fully disable emphasis (no hover interaction)
|
|
185
|
+
item.emphasis = { disabled: true };
|
|
164
186
|
}
|
|
165
187
|
|
|
166
188
|
return item;
|
|
@@ -174,6 +196,10 @@ export class LabelRenderer implements SeriesRenderer {
|
|
|
174
196
|
yAxisIndex: yAxisIndex,
|
|
175
197
|
data: labelData,
|
|
176
198
|
z: 20,
|
|
199
|
+
// Per-item emphasis: disabled for labels without tooltips,
|
|
200
|
+
// scale:false for labels with tooltips (allows hover for custom tooltip).
|
|
201
|
+
animation: false, // Prevent labels disappearing on zoom
|
|
202
|
+
clip: false, // Keep labels visible when partially outside viewport
|
|
177
203
|
};
|
|
178
204
|
}
|
|
179
205
|
|
|
@@ -37,20 +37,12 @@ export class LinefillRenderer implements SeriesRenderer {
|
|
|
37
37
|
return { name: seriesName, type: 'custom', xAxisIndex, yAxisIndex, data: [], silent: true };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// Compute y-range for axis scaling
|
|
41
|
-
let yMin = Infinity, yMax = -Infinity;
|
|
42
|
-
for (const lf of fillObjects) {
|
|
43
|
-
for (const y of [lf.line1.y1, lf.line1.y2, lf.line2.y1, lf.line2.y2]) {
|
|
44
|
-
if (y < yMin) yMin = y;
|
|
45
|
-
if (y > yMax) yMax = y;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
40
|
// Use a SINGLE data entry spanning the full x-range so renderItem is always called.
|
|
50
41
|
// ECharts filters a data item only when ALL its x-dimensions are on the same side
|
|
51
42
|
// of the visible window. With dims 0=0 and 1=lastBar the item always straddles
|
|
52
43
|
// the viewport, so renderItem fires exactly once regardless of scroll position.
|
|
53
|
-
//
|
|
44
|
+
// Note: We do NOT encode y-dimensions — drawing objects should not influence the
|
|
45
|
+
// y-axis auto-scaling.
|
|
54
46
|
const totalBars = (context.candlestickData?.length || 0) + offset;
|
|
55
47
|
const lastBarIndex = Math.max(0, totalBars - 1);
|
|
56
48
|
|
|
@@ -103,9 +95,9 @@ export class LinefillRenderer implements SeriesRenderer {
|
|
|
103
95
|
|
|
104
96
|
return { type: 'group', children };
|
|
105
97
|
},
|
|
106
|
-
data: [[0, lastBarIndex
|
|
98
|
+
data: [[0, lastBarIndex]],
|
|
107
99
|
clip: true,
|
|
108
|
-
encode: { x: [0, 1]
|
|
100
|
+
encode: { x: [0, 1] },
|
|
109
101
|
z: 10, // Behind lines (z=15) but above other elements
|
|
110
102
|
silent: true,
|
|
111
103
|
emphasis: { disabled: true },
|