Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Ticks should align when multiple Y axis for line and bar chart #3484

Closed
emn178 opened this issue Oct 18, 2016 · 27 comments · Fixed by #8643
Closed

[BUG] Ticks should align when multiple Y axis for line and bar chart #3484

emn178 opened this issue Oct 18, 2016 · 27 comments · Fixed by #8643

Comments

@emn178
Copy link

emn178 commented Oct 18, 2016

See https://jsfiddle.net/kng1d49d/
When I use two Y axis for line and bar in the same time, Y ticks does not align and show extra grid lines.

Expected Behavior

Y ticks should align.

Current Behavior

Y ticks does not align and show extra grid lines.

Possible Solution

In this sample, it show only one Y-axis grid line. See https://jsfiddle.net/kng1d49d/1/
But the ticks on right side still can't align, looks not good. The problem is caused by different count of ticks. Count of ticks on left side is 6, right is 5. I think to make sure rendering in the same count can fix this.

Environment

  • Chart.js version: 2.3.0
@madmoizo
Copy link

maxTicksLimit + suggestedMax
https://jsfiddle.net/kng1d49d/2/

@etimberg
Copy link
Member

@frlinw has a good answer.

CC @chartjs/maintainers should we provide a way of syncing number of ticks between parallel axes?

@emn178
Copy link
Author

emn178 commented Oct 19, 2016

thanks for comment.
but it still happens if they use different suggestedMax.
eg. 100 and 600, https://jsfiddle.net/kng1d49d/3/

@madmoizo
Copy link

You can disable gridLines for the right y axis... It's not the best solution but it's probably the only one (I did that for my charts)

@SilentFlame
Copy link

I too agree with @frlinw , as it's just there for the ease in the interpretation.

Anyway is this issue resolved?
If not, would like to contribute.

@JC5
Copy link

JC5 commented Dec 23, 2016

It is not yet resolved, my charts suffer from this as well. Due to their dynamic nature, I can only use "maxTicksLimit" what does not work.

@TylerYang
Copy link

TylerYang commented Dec 28, 2016

Same here, would appreciate it if someone could show me the way.

@dregini
Copy link

dregini commented Jul 20, 2017

Same problem here 2 scale misaligned, i'm using horizontalBar with 2 Y-axes with type:category so no use for suggestedMax

@pierfreeman
Copy link

Any news about this issue? the solution proposed by @frlinw only works if the two datasets have the same order of magnitude or the same measure.
My chart renders datasets representing different metrics, so with very different values, and since the rendering is dynamic with respect to the metrics selected (by the user), I cannot know which suggestedMax value to set in order to have the same ticks line shared by the two Y-axes...

@madmoizo
Copy link

@pierfreeman
In most case the left axis is the "main" so you can disable gridLines for the "optionnal" right axis. You end up with a clean chart, very readable with 2 clean scales.
image

Now, I agree, a perfect matching between y-axis with perfect scales is a must have.
My first idea was to chose the main axis, if necessary force the typeof data you can accept for the second scale (integer, float) and build it with these constraints. But it's only possible if there is an order in scales building.
Maybe @etimberg can tell us what are the pain points to achieve this

@mikeantkowiak
Copy link

Hi everyone. Did anyone ever find a solution for this bug.
It would be great if there was a minTicksLimit just like the maxTicksLimit, that way we could just use the same number for both and it would alway only display x.

If there is any other work around please help!

@ralphking
Copy link

@mikeantkowiak I'm looking for an answer to this as well. Any luck? Just hiding the gridlines for the second axis isn't really an option. Using maxTicksLimit and suggestedLimit doesn't work either

@fhidalgor
Copy link

I had to do the following to get my labels aligned:
ax2 = ax.twiny()
ax3 = ax.twinx()
and then:
ax2.set_xlim(ax.get_xlim())
ax3.set_ylim(ax.get_ylim())

It seemed to solve the problem

@AlexSapoznikov
Copy link

AlexSapoznikov commented Apr 5, 2018

I did it like this:

// Find max values from data, so at this point you should have values for:
const leftYMax;
const rightYMax;

// Amount of labels you want to see on y axis
const amountOfLabels = 10;

const newLeftYMax = calculateMax(amountOfLabels, leftYMax);
const leftYStep = newLeftYMax / amountOfLabels;

const newRightYMax = calculateMax(amountOfLabels, rightYMax);
const rightYStep = newRightYMax / amountOfLabels;

// Function for calculating new max
function calculateMax (amountOfLabels, max) {
  // If max is divisible by amount of labels, then it's a perfect fit
  if (max % amountOfLabels === 0) {
    return max;
  }
  // If max is not divisible by amount of labels, let's find out how much there
  // is missing from max so it could be divisible
  const diffDivisibleByAmountOfLabels = amountOfLabels - (max % amountOfLabels);

  // Add missing value to max to get it divisible and achieve perfect fit
  return max + diffDivisibleByAmountOfLabels;
}

// Attach calculated data to your Chart config
this.mixedChart = new Chart(context, {
  options: {
    scales: {
       yAxes: [
         {
           ticks: {
             beginAtZero: true,
             max: newLeftYMax,
             stepSize: leftYStep
           },
         },
         {
           ticks: {
             beginAtZero: true,
             max: newRightYMax,
             stepSize: rightYStep
           },
         }
       ]
    }
  }
});

@SatanEnglish
Copy link

SatanEnglish commented Aug 20, 2018

Found a fix for this that I can't find posed anywhere not sure what version it works on.

                yAxes: [
                    {                            
                        display: true,
                        position: 'left',
                        type: 'category',                         
                        weight: 1,
                    },
                    {      
                        offset: true,
                        position: 'left',
                        labels: horizontalBarChartData.leftLabels,
                        type: 'category',
                        gridLines: {
                            display: false
                        }
                    },
                    {
                        offset: true, // The magic that makes them align.
                        position: 'right',
                        type: 'category',
                        labels: horizontalBarChartData.rightLabels,
                        gridLines: {
                            display: false
                        }                            
                    }
                ]

@pimvanderheijden
Copy link

Is there any news about this?

Calculating the ticks can be tricky. It would be great to let Chart.js make sure the amount of ticks of all axes are always identical.

@ramykl
Copy link

ramykl commented Nov 6, 2020

My solution was to let ChartJS do the calculations for the ticks, but keep getting it to recalculate it until both axes have the same number of ticks by forcing them to use the minimum number of ticks for either axis for both of them as the max.
That way you still get the benefit of the nice number calculation that it uses, but still getting the ticks in sync. This should work with any number of axes, but may be reduced to the minimum number of ticks (2) set by the library. Also, this may have performance issues for really large datasets.

let y1Ticks = 11; // set as 11 as it is the default max in ChartJS
let y2Ticks = 11; // set as 11 as it is the default max in ChartJS

const myChart = new Chart(ctx, {
  type: 'bar',
  data: {
    labels: labels,
    datasets: [
      {
        label: 'data1',
        data: data1,
        backgroundColor: 'rgb(54, 162, 235)',
        yAxisID: 'y1',
      },
      {
        label: 'data2',
        data: data2,
        backgroundColor: 'rgb(235, 162, 54)',
        yAxisID: 'y2',
      },
    ],
  },
  options: {
    scales: {
      yAxes: [
        {
          id: 'y1',
          ticks: {
            beginAtZero: true,
            maxTicksLimit: y1Ticks,
          },
          position: 'left',
          afterBuildTicks: function (axis) {
            if (y1Ticks !== Math.min(axis.ticks.length, y2Ticks)) {
              y1Ticks = Math.min(axis.ticks.length, y2Ticks);
              axis.options.ticks.maxTicksLimit = y1Ticks;
            }
          },
        },
        {
          id: 'y2',
          ticks: {
            beginAtZero: true,
            maxTicksLimit: y2Ticks,
          },
          position: 'right',
          afterBuildTicks: function (axis) {
            if (y2Ticks !== Math.min(axis.ticks.length, y1Ticks)) {
              y2Ticks = Math.min(axis.ticks.length, y1Ticks);
              axis.options.ticks.maxTicksLimit = y2Ticks;
            }
          },
        },
      ],
    },
  },
});

while (myChart.scales.y1.ticks.length !== myChart.scales.y2.ticks.length) {
  myChart.update();
}

It would be really nice to have this functionality inbuilt in ChartJS to save having to loop through this ourselves. Having access to the tick calculation through an API would be could be another way to solve this and would save having to redraw the chart multiple times.

@Troffifi
Copy link

Troffifi commented Feb 9, 2021

If anyone needs to do this without forcing the scales to start from 0, I did it like this:

function setYAxesOptionsForTicks(cChart, extremesByAxis) {
	var minNumberOfTicks = cChart.options.scales.yAxes[0].ticks.minNumberOfTicks;
	
	var maxNumberOfTicks = 0;
	for (var yAxis in extremesByAxis) {
		var max = roundValueToNextMultipleOf(extremesByAxis[yAxis]["max"], minNumberOfTicks);
		var min = roundValueToPreviousMultipleOf(extremesByAxis[yAxis]["min"], minNumberOfTicks);
		var stepSize = max - min;
		
		stepSize = roundValueToNextMultipleOf(stepSize / minNumberOfTicks, minNumberOfTicks);
		max = roundValueToNextMultipleOf(max, stepSize);
		min = roundValueToPreviousMultipleOf(min, stepSize);
		
		var numberOfTicks = (max - min) / stepSize;
		if (numberOfTicks > maxNumberOfTicks) {
			maxNumberOfTicks = numberOfTicks;
		}
		
		cChart.options.scales.yAxes[yAxis].ticks.min = min;
		cChart.options.scales.yAxes[yAxis].ticks.stepSize = stepSize;	
	}
	
	for (var yAxis in extremesByAxis) {
		cChart.options.scales.yAxes[yAxis].ticks.max = cChart.options.scales.yAxes[yAxis].ticks.min + cChart.options.scales.yAxes[yAxis].ticks.stepSize * maxNumberOfTicks;
	}
}

function roundValueToNextMultipleOf(value, multipleOf) {
	return Math.ceil(value / multipleOf) * multipleOf;
}

function roundValueToPreviousMultipleOf(value, multipleOf, next) {
	return Math.floor(value / multipleOf) * multipleOf;
}

@ramykl
Copy link

ramykl commented Apr 8, 2021

It would be really nice to be able to keep the logic of getting good numbers and just getting the ticks to align, not just setting a fixed number of ticks and forcing the labels to be random numbers to work. So I think this issue should remain open as the current merge fix doesn't solve the specific issue

@kurkle
Copy link
Member

kurkle commented Apr 8, 2021

@ramykl I don't think we generate random numbers in any case. For your specific use case, please open a new issue with interactive reproduce if you want someone to take a look at it.

@ChefCodev
Copy link

ChefCodev commented Aug 31, 2021

What I just did that seems to work is to determine the maximum y-axis value across all your data displayed. I then set the maximum value for each of the y-axis to that value but rounded up to a round x00000 number to avoid a silly looking maximum number on the axis. This just worked. I was pleasantly surprised i didnt have to write complex code.

So my graph is comprised of a 4 stacked bars and 3 not-stacked lines; 2 Y-Axis

I first set the max value to 0:
yAxisMax = 0

Then in the code that generates the datasets (1 per dataset) I had a line similar to this:
yAxisMax = Math.max(parseFloat(focusForecast),parseFloat(focusForecastSpending),yAxisMax)

Once all the dataset variables were calculated I also had a yAxisMax value that matched my highest value from all the datasets. In my case i rounded this value up to the next 5000; you can set it to anything that works for you. I just found this looked best in my case.

yAxisMax = Math.ceil(yAxisMax/5000)*5000

Then in the scales configuration:

      y: {
              stacked: 'single',
              min: 0,
              max: yAxisMax,

            },
    y1: {
              stacked: false,
              display: false,
              min: 0,
              max: yAxisMax,
            },

Simple. Let me know if this works for you.

EDIT:
I discovered that i needed to do dynamic step sizing because some of the graphs had bigger numbers than others. It didnt look quite right. Here is what i did...

    if (parseFloat(yAxisMax) < 5000) {
      step = 1000
    } else if (parseFloat(yAxisMax) < 10000) {
      step = 2500
    } else if (parseFloat(yAxisMax) < 50000) {
      step = 5000
    } else {
      step = 10000
    }

yAxisMax = Math.ceil(yAxisMax/step)*step + step

@ramykl
Copy link

ramykl commented Sep 6, 2021

@ChefCodev I did the opposite way and used the # of steps for each axis and kept getting the chart to keep redrawing until the # of steps were the same for both axes with a min of 3 steps

@JeroenMBooij
Copy link

It's 2024 and we still can't align ticks if we have multiple y-axis

@stockiNail
Copy link
Contributor

@JeroenMBooij #10171

@JeroenMBooij
Copy link

@stockiNail hardcoding the tick count creates offbeat labels depending on your dataset

@amigian74
Copy link

Hi there. Does anyone has a working solution for this problem. My own solution stopped working with V3.x. Thx in advance.

@Yash-Chauhan-Skords
Copy link

anyone able to fig out the solution ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.