@qfo/qfchart 0.8.4 → 0.8.5
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/qfchart.min.browser.js +19 -18
- package/dist/qfchart.min.es.js +19 -18
- package/package.json +1 -1
- package/src/components/GraphicBuilder.ts +44 -0
- package/src/components/Indicator.ts +106 -106
- package/src/components/SeriesBuilder.ts +5 -0
- package/src/components/renderers/BoxRenderer.ts +7 -5
- package/src/components/renderers/DrawingLineRenderer.ts +7 -5
- package/src/components/renderers/FillRenderer.ts +54 -45
- package/src/components/renderers/LabelRenderer.ts +22 -9
- package/src/components/renderers/LineRenderer.ts +44 -44
- package/src/components/renderers/LinefillRenderer.ts +11 -8
- package/src/components/renderers/PolylineRenderer.ts +11 -4
- package/src/components/renderers/SeriesRenderer.ts +78 -0
- package/src/components/renderers/StepRenderer.ts +39 -39
- package/src/utils/ShapeUtils.ts +5 -0
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
-
|
|
3
|
-
export class LineRenderer implements SeriesRenderer {
|
|
4
|
-
render(context: RenderContext): any {
|
|
5
|
-
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, plotOptions } = context;
|
|
6
|
-
const defaultColor = '#2962ff';
|
|
7
|
-
|
|
8
|
-
return {
|
|
9
|
-
name: seriesName,
|
|
10
|
-
type: 'custom',
|
|
11
|
-
xAxisIndex: xAxisIndex,
|
|
12
|
-
yAxisIndex: yAxisIndex,
|
|
13
|
-
renderItem: (params: any, api: any) => {
|
|
14
|
-
const index = params.dataIndex;
|
|
15
|
-
if (index === 0) return; // Need at least two points for a line segment
|
|
16
|
-
|
|
17
|
-
const y2 = api.value(1);
|
|
18
|
-
const y1 = api.value(2); // We'll store prevValue in the data
|
|
19
|
-
|
|
20
|
-
if (y2 === null || isNaN(y2) || y1 === null || isNaN(y1)) return;
|
|
21
|
-
|
|
22
|
-
const p1 = api.coord([index - 1, y1]);
|
|
23
|
-
const p2 = api.coord([index, y2]);
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
type: 'line',
|
|
27
|
-
shape: {
|
|
28
|
-
x1: p1[0],
|
|
29
|
-
y1: p1[1],
|
|
30
|
-
x2: p2[0],
|
|
31
|
-
y2: p2[1],
|
|
32
|
-
},
|
|
33
|
-
style: {
|
|
34
|
-
stroke: colorArray[index] || plotOptions.color || defaultColor,
|
|
35
|
-
lineWidth: plotOptions.linewidth || 1,
|
|
36
|
-
},
|
|
37
|
-
silent: true,
|
|
38
|
-
};
|
|
39
|
-
},
|
|
40
|
-
// Data format: [index, value, prevValue]
|
|
41
|
-
data: dataArray.map((val, i) => [i, val, i > 0 ? dataArray[i - 1] : null]),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
}
|
|
1
|
+
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
+
|
|
3
|
+
export class LineRenderer implements SeriesRenderer {
|
|
4
|
+
render(context: RenderContext): any {
|
|
5
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, plotOptions } = context;
|
|
6
|
+
const defaultColor = '#2962ff';
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
name: seriesName,
|
|
10
|
+
type: 'custom',
|
|
11
|
+
xAxisIndex: xAxisIndex,
|
|
12
|
+
yAxisIndex: yAxisIndex,
|
|
13
|
+
renderItem: (params: any, api: any) => {
|
|
14
|
+
const index = params.dataIndex;
|
|
15
|
+
if (index === 0) return; // Need at least two points for a line segment
|
|
16
|
+
|
|
17
|
+
const y2 = api.value(1);
|
|
18
|
+
const y1 = api.value(2); // We'll store prevValue in the data
|
|
19
|
+
|
|
20
|
+
if (y2 === null || isNaN(y2) || y1 === null || isNaN(y1)) return;
|
|
21
|
+
|
|
22
|
+
const p1 = api.coord([index - 1, y1]);
|
|
23
|
+
const p2 = api.coord([index, y2]);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
type: 'line',
|
|
27
|
+
shape: {
|
|
28
|
+
x1: p1[0],
|
|
29
|
+
y1: p1[1],
|
|
30
|
+
x2: p2[0],
|
|
31
|
+
y2: p2[1],
|
|
32
|
+
},
|
|
33
|
+
style: {
|
|
34
|
+
stroke: colorArray[index] || plotOptions.color || defaultColor,
|
|
35
|
+
lineWidth: plotOptions.linewidth || 1,
|
|
36
|
+
},
|
|
37
|
+
silent: true,
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
// Data format: [index, value, prevValue]
|
|
41
|
+
data: dataArray.map((val, i) => [i, val, i > 0 ? dataArray[i - 1] : null]),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
1
|
+
import { SeriesRenderer, RenderContext, resolveXCoord } from './SeriesRenderer';
|
|
2
2
|
import { ColorUtils } from '../../utils/ColorUtils';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -9,7 +9,7 @@ import { ColorUtils } from '../../utils/ColorUtils';
|
|
|
9
9
|
*/
|
|
10
10
|
export class LinefillRenderer implements SeriesRenderer {
|
|
11
11
|
render(context: RenderContext): any {
|
|
12
|
-
const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset } = context;
|
|
12
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset, timeToIndex, marketData } = context;
|
|
13
13
|
const offset = dataIndexOffset || 0;
|
|
14
14
|
|
|
15
15
|
// Collect all non-deleted linefill objects from the sparse dataArray.
|
|
@@ -60,13 +60,16 @@ export class LinefillRenderer implements SeriesRenderer {
|
|
|
60
60
|
const line2 = lf.line2;
|
|
61
61
|
if (!line1 || !line2 || line1._deleted || line2._deleted) continue;
|
|
62
62
|
|
|
63
|
-
const
|
|
64
|
-
const
|
|
63
|
+
const l1x1 = resolveXCoord(line1.x1, line1.xloc, offset, timeToIndex, marketData);
|
|
64
|
+
const l1x2 = resolveXCoord(line1.x2, line1.xloc, offset, timeToIndex, marketData);
|
|
65
|
+
const l2x1 = resolveXCoord(line2.x1, line2.xloc, offset, timeToIndex, marketData);
|
|
66
|
+
const l2x2 = resolveXCoord(line2.x2, line2.xloc, offset, timeToIndex, marketData);
|
|
67
|
+
if (isNaN(l1x1) || isNaN(l1x2) || isNaN(l2x1) || isNaN(l2x2)) continue;
|
|
65
68
|
|
|
66
|
-
let p1Start = api.coord([
|
|
67
|
-
let p1End = api.coord([
|
|
68
|
-
let p2Start = api.coord([
|
|
69
|
-
let p2End = api.coord([
|
|
69
|
+
let p1Start = api.coord([l1x1, line1.y1]);
|
|
70
|
+
let p1End = api.coord([l1x2, line1.y2]);
|
|
71
|
+
let p2Start = api.coord([l2x1, line2.y1]);
|
|
72
|
+
let p2End = api.coord([l2x2, line2.y2]);
|
|
70
73
|
|
|
71
74
|
// Handle line extensions
|
|
72
75
|
const extend1 = line1.extend || 'none';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
1
|
+
import { SeriesRenderer, RenderContext, resolveXCoord } from './SeriesRenderer';
|
|
2
2
|
import { ColorUtils } from '../../utils/ColorUtils';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -10,7 +10,7 @@ import { ColorUtils } from '../../utils/ColorUtils';
|
|
|
10
10
|
*/
|
|
11
11
|
export class PolylineRenderer implements SeriesRenderer {
|
|
12
12
|
render(context: RenderContext): any {
|
|
13
|
-
const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset } = context;
|
|
13
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset, timeToIndex, marketData } = context;
|
|
14
14
|
const offset = dataIndexOffset || 0;
|
|
15
15
|
|
|
16
16
|
// Collect all non-deleted polyline objects from the sparse dataArray.
|
|
@@ -57,15 +57,22 @@ export class PolylineRenderer implements SeriesRenderer {
|
|
|
57
57
|
if (!points || points.length < 2) continue;
|
|
58
58
|
|
|
59
59
|
const useBi = pl.xloc === 'bi' || pl.xloc === 'bar_index';
|
|
60
|
-
const xOff = useBi ? offset : 0;
|
|
61
60
|
|
|
62
61
|
// Convert chart.point objects to pixel coordinates
|
|
63
62
|
const pixelPoints: number[][] = [];
|
|
63
|
+
let skipPoly = false;
|
|
64
64
|
for (const pt of points) {
|
|
65
|
-
|
|
65
|
+
let x: number;
|
|
66
|
+
if (useBi) {
|
|
67
|
+
x = (pt.index ?? 0) + offset;
|
|
68
|
+
} else {
|
|
69
|
+
x = resolveXCoord(pt.time ?? 0, 'bt', offset, timeToIndex, marketData);
|
|
70
|
+
if (isNaN(x)) { skipPoly = true; break; }
|
|
71
|
+
}
|
|
66
72
|
const y = pt.price ?? 0;
|
|
67
73
|
pixelPoints.push(api.coord([x, y]));
|
|
68
74
|
}
|
|
75
|
+
if (skipPoly) continue;
|
|
69
76
|
|
|
70
77
|
if (pixelPoints.length < 2) continue;
|
|
71
78
|
|
|
@@ -14,8 +14,86 @@ export interface RenderContext {
|
|
|
14
14
|
plotName?: string;
|
|
15
15
|
indicator?: any; // Reference to parent indicator object if needed
|
|
16
16
|
dataIndexOffset?: number; // Padding offset for converting bar_index to ECharts index
|
|
17
|
+
timeToIndex?: Map<number, number>; // Map timestamp → real data index (for xloc.bar_time)
|
|
18
|
+
marketData?: OHLCV[]; // Raw market data (for interpolating future timestamps)
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface SeriesRenderer {
|
|
20
22
|
render(context: RenderContext): any;
|
|
21
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert an x-coordinate from a drawing object to an ECharts padded bar index.
|
|
27
|
+
* Handles both xloc modes:
|
|
28
|
+
* - 'bar_index' / 'bi': x is already a bar index, just add padding offset
|
|
29
|
+
* - 'bar_time' / 'bt': x is a timestamp, look up in timeToIndex or interpolate
|
|
30
|
+
*
|
|
31
|
+
* For future timestamps (beyond the last candle), extrapolates position using
|
|
32
|
+
* the average bar duration from market data.
|
|
33
|
+
*
|
|
34
|
+
* Returns NaN if the coordinate cannot be resolved.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveXCoord(
|
|
37
|
+
x: number,
|
|
38
|
+
xloc: string | undefined,
|
|
39
|
+
offset: number,
|
|
40
|
+
timeToIndex?: Map<number, number>,
|
|
41
|
+
marketData?: OHLCV[],
|
|
42
|
+
): number {
|
|
43
|
+
if (!xloc || xloc === 'bar_index' || xloc === 'bi') {
|
|
44
|
+
return x + offset;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// xloc is 'bar_time' / 'bt' — x is a timestamp
|
|
48
|
+
if (timeToIndex) {
|
|
49
|
+
const idx = timeToIndex.get(x);
|
|
50
|
+
if (idx !== undefined) {
|
|
51
|
+
return idx + offset;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Timestamp not in the map — interpolate (likely a future timestamp)
|
|
56
|
+
if (marketData && marketData.length >= 2) {
|
|
57
|
+
const lastTime = marketData[marketData.length - 1].time;
|
|
58
|
+
const lastIndex = marketData.length - 1;
|
|
59
|
+
|
|
60
|
+
if (x > lastTime) {
|
|
61
|
+
// Future timestamp: extrapolate using average bar duration
|
|
62
|
+
// Use the last bar's interval as representative
|
|
63
|
+
const prevTime = marketData[marketData.length - 2].time;
|
|
64
|
+
const barDuration = lastTime - prevTime;
|
|
65
|
+
if (barDuration > 0) {
|
|
66
|
+
const barsAhead = (x - lastTime) / barDuration;
|
|
67
|
+
return lastIndex + barsAhead + offset;
|
|
68
|
+
}
|
|
69
|
+
} else if (x < marketData[0].time) {
|
|
70
|
+
// Past timestamp before data start: extrapolate backwards
|
|
71
|
+
const firstTime = marketData[0].time;
|
|
72
|
+
const secondTime = marketData[1].time;
|
|
73
|
+
const barDuration = secondTime - firstTime;
|
|
74
|
+
if (barDuration > 0) {
|
|
75
|
+
const barsBehind = (firstTime - x) / barDuration;
|
|
76
|
+
return 0 - barsBehind + offset;
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
// Timestamp within data range but not an exact match — find nearest
|
|
80
|
+
// Binary search for the closest bar
|
|
81
|
+
let lo = 0, hi = marketData.length - 1;
|
|
82
|
+
while (lo < hi) {
|
|
83
|
+
const mid = (lo + hi) >> 1;
|
|
84
|
+
if (marketData[mid].time < x) lo = mid + 1;
|
|
85
|
+
else hi = mid;
|
|
86
|
+
}
|
|
87
|
+
// Interpolate between lo-1 and lo
|
|
88
|
+
if (lo > 0) {
|
|
89
|
+
const t0 = marketData[lo - 1].time;
|
|
90
|
+
const t1 = marketData[lo].time;
|
|
91
|
+
const frac = (x - t0) / (t1 - t0);
|
|
92
|
+
return (lo - 1) + frac + offset;
|
|
93
|
+
}
|
|
94
|
+
return lo + offset;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return NaN;
|
|
99
|
+
}
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
-
|
|
3
|
-
export class StepRenderer implements SeriesRenderer {
|
|
4
|
-
render(context: RenderContext): any {
|
|
5
|
-
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, plotOptions } = context;
|
|
6
|
-
const defaultColor = '#2962ff';
|
|
7
|
-
|
|
8
|
-
return {
|
|
9
|
-
name: seriesName,
|
|
10
|
-
type: 'custom',
|
|
11
|
-
xAxisIndex: xAxisIndex,
|
|
12
|
-
yAxisIndex: yAxisIndex,
|
|
13
|
-
renderItem: (params: any, api: any) => {
|
|
14
|
-
const x = api.value(0);
|
|
15
|
-
const y = api.value(1);
|
|
16
|
-
if (isNaN(y) || y === null) return;
|
|
17
|
-
|
|
18
|
-
const coords = api.coord([x, y]);
|
|
19
|
-
const width = api.size([1, 0])[0];
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
type: 'line',
|
|
23
|
-
shape: {
|
|
24
|
-
x1: coords[0] - width / 2,
|
|
25
|
-
y1: coords[1],
|
|
26
|
-
x2: coords[0] + width / 2,
|
|
27
|
-
y2: coords[1],
|
|
28
|
-
},
|
|
29
|
-
style: {
|
|
30
|
-
stroke: colorArray[params.dataIndex] || plotOptions.color || defaultColor,
|
|
31
|
-
lineWidth: plotOptions.linewidth || 1,
|
|
32
|
-
},
|
|
33
|
-
silent: true,
|
|
34
|
-
};
|
|
35
|
-
},
|
|
36
|
-
data: dataArray.map((val, i) => [i, val]),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
1
|
+
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
+
|
|
3
|
+
export class StepRenderer implements SeriesRenderer {
|
|
4
|
+
render(context: RenderContext): any {
|
|
5
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, plotOptions } = context;
|
|
6
|
+
const defaultColor = '#2962ff';
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
name: seriesName,
|
|
10
|
+
type: 'custom',
|
|
11
|
+
xAxisIndex: xAxisIndex,
|
|
12
|
+
yAxisIndex: yAxisIndex,
|
|
13
|
+
renderItem: (params: any, api: any) => {
|
|
14
|
+
const x = api.value(0);
|
|
15
|
+
const y = api.value(1);
|
|
16
|
+
if (isNaN(y) || y === null) return;
|
|
17
|
+
|
|
18
|
+
const coords = api.coord([x, y]);
|
|
19
|
+
const width = api.size([1, 0])[0];
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
type: 'line',
|
|
23
|
+
shape: {
|
|
24
|
+
x1: coords[0] - width / 2,
|
|
25
|
+
y1: coords[1],
|
|
26
|
+
x2: coords[0] + width / 2,
|
|
27
|
+
y2: coords[1],
|
|
28
|
+
},
|
|
29
|
+
style: {
|
|
30
|
+
stroke: colorArray[params.dataIndex] || plotOptions.color || defaultColor,
|
|
31
|
+
lineWidth: plotOptions.linewidth || 1,
|
|
32
|
+
},
|
|
33
|
+
silent: true,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
data: dataArray.map((val, i) => [i, val]),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/utils/ShapeUtils.ts
CHANGED
|
@@ -47,6 +47,11 @@ export class ShapeUtils {
|
|
|
47
47
|
case 'shape_label_up':
|
|
48
48
|
return 'path://M12 1l2 3h8a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-20a1 1 0 0 1-1-1v-14a1 1 0 0 1 1-1h8z';
|
|
49
49
|
|
|
50
|
+
case 'labelcenter':
|
|
51
|
+
case 'shape_label_center':
|
|
52
|
+
// Rounded rectangle with no pointer — centered at anchor
|
|
53
|
+
return 'path://M1 1h22a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-22a1 1 0 0 1-1-1v-16a1 1 0 0 1 1-1z';
|
|
54
|
+
|
|
50
55
|
case 'square':
|
|
51
56
|
case 'shape_square':
|
|
52
57
|
return 'rect';
|