567 lines
No EOL
21 KiB
TypeScript
567 lines
No EOL
21 KiB
TypeScript
import {
|
|
Input, Component, AfterViewInit, ViewChild, ElementRef, SimpleChanges, DoCheck, IterableDiffer, IterableDiffers
|
|
} from '@angular/core';
|
|
// import * as d3 from 'd3';
|
|
import { Chart, ChartDataset, registerables } from 'chart.js';
|
|
|
|
import { GeomonTimeseries, DataConst, GeomonTimeseriesData } from '../../../shared/models/dataset';
|
|
import { DatasetApiService } from '../../../app/services/dataset-api.service';
|
|
import { DatasetService } from '../../../app/services/dataset.service';
|
|
|
|
import { Timespan } from '../../../shared/models/timespan';
|
|
import { DatasetOptions } from '../../../shared/models/options';
|
|
import { TimeService } from '../../core/time/time.service';
|
|
import * as moment from 'moment';
|
|
// import 'moment-duration-format';
|
|
import { TimeValueTuple, Data } from '../../../shared/models/dataset';
|
|
import 'chartjs-adapter-moment';
|
|
|
|
import zoomPlugin from 'chartjs-plugin-zoom';
|
|
|
|
Chart.register(zoomPlugin);
|
|
import { InternalIdHandler, InternalDatasetId } from '../../../common/components/services/internal-id-handler.service';
|
|
|
|
import { InternalDataEntry } from '../../../shared/models/chart';
|
|
// interface Color {
|
|
// borderColor: string,
|
|
// pointBackgroundColor: string
|
|
// }
|
|
|
|
@Component({
|
|
selector: 'geomon-timeseries-chart',
|
|
templateUrl: './geomon-timeseries-chart.component.html',
|
|
styleUrls: ['./geomon-timeseries-chart.component.scss']
|
|
})
|
|
export class GeomonTimeseriesChartComponent implements AfterViewInit, DoCheck {
|
|
|
|
|
|
|
|
// @ViewChild('geomon_timeseries', { static: true })
|
|
// public chartElem: ElementRef;
|
|
|
|
|
|
@ViewChild('chart') private chartElem: ElementRef;
|
|
|
|
private lineChart: Chart;
|
|
/**
|
|
* The corresponding dataset options.
|
|
*/
|
|
@Input()
|
|
public datasetOptions: Map<string, DatasetOptions>;
|
|
|
|
/**
|
|
* List of presented dataset ids.
|
|
*/
|
|
@Input()
|
|
public datasetIds: string[] = [];
|
|
|
|
|
|
/**
|
|
* List of presented selected dataset ids.
|
|
*/
|
|
@Input()
|
|
public selectedDatasetIds: string[] = [];
|
|
|
|
|
|
/**
|
|
* The time interval in which the data should presented.
|
|
*/
|
|
@Input()
|
|
public timeInterval: Timespan;
|
|
|
|
protected timespan: Timespan;
|
|
|
|
// data types
|
|
protected datasetMap: Map<string, DataConst> = new Map();
|
|
protected listOfUoms: string[] = [];
|
|
protected preparedData: InternalDataEntry[] = [];
|
|
|
|
// private loadingData: Set<string> = new Set();
|
|
|
|
private width: number;
|
|
private canvas: HTMLCanvasElement;
|
|
private margin = {
|
|
top: 10,
|
|
right: 10,
|
|
bottom: 40,
|
|
left: 10
|
|
};
|
|
|
|
private datasetIdsDiffer: IterableDiffer<string>;
|
|
private selectedDatasetIdsDiffer: IterableDiffer<string>;
|
|
|
|
constructor(
|
|
protected iterableDiffers: IterableDiffers,
|
|
protected datasetIdResolver: InternalIdHandler,
|
|
protected datasetApiService: DatasetApiService,
|
|
protected timeService: TimeService,
|
|
public datasetService: DatasetService<DatasetOptions>,
|
|
) {
|
|
this.datasetIdsDiffer = this.iterableDiffers.find([]).create();
|
|
this.selectedDatasetIdsDiffer = this.iterableDiffers.find([]).create();
|
|
}
|
|
|
|
public ngDoCheck(): void {
|
|
|
|
const selectedDatasetIdsChanges = this.selectedDatasetIdsDiffer.diff(this.selectedDatasetIds);
|
|
if (selectedDatasetIdsChanges) {
|
|
selectedDatasetIdsChanges.forEachAddedItem((addedItem) => {
|
|
this.setSelectedId(addedItem.item);
|
|
});
|
|
selectedDatasetIdsChanges.forEachRemovedItem((removedItem) => {
|
|
this.removeSelectedId(removedItem.item);
|
|
});
|
|
}
|
|
|
|
if(this.datasetOptions){
|
|
let test = this.datasetOptions;
|
|
}
|
|
}
|
|
|
|
protected setSelectedId(internalId: string): void {
|
|
const internalEntry = this.preparedData.find((e) => e.internalId === internalId);
|
|
if (internalEntry) { internalEntry.selected = true; }
|
|
this.redrawCompleteGraph();
|
|
}
|
|
|
|
protected removeSelectedId(internalId: string): void {
|
|
const internalEntry = this.preparedData.find((e) => e.internalId === internalId);
|
|
if (internalEntry) { internalEntry.selected = false; }
|
|
this.redrawCompleteGraph();
|
|
}
|
|
|
|
ngAfterViewInit(): void {
|
|
|
|
this.canvas = document.getElementById("line-chart") as HTMLCanvasElement;
|
|
|
|
|
|
// this.drawBars(this.data);
|
|
Chart.register(...registerables);
|
|
this.initChart();
|
|
|
|
for (let i = 0; i < this.datasetIds.length; i++) {
|
|
let datasetId = this.datasetIds[i];
|
|
let datasetOptions = this.datasetOptions.get(datasetId);
|
|
// this.internalId = this.internalIdHandler.resolveInternalId(datasetId);
|
|
this.addDatasetByInternalId(datasetOptions.internalId);
|
|
}
|
|
|
|
// let firstDatasetId = this.datasetIds[0];
|
|
// let dataset = this.datasetOptions.get(firstDatasetId);
|
|
// this.addDataset(dataset.id, 'https://geomon.geologie.ac.at/52n-sos-webapp/api/');
|
|
|
|
|
|
}
|
|
|
|
public ngOnChanges(changes: SimpleChanges): void {
|
|
if (changes.timeInterval && this.timeInterval) {
|
|
this.timespan = this.timeService.createTimespanOfInterval(this.timeInterval);
|
|
// this.timeIntervalChanges();
|
|
}
|
|
// if (changes.reloadForDatasets && this.reloadForDatasets && this.reloadDataForDatasets.length > 0) {
|
|
// this.reloadDataForDatasets(this.reloadForDatasets);
|
|
// }
|
|
}
|
|
|
|
public getDataset(internalId: string) {
|
|
return this.datasetMap.get(internalId);
|
|
}
|
|
|
|
protected addDatasetByInternalId(internalId: string) {
|
|
const internalIdObj = this.datasetIdResolver.resolveInternalId(internalId);
|
|
this.addDataset(internalIdObj.id, internalIdObj.url);
|
|
}
|
|
|
|
protected addDataset(id: string, url: string): void {
|
|
// this.servicesConnector.getDataset({ id, url }, { locale: this.translateService.currentLang, type: DatasetType.Timeseries }).subscribe(
|
|
// res => this.loadAddedDataset(res),
|
|
// error => this.errorHandler.handleDatasetLoadError(error)
|
|
// );
|
|
|
|
this.datasetApiService.getDataset(id, url).subscribe({
|
|
next: (res: GeomonTimeseries) => this.loadAddedDataset(res),
|
|
error: (err: any) => console.error('Observer got an error: ' + err),
|
|
complete: () => console.log('HTTP request completed.')
|
|
// error => this.errorHandler.handleDatasetLoadError(error)
|
|
});
|
|
}
|
|
|
|
private loadAddedDataset(dataset: GeomonTimeseries): void {
|
|
this.datasetMap.set(dataset.internalId, dataset);
|
|
this.loadDatasetData(dataset, false);
|
|
}
|
|
|
|
// load data of dataset
|
|
private loadDatasetData(dataset: GeomonTimeseries, fprce: boolean): void {
|
|
// const datasetOptions = this.datasetOptions.get(dataset.internalId);
|
|
|
|
// https://github.com/52North/helgoland-toolbox/blob/9ff9a42b34cd3deb181d56d76d48eba7c101554e/libs/core/src/lib/api-communication/connectors/dataset-api-v3-connector/dataset-api-v3-connector.ts#L270
|
|
|
|
const buffer = this.timeService.getBufferedTimespan(this.timespan, 5, moment.duration(1, 'day').asMilliseconds());
|
|
|
|
this.datasetApiService.getDatasetData(dataset, buffer, {})
|
|
.subscribe({
|
|
next: (result: GeomonTimeseriesData) => {
|
|
this.prepareData(dataset, result);
|
|
// this.onCompleteLoadingData(dataset);
|
|
let test = result;
|
|
},
|
|
error: (error) => {
|
|
// this.errorHandler.handleDataLoadError(error, dataset);
|
|
// this.onCompleteLoadingData(dataset);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Function to prepare each dataset for the graph and adding it to an array of datasets.
|
|
* @param dataset {IDataset} Object of the whole dataset
|
|
*/
|
|
private prepareData(dataset: GeomonTimeseries, rawdata: GeomonTimeseriesData): void {
|
|
|
|
if (rawdata instanceof GeomonTimeseriesData) {
|
|
// add surrounding entries to the set
|
|
if (rawdata.valueBeforeTimespan) { rawdata.values.unshift(rawdata.valueBeforeTimespan); }
|
|
if (rawdata.valueAfterTimespan) { rawdata.values.push(rawdata.valueAfterTimespan); }
|
|
|
|
// const data = this.generalizeData(rawdata, this.width, this.timespan);
|
|
|
|
|
|
let grouped_items = groupBy(rawdata.values, function (b: TimeValueTuple) {
|
|
return moment(b[0]).format('YYYY-MM-DD');
|
|
});
|
|
|
|
// Object.keys(grouped_items).forEach(function (value: any, key: string) {
|
|
for (let [key, value] of grouped_items) {
|
|
// grouped_items[key] = findMinMax(grouped_items.get(key));
|
|
// let test = grouped_items.get(key);
|
|
let reducedValues = findMinMax(grouped_items.get(key));
|
|
let ar = new Array<TimeValueTuple>(reducedValues.min);
|
|
// ar.push(reducedValues.min, reducedValues.max);
|
|
grouped_items.set(key, ar);
|
|
}
|
|
|
|
function groupBy(list: Array<[number, number]>, keyGetter: any): Map<string, TimeValueTuple[]> {
|
|
const map = new Map();
|
|
list.forEach((item: any) => {
|
|
const key = keyGetter(item);
|
|
const collection = map.get(key);
|
|
if (!collection) {
|
|
map.set(key, [item]);
|
|
} else {
|
|
collection.push(item);
|
|
}
|
|
});
|
|
return map;
|
|
}
|
|
|
|
|
|
function findMinMax(values: TimeValueTuple[]) {
|
|
var res = { min: values[0], max: values[0] };
|
|
|
|
values.forEach(function (val: [number, number]) {
|
|
res.min = val[0] < res.min[0] ? val : res.min;
|
|
res.max = val[0] > res.max[0] ? val : res.max;
|
|
});
|
|
|
|
return res;
|
|
}
|
|
|
|
let values = Array.from( grouped_items.values() );
|
|
values = [].concat(...values);
|
|
let xLabels = values.map(function (label) {
|
|
let date = moment(label[0]).format("DD/MM HH:mm");
|
|
return date;
|
|
});
|
|
// console.log(values);
|
|
|
|
// this.datasetMap.get(dataset.internalId).data = data;
|
|
this.addData(this.lineChart, dataset, rawdata, xLabels);
|
|
}
|
|
}
|
|
|
|
public generalizeData(data: GeomonTimeseriesData, imageWidth: number, timespan: Timespan): Data<TimeValueTuple> {
|
|
if (data.values.length > imageWidth && data.values.length > 0) {
|
|
const duration = timespan.to - timespan.from;
|
|
const dataduration = data.values[data.values.length - 1][0] - data.values[0][0];
|
|
const factor = duration / dataduration;
|
|
const realWidth = imageWidth / factor;
|
|
const modulo = 1 / (data.values.length / realWidth);
|
|
const generalizedData = {
|
|
values: data.values.filter((v, i) => i % Math.round(modulo) === 0),
|
|
referenceValues: data.referenceValues
|
|
};
|
|
console.log(`reduce from ${data.values.length} to ${generalizedData.values.length}`);
|
|
return generalizedData;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
private addData(chart: Chart, dataset: GeomonTimeseries, data: GeomonTimeseriesData, xLabels: string[]): void {
|
|
|
|
let labels = data.values.map(function (label) {
|
|
let date = moment(label[0]).format("YYYY-MM-DD HH:mm");
|
|
return date;
|
|
});
|
|
|
|
let values = data.values.map(function (value) {
|
|
return value[1];
|
|
});
|
|
|
|
chart.data.labels = labels;
|
|
// chart.data.datasets.forEach((dataset) => {
|
|
// dataset.data.push(data);
|
|
// });
|
|
let letters = '0123456789ABCDEF'.split('');
|
|
let color = '#';
|
|
for (let i = 0; i < 6; i++) {
|
|
color += letters[Math.floor(Math.random() * 16)];
|
|
}
|
|
|
|
const datasetIdx = this.preparedData.findIndex((e) => e.internalId === dataset.internalId);
|
|
let datasetOptions = this.datasetOptions.get(dataset.internalId);
|
|
datasetOptions.color = color;
|
|
|
|
let dataEntry: InternalDataEntry = {
|
|
internalId: dataset.internalId,
|
|
selected: this.selectedDatasetIds.indexOf(dataset.internalId) >= 0,
|
|
// data: datasetOptions.visible ? data.values.map(d => ({ timestamp: d[0], value: d[1] })) : [],
|
|
data: values,
|
|
options: datasetOptions,
|
|
axisOptions: {
|
|
uom: dataset.uom,
|
|
label: dataset.label,
|
|
// zeroBased: datasetOptions.zeroBasedYAxis,
|
|
// yAxisRange: options.yAxisRange,
|
|
// autoRangeSelection: datasetOptions.autoRangeSelection,
|
|
// separateYAxis: datasetOptions.separateYAxis,
|
|
parameters: {
|
|
feature: dataset.parameters.feature,
|
|
phenomenon: dataset.parameters.phenomenon,
|
|
offering: dataset.parameters.offering
|
|
}
|
|
},
|
|
referenceValueData: [],
|
|
visible: datasetOptions.visible,
|
|
// bar: barConfig
|
|
};
|
|
|
|
if (datasetIdx >= 0) {
|
|
this.preparedData[datasetIdx] = dataEntry;
|
|
} else {
|
|
this.preparedData.push(dataEntry);
|
|
}
|
|
|
|
this.processData(dataEntry);
|
|
// this.redrawCompleteGraph();
|
|
|
|
// var newDataset = {
|
|
// label: dataEntry.axisOptions.label,
|
|
// selected: dataEntry.selected,
|
|
// backgroundColor: color,
|
|
// borderColor: color,
|
|
// borderWidth: 1,
|
|
// data: values,
|
|
// }
|
|
// // You add the newly created dataset to the list of `data`
|
|
// chart.data.datasets.push(newDataset);
|
|
// chart.options.scales.x.ticks.callback = (val, index) => {
|
|
// // // Hide the label of every 2nd dataset
|
|
// // return xLabels.includes(val.toString()) ? val : null;
|
|
// // return index % 2 === 0 ? (val) : '';
|
|
// let valTime = moment(val, "DD/MM HH:mm").format("HH:mm");
|
|
// if (valTime == "08:00" || valTime == "18:00"){
|
|
// return val;
|
|
// } else {
|
|
// return null;
|
|
// }
|
|
|
|
// }
|
|
// chart.options.scales.y.ticks.callback = (value, index, values) => {
|
|
// return value + '°';
|
|
// }
|
|
|
|
chart.update();
|
|
this.width = this.calculateWidth();
|
|
}
|
|
|
|
private processData(dataEntry: InternalDataEntry, datasetIndex?: number): void {
|
|
|
|
let dataset: ChartDataset;
|
|
|
|
if (datasetIndex != null) {
|
|
dataset = this.lineChart.data.datasets[datasetIndex];
|
|
dataset.label = dataEntry.axisOptions.label;
|
|
dataset.borderWidth = dataEntry.selected ? 4 : 1;
|
|
// dataset.hidden = dataEntry.selected;
|
|
} else {
|
|
dataset = {
|
|
label: dataEntry.axisOptions.label,
|
|
// selected: dataEntry.selected,
|
|
// backgroundColor: 'rgba(99, 255, 132, 0.2)',
|
|
backgroundColor: dataEntry.options.color,
|
|
borderColor: dataEntry.options.color, //'rgba(99, 255, 132, 1)',
|
|
borderWidth: 1,
|
|
data: dataEntry.data,
|
|
};
|
|
this.lineChart.data.datasets.push(dataset);
|
|
}
|
|
|
|
// You add the newly created dataset to the list of `data`
|
|
// this.lineChart.data.datasets.push(newDataset);
|
|
this.lineChart.options.scales.x.ticks.callback = (val, index) => {
|
|
// // Hide the label of every 2nd dataset
|
|
// return xLabels.includes(val.toString()) ? val : null;
|
|
// return index % 2 === 0 ? (val) : '';
|
|
let valTime = moment(val, "DD/MM HH:mm").format("HH:mm");
|
|
if (valTime == "08:00" || valTime == "18:00"){
|
|
return val;
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
}
|
|
this.lineChart.options.scales.y.ticks.callback = (value, index, values) => {
|
|
return value + '°';
|
|
}
|
|
}
|
|
|
|
private redrawCompleteGraph(): void {
|
|
this.preparedData.forEach((dataEntry: InternalDataEntry, index) => {
|
|
|
|
this.processData(dataEntry, index);
|
|
});
|
|
this.lineChart.update();
|
|
}
|
|
|
|
private initChart(): void {
|
|
|
|
this.lineChart = new Chart(this.chartElem.nativeElement, {
|
|
type: 'line',
|
|
data: {
|
|
// labels: [1500, 1600, 1700, 1750, 1800, 1850, 1900, 1950, 1999, 2050],
|
|
labels: [],
|
|
datasets: [
|
|
// {
|
|
// data: [86, 114, 106, 106, 107, 111, 133, 221, 783, 2478],
|
|
// label: "Africa",
|
|
// borderColor: "#3e95cd",
|
|
// fill: false
|
|
// }, {
|
|
// data: [282, 350, 411, 502, 635, 809, 947, 1402, 3700, 5267],
|
|
// label: "Asia",
|
|
// borderColor: "#8e5ea2",
|
|
// fill: false
|
|
// }, {
|
|
// data: [168, 170, 178, 190, 203, 276, 408, 547, 675, 734],
|
|
// label: "Europe",
|
|
// borderColor: "#3cba9f",
|
|
// fill: false
|
|
// }, {
|
|
// data: [40, 20, 10, 16, 24, 38, 74, 167, 508, 784],
|
|
// label: "Latin America",
|
|
// borderColor: "#e8c3b9",
|
|
// fill: false
|
|
// }, {
|
|
// data: [6, 3, 2, 2, 7, 26, 82, 172, 312, 433],
|
|
// label: "North America",
|
|
// borderColor: "#c45850",
|
|
// fill: false
|
|
// }
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
zoom: {
|
|
pan: {
|
|
enabled: true
|
|
},
|
|
zoom: {
|
|
wheel: {
|
|
enabled: true,
|
|
},
|
|
pinch: {
|
|
enabled: true
|
|
},
|
|
mode: 'xy',
|
|
}
|
|
},
|
|
legend: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
suggestedMin: 0, // minimum will be 0, unless there is a lower value.
|
|
// OR //
|
|
beginAtZero: true // minimum value will be 0.
|
|
},
|
|
|
|
x: {
|
|
type: 'time',
|
|
|
|
time: {
|
|
unit: 'minute',
|
|
displayFormats: {
|
|
minute: "DD/MM HH:mm",
|
|
hour: "DD/MM HH:mm",
|
|
day: "dd/MM",
|
|
week: "dd/MM",
|
|
month: "MMMM yyyy",
|
|
quarter: 'MMMM yyyy',
|
|
year: "yyyy",
|
|
}
|
|
},
|
|
|
|
ticks: {
|
|
|
|
|
|
// callback: function(val, index) {
|
|
// // Hide the label of every 2nd dataset
|
|
// return index % 2 === 0 ? (val) : '';
|
|
// },
|
|
// autoSkip: true,
|
|
// maxRotation: 0,
|
|
// minRotation: 0
|
|
}
|
|
}
|
|
},
|
|
}
|
|
// options: {
|
|
// title: {
|
|
// display: true,
|
|
// text: 'World population per region (in millions)'
|
|
// }
|
|
// }
|
|
});
|
|
|
|
// this.width = this.calculateWidth() - 20; // add buffer to the left to garantee visualization of last date (tick x-axis)
|
|
}
|
|
|
|
/**
|
|
* Function to generate uuid for a diagram
|
|
*/
|
|
private uuidv4(): string {
|
|
return this.s4() + this.s4() + '-' + this.s4() + '-' + this.s4() + '-' + this.s4() + '-' + this.s4() + this.s4() + this.s4();
|
|
}
|
|
|
|
/**
|
|
* Function to generate components of the uuid for a diagram
|
|
*/
|
|
private s4(): string {
|
|
return Math.floor((1 + Math.random()) * 0x10000)
|
|
.toString(16)
|
|
.substring(1);
|
|
}
|
|
|
|
/**
|
|
* Function that returns the width of the graph diagram.
|
|
*/
|
|
private calculateWidth(): number {
|
|
return this.canvas.width - this.margin.left - this.margin.right;
|
|
}
|
|
|
|
|
|
} |