user3284707
user3284707

Reputation: 3341

Charts.js tooltip overlapping text on chart

As Charts.js does not yet support annotations, I have added annotations of the data points after the chart is drawn. using ctx.fillText as shown below.

        animation: {
            animateScale: true,
            animateRotate: true,
            onComplete: function () {
                var chartInstance = this.chart,
                    ctx = chartInstance.ctx;
                ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontSize, Chart.defaults.global.defaultFontStyle, Chart.defaults.global.defaultFontFamily);
                ctx.textAlign = 'center';
                ctx.fillStyle = this.chart.config.options.defaultFontColor;
                ctx.textBaseline = 'bottom';

                this.data.datasets.forEach(function (dataset, i) {
                    var meta = chartInstance.controller.getDatasetMeta(i);
                    meta.data.forEach(function (bar, index) {
                        data = dataset.data[index];
                        ctx.fillText(data, bar._model.x, bar._model.y - 5);
                    });
                });
            }
        }

This works great, other than the fact that now the tooltip is shown below the newly added text. This is not that obvious, however sometimes it overlaps in a bad place meaning that you cannot see the tooltip behind.

enter image description here

Is there a way to set the z-index of the ctx.fillText or tooltip so I can layer them correctly?

Upvotes: 5

Views: 7462

Answers (4)

Saroj Shrestha
Saroj Shrestha

Reputation: 2875

If you are looking for the close solution of the code written in the question then here is the code:

let ctx2 = document.getElementById("barChart").getContext("2d");

        let chart = new Chart(ctx2, {
            type: 'bar',
            data: {
                labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
                datasets: [{
                    label: 'Months',
                    data: ['20','30','10','15','50','35','25'],
                    backgroundColor: 'rgba(26,179,148,0.5)',
                    borderColor: 'rgba(75, 192, 192, 1)',
                    borderWidth: 1
                }]
            },
            options: {
                legend: {
                    display: false
                },
                responsive: true,
                maintainAspectRatio: true,
                legendCallback: function(chart) {
                    var text = [];
                    for (var i=0; i<chart.data.datasets.length; i++) {
                        text.push(chart.data.labels[i]);
                    }
                    return text.join("");
                },
                tooltips: {
                    mode: 'index',
                    callbacks: {
                        // Use the footer callback to display the sum of the items showing in the tooltip
                        title: function(tooltipItem, data) {
                            let title_str = data['labels'][tooltipItem[0]['index']];
                            let lastIndex = title_str.lastIndexOf(" ");
                            return title_str.substring(0, lastIndex);
                        },
                        label: function(tooltipItem, data) {
                            return 'val: '+data['datasets'][0]['data'][tooltipItem['index']];
                        },
                    },
                },

                scales: {
                    xAxes: [{
                        stacked: false,
                        beginAtZero: true,
                        // scaleLabel: {
                        //     labelString: 'Month'
                        // },
                        ticks: {
                            min: 0,
                            autoSkip: false,
                            maxRotation: 60,
                            callback: function(label, index, labels) {
                                return label;
                            }
                        }
                    }]
                }
            },
            plugins:[{
                afterDatasetsDraw: function(chart,options) {
                    // var chartInstance = chart,
                    let ctx = chart.ctx;
                    ctx.font = Chart.defaults.global.defaultFontStyle;
                    ctx.fillStyle = Chart.defaults.global.textColor;
                    ctx.textAlign = "center";
                    ctx.textBaseline = "bottom";

                    chart.data.datasets.forEach(function (dataset, i) {
                        var meta = chart.controller.getDatasetMeta(i);
                        meta.data.forEach(function (bar, index) {
                            ctx.fillText(Math.round(dataset.data[index]), bar._model.x, bar._model.y - 5);
                        });
                    })
                }
            }]
        });
        document.getElementById('barChart').innerHTML = chart.generateLegend();
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div>
    <canvas id="barChart" height="140"></canvas>
</div>

Here, I have made use of plugin afterDatasetDraw. https://www.chartjs.org/docs/latest/developers/plugins.html?h=afterdatasetsdraw

Upvotes: 2

marxin
marxin

Reputation: 3922

If anyone would look for the solution for this issue, there is an easier way of achieving what OP needed.

Instead of drawing in onComplete callback, draw it in afterDatasetsDraw callback. It's being called just before the tooltip gets drawn.

Upvotes: 1

Shinta
Shinta

Reputation: 256

@user3284707 Actually what you have to do is draw the numbers on top of your bars before the tooltips, you are drawing them onComplete, putting them on top of everything.

I draw those numbers using:

Chart.plugins.register({
  beforeDraw: function(chartInstance) {
    if (chartInstance.config.options.showDatapoints) {
      var helpers = Chart.helpers;
      var ctx = chartInstance.chart.ctx;
      var fontColor = helpers.getValueOrDefault(chartInstance.config.options.showDatapoints.fontColor, chartInstance.config.options.defaultFontColor);

      // render the value of the chart above the bar
      ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontSize, 'normal', Chart.defaults.global.defaultFontFamily);
      ctx.textAlign = 'center';
      ctx.textBaseline = 'bottom';
      ctx.fillStyle = fontColor;

      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;
          var scaleMax = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._yScale.maxHeight;
          var yPos = (scaleMax - model.y) / scaleMax >= 0.93 ? model.y + 20 : model.y - 5;
          var label = dataset.data[i] || '';
          ctx.fillText(label.toLocaleString(), model.x, yPos);
        }
      });
    }
  }
});

Notice the beforeDraw there.

Hope this helps 3 years later, I spent the last 30 minutes trying to fix this 🤣

Upvotes: 6

user3284707
user3284707

Reputation: 3341

For anyone interested I managed to figure this out, I ended up looking at the tooltip drawing functions within charts.js and using a modified version of this as a custom tooltip, thus drawing the tooltip after the annotations are added.

First add this to your opptions

config = {
    options: {
        tooltips: {
            enabled: false,
            custom: customTooltips
        }

This then calls the custom tooltip function below.

var currentX = null;
var currentY = null;

var customTooltips = function (tooltip) {

    var helpers = Chart.helpers;
    var ctx = this._chart.ctx;
    var vm = this._view;

    if (vm == null || ctx == null || helpers == null || vm.opacity === 0) {
        return;
    }

    var tooltipSize = this.getTooltipSize(vm);

    var pt = {
        x: vm.x,
        y: vm.y
    };

    if (currentX == vm.x && currentY == vm.y) {
        return;
    }

    currentX = vm.x;
    currentY = vm.y;

   //  IE11/Edge does not like very small opacities, so snap to 0
    var opacity = Math.abs(vm.opacity < 1e-3) ? 0 : vm.opacity;

    // Draw Background
    var bgColor = helpers.color(vm.backgroundColor);
    ctx.fillStyle = bgColor.alpha(opacity * bgColor.alpha()).rgbString();
    helpers.drawRoundedRectangle(ctx, pt.x, pt.y, tooltipSize.width, tooltipSize.height, vm.cornerRadius);
    ctx.fill();

    // Draw Caret
    this.drawCaret(pt, tooltipSize, opacity);

    // Draw Title, Body, and Footer
    pt.x += vm.xPadding;
    pt.y += vm.yPadding;

    // Titles
    this.drawTitle(pt, vm, ctx, opacity);

    // Body
    this.drawBody(pt, vm, ctx, opacity);

    // Footer
    this.drawFooter(pt, vm, ctx, opacity);
};

Upvotes: 1

Related Questions