@qfo/qfchart 0.6.4 → 0.6.6
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/README.md +1 -0
- package/dist/qfchart.min.browser.js +15 -15
- package/dist/qfchart.min.es.js +15 -15
- package/package.json +1 -1
- package/src/QFChart.ts +9 -6
- package/src/components/LayoutManager.ts +53 -49
- package/src/components/SeriesBuilder.ts +250 -941
- package/src/components/SeriesRendererFactory.ts +36 -0
- package/src/components/renderers/BackgroundRenderer.ts +47 -0
- package/src/components/renderers/FillRenderer.ts +99 -0
- package/src/components/renderers/HistogramRenderer.ts +20 -0
- package/src/components/renderers/LineRenderer.ts +44 -0
- package/src/components/renderers/OHLCBarRenderer.ts +161 -0
- package/src/components/renderers/ScatterRenderer.ts +54 -0
- package/src/components/renderers/SeriesRenderer.ts +20 -0
- package/src/components/renderers/ShapeRenderer.ts +121 -0
- package/src/components/renderers/StepRenderer.ts +39 -0
- package/src/types.ts +205 -205
- package/src/utils/AxisUtils.ts +63 -0
- package/src/utils/ColorUtils.ts +32 -0
- package/src/utils/ShapeUtils.ts +140 -0
- /package/src/{Utils.ts → utils/CanvasUtils.ts} +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { SeriesRenderer } from './renderers/SeriesRenderer';
|
|
2
|
+
import { LineRenderer } from './renderers/LineRenderer';
|
|
3
|
+
import { StepRenderer } from './renderers/StepRenderer';
|
|
4
|
+
import { HistogramRenderer } from './renderers/HistogramRenderer';
|
|
5
|
+
import { ScatterRenderer } from './renderers/ScatterRenderer';
|
|
6
|
+
import { OHLCBarRenderer } from './renderers/OHLCBarRenderer';
|
|
7
|
+
import { ShapeRenderer } from './renderers/ShapeRenderer';
|
|
8
|
+
import { BackgroundRenderer } from './renderers/BackgroundRenderer';
|
|
9
|
+
import { FillRenderer } from './renderers/FillRenderer';
|
|
10
|
+
|
|
11
|
+
export class SeriesRendererFactory {
|
|
12
|
+
private static renderers: Map<string, SeriesRenderer> = new Map();
|
|
13
|
+
|
|
14
|
+
static {
|
|
15
|
+
this.register('line', new LineRenderer());
|
|
16
|
+
this.register('step', new StepRenderer());
|
|
17
|
+
this.register('histogram', new HistogramRenderer());
|
|
18
|
+
this.register('columns', new HistogramRenderer());
|
|
19
|
+
this.register('circles', new ScatterRenderer());
|
|
20
|
+
this.register('cross', new ScatterRenderer());
|
|
21
|
+
this.register('char', new ScatterRenderer());
|
|
22
|
+
this.register('bar', new OHLCBarRenderer());
|
|
23
|
+
this.register('candle', new OHLCBarRenderer());
|
|
24
|
+
this.register('shape', new ShapeRenderer());
|
|
25
|
+
this.register('background', new BackgroundRenderer());
|
|
26
|
+
this.register('fill', new FillRenderer());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public static register(style: string, renderer: SeriesRenderer) {
|
|
30
|
+
this.renderers.set(style, renderer);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static get(style: string): SeriesRenderer {
|
|
34
|
+
return this.renderers.get(style) || this.renderers.get('line')!; // Default to line
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
+
|
|
3
|
+
export class BackgroundRenderer implements SeriesRenderer {
|
|
4
|
+
render(context: RenderContext): any {
|
|
5
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray } = context;
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
name: seriesName,
|
|
9
|
+
type: 'custom',
|
|
10
|
+
xAxisIndex: xAxisIndex,
|
|
11
|
+
yAxisIndex: yAxisIndex,
|
|
12
|
+
z: -10,
|
|
13
|
+
renderItem: (params: any, api: any) => {
|
|
14
|
+
const xVal = api.value(0);
|
|
15
|
+
if (isNaN(xVal)) return;
|
|
16
|
+
|
|
17
|
+
const start = api.coord([xVal, 0.5]); // Use 0.5 as a fixed Y-value within [0,1] range
|
|
18
|
+
const size = api.size([1, 0]);
|
|
19
|
+
const width = size[0];
|
|
20
|
+
const sys = params.coordSys;
|
|
21
|
+
const x = start[0] - width / 2;
|
|
22
|
+
const barColor = colorArray[params.dataIndex];
|
|
23
|
+
const val = api.value(1);
|
|
24
|
+
|
|
25
|
+
if (!barColor || val === null || val === undefined || isNaN(val)) return;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
type: 'rect',
|
|
29
|
+
shape: {
|
|
30
|
+
x: x,
|
|
31
|
+
y: sys.y,
|
|
32
|
+
width: width,
|
|
33
|
+
height: sys.height,
|
|
34
|
+
},
|
|
35
|
+
style: {
|
|
36
|
+
fill: barColor,
|
|
37
|
+
opacity: 0.3,
|
|
38
|
+
},
|
|
39
|
+
silent: true,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
// Normalize data values to 0.5 (middle of [0,1] range) to prevent Y-axis scaling issues
|
|
43
|
+
// The actual value is only used to check if the background should render (non-null/non-NaN)
|
|
44
|
+
data: dataArray.map((val, i) => [i, val !== null && val !== undefined && !isNaN(val) ? 0.5 : null]),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
+
import { ColorUtils } from '../../utils/ColorUtils';
|
|
3
|
+
|
|
4
|
+
export class FillRenderer implements SeriesRenderer {
|
|
5
|
+
render(context: RenderContext): any {
|
|
6
|
+
const { seriesName, xAxisIndex, yAxisIndex, plotOptions, plotDataArrays, indicatorId, plotName } = context;
|
|
7
|
+
const totalDataLength = context.dataArray.length; // Use length from dataArray placeholder
|
|
8
|
+
|
|
9
|
+
// Fill plots reference other plots to fill the area between them
|
|
10
|
+
const plot1Key = plotOptions.plot1 ? `${indicatorId}::${plotOptions.plot1}` : null;
|
|
11
|
+
const plot2Key = plotOptions.plot2 ? `${indicatorId}::${plotOptions.plot2}` : null;
|
|
12
|
+
|
|
13
|
+
if (!plot1Key || !plot2Key) {
|
|
14
|
+
console.warn(`Fill plot "${plotName}" missing plot1 or plot2 reference`);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const plot1Data = plotDataArrays?.get(plot1Key);
|
|
19
|
+
const plot2Data = plotDataArrays?.get(plot2Key);
|
|
20
|
+
|
|
21
|
+
if (!plot1Data || !plot2Data) {
|
|
22
|
+
console.warn(`Fill plot "${plotName}" references non-existent plots: ${plotOptions.plot1}, ${plotOptions.plot2}`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Parse color to extract opacity
|
|
27
|
+
const { color: fillColor, opacity: fillOpacity } = ColorUtils.parseColor(plotOptions.color || 'rgba(128, 128, 128, 0.2)');
|
|
28
|
+
|
|
29
|
+
// Create fill data with previous values for smooth polygon rendering
|
|
30
|
+
const fillDataWithPrev: any[] = [];
|
|
31
|
+
for (let i = 0; i < totalDataLength; i++) {
|
|
32
|
+
const y1 = plot1Data[i];
|
|
33
|
+
const y2 = plot2Data[i];
|
|
34
|
+
const prevY1 = i > 0 ? plot1Data[i - 1] : null;
|
|
35
|
+
const prevY2 = i > 0 ? plot2Data[i - 1] : null;
|
|
36
|
+
|
|
37
|
+
fillDataWithPrev.push([i, y1, y2, prevY1, prevY2]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Add fill series with smooth area rendering
|
|
41
|
+
return {
|
|
42
|
+
name: seriesName,
|
|
43
|
+
type: 'custom',
|
|
44
|
+
xAxisIndex: xAxisIndex,
|
|
45
|
+
yAxisIndex: yAxisIndex,
|
|
46
|
+
z: -5, // Render behind lines but above background
|
|
47
|
+
renderItem: (params: any, api: any) => {
|
|
48
|
+
const index = params.dataIndex;
|
|
49
|
+
|
|
50
|
+
// Skip first point (no previous to connect to)
|
|
51
|
+
if (index === 0) return null;
|
|
52
|
+
|
|
53
|
+
const y1 = api.value(1); // Current upper
|
|
54
|
+
const y2 = api.value(2); // Current lower
|
|
55
|
+
const prevY1 = api.value(3); // Previous upper
|
|
56
|
+
const prevY2 = api.value(4); // Previous lower
|
|
57
|
+
|
|
58
|
+
// Skip if any value is null/NaN
|
|
59
|
+
if (
|
|
60
|
+
y1 === null ||
|
|
61
|
+
y2 === null ||
|
|
62
|
+
prevY1 === null ||
|
|
63
|
+
prevY2 === null ||
|
|
64
|
+
isNaN(y1) ||
|
|
65
|
+
isNaN(y2) ||
|
|
66
|
+
isNaN(prevY1) ||
|
|
67
|
+
isNaN(prevY2)
|
|
68
|
+
) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get pixel coordinates for all 4 points
|
|
73
|
+
const p1Prev = api.coord([index - 1, prevY1]); // Previous upper
|
|
74
|
+
const p1Curr = api.coord([index, y1]); // Current upper
|
|
75
|
+
const p2Curr = api.coord([index, y2]); // Current lower
|
|
76
|
+
const p2Prev = api.coord([index - 1, prevY2]); // Previous lower
|
|
77
|
+
|
|
78
|
+
// Create a smooth polygon connecting the segments
|
|
79
|
+
return {
|
|
80
|
+
type: 'polygon',
|
|
81
|
+
shape: {
|
|
82
|
+
points: [
|
|
83
|
+
p1Prev, // Top-left
|
|
84
|
+
p1Curr, // Top-right
|
|
85
|
+
p2Curr, // Bottom-right
|
|
86
|
+
p2Prev, // Bottom-left
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
style: {
|
|
90
|
+
fill: fillColor,
|
|
91
|
+
opacity: fillOpacity,
|
|
92
|
+
},
|
|
93
|
+
silent: true,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
data: fillDataWithPrev,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,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
|
+
|
|
8
|
+
return {
|
|
9
|
+
name: seriesName,
|
|
10
|
+
type: 'bar',
|
|
11
|
+
xAxisIndex: xAxisIndex,
|
|
12
|
+
yAxisIndex: yAxisIndex,
|
|
13
|
+
data: dataArray.map((val, i) => ({
|
|
14
|
+
value: val,
|
|
15
|
+
itemStyle: colorArray[i] ? { color: colorArray[i] } : undefined,
|
|
16
|
+
})),
|
|
17
|
+
itemStyle: { color: plotOptions.color || defaultColor },
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,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
|
+
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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
+
import { textToBase64Image } from '../../utils/CanvasUtils';
|
|
3
|
+
|
|
4
|
+
export class ScatterRenderer implements SeriesRenderer {
|
|
5
|
+
render(context: RenderContext): any {
|
|
6
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, plotOptions } = context;
|
|
7
|
+
const defaultColor = '#2962ff';
|
|
8
|
+
const style = plotOptions.style; // 'circles', 'cross', 'char'
|
|
9
|
+
|
|
10
|
+
// Special handling for invisible 'char' style
|
|
11
|
+
if (style === 'char') {
|
|
12
|
+
return {
|
|
13
|
+
name: seriesName,
|
|
14
|
+
type: 'scatter',
|
|
15
|
+
xAxisIndex: xAxisIndex,
|
|
16
|
+
yAxisIndex: yAxisIndex,
|
|
17
|
+
symbolSize: 0, // Invisible
|
|
18
|
+
data: dataArray.map((val, i) => ({
|
|
19
|
+
value: [i, val],
|
|
20
|
+
itemStyle: { opacity: 0 },
|
|
21
|
+
})),
|
|
22
|
+
silent: true, // No interaction
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const scatterData = dataArray
|
|
27
|
+
.map((val, i) => {
|
|
28
|
+
if (val === null) return null;
|
|
29
|
+
const pointColor = colorArray[i] || plotOptions.color || defaultColor;
|
|
30
|
+
const item: any = {
|
|
31
|
+
value: [i, val],
|
|
32
|
+
itemStyle: { color: pointColor },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (style === 'cross') {
|
|
36
|
+
item.symbol = `image://${textToBase64Image('+', pointColor, '24px')}`;
|
|
37
|
+
item.symbolSize = 16;
|
|
38
|
+
} else {
|
|
39
|
+
item.symbol = 'circle';
|
|
40
|
+
item.symbolSize = 6;
|
|
41
|
+
}
|
|
42
|
+
return item;
|
|
43
|
+
})
|
|
44
|
+
.filter((item) => item !== null);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
name: seriesName,
|
|
48
|
+
type: 'scatter',
|
|
49
|
+
xAxisIndex: xAxisIndex,
|
|
50
|
+
yAxisIndex: yAxisIndex,
|
|
51
|
+
data: scatterData,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IndicatorPlot, OHLCV } from '../../types';
|
|
2
|
+
|
|
3
|
+
export interface RenderContext {
|
|
4
|
+
seriesName: string;
|
|
5
|
+
xAxisIndex: number;
|
|
6
|
+
yAxisIndex: number;
|
|
7
|
+
dataArray: any[];
|
|
8
|
+
colorArray: any[];
|
|
9
|
+
optionsArray: any[];
|
|
10
|
+
plotOptions: any;
|
|
11
|
+
candlestickData?: OHLCV[]; // For shape positioning
|
|
12
|
+
plotDataArrays?: Map<string, number[]>; // For fill plots
|
|
13
|
+
indicatorId?: string;
|
|
14
|
+
plotName?: string;
|
|
15
|
+
indicator?: any; // Reference to parent indicator object if needed
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SeriesRenderer {
|
|
19
|
+
render(context: RenderContext): any;
|
|
20
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
+
import { ShapeUtils } from '../../utils/ShapeUtils';
|
|
3
|
+
|
|
4
|
+
export class ShapeRenderer implements SeriesRenderer {
|
|
5
|
+
render(context: RenderContext): any {
|
|
6
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray, optionsArray, plotOptions, candlestickData } = context;
|
|
7
|
+
const defaultColor = '#2962ff';
|
|
8
|
+
|
|
9
|
+
const shapeData = dataArray
|
|
10
|
+
.map((val, i) => {
|
|
11
|
+
// Merge global options with per-point options to get location first
|
|
12
|
+
const pointOpts = optionsArray[i] || {};
|
|
13
|
+
const globalOpts = plotOptions;
|
|
14
|
+
const location = pointOpts.location || globalOpts.location || 'absolute';
|
|
15
|
+
|
|
16
|
+
// For location="absolute", always draw the shape (ignore value)
|
|
17
|
+
// For other locations, only draw if value is truthy (TradingView behavior)
|
|
18
|
+
if (location !== 'absolute' && !val) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// If we get here and val is null/undefined, it means location is absolute
|
|
23
|
+
// In that case, we still need a valid value for positioning
|
|
24
|
+
// Use the value if it exists, otherwise we'd need a fallback
|
|
25
|
+
// But in TradingView, absolute location still expects a value for Y position
|
|
26
|
+
if (val === null || val === undefined) {
|
|
27
|
+
return null; // Can't plot without a Y coordinate
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const color = pointOpts.color || globalOpts.color || defaultColor;
|
|
31
|
+
const shape = pointOpts.shape || globalOpts.shape || 'circle';
|
|
32
|
+
const size = pointOpts.size || globalOpts.size || 'normal';
|
|
33
|
+
const text = pointOpts.text || globalOpts.text;
|
|
34
|
+
const textColor = pointOpts.textcolor || globalOpts.textcolor || 'white';
|
|
35
|
+
|
|
36
|
+
// NEW: Get width and height
|
|
37
|
+
const width = pointOpts.width || globalOpts.width;
|
|
38
|
+
const height = pointOpts.height || globalOpts.height;
|
|
39
|
+
|
|
40
|
+
// Positioning based on location
|
|
41
|
+
let yValue = val; // Default to absolute value
|
|
42
|
+
let symbolOffset: (string | number)[] = [0, 0];
|
|
43
|
+
|
|
44
|
+
if (location === 'abovebar') {
|
|
45
|
+
// Shape above the candle
|
|
46
|
+
if (candlestickData && candlestickData[i]) {
|
|
47
|
+
yValue = candlestickData[i].high;
|
|
48
|
+
}
|
|
49
|
+
symbolOffset = [0, '-150%']; // Shift up
|
|
50
|
+
} else if (location === 'belowbar') {
|
|
51
|
+
// Shape below the candle
|
|
52
|
+
if (candlestickData && candlestickData[i]) {
|
|
53
|
+
yValue = candlestickData[i].low;
|
|
54
|
+
}
|
|
55
|
+
symbolOffset = [0, '150%']; // Shift down
|
|
56
|
+
} else if (location === 'top') {
|
|
57
|
+
// Shape at top of chart - we need to use a very high value
|
|
58
|
+
// This would require knowing the y-axis max, which we don't have here easily
|
|
59
|
+
// For now, use a placeholder approach - might need to calculate from data
|
|
60
|
+
// Or we can use a percentage of the viewport? ECharts doesn't support that directly in scatter.
|
|
61
|
+
// Best approach: use a large multiplier of current value or track max
|
|
62
|
+
// Simplified: use coordinate system max (will need enhancement)
|
|
63
|
+
yValue = val; // For now, keep absolute - would need axis max
|
|
64
|
+
symbolOffset = [0, 0];
|
|
65
|
+
} else if (location === 'bottom') {
|
|
66
|
+
// Shape at bottom of chart
|
|
67
|
+
yValue = val; // For now, keep absolute - would need axis min
|
|
68
|
+
symbolOffset = [0, 0];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const symbol = ShapeUtils.getShapeSymbol(shape);
|
|
72
|
+
const symbolSize = ShapeUtils.getShapeSize(size, width, height);
|
|
73
|
+
const rotate = ShapeUtils.getShapeRotation(shape);
|
|
74
|
+
|
|
75
|
+
// Special handling for labelup/down sizing - they contain text so they should be larger
|
|
76
|
+
let finalSize: number | number[] = symbolSize;
|
|
77
|
+
if (shape.includes('label')) {
|
|
78
|
+
// If custom size, scale it up for labels
|
|
79
|
+
if (Array.isArray(symbolSize)) {
|
|
80
|
+
finalSize = [symbolSize[0] * 2.5, symbolSize[1] * 2.5];
|
|
81
|
+
} else {
|
|
82
|
+
finalSize = symbolSize * 2.5;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Get label configuration based on location
|
|
87
|
+
const labelConfig = ShapeUtils.getLabelConfig(shape, location);
|
|
88
|
+
|
|
89
|
+
const item: any = {
|
|
90
|
+
value: [i, yValue],
|
|
91
|
+
symbol: symbol,
|
|
92
|
+
symbolSize: finalSize,
|
|
93
|
+
symbolRotate: rotate,
|
|
94
|
+
symbolOffset: symbolOffset,
|
|
95
|
+
itemStyle: {
|
|
96
|
+
color: color,
|
|
97
|
+
},
|
|
98
|
+
label: {
|
|
99
|
+
show: !!text,
|
|
100
|
+
position: labelConfig.position,
|
|
101
|
+
distance: labelConfig.distance,
|
|
102
|
+
formatter: text,
|
|
103
|
+
color: textColor,
|
|
104
|
+
fontSize: 10,
|
|
105
|
+
fontWeight: 'bold',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return item;
|
|
110
|
+
})
|
|
111
|
+
.filter((item) => item !== null);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
name: seriesName,
|
|
115
|
+
type: 'scatter',
|
|
116
|
+
xAxisIndex: xAxisIndex,
|
|
117
|
+
yAxisIndex: yAxisIndex,
|
|
118
|
+
data: shapeData,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +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
|
+
}
|