@qfo/qfchart 0.8.2 → 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/index.d.ts +10 -0
- package/dist/qfchart.min.browser.js +21 -19
- package/dist/qfchart.min.es.js +19 -17
- package/package.json +1 -1
- package/src/QFChart.ts +135 -1
- package/src/components/GraphicBuilder.ts +44 -0
- package/src/components/Indicator.ts +106 -106
- package/src/components/LayoutManager.ts +25 -11
- package/src/components/PluginManager.ts +229 -229
- package/src/components/SeriesBuilder.ts +26 -14
- 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 +28 -12
- 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/ScatterRenderer.ts +92 -54
- package/src/components/renderers/SeriesRenderer.ts +78 -0
- package/src/components/renderers/ShapeRenderer.ts +12 -0
- package/src/components/renderers/StepRenderer.ts +39 -39
- package/src/utils/ShapeUtils.ts +5 -0
|
@@ -1,229 +1,229 @@
|
|
|
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: '0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.15)',
|
|
36
|
-
fontFamily: this.context.getOptions().fontFamily || 'sans-serif',
|
|
37
|
-
transition: 'opacity 0.15s ease-in-out, transform 0.15s ease-in-out',
|
|
38
|
-
opacity: '0',
|
|
39
|
-
transform: 'translateX(-5px)',
|
|
40
|
-
});
|
|
41
|
-
document.body.appendChild(this.tooltipElement);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
public destroy() {
|
|
45
|
-
if (this.tooltipElement && this.tooltipElement.parentNode) {
|
|
46
|
-
this.tooltipElement.parentNode.removeChild(this.tooltipElement);
|
|
47
|
-
}
|
|
48
|
-
this.tooltipElement = null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private showTooltip(target: HTMLElement, text: string) {
|
|
52
|
-
if (!this.tooltipElement) return;
|
|
53
|
-
|
|
54
|
-
// Clear any pending hide to prevent race conditions
|
|
55
|
-
if (this.hideTimeout) {
|
|
56
|
-
clearTimeout(this.hideTimeout);
|
|
57
|
-
this.hideTimeout = null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const rect = target.getBoundingClientRect();
|
|
61
|
-
this.tooltipElement.textContent = text;
|
|
62
|
-
this.tooltipElement.style.display = 'block';
|
|
63
|
-
|
|
64
|
-
// Position to the right of the button, centered vertically
|
|
65
|
-
const tooltipRect = this.tooltipElement.getBoundingClientRect();
|
|
66
|
-
const top = rect.top + (rect.height - tooltipRect.height) / 2;
|
|
67
|
-
const left = rect.right + 10; // 10px gap
|
|
68
|
-
|
|
69
|
-
this.tooltipElement.style.top = `${top}px`;
|
|
70
|
-
this.tooltipElement.style.left = `${left}px`;
|
|
71
|
-
|
|
72
|
-
// Trigger animation
|
|
73
|
-
requestAnimationFrame(() => {
|
|
74
|
-
if (this.tooltipElement) {
|
|
75
|
-
this.tooltipElement.style.opacity = '1';
|
|
76
|
-
this.tooltipElement.style.transform = 'translateX(0)';
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private hideTooltip() {
|
|
82
|
-
if (!this.tooltipElement) return;
|
|
83
|
-
this.tooltipElement.style.opacity = '0';
|
|
84
|
-
this.tooltipElement.style.transform = 'translateX(-5px)';
|
|
85
|
-
|
|
86
|
-
if (this.hideTimeout) {
|
|
87
|
-
clearTimeout(this.hideTimeout);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Wait for transition to finish before hiding
|
|
91
|
-
this.hideTimeout = setTimeout(() => {
|
|
92
|
-
if (this.tooltipElement) {
|
|
93
|
-
this.tooltipElement.style.display = 'none';
|
|
94
|
-
}
|
|
95
|
-
this.hideTimeout = null;
|
|
96
|
-
}, 150);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
public register(plugin: Plugin): void {
|
|
100
|
-
if (this.plugins.has(plugin.id)) {
|
|
101
|
-
console.warn(`Plugin with id ${plugin.id} is already registered.`);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
this.plugins.set(plugin.id, plugin);
|
|
105
|
-
plugin.init(this.context);
|
|
106
|
-
this.addButton(plugin);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
public unregister(pluginId: string): void {
|
|
110
|
-
const plugin = this.plugins.get(pluginId);
|
|
111
|
-
if (plugin) {
|
|
112
|
-
if (this.activePluginId === pluginId) {
|
|
113
|
-
this.deactivatePlugin();
|
|
114
|
-
}
|
|
115
|
-
plugin.destroy?.();
|
|
116
|
-
this.plugins.delete(pluginId);
|
|
117
|
-
this.removeButton(pluginId);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
public activatePlugin(pluginId: string): void {
|
|
122
|
-
// If same plugin is clicked, deactivate it (toggle)
|
|
123
|
-
if (this.activePluginId === pluginId) {
|
|
124
|
-
this.deactivatePlugin();
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Deactivate current active plugin
|
|
129
|
-
if (this.activePluginId) {
|
|
130
|
-
this.deactivatePlugin();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const plugin = this.plugins.get(pluginId);
|
|
134
|
-
if (plugin) {
|
|
135
|
-
this.activePluginId = pluginId;
|
|
136
|
-
this.setButtonActive(pluginId, true);
|
|
137
|
-
plugin.activate?.();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
public deactivatePlugin(): void {
|
|
142
|
-
if (this.activePluginId) {
|
|
143
|
-
const plugin = this.plugins.get(this.activePluginId);
|
|
144
|
-
plugin?.deactivate?.();
|
|
145
|
-
this.setButtonActive(this.activePluginId, false);
|
|
146
|
-
this.activePluginId = null;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// --- UI Handling ---
|
|
151
|
-
|
|
152
|
-
private renderToolbar(): void {
|
|
153
|
-
this.toolbarContainer.innerHTML = '';
|
|
154
|
-
this.toolbarContainer.classList.add('qfchart-toolbar');
|
|
155
|
-
this.toolbarContainer.style.display = 'flex';
|
|
156
|
-
this.toolbarContainer.style.flexDirection = 'column';
|
|
157
|
-
this.toolbarContainer.style.width = '40px';
|
|
158
|
-
this.toolbarContainer.style.backgroundColor = this.context.getOptions().backgroundColor || '#1e293b';
|
|
159
|
-
this.toolbarContainer.style.borderRight = '1px solid #334155';
|
|
160
|
-
this.toolbarContainer.style.padding = '5px';
|
|
161
|
-
this.toolbarContainer.style.boxSizing = 'border-box';
|
|
162
|
-
this.toolbarContainer.style.gap = '5px';
|
|
163
|
-
this.toolbarContainer.style.flexShrink = '0';
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
private addButton(plugin: Plugin): void {
|
|
167
|
-
const btn = document.createElement('button');
|
|
168
|
-
btn.id = `qfchart-plugin-btn-${plugin.id}`;
|
|
169
|
-
// Removed native title to use custom tooltip
|
|
170
|
-
// btn.title = plugin.name || plugin.id;
|
|
171
|
-
btn.style.width = '30px';
|
|
172
|
-
btn.style.height = '30px';
|
|
173
|
-
btn.style.padding = '4px';
|
|
174
|
-
btn.style.border = '1px solid transparent';
|
|
175
|
-
btn.style.borderRadius = '4px';
|
|
176
|
-
btn.style.backgroundColor = 'transparent';
|
|
177
|
-
btn.style.cursor = 'pointer';
|
|
178
|
-
btn.style.color = this.context.getOptions().fontColor || '#cbd5e1';
|
|
179
|
-
btn.style.display = 'flex';
|
|
180
|
-
btn.style.alignItems = 'center';
|
|
181
|
-
btn.style.justifyContent = 'center';
|
|
182
|
-
|
|
183
|
-
// Icon
|
|
184
|
-
if (plugin.icon) {
|
|
185
|
-
btn.innerHTML = plugin.icon;
|
|
186
|
-
} else {
|
|
187
|
-
btn.innerText = (plugin.name || plugin.id).substring(0, 2).toUpperCase();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Hover effects and Tooltip
|
|
191
|
-
btn.addEventListener('mouseenter', () => {
|
|
192
|
-
if (this.activePluginId !== plugin.id) {
|
|
193
|
-
btn.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
|
|
194
|
-
}
|
|
195
|
-
this.showTooltip(btn, plugin.name || plugin.id);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
btn.addEventListener('mouseleave', () => {
|
|
199
|
-
if (this.activePluginId !== plugin.id) {
|
|
200
|
-
btn.style.backgroundColor = 'transparent';
|
|
201
|
-
}
|
|
202
|
-
this.hideTooltip();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
btn.onclick = () => this.activatePlugin(plugin.id);
|
|
206
|
-
|
|
207
|
-
this.toolbarContainer.appendChild(btn);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
private removeButton(pluginId: string): void {
|
|
211
|
-
const btn = this.toolbarContainer.querySelector(`#qfchart-plugin-btn-${pluginId}`);
|
|
212
|
-
if (btn) {
|
|
213
|
-
btn.remove();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private setButtonActive(pluginId: string, active: boolean): void {
|
|
218
|
-
const btn = this.toolbarContainer.querySelector(`#qfchart-plugin-btn-${pluginId}`) as HTMLElement;
|
|
219
|
-
if (btn) {
|
|
220
|
-
if (active) {
|
|
221
|
-
btn.style.backgroundColor = '#2563eb'; // Blue highlight
|
|
222
|
-
btn.style.color = '#ffffff';
|
|
223
|
-
} else {
|
|
224
|
-
btn.style.backgroundColor = 'transparent';
|
|
225
|
-
btn.style.color = this.context.getOptions().fontColor || '#cbd5e1';
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
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: '0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.15)',
|
|
36
|
+
fontFamily: this.context.getOptions().fontFamily || 'sans-serif',
|
|
37
|
+
transition: 'opacity 0.15s ease-in-out, transform 0.15s ease-in-out',
|
|
38
|
+
opacity: '0',
|
|
39
|
+
transform: 'translateX(-5px)',
|
|
40
|
+
});
|
|
41
|
+
document.body.appendChild(this.tooltipElement);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public destroy() {
|
|
45
|
+
if (this.tooltipElement && this.tooltipElement.parentNode) {
|
|
46
|
+
this.tooltipElement.parentNode.removeChild(this.tooltipElement);
|
|
47
|
+
}
|
|
48
|
+
this.tooltipElement = null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private showTooltip(target: HTMLElement, text: string) {
|
|
52
|
+
if (!this.tooltipElement) return;
|
|
53
|
+
|
|
54
|
+
// Clear any pending hide to prevent race conditions
|
|
55
|
+
if (this.hideTimeout) {
|
|
56
|
+
clearTimeout(this.hideTimeout);
|
|
57
|
+
this.hideTimeout = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const rect = target.getBoundingClientRect();
|
|
61
|
+
this.tooltipElement.textContent = text;
|
|
62
|
+
this.tooltipElement.style.display = 'block';
|
|
63
|
+
|
|
64
|
+
// Position to the right of the button, centered vertically
|
|
65
|
+
const tooltipRect = this.tooltipElement.getBoundingClientRect();
|
|
66
|
+
const top = rect.top + (rect.height - tooltipRect.height) / 2;
|
|
67
|
+
const left = rect.right + 10; // 10px gap
|
|
68
|
+
|
|
69
|
+
this.tooltipElement.style.top = `${top}px`;
|
|
70
|
+
this.tooltipElement.style.left = `${left}px`;
|
|
71
|
+
|
|
72
|
+
// Trigger animation
|
|
73
|
+
requestAnimationFrame(() => {
|
|
74
|
+
if (this.tooltipElement) {
|
|
75
|
+
this.tooltipElement.style.opacity = '1';
|
|
76
|
+
this.tooltipElement.style.transform = 'translateX(0)';
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private hideTooltip() {
|
|
82
|
+
if (!this.tooltipElement) return;
|
|
83
|
+
this.tooltipElement.style.opacity = '0';
|
|
84
|
+
this.tooltipElement.style.transform = 'translateX(-5px)';
|
|
85
|
+
|
|
86
|
+
if (this.hideTimeout) {
|
|
87
|
+
clearTimeout(this.hideTimeout);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Wait for transition to finish before hiding
|
|
91
|
+
this.hideTimeout = setTimeout(() => {
|
|
92
|
+
if (this.tooltipElement) {
|
|
93
|
+
this.tooltipElement.style.display = 'none';
|
|
94
|
+
}
|
|
95
|
+
this.hideTimeout = null;
|
|
96
|
+
}, 150);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public register(plugin: Plugin): void {
|
|
100
|
+
if (this.plugins.has(plugin.id)) {
|
|
101
|
+
console.warn(`Plugin with id ${plugin.id} is already registered.`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.plugins.set(plugin.id, plugin);
|
|
105
|
+
plugin.init(this.context);
|
|
106
|
+
this.addButton(plugin);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public unregister(pluginId: string): void {
|
|
110
|
+
const plugin = this.plugins.get(pluginId);
|
|
111
|
+
if (plugin) {
|
|
112
|
+
if (this.activePluginId === pluginId) {
|
|
113
|
+
this.deactivatePlugin();
|
|
114
|
+
}
|
|
115
|
+
plugin.destroy?.();
|
|
116
|
+
this.plugins.delete(pluginId);
|
|
117
|
+
this.removeButton(pluginId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public activatePlugin(pluginId: string): void {
|
|
122
|
+
// If same plugin is clicked, deactivate it (toggle)
|
|
123
|
+
if (this.activePluginId === pluginId) {
|
|
124
|
+
this.deactivatePlugin();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Deactivate current active plugin
|
|
129
|
+
if (this.activePluginId) {
|
|
130
|
+
this.deactivatePlugin();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const plugin = this.plugins.get(pluginId);
|
|
134
|
+
if (plugin) {
|
|
135
|
+
this.activePluginId = pluginId;
|
|
136
|
+
this.setButtonActive(pluginId, true);
|
|
137
|
+
plugin.activate?.();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public deactivatePlugin(): void {
|
|
142
|
+
if (this.activePluginId) {
|
|
143
|
+
const plugin = this.plugins.get(this.activePluginId);
|
|
144
|
+
plugin?.deactivate?.();
|
|
145
|
+
this.setButtonActive(this.activePluginId, false);
|
|
146
|
+
this.activePluginId = null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- UI Handling ---
|
|
151
|
+
|
|
152
|
+
private renderToolbar(): void {
|
|
153
|
+
this.toolbarContainer.innerHTML = '';
|
|
154
|
+
this.toolbarContainer.classList.add('qfchart-toolbar');
|
|
155
|
+
this.toolbarContainer.style.display = 'flex';
|
|
156
|
+
this.toolbarContainer.style.flexDirection = 'column';
|
|
157
|
+
this.toolbarContainer.style.width = '40px';
|
|
158
|
+
this.toolbarContainer.style.backgroundColor = this.context.getOptions().backgroundColor || '#1e293b';
|
|
159
|
+
this.toolbarContainer.style.borderRight = '1px solid #334155';
|
|
160
|
+
this.toolbarContainer.style.padding = '5px';
|
|
161
|
+
this.toolbarContainer.style.boxSizing = 'border-box';
|
|
162
|
+
this.toolbarContainer.style.gap = '5px';
|
|
163
|
+
this.toolbarContainer.style.flexShrink = '0';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private addButton(plugin: Plugin): void {
|
|
167
|
+
const btn = document.createElement('button');
|
|
168
|
+
btn.id = `qfchart-plugin-btn-${plugin.id}`;
|
|
169
|
+
// Removed native title to use custom tooltip
|
|
170
|
+
// btn.title = plugin.name || plugin.id;
|
|
171
|
+
btn.style.width = '30px';
|
|
172
|
+
btn.style.height = '30px';
|
|
173
|
+
btn.style.padding = '4px';
|
|
174
|
+
btn.style.border = '1px solid transparent';
|
|
175
|
+
btn.style.borderRadius = '4px';
|
|
176
|
+
btn.style.backgroundColor = 'transparent';
|
|
177
|
+
btn.style.cursor = 'pointer';
|
|
178
|
+
btn.style.color = this.context.getOptions().fontColor || '#cbd5e1';
|
|
179
|
+
btn.style.display = 'flex';
|
|
180
|
+
btn.style.alignItems = 'center';
|
|
181
|
+
btn.style.justifyContent = 'center';
|
|
182
|
+
|
|
183
|
+
// Icon
|
|
184
|
+
if (plugin.icon) {
|
|
185
|
+
btn.innerHTML = plugin.icon;
|
|
186
|
+
} else {
|
|
187
|
+
btn.innerText = (plugin.name || plugin.id).substring(0, 2).toUpperCase();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Hover effects and Tooltip
|
|
191
|
+
btn.addEventListener('mouseenter', () => {
|
|
192
|
+
if (this.activePluginId !== plugin.id) {
|
|
193
|
+
btn.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
|
|
194
|
+
}
|
|
195
|
+
this.showTooltip(btn, plugin.name || plugin.id);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
btn.addEventListener('mouseleave', () => {
|
|
199
|
+
if (this.activePluginId !== plugin.id) {
|
|
200
|
+
btn.style.backgroundColor = 'transparent';
|
|
201
|
+
}
|
|
202
|
+
this.hideTooltip();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
btn.onclick = () => this.activatePlugin(plugin.id);
|
|
206
|
+
|
|
207
|
+
this.toolbarContainer.appendChild(btn);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private removeButton(pluginId: string): void {
|
|
211
|
+
const btn = this.toolbarContainer.querySelector(`#qfchart-plugin-btn-${pluginId}`);
|
|
212
|
+
if (btn) {
|
|
213
|
+
btn.remove();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private setButtonActive(pluginId: string, active: boolean): void {
|
|
218
|
+
const btn = this.toolbarContainer.querySelector(`#qfchart-plugin-btn-${pluginId}`) as HTMLElement;
|
|
219
|
+
if (btn) {
|
|
220
|
+
if (active) {
|
|
221
|
+
btn.style.backgroundColor = '#2563eb'; // Blue highlight
|
|
222
|
+
btn.style.color = '#ffffff';
|
|
223
|
+
} else {
|
|
224
|
+
btn.style.backgroundColor = 'transparent';
|
|
225
|
+
btn.style.color = this.context.getOptions().fontColor || '#cbd5e1';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -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
|
|
|
@@ -146,7 +149,7 @@ export class SeriesBuilder {
|
|
|
146
149
|
let plotOverlay = plot.options.overlay;
|
|
147
150
|
|
|
148
151
|
// Fill plots inherit overlay from their referenced plots.
|
|
149
|
-
// If
|
|
152
|
+
// If either referenced plot is overlay, the fill should render on the
|
|
150
153
|
// overlay pane too — otherwise its price-scale data stretches the
|
|
151
154
|
// indicator sub-pane's y-axis to extreme ranges.
|
|
152
155
|
if (plot.options.style === 'fill' && plotOverlay === undefined) {
|
|
@@ -155,7 +158,7 @@ export class SeriesBuilder {
|
|
|
155
158
|
if (p1Name && p2Name) {
|
|
156
159
|
const p1 = indicator.plots[p1Name];
|
|
157
160
|
const p2 = indicator.plots[p2Name];
|
|
158
|
-
if (p1?.options?.overlay === true
|
|
161
|
+
if (p1?.options?.overlay === true || p2?.options?.overlay === true) {
|
|
159
162
|
plotOverlay = true;
|
|
160
163
|
}
|
|
161
164
|
}
|
|
@@ -309,21 +312,28 @@ export class SeriesBuilder {
|
|
|
309
312
|
}
|
|
310
313
|
}
|
|
311
314
|
|
|
312
|
-
// Skip fully transparent plots — they exist only as data sources for fills.
|
|
315
|
+
// Skip fully transparent / invisible plots — they exist only as data sources for fills.
|
|
313
316
|
// Their data is already stored in plotDataArrays for fill references.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
317
|
+
// Covers: color(na) → null, color.new(x, 100) → fully transparent string, etc.
|
|
318
|
+
{
|
|
319
|
+
const plotColor = plot.options.color;
|
|
320
|
+
let skipPlot = false;
|
|
321
|
+
if (plotColor == null) {
|
|
322
|
+
// color(na) — plot-level color is null/undefined; skip if no bar has a visible color
|
|
323
|
+
const hasVisibleBarColor = colorArray.some((c: any) => c != null);
|
|
324
|
+
skipPlot = !hasVisibleBarColor;
|
|
325
|
+
} else if (typeof plotColor === 'string') {
|
|
326
|
+
const parsed = ColorUtils.parseColor(plotColor);
|
|
327
|
+
if (parsed.opacity < 0.01) {
|
|
328
|
+
const hasVisibleBarColor = colorArray.some((c: any) => {
|
|
329
|
+
if (c == null) return false;
|
|
330
|
+
const pc = ColorUtils.parseColor(c);
|
|
331
|
+
return pc.opacity >= 0.01;
|
|
332
|
+
});
|
|
333
|
+
skipPlot = !hasVisibleBarColor;
|
|
325
334
|
}
|
|
326
335
|
}
|
|
336
|
+
if (skipPlot) return;
|
|
327
337
|
}
|
|
328
338
|
|
|
329
339
|
// Use Factory to get appropriate renderer
|
|
@@ -341,6 +351,8 @@ export class SeriesBuilder {
|
|
|
341
351
|
indicatorId: id,
|
|
342
352
|
plotName: plotName,
|
|
343
353
|
dataIndexOffset,
|
|
354
|
+
timeToIndex,
|
|
355
|
+
marketData: rawMarketData,
|
|
344
356
|
});
|
|
345
357
|
|
|
346
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';
|