Bladeren bron

Angular: simplified Chart Component; implemented dragging & zooming (WIP)

Christian Kahlau 3 jaren geleden
bovenliggende
commit
645fea4e78

+ 7 - 7
ng/src/app/components/server-data-chart/server-data-chart.component.html

@@ -1,16 +1,16 @@
-<div class="card mb-2">
+<div class="card mb-2" (mousewheel)="zoom($event)" (dragstart)="dragStart($event)" (drag)="drag($event)" (dragend)="dragEnd($event)" draggable="true">
   <div class="card-header d-flex flex-row">
     <span class="flex-fill">
-      <fa-icon [icon]="parent ?? type.type | faType" class="pe-2"></fa-icon>
-      <span class="text-uppercase">{{ parent ?? type.type }}</span>
-      <ng-container *ngIf="parent"> : {{ type.type }} </ng-container>
+      <fa-icon [icon]="parent ?? type | faType" class="pe-2"></fa-icon>
+      <span class="text-uppercase">{{ parent ?? type }}</span>
+      <ng-container *ngIf="parent"> : {{ type }} </ng-container>
     </span>
     <span>
-      <span class="badge bd-blue-100">{{ type.start | date: 'YYYY-MM-dd HH:mm:ss' }}</span> -
-      <span class="badge bd-blue-100">{{ type.end | date: 'YYYY-MM-dd HH:mm:ss' }}</span>
+      <span class="badge bd-blue-100">{{ start | date: 'YYYY-MM-dd HH:mm:ss' }}</span> -
+      <span class="badge bd-blue-100">{{ end | date: 'YYYY-MM-dd HH:mm:ss' }}</span>
     </span>
   </div>
   <div class="card-body">
-    <canvas baseChart type="line" [data]="type.data" [options]="type.options"></canvas>
+    <canvas baseChart type="line" [data]="data" [options]="options"></canvas>
   </div>
 </div>

+ 131 - 39
ng/src/app/components/server-data-chart/server-data-chart.component.ts

@@ -1,18 +1,11 @@
 import { Component, Input, OnInit } from '@angular/core';
 import { ChartData, ChartOptions, TooltipItem } from 'chart.js';
+import { debounceTime, throttleTime, Subject } from 'rxjs';
 
 import { BytePipe } from 'src/app/pipes/byte.pipe';
 import { ServerApiService } from 'src/app/services/server-api.service';
-import { deepCopy } from '../../../../../common/util/object-utils';
 
 type ChartConfigTimeUnit = false | 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' | undefined;
-type ServerDataGraphOptions = ServerDataTypesConfig & {
-  subtypes?: Array<ServerDataGraphOptions>;
-  data?: ChartData<'line', ServerData[], Date>;
-  options: ChartOptions<'line'>;
-  start: Date;
-  end: Date;
-};
 
 const typeDotColors: { [k: string]: string } = {
   avg: '#cc9a06',
@@ -32,40 +25,42 @@ const typeBgColors: { [k: string]: string } = {
   styleUrls: ['./server-data-chart.component.scss']
 })
 export class ServerDataChartComponent implements OnInit {
-  private _type!: ServerDataGraphOptions;
+  private zoomEvent$ = new Subject<void>();
+  private dragEvent$ = new Subject<DragEvent>();
+  private _type!: string;
 
   @Input() parent?: string;
   @Input() server!: ServerConfig;
-  @Input() set type(type: ServerDataTypesConfig) {
-    if (!this._type) {
-      const end = new Date();
-      const start = new Date(end.getTime() - 1000 * 60 * 60 * 4);
-      this._type = deepCopy({
-        ...type,
-        end,
-        start
-      }) as ServerDataGraphOptions;
-    } else {
-      this._type.type = type.type;
-      this._type.subtypes = type.subtypes as ServerDataGraphOptions[];
-    }
+  @Input() set type(type: string) {
+    this._type = type;
   }
 
-  public get type(): ServerDataGraphOptions {
+  public get type(): string {
     return this._type;
   }
 
-  constructor(private apiService: ServerApiService, private bytePipe: BytePipe) {}
+  public start!: Date;
+  public end!: Date;
+  public data?: ChartData<'line', ServerData[], Date>;
+  public options!: ChartOptions<'line'> & any;
+
+  constructor(private apiService: ServerApiService, private bytePipe: BytePipe) {
+    this.defaults();
+    this.zoomEvent$.pipe(debounceTime(400)).subscribe({ next: this.updateData.bind(this) });
+    this.dragEvent$.pipe(throttleTime(400)).subscribe({ next: this.dragDebounced.bind(this) });
+  }
 
   ngOnInit(): void {
     this.updateData();
   }
 
-  private options(type: ServerDataGraphOptions): ChartOptions<'line'> & any {
-    const end = type.end;
-    const start = type.start;
+  private defaults() {
+    this.end = new Date();
+    this.start = new Date(this.end.getTime() - 1000 * 60 * 60 * 4);
+  }
 
-    const diffHrs = (end.getTime() - start.getTime()) / 1000 / 60 / 60;
+  private updateOptions() {
+    const diffHrs = (this.end.getTime() - this.start.getTime()) / 1000 / 60 / 60;
     const timeFormat: {
       tooltipFormat: string;
       unit: ChartConfigTimeUnit;
@@ -94,7 +89,7 @@ export class ServerDataChartComponent implements OnInit {
             unit: 'minute',
             stepSize: 30
           };
-    return {
+    this.options = {
       scales: {
         xAxis: {
           display: true,
@@ -106,13 +101,15 @@ export class ServerDataChartComponent implements OnInit {
               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' && type.type !== 'cpu' ? this.bytePipe.transform(val) : `${val} %`)
+            callback: (val: string | number) => (typeof val === 'number' && this.type !== 'cpu' ? this.bytePipe.transform(val) : `${val} %`)
           }
         }
       }
@@ -121,13 +118,11 @@ export class ServerDataChartComponent implements OnInit {
 
   private async updateData() {
     try {
-      const end = this.type.end;
-      const start = this.type.start;
       const data = await this.apiService.queryServerData(
         this.server.id,
-        `${this.parent ?? this.type.type}${this.parent ? `:${this.type.type}` : ''}`,
-        start,
-        end
+        `${this.parent ?? this.type}${this.parent ? `:${this.type}` : ''}`,
+        this.start,
+        this.end
       );
       const chartData: ChartData<'line', ServerData[], Date> = {
         labels: data.map(d => d.time),
@@ -148,7 +143,7 @@ export class ServerDataChartComponent implements OnInit {
                 tooltip: {
                   callbacks: {
                     label:
-                      this.type.type !== 'cpu'
+                      this.type !== 'cpu'
                         ? (item: TooltipItem<'line'> & any) => this.bytePipe.transform(item.raw[key])
                         : (item: TooltipItem<'line'> & any) => `${(item.raw[key] as number).toFixed(2)} %`
                   }
@@ -157,10 +152,107 @@ export class ServerDataChartComponent implements OnInit {
           : []
       };
 
-      this.type.options = this.options(this.type);
-      this.type.data = chartData;
+      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.updateOptions();
+      this.zoomEvent$.next();
+    }
+  }
+
+  private dragStartParams = {
+    offsetX: 0,
+    rangeStart: 0,
+    rangeEnd: 0
+  };
+
+  dragStart(ev: Event) {
+    const event = ev as DragEvent;
+
+    console.log('[DRAGSTART]', {
+      offsetX: event.offsetX,
+      clientWidth: (event.target as HTMLCanvasElement).clientWidth,
+      xRel: ((event.offsetX / (event.target as HTMLCanvasElement).clientWidth) * 100).toFixed(2)
+    });
+    this.dragStartParams = {
+      offsetX: event.offsetX,
+      rangeStart: this.start.getTime(),
+      rangeEnd: this.end.getTime()
+    };
+
+    event.stopPropagation();
+    // event.preventDefault();
+    event.cancelBubble = true;
+  }
+
+  drag(ev: Event) {
+    const event = ev as DragEvent;
+    event.preventDefault();
+    event.stopPropagation();
+    event.cancelBubble = true;
+
+    console.log('[DRAG]');
+    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);
+
+      console.log('[DRAGDEBOUNCE]', {
+        offsetX: event.offsetX,
+        clientWidth: (event.target as HTMLCanvasElement).clientWidth,
+        xDiffRel,
+        newStart: this.start,
+        newEnd: this.end
+      });
+
+      this.updateOptions();
+    }
+  }
+
+  dragEnd(ev: Event) {
+    const event = ev as DragEvent;
+
+    console.log('[DRAGEND]', {
+      offsetX: event.offsetX,
+      clientWidth: (event.target as HTMLCanvasElement).clientWidth,
+      xRel: (((event.offsetX - this.dragStartParams.offsetX) / (event.target as HTMLCanvasElement).clientWidth) * 100).toFixed(2)
+    });
+
+    this.updateOptions();
+    this.zoomEvent$.next();
+  }
 }

+ 2 - 2
ng/src/app/pages/server-data-page/server-data-page.component.html

@@ -4,10 +4,10 @@
   <ng-container *ngIf="types?.length">
     <ng-container *ngFor="let type of types">
       <ng-container *ngIf="type.subtypes?.length; else singleDataType">
-        <app-server-data-chart *ngFor="let sub of type.subtypes" [parent]="type.type" [type]="sub" [server]="server"></app-server-data-chart>
+        <app-server-data-chart *ngFor="let sub of type.subtypes" [parent]="type.type" [type]="sub.type" [server]="server"></app-server-data-chart>
       </ng-container>
       <ng-template #singleDataType>
-        <app-server-data-chart [type]="type" [server]="server"></app-server-data-chart>
+        <app-server-data-chart [type]="type.type" [server]="server"></app-server-data-chart>
       </ng-template>
     </ng-container>
   </ng-container>