Skip to content

Commit 4ed366a

Browse files
author
idf_karolisstr
committed
Refactoring.
1 parent 605aa2d commit 4ed366a

15 files changed

Lines changed: 2565 additions & 1219 deletions

‎src/charts/components/ChartConfigurationManager.ts‎

Lines changed: 414 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
import { getLabelDefinitions } from '../../ui/dropdowns';
2+
import type { ECharts } from '../echarts';
3+
import type { TimeSeriesConfig, TimeSeriesData } from '../timeSeries';
4+
import { createThresholdMarkLine } from '../types';
5+
6+
/**
7+
* Events emitted by the chart renderer
8+
*/
9+
export interface ChartRendererEvents {
10+
'zoom-changed': { start?: number; end?: number };
11+
}
12+
13+
/**
14+
* Manages chart rendering and display updates.
15+
* Handles data visualization, labels, thresholds, and zoom interactions.
16+
*/
17+
export class ChartRenderer {
18+
private chart: ECharts | null = null;
19+
private listeners = new Map<keyof ChartRendererEvents, Array<(event: unknown) => void>>();
20+
private currentData: ReadonlyArray<readonly [number | string, number]> = [];
21+
private highlightedLabelId: string | null = null;
22+
23+
/**
24+
* Initialize chart renderer
25+
*/
26+
27+
/**
28+
* Initialize with chart instance
29+
*/
30+
initialize(chart: ECharts): void {
31+
this.chart = chart;
32+
this.setupZoomListener();
33+
}
34+
35+
/**
36+
* Add event listener
37+
*/
38+
on<K extends keyof ChartRendererEvents>(
39+
event: K,
40+
listener: (data: ChartRendererEvents[K]) => void
41+
): void {
42+
if (!this.listeners.has(event)) {
43+
this.listeners.set(event, []);
44+
}
45+
const eventListeners = this.listeners.get(event);
46+
if (eventListeners) {
47+
eventListeners.push(listener as (event: unknown) => void);
48+
}
49+
}
50+
51+
/**
52+
* Remove event listener
53+
*/
54+
off<K extends keyof ChartRendererEvents>(
55+
event: K,
56+
listener: (data: ChartRendererEvents[K]) => void
57+
): void {
58+
const eventListeners = this.listeners.get(event);
59+
if (eventListeners) {
60+
const index = eventListeners.indexOf(listener as (event: unknown) => void);
61+
if (index >= 0) {
62+
eventListeners.splice(index, 1);
63+
}
64+
}
65+
}
66+
67+
/**
68+
* Emit event to listeners
69+
*/
70+
private emit<K extends keyof ChartRendererEvents>(
71+
event: K,
72+
data: ChartRendererEvents[K]
73+
): void {
74+
const eventListeners = this.listeners.get(event);
75+
if (eventListeners) {
76+
eventListeners.forEach((listener) => {
77+
listener(data);
78+
});
79+
}
80+
}
81+
82+
/**
83+
* Set highlighted label ID
84+
*/
85+
setHighlightedLabel(labelId: string | null): void {
86+
this.highlightedLabelId = labelId;
87+
}
88+
89+
/**
90+
* Update chart display with configuration and data
91+
*/
92+
updateDisplay(
93+
config: TimeSeriesConfig,
94+
seriesData: ReadonlyArray<readonly [number | string, number]>,
95+
xAxis: any,
96+
yAxis: any,
97+
tooltip: any,
98+
seriesConfig: any,
99+
source: TimeSeriesData | null
100+
): void {
101+
if (!this.chart) return;
102+
103+
this.currentData = seriesData;
104+
105+
// Configure threshold line
106+
let markLine: any;
107+
if (config.thresholdEnabled && config.thresholdValue !== undefined) {
108+
markLine = createThresholdMarkLine(config.thresholdValue);
109+
}
110+
111+
// Configure label areas
112+
const markArea = this.buildLabelMarkAreas(source);
113+
114+
// Update chart with complete configuration
115+
this.chart.setOption(
116+
{
117+
tooltip,
118+
xAxis,
119+
yAxis,
120+
series: [
121+
{
122+
...seriesConfig,
123+
markLine,
124+
markArea,
125+
data: [...seriesData] as Array<[number | string, number]>,
126+
},
127+
],
128+
},
129+
false,
130+
true
131+
);
132+
133+
// Update Y-axis for auto-scaling
134+
this.updateYAxisFromZoom();
135+
}
136+
137+
/**
138+
* Update Y-axis range based on zoom level (for auto-scaling)
139+
*/
140+
updateYAxisFromZoom(start?: number, end?: number): void {
141+
if (!this.chart || this.currentData.length === 0) {
142+
return;
143+
}
144+
145+
type ZoomOption = { start: number; end: number };
146+
const zoomOpts = this.chart.getOption().dataZoom as ReadonlyArray<ZoomOption> | undefined;
147+
const s = start ?? zoomOpts?.[0]?.start ?? 0;
148+
const e = end ?? zoomOpts?.[0]?.end ?? 100;
149+
150+
const len = this.currentData.length;
151+
const startIdx = Math.max(0, Math.floor((s / 100) * (len - 1)));
152+
const endIdx = Math.min(len - 1, Math.ceil((e / 100) * (len - 1)));
153+
154+
let min = Number.POSITIVE_INFINITY;
155+
let max = Number.NEGATIVE_INFINITY;
156+
for (let i = startIdx; i <= endIdx; i++) {
157+
const point = this.currentData[i];
158+
if (!point) continue;
159+
const y = point[1];
160+
if (y < min) min = y;
161+
if (y > max) max = y;
162+
}
163+
if (!isFinite(min) || !isFinite(max)) {
164+
return;
165+
}
166+
const range = max - min;
167+
const padding = range === 0 ? Math.abs(min || 1) * 0.1 : range * 0.1;
168+
169+
this.chart.setOption({
170+
yAxis: { min: min - padding, max: max + padding },
171+
});
172+
}
173+
174+
/**
175+
* Clear chart data (show empty series)
176+
*/
177+
clearDisplay(): void {
178+
if (!this.chart) return;
179+
180+
this.chart.setOption({ series: [{ data: [] }] });
181+
this.currentData = [];
182+
}
183+
184+
/**
185+
* Resize chart
186+
*/
187+
resize(): void {
188+
if (this.chart) {
189+
this.chart.resize();
190+
}
191+
}
192+
193+
/**
194+
* Setup zoom event listener
195+
*/
196+
private setupZoomListener(): void {
197+
if (!this.chart) return;
198+
199+
const handleZoomEvent = (params: { start?: number; end?: number }): void => {
200+
this.updateYAxisFromZoom(params.start, params.end);
201+
this.emit('zoom-changed', params);
202+
};
203+
204+
this.chart.on('dataZoom', handleZoomEvent);
205+
}
206+
207+
/**
208+
* Build markArea configuration for displaying labels
209+
*/
210+
private buildLabelMarkAreas(source: TimeSeriesData | null): any {
211+
if (!source) return undefined;
212+
213+
const labels = source.getLabels();
214+
if (labels.length === 0) return undefined;
215+
216+
// Filter to only show visible labels
217+
const visibleLabels = labels.filter((label) => label.visible !== false);
218+
if (visibleLabels.length === 0) return undefined;
219+
220+
// Create markArea data for each visible label
221+
const markAreaData = visibleLabels.map((label) => {
222+
const isHighlighted = this.highlightedLabelId === label.id;
223+
const opacity = isHighlighted ? 0.6 : 0.3; // Higher opacity for highlighted labels
224+
const baseColor = this.getLabelColor(label.labelDefId, 1.0);
225+
226+
return [
227+
{
228+
xAxis: label.startTime,
229+
itemStyle: {
230+
color: this.getLabelColor(label.labelDefId, opacity),
231+
borderColor: baseColor,
232+
borderWidth: isHighlighted ? 2 : 1, // Always show borders, thicker when highlighted
233+
borderType: 'solid',
234+
},
235+
label: {
236+
show: false, // Hide label names on chart areas
237+
},
238+
},
239+
{
240+
xAxis: label.endTime,
241+
},
242+
];
243+
});
244+
245+
return {
246+
silent: true, // Don't intercept mouse events - allow drawing over labels
247+
data: markAreaData as never, // ECharts markArea expects complex nested array structure
248+
};
249+
}
250+
251+
/**
252+
* Get display color for a label definition
253+
*/
254+
private getLabelColor(labelDefId: string, opacity = 1): string {
255+
// Try to find the label definition by UUID
256+
const labelDefinitions = getLabelDefinitions();
257+
const definition = labelDefinitions.find((def) => def.id === labelDefId);
258+
259+
if (definition && definition.color && typeof definition.color === 'string') {
260+
// Convert hex to rgba with opacity
261+
const hex = definition.color.replace('#', '');
262+
const r = parseInt(hex.substring(0, 2), 16);
263+
const g = parseInt(hex.substring(2, 4), 16);
264+
const b = parseInt(hex.substring(4, 6), 16);
265+
return `rgba(${String(r)}, ${String(g)}, ${String(b)}, ${String(opacity)})`;
266+
}
267+
268+
// Fallback for legacy format "label-{index}" or hardcoded labels
269+
const match = labelDefId.match(/^label-(\d+)$/);
270+
if (match && match[1]) {
271+
const index = parseInt(match[1], 10);
272+
const definition = labelDefinitions[index];
273+
if (definition && definition.color && typeof definition.color === 'string') {
274+
// Convert hex to rgba with opacity
275+
const hex = definition.color.replace('#', '');
276+
const r = parseInt(hex.substring(0, 2), 16);
277+
const g = parseInt(hex.substring(2, 4), 16);
278+
const b = parseInt(hex.substring(4, 6), 16);
279+
return `rgba(${String(r)}, ${String(g)}, ${String(b)}, ${String(opacity)})`;
280+
}
281+
}
282+
283+
// Fallback for hardcoded labels or invalid IDs
284+
const colors: Record<string, string> = {
285+
'default-positive': '#28a745',
286+
'default-negative': '#dc3545',
287+
'default-neutral': '#6c757d',
288+
};
289+
290+
const baseColor = colors[labelDefId] || '#007bff';
291+
292+
// Convert hex to rgba with opacity
293+
const hex = baseColor.replace('#', '');
294+
const r = parseInt(hex.substring(0, 2), 16);
295+
const g = parseInt(hex.substring(2, 4), 16);
296+
const b = parseInt(hex.substring(4, 6), 16);
297+
298+
return `rgba(${String(r)}, ${String(g)}, ${String(b)}, ${String(opacity)})`;
299+
}
300+
301+
/**
302+
* Clean up resources
303+
*/
304+
dispose(): void {
305+
this.listeners.clear();
306+
}
307+
}

0 commit comments

Comments
 (0)