Make Easy Graphs and Charts on Rails with Chartkick
We work with data presented in the various formats every day. From my point of view, one of the most convenient formats for presenting numerical data is a graph. Users like graphs, especially interactive ones, because they are beautiful and fun – in contrast to dull static tables.
There are many solutions to render beautiful interactive graphs in web apps, but today we will discuss something special: a solution built for Rails that makes rendering graphs a breeze. This solution is called Chartkick and it was built by Andrew Kane. Chartkick can work with Google Charts, Highcharts, and Chart.js. It has many options for customization and also has a bunch of supporting libraries like groupdate, hightop, and active_median.
In this article we will discuss how to integrate Chartkick into a Rails app, render various graphs, customize them, make them load asynchronously, and how to further power up your code with groupdate and hightop gems.
The source code can be found on GitHub.
The working demo is available at Heroku.
Preparing the Application
Go ahead and create a new Rails application:
$ rails new HappyGrapher -T
For this article I’ll be using Rails 5 release candidate, but the provided code examples should work with Rails 4 and 3 as well.
Suppose our app keeps track of “sporters” (athletes) and the competitions in which they participate. Here is the information about all tables we will require:
sporters
name
(string
)age
(integer
) – for this demo let’s suppose the age lies in the interval from 18 to 50country_id
(integer
) – a foreign key to establish relation between thesporters
andcountries
countries
title
(string
) – we will add 50 random countries into our database
competitions
title
(string
) – we will have a bunch of absolutely random competitions
competition_results
This will be an intermediate table to establish a many-to-many relation between sporters
and competitions
.
sporter_id
(integer
) – a foreign keycompetition_id
(integer
) – a foreign keyplace
(integer
) – which place did the sporter take. For this demo, we’ll suppose that this column may take values from 1 to 6.
Create and apply all the necessary migrations:
$ rails g model Country name:string
$ rails g model Sporter name:string age:integer country:references
$ rails g model Competition title:string
$ rails g model CompetitionResult sporter:references competition:references place:integer
$ rake db:migrate
Also create a controller, a view and a root route:
statistics_controller.rb
class StatisticsController < ApplicationController
def index
end
end
views/statistics/index.html.erb
<h1>Statistics</h1>
config/routes.rb
[...]
root 'statistics#index'
[...]
So far so good, but to render charts we will obviously require some sample data, so let’s add that now.
Loading Sample Data
To speed up the process of loading sample data, I’ll use two gems: faker that allows you to generate various texts from names and e-mails to paragraphs and pseudo-hacker phrases, and countries that greatly simplifies fetching information about existing countries. Add these gems into the Gemfile:
Gemfile
[...]
gem 'countries'
gem 'faker'
[...]
and install them:
$ bundle install
Now open up seeds.rb file and paste this code to add 50 countries:
db/seeds.rb
ISO3166::Country.all.shuffle.first(50).each do |country|
Country.create({name: country.name})
end
This will add totally random countries so don’t be surprised if you’ll end up having Antarctica or Sao Tome in your table.
Now we also need a bunch of sporters:
db/seeds.rb
100.times { Sporter.create({
name: Faker::Name.name,
age: rand(18..50),
country_id: rand(1..50)
}) }
Here we use Faker to generate a sample name.
Next, competitions. I wasn’t able to find any ready-to-use list, so we’ll type sports names by hand:
db/seeds.rb
%w(tennis parachuting badminton archery chess boxing racing golf running skiing walking cycling surfing swimming skeleton).each {|c| Competition.create({title: c}) }
And, lastly, the competition results:
db/seeds.rb
Competition.all.each do |competition|
sporters = Sporter.all.shuffle.first(6)
(1..6).each do |place|
CompetitionResult.create({
sporter_id: sporters.pop.id,
competition_id: competition.id,
place: place,
created_at: rand(5.years.ago..Time.now)
})
end
end
Notes that we override the created_at
column pretending that a competition took place some years or months ago.
Displaying a Simple Chart
Okay, everything is ready to start implementing the core functionality – graphs. Add the chartkick gem into the Gemfile:
Gemfile
[...]
gem 'chartkick'
[...]
and install it:
$ bundle install
Chartkick supports both Google Charts and Highcharts and Chart.js (starting from version 2.0 this is the default adapter.) For this demo I’ll be using Highcharts, but the installation process of Google Charts is very similar.
First of all, download the latest version of Highcharts and place it inside the javascripts directory (alternatively you may load it via CDN by adding javascript_include_tag
into the layouts/application.html.erb). Next, add these two files:
javascripts/application.js
[...]
//= require highcharts
//= require chartkick
[...]
That’s pretty much – we can now render charts. For starters, let’s display a bar chart illustrating sporters’ ages.
Load all sporters:
statistics_controller.rb
[...]
def index
@sporters = Sporter.all
end
[...]
and tweak the view:
views/statistics/index.html.erb
<%= bar_chart @sporters.group(:age).count %>
We simply group sporters by age and calculate the number of items in each group. Really simple.
You are probably wondering how to define settings for your graph to give it a name, adjust width, height, and other stuff. That’s easy as well – some settings are being passed as arguments directly to the bar_chart
(and other similar methods), other are being set inside the :library
option.
Create a new StatisticsHelper
and extract code there:
statistics_helper.rb
module StatisticsHelper
def sporters_by_age
bar_chart @sporters.group(:age).count, height: '500px', library: {
title: {text: 'Sporters by age', x: -20},
yAxis: {
allowDecimals: false,
title: {
text: 'Ages count'
}
},
xAxis: {
title: {
text: 'Age'
}
}
}
end
end
:library
contains library-specific settings. Here we are preventing decimal numbers from appearing on the Y axis (obviously, we can’t have 2,5 sporters aged 20) and give it a name. X axis also has a name defined. On top of that, provide the name for the whole graph (it will appear at the bottom by default). Highcharts has loads of other options available, so be sure to browse its documentation.
Also note that settings can be defined globally as well.
Using Hightop
If you wish to display only the most “popular” ages among sporters, use the hightop gem – a small but useful library designed to solve such tasks. Simply include it into the Gemfile:
Gemfile
[...]
gem 'hightop'
[...]
and run
$ bundle install
Now you can display, for example, the ten most popular ages:
views/statistics/index.html.erb
<%= bar_chart @sporters.top(:age, 10) %>
Rendering Graphs Asynchronously
If your database has a lot of data to process in order to render a graph, the page will load slowly. Therefore, it is better to render your graphs asynchronously. Chartkick supports this functionality, too. All you have to do is create a separate route and a controller action, then use this route inside a view. Note that it requires jQuery or Zepto.js to be present.
Create a new route:
config/routes.rb
[...]
resources :charts, only: [] do
collection do
get 'sporters_by_age'
end
end
[...]
and a controller:
charts_controller.rb
class ChartsController < ApplicationController
def sporters_by_age
result = Sporter.group(:age).count
render json: [{name: 'Count', data: result}]
end
end
Having this in place, simply modify your helper to call the newly created route:
statistics_helper.rb
[...]
def sporters_by_age
bar_chart sporters_by_age_charts_path, height: '500px', library: {
[...]
}
end
[...]
Now your chart will be loaded asynchronously allowing users to browse other contents on the page. The @sporters
instance variable is not needed anymore, so you can remove it from the index
method of StatisticsController
.
More Graph Types
Column Chart
To demonstrate the usage of the column chart, let’s display how many sporters each country has. First of all, create a new helper similar to the one we defined earlier:
statistics_helper.rb
[...]
def sporters_by_country
column_chart sporters_by_country_charts_path, library: {
title: {text: 'Sporters by country', x: -20},
yAxis: {
title: {
text: 'Sporters count'
}
},
xAxis: {
title: {
text: 'Country'
}
}
}
end
[...]
Use it inside the view:
views/statistics/index.html.erb
[...]
<%= sporters_by_country %>
[...]
Add the route:
config/routes.rb
[...]
resources :charts, only: [] do
collection do
get 'sporters_by_age'
get 'sporters_by_country'
end
end
[...]
As for the controller action, it is going to be a bit more complex as we have to construct the result manually:
charts_controller.rb
[...]
def sporters_by_country
result = {}
Country.all.map do |c|
result[c.name] = c.sporters.count
end
render json: [{name: 'Count', data: result}]
end
[...]
Note how the result
hash is being constructed – the key is the country’s name and the value is the total number of sporters.
Reload the page and observe the result!
Stacked Column Chart
Let’s also display how many times each country took a certain place (from 1 to 6). Once again, define a new helper:
statistics_helper.rb
[...]
def results_by_country
column_chart results_by_country_charts_path, stacked: true, height: '500px', library: {
title: {text: 'Results by country', x: -20},
yAxis: {
title: {
text: 'Count'
}
},
xAxis: {
title: {
text: 'Countries and places'
}
}
}
end
[...]
Note the stacked: true
option that provides the following result:
Use the helper inside the view:
views/statistics/index.html.erb
[...]
<%= results_by_country %>
[...]
Add the route:
config/routes.rb
[...]
resources :charts, only: [] do
collection do
get 'sporters_by_age'
get 'sporters_by_country'
get 'results_by_country'
end
end
[...]
Lastly, create the controller action:
charts_controller.rb
[...]
def results_by_country
result = Country.all.map do |c|
places = {}
(1..6).each do |place|
places[place] = c.sporters.joins(:competition_results).
where("competition_results.place = #{place}").count
end
{
name: c.name,
data: places
}
end
render json: result
end
[...]
We take all the countries and use map
to construct an array of data. Inside, find all the sporters from this country who took a certain place. joins
is used to join with the competition_results
table because information about the place is stored there. Then simply use where
and count
to get the desired value. Then, as we’ve already seen, assign the country’s name for the :name
and the places
hash for the :data
. As a result, an array of hashes will be created.
Line Chart and Groupdate
The last chart type we will tackle today is the line chart. To demonstrate it, let’s display how many competitions were held each year.
Once again, create a helper
statistics_helper.rb
[...]
def competitions_by_year
line_chart competitions_by_year_charts_path, library: {
title: {text: 'Competitions by year', x: -20},
yAxis: {
crosshair: true,
title: {
text: 'Competitions count'
}
},
xAxis: {
crosshair: true,
title: {
text: 'Year'
}
}
}
end
[...]
:crosshair
option is used to display a helpful crosshair following the user’s pointer.
Use this helper in your view:
views/statistics/index.html.erb
[...]
<%= competitions_by_year %>
[...]
and add a new route:
config/routes.rb
[...]
resources :charts, only: [] do
collection do
get 'sporters_by_age'
get 'sporters_by_country'
get 'results_by_country'
get 'competitions_by_year'
end
end
[...]
Now we need to create a new controller action, but how are we going to group competitions by year and count them? Of course, we may construct our own query, but the author of Chartkick already took care of it and crafted a handy Groupdate gem. As the name suggests, it allows you to group records by year, month, day, and more. It supports timezones, ranges of dates, formatting, ordering, and other fancy stuff, so it is a nice solution to use with Chartkick.
Add Groupdate into your Gemfile:
Gemfile
[...]
gem 'groupdate'
[...]
The only problem with Groupdate is that it does not support SQLite3, so you’ll have to user some other DMBS. For this demo I’ll be using PostgreSQL. Please note that if you decide to use MySQL, timezone support must be installed as well.
Tweak your Gemfile once again by replacing sqlite3 with the pg gem:
Gemfile
[...]
gem 'pg'
[...]
Then install the gems:
$ bundle install
and modify the database.yml config file:
config/database.yml
[...]
development:
adapter: postgresql
encoding: unicode
database: your_database
pool: 5
username: your_user
password: your_password
host: localhost
port: 5432
[...]
Now run the migrations again and populate the tables with sample data
$ rake db:migrate
$ rake db:seed
Don’t forget to create a database (rake db:create
) prior to performing those commands.
Now we can code the controller’s action:
charts_controller.rb
[...]
def competitions_by_year
result = CompetitionResult.group_by_year(:created_at, format: "%Y").count
render json: [{name: 'Count', data: result}]
end
[...]
Looks great. The :format
option allows you to provide the format for the keys. As long as we want to display only the years, I used %Y
. The full list of available directives can be found in the official Ruby documentation.
Reload your page once again and observe the final result. If you are not satisfied with how the graphs look like, play with the display settings found in the Highcharts documentation.
Conclusion
In this article we’ve discussed Chartkick – a great gem to simplify chart rendering. We tried using various types of charts, made them loading asynchronously, and also employed additional gems, like Groupdate and Hightop. Of course, there is more to these gems, so be sure to browse their docs and experiment further with the code.
As always, feel free to post your questions and feedback. Happy charting and see you soon!