| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- import { Component, ElementRef, HostListener, Input, OnInit, ViewChild } from '@angular/core';
- import { faLocationDot, faEllipsis, faMagnifyingGlassPlus } from '@fortawesome/free-solid-svg-icons';
- import { ChartData, ChartOptions, TooltipItem } from 'chart.js';
- import * as dateFns from 'date-fns';
- import { debounceTime, throttleTime, Subject } from 'rxjs';
- import { BytePipe } from 'src/app/pipes/byte.pipe';
- import { ServerApiService } from 'src/app/services/server-api.service';
- type ChartConfigTimeUnit = false | 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' | undefined;
- const typeDotColors: { [k: string]: string } = {
- avg: '#cc9a06',
- peak: '#005299',
- max: '#941320'
- };
- const typeBgColors: { [k: string]: string } = {
- avg: '#ffe69c',
- peak: '#cce7ff',
- max: '#e3a3a9'
- };
- @Component({
- selector: 'app-server-data-chart',
- templateUrl: './server-data-chart.component.html',
- styleUrls: ['./server-data-chart.component.scss']
- })
- export class ServerDataChartComponent implements OnInit {
- private zoomEvent$ = new Subject<void>();
- private dragEvent$ = new Subject<DragEvent>();
- @ViewChild('dragImage') dragImage!: ElementRef<HTMLImageElement>;
- @Input() parent?: string;
- @Input() server!: ServerConfig;
- @Input() type!: string;
- public start!: Date;
- public end!: Date;
- public data?: ChartData<'line', ServerData[], Date>;
- public options!: ChartOptions<'line'> & any;
- public fa = { locationDot: faLocationDot, ellipsis: faEllipsis, zoomPlus: faMagnifyingGlassPlus };
- public zoomRanges = ['3M', '1M', '2W', '4D', '1D', '12H', '4H', '1H'];
- public selectedZoomRange = '4H';
- constructor(private apiService: ServerApiService, private bytePipe: BytePipe) {
- this.defaults();
- this.zoomEvent$.pipe(debounceTime(400)).subscribe({ next: this.updateData.bind(this) });
- this.dragEvent$.pipe(throttleTime(80)).subscribe({ next: this.dragDebounced.bind(this) });
- }
- ngOnInit(): void {
- this.updateData();
- }
- private defaults() {
- this.end = new Date();
- this.start = new Date(this.end.getTime() - 1000 * 60 * 60 * 4);
- }
- private updateOptions() {
- const diffHrs = (this.end.getTime() - this.start.getTime()) / 1000 / 60 / 60;
- const timeFormat: {
- tooltipFormat: string;
- unit: ChartConfigTimeUnit;
- stepSize: number;
- } =
- diffHrs > 24 * 4
- ? {
- tooltipFormat: 'yyyy-MM-dd HH:mm',
- unit: 'day',
- stepSize: 1
- }
- : diffHrs > 24 * 2
- ? {
- tooltipFormat: 'yyyy-MM-dd HH:mm',
- unit: 'hour',
- stepSize: 6
- }
- : diffHrs > 12
- ? {
- tooltipFormat: 'HH:mm:ss',
- unit: 'hour',
- stepSize: 1
- }
- : {
- tooltipFormat: 'HH:mm:ss',
- unit: 'minute',
- stepSize: 30
- };
- this.options = {
- scales: {
- xAxis: {
- display: true,
- type: 'time',
- time: {
- ...timeFormat,
- displayFormats: {
- day: 'yyyy-MM-dd',
- hour: 'yyyy-MM-dd HH:mm',
- minute: 'HH:mm:ss'
- }
- },
- min: this.start,
- max: this.end
- },
- yAxis: {
- beginAtZero: true,
- type: 'linear',
- ticks: {
- callback: (val: string | number) => (typeof val === 'number' && this.type !== 'cpu' ? this.bytePipe.transform(val) : `${val} %`)
- },
- max: this.type === 'cpu' ? 100 : undefined
- }
- }
- };
- }
- private async updateData() {
- try {
- const data = await this.apiService.queryServerData(
- this.server.id,
- `${this.parent ?? this.type}${this.parent ? `:${this.type}` : ''}`,
- this.start,
- this.end
- );
- const chartData: ChartData<'line', ServerData[], Date> = {
- labels: data.map(d => d.time),
- datasets: data.length
- ? Object.keys(data[0])
- .filter(k => k !== 'time')
- .map(key => ({
- label: key,
- data,
- parsing: { yAxisKey: key, xAxisKey: 'time' },
- fill: key !== 'max',
- tension: 0.3,
- backgroundColor: typeBgColors[key],
- pointBorderColor: typeDotColors[key],
- borderColor: typeDotColors[key],
- pointBackgroundColor: typeBgColors[key],
- pointRadius: key === 'max' ? 0 : undefined,
- tooltip: {
- callbacks: {
- label:
- this.type !== 'cpu'
- ? (item: TooltipItem<'line'> & any) => this.bytePipe.transform(item.raw[key])
- : (item: TooltipItem<'line'> & any) => `${(item.raw[key] as number).toFixed(2)} %`
- }
- }
- }))
- : []
- };
- this.updateOptions();
- this.data = chartData;
- } catch (err) {
- console.error(err);
- }
- }
- zoom(ev: Event) {
- const event = ev as WheelEvent;
- if (event.shiftKey) {
- event.stopPropagation();
- event.preventDefault();
- event.cancelBubble = true;
- const xRel = event.offsetX / (event.target as HTMLCanvasElement).clientWidth;
- const nowMs = new Date().getTime();
- const currentRangeMs = this.end.getTime() - this.start.getTime();
- const cursorTimeMs = this.start.getTime() + currentRangeMs * xRel;
- const step = event.deltaY / 10;
- const newRangeMs = currentRangeMs * (1 + 1 / step);
- const newStartMs = Math.min(cursorTimeMs - newRangeMs * xRel, nowMs - newRangeMs);
- const newEndMs = newStartMs + newRangeMs;
- this.start = new Date(newStartMs);
- this.end = new Date(newEndMs);
- this.selectedZoomRange = '';
- this.updateOptions();
- this.zoomEvent$.next();
- }
- }
- private dragStartParams = {
- offsetX: 0,
- rangeStart: 0,
- rangeEnd: 0
- };
- public zoomMode = false;
- @HostListener('window:keydown', ['$event'])
- hotKeydown(event: KeyboardEvent) {
- if (event.shiftKey) this.zoomMode = true;
- }
- @HostListener('window:keyup', ['$event'])
- hotKeyup(event: KeyboardEvent) {
- if (event.key.toLowerCase() === 'shift') this.zoomMode = false;
- }
- dragStart(ev: Event) {
- const event = ev as DragEvent;
- this.dragStartParams = {
- offsetX: event.offsetX,
- rangeStart: this.start.getTime(),
- rangeEnd: this.end.getTime()
- };
- if (event.dataTransfer) {
- event.dataTransfer.setDragImage(this.dragImage.nativeElement, -10, -10);
- event.dataTransfer.effectAllowed = 'all';
- }
- (event.target as HTMLCanvasElement).style.cursor = 'grabbing';
- event.stopPropagation();
- // event.preventDefault();
- event.cancelBubble = true;
- }
- drag(ev: Event) {
- const event = ev as DragEvent;
- event.preventDefault();
- event.stopPropagation();
- event.cancelBubble = true;
- this.dragEvent$.next(event);
- }
- dragDebounced(event: DragEvent) {
- const nowMs = new Date().getTime();
- const xDiffRel = (event.offsetX - this.dragStartParams.offsetX) / (event.target as HTMLCanvasElement).clientWidth;
- const currentRangeMs = this.dragStartParams.rangeEnd - this.dragStartParams.rangeStart;
- const xDiff = xDiffRel * currentRangeMs;
- const newStartMs = Math.min(this.dragStartParams.rangeStart - xDiff, nowMs - currentRangeMs);
- const newEndMs = newStartMs + currentRangeMs;
- if (this.start.getTime() !== newStartMs) {
- this.start = new Date(newStartMs);
- this.end = new Date(newEndMs);
- this.updateOptions();
- }
- }
- dragEnd(ev: Event) {
- const event = ev as DragEvent;
- (event.target as HTMLCanvasElement).style.removeProperty('cursor');
- this.updateOptions();
- this.zoomEvent$.next();
- }
- zoomButton(zoomRange: string) {
- this.selectedZoomRange = zoomRange;
- const m = /^(\d+)(\w)$/.exec(zoomRange);
- if (!m) return;
- switch (m[2]) {
- case 'H':
- this.start = dateFns.addHours(this.end, -1 * Number(m[1]));
- break;
- case 'D':
- this.start = dateFns.addDays(this.end, -1 * Number(m[1]));
- break;
- case 'W':
- this.start = dateFns.addWeeks(this.end, -1 * Number(m[1]));
- break;
- case 'M':
- this.start = dateFns.addMonths(this.end, -1 * Number(m[1]));
- break;
- }
- this.zoomEvent$.next();
- }
- }
|