null

Кастомная легенда к графику в react-chartjs-2

Проблема

Так уж вышло, что Chart.js не предоставляет возможности влиять на внешний вид легенды графика. Можно изменить некоторые её параметры, но чтобы хоть как-то поменять структуру её элемента необходимо прибегнуть к помощи плагина

При попытке воспроизвести пример с плагином в среде с реактом я столкнулся с проблемой, при которой элементы легенды рисовались несколько раз. Возможно, существует какая-то глубоко зарытая опция для её решения, но на мой взгляд куда большей проблемой является включение ванильного джаваскрипта из плагина в код реактового компонента.

 

Решение проблемы

Было принято решение рисовать легенду средствами реакта, используя ту информацию, которую можно получить из объекта класса Chart, лежащего в основе графика. react-chartjs-2 позволяет получить этот объект с помощью свойства ref. В связи с тем, что Chart будет использоваться во время рендеринга, необходимо поместить его в состояние:

const [chart, setChart] = useState();
return <Line
  ref={chart => setChart(chart)}
  data={
    datasets: [
      {label: 'Данные 1', ...},
      {label: 'Данные 2', ...},
    ],
    ...
  }
  ...
/>

После первого рендера в переменной chart окажется нужный нам объект, который можно использовать для рендера элементов легенды (они указаны в свойстве label у datasets):

return <>
  <ul className="legend">
    {/* Получаем элементы легенды и рисуем их */}
    {chart?.legend?.legendItems?.map(({datasetIndex, text, fillStyle, strokeStyle}) => {
      if (datasetIndex === undefined) return null;
      return <li className="legend__item">
        {/* Следующий элемент отображает цвет графика */}
        <div className='stsos-legend-item__color-box' style={{background: fillStyle, borderColor: strokeStyle}}/>
        {/* text содержит в себе label dataset'a */}
        {text}
      </li>;
    })}
  </ul>
  <Line
    options={{
      // Не забываем выключить дефолтную легенду
      plugins: {
        legend: {
          display: false,
        }
      }
    }} 
  />
</>;

Дело осталось за малым. Дефолтная легенда позволяет по клику на элемент скрывать/отображать относящийся к нему график (в легенде при этом элемент перечёркивается в случае скрытия). Чтобы воспроизвести такое поведение, необходимо создать дополнительное состояние, в котором будет храниться массив с логическими значениями факта скрытия графика. При клике на элемент, используя chart, будем скрывать соответствующий график, а затем обновлять значение в ранее упомянутом массиве по нужному индексу, чтобы отслеживать состояние элемента легенды. Финальный код примет примерно следующий вид:

const [prevChart, setPrevChart] = useState();
const [chart, setChart] = useState();
// Состояние для ранее упомянутого массива
// Пример: [true, false] (график с "Данные 1" скрыт, график с "Данные 2" отображён)
const [datasetHiddens, setDatasetHiddens] = useState();

// chart инициализируется только один раз, обновления легенды не обновляют сам chart
if (chart !== prevChart) {
  setPrevChart(chart);
  setDatasetHiddens(chart?.legend?.legendItems?.map((item) => !item.hidden));
}

return <>
  <ul className="legend">
    {chart?.legend?.legendItems?.map(({datasetIndex, text, fillStyle, strokeStyle}) => {
      if (datasetIndex === undefined || !datasetHiddens) return null;
      const isHidden = datasetHiddens[datasetIndex];
      return <li
        className="legend__item"
        // Собственно то, зачем мы и заводили состояние с массивом
        style={isHidden ? {
          textDecoration: 'line-through'
        }: undefined}
        onClick={() => {
          chart.setDatasetVisibility(datasetIndex, !isHidden);
          setDatasetHiddens(prev => {
            if (!prev) return;
            const result = [...prev];
            result[datasetIndex] = !isHidden;
            return result;
          });
          chart.update();
        }}
      >
        {/* Следующий элемент отображает цвет графика */}
        <div className='stsos-legend-item__color-box' style={{background: fillStyle, borderColor: strokeStyle}}/>
        {/* text содержит в себе label dataset'a */}
        {text}
      </li>;
    })}

  </ul>

  <Line
    ref={chart => setChart(chart)}
    data={{ datasets: [ {label: 'Данные 1'}, {label: 'Данные 2'}, ] }}
    options={{
      plugins: {
        legend: {
          display: false,
        }
      }
    }} />
</>;

 

В моём случае, после некоторых изменений в шаблоне и стилях, данный подход позволил визуально и функционально воспроизвести дефолтную легенду с добавлением в неё своих данных (красная надпись с процентами).