Sven Kannenberg
Sven Kannenberg

Reputation: 869

chart.js: Show labels outside pie chart

chart.js 2.6.0

I need to render a chart that looks like this:

enter image description here

Always showing all tooltips is not an acceptable way, since they won't get rendered in a proper manner:

enter image description here

Unfortunately I couldn't find a solution yet. I've tried the piece-label plugin, but this has the same problems, since it's labels overlap and I can't hide certain labels.

Here is the code, that creates my chart using piece-label to position the labels above the slices:

private createStatusChart(): void {
    const chartData = this.getStatusChartData();

    if (!chartData) {
        return;
    }

    const $container = $(Templates.Dashboard.ChartContainer({
        ContainerID: 'chart-status',
        HeaderText: 'Status'
    }));

    this._$content.append($container);

    const legendOptions =
        new Model.Charts.LegendOptions()
            .SetDisplay(false);

    const pieceLabelOptions =
        new Model.Charts.PieceLabelOptions()
            .SetRender('label')
            .SetPosition('outside')
            .SetArc(true)
            .SetOverlap(true);

    const options =
         new Model.Charts.Options()
             .SetLegend(legendOptions)
             .SetPieceLabel(pieceLabelOptions);

    const chartDefinition = new Model.Charts.Pie(chartData, options);
    const ctx = this._$content.find('#chart-status canvas').get(0);

    const chart = new Chart(ctx, chartDefinition);
}

private getStatusChartData(): Model.Charts.PieChartData {
    if (!this._data) {
        return;
    }

    const instance = this;
    const data: Array<number> = [];
    const labels: Array<string> = [];
    const colors: Array<string> = [];

    this._data.StatusGroupings.forEach(sg => {
        if (!sg.StatusOID) {
            data.push(sg.Count);
            labels.push(i18next.t('Dashboard.NoStateSet'));
            colors.push('#4572A7');

            return;
        }

        const status = DAL.Properties.GetByOID(sg.StatusOID);

        data.push(sg.Count);
        labels.push(status ? status.Title : i18next.t('Misc.Unknown'));
        colors.push(status ? status.Color : '#fff');
    });

    const dataset = new Model.Charts.Dataset(data).setBackgroundColor(colors);

    return new Model.Charts.PieChartData(labels, [dataset]);
}

The result:

enter image description here

Upvotes: 22

Views: 45490

Answers (4)

B.D
B.D

Reputation: 183

I can't find an exact plugin but I make one.

      const data = {
        labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
        datasets: [
          {
            data: [1, 2, 3, 4, 5, 6],
            backgroundColor: [
              "#316065",
              "#1A7F89",
              "#2D9CA7",
              "#2D86A7",
              "#1167A7",
              "#142440",
            ],
            borderColor: [
              "#316065",
              "#1A7F89",
              "#2D9CA7",
              "#2D86A7",
              "#1167A7",
              "#142440",
            ],
          },
        ],
      };

      // pieLabelsLine plugin
      const pieLabelsLine = {
        id: "pieLabelsLine",
        afterDraw(chart) {
          const {
            ctx,
            chartArea: { width, height },
          } = chart;

          const cx = chart._metasets[0].data[0].x;
          const cy = chart._metasets[0].data[0].y;

          const sum = chart.data.datasets[0].data.reduce((a, b) => a + b, 0);

          chart.data.datasets.forEach((dataset, i) => {
            chart.getDatasetMeta(i).data.forEach((datapoint, index) => {
              const { x: a, y: b } = datapoint.tooltipPosition();

              const x = 2 * a - cx;
              const y = 2 * b - cy;

              // draw line
              const halfwidth = width / 2;
              const halfheight = height / 2;
              const xLine = x >= halfwidth ? x + 20 : x - 20;
              const yLine = y >= halfheight ? y + 20 : y - 20;

              const extraLine = x >= halfwidth ? 10 : -10;

              ctx.beginPath();
              ctx.moveTo(x, y);
              ctx.arc(x, y, 2, 0, 2 * Math.PI, true);
              ctx.fill();
              ctx.moveTo(x, y);
              ctx.lineTo(xLine, yLine);
              ctx.lineTo(xLine + extraLine, yLine);
              // ctx.strokeStyle = dataset.backgroundColor[index];
              ctx.strokeStyle = "black";
              ctx.stroke();

              // text
              const textWidth = ctx.measureText(chart.data.labels[index]).width;
              ctx.font = "12px Arial";
              // control the position
              const textXPosition = x >= halfwidth ? "left" : "right";
              const plusFivePx = x >= halfwidth ? 5 : -5;
              ctx.textAlign = textXPosition;
              ctx.textBaseline = "middle";
              // ctx.fillStyle = dataset.backgroundColor[index];
              ctx.fillStyle = "black";

              ctx.fillText(
                ((chart.data.datasets[0].data[index] * 100) / sum).toFixed(2) +
                  "%",
                xLine + extraLine + plusFivePx,
                yLine
              );
            });
          });
        },
      };
      // config
      const config = {
        type: "pie",
        data,
        options: {
          maintainAspectRatio: false,
          layout: {
            padding: 30,
          },
          scales: {
            y: {
              display: false,
              beginAtZero: true,
              ticks: {
                display: false,
              },
              grid: {
                display: false,
              },
            },
            x: {
              display: false,
              ticks: {
                display: false,
              },
              grid: {
                display: false,
              },
            },
          },
          plugins: {
            legend: {
              display: false,
            },
          },
        },
        plugins: [pieLabelsLine],
      };

      // render init block
      const myChart = new Chart(document.getElementById("myChart"), config);

https://codepen.io/BillDou/pen/oNoGBXb

Upvotes: 6

Thiện Nguyễn
Thiện Nguyễn

Reputation: 11

I resolved: We add script to file global:

if(window.Chartist && Chartist.Pie && !Chartist.Pie.prototype.resolveOverlap) {
        Chartist.Pie.prototype.resolveOverlap = function() {
            this.on('draw', function(ctx) {
                if(ctx.type == 'label') {
                    let gText = $(ctx.group._node).find('text');
                    let ctxHeight = ctx.element.height();
                    gText.each(function(index, ele){
                        let item = $(ele);
                        let diff = ctx.element.attr('dy') - item.attr('dy');
                        if(diff == 0) {
                            return false;
                        }
                        if(Math.abs(diff) < ctxHeight) {
                            ctx.element.attr({dy: ctx.element.attr('dy') - ctxHeight});
                        }
                    });
                }
            });
        };
    }

and then:

new Chartist.Pie(element, data, options).resolveOverlap();

Upvotes: 1

Simon Sheep
Simon Sheep

Reputation: 310

There is a new plugin (since a year), called chartjs-plugin-piechart-outlabels

Just import the source
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-piechart-outlabels"></script>
and use it with the outlabeledPie type

var randomScalingFactor = function() {
return Math.round(Math.random() * 100);
};
var ctx = document.getElementById("chart-area").getContext("2d");
var myDoughnut = new Chart(ctx, {
type: 'outlabeledPie',
data: {
labels: ["January", "February", "March", "April", "May"],
...
plugins: {
        legend: false,
        outlabels: {
           text: '%l %p',
           color: 'white',
           stretch: 45,
           font: {
               resizable: true,
               minSize: 12,
               maxSize: 18
           }
        }
     }
})

Upvotes: 10

Deep 3015
Deep 3015

Reputation: 10075

The real problem lies with the overlapping of the labels when the slices are small.You can use PieceLabel.js which solves the issue of overlapping labels by hiding it . You mentioned that you cannot hide labels so use legends, which will display names of all slices

Or if you want exact behavior you can go with the highcharts, but it requires licence for commercial use.

var randomScalingFactor = function() {
  return Math.round(Math.random() * 100);
};
var ctx = document.getElementById("chart-area").getContext("2d");
var myDoughnut = new Chart(ctx, {
  type: 'pie',
  data: {
    labels: ["January", "February", "March", "April", "May"],
    datasets: [{
      data: [
        250,
        30,
        5,
        4,
        2,

      ],
      backgroundColor: ['#ff3d67', '#ff9f40', '#ffcd56', '#4bc0c0', '#999999'],
      borderColor: 'white',
      borderWidth: 5,
    }]
  },
  showDatapoints: true,
  options: {
    tooltips: {
      enabled: false
    },
    pieceLabel: {
      render: 'label',
      arc: true,
      fontColor: '#000',
      position: 'outside'
    },
    responsive: true,
    legend: {
      position: 'top',
    },
    title: {
      display: true,
      text: 'Testing',
      fontSize: 20
    },
    animation: {
      animateScale: true,
      animateRotate: true
    }
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.6.0/Chart.min.js"></script>
<script src="https://cdn.rawgit.com/emn178/Chart.PieceLabel.js/master/build/Chart.PieceLabel.min.js"></script>
<canvas id="chart-area"></canvas>

Fiddle demo

Upvotes: 2

Related Questions