@qfo/qfchart 0.6.8 → 0.7.1
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 +3 -0
- package/dist/qfchart.min.browser.js +15 -15
- package/dist/qfchart.min.es.js +15 -15
- package/package.json +2 -2
- package/src/QFChart.ts +52 -0
- package/src/components/LayoutManager.ts +682 -679
- package/src/components/SeriesBuilder.ts +12 -3
- package/src/components/SeriesRendererFactory.ts +4 -0
- package/src/components/TableOverlayRenderer.ts +322 -0
- package/src/components/renderers/BoxRenderer.ts +258 -0
- package/src/components/renderers/DrawingLineRenderer.ts +58 -52
- package/src/components/renderers/LabelRenderer.ts +9 -8
- package/src/components/renderers/LinefillRenderer.ts +60 -72
- package/src/components/renderers/PolylineRenderer.ts +197 -0
- package/src/components/renderers/ShapeRenderer.ts +121 -121
- package/src/utils/ShapeUtils.ts +22 -14
|
@@ -1,121 +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
|
-
}
|
|
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' && 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' || location === 'AboveBar' || location === 'ab') {
|
|
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' || location === 'BelowBar' || location === 'bl') {
|
|
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' || 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' || 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
|
+
}
|
package/src/utils/ShapeUtils.ts
CHANGED
|
@@ -8,56 +8,59 @@ export class ShapeUtils {
|
|
|
8
8
|
|
|
9
9
|
switch (shape) {
|
|
10
10
|
case 'arrowdown':
|
|
11
|
-
|
|
11
|
+
case 'shape_arrow_down':
|
|
12
12
|
return 'path://M12 24l-12-12h8v-12h8v12h8z';
|
|
13
13
|
|
|
14
14
|
case 'arrowup':
|
|
15
|
-
|
|
15
|
+
case 'shape_arrow_up':
|
|
16
16
|
return 'path://M12 0l12 12h-8v12h-8v-12h-8z';
|
|
17
17
|
|
|
18
18
|
case 'circle':
|
|
19
|
+
case 'shape_circle':
|
|
19
20
|
return 'circle';
|
|
20
21
|
|
|
21
22
|
case 'cross':
|
|
22
|
-
|
|
23
|
+
case 'shape_cross':
|
|
23
24
|
return 'path://M11 2h2v9h9v2h-9v9h-2v-9h-9v-2h9z';
|
|
24
25
|
|
|
25
26
|
case 'diamond':
|
|
26
|
-
|
|
27
|
+
case 'shape_diamond':
|
|
28
|
+
return 'diamond';
|
|
27
29
|
|
|
28
30
|
case 'flag':
|
|
29
|
-
|
|
31
|
+
case 'shape_flag':
|
|
30
32
|
return 'path://M6 2v20h2v-8h12l-2-6 2-6h-12z';
|
|
31
33
|
|
|
32
34
|
case 'labeldown':
|
|
33
|
-
|
|
35
|
+
case 'shape_label_down':
|
|
34
36
|
return 'path://M2 1h20a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-8l-2 3-2-3h-8a1 1 0 0 1-1-1v-14a1 1 0 0 1 1-1z';
|
|
35
37
|
|
|
36
38
|
case 'labelleft':
|
|
37
|
-
|
|
39
|
+
case 'shape_label_left':
|
|
38
40
|
return 'path://M0 10l3-3v-5a1 1 0 0 1 1-1h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-18a1 1 0 0 1-1-1v-5z';
|
|
39
41
|
|
|
40
42
|
case 'labelright':
|
|
41
|
-
|
|
43
|
+
case 'shape_label_right':
|
|
42
44
|
return 'path://M24 10l-3-3v-5a1 1 0 0 0-1-1h-18a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1v-5z';
|
|
43
45
|
|
|
44
46
|
case 'labelup':
|
|
45
|
-
|
|
47
|
+
case 'shape_label_up':
|
|
46
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';
|
|
47
49
|
|
|
48
50
|
case 'square':
|
|
51
|
+
case 'shape_square':
|
|
49
52
|
return 'rect';
|
|
50
53
|
|
|
51
54
|
case 'triangledown':
|
|
52
|
-
|
|
55
|
+
case 'shape_triangle_down':
|
|
53
56
|
return 'path://M12 21l-10-18h20z';
|
|
54
57
|
|
|
55
58
|
case 'triangleup':
|
|
56
|
-
|
|
57
|
-
return 'triangle';
|
|
59
|
+
case 'shape_triangle_up':
|
|
60
|
+
return 'triangle';
|
|
58
61
|
|
|
59
62
|
case 'xcross':
|
|
60
|
-
|
|
63
|
+
case 'shape_xcross':
|
|
61
64
|
return 'path://M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z';
|
|
62
65
|
|
|
63
66
|
default:
|
|
@@ -120,25 +123,30 @@ export class ShapeUtils {
|
|
|
120
123
|
|
|
121
124
|
switch (location) {
|
|
122
125
|
case 'abovebar':
|
|
126
|
+
case 'AboveBar':
|
|
123
127
|
// Shape is above the candle, text should be above the shape
|
|
124
128
|
return { position: 'top', distance: 5 };
|
|
125
129
|
|
|
126
130
|
case 'belowbar':
|
|
131
|
+
case 'BelowBar':
|
|
127
132
|
// Shape is below the candle, text should be below the shape
|
|
128
133
|
return { position: 'bottom', distance: 5 };
|
|
129
134
|
|
|
130
135
|
case 'top':
|
|
136
|
+
case 'Top':
|
|
131
137
|
// Shape at top of chart, text below it
|
|
132
138
|
return { position: 'bottom', distance: 5 };
|
|
133
139
|
|
|
134
140
|
case 'bottom':
|
|
141
|
+
case 'Bottom':
|
|
135
142
|
// Shape at bottom of chart, text above it
|
|
136
143
|
return { position: 'top', distance: 5 };
|
|
137
144
|
|
|
138
145
|
case 'absolute':
|
|
146
|
+
case 'Absolute':
|
|
139
147
|
default:
|
|
140
148
|
// For labelup/down, text is INSIDE the shape
|
|
141
|
-
if (shape === 'labelup' || shape === 'labeldown') {
|
|
149
|
+
if (shape === 'labelup' || shape === 'labeldown' || shape === 'shape_label_up' || shape === 'shape_label_down') {
|
|
142
150
|
return { position: 'inside', distance: 0 };
|
|
143
151
|
}
|
|
144
152
|
// For other shapes, text above by default
|