File

libs/d3/src/lib/d3-trajectory-graph/d3-trajectory-graph.component.ts

Extends

DatasetPresenterComponent

Implements

AfterViewInit OnChanges

Metadata

encapsulation ViewEncapsulation.None
selector n52-d3-trajectory-graph
styleUrls ./d3-trajectory-graph.component.scss
templateUrl ./d3-trajectory-graph.component.html

Index

Properties
Methods
Inputs
Outputs
HostListeners

Constructor

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

Inputs

selection
Type : D3SelectionRange
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

onHoverHighlight
Type : EventEmitter<number>
onSelectionChanged
Type : EventEmitter<D3SelectionRange>
onSelectionChangedFinished
Type : EventEmitter<D3SelectionRange>
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
Protected calculateHeight
calculateHeight()
Returns : number
Protected calculateWidth
calculateWidth()
Returns : number
Protected createDataEntry
createDataEntry(internalId: string, entry: LocatedTimeValueEntry, previous: DataEntry, index: number)
Parameters :
Name Type Optional
internalId string No
entry LocatedTimeValueEntry No
previous DataEntry No
index number No
Returns : DataEntry
Protected datasetOptionsChanged
datasetOptionsChanged(internalId: string, options: DatasetOptions, firstChange: boolean)
Parameters :
Name Type Optional
internalId string No
options DatasetOptions No
firstChange boolean No
Returns : void
Protected distanceBetween
distanceBetween(latitude1, longitude1, latitude2, longitude2)
Parameters :
Name Optional
latitude1 No
longitude1 No
latitude2 No
longitude2 No
Returns : number
Protected drawDots
drawDots(values: DataEntry[], yScale: d3.ScaleLinear, options: DrawOptions)
Parameters :
Name Type Optional
values DataEntry[] No
yScale d3.ScaleLinear<number | number> No
options DrawOptions No
Returns : void
Protected drawDragRectangle
drawDragRectangle()
Returns : void
Protected drawGraph
drawGraph(yScale: d3.ScaleLinear, options: DrawOptions)
Parameters :
Name Type Optional
yScale d3.ScaleLinear<number | number> No
options DrawOptions No
Returns : void
Protected drawLineGraph
drawLineGraph()
Returns : void
Protected drawValueLine
drawValueLine(values: DataEntry[], yScale: d3.ScaleLinear, options: DrawOptions)
Parameters :
Name Type Optional
values DataEntry[] No
yScale d3.ScaleLinear<number | number> No
options DrawOptions No
Returns : void
Protected drawXAxis
drawXAxis(buffer: number)
Parameters :
Name Type Optional
buffer number No
Returns : void
Protected drawYAxis
drawYAxis(options: DrawOptions)
Parameters :
Name Type Optional
options DrawOptions No
Returns : any
Protected getDimensions
getDimensions(el: any)
Parameters :
Name Type Optional
el any No
Returns : { w: number; h: number; }
Protected getItemForX
getItemForX(x: number, data: DataEntry[])
Parameters :
Name Type Optional
x number No
data DataEntry[] No
Returns : any
Protected getXAxisLabel
getXAxisLabel()
Protected getXDomain
getXDomain(values: DataEntry[])
Parameters :
Name Type Optional
values DataEntry[] No
Returns : {}
Protected getXValue
getXValue(data: DataEntry)
Parameters :
Name Type Optional
data DataEntry No
Returns : any
Protected hideDiagramIndicator
hideDiagramIndicator()
Returns : void
Protected loadData
loadData(dataset: HelgolandTrajectory)
Parameters :
Name Type Optional
dataset HelgolandTrajectory No
Returns : void
Public ngAfterViewInit
ngAfterViewInit()
Returns : void
Public ngOnChanges
ngOnChanges(changes: SimpleChanges)
Parameters :
Name Type Optional
changes SimpleChanges No
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
Protected prepareRange
prepareRange(from: number, to: number)
Parameters :
Name Type Optional
from number No
to number No
Returns : D3SelectionRange
Protected presenterOptionsChanged
presenterOptionsChanged(options: D3GraphOptions)
Parameters :
Name Type Optional
options D3GraphOptions No
Returns : void
Protected processAllData
processAllData()
Returns : void
Protected processDataForId
processDataForId(internalId: string)
Parameters :
Name Type Optional
internalId string 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 resetDrag
resetDrag()
Returns : void
Protected setSelectedId
setSelectedId(internalId: string)
Parameters :
Name Type Optional
internalId string No
Returns : void
Protected showBottomIndicatorLabel
showBottomIndicatorLabel(item: DataEntry, onLeftSide: boolean)
Parameters :
Name Type Optional
item DataEntry No
onLeftSide boolean No
Returns : void
Protected showLabelValues
showLabelValues(item: DataEntry, onLeftSide: boolean)
Parameters :
Name Type Optional
item DataEntry No
onLeftSide boolean No
Returns : void
Protected showTimeIndicatorLabel
showTimeIndicatorLabel(item: DataEntry, onLeftSide: boolean)
Parameters :
Name Type Optional
item DataEntry No
onLeftSide boolean 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

Protected area
Type : d3.Area<DataEntry>
Protected background
Type : any
Protected baseValues
Type : DataEntry[]
Default value : []
Protected bufferSum
Type : number
Protected calcXValue
Default value : () => {...}
Protected calcYValue
Default value : () => {...}
Public d3Elem
Type : ElementRef
Decorators :
@ViewChild('dthree', {static: true})
Protected dataLength
Type : number
Protected datasetMap
Type : Map<string | DatasetConstellation>
Default value : new Map()
Protected defaultGraphOptions
Type : D3GraphOptions
Default value : { axisType: D3AxisType.Distance, dotted: false }
Protected dragCurrent
Type : [number, number]
Protected dragEndHandler
Default value : () => {...}
Protected dragging
Type : boolean
Protected dragHandler
Default value : () => {...}
Protected dragRect
Type : any
Protected dragRectG
Type : any
Protected dragStart
Type : [number, number]
Protected dragStartHandler
Default value : () => {...}
Protected focusG
Type : any
Protected focuslabelTime
Type : any
Protected focuslabelY
Type : any
Protected graph
Type : any
Protected height
Type : number
Protected highlightFocus
Type : any
Protected lineFun
Type : d3.Line<DataEntry>
Protected margin
Type : object
Default value : { top: 10, right: 10, bottom: 40, left: 40 }
Protected maxLabelwidth
Type : number
Default value : 0
Protected mousemoveHandler
Default value : () => {...}
Protected mouseoutHandler
Default value : () => {...}
Protected rawSvg
Type : any
Protected showDiagramIndicator
Default value : () => {...}
Protected width
Type : number
Protected xScaleBase
Type : d3.ScaleLinear<number | number>
Protected yAxisGen
Type : d3.Axis<number | literal type>
Protected yScaleBase
Type : d3.ScaleLinear<number | number>
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 {
    AfterViewInit,
    Component,
    ElementRef,
    EventEmitter,
    Input,
    IterableDiffers,
    OnChanges,
    Output,
    SimpleChanges,
    ViewChild,
    ViewEncapsulation,
} from '@angular/core';
import {
    DatasetOptions,
    DatasetPresenterComponent,
    DatasetType,
    HelgolandServicesConnector,
    HelgolandTrajectory,
    InternalIdHandler,
    LocatedTimeValueEntry,
    Time,
    TimezoneService,
} from '@helgoland/core';
import { LangChangeEvent, TranslateService } from '@ngx-translate/core';
import {
    area,
    axisBottom,
    axisLeft,
    axisRight,
    axisTop,
    bisector,
    curveLinear,
    extent,
    line,
    mouse,
    ScaleLinear,
    scaleLinear,
    select,
    timeFormat,
} from 'd3';
import moment from 'moment';

import { D3AxisType } from '../model/d3-axis-type';
import { D3GraphOptions } from '../model/d3-graph-options';
import { D3SelectionRange } from '../model/d3-selection-range';

interface DataEntry extends LocatedTimeValueEntry {
    dist: number;
    tick: number;
    x: number;
    y: number;
    xDiagCoord?: number;
    [id: string]: any;
}

interface DatasetConstellation {
    dataset?: HelgolandTrajectory;
    data?: LocatedTimeValueEntry[];
    yScale?: ScaleLinear<number, number>;
    drawOptions?: DrawOptions;
    focusLabelRect?: any;
    focusLabel?: any;
}

interface DrawOptions {
    uom: string;
    id: string;
    color: string;
    first: boolean;
    offset: number;
}

@Component({
    selector: 'n52-d3-trajectory-graph',
    templateUrl: './d3-trajectory-graph.component.html',
    styleUrls: ['./d3-trajectory-graph.component.scss'],
    encapsulation: ViewEncapsulation.None
})
export class D3TrajectoryGraphComponent
    extends DatasetPresenterComponent<DatasetOptions, D3GraphOptions>
    implements AfterViewInit, OnChanges {

    @Input()
    public selection: D3SelectionRange;

    @Output()
    public onSelectionChangedFinished: EventEmitter<D3SelectionRange> = new EventEmitter();

    @Output()
    public onSelectionChanged: EventEmitter<D3SelectionRange> = new EventEmitter();

    @Output()
    public onHoverHighlight: EventEmitter<number> = new EventEmitter();

    @ViewChild('dthree', { static: true })
    public d3Elem: ElementRef;

    protected datasetMap: Map<string, DatasetConstellation> = new Map();
    protected rawSvg: any;
    protected graph: any;
    protected height: number;
    protected width: number;
    protected margin = {
        top: 10,
        right: 10,
        bottom: 40,
        left: 40
    };
    protected maxLabelwidth = 0;
    protected lineFun: d3.Line<DataEntry>;
    protected area: d3.Area<DataEntry>;
    protected xScaleBase: d3.ScaleLinear<number, number>;
    protected yScaleBase: d3.ScaleLinear<number, number>;
    protected background: any;
    protected focusG: any;
    protected highlightFocus: any;
    protected focuslabelTime: any;
    protected focuslabelY: any;
    protected yAxisGen: d3.Axis<number | { valueOf(): number; }>;
    protected baseValues: DataEntry[] = [];
    protected dragging: boolean;
    protected dragStart: [number, number];
    protected dragCurrent: [number, number];
    protected dragRect: any;
    protected dragRectG: any;
    protected bufferSum: number;
    protected dataLength: number;

    protected defaultGraphOptions: D3GraphOptions = {
        axisType: D3AxisType.Distance,
        dotted: false
    };

    constructor(
        protected iterableDiffers: IterableDiffers,
        protected servicesConnector: HelgolandServicesConnector,
        protected datasetIdResolver: InternalIdHandler,
        protected timeSrvc: Time,
        protected translateService: TranslateService,
        protected timezoneSrvc: TimezoneService
    ) {
        super(iterableDiffers, servicesConnector, datasetIdResolver, timeSrvc, translateService, timezoneSrvc);
        this.presenterOptions = this.defaultGraphOptions;
    }

    public ngOnChanges(changes: SimpleChanges) {
        super.ngOnChanges(changes);
        if (changes.selection && this.selection) {
            this.processAllData();
            this.drawLineGraph();
        }
    }

    public ngAfterViewInit(): void {
        this.rawSvg = select(this.d3Elem.nativeElement)
            .append('svg')
            .attr('width', '100%')
            .attr('height', '100%');

        this.graph = this.rawSvg
            .append('g')
            .attr('transform', 'translate(' + (this.margin.left + this.maxLabelwidth) + ',' + this.margin.top + ')');

        this.lineFun = line<DataEntry>()
            .x(this.calcXValue)
            .y(this.calcYValue)
            .curve(curveLinear);

        this.area = area<DataEntry>()
            .x(this.calcXValue)
            .y0(this.height)
            .y1(this.calcYValue)
            .curve(curveLinear);

        this.drawLineGraph();
    }

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

    protected onLanguageChanged(langChangeEvent: LangChangeEvent): void { }

    protected onTimezoneChanged(timezone: string): void { }

    protected timeIntervalChanges(): void {
        this.datasetMap.forEach((entry) => {
            if (entry.dataset) {
                this.loadData(entry.dataset);
            }
        });
    }

    protected addDataset(id: string, url: string): void {
        this.servicesConnector.getDataset({ id, url }, { type: DatasetType.Trajectory })
            .subscribe(dataset => {
                this.datasetMap.set(dataset.internalId, { dataset });
                this.loadData(dataset);
            });
    }

    protected removeDataset(internalId: string): void {
        this.datasetMap.delete(internalId);
        this.processAllData();
        this.drawLineGraph();
    }

    protected setSelectedId(internalId: string): void {
        throw new Error('Method not implemented.');
    }

    protected removeSelectedId(internalId: string): void {
        throw new Error('Method not implemented.');
    }

    protected presenterOptionsChanged(options: D3GraphOptions): void {
        this.timeIntervalChanges();
    }

    protected datasetOptionsChanged(internalId: string, options: DatasetOptions, firstChange: boolean): void {
        if (!firstChange && this.datasetMap.has(internalId)) {
            this.loadData(this.datasetMap.get(internalId).dataset);
        }
    }

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

    protected loadData(dataset: HelgolandTrajectory) {
        if (this.timespan &&
            this.datasetOptions.has(dataset.internalId) &&
            this.datasetOptions.get(dataset.internalId).visible) {
            const buffer = this.timeSrvc.getBufferedTimespan(this.timespan, 0.2);
            const option = this.datasetOptions.get(dataset.internalId);
            this.servicesConnector.getDatasetData(dataset, buffer, { generalize: option.generalize })
                .subscribe((result) => {
                    this.dataLength = result.values.length;
                    this.datasetMap.get(dataset.internalId).data = result.values;
                    this.processDataForId(dataset.internalId);
                    this.drawLineGraph();
                });
        } else {
            this.drawLineGraph();
        }
    }

    protected processAllData() {
        this.baseValues = [];
        this.datasetIds.forEach((id) => this.processDataForId(id));
    }

    protected processDataForId(internalId: string) {
        const dataset = this.datasetMap.get(internalId);
        const options = this.datasetOptions.get(internalId);
        if (options.visible && dataset.data && dataset.data.length > 0) {
            const firstEntry = this.baseValues.length === 0;
            let previous: DataEntry = null;
            if (dataset && dataset.data && dataset.data.length >= 0) {
                dataset.data.forEach((elem, idx) => {
                    if (firstEntry) {
                        const entry = this.createDataEntry(internalId, elem, previous, idx);
                        if (this.selection) {
                            if (idx >= this.selection.from && idx <= this.selection.to) {
                                this.baseValues.push(entry);
                            }
                        } else {
                            this.baseValues.push(entry);
                        }
                        previous = entry;
                    } else {
                        if (this.selection) {
                            if (idx >= this.selection.from && idx <= this.selection.to) {
                                if (this.baseValues[idx - this.selection.from]) {
                                    this.baseValues[idx - this.selection.from][internalId] = elem.value;
                                }
                            }
                        } else {
                            if (this.baseValues[idx]) {
                                this.baseValues[idx][internalId] = elem.value;
                            }
                        }
                    }
                });
            }
        }
    }

    protected createDataEntry(
        internalId: string,
        entry: LocatedTimeValueEntry,
        previous: DataEntry,
        index: number
    ): DataEntry {
        let dist: number;
        if (previous) {
            const newdist = this.distanceBetween(
                entry.geometry.coordinates[1],
                entry.geometry.coordinates[0],
                previous.geometry.coordinates[1],
                previous.geometry.coordinates[0]
            );
            dist = previous.dist + Math.round(newdist / 1000 * 100000) / 100000;
        } else {
            dist = 0;
        }
        return {
            tick: index,
            dist: Math.round(dist * 10) / 10,
            timestamp: entry.timestamp,
            value: entry.value,
            [internalId]: entry.value,
            x: entry.geometry.coordinates[0],
            y: entry.geometry.coordinates[1],
            geometry: entry.geometry
        };
    }

    protected distanceBetween(latitude1, longitude1, latitude2, longitude2): number {
        const R = 6371000;
        const rad = Math.PI / 180;
        const lat1 = latitude1 * rad;
        const lat2 = latitude2 * rad;
        const sinDLat = Math.sin((latitude2 - latitude1) * rad / 2);
        const sinDLon = Math.sin((longitude2 - longitude1) * rad / 2);
        const a = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon;
        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return R * c;
    }

    protected calcYValue = (d: DataEntry) => {
        return this.yScaleBase(d.value);
    }

    protected calcXValue = (d: DataEntry, i: number) => {
        const xDiagCoord = this.xScaleBase(this.getXValue(d));
        d.xDiagCoord = xDiagCoord;
        return xDiagCoord;
    }

    protected calculateHeight(): number {
        return (this.d3Elem.nativeElement as HTMLElement).clientHeight - this.margin.top - this.margin.bottom;
    }

    protected calculateWidth(): number {
        return (this.d3Elem.nativeElement as HTMLElement).clientWidth - this.margin.left - this.margin.right - this.maxLabelwidth;
    }

    protected getXValue(data: DataEntry) {
        switch (this.presenterOptions.axisType) {
            case D3AxisType.Distance:
                return data.dist;
            case D3AxisType.Time:
                return data.timestamp;
            case D3AxisType.Ticks:
                return data.tick;
            default:
                return data.tick;
        }
    }

    protected drawDots(values: DataEntry[], yScale: d3.ScaleLinear<number, number>, options: DrawOptions) {
        this.graph.selectAll('dot')
            .data(values)
            .enter().append('circle')
            .attr('stroke', options.color)
            .attr('r', 1.5)
            .attr('fill', options.color)
            .attr('cx', this.calcXValue)
            .attr('cy', (d: DataEntry) => yScale(d[options.id]));
    }

    protected drawValueLine(values: DataEntry[], yScale: d3.ScaleLinear<number, number>, options: DrawOptions) {
        this.graph.append('svg:path')
            .datum(values)
            .attr('class', 'line')
            .attr('fill', 'none')
            .attr('stroke', options.color)
            .attr('stroke-width', 1)
            .attr('d', line<DataEntry>()
                .x(this.calcXValue)
                .y((d: DataEntry) => yScale(d[options.id]))
                .curve(curveLinear));
    }

    protected drawGraph(yScale: d3.ScaleLinear<number, number>, options: DrawOptions) {
        if (this.presenterOptions.dotted) {
            this.drawDots(this.baseValues, yScale, options);
        } else {
            this.drawValueLine(this.baseValues, yScale, options);
        }
    }

    protected drawLineGraph() {
        if (!this.baseValues || this.baseValues.length === 0 || !this.graph) {
            return;
        }

        this.height = this.calculateHeight();
        this.width = this.calculateWidth();

        this.graph.selectAll('*').remove();

        this.bufferSum = 0;

        this.yScaleBase = null;

        this.datasetMap.forEach((datasetEntry, id) => {
            if (this.datasetOptions.has(id) && datasetEntry.data && datasetEntry.data.length > 0 && this.datasetOptions.get(id).visible) {
                datasetEntry.drawOptions = {
                    uom: datasetEntry.dataset.uom,
                    id: datasetEntry.dataset.internalId,
                    color: this.datasetOptions.get(id).color,
                    first: this.yScaleBase === null,
                    offset: this.bufferSum
                };
                const axisResult = this.drawYAxis(datasetEntry.drawOptions);
                if (this.yScaleBase === null) {
                    this.yScaleBase = axisResult.yScale;
                } else {
                    this.bufferSum = axisResult.buffer;
                }
                datasetEntry.yScale = axisResult.yScale;
            }
        });

        if (!this.yScaleBase) {
            return;
        }

        // draw right axis as border
        this.graph.append('svg:g')
            .attr('class', 'y axis')
            .attr('transform', 'translate(' + this.width + ', 0)')
            .call(axisRight(this.yScaleBase).tickSize(0).ticks(0));

        this.drawXAxis(this.bufferSum);

        this.datasetMap.forEach((entry, id) => {
            if (this.datasetOptions.has(id) && this.datasetOptions.get(id).visible && entry.data && entry.data.length > 0) {
                this.drawGraph(entry.yScale, entry.drawOptions);
            }
        });

        this.background = this.graph.append('svg:rect')
            .attr('width', this.width - this.bufferSum)
            .attr('height', this.height)
            .attr('fill', 'none')
            .attr('stroke', 'none')
            .attr('pointer-events', 'all')
            .attr('transform', 'translate(' + this.bufferSum + ', 0)')
            .on('mousemove.focus', this.mousemoveHandler)
            .on('mouseout.focus', this.mouseoutHandler)
            .on('mousedown.drag', this.dragStartHandler)
            .on('mousemove.drag', this.dragHandler)
            .on('mouseup.drag', this.dragEndHandler);

        this.focusG = this.graph.append('g');
        this.highlightFocus = this.focusG.append('svg:line')
            .attr('class', 'mouse-focus-line')
            .attr('x2', '0')
            .attr('y2', '0')
            .attr('x1', '0')
            .attr('y1', '0')
            .style('stroke', 'black')
            .style('stroke-width', '1px');

        this.datasetMap.forEach((entry, id) => {
            if (this.datasetOptions.has(id) && this.datasetOptions.get(id).visible && entry.data) {
                entry.focusLabelRect = this.focusG.append('svg:rect')
                    .style('fill', 'white')
                    .style('stroke', 'none')
                    .style('pointer-events', 'none');
                entry.focusLabel = this.focusG.append('svg:text').attr('class', 'mouse-focus-label-x')
                    .style('pointer-events', 'none')
                    .style('fill', this.datasetOptions.get(id).color)
                    .style('font-weight', 'lighter');
            }
        });

        this.focuslabelTime = this.focusG.append('svg:text')
            .style('pointer-events', 'none')
            .attr('class', 'mouse-focus-label-x');
        this.focuslabelY = this.focusG.append('svg:text')
            .style('pointer-events', 'none')
            .attr('class', 'mouse-focus-label-y');
    }

    protected mousemoveHandler = () => {
        if (!this.baseValues || this.baseValues.length === 0) {
            return;
        }
        const coords = mouse(this.background.node());
        const idx = this.getItemForX(coords[0] + this.bufferSum, this.baseValues);
        this.showDiagramIndicator(idx);
        this.onHoverHighlight.emit(this.baseValues[idx].tick);
    }

    protected mouseoutHandler = () => {
        this.hideDiagramIndicator();
    }

    protected dragStartHandler = () => {
        this.dragging = false;
        this.dragStart = mouse(this.background.node());
    }

    protected dragHandler = () => {
        this.dragging = true;
        this.drawDragRectangle();
    }

    protected dragEndHandler = () => {
        if (!this.dragStart || !this.dragging) {
            this.onSelectionChangedFinished.emit({ from: 0, to: this.dataLength });
        } else {
            const from = this.getItemForX(this.dragStart[0] + this.bufferSum, this.baseValues);
            const to = this.getItemForX(this.dragCurrent[0] + this.bufferSum, this.baseValues);
            this.onSelectionChangedFinished.emit(this.prepareRange(this.baseValues[from].tick, this.baseValues[to].tick));
        }
        this.dragStart = null;
        this.dragging = false;
        this.resetDrag();
    }

    protected prepareRange(from: number, to: number): D3SelectionRange {
        if (from <= to) {
            return { from, to };
        }
        return { from: to, to: from };
    }

    protected drawDragRectangle() {
        if (!this.dragStart) { return; }

        this.dragCurrent = mouse(this.background.node());

        const from = this.getItemForX(this.dragStart[0] + this.bufferSum, this.baseValues);
        const to = this.getItemForX(this.dragCurrent[0] + this.bufferSum, this.baseValues);
        this.onSelectionChanged.emit(this.prepareRange(this.baseValues[from].tick, this.baseValues[to].tick));

        const x1 = Math.min(this.dragStart[0], this.dragCurrent[0]);
        const x2 = Math.max(this.dragStart[0], this.dragCurrent[0]);

        if (!this.dragRect && !this.dragRectG) {

            this.dragRectG = this.graph.append('g');

            this.dragRect = this.dragRectG.append('rect')
                .attr('width', x2 - x1)
                .attr('height', this.height)
                .attr('x', x1 + this.bufferSum)
                .attr('class', 'mouse-drag')
                .style('pointer-events', 'none');
        } else {
            this.dragRect.attr('width', x2 - x1)
                .attr('x', x1 + this.bufferSum);
        }
    }

    protected resetDrag() {
        if (this.dragRectG) {
            this.dragRectG.remove();
            this.dragRectG = null;
            this.dragRect = null;
        }
    }

    protected hideDiagramIndicator() {
        this.focusG.style('visibility', 'hidden');
    }

    protected showDiagramIndicator = (idx: number) => {
        const item = this.baseValues[idx];
        this.focusG.style('visibility', 'visible');
        this.highlightFocus.attr('x1', item.xDiagCoord)
            .attr('y1', 0)
            .attr('x2', item.xDiagCoord)
            .attr('y2', this.height)
            .classed('hidden', false);

        let onLeftSide = false;
        if ((this.background.node().getBBox().width + this.bufferSum) / 2 > item.xDiagCoord) { onLeftSide = true; }

        this.showLabelValues(item, onLeftSide);
        this.showTimeIndicatorLabel(item, onLeftSide);
        this.showBottomIndicatorLabel(item, onLeftSide);
    }

    protected showLabelValues(item: DataEntry, onLeftSide: boolean) {
        this.datasetMap.forEach((entry, id) => {
            if (this.datasetOptions.get(id).visible) {
                if (entry.focusLabel && entry.yScale && item[id]) {
                    entry.focusLabel.text(item[id] + (entry.dataset.uom ? entry.dataset.uom : ''));
                    const entryX = onLeftSide ?
                        item.xDiagCoord + 2 : item.xDiagCoord - this.getDimensions(entry.focusLabel.node()).w;
                    entry.focusLabel
                        .attr('x', entryX)
                        .attr('y', entry.yScale(item[id]) + this.getDimensions(entry.focusLabel.node()).h - 3);
                    entry.focusLabelRect
                        .attr('x', entryX)
                        .attr('y', entry.yScale(item[id]))
                        .attr('width', this.getDimensions(entry.focusLabel.node()).w)
                        .attr('height', this.getDimensions(entry.focusLabel.node()).h);
                }
            }
        });
    }

    protected showTimeIndicatorLabel(item: DataEntry, onLeftSide: boolean) {
        this.focuslabelTime.text(moment(item.timestamp).format('DD.MM.YY HH:mm'));
        this.focuslabelTime
            .attr('x', onLeftSide ? item.xDiagCoord + 2 : item.xDiagCoord - this.getDimensions(this.focuslabelTime.node()).w)
            .attr('y', 13);
    }

    protected showBottomIndicatorLabel(item: DataEntry, onLeftSide: boolean) {
        if (this.presenterOptions.axisType === D3AxisType.Distance) {
            this.focuslabelY.text(item.dist + ' km');
        }
        if (this.presenterOptions.axisType === D3AxisType.Ticks) {
            this.focuslabelY.text('Measurement: ' + item.tick);
        }
        this.focuslabelY
            .attr('y', this.calculateHeight() - 5)
            .attr('x', onLeftSide ? item.xDiagCoord + 2 : item.xDiagCoord - this.getDimensions(this.focuslabelY.node()).w);
    }

    protected getDimensions(el: any) {
        let w = 0;
        let h = 0;
        if (el) {
            const dimensions = el.getBBox();
            w = dimensions.width;
            h = dimensions.height;
        } else {
            console.log('error: getDimensions() ' + el + ' not found.');
        }
        return {
            w,
            h
        };
    }

    protected getItemForX(x: number, data: DataEntry[]) {
        const index = this.xScaleBase.invert(x);
        const bisectDate = bisector((d: DataEntry) => {
            switch (this.presenterOptions.axisType) {
                case D3AxisType.Distance:
                    return d.dist;
                case D3AxisType.Time:
                    return d.timestamp;
                case D3AxisType.Ticks:
                default:
                    return d.tick;
            }
        }).left;
        return bisectDate(this.baseValues, index);
    }

    protected drawYAxis(options: DrawOptions): any {
        const range = extent<DataEntry, number>(this.baseValues, (datum, index, array) => {
            return datum[options.id]; // here with ID
        });
        const rangeOffset = (range[1] - range[0]) * 0.10;
        const yScale = scaleLinear()
            .domain([range[0] - rangeOffset, range[1] + rangeOffset])
            .range([this.height, 0]);

        this.yAxisGen = axisLeft(yScale).ticks(5);

        // draw y axis
        const axis = this.graph.append('svg:g')
            .attr('class', 'y axis')
            .call(this.yAxisGen);

        // draw y axis label
        const text = this.graph.append('text')
            .attr('transform', 'rotate(-90)')
            .attr('dy', '1em')
            .style('text-anchor', 'middle')
            .style('fill', options.color)
            .text(options.uom);

        const axisWidth = axis.node().getBBox().width + 5 + this.getDimensions(text.node()).h;
        const buffer = options.offset + (axisWidth < 30 ? 30 : axisWidth);
        if (!options.first) {
            axis.attr('transform', 'translate(' + buffer + ', 0)');
        }

        const textOffset = !options.first ? buffer : options.offset;
        text.attr('y', 0 - this.margin.left - this.maxLabelwidth + textOffset)
            .attr('x', 0 - (this.height / 2));

        // draw the y grid lines when there is only one dataset
        if (this.datasetIds.length === 1) {
            this.graph.append('svg:g')
                .attr('class', 'grid')
                .call(axisLeft(yScale)
                    .ticks(5)
                    .tickSize(-this.width)
                    .tickFormat(() => '')
                );
        }

        return {
            buffer,
            yScale
        };
    }

    protected drawXAxis(buffer: number) {
        this.xScaleBase = scaleLinear()
            .domain(this.getXDomain(this.baseValues))
            .range([buffer, this.width]);

        const xAxisGen = axisBottom(this.xScaleBase).ticks(5);

        if (this.presenterOptions.axisType === D3AxisType.Time) {
            xAxisGen.tickFormat((d) => {
                return timeFormat('%d.%m.%Y %H:%M:%S')(new Date(d.valueOf()));
            });
        }

        // draw x axis
        this.graph.append('svg:g')
            .attr('class', 'x axis')
            .attr('transform', 'translate(0,' + this.height + ')')
            .call(xAxisGen);

        // draw the x grid lines
        this.graph.append('svg:g')
            .attr('class', 'grid')
            .attr('transform', 'translate(0,' + this.height + ')')
            .call(axisBottom(this.xScaleBase)
                .ticks(10)
                .tickSize(-this.height)
                .tickFormat(() => '')
            );

        // draw upper axis as border
        this.graph.append('svg:g')
            .attr('class', 'x axis')
            .call(axisTop(this.xScaleBase).ticks(0).tickSize(0));

        // text label for the x axis
        this.graph.append('text')
            .attr('x', (this.width + buffer) / 2)
            .attr('y', this.height + this.margin.bottom - 5)
            .style('text-anchor', 'middle')
            .text(this.getXAxisLabel());
    }

    protected getXDomain(values: DataEntry[]) {
        switch (this.presenterOptions.axisType) {
            case D3AxisType.Distance:
                return [values[0].dist, values[values.length - 1].dist];
            case D3AxisType.Time:
                return [values[0].timestamp, values[values.length - 1].timestamp];
            default:
                return [values[0].tick, values[values.length - 1].tick];
        }
    }

    protected getXAxisLabel() {
        switch (this.presenterOptions.axisType) {
            case D3AxisType.Distance:
                return 'Distance';
            case D3AxisType.Time:
                return 'Time';
            default:
                return 'Ticks';
        }
    }

}
<div class="d3" #dthree></div>

./d3-trajectory-graph.component.scss

$drag-color: rgba(0, 0, 255, 0.40);
.d3 {
    height: 100%;
    .axis {
        line,
        path {
            fill: none;
            stroke: black;
        }
    }
    text {
        font-size: 14px;
    }
    .graphArea {
        fill: lightsteelblue;
        fill-opacity: 0.7;
    }
    .grid .tick line {
        stroke: lightgrey;
        stroke-opacity: 0.7;
        shape-rendering: crispEdges;
    }
    .map-highlight-label {
        fill: white;
        fill-opacity: 0.7;
    }
    .mouse-focus-line {
        pointer-events: none;
        stroke-width: 1px;
        stroke: black;
    }
    .mouse-drag {
        fill: $drag-color;
        pointer-events: all;
        cursor: move;
    }
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""