Статьи

Практические графики на рельсах: Chartkick на практике

В предыдущей статье я рассмотрел основы Chartkick — отличной библиотеки для простой визуализации графиков в приложениях Rails. Статья привлекла определенное внимание, поэтому я решил немного подробнее рассказать и показать, как Chartkick, наряду с Groupdate , можно использовать для решения реальных задач.

Напомню, что Chartkick — это гем, который интегрируется с Rails и предоставляет методы для быстрой визуализации графиков на основе ваших данных. Этот гем поддерживает Chart.js, Google Charts и Highchart адаптеры. Chartkick поставляется с готовой поддержкой Groupdate, которая упрощает написание сложных запросов группирования.

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

Исходный код можно найти на GitHub .

Рабочую демонстрацию можно найти на Heroku .

Препараты

Для этой демонстрации я буду использовать Rails 5, но шаги для Rails 3 и 4 практически идентичны.

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

Хорошо, задача ясна, и мы сейчас углубимся в код.

Создайте новое приложение Rails под названием Tracker без набора тестов по умолчанию:

 $ rails new Tracker -T 

Загляните в эти драгоценные камни:

Gemfile

 [...] gem 'chartkick' gem 'groupdate' gem 'bootstrap-sass' gem 'pg' [...] 

Chartkick — это, конечно, наш основной инструмент на сегодня. groupdate — это приятное дополнение, позволяющее легко использовать сложные запросы. bootstrap-sass будет использоваться для стилизации, а pg — адаптер PostgreSQL. Обратите внимание, что Groupdate не будет работать с SQLite 3!

Установите ваши драгоценные камни сейчас:

 $ bundle install 

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

конфиг / database.yml

 development: adapter: postgresql encoding: unicode database: tracker pool: 5 username: "PG_USER" password: "PG_PASSWORD" host: localhost port: 5432 

Кроме того, нам также понадобятся некоторые дополнительные активы:

Highcharts — отличная библиотека для построения интерактивных графиков, и я собираюсь использовать ее в этой демонстрации. Тем не менее, вы можете придерживаться либо Chart.js (который используется по умолчанию начиная с Chartkick v2), либо Google Charts . Bootstrap Datepicker — это надстройка для Bootstrap, которую мы будем использовать для создания формы.

Теперь подключите все эти активы (обратите внимание, что Turbolinks должен быть размещен последним):

JavaScripts / application.js

 //= require jquery //= require jquery_ujs //= require datepicker //= require highcharts //= require chartkick //= require turbolinks 

таблицы стилей / application.scss

 @import 'bootstrap-sprockets'; @import 'bootstrap'; @import 'datepicker'; 

Большой! Конечно, нам нужны данные и место для их хранения, поэтому давайте перейдем к следующему разделу и создадим модели.

Модели

Генерация двух очень простых моделей: Item и ClickTrack

 $ rails g model Item title:string $ rails g model ClickTrack item:belongs_to $ rake db:migrate 

Элемент — это наш продукт, и каждый раз при нажатии на элемент создается трек кликов. Мы не собираемся кодировать эту логику, но, как вы видите, она действительно проста. Убедитесь, что вы установили отношение «один ко многим» между моделями:

модели / item.rb

 class Item < ApplicationRecord has_many :click_tracks end 

модели / click_track.rb

 class ClickTrack < ApplicationRecord belongs_to :item end 

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

дБ / seeds.rb

 %w(apple cherry).each do |item| new_item = Item.create({title: item}) 1000.times do new_item.click_tracks.create!({created_at: rand(3.years.ago..Time.now) }) end end 

Загрузите пример данных в базу данных:

 $ rails db:seed # use rake for Rails < 5 

Еще раз, не забывайте, что начиная с версии 5 Rails все команды живут в пространстве имен rails :

 $ rails db:migrate # use rake for Rails < 5 

Маршруты и главная страница

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

конфиг / routes.rb

 [...] resources :items, only: [:index] do resources :click_tracks, only: [:index] end root 'items#index' [...] 

Создайте два контроллера:

items_controller.rb

 class ItemsController < ApplicationController def index @items = Item.all end end 

click_tracks_controller.rb

 class ClickTracksController < ApplicationController def index @item = Item.find_by(id: params[:item_id]) end end 

Они действительно просты, поэтому здесь нечего комментировать. Теперь просмотр корневой страницы:

просмотров / пункты / index.html.erb

 <h1>Items</h1> <ul><%= render @items %></ul> 

И соответствующий частичный

просмотров / пункты / _item.html.erb

 <li> <%= item.title %> <%= link_to 'View clicks', item_click_tracks_path(item), class: 'btn btn-primary' %> </li> 

Пока у нас есть вложенные маршруты, мы должны использовать помощник item_click_tracks_path , а не click_tracks_path . Вы можете прочитать больше здесь о вложенных ресурсах.

Наконец, давайте также завернем содержимое всей страницы в div оснащенный классом Bootstrap:

просмотров / макеты / application.html.erb

 [...] <body> <div class="container"> <%= yield %> </div> </body> [...] 

Создание формы

Отлично, первый шаг сделан, и мы готовы построить основную функцию сейчас. Говоря о форме, она определенно должна содержать два ввода: один для даты начала и другой для даты окончания. Имея их на месте, пользователь может выбрать период времени для отображения треков кликов. Эти входы будут питаться от стиля Bootstrap и плагина Datepicker:

просмотров / click_tracks / index.html.erb

 <h1>Click tracking</h1> <div id="event_period" class="row"> <%= form_tag api_item_click_tracks_path(@item), remote: true do %> <div class="col-sm-1"> <label for="start_date">Start date</label> </div> <div class="col-sm-3"> <div class="input-group"> <input type="text" class="actual_range form-control datepicker" id="start_date" name="start_date"> <div class="input-group-addon"> <span class="glyphicon glyphicon-th"></span> </div> </div> </div> <div class="col-sm-1 col-sm-offset-1"> <label for="end_date">End date</label> </div> <div class="col-sm-3"> <div class="input-group"> <input type="text" class="actual_range form-control datepicker" id="end_date" name="end_date"> <div class="input-group-addon"> <span class="glyphicon glyphicon-th"></span> </div> </div> </div> <div class="col-sm-2"> <%= submit_tag 'Show!', class: 'btn btn-primary' %> </div> <% end %> </div> 

Этот код довольно длинный, но очень простой. Мы добавляем форму, которая должна быть отправлена ​​асинхронно в api_item_click_tracks_path (этот маршрут еще не существует). Внутри формы есть два входа с идентификаторами #start_date и #end_date .

Чтобы сделать их немного красивее, я использую .input-group-addon который добавляет маленький значок рядом с каждым
вход. И, наконец, есть кнопка «Отправить» для отправки формы.

Теперь нам нужны маршруты:

конфиг / routes.rb

 [...] namespace :api do resources :items, only: [] do resources :click_tracks, only: [:create] do collection do get 'by_day' end end end end [...] 

Мы называем пространство этих маршрутов под api . Соответствующие действия будут закодированы на следующих шагах.

Чтобы воспользоваться подключаемым модулем Datepicker, поместите следующий код в ваше представление (конечно, вы также можете поместить его в отдельный файл CoffeeScript):

просмотров / click_tracks / index.html.erb

 [...] <script data-turbolinks-track> $(document).ready(function() { $('#event_period').datepicker({ inputs: $('.actual_range'), startDate: '-3y', endDate: '0d', todayBtn: 'linked', todayHighlight: 'true', format: 'yyyy-mm-dd' }); }); </script> [...] 

Мы снабжаем всю форму этой новой функциональностью и предоставляем фактические входные данные, используя опцию inputs . Кроме того, startDate установлен на 3 года назад (потому что, как вы помните, даты создания треков кликов были определены как rand(3.years.ago..Time.now) ), а endDate 0d что означает, что он должен содержать сегодняшний Дата. Затем отобразите кнопку «Сегодня», выделите сегодняшнюю дату и укажите формат даты. Большой!

date_picker

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

Отображение графика

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

просмотров / click_tracks / index.html.erb

 [...] <%= render 'graph' %> 

просмотров / click_tracks / _graph.html.erb

 <div id="graph"> <%= stat_by(@start_date, @end_date) %> </div> 

stat_by — это вспомогательный метод (мы скоро его создадим), который принимает даты начала и окончания. Когда страница загружается, эти даты не устанавливаются, поэтому мы должны сами позаботиться о таком сценарии.

хелперы / click_tracks_helper.rb

 module ClickTracksHelper def stat_by(start_date, end_date) start_date ||= 1.month.ago end_date ||= Time.current end end 

Здесь мы используем так называемые «нулевые стражи» ( ||= ) для установки значений по умолчанию. Давайте отобразим диаграмму, используя функцию асинхронной загрузки Chartkick:

хелперы / click_tracks_helper.rb

 module ClickTracksHelper def stat_by(start_date, end_date) start_date ||= 1.month.ago end_date ||= Time.current line_chart by_day_api_item_click_tracks_path(@item, start_date: start_date, end_date: end_date), basic_opts('Click count', start_date, end_date) end end 

Таким образом, вместо загрузки диаграммы во время загрузки страницы, это делается в фоновом режиме, что, конечно, лучше с точки зрения пользовательского опыта. Чтобы это работало, вам нужно настроить действие в своем приложении, которое представляет правильно отформатированные данные и подключает jQuery или Zepto.js. В этом случае мы используем by_day_api_item_click_tracks_path который уже был настроен, но действие не закодировано — это будет на следующем шаге.

Метод basic_opts , как следует из названия, подготавливает некоторые параметры (включая специфичные для библиотеки) для графа:

хелперы / click_tracks_helper.rb

 private def basic_opts(title, start_date, end_date) { discrete: true, library: { title: {text: title, x: -20}, subtitle: {text: "from #{l(start_date, format: :medium)} to #{l(end_date, format: :medium)}", x: -20}, yAxis: { title: { text: 'Count' } }, tooltip: { valueSuffix: 'click(s)' }, credits: { enabled: false } } } end 

Метод l является псевдонимом для localize которая форматирует метку времени. Формат по умолчанию :medium по умолчанию отсутствует, поэтому давайте добавим один:

конфиг / локали / en.yml

 en: time: formats: medium: '%d %B %Y' 

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

Действия контроллера

Передний конец готов, и теперь пришло время позаботиться о заднем конце. Пока мы определяем пространство маршрутов под api , новые файлы контроллера должны быть помещены в папку api :

Контроллеры / API / click_tracks_controller.rb

 class Api::ClickTracksController < Api::BaseController end 

Обратите внимание, что Api::ClickTracksController наследуется от Api::BaseController , как и все контроллеры Api::* . Например, в моем производственном приложении у меня есть два схожих контроллера, которые используют эти методы.

Контроллеры / API / base_controller.rb

 class Api::BaseController < ApplicationController end 

Что мы хотим, чтобы произошло внутри Api::BaseController ? Он собирается разместить два обратных вызова: один для загрузки необходимых данных (элемент и его треки кликов) и другой для форматирования полученных дат (потому что пользователь может вводить их вручную или вообще не указывать дату).

Загрузка данных для нас не проблема:

Контроллеры / API / base_controller.rb

 [...] before_action :load_data private def load_data @item = Item.includes(:click_tracks).find_by(id: params[:item_id]) @click_tracks = @item.click_tracks end [...] 

Что касается форматирования дат, метод будет немного более сложным:

Контроллеры / API / base_controller.rb

 [...] before_action :load_data before_action :format_dates private def format_dates @start_date = params[:start_date].nil? || params[:start_date].empty? ? 1.month.ago.midnight : params[:start_date].to_datetime.midnight @end_date = params[:end_date].nil? || params[:end_date].empty? ? Time.current.at_end_of_day : params[:end_date].to_datetime.at_end_of_day @start_date, @end_date = @end_date, @start_date if @end_date < @start_date end [...] 

Если одна из дат пуста, мы заполняем ее значением по умолчанию. Обратите внимание на использование довольно at_end_of_day методов midnight и at_end_of_day . Если дата не пуста, преобразуйте ее в datetime (потому что изначально это строка). Наконец, мы меняем дату, если дата окончания следует после даты начала.

Теперь create код действия create которое запускается при отправке формы. Конечно, он отвечает Javascript, так как отправка формы осуществляется через AJAX:

Контроллеры / API / click_tracks_controller.rb

 [...] def create respond_to do |format| format.js end end [...] 

Фактический Javascript прост: просто замените старую диаграмму новой. Вот где частичное, созданное ранее, пригодится:

просмотров / апи / click_tracks / create.js.erb

 $('#graph').replaceWith('<%= j render 'click_tracks/graph' %>'); 

Последний шаг — это кодирование действия, которое подготавливает данные для нашего графика. Как вы помните, у нас есть следующие маршруты:

конфиг / routes.rb

 [...] namespace :api do resources :items, only: [] do resources :click_tracks, only: [:create] do collection do get 'by_day' end end end end [...] 

Следовательно, действие должно быть by_day . Мы хотим, чтобы он отображал JSON, содержащий информацию о том, сколько кликов произошло за каждый день.

Контроллеры / API / click_tracks_controller.rb

 [...] def by_day clicks = @click_tracks.group_by_day('created_at', format: '%d %b', range: @start_date..@end_date).count render json: [{name: 'Click count', data: clicks}].chart_json end [...] 

group_by_day — это метод, представленный гемом groupdate, который группирует треки кликов по дате создания. Метод count , как вы, наверное, догадались, подсчитывает, сколько кликов произошло за каждый день. В результате переменная click будет содержать такой объект, как {'07 Jun': 10, '08 Jun': 4} (форматирование ключей контролируется параметром :format ). chart_json — это специальный метод Chartkick для подготовки JSON к визуализации.

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

Прежде всего, добавьте еще два метода в BaseController :

Контроллеры / API / base_controller.rb

 [...] private def by_year? @end_date - (1.year + 2.days) > @start_date end def by_month? @end_date - (3.month + 2.days) > @start_date end [...] 

Эти методы просто проверяют длину выбранного периода — теперь мы будем использовать их в методе by_day :

Контроллеры / API / click_tracks_controller.rb

 [...] def by_day opts = ['created_at', {range: @start_date..@end_date, format: '%d %b'}] method_name = :group_by_day if by_year? opts[1].merge!({format: '%Y'}) method_name = :group_by_year elsif by_month? opts[1].merge!({format: '%b %Y'}) method_name = :group_by_month end clicks = @click_tracks.send(method_name, *opts).count render json: [{name: 'Click count', data: clicks}].chart_json end [...] 

Здесь мы подготовим массив аргументов, которые будут переданы одному из методов group_by_day ( group_by_day ,
group_by_month или group_by_year ). Затем установите метод по умолчанию для вызова и выполните некоторые проверки. Если диапазон больше месяца, обновите параметры форматирования, чтобы отображались только год или месяц и год, а затем динамически вызовите метод. *opts возьмет массив и преобразует его в список аргументов. С этим кодом вы можете легко определить свои собственные условия и правила группировки.

Теперь работа выполнена, и вы можете наблюдать за конечным результатом!

Финальный сайт

Вывод

В этой статье мы продолжили обсуждение Chartkick и Groupdate и использовали их на практике. Чтобы сделать работу пользователей более приятной, мы также использовали плагин Bootstrap Datepicker. Код, указанный здесь, может быть расширен. Например, если вы хотите визуализировать количество отображений для каждого элемента, это также легко сделать.

Если у вас остались какие-либо вопросы, не стесняйтесь обращаться ко мне — я очень рад, когда вы отправите свой отзыв. Как всегда, спасибо, что остаетесь со мной и до скорой встречи!