Sejak memasang "dark" theme, saya cenderung menjadi malas menulis. Untuk sementara, dark theme saya disable dulu yaa. Terima kasih (^_^) (bandithijo, 2024/09/15) ●
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: {...}
,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
fills: {
defaultFill: '#dddddd',
'AAA': '#DB1836',
'BBB': '#F15A23',
'CCC': '#F89A1C',
'DDD': '#FFD500',
// ...
},
data: {
'ID.AC': {fillKey: 'AAA', totalCases: '12.345'},
'ID.BA': {fillKey: 'BBB', totalCases: '12.345'},
'ID.BT': {fillKey: 'CCC', totalCases: '12.345'},
'ID.BE': {fillKey: 'DDD', totalCases: '12.345'},
// ...
},
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:
'ID.AC': {fillKey: 'AAA', totalCases: '12.345'},
Nah, artinya kita bisa membuat format seperti ini pada controller.
1
2
3
4
5
6
@last_updated = Province.last.fetched_at
@cumulative_cases = Province.select(:name, :total_cases)
.where(fetched_at: @last_updated)
.map { |n|
"'#{n.name}': {fillKey: 'AAA', totalCases: 'n.total_cases'},\n"
}.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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
module ConvertProvNameToProvCode
def convert_name_to_province_code(province_name)
provinces = {
'Aceh' => 'ID.AC',
'Bali' => 'ID.BA',
'Banten' => 'ID.BT',
'Bengkulu' => 'ID.BE',
'DKI Jakarta' => 'ID.JK',
'Daerah Istimewa Yogyakarta' => 'ID.YO',
'Gorontalo' => 'ID.GO',
'Jambi' => 'ID.JA',
'Jawa Barat' => 'ID.JR',
'Jawa Tengah' => 'ID.JT',
'Jawa Timur' => 'ID.JI',
'Kalimantan Barat' => 'ID.KB',
'Kalimantan Selatan' => 'ID.KS',
'Kalimantan Tengah' => 'ID.KT',
'Kalimantan Timur' => 'ID.KI',
'Kalimantan Utara' => 'ID.KU',
'Kepulauan Bangka Belitung' => 'ID.BB',
'Kepulauan Riau' => 'ID.KR',
'Lampung' => 'ID.LA',
'Maluku' => 'ID.MA',
'Maluku Utara' => 'ID.MU',
'Nusa Tenggara Barat' => 'ID.NB',
'Nusa Tenggara Timur' => 'ID.NT',
'Papua' => 'ID.PA',
'Papua Barat' => 'ID.IB',
'Riau' => 'ID.RI',
'Sulawesi Barat' => 'ID.SR',
'Sulawesi Selatan' => 'ID.SE',
'Sulawesi Tengah' => 'ID.ST',
'Sulawesi Tenggara' => 'ID.SG',
'Sulawesi Utara' => 'ID.SW',
'Sumatera Barat' => 'ID.SB',
'Sumatera Selatan' => 'ID.SL',
'Sumatera Utara' => 'ID.SU'
}
provinces[province_name] if provinces.include? province_name
end
end
Oke, setelah jadi, tinggal di-include-kan ke data_peta_controller.rb.
1
2
3
4
5
6
7
class DataPetaController < ApplicationController
include ConvertProvNameToProvCode
def index
# ...
end
end
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module ConvertTotalCasesToCode
def convert_total_cases_to_code(total_cases)
case total_cases
when 200_000..300_000
'AAA'
when 150_000..200_000
'BBB'
when 90_000..150_000
'CCC'
when 70_000..90_000
'DDD'
when 50_000..70_000
'EEE'
when 30_000..50_000
'FFF'
when 10_000..30_000
'GGG'
when 100..10_000
'HHH'
end
end
end
Oke, setelah jadi, tinggal di-include-kan ke data_peta_controller.rb.
1
2
3
4
5
6
7
8
class DataPetaController < ApplicationController
include ConvertProvNameToProvCode
include ConvertTotalCasesToCode
def index
# ...
end
end
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
.
1
2
3
4
5
6
7
8
9
class DataPetaController < ApplicationController
include ConvertProvNameToProvCode
include ConvertTotalCasesToCode
include ActionView::Helpers::NumberHelper
def index
# ...
end
end
Selanjutnya tinggal kita gunakan pada object query yang sudah kita racik sebelumnya.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DataPetaController < ApplicationController
include ConvertProvNameToProvCode
include ConvertTotalCasesToCode
include ActionView::Helpers::NumberHelper
def index
@last_updated = Province.last.fetched_at
@cumulative_cases = Province.select(:name, :total_cases)
.where(fetched_at: @last_updated)
.map { |n|
"'#{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"
}.join
end
end
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.
-
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.
1
2
3
4
5
6
7
8
9
10
# Be sure to restart your server when you modify this file.
# ...
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
Rails.application.config.assets.precompile += %w(
d3.min.js topojson.min.js datamaps.idn.min.js
)
Tambahkan seperti pada baris 8, 9, 10.
Mantap!
Sekarang kita lanjut ke view template.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<div class="container px-0 pt-2 pb-5 mt-5" style="overflow-y: auto">
<%= javascript_include_tag 'd3.min' %>
<%= javascript_include_tag 'topojson.min' %>
<%= javascript_include_tag 'datamaps.idn.min' %>
<div id="container1" style="position: relative; width: 1100px; height: 400px; margin: 0 auto;"></div>
<script type="text/javascript">
//basic map config with custom fills, mercator projection
var map = new Datamap({
scope: 'idn',
element: document.getElementById('container1'),
setProjection: function (element) {
var projection = d3.geo.mercator()
.center([115, -5])
.rotate([0, 0])
.scale(3900 / 3)
var path = d3.geo.path()
.projection(projection);
return {path: path, projection: projection};
},
fills: {
defaultFill: '#dddddd',
'AAA': '#DB1836',
'BBB': '#F15A23',
'CCC': '#F89A1C',
'DDD': '#FFD500',
'EEE': '#C1D737',
'FFF': '#44B549',
'GGG': '#0EB049',
'HHH': '#016533',
},
data: {
<%= raw @cumulative_cases %>
},
geographyConfig: {
popupTemplate: function(geo, data) {
return ['<div class="hoverinfo"><strong>',
geo.properties.name + '</strong><br>Kasus (Kulumatif)',
': ' + data.totalCases,
'</div>'].join('');
}
}
});
</script>
</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
-
github.com/markmarkoh/datamaps
Diakses tanggal: 2021/02/07 -
http://datamaps.github.io/
Diakses tanggal: 2021/02/07 -
github.com/d3/d3
Diakses tanggal: 2021/02/07 -
api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
Diakses tanggal: 2021/02/07
Lisensi
Atribusi-NonKomersial-BerbagiSerupa 4.0 Internasional (CC BY-NC-SA 4.0)
Penulis
My journey kicks off from reading textbooks as a former Medical Student to digging bugs as a Software Engineer – a delightful rollercoaster of career twists. Embracing failure with the grace of a Cat avoiding water, I've seamlessly transitioned from Stethoscope to Keyboard. Armed with ability for learning and adapting faster than a Heart Beat, I'm on a mission to turn Code into a Product.
- Rizqi Nur Assyaufi