How can I calculate a value and place it inside the last bar in a Chart.JS bar chart?

≯℡__Kan透↙ 提交于 2019-12-11 07:08:24

问题


Since it seems impossible or highly difficult (no working answers here) to place a label above the last bar on a bar chart, I would like to now know how to place a label inside the last bar.

I am adding labels to all the bars of a horizontal bar chart with this code:

Chart.pluginService.register({
    afterDraw: function (chartInstance) {
        if (chartInstance.id !== 1) return; // affect this one only
        var ctx = chartInstance.chart.ctx;
        // render the value of the chart above the bar
        ctx.font = Chart.helpers.fontString(14, 'bold', Chart.defaults.global.defaultFontFamily);
        ctx.textAlign = 'center';
        ctx.textBaseline = 'bottom';

        chartInstance.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;
                ctx.fillText(dataset.data[i] + (Number.isInteger(dataset.data[i]) ? ".0" : "") + "%", ((model.x + model.base) / 2), model.y + (model.height / 3));
            }
        });
    }
});

That working chart looks like this:

...but this is a "regular" (vertical) bar chart, and my attempt to add a label to just the last bar like so:

Chart.pluginService.register({
    afterDraw: function (chartInstance) {
        if (chartInstance.id !== 2) return; // affect this one only
        var ctx = chartInstance.chart.ctx;
        // render the value of the chart above the bar
        ctx.font = Chart.helpers.fontString(14, 'bold', Chart.defaults.global.defaultFontFamily);
        ctx.textAlign = 'center';
        ctx.textBaseline = 'center';

        var model = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._model;
        ctx.fillText(dataset.data[dataset.data.length - 1]);
    }
});

...fails (nothing displays and, in fact, it causes problems not only with its own appearance, but that of other charts on the page, even though I use code to only apply this event to this particular chart:

So how can I calculate a value (based on last and penultimate bar values) and then place that calculated "label" inside the last bar in a Chart.JS bar chart?

With the code I attempted above, I just wanted to get the raw value for the bar inside the bar for now; the calculation portion of the logic can be left until later.

One possibility would be to make it a stacked bar chart, with nothing added to any of the bars but the last one, and then make its background color white to blend in with the chart background...

UPDATE

I tried my last idea; thinking maybe I could accomplish what I wanted by morphing my chart into a stacked bar chart (from a simple bar chart), adding a second set of mostly "zero" data as the second piece of the stack, like so:

Old code excerpt:

var forecastChartData = {
    labels: [
        "Total Sales"
    ],
    datasets: [
    {
        label: "8/28/2016 - 9/3/2016",
        backgroundColor: "rgba(255,0,0,0.75)",
        hoverBackgroundColor: "rgba(255,0,0,1)",
        data: [240]
    },
    {
        label: "9/4/2016 - 9/10/2016",
        backgroundColor: "rgba(255,153,0,0.75)",
        hoverBackgroundColor: "rgba(255,153,0,1)",
        data: [272]
    },
    {
        label: "9/11/2016 - 9/17/2016",
        backgroundColor: "rgba(255,255,0,0.75)",
        hoverBackgroundColor: "rgba(255,255,0,1)",
        data: [250]
    },
    {
        label: "9/18/2016 - 9/24/2016",
        backgroundColor: "rgba(0,255,0,0.75)",
        hoverBackgroundColor: "rgba(0,255,0,1)",
        data: [232]
    },
    {
        label: "9/25/2016 - 10/1/2016",
        backgroundColor: "rgba(0,0,255,0.75)",
        hoverBackgroundColor: "rgba(0,0,255,1)",
        data: [244]
    }],
};

var forecastOptions = {
    tooltips: {
        enabled: true
    }
};

New code:

var forecastChartData = {
    labels: [
        "Total Sales"
    ],
    datasets: [
    {
        label: "8/28/2016 - 9/3/2016",
        backgroundColor: "rgba(255,0,0,0.75)",
        hoverBackgroundColor: "rgba(255,0,0,1)",
        data: [240]
    },
    {
        label: "9/4/2016 - 9/10/2016",
        backgroundColor: "rgba(255,153,0,0.75)",
        hoverBackgroundColor: "rgba(255,153,0,1)",
        data: [272]
    },
    {
        label: "9/11/2016 - 9/17/2016",
        backgroundColor: "rgba(255,255,0,0.75)",
        hoverBackgroundColor: "rgba(255,255,0,1)",
        data: [250]
    },
    {
        label: "9/18/2016 - 9/24/2016",
        backgroundColor: "rgba(0,255,0,0.75)",
        hoverBackgroundColor: "rgba(0,255,0,1)",
        data: [232]
    },
    {
        label: "9/25/2016 - 10/1/2016",
        backgroundColor: "rgba(0,0,255,0.75)",
        hoverBackgroundColor: "rgba(0,0,255,1)",
        data: [244]
    }],
    [{
        backgroundColor: "rgba(255, 255, 255, 0.5)",
        hoverBackgroundColor: "rgba(200, 200, 200, 1)",
        data: [0, 0, 0, 0, 5.2]
    }]
};

var forecastOptions = {
    scales: {
        xAxes: [
        {
            stacked: true
        }
        ],
        yAxes: [
        {
            stacked: true
        }
        ]
    },
    tooltips: {
        enabled: true
    }
};

But it didn't help at all; what am I doing wrong; if everything, how can I proceed after pursuing the nuclear option with my new code?

UPDATE 2

The code from Hung Tran here gets me close. My options is now:

var forecastOptions = {
    tooltips: {
        enabled: true
    },
    animation: {
        duration: 500,
        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,
                        scale_max = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._yScale.maxHeight;
                    ctx.fillStyle = '#444';
                    var y_pos = model.y - 5;
                    // Make sure data value does not get overflown and hidden
                    // when the bar's value is too close to max value of scale
                    // Note: The y value is reverse, it counts from top down
                    if ((scale_max - model.y) / scale_max >= 0.93)
                        y_pos = model.y + 20;
                    ctx.fillText(dataset.data[i], model.x, y_pos);
                }
            });
        }
    }
};

...and it places the data value above each bar.

As stated, this is close to what I want; what I really want, though, is the difference between the last value and the current value. For example, with this chart:

...what I want to see is:

240, 272 (30), 250 (-22), 232 (-18), 244 (12)

Or perhaps just a value above the last bar, either "244 (12)" or just "12".

That's what they say they want (the latter), but I wouldn't be surprised if they change their mind and want all the vals/diffs.

UPDATE 3

The solution adds no values atop the bars, and eats one of my slices of pie:

UPDATE 4

Here is pretty much all the code in Index.cshtml:

    <style>

        .pieLegend li span {
            display: inline-block;
            width: 12px;
            height: 12px;
            margin-right: 5px;
        }

        #piechartlegendleft {
            height: 360px;
            width: 100%;
        }

        #top10Legend {
            display: inline-block;
        }

            #top10Legend > ul {
                list-style: none;
            }

        #container {
            display: inline-block;
            height: 50%;
            width: 50%;
        }
    </style>
    <script>
        $(document).ready(function () {
            $("body").on("click", "#btnGetData",
        . . .
                    });

            Chart.defaults.global.defaultFontFamily = "Candara";
            Chart.defaults.global.defaultFontSize = 16;

            // Top 10 Pie Chart
            var formatter = new Intl.NumberFormat("en-US");

            Chart.pluginService.register({
                afterDatasetsDraw: function (chartInstance) {
                    var ctx = chartInstance.chart.ctx;

                    ctx.font = Chart.helpers.fontString(14, 'bold', Chart.defaults.global.defaultFontFamily);
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'bottom';
                    ctx.fillStyle = '#666';

                    chartInstance.config.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 * 1.5 * Math.cos(mid_angle);
                            var y = mid_radius * 1.5 * Math.sin(mid_angle);

                            ctx.fillStyle = '#fff';
                            if (i === 0 || i === 3 || i === 7 || i === 9) { // Darker text color for lighter background
                                ctx.fillStyle = '#000';
                            }
                            var percent = String(Math.round(dataset.data[i] / total * 100)) + "%";
                            // this prints the data number
                            // this prints the percentage
                            ctx.fillText(percent, model.x + x, model.y + y);
                        }
                    });
                }
            });

            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]);
                        }
                    }
                },
            };

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

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

            // Price Compliance Bar Chart
            Chart.pluginService.register({
                afterDraw: function (chartInstance) {
                    if (chartInstance.id !== 1) return; // affect this one only
                    var ctx = chartInstance.chart.ctx;
                    // render the value of the chart above the bar
                    ctx.font = Chart.helpers.fontString(14, 'bold', Chart.defaults.global.defaultFontFamily);
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'bottom';

                    ctx.fillStyle = "#000";

                    chartInstance.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;
                            ctx.fillText(dataset.data[i] + (Number.isInteger(dataset.data[i]) ? ".0" : "") + "%", ((model.x + model.base) / 2), model.y + (model.height / 3));
                        }
                    });
                }
            });

            var ctxBarChart = $("#priceComplianceBarChart").get(0).getContext("2d");
            var priceComplianceData = {
                labels: [
                    "Bix Produce", "Capitol City", "Charlies Portland", "Costa Fruit and Produce",
                    "Get Fresh Sales", "Loffredo East", "Loffredo West", "Paragon", "Piazza Produce"
                ],
                datasets: [
                {
                    label: "Price Compliant",
                    backgroundColor: "rgba(34,139,34,0.5)",
                    hoverBackgroundColor: "rgba(34,139,34,1)",
                    data: [99.0, 99.2, 99.4, 98.9, 99.1, 99.5, 99.6, 99.2, 99.7]
                },
                {
                    label: "Non-Compliant",
                    backgroundColor: "rgba(255, 0, 0, 0.5)",
                    hoverBackgroundColor: "rgba(255, 0, 0, 1)",
                    data: [1.0, 0.8, 0.6, 1.1, 0.9, 0.5, 0.4, 0.8, 0.3]
                }
                ]
            }

            var priceComplianceOptions = {
                scales: {
                    xAxes: [
                    {
                        stacked: true
                    }],
                    yAxes: [
                    {
                        stacked: true
                    }]
                },
                tooltips: {
                    enabled: false
                }
            };

            var priceBarChart = new Chart(ctxBarChart,
            {
                type: 'horizontalBar',
                data: priceComplianceData,
                options: priceComplianceOptions
            });
            // </Price Compliance Bar Chart

            // Forecast/Impact Analysis chart
            var ctxForecastChart = $("#forecastLineChart"); //.get(0).getContext("2d");
            // See if the "olde style" makes any diff (used by the Chart.JS Whisperer)
            //var ctxForecastChart = document.getElementById("forecastLineChart"); // no diff

            var forecastChartData = {
                labels: ["Total Sales"],
                datasets: [
                {
                    label: "8/28/2016 - 9/3/2016",
                    backgroundColor: "rgba(255,0,0,0.75)",
                    hoverBackgroundColor: "rgba(255,0,0,1)",
                    data: [240]
                },
                {
                    label: "9/4/2016 - 9/10/2016",
                    backgroundColor: "rgba(255,153,0,0.75)",
                    hoverBackgroundColor: "rgba(255,153,0,1)",
                    data: [272]
                },
                {
                    label: "9/11/2016 - 9/17/2016",
                    backgroundColor: "rgba(255,255,0,0.75)",
                    hoverBackgroundColor: "rgba(255,255,0,1)",
                    data: [250]
                },
                {
                    label: "9/18/2016 - 9/24/2016",
                    backgroundColor: "rgba(0,255,0,0.75)",
                    hoverBackgroundColor: "rgba(0,255,0,1)",
                    data: [232]
                },
                {
                    label: "9/25/2016 - 10/1/2016",
                    backgroundColor: "rgba(0,0,255,0.75)",
                    hoverBackgroundColor: "rgba(0,0,255,1)",
                    data: [244]
                }]
        };

        var forecastOptions = {
            tooltips: {
                enabled: true
            },
            animation: {
                    duration: 500,
                    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,
                                    scale_max = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._yScale.maxHeight;
                                ctx.fillStyle = '#444';
                                var y_pos = model.y - 5;
                                // Make sure data value does not get overflown and hidden
                                // when the bar's value is too close to max value of scale
                                // Note: The y value is reverse, it counts from top down
                                if ((scale_max - model.y) / scale_max >= 0.93)
                                    y_pos = model.y + 20;
                                ctx.fillText(dataset.data[i], model.x, y_pos);
                            }
                        });
                    }
                }
            };

        var forecastBarChart = new Chart(ctxForecastChart, {
            type: 'bar',
            //type: 'line',
            data: forecastChartData,
            options: forecastOptions
        });
        // </ Forecast/Impact Analysis chart

        $('#delperfTable').DataTable({
            "paging": false,
            "info": false,
            "searching": false
        });
        });


    </script>
</head>

<body>
    <div class="container body-content">
        <div class="jumbotronjr">
            <div class="col-md-3" style="margin-top: 0.6cm">
                <img src="http://www.proactusa.com/wp-content/themes/proact/images/pa_logo_notag.png" height="86" width="133" alt="PRO*ACT usa logo">
            </div>
            <div class="col-md-9">
                <label class="titletext" style="margin-top: 0.2cm;">Customer Dashboard</label>
                <br />
                <label class="titletextjr" style="margin-top: -2.2cm;" id="unitName">SODEXO</label>
                <label class="cccsfont"> for the week of September 25          </label>
                <input class="smalldatepicker" type="date" id="datepickerFrom" value="2016-09-25">
                </input>
                <label class="cccsfont"> to </label>
                <input type="date" class="smalldatepicker" id="datepickerTo" value="2016-10-01">
                </input>
                <span id="newhourglass" class="fa fa-4x fa-refresh fa-spin boxRefresh hide" runat="server"></span>
                <button class="btn btn-success btn-sm green" id="btnGetData"><span class="glyphicon glyphicon-refresh"></span></button>
            </div>
        </div>

        <div class="row">
            <div class="col-md-12">
                <hr />
            </div>
        </div>

        <div class="row">
            <div class="col-md-12">
            </div>
        </div>

        <div class="row" id="top10Items">
            <div class="col-md-6">
                <div class="topleft">
                    <h2 class="sectiontext">Top 10 Items</h2>
                    <br />
                    <div id="piechartlegendleft">
                        <div id="container">
                            <canvas id="top10ItemsChart" width="800" height="800"></canvas>
                        </div>
                        <div id="top10Legend" class="pieLegend"></div>
                    </div>
                </div>
            </div>

            <div class="col-md-6">
                <div class="topright">
                    <h2 class="sectiontext">Price Compliance</h2>
                    <div class="graph_container">
                        <canvas id="priceComplianceBarChart"></canvas>
                    </div>
                </div>
            </div>
        </div>

        <div class="row">
            <div class="col-md-6">
                <div class="bottomleft">
                    <h2 class="sectiontext">Forecast/Impact Analysis</h2>
                    <div class="graph_container">
                        <canvas id="forecastLineChart"></canvas>
                    </div>
                </div>
            </div>

            <div class="col-md-6">
                <div class="bottomright">
                    <h2 class="sectiontext">Delivery Performance</h2>
                    <table id="delperfTable">
            . . .
                    </table>
                </div>
            </div>
        </div>

    </div>
</body>
</html>

回答1:


In your animation.onComplete callback, you can get the previous dataset with the following code :

// `dataset` here -----------------------------------------┐
// is the current dataset you are working on               |
// (since you loop through all of them in your callback)   V
var previousDataset = chartInstance.config.data.datasets[dataset._meta[Object.keys(dataset._meta)[0]].controller.index - 1];

Now that you can access the previous dataset, you can also get its value. Follows the full callback to make it more understandable :

var forecastOptions = {
    tooltips: {
        enabled: true
    },
    animation: {
        duration: 500,
        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++) {

                    // We get the previous dataset here
                    var previousDataset = dataset._meta[Object.keys(dataset._meta)[0]].controller.chart.config.data.datasets[dataset._meta[Object.keys(dataset._meta)[0]].controller.index - 1];

                    var model = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._model;
                    var scale_max = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._yScale.maxHeight;
                    ctx.fillStyle = '#444';
                    var y_pos = model.y - 5;
                    if ((scale_max - model.y) / scale_max >= 0.93)
                        y_pos = model.y + 20;

                    // If the previous dataset is not `undefined` (actually exists) ..
                    if(typeof previousDataset !== "undefined") {
                        // We write the data, with the difference with the previous one
                        ctx.fillText(dataset.data[i] + " (" + (dataset.data[i] - previousDataset.data[i]) + ")", model.x, y_pos);
                    }
                    else {
                        // We only write the data
                        ctx.fillText(dataset.data[i], model.x, y_pos);
                    }
                }
            });
        }
    }
};

You can see this callback working on this jsFiddle, and here is its result :

Note: Feel free to comment if you need any small changes such as a percentage difference instead.



来源:https://stackoverflow.com/questions/39779510/how-can-i-calculate-a-value-and-place-it-inside-the-last-bar-in-a-chart-js-bar-c

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!