Semua angka sudah tervalidasi, dan kini tiba saatnya menyajikannya. Pendekatan yang kita pakai: pipeline mengekspor satu berkas data.json dari tabel analitik, lalu dashboard HTML statis membaca berkas tersebut dan menggambar diagram sendiri. Konsep penyajiannya mengikuti chapter Integrasi Business Intelligence: lapisan serving dipisahkan dari penyimpanan agar konsumen tidak perlu koneksi langsung ke basis data. Keputusan ini membuat dashboard bisa dibuka dari berkas lokal, ditautkan di email, atau diunggah ke hosting statis mana pun — tanpa DuckDB berjalan di belakangnya.

Menyusun Muatan Dashboard #

src/export_dashboard.py mengambil seluruh angka dari tabel mkt_* dan fct_pesanan. Kartu KPI dirangkum satu query:

python
def bangun_muatan(con, ringkasan_pembersihan) -> dict:
    """Susun seluruh muatan dashboard dari tabel analitik."""
    kpi = _baris(
        con,
        """
        SELECT
            (SELECT SUM(gmv) FROM mkt_gmv_harian) AS gmv_total,
            (SELECT SUM(jumlah_pesanan) FROM mkt_gmv_harian) AS jumlah_pesanan_selesai,
            (SELECT COUNT(*) FROM mkt_pelanggan) AS pelanggan_aktif,
            (SELECT ROUND(SUM(gmv) * 1.0 / SUM(jumlah_pesanan), 0)
               FROM mkt_gmv_harian) AS nilai_pesanan_rata_rata,
            (SELECT COUNT(DISTINCT pesanan_id) FROM fct_pesanan
              WHERE status = 'dibatalkan') AS pesanan_dibatalkan,
            (SELECT COUNT(DISTINCT pesanan_id) FROM fct_pesanan) AS pesanan_total,
            (SELECT jumlah_sesi FROM mkt_funnel WHERE tahap = 'view_product') AS sesi_view,
            (SELECT jumlah_sesi FROM mkt_funnel WHERE tahap = 'purchase') AS sesi_beli
        """,
    )[0]
    kpi["tingkat_pembatalan_persen"] = round(
        100.0 * kpi["pesanan_dibatalkan"] / kpi["pesanan_total"], 1
    )
    kpi["konversi_pembelian_persen"] = round(
        100.0 * kpi["sesi_beli"] / kpi["sesi_view"], 1
    )
    for kunci in ("pesanan_dibatalkan", "pesanan_total", "sesi_view", "sesi_beli"):
        kpi.pop(kunci)

Mari kita telaah bagian-bagian di atas (nomor baris merujuk src/export_dashboard.py):

  1. Baris 38-47 (subquery skalar): delapan angka KPI diambil dalam satu pernyataan SELECT; setiap subquery membaca mart yang tepat sesuai pertanyaan bisnis Bagian 1.
  2. Baris 50-55 (turunan): tingkat pembatalan dan konversi dihitung di Python dengan pembulatan satu desimal, mengikuti rumus turunan pada metric_definitions.yaml.
  3. Baris 56-57 (kpi.pop): kolom perantara dibuang agar JSON hanya memuat angka yang ditampilkan, bukan bahan bakunya.

Muatan lengkap berisi delapan kunci: periode, kpi, gmv_harian, kategori, funnel, top_pelanggan, top_produk, dan kualitas_data. Isi bagian periode dan kpi hasil pipeline:

json
{
  "periode": {
    "mulai": "2025-01-02",
    "akhir": "2025-01-15"
  },
  "kpi": {
    "gmv_total": 3926000,
    "jumlah_pesanan_selesai": 22,
    "pelanggan_aktif": 11,
    "nilai_pesanan_rata_rata": 178455.0,
    "tingkat_pembatalan_persen": 4.2,
    "konversi_pembelian_persen": 42.9
  }
}

Kunci funnel di dalam berkas yang sama memuat empat tahap perjalanan belanja: 42 sesi melihat produk, 32 masuk keranjang, 20 mulai checkout, dan 18 berujung pembelian — konversi 42,9 persen dari tahap pertama.

Muatan sengaja tidak memuat waktu eksekusi — itulah yang membuat ekspor deterministik dan uji idempotensi pada Bagian 5 bisa membandingkan byte per byte.

Dashboard Statis Tanpa Pustaka Eksternal #

Dashboard adalah satu berkas HTML dengan CSS dan JavaScript murni. Pada situs tutorial berkas ini disajikan langsung dari folder static/proyek/dashboard-analitik/index.html dan dapat dibuka pada alamat /proyek/dashboard-analitik/; untuk dijalankan lokal, unduh berkas tersebut dan letakkan di samping data.json hasil pipeline. Grafik garis GMV harian digambar sebagai SVG yang dirakit manual:

javascript
function grafikGaris(titikData) {
  var lebar = 760, tinggi = 240, kiri = 70, kanan = 20, atas = 14, bawah = 34;
  var w = lebar - kiri - kanan, h = tinggi - atas - bawah;
  var nilai = titikData.map(function (d) { return d.gmv; });
  var maks = Math.max.apply(null, nilai);
  var langkahY = Math.ceil(maks / 4 / 50000) * 50000;
  var skalaY = function (v) { return atas + h - (v / (langkahY * 4)) * h; };
  var skalaX = function (i) { return kiri + (i / (titikData.length - 1)) * w; };

  var svg = '<svg viewBox="0 0 ' + lebar + " " + tinggi +
            '" role="img" aria-label="Grafik garis GMV harian">';

Logika intinya: skalaY memetakan rupiah ke koordinat piksel dan skalaX memetakan urutan tanggal; langkahY membulatkan skala sumbu-y ke kelipatan Rp50.000 agar garis kisiarnya rapi. Titik data menjadi elemen circle dengan title berisi nilai rupiah, sehingga angka tetap terbaca saat kursor melayang.

Pemuatan datanya satu rantai fetch:

javascript
fetch("data.json")
  .then(function (r) { if (!r.ok) { throw new Error(r.status); } return r.json(); })
  .then(tampil)
  .catch(gagal);

Berkas diambil relatif terhadap lokasi index.html, bukan dari domain lain, sehingga tidak ada permintaan jaringan eksternal. Bila fetch gagal — misalnya halaman dibuka langsung lewat skema file:// yang membatasi pembacaan berkas lokal — fungsi gagal menampilkan petunjuk menjalankan server lokal alih-alih diam.

Membuka Dashboard #

Karena penjelajah membatasi fetch pada skema file://, cara termudah memakai server lokal bawaan Python. Unduh index.html dari /proyek/dashboard-analitik/ pada situs tutorial, letakkan di folder data/dashboard/ di samping data.json hasil ekspor, lalu:

bash
cd data/dashboard
python3 -m http.server 8000

Lalu buka http://localhost:8000/ di penjelajah. Dashboard ini diverifikasi pada penjelajah headless lewat server lokal; hasil pemeriksaannya:

text
periode       : Periode data: 2025-01-02 s.d. 2025-01-15 - sumber: data.json dari pipeline DuckDB
kartu KPI     : 6
titik grafik  : 14 (lingkaran SVG GMV harian)
batang        : 8 (4 kategori + 4 funnel)
baris tabel   : 10 (5 produk + 5 pelanggan)
galat konsol  : tidak ada

Nilai kartu KPI yang dirender:
GMV Total = Rp 3.926.000 | Pesanan Selesai = 22 | Pelanggan Aktif = 11 |
Rata-rata per Pesanan = Rp 178.455 | Tingkat Pembatalan = 4.2% | Konversi Pembelian = 42.9%

Skenario file:// (dibuka langsung tanpa server):
pesan fallback tampil: "Gagal memuat data.json ... jalankan python3 -m http.server 8000"

Enam kartu KPI, 14 titik grafik, delapan batang, dan dua tabel ter-render dari data.json hasil pipeline — tidak ada satu angka pun yang diketik manual pada HTML.

Jalur ke Alat BI #

Pola data.json cocok untuk dashboard statis, tetapi tim analisis biasanya memakai alat BI interaktif seperti Metabase, Superset, atau Looker Studio. Ketiganya tidak membaca data.json; masing-masing menghubungi sumber data langsung:

  • Metabase dan Superset terhubung ke basis data SQL. Untuk skala proyek ini, DuckDB bisa dipasang sebagai sumber data (Superset mendukungnya melalui dialek duckdb), sedangkan untuk tim yang lebih besar, tabel marts dimuat ke PostgreSQL dan alat BI membaca dari sana — pola serving yang sama, hanya perantara beda.
  • Looker Studio tidak memiliki konektor DuckDB resmi, sehingga jalurnya lewat database yang didukung (PostgreSQL, BigQuery) atau unggah berkas CSV — di sinilah lapisan data/gold/ dari Bagian 6 terpakai: keempat CSV mart sudah berbentuk siap unggah.
  • DuckDB sendiri juga menyediakan ekstensi httpserver untuk mengekspos query sebagai API lokal saat kita butuh konsumsi terprogram tanpa alat BI; dokumentasinya ada di https://duckdb.org/docs/ .

Inti ketiganya sama dengan pola proyek ini: alat presentasi tidak pernah mengubah angka, alat presentasi hanya membaca lapisan serving yang sudah tervalidasi.

Dengan dashboard hidup dan jalur BI terpetakan, satu pekerjaan terakhir tersisa: mendokumentasikan seluruh sistem agar orang lain bisa menjalankan dan memperluasnya. Bagian 9 menutup proyek — sekaligus menutup section proyek course ini.