@qfo/qfchart 0.8.1 → 0.8.4
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 +216 -1
- package/dist/qfchart.min.browser.js +20 -19
- package/dist/qfchart.min.es.js +18 -17
- package/package.json +1 -1
- package/src/QFChart.ts +146 -11
- package/src/components/LayoutManager.ts +76 -28
- package/src/components/PluginManager.ts +229 -229
- package/src/components/SeriesBuilder.ts +21 -14
- package/src/components/renderers/LabelRenderer.ts +6 -3
- package/src/components/renderers/ScatterRenderer.ts +92 -54
- package/src/components/renderers/ShapeRenderer.ts +12 -0
- package/src/index.ts +8 -0
- package/src/plugins/CrossLineTool/CrossLineDrawingRenderer.ts +49 -0
- package/src/plugins/CrossLineTool/CrossLineTool.ts +52 -0
- package/src/plugins/CrossLineTool/index.ts +2 -0
- package/src/plugins/ExtendedLineTool/ExtendedLineDrawingRenderer.ts +73 -0
- package/src/plugins/ExtendedLineTool/ExtendedLineTool.ts +173 -0
- package/src/plugins/ExtendedLineTool/index.ts +2 -0
- package/src/plugins/HorizontalLineTool/HorizontalLineDrawingRenderer.ts +54 -0
- package/src/plugins/HorizontalLineTool/HorizontalLineTool.ts +52 -0
- package/src/plugins/HorizontalLineTool/index.ts +2 -0
- package/src/plugins/HorizontalRayTool/HorizontalRayDrawingRenderer.ts +34 -0
- package/src/plugins/HorizontalRayTool/HorizontalRayTool.ts +52 -0
- package/src/plugins/HorizontalRayTool/index.ts +2 -0
- package/src/plugins/InfoLineTool/InfoLineDrawingRenderer.ts +72 -0
- package/src/plugins/InfoLineTool/InfoLineTool.ts +130 -0
- package/src/plugins/InfoLineTool/index.ts +2 -0
- package/src/plugins/LineTool/LineDrawingRenderer.ts +2 -2
- package/src/plugins/LineTool/LineTool.ts +5 -5
- package/src/plugins/RayTool/RayDrawingRenderer.ts +69 -0
- package/src/plugins/RayTool/RayTool.ts +162 -0
- package/src/plugins/RayTool/index.ts +2 -0
- package/src/plugins/TrendAngleTool/TrendAngleDrawingRenderer.ts +87 -0
- package/src/plugins/TrendAngleTool/TrendAngleTool.ts +176 -0
- package/src/plugins/TrendAngleTool/index.ts +2 -0
- package/src/plugins/VerticalLineTool/VerticalLineDrawingRenderer.ts +35 -0
- package/src/plugins/VerticalLineTool/VerticalLineTool.ts +52 -0
- package/src/plugins/VerticalLineTool/index.ts +2 -0
- package/src/types.ts +2 -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
|
+
}
|
|
@@ -146,7 +146,7 @@ export class SeriesBuilder {
|
|
|
146
146
|
let plotOverlay = plot.options.overlay;
|
|
147
147
|
|
|
148
148
|
// Fill plots inherit overlay from their referenced plots.
|
|
149
|
-
// If
|
|
149
|
+
// If either referenced plot is overlay, the fill should render on the
|
|
150
150
|
// overlay pane too — otherwise its price-scale data stretches the
|
|
151
151
|
// indicator sub-pane's y-axis to extreme ranges.
|
|
152
152
|
if (plot.options.style === 'fill' && plotOverlay === undefined) {
|
|
@@ -155,7 +155,7 @@ export class SeriesBuilder {
|
|
|
155
155
|
if (p1Name && p2Name) {
|
|
156
156
|
const p1 = indicator.plots[p1Name];
|
|
157
157
|
const p2 = indicator.plots[p2Name];
|
|
158
|
-
if (p1?.options?.overlay === true
|
|
158
|
+
if (p1?.options?.overlay === true || p2?.options?.overlay === true) {
|
|
159
159
|
plotOverlay = true;
|
|
160
160
|
}
|
|
161
161
|
}
|
|
@@ -309,21 +309,28 @@ export class SeriesBuilder {
|
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
// Skip fully transparent plots — they exist only as data sources for fills.
|
|
312
|
+
// Skip fully transparent / invisible plots — they exist only as data sources for fills.
|
|
313
313
|
// Their data is already stored in plotDataArrays for fill references.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
314
|
+
// Covers: color(na) → null, color.new(x, 100) → fully transparent string, etc.
|
|
315
|
+
{
|
|
316
|
+
const plotColor = plot.options.color;
|
|
317
|
+
let skipPlot = false;
|
|
318
|
+
if (plotColor == null) {
|
|
319
|
+
// color(na) — plot-level color is null/undefined; skip if no bar has a visible color
|
|
320
|
+
const hasVisibleBarColor = colorArray.some((c: any) => c != null);
|
|
321
|
+
skipPlot = !hasVisibleBarColor;
|
|
322
|
+
} else if (typeof plotColor === 'string') {
|
|
323
|
+
const parsed = ColorUtils.parseColor(plotColor);
|
|
324
|
+
if (parsed.opacity < 0.01) {
|
|
325
|
+
const hasVisibleBarColor = colorArray.some((c: any) => {
|
|
326
|
+
if (c == null) return false;
|
|
327
|
+
const pc = ColorUtils.parseColor(c);
|
|
328
|
+
return pc.opacity >= 0.01;
|
|
329
|
+
});
|
|
330
|
+
skipPlot = !hasVisibleBarColor;
|
|
325
331
|
}
|
|
326
332
|
}
|
|
333
|
+
if (skipPlot) return;
|
|
327
334
|
}
|
|
328
335
|
|
|
329
336
|
// Use Factory to get appropriate renderer
|
|
@@ -77,11 +77,14 @@ export class LabelRenderer implements SeriesRenderer {
|
|
|
77
77
|
let labelTextOffset: [number, number] = [0, 0];
|
|
78
78
|
|
|
79
79
|
if (isBubble) {
|
|
80
|
-
//
|
|
81
|
-
const
|
|
80
|
+
// For multi-line text, size based on the longest line and number of lines
|
|
81
|
+
const lines = text.split('\n');
|
|
82
|
+
const longestLine = lines.reduce((a: string, b: string) => a.length > b.length ? a : b, '');
|
|
83
|
+
const textWidth = longestLine.length * fontSize * 0.65;
|
|
82
84
|
const minWidth = fontSize * 2.5;
|
|
83
85
|
const bubbleWidth = Math.max(minWidth, textWidth + fontSize * 1.6);
|
|
84
|
-
const
|
|
86
|
+
const lineHeight = fontSize * 1.4;
|
|
87
|
+
const bubbleHeight = Math.max(fontSize * 2.8, lines.length * lineHeight + fontSize * 1.2);
|
|
85
88
|
|
|
86
89
|
// SVG pointer takes 3/24 = 12.5% of the path dimension
|
|
87
90
|
const pointerRatio = 3 / 24;
|