How can I show a subset of data on pie pieces in Chart.JS while still displaying the superset when hovering?
Asked Answered
M

2

1

I've got a pie chart that looks like this when hovering over a piece of pie:

enter image description here

Except for wanting the legend to the right instead of on top, I'm fairly content with this, but I want just the percentage value to display "all the time" in the pie pieces - and still have the <name> (<%val>): <data> displayed on hover.

In other words, I want the pie to look something like this:

enter image description here

How can I break that one piece of data out (the percentage) and draw that onto each piece of pie?

Here is the code I'm using so far:

var formatter = new Intl.NumberFormat("en-US");
var data = {
    labels: [
        "Bananas (18%)",
        "Lettuce, Romaine (14%)",
        "Melons, Watermelon (10%)",
        "Pineapple (10%)",
        "Berries (10%)",
        "Lettuce, Spring Mix (9%)",
        "Broccoli (8%)",
        "Melons, Honeydew (7%)",
        "Grapes (7%)",
        "Melons, Cantaloupe (7%)"
    ],
    datasets: [
        {
            data: [2755, 2256, 1637, 1608, 1603, 1433, 1207, 1076, 1056, 1048],
            backgroundColor: [
                "#FFE135",
                "#3B5323",
                "#fc6c85",
                "#ffec89",
                "#021c3d",
                "#3B5323",
                "#046b00",
                "#cef45a",
                "#421C52",
                "#FEA620"
            ]
        }
    ]
};

var optionsPie = {
    responsive: true,
    scaleBeginAtZero: true,
    tooltips: {
        callbacks: {
            label: function (tooltipItem, data) {
                return data.labels[tooltipItem.index] + ": " +
                    formatter.format(data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]);
            }
        }
    }
};

var ctx = $("#top10ItemsChart").get(0).getContext("2d");
var top10PieChart = new Chart(ctx,
{
    type: 'pie',
    data: data,
    options: optionsPie
});

$("#top10Legend").html(top10PieChart.generateLegend());

Do I need to add an afterDraw() event and, if so, just how does it need to look to accomplish this?

UPDATE

I tried adding an onAnimationComplete() callback to the chart constructor:

var top10PieChart = new Chart(ctx,
{
    type: 'pie',
    data: data,
    options: optionsPie,
    onAnimationComplete: function () {
        var ctx = this.chart.ctx;
        ctx.font = this.scale.font;
        ctx.fillStyle = this.scale.textColor;
        ctx.textAlign = "center";
        ctx.textBaseline = "center";

        this.datasets.forEach(function(dataset) {
            dataset.points.forEach(function(points) {
                ctx.fillText(points.value, points.x, points.y - 10);
            });
        });
    }
});

...but it does nothing.

UPDATE 2

I also tried appending the following to the options object:

,
tooltipTemplate: "<%= value %>",
onAnimationComplete: function () {
    this.showTooltip(this.datasets[0].bars, true);
}

...with the same results (no change).

UPDATE 3

Okay, I was hopeful that the answer here would work, and ended up with this new code for my pie chart based on the answer there:

Added to optionsPie:

showTooltips: false,
onAnimationProgress: drawSegmentValues

Added after reference to element is retrieved:

var midX = canvas.width / 2;
var midY = canvas.height / 2

Added after pie chart instance is constructed:

var radius = top10PieChart.outerRadius;

Added function drawSegmentValues()

In context (no pun intended):

        // Top 10 Pie Chart
        var formatter = new Intl.NumberFormat("en-US");
        var data = {
            labels: [
                "Bananas (18%)",
                "Lettuce, Romaine (14%)",
                "Melons, Watermelon (10%)",
                "Pineapple (10%)",
                "Berries (10%)",
                "Lettuce, Spring Mix (9%)",
                "Broccoli (8%)",
                "Melons, Honeydew (7%)",
                "Grapes (7%)",
                "Melons, Cantaloupe (7%)"
            ],
            datasets: [
                {
                    data: [2755, 2256, 1637, 1608, 1603, 1433, 1207, 1076
1056, 1048],
                    backgroundColor: [
                        "#FFE135",
                        "#3B5323",
                        "#fc6c85",
                        "#ffec89",
                        "#021c3d",
                        "#3B5323",
                        "#046b00",
                        "#cef45a",
                        "#421C52",
                        "#FEA620"
                    ]
                }
            ]
        };

        var optionsPie = {
            responsive: true,
            scaleBeginAtZero: true,
            legend: {
                display: false
            },
            tooltips: {
                callbacks: {
                    label: function (tooltipItem, data) {
                        return data.labels[tooltipItem.index] + ": " +
                         formatter.format
data.datasets[tooltipItem.datasetIndex].data[
tooltipItem.index]);
                    }
                }
            },
            showTooltips: false,
            onAnimationProgress: drawSegmentValues
        };

        var ctx = $("#top10ItemsChart").get(0).getContext("2d");
        var midX = canvas.width / 2;
        var midY = canvas.height / 2
        var top10PieChart = new Chart(ctx,
        {
            type: 'pie',
            data: data,
            options: optionsPie//,
        });
        var radius = top10PieChart.outerRadius;

        $("#top10Legend").html(top10PieChart.generateLegend());
        // </ Top 10 Pie Chart

        // Price Compliance Bar Chart
        . . . this horizontal bar chart code elided for brevity, bu
unchanged from their working state
        // Forecast/Impact Analysis Bar chart
        . . . this horizontal bar chart code also elided for brevity, bu
unchanged from their working state

    function drawSegmentValues() {
        for (var i = 0; i < top10PieChart.segments.length; i++) {
            ctx.fillStyle = "white";
            var textSize = canvas.width / 10;
            ctx.font = textSize + "px Verdana";
            // Get needed variables
            var value = top10PieChart.segments[i].value;
            var startAngle = top10PieChart.segments[i].startAngle;
            var endAngle = top10PieChart.segments[i].endAngle;
            var middleAngle = startAngle + ((endAngle - startAngle)
2);

            // Compute text location
            var posX = (radius / 2) * Math.cos(middleAngle) + midX;
            var posY = (radius / 2) * Math.sin(middleAngle) + midY;

            // Text offside by middle
            var w_offset = ctx.measureText(value).width / 2;
            var h_offset = textSize / 4;

            ctx.fillText(value, posX - w_offset, posY + h_offset);
        }
    }

...but it completely hosed all three charts, leaving nothing visible/drawing.

UPDATE 4

By the way, I tried the given answer, but with no result/change - it still displays data on hover, as designed, but there is nothing otherwise.

Also, the related fiddle shows both the value and the percentage; due to real estate restrictions, I want the percentage only to be "always on" - the "whole enchilada" is only to be seen when hovering.

Manos answered 22/9, 2016 at 18:25 Comment(2)
Hi I noticed you comment on my answer. Would you mind providing a snippet/fiddle for what you have currently? For the most part it appears to be syntax related problems such as in middleAngle there is a new line other than a division (/).Teeter
I see no answer of yours - was it deleted (I have been away for several daze, on vacation).Manos
M
2

The following solution is using the same calculation as lamelemon's but using Chart.js plugins, which brings additional benefits :

  • There is no longer this blink effect caused by the animation.onComplete since it occurs immediatly without waiting for antything.

  • The tooltip is above the text, which is more natural.

Follows the plugin that does it :

Chart.pluginService.register({
    afterDatasetsDraw: function(chartInstance) {

        // We get the canvas context
        var ctx = chartInstance.chart.ctx;

        // And set the properties we need
        ctx.font = Chart.helpers.fontString(14, 'bold', Chart.defaults.global.defaultFontFamily);
        ctx.textAlign = 'center';
        ctx.textBaseline = 'bottom';
        ctx.fillStyle = '#666';

        // For ervery dataset ...
        chartInstance.config.data.datasets.forEach(function(dataset) {

            // For every data in the dataset ...
            for (var i = 0; i < dataset.data.length; i++) {

                // We get all the properties & values we need
                var model = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._model,
                    total = dataset._meta[Object.keys(dataset._meta)[0]].total,
                    mid_radius = model.innerRadius + (model.outerRadius - model.innerRadius) / 2,
                    start_angle = model.startAngle,
                    end_angle = model.endAngle,
                    mid_angle = start_angle + (end_angle - start_angle) / 2;

                // We calculate the right positions for our text
                var x = mid_radius * 1.5 * Math.cos(mid_angle);
                var y = mid_radius * 1.5 * Math.sin(mid_angle);

                // We calculate the percentage
                var percent = String(Math.round(dataset.data[i] / total * 100)) + "%";
                // And display it
                ctx.fillText(percent, model.x + x, model.y + y);
            }
        });
    }
});


You can check this plugin working on this jsFiddle, and here is the result :

enter image description here

Microeconomics answered 28/9, 2016 at 9:34 Comment(9)
It does nothing for me; I added it first (above all the other code of mine that is listed); does placement matter - should it be before or after any specific piece of code?Manos
@B.ClayShannon Plugins must always be placed before you create the chart (before calling new Chart()). Apart from this, there is no specific rules for this, as you can see in the fiddle.Microeconomics
I have it before everything else, so I don't know what the problem is. No err msgs in the console...Manos
@B.ClayShannon Weird .. Which version of Chart.js are you using ? The fiddle uses the v2.2.2Microeconomics
2,1,3; and if I remove " legend: { display: false }, " from the options (as you have), a second legend replaces the pie, displaying in a circular fashion.Manos
Well, that was the "rub"; changing to version 2.2.2 causes it to work. The only "gotcha" now is the cutoff point between using white and black text for the percentages - the "honeydew" and "bananas" slices should be black instead of white (like pineapple).Manos
Oh, I see, you're just manually specifying element 3 (pineapple) for special treatment.Manos
For now (mock data), I'm using if (i === 0 || i === 3 || i === 7) but will need a way to programmatically determine which font color to use.Manos
@B.ClayShannon Yeah it's hard coded, which is not the best, I agree. I'm sure you can find a simple function that calculates the best color depending on the background's.Microeconomics
M
1

Alright looks like you have some simple syntax errors in your updated code that probably happened with copying over. Ignoring those, you can manage this by creating a function in your animation options. Here is the fiddle. Thanks to Hung Tran's work here.

animation: {
  duration: 0,
  easing: "easeOutQuart",
  onComplete: function () {
    var ctx = this.chart.ctx;
    ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontFamily, 'normal', Chart.defaults.global.defaultFontFamily);
    ctx.textAlign = 'center';
    ctx.textBaseline = 'bottom';

    this.data.datasets.forEach(function (dataset) {

      for (var i = 0; i < dataset.data.length; i++) {
        var model = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._model,
            total = dataset._meta[Object.keys(dataset._meta)[0]].total,
            mid_radius = model.innerRadius + (model.outerRadius - model.innerRadius)/2,
            start_angle = model.startAngle,
            end_angle = model.endAngle,
            mid_angle = start_angle + (end_angle - start_angle)/2;

        var x = mid_radius * Math.cos(mid_angle);
        var y = mid_radius * Math.sin(mid_angle);

        ctx.fillStyle = '#fff';
        if (i == 3){ // Darker text color for lighter background
          ctx.fillStyle = '#444';
        }
        var percent = String(Math.round(dataset.data[i]/total*100)) + "%";
        // this prints the data number
        ctx.fillText(dataset.data[i], model.x + x, model.y + y);
        // this prints the percentage
        ctx.fillText(percent, model.x + x, model.y + y + 15);
      }
    });               
  }
}
Micronesia answered 23/9, 2016 at 3:58 Comment(3)
Should the "if (i == 3)" be "if (i === 3)" instead? I get squigglies there with just double equals.Manos
I still get only text on the pie when I hover with the above animation.Manos
@B.ClayShannon it honestly shouldn't matter as === tests for type equality as well. But seeing as i is assigned 0 on declaration, it's assumed integer to begin with and 3 is obviously an integer. Try removing your tooltips option and just add showToolTips: trueMicronesia

© 2022 - 2024 — McMap. All rights reserved.