@qfo/qfchart 0.7.1 → 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.
|
@@ -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
|
}
|
package/src/utils/ColorUtils.ts
CHANGED
|
@@ -1,32 +1,77 @@
|
|
|
1
|
-
export class ColorUtils {
|
|
2
|
-
/**
|
|
3
|
-
* Parse color string and extract opacity
|
|
4
|
-
* Supports: hex (#RRGGBB), named colors (green, red), rgba(r,g,b,a), rgb(r,g,b)
|
|
5
|
-
*/
|
|
6
|
-
public static parseColor(colorStr: string): { color: string; opacity: number } {
|
|
7
|
-
if (!colorStr) {
|
|
8
|
-
return { color: '#888888', opacity: 0.2 };
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// Check for rgba format
|
|
12
|
-
const rgbaMatch = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
13
|
-
if (rgbaMatch) {
|
|
14
|
-
const r = rgbaMatch[1];
|
|
15
|
-
const g = rgbaMatch[2];
|
|
16
|
-
const b = rgbaMatch[3];
|
|
17
|
-
const a = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1;
|
|
18
|
-
|
|
19
|
-
// Return rgb color and separate opacity
|
|
20
|
-
return {
|
|
21
|
-
color: `rgb(${r},${g},${b})`,
|
|
22
|
-
opacity: a,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
1
|
+
export class ColorUtils {
|
|
2
|
+
/**
|
|
3
|
+
* Parse color string and extract opacity
|
|
4
|
+
* Supports: hex (#RRGGBB, #RRGGBBAA), named colors (green, red), rgba(r,g,b,a), rgb(r,g,b)
|
|
5
|
+
*/
|
|
6
|
+
public static parseColor(colorStr: string): { color: string; opacity: number } {
|
|
7
|
+
if (!colorStr) {
|
|
8
|
+
return { color: '#888888', opacity: 0.2 };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Check for rgba format
|
|
12
|
+
const rgbaMatch = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
13
|
+
if (rgbaMatch) {
|
|
14
|
+
const r = rgbaMatch[1];
|
|
15
|
+
const g = rgbaMatch[2];
|
|
16
|
+
const b = rgbaMatch[3];
|
|
17
|
+
const a = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1;
|
|
18
|
+
|
|
19
|
+
// Return rgb color and separate opacity
|
|
20
|
+
return {
|
|
21
|
+
color: `rgb(${r},${g},${b})`,
|
|
22
|
+
opacity: a,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check for 8-digit hex with alpha (#RRGGBBAA)
|
|
27
|
+
const hex8Match = colorStr.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);
|
|
28
|
+
if (hex8Match) {
|
|
29
|
+
const r = parseInt(hex8Match[1], 16);
|
|
30
|
+
const g = parseInt(hex8Match[2], 16);
|
|
31
|
+
const b = parseInt(hex8Match[3], 16);
|
|
32
|
+
const a = parseInt(hex8Match[4], 16) / 255;
|
|
33
|
+
return {
|
|
34
|
+
color: `rgb(${r},${g},${b})`,
|
|
35
|
+
opacity: a,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// For 6-digit hex or named colors, default opacity to 0.3 for fill areas
|
|
40
|
+
return {
|
|
41
|
+
color: colorStr,
|
|
42
|
+
opacity: 0.3,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert a parsed color + opacity to an rgba string.
|
|
48
|
+
*/
|
|
49
|
+
public static toRgba(color: string, opacity: number): string {
|
|
50
|
+
// If already rgba/rgb format
|
|
51
|
+
const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
52
|
+
if (rgbMatch) {
|
|
53
|
+
return `rgba(${rgbMatch[1]},${rgbMatch[2]},${rgbMatch[3]},${opacity})`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Handle 6-digit hex colors
|
|
57
|
+
const hexMatch = color.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);
|
|
58
|
+
if (hexMatch) {
|
|
59
|
+
const r = parseInt(hexMatch[1], 16);
|
|
60
|
+
const g = parseInt(hexMatch[2], 16);
|
|
61
|
+
const b = parseInt(hexMatch[3], 16);
|
|
62
|
+
return `rgba(${r},${g},${b},${opacity})`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Handle 8-digit hex colors (#RRGGBBAA) — use alpha from hex, override with opacity
|
|
66
|
+
const hex8Match = color.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);
|
|
67
|
+
if (hex8Match) {
|
|
68
|
+
const r = parseInt(hex8Match[1], 16);
|
|
69
|
+
const g = parseInt(hex8Match[2], 16);
|
|
70
|
+
const b = parseInt(hex8Match[3], 16);
|
|
71
|
+
return `rgba(${r},${g},${b},${opacity})`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Fallback: return color as-is
|
|
75
|
+
return color;
|
|
76
|
+
}
|
|
77
|
+
}
|