Explorar el Código

Custom dashboard prototype

A-Konig hace 1 semana
padre
commit
1a9724d18c

+ 10 - 0
src/app/dashboard/components/custom-dashboard/custom-dashboard.component.html

@@ -1 +1,11 @@
 <p>Currently under construction!</p>
+
+<div class="dashboard-container" *ngIf="config">
+    <h1>{{ config.configName }}</h1>
+
+    <div *ngFor="let graph of config.graphs" class="graph-card">
+        <h3>{{ graph.title }}</h3>
+        <!-- Unique ID for each graph -->
+        <div [id]="'graph-' + graph.graphId" class="vega-container">  </div>
+    </div>
+</div>

+ 141 - 36
src/app/dashboard/components/custom-dashboard/custom-dashboard.component.ts

@@ -1,4 +1,4 @@
-import {Component, Input, OnChanges, SimpleChanges} from '@angular/core';
+import {Component, HostListener, Input, OnChanges, SimpleChanges} from '@angular/core';
 import {User} from '../../../auth/models/user';
 import {Drivers} from '../../../shared/api/endpoints/models/drivers';
 import {GeneralInfo} from '../../../shared/api/endpoints/models/general-info';
@@ -12,7 +12,13 @@ import {ManagementService} from '../../../shared/api/endpoints/services/manageme
 import {ToastService} from '../../../shared/services/toast.service';
 import {AuthService} from '../../../auth/services/auth.service';
 import {Config} from '../../../shared/api/endpoints/models/config';
-import {HttpClient} from '@angular/common/http';
+import {HttpClient, HttpResponse} from '@angular/common/http';
+import {GraphLoader} from '../../../shared/graph-loading/graphloader';
+import {ObservationService} from '../../../shared/api/endpoints/services/observation.service';
+import {catchError, map} from 'rxjs/operators';
+import {forkJoin, of} from 'rxjs';
+import * as moment from 'moment-timezone';
+import type {View} from 'vega';
 
 @Component({
   selector: 'app-custom-dashboard',
@@ -25,8 +31,11 @@ export class CustomDashboardComponent implements OnChanges {
       Array<{ drivers?: Drivers; generalInfo?: GeneralInfo; holder?: any; lastpos?: Lastpos; sensors?: Array<Sensor>; unit?: Unit }>;
 
   cstmUnits: Array<{ drivers?: Drivers; generalInfo?: GeneralInfo; holder?: any; lastpos?: Lastpos; sensors?: Array<Sensor>; unit?: Unit }>;
+  config : Config;
+  graphViews: Record<number, View> = {};
 
   constructor(
+      private observationService: ObservationService,
       private dataService: DataService,
       private sensorService: SensorsService,
       private confirmationService: ConfirmationService,
@@ -41,55 +50,151 @@ export class CustomDashboardComponent implements OnChanges {
   ngOnChanges(changes: SimpleChanges): void {
     if (changes.units && changes.units.currentValue) {
       console.log('New units data arrived in child.');
-      // Once data is here, process
       this.createGraphs();
     }
   }
 
-  /**
-   * Get all units and theirs sensors from backend
-   */
-  createGraphs() {
-    this.http.get<Config>('assets/example.json').subscribe(config => {
+  async loadAllGraphs(range: Date[]) {
+    for (const graph of this.config.graphs) {
+      await this.loadSingleGraph(graph, range);
+    }
+  }
 
-      // 1. Store and sort the raw master data
-      this.units.forEach(u => u.sensors?.sort((a, b) => (a.sensorId ?? 0) - (b.sensorId ?? 0)));
+  private async loadSingleGraph(graph: any, range: Date[]) {
+    const containerId = `#graph-${graph.graphId}`;
+    const element = document.querySelector(containerId) as HTMLElement;
 
-      // 2. Extract configuration selections
-      const {units: unitSelections, globalSensors} = config.preferences.selections;
+    if (!element) return;
 
-      // 3. Perform the filtering logic
-      this.cstmUnits = this.units.map(masterUnit => {
+    // 2. Setup ResizeObserver to wait for a valid width
+    const resizeObserver = new ResizeObserver((entries) => {
+      for (const entry of entries) {
+        const width = entry.contentRect.width;
 
-        // Find specific config for this unit
-        const selection = unitSelections.find(u => u.unitId === masterUnit.unit?.unitId);
+        // Only proceed if width is greater than 0 (Tab is now visible)
+        if (width > 0) {
+          console.log(`Tab activated. Valid width detected for ${containerId}: ${width}px`);
 
-        const filteredSensors = (masterUnit.sensors || []).filter(sensor => {
-          const sId = sensor.sensorId;
-          if (sId === undefined) return false;
+          // Set the size
+          GraphLoader.setSize(width - 50, 300);
 
-          const isGlobal = globalSensors.includes(sId);
-          let isLocallySelected = false;
+          // Fetch data and draw
+          this.fetchDataAndDraw(graph, range, containerId);
 
-          if (selection) {
-            isLocallySelected = selection.sensors === 'ALL' ||
-                (Array.isArray(selection.sensors) && selection.sensors.includes(sId));
-          }
+          // Stop observing once we have successfully rendered
+          resizeObserver.disconnect();
+        }
+      }
+    });
 
-          return isGlobal || isLocallySelected;
-        });
+    resizeObserver.observe(element);
+  }
 
-        // Return the unit structure with the new filtered sensor array
-        return {
-          ...masterUnit,
-          sensors: filteredSensors
-        };
-      })
-      // 4. Final step: Only keep units that have at least one valid sensor
-      .filter(u => u.sensors.length > 0);
+  /**
+   * Helper method to handle the actual data request and graph rendering
+   */
+  private fetchDataAndDraw(graph: any, range: Date[], containerId: string) {
+    const groupId = graph.graphId;
+    const sensorIds = [];
+    const sensors = [];
+    const requests = []; // Array to hold all observable requests
+
+    this.graphViews[groupId] = null;
+    console.log(groupId);
+
+    for (const source of graph.sources) {
+      // 1. Find the sensor metadata
+      const unit = this.units.find(u => u.unit.unitId === graph.sources[0].unitId);
+      const sensor = unit?.sensors?.find(s => s.sensorId === graph.sources[0].sensorIds[0]);
+
+      if (!sensor) {
+        console.error(`Metadata for sensor ${graph.sources[0].sensorIds[0]} not found!`);
+        return;
+      }
+
+      const unitId = unit.unit.unitId;
+      const sensorId = sensor.sensorId;
+
+      sensorIds.push(sensorId);
+      sensors.push(sensor);
+
+      // 4. Create the request for this specific source/sensor
+      const request = this.observationService.getObservation$Response({
+          unit_id: unitId,
+          sensor_id: sensorId,
+          from: moment(range[0]).format('YYYY-MM-DD HH:mm:ss'),
+          to: moment(range[1]).format('YYYY-MM-DD HH:mm:ss')
+        }).pipe(
+            map(response => {
+              if (response.status === 200) return response.body;
+              if (response.status === 204) this.toastService.showWarningNoData();
+              return null;
+            }),
+            catchError(err => {
+              this.toastService.showError(err.error?.message || 'Error loading data');
+              return of(null);
+            })
+      );
+
+      requests.push(request);
+    }
 
-      console.log('Filtered cstmUnits:', this.cstmUnits);
+    // Execute all requests and render
+    if (requests.length > 0) {
+      forkJoin(requests).subscribe(async (results: any[][]) => {
+        const allObservations = results.map(res => res ? res : []);
+
+        // TODO here i want to load a custom graph with multiple datasets
+        //  - each dataset could have different type (line or bar)
+        //  - sensorIds, sensors - information about sensors
+        //  - allObservations - the recieved data that will be displayed
+
+        if (allObservations.length > 0) {
+          this.graphViews[groupId] = await GraphLoader.getGraph(sensorIds, allObservations, sensors, containerId, false);
+        } else {
+          this.graphViews[groupId] = await GraphLoader.getGraph(null, null, null, containerId, null);
+        }
+      });
+    }
+  }
+
+  /**
+   * Get all units and theirs sensors from backend
+   */
+  createGraphs() {
+    this.http.get<Config>('assets/example.json').subscribe(config => {
+      console.log(config);
+      this.config = config;
+
+      // Once data is here, process
+      setTimeout(() => {
+        const range = [moment().subtract(1, 'day').toDate(), moment().toDate()];
+        this.loadAllGraphs(range);
+      }, 0);
     }, err => this.toastService.showError(err.error.message));
   }
 
+  /**
+   * Fires on every resize event
+   */
+  @HostListener('window:resize', ['$event'])
+  public onWindowResize(event: UIEvent): void {
+    this.onResize();
+  }
+
+  private onResize() {
+    const itemCount = Object.keys(this.graphViews).length;
+    // console.log('Number of entries:', itemCount);
+
+    Object.entries(this.graphViews).forEach(([key, view]) => {
+      if (view) {
+        console.log('#graph-' + key);
+        const containerId = `#graph-${key}`;
+        const box = document.querySelector(containerId) as HTMLElement;
+        const newWidth = box.getBoundingClientRect().width - 50;
+        GraphLoader.ResizeGraph(view, newWidth);
+      }
+    })
   }
+
+}

+ 2 - 0
src/app/dashboard/components/dashboard.component.ts

@@ -101,6 +101,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
     this.dataService.getData().subscribe(data => {
       this.units = data;
       this.units.forEach(unit => unit.sensors.sort((a, b) => a.sensorId - b.sensorId));
+
+      console.log(data);
     }, err => this.toastService.showError(err.error.message));
   }
 

+ 7 - 6
src/app/shared/api/endpoints/models/config.ts

@@ -1,8 +1,9 @@
 export interface Config {
-    preferences: {
-        selections: {
-            units: { unitId: number; sensors: number[] | 'ALL' }[];
-            globalSensors: number[];
-        };
-    };
+    configName : string;
+    graphs: {
+        graphId : number;
+        title : string;
+        sources : { unitId: number; sensors : number[]; renderType : 'line' | 'bar'; } [];
+
+    } [];
 }

+ 19 - 18
src/app/shared/graph-loading/multigraph.ts

@@ -1,4 +1,4 @@
-import {Graph} from "./graph";
+import {Graph} from './graph';
 
 declare var require: any
 
@@ -18,7 +18,8 @@ export class MultiGraph implements Graph{
    * @param isAnalytics true/false analytics/observations
    * @param data source of values for vega graph
    * @param legend source of legend for graph (e.g. sensors phenomenon)
-   * @param interval default graph interval used for different purposes (in milliseconds) (see timeWindow and maxTimeDifference in vega specification)
+   * @param interval default graph interval used for different purposes (in milliseconds)
+   * (see timeWindow and maxTimeDifference in vega specification)
    */
   constructor(isAnalytics: boolean, data: any [], legend: {}, interval: number) {
     this.isAnalytics = isAnalytics;
@@ -32,11 +33,11 @@ export class MultiGraph implements Graph{
    * Returns vega configuration (see readme.txt in vega folder)
    */
   getConfig(): {} {
-    //from folder vega/config merge corresponding files
+    // from folder vega/config merge corresponding files
 
     const lodash = require('lodash/object');
     const rvalue: any = {};
-    let config1 = require('/src/vega/config/config.json');
+    const config1 = require('/src/vega/config/config.json');
     lodash.merge(rvalue, config1);
 
     let config2;
@@ -46,16 +47,15 @@ export class MultiGraph implements Graph{
       config2 = require('/src/vega/config/config-multiline-observations.json');
     }
 
-    let config3 = require('/src/vega/config/config-barline.json');
-
+    const config3 = require('/src/vega/config/config-barline.json');
 
     for(let key in config2.signals) {
-      let obj = config2.signals[key];
+      const obj = config2.signals[key];
       rvalue.signals.push(obj);
     }
 
     for(let key in config3.signals) {
-      let obj = config3.signals[key];
+      const obj = config3.signals[key];
       rvalue.signals.push(obj);
     }
 
@@ -76,21 +76,21 @@ export class MultiGraph implements Graph{
 
     lodash.merge(rvalue, base, body, legend, tooltip);
 
-    //setting legend to result
+    // setting legend to result
     rvalue.data[0].values = this.legend;
 
-    //setting data to result
+    // setting data to result
     rvalue.data[1].values = this.data;
 
-    //setting interval to json
-    //interval used for
+    // setting interval to json
+    // interval used for
     // 1) appending graph domain
     // 2) 5 * interval is max difference for connecting the dots
     // 3) setting zoom in windcharts
     rvalue.signals[0].value = this.interval;
 
-    //setting tooltip message
-    rvalue.marks[0].marks[0].marks[0].marks[2].encode.enter.tooltip.signal = this.getTooltipMessage();
+    // setting tooltip message
+    // rvalue.marks[0].marks[0].marks[0].marks[2].encode.enter.tooltip.signal = this.getTooltipMessage();
 
     return rvalue;
   }
@@ -99,12 +99,13 @@ export class MultiGraph implements Graph{
    * Generates the tooltip message
    */
   private getTooltipMessage() {
-    let message = "{title: timeFormat(datum.dateTime, '%A, %B %e, %Y %X')";
-    this.data.forEach(function (e) {
-      message += (", '" + e.sensor.sensorName + "': datum['" + e.sensor.sensorName + "'] + ' " + e.sensor.phenomenon.unit +"' ");
+    let message = '{title: timeFormat(datum.dateTime, \'%A, %B %e, %Y %X\')';
+    this.data.forEach(e => {
+      console.log(e);
+      message += (', \'' + e.sensor.sensorName + '\': datum[\'' + e.sensor.sensorName + '\'] + \' ' + e.sensor.phenomenon.unit +'\' ');
     })
 
-    message += "}";
+    message += '}';
     return message;
   }
 

+ 25 - 12
src/assets/example.json

@@ -1,19 +1,32 @@
 {
-  "userId": "zcu",
-  "preferences": {
-    "displayMode": "dashboard",
-    "selections": {
-      "units": [
+  "configName": "Testing dashboard",
+  "graphs": [
+    {
+      "graphId": 1,
+      "title": "Temperature",
+      "sources": [
         {
-          "unitId": 1305167562258386,
-          "sensors": "ALL"
+          "unitId": 1305167562011818,
+          "sensorIds": [340240003],
+          "renderType": "line"
         },
         {
-          "unitId": 1305167562287501,
-          "sensors": [460080000]
+          "unitId": 1305167562033952,
+          "sensorIds": [340240003],
+          "renderType": "line"
         }
-      ],
-      "globalSensors": [480080000]
+      ]
+    },
+    {
+      "graphId": 2,
+      "title": "Precipitation",
+      "sources": [
+        {
+          "unitId": 1305167562011818,
+          "sensorIds": [360200000],
+          "renderType": "bar"
+        }
+      ]
     }
-  }
+  ]
 }