Создаём графики на основе плагина Chart.js (обновлено 17.08.2021)

В данной статье рассмотрим вариант создания графиков на основе плагина Charts.js

Статья планируется в двух частях. В первой части рассмотрим базовое использование плагина, во второй части добавим в график динамики используя Vue.js

'9'


Первая часть

В первой части подключим плагин Chart.js и рассмотрим его базовое использование


HTML

Скачаем файл chart.min.js по ссылке https://unpkg.com/chart.js@3.5.0/dist/chart.min.js

Подключим chart.min.js перед основным файлом main.js

Напишем стандартную HTML-структуру

Добавим элемент <canvas class="chart"></canvas> в котором и будет отрисовываться график

<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
    <title>Charts</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="css/reset.min.css"/>
    <link rel="stylesheet" href="css/main.css"/>
  </head>
  <body>
  <div id="app">
    <div class="container">
      <h1 class="app__h1 app-h1">Chart</h1>
      <div class="app__chart app-chart">
    
        <!-- нас интересует данная часть -->
        <div class="app-chart__canvas">
          <canvas class="chart"></canvas>
        </div>

      </div>
    </div>
  </div>
  
  <script src="js/chart.min.js"></script> <!-- подключаем плагин -->
  <script src="js/main.js"></script>
  
  </body>
</html>


CSS

Напишем стандартные стили

body {
  font-family: 'Rubik', sans-serif;
  color: #333;
  background: #fff;
}
.container {
  padding: 0 32px;
}
.app-h1 {
  text-align: center;
}
.app-chart__canvas {
  position: relative;
  max-width: 960px;
  margin: 0 auto;
}


JavaScript

Запустим плагин с минимальными настройками

document.addEventListener('DOMContentLoaded', () => { // структура документа загружена   
  
  new Chart( // инициализируем плагин
    document.querySelector('.chart'), // первым параметром передаем элемент canvas по селектору
    // вторым параметром передаем настройки в виде объекта
    { 
      type: 'line', // тип графика, в данном случае линейный
      data: { // общие данные графика в виде объекта
        labels: ['April', 'May', 'June', 'July', 'August'], // метки по оси X
        datasets: [ // набор данных, который будет отрисовываться в виде массива с объектами
          { 
              label: 'Books read', // название для определенного графика в виде строки
              data: [3, 6, 2, 7, 4] // данные в виде массива с числами, количество должно совпадать с количеством меток по оси X
          }
        ]
      },
      options: {} // дополнительные опции для графика в виде объекта, если не нужны - передаем пустой объект
    }
  );

})

'1'


Добавим цвет для линий

document.addEventListener('DOMContentLoaded', () => {

  new Chart(
    document.querySelector('.chart'),
    {
      type: 'line',
      data: {
        labels: ['April', 'May', 'June', 'July', 'August'],
        datasets: [
          {
              label: 'Books read',
              data: [3, 6, 2, 7, 4],
              borderColor: 'crimson' // назначаем цвет для линий в виде строки
          }
        ]
      },
      options: {}
    }
  );

})

'2'


Изменим ширину линий

document.addEventListener('DOMContentLoaded', () => {

  new Chart(
    document.querySelector('.chart'),
    {
      type: 'line',
      data: {
        labels: ['April', 'May', 'June', 'July', 'August'],
        datasets: [
          {
              label: 'Books read',
              data: [3, 6, 2, 7, 4],
              borderColor: 'crimson',
              borderWidth: 5 // назначаем ширину линий
          }
        ]
      },
      options: {}
    }
  );

})

'3'


Изменим тип графика на 'bar'

document.addEventListener('DOMContentLoaded', () => {

  new Chart(
    document.querySelector('.chart'),
    {
      type: 'bar', // изменили тип графика
      data: {
        labels: ['April', 'May', 'June', 'July', 'August'],
        datasets: [
          {
              label: 'Books read',
              data: [3, 6, 2, 7, 4],
              borderColor: 'crimson',
              borderWidth: 5
          }
        ]
      },
      options: {}
    }
  );

})

'4'


Добавим столбцам фоновый цвет

document.addEventListener('DOMContentLoaded', () => {

  new Chart(
    document.querySelector('.chart'),
    {
      type: 'bar',
      data: {
        labels: ['April', 'May', 'June', 'July', 'August'],
        datasets: [
          {
              label: 'Books read',
              data: [3, 6, 2, 7, 4],
              borderColor: 'crimson',
              borderWidth: 5,
              backgroundColor: 'crimson' // назначаем столбцам фон
          }
        ]
      },
      options: {}
    }
  );

})

'5'


Вернём линейный тип графика и добавим сглаживание углов

document.addEventListener('DOMContentLoaded', () => {

  new Chart(
    document.querySelector('.chart'),
    {
      type: 'line', // вернули линейный тип
      data: {
        labels: ['April', 'May', 'June', 'July', 'August'],
        datasets: [
          {
              label: 'Books read',
              data: [3, 6, 2, 7, 4],
              borderColor: 'crimson',
              borderWidth: 5,
              backgroundColor: 'crimson',
              cubicInterpolationMode: 'monotone' // добавили сглаживание углов
          }
        ]
      },
      options: {}
    }
  );

})

'6'


Также можем залить линейный график цветом

document.addEventListener('DOMContentLoaded', () => {

  new Chart(
    document.querySelector('.chart'),
    {
      type: 'line',
      data: {
        labels: ['April', 'May', 'June', 'July', 'August'],
        datasets: [
          {
              label: 'Books read',
              data: [3, 6, 2, 7, 4],
              borderColor: 'crimson',
              borderWidth: 5,
              backgroundColor: 'crimson',
              cubicInterpolationMode: 'monotone',
              fill: true // залили линейный график цветом
          }
        ]
      },
      options: {}
    }
  );

})


'7'


Если хотим, чтобы по оси Y график начинался с нуля, добавим дополнительную опцию

document.addEventListener('DOMContentLoaded', () => {

  new Chart(
    document.querySelector('.chart'),
    {
      type: 'line',
      data: {
        labels: ['April', 'May', 'June', 'July', 'August'],
        datasets: [
          {
            label: 'Books read',
            data: [3, 6, 2, 7, 4],
            borderColor: 'crimson',
            borderWidth: 5,
            backgroundColor: 'crimson',
            cubicInterpolationMode: 'monotone',
            fill: true
          }
        ]
      },
      options: {
        scales: {
          y: {
            beginAtZero: true // назначили оси Y начинать отсчет с нуля
          }
        }
      }
    }
  );

})

'8'


Уберём заливку у графика и добавим еще один график с другими значениями и цветом

document.addEventListener('DOMContentLoaded', () => {

  new Chart(
    document.querySelector('.chart'),
    {
      type: 'line',
      data: {
        labels: ['April', 'May', 'June', 'July', 'August'],
        datasets: [
          {
            label: 'Books read',
            data: [3, 6, 2, 7, 4],
            borderColor: 'crimson',
            borderWidth: 5,
            backgroundColor: 'crimson',
            cubicInterpolationMode: 'monotone',
          },
          // добавили еще один график с другими значениями и цветом
          {
            label: 'Books bought',
            data: [5, 2, 3, 1, 4],
            borderColor: 'teal',
            borderWidth: 5,
            backgroundColor: 'teal',
            cubicInterpolationMode: 'monotone'
          }
        ]
      },
      options: {
        scales: {
          y: {
            beginAtZero: true
          }
        }
      }
    }
  );

})

'9'


Таким образом комбинируя различные настройки и данные мы можем очень гибко создавать графики различной сложности

Графики по-умолчанию являются адаптивными, но нужно учитывать один важный нюанс - чтобы адаптивность графика работала корректно, элемент canvas необходимо обернуть в родительский элемент, который не будет иметь никакого другого контента, кроме элемента canvas - в нашем примере соблюдено данное требование

В официальной документации подробно расписаны все настройки/возможности, приведены примеры самых различных графиков - https://www.chartjs.org/

По итогу первой части мы получаем следующий результат

Ссылка с результатом на Codepen



Вторая часть (добавлено 17.08.2021)

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

Эта часть экспериментальная, и она не претендует на эталонность. Мы просто сделаем работающий вариант динамически обновляемого графика, если вы знаете, как улучшить ту или иную часть текста или кода, напишите мне, в телеграм бота - @frontips_feedback_bot


Начнём

Для понимания этой части необходимо иметь представление о Vue.js. Если не знаете что это или имеете поверхностное представление, то возможно эта статья станет мотивацией к изучению данного фреймворка


HTML структура будет следующей

Подключим:

В структуре добавим возможности Vue:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
  <title>Charts</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;700&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="css/main.css"/>
</head>
<body>
<div id="app">
  <div class="container">
    <h1 class="app__h1 app-h1">Chart</h1>
    <div class="app__config">
      <div class="app__input">
        <label>
          Search
          <input type="text" v-model="search" @input="onSearch">
          <span v-if="loading">loading...</span>
        </label>
      </div>
      <div class="app__select">
        <label>
          PerPage
          <select
            v-model="perPage"
            @change="onChangePerPage"
          >
            <option
              v-for="perPage of perPages"
              :value="perPage"
            >
            </option>
          </select>
        </label>
      </div>
      <div class="app__select">
        <label>
          Type
          <select
            v-model="chartType"
            @change="onChangeType"
          >
            <option
              v-for="type of chartTypes"
              :value="type"
            >
            </option>
          </select>
        </label>
      </div>
      <div class="app__select">
        <label>
          Color
          <select
            v-model="chartColor"
            @change="onChangeColor"
          >
            <option
              v-for="color of chartColors"
              :value="color"
            >
            </option>
          </select>
        </label>
      </div>
    </div>
    
    <div class="app__chart app-chart">
      <div class="app-chart__canvas">
        <canvas ref="chart" class="chart"></canvas>
      </div>
    </div>
  
  
  </div>
</div>

<script src="js/vue.global.prod.js"></script>
<script src="js/lodash.min.js"></script>
<script src="js/axios.min.js"></script>
<script src="js/chart.min.js"></script>
<script src="js/main.js"></script>

</body>
</html>


CSS стили для этого примера

Напишем минимальные CSS стили

body {
  font-family: 'Rubik', sans-serif;
  color: #333;
  background: #fff;
}
.container {
  padding: 0 32px;
}
.app-h1 {
  text-align: center;
}
.app-chart__canvas {
  position: relative;
  max-width: 960px;
  margin: 0 auto;
}
.app__input {
  position: relative;
}
.app__input span {
  position: absolute;
  top: calc(100% + 4px);
  font-size: 10px;
  right: 0;
  left: 0;
  text-align: center;
}
.app__config {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 24px;
  margin-bottom: 24px;
}


Javascript код примера

Будем использовать третью версию Vue.js

Данные будем получать из Github API, а конкретно список репозиториев по названию и их рейтинг

Логика следующая - при загрузке страницы инициализируем график, затем делаем запрос на Github API, получаем данные, назначаем полученные данные графику и обновляем сам график

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

Сначала можно посмотреть полную логику нашего примера, а ниже я опишу отдельно части кода и некоторые нюансы

const App = {
  data() {
    return {
      search: 'chart',
      perPage: 10,
      perPages: [5,10,15,20],
      chart: {},
      labels: [],
      stars: [],
      chartType: 'bar',
      chartTypes: ['bar', 'line', 'bubble'],
      chartColor: 'crimson',
      chartColors: ['crimson', 'teal', 'royalblue'],
      loading: false
    }
  },
  methods: {
    initChart() {
      this.chart = Vue.markRaw(new Chart(this.$refs.chart, {
        type: this.chartType,
        data: {
          labels: [],
          datasets: [{
            label: 'Repos Stars',
            backgroundColor: 'crimson',
            borderColor: 'crimson',
            data: [],
            cubicInterpolationMode: 'monotone',
            fill: true
          }]
        },
        options: {
          responsive: true,
          tooltips: {
            mode: 'index'
          },
          scales: {
            y: {
              beginAtZero: true
            }
          }
        }
      }));
    },
    onChangeType() {
      this.chart.config.type = this.chartType
      this.chart.update()
    },
    onChangeColor() {
      this.chart.data.datasets[0].backgroundColor = this.chartColor
      this.chart.data.datasets[0].borderColor = this.chartColor
      this.chart.update()
    },
    async onChangePerPage() {
      if (!this.search) return
      this.loading = true
      await this.fetchRepos()
      this.updateChart()
      this.loading = false
    },
    updateChart() {
      this.chart.data.labels = this.labels
      this.chart.data.datasets[0].data = this.stars
      this.chart.update()
    },
    async fetchRepos() {
      const response = await axios.get(`https://api.github.com/search/repositories?q=${this.search}&per_page=${this.perPage}`)
      this.labels = response.data.items.map(item => item.full_name)
      this.stars = response.data.items.map(item => item.stargazers_count)
    },
    onSearch: _.debounce(async function () {
      if (!this.search) return
      this.loading = true
      await this.fetchRepos()
      this.updateChart()
      this.loading = false
    }, 1000),
  },
  async mounted() {
    this.initChart()
    await this.fetchRepos()
    this.updateChart()
  }
}

Vue.createApp(App).mount('#app')


Теперь пояснения в порядке выполнения кода

При загрузке страницы инициализируем экземпляр Vue

Vue.createApp(App).mount('#app')


Внутри экземпляра указываем данные

data() {
  return {
    search: 'chart', // модель строки поиска
    perPage: 10, // количество отображаемых репозиториев
    perPages: [5,10,15,20], // варианты количества отображаемых репозиториев - массив
    chart: {}, // модель графика - пустой объект
    labels: [], // массив заголовков(меток) в графике 
    stars: [], // массив рейтингов репозиториев
    chartType: 'bar', // начальный тип графика
    chartTypes: ['bar', 'line', 'bubble'], // варианта типов графика
    chartColor: 'crimson', // начальный цвет графика 
    chartColors: ['crimson', 'teal', 'royalblue'], // варианты цвета графика
    loading: false // модель загрузки данных
  }
},


Далее отрабатывает жизненный цикл экземпляра Vue - mounted(). Делаем его асинхронным, так как нам необходимо дождаться запрашиваемых данных

async mounted() {
  this.initChart() // запускаем метод инициализации графика 
  await this.fetchRepos() // запрашиваем данные и ждем пока данные будут получены
  this.updateChart() // обновляем график с новыми данными
}


Метод initChart() инициализирует экземпляр графика и записывает его в модель chart

Так как в третьей версии Vue реактивность работает через Proxy, то инициализацию графика оборачиваем в Vue.markRaw(), чтобы мы могли взаимодействовать с оригинальным экземпляром графика

Чтобы получить элемент <canvas ref="chart" class="chart"></canvas> воспользуемся ref-ссылкой

Остальные опции графика вам должны быть известны из первой части

initChart() {
  this.chart = Vue.markRaw(new Chart(this.$refs.chart, {
    type: this.chartType, // начальный тип графика указан в модели chartType
    data: {
      labels: [],
      datasets: [{
        label: 'Repos Stars',
        backgroundColor: 'crimson',
        borderColor: 'crimson',
        data: [],
        cubicInterpolationMode: 'monotone',
        fill: true
      }]
    },
    options: {
      responsive: true,
      tooltips: {
        mode: 'index'
      },
      scales: {
        y: {
          beginAtZero: true
        }
      }
    }
  }));
},


Далее асинхронно делаем запрос данных

Для подстановки значений в запрос используем шаблонные строки, результат запроса записываем в константу response, далее преобразовываем массив полученных данных для получения массива заголовков и рейтинга каждого репозитория и записываем их в соответствующие модели (labels и stars)

async fetchRepos() {
  const response = await axios.get(`https://api.github.com/search/repositories?q=${this.search}&per_page=${this.perPage}`)
  this.labels = response.data.items.map(item => item.full_name)
  this.stars = response.data.items.map(item => item.stargazers_count)
},


Далее обновляем необходимые данные графика (заголовки и рейтинг репозиториев) на полученные и обновляем график методом update()

updateChart() {
  this.chart.data.labels = this.labels
  this.chart.data.datasets[0].data = this.stars
  this.chart.update()
},


Разберём метод onSearch

Этот метод планируем запускать при вводе значений в поле <input type="text" v-model="search" @input="onSearch">, но в таком случае будет очень большое количество запросов после каждого введенного символа. Чтобы ограничить количество запросов, воспользуемся утилитой debounce из библиотеки Lodash

Подробнее про debounce можно почитать в статье Повышаем производительность при resize, scroll, …

Если кратко, то теперь метод onSearch будет запускаться через секунду, после того, как будет введен последний символ запроса

Если поле запроса пустое, то прекращаем выполнение метода

Если не пустое, тогда показываем индикатор загрузки, запрашиваем данные (await this.fetchRepos()), обновляем данные (this.updateChart()), скрываем индикатор загрузки

onSearch: _.debounce(async function () {
  if (!this.search) return
  this.loading = true
  await this.fetchRepos()
  this.updateChart()
  this.loading = false
}, 1000),


Аналогичным образом работает метод onChangePerPage(), только метод запускается при изменении значения в <select v-model="perPage" @change="onChangePerPage"></select>

async onChangePerPage() {
  if (!this.search) return
  this.loading = true
  await this.fetchRepos()
  this.updateChart()
  this.loading = false
},


Осталось рассмотреть два метода onChangeType() и onChangeColor() - они также вызываются при изменении значений в соответствующих элементах select

Меняем необходимые значения в объекте экземпляра графика и обновляем график методом update()

onChangeType() {
  this.chart.config.type = this.chartType
  this.chart.update()
},
onChangeColor() {
  this.chart.data.datasets[0].backgroundColor = this.chartColor
  this.chart.data.datasets[0].borderColor = this.chartColor
  this.chart.update()
},


Результат второй части на Codepen



Буду рад, если статья оказалась полезной

Спасибо за ваше внимание и уделённое время!