File

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

Implements

AfterViewInit OnChanges

Metadata

selector n52-d3-general-graph
styleUrls ./d3-general-graph.component.scss
templateUrl ./d3-general-graph.component.html

Index

Properties
Methods
Inputs

Constructor

constructor(timeFormatLocaleService: D3TimeFormatLocaleService)
Parameters :
Name Type Optional
timeFormatLocaleService D3TimeFormatLocaleService No

Inputs

generalD3Input
Type : D3GeneralInput

Methods

Private calcDistanceHovering
calcDistanceHovering(dataset: D3GeneralDataPoint, coords: [number, number])

Function to calculate distance between mouse and a hovered point.

Parameters :
Name Type Optional Description
dataset D3GeneralDataPoint No

Coordinates of the hovered point.

coords [number, number] No

Coordinates of the mouse.

Returns : number
Private calculateHeight
calculateHeight()

Function that returns the height of the graph diagram.

Returns : number
Private calculateWidth
calculateWidth()

Function that returns the width of the graph diagram.

Returns : number
Private createHoveringNet
createHoveringNet(inputData)

Function to create a net of polygons overlaying the graphs to divide sections for hovering.

Parameters :
Name Optional Description
inputData No

data containing an array with all datapoints and an id for each dataset

Returns : void
Private drawGraphLine
drawGraphLine(dataset: D3GeneralDataset)

Function to draw the line of the graph.

Parameters :
Name Type Optional Description
dataset D3GeneralDataset No

Object with information about the datset.

Returns : void
Private drawXaxis
drawXaxis(options: D3GeneralPlotOptions)

Function to draw x axis.

Parameters :
Name Type Optional
options D3GeneralPlotOptions No
Returns : any
Private drawYaxis
drawYaxis(options: D3GeneralPlotOptions)

Function to draw y axis.

Parameters :
Name Type Optional
options D3GeneralPlotOptions No
Returns : any
Private getDimensions
getDimensions(el: any)

Function that returns the boundings of a html element.

Parameters :
Name Type Optional Description
el any No

Object of the html element.

Returns : literal type
Private getRange
getRange(data: D3GeneralDataPoint[], selector: string)
Parameters :
Name Type Optional
data D3GeneralDataPoint[] No
selector string No
Returns : Range
ngAfterViewInit
ngAfterViewInit()
Returns : void
ngOnChanges
ngOnChanges(changes)
Parameters :
Name Optional
changes No
Returns : void
Private plotGraph
plotGraph()

Function to call functions related to plotting a dataset in a graph.

Returns : void
Private prepareData
prepareData()
Returns : void

Properties

Private axisOptions
Type : D3GeneralAxisOptions
Default value : {}
Private background
Type : any
Private buffer
Type : number
Default value : 0
Public d3Elem
Type : ElementRef
Decorators :
@ViewChild('d3general', {static: true})
Private defaultGraphOptions
Type : D3GeneralGraphOptions
Default value : { color: 'red', lines: { lineWidth: 2, pointRadius: 2 } }
Private focusG
Type : any
Private generalData
Type : D3GeneralDataset[]
Default value : []
Private graph
Type : any
Private graphBody
Type : any
Private graphFocus
Type : any
Private height
Type : number
Private highlightRect
Type : any
Private highlightText
Type : any
Private margin
Type : object
Default value : { top: 10, right: 10, bottom: 40, left: 10 }
Private maxLabelwidth
Type : number
Default value : 0
Private plotOptions
Type : D3GeneralPlotOptions
Default value : { xlabel: 'x', ylabel: 'y', date: false }
Private rawSvg
Type : any
Private width
Type : number
import { AfterViewInit, Component, ElementRef, Input, OnChanges, ViewChild } from '@angular/core';
import * as d3 from 'd3';
import moment from 'moment';

import { D3TimeFormatLocaleService } from '../helper/d3-time-format-locale.service';
import {
    D3GeneralAxisOptions,
    D3GeneralDataPoint,
    D3GeneralDataset,
    D3GeneralGraphOptions,
    D3GeneralInput,
    D3GeneralPlotOptions,
    Range,
} from '../model/d3-general';

@Component({
    selector: 'n52-d3-general-graph',
    templateUrl: './d3-general-graph.component.html',
    styleUrls: ['./d3-general-graph.component.scss']
})
export class D3GeneralGraphComponent implements AfterViewInit, OnChanges {

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

    @Input()
    public generalD3Input: D3GeneralInput;

    // componennt data variables
    private generalData: D3GeneralDataset[] = [];
    private axisOptions: D3GeneralAxisOptions = {};
    private plotOptions: D3GeneralPlotOptions = {
        xlabel: 'x',
        ylabel: 'y',
        date: false
    };

    private defaultGraphOptions: D3GeneralGraphOptions = {
        color: 'red',
        lines: {
            lineWidth: 2,
            pointRadius: 2
        }
    };

    // graph components
    private rawSvg: any;
    private graph: any;
    private graphBody: any;
    private background: any;
    private graphFocus: any;
    private focusG: any;
    private highlightRect: any;
    private highlightText: any;

    // component settings
    private height: number;
    private width: number;
    private buffer = 0;
    private maxLabelwidth = 0;

    private margin = {
        top: 10,
        right: 10,
        bottom: 40,
        left: 10
    };

    constructor(
        protected timeFormatLocaleService: D3TimeFormatLocaleService
    ) { }

    ngAfterViewInit() {
        this.rawSvg = d3.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.margin.top + ')');

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


        this.prepareData();
    }

    ngOnChanges(changes) {
        if (changes.generalD3Input && this.rawSvg) {
            this.generalD3Input = changes.generalD3Input.currentValue;
            this.prepareData();
        }
    }

    private prepareData() {
        if (this.generalD3Input) {
            // add all input dataset into one array (public generalData)
            let data = [];

            this.generalD3Input.datasets.forEach((ds, index) => {
                const dataset: D3GeneralDataset = {
                    data: ds.data,
                    id: index
                };
                data = data.concat(ds.data);
                this.generalData.push(dataset);
            });

            this.plotOptions = this.generalD3Input.plotOptions;
            this.axisOptions.date = true;
            this.axisOptions.xRange = this.getRange(data, 'x');
            this.axisOptions.yRange = this.getRange(data, 'y');

            this.plotGraph();
        }
    }

    /**
     * Function to call functions related to plotting a dataset in a graph.
     */
    private plotGraph() {
        this.height = this.calculateHeight();
        this.width = this.calculateWidth();

        this.axisOptions.yScale = this.drawYaxis(this.plotOptions);
        this.axisOptions.xScale = this.drawXaxis(this.plotOptions);

        // create background as rectangle providing panning
        this.background = this.graph.append('svg:rect')
            .attr('width', this.width - this.buffer)
            .attr('height', this.height)
            .attr('id', 'backgroundRect')
            .attr('fill', 'none')
            .attr('stroke', 'none')
            .attr('pointer-events', 'all')
            .attr('transform', 'translate(' + this.buffer + ', 0)');


        this.focusG = this.graphFocus.append('g');
        this.highlightRect = this.focusG.append('svg:rect');
        this.highlightText = this.focusG.append('svg:text');

        this.generalData.forEach(dataset => {
            this.drawGraphLine(dataset);
        });

        this.createHoveringNet(this.generalData);
        this.createHoveringNet(this.generalData);
    }

    /**
     * Function to draw y axis.
     * @param dataset {D3GeneralDataset} Object with information about the dataset.
     */
    private drawYaxis(options: D3GeneralPlotOptions) {

        // set range offset for y axis scale
        let yRangeOffset = 10;
        const yRange = this.axisOptions.yRange;
        // check for multiple datapoints
        if (yRange.max !== yRange.min) {
            yRangeOffset = (yRange.max - yRange.min) * 0.10;
        } else {
            yRangeOffset = yRange.min * 0.10;
        }

        const yScale = d3.scaleLinear()
            .domain([yRange.min - yRangeOffset, yRange.max + yRangeOffset])
            .range([this.height, 0]);

        const yAxisGen = d3.axisLeft(yScale).ticks(5);

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

        // draw y axis label
        const yAxisLabel = this.graph.append('text')
            // .attr('transform', 'rotate(-90)')
            .attr('transform', 'translate(0, ' + this.height / 2 + ')rotate(-90)')
            .attr('dy', '1em')
            .attr('class', 'yAxisTextLabel')
            .style('font', '18px times')
            .style('text-anchor', 'middle')
            .style('fill', 'black')
            .text(options.ylabel);

        // this.graph.selectAll('.yAxisTextLabel')
        this.buffer = yAxis.node().getBBox().width + 10 + this.getDimensions(yAxisLabel.node()).h;

        yAxis.attr('transform', 'translate(' + this.buffer + ', 0)');

        // draw y grid lines
        this.graph.append('svg:g')
            .attr('class', 'grid')
            .attr('transform', 'translate(' + this.buffer + ', 0)')
            .call(d3.axisLeft(yScale)
                .ticks(5)
                .tickSize(-this.width + this.buffer)
                .tickFormat(() => '')
            );

        return yScale;
    }

    /**
     * Function to draw x axis.
     * @param dataset {D3GeneralDataset} Object with information about the dataset.
     */
    private drawXaxis(options: D3GeneralPlotOptions) {
        // set range offset for x axis scale
        const xRange = this.axisOptions.xRange;
        // check for multiple datapoints
        let ticks = 10;
        let xRangeOffset = (xRange.max - xRange.min) * 0.10;
        if (xRange.max === xRange.min) {
            ticks = 5;
            xRangeOffset = xRange.min * 0.10;
        }

        const xScale = d3.scaleLinear()
            .domain([xRange.min - xRangeOffset, xRange.max + xRangeOffset])
            .range([this.buffer, this.width]);

        const xAxis = d3.axisBottom(xScale)
            .ticks(ticks)
            .tickFormat(d => {
                if (options.date) {
                    return this.timeFormatLocaleService.formatTime(d.valueOf());
                } else {
                    return '' + d.valueOf();
                }
            });

        this.graph.append('g')
            .attr('class', 'x axis')
            .attr('transform', 'translate(0,' + this.height + ')')
            .call(xAxis)
            .selectAll('text')
            .style('text-anchor', 'middle');

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

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

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

        return xScale;
    }

    /**
     * Function to draw the line of the graph.
     * @param dataset {D3GeneralDataset} Object with information about the datset.
     */
    private drawGraphLine(dataset: D3GeneralDataset) {
        // create grah line component
        this.graphBody = this.graph
            .append('g')
            .attr('clip-path', 'url(#' + dataset.id + ')');

        // create line with dataset
        const graphLine = d3.line<D3GeneralDataPoint>()
            .defined(d => (!isNaN(d.x) && !isNaN(d.y)))
            .x((d) => {
                const xCoord = this.axisOptions.xScale(d.x);
                if (!isNaN(xCoord)) {
                    d.xCoord = xCoord;
                    return xCoord;
                }
            })
            .y((d) => {
                const yCoord = this.axisOptions.yScale(d.y);
                if (!isNaN(yCoord)) {
                    d.yCoord = yCoord;
                    return yCoord;
                }
            })
            .curve(d3.curveLinear);

        this.graphBody
            .append('svg:path')
            .datum(dataset.data)
            .attr('class', 'line')
            .attr('fill', 'none')
            .attr('stroke', this.plotOptions.graph ? this.plotOptions.graph.color : this.defaultGraphOptions.color)
            .attr('stroke-width', this.plotOptions.graph ? this.plotOptions.graph.lines.lineWidth : this.defaultGraphOptions.lines.lineWidth)
            .attr('d', graphLine);

        // draw circles around datapoints
        this.graphBody.selectAll('.graphDots')
            .data(dataset.data.filter((d) => !isNaN(d.y)))
            .enter().append('circle')
            .attr('class', 'graphDots')
            .attr('id', function (d) {
                const datasetxCoordSplit = d.xCoord.toString().split('.')[0] + '-' + d.xCoord.toString().split('.')[1];
                return 'dot-' + datasetxCoordSplit + '-' + dataset.id + '';
            })
            .attr('stroke', this.plotOptions.graph ? this.plotOptions.graph.color : this.defaultGraphOptions.color)
            .attr('fill', this.plotOptions.graph ? this.plotOptions.graph.color : this.defaultGraphOptions.color)
            .attr('cx', graphLine.x())
            .attr('cy', graphLine.y())
            .attr('r', this.plotOptions.graph ? this.plotOptions.graph.lines.pointRadius : this.defaultGraphOptions.lines.pointRadius);

    }

    /**
     * Function to create a net of polygons overlaying the graphs to divide sections for hovering.
     * @param inputData {D3GeneralDataset[]} data containing an array with all datapoints and an id for each dataset
     */
    private createHoveringNet(inputData): void {
        const data = inputData.map(function (series, i) {
            series.data = series.data.map(function (point) {
                point.series = i;
                point[0] = point.x;
                point[1] = point.y;
                return point;
            });
            return series;
        });

        const x = d3.scaleLinear(),
            y = d3.scaleLinear();

        const vertices: [number, number][] = d3.merge(data.map(function (cl, lineIndex) {
            /**
             * cl = { data: [{0: number, 1: number, series: number, x: number, y: number}, {}, ...], id: number }
             * point = each point in a dataset
            */
            const outputLine = cl.data.map(function (point, pointIndex) {
                const outputPoint = [x(point.xCoord), y(point.yCoord), lineIndex, pointIndex, point, cl];
                return outputPoint; // adding series index to point because data is being flattened
            });
            return outputLine;
        }));

        const left = this.buffer, // + this.margin.left,
            top = this.margin.top,
            right = this.background.node().getBBox().width + this.buffer, // + this.margin.left,
            bottom = this.margin.top + this.background.node().getBBox().height;

        // filter dataset - delete all entries that are NaN
        const verticesFiltered = vertices.filter(d => !isNaN(d[0]) || !isNaN(d[1]));
        const Diffvoronoi = d3.voronoi()
            .extent([[left, top], [right, bottom]]);
        const diffVoronoi2 = Diffvoronoi.polygons(verticesFiltered);

        const wrap = this.rawSvg.selectAll('g.d3line').data([verticesFiltered]);
        const gEnter = wrap.enter().append('g').attr('class', 'd3line').append('g');
        gEnter.append('g').attr('class', 'point-paths');

        // to avoid no hovering for only one dataset without interaction the following lines are doubled
        // this will create the paths, which can be updated later on (by the 'exit().remove()' function calls)
        let pointPaths = wrap.select('.point-paths').selectAll('path')
            .data(diffVoronoi2);
        pointPaths
            .enter().append('path')
            .attr('class', function (d, i) {
                return 'path-' + i;
            });

        pointPaths = wrap.select('.point-paths').selectAll('path')
            .data(diffVoronoi2);
        pointPaths
            .enter().append('path')
            .attr('class', function (d, i) {
                return 'path-' + i;
            });
        pointPaths.exit().remove();
        pointPaths
            .attr('clip-path', function (d) {
                if (d !== undefined) {
                    const datasetxCoordSplit = d.data[4].xCoord.toString().split('.')[0] + '-' + d.data[4].xCoord.toString().split('.')[1];
                    return 'url(#clip-' + d.data[5].id + '-' + datasetxCoordSplit + ')';
                }
            })
            .attr('d', function (d) {
                if (d !== undefined) {
                    return 'M' + d.join(' ') + 'Z';
                }
            })
            .attr('transform', 'translate(' + this.margin.left + ', ' + this.margin.top + ')')
            .on('mousemove', (d) => {
                if (d !== undefined) {
                    const coords = d3.mouse(this.background.node());
                    const dataset = d.data[4];
                    const dist = this.calcDistanceHovering(dataset, coords);
                    const radius = this.plotOptions.graph ? this.plotOptions.graph.lines.pointRadius : this.defaultGraphOptions.lines.pointRadius;
                    const color = this.plotOptions.graph ? this.plotOptions.graph.color : this.defaultGraphOptions.color;
                    if (dist <= 8) {
                        const rectBack = this.background.node().getBBox();
                        if (coords[0] >= 0 && coords[0] <= rectBack.width && coords[1] >= 0 && coords[1] <= rectBack.height) {
                            // highlight hovered dot
                            const datasetxCoordSplit = dataset.xCoord.toString().split('.')[0] + '-' + dataset.xCoord.toString().split('.')[1];
                            d3.select('#dot-' + datasetxCoordSplit + '-' + d.data[5].id + '')
                                .attr('opacity', 0.8)
                                .attr('r', (radius * 2));

                            this.highlightRect
                                .style('visibility', 'visible');
                            this.highlightText
                                .style('visibility', 'visible');

                            // create text for hovering label
                            const text = this.plotOptions.date ? 'x: ' + moment(dataset.x).format('DD.MM.YY HH:mm') + ' y: ' + dataset.y : 'x: ' + dataset.x + ' y: ' + dataset.y;
                            const dotLabel = this.highlightText
                                .text(text)
                                .attr('class', 'mouseHoverDotLabel')
                                .style('pointer-events', 'none')
                                .style('fill', color);

                            let onLeftSide = false;
                            if ((this.background.node().getBBox().width + this.buffer) / 2 > coords[0]) { onLeftSide = true; }

                            let rectX: number = dataset.xCoord + 15;
                            let rectY: number = dataset.yCoord;
                            const rectW: number = this.getDimensions(dotLabel.node()).w + 8;
                            const rectH: number = this.getDimensions(dotLabel.node()).h; // + 4;

                            if (!onLeftSide) {
                                rectX = dataset.xCoord - 15 - rectW;
                                rectY = dataset.yCoord;
                            }

                            if ((coords[1] + rectH + 4) > this.background.node().getBBox().height) {
                                // when label below x axis
                                console.log('Translate label to a higher place. - not yet implemented');
                            }

                            // create hovering label
                            const dotRectangle = this.highlightRect
                                .attr('class', 'mouseHoverDotRect')
                                .style('fill', 'white')
                                .style('fill-opacity', 1)
                                .style('stroke', color)
                                .style('stroke-width', '1px')
                                .style('pointer-events', 'none')
                                .attr('width', rectW)
                                .attr('height', rectH)
                                .attr('transform', 'translate(' + rectX + ', ' + rectY + ')');

                            let labelX: number = dataset.xCoord + 4 + 15;
                            let labelY: number = dataset.yCoord + this.getDimensions(dotRectangle.node()).h - 4;

                            if (!onLeftSide) {
                                labelX = dataset.xCoord - rectW + 4 - 15;
                                labelY = dataset.yCoord + this.getDimensions(dotRectangle.node()).h - 4;
                            }

                            this.highlightText
                                .attr('transform', 'translate(' + labelX + ', ' + labelY + ')');
                        }
                    } else {
                        // unhighlight hovered dot
                        const datasetxCoordSplit = dataset.xCoord.toString().split('.')[0] + '-' + dataset.xCoord.toString().split('.')[1];
                        d3.select('#dot-' + datasetxCoordSplit + '-' + d.data[5].id + '')
                            .attr('opacity', 1)
                            .attr('r', radius);

                        // make label invisible
                        this.highlightRect
                            .style('visibility', 'hidden');
                        this.highlightText
                            .style('visibility', 'hidden');
                    }
                }
            })
            .on('mouseout', (d) => {
                if (d !== undefined) {
                    const dataset = d.data[4];
                    const radius = this.plotOptions.graph ? this.plotOptions.graph.lines.pointRadius : this.defaultGraphOptions.lines.pointRadius;
                    // unhighlight hovered dot
                    const datasetxCoordSplit = dataset.xCoord.toString().split('.')[0] + '-' + dataset.xCoord.toString().split('.')[1];
                    d3.select('#dot-' + datasetxCoordSplit + '-' + d.data[5].id + '')
                        .attr('opacity', 1)
                        .attr('r', radius);

                    // make label invisible
                    this.highlightRect
                        .style('visibility', 'hidden');
                    this.highlightText
                        .style('visibility', 'hidden');
                }
            });
    }

    /**
     * Function to calculate distance between mouse and a hovered point.
     * @param dataset {} Coordinates of the hovered point.
     * @param coords {} Coordinates of the mouse.
     */
    private calcDistanceHovering(dataset: D3GeneralDataPoint, coords: [number, number]): number {
        const mX = coords[0] + this.buffer,
            mY = coords[1], // + this.margin.top,
            pX = dataset.xCoord,
            pY = dataset.yCoord;
        // calculate distance between point and mouse when hovering
        return Math.sqrt(Math.pow((pX - mX), 2) + Math.pow((pY - mY), 2));
    }

    private getRange(data: D3GeneralDataPoint[], selector: string): Range {
        // range for axis scale
        const range: [number, number] = d3.extent(d3.values(data.map((d) => {
            if ((!isNaN(d.x) && !isNaN(d.y))) {
                return d[selector];
            }
        })));
        return { min: range[0], max: range[1] };
    }

    /**
     * Function that returns the height of the graph diagram.
     */
    private calculateHeight(): number {
        return (this.d3Elem.nativeElement as HTMLElement).clientHeight - this.margin.top - this.margin.bottom;
    }

    /**
     * Function that returns the width of the graph diagram.
     */
    private calculateWidth(): number {
        return this.rawSvg.node().width.baseVal.value - this.margin.left - this.margin.right;
    }

    /**
     * Function that returns the boundings of a html element.
     * @param el {Object} Object of the html element.
     */
    private getDimensions(el: any): { w: number, h: number } {
        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
        };
    }

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

./d3-general-graph.component.scss

.d3 {
    height: 100%;
    width: 100%;
    /* disable text selection */
    -webkit-touch-callout: none; /* iOS Safari */
    -webkit-user-select: none; /* Safari */
    -khtml-user-select: none; /* Konqueror HTML */
    -moz-user-select: none; /* Firefox */
    -ms-user-select: none; /* Internet Explorer/Edge */
    user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */
    
    .grid .tick line {
        stroke: lightgrey;
        stroke-opacity: 0.7;
        shape-rendering: crispEdges;
    }

    .x {
        fill: orange;
        fill-opacity: 0.4;

        .tick {
            stroke: blue;
            stroke-width: 10px;
        
            line {
                stroke: red;
                stroke-width: 15px;
            }
        }
    }
    
    .axis {
        fill: orange;
        fill-opacity: 0.4;

        .tick {
            stroke: blue;
            stroke-width: 10px;
        
            line {
                stroke: lightsalmon;
                stroke-width: 15px;
            }
        }
    }

    .graphDots {
        stroke-width: 0px;
        stroke-opacity: 1;
    }

    .graphDots .hover {
        stroke-width: 20px;
        stroke-opacity: .5;
    }
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""