Ruby
Article

Make Easy Graphs and Charts on Rails with Chartkick

By Ilya Bodrov-Krukowski

chart

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 50
  • country_id (integer) – a foreign key to establish relation between the sporters and countries

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 key
  • competition_id (integer) – a foreign key
  • place (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:

stacked

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.

crosshair

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!

  • farah2

    This post needs many more pictures. :)

    • Ilya Bodrov

      Well, that’s possible, but which ones? To view this in action there is a demo available https://sitepoint-graphs.herokuapp.com/ illustrating all the discussed concepts

      • Salym Senyonga

        Very insightful article. I also thought it could use more pictures, inline, like showing a rendered HTML page at the end of every graph/chart. It makes it easier to follow along especially for folks who code as they read along. Otherwise really good post. Please keep up the good work. I learned a lot.

        • Ilya Bodrov

          Thank you, got your point. Will consider it for the future articles.

  • PeeDee

    Very nice. And the demo is clean, too. Thanks.

    • Ilya Bodrov

      Thank you for reading!

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.