score:9

Accepted answer

Here is a revised version of the original with a little more flexibility. The logic is pretty much the same, but extended to more than 2 datasets


Preview

enter image description here


Script

Chart.types.Line.extend({
    name: "Line2Y",
    getScale: function(data) {
        var startPoint = this.options.scaleFontSize;
        var endPoint = this.chart.height - (this.options.scaleFontSize * 1.5) - 5;
        return Chart.helpers.calculateScaleRange(
            data,
            endPoint - startPoint,
            this.options.scaleFontSize,
            this.options.scaleBeginAtZero,
            this.options.scaleIntegersOnly);
    },
    initialize: function (data) {
        var y2datasetLabels = [];
        var y2data = [];
        var y1data = [];
        data.datasets.forEach(function (dataset, i) {
            if (dataset.y2axis == true) {
                y2datasetLabels.push(dataset.label);
                y2data = y2data.concat(dataset.data);
            } else {
                y1data = y1data.concat(dataset.data);
            }
        });

        // use the helper function to get the scale for both datasets
        var y1Scale = this.getScale(y1data);
        this.y2Scale = this.getScale(y2data);
        var normalizingFactor = y1Scale.max / this.y2Scale.max;

        // update y2 datasets
        data.datasets.forEach(function(dataset) {
            if (y2datasetLabels.indexOf(dataset.label) !== -1) {
                dataset.data.forEach(function (e, j) {
                    dataset.data[j] = e * normalizingFactor;
                })
            }
        })

        // denormalize tooltip for y2 datasets
        this.options.multiTooltipTemplate = function (d) {
            if (y2datasetLabels.indexOf(d.datasetLabel) !== -1) 
                return Math.round(d.value / normalizingFactor, 6);
            else 
                return d.value;
        }

        Chart.types.Line.prototype.initialize.apply(this, arguments);
    },
    draw: function () {
        this.scale.xScalePaddingRight = this.scale.xScalePaddingLeft;
        Chart.types.Line.prototype.draw.apply(this, arguments);

        this.chart.ctx.textAlign = 'left';
        this.chart.ctx.textBaseline = "middle";
        this.chart.ctx.fillStyle = "#666";
        var yStep = (this.scale.endPoint - this.scale.startPoint) / this.y2Scale.steps
        for (var i = 0, y = this.scale.endPoint, label = this.y2Scale.min; 
             i <= this.y2Scale.steps; 
             i++) {
                this.chart.ctx.fillText(label, this.chart.width - this.scale.xScalePaddingRight + 10, y);
                y -= yStep;
                label += this.y2Scale.stepValue
        }
    }
});

You send a dataset to the y2 axis with an additional property (y2axis: true). For example

{
    label: "My Second dataset",
    fillColor: "rgba(151,187,205,0.5)",
    strokeColor: "rgba(151,187,205,1)",
    pointColor: "rgba(151,187,205,1)",
    pointStrokeColor: "#fff",
    data: [150, 48, 120, 19, 46, 27, 100],
    y2axis: true
}

Fiddle - http://jsfiddle.net/1va2kx18/


You could use one shade of colors for the series on the y axes and another for colors on the y2 axes (otherwise it's a bit confusing). Additionally you could modify your tooltip function to show the y2 values a bit differently. For example

return '[' + Math.round(d.value / normalizingFactor, 6) + ']';

would put square brackets around y2 values in the tooltip


If you are adding new points to the datasets using addData, there is an issue with dataset labels not being updated in the newly added points that you have to work around by updating the addData function.

If you don't want to do that just use the dataset point colors (instead of using dataset labels) to distinguish between the y and y2 series IF you use distinct point colors for y and y2 series. Here are the lines to substitute in

 var y2datasetColors = [];
 ...
 y2datasetColors.push(dataset.pointColor);
 ...
 if (y2datasetColors.indexOf(dataset.pointColor) !== -1) {
 ...
 if (y2datasetColors.indexOf(d._saved.fillColor) !== -1) 

where you previously had y2datasets

score:1

https://github.com/Wikunia/Chart.js/tree/Double-Y-Axis was forked from an early (2 years back) version of Chart.js

currentChart.addData is not a function

The fork does not have this function. That's why you are getting this error.

You might want to take a look at the latest version of Chart.js by the way. There is a related issue that's available in the alpha version - https://github.com/nnnick/Chart.js/issues/17


This said: Is it possible to add a second Y-axis in another way?

With the current version? Yes, if you are willing to compromise on a couple of options. I believe you could workaround these compromises too, but the added complexity is a bit too much :-)

High Level Steps

  1. One of the datasets drives the scale - pick the other dataset, figure out the scale based on it alone and then normalize the values based on this scale and the actual scale
  2. You don't want the tooltips to show the normalized value, so you need to modify the tooltip function to denormalize the values
  3. Render the labels for the secondary y axis

The first 2 can be done in the initialize override and the last 1 in the draw override.


Declaration and Initialization

Of course, we need to extend the chart first. And let's begin the scale at 0 and turn off the grid lines to reduce the complexity.

So

Chart.types.Line.extend({
    name: "Line2Y",

and

var ctx = document.getElementById("chart").getContext("2d");
var myLine1 = new Chart(ctx).Line2Y(lineChartData1, {
    scaleBeginAtZero: true,
    scaleShowGridLines: false
});

Calculating the Normalizing Factor

initialize: function (data) {
    // figure out which dataset has the max value - that is the one that drives the scale
    var max = 0;
    var datasetToNotScale = 0;
    var datasetToScale = 1;
    data.datasets.forEach(function (dataset, i) {
        dataset.data.forEach(function (e) {
            if (e > max) {
                max = e;
                datasetToNotScale = i;
                datasetToScale = (i == 0 ? 1 : 0);
            }
        })
    })
    var datasetToScaleLabel = data.datasets[datasetToScale].label;

    var startPoint = this.options.scaleFontSize;
    var endPoint = this.chart.height - (this.options.scaleFontSize * 1.5) - 5;
    // use the helper function to get the scale for both datasets
    var notScaleRange = Chart.helpers.calculateScaleRange(
        data.datasets[datasetToNotScale].data,
        endPoint - startPoint,
        this.options.scaleFontSize,
        this.options.scaleBeginAtZero,
        this.options.scaleIntegersOnly
    )
    this.scaleRange = Chart.helpers.calculateScaleRange(
        data.datasets[datasetToScale].data,
        endPoint - startPoint,
        this.options.scaleFontSize,
        this.options.scaleBeginAtZero,
        this.options.scaleIntegersOnly
    )

Once we have the scale for both datasets, calculate the normalizing factor (ratio of max value of both scales, since we set the chart scale to begin at 0)

var normalizingFactor = notScaleRange.max / this.scaleRange.max;

Normalizing (for plotting) and Denormalizing (for the Tooltips)

Use this to update the dataset that does not drive the scale

// update one of our datasets!
data.datasets[datasetToScale].data.forEach(function (e, i) {
    data.datasets[datasetToScale].data[i] = e * normalizingFactor;
})

And of course, counteract this by denormalizing in the tooltip function (notice the Math.round - that takes care of a slight loss of precision converting back and forth)

this.options.multiTooltipTemplate = function (d) {
    if (d.datasetLabel == datasetToScaleLabel)
        return Math.round(d.value / normalizingFactor, 6);
    else
        return d.value;
}

Rendering the Secondary Axis Labels

First make sure you have enough space on the right hand side

draw: function () {
    this.scale.xScalePaddingRight = this.scale.xScalePaddingLeft;

Then, once the actual chart is drawn, draw our secondary axis labels

this.chart.ctx.font = Chart.helpers.fontString(self.fontSize, self.fontStyle, self.fontFamily)
this.chart.ctx.textAlign = 'left';
this.chart.ctx.textBaseline = "middle";
this.chart.ctx.fillStyle = "#666";
var label = this.scaleRange.min;
var yStep = (this.scale.endPoint - this.scale.startPoint) / this.scaleRange.steps
for (var i = 0, y = this.scale.endPoint; i <= this.scaleRange.steps; i++) {
    this.chart.ctx.fillText(label, this.chart.width - this.scale.xScalePaddingRight + 10, y);
    y -= yStep;
    label += this.scaleRange.stepValue
}

and we are done!


Fiddle - http://jsfiddle.net/u2Lru6vv/

Note - overlaying 2 charts with a mirrored y axis (like we did above) is another (slightly less invasive) option, but the problem is you lose tooltips for the underlying chart.


Related Query

More Query from same tag