Chart.js line chart - mouse hover로 강조하기

2023년 11월 22일
6

목표

before mouse hoverbefore mouse hover

when hovering mousewhen hovering mouse

위 이미지(출처)와 같이,
여러 dataset을 하나의 line chart에서 보여주고,
label에 mouse hover했을 때 해당 dataset만 강조하려고 한다.

이미지는 이해를 돕기 위한 예시이다.

위 예시는 amchart 라이브러리를 사용하여 구현되었으며,
나는 chart.js로 위 기능을 구현하고자 한다.


1차 Solution

  • chart option > legend에 onHover, onLeave 이벤트 핸들러 추가
function handleHoverLineLegend(...) {
  logic(); // hover된 legend 연결 dataset 색상을 강조색으로 변경. 그 외 dataset 색상을 gray로 변경
  chart.update(); // 변경된 색상을 기반으로 chart update
}
 
function handleLeaveLineLegend(...) {
  logic(); // 모든 dataset의 색상을 기본값으로 초기화
  chart.update(); // 초기화된 색상을 기반으로 chart update
}
 
const chartOption = {
  plugins: {
    legend: {
      onHover: handleHoverLineLegend,
      onLeave: handleLeaveLineLegend,
    },
  },
}

1차 Result

아래 이미지와 같이 매우 잘 동작하는 것을 확인할 수 있다. 깔끔!

before mouse hoverbefore mouse hoverwhen hovering mouse3when hovering mouse3


문제 상황 1

chart와 legend 사이의 간격을 조절하고 싶어졌다.
chart.js 기본 legend를 사용하면, legend와 chart 사이 간격을 조절할 수 없다.
legend가 canvas 내부에 같이 그려지기 때문이다.

chart option > legend > padding 을 부여하여 gap을 띄울 수 있는데,
legend item 기준 상하좌우 모두로 padding이 먹어버려
legend item간 간격도 벌어지고 legend 위로도 여백이 생겨 보기 좋지 않다.
(paddingLeft 등으로 상하좌우 구분하여 먹일 수 없다 ㅜ)

출처 : https://stackoverflow.com/questions/42585861/chart-js-increase-spacing-between-legend-and-chart출처 : https://stackoverflow.com/questions/42585861/chart-js-increase-spacing-between-legend-and-chart

나만 이런 생각을 한 것은 당연히 아닐테니,, reference를 찾아보았다.

Chart.js 공식 github에 다음과 같은 issue가 있었다.

내 생각과 연관된 chart.js github issue내 생각과 연관된 chart.js github issue

아쉽지만 결론은, chart.js의 내장 legend 말고 HTML Legend를 사용하라! 였다.

2차 Solution : HTML Legend

chart.js 공식 문서를 바탕으로, HTML legend를 구현해 보았다.
기존 onHover, onLeave 이벤트 핸들러도 부착했다.

const getOrCreateLegendList = (chart, id) => {
  const legendContainer = document.getElementById(id);
  let listContainer = legendContainer.querySelector("ul");
 
  if (!listContainer) {
    listContainer = document.createElement("ul");
    /* set listContainer style */
    legendContainer.appendChild(listContainer);
  }
 
  return listContainer;
};
 
export const htmlLegendPlugin = {
  id: "htmlLegend",
  afterUpdate(chart, args, options) {
    const ul = getOrCreateLegendList(chart, options.containerID);
 
    // Remove old legend items
    while (ul.firstChild) ul.firstChild.remove();
 
    // Reuse the built-in legendItems generator
    const items = chart.options.plugins.legend.labels.generateLabels(chart);
 
    items.forEach((item) => {
      const li = document.createElement("li");
      /* set li style */
 
      // toggle show each line chart when click legend
      li.onclick = () => {...};
 
      // find dataset & highlight
      const highlightLine = () => {
        someLogic();
        chart.update();
      };
 
      // reset color for all datasets
      const resetLine = () => {
        someLogic();
        chart.update();
      };
 
      li.addEventListener("mouseenter", highlightLine);
      li.addEventListener("mouseleave", resetLine);
 
      ul.appendChild(li);
    });
  },
};

문제 상황 2

기본 기능들은 잘 동작한다.

다만 마우스를 빠르게 이동하여 legend를 스쳐지나갈 때,
hover는 catch하고 leave는 catch하지 못하여

마우스가 chart 바깥에 있음에도 dataset 하나를 계속 강조하고 있게 되는 현상이 발생했다.

hover 후 빠르게 chart에서 마우스 leave 했을 때 강조가 남아있는 현상hover 후 빠르게 chart에서 마우스 leave 했을 때 강조가 남아있는 현상

원인

정상적인 이벤트 발생 순서는 아래와 같다.

  1. hover, leave 이벤트 핸들러 부착
  2. legend mouse hover
  3. highlight 로직 실행
  4. highlight 완료 후 chart update 실행
  5. 이벤트 핸들러 초기화 및 차트 색상 업데이트
  6. 초기화된 요소에 hover, leave 이벤트 핸들러 재부착
  7. legend mouse leave
  8. reset 로직 실행
  9. reset 완료 후 chart update 실행
  10. 이벤트 핸들러 초기화 및 차트 색상 업데이트

마우스를 매우 빠르게 이동시켰을 경우,
5번 과정 완료 후 6번 과정이 수행되기 전에 마우스가 이미 요소 바깥에 위치하게 되어
leave 이벤트 핸들러가 이를 catch하지 못하는 것이다.

highlight, reset 로직 내에서 chart.update()를 수행하기 때문에 발생하는 필연적인 결과였다.
(색상 변경 후 chart.update() 를 반드시 수행해야만 차트에 반영된다.)

이를 해결하기 위해 이것저것 시도해 보았으나, 실패였다.

chart.js 의 plugin을 직접 구현한 것이기 때문에,
Chart Plugin Life Cycle을 제대로 이해하고 적절한 시기에 이벤트 핸들러를 지정해주면 될 것 같다고는 생각되나,
그 정확한 시점을 찾을 수 없었다.

일단 이 정도로만 마무리하고, 더 고민해본 다음 해결해 봐야겠다..


Reference