@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.
@@ -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
- const ohlcData = dataArray
10
- .map((val, i) => {
11
- if (val === null || !Array.isArray(val) || val.length !== 4) return null;
12
-
13
- const [open, high, low, close] = val;
14
- const pointOpts = optionsArray[i] || {};
15
- const color = pointOpts.color || colorArray[i] || plotOptions.color || defaultColor;
16
- const wickColor = pointOpts.wickcolor || plotOptions.wickcolor || color;
17
- const borderColor = pointOpts.bordercolor || plotOptions.bordercolor || wickColor;
18
-
19
- // Store colors in value array at positions 5, 6, and 7 for access in renderItem
20
- return [i, open, close, low, high, color, wickColor, borderColor];
21
- })
22
- .filter((item) => item !== null);
23
-
24
- return {
25
- name: seriesName,
26
- type: 'custom',
27
- xAxisIndex: xAxisIndex,
28
- yAxisIndex: yAxisIndex,
29
- renderItem: (params: any, api: any) => {
30
- const xValue = api.value(0);
31
- const openValue = api.value(1);
32
- const closeValue = api.value(2);
33
- const lowValue = api.value(3);
34
- const highValue = api.value(4);
35
- const color = api.value(5);
36
- const wickColor = api.value(6);
37
- const borderColor = api.value(7);
38
-
39
- if (isNaN(openValue) || isNaN(closeValue) || isNaN(lowValue) || isNaN(highValue)) {
40
- return null;
41
- }
42
-
43
- const xPos = api.coord([xValue, 0])[0];
44
- const openPos = api.coord([xValue, openValue])[1];
45
- const closePos = api.coord([xValue, closeValue])[1];
46
- const lowPos = api.coord([xValue, lowValue])[1];
47
- const highPos = api.coord([xValue, highValue])[1];
48
-
49
- const barWidth = api.size([1, 0])[0] * 0.6;
50
-
51
- if (isCandle) {
52
- // Classic candlestick rendering
53
- const bodyTop = Math.min(openPos, closePos);
54
- const bodyBottom = Math.max(openPos, closePos);
55
- const bodyHeight = Math.abs(closePos - openPos);
56
-
57
- return {
58
- type: 'group',
59
- children: [
60
- // Upper wick
61
- {
62
- type: 'line',
63
- shape: {
64
- x1: xPos,
65
- y1: highPos,
66
- x2: xPos,
67
- y2: bodyTop,
68
- },
69
- style: {
70
- stroke: wickColor,
71
- lineWidth: 1,
72
- },
73
- },
74
- // Lower wick
75
- {
76
- type: 'line',
77
- shape: {
78
- x1: xPos,
79
- y1: bodyBottom,
80
- x2: xPos,
81
- y2: lowPos,
82
- },
83
- style: {
84
- stroke: wickColor,
85
- lineWidth: 1,
86
- },
87
- },
88
- // Body
89
- {
90
- type: 'rect',
91
- shape: {
92
- x: xPos - barWidth / 2,
93
- y: bodyTop,
94
- width: barWidth,
95
- height: bodyHeight || 1, // Minimum height for doji
96
- },
97
- style: {
98
- fill: color,
99
- stroke: borderColor,
100
- lineWidth: 1,
101
- },
102
- },
103
- ],
104
- };
105
- } else {
106
- // Bar style (OHLC bar)
107
- const tickWidth = barWidth * 0.5;
108
-
109
- return {
110
- type: 'group',
111
- children: [
112
- // Vertical line (low to high)
113
- {
114
- type: 'line',
115
- shape: {
116
- x1: xPos,
117
- y1: lowPos,
118
- x2: xPos,
119
- y2: highPos,
120
- },
121
- style: {
122
- stroke: color,
123
- lineWidth: 1,
124
- },
125
- },
126
- // Open tick (left)
127
- {
128
- type: 'line',
129
- shape: {
130
- x1: xPos - tickWidth,
131
- y1: openPos,
132
- x2: xPos,
133
- y2: openPos,
134
- },
135
- style: {
136
- stroke: color,
137
- lineWidth: 1,
138
- },
139
- },
140
- // Close tick (right)
141
- {
142
- type: 'line',
143
- shape: {
144
- x1: xPos,
145
- y1: closePos,
146
- x2: xPos + tickWidth,
147
- y2: closePos,
148
- },
149
- style: {
150
- stroke: color,
151
- lineWidth: 1,
152
- },
153
- },
154
- ],
155
- };
156
- }
157
- },
158
- data: ohlcData,
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
- // Dims 2/3 are yMin/yMax for axis scaling.
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
- const lineColor = pl.line_color || '#2962ff';
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 (pl.curved) {
109
- const pathData = this.buildCurvedPath(pixelPoints, pl.closed);
110
- children.push({
111
- type: 'path',
112
- shape: { pathData },
113
- style: { fill: 'none', stroke: lineColor, lineWidth, lineDash: dashPattern },
114
- silent: true,
115
- });
116
- } else {
117
- const allPoints = pl.closed ? [...pixelPoints, pixelPoints[0]] : pixelPoints;
118
- children.push({
119
- type: 'polyline',
120
- shape: { points: allPoints },
121
- style: { fill: 'none', stroke: lineColor, lineWidth, lineDash: dashPattern },
122
- silent: true,
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, yMin, yMax]],
127
+ data: [[0, lastBarIndex]],
130
128
  clip: true,
131
- encode: { x: [0, 1], y: [2, 3] },
132
- z: 12,
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: string; // e.g. "60%"
121
- gap: number; // e.g. 5 (percent)
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
  }
@@ -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