File

libs/plotly/src/lib/plotly-profile-graph/plotly-profile-graph.component.ts

Extends

Partial

Index

Properties

Properties

id
id: string
Type : string
timestamp
timestamp: number
Type : number
import { AfterViewInit, Component, ElementRef, EventEmitter, IterableDiffers, Output, ViewChild } from '@angular/core';
import {
    DatasetPresenterComponent,
    DatasetType,
    HelgolandProfile,
    HelgolandServicesConnector,
    InternalIdHandler,
    PresenterHighlight,
    ProfileDataEntry,
    Time,
    TimedDatasetOptions,
    Timespan,
    TimezoneService,
} from '@helgoland/core';
import { LangChangeEvent, TranslateService } from '@ngx-translate/core';
import * as d3 from 'd3';
import * as Plotly from 'plotly.js';

interface RawData {
    dataset: HelgolandProfile;
    datas: ProfileDataEntry[];
    options: TimedDatasetOptions[];
}

interface ExtendedScatterData extends Partial<Plotly.ScatterData> {
    timestamp: number;
    id: string;
}

const LINE_WIDTH_SELECTED = 5;
const LINE_WIDTH = 2;
const MARKER_SIZE_SELECTED = 10;
const MARKER_SIZE = 6;

@Component({
    selector: 'n52-plotly-profile-graph',
    templateUrl: './plotly-profile-graph.component.html',
    styleUrls: ['./plotly-profile-graph.component.scss']
})
export class PlotlyProfileGraphComponent
    extends DatasetPresenterComponent<TimedDatasetOptions[], any>
    implements AfterViewInit {

    @Output()
    public onHighlight: EventEmitter<PresenterHighlight> = new EventEmitter();

    @ViewChild('plotly', { static: true })
    public plotlyElem: ElementRef;

    private plotlyArea: any;
    private preparedData: ExtendedScatterData[] = [];
    private rawData: Map<string, RawData> = new Map();
    private counterXAxis = 0;
    private counterYAxis = 0;

    private layout: Layout = {
        autosize: true,
        showlegend: false,
        dragmode: 'pan',
        margin: {
            l: 40,
            r: 10,
            b: 40,
            t: 10
            // pad: 100
        },
        hovermode: 'closest'
    };

    private settings: Partial<any> = {
        displayModeBar: false,
        modeBarButtonsToRemove: [
            'sendDataToCloud',
            'hoverCompareCartesian'
        ],
        displaylogo: false,
        showTips: false,
        scrollZoom: true
    };

    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 ngAfterViewInit(): void {
        this.plotlyArea = this.plotlyElem.nativeElement;
        this.drawChart();
    }

    protected onLanguageChanged(langChangeEvent: LangChangeEvent): void { }

    protected onTimezoneChanged(timezone: string): void { }

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

    // tslint:disable-next-line:no-empty
    protected timeIntervalChanges(): void { }

    protected addDataset(id: string, url: string): void {
        this.servicesConnector.getDataset({ id, url }, { type: DatasetType.Profile }).subscribe(dataset => {
            const options = this.datasetOptions.get(dataset.internalId);
            options.forEach((option) => {
                if (option.timestamp) {
                    const timespan = new Timespan(option.timestamp);
                    this.servicesConnector.getDatasetData(dataset, timespan).subscribe(data => {
                        if (data.values.length === 1) {
                            if (this.rawData.has(dataset.internalId)) {
                                this.rawData.get(dataset.internalId).datas.push(data.values[0]);
                                this.rawData.get(dataset.internalId).options.push(option);
                            } else {
                                this.rawData.set(dataset.internalId, {
                                    dataset,
                                    datas: [data.values[0]],
                                    options: [option]
                                });
                            }
                        }
                        this.drawChart();
                    });
                }
            });
        });
    }

    protected removeDataset(internalId: string): void {
        this.rawData.delete(internalId);
        this.drawChart();
    }

    protected setSelectedId(internalId: string): void {
        this.drawChart();
    }

    protected removeSelectedId(internalId: string): void {
        this.drawChart();
    }

    // tslint:disable-next-line:no-empty
    protected presenterOptionsChanged(options: any): void { }

    protected datasetOptionsChanged(internalId: string, options: TimedDatasetOptions[], firstChange: boolean): void {
        if (!firstChange) {
            // remove unused options
            const removedIdx = this.rawData.get(internalId).options.findIndex((option) => {
                const idx = options.findIndex((e) => e.timestamp === option.timestamp);
                if (idx === -1) {
                    return true;
                }
            });
            if (removedIdx > -1) {
                this.rawData.get(internalId).options.splice(removedIdx, 1);
                this.rawData.get(internalId).datas.splice(removedIdx, 1);
            }
            this.drawChart();
        }
    }

    protected onResize(): void {
        this.redrawChart();
    }

    private processData() {
        this.clearLayout();
        this.clearData();
        this.rawData.forEach((dataEntry) => {
            dataEntry.options.forEach((option, key) => {
                if (option.visible) {
                    const x = new Array<number>();
                    const y = new Array<number>();
                    const selected = this.selectedDatasetIds.indexOf(dataEntry.dataset.internalId) >= 0;
                    dataEntry.datas[key].value.forEach((entry) => {
                        x.push(entry.value);
                        y.push(entry.vertical);
                    });
                    const prepared: ExtendedScatterData = {
                        x,
                        y,
                        type: 'scatter',
                        name: '',
                        timestamp: option.timestamp,
                        id: dataEntry.dataset.internalId,
                        yaxis: this.createYAxis(dataEntry.dataset, dataEntry.datas[key]),
                        xaxis: this.createXAxis(dataEntry.dataset, dataEntry.datas[key]),
                        // hovertext: dataEntry.label,
                        line: {
                            color: option.color,
                            width: selected ? LINE_WIDTH_SELECTED : LINE_WIDTH
                        },
                        marker: {
                            size: selected ? MARKER_SIZE_SELECTED : MARKER_SIZE
                        }
                    };
                    this.preparedData.push(prepared);
                }
            });
        });

        this.updateAxis();
    }

    private createXAxis(dataset: HelgolandProfile, data: ProfileDataEntry): string {
        let axis;
        for (const key in this.layout) {
            if (this.layout.hasOwnProperty(key) && key.startsWith('xaxis') && this.layout[key].title === dataset.uom) {
                axis = this.layout[key];
            }
        }
        const range = d3.extent(data.value, (d) => d.value);
        if (!axis) {
            this.counterXAxis = this.counterXAxis + 1;
            axis = this.layout['xaxis' + this.counterXAxis] = {
                id: 'x' + (this.counterXAxis > 1 ? this.counterXAxis : ''),
                anchor: 'free',
                title: dataset.uom,
                zeroline: true,
                hoverformat: '.2f',
                showline: false,
                range: [range[0], range[1]],
                overlaying: '',
                // rangemode: 'tozero',
                fixedrange: false
            };
            if (this.counterXAxis !== 1) {
                axis.overlaying = 'x';
            }
        } else {
            axis.range = d3.extent([range[0], range[1], axis.range[0], axis.range[1]]);
        }
        return axis.id;
    }

    private createYAxis(dataset: HelgolandProfile, data: ProfileDataEntry): string {
        let axis;
        // find axis
        for (const key in this.layout) {
            if (this.layout.hasOwnProperty(key) &&
                key.startsWith('yaxis') &&
                this.layout[key].title === data.verticalUnit) {
                axis = this.layout[key];
            }
        }
        if (!axis) {
            // add axis
            this.counterYAxis = this.counterYAxis + 1;
            axis = this.layout[('yaxis' + this.counterYAxis)] = {
                id: 'y' + (this.counterYAxis > 1 ? this.counterYAxis : ''),
                // zeroline: true,
                anchor: 'free',
                hoverformat: '.2r',
                side: 'left',
                autorange: 'reversed',
                showline: false,
                overlaying: '',
                title: data.verticalUnit,
                fixedrange: false
            };
            if (this.counterYAxis !== 1) {
                axis.overlaying = 'y';
            }
        }
        return axis.id;
    }

    private updateAxis() {
        if (this.counterYAxis > 1) {
            for (const key in this.layout) {
                if (this.layout.hasOwnProperty(key) && key.startsWith('xaxis')) {
                    this.layout[key].domain = [(0.1 * this.counterYAxis) - 0.1, 1];
                }
            }
            let yaxisCount = 0;
            for (const key in this.layout) {
                if (this.layout.hasOwnProperty(key) && key.startsWith('yaxis')) {
                    this.layout[key].position = 0.1 * yaxisCount;
                    yaxisCount += 1;
                }
            }
        }
        if (this.counterXAxis > 1) {
            for (const key in this.layout) {
                if (this.layout.hasOwnProperty(key) && key.startsWith('yaxis')) {
                    this.layout[key].domain = [(0.06 * this.counterXAxis) - 0.06, 1];
                }
            }
            let xaxisCount = 0;
            for (const key in this.layout) {
                if (this.layout.hasOwnProperty(key) && key.startsWith('xaxis')) {
                    this.layout[key].position = 0.06 * xaxisCount;
                    xaxisCount += 1;
                }
            }
        }
        // add offset to xaxis ranges
        for (const key in this.layout) {
            if (this.layout.hasOwnProperty(key) && key.startsWith('xaxis')) {
                const range = this.layout[key].range;
                const rangeOffset = (range[1] - range[0]) * 0.05;
                this.layout[key].range = [range[0] - rangeOffset, range[1] + rangeOffset];
            }
        }
    }

    private drawChart() {
        if (this.plotlyArea && this.rawData.size > 0) {
            this.processData();
            Plotly.newPlot(this.plotlyArea, this.preparedData, this.layout, this.settings);
            this.plotlyArea.on('plotly_hover', (entry: any) => {
                if (entry.points.length === 1) {
                    this.onHighlight.emit({
                        internalId: entry.points[0].data.id,
                        dataIndex: entry.points[0].pointNumber
                    });
                }
            });
        }
    }

    private clearLayout() {
        // todo remove yaxis
        for (const key in this.layout) {
            if (this.layout.hasOwnProperty(key) && (key.startsWith('yaxis') || key.startsWith('xaxis'))) {
                delete this.layout[key];
            }
        }
        // reset counter
        this.counterYAxis = 0;
        this.counterXAxis = 0;
    }

    private clearData() {
        this.preparedData = [];
    }

    private redrawChart() {
        if (this.plotlyArea) {
            Plotly.relayout(this.plotlyArea, {});
        }
    }
}

interface ScatterData extends Partial<any> {
    id: string;
    timestamp: number;
}

interface Layout extends Partial<any> {
    [key: string]: any;
}

result-matching ""

    No results matching ""