10

there. I use ChartJS and customise tooltip, but have issue with position first and last tooltip's. Look:

enter image description here I suppose that in order to fix the problem, I need to use the https://www.chartjs.org/docs/latest/configuration/tooltip.html#position-modes but, I cannot understand what the formula should be.

CodePen example - https://codepen.io/anon/pen/JzRooy

<html>

<head>
 <title>Line Chart with Custom Tooltips</title>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.bundle.js"></script>
 <script>
 window.chartColors = {
  red: 'rgb(255, 99, 132)',
  orange: 'rgb(255, 159, 64)',
  yellow: 'rgb(255, 205, 86)',
  green: 'rgb(75, 192, 192)',
  blue: 'rgb(54, 162, 235)',
  purple: 'rgb(153, 102, 255)',
  grey: 'rgb(231,233,237)'
 };

 window.randomScalingFactor = function() {
  return (Math.random() > 0.5 ? 1.0 : -1.0) * Math.round(Math.random() * 100);
 }
 </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>
  <canvas id="chart"/>
 <script>
  Chart.defaults.global.pointHitDetectionRadius = 1;

  var customTooltips = function(tooltip) {
   var tooltipEl = document.getElementById('chartjs-tooltip');
   if (!tooltipEl) {
    tooltipEl = document.createElement('div');
    tooltipEl.id = 'chartjs-tooltip';
    tooltipEl.innerHTML = "<div class='wrapper'></div>"
    document.body.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 = '';

    titleLines.forEach(function(title) {
     innerHtml += '<span style="margin-bottom: 10px;display: inline-block;">' + title + '</span>';
    });
    innerHtml += '<div style="display: flex;flex-direction: row;">';

    bodyLines.forEach(function(body, i) {
     var parts = body[0].split(':');
     innerHtml += '<div style="display: flex;flex-direction: column;margin-right: 10px;font-size: 12px;">';
     innerHtml += '<span>' + parts[0].trim() + '</span>';
     innerHtml += '<b>' + parts[1].trim() + '</b>';
     innerHtml += '</div>';
    });
    innerHtml += '</div>';

    var root = tooltipEl.querySelector('.wrapper');
    root.innerHTML = innerHtml;
   }

   var canvas = this._chart.canvas;
   tooltipEl.style.opacity = 1;
   tooltipEl.style.left = canvas.offsetLeft + tooltip.caretX + 'px';
   tooltipEl.style.top = canvas.offsetTop + tooltip.caretY + 'px';
   tooltipEl.style.fontFamily = tooltip._fontFamily;
   tooltipEl.style.fontSize = tooltip.fontSize;
   tooltipEl.style.fontStyle = tooltip._fontStyle;
   tooltipEl.style.padding = "10px";
   tooltipEl.style.border = "1px solid #B4B6C1";
   tooltipEl.style.backgroundColor = "#FFFFFF";
   tooltipEl.style.color = "#4C4F59";
   tooltipEl.style.fontFamily = '"open sans", "helvetica neue", "arial", "sans-serif"';
  };

  var lineChartData = {
   labels: ["January", "February", "March", "April", "May", "June", "July"],
   datasets: [{
    label: "My First dataset",
    borderColor: window.chartColors.red,
    pointBackgroundColor: window.chartColors.red,
    fill: false,
    data: [
     randomScalingFactor(),
     randomScalingFactor(),
     randomScalingFactor(),
     randomScalingFactor(),
     randomScalingFactor(),
     randomScalingFactor(),
     randomScalingFactor()
    ]
   }, {
    label: "My Second dataset",
    borderColor: window.chartColors.blue,
    pointBackgroundColor: window.chartColors.blue,
    fill: false,
    data: [
     randomScalingFactor(),
     randomScalingFactor(),
     randomScalingFactor(),
     randomScalingFactor(),
     randomScalingFactor(),
     randomScalingFactor(),
     randomScalingFactor()
    ]
   }]
  };

  window.onload = function() {
   var chartEl = document.getElementById("chart");
   window.myLine = new Chart(chartEl, {
    type: 'line',
    data: lineChartData,
    options: {
     title:{
      display:true,
      text:'Chart.js Line Chart - Custom Tooltips'
     },
     tooltips: {
      enabled: false,
      mode: 'nearest',
      position: 'average',
      intersect: false,
      custom: customTooltips
     }
    }
   });
  };
 </script>
</body>

</html>
user2164613
  • 157
  • 1
  • 2
  • 7
  • 1
    Did you try external tooltip handler? https://www.chartjs.org/docs/latest/samples/tooltip/html.html And also wrap the chart canvas in a div and make the div position as relative – Umesh Naik Jun 10 '22 at 06:30
  • Adding *div* with bootstrap class *position-relative* and *h-100* solved the position issue of the external custom tooltip. Thank you @UmeshNaik. – Vivek S Jun 27 '22 at 11:58

4 Answers4

13

New modes can be defined by adding functions to the Chart.Tooltip.positioners map (DOC). This function returns the x and y position for the tooltip.

You can add a custom one to adjust the x at an offset. One way to do this would be to be:

    //register custome positioner
Chart.Tooltip.positioners.custom = function(elements, position) {
    if (!elements.length) {
      return false;
    }
    var offset = 0;
    //adjust the offset left or right depending on the event position
    if (elements[0]._chart.width / 2 > position.x) {
      offset = 20;
    } else {
      offset = -20;
    }
    return {
      x: position.x + offset,
      y: position.y
    }
  }

Fiddle example that I created

I hope it helps.

Edi
  • 615
  • 5
  • 15
6

I had the same issue and I didn't find a good solution, so I had to dot it myself.

Actually, it's simple than I thought, wish it helps someone.

Demo: https://codepen.io/themustafaomar/pen/wvWZrod

 const labels = ["1 April","2 April","3 April","4 April","5 April","6 April","7 April","8 April","9 April","10 April","11 April","12 April","13 April","14 April","15 April","16 April","17 April","18 April","19 April","20 April","21 April","22 April","23 April","24 April","25 April","26 April","27 April","28 April","29 April","30 April","31 April"]
const data = [ 95, 57, 72, 54, 73, 53, 98, 75, 52, 93, 50, 65, 99, 67, 77, 61, 74, 65, 86, 92, 64, 89, 82, 62, 64, 89, 59, 75, 56, 63 ];

function customTooltips(tooltipModel) {
// Tooltip Element
var tooltipEl = document.getElementById("chartjs-tooltip");

const yAlign = tooltipModel.yAlign;
const xAlign = tooltipModel.xAlign;

// Create element on first render
if (!tooltipEl) {
  tooltipEl = document.createElement("div");
  tooltipEl.id = "chartjs-tooltip";
  tooltipEl.innerHTML = "<table></table>";
  document.body.appendChild(tooltipEl);
}

// Hide if no tooltip
if (tooltipModel.opacity === 0) {
  tooltipEl.style.opacity = 0;
  return;
}

// Set caret Position
tooltipEl.classList.remove("top", "bottom", "center", "left", "right");
if (tooltipModel.yAlign || tooltipModel.xAlign) {
  tooltipEl.classList.add(tooltipModel.yAlign);
  tooltipEl.classList.add(tooltipModel.xAlign);
}

// Set Text
if (tooltipModel.body) {
  var titleLines = tooltipModel.title || [];
  var bodyLines = tooltipModel.body.map((bodyItem) => {
    return bodyItem.lines;
  });

  var innerHtml = "<thead>";

  titleLines.forEach(function (title) {
    innerHtml += '<tr><th><div class="mb-1">' + title + "</div></th></tr>";
  });
  innerHtml += "</thead><tbody>";

  bodyLines.forEach((body, i) => {
    var colors = tooltipModel.labelColors[i];
    // var style = 'background-color:' + colors.borderColor
    var style =
      "background-color:" + this._chart.data.datasets[i].borderColor;
    var value = tooltipModel.dataPoints[i].value;
    var label = this._chart.data.datasets[i].label;

    style += "; border-color:" + colors.borderColor;
    style += "; border-color:" + this._chart.data.datasets[i].borderColor;
    style += "; border-width: 2px";

    var span =
      '<span class="chartjs-tooltip-key" style="' + style + '"></span>';

    innerHtml += `<tr><td> ${span} $${value}K </td></tr>`;
  });
  innerHtml += "</tbody>";

  var tableRoot = tooltipEl.querySelector("table");
  tableRoot.innerHTML = innerHtml;
}

// Tooltip height and width
const { height, width } = tooltipEl.getBoundingClientRect();

// Chart canvas positions
const positionY = this._chart.canvas.offsetTop;
const positionX = this._chart.canvas.offsetLeft;

// Carets
const caretY = tooltipModel.caretY;
const caretX = tooltipModel.caretX;

// Final coordinates
let top = positionY + caretY - height;
let left = positionX + caretX - width / 2;
let space = 8; // This for making space between the caret and the element.

// yAlign could be: `top`, `bottom`, `center`
if (yAlign === "top") {
  top += height + space;
} else if (yAlign === "center") {
  top += height / 2;
} else if (yAlign === "bottom") {
  top -= space;
}
// xAlign could be: `left`, `center`, `right`
if (xAlign === "left") {
  left = left + width / 2 - tooltipModel.xPadding - space / 2;
  if (yAlign === "center") {
    left = left + space * 2;
  }
} else if (xAlign === "right") {
  left -= width / 2;
  if (yAlign === "center") {
    left = left - space;
  } else {
    left += space;
  }
}

// Display, position, and set styles for font
tooltipEl.style.opacity = 1;

// Left and right
tooltipEl.style.top = `${top}px`;
tooltipEl.style.left = `${left}px`;

// Font
tooltipEl.style.fontFamily = tooltipModel._bodyFontFamily;
tooltipEl.style.fontSize = tooltipModel.bodyFontSize + "px";
tooltipEl.style.fontStyle = tooltipModel._bodyFontStyle;

// Paddings
tooltipEl.style.padding =
  tooltipModel.yPadding + "px " + tooltipModel.xPadding + "px";
}

// Chart
new Chart("chart", {
type: "line",
data: {
  labels,
  datasets: [
    {
      label: "Custom tooltip demo",
      borderColor: "#f66",
      backgroundColor: "transparent",
      lineTension: 0,
      borderWidth: 1.5,
      pointRadius: 2,
      data
    }
  ]
},
options: {
  responsive: true,
  maintainAspectRatio: false,
  legend: { display: false },
  scales: {
    // YAxes
    yAxes: [{ display: false }],

    // XAxes
    xAxes: [
      {
        display: false,
        gridLines: { display: false },
        ticks: {
          padding: 20,
          autoSkipPadding: 30,
          maxRotation: 0
        }
      }
    ]
  },
  tooltips: {
    enabled: false,
    intersect: false,
    mode: "index",
    position: "average",
    custom: customTooltips
  }
}
});
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}

.chartjs-wrapper {
height: 90px;
width: 300px;
margin: 25px auto 0;
border: 1px solid #e6e6e6;
}

#chartjs-tooltip {
opacity: 1;
position: absolute;
color: #fff;
background-color: #000;
border-radius: 6px;
transition: all 0.25s ease-in-out;
pointer-events: none;
}

#chartjs-tooltip:after {
content: "";
display: block;
position: absolute;
margin: auto;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 6px;
}

/* Top center */
#chartjs-tooltip.top.center:after {
border-bottom-color: #000;
top: -11px;
left: 0;
right: 0;
}
/* Top left */
#chartjs-tooltip.top.left:after {
border-bottom-color: #000;
left: 5px;
top: -11px;
}
/* Top right */
#chartjs-tooltip.top.right:after {
border-bottom-color: #000;
right: 5px;
top: -11px;
}

/* Bottom center */
#chartjs-tooltip.bottom.center:after {
border-top-color: #000;
bottom: -11px;
right: 0;
left: 0;
}
/* Bottom left */
#chartjs-tooltip.bottom.left:after {
border-top-color: #000;
bottom: -11px;
}
/* Bottom right */
#chartjs-tooltip.bottom.right:after {
border-top-color: #000;
bottom: -11px;
right: 5px;
}

/* Center left */
#chartjs-tooltip.center.left:after {
border-right-color: #000;
margin: auto;
left: -11px;
bottom: 0;
top: 0;
}
/* Center right */
#chartjs-tooltip.center.right:after {
border-left-color: #000;
margin: auto;
right: -11px;
bottom: 0;
top: 0;
}

.chartjs-tooltip-key {
display: inline-block;
border-radius: 50%;
width: 10px;
height: 10px;
margin-right: 7px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js"></script>
<div class="chartjs-wrapper">
  <canvas id="chart"></canvas>
</div>
Mustafa Omar
  • 399
  • 4
  • 15
  • Hi Mustafa, and thanks for this nice snippet! I put a console.log(tooltipEl.getBoundingClientRect()); and I saw that, on first tooltip show, width and height are different from subsequent calls. In the project I'm working on this makes first tooltip to slightly apper in wrong position, and I have to re-trigger tooltip to be shown right (please check this video https://ivansweb.com/tooltip.mp4 ) Can this be fixed? Thanks – Ivan Jan 21 '21 at 11:13
  • I've just tested the chart many times can't see anything wrong with this, could you please upload an example that reproduces the issue on Codepen or Github, I'd be happy to help! – Mustafa Omar Jan 21 '21 at 12:54
  • I replaced : const caretY = tooltipModel.caretY; with : const { caretY } = context.chart.tooltip and it worked, thanks – m0r Apr 25 '23 at 15:34
3

I did this: Subtract the pixels by way of centering it or putting it in the position.

tooltipEl.style.left = canvas.offsetLeft + tooltip.caretX - 55 + 'px';
elarcoiris
  • 1,914
  • 4
  • 28
  • 30
0

U use this in addition to externalTooltipHandler from chartJs This will modify position only for latest tooltip

  var currentTooltip = tooltip.dataPoints[0].dataIndex;
  var keys = Object.keys(tooltip.dataPoints[0].dataset.data);
  var latestTooltip = keys[keys.length - 1];

  if (currentTooltip == latestTooltip) {
    tooltipEl.style.left = chart.canvas.offsetLeft + tooltip.caretX - 70 + "px";
  } else {
    tooltipEl.style.left = positionX + tooltip.caretX + "px";
  }
despotbg
  • 740
  • 6
  • 12