GProst
GProst

Reputation: 10227

Position tooltip in center of bar

Can't find a way to position tooltip in center of bar like this (yeah, I know it is not exactly in center on this screenshot, but still):

enter image description here

If I use custom tooltip option, I can get only x and y positions of a caret on top of a bar. But can't get height/width of a bar.

Here is a part of an options object that I pass to Chart constructor:

const options = {
  tooltips: {
    enabled: false,
    custom: (tooltip) => {
      // Retrieving valuable props from tooltip (caretX, caretY)
      // and creating custom tooltip that is positioned
      // on top of a bar
    }
  }
  // other options
}

const chart = new Chart(ctx, {
  type: 'bar',
  data,
  options
})

Upvotes: 1

Views: 7798

Answers (3)

Astro Ash
Astro Ash

Reputation: 161

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/

Upvotes: 4

Ι’Κ€α΄œΙ΄α΄›
Ι’Κ€α΄œΙ΄α΄›

Reputation: 32879

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>

Upvotes: 4

K Scandrett
K Scandrett

Reputation: 16540

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

Upvotes: 1

Related Questions