File

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

Index

Properties

Properties

color
color: string
Type : string
first
first: boolean
Type : boolean
id
id: string
Type : string
offset
offset: number
Type : number
uom
uom: string
Type : string
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';
        }
    }

}

result-matching ""

    No results matching ""