1

I have with this answer on SO (Using ChartJS to create a multiple grouped bar chart - see picture below) been able to successfully get the secondary row of labels on the y-axis working correctly and dynamically.

I was wondering if there is a way to make them go on multiple lines? See the labels in the image below:

enter image description here

Since it is a dynamically built chart and behaves pretty responsively, I sometimes have data that is longer than the allowed space.

So the Lower than 2.50 sometimes might be so long it overruns into the next (Total) box. For instance if Lower than 2.50 was something like "The quick brown fox jumps over the lazy dog a bunch of times and ran into the Total column", then it would overlap.

I need to find a way to fix this.

cdub
  • 24,555
  • 57
  • 174
  • 303

1 Answers1

1

TL;DR

Look at the very bottom of this answer (there are two ways, choose the second): the easiest way is to use a string array (string []) for individual categories at data side and print each array item with a separate ctx.fillText() call on the printing side.

Code part #1:

CanvasRenderingContext2D.prototype.wrapText = function(text, x, y, maxWidth, lineHeight) {

  var lines = text.split("\n");
  var linesCount = 0;
  for (var i = 0; i < lines.length; i++) {

    var words = lines[i].split(' ');
    var line = '';

    for (var n = 0; n < words.length; n++) {
      var testLine = line + words[n] + ' ';
      var metrics = this.measureText(testLine);
      var testWidth = metrics.width;
      if (testWidth > maxWidth && n > 0) {
        this.fillText(line, x, y);
        linesCount += 1;
        line = words[n] + ' ';
        y += lineHeight;
      } else {
        line = testLine;
      }
    }

    this.fillText(line, x, y);
    linesCount += 1;
    y += lineHeight;
  }
  return linesCount;
}
new Chart('myChart', {
  type: 'bar',
  plugins: [{
    afterDraw: chart => {
      let ctx = chart.chart.ctx;
      ctx.save();
      let xAxis = chart.scales['x-axis-0'];
      let xCenter = (xAxis.left + xAxis.right) / 2;
      let yBottom = chart.scales['y-axis-0'].bottom;
      ctx.textAlign = 'center';
      ctx.font = '12px Arial';
      var size1 = ctx.wrapText(chart.data.categories[0], (xAxis.left + xCenter) / 2, yBottom + 40, 160, 16);
      var size2 = ctx.wrapText(chart.data.categories[1], (xCenter + xAxis.right) / 2, yBottom + 40, 160, 16);
      var size = size1 > size2 ? size1 : size2;
      chart.options.legend.labels.padding = size * 16 + 5;
      ctx.strokeStyle = 'lightgray';
      [xAxis.left, xCenter, xAxis.right].forEach(x => {
        ctx.beginPath();
        ctx.moveTo(x, yBottom);
        ctx.lineTo(x, yBottom + 40);
        ctx.stroke();
      });
      ctx.restore();
    }
  }],
  data: {
    labels: ['2004', '2008', '2012', '2016', '2004', '2008', '2012', '2016'],
    categories: ['The quick brown fox jumps over the lazy dog', 'Lower than 2.50'],
    datasets: [{
        label: 'Male',
        data: [42.4, 43.0, 43.0, 50.3, 49.4, 48.4, 51.2, 51.8],
        backgroundColor: 'rgba(124, 181, 236, 0.9)',
        borderColor: 'rgb(124, 181, 236)',
        borderWidth: 1
      },
      {
        label: 'Female',
        data: [57.6, 57.0, 57.0, 49.7, 50.6, 51.6, 53.7, 54.6],
        backgroundColor: 'rgba(67, 67, 72, 0.9)',
        borderColor: 'rgb(67, 67, 72)',
        borderWidth: 1
      }
    ]
  },
  options: {
    legend: {
      position: 'bottom',
      labels: {
        padding: 30,
        usePointStyle: true
      }
    },
    scales: {
      yAxes: [{
        ticks: {
          min: 0,
          max: 80,
          stepSize: 20
        },
        scaleLabel: {
          display: true,
          labelString: 'Percent (%)'
        }
      }],
      xAxes: [{
        gridLines: {
          drawOnChartArea: false
        }
      }]
    }
  }
});
canvas {
  max-width: 400px;
}
<!DOCTYPE html>
<html>

<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js"></script>
  <canvas id="myChart" height="200"></canvas>
</body>

</html>

Explanations #1:

First thing I fixed was to solve, why the chart title on lower x-axis did not write to two lines. There in the linked solution, fillText was used. It is a method of surrounding canvas element, but it has a limitation, that you cannot make it without a work to go on several lines. It can, though, be schrinked to fit the space [1] but soon the text looks awful. So, StackOveflow, find a workaround [2]. There were several solutions, none easy, but I found that wrapText approach most convenient. It makes the text as long as I want and all comes nicely lined.

Second issue was that I had to adjust legend downwards to avoid multiple content overlapping in same space. Therewhore I added a calculation of lines to wrapText function and used that to generate the correct amount of space.

The above works fine with a two liner of that font used. For example a sentence "The quick brown fox jumps over the lazy dog" works just fine. Then becomes the limitations of space in chart to take place: if content goes to third line the legend is cut from bottom as it does not fit the canvas anymore. For that I tried the following change:

  ctx.font = '8px Arial';
  var size1 = ctx.wrapText(chart.data.categories[0], (xAxis.left + xCenter) / 2, yBottom + 40, 160, 8);
  var size2 = ctx.wrapText(chart.data.categories[1], (xCenter + xAxis.right) / 2, yBottom + 40, 160, 8);
  var size = size1 > size2 ? size1 : size2;
  chart.options.legend.labels.padding = size * 8 + 8;

where I just played the font size to 8 pixels. It shows all data (that your text also) but looks not so pretty.

Final notes on part #1:

This solution is a work around to a work around (the plugin solution). The ultimate solution with the longest possible text looks so awkward that I believe there is a nicer way around somewhere, but as I could make it work this far I decided to share my trial in case no other fix will not be possible so you have at least this one.

EDIT: More 'Chart.js' way, and convenient:

Code part, #2

new Chart('myChart', {
  type: 'bar',
  plugins: [{
    beforeDraw: chart => {
      let ctx = chart.chart.ctx;
      ctx.save();    
      let xAxis = chart.scales['x-axis-0'];
      let xCenter = (xAxis.left + xAxis.right) / 2;
      let yBottom = chart.scales['y-axis-0'].bottom;
      ctx.textAlign = 'center';
      ctx.font = '12px Arial';
      ctx.fillText(chart.data.categories[0][0], (xAxis.left + xCenter) / 2, yBottom + 30);
      ctx.fillText(chart.data.categories[0][1], (xAxis.left + xCenter) / 2, yBottom + 40);
      ctx.fillText(chart.data.categories[0][2], (xAxis.left + xCenter) / 2, yBottom + 50);
      ctx.fillText(chart.data.categories[1], (xCenter + xAxis.right) / 2, yBottom + 40);
      ctx.strokeStyle  = 'lightgray';
      [xAxis.left, xCenter, xAxis.right].forEach(x => {
        ctx.beginPath();
        ctx.moveTo(x, yBottom);
        ctx.lineTo(x, yBottom + 40);
        ctx.stroke();
      });
      ctx.restore();
    }
  }],
  data: {
    labels: ['2004', '2008', '2012', '2016', '2004', '2008', '2012', '2016'],
    categories: [['The quick brown fox jumps over', 'the lazy dog a bunch of times','and ran into the Total column'], ['Lower than 2.50']],
    datasets: [{
        label: 'Male',
        data: [42.4, 43.0, 43.0, 50.3, 49.4, 48.4, 51.2, 51.8],
        backgroundColor: 'rgba(124, 181, 236, 0.9)',
        borderColor: 'rgb(124, 181, 236)',
        borderWidth: 1
      },
      {
        label: 'Female',
        data: [57.6, 57.0, 57.0, 49.7, 50.6, 51.6, 53.7, 54.6],
        backgroundColor: 'rgba(67, 67, 72, 0.9)',
        borderColor: 'rgb(67, 67, 72)',
        borderWidth: 1
      }
    ]
  },
  options: {
    legend: {
      position: 'bottom',
      labels: {
        padding: 30,
    align: 'end',
        usePointStyle: true
      }
    },
    scales: {
      yAxes: [{
        ticks: {
          min: 0,
          max: 80,
          stepSize: 20
        },
        scaleLabel: {
          display: true,
          labelString: 'Percent (%)'
        }
      }],
      xAxes: [{
        gridLines: {
          drawOnChartArea: false
        }
      }]
    }
  }
});
canvas {
  max-width: 400px;
}
<!DOCTYPE html>
<html>
<head>
    <script src="/scripts/snippet-javascript-console.min.js?v=1"></script>
</head>
<body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js"></script>
<canvas id="myChart" height="200"></canvas>
</body>

Explanation, part #2

So, all labels, titles, etc has two options (for example that on [3]):

  1. string

  2. string[]

From these the option 1 is all in one row, and guess what: in array you can define it row wise. One item is one row in inner array!

In your example you print this time by yourself, so you just refer the string array and define where the referred content is written!

I took instead of smaller font an approach to take the content little bit up on page and the example text fits just perfectly!!

Sources:

[1] https://www.w3schools.com/tags/playcanvas.asp?filename=playcanvas_filltextmaxwidth

[2] HTML5 canvas ctx.fillText won't do line breaks?

[3] https://www.chartjs.org/docs/latest/axes/labelling.html

mico
  • 12,730
  • 12
  • 59
  • 99
  • This is amazing. Thanks for all the Chart.JS help. – cdub May 11 '21 at 22:33
  • The downside is I need to figure out how to make this dynamic to fit in when the text is random. – cdub May 13 '21 at 19:44
  • The approach one is the most dynamic one. What I did not test on that was to take off padding like in the approach 2. That way, would there be enough space on normal font also? – mico May 14 '21 at 08:17
  • Apparently my VS Code built with webpack project doesn't have wrapText included. – cdub May 14 '21 at 21:25
  • I have this: https://stackoverflow.com/q/67540432/665557 I am unsure how to adjust it. Looking into it. – cdub May 14 '21 at 21:28
  • I found this: https://www.html5canvastutorials.com/tutorials/html5-canvas-wrap-text-tutorial/ I think this will be the work around. – cdub May 14 '21 at 21:33
  • How did you make this possible? It's just what I need, but... 'categories' doesn't exist in the Chartjs interface. I am so confused – Izlia Sep 18 '22 at 17:53
  • I see, I have quite much freely extended the data content interface. And also if there was a category there, I made it having one dimension more, an array. I think it would work just because I added sth completely new in there, not by altering any existing interface. I cannot point out a mechanism why is this allowed, maybe because the underlying code uses json and it omits in normal code the extra variables in data. As you see I manually draw the contents, no feature from chartjs is directly used. Only I knew it is a canvas and I looked how to write there. – mico Sep 19 '22 at 09:30