chartjs / Chart.js

Simple HTML5 Charts using the <canvas> tag
https://www.chartjs.org/
MIT License
64.36k stars 11.89k forks source link

Axis onHover & onLeave #9844

Open peteruithoven opened 2 years ago

peteruithoven commented 2 years ago

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

gerbenqikker commented 2 years ago

+1

LeeLenaleee commented 2 years ago

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/

mitchtabian commented 1 year ago

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

ysk3a commented 1 year ago

@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/

Offbeatmammal commented 1 year ago

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