cats4u-charts 0.0.1
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/.editorconfig +17 -0
- package/.vscode/extensions.json +4 -0
- package/.vscode/launch.json +20 -0
- package/.vscode/tasks.json +42 -0
- package/README.md +59 -0
- package/angular.json +118 -0
- package/dist/charts-lib/README.md +63 -0
- package/dist/charts-lib/fesm2022/charts-lib.mjs +3418 -0
- package/dist/charts-lib/fesm2022/charts-lib.mjs.map +1 -0
- package/dist/charts-lib/index.d.ts +110 -0
- package/package.json +58 -0
- package/projects/charts-lib/README.md +63 -0
- package/projects/charts-lib/ng-package.json +8 -0
- package/projects/charts-lib/package.json +12 -0
- package/projects/charts-lib/src/lib/charts-lib.html +1 -0
- package/projects/charts-lib/src/lib/charts-lib.spec.ts +23 -0
- package/projects/charts-lib/src/lib/charts-lib.ts +121 -0
- package/projects/charts-lib/src/lib/component/area-chart/area-chart.html +1 -0
- package/projects/charts-lib/src/lib/component/area-chart/area-chart.scss +0 -0
- package/projects/charts-lib/src/lib/component/area-chart/area-chart.spec.ts +23 -0
- package/projects/charts-lib/src/lib/component/area-chart/area-chart.ts +266 -0
- package/projects/charts-lib/src/lib/component/bar-chart/bar-chart.html +1 -0
- package/projects/charts-lib/src/lib/component/bar-chart/bar-chart.scss +0 -0
- package/projects/charts-lib/src/lib/component/bar-chart/bar-chart.spec.ts +23 -0
- package/projects/charts-lib/src/lib/component/bar-chart/bar-chart.ts +301 -0
- package/projects/charts-lib/src/lib/component/line-chart/line-chart.html +1 -0
- package/projects/charts-lib/src/lib/component/line-chart/line-chart.scss +0 -0
- package/projects/charts-lib/src/lib/component/line-chart/line-chart.spec.ts +23 -0
- package/projects/charts-lib/src/lib/component/line-chart/line-chart.ts +266 -0
- package/projects/charts-lib/src/lib/modal/charts-lib.modal.ts +79 -0
- package/projects/charts-lib/src/lib/services/chart.service.ts +296 -0
- package/projects/charts-lib/src/lib/themes/chalk.ts +357 -0
- package/projects/charts-lib/src/lib/themes/dark.ts +380 -0
- package/projects/charts-lib/src/lib/themes/default.ts +377 -0
- package/projects/charts-lib/src/lib/themes/essos.ts +357 -0
- package/projects/charts-lib/src/lib/themes/roma.ts +399 -0
- package/projects/charts-lib/src/lib/themes/vintage.ts +378 -0
- package/projects/charts-lib/src/public-api.ts +2 -0
- package/projects/charts-lib/tsconfig.lib.json +14 -0
- package/projects/charts-lib/tsconfig.lib.prod.json +11 -0
- package/projects/charts-lib/tsconfig.spec.json +15 -0
- package/projects/demo-app/public/favicon.ico +0 -0
- package/projects/demo-app/src/app/app.config.ts +16 -0
- package/projects/demo-app/src/app/app.html +43 -0
- package/projects/demo-app/src/app/app.routes.ts +3 -0
- package/projects/demo-app/src/app/app.scss +47 -0
- package/projects/demo-app/src/app/app.spec.ts +25 -0
- package/projects/demo-app/src/app/app.ts +98 -0
- package/projects/demo-app/src/index.html +13 -0
- package/projects/demo-app/src/main.ts +6 -0
- package/projects/demo-app/src/styles.scss +4 -0
- package/projects/demo-app/tsconfig.app.json +15 -0
- package/projects/demo-app/tsconfig.spec.json +15 -0
- package/tsconfig.json +43 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
Input,
|
|
4
|
+
OnInit,
|
|
5
|
+
OnDestroy,
|
|
6
|
+
ChangeDetectionStrategy,
|
|
7
|
+
ChangeDetectorRef,
|
|
8
|
+
ViewChild,
|
|
9
|
+
ElementRef,
|
|
10
|
+
inject,
|
|
11
|
+
effect,
|
|
12
|
+
} from '@angular/core';
|
|
13
|
+
import { CommonModule } from '@angular/common';
|
|
14
|
+
import * as echarts from 'echarts';
|
|
15
|
+
import { Subject } from 'rxjs';
|
|
16
|
+
import { ChartService } from '../../services/chart.service';
|
|
17
|
+
import type { EChartsOption } from 'echarts';
|
|
18
|
+
import { OptionsConfig } from '../../modal/charts-lib.modal';
|
|
19
|
+
import '../../themes/default';
|
|
20
|
+
import '../../themes/vintage';
|
|
21
|
+
import '../../themes/dark';
|
|
22
|
+
import '../../themes/essos';
|
|
23
|
+
import '../../themes/chalk';
|
|
24
|
+
import '../../themes/roma';
|
|
25
|
+
|
|
26
|
+
@Component({
|
|
27
|
+
selector: 'lib-bar-chart',
|
|
28
|
+
imports: [CommonModule],
|
|
29
|
+
templateUrl: './bar-chart.html',
|
|
30
|
+
styleUrl: './bar-chart.scss',
|
|
31
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
32
|
+
})
|
|
33
|
+
export class BarChart implements OnInit, OnDestroy {
|
|
34
|
+
@Input() title: string = '';
|
|
35
|
+
@Input() height = '600px';
|
|
36
|
+
@Input() enableSampling = true; // Sample data if > threshold for perf
|
|
37
|
+
@Input() sampleThreshold = 10000; // Sample if data > 10k (keep full for 50k if hardware allows)
|
|
38
|
+
@Input() isLoading: boolean = true;
|
|
39
|
+
private defaultConfig: OptionsConfig = new OptionsConfig();
|
|
40
|
+
private optionsConfig: OptionsConfig = {};
|
|
41
|
+
|
|
42
|
+
@ViewChild('barChartContainer', { static: false }) chartContainer!: ElementRef<HTMLDivElement>;
|
|
43
|
+
|
|
44
|
+
private chartInstance: echarts.ECharts | null = null;
|
|
45
|
+
private chartService = inject(ChartService);
|
|
46
|
+
|
|
47
|
+
private destroy$ = new Subject<void>();
|
|
48
|
+
private currentData: any[][] = [];
|
|
49
|
+
private currentColumns: string[] = [];
|
|
50
|
+
|
|
51
|
+
private boundHandleContextMenu: (event: MouseEvent) => void;
|
|
52
|
+
|
|
53
|
+
constructor(private cdr: ChangeDetectorRef) {
|
|
54
|
+
this.boundHandleContextMenu = this.handleContextMenu.bind(this);
|
|
55
|
+
// Effect for theme changes
|
|
56
|
+
effect(() => {
|
|
57
|
+
this.chartService.theme(); // Read signal
|
|
58
|
+
if (this.chartInstance) {
|
|
59
|
+
this.reinitializeChart();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// Effect for data changes
|
|
63
|
+
effect(() => {
|
|
64
|
+
const data = this.chartService.data(); // Read signal
|
|
65
|
+
const columns = this.chartService.columns();
|
|
66
|
+
this.currentColumns = columns;
|
|
67
|
+
this.currentData = data;
|
|
68
|
+
this.processAndUpdateData();
|
|
69
|
+
});
|
|
70
|
+
// Effect for chartOptionsConfig changes
|
|
71
|
+
effect(() => {
|
|
72
|
+
this.chartService.chartOptionsConfig();
|
|
73
|
+
this.optionsConfig = this.chartService.chartOptionsConfig();
|
|
74
|
+
this.reinitializeChart();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
ngOnInit(): void {
|
|
79
|
+
if (this.currentData.length > 0) {
|
|
80
|
+
this.processAndUpdateData();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
ngAfterViewInit(): void {
|
|
85
|
+
this.initializeChart();
|
|
86
|
+
if (this.currentData.length > 0) {
|
|
87
|
+
this.updateChartWithData();
|
|
88
|
+
}
|
|
89
|
+
this.isLoading = false;
|
|
90
|
+
this.cdr.markForCheck();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ngOnDestroy(): void {
|
|
94
|
+
this.destroy$.next();
|
|
95
|
+
this.destroy$.complete();
|
|
96
|
+
this.chartService.hideContextMenu();
|
|
97
|
+
this.disposeChart();
|
|
98
|
+
if (this.chartInstance) {
|
|
99
|
+
this.chartInstance.dispose(); // Clean up to free memory
|
|
100
|
+
}
|
|
101
|
+
if (this.chartContainer) {
|
|
102
|
+
this.chartContainer.nativeElement.removeEventListener(
|
|
103
|
+
'contextmenu',
|
|
104
|
+
this.boundHandleContextMenu
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
document.removeEventListener('click', () => {
|
|
108
|
+
this.chartService.hideContextMenu();
|
|
109
|
+
this.chartService.resetContextEvent();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private reinitializeChart(): void {
|
|
114
|
+
this.initializeChart();
|
|
115
|
+
if (this.currentData.length > 0) {
|
|
116
|
+
this.updateChartWithData();
|
|
117
|
+
}
|
|
118
|
+
this.cdr.markForCheck();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private initializeChart(): void {
|
|
122
|
+
if (!this.chartContainer) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const chartDom = this.chartContainer.nativeElement;
|
|
126
|
+
chartDom.innerHTML = '';
|
|
127
|
+
|
|
128
|
+
if (this.chartInstance) {
|
|
129
|
+
this.chartInstance.dispose();
|
|
130
|
+
this.chartInstance = null;
|
|
131
|
+
}
|
|
132
|
+
this.chartInstance = echarts.init(chartDom, this.chartService.theme()); // Init ECharts
|
|
133
|
+
this.chartInstance.setOption({
|
|
134
|
+
...this.defaultConfig,
|
|
135
|
+
...this.optionsConfig,
|
|
136
|
+
title: {
|
|
137
|
+
...this.defaultConfig.title,
|
|
138
|
+
...this.optionsConfig.title,
|
|
139
|
+
textStyle: {
|
|
140
|
+
...this.defaultConfig.title?.textStyle,
|
|
141
|
+
...this.optionsConfig.title?.textStyle,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
tooltip: {
|
|
145
|
+
...this.defaultConfig.tooltip,
|
|
146
|
+
...this.optionsConfig.tooltip,
|
|
147
|
+
},
|
|
148
|
+
legend: { ...this.defaultConfig.legend, ...this.optionsConfig.legend },
|
|
149
|
+
dataZoom: this.optionsConfig.dataZoom || this.defaultConfig.dataZoom,
|
|
150
|
+
grid: {
|
|
151
|
+
...this.defaultConfig.grid,
|
|
152
|
+
...this.optionsConfig.grid,
|
|
153
|
+
},
|
|
154
|
+
xAxis: {
|
|
155
|
+
...this.defaultConfig.xAxis,
|
|
156
|
+
...this.optionsConfig.xAxis,
|
|
157
|
+
data: [],
|
|
158
|
+
},
|
|
159
|
+
yAxis: {
|
|
160
|
+
...this.defaultConfig.yAxis,
|
|
161
|
+
...this.optionsConfig.yAxis,
|
|
162
|
+
},
|
|
163
|
+
series: [],
|
|
164
|
+
} as EChartsOption);
|
|
165
|
+
|
|
166
|
+
// Responsive: Resize on window change
|
|
167
|
+
window.addEventListener('resize', () => this.chartInstance?.resize(), { passive: true });
|
|
168
|
+
|
|
169
|
+
// Handle dataZoom events (optional: Log zoom changes)
|
|
170
|
+
this.chartInstance.on('dataZoom', (params) => {
|
|
171
|
+
// console.log('Zoomed to:', params.start, params.end);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
this.chartInstance.on('rendered', () => {
|
|
175
|
+
// console.log('Chart rendered successfully');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
this.chartInstance.on('click', (params) => {
|
|
179
|
+
this.chartService.hideContextMenu();
|
|
180
|
+
this.chartService.resetContextEvent();
|
|
181
|
+
this.chartService.handleClick(params);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
this.chartInstance.on('contextmenu', (params) => {
|
|
185
|
+
this.chartService.openContextMenu(params, this.chartContainer);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
chartDom.addEventListener('contextmenu', this.boundHandleContextMenu);
|
|
189
|
+
document.addEventListener(
|
|
190
|
+
'click',
|
|
191
|
+
() => {
|
|
192
|
+
this.chartService.hideContextMenu();
|
|
193
|
+
this.chartService.resetContextEvent();
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
passive: true,
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
this.isLoading = false;
|
|
200
|
+
this.cdr.markForCheck();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private handleContextMenu(event: MouseEvent): void {
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
setTimeout(() => {
|
|
206
|
+
if (!this.chartService.contextMenuEvent().seriesName) {
|
|
207
|
+
this.chartService.openContextMenu(null, this.chartContainer, event);
|
|
208
|
+
}
|
|
209
|
+
}, 0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private updateChartWithData(): void {
|
|
213
|
+
this.processData(); // Process without updating yet
|
|
214
|
+
if (this.chartInstance) {
|
|
215
|
+
const updatedOptions: Partial<echarts.EChartsOption> = {
|
|
216
|
+
xAxis: { data: this.getProcessedCategories() },
|
|
217
|
+
series: this.getProcessedValues(),
|
|
218
|
+
};
|
|
219
|
+
this.chartInstance.setOption(updatedOptions, {
|
|
220
|
+
notMerge: false,
|
|
221
|
+
lazyUpdate: true,
|
|
222
|
+
silent: false,
|
|
223
|
+
replaceMerge: ['series'],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private processedData: any[][] = [];
|
|
229
|
+
private processedColumns: string[] = [];
|
|
230
|
+
private getProcessedCategories(): string[] {
|
|
231
|
+
return this.processedData.map((d) => d[0]);
|
|
232
|
+
}
|
|
233
|
+
private getProcessedValues(): any[] {
|
|
234
|
+
const columnData = this.processedColumns.slice(1);
|
|
235
|
+
return columnData.map((c, i) => {
|
|
236
|
+
const seriesData = this.processedData.map((d) => d[i + 1]);
|
|
237
|
+
return {
|
|
238
|
+
name: c,
|
|
239
|
+
type: 'bar',
|
|
240
|
+
data: seriesData, // Populated dynamically
|
|
241
|
+
barWidth: 'auto', // Auto-adjust for density
|
|
242
|
+
large: true, // Enable large mode: Canvas rendering for 50k+ efficiency
|
|
243
|
+
largeThreshold: 5000, // Start large mode at 5k points
|
|
244
|
+
progressive: 300, // Render 300 bars per chunk (tune for your hardware)
|
|
245
|
+
progressiveChunkMode: 'sequential', // Sequential rendering for smooth init
|
|
246
|
+
progressiveThreshold: 5000, // Apply progressive above 5k
|
|
247
|
+
animation: true, // Disable initial animation for faster load on large data (re-enable for updates)
|
|
248
|
+
itemStyle: {
|
|
249
|
+
// color: '#5470c6', // Bar color
|
|
250
|
+
// shadowBlur: 0, // Reduce shadows for perf
|
|
251
|
+
},
|
|
252
|
+
// emphasis: {
|
|
253
|
+
// // Hover highlight (lightweight)
|
|
254
|
+
// focus: 'series',
|
|
255
|
+
// itemStyle: { opacity: 0.8 },
|
|
256
|
+
// },
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
private processData(): void {
|
|
261
|
+
let dataToProcess = this.currentData;
|
|
262
|
+
if (this.enableSampling && this.currentData.length > this.sampleThreshold) {
|
|
263
|
+
const step = Math.ceil(this.currentData.length / this.sampleThreshold);
|
|
264
|
+
dataToProcess = this.currentData.filter((_, index) => index % step === 0);
|
|
265
|
+
console.warn(
|
|
266
|
+
`Sampled data from ${this.currentData.length} to ${dataToProcess.length} points.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
this.processedData = dataToProcess; // Cache processed
|
|
270
|
+
this.processedColumns = this.currentColumns;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private processAndUpdateData(): void {
|
|
274
|
+
this.processData();
|
|
275
|
+
if (this.chartInstance) {
|
|
276
|
+
this.updateChartWithData();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private handleResize(): void {
|
|
281
|
+
if (this.chartInstance) {
|
|
282
|
+
this.chartInstance.resize();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private disposeChart(): void {
|
|
287
|
+
if (this.chartInstance) {
|
|
288
|
+
this.chartInstance.dispose();
|
|
289
|
+
this.chartInstance.off('click');
|
|
290
|
+
this.chartInstance = null;
|
|
291
|
+
// console.log('ECharts instance disposed');
|
|
292
|
+
}
|
|
293
|
+
window.removeEventListener('resize', this.handleResize.bind(this));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Public method: Update data externally (e.g., from API)
|
|
297
|
+
updateData(newData: any[][]): void {
|
|
298
|
+
this.currentData = newData;
|
|
299
|
+
this.processAndUpdateData();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div #lineChartContainer style="width: 100%; height: 100%"></div>
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { LineChart } from './line-chart';
|
|
4
|
+
|
|
5
|
+
describe('LineChart', () => {
|
|
6
|
+
let component: LineChart;
|
|
7
|
+
let fixture: ComponentFixture<LineChart>;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await TestBed.configureTestingModule({
|
|
11
|
+
imports: [LineChart]
|
|
12
|
+
})
|
|
13
|
+
.compileComponents();
|
|
14
|
+
|
|
15
|
+
fixture = TestBed.createComponent(LineChart);
|
|
16
|
+
component = fixture.componentInstance;
|
|
17
|
+
fixture.detectChanges();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should create', () => {
|
|
21
|
+
expect(component).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
Input,
|
|
4
|
+
OnInit,
|
|
5
|
+
OnDestroy,
|
|
6
|
+
ChangeDetectionStrategy,
|
|
7
|
+
ChangeDetectorRef,
|
|
8
|
+
ViewChild,
|
|
9
|
+
ElementRef,
|
|
10
|
+
inject,
|
|
11
|
+
effect,
|
|
12
|
+
} from '@angular/core';
|
|
13
|
+
import * as echarts from 'echarts'; // Core ECharts
|
|
14
|
+
import { Subject } from 'rxjs'; // For cleanup
|
|
15
|
+
import { ChartService } from '../../services/chart.service';
|
|
16
|
+
import '../../themes/default';
|
|
17
|
+
import '../../themes/vintage';
|
|
18
|
+
import '../../themes/dark';
|
|
19
|
+
import '../../themes/essos';
|
|
20
|
+
import '../../themes/chalk';
|
|
21
|
+
import '../../themes/roma';
|
|
22
|
+
|
|
23
|
+
@Component({
|
|
24
|
+
selector: 'lib-line-chart',
|
|
25
|
+
imports: [],
|
|
26
|
+
templateUrl: './line-chart.html',
|
|
27
|
+
styleUrl: './line-chart.scss',
|
|
28
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
29
|
+
})
|
|
30
|
+
export class LineChart implements OnInit, OnDestroy {
|
|
31
|
+
@Input() title: string = '';
|
|
32
|
+
@Input() height = '600px';
|
|
33
|
+
@Input() enableSampling = true; // Sample data if > threshold for perf
|
|
34
|
+
@Input() sampleThreshold = 10000; // Sample if data > 10k (keep full for 50k if hardware allows)
|
|
35
|
+
@Input() isLoading: boolean = true;
|
|
36
|
+
|
|
37
|
+
@ViewChild('lineChartContainer', { static: false }) chartContainer!: ElementRef<HTMLDivElement>;
|
|
38
|
+
|
|
39
|
+
chartInstance: echarts.ECharts | null = null;
|
|
40
|
+
private destroy$ = new Subject<void>();
|
|
41
|
+
private chartService = inject(ChartService);
|
|
42
|
+
private currentData: any[][] = [];
|
|
43
|
+
private currentColumns: string[] = [];
|
|
44
|
+
|
|
45
|
+
// ECharts options optimized for 50k+ data
|
|
46
|
+
chartOptions = {
|
|
47
|
+
title: {
|
|
48
|
+
text: this.title,
|
|
49
|
+
left: 'center',
|
|
50
|
+
textStyle: { fontSize: 16 },
|
|
51
|
+
},
|
|
52
|
+
tooltip: {
|
|
53
|
+
// Hover info (efficient for large data)
|
|
54
|
+
trigger: 'axis',
|
|
55
|
+
axisPointer: { type: 'shadow' },
|
|
56
|
+
// formatter: (params = [{ name: '', value: '' }]) => {
|
|
57
|
+
// const param = params[0];
|
|
58
|
+
// return `${param.name}<br/>Value: ${param.value}`;
|
|
59
|
+
// },
|
|
60
|
+
},
|
|
61
|
+
legend: { show: false }, // Disable for single series; add if multi-series
|
|
62
|
+
dataZoom: [
|
|
63
|
+
// Critical for large data: Zoom/slider to navigate 50k data points
|
|
64
|
+
{
|
|
65
|
+
type: 'slider', // Bottom slider
|
|
66
|
+
start: 0,
|
|
67
|
+
end: 100, // Initial view: First 10% (e.g., 5k of 50k)
|
|
68
|
+
height: 25,
|
|
69
|
+
bottom: 20,
|
|
70
|
+
textStyle: { fontSize: 12 },
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: 'inside', // Mouse wheel/pinch zoom
|
|
74
|
+
start: 0,
|
|
75
|
+
end: 10,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
grid: {
|
|
79
|
+
left: '5%',
|
|
80
|
+
right: '5%',
|
|
81
|
+
bottom: '80px', // Space for dataZoom slider
|
|
82
|
+
containLabel: true,
|
|
83
|
+
},
|
|
84
|
+
xAxis: {
|
|
85
|
+
type: 'category',
|
|
86
|
+
data: [], // Populated dynamically
|
|
87
|
+
axisLabel: { rotate: 45, fontSize: 10 },
|
|
88
|
+
axisTick: { show: false },
|
|
89
|
+
},
|
|
90
|
+
yAxis: {
|
|
91
|
+
type: 'value',
|
|
92
|
+
axisLabel: { fontSize: 12 },
|
|
93
|
+
},
|
|
94
|
+
series: [],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
constructor(private cdr: ChangeDetectorRef) {
|
|
98
|
+
// Effect for theme changes
|
|
99
|
+
effect(() => {
|
|
100
|
+
const theme = this.chartService.theme(); // Read signal
|
|
101
|
+
if (this.chartInstance) {
|
|
102
|
+
this.reinitializeChart();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// Effect for data changes
|
|
106
|
+
effect(() => {
|
|
107
|
+
const data = this.chartService.data(); // Read signal
|
|
108
|
+
const columns = this.chartService.columns();
|
|
109
|
+
this.currentColumns = columns;
|
|
110
|
+
this.currentData = data;
|
|
111
|
+
this.processAndUpdateData();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
ngOnInit(): void {
|
|
116
|
+
if (this.currentData.length > 0) {
|
|
117
|
+
this.processAndUpdateData();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
ngAfterViewInit(): void {
|
|
122
|
+
this.initializeChart();
|
|
123
|
+
if (this.currentData.length > 0) {
|
|
124
|
+
this.updateChartWithData();
|
|
125
|
+
}
|
|
126
|
+
this.isLoading = false;
|
|
127
|
+
this.cdr.markForCheck();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
ngOnDestroy(): void {
|
|
131
|
+
this.destroy$.next();
|
|
132
|
+
this.destroy$.complete();
|
|
133
|
+
this.disposeChart();
|
|
134
|
+
if (this.chartInstance) {
|
|
135
|
+
this.chartInstance.dispose(); // Clean up to free memory
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private reinitializeChart(): void {
|
|
140
|
+
this.initializeChart();
|
|
141
|
+
if (this.currentData.length > 0) {
|
|
142
|
+
this.updateChartWithData();
|
|
143
|
+
}
|
|
144
|
+
this.cdr.markForCheck();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private initializeChart(): void {
|
|
148
|
+
if (!this.chartContainer) {
|
|
149
|
+
console.error('Chart container not found!');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const chartDom = this.chartContainer.nativeElement;
|
|
153
|
+
chartDom.innerHTML = '';
|
|
154
|
+
|
|
155
|
+
if (this.chartInstance) {
|
|
156
|
+
this.chartInstance.dispose();
|
|
157
|
+
this.chartInstance = null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.chartInstance = echarts.init(chartDom, this.chartService.theme()); // Init ECharts
|
|
161
|
+
this.chartInstance.setOption(this.chartOptions);
|
|
162
|
+
|
|
163
|
+
// Responsive: Resize on window change
|
|
164
|
+
window.addEventListener('resize', () => this.chartInstance?.resize(), { passive: true });
|
|
165
|
+
|
|
166
|
+
// Handle dataZoom events (optional: Log zoom changes)
|
|
167
|
+
this.chartInstance.on('dataZoom', (params) => {
|
|
168
|
+
// console.log('Zoomed to:', params.start, params.end);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
this.chartInstance.on('rendered', () => {
|
|
172
|
+
// console.log('Chart rendered successfully');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
this.isLoading = false;
|
|
176
|
+
this.cdr.markForCheck();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private updateChartWithData(): void {
|
|
180
|
+
this.processData(); // Process without updating yet
|
|
181
|
+
if (this.chartInstance) {
|
|
182
|
+
const updatedOptions: Partial<echarts.EChartsOption> = {
|
|
183
|
+
xAxis: { data: this.getProcessedCategories() },
|
|
184
|
+
series: this.getProcessedValues(),
|
|
185
|
+
};
|
|
186
|
+
this.chartInstance.setOption(updatedOptions, {
|
|
187
|
+
notMerge: false,
|
|
188
|
+
lazyUpdate: true,
|
|
189
|
+
silent: false,
|
|
190
|
+
replaceMerge: ['series'],
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private processedData: any[][] = [];
|
|
196
|
+
private processedColumns: string[] = [];
|
|
197
|
+
private getProcessedCategories(): string[] {
|
|
198
|
+
return this.processedData.map((d) => d[0]);
|
|
199
|
+
}
|
|
200
|
+
private getProcessedValues(): any[] {
|
|
201
|
+
const columnData = this.processedColumns.slice(1);
|
|
202
|
+
return columnData.map((c, i) => {
|
|
203
|
+
const seriesData = this.processedData.map((d) => d[i + 1]);
|
|
204
|
+
return {
|
|
205
|
+
name: c,
|
|
206
|
+
type: 'line',
|
|
207
|
+
data: seriesData, // Populated dynamically
|
|
208
|
+
large: true, // Enable large mode: Canvas rendering for 50k+ efficiency
|
|
209
|
+
largeThreshold: 5000, // Start large mode at 5k points
|
|
210
|
+
progressive: 300, // Render 300 points per chunk (tune for your hardware)
|
|
211
|
+
progressiveChunkMode: 'sequential', // Sequential rendering for smooth init
|
|
212
|
+
progressiveThreshold: 5000, // Apply progressive above 5k
|
|
213
|
+
animation: true, // Disable initial animation for faster load on large data (re-enable for updates)
|
|
214
|
+
itemStyle: {
|
|
215
|
+
// color: '#5470c6', // line color
|
|
216
|
+
// shadowBlur: 0, // Reduce shadows for perf
|
|
217
|
+
},
|
|
218
|
+
// emphasis: {
|
|
219
|
+
// // Hover highlight (lightweight)
|
|
220
|
+
// focus: 'series',
|
|
221
|
+
// itemStyle: { opacity: 0.8 },
|
|
222
|
+
// },
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
private processData(): void {
|
|
227
|
+
let dataToProcess = this.currentData;
|
|
228
|
+
if (this.enableSampling && this.currentData.length > this.sampleThreshold) {
|
|
229
|
+
const step = Math.ceil(this.currentData.length / this.sampleThreshold);
|
|
230
|
+
dataToProcess = this.currentData.filter((_, index) => index % step === 0);
|
|
231
|
+
console.warn(
|
|
232
|
+
`Sampled data from ${this.currentData.length} to ${dataToProcess.length} points.`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
this.processedData = dataToProcess; // Cache processed
|
|
236
|
+
this.processedColumns = this.currentColumns;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private processAndUpdateData(): void {
|
|
240
|
+
this.processData();
|
|
241
|
+
if (this.chartInstance) {
|
|
242
|
+
this.updateChartWithData();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private handleResize(): void {
|
|
247
|
+
if (this.chartInstance) {
|
|
248
|
+
this.chartInstance.resize();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private disposeChart(): void {
|
|
253
|
+
if (this.chartInstance) {
|
|
254
|
+
this.chartInstance.dispose();
|
|
255
|
+
this.chartInstance = null;
|
|
256
|
+
// console.log('ECharts instance disposed');
|
|
257
|
+
}
|
|
258
|
+
window.removeEventListener('resize', this.handleResize.bind(this));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Public method: Update data externally (e.g., from API)
|
|
262
|
+
updateData(newData: any[][]): void {
|
|
263
|
+
this.currentData = newData;
|
|
264
|
+
this.processAndUpdateData();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { EChartsOption } from 'echarts';
|
|
2
|
+
import {
|
|
3
|
+
Color,
|
|
4
|
+
DataZoomComponentOption,
|
|
5
|
+
GridComponentOption,
|
|
6
|
+
LegendComponentOption,
|
|
7
|
+
SeriesOption,
|
|
8
|
+
TitleComponentOption,
|
|
9
|
+
TooltipComponentOption,
|
|
10
|
+
XAXisComponentOption,
|
|
11
|
+
YAXisComponentOption,
|
|
12
|
+
} from 'echarts';
|
|
13
|
+
|
|
14
|
+
export class ClickEvent {
|
|
15
|
+
seriesName: string = '';
|
|
16
|
+
value: any = undefined;
|
|
17
|
+
name: string = '';
|
|
18
|
+
dataIndex?: number = -1;
|
|
19
|
+
seriesIndex?: number = -1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ChartsLibType {
|
|
23
|
+
chartType: 'line' | 'bar' | 'area' | 'pie' = 'bar';
|
|
24
|
+
columns: string[] = [];
|
|
25
|
+
data: any[][] = [];
|
|
26
|
+
themeName?: 'default' | 'dark' | 'vintage' | 'essos' | 'chalk' | 'roma' = 'default';
|
|
27
|
+
colorsScheme?: string[] | undefined;
|
|
28
|
+
chartOptionsConfig?: OptionsConfig;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class OptionsConfig implements EChartsOption {
|
|
32
|
+
title?: TitleComponentOption = {
|
|
33
|
+
show: true,
|
|
34
|
+
text: '',
|
|
35
|
+
left: 'center',
|
|
36
|
+
textStyle: { fontSize: 16 },
|
|
37
|
+
};
|
|
38
|
+
legend?: LegendComponentOption = { show: true };
|
|
39
|
+
tooltip?: TooltipComponentOption = { trigger: 'axis', axisPointer: { type: 'shadow' } };
|
|
40
|
+
xAxis?: XAXisComponentOption = {
|
|
41
|
+
type: 'category',
|
|
42
|
+
data: [], // Populated dynamically
|
|
43
|
+
axisLabel: { rotate: 45, fontSize: 10 },
|
|
44
|
+
axisTick: { show: false },
|
|
45
|
+
};
|
|
46
|
+
yAxis?: YAXisComponentOption = {
|
|
47
|
+
type: 'value',
|
|
48
|
+
axisLabel: { fontSize: 12 },
|
|
49
|
+
};
|
|
50
|
+
dataZoom?: DataZoomComponentOption[] = [
|
|
51
|
+
{
|
|
52
|
+
type: 'slider',
|
|
53
|
+
start: 0,
|
|
54
|
+
end: 100,
|
|
55
|
+
height: 25,
|
|
56
|
+
bottom: 20,
|
|
57
|
+
textStyle: { fontSize: 12 },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: 'inside', // Mouse wheel/pinch zoom
|
|
61
|
+
start: 0,
|
|
62
|
+
end: 10,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
series?: SeriesOption[] = [];
|
|
66
|
+
grid?: GridComponentOption = {
|
|
67
|
+
left: '5%',
|
|
68
|
+
right: '5%',
|
|
69
|
+
containLabel: true,
|
|
70
|
+
};
|
|
71
|
+
[key: string]: any;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class ContextMenuListItem {
|
|
75
|
+
label: string = '';
|
|
76
|
+
options?: ContextMenuListItem[] = [];
|
|
77
|
+
action?: (event?: ClickEvent) => void;
|
|
78
|
+
disabled?: boolean = false;
|
|
79
|
+
}
|