@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
package/package.json
CHANGED
|
@@ -184,6 +184,50 @@ export class GraphicBuilder {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// Pane Separator Lines (between main chart and indicator panes, and between indicators)
|
|
188
|
+
// Offset upward from center so the line doesn't overlap the lower pane's y-axis labels
|
|
189
|
+
if (!maximizedPaneId && layout.paneBoundaries.length > 0) {
|
|
190
|
+
const sepOffset = -8 * pixelToPercent; // shift 8px up from gap center
|
|
191
|
+
for (const boundary of layout.paneBoundaries) {
|
|
192
|
+
graphic.push({
|
|
193
|
+
type: 'group',
|
|
194
|
+
left: '10%',
|
|
195
|
+
top: (boundary.yPercent + sepOffset) + '%',
|
|
196
|
+
children: [
|
|
197
|
+
// Invisible wide hit target for easier hover/drag
|
|
198
|
+
{
|
|
199
|
+
type: 'rect',
|
|
200
|
+
shape: { width: 5000, height: 12, y: -6 },
|
|
201
|
+
style: { fill: 'transparent' },
|
|
202
|
+
cursor: 'row-resize',
|
|
203
|
+
},
|
|
204
|
+
// Visible line — moderately visible default, bright on hover
|
|
205
|
+
{
|
|
206
|
+
type: 'rect',
|
|
207
|
+
shape: { width: 5000, height: 2, y: -1 },
|
|
208
|
+
style: { fill: '#475569', opacity: 0.7 },
|
|
209
|
+
cursor: 'row-resize',
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
z: 50,
|
|
213
|
+
onmouseover: function () {
|
|
214
|
+
const line = this.children()[1];
|
|
215
|
+
if (line) {
|
|
216
|
+
line.setStyle({ fill: '#94a3b8', opacity: 1.0 });
|
|
217
|
+
line.setShape({ height: 3, y: -1.5 });
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
onmouseout: function () {
|
|
221
|
+
const line = this.children()[1];
|
|
222
|
+
if (line) {
|
|
223
|
+
line.setStyle({ fill: '#475569', opacity: 0.7 });
|
|
224
|
+
line.setShape({ height: 2, y: -1 });
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
187
231
|
// Indicator Panes
|
|
188
232
|
layout.paneLayout.forEach((pane) => {
|
|
189
233
|
// If maximizedPaneId is set, and this is NOT the maximized pane, skip rendering its controls
|
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
import { Indicator as IndicatorInterface, IndicatorPlot, IndicatorPoint } from '../types';
|
|
2
|
-
|
|
3
|
-
export class Indicator implements IndicatorInterface {
|
|
4
|
-
public id: string;
|
|
5
|
-
public plots: { [name: string]: IndicatorPlot };
|
|
6
|
-
public paneIndex: number;
|
|
7
|
-
public height?: number;
|
|
8
|
-
public collapsed: boolean;
|
|
9
|
-
public titleColor?: string;
|
|
10
|
-
public controls?: { collapse?: boolean; maximize?: boolean };
|
|
11
|
-
|
|
12
|
-
constructor(
|
|
13
|
-
id: string,
|
|
14
|
-
plots: { [name: string]: IndicatorPlot },
|
|
15
|
-
paneIndex: number,
|
|
16
|
-
options: {
|
|
17
|
-
height?: number;
|
|
18
|
-
collapsed?: boolean;
|
|
19
|
-
titleColor?: string;
|
|
20
|
-
controls?: { collapse?: boolean; maximize?: boolean };
|
|
21
|
-
} = {}
|
|
22
|
-
) {
|
|
23
|
-
this.id = id;
|
|
24
|
-
this.plots = plots;
|
|
25
|
-
this.paneIndex = paneIndex;
|
|
26
|
-
this.height = options.height;
|
|
27
|
-
this.collapsed = options.collapsed || false;
|
|
28
|
-
this.titleColor = options.titleColor;
|
|
29
|
-
this.controls = options.controls;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
public toggleCollapse(): void {
|
|
33
|
-
this.collapsed = !this.collapsed;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
public isVisible(): boolean {
|
|
37
|
-
return !this.collapsed;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Update indicator data incrementally by merging new points
|
|
42
|
-
*
|
|
43
|
-
* @param plots - New plots data to merge (same structure as constructor)
|
|
44
|
-
*
|
|
45
|
-
* @remarks
|
|
46
|
-
* This method merges new indicator data with existing data by timestamp.
|
|
47
|
-
* - New timestamps are added
|
|
48
|
-
* - Existing timestamps are updated with new values
|
|
49
|
-
* - All data is automatically sorted by time after merge
|
|
50
|
-
*
|
|
51
|
-
* **Important**: This method only updates the indicator's internal data structure.
|
|
52
|
-
* To see the changes reflected in the chart, you MUST call `chart.updateData()`
|
|
53
|
-
* after updating indicator data.
|
|
54
|
-
*
|
|
55
|
-
* **Usage Pattern**:
|
|
56
|
-
* ```typescript
|
|
57
|
-
* // 1. Update indicator data first
|
|
58
|
-
* indicator.updateData({
|
|
59
|
-
* macd: { data: [{ time: 1234567890, value: 150 }], options: { style: 'line', color: '#2962FF' } }
|
|
60
|
-
* });
|
|
61
|
-
*
|
|
62
|
-
* // 2. Then update chart data to trigger re-render
|
|
63
|
-
* chart.updateData([
|
|
64
|
-
* { time: 1234567890, open: 100, high: 105, low: 99, close: 103, volume: 1000 }
|
|
65
|
-
* ]);
|
|
66
|
-
* ```
|
|
67
|
-
*
|
|
68
|
-
* **Note**: If you update indicator data without corresponding market data changes,
|
|
69
|
-
* this typically indicates a recalculation scenario. In normal workflows, indicator
|
|
70
|
-
* values are derived from market data, so indicator updates should correspond to
|
|
71
|
-
* new or modified market bars.
|
|
72
|
-
*/
|
|
73
|
-
public updateData(plots: { [name: string]: IndicatorPlot }): void {
|
|
74
|
-
Object.keys(plots).forEach((plotName) => {
|
|
75
|
-
if (!this.plots[plotName]) {
|
|
76
|
-
// New plot - add it
|
|
77
|
-
this.plots[plotName] = plots[plotName];
|
|
78
|
-
} else {
|
|
79
|
-
// Existing plot - merge data points
|
|
80
|
-
const existingPlot = this.plots[plotName];
|
|
81
|
-
const newPlot = plots[plotName];
|
|
82
|
-
|
|
83
|
-
if (!existingPlot.data) return;
|
|
84
|
-
|
|
85
|
-
// Update options if provided
|
|
86
|
-
if (newPlot.options) {
|
|
87
|
-
existingPlot.options = { ...existingPlot.options, ...newPlot.options };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Merge data points by time
|
|
91
|
-
const existingTimeMap = new Map<number, IndicatorPoint>();
|
|
92
|
-
existingPlot.data?.forEach((point) => {
|
|
93
|
-
existingTimeMap.set(point.time, point);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Update or add new points
|
|
97
|
-
newPlot.data?.forEach((point) => {
|
|
98
|
-
existingTimeMap.set(point.time, point);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// Rebuild data array sorted by time
|
|
102
|
-
existingPlot.data = Array.from(existingTimeMap.values()).sort((a, b) => a.time - b.time);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
1
|
+
import { Indicator as IndicatorInterface, IndicatorPlot, IndicatorPoint } from '../types';
|
|
2
|
+
|
|
3
|
+
export class Indicator implements IndicatorInterface {
|
|
4
|
+
public id: string;
|
|
5
|
+
public plots: { [name: string]: IndicatorPlot };
|
|
6
|
+
public paneIndex: number;
|
|
7
|
+
public height?: number;
|
|
8
|
+
public collapsed: boolean;
|
|
9
|
+
public titleColor?: string;
|
|
10
|
+
public controls?: { collapse?: boolean; maximize?: boolean };
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
id: string,
|
|
14
|
+
plots: { [name: string]: IndicatorPlot },
|
|
15
|
+
paneIndex: number,
|
|
16
|
+
options: {
|
|
17
|
+
height?: number;
|
|
18
|
+
collapsed?: boolean;
|
|
19
|
+
titleColor?: string;
|
|
20
|
+
controls?: { collapse?: boolean; maximize?: boolean };
|
|
21
|
+
} = {}
|
|
22
|
+
) {
|
|
23
|
+
this.id = id;
|
|
24
|
+
this.plots = plots;
|
|
25
|
+
this.paneIndex = paneIndex;
|
|
26
|
+
this.height = options.height;
|
|
27
|
+
this.collapsed = options.collapsed || false;
|
|
28
|
+
this.titleColor = options.titleColor;
|
|
29
|
+
this.controls = options.controls;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public toggleCollapse(): void {
|
|
33
|
+
this.collapsed = !this.collapsed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public isVisible(): boolean {
|
|
37
|
+
return !this.collapsed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Update indicator data incrementally by merging new points
|
|
42
|
+
*
|
|
43
|
+
* @param plots - New plots data to merge (same structure as constructor)
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* This method merges new indicator data with existing data by timestamp.
|
|
47
|
+
* - New timestamps are added
|
|
48
|
+
* - Existing timestamps are updated with new values
|
|
49
|
+
* - All data is automatically sorted by time after merge
|
|
50
|
+
*
|
|
51
|
+
* **Important**: This method only updates the indicator's internal data structure.
|
|
52
|
+
* To see the changes reflected in the chart, you MUST call `chart.updateData()`
|
|
53
|
+
* after updating indicator data.
|
|
54
|
+
*
|
|
55
|
+
* **Usage Pattern**:
|
|
56
|
+
* ```typescript
|
|
57
|
+
* // 1. Update indicator data first
|
|
58
|
+
* indicator.updateData({
|
|
59
|
+
* macd: { data: [{ time: 1234567890, value: 150 }], options: { style: 'line', color: '#2962FF' } }
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* // 2. Then update chart data to trigger re-render
|
|
63
|
+
* chart.updateData([
|
|
64
|
+
* { time: 1234567890, open: 100, high: 105, low: 99, close: 103, volume: 1000 }
|
|
65
|
+
* ]);
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* **Note**: If you update indicator data without corresponding market data changes,
|
|
69
|
+
* this typically indicates a recalculation scenario. In normal workflows, indicator
|
|
70
|
+
* values are derived from market data, so indicator updates should correspond to
|
|
71
|
+
* new or modified market bars.
|
|
72
|
+
*/
|
|
73
|
+
public updateData(plots: { [name: string]: IndicatorPlot }): void {
|
|
74
|
+
Object.keys(plots).forEach((plotName) => {
|
|
75
|
+
if (!this.plots[plotName]) {
|
|
76
|
+
// New plot - add it
|
|
77
|
+
this.plots[plotName] = plots[plotName];
|
|
78
|
+
} else {
|
|
79
|
+
// Existing plot - merge data points
|
|
80
|
+
const existingPlot = this.plots[plotName];
|
|
81
|
+
const newPlot = plots[plotName];
|
|
82
|
+
|
|
83
|
+
if (!existingPlot.data) return;
|
|
84
|
+
|
|
85
|
+
// Update options if provided
|
|
86
|
+
if (newPlot.options) {
|
|
87
|
+
existingPlot.options = { ...existingPlot.options, ...newPlot.options };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Merge data points by time
|
|
91
|
+
const existingTimeMap = new Map<number, IndicatorPoint>();
|
|
92
|
+
existingPlot.data?.forEach((point) => {
|
|
93
|
+
existingTimeMap.set(point.time, point);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Update or add new points
|
|
97
|
+
newPlot.data?.forEach((point) => {
|
|
98
|
+
existingTimeMap.set(point.time, point);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Rebuild data array sorted by time
|
|
102
|
+
existingPlot.data = Array.from(existingTimeMap.values()).sort((a, b) => a.time - b.time);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -104,6 +104,9 @@ export class SeriesBuilder {
|
|
|
104
104
|
const series: any[] = [];
|
|
105
105
|
const barColors: (string | null)[] = new Array(totalDataLength).fill(null);
|
|
106
106
|
|
|
107
|
+
// Extract raw (non-null) market data for resolving xloc.bar_time coordinates
|
|
108
|
+
const rawMarketData = candlestickData?.filter((d): d is OHLCV => d != null && d.time !== undefined);
|
|
109
|
+
|
|
107
110
|
// Store plot data arrays for fill plots to reference
|
|
108
111
|
const plotDataArrays = new Map<string, number[]>();
|
|
109
112
|
|
|
@@ -348,6 +351,8 @@ export class SeriesBuilder {
|
|
|
348
351
|
indicatorId: id,
|
|
349
352
|
plotName: plotName,
|
|
350
353
|
dataIndexOffset,
|
|
354
|
+
timeToIndex,
|
|
355
|
+
marketData: rawMarketData,
|
|
351
356
|
});
|
|
352
357
|
|
|
353
358
|
if (seriesConfig) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
1
|
+
import { SeriesRenderer, RenderContext, resolveXCoord } from './SeriesRenderer';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Convert any color string to a format ECharts canvas can render with opacity.
|
|
@@ -60,7 +60,7 @@ function luminance(r: number, g: number, b: number): number {
|
|
|
60
60
|
*/
|
|
61
61
|
export class BoxRenderer implements SeriesRenderer {
|
|
62
62
|
render(context: RenderContext): any {
|
|
63
|
-
const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset } = context;
|
|
63
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset, timeToIndex, marketData } = context;
|
|
64
64
|
const offset = dataIndexOffset || 0;
|
|
65
65
|
|
|
66
66
|
// Collect all non-deleted box objects from the sparse dataArray.
|
|
@@ -103,9 +103,11 @@ export class BoxRenderer implements SeriesRenderer {
|
|
|
103
103
|
for (const bx of boxObjects) {
|
|
104
104
|
if (bx._deleted) continue;
|
|
105
105
|
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
106
|
+
const leftX = resolveXCoord(bx.left, bx.xloc, offset, timeToIndex, marketData);
|
|
107
|
+
const rightX = resolveXCoord(bx.right, bx.xloc, offset, timeToIndex, marketData);
|
|
108
|
+
if (isNaN(leftX) || isNaN(rightX)) continue;
|
|
109
|
+
const pTopLeft = api.coord([leftX, bx.top]);
|
|
110
|
+
const pBottomRight = api.coord([rightX, bx.bottom]);
|
|
109
111
|
|
|
110
112
|
let x = pTopLeft[0];
|
|
111
113
|
let y = pTopLeft[1];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
1
|
+
import { SeriesRenderer, RenderContext, resolveXCoord } from './SeriesRenderer';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Renderer for Pine Script line.* drawing objects.
|
|
@@ -9,7 +9,7 @@ import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
|
9
9
|
*/
|
|
10
10
|
export class DrawingLineRenderer 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
|
const defaultColor = '#2962ff';
|
|
15
15
|
|
|
@@ -55,10 +55,12 @@ export class DrawingLineRenderer implements SeriesRenderer {
|
|
|
55
55
|
|
|
56
56
|
for (const ln of lineObjects) {
|
|
57
57
|
if (ln._deleted) continue;
|
|
58
|
-
const
|
|
58
|
+
const x1Resolved = resolveXCoord(ln.x1, ln.xloc, offset, timeToIndex, marketData);
|
|
59
|
+
const x2Resolved = resolveXCoord(ln.x2, ln.xloc, offset, timeToIndex, marketData);
|
|
60
|
+
if (isNaN(x1Resolved) || isNaN(x2Resolved)) continue;
|
|
59
61
|
|
|
60
|
-
let p1 = api.coord([
|
|
61
|
-
let p2 = api.coord([
|
|
62
|
+
let p1 = api.coord([x1Resolved, ln.y1]);
|
|
63
|
+
let p2 = api.coord([x2Resolved, ln.y2]);
|
|
62
64
|
|
|
63
65
|
// Handle extend (none/n | left/l | right/r | both/b)
|
|
64
66
|
const extend = ln.extend || 'none';
|
|
@@ -201,8 +201,9 @@ export class FillRenderer implements SeriesRenderer {
|
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
203
|
* Render a gradient fill between two plots.
|
|
204
|
-
* Uses
|
|
205
|
-
*
|
|
204
|
+
* Uses per-bar top_value/bottom_value as the actual Y boundaries (not the raw plot values).
|
|
205
|
+
* A vertical linear gradient goes from top_color (at top_value) to bottom_color (at bottom_value).
|
|
206
|
+
* When top_value or bottom_value is na/NaN, the fill is hidden for that bar.
|
|
206
207
|
*/
|
|
207
208
|
private renderGradientFill(
|
|
208
209
|
seriesName: string,
|
|
@@ -214,40 +215,52 @@ export class FillRenderer implements SeriesRenderer {
|
|
|
214
215
|
optionsArray: any[],
|
|
215
216
|
plotOptions: any
|
|
216
217
|
): any {
|
|
217
|
-
// Build per-bar gradient
|
|
218
|
-
// Each entry
|
|
219
|
-
|
|
218
|
+
// Build per-bar gradient data from optionsArray
|
|
219
|
+
// Each entry has: { top_value, bottom_value, top_color, bottom_color }
|
|
220
|
+
interface GradientBar {
|
|
221
|
+
topValue: number | null;
|
|
222
|
+
bottomValue: number | null;
|
|
223
|
+
topColor: string;
|
|
224
|
+
topOpacity: number;
|
|
225
|
+
bottomColor: string;
|
|
226
|
+
bottomOpacity: number;
|
|
227
|
+
}
|
|
228
|
+
const gradientBars: (GradientBar | null)[] = [];
|
|
220
229
|
|
|
221
230
|
for (let i = 0; i < totalDataLength; i++) {
|
|
222
231
|
const opts = optionsArray?.[i];
|
|
223
232
|
if (opts && opts.top_color !== undefined) {
|
|
233
|
+
const tv = opts.top_value;
|
|
234
|
+
const bv = opts.bottom_value;
|
|
235
|
+
// na/NaN/null/undefined → null (hidden bar)
|
|
236
|
+
const topVal = (tv == null || (typeof tv === 'number' && isNaN(tv))) ? null : tv;
|
|
237
|
+
const btmVal = (bv == null || (typeof bv === 'number' && isNaN(bv))) ? null : bv;
|
|
238
|
+
|
|
224
239
|
const top = ColorUtils.parseColor(opts.top_color);
|
|
225
240
|
const bottom = ColorUtils.parseColor(opts.bottom_color);
|
|
226
|
-
|
|
241
|
+
gradientBars[i] = {
|
|
242
|
+
topValue: topVal,
|
|
243
|
+
bottomValue: btmVal,
|
|
227
244
|
topColor: top.color,
|
|
228
245
|
topOpacity: top.opacity,
|
|
229
246
|
bottomColor: bottom.color,
|
|
230
247
|
bottomOpacity: bottom.opacity,
|
|
231
248
|
};
|
|
232
249
|
} else {
|
|
233
|
-
|
|
234
|
-
gradientColors[i] = {
|
|
235
|
-
topColor: 'rgba(128,128,128,0.2)',
|
|
236
|
-
topOpacity: 0.2,
|
|
237
|
-
bottomColor: 'rgba(128,128,128,0.2)',
|
|
238
|
-
bottomOpacity: 0.2,
|
|
239
|
-
};
|
|
250
|
+
gradientBars[i] = null;
|
|
240
251
|
}
|
|
241
252
|
}
|
|
242
253
|
|
|
243
|
-
// Create fill data
|
|
244
|
-
const
|
|
254
|
+
// Create fill data using top_value/bottom_value as Y boundaries
|
|
255
|
+
const fillData: any[] = [];
|
|
245
256
|
for (let i = 0; i < totalDataLength; i++) {
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
257
|
+
const gb = gradientBars[i];
|
|
258
|
+
const prevGb = i > 0 ? gradientBars[i - 1] : null;
|
|
259
|
+
const topY = gb?.topValue ?? null;
|
|
260
|
+
const btmY = gb?.bottomValue ?? null;
|
|
261
|
+
const prevTopY = prevGb?.topValue ?? null;
|
|
262
|
+
const prevBtmY = prevGb?.bottomValue ?? null;
|
|
263
|
+
fillData.push([i, topY, btmY, prevTopY, prevBtmY]);
|
|
251
264
|
}
|
|
252
265
|
|
|
253
266
|
return {
|
|
@@ -263,57 +276,53 @@ export class FillRenderer implements SeriesRenderer {
|
|
|
263
276
|
const index = params.dataIndex;
|
|
264
277
|
if (index === 0) return null;
|
|
265
278
|
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
const
|
|
279
|
+
const topY = api.value(1);
|
|
280
|
+
const btmY = api.value(2);
|
|
281
|
+
const prevTopY = api.value(3);
|
|
282
|
+
const prevBtmY = api.value(4);
|
|
270
283
|
|
|
284
|
+
// Skip when any boundary is na (hidden bar)
|
|
271
285
|
if (
|
|
272
|
-
|
|
273
|
-
isNaN(
|
|
286
|
+
topY == null || btmY == null || prevTopY == null || prevBtmY == null ||
|
|
287
|
+
isNaN(topY) || isNaN(btmY) || isNaN(prevTopY) || isNaN(prevBtmY)
|
|
274
288
|
) {
|
|
275
289
|
return null;
|
|
276
290
|
}
|
|
277
291
|
|
|
278
|
-
const p1Prev = api.coord([index - 1, prevY1]);
|
|
279
|
-
const p1Curr = api.coord([index, y1]);
|
|
280
|
-
const p2Curr = api.coord([index, y2]);
|
|
281
|
-
const p2Prev = api.coord([index - 1, prevY2]);
|
|
282
|
-
|
|
283
292
|
// Get gradient colors for this bar
|
|
284
|
-
const
|
|
285
|
-
if (!
|
|
293
|
+
const gb = gradientBars[index];
|
|
294
|
+
if (!gb) return null;
|
|
286
295
|
|
|
287
296
|
// Skip fully transparent gradient fills
|
|
288
|
-
if (
|
|
297
|
+
if (gb.topOpacity < 0.01 && gb.bottomOpacity < 0.01) return null;
|
|
289
298
|
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
const bottomRgba = ColorUtils.toRgba(gc.bottomColor, gc.bottomOpacity);
|
|
299
|
+
const topRgba = ColorUtils.toRgba(gb.topColor, gb.topOpacity);
|
|
300
|
+
const bottomRgba = ColorUtils.toRgba(gb.bottomColor, gb.bottomOpacity);
|
|
293
301
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
302
|
+
const pTopPrev = api.coord([index - 1, prevTopY]);
|
|
303
|
+
const pTopCurr = api.coord([index, topY]);
|
|
304
|
+
const pBtmCurr = api.coord([index, btmY]);
|
|
305
|
+
const pBtmPrev = api.coord([index - 1, prevBtmY]);
|
|
297
306
|
|
|
298
307
|
return {
|
|
299
308
|
type: 'polygon',
|
|
300
309
|
shape: {
|
|
301
|
-
points: [
|
|
310
|
+
points: [pTopPrev, pTopCurr, pBtmCurr, pBtmPrev],
|
|
302
311
|
},
|
|
303
312
|
style: {
|
|
304
313
|
fill: {
|
|
305
314
|
type: 'linear',
|
|
306
|
-
x: 0, y: 0, x2: 0, y2: 1,
|
|
315
|
+
x: 0, y: 0, x2: 0, y2: 1,
|
|
307
316
|
colorStops: [
|
|
308
|
-
{ offset: 0, color:
|
|
309
|
-
{ offset: 1, color:
|
|
317
|
+
{ offset: 0, color: topRgba },
|
|
318
|
+
{ offset: 1, color: bottomRgba },
|
|
310
319
|
],
|
|
311
320
|
},
|
|
312
321
|
},
|
|
313
322
|
silent: true,
|
|
314
323
|
};
|
|
315
324
|
},
|
|
316
|
-
data:
|
|
325
|
+
data: fillData,
|
|
317
326
|
silent: true,
|
|
318
327
|
};
|
|
319
328
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
1
|
+
import { SeriesRenderer, RenderContext, resolveXCoord } from './SeriesRenderer';
|
|
2
2
|
import { ShapeUtils } from '../../utils/ShapeUtils';
|
|
3
3
|
|
|
4
4
|
export class LabelRenderer implements SeriesRenderer {
|
|
5
5
|
render(context: RenderContext): any {
|
|
6
|
-
const { seriesName, xAxisIndex, yAxisIndex, dataArray, candlestickData, dataIndexOffset } = context;
|
|
6
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, candlestickData, dataIndexOffset, timeToIndex, marketData } = context;
|
|
7
7
|
const offset = dataIndexOffset || 0;
|
|
8
8
|
|
|
9
9
|
// Collect all non-null, non-deleted label objects from the sparse dataArray.
|
|
@@ -42,7 +42,8 @@ export class LabelRenderer implements SeriesRenderer {
|
|
|
42
42
|
const shape = this.styleToShape(styleRaw);
|
|
43
43
|
|
|
44
44
|
// Determine X position using label's own x coordinate
|
|
45
|
-
const xPos = (lbl.
|
|
45
|
+
const xPos = resolveXCoord(lbl.x, lbl.xloc, offset, timeToIndex, marketData);
|
|
46
|
+
if (isNaN(xPos)) return null;
|
|
46
47
|
|
|
47
48
|
// Determine Y value based on yloc
|
|
48
49
|
let yValue = lbl.y;
|
|
@@ -126,6 +127,18 @@ export class LabelRenderer implements SeriesRenderer {
|
|
|
126
127
|
labelTextOffset = [0, totalHeight * pointerRatio * 0.5];
|
|
127
128
|
}
|
|
128
129
|
}
|
|
130
|
+
} else if (shape === 'labelcenter') {
|
|
131
|
+
// label_center: no pointer, centered at exact coordinate.
|
|
132
|
+
// Size the bubble body to fit text but apply NO offset.
|
|
133
|
+
const lines = text.split('\n');
|
|
134
|
+
const longestLine = lines.reduce((a: string, b: string) => a.length > b.length ? a : b, '');
|
|
135
|
+
const textWidth = longestLine.length * fontSize * 0.65;
|
|
136
|
+
const minWidth = fontSize * 2.5;
|
|
137
|
+
const bubbleWidth = Math.max(minWidth, textWidth + fontSize * 1.6);
|
|
138
|
+
const lineHeight = fontSize * 1.4;
|
|
139
|
+
const bubbleHeight = Math.max(fontSize * 2.8, lines.length * lineHeight + fontSize * 1.2);
|
|
140
|
+
finalSize = [bubbleWidth, bubbleHeight];
|
|
141
|
+
// No symbolOffset — center exactly at the coordinate
|
|
129
142
|
} else if (shape === 'none') {
|
|
130
143
|
finalSize = 0;
|
|
131
144
|
} else {
|
|
@@ -228,7 +241,7 @@ export class LabelRenderer implements SeriesRenderer {
|
|
|
228
241
|
case 'label_upper_right':
|
|
229
242
|
return 'labelup';
|
|
230
243
|
case 'label_center':
|
|
231
|
-
return '
|
|
244
|
+
return 'labelcenter';
|
|
232
245
|
case 'circle':
|
|
233
246
|
return 'circle';
|
|
234
247
|
case 'square':
|
|
@@ -289,16 +302,16 @@ export class LabelRenderer implements SeriesRenderer {
|
|
|
289
302
|
case 'tiny':
|
|
290
303
|
return 8;
|
|
291
304
|
case 'small':
|
|
292
|
-
return
|
|
305
|
+
return 11;
|
|
293
306
|
case 'normal':
|
|
294
307
|
case 'auto':
|
|
295
|
-
return
|
|
308
|
+
return 14;
|
|
296
309
|
case 'large':
|
|
297
|
-
return
|
|
310
|
+
return 20;
|
|
298
311
|
case 'huge':
|
|
299
|
-
return
|
|
312
|
+
return 36;
|
|
300
313
|
default:
|
|
301
|
-
return
|
|
314
|
+
return 14;
|
|
302
315
|
}
|
|
303
316
|
}
|
|
304
317
|
}
|