У меня есть крошечный веб-проект, который не приносит удовольствия, плохо работает, и я продолжаю работать над ним раз в полгода или около того (возможно, это его главная проблема). Одна из его функций связана с представлением множества данных, поэтому было бы здорово иметь диаграмму. Сначала я смотрел библиотеки для диаграмм, такие как Chart.js и Highcharts, и остановился на Chart.js из-за его лицензии. К сожалению, у него есть этот неинтуитивный API, который требует передать ему холст вместо прямого объявления линейной диаграммы.

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

Потом я сравнил свой код с репозиторием Chart.js GitHub и решил, что не стоит начинать с нуля. Они очень хорошо умеют кодировать! Поэтому я попробовал в другой раз.

Спасение бесполезного кода

Я решил заменить старую диаграмму, которую я сделал непосредственно, на новую. Таким образом было легче отлаживать и тестировать. Это позволило мне повторно использовать настройку веб-компонента, над внедрением которой я так усердно работал, в свой неудачный проект. По крайней мере, есть время оправиться от того фиаско.

Как сделать веб-компонент

Несмотря на то, что я сохранил его из предыдущей разработки, я считаю правильным объяснить, как сделать веб-компонент. Таким образом, довольно ясно, что вам нужно, чтобы все это работало.

Следуя учебникам MDN, я создал класс, расширяющий HTMLElement, и в его конструкторе сделал следующее:

class LineChart extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode:'open'});
    this.shadowRootDiv = document.createElement('div');
    this.canvas = document.createElement('canvas');
    this.shadowRootDiv.append(this.canvas);
    this.props = {};
    this.shadowRoot.appendChild(this.shadowRootDiv);
   }
}

Очень важно вызвать конструктор super() для инициализации веб-компонента.

Затем нам нужно прикрепить теневой DOM, после чего у нас созданы и структурированы все необходимые элементы компонента. Сюда входит canvas, который мы будем использовать для Chart.js:

this.attachShadow({mode:'open'});
this.shadowRootDiv = document.createElement('div');
this.canvas = document.createElement('canvas');
this.shadowRootDiv.append(this.canvas);

Наконец, конечно, нам нужно добавить элементы компонента к shadowRoot:

this.shadowRoot.appendChild(this.shadowRootDiv);

Взаимодействие с холстом в веб-компоненте

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

Это означает, что вам нужно дождаться загрузки холста, прежде чем вы сможете получить к нему доступ. Поэтому я не стал создавать новый объект Chart прямо в контроллере, а вынес его в отдельную функцию:

render() {
  const data = JSON.parse(this.getAttribute('data'));
  const context = this.canvas.getContext('2d');
  const chart = new Chart(context, data);
}

Вы также можете видеть, что я извлекаю данные для диаграммы из атрибутов веб-компонента. Его необходимо проанализировать перед передачей, так как он имеет форму строки.

Однако эта функция все еще не выполняется. Мне нужно было найти подходящий способ вызвать его, когда все элементы веб-компонента загружены.

Где-то в документах MDN я нашел эту функцию жизненного цикла, которую имеют веб-компоненты. Он называется connectedCallback и выполняется сразу после подключения shadowDOM к DOM страницы. После этого я могу с радостью редактировать холст и видеть результаты на экране. Поэтому я использовал это так:

connectedCallback() {
  this.render();
}

Импорт модулей и зависимостей Chart.js

Конечно, даже если вы соберете приведенные выше фрагменты кода, код не будет работать. Ведь мы еще не импортировали объект Chart. Но сделать это не так уж и сложно:

import Chart from 'chart.js/auto'

Это позволит импортировать все различные модули, которые использует Chart.js.

Но почему в этом посте есть целый заголовок только для одной очевидной строки кода?

Что ж, Документы говорят, что вместо этого вы также можете импортировать части модулей. Это кажется хорошей практикой в ​​более крупном проекте, где вы не хотите, чтобы огромные куски бесполезного кода доставлялись вашему пользователю, несмотря на то, что вы хотите просто использовать линейную диаграмму.

Чтобы импортировать только модули, относящиеся к линейной диаграмме, вам необходимо сначала импортировать диаграмму, затем модули, а затем зарегистрировать их следующим образом:

import { 
  Chart,
  LineController,
  LineElement,
  PointElement,
  CategoryScale,
  LinearScale
} from 'chart.js'
Chart.register(
  LineController,
  LineElement,
  PointElement,
  CategoryScale,
  LinearScale
)

После этого на холсте можно будет нарисовать диаграмму.

Одна вещь, которую я еще не пробовал, — это то, что происходит, когда два веб-компонента импортируют диаграмму, а затем используют ее на одной странице. Я надеюсь, что веб-компонент работает как модуль и с этим проблем нет, но мне нужно его протестировать.

Передача данных в объект Chart.js

На данный момент у меня есть установка, которая будет работать, только если есть данные для отображения.

Чтобы передать данные на диаграмму, я бы поместил их в атрибут data тега <line-chart>:

<line-chart id='line-chart-id' data="{type: 'line', ...}" className="chart"></line-chart>

Таким образом, наш веб-компонент сможет прочитать его и передать в Chart.js.

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

Динамически задавайте данные веб-компоненту

Для этого вместо атрибута данных в определении тега мы можем настроить обработчик событий, который загружает данные и устанавливает их на линейный график:

document.addEventListener('DOMContentLoaded', async (event) => {
  const lineChart = document.getElementById('line-chart-id');
  lineChart.setAttribute('data', await fetchData());
});

Вы можете видеть, что данные загружаются, вызывая асинхронный fetchData() и ожидая его результата. Там мы можем записать json, представляющий наши данные:

function fetchData() {
  return JSON.stringify({
    type: 'line',
    data: {
      labels: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ],
      datasets: [
        {
          label: 'dataset 1',
          data: [
            { x: 0, y1:2, y2:1 },
            { x: 1,y1:2},
            { x: 1.4,y1:6 },
            { x:2,y1:3 },
            { x:3,y1:6 },
            { x:5, y2: 2 },
            { x:6, y2:1 },
            { x:8, y2:6 },
            { x:9,y2:6 },
            { x:10,y2:0 }
          ],
          parsing: { yAxisKey: 'y1' }
        }, { 
          label: 'dataset 2',
          data: [
            { x: 0, y1:2, y2:1 },
            { x: 1,y1:2},
            { x: 1.4,y1:6 },
            { x:2,y1:3 },
            { x:3,y1:6 },
            { x:5, y2: 2 },
            { x:6, y2:1 },
            { x:8, y2:6 },
            { x:9,y2:6 },
            { x:10,y2:0 }
          ],
          parsing: { yAxisKey: 'y2' }
        },
      ],
    }
  });
}

Здесь важно то, что конфиг чарта пишется в JSON, но перед возвратом превращается в строку. Таким образом, мы можем напрямую установить его в атрибут.

Остальное вы можете увидеть в собственной документации Chart.js.

К сожалению, результат выглядит так:

Почему не загружается график?

Поскольку сейчас мы устанавливаем данные динамически, наш веб-компонент слишком быстро создает объект Chart. Это означает, что он передает ему пустой объект данных.

Чтобы исправить это, нам нужно отрендерить немного позже. Например, при изменении атрибута data. Вот почему я добавляю в веб-компонент следующие методы:

static get observedAttributes() {
  return ['data'];
}

attributeChangedCallback(name, oldValue, newValue) {
  this.render()
}

Одно это, к сожалению, не меняет страницу. Что это меняет в консоли:

Uncaught Error: Canvas is already in use. Chart with ID '0' must be destroyed before the canvas with ID '' can be reused.

Как заставить Chart.js перерисовываться?

Что ж, теперь проблема ясна — мы не можем закрасить график, так какое же решение? Сначала нам нужно уничтожить диаграмму. Холст можно использовать повторно, но объект Chart из Chart.js требует уничтожения до того, как холст можно будет перекрасить.

Поэтому я добавил этот код в attributeChangedCallback:

if (this.chart) {
  this.chart.destroy();
  this.chart = null;
}

Я также решил, что неплохо заставить сборщик мусора удалить старый объект, установив ссылку на null. Да, и еще обратите внимание, что диаграмма теперь является свойством объекта веб-компонента. Это означает, что наша функция render() должна его заполнить. Теперь это выглядит так:

render() {
  const data = JSON.parse(this.getAttribute('data'));
  const context = this.canvas.getContext('2d');
  this.chart = new Chart(context, data);
}

График жив

И, наконец, у нас есть рабочая диаграмма:

Хотя серые линии не так легко увидеть, а в строках одного из рядов данных есть разрывы, версия Chart.js намного красивее, чем моя собственная.

Потребовалось бы еще несколько часов, чтобы исправить шероховатости, но это определенно ускорило мой проект на несколько месяцев, если не лет. И это делает меня по-настоящему счастливым. Я узнал, что создание библиотеки диаграмм — это не двухчасовая работа, а многолетний проект по совместительству, если у вас есть команда из одного человека.

Я также узнал, что рисование в браузере не невозможно. И если вы правильно выберете свои инструменты, вы можете сделать несколько действительно красивых проектов.

Если это ваше развлечение, то дерзайте! Я точно не жалею.

Ваше здоровье,

Если вы хотите увидеть, как я сделал диаграмму размером со страницу, вы можете прочитать эту статью: Создание жадного холста с помощью CSS flexbox

Или, если вам хочется увидеть созданную форму входа целиком, вы можете начать здесь: Вступление в 21-й век: обучение React

Первоначально опубликовано на https://geneshki.com 21 января 2023 г.