6

I would like to add a second Y-axis on a linechart, possibly on the right side of the canvas. I tried to use Chart.js taken from https://github.com/Wikunia/Chart.js/tree/Double-Y-Axis in which LineDoubleY is defined. But:

  1. I cannot see the example on Firefox:

ReferenceError: xPos is not defined Chart.js:1147:7

  1. If I use this Chart.js in my app:

currentChart.addData is not a function

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

Sabba
  • 257
  • 2
  • 6
  • 17

2 Answers2

9

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

Community
  • 1
  • 1
potatopeelings
  • 40,709
  • 7
  • 95
  • 119
  • THAT'S IT! By now you should know that I am a huge fan of _addData_ . How can I add new value? I mean when I add new values they are taken as they belong to left-Y-axis even if they are in 2nd and 3rd dataset. http://jsfiddle.net/hrycnrgo/ – Sabba Jun 25 '15 at 14:03
  • just store the normalizingFactor in this.normalizingFactor and you will be able to access it via myLine1.normalizingFactor. Just scale the values when you insert it into the array using `d.push(randomScalingFactor())` – potatopeelings Jun 25 '15 at 14:08
  • That works ok too, as long as you have only 1 instance of the chart. Cheers! – potatopeelings Jun 25 '15 at 14:33
  • Ok, I did something similar i defined `normalizingFactor` as a global variable and, correct me if wrong, get the same result. So I did define 2 randomScalingFactor() to respect data limits: randomScale1 [0-400] and randomScale2[0-200]. To update tooltips? `myLine1.options.multiTooltipTemplate = function (d) { if (y2datasetLabels.indexOf(d.datasetLabel) !== -1) return Math.round(d.value / normalizingFactor, 6); else return d.value; }` Says that `y2datasetLabels` is undefined. – Sabba Jun 25 '15 at 14:41
  • I think y2datasetLabels was defined in the replacement initialize function. So you can't actually access it outside of that closure, like if you update the multiTooltipTemplate like in your comment which I assume is in the main body. – potatopeelings Jun 25 '15 at 14:45
  • Er.. then it won't work :-). You have to make y2datasetLabels a global too if you want to do it that way. – potatopeelings Jun 25 '15 at 14:54
  • http://jsfiddle.net/hrycnrgo/1/ Defined it as y2datasetLabel as a global variable still i have not the desired behaviour. I think that "messing" with `multiToolTemplate` is not the answer. I'll try something different. Thank you! – Sabba Jun 25 '15 at 15:31
  • I don't seem to be getting any console errors with code from your fiddle. What's off when you run it? – potatopeelings Jun 25 '15 at 16:22
  • After the first AddData 2nd and 3rd dataset have values in the "1st range" in Tooltip. – Sabba Jun 26 '15 at 06:39
  • Strange, that looks like a bug! Anyhow, one dirty work around would be to use the pointColors to distinguish between the series. http://jsfiddle.net/s2duL6rp/ - just make sure you have different pointColors for each series (or atleast different ones for the ones on y and y2 axes) – potatopeelings Jun 26 '15 at 12:47
  • From your fiddle I see that now Tooltip values are "left-scaled" from the beggining now. Is it possible to select one datasets at the time and do something like this: `tooltipTemplate: "<%if (label){%><%=My First dataset}%><%= value %>" tooltipTemplate: "<%if (label){%><%=My Second dataset}%><%= value/2 %>" tooltipTemplate: "<%if (label){%><%=My Third dataset}%><%= value/2 %>"` – Sabba Jun 26 '15 at 13:54
  • Oops! my bad - forgot to change label to pointColor in the forEach loop. http://jsfiddle.net/2df17d3b/. The code in your comment is pretty much what we had initially, but the problem seems to be that chart.js is not adding the label for addData points. – potatopeelings Jun 27 '15 at 02:52
  • You should change: `dataset.data[j] = e * normalizingFactor;` To: `if (e) dataset.data[j] = e * normalizingFactor;` – fcaserio Dec 17 '15 at 15:30
  • Have you find a way to limit the width of the x-axis to just a mm. after the 2nd y-axis? I'm going nuts to achieve that! – jdlcgarcia Dec 21 '15 at 23:13
  • @jdlcgarcia Add `this.chart.ctx.clearRect(this.chart.width - this.scale.xScalePaddingRight + , 0, this.scale.xScalePaddingRight, this.chart.height); ` after the prototype draw. See http://jsfiddle.net/rwmp46xh/ – potatopeelings Dec 22 '15 at 04:04
  • @fcaserio - didn't quite get what that was for. What happens if you don't add that? Thanks! – potatopeelings Dec 22 '15 at 04:05
  • awesome @potatopeelings! thank you very much!! you lack an adding there, I correct your code: `this.chart.ctx.clearRect(this.chart.width - this.scale.xScalePaddingRight + 5,0, this.scale.xScalePaddingRight, this.chart.height);` – jdlcgarcia Dec 22 '15 at 13:52
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.

potatopeelings
  • 40,709
  • 7
  • 95
  • 119
  • This is a good start for me. I try to add a third dataset, and i want it to be scaled with the Y-axis on the right. but it always get the left axis. how to do this? – Sabba Jun 25 '15 at 09:52
  • This may help, I have 6 datasets divided in 2 categories let say "current and voltage" values. I'd like to define 2 categories one using left-Y-axis and the other category the right-Y-axis. – Sabba Jun 25 '15 at 10:01
  • Check out the other answer. I didn't want to modify this one because it was too much effort :-). Also, I didn't rehash the explanation in the 2nd one, so not deleting this one either. – potatopeelings Jun 25 '15 at 10:46