BanditHijo.dev

Visualisasi Data Wilayah dengan Datamaps pada Rails

Created at: 2021-02-07
Author by: BanditHijo

Prerequisite

ruby 2.7.2 rails 6.1.1 datamaps 0.5.9

Latar Belakang

Misalkan saya memiliki sebuah data peta sebaran kasus kumulativ COVID-19 seluruh provinsi di Indonesia.

fetched_at name total_cases total_recovered total_deaths active_cases
2021-02-07 DKI Jakarta 293825 265291 4573 23961
2021-02-07 Jawa Barat 167707 134255 2039 31413
2021-02-07 Jawa Tengah 135552 86400 5646 43506
2021-02-07 Jawa Timur 117851 103219 8152 6480
2021-02-07 Jawa Timur 117851 103219 8152 6480

Saya ingin membuat sebuah visualisasi data peta Indonesia yang terbagi-bagi berdasarkan wilayah provinsi. Kemudian pada masing-masing provinsi tersebut menampilkan data total kasus (total_cases).

Kira-kira ilustrasinya seperti ini:

Visualisasi peta di atas menggunakan bantuan datamaps yang menggunakan D3.js library.

Datamaps is intended to provide some data visualizations based on geographical data. It’s SVG-based, can scale to any screen size, and includes everything inside of 1 script file. It heavily relies on the amazing D3.js library.

Permasalahan

Bagaimana caranya menghubungkan data yang ada di database Rails, dengan datamaps?

Pemecahan Masalah

Kalau kita lihat pada bagian data: {...},

app/views/data_peta/index.html.erb
1// ...
2
3 fills: {
4 defaultFill: '#dddddd',
5 'AAA': '#DB1836',
6 'BBB': '#F15A23',
7 'CCC': '#F89A1C',
8 'DDD': '#FFD500',
9 // ...
10 },
11 data: {
12 'ID.AC': {fillKey: 'AAA', totalCases: '12.345'},
13 'ID.BA': {fillKey: 'BBB', totalCases: '12.345'},
14 'ID.BT': {fillKey: 'CCC', totalCases: '12.345'},
15 'ID.BE': {fillKey: 'DDD', totalCases: '12.345'},
16 // ...
17 },

Data contohnya seperti di atas.

Kita akan mengganti data statis tersebut dengan data yang ada di database yang kita miliki.

ActionController

Kalau melihat format data di atas pada baris 12-15, data: {...} tersebut memiliki format persatuan data, seperti ini:

1'ID.AC': {fillKey: 'AAA', totalCases: '12.345'},

Nah, artinya kita bisa membuat format seperti ini pada controller.

app/controllers/data_peta_controller.rb
1@last_updated = Province.last.fetched_at
2@cumulative_cases = Province.select(:name, :total_cases)
3 .where(fetched_at: @last_updated)
4 .map { |n|
5 "'#{n.name}': {fillKey: 'AAA', totalCases: 'n.total_cases'},\n"
6 }.join

Pada baris 1, saya mengambil tanggal dari data terakhir.

Baris 2, saya memanggil Object Province dan melakukan SELECT terhadap field yang diperlukan saja, yaitu field :nama dan :total_cases.

Baris 3, saya hanya mengambil data pada tanggal paling baru di database yang saya simpan pada variable @last_updated.

Baris 4-5 saya melakukan mapping untuk agar value yang dikembalikan dalam bentuk array.

=> ["'DKI Jakarta': {fillKey: 'AAA', totalCases: '293825'},\n", "'Jawa Barat': {fillKey: 'AAA', totalCases: '167707'},\n", "'Jawa Tengah': {fillKey: 'AAA', totalCas...

Baris 6, saya menggunakan method .join untuk membuat array mejadi string yang nantinya, pada view template, akan menggunakan method raw() untuk melakukan escaping string.

=> "'DKI Jakarta': {fillKey: 'AAA', totalCases: '293825'},\n'Jawa Barat': {fillKey: 'AAA', totalCases: '167707'},\n'Jawa Tengah': {fillKey: 'AAA', totalCases: '1355...

Mengkonversi Nama Provinsi ke Kode Provinsi

Kalau teman-teman perhatikan, bagian nama provinsi dan fillKey: masih belum sesuai dengan format yang diperlukan.

Karena nama provinsi harus berupa kode ISO format dari provinsi tersebut,

Misal untuk Aceh berarti kodenya adalah ID.AC.

Lantas, kita perlu melakukan konversi terhadap data :name terlebih dahulu.

Caranya mudah, saya tinggal buatkan sebuah method baru yang saya beri nama,

convert_name_to_province_code(province_name).

Agar controller saya tetap bersih, saya akan menggunakan controller concern saja.

app/controllers/concerns/convert_name_to_province_code.rb
1module ConvertProvNameToProvCode
2 def convert_name_to_province_code(province_name)
3 provinces = {
4 'Aceh' => 'ID.AC',
5 'Bali' => 'ID.BA',
6 'Banten' => 'ID.BT',
7 'Bengkulu' => 'ID.BE',
8 'DKI Jakarta' => 'ID.JK',
9 'Daerah Istimewa Yogyakarta' => 'ID.YO',
10 'Gorontalo' => 'ID.GO',
11 'Jambi' => 'ID.JA',
12 'Jawa Barat' => 'ID.JR',
13 'Jawa Tengah' => 'ID.JT',
14 'Jawa Timur' => 'ID.JI',
15 'Kalimantan Barat' => 'ID.KB',
16 'Kalimantan Selatan' => 'ID.KS',
17 'Kalimantan Tengah' => 'ID.KT',
18 'Kalimantan Timur' => 'ID.KI',
19 'Kalimantan Utara' => 'ID.KU',
20 'Kepulauan Bangka Belitung' => 'ID.BB',
21 'Kepulauan Riau' => 'ID.KR',
22 'Lampung' => 'ID.LA',
23 'Maluku' => 'ID.MA',
24 'Maluku Utara' => 'ID.MU',
25 'Nusa Tenggara Barat' => 'ID.NB',
26 'Nusa Tenggara Timur' => 'ID.NT',
27 'Papua' => 'ID.PA',
28 'Papua Barat' => 'ID.IB',
29 'Riau' => 'ID.RI',
30 'Sulawesi Barat' => 'ID.SR',
31 'Sulawesi Selatan' => 'ID.SE',
32 'Sulawesi Tengah' => 'ID.ST',
33 'Sulawesi Tenggara' => 'ID.SG',
34 'Sulawesi Utara' => 'ID.SW',
35 'Sumatera Barat' => 'ID.SB',
36 'Sumatera Selatan' => 'ID.SL',
37 'Sumatera Utara' => 'ID.SU'
38 }
39
40 provinces[province_name] if provinces.include? province_name
41 end
42end

Oke, setelah jadi, tinggal di-include-kan ke data_peta_controller.rb.

app/controllers/data_peta_controller.rb
1class DataPetaController < ApplicationController
2 include ConvertProvNameToProvCode
3
4 def index
5 # ...
6 end
7end

Mengklasifikasi total_cases Berdasaran Warna

Selanjutnya kita perlu mengklasifikasi jumlah dari total_cases ke dalam format warna yang tersedia.

'AAA': '#DB1836'
'BBB': '#F15A23'
'CCC': '#F89A1C'
'DDD': '#FFD500'
'EEE': '#C1D737'
'FFF': '#44B549'
'GGG': '#0EB049'
'HHH': '#016533'

Anggaplah ‘AAA’ adalah yang paling banyak dan ‘HHH’ yang paling sedikit.

Saya akan menggunakan controller concern lagi yang saya beri nama, convert_total_cases_to_code(total_cases).

app/controllers/concerns/convert_total_cases_to_code.rb
1module ConvertTotalCasesToCode
2 def convert_total_cases_to_code(total_cases)
3 case total_cases
4 when 200_000..300_000
5 'AAA'
6 when 150_000..200_000
7 'BBB'
8 when 90_000..150_000
9 'CCC'
10 when 70_000..90_000
11 'DDD'
12 when 50_000..70_000
13 'EEE'
14 when 30_000..50_000
15 'FFF'
16 when 10_000..30_000
17 'GGG'
18 when 100..10_000
19 'HHH'
20 end
21 end
22end

Oke, setelah jadi, tinggal di-include-kan ke data_peta_controller.rb.

app/controllers/data_peta_controller.rb
1class DataPetaController < ApplicationController
2 include ConvertProvNameToProvCode
3 include ConvertTotalCasesToCode
4
5 def index
6 # ...
7 end
8end

Memberikan Delimiter , untuk Ribuan

Data total_cases tidak memiliki format string berupa delimiter koma (,) untuk memberikan kemudahan dalam membaca satuan ribuan dalam nominal angka.

Rails sudah menyediakan helper method untuk menghandle ini namun adanya di view template yang disediakan oleh ActionView yang bernama number_with_delimiter(number, options = {}).

Apakah bisa digunakan di Controller?

Kalau tidak ada, apakah kita perlu membuat sendiri?

Apakah di ActionController ada juga method helper yang sama?

Mudahnya tinggal kita include saja ActionView::Helpers::NumberHelper.

app/controllers/data_peta_controller.rb
1class DataPetaController < ApplicationController
2 include ConvertProvNameToProvCode
3 include ConvertTotalCasesToCode
4 include ActionView::Helpers::NumberHelper
5
6 def index
7 # ...
8 end
9end

Selanjutnya tinggal kita gunakan pada object query yang sudah kita racik sebelumnya.

app/controllers/data_peta_controller.rb
1class DataPetaController < ApplicationController
2 include ConvertProvNameToProvCode
3 include ConvertTotalCasesToCode
4 include ActionView::Helpers::NumberHelper
5
6 def index
7 @last_updated = Province.last.fetched_at
8 @cumulative_cases = Province.select(:name, :total_cases)
9 .where(fetched_at: @last_updated)
10 .map { |n|
11 "'#{convert_name_to_province_code(n.name)}': {fillKey: '#{convert_total_cases_to_code(n.total_cases)}', totalCases: '#{number_with_delimiter(n.total_cases, delimiter: ',')}'},\n"
12 }.join
13 end
14end

Instance variable @cumulative_cases inilah yang akan kita gunakan pada view template.

ActionView

Setelah selesai membuat object query di controller, selanjutnya tinggal kita gunakan di view template.

Tapi sebelumnya, kita perlu untuk menyiapkan beberapa Javascript library yang akan diperlukan oleh datamaps.

  1. d3.min.js
  2. topojson.min.js
  3. datamaps.idn.min.js, saya menggunakan datamaps wilayah Indonesia.

Kita akan letakkan pada direktori vendor/assets/javascripts/ saja.

.
├─ 📁 app/
├─ 📁 bin/
├─ 📁 config/
├─ 📁 db/
├─ 📁 lib/
├─ 📁 log/
├─ 📁 node_modules/
├─ 📁 public/
├─ 📁 spec/
├─ 📁 storage/
├─ 📁 tmp/
├─ 📂 vendor/
│   └─ 📂 assets/
│      └─ 📂 javascripts/
│         ├─ 📄 d3.min.js 👈️
│         ├─ 📄 topojson.min.js 👈️
│         └─ 📄 datamaps.idn.min.js 👈️
│
├─ 📄 Gemfile
...
...

Buatkan struktur seperti di atas.

Kemudian, kita akan masukkan kepada daftar assets precompile, di config/initializers/assets.rb.

config/initializers/assets.rb
1# Be sure to restart your server when you modify this file.
2
3# ...
4
5# Precompile additional assets.
6# application.js, application.css, and all non-JS/CSS in the app/assets
7# folder are already added.
8Rails.application.config.assets.precompile += %w(
9 d3.min.js topojson.min.js datamaps.idn.min.js
10)

Tambahkan seperti pada baris 8, 9, 10.

Mantap!

Sekarang kita lanjut ke view template.

app/views/data_peta/index.html.erb
1<div class="container px-0 pt-2 pb-5 mt-5" style="overflow-y: auto">
2 <%= javascript_include_tag 'd3.min' %>
3 <%= javascript_include_tag 'topojson.min' %>
4 <%= javascript_include_tag 'datamaps.idn.min' %>
5
6 <div id="container1" style="position: relative; width: 1100px; height: 400px; margin: 0 auto;"></div>
7
8 <script type="text/javascript">
9 //basic map config with custom fills, mercator projection
10 var map = new Datamap({
11 scope: 'idn',
12 element: document.getElementById('container1'),
13 setProjection: function (element) {
14 var projection = d3.geo.mercator()
15 .center([115, -5])
16 .rotate([0, 0])
17 .scale(3900 / 3)
18 var path = d3.geo.path()
19 .projection(projection);
20 return {path: path, projection: projection};
21 },
22 fills: {
23 defaultFill: '#dddddd',
24 'AAA': '#DB1836',
25 'BBB': '#F15A23',
26 'CCC': '#F89A1C',
27 'DDD': '#FFD500',
28 'EEE': '#C1D737',
29 'FFF': '#44B549',
30 'GGG': '#0EB049',
31 'HHH': '#016533',
32 },
33 data: {
34 <%= raw @cumulative_cases %>
35 },
36 geographyConfig: {
37 popupTemplate: function(geo, data) {
38 return ['<div class="hoverinfo"><strong>',
39 geo.properties.name + '</strong><br>Kasus (Kulumatif)',
40 ': ' + data.totalCases,
41 '</div>'].join('');
42 }
43 }
44 });
45 </script>
46</div>

Baris 2, 3, 4, adalah cara memanggil Javascript library yang kita masukkan ke dalam direktori vendor sebelumnya.

Baris 34, adalah cara memanggil instance variable @cumulative_cases yang telah kita buat object querynya di app/controllers/data_peta_controller.rb.

Selesai!

Hanya seperti itu saja.

Apabila dirasa ada yan kurang pas, teman-teman bisa memodifikiasi dan memperbaiki sesuai keinginan.

Pesan Penulis

Sepertinya, segini dulu yang dapat saya tuliskan.

Mudah-mudahan dapat bermanfaat.

Terima kasih.

(^_^)

Referensi

  1. github.com/markmarkoh/datamaps
    Diakses tanggal: 2021/02/07

  2. http://datamaps.github.io/
    Diakses tanggal: 2021/02/07

  3. github.com/d3/d3
    Diakses tanggal: 2021/02/07

  4. api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
    Diakses tanggal: 2021/02/07