@qfo/qfchart 0.6.8 → 0.7.2
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 +3 -0
- package/dist/qfchart.min.browser.js +16 -16
- package/dist/qfchart.min.es.js +16 -16
- package/package.json +2 -2
- package/src/QFChart.ts +52 -0
- package/src/components/LayoutManager.ts +682 -679
- package/src/components/SeriesBuilder.ts +20 -6
- package/src/components/SeriesRendererFactory.ts +4 -0
- package/src/components/TableOverlayRenderer.ts +322 -0
- package/src/components/renderers/BoxRenderer.ts +258 -0
- package/src/components/renderers/DrawingLineRenderer.ts +58 -52
- package/src/components/renderers/FillRenderer.ts +138 -31
- package/src/components/renderers/LabelRenderer.ts +9 -8
- package/src/components/renderers/LinefillRenderer.ts +60 -72
- package/src/components/renderers/PolylineRenderer.ts +197 -0
- package/src/components/renderers/ShapeRenderer.ts +121 -121
- package/src/utils/ColorUtils.ts +77 -32
- package/src/utils/ShapeUtils.ts +22 -14
|
@@ -18,7 +18,6 @@ export class DrawingLineRenderer implements SeriesRenderer {
|
|
|
18
18
|
// (since multiple objects at the same bar would overwrite each other in the
|
|
19
19
|
// sparse array). Handle both array-of-objects and single-object entries.
|
|
20
20
|
const lineObjects: any[] = [];
|
|
21
|
-
const lineData: number[][] = [];
|
|
22
21
|
|
|
23
22
|
for (let i = 0; i < dataArray.length; i++) {
|
|
24
23
|
const val = dataArray[i];
|
|
@@ -28,75 +27,82 @@ export class DrawingLineRenderer implements SeriesRenderer {
|
|
|
28
27
|
for (const ln of items) {
|
|
29
28
|
if (ln && typeof ln === 'object' && !ln._deleted) {
|
|
30
29
|
lineObjects.push(ln);
|
|
31
|
-
// Apply padding offset for bar_index-based coordinates
|
|
32
|
-
const xOff = ln.xloc === 'bar_index' ? offset : 0;
|
|
33
|
-
lineData.push([ln.x1 + xOff, ln.y1, ln.x2 + xOff, ln.y2]);
|
|
34
30
|
}
|
|
35
31
|
}
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
if (
|
|
34
|
+
if (lineObjects.length === 0) {
|
|
39
35
|
return { name: seriesName, type: 'custom', xAxisIndex, yAxisIndex, data: [], silent: true };
|
|
40
36
|
}
|
|
41
37
|
|
|
38
|
+
// Compute y-range for axis scaling
|
|
39
|
+
let yMin = Infinity, yMax = -Infinity;
|
|
40
|
+
for (const ln of lineObjects) {
|
|
41
|
+
if (ln.y1 < yMin) yMin = ln.y1;
|
|
42
|
+
if (ln.y1 > yMax) yMax = ln.y1;
|
|
43
|
+
if (ln.y2 < yMin) yMin = ln.y2;
|
|
44
|
+
if (ln.y2 > yMax) yMax = ln.y2;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Use a SINGLE data entry spanning the full x-range so renderItem is always called.
|
|
48
|
+
// ECharts filters a data item only when ALL its x-dimensions are on the same side
|
|
49
|
+
// of the visible window. With dims 0=0 and 1=lastBar the item always straddles
|
|
50
|
+
// the viewport, so renderItem fires exactly once regardless of scroll position.
|
|
51
|
+
// Dims 2/3 are yMin/yMax for axis scaling.
|
|
52
|
+
const totalBars = (context.candlestickData?.length || 0) + offset;
|
|
53
|
+
const lastBarIndex = Math.max(0, totalBars - 1);
|
|
54
|
+
|
|
42
55
|
return {
|
|
43
56
|
name: seriesName,
|
|
44
57
|
type: 'custom',
|
|
45
58
|
xAxisIndex,
|
|
46
59
|
yAxisIndex,
|
|
47
60
|
renderItem: (params: any, api: any) => {
|
|
48
|
-
const idx = params.dataIndex;
|
|
49
|
-
const ln = lineObjects[idx];
|
|
50
|
-
if (!ln || ln._deleted) return;
|
|
51
|
-
|
|
52
|
-
const x1 = api.value(0);
|
|
53
|
-
const y1 = api.value(1);
|
|
54
|
-
const x2 = api.value(2);
|
|
55
|
-
const y2 = api.value(3);
|
|
56
|
-
|
|
57
|
-
let p1 = api.coord([x1, y1]);
|
|
58
|
-
let p2 = api.coord([x2, y2]);
|
|
59
|
-
|
|
60
|
-
// Handle extend (none | left | right | both)
|
|
61
|
-
const extend = ln.extend || 'none';
|
|
62
|
-
if (extend !== 'none') {
|
|
63
|
-
const cs = params.coordSys;
|
|
64
|
-
const left = cs.x;
|
|
65
|
-
const right = cs.x + cs.width;
|
|
66
|
-
const top = cs.y;
|
|
67
|
-
const bottom = cs.y + cs.height;
|
|
68
|
-
[p1, p2] = this.extendLine(p1, p2, extend, left, right, top, bottom);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
61
|
const children: any[] = [];
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
62
|
+
|
|
63
|
+
for (const ln of lineObjects) {
|
|
64
|
+
if (ln._deleted) continue;
|
|
65
|
+
const xOff = (ln.xloc === 'bar_index' || ln.xloc === 'bi') ? offset : 0;
|
|
66
|
+
|
|
67
|
+
let p1 = api.coord([ln.x1 + xOff, ln.y1]);
|
|
68
|
+
let p2 = api.coord([ln.x2 + xOff, ln.y2]);
|
|
69
|
+
|
|
70
|
+
// Handle extend (none | left | right | both)
|
|
71
|
+
const extend = ln.extend || 'none';
|
|
72
|
+
if (extend !== 'none') {
|
|
73
|
+
const cs = params.coordSys;
|
|
74
|
+
[p1, p2] = this.extendLine(p1, p2, extend, cs.x, cs.x + cs.width, cs.y, cs.y + cs.height);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const color = ln.color || defaultColor;
|
|
78
|
+
const lineWidth = ln.width || 1;
|
|
79
|
+
|
|
80
|
+
children.push({
|
|
81
|
+
type: 'line',
|
|
82
|
+
shape: { x1: p1[0], y1: p1[1], x2: p2[0], y2: p2[1] },
|
|
83
|
+
style: {
|
|
84
|
+
stroke: color,
|
|
85
|
+
lineWidth,
|
|
86
|
+
lineDash: this.getDashPattern(ln.style),
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const style = ln.style || 'style_solid';
|
|
91
|
+
if (style === 'style_arrow_left' || style === 'style_arrow_both') {
|
|
92
|
+
const arrow = this.arrowHead(p2, p1, lineWidth, color);
|
|
93
|
+
if (arrow) children.push(arrow);
|
|
94
|
+
}
|
|
95
|
+
if (style === 'style_arrow_right' || style === 'style_arrow_both') {
|
|
96
|
+
const arrow = this.arrowHead(p1, p2, lineWidth, color);
|
|
97
|
+
if (arrow) children.push(arrow);
|
|
98
|
+
}
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
return { type: 'group', children };
|
|
98
102
|
},
|
|
99
|
-
data:
|
|
103
|
+
data: [[0, lastBarIndex, yMin, yMax]],
|
|
104
|
+
clip: true,
|
|
105
|
+
encode: { x: [0, 1], y: [2, 3] },
|
|
100
106
|
z: 15,
|
|
101
107
|
silent: true,
|
|
102
108
|
emphasis: { disabled: true },
|
|
@@ -3,7 +3,7 @@ import { ColorUtils } from '../../utils/ColorUtils';
|
|
|
3
3
|
|
|
4
4
|
export class FillRenderer implements SeriesRenderer {
|
|
5
5
|
render(context: RenderContext): any {
|
|
6
|
-
const { seriesName, xAxisIndex, yAxisIndex, plotOptions, plotDataArrays, indicatorId, plotName } = context;
|
|
6
|
+
const { seriesName, xAxisIndex, yAxisIndex, plotOptions, plotDataArrays, indicatorId, plotName, optionsArray } = context;
|
|
7
7
|
const totalDataLength = context.dataArray.length; // Use length from dataArray placeholder
|
|
8
8
|
|
|
9
9
|
// Fill plots reference other plots to fill the area between them
|
|
@@ -23,7 +23,18 @@ export class FillRenderer implements SeriesRenderer {
|
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// Detect gradient fill mode
|
|
27
|
+
const isGradient = plotOptions.gradient === true;
|
|
28
|
+
|
|
29
|
+
if (isGradient) {
|
|
30
|
+
return this.renderGradientFill(
|
|
31
|
+
seriesName, xAxisIndex, yAxisIndex,
|
|
32
|
+
plot1Data, plot2Data, totalDataLength,
|
|
33
|
+
optionsArray, plotOptions
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- Simple (solid color) fill ---
|
|
27
38
|
const { color: fillColor, opacity: fillOpacity } = ColorUtils.parseColor(plotOptions.color || 'rgba(128, 128, 128, 0.2)');
|
|
28
39
|
|
|
29
40
|
// Create fill data with previous values for smooth polygon rendering
|
|
@@ -37,54 +48,37 @@ export class FillRenderer implements SeriesRenderer {
|
|
|
37
48
|
fillDataWithPrev.push([i, y1, y2, prevY1, prevY2]);
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
// Add fill series with smooth area rendering
|
|
41
51
|
return {
|
|
42
52
|
name: seriesName,
|
|
43
53
|
type: 'custom',
|
|
44
54
|
xAxisIndex: xAxisIndex,
|
|
45
55
|
yAxisIndex: yAxisIndex,
|
|
46
|
-
z: 1,
|
|
56
|
+
z: 1,
|
|
47
57
|
renderItem: (params: any, api: any) => {
|
|
48
58
|
const index = params.dataIndex;
|
|
49
|
-
|
|
50
|
-
// Skip first point (no previous to connect to)
|
|
51
59
|
if (index === 0) return null;
|
|
52
60
|
|
|
53
|
-
const y1 = api.value(1);
|
|
54
|
-
const y2 = api.value(2);
|
|
55
|
-
const prevY1 = api.value(3);
|
|
56
|
-
const prevY2 = api.value(4);
|
|
61
|
+
const y1 = api.value(1);
|
|
62
|
+
const y2 = api.value(2);
|
|
63
|
+
const prevY1 = api.value(3);
|
|
64
|
+
const prevY2 = api.value(4);
|
|
57
65
|
|
|
58
|
-
// Skip if any value is null/NaN
|
|
59
66
|
if (
|
|
60
|
-
y1 === null ||
|
|
61
|
-
y2
|
|
62
|
-
prevY1 === null ||
|
|
63
|
-
prevY2 === null ||
|
|
64
|
-
isNaN(y1) ||
|
|
65
|
-
isNaN(y2) ||
|
|
66
|
-
isNaN(prevY1) ||
|
|
67
|
-
isNaN(prevY2)
|
|
67
|
+
y1 === null || y2 === null || prevY1 === null || prevY2 === null ||
|
|
68
|
+
isNaN(y1) || isNaN(y2) || isNaN(prevY1) || isNaN(prevY2)
|
|
68
69
|
) {
|
|
69
70
|
return null;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const p2Prev = api.coord([index - 1, prevY2]); // Previous lower
|
|
73
|
+
const p1Prev = api.coord([index - 1, prevY1]);
|
|
74
|
+
const p1Curr = api.coord([index, y1]);
|
|
75
|
+
const p2Curr = api.coord([index, y2]);
|
|
76
|
+
const p2Prev = api.coord([index - 1, prevY2]);
|
|
77
77
|
|
|
78
|
-
// Create a smooth polygon connecting the segments
|
|
79
78
|
return {
|
|
80
79
|
type: 'polygon',
|
|
81
80
|
shape: {
|
|
82
|
-
points: [
|
|
83
|
-
p1Prev, // Top-left
|
|
84
|
-
p1Curr, // Top-right
|
|
85
|
-
p2Curr, // Bottom-right
|
|
86
|
-
p2Prev, // Bottom-left
|
|
87
|
-
],
|
|
81
|
+
points: [p1Prev, p1Curr, p2Curr, p2Prev],
|
|
88
82
|
},
|
|
89
83
|
style: {
|
|
90
84
|
fill: fillColor,
|
|
@@ -96,4 +90,117 @@ export class FillRenderer implements SeriesRenderer {
|
|
|
96
90
|
data: fillDataWithPrev,
|
|
97
91
|
};
|
|
98
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Render a gradient fill between two plots.
|
|
96
|
+
* Uses a vertical linear gradient from top_color (at the upper boundary)
|
|
97
|
+
* to bottom_color (at the lower boundary) for each polygon segment.
|
|
98
|
+
*/
|
|
99
|
+
private renderGradientFill(
|
|
100
|
+
seriesName: string,
|
|
101
|
+
xAxisIndex: number,
|
|
102
|
+
yAxisIndex: number,
|
|
103
|
+
plot1Data: (number | null)[],
|
|
104
|
+
plot2Data: (number | null)[],
|
|
105
|
+
totalDataLength: number,
|
|
106
|
+
optionsArray: any[],
|
|
107
|
+
plotOptions: any
|
|
108
|
+
): any {
|
|
109
|
+
// Build per-bar gradient color arrays from optionsArray
|
|
110
|
+
// Each entry in optionsArray has: { top_value, bottom_value, top_color, bottom_color }
|
|
111
|
+
const gradientColors: { topColor: string; topOpacity: number; bottomColor: string; bottomOpacity: number }[] = [];
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < totalDataLength; i++) {
|
|
114
|
+
const opts = optionsArray?.[i];
|
|
115
|
+
if (opts && opts.top_color !== undefined) {
|
|
116
|
+
const top = ColorUtils.parseColor(opts.top_color);
|
|
117
|
+
const bottom = ColorUtils.parseColor(opts.bottom_color);
|
|
118
|
+
gradientColors[i] = {
|
|
119
|
+
topColor: top.color,
|
|
120
|
+
topOpacity: top.opacity,
|
|
121
|
+
bottomColor: bottom.color,
|
|
122
|
+
bottomOpacity: bottom.opacity,
|
|
123
|
+
};
|
|
124
|
+
} else {
|
|
125
|
+
// Fallback: use a default semi-transparent fill
|
|
126
|
+
gradientColors[i] = {
|
|
127
|
+
topColor: 'rgba(128,128,128,0.2)',
|
|
128
|
+
topOpacity: 0.2,
|
|
129
|
+
bottomColor: 'rgba(128,128,128,0.2)',
|
|
130
|
+
bottomOpacity: 0.2,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Create fill data with previous values
|
|
136
|
+
const fillDataWithPrev: any[] = [];
|
|
137
|
+
for (let i = 0; i < totalDataLength; i++) {
|
|
138
|
+
const y1 = plot1Data[i];
|
|
139
|
+
const y2 = plot2Data[i];
|
|
140
|
+
const prevY1 = i > 0 ? plot1Data[i - 1] : null;
|
|
141
|
+
const prevY2 = i > 0 ? plot2Data[i - 1] : null;
|
|
142
|
+
fillDataWithPrev.push([i, y1, y2, prevY1, prevY2]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
name: seriesName,
|
|
147
|
+
type: 'custom',
|
|
148
|
+
xAxisIndex: xAxisIndex,
|
|
149
|
+
yAxisIndex: yAxisIndex,
|
|
150
|
+
z: 1,
|
|
151
|
+
renderItem: (params: any, api: any) => {
|
|
152
|
+
const index = params.dataIndex;
|
|
153
|
+
if (index === 0) return null;
|
|
154
|
+
|
|
155
|
+
const y1 = api.value(1);
|
|
156
|
+
const y2 = api.value(2);
|
|
157
|
+
const prevY1 = api.value(3);
|
|
158
|
+
const prevY2 = api.value(4);
|
|
159
|
+
|
|
160
|
+
if (
|
|
161
|
+
y1 === null || y2 === null || prevY1 === null || prevY2 === null ||
|
|
162
|
+
isNaN(y1) || isNaN(y2) || isNaN(prevY1) || isNaN(prevY2)
|
|
163
|
+
) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const p1Prev = api.coord([index - 1, prevY1]);
|
|
168
|
+
const p1Curr = api.coord([index, y1]);
|
|
169
|
+
const p2Curr = api.coord([index, y2]);
|
|
170
|
+
const p2Prev = api.coord([index - 1, prevY2]);
|
|
171
|
+
|
|
172
|
+
// Get gradient colors for this bar
|
|
173
|
+
const gc = gradientColors[index] || gradientColors[index - 1];
|
|
174
|
+
if (!gc) return null;
|
|
175
|
+
|
|
176
|
+
// Convert colors to rgba strings with their opacities
|
|
177
|
+
const topRgba = ColorUtils.toRgba(gc.topColor, gc.topOpacity);
|
|
178
|
+
const bottomRgba = ColorUtils.toRgba(gc.bottomColor, gc.bottomOpacity);
|
|
179
|
+
|
|
180
|
+
// Determine if plot1 is above plot2 (in value space, higher value = higher on chart)
|
|
181
|
+
// We want top_color at the higher value, bottom_color at the lower value
|
|
182
|
+
const plot1IsAbove = y1 >= y2;
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
type: 'polygon',
|
|
186
|
+
shape: {
|
|
187
|
+
points: [p1Prev, p1Curr, p2Curr, p2Prev],
|
|
188
|
+
},
|
|
189
|
+
style: {
|
|
190
|
+
fill: {
|
|
191
|
+
type: 'linear',
|
|
192
|
+
x: 0, y: 0, x2: 0, y2: 1, // vertical gradient
|
|
193
|
+
colorStops: [
|
|
194
|
+
{ offset: 0, color: plot1IsAbove ? topRgba : bottomRgba },
|
|
195
|
+
{ offset: 1, color: plot1IsAbove ? bottomRgba : topRgba },
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
silent: true,
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
data: fillDataWithPrev,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
99
206
|
}
|
|
@@ -37,18 +37,18 @@ export class LabelRenderer implements SeriesRenderer {
|
|
|
37
37
|
const shape = this.styleToShape(styleRaw);
|
|
38
38
|
|
|
39
39
|
// Determine X position using label's own x coordinate
|
|
40
|
-
const xPos = lbl.xloc === 'bar_index' ? (lbl.x + offset) : lbl.x;
|
|
40
|
+
const xPos = (lbl.xloc === 'bar_index' || lbl.xloc === 'bi') ? (lbl.x + offset) : lbl.x;
|
|
41
41
|
|
|
42
42
|
// Determine Y value based on yloc
|
|
43
43
|
let yValue = lbl.y;
|
|
44
44
|
let symbolOffset: (string | number)[] = [0, 0];
|
|
45
45
|
|
|
46
|
-
if (yloc === 'abovebar') {
|
|
46
|
+
if (yloc === 'abovebar' || yloc === 'AboveBar' || yloc === 'ab') {
|
|
47
47
|
if (candlestickData && candlestickData[xPos]) {
|
|
48
48
|
yValue = candlestickData[xPos].high;
|
|
49
49
|
}
|
|
50
50
|
symbolOffset = [0, '-150%'];
|
|
51
|
-
} else if (yloc === 'belowbar') {
|
|
51
|
+
} else if (yloc === 'belowbar' || yloc === 'BelowBar' || yloc === 'bl') {
|
|
52
52
|
if (candlestickData && candlestickData[xPos]) {
|
|
53
53
|
yValue = candlestickData[xPos].low;
|
|
54
54
|
}
|
|
@@ -64,7 +64,8 @@ export class LabelRenderer implements SeriesRenderer {
|
|
|
64
64
|
|
|
65
65
|
// Dynamically size the bubble to fit text content
|
|
66
66
|
let finalSize: number | number[];
|
|
67
|
-
const isBubble = shape === 'labeldown' || shape === '
|
|
67
|
+
const isBubble = shape === 'labeldown' || shape === 'shape_label_down' ||
|
|
68
|
+
shape === 'labelup' || shape === 'shape_label_up' ||
|
|
68
69
|
shape === 'labelleft' || shape === 'labelright';
|
|
69
70
|
// Track label text offset for centering text within the body
|
|
70
71
|
// (excluding the pointer area)
|
|
@@ -150,8 +151,8 @@ export class LabelRenderer implements SeriesRenderer {
|
|
|
150
151
|
fontSize: fontSize,
|
|
151
152
|
fontWeight: 'bold',
|
|
152
153
|
align: isInsideLabel ? 'center'
|
|
153
|
-
: textalign === 'align_left' ? 'left'
|
|
154
|
-
: textalign === 'align_right' ? 'right'
|
|
154
|
+
: (textalign === 'align_left' || textalign === 'left') ? 'left'
|
|
155
|
+
: (textalign === 'align_right' || textalign === 'right') ? 'right'
|
|
155
156
|
: 'center',
|
|
156
157
|
verticalAlign: 'middle',
|
|
157
158
|
padding: [2, 6],
|
|
@@ -247,10 +248,10 @@ export class LabelRenderer implements SeriesRenderer {
|
|
|
247
248
|
case 'text_outline':
|
|
248
249
|
case 'none':
|
|
249
250
|
// Text only, positioned based on yloc
|
|
250
|
-
return yloc === 'abovebar' ? 'top' : yloc === 'belowbar' ? 'bottom' : 'top';
|
|
251
|
+
return (yloc === 'abovebar' || yloc === 'AboveBar' || yloc === 'ab') ? 'top' : (yloc === 'belowbar' || yloc === 'BelowBar' || yloc === 'bl') ? 'bottom' : 'top';
|
|
251
252
|
default:
|
|
252
253
|
// For simple shapes (circle, diamond, etc.), text goes outside
|
|
253
|
-
return yloc === 'belowbar' ? 'bottom' : 'top';
|
|
254
|
+
return (yloc === 'belowbar' || yloc === 'BelowBar' || yloc === 'bl') ? 'bottom' : 'top';
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
257
|
|
|
@@ -16,7 +16,6 @@ export class LinefillRenderer implements SeriesRenderer {
|
|
|
16
16
|
// Same aggregation pattern as DrawingLineRenderer — objects are stored
|
|
17
17
|
// as an array in a single data entry.
|
|
18
18
|
const fillObjects: any[] = [];
|
|
19
|
-
const fillData: number[][] = [];
|
|
20
19
|
|
|
21
20
|
for (let i = 0; i < dataArray.length; i++) {
|
|
22
21
|
const val = dataArray[i];
|
|
@@ -31,93 +30,82 @@ export class LinefillRenderer implements SeriesRenderer {
|
|
|
31
30
|
if (!line1 || !line2 || line1._deleted || line2._deleted) continue;
|
|
32
31
|
|
|
33
32
|
fillObjects.push(lf);
|
|
34
|
-
|
|
35
|
-
// Store all 8 coordinates for the two lines
|
|
36
|
-
const xOff1 = line1.xloc === 'bar_index' ? offset : 0;
|
|
37
|
-
const xOff2 = line2.xloc === 'bar_index' ? offset : 0;
|
|
38
|
-
fillData.push([
|
|
39
|
-
line1.x1 + xOff1, line1.y1,
|
|
40
|
-
line1.x2 + xOff1, line1.y2,
|
|
41
|
-
line2.x1 + xOff2, line2.y1,
|
|
42
|
-
line2.x2 + xOff2, line2.y2,
|
|
43
|
-
]);
|
|
44
33
|
}
|
|
45
34
|
}
|
|
46
35
|
|
|
47
|
-
if (
|
|
36
|
+
if (fillObjects.length === 0) {
|
|
48
37
|
return { name: seriesName, type: 'custom', xAxisIndex, yAxisIndex, data: [], silent: true };
|
|
49
38
|
}
|
|
50
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
|
+
// Use a SINGLE data entry spanning the full x-range so renderItem is always called.
|
|
50
|
+
// ECharts filters a data item only when ALL its x-dimensions are on the same side
|
|
51
|
+
// of the visible window. With dims 0=0 and 1=lastBar the item always straddles
|
|
52
|
+
// the viewport, so renderItem fires exactly once regardless of scroll position.
|
|
53
|
+
// Dims 2/3 are yMin/yMax for axis scaling.
|
|
54
|
+
const totalBars = (context.candlestickData?.length || 0) + offset;
|
|
55
|
+
const lastBarIndex = Math.max(0, totalBars - 1);
|
|
56
|
+
|
|
51
57
|
return {
|
|
52
58
|
name: seriesName,
|
|
53
59
|
type: 'custom',
|
|
54
60
|
xAxisIndex,
|
|
55
61
|
yAxisIndex,
|
|
56
62
|
renderItem: (params: any, api: any) => {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const left = cs.x;
|
|
87
|
-
const right = cs.x + cs.width;
|
|
88
|
-
const top = cs.y;
|
|
89
|
-
const bottom = cs.y + cs.height;
|
|
90
|
-
|
|
91
|
-
if (extend1 !== 'none') {
|
|
92
|
-
[p1Start, p1End] = this.extendLine(p1Start, p1End, extend1, left, right, top, bottom);
|
|
93
|
-
}
|
|
94
|
-
if (extend2 !== 'none') {
|
|
95
|
-
[p2Start, p2End] = this.extendLine(p2Start, p2End, extend2, left, right, top, bottom);
|
|
63
|
+
const children: any[] = [];
|
|
64
|
+
|
|
65
|
+
for (const lf of fillObjects) {
|
|
66
|
+
if (lf._deleted) continue;
|
|
67
|
+
const line1 = lf.line1;
|
|
68
|
+
const line2 = lf.line2;
|
|
69
|
+
if (!line1 || !line2 || line1._deleted || line2._deleted) continue;
|
|
70
|
+
|
|
71
|
+
const xOff1 = (line1.xloc === 'bar_index' || line1.xloc === 'bi') ? offset : 0;
|
|
72
|
+
const xOff2 = (line2.xloc === 'bar_index' || line2.xloc === 'bi') ? offset : 0;
|
|
73
|
+
|
|
74
|
+
let p1Start = api.coord([line1.x1 + xOff1, line1.y1]);
|
|
75
|
+
let p1End = api.coord([line1.x2 + xOff1, line1.y2]);
|
|
76
|
+
let p2Start = api.coord([line2.x1 + xOff2, line2.y1]);
|
|
77
|
+
let p2End = api.coord([line2.x2 + xOff2, line2.y2]);
|
|
78
|
+
|
|
79
|
+
// Handle line extensions
|
|
80
|
+
const extend1 = line1.extend || 'none';
|
|
81
|
+
const extend2 = line2.extend || 'none';
|
|
82
|
+
if (extend1 !== 'none' || extend2 !== 'none') {
|
|
83
|
+
const cs = params.coordSys;
|
|
84
|
+
const csLeft = cs.x, csRight = cs.x + cs.width;
|
|
85
|
+
const csTop = cs.y, csBottom = cs.y + cs.height;
|
|
86
|
+
if (extend1 !== 'none') {
|
|
87
|
+
[p1Start, p1End] = this.extendLine(p1Start, p1End, extend1, csLeft, csRight, csTop, csBottom);
|
|
88
|
+
}
|
|
89
|
+
if (extend2 !== 'none') {
|
|
90
|
+
[p2Start, p2End] = this.extendLine(p2Start, p2End, extend2, csLeft, csRight, csTop, csBottom);
|
|
91
|
+
}
|
|
96
92
|
}
|
|
93
|
+
|
|
94
|
+
const { color: fillColor, opacity: fillOpacity } = ColorUtils.parseColor(lf.color || 'rgba(128, 128, 128, 0.2)');
|
|
95
|
+
|
|
96
|
+
children.push({
|
|
97
|
+
type: 'polygon',
|
|
98
|
+
shape: { points: [p1Start, p1End, p2End, p2Start] },
|
|
99
|
+
style: { fill: fillColor, opacity: fillOpacity },
|
|
100
|
+
silent: true,
|
|
101
|
+
});
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
|
|
100
|
-
const { color: fillColor, opacity: fillOpacity } = ColorUtils.parseColor(lf.color || 'rgba(128, 128, 128, 0.2)');
|
|
101
|
-
|
|
102
|
-
// Create a polygon: line1.start → line1.end → line2.end → line2.start
|
|
103
|
-
return {
|
|
104
|
-
type: 'polygon',
|
|
105
|
-
shape: {
|
|
106
|
-
points: [
|
|
107
|
-
p1Start,
|
|
108
|
-
p1End,
|
|
109
|
-
p2End,
|
|
110
|
-
p2Start,
|
|
111
|
-
],
|
|
112
|
-
},
|
|
113
|
-
style: {
|
|
114
|
-
fill: fillColor,
|
|
115
|
-
opacity: fillOpacity,
|
|
116
|
-
},
|
|
117
|
-
silent: true,
|
|
118
|
-
};
|
|
104
|
+
return { type: 'group', children };
|
|
119
105
|
},
|
|
120
|
-
data:
|
|
106
|
+
data: [[0, lastBarIndex, yMin, yMax]],
|
|
107
|
+
clip: true,
|
|
108
|
+
encode: { x: [0, 1], y: [2, 3] },
|
|
121
109
|
z: 10, // Behind lines (z=15) but above other elements
|
|
122
110
|
silent: true,
|
|
123
111
|
emphasis: { disabled: true },
|