Chart.js icon indicating copy to clipboard operation
Chart.js copied to clipboard

Axis onHover & onLeave

Open peteruithoven opened this issue 2 years ago • 5 comments

Feature Proposal

Support onHover and onLeave handlers per axis similar to the onHover and onLeave handler of the legend. Ideally triggered per tick? And ideally through arguments we can determine the tick index.

Feature Use Case

This in combination with tooltip.setActiveElements would allow us to show tooltips for axis when hovering over ticks. Like also mentioned in: https://github.com/chartjs/Chart.js/issues/3907

peteruithoven avatar Nov 11 '21 21:11 peteruithoven

+1

gerbenqikker avatar Dec 01 '21 23:12 gerbenqikker

You can write a custom plugin that will fire when hovering over labels (mind its not perfect, not fully optimized etc):

const findLabel = (labels, evt) => {
  let found = false;
  let res = null;

  labels.forEach(l => {
    l.labels.forEach(label => {
      if (evt.x > label.x && evt.x < label.x2 && evt.y > label.y && evt.y < label.y2) {
        res = label.label;
        found = true;
      }
    });
  });

  return [found, res];
};

const getLabelHitboxes = (scales) => (Object.values(scales).map((s) => ({
  scaleId: s.id,
  labels: s._labelItems.map((e, i) => ({
    x: e.translation[0] - s._labelSizes.widths[i] / 2,
    x2: e.translation[0] + s._labelSizes.widths[i] / 2,
    y: e.translation[1] - s._labelSizes.heights[i] / 2,
    y2: e.translation[1] + s._labelSizes.heights[i] / 2,
    label: e.label,
    index: i
  }))
})));

const options = {
  type: 'line',
  data: {
    labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
    datasets: [{
        label: '# of Votes',
        data: [12, 19, 3, 5, 2, 3],
        borderColor: 'pink'
      },
      {
        label: '# of Points',
        data: [7, 11, 5, 8, 3, 7],
        borderColor: 'orange'
      }
    ]
  },
  options: {},
  plugins: [{
    id: 'customHover',
    afterEvent: (chart, event, opts) => {
      const evt = event.event;

      if (evt.type !== 'mousemove') {
        return;
      }

      const [found, label] = findLabel(getLabelHitboxes(chart.scales), evt);

      if (found) {
        console.log(label);
      }

    }
  }]
}

const ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);

fiddle: https://jsfiddle.net/Leelenaleee/9zqxfn0y/33/

LeeLenaleee avatar Feb 01 '22 19:02 LeeLenaleee

Update for 2023. translation was moved into labelItem.options. Also the example above doesn't show you how to actually show/hide a tooltip. So I added my impl below.

const getLabelHitboxes = (scales) => (Object.values(scales).map((s) => ({
  scaleId: s.id,
  labels: s._labelItems.map((labelItem, i) => ({
    x: labelItem.options.translation[0] - s._labelSizes.widths[i] / 2,
    x2: labelItem.options.translation[0] + s._labelSizes.widths[i] / 2,
    y: labelItem.options.translation[1] - s._labelSizes.heights[i] / 2,
    y2: labelItem.options.translation[1] + s._labelSizes.heights[i] / 2,
    label: labelItem.label,
    index: i
  }))
})));

Then for showing the tooltip the only way I could get it to work was with a custom one.

// Showing the tooltip
function showTooltip(context, label, completed_at){
   // Tooltip Element
    let tooltipEl = document.getElementById('chartjs-tooltip');

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

    const tooltipModel = context.tooltip;

    // Set caret Position
    tooltipEl.classList.remove('above', 'below', 'no-transform');
    if (tooltipModel.yAlign) {
        tooltipEl.classList.add(tooltipModel.yAlign);
    } else {
        tooltipEl.classList.add('no-transform');
    }

    // Set Text
    const titleLines = [label]
    const bodyLines = [completed_at];

    let innerHtml = '<thead>';

    titleLines.forEach(function(title) {
        innerHtml += '<tr><th>' + title + '</th></tr>';
    });
    innerHtml += '</thead><tbody>';

    bodyLines.forEach(function(body, i) {
      innerHtml += '<tr><td><span>' + body + '</span></td></tr>';
    });
    innerHtml += '</tbody>';

    let tableRoot = tooltipEl.querySelector('table');
    tableRoot.innerHTML = innerHtml;

    const position = context.canvas.getBoundingClientRect();

    const bodyFont = Chart.helpers.toFont(tooltipModel.options.bodyFont);

    // Display, position, and set styles for font
    tooltipEl.style.opacity = .75;
    tooltipEl.style.position = 'absolute';
    tooltipEl.style.left = mouseX + 'px';
    tooltipEl.style.top = mouseY + 'px';
    tooltipEl.style.font = bodyFont.string;
    tooltipEl.style.padding = '10px';
    tooltipEl.style.backgroundColor = '#000';
    tooltipEl.style.color = '#fff';
    tooltipEl.style.borderRadius = '8px';
    tooltipEl.style.pointerEvents = 'none';
}

Hiding the tooltip

function hideTooltip(context) {
  let tooltipEl = document.getElementById('chartjs-tooltip');

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

Then in the afterEvent fn just called show or hide.

plugins: [{
  id: 'customHover',
  afterEvent: (chart, event, opts) => {
    const evt = event.event;
    if (evt.type !== 'mousemove') {
      return;
    }
    const [found, label] = findLabel(getLabelHitboxes(tournamentPlayerResultsChart.scales), evt);
    if (found)) {
      showTooltip(chart, label, 'I added a date to the body here')
    } else {
      hideTooltip(chart)
    }
  },
}]

Mine looks like this: Screenshot 2023-02-24 at 11 30 43 AM

mitchtabian avatar Feb 24 '23 17:02 mitchtabian

@mitchtabian sorry but do you have a working example? i tried to follow what you wrote in a simple jsfiddle before trying in angular15 but could not get it to work. https://jsfiddle.net/4z6hr0qw/9/

ysk3a avatar Apr 26 '23 23:04 ysk3a

not been able to get anything working so far.

would be really handy to have (eg) a tick: { onHover: (event, active, chart)=> .... } type function that can be used to trigger an action when the Axis label is hovered over, similar to the onhover for each bar on a barchart etc

Offbeatmammal avatar Sep 13 '23 06:09 Offbeatmammal