使用额外维度在 ChartJS 的多行上对 y 轴标签进行分组

Grouping y-axis labels on multiple lines on a ChartJS with an extra dimension

我已经在 SO (Using ChartJS to create a multiple grouped bar chart - see picture below) 上有了这个答案,能够成功地使 y 轴上的第二行标签正常且动态地工作。

我想知道是否有办法让它们走多条线?请参阅下图中的标签:

由于它是一个动态构建的图表并且表现非常灵敏,所以有时我的数据比允许的要长 space。

所以低于 2.50 有时可能太长以至于超出下一个(总计)框。例如,如果 Lower than 2.50 类似于“The quick brown fox jumps over the lazy dog a bunch of times and 运行 into the Total column”,那么它将重叠。

我需要找到解决这个问题的方法。

TL;DR

看这个答案的最底部(有两种方法,选择第二种):最简单的方法是使用字符串数组(string [])用于数据端的各个类别并打印每个数组项在打印端进行单独的 ctx.fillText() 调用。

代码第 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>

解释 #1:

我修复的第一件事是解决,为什么下方 x-axis 的图表标题没有写成两行。在链接的解决方案中,使用了 fillText 。它是一种围绕 canvas 元素的方法,但它有一个局限性,你不能在没有工作的情况下进行多行。不过,它可以缩小以适应 space [1],但很快文本看起来很糟糕。所以,StackOveflow,找到一个解决方法 [2]。有几种解决方案,none 简单,但我发现 wrapText 方法最方便。它可以让文字随心所欲,而且线条优美。

第二个问题是我必须向下调整图例以避免多个内容重叠在同一 space 中。然后我在 wrapText 函数中添加了一个行计算,并用它来生成正确数量的 space.

以上使用该字体的两个衬里效果很好。例如,一句话“The quick brown fox jumps over the lazy dog”就可以了。然后成为图表中 space 的限制:如果内容进入第三行,图例将从底部切掉,因为它不再适合 canvas。为此,我尝试了以下更改:

  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;

这里我刚刚把字体大小打到8像素。它显示所有数据(您的文本也显示)但看起来不太漂亮。

第 1 部分的最后注释:

此解决方案是一个解决方案(插件解决方案)。文本最长的最终解决方案看起来很尴尬,我相信在某个地方有更好的方法,但由于我可以做到这一点,所以我决定分享我的试用版,以防万一无法解决其他问题,所以你有至少这个。

编辑:更多'Chart.js'方式,方便:

代码部分,#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>

解释,第 2 部分

因此,所有标签、标题等都有两个选项(例如 [3] 上的选项):

  1. 字符串

  2. 字符串[]

从这些选项 1 全部在一行中,猜猜是什么:在数组中,您可以按行定义它。一项是内部数组中的一行!

在你的例子中,这次你自己打印,所以你只引用字符串数组并定义引用内容的写入位置!

我采用的方法不是使用较小的字体,而是将内容稍微放在页面上,示例文本非常适合!

来源:

[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