@qfo/qfchart 0.5.0
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/LICENSE +222 -0
- package/README.md +271 -0
- package/dist/index.d.ts +465 -0
- package/dist/qfchart.min.browser.js +109 -0
- package/package.json +71 -0
- package/src/QFChart.ts +1198 -0
- package/src/Utils.ts +31 -0
- package/src/components/AbstractPlugin.ts +104 -0
- package/src/components/DrawingEditor.ts +248 -0
- package/src/components/GraphicBuilder.ts +263 -0
- package/src/components/Indicator.ts +104 -0
- package/src/components/LayoutManager.ts +459 -0
- package/src/components/PluginManager.ts +234 -0
- package/src/components/SeriesBuilder.ts +192 -0
- package/src/components/TooltipFormatter.ts +97 -0
- package/src/index.ts +6 -0
- package/src/plugins/FibonacciTool.ts +192 -0
- package/src/plugins/LineTool.ts +190 -0
- package/src/plugins/MeasureTool.ts +344 -0
- package/src/types.ts +160 -0
- package/src/utils/EventBus.ts +67 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { ChartContext, Plugin } from "../types";
|
|
2
|
+
// We need to import AbstractPlugin if we check instanceof, or just treat all as Plugin interface
|
|
3
|
+
|
|
4
|
+
export class PluginManager {
|
|
5
|
+
private plugins: Map<string, Plugin> = new Map();
|
|
6
|
+
private activePluginId: string | null = null;
|
|
7
|
+
private context: ChartContext;
|
|
8
|
+
private toolbarContainer: HTMLElement;
|
|
9
|
+
private tooltipElement: HTMLElement | null = null;
|
|
10
|
+
private hideTimeout: any = null;
|
|
11
|
+
|
|
12
|
+
constructor(context: ChartContext, toolbarContainer: HTMLElement) {
|
|
13
|
+
this.context = context;
|
|
14
|
+
this.toolbarContainer = toolbarContainer;
|
|
15
|
+
this.createTooltip();
|
|
16
|
+
this.renderToolbar();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private createTooltip() {
|
|
20
|
+
this.tooltipElement = document.createElement("div");
|
|
21
|
+
Object.assign(this.tooltipElement.style, {
|
|
22
|
+
position: "fixed",
|
|
23
|
+
display: "none",
|
|
24
|
+
backgroundColor: "#1e293b",
|
|
25
|
+
color: "#e2e8f0",
|
|
26
|
+
padding: "6px 10px",
|
|
27
|
+
borderRadius: "6px",
|
|
28
|
+
fontSize: "13px",
|
|
29
|
+
lineHeight: "1.4",
|
|
30
|
+
fontWeight: "500",
|
|
31
|
+
border: "1px solid #334155",
|
|
32
|
+
zIndex: "9999",
|
|
33
|
+
pointerEvents: "none",
|
|
34
|
+
whiteSpace: "nowrap",
|
|
35
|
+
boxShadow:
|
|
36
|
+
"0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.15)",
|
|
37
|
+
fontFamily: this.context.getOptions().fontFamily || "sans-serif",
|
|
38
|
+
transition: "opacity 0.15s ease-in-out, transform 0.15s ease-in-out",
|
|
39
|
+
opacity: "0",
|
|
40
|
+
transform: "translateX(-5px)",
|
|
41
|
+
});
|
|
42
|
+
document.body.appendChild(this.tooltipElement);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public destroy() {
|
|
46
|
+
if (this.tooltipElement && this.tooltipElement.parentNode) {
|
|
47
|
+
this.tooltipElement.parentNode.removeChild(this.tooltipElement);
|
|
48
|
+
}
|
|
49
|
+
this.tooltipElement = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private showTooltip(target: HTMLElement, text: string) {
|
|
53
|
+
if (!this.tooltipElement) return;
|
|
54
|
+
|
|
55
|
+
// Clear any pending hide to prevent race conditions
|
|
56
|
+
if (this.hideTimeout) {
|
|
57
|
+
clearTimeout(this.hideTimeout);
|
|
58
|
+
this.hideTimeout = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const rect = target.getBoundingClientRect();
|
|
62
|
+
this.tooltipElement.textContent = text;
|
|
63
|
+
this.tooltipElement.style.display = "block";
|
|
64
|
+
|
|
65
|
+
// Position to the right of the button, centered vertically
|
|
66
|
+
const tooltipRect = this.tooltipElement.getBoundingClientRect();
|
|
67
|
+
const top = rect.top + (rect.height - tooltipRect.height) / 2;
|
|
68
|
+
const left = rect.right + 10; // 10px gap
|
|
69
|
+
|
|
70
|
+
this.tooltipElement.style.top = `${top}px`;
|
|
71
|
+
this.tooltipElement.style.left = `${left}px`;
|
|
72
|
+
|
|
73
|
+
// Trigger animation
|
|
74
|
+
requestAnimationFrame(() => {
|
|
75
|
+
if (this.tooltipElement) {
|
|
76
|
+
this.tooltipElement.style.opacity = "1";
|
|
77
|
+
this.tooltipElement.style.transform = "translateX(0)";
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private hideTooltip() {
|
|
83
|
+
if (!this.tooltipElement) return;
|
|
84
|
+
this.tooltipElement.style.opacity = "0";
|
|
85
|
+
this.tooltipElement.style.transform = "translateX(-5px)";
|
|
86
|
+
|
|
87
|
+
if (this.hideTimeout) {
|
|
88
|
+
clearTimeout(this.hideTimeout);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Wait for transition to finish before hiding
|
|
92
|
+
this.hideTimeout = setTimeout(() => {
|
|
93
|
+
if (this.tooltipElement) {
|
|
94
|
+
this.tooltipElement.style.display = "none";
|
|
95
|
+
}
|
|
96
|
+
this.hideTimeout = null;
|
|
97
|
+
}, 150);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public register(plugin: Plugin): void {
|
|
101
|
+
if (this.plugins.has(plugin.id)) {
|
|
102
|
+
console.warn(`Plugin with id ${plugin.id} is already registered.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.plugins.set(plugin.id, plugin);
|
|
106
|
+
plugin.init(this.context);
|
|
107
|
+
this.addButton(plugin);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public unregister(pluginId: string): void {
|
|
111
|
+
const plugin = this.plugins.get(pluginId);
|
|
112
|
+
if (plugin) {
|
|
113
|
+
if (this.activePluginId === pluginId) {
|
|
114
|
+
this.deactivatePlugin();
|
|
115
|
+
}
|
|
116
|
+
plugin.destroy?.();
|
|
117
|
+
this.plugins.delete(pluginId);
|
|
118
|
+
this.removeButton(pluginId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public activatePlugin(pluginId: string): void {
|
|
123
|
+
// If same plugin is clicked, deactivate it (toggle)
|
|
124
|
+
if (this.activePluginId === pluginId) {
|
|
125
|
+
this.deactivatePlugin();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Deactivate current active plugin
|
|
130
|
+
if (this.activePluginId) {
|
|
131
|
+
this.deactivatePlugin();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const plugin = this.plugins.get(pluginId);
|
|
135
|
+
if (plugin) {
|
|
136
|
+
this.activePluginId = pluginId;
|
|
137
|
+
this.setButtonActive(pluginId, true);
|
|
138
|
+
plugin.activate?.();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public deactivatePlugin(): void {
|
|
143
|
+
if (this.activePluginId) {
|
|
144
|
+
const plugin = this.plugins.get(this.activePluginId);
|
|
145
|
+
plugin?.deactivate?.();
|
|
146
|
+
this.setButtonActive(this.activePluginId, false);
|
|
147
|
+
this.activePluginId = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- UI Handling ---
|
|
152
|
+
|
|
153
|
+
private renderToolbar(): void {
|
|
154
|
+
this.toolbarContainer.innerHTML = "";
|
|
155
|
+
this.toolbarContainer.style.display = "flex";
|
|
156
|
+
this.toolbarContainer.style.flexDirection = "column";
|
|
157
|
+
this.toolbarContainer.style.width = "40px";
|
|
158
|
+
this.toolbarContainer.style.backgroundColor =
|
|
159
|
+
this.context.getOptions().backgroundColor || "#1e293b";
|
|
160
|
+
this.toolbarContainer.style.borderRight = "1px solid #334155";
|
|
161
|
+
this.toolbarContainer.style.padding = "5px";
|
|
162
|
+
this.toolbarContainer.style.boxSizing = "border-box";
|
|
163
|
+
this.toolbarContainer.style.gap = "5px";
|
|
164
|
+
this.toolbarContainer.style.flexShrink = "0";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private addButton(plugin: Plugin): void {
|
|
168
|
+
const btn = document.createElement("button");
|
|
169
|
+
btn.id = `qfchart-plugin-btn-${plugin.id}`;
|
|
170
|
+
// Removed native title to use custom tooltip
|
|
171
|
+
// btn.title = plugin.name || plugin.id;
|
|
172
|
+
btn.style.width = "30px";
|
|
173
|
+
btn.style.height = "30px";
|
|
174
|
+
btn.style.padding = "4px";
|
|
175
|
+
btn.style.border = "1px solid transparent";
|
|
176
|
+
btn.style.borderRadius = "4px";
|
|
177
|
+
btn.style.backgroundColor = "transparent";
|
|
178
|
+
btn.style.cursor = "pointer";
|
|
179
|
+
btn.style.color = this.context.getOptions().fontColor || "#cbd5e1";
|
|
180
|
+
btn.style.display = "flex";
|
|
181
|
+
btn.style.alignItems = "center";
|
|
182
|
+
btn.style.justifyContent = "center";
|
|
183
|
+
|
|
184
|
+
// Icon
|
|
185
|
+
if (plugin.icon) {
|
|
186
|
+
btn.innerHTML = plugin.icon;
|
|
187
|
+
} else {
|
|
188
|
+
btn.innerText = (plugin.name || plugin.id).substring(0, 2).toUpperCase();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Hover effects and Tooltip
|
|
192
|
+
btn.addEventListener("mouseenter", () => {
|
|
193
|
+
if (this.activePluginId !== plugin.id) {
|
|
194
|
+
btn.style.backgroundColor = "rgba(255, 255, 255, 0.1)";
|
|
195
|
+
}
|
|
196
|
+
this.showTooltip(btn, plugin.name || plugin.id);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
btn.addEventListener("mouseleave", () => {
|
|
200
|
+
if (this.activePluginId !== plugin.id) {
|
|
201
|
+
btn.style.backgroundColor = "transparent";
|
|
202
|
+
}
|
|
203
|
+
this.hideTooltip();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
btn.onclick = () => this.activatePlugin(plugin.id);
|
|
207
|
+
|
|
208
|
+
this.toolbarContainer.appendChild(btn);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private removeButton(pluginId: string): void {
|
|
212
|
+
const btn = this.toolbarContainer.querySelector(
|
|
213
|
+
`#qfchart-plugin-btn-${pluginId}`
|
|
214
|
+
);
|
|
215
|
+
if (btn) {
|
|
216
|
+
btn.remove();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private setButtonActive(pluginId: string, active: boolean): void {
|
|
221
|
+
const btn = this.toolbarContainer.querySelector(
|
|
222
|
+
`#qfchart-plugin-btn-${pluginId}`
|
|
223
|
+
) as HTMLElement;
|
|
224
|
+
if (btn) {
|
|
225
|
+
if (active) {
|
|
226
|
+
btn.style.backgroundColor = "#2563eb"; // Blue highlight
|
|
227
|
+
btn.style.color = "#ffffff";
|
|
228
|
+
} else {
|
|
229
|
+
btn.style.backgroundColor = "transparent";
|
|
230
|
+
btn.style.color = this.context.getOptions().fontColor || "#cbd5e1";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { OHLCV, Indicator as IndicatorType, QFChartOptions, IndicatorPlot } from '../types';
|
|
2
|
+
import { PaneConfiguration } from './LayoutManager';
|
|
3
|
+
import { textToBase64Image } from '../Utils';
|
|
4
|
+
|
|
5
|
+
export class SeriesBuilder {
|
|
6
|
+
public static buildCandlestickSeries(marketData: OHLCV[], options: QFChartOptions, totalLength?: number): any {
|
|
7
|
+
const upColor = options.upColor || '#00da3c';
|
|
8
|
+
const downColor = options.downColor || '#ec0000';
|
|
9
|
+
|
|
10
|
+
const data = marketData.map((d) => [d.open, d.close, d.low, d.high]);
|
|
11
|
+
|
|
12
|
+
// Pad with nulls if totalLength is provided and greater than current data length
|
|
13
|
+
if (totalLength && totalLength > data.length) {
|
|
14
|
+
const padding = totalLength - data.length;
|
|
15
|
+
for (let i = 0; i < padding; i++) {
|
|
16
|
+
data.push(null as any);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
type: 'candlestick',
|
|
22
|
+
name: options.title || 'Market',
|
|
23
|
+
data: data,
|
|
24
|
+
itemStyle: {
|
|
25
|
+
color: upColor,
|
|
26
|
+
color0: downColor,
|
|
27
|
+
borderColor: upColor,
|
|
28
|
+
borderColor0: downColor,
|
|
29
|
+
},
|
|
30
|
+
xAxisIndex: 0,
|
|
31
|
+
yAxisIndex: 0,
|
|
32
|
+
z: 5,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public static buildIndicatorSeries(
|
|
37
|
+
indicators: Map<string, IndicatorType>,
|
|
38
|
+
timeToIndex: Map<number, number>,
|
|
39
|
+
paneLayout: PaneConfiguration[],
|
|
40
|
+
totalDataLength: number,
|
|
41
|
+
dataIndexOffset: number = 0
|
|
42
|
+
): any[] {
|
|
43
|
+
const series: any[] = [];
|
|
44
|
+
|
|
45
|
+
indicators.forEach((indicator, id) => {
|
|
46
|
+
if (indicator.collapsed) return; // Skip if collapsed
|
|
47
|
+
|
|
48
|
+
// Find axis index
|
|
49
|
+
let xAxisIndex = 0;
|
|
50
|
+
let yAxisIndex = 0;
|
|
51
|
+
|
|
52
|
+
if (indicator.paneIndex > 0) {
|
|
53
|
+
// paneLayout contains only separate panes.
|
|
54
|
+
// The index in xAxis/yAxis array is 1 + index_in_paneLayout
|
|
55
|
+
const confIndex = paneLayout.findIndex((p) => p.index === indicator.paneIndex);
|
|
56
|
+
if (confIndex !== -1) {
|
|
57
|
+
xAxisIndex = confIndex + 1;
|
|
58
|
+
yAxisIndex = confIndex + 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Object.keys(indicator.plots).forEach((plotName) => {
|
|
63
|
+
const plot = indicator.plots[plotName];
|
|
64
|
+
const seriesName = `${id}::${plotName}`;
|
|
65
|
+
|
|
66
|
+
const dataArray = new Array(totalDataLength).fill(null);
|
|
67
|
+
const colorArray = new Array(totalDataLength).fill(null);
|
|
68
|
+
|
|
69
|
+
plot.data.forEach((point) => {
|
|
70
|
+
const index = timeToIndex.get(point.time);
|
|
71
|
+
if (index !== undefined) {
|
|
72
|
+
const offsetIndex = index + dataIndexOffset;
|
|
73
|
+
dataArray[offsetIndex] = point.value;
|
|
74
|
+
colorArray[offsetIndex] = point.options?.color || plot.options.color;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
switch (plot.options.style) {
|
|
79
|
+
case 'histogram':
|
|
80
|
+
case 'columns':
|
|
81
|
+
series.push({
|
|
82
|
+
name: seriesName,
|
|
83
|
+
type: 'bar',
|
|
84
|
+
xAxisIndex: xAxisIndex,
|
|
85
|
+
yAxisIndex: yAxisIndex,
|
|
86
|
+
data: dataArray.map((val, i) => ({
|
|
87
|
+
value: val,
|
|
88
|
+
itemStyle: colorArray[i] ? { color: colorArray[i] } : undefined,
|
|
89
|
+
})),
|
|
90
|
+
itemStyle: { color: plot.options.color },
|
|
91
|
+
});
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case 'circles':
|
|
95
|
+
case 'cross':
|
|
96
|
+
// Scatter
|
|
97
|
+
const scatterData = dataArray
|
|
98
|
+
.map((val, i) => {
|
|
99
|
+
if (val === null) return null;
|
|
100
|
+
const pointColor = colorArray[i] || plot.options.color;
|
|
101
|
+
const item: any = {
|
|
102
|
+
value: [i, val],
|
|
103
|
+
itemStyle: { color: pointColor },
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (plot.options.style === 'cross') {
|
|
107
|
+
item.symbol = `image://${textToBase64Image('+', pointColor, '24px')}`;
|
|
108
|
+
item.symbolSize = 16;
|
|
109
|
+
} else {
|
|
110
|
+
item.symbol = 'circle';
|
|
111
|
+
item.symbolSize = 6;
|
|
112
|
+
}
|
|
113
|
+
return item;
|
|
114
|
+
})
|
|
115
|
+
.filter((item) => item !== null);
|
|
116
|
+
|
|
117
|
+
series.push({
|
|
118
|
+
name: seriesName,
|
|
119
|
+
type: 'scatter',
|
|
120
|
+
xAxisIndex: xAxisIndex,
|
|
121
|
+
yAxisIndex: yAxisIndex,
|
|
122
|
+
data: scatterData,
|
|
123
|
+
});
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
case 'background':
|
|
127
|
+
series.push({
|
|
128
|
+
name: seriesName,
|
|
129
|
+
type: 'custom',
|
|
130
|
+
xAxisIndex: xAxisIndex,
|
|
131
|
+
yAxisIndex: yAxisIndex,
|
|
132
|
+
z: -10,
|
|
133
|
+
renderItem: (params: any, api: any) => {
|
|
134
|
+
const xVal = api.value(0);
|
|
135
|
+
if (isNaN(xVal)) return;
|
|
136
|
+
|
|
137
|
+
const start = api.coord([xVal, 0]);
|
|
138
|
+
const size = api.size([1, 0]);
|
|
139
|
+
const width = size[0];
|
|
140
|
+
const sys = params.coordSys;
|
|
141
|
+
const x = start[0] - width / 2;
|
|
142
|
+
const barColor = colorArray[params.dataIndex];
|
|
143
|
+
const val = api.value(1);
|
|
144
|
+
|
|
145
|
+
if (!barColor || !val) return;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
type: 'rect',
|
|
149
|
+
shape: {
|
|
150
|
+
x: x,
|
|
151
|
+
y: sys.y,
|
|
152
|
+
width: width,
|
|
153
|
+
height: sys.height,
|
|
154
|
+
},
|
|
155
|
+
style: {
|
|
156
|
+
fill: barColor,
|
|
157
|
+
opacity: 0.3,
|
|
158
|
+
},
|
|
159
|
+
silent: true,
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
data: dataArray.map((val, i) => [i, val]),
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case 'line':
|
|
167
|
+
default:
|
|
168
|
+
series.push({
|
|
169
|
+
name: seriesName,
|
|
170
|
+
type: 'line',
|
|
171
|
+
xAxisIndex: xAxisIndex,
|
|
172
|
+
yAxisIndex: yAxisIndex,
|
|
173
|
+
smooth: true,
|
|
174
|
+
showSymbol: false,
|
|
175
|
+
data: dataArray.map((val, i) => ({
|
|
176
|
+
value: val,
|
|
177
|
+
itemStyle: colorArray[i] ? { color: colorArray[i] } : undefined,
|
|
178
|
+
})),
|
|
179
|
+
itemStyle: { color: plot.options.color },
|
|
180
|
+
lineStyle: {
|
|
181
|
+
width: plot.options.linewidth || 1,
|
|
182
|
+
color: plot.options.color,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return series;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { QFChartOptions } from "../types";
|
|
2
|
+
|
|
3
|
+
export class TooltipFormatter {
|
|
4
|
+
public static format(params: any[], options: QFChartOptions): string {
|
|
5
|
+
if (!params || params.length === 0) return "";
|
|
6
|
+
|
|
7
|
+
const marketName = options.title || "Market";
|
|
8
|
+
const upColor = options.upColor || "#00da3c";
|
|
9
|
+
const downColor = options.downColor || "#ec0000";
|
|
10
|
+
const fontFamily = options.fontFamily || "sans-serif";
|
|
11
|
+
|
|
12
|
+
// 1. Header: Date/Time (from the first param)
|
|
13
|
+
const date = params[0].axisValue;
|
|
14
|
+
let html = `<div style="font-weight: bold; margin-bottom: 5px; color: #cbd5e1; font-family: ${fontFamily};">${date}</div>`;
|
|
15
|
+
|
|
16
|
+
// 2. Separate Market Data (Candlestick) from Indicators
|
|
17
|
+
const marketSeries = params.find(
|
|
18
|
+
(p: any) => p.seriesType === "candlestick"
|
|
19
|
+
);
|
|
20
|
+
const indicatorParams = params.filter(
|
|
21
|
+
(p: any) => p.seriesType !== "candlestick"
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// 3. Market Data Section
|
|
25
|
+
if (marketSeries) {
|
|
26
|
+
const [_, open, close, low, high] = marketSeries.value;
|
|
27
|
+
const color = close >= open ? upColor : downColor;
|
|
28
|
+
|
|
29
|
+
html += `
|
|
30
|
+
<div style="margin-bottom: 8px; font-family: ${fontFamily};">
|
|
31
|
+
<div style="display:flex; justify-content:space-between; color:${color}; font-weight:bold;">
|
|
32
|
+
<span>${marketName}</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div style="display: grid; grid-template-columns: auto auto; gap: 2px 15px; font-size: 0.9em; color: #cbd5e1;">
|
|
35
|
+
<span>Open:</span> <span style="text-align: right; color: ${
|
|
36
|
+
close >= open ? upColor : downColor
|
|
37
|
+
}">${open}</span>
|
|
38
|
+
<span>High:</span> <span style="text-align: right; color: ${upColor}">${high}</span>
|
|
39
|
+
<span>Low:</span> <span style="text-align: right; color: ${downColor}">${low}</span>
|
|
40
|
+
<span>Close:</span> <span style="text-align: right; color: ${
|
|
41
|
+
close >= open ? upColor : downColor
|
|
42
|
+
}">${close}</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 4. Indicators Section
|
|
49
|
+
if (indicatorParams.length > 0) {
|
|
50
|
+
html += `<div style="border-top: 1px solid #334155; margin: 5px 0; padding-top: 5px;"></div>`;
|
|
51
|
+
|
|
52
|
+
// Group by Indicator ID (extracted from seriesName "ID::PlotName")
|
|
53
|
+
const indicators: { [key: string]: any[] } = {};
|
|
54
|
+
|
|
55
|
+
indicatorParams.forEach((p: any) => {
|
|
56
|
+
const parts = p.seriesName.split("::");
|
|
57
|
+
const indId = parts.length > 1 ? parts[0] : "Unknown";
|
|
58
|
+
const plotName = parts.length > 1 ? parts[1] : p.seriesName;
|
|
59
|
+
|
|
60
|
+
if (!indicators[indId]) indicators[indId] = [];
|
|
61
|
+
indicators[indId].push({ ...p, displayName: plotName });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Render groups
|
|
65
|
+
Object.keys(indicators).forEach((indId) => {
|
|
66
|
+
html += `
|
|
67
|
+
<div style="margin-top: 8px; font-family: ${fontFamily};">
|
|
68
|
+
<div style="font-weight:bold; color: #fff; margin-bottom: 2px;">${indId}</div>
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
indicators[indId].forEach((p) => {
|
|
72
|
+
let val = p.value;
|
|
73
|
+
if (Array.isArray(val)) {
|
|
74
|
+
val = val[1]; // Assuming [index, value]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (val === null || val === undefined) return;
|
|
78
|
+
|
|
79
|
+
const valStr =
|
|
80
|
+
typeof val === "number"
|
|
81
|
+
? val.toLocaleString(undefined, { maximumFractionDigits: 4 })
|
|
82
|
+
: val;
|
|
83
|
+
|
|
84
|
+
html += `
|
|
85
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px; padding-left: 8px;">
|
|
86
|
+
<div>${p.marker} <span style="color: #cbd5e1;">${p.displayName}</span></div>
|
|
87
|
+
<div style="font-size: 10px; color: #fff;padding-left:10px;">${valStr}</div>
|
|
88
|
+
</div>`;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
html += `</div>`;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return html;
|
|
96
|
+
}
|
|
97
|
+
}
|