Conditional Custom Drawing in JavaScript Charts — Challenge AnyChart!

December 13th, 2017 by Vitaly Radionov

Conditional Custom Drawing in JavaScript Charts — Challenge AnyChart!The time has come for a new Challenge AnyChart! article! We continue to receive interesting data visualization tasks from our wonderful customers and are happy to share with our blog readers how to solve some of the most inspiring ones with the help of our JavaScript charting libraries.

In today’s tutorial, let’s dig into custom drawing and create an interactive HTML5 line chart of which the segments with negative values are painted with a different color than the rest of the graph.

Data Visualization Task

To be more precise, we’ll look into the following situation as requested by one of our customers.

Here’s the kind of Line Chart we need: When the data goes to negative values, the color of the line must change to red and the crossing point between the graph and the X-axis must feature a timestamp displayed. Is it possible to do it in AnyChart JS Charts?

Below is the picture that the customer attached to illustrate the data visualization task and show us what chart exactly was needed:

We'll use custom drawing to create such an interactive JavaScript (HTML5) line chart where negative value segments feature a different color than the rest

To create a data visualization solution according to this task, we need to use the following:

Solution Overview

To display a chart like this we need to change the function that is responsible for drawing a line in Line Charts.

We need to get two sets of line segments, one with all the points above zero and the other with all the points with negative values. Each line segment will be connected with others from the same set or with an additional point on the zero line.

Basic math will help us create a set of additional points – intersections between the series line and the zero line (a set of X values where Y equals 0).

Custom Drawing

You can learn about the customization of the series drawing functions in the dedicated section of our chart documentation.

The custom drawing function works with point coordinates and 0 on the Y-axis for the current point. This means we only need to calculate X and draw line segments each of which will be colored according to its value.

var zeroX = (this.zero - context.prevY) / (this.value - context.prevY) * (this.x - context.prevX) + context.prevX;

Different Colors

Each series has methods that can’t be used. For example, a simple line series uses the stroke() method but it doesn’t use lowStroke().

Let’s use lowStroke() to set the color for the negative part of the chart.

series.lowStroke('red');

Then, when creating a custom shape, we will specify that the color for negative segments comes from the value set with the lowStroke.

strokeName: 'lowStroke',

Here’s the result we’ve got:

See the Pen Series Conditional Draw by Vitaly (@Radionov) on CodePen.light

Check out the full code of the chart:

anychart.onDocumentReady(function () {

    // create data
    var data = [
        {x: Date.UTC(2014, 5, 6, 1, 15, 0), value: -10},
        {x: Date.UTC(2014, 5, 6, 6, 30, 0), value: 15},
        {x: Date.UTC(2014, 5, 6, 12, 45, 0), value: 5},
        {x: Date.UTC(2014, 5, 6, 19, 20, 0), value: 45},
        {x: Date.UTC(2014, 5, 6, 22, 0, 0), value: 25},
        {x: Date.UTC(2014, 5, 7, 2, 5, 0), value: -12},
        {x: Date.UTC(2014, 5, 7, 7, 25, 0), value: -13},
        {x: Date.UTC(2014, 5, 7, 13, 45, 0), value: 4},
        {x: Date.UTC(2014, 5, 7, 20, 5, 0), value: 43},
        {x: Date.UTC(2014, 5, 7, 23, 15, 0), value: 23},
        {x: Date.UTC(2014, 5, 8, 2, 5, 0), value: -20}
    ];

    // create a chart
    var chart = anychart.line();

    //set the DateTime type of scale
    chart.xScale(anychart.scales.dateTime());

    //set the zero line by yAxis
    var zeroLine = chart.lineMarker();
    zeroLine.value(0);
    zeroLine.stroke("2 grey");

    // create a spline series and set the data
    var series = chart.line(data);

    // point settings
    setupDrawer(series, chart);
    // set the red stroke for line segments below the zero line
    series.normal().lowStroke('red');

    // tooltip settings
    chart.tooltip({
        titleFormat: function () {
            return anychart.format.dateTime(this.x, "dd MMMM HH:mm");
        },
        format: "Value: {%Value}"
    });

    chart.container('container');
    chart.draw();

    /**
     * Custom series drawing function
     * @param series - current series
     * @param chart - current chart
     */
    function setupDrawer(series, chart) {
        var xAxis = chart.xAxis();

        // array for standalone labels and counter
        var zeroLabels = [];

        // remove old current labels when resizing the container
        window.onresize = function () {
            var label;
            while (label = zeroLabels.pop()) label.dispose();
        };

        // add the second shape for the line path below the zero line
        var tmp = series.rendering().shapes();
        tmp.push({
            name: 'negative',
            shapeType: 'path',
            fillName: null,
            strokeName: 'lowStroke',
            isHatchFill: false,
            zIndex: 1
        });

        // create a context for how each point will be drawn
        var context = {
            series: series,
            prevPointDrawn: false,
            prevWasNegative: false,
            prevX: null,
            prevY: null
        };

        var customRenderer = series.rendering();
        customRenderer.needsZero(true);
        customRenderer.shapes(tmp);
        customRenderer.start(function () {
            context.prevPointDrawn = false;
        });
        var customPointDrawer = function () {
            if (this.missing) {
                context.prevPointDrawn = context.prevPointDrawn && context.series.connectMissing();
            } else {
                // get a shape depending on a point's state
                var shapes = this.getShapesGroup(this.seriesState);
                // set what point is negative
                var isNegative = this.getDataValue('value') < 0;
                // determine the path of the current point to continue drawing
                var currPath = isNegative ? shapes['negative'] : shapes['stroke'];
                if (context.prevPointDrawn) {
                    // enter only if the plus-minus nature has changed
                    if (isNegative != context.prevWasNegative) {
                        // determine the path of the previous point to continue drawing
                        var prevPath = context.prevWasNegative ? shapes['negative'] : shapes['stroke'];
                        // determine the position of 0 (by X)
                        var zeroX = (this.zero - context.prevY) / (this.value - context.prevY) * (this.x - context.prevX) + context.prevX;
                        // draw the previous point from zero
                        prevPath.lineTo(zeroX, this.zero);
                        // draw the current point from zero
                        currPath.moveTo(zeroX, this.zero);

                        var localCoordinates = chart.globalToLocal(zeroX, this.zero);
                        var ratio = (localCoordinates.x - xAxis.getPixelBounds().left ) / xAxis.getPixelBounds().width;
                        var time = chart.xScale().inverseTransform(ratio);

                        // draw a label for the crossing between the series line and the zero line
                        var zeroLbl = anychart.standalones.label();
                        zeroLbl.text(anychart.format.dateTime(time, "HH:mm"));
                        zeroLbl.offsetX(zeroX);
                        zeroLbl.offsetY(this.zero);
                        zeroLbl.container(chart.container());
                        zeroLbl.draw();
                        zeroLabels.push(zeroLbl);
                    }
                    currPath.lineTo(this.x, this.value);
                } else {
                    // draw the very first point
                    currPath.moveTo(this.x, this.value);
                }
                context.prevX = this.x;
                context.prevY = this.value;
                context.prevWasNegative = isNegative;
                context.prevPointDrawn = true;
            }
        };
        customRenderer.point(customPointDrawer);
    }
});

Have a sophisticated data visualization task? Not sure how to deal with it best or whether it is actually solvable with AnyChart JS Charts? Simply email us all the relevant details at support@anychart.com with "Challenge" in the subject line. Our Support Team will be happy to help you asap and we might even decide to make a tutorial based on your question and share it in one of the next articles!

Indeed, AnyChart can help you cope with any, even complicated and extraordinary data visualization tasks and any custom drawing. We'll continue the Challenge AnyChart! series on our blog to show you how.


No Comments Yet

*