@qfo/qfchart 0.7.1 → 0.7.3
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 +5 -0
- package/dist/qfchart.min.browser.js +16 -16
- package/dist/qfchart.min.es.js +16 -16
- package/package.json +1 -1
- package/src/QFChart.ts +172 -10
- package/src/components/LayoutManager.ts +34 -2
- package/src/components/SeriesBuilder.ts +26 -4
- package/src/components/TableOverlayRenderer.ts +38 -15
- package/src/components/renderers/BoxRenderer.ts +21 -21
- package/src/components/renderers/DrawingLineRenderer.ts +12 -16
- package/src/components/renderers/FillRenderer.ts +138 -31
- package/src/components/renderers/HistogramRenderer.ts +67 -20
- package/src/components/renderers/LinefillRenderer.ts +4 -12
- package/src/components/renderers/PolylineRenderer.ts +6 -13
- package/src/utils/ColorUtils.ts +77 -32
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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 },
|
|
@@ -34,21 +34,12 @@ export class PolylineRenderer implements SeriesRenderer {
|
|
|
34
34
|
return { name: seriesName, type: 'custom', xAxisIndex, yAxisIndex, data: [], silent: true };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// Compute y-range across all polylines for axis scaling
|
|
38
|
-
let yMin = Infinity, yMax = -Infinity;
|
|
39
|
-
for (const pl of polyObjects) {
|
|
40
|
-
for (const pt of pl.points) {
|
|
41
|
-
const p = pt.price ?? 0;
|
|
42
|
-
if (p < yMin) yMin = p;
|
|
43
|
-
if (p > yMax) yMax = p;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
37
|
// Use a SINGLE data entry spanning the full x-range so renderItem is always called.
|
|
48
38
|
// ECharts filters a data item only when ALL its x-dimensions are on the same side
|
|
49
39
|
// of the visible window. With dims 0=0 and 1=lastBar the item always straddles
|
|
50
40
|
// the viewport, so renderItem fires exactly once regardless of scroll position.
|
|
51
|
-
//
|
|
41
|
+
// Note: We do NOT encode y-dimensions — drawing objects should not influence the
|
|
42
|
+
// y-axis auto-scaling.
|
|
52
43
|
const totalBars = (context.candlestickData?.length || 0) + offset;
|
|
53
44
|
const lastBarIndex = Math.max(0, totalBars - 1);
|
|
54
45
|
|
|
@@ -126,9 +117,11 @@ export class PolylineRenderer implements SeriesRenderer {
|
|
|
126
117
|
|
|
127
118
|
return { type: 'group', children };
|
|
128
119
|
},
|
|
129
|
-
data: [[0, lastBarIndex
|
|
120
|
+
data: [[0, lastBarIndex]],
|
|
130
121
|
clip: true,
|
|
131
|
-
encode: { x: [0, 1]
|
|
122
|
+
encode: { x: [0, 1] },
|
|
123
|
+
// Prevent ECharts visual system from overriding element colors with palette
|
|
124
|
+
itemStyle: { color: 'transparent', borderColor: 'transparent' },
|
|
132
125
|
z: 12,
|
|
133
126
|
silent: true,
|
|
134
127
|
emphasis: { disabled: true },
|
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
|
+
}
|