[Chartjs]-Position tooltip in center of bar

4πŸ‘

βœ…

As you may already know, to position a custom tooltip at the center of a bar, you might need some of itΒ­β€˜s (bar) properties, such as – width, height, top and left position. But unfortunately, there is no straight-forward way of getting these properties, rather you need to calculate them yourself.

To obtain / calculate those properties, you can use the following function (can be named anything), which will return an object containing all these (width, height, top, left) properties of a particular bar, when hovered over.

function getBAR(chart) {
   const dataPoints = tooltipModel.dataPoints,
         datasetIndex = chart.data.datasets.length - 1,
         datasetMeta = chart.getDatasetMeta(datasetIndex),
         scaleBottom = chart.scales['y-axis-0'].bottom,
         bar = datasetMeta.data[dataPoints[0].index]._model,
         canvasPosition = chart.canvas.getBoundingClientRect(),
         paddingLeft = parseFloat(getComputedStyle(chart.canvas).paddingLeft),
         paddingTop = parseFloat(getComputedStyle(chart.canvas).paddingTop),
         scrollLeft = document.body.scrollLeft,
         scrollTop = document.body.scrollTop;

   return {
      top: bar.y + canvasPosition.top + paddingTop + scrollTop,
      left: bar.x - (bar.width / 2) + canvasPosition.left + paddingLeft + scrollLeft,
      width: bar.width,
      height: scaleBottom - bar.y
   }
}

Calculate Center Position

After retrieving the required properties, you can calculate center position of a bar as such :

πšŒπšŽπš—πšπšŽπš›πš‡ β€€=β€€ πš‹πšŠπš›-πš•πšŽπšπš + (πš‹πšŠπš›-πš πš’πšπšπš‘ / 𝟸)

Β­

πšŒπšŽπš—πšπšŽπš›πšˆ β€€=β€€ πš‹πšŠπš›-πšπš˜πš™ + (πš‹πšŠπš›-πš‘πšŽπš’πšπš‘πš / 𝟸)

then, create your custom tooltip element and position it accordingly.


α΄˜Κ€α΄‡α΄ Ιͺᴇᴑ

bar-chart

ʟΙͺᴠᴇ ᴇxα΄€α΄α΄˜ΚŸα΄‡ ⧩

const chart = new Chart(ctx, {
   type: 'bar',
   data: {
      labels: ['Jan', 'Feb', 'Mar', 'Apr'],
      datasets: [{
         label: 'Revenue',
         data: [4, 2, 3, 3],
         backgroundColor: '#2d4e6d'
      }, {
         label: 'Expenses',
         data: [3, 3.5, 4, 1],
         backgroundColor: '#c06526'
      }, {
         label: 'Profit',
         data: [3, 2.5, 4, 2],
         backgroundColor: '#e0ecf0'
      }]
   },
   options: {
      scales: {
         xAxes: [{
            stacked: true
         }],
         yAxes: [{
            stacked: true,
            ticks: {
               beginAtZero: true
            }
         }]
      },
      tooltips: {
         enabled: false,
         custom: function(tooltipModel) {
         /*** jQuery IS USED FOR SIMPLICITY ***/
         
            /* TOOLTIP & CARET ELEMENT */
            let tooltip = $('#tooltip');
            let tooltipCaret = $('#tooltip-caret');

            /* CREATE TOOLTIP & CARET ELEMENT AT FIRST RENDER */
            if (!tooltip.length && !tooltipCaret.length) {
               tooltip = $('<div></div>').attr('id', 'tooltip');
               tooltipCaret = $('<div></div>').attr('id', 'tooltip-caret');
               $('body').append(tooltip, tooltipCaret);
            }

            /* HIDE IF NO TOOLTIP */
            if (!tooltipModel.opacity) {
               tooltip.hide();
               tooltipCaret.hide();
               return;
            }

            /* GET BAR PROPS (width, height, top, left) */
            const barWidth = getBAR(this._chart).width,
                  barHeight = getBAR(this._chart).height,
                  barTop = getBAR(this._chart).top,
                  barLeft = getBAR(this._chart).left;

            /* SET STYLE FOR TOOLTIP 
            	(these can also be set in separate css file) */
            tooltip.css({
               "display": "inline-block",
               "position": "absolute",
               "color": "rgba(255, 255, 255, 1)",
               "background": "rgba(0, 0, 0, 0.7)",
               "padding": "5px",
               "font": "12px Arial",
               "border-radius": "3px",
               "white-space": "nowrap",
               "pointerEvents": "none"
            });

            /* SET STYLE FOR TOOLTIP CARET 
            	(these can also be set in separate css file) */
            tooltipCaret.css({
               "display": "block",
               "position": "absolute",
               "width": 0,
               "pointerEvents": "none",
               "border-style": "solid",
               "border-width": "8px 10px 8px 0",
               "border-color": "transparent rgba(0, 0, 0, 0.7) transparent transparent"
            });

            /* ADD CONTENT IN TOOLTIP */
            tooltip.text('ChartJS');
            tooltip.append('<br><div class="color-box"></div><label style="display: block; margin: -16px 0 0 16px;"> Custom Tooltip<label>');

            /* POSITION TOOLTIP & CARET
            (position should be set after tooltip & caret is rendered) */
            const centerX = barLeft + (barWidth / 2),
                  centerY = barTop + (barHeight / 2)
            
            tooltip.css({
               "top": centerY - (tooltip.outerHeight() / 2) + 'px',
               "left": centerX + tooltipCaret.outerWidth() + 'px'
            });
            tooltipCaret.css({
               "top": centerY - (tooltipCaret.outerHeight() / 2) + 'px',
               "left": centerX + 'px'
            });

            /* FUNCTION TO GET BAR PROPS */
            function getBAR(chart) {
               const dataPoints = tooltipModel.dataPoints,
                     datasetIndex = chart.data.datasets.length - 1,
                     datasetMeta = chart.getDatasetMeta(datasetIndex),
                     scaleBottom = chart.scales['y-axis-0'].bottom,
                     bar = datasetMeta.data[dataPoints[0].index]._model,
                     canvasPosition = chart.canvas.getBoundingClientRect(),
                     paddingLeft = parseFloat(getComputedStyle(chart.canvas).paddingLeft),
                     paddingTop = parseFloat(getComputedStyle(chart.canvas).paddingTop),
                     scrollLeft = document.body.scrollLeft,
                     scrollTop = document.body.scrollTop;

               return {
                  top: bar.y + canvasPosition.top + paddingTop + scrollTop,
                  left: bar.x - (bar.width / 2) + canvasPosition.left + paddingLeft + scrollLeft,
                  width: bar.width,
                  height: scaleBottom - bar.y
               }
            }

         }
      }
   }
});
.color-box{width:12px;height:12px;background:#c06526;display:inline-block;margin-top:5px}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.6.0/Chart.min.js"></script>
<canvas id="ctx"></canvas>

UPDATE

IF you wish to position tooltip at the center of each bar segment then, use the following function :

function getBARSegment(chart) {
   const dataPoints = tooltipModel.dataPoints,
         bar = chart.active[dataPoints[0].datasetIndex]._model,
         canvasPosition = chart.canvas.getBoundingClientRect(),
         paddingLeft = parseFloat(getComputedStyle(chart.canvas).paddingLeft),
         paddingTop = parseFloat(getComputedStyle(chart.canvas).paddingTop),
         scrollLeft = document.body.scrollLeft,
         scrollTop = document.body.scrollTop;

   return {
      top: bar.y + canvasPosition.top + paddingTop + scrollTop,
      left: bar.x - (bar.width / 2) + canvasPosition.left + paddingLeft + scrollLeft,
      width: bar.width,
      height: bar.base - bar.y
   }
}

α΄˜Κ€α΄‡α΄ Ιͺᴇᴑ

bar-chart

ʟΙͺᴠᴇ ᴇxα΄€α΄α΄˜ΚŸα΄‡ ⧩

const chart = new Chart(ctx, {
   type: 'bar',
   data: {
      labels: ['Jan', 'Feb', 'Mar', 'Apr'],
      datasets: [{
         label: 'Revenue',
         data: [4, 2, 3, 3],
         backgroundColor: '#2d4e6d'
      }, {
         label: 'Expenses',
         data: [3, 3.5, 4, 1],
         backgroundColor: '#c06526'
      }, {
         label: 'Profit',
         data: [3, 2.5, 4, 2],
         backgroundColor: '#e0ecf0'
      }]
   },
   options: {
      scales: {
         xAxes: [{
            stacked: true
         }],
         yAxes: [{
            stacked: true,
            ticks: {
               beginAtZero: true
            }
         }]
      },
      tooltips: {
         enabled: false,
         custom: function(tooltipModel) {
            /*** jQuery IS USED FOR SIMPLICITY ***/

            /* TOOLTIP & CARET ELEMENT */
            let tooltip = $('#tooltip');
            let tooltipCaret = $('#tooltip-caret');

            /* CREATE TOOLTIP & CARET ELEMENT AT FIRST RENDER */
            if (!tooltip.length && !tooltipCaret.length) {
               tooltip = $('<div></div>').attr('id', 'tooltip');
               tooltipCaret = $('<div></div>').attr('id', 'tooltip-caret');
               $('body').append(tooltip, tooltipCaret);
            }

            /* HIDE IF NO TOOLTIP */
            if (!tooltipModel.opacity) {
               tooltip.hide();
               tooltipCaret.hide();
               return;
            }

            /* GET BAR PROPS (width, height, top, left) */
            const barWidth = getBARSegment(this._chart).width,
                  barHeight = getBARSegment(this._chart).height,
                  barTop = getBARSegment(this._chart).top,
                  barLeft = getBARSegment(this._chart).left;

            /* SET STYLE FOR TOOLTIP 
            	(these can also be set in separate css file) */
            tooltip.css({
               "display": "inline-block",
               "position": "absolute",
               "color": "rgba(255, 255, 255, 1)",
               "background": "rgba(0, 0, 0, 0.7)",
               "padding": "5px",
               "font": "12px Arial",
               "border-radius": "3px",
               "white-space": "nowrap",
               "pointerEvents": "none"
            });

            /* SET STYLE FOR TOOLTIP CARET 
            	(these can also be set in separate css file) */
            tooltipCaret.css({
               "display": "block",
               "position": "absolute",
               "width": 0,
               "pointerEvents": "none",
               "border-style": "solid",
               "border-width": "8px 10px 8px 0",
               "border-color": "transparent rgba(0, 0, 0, 0.7) transparent transparent"
            });

            /* ADD CONTENT IN TOOLTIP */
            tooltip.text('ChartJS');
            tooltip.append('<br><div class="color-box"></div><label style="display: block; margin: -16px 0 0 16px;"> Custom Tooltip<label>');

            /* POSITION TOOLTIP & CARET
            (position should be set after tooltip & caret is rendered) */
            const centerX = barLeft + (barWidth / 2),
                  centerY = barTop + (barHeight / 2)

            tooltip.css({
               "top": centerY - (tooltip.outerHeight() / 2) + 'px',
               "left": centerX + tooltipCaret.outerWidth() + 'px'
            });
            tooltipCaret.css({
               "top": centerY - (tooltipCaret.outerHeight() / 2) + 'px',
               "left": centerX + 'px'
            });

            /* FUNCTION TO GET BAR PROPS */
            function getBARSegment(chart) {
               const dataPoints = tooltipModel.dataPoints,
                     bar = chart.active[dataPoints[0].datasetIndex]._model,
                     canvasPosition = chart.canvas.getBoundingClientRect(),
                     paddingLeft = parseFloat(getComputedStyle(chart.canvas).paddingLeft),
                     paddingTop = parseFloat(getComputedStyle(chart.canvas).paddingTop),
                     scrollLeft = document.body.scrollLeft,
                     scrollTop = document.body.scrollTop;

               return {
                  top: bar.y + canvasPosition.top + paddingTop + scrollTop,
                  left: bar.x - (bar.width / 2) + canvasPosition.left + paddingLeft + scrollLeft,
                  width: bar.width,
                  height: bar.base - bar.y
               }
            }

         }
      }
   }
});
.color-box{width:12px;height:12px;background:#c06526;display:inline-block;margin-top:5px}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.6.0/Chart.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="ctx"></canvas>

4πŸ‘

For Chartjs 3+ (tested in 3.5.1)

Chart.Tooltip.positioners.center = function (elements, eventPosition) {
    if(elements.length){ // to prevent errors in the console
        const { x, y, base } = elements[0].element; // _model doesn't exist anymore
        const height = !base ? 0 : base - y;// so it doesn't break in combo graphs like lines + bars
        return { x, y: y + (height / 2) };
    }
    return false; // without this it gets stuck in the last active tooltip 
};

Set this custom "center" position in options.plugins.tooltip.position instead of the previous options.tooltips.position

Chartjs 2.8 allows you to add custom position modes for tooltips. With this you can create a center position option:

Chart.Tooltip.positioners.center = function (elements) {
    const { x, y, base } = elements[0]._model;
    const height = base - y;
    return { x, y: y + (height / 2) };
};

See fiddle for working example: https://jsfiddle.net/astroash/wk5y0fqd/36/

1πŸ‘

You can use values from the datasets to workout the relative height of the item being hovered over and adjust the CSS accordingly.

The following is close to the centre, but is not the exact centre. My calculations need correcting if you want exactness.

Inside the custom tooltip function include the following:

// find relative proportion
var dataIndex = tooltip.dataPoints[0].index;
var datasetIndex = tooltip.dataPoints[0].datasetIndex;
var totalHeight = 0;

var thisHeight = this._chart.config.data.datasets[datasetIndex].data[dataIndex];

for (var i = 0; i <= datasetIndex; i++)
{
  totalHeight += this._chart.config.data.datasets[i].data[dataIndex];
}

var positionY = this._chart.canvas.offsetTop;
var positionX = this._chart.canvas.offsetLeft;
var chartHeight = this._chart.canvas.scrollHeight;
var tooltipHalfHeight = tooltip.height / 2;

// Display, position, and set styles for font
tooltipEl.style.left = positionX + tooltip.caretX + 'px';
tooltipEl.style.top = tooltip.caretY + ((chartHeight - tooltip.caretY - positionY) * (thisHeight / totalHeight / 2)) - tooltipHalfHeight + 'px';
<!DOCTYPE html>
<html>

<head>
  <link rel="stylesheet" href="style.css" />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.6.0/Chart.bundle.min.js"></script>
  <script src="script.js"></script>
  <style>
		canvas{
			-moz-user-select: none;
			-webkit-user-select: none;
			-ms-user-select: none;
		}
		#chartjs-tooltip {
			opacity: 1;
			position: absolute;
			background: rgba(0, 0, 0, .7);
			color: white;
			border-radius: 3px;
			-webkit-transition: all .1s ease;
			transition: all .1s ease;
			pointer-events: none;
			/*-webkit-transform: translate(-50%, 0);
			transform: translate(-50%, 0);*/
		}
		.chartjs-tooltip-key {
			display: inline-block;
			width: 10px;
			height: 10px;
			margin-right: 10px;
		}
	</style>
</head>

<body>
  <div id="chartjs-tooltip">
			<table></table>
</div>
  <canvas id="myChart" width="400" height="400"></canvas>

  <script>
var customTooltips = function(tooltip) {
			// Tooltip Element
			var tooltipEl = document.getElementById('chartjs-tooltip');
			if (!tooltipEl) {
				tooltipEl = document.createElement('div');
				tooltipEl.id = 'chartjs-tooltip';
				tooltipEl.innerHTML = "<table></table>"
				this._chart.canvas.parentNode.appendChild(tooltipEl);
			}
			// Hide if no tooltip
			if (tooltip.opacity === 0) {
				tooltipEl.style.opacity = 0;
				return;
			}
			// Set caret Position
			tooltipEl.classList.remove('above', 'below', 'no-transform');
			if (tooltip.yAlign) {
				tooltipEl.classList.add(tooltip.yAlign);
			} else {
				tooltipEl.classList.add('no-transform');
			}
			function getBody(bodyItem) {
				return bodyItem.lines;
			}
			// Set Text
			if (tooltip.body) {
				var titleLines = tooltip.title || [];
				var bodyLines = tooltip.body.map(getBody);
				var innerHtml = '<thead>';
				titleLines.forEach(function(title) {
					innerHtml += '<tr><th>' + title + '</th></tr>';
				});
				innerHtml += '</thead><tbody>';
				bodyLines.forEach(function(body, i) {
					var colors = tooltip.labelColors[i];
					var style = 'background:' + colors.backgroundColor;
					style += '; border-color:' + colors.borderColor;
					style += '; border-width: 2px'; 
					var span = '<span class="chartjs-tooltip-key" style="' + style + '"></span>';
					innerHtml += '<tr><td>' + span + body + '</td></tr>';
				});
				innerHtml += '</tbody>';
				var tableRoot = tooltipEl.querySelector('table');
				tableRoot.innerHTML = innerHtml;
			}
			
			// find relative proportion
			var dataIndex = tooltip.dataPoints[0].index;
			var datasetIndex = tooltip.dataPoints[0].datasetIndex;
			var totalHeight = 0;
			
			var thisHeight = this._chart.config.data.datasets[datasetIndex].data[dataIndex];
			
			for (var i = 0; i <= datasetIndex; i++)
			{
  			  totalHeight += this._chart.config.data.datasets[i].data[dataIndex];
			}

			var positionY = this._chart.canvas.offsetTop;
			var positionX = this._chart.canvas.offsetLeft;
			var chartHeight = this._chart.canvas.scrollHeight;
			var tooltipHalfHeight = tooltip.height / 2;
			
			// Display, position, and set styles for font
			tooltipEl.style.opacity = 1;
			tooltipEl.style.left = positionX + tooltip.caretX + 'px';
			tooltipEl.style.top = tooltip.caretY + ((chartHeight - tooltip.caretY - positionY) * (thisHeight / totalHeight / 2)) - tooltipHalfHeight + 'px';
			tooltipEl.style.fontFamily = tooltip._fontFamily;
			tooltipEl.style.fontSize = tooltip.fontSize;
			tooltipEl.style.fontStyle = tooltip._fontStyle;
			tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px';
		};
		
		var ctx = document.getElementById("myChart").getContext('2d');
    var myChart = new Chart(ctx, {
      type: 'bar',
      data: {
        labels: ["This", "That", "Something else", "Important thing", "Oh really?", "What!!"],
        datasets: [{
          label: '# of Votes',
          data: [12, 19, 3, 5, 2, 3],
          backgroundColor: [
            'rgba(255, 99, 132, 0.2)',
            'rgba(54, 162, 235, 0.2)',
            'rgba(255, 206, 86, 0.2)',
            'rgba(75, 192, 192, 0.2)',
            'rgba(153, 102, 255, 0.2)',
            'rgba(255, 159, 64, 0.2)'
          ],
          borderColor: [
            'rgba(255,99,132,1)',
            'rgba(54, 162, 235, 1)',
            'rgba(255, 206, 86, 1)',
            'rgba(75, 192, 192, 1)',
            'rgba(153, 102, 255, 1)',
            'rgba(255, 159, 64, 1)'
          ],
          borderWidth: 1
        }, {
          data: [2, 5, 13, 5, 3, 4],
          backgroundColor: [
            'rgba(255, 206, 86, 0.2)',
            'rgba(75, 192, 192, 0.2)',
            'rgba(153, 102, 255, 0.2)',
            'rgba(255, 159, 64, 0.2)',
            'rgba(255, 99, 132, 0.2)',
            'rgba(54, 162, 235, 0.2)'
          ],
          borderColor: [
            'rgba(255, 206, 86, 1)',
            'rgba(75, 192, 192, 1)',
            'rgba(153, 102, 255, 1)',
            'rgba(255, 159, 64, 1)',
            'rgba(255,99,132,1)',
            'rgba(54, 162, 235, 1)'
          ],
          borderWidth: 1
        }]
      },
      options: {
        scales: {
          xAxes: [{
            stacked: true,
          }],
          yAxes: [{
            stacked: true
          }]
        },
        tooltips: {
          enabled: false,
          custom: customTooltips,
        }
      }
    });
  </script>
</body>

</html>

Plunker: http://plnkr.co/edit/f0EqpYe6zJMyIDxY4Xg9?p=preview

Leave a comment