File

libs/depiction/src/lib/dataset-table/dataset-table.component.ts

Extends

DatasetPresenterComponent

Implements

OnInit

Metadata

selector n52-dataset-table
styleUrls ./dataset-table.component.scss
templateUrl ./dataset-table.component.html

Index

Properties
Methods
Inputs
Outputs
HostListeners

Constructor

constructor(iterableDiffers: IterableDiffers, servicesConnector: HelgolandServicesConnector, datasetIdResolver: InternalIdHandler, timeSrvc: Time, translateSrvc: TranslateService, timezoneSrvc: TimezoneService)
Parameters :
Name Type Optional
iterableDiffers IterableDiffers No
servicesConnector HelgolandServicesConnector No
datasetIdResolver InternalIdHandler No
timeSrvc Time No
translateSrvc TranslateService No
timezoneSrvc TimezoneService No

Inputs

datasetIds
Type : string[]
Default value : []

List of presented dataset ids.

datasetOptions
Type : Map<string | T>

The corresponding dataset options.

presenterOptions
Type : U

Options for general presentation of the data.

reloadForDatasets
Type : string[]

List of datasets for which a reload should be triggered, when the Array is set to new value.

selectedDatasetIds
Type : string[]
Default value : []

List of presented selected dataset ids.

timeInterval
Type : TimeInterval

The time interval in which the data should presented.

Outputs

dataLoaded
Type : EventEmitter<Set<string>>

Event, which triggers list of datasets where data is currently loaded.

onContentLoading
Type : EventEmitter<boolean>

Event flag, while there is data loaded in the component.

onDatasetSelected
Type : EventEmitter<string[]>

Event with a list of selected datasets.

onMessageThrown
Type : EventEmitter<PresenterMessage>

Event, when there occured a message in the component.

onTimespanChanged
Type : EventEmitter<Timespan>

Event when the timespan in the presentation is adjusted.

HostListeners

window:resize
Arguments : '$event'
window:resize(event: Event)
Inherited from ResizableComponent

Methods

Protected addDataset
addDataset(id: string, url: string)
Parameters :
Name Type Optional
id string No
url string No
Returns : void
Private addTimeseries
addTimeseries(timeseries: HelgolandTimeseries)
Parameters :
Name Type Optional
timeseries HelgolandTimeseries No
Returns : void
Protected datasetOptionsChanged
datasetOptionsChanged(internalId: string, options: DatasetOptions)
Parameters :
Name Type Optional
internalId string No
options DatasetOptions No
Returns : void
Protected getIndexFromInternalId
getIndexFromInternalId(internalId: string)
Parameters :
Name Type Optional
internalId string No
Returns : any
Private loadTsData
loadTsData(timeseries: HelgolandTimeseries)
Parameters :
Name Type Optional
timeseries HelgolandTimeseries No
Returns : void
Public ngOnInit
ngOnInit()
Returns : void
Protected onLanguageChanged
onLanguageChanged(langChangeEvent: LangChangeEvent)
Parameters :
Name Type Optional
langChangeEvent LangChangeEvent No
Returns : void
Protected onResize
onResize()
Returns : void
Protected onTimezoneChanged
onTimezoneChanged(timezone: string)
Parameters :
Name Type Optional
timezone string No
Returns : void
Private prepareData
prepareData(timeseries: HelgolandTimeseries, newdata: DatasetTableData[])
Parameters :
Name Type Optional
timeseries HelgolandTimeseries No
newdata DatasetTableData[] No
Returns : void
Protected presenterOptionsChanged
presenterOptionsChanged(options: any)
Parameters :
Name Type Optional
options any No
Returns : void
Public reloadDataForDatasets
reloadDataForDatasets(datasetIds: string[])
Parameters :
Name Type Optional
datasetIds string[] No
Returns : void
Protected removeDataset
removeDataset(internalId: string)
Parameters :
Name Type Optional
internalId string No
Returns : void
Protected removeSelectedId
removeSelectedId(internalId: string)
Parameters :
Name Type Optional
internalId string No
Returns : void
Protected setSelectedId
setSelectedId(internalId: string)
Parameters :
Name Type Optional
internalId string No
Returns : void
Public sort
sort(event: any)
Parameters :
Name Type Optional
event any No
Returns : void
Protected timeIntervalChanges
timeIntervalChanges()
Returns : void
Protected Abstract addDataset
addDataset(id: string, url: string)
Parameters :
Name Type Optional
id string No
url string No
Returns : void
Protected addDatasetByInternalId
addDatasetByInternalId(internalId: string)
Parameters :
Name Type Optional
internalId string No
Returns : void
Protected Abstract datasetOptionsChanged
datasetOptionsChanged(internalId: string, options: T, firstChange: boolean)
Parameters :
Name Type Optional
internalId string No
options T No
firstChange boolean No
Returns : void
Public ngDoCheck
ngDoCheck()
Returns : void
Public ngOnChanges
ngOnChanges(changes: SimpleChanges)
Parameters :
Name Type Optional
changes SimpleChanges No
Returns : void
Public ngOnDestroy
ngOnDestroy()
Returns : void
Protected Abstract onLanguageChanged
onLanguageChanged(langChangeEvent: LangChangeEvent)
Parameters :
Name Type Optional
langChangeEvent LangChangeEvent No
Returns : void
Protected Abstract onTimezoneChanged
onTimezoneChanged(timezone: string)
Parameters :
Name Type Optional
timezone string No
Returns : void
Protected Abstract presenterOptionsChanged
presenterOptionsChanged(options: U)
Parameters :
Name Type Optional
options U No
Returns : void
Public Abstract reloadDataForDatasets
reloadDataForDatasets(datasets: string[])
Parameters :
Name Type Optional
datasets string[] No
Returns : void
Protected Abstract removeDataset
removeDataset(internalId: string)
Parameters :
Name Type Optional
internalId string No
Returns : void
Protected Abstract removeSelectedId
removeSelectedId(internalId: string)
Parameters :
Name Type Optional
internalId string No
Returns : void
Protected Abstract setSelectedId
setSelectedId(internalId: string)
Parameters :
Name Type Optional
internalId string No
Returns : void
Protected Abstract timeIntervalChanges
timeIntervalChanges()
Returns : void
Protected Abstract onResize
onResize()
Inherited from ResizableComponent
Returns : void

Properties

Private additionalStylesheet
Type : HTMLElement
Public preparedColors
Type : string[]
Default value : Array()
Public preparedData
Type : DatasetTableData[]
Default value : Array()
Public ready
Default value : false
Public timeseriesArray
Type : HelgolandTimeseries[]
Default value : new Array()
Private datasetIdsDiffer
Type : IterableDiffer<string>
Private langChangeSubscription
Type : Subscription
Protected oldDatasetOptions
Type : Map<string | T>
Protected oldPresenterOptions
Type : U
Private selectedDatasetIdsDiffer
Type : IterableDiffer<string>
Protected timespan
Type : Timespan
Private timezoneSubscription
Type : Subscription
import { Component, IterableDiffers, OnInit } from '@angular/core';
import {
  DatasetOptions,
  DatasetPresenterComponent,
  DatasetTableData,
  DatasetType,
  HelgolandServicesConnector,
  HelgolandTimeseries,
  HelgolandTimeseriesData,
  InternalIdHandler,
  Time,
  TimezoneService,
} from '@helgoland/core';
import { LangChangeEvent, TranslateService } from '@ngx-translate/core';

@Component({
  selector: 'n52-dataset-table',
  templateUrl: './dataset-table.component.html',
  styleUrls: ['./dataset-table.component.scss']
})
export class DatasetTableComponent extends DatasetPresenterComponent<DatasetOptions, any> implements OnInit {
  /*
    The component extends DatasetGraphComponent, but implements only parts of that components inputs and outputs.
    Implemented: datasetIds, timeInterval, selectedDatasetIds and datasetOptions inputs; no outputs
    Not implemented: graphOptions input; all outputs (onDatasetSelected, onTimespanChanged, onMessageThrown, onLoading)
  */

  public preparedData: DatasetTableData[] = Array();
  public preparedColors: string[] = Array();
  public ready = false;

  public timeseriesArray: HelgolandTimeseries[] = new Array();
  private additionalStylesheet: HTMLElement;

  constructor(
    protected iterableDiffers: IterableDiffers,
    protected servicesConnector: HelgolandServicesConnector,
    protected datasetIdResolver: InternalIdHandler,
    protected timeSrvc: Time,
    protected translateSrvc: TranslateService,
    protected timezoneSrvc: TimezoneService
  ) {
    super(iterableDiffers, servicesConnector, datasetIdResolver, timeSrvc, translateSrvc, timezoneSrvc);
  }

  public ngOnInit() {
    this.additionalStylesheet = document.getElementById('selectedIdsStylesheet');
    if (!this.additionalStylesheet) {
      this.additionalStylesheet = document.createElement('style');
      this.additionalStylesheet.id = 'selectedIdsStylesheet';
      document.body.appendChild(this.additionalStylesheet);
    }
  }

  /* called when user clicks on table headers */
  public sort(event: any) {
    // can be 'datetime' or an integer indicating the index of the column in the values array
    const by = event.target.dataset.columnId;
    const direction = event.target.classList.contains('sorted-asc') ? 'desc' : 'asc';
    const directionNumber = (direction === 'asc' ? 1 : -1);

    // set CSS classes
    Array.from(event.target.parentElement.children).forEach((child: Element) => child.className = '');
    if (direction === 'asc') {
      (event.target as Element).classList.add('sorted-asc');
    } else {
      (event.target as Element).classList.add('sorted-desc');
    }

    // define correct callback function for sort method
    let sortCallback;
    if (by === 'datetime') {
      sortCallback = (e1: any, e2: any) => directionNumber * (e1.datetime - e2.datetime);
    } else {
      const index = parseInt(by, 10);
      // basically the same as above, but take care of 'undefined' values
      sortCallback = (e1: any, e2: any) =>
        (e1.values[index] === undefined ? 1 :
          (e2.values[index] === undefined ? -1 :
            (directionNumber * (e1.values[index] - e2.values[index]))
          )
        );
    }

    // do the sort
    this.preparedData = this.preparedData.sort(sortCallback);
  }

  protected onLanguageChanged(langChangeEvent: LangChangeEvent): void { }

  protected onTimezoneChanged(timezone: string): void { }

  public reloadDataForDatasets(datasetIds: string[]): void {
    // console.log('reload data at ' + new Date());
  }

  protected presenterOptionsChanged(options: any) {
    // only included because it's required by abstract parent class (wouldn't compile without)
    // no point in implementing this method in a non-graphing component
  }

  protected getIndexFromInternalId(internalId: string) {
    // helper method
    return this.datasetIds.indexOf(internalId);
  }

  protected setSelectedId(internalId: string) {
    // quite fairly tested
    const rules = this.additionalStylesheet.innerHTML.split('\r\n');
    const index = this.getIndexFromInternalId(internalId);
    rules[index] = 'td:nth-child(' + (index + 2) + ') {font-weight: bold}';
    this.additionalStylesheet.innerHTML = rules.join('\r\n');
  }

  protected removeSelectedId(internalId: string) {
    // fairly tested
    const rules = this.additionalStylesheet.innerHTML.split('\r\n');
    const index = this.getIndexFromInternalId(internalId);
    rules[index] = '';
    this.additionalStylesheet.innerHTML = rules.join('\r\n');
  }

  protected timeIntervalChanges() {
    // the easiest method: delete everything and build preparedData from scratch.
    this.preparedData = [];
    this.timeseriesArray.forEach((timeseries) => this.loadTsData(timeseries));
  }

  protected removeDataset(internalId: string) {
    // fairly tested
    const index = this.getIndexFromInternalId(internalId);

    // remove entries of this dataset in each datetime's `values` arrays
    this.preparedData.forEach((e) => e.values.splice(index, 1));
    // if a datetime became completely empty (i.e. there's only `undefined`s in the `values` array, delete this datetime)
    this.preparedData = this.preparedData.filter((e) => e.values.reduce((a, c) => a || c, undefined) !== undefined);

    this.preparedColors.splice(index, 1);

    const rules = this.additionalStylesheet.innerHTML.split('\r\n');
    rules.splice(index, 1);
    this.additionalStylesheet.innerHTML = rules.join('\r\n');

    this.timeseriesArray.splice(index, 1);
  }

  protected addDataset(id: string, url: string): void {
    this.timeseriesArray.length += 1;  // create new empty slot
    this.preparedColors.push('darkgrey');
    this.additionalStylesheet.innerHTML += '\r\n';
    this.servicesConnector.getDataset({ id, url }, { type: DatasetType.Timeseries })
      .subscribe(ds => this.addTimeseries(ds));
  }

  protected datasetOptionsChanged(internalId: string, options: DatasetOptions): void {
    if (this.timeseriesArray.some((e) => e !== undefined && e.internalId === internalId)) {
      const index = this.getIndexFromInternalId(internalId);
      this.preparedColors[index] = options.color;
      // TODO-CF: Page isn't refreshed instantly, but only after the next sort (or possible other actions as well)
    }
  }

  protected onResize(): void {
    // TODO-CF: needed???? probably not
  }

  private addTimeseries(timeseries: HelgolandTimeseries) {
    this.timeseriesArray[this.getIndexFromInternalId(timeseries.internalId)] = timeseries;
    this.loadTsData(timeseries);
  }

  private loadTsData(timeseries: HelgolandTimeseries) {
    if (this.timespan) {
      // const datasetOptions = this.datasetOptions.get(timeseries.internalId);
      this.servicesConnector.getDatasetData(timeseries, this.timespan).subscribe(
        result => {
          // bring result into Array<DatasetTableData> format and pass to prepareData
          // convention for layout of newdata argument: see 3-line-comment in prepareData function
          if (result instanceof HelgolandTimeseriesData) {
            const index = this.getIndexFromInternalId(timeseries.internalId);
            this.prepareData(timeseries, result.values.map((e) => {
              const a = new Array(this.datasetIds.length).fill(undefined);
              a[index] = e[1];
              return { datetime: e[0], values: a };
            }));
          }
        }
      );
    }
  }

  private prepareData(timeseries: HelgolandTimeseries, newdata: DatasetTableData[]) {
    const index = this.getIndexFromInternalId(timeseries.internalId);

    // if datasetOptions are provided, use their color to style the header's "color band" (i.e. the 7px border-bottom of th)
    if (this.datasetOptions) {
      const datasetOptions = this.datasetOptions.get(timeseries.internalId);
      this.preparedColors[index] = datasetOptions.color;
    } else {
      // when no color is specified: make border transparent so the header's background color is used for the color band, too
      this.preparedColors[index] = 'rgba(0,0,0,0)';
    }

    if (this.selectedDatasetIds.indexOf(timeseries.internalId) !== -1) {
      this.setSelectedId(timeseries.internalId);
    }

    // `newdata` is expected in exactly the same format `preparedData` would look like if that timeseries was the only one
    // to actually have data (i.e. `values` has the length of timeseriesArray, but all slots are `undefined`, except for
    // the slot that corresponds to that timeseries)

    // `timeseries` is first timeseries added -> no other `preparedData` to merge with
    if (this.preparedData.length === 0) {
      // set newdata as preparedData (as per above)
      this.preparedData = newdata;

      // `timeseries` is not the first timeseries added -> we have to merge `newdata` into the existing `preparedData`
    } else {
      let i = 0;  // loop variable for `preparedData`
      let j = 0;  // loop variable for `newdata`

      // go through all data points in `newdata`
      while (j < newdata.length) {

        // timestamps match
        if (this.preparedData[i] && this.preparedData[i].datetime === newdata[j].datetime) {
          // just add `newdata`'s value to the existing `values` array in `preparedData`
          this.preparedData[i].values[index] = newdata[j].values[index];
          // increment both
          i++;
          j++;

          // `newdata` is ahead of `preparedData`
        } else if (this.preparedData[i] && this.preparedData[i].datetime < newdata[j].datetime) {
          // do nothing because there's already an undefined there
          // give preparedData the chance to catch up with newdata
          i++;

          // `preparedData` is ahead of `newdata`
        } else {
          // the current `newdata` is the first dataset that has this datetime -> add it to the preparedData array
          this.preparedData.splice(i, 0, newdata[j]);
          // give newdata the chance to catch up with preparedData
          j++;
          // but preparedData is 1 longer now, too
          i++;
        }
      }
    }

    this.ready = this.timeseriesArray.every((e) => e !== undefined);
  }
}
<table *ngIf="ready">
  <thead>
    <tr>
      <th (click)="sort($event)" [attr.data-column-id]="'datetime'" class="sorted-asc">
        Zeit
      </th>
      <th *ngFor="let series of this.timeseriesArray; let i = index" (click)="sort($event)" [attr.data-column-id]="i" [ngStyle]="{ 'border-color': preparedColors[i] }">
        {{series?.label}} [{{series?.uom}}]
      </th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let row of this.preparedData">
      <td>{{row.datetime | tzDate: 'L LT z'}}</td>
      <td *ngFor="let value of row.values">{{value}}</td>
    </tr>
  </tbody>
</table>

./dataset-table.component.scss

:host {
  flex: 1;
  overflow-y: scroll;  /* make table scrollable */
  overflow-x: hidden;  /* suppress horizontal scrollbar */

  /* Start fixed table header: http://jsfiddle.net/yeAhU/261/ */
  tbody, thead tr {
    display: table;
    table-layout: fixed;
    width: 100%;
  }

  table {
    display: block;
    border-collapse: separate;
    border-spacing: 0 1px;
  }

  thead {
    display:block;
    position: sticky;
    top: 0;
    border-spacing: 0;
  }
  /* End fixed table header */

  tr {
    /* zebra stripes */
    &:nth-child(2n) {
      background-color: #eee;
    }
  }

  th {
    /* darken table head */
    background-color: darkgray;
    cursor: pointer;
    border-bottom-width: 7px;
    border-bottom-style: solid;

    /* if word doesn't fit on whole line, breaks the word after any character without inserting a hyphen or anything else */
    overflow-wrap: break-word;
    /* if word doesn't fit on the rest of the current line, breaks the word where the browser's algorithm thinks it's approriate and renders a hyphen */
    /*hyphens: auto;*/

    &:first-child {
      border-bottom-color: darkgray;
      &.sorted-asc, &.sorted-desc {
        border-bottom-color: #555;
      }
    }

    /* highlight sorted column */
    &.sorted-asc, &.sorted-desc {
      background-color: #555;
      color: white;
    }

    /* Display "up" and "down" triangles accordingly */
    &.sorted-asc:after {
      content: "\25B4"; /* up triangle */
      float: right;
    }
    
    &.sorted-desc:after {
      content: "\25BE"; /* down triangle */
      float: right;
    }
  }

  td {
    /* prevents datetime from wrapping in small windows or when there's many columns */
    white-space: nowrap;

    /* thin horizontal lines between rows */
    border-bottom: 1px solid gray;
  }

  th, td {
    padding: 5px 10px;
  }
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""