Chartjs-Change tooltip positioning in doughnut chart using Chart.js

0πŸ‘

βœ…

It used to be a lot easier to reverse the tooltips in previous versions of chart.js (v2.3 and before). All you had to do was overwrite the determineAlignment tooltip method and reverse the logic.

However starting in v2.4, the functions that calculate the tooltip positions (including determineAlignment) were made private, so there is no longer a way to simply overwrite them (instead you have to duplicate them).

Here is a working reversed tooltip solution that unfortunately requires a lot of copy and paste from the chart.js source (this is required since the methods are private). The risk with this approach is that the underlying private functions could change in new releases at any time and your new reverse tooltip could break unexpectedly.

With that said, here is walk through of the implementation (with a codepen example at the bottom).

1) First, let’s extend the Chart.Tooltip object and create a new Chart.ReversedTooltip object. We really only need to overwrite the update method since it performs all the positioning logic. In fact, this overwrite is just a straight copy and paste from the source because we actually only need to modify the private determineAlignment method which is called by update.

// create a new reversed tooltip.  we must overwrite the update method which is
// where all the positioning occurs
Chart.ReversedTooltip = Chart.Tooltip.extend({
  update: function(changed) {
    var me = this;
    var opts = me._options;

    // Need to regenerate the model because its faster than using extend and it is necessary due to the optimization in Chart.Element.transition
    // that does _view = _model if ease === 1. This causes the 2nd tooltip update to set properties in both the view and model at the same time
    // which breaks any animations.
    var existingModel = me._model;
    var model = me._model = getBaseModel(opts);
    var active = me._active;

    var data = me._data;
    var chartInstance = me._chartInstance;

    // In the case where active.length === 0 we need to keep these at existing values for good animations
    var alignment = {
      xAlign: existingModel.xAlign,
      yAlign: existingModel.yAlign
    };
    var backgroundPoint = {
      x: existingModel.x,
      y: existingModel.y
    };
    var tooltipSize = {
      width: existingModel.width,
      height: existingModel.height
    };
    var tooltipPosition = {
      x: existingModel.caretX,
      y: existingModel.caretY
    };

    var i, len;

    if (active.length) {
      model.opacity = 1;

      var labelColors = [];
      tooltipPosition = Chart.Tooltip.positioners[opts.position](active, me._eventPosition);

      var tooltipItems = [];
      for (i = 0, len = active.length; i < len; ++i) {
        tooltipItems.push(createTooltipItem(active[i]));
      }

      // If the user provided a filter function, use it to modify the tooltip items
      if (opts.filter) {
        tooltipItems = tooltipItems.filter(function(a) {
          return opts.filter(a, data);
        });
      }

      // If the user provided a sorting function, use it to modify the tooltip items
      if (opts.itemSort) {
        tooltipItems = tooltipItems.sort(function(a, b) {
          return opts.itemSort(a, b, data);
        });
      }

      // Determine colors for boxes
      helpers.each(tooltipItems, function(tooltipItem) {
        labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, chartInstance));
      });

      // Build the Text Lines
      model.title = me.getTitle(tooltipItems, data);
      model.beforeBody = me.getBeforeBody(tooltipItems, data);
      model.body = me.getBody(tooltipItems, data);
      model.afterBody = me.getAfterBody(tooltipItems, data);
      model.footer = me.getFooter(tooltipItems, data);

      // Initial positioning and colors
      model.x = Math.round(tooltipPosition.x);
      model.y = Math.round(tooltipPosition.y);
      model.caretPadding = helpers.getValueOrDefault(tooltipPosition.padding, 2);
      model.labelColors = labelColors;

      // data points
      model.dataPoints = tooltipItems;

      // We need to determine alignment of the tooltip
      tooltipSize = getTooltipSize(this, model);
      alignment = determineAlignment(this, tooltipSize);
      // Final Size and Position
      backgroundPoint = getBackgroundPoint(model, tooltipSize, alignment);
    } else {
      model.opacity = 0;
    }

    model.xAlign = alignment.xAlign;
    model.yAlign = alignment.yAlign;
    model.x = backgroundPoint.x;
    model.y = backgroundPoint.y;
    model.width = tooltipSize.width;
    model.height = tooltipSize.height;

    // Point where the caret on the tooltip points to
    model.caretX = tooltipPosition.x;
    model.caretY = tooltipPosition.y;

    me._model = model;

    if (changed && opts.custom) {
      opts.custom.call(me, model);
    }

    return me;
  },
});

2) As you can see, the update method uses a handful of private methods (e.g. getBaseModel, createTooltipItem, determineAlignment, etc.). In order for our update method to actually work, we have to provide an implementation for each of these methods. Here again is another copy and paste from the source. The only method that we need to modify however is the determineAlignment method. Here is the modified version that reverses the alignment logic.

// modified from source to reverse the position
function determineAlignment(tooltip, size) {
  var model = tooltip._model;
  var chart = tooltip._chart;
  var chartArea = tooltip._chartInstance.chartArea;
  var xAlign = 'center';
  var yAlign = 'center';

  // set caret position to top or bottom if tooltip y position will extend outsite the chart top/bottom
  if (model.y < size.height) {
    yAlign = 'top';
  } else if (model.y > (chart.height - size.height)) {
    yAlign = 'bottom';
  }

  var leftAlign, rightAlign; // functions to determine left, right alignment
  var overflowLeft, overflowRight; // functions to determine if left/right alignment causes tooltip to go outside chart
  var yAlign; // function to get the y alignment if the tooltip goes outside of the left or right edges
  var midX = (chartArea.left + chartArea.right) / 2;
  var midY = (chartArea.top + chartArea.bottom) / 2;

  if (yAlign === 'center') {
    leftAlign = function(x) {
      return x >= midX;
    };
    rightAlign = function(x) {
      return x < midX;
    };
  } else {
    leftAlign = function(x) {
      return x <= (size.width / 2);
    };
    rightAlign = function(x) {
      return x >= (chart.width - (size.width / 2));
    };
  }

  overflowLeft = function(x) {
    return x - size.width < 0;
  };
  overflowRight = function(x) {
    return x + size.width > chart.width;
  };
  yAlign = function(y) {
    return y <= midY ? 'bottom' : 'top';
  };

  if (leftAlign(model.x)) {
    xAlign = 'left';

    // Is tooltip too wide and goes over the right side of the chart.?
    if (overflowLeft(model.x)) {
      xAlign = 'center';
      yAlign = yAlign(model.y);
    }
  } else if (rightAlign(model.x)) {
    xAlign = 'right';

    // Is tooltip too wide and goes outside left edge of canvas?
    if (overflowRight(model.x)) {
      xAlign = 'center';
      yAlign = yAlign(model.y);
    }
  }

  var opts = tooltip._options;
  return {
    xAlign: opts.xAlign ? opts.xAlign : xAlign,
    yAlign: opts.yAlign ? opts.yAlign : yAlign
  };
};

3) Now that our new Chart.ReversedTooltip is complete, we need to use the plugin system to change the original tooltip to our reversed tooltip. We can do this using the afterInit plugin method.

Chart.plugins.register({
  afterInit: function (chartInstance) {
    // replace the original tooltip with the reversed tooltip
    chartInstance.tooltip = new Chart.ReversedTooltip({
      _chart: chartInstance.chart,
      _chartInstance: chartInstance,
      _data: chartInstance.data,
      _options: chartInstance.options.tooltips
    }, chartInstance);

    chartInstance.tooltip.initialize();
  }
});

After all that, we finally have reversed tooltips! Checkout a full working example at this codepen.

It’s also worth mentioning that this approach is very brittle and, as I mentioned, can easily break overtime (on account of the copy and pasting required). Another option would be to just use a custom tooltip instead and position it wherever you desire on the chart.

Checkout this chart.js sample that shows how to setup and use a custom tooltip. You could go with this approach and just modify the positioning logic.

0πŸ‘

If you have a small tooltip label, you can use simple chart.js options to fix overlaps issue:

plugins: {
  tooltip: {
    xAlign: 'center',
    yAlign: 'bottom'
  }
}

0πŸ‘

I managed to solve the same by setting zIndex of Doughnut wrapper div to 1, settting the zIndex of text shown in the middle of Doughnut to -1, and canvas is transparent by default.
Hope this hels.

Leave a comment