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; /** * 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 = new Map(); protected listOfUoms: string[] = []; protected preparedData: InternalDataEntry[] = []; // private loadingData: Set = new Set(); private width: number; private canvas: HTMLCanvasElement; private margin = { top: 10, right: 10, bottom: 40, left: 10 }; private datasetIdsDiffer: IterableDiffer; private selectedDatasetIdsDiffer: IterableDiffer; constructor( protected iterableDiffers: IterableDiffers, protected datasetIdResolver: InternalIdHandler, protected datasetApiService: DatasetApiService, protected timeService: TimeService, public datasetService: DatasetService, ) { 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(reducedValues.min); // ar.push(reducedValues.min, reducedValues.max); grouped_items.set(key, ar); } function groupBy(list: Array<[number, number]>, keyGetter: any): Map { 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 { 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; } }