@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.
@@ -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 xOff1 = (line1.xloc === 'bar_index' || line1.xloc === 'bi') ? offset : 0;
64
- const xOff2 = (line2.xloc === 'bar_index' || line2.xloc === 'bi') ? offset : 0;
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([line1.x1 + xOff1, line1.y1]);
67
- let p1End = api.coord([line1.x2 + xOff1, line1.y2]);
68
- let p2Start = api.coord([line2.x1 + xOff2, line2.y1]);
69
- let p2End = api.coord([line2.x2 + xOff2, line2.y2]);
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
- const x = useBi ? (pt.index ?? 0) + xOff : (pt.time ?? 0);
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
+ }
@@ -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';