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: {...},

FILEapp/views/data_peta/index.html.erb
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.

FILEapp/controllers/data_peta_controller.rb
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.

FILEapp/controllers/concerns/convert_name_to_province_code.rb
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.

FILEapp/controllers/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)

FILEapp/controllers/concerns/convert_total_cases_to_code.rb
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.

FILEapp/controllers/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.

FILEapp/controllers/data_peta_controller.rb
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.

FILEapp/controllers/data_peta_controller.rb
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.

  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.

FILEconfig/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.

FILEapp/views/data_peta/index.html.erb
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

  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


Penulis

bandithijo

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

944e8edeccab170ecee65673676b75514b2f62ed