@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,161 +1,171 @@
|
|
|
1
|
-
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
-
|
|
3
|
-
export class OHLCBarRenderer implements SeriesRenderer {
|
|
4
|
-
render(context: RenderContext): any {
|
|
5
|
-
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, optionsArray, plotOptions } = context;
|
|
6
|
-
const defaultColor = '#2962ff';
|
|
7
|
-
const isCandle = plotOptions.style === 'candle';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
},
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
},
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
1
|
+
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
+
|
|
3
|
+
export class OHLCBarRenderer implements SeriesRenderer {
|
|
4
|
+
render(context: RenderContext): any {
|
|
5
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, optionsArray, plotOptions } = context;
|
|
6
|
+
const defaultColor = '#2962ff';
|
|
7
|
+
const isCandle = plotOptions.style === 'candle';
|
|
8
|
+
|
|
9
|
+
// Build a separate color lookup — ECharts custom series coerces data values to numbers,
|
|
10
|
+
// so string colors stored in the data array would become NaN via api.value().
|
|
11
|
+
const colorLookup: { color: string; wickColor: string; borderColor: string }[] = [];
|
|
12
|
+
|
|
13
|
+
const ohlcData = dataArray
|
|
14
|
+
.map((val, i) => {
|
|
15
|
+
if (val === null || !Array.isArray(val) || val.length !== 4) return null;
|
|
16
|
+
|
|
17
|
+
const [open, high, low, close] = val;
|
|
18
|
+
const pointOpts = optionsArray[i] || {};
|
|
19
|
+
const color = pointOpts.color || colorArray[i] || plotOptions.color || defaultColor;
|
|
20
|
+
const wickColor = pointOpts.wickcolor || plotOptions.wickcolor || color;
|
|
21
|
+
const borderColor = pointOpts.bordercolor || plotOptions.bordercolor || wickColor;
|
|
22
|
+
|
|
23
|
+
// Store colors in a closure-accessible lookup keyed by the data index
|
|
24
|
+
colorLookup[i] = { color, wickColor, borderColor };
|
|
25
|
+
|
|
26
|
+
// Data array contains only numeric values for ECharts
|
|
27
|
+
return [i, open, close, low, high];
|
|
28
|
+
})
|
|
29
|
+
.filter((item) => item !== null);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
name: seriesName,
|
|
33
|
+
type: 'custom',
|
|
34
|
+
xAxisIndex: xAxisIndex,
|
|
35
|
+
yAxisIndex: yAxisIndex,
|
|
36
|
+
renderItem: (params: any, api: any) => {
|
|
37
|
+
const xValue = api.value(0);
|
|
38
|
+
const openValue = api.value(1);
|
|
39
|
+
const closeValue = api.value(2);
|
|
40
|
+
const lowValue = api.value(3);
|
|
41
|
+
const highValue = api.value(4);
|
|
42
|
+
|
|
43
|
+
if (isNaN(openValue) || isNaN(closeValue) || isNaN(lowValue) || isNaN(highValue)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Retrieve colors from the closure-based lookup using the original data index
|
|
48
|
+
const colors = colorLookup[xValue] || { color: defaultColor, wickColor: defaultColor, borderColor: defaultColor };
|
|
49
|
+
const color = colors.color;
|
|
50
|
+
const wickColor = colors.wickColor;
|
|
51
|
+
const borderColor = colors.borderColor;
|
|
52
|
+
|
|
53
|
+
const xPos = api.coord([xValue, 0])[0];
|
|
54
|
+
const openPos = api.coord([xValue, openValue])[1];
|
|
55
|
+
const closePos = api.coord([xValue, closeValue])[1];
|
|
56
|
+
const lowPos = api.coord([xValue, lowValue])[1];
|
|
57
|
+
const highPos = api.coord([xValue, highValue])[1];
|
|
58
|
+
|
|
59
|
+
const barWidth = api.size([1, 0])[0] * 0.6;
|
|
60
|
+
|
|
61
|
+
if (isCandle) {
|
|
62
|
+
// Classic candlestick rendering
|
|
63
|
+
const bodyTop = Math.min(openPos, closePos);
|
|
64
|
+
const bodyBottom = Math.max(openPos, closePos);
|
|
65
|
+
const bodyHeight = Math.abs(closePos - openPos);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
type: 'group',
|
|
69
|
+
children: [
|
|
70
|
+
// Upper wick
|
|
71
|
+
{
|
|
72
|
+
type: 'line',
|
|
73
|
+
shape: {
|
|
74
|
+
x1: xPos,
|
|
75
|
+
y1: highPos,
|
|
76
|
+
x2: xPos,
|
|
77
|
+
y2: bodyTop,
|
|
78
|
+
},
|
|
79
|
+
style: {
|
|
80
|
+
stroke: wickColor,
|
|
81
|
+
lineWidth: 1,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
// Lower wick
|
|
85
|
+
{
|
|
86
|
+
type: 'line',
|
|
87
|
+
shape: {
|
|
88
|
+
x1: xPos,
|
|
89
|
+
y1: bodyBottom,
|
|
90
|
+
x2: xPos,
|
|
91
|
+
y2: lowPos,
|
|
92
|
+
},
|
|
93
|
+
style: {
|
|
94
|
+
stroke: wickColor,
|
|
95
|
+
lineWidth: 1,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
// Body
|
|
99
|
+
{
|
|
100
|
+
type: 'rect',
|
|
101
|
+
shape: {
|
|
102
|
+
x: xPos - barWidth / 2,
|
|
103
|
+
y: bodyTop,
|
|
104
|
+
width: barWidth,
|
|
105
|
+
height: bodyHeight || 1, // Minimum height for doji
|
|
106
|
+
},
|
|
107
|
+
style: {
|
|
108
|
+
fill: color,
|
|
109
|
+
stroke: borderColor,
|
|
110
|
+
lineWidth: 1,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
} else {
|
|
116
|
+
// Bar style (OHLC bar)
|
|
117
|
+
const tickWidth = barWidth * 0.5;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
type: 'group',
|
|
121
|
+
children: [
|
|
122
|
+
// Vertical line (low to high)
|
|
123
|
+
{
|
|
124
|
+
type: 'line',
|
|
125
|
+
shape: {
|
|
126
|
+
x1: xPos,
|
|
127
|
+
y1: lowPos,
|
|
128
|
+
x2: xPos,
|
|
129
|
+
y2: highPos,
|
|
130
|
+
},
|
|
131
|
+
style: {
|
|
132
|
+
stroke: color,
|
|
133
|
+
lineWidth: 1,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
// Open tick (left)
|
|
137
|
+
{
|
|
138
|
+
type: 'line',
|
|
139
|
+
shape: {
|
|
140
|
+
x1: xPos - tickWidth,
|
|
141
|
+
y1: openPos,
|
|
142
|
+
x2: xPos,
|
|
143
|
+
y2: openPos,
|
|
144
|
+
},
|
|
145
|
+
style: {
|
|
146
|
+
stroke: color,
|
|
147
|
+
lineWidth: 1,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
// Close tick (right)
|
|
151
|
+
{
|
|
152
|
+
type: 'line',
|
|
153
|
+
shape: {
|
|
154
|
+
x1: xPos,
|
|
155
|
+
y1: closePos,
|
|
156
|
+
x2: xPos + tickWidth,
|
|
157
|
+
y2: closePos,
|
|
158
|
+
},
|
|
159
|
+
style: {
|
|
160
|
+
stroke: color,
|
|
161
|
+
lineWidth: 1,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
data: ohlcData,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -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
|
|
|
@@ -78,7 +69,12 @@ export class PolylineRenderer implements SeriesRenderer {
|
|
|
78
69
|
|
|
79
70
|
if (pixelPoints.length < 2) continue;
|
|
80
71
|
|
|
81
|
-
|
|
72
|
+
// Detect na/NaN line_color (means no stroke)
|
|
73
|
+
const rawLineColor = pl.line_color;
|
|
74
|
+
const isNaLineColor = rawLineColor === null || rawLineColor === undefined ||
|
|
75
|
+
(typeof rawLineColor === 'number' && isNaN(rawLineColor)) ||
|
|
76
|
+
rawLineColor === 'na' || rawLineColor === 'NaN';
|
|
77
|
+
const lineColor = isNaLineColor ? null : (rawLineColor || '#2962ff');
|
|
82
78
|
const lineWidth = pl.line_width || 1;
|
|
83
79
|
const dashPattern = this.getDashPattern(pl.line_style);
|
|
84
80
|
|
|
@@ -104,32 +100,36 @@ export class PolylineRenderer implements SeriesRenderer {
|
|
|
104
100
|
}
|
|
105
101
|
}
|
|
106
102
|
|
|
107
|
-
// Stroke (line segments)
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
103
|
+
// Stroke (line segments) — skip entirely if line_color is na
|
|
104
|
+
if (lineColor && lineWidth > 0) {
|
|
105
|
+
if (pl.curved) {
|
|
106
|
+
const pathData = this.buildCurvedPath(pixelPoints, pl.closed);
|
|
107
|
+
children.push({
|
|
108
|
+
type: 'path',
|
|
109
|
+
shape: { pathData },
|
|
110
|
+
style: { fill: 'none', stroke: lineColor, lineWidth, lineDash: dashPattern },
|
|
111
|
+
silent: true,
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
const allPoints = pl.closed ? [...pixelPoints, pixelPoints[0]] : pixelPoints;
|
|
115
|
+
children.push({
|
|
116
|
+
type: 'polyline',
|
|
117
|
+
shape: { points: allPoints },
|
|
118
|
+
style: { fill: 'none', stroke: lineColor, lineWidth, lineDash: dashPattern },
|
|
119
|
+
silent: true,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
124
122
|
}
|
|
125
123
|
}
|
|
126
124
|
|
|
127
125
|
return { type: 'group', children };
|
|
128
126
|
},
|
|
129
|
-
data: [[0, lastBarIndex
|
|
127
|
+
data: [[0, lastBarIndex]],
|
|
130
128
|
clip: true,
|
|
131
|
-
encode: { x: [0, 1]
|
|
132
|
-
|
|
129
|
+
encode: { x: [0, 1] },
|
|
130
|
+
// Prevent ECharts visual system from overriding element colors with palette
|
|
131
|
+
itemStyle: { color: 'transparent', borderColor: 'transparent' },
|
|
132
|
+
z: 15,
|
|
133
133
|
silent: true,
|
|
134
134
|
emphasis: { disabled: true },
|
|
135
135
|
};
|
package/src/types.ts
CHANGED
|
@@ -116,9 +116,18 @@ export interface QFChartOptions {
|
|
|
116
116
|
position: 'floating' | 'left' | 'right';
|
|
117
117
|
triggerOn?: 'mousemove' | 'click' | 'none'; // When to show tooltip/crosshair, default 'mousemove'
|
|
118
118
|
};
|
|
119
|
+
grid?: {
|
|
120
|
+
show?: boolean; // Show/hide split lines (default true)
|
|
121
|
+
lineColor?: string; // Split line color (default '#334155')
|
|
122
|
+
lineOpacity?: number; // Split line opacity (default 0.5 main, 0.3 indicator panes)
|
|
123
|
+
borderColor?: string; // Axis line color (default '#334155')
|
|
124
|
+
borderShow?: boolean; // Show/hide axis border lines (default true)
|
|
125
|
+
};
|
|
119
126
|
layout?: {
|
|
120
|
-
mainPaneHeight
|
|
121
|
-
gap
|
|
127
|
+
mainPaneHeight?: string; // e.g. "60%"
|
|
128
|
+
gap?: number; // Gap between panes in % (default ~5)
|
|
129
|
+
left?: string; // Grid left margin (default '10%')
|
|
130
|
+
right?: string; // Grid right margin (default '10%')
|
|
122
131
|
};
|
|
123
132
|
watermark?: boolean; // Default true
|
|
124
133
|
}
|
package/src/utils/ColorUtils.ts
CHANGED
|
@@ -4,7 +4,7 @@ export class ColorUtils {
|
|
|
4
4
|
* Supports: hex (#RRGGBB, #RRGGBBAA), named colors (green, red), rgba(r,g,b,a), rgb(r,g,b)
|
|
5
5
|
*/
|
|
6
6
|
public static parseColor(colorStr: string): { color: string; opacity: number } {
|
|
7
|
-
if (!colorStr) {
|
|
7
|
+
if (!colorStr || typeof colorStr !== 'string') {
|
|
8
8
|
return { color: '#888888', opacity: 0.2 };
|
|
9
9
|
}
|
|
10
10
|
|