Advanced Data Visualization & Text Animation dengan GSAP: Membuat Dashboard Interaktif yang Menceritakan Data

Data visualization bukan hanya tentang menampilkan angka—ini tentang menceritakan kisah melalui animasi yang bermakna. Dalam tutorial ini, kita akan belajar cara membuat dashboard interaktif dengan GSAP yang menggabungkan:

  1. Animated Counter (Angka yang "bertumbuh" secara smooth).
  2. Morphing SVG Charts (Grafik yang berubah bentuk/warna saat scroll).
  3. Text Reveal Animation (Teks muncul huruf demi huruf).
  4. Progress Bars dengan Wave Effect (Progress bar bergelombang).
  5. Interactive Data Cards (Kartu yang responsif terhadap mouse).

Kami akan membuat dashboard "Analytics Hub" fiktif yang memvisualisasikan metrik performa produk.


Kebutuhan

  • Pemahaman dasar HTML, CSS, Tailwind.
  • GSAP versi 3.12+.
  • Editor kode (VS Code).

Langkah 1: Setup & Animated Counter

Animasi Counter

Counter yang beranimasi adalah cara sempurna untuk menarik perhatian pengguna pada angka penting.

Konsep:

Kita menggunakan gsap.to() dengan callback function untuk mengupdate teks counter setiap frame. GSAP akan menghitung dari nilai awal ke nilai akhir secara smooth.

file: hero.html

<!DOCTYPE html>
<html lang="id">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Langkah 1: Hero & Animated Counter</title>

    <!-- 1. External Libraries -->
    <script src="<https://cdn.tailwindcss.com>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js>"></script>

    <style>
      @import url("<https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap>");
      body {
        font-family: "Inter", sans-serif;
      }

      /* Custom Grid Pattern */
      .bg-grid {
        background-size: 40px 40px;
        background-image: linear-gradient(
            to right,
            rgba(255, 255, 255, 0.05) 1px,
            transparent 1px
          ),
          linear-gradient(
            to bottom,
            rgba(255, 255, 255, 0.05) 1px,
            transparent 1px
          );
      }

      /* Prevent FOUC (Flash of Unstyled Content) */
      .hero-anim,
      .card-anim {
        visibility: hidden;
      }
    </style>
  </head>
  <body
    class="bg-[#020617] text-white overflow-x-hidden selection:bg-indigo-500/30"
  >
    <!-- HERO SECTION -->
    <section
      class="relative min-h-screen flex flex-col items-center justify-center py-20"
    >
      <!-- Background Elements -->
      <div class="absolute inset-0 bg-[#020617] -z-10 overflow-hidden">
        <div class="absolute inset-0 bg-grid opacity-[0.4]"></div>
        <div
          class="absolute inset-0 bg-gradient-to-t from-[#020617] via-transparent to-transparent"
        ></div>
        <!-- Animated Blobs -->
        <div
          class="absolute top-0 -left-40 w-96 h-96 bg-indigo-500/30 rounded-full mix-blend-screen filter blur-[128px] opacity-50 animate-pulse"
        ></div>
        <div
          class="absolute bottom-0 -right-40 w-96 h-96 bg-cyan-500/30 rounded-full mix-blend-screen filter blur-[128px] opacity-50 animate-pulse"
          style="animation-delay: 2s"
        ></div>
      </div>

      <!-- Content -->
      <div class="relative z-10 container mx-auto px-6 text-center">
        <!-- Badge -->
        <div
          class="hero-anim inline-flex items-center gap-2 px-3 py-1 rounded-full border border-indigo-500/30 bg-indigo-500/10 text-indigo-300 text-xs font-medium mb-8 backdrop-blur-md"
        >
          <span class="relative flex h-2 w-2">
            <span
              class="animate-ping absolute inline-flex h-full w-full rounded-full bg-indigo-400 opacity-75"
            ></span>
            <span
              class="relative inline-flex rounded-full h-2 w-2 bg-indigo-500"
            ></span>
          </span>
          Live Data Dashboard
        </div>

        <!-- Main Title -->
        <h1
          class="hero-anim text-5xl md:text-7xl lg:text-8xl font-black tracking-tight mb-6 leading-tight"
        >
          Analytics
          <span
            class="text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 via-cyan-400 to-emerald-400"
            >Hub</span
          >
        </h1>

        <p
          class="hero-anim text-lg md:text-xl text-slate-400 mb-12 max-w-2xl mx-auto leading-relaxed"
        >
          Platform visualisasi data real-time yang mengubah angka kompleks
          menjadi cerita yang dapat ditindaklanjuti.
        </p>

        <!-- Counter Cards Grid -->
        <div
          class="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto mt-16 w-full"
        >
          <!-- Card 1 -->
          <div
            class="card-anim group relative p-8 rounded-3xl border border-white/5 bg-white/[0.02] hover:bg-white/[0.05] backdrop-blur-xl transition-all duration-500 hover:-translate-y-2 hover:shadow-[0_20px_40px_-15px_rgba(99,102,241,0.3)] text-left"
          >
            <div
              class="absolute inset-0 rounded-3xl border border-indigo-500/0 group-hover:border-indigo-500/50 transition-colors duration-500"
            ></div>
            <div class="relative z-10">
              <div
                class="w-12 h-12 mb-4 rounded-2xl bg-indigo-500/20 flex items-center justify-center text-indigo-400"
              >
                <svg
                  class="w-6 h-6"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="2"
                    d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
                  ></path>
                </svg>
              </div>
              <p class="text-slate-400 text-sm font-medium mb-1">Total Users</p>
              <div
                class="text-4xl font-bold text-white counter"
                data-target="125000"
              >
                0
              </div>
              <div
                class="flex items-center gap-1 mt-3 text-emerald-400 text-xs font-medium bg-emerald-400/10 w-fit px-2 py-1 rounded-lg"
              >
                <span>+12% growth</span>
              </div>
            </div>
          </div>

          <!-- Card 2 -->
          <div
            class="card-anim group relative p-8 rounded-3xl border border-white/5 bg-white/[0.02] hover:bg-white/[0.05] backdrop-blur-xl transition-all duration-500 hover:-translate-y-2 hover:shadow-[0_20px_40px_-15px_rgba(6,182,212,0.3)] text-left"
          >
            <div
              class="absolute inset-0 rounded-3xl border border-cyan-500/0 group-hover:border-cyan-500/50 transition-colors duration-500"
            ></div>
            <div class="relative z-10">
              <div
                class="w-12 h-12 mb-4 rounded-2xl bg-cyan-500/20 flex items-center justify-center text-cyan-400"
              >
                <svg
                  class="w-6 h-6"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="2"
                    d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
                  ></path>
                </svg>
              </div>
              <p class="text-slate-400 text-sm font-medium mb-1">
                Total Revenue
              </p>
              <div class="flex items-baseline gap-2">
                <span class="text-2xl text-cyan-400 font-semibold">$</span>
                <div
                  class="text-4xl font-bold text-white counter"
                  data-target="2500000"
                >
                  0
                </div>
              </div>
              <div
                class="flex items-center gap-1 mt-3 text-emerald-400 text-xs font-medium bg-emerald-400/10 w-fit px-2 py-1 rounded-lg"
              >
                <span>+28% vs last Q</span>
              </div>
            </div>
          </div>
<!-- Card 3 -->
          <div
            class="card-anim group relative p-8 rounded-3xl border border-white/5 bg-white/[0.02] hover:bg-white/[0.05] backdrop-blur-xl transition-all duration-500 hover:-translate-y-2 hover:shadow-[0_20px_40px_-15px_rgba(16,185,129,0.3)] text-left"
          >
            <div
              class="absolute inset-0 rounded-3xl border border-emerald-500/0 group-hover:border-emerald-500/50 transition-colors duration-500"
            ></div>
            <div class="relative z-10">
              <div
                class="w-12 h-12 mb-4 rounded-2xl bg-emerald-500/20 flex items-center justify-center text-emerald-400"
              >
                <svg
                  class="w-6 h-6"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="2"
                    d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
                  ></path>
                </svg>
              </div>
              <p class="text-slate-400 text-sm font-medium mb-1">
                Conversion Rate
              </p>
              <div class="flex items-baseline gap-1">
                <div
                  class="text-4xl font-bold text-white counter"
                  data-target="8.5"
                >
                  0
                </div>
                <span class="text-2xl text-emerald-400 font-semibold">%</span>
              </div>
              <div
                class="flex items-center gap-1 mt-3 text-indigo-400 text-xs font-medium bg-indigo-400/10 w-fit px-2 py-1 rounded-lg"
              >
                <span>92% target hit</span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>

    <script>
      gsap.registerPlugin(ScrollTrigger);

      // 1. ENTRANCE ANIMATION (Muncul dari bawah)
      const tl = gsap.timeline();

      tl.to(".hero-anim", {
        autoAlpha: 1,
        y: 0,
        duration: 1,
        stagger: 0.2,
        ease: "power3.out",
        startAt: { y: 30, autoAlpha: 0 },
      }).to(
        ".card-anim",
        {
          autoAlpha: 1,
          y: 0,
          duration: 1,
          stagger: 0.2,
          ease: "back.out(1.7)",
          startAt: { y: 50, autoAlpha: 0 },
          onComplete: startCounters, // Panggil counter setelah kartu muncul
        },
        "-=0.5"
      );

      // 2. COUNTER LOGIC
      function startCounters() {
        const counters = document.querySelectorAll(".counter");
        counters.forEach((counter) => {
          const target = parseFloat(counter.getAttribute("data-target"));
          const isDecimal = target % 1 !== 0;

          gsap.to(counter, {
            textContent: target,
            duration: 2.5,
            ease: "power2.out",
            snap: { textContent: isDecimal ? 0.1 : 1 },
            onUpdate: function () {
              let value = parseFloat(this.targets()[0].textContent);
              counter.textContent = isDecimal
                ? value.toFixed(1)
                : Math.floor(value).toLocaleString("en-US");
            },
          });
        });
      }
    </script>
  </body>
</html>

Penjelasan Kode:

  1. Struktur: Memuat library eksternal (Tailwind, GSAP, ScrollTrigger).
  2. Styling: Menambahkan bg-grid untuk tekstur background dan mencegah FOUC (Flash of Unstyled Content) menggunakan CSS visibility: hidden.
  3. Logika:
    • Entrance Animation: Menggunakan gsap.timeline() untuk memunculkan elemen secara berurutan dari bawah ke atas.
    • Counter Logic: Fungsi startCounters dipanggil tepat setelah animasi masuk selesai (onComplete), memastikan angka mulai bergerak hanya ketika pengguna sudah melihat kartunya.

Langkah 2: Progress Bar dengan Wave Effect

Animasi Progress Bar

Progress bar bukan hanya garis horizontal kita akan membuat efek bergelombang.

File 2: progress.html

<!DOCTYPE html>
<html lang="id">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Langkah 2: Progress Bar Wave Optimized</title>

    <script src="<https://cdn.tailwindcss.com>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js>"></script>

    <style>
      @import url("<https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap>");
      body {
        font-family: "Inter", sans-serif;
      }

      @keyframes wave-slide {
        0% {
          transform: translateX(0);
        }
        100% {
          transform: translateX(-50%);
        }
      }

      .animate-wave {
        width: 200%;
        animation: wave-slide 2s linear infinite;
        will-change: transform; 
      }
    </style>
  </head>
  <body
    class="bg-[#020617] text-white overflow-x-hidden selection:bg-indigo-500/30"
  >
    <!-- PROGRESS BAR SECTION -->
    <section
      class="relative min-h-screen flex items-center justify-center py-20 px-6"
    >
      <!-- Background Glow -->
      <div
        class="absolute right-0 top-1/4 w-96 h-96 bg-indigo-600/10 rounded-full blur-[100px] pointer-events-none"
      ></div>

      <div class="relative z-10 container mx-auto max-w-4xl">
        <!-- Header -->
        <div class="mb-16 text-center md:text-left">
          <h2 class="text-4xl md:text-5xl font-black mb-4">
            Product <span class="text-indigo-400">Metrics</span>
          </h2>
          <p class="text-slate-400 text-lg max-w-2xl mx-auto md:mx-0">
            Analisis mendalam terhadap performa sistem secara real-time.
          </p>
        </div>

        <div
          class="space-y-12 bg-white/[0.02] border border-white/5 p-8 md:p-12 rounded-3xl backdrop-blur-sm"
        >
          <div class="progress-item group">
            <div class="flex justify-between items-end mb-3">
              <div>
                <h3 class="text-white font-bold text-lg">Performance Score</h3>
                <p class="text-slate-500 text-sm">Sistem berjalan optimal</p>
              </div>
              <div class="text-right">
                <span
                  class="text-3xl font-black text-white progress-value"
                  data-target="92"
                  >0</span
                >
                <span class="text-indigo-400 text-lg font-bold">%</span>
              </div>
            </div>

            <div
              class="h-4 bg-slate-800/50 rounded-full overflow-hidden border border-white/5 relative"
            >
              <div
                class="progress-bar absolute top-0 left-0 h-full bg-gradient-to-r from-indigo-900 to-indigo-500 rounded-full w-0 shadow-[0_0_20px_rgba(99,102,241,0.5)]"
              >
                <div
                  class="absolute inset-0 opacity-30 h-full animate-wave flex"
                >
                  <svg
                    class="w-1/2 h-full"
                    viewBox="0 0 100 10"
                    preserveAspectRatio="none"
                  >
                    <path d="M0 10 V5 Q25 0 50 5 T100 5 V10 Z" fill="white" />
                  </svg>
                  <svg
                    class="w-1/2 h-full"
                    viewBox="0 0 100 10"
                    preserveAspectRatio="none"
                  >
                    <path d="M0 10 V5 Q25 0 50 5 T100 5 V10 Z" fill="white" />
                  </svg>
                </div>

                <div
                  class="absolute right-0 top-0 bottom-0 w-1 bg-white/50 blur-[2px]"
                ></div>
              </div>
            </div>
          </div>

          <div class="progress-item group">
            <div class="flex justify-between items-end mb-3">
              <div>
                <h3 class="text-white font-bold text-lg">User Satisfaction</h3>
                <p class="text-slate-500 text-sm">Berdasarkan NPS survey</p>
              </div>
              <div class="text-right">
                <span
                  class="text-3xl font-black text-white progress-value"
                  data-target="87"
                  >0</span
                >
                <span class="text-cyan-400 text-lg font-bold">/100</span>
              </div>
            </div>

            <div
              class="h-4 bg-slate-800/50 rounded-full overflow-hidden border border-white/5 relative"
            >
              <div
                class="progress-bar absolute top-0 left-0 h-full bg-gradient-to-r from-cyan-900 to-cyan-500 rounded-full w-0 shadow-[0_0_20px_rgba(6,182,212,0.5)]"
              >
                <div
                  class="absolute inset-0 opacity-30 h-full animate-wave flex"
                >
                  <svg
                    class="w-1/2 h-full"
                    viewBox="0 0 100 10"
                    preserveAspectRatio="none"
                  >
                    <path d="M0 10 V5 Q25 0 50 5 T100 5 V10 Z" fill="white" />
                  </svg>
                  <svg
                    class="w-1/2 h-full"
                    viewBox="0 0 100 10"
                    preserveAspectRatio="none"
                  >
                    <path d="M0 10 V5 Q25 0 50 5 T100 5 V10 Z" fill="white" />
                  </svg>
                </div>
                <div
                  class="absolute right-0 top-0 bottom-0 w-1 bg-white/50 blur-[2px]"
                ></div>
              </div>
            </div>
          </div>

          <div class="progress-item group">
            <div class="flex justify-between items-end mb-3">
              <div>
                <h3 class="text-white font-bold text-lg">System Uptime</h3>
                <p class="text-slate-500 text-sm">30 hari terakhir</p>
              </div>
              <div class="text-right">
                <span
                  class="text-3xl font-black text-white progress-value"
                  data-target="99.9"
                  >0</span
                >
                <span class="text-emerald-400 text-lg font-bold">%</span>
              </div>
            </div>

            <div
              class="h-4 bg-slate-800/50 rounded-full overflow-hidden border border-white/5 relative"
            >
              <div
                class="progress-bar absolute top-0 left-0 h-full bg-gradient-to-r from-emerald-900 to-emerald-500 rounded-full w-0 shadow-[0_0_20px_rgba(16,185,129,0.5)]"
              >
                <div
                  class="absolute inset-0 opacity-30 h-full animate-wave flex"
                >
                  <svg
                    class="w-1/2 h-full"
                    viewBox="0 0 100 10"
                    preserveAspectRatio="none"
                  >
                    <path d="M0 10 V5 Q25 0 50 5 T100 5 V10 Z" fill="white" />
                  </svg>
                  <svg
                    class="w-1/2 h-full"
                    viewBox="0 0 100 10"
                    preserveAspectRatio="none"
                  >
                    <path d="M0 10 V5 Q25 0 50 5 T100 5 V10 Z" fill="white" />
                  </svg>
                </div>
                <div
                  class="absolute right-0 top-0 bottom-0 w-1 bg-white/50 blur-[2px]"
                ></div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>

    <script>
      gsap.registerPlugin(ScrollTrigger);

      const progressItems = document.querySelectorAll(".progress-item");

      progressItems.forEach((item) => {
        const bar = item.querySelector(".progress-bar");
        const valueText = item.querySelector(".progress-value");
        const targetValue = parseFloat(valueText.getAttribute("data-target"));
        const isDecimal = targetValue % 1 !== 0;

        ScrollTrigger.create({
          trigger: item,
          start: "top 85%",
          onEnter: () => {
            // 1. Animasi Lebar Bar
            gsap.to(bar, {
              width: `${targetValue}%`,
              duration: 2,
              ease: "power3.inOut",
            });

            gsap.to(valueText, {
              textContent: targetValue,
              duration: 2,
              ease: "power3.inOut",
              snap: { textContent: isDecimal ? 0.1 : 1 },
              onUpdate: function () {
                let val = parseFloat(this.targets()[0].textContent);
                valueText.textContent = isDecimal
                  ? val.toFixed(1)
                  : Math.floor(val);
              },
            });
          },
          once: true,
        });
      });
    </script>
  </body>
</html>

Penjelasan:

Berfokus pada visualisasi persentase dengan cara yang artistik.

  1. Struktur: Menggunakan layout yang konsisten dengan Langkah 1 (background gelap, tipografi Inter).
  2. Visual Effect (Wave):
    • Kita menyisipkan elemen SVG (<path>) di dalam bar untuk membuat bentuk gelombang.
    • CSS @keyframes wave-move menggeser gelombang tersebut secara infinite ke kiri, menciptakan ilusi cairan yang mengalir.
  3. Logika GSAP (ScrollTrigger):
    • Animasi hanya berjalan ketika elemen masuk viewport (start: "top 85%").
    • Lebar bar (width) dan angka persentase (textContent) dianimasikan secara bersamaan menggunakan easing power3.inOut agar gerakannya halus (lambat-cepat-lambat).

Langkah 3: SVG Chart dengan Morphing Effect

Animasi Chart

Grafik yang berubah bentuk saat scroll adalah cara powerful untuk menunjukkan perubahan data.

File: morphing.html

<!DOCTYPE html>
<html lang="id">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Langkah 3: Morphing SVG Chart</title>

    <script src="<https://cdn.tailwindcss.com>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js>"></script>

    <style>
      @import url("<https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap>");
      body {
        font-family: "Inter", sans-serif;
      }

      /* Custom utility agar chart tidak terpotong */
      .chart-overflow-visible {
        overflow: visible;
      }
    </style>
  </head>
  <body
    class="bg-[#020617] text-white overflow-x-hidden selection:bg-indigo-500/30"
  >
    <!-- CHART SECTION -->
    <section
      class="relative min-h-screen flex items-center justify-center py-20 px-6 overflow-hidden"
    >
      <!-- Background Accents -->
      <div
        class="absolute left-0 bottom-0 w-full h-1/2 bg-gradient-to-t from-indigo-900/10 to-transparent pointer-events-none"
      ></div>

      <div class="container mx-auto max-w-6xl relative z-10">
        <div class="mb-16 text-center md:text-left">
          <h2 class="text-4xl md:text-5xl font-black mb-4 text-white">
            Growth <span class="text-cyan-400">Trends</span>
          </h2>
          <p class="text-slate-400 text-lg">
            Visualisasi pergerakan data historis dan proyeksi masa depan.
          </p>
        </div>

        <div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
          <!-- CHART 1: Revenue Trend (Area Chart) -->
          <div
            class="bg-slate-900/40 border border-white/5 rounded-3xl p-8 backdrop-blur-xl relative group pl-16 pb-12"
          >
            <!-- Y-Axis Title -->
            <div
              class="absolute left-4 top-1/2 -translate-y-1/2 -rotate-90 text-xs font-bold tracking-widest text-slate-500 uppercase whitespace-nowrap origin-center"
            >
              Revenue (USD)
            </div>

            <!-- Header -->
            <div class="flex justify-between items-start mb-6">
              <div>
                <h3 class="text-xl font-bold text-white">Monthly Revenue</h3>
                <p class="text-sm text-slate-500">6 Bulan Terakhir</p>
              </div>
              <div class="p-2 bg-indigo-500/10 rounded-lg text-indigo-400">
                <svg
                  class="w-6 h-6"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="2"
                    d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"
                  ></path>
                </svg>
              </div>
            </div>

            <!-- SVG Container -->
            <div class="relative h-64 w-full chart-container-line">
              <!-- Grid Lines -->
              <svg
                class="absolute inset-0 w-full h-full text-slate-800"
                preserveAspectRatio="none"
              >
                <line
                  x1="0"
                  y1="25%"
                  x2="100%"
                  y2="25%"
                  stroke="currentColor"
                  stroke-width="1"
                  stroke-dasharray="4 4"
                />
                <line
                  x1="0"
                  y1="50%"
                  x2="100%"
                  y2="50%"
                  stroke="currentColor"
                  stroke-width="1"
                  stroke-dasharray="4 4"
                />
                <line
                  x1="0"
                  y1="75%"
                  x2="100%"
                  y2="75%"
                  stroke="currentColor"
                  stroke-width="1"
                  stroke-dasharray="4 4"
                />
              </svg>

              <!-- Main Chart SVG -->
              <svg
                class="w-full h-full chart-overflow-visible"
                viewBox="0 0 500 250"
                preserveAspectRatio="none"
              >
                <defs>
                  <linearGradient
                    id="gradLine"
                    x1="0%"
                    y1="0%"
                    x2="0%"
                    y2="100%"
                  >
                    <stop
                      offset="0%"
                      style="stop-color: #6366f1; stop-opacity: 0.5"
                    />
                    <stop
                      offset="100%"
                      style="stop-color: #6366f1; stop-opacity: 0"
                    />
                  </linearGradient>
                </defs>

                <!-- Area Path (Filled) -->
                <!-- d: bentuk awal (datar di bawah) -->
                <!-- data-d-to: bentuk akhir (kurva data) -->
                <path
                  d="M0,250 L0,250 L83,250 L166,250 L250,250 L333,250 L416,250 L500,250 Z"
                  fill="url(#gradLine)"
                  class="morphing-area"
                  data-d-to="M0,250 L0,150 L83,180 L166,120 L250,160 L333,90 L416,110 L500,40 L500,250 Z"
                />

                <!-- Stroke Path (Line only) -->
                <path
                  d="M0,250 L83,250 L166,250 L250,250 L333,250 L416,250 L500,250"
                  fill="none"
                  stroke="#818cf8"
                  stroke-width="4"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  class="morphing-line"
                  data-d-to="M0,150 L83,180 L166,120 L250,160 L333,90 L416,110 L500,40"
                />

                <!-- Data Points -->
                <g class="data-points">
                  <circle
                    cx="0"
                    cy="150"
                    r="6"
                    fill="#fff"
                    stroke="#6366f1"
                    stroke-width="3"
                  />
                  <circle
                    cx="83"
                    cy="180"
                    r="6"
                    fill="#fff"
                    stroke="#6366f1"
                    stroke-width="3"
                  />
                  <circle
                    cx="166"
                    cy="120"
                    r="6"
                    fill="#fff"
                    stroke="#6366f1"
                    stroke-width="3"
                  />
                  <circle
                    cx="250"
                    cy="160"
                    r="6"
                    fill="#fff"
                    stroke="#6366f1"
                    stroke-width="3"
                  />
                  <circle
                    cx="333"
                    cy="90"
                    r="6"
                    fill="#fff"
                    stroke="#6366f1"
                    stroke-width="3"
                  />
                  <circle
                    cx="416"
                    cy="110"
                    r="6"
                    fill="#fff"
                    stroke="#6366f1"
                    stroke-width="3"
                  />
                  <circle
                    cx="500"
                    cy="40"
                    r="6"
                    fill="#fff"
                    stroke="#6366f1"
                    stroke-width="3"
                  />
                </g>
              </svg>
            </div>

            <!-- X-Axis Title -->
            <div
              class="absolute bottom-4 left-1/2 -translate-x-1/2 text-xs font-bold tracking-widest text-slate-500 uppercase mt-4"
            >
              Timeline (Months)
            </div>
          </div>

          <!-- CHART 2: User Engagement (Bar Chart) -->
          <div
            class="bg-slate-900/40 border border-white/5 rounded-3xl p-8 backdrop-blur-xl relative group pl-16 pb-12"
          >
            <!-- Y-Axis Title -->
            <div
              class="absolute left-4 top-1/2 -translate-y-1/2 -rotate-90 text-xs font-bold tracking-widest text-slate-500 uppercase whitespace-nowrap origin-center"
            >
              Active Users
            </div>

            <!-- Header -->
            <div class="flex justify-between items-start mb-6">
              <div>
                <h3 class="text-xl font-bold text-white">User Engagement</h3>
                <p class="text-sm text-slate-500">Aktivitas Harian</p>
              </div>
              <div class="p-2 bg-cyan-500/10 rounded-lg text-cyan-400">
                <svg
                  class="w-6 h-6"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="2"
                    d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
                  ></path>
                </svg>
              </div>
            </div>

<!-- SVG Container -->
            <div
              class="relative h-64 w-full chart-container-bar flex items-end justify-between px-2 gap-2 md:gap-4"
            >
              <!-- Bar Items -->
              <div
                class="bar-item w-full bg-cyan-500/20 rounded-t-lg relative group/bar h-0"
                data-height="40%"
              >
                <div
                  class="absolute bottom-0 w-full bg-cyan-500 rounded-t-lg transition-all duration-300 group-hover/bar:bg-cyan-400 h-full"
                ></div>
                <div
                  class="absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-cyan-400 text-xs font-bold px-2 py-1 rounded opacity-0 group-hover/bar:opacity-100 transition-opacity z-20 pointer-events-none"
                >
                  Mon
                </div>
              </div>
              <div
                class="bar-item w-full bg-cyan-500/20 rounded-t-lg relative group/bar h-0"
                data-height="70%"
              >
                <div
                  class="absolute bottom-0 w-full bg-cyan-500 rounded-t-lg transition-all duration-300 group-hover/bar:bg-cyan-400 h-full"
                ></div>
                <div
                  class="absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-cyan-400 text-xs font-bold px-2 py-1 rounded opacity-0 group-hover/bar:opacity-100 transition-opacity z-20 pointer-events-none"
                >
                  Tue
                </div>
              </div>
              <div
                class="bar-item w-full bg-cyan-500/20 rounded-t-lg relative group/bar h-0"
                data-height="55%"
              >
                <div
                  class="absolute bottom-0 w-full bg-cyan-500 rounded-t-lg transition-all duration-300 group-hover/bar:bg-cyan-400 h-full"
                ></div>
                <div
                  class="absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-cyan-400 text-xs font-bold px-2 py-1 rounded opacity-0 group-hover/bar:opacity-100 transition-opacity z-20 pointer-events-none"
                >
                  Wed
                </div>
              </div>
              <div
                class="bar-item w-full bg-cyan-500/20 rounded-t-lg relative group/bar h-0"
                data-height="90%"
              >
                <div
                  class="absolute bottom-0 w-full bg-cyan-500 rounded-t-lg transition-all duration-300 group-hover/bar:bg-cyan-400 h-full shadow-[0_0_20px_rgba(6,182,212,0.5)]"
                ></div>
                <div
                  class="absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-cyan-400 text-xs font-bold px-2 py-1 rounded opacity-0 group-hover/bar:opacity-100 transition-opacity z-20 pointer-events-none"
                >
                  Thu
                </div>
              </div>
              <div
                class="bar-item w-full bg-cyan-500/20 rounded-t-lg relative group/bar h-0"
                data-height="65%"
              >
                <div
                  class="absolute bottom-0 w-full bg-cyan-500 rounded-t-lg transition-all duration-300 group-hover/bar:bg-cyan-400 h-full"
                ></div>
                <div
                  class="absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-cyan-400 text-xs font-bold px-2 py-1 rounded opacity-0 group-hover/bar:opacity-100 transition-opacity z-20 pointer-events-none"
                >
                  Fri
                </div>
              </div>
              <div
                class="bar-item w-full bg-cyan-500/20 rounded-t-lg relative group/bar h-0"
                data-height="80%"
              >
                <div
                  class="absolute bottom-0 w-full bg-cyan-500 rounded-t-lg transition-all duration-300 group-hover/bar:bg-cyan-400 h-full"
                ></div>
                <div
                  class="absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-cyan-400 text-xs font-bold px-2 py-1 rounded opacity-0 group-hover/bar:opacity-100 transition-opacity z-20 pointer-events-none"
                >
                  Sat
                </div>
              </div>
            </div>

            <!-- X-Axis Title -->
            <div
              class="absolute bottom-4 left-1/2 -translate-x-1/2 text-xs font-bold tracking-widest text-slate-500 uppercase mt-4"
            >
              Day of Week
            </div>
          </div>
        </div>
      </div>
    </section>

    <script>
      gsap.registerPlugin(ScrollTrigger);

      // --- LOGIC MORPHING CHART ---

      // 1. LINE CHART ANIMATION
      const chartContainerLine = document.querySelector(
        ".chart-container-line"
      );
      const morphingArea = document.querySelector(".morphing-area");
      const morphingLine = document.querySelector(".morphing-line");
      const dataPoints = document.querySelectorAll(".data-points circle");

      // Set state awal: Data Points tidak terlihat & posisi acak di bawah
      gsap.set(dataPoints, { autoAlpha: 0, y: 50 });

      ScrollTrigger.create({
        trigger: chartContainerLine,
        start: "top 80%",
        onEnter: () => {
          // Animasi Area Fill (Dari datar ke bergelombang)
          gsap.to(morphingArea, {
            attr: { d: morphingArea.getAttribute("data-d-to") },
            duration: 2,
            ease: "power3.out",
          });

          // Animasi Garis (Line)
          gsap.to(morphingLine, {
            attr: { d: morphingLine.getAttribute("data-d-to") },
            duration: 2,
            ease: "power3.out",
          });

          // Animasi Titik Data (Muncul satu per satu)
          gsap.to(dataPoints, {
            autoAlpha: 1,
            y: 0,
            duration: 0.5,
            stagger: 0.1,
            ease: "back.out(2)",
            delay: 0.5,
          });
        },
        once: true,
      });

      // 2. BAR CHART ANIMATION
      const chartContainerBar = document.querySelector(".chart-container-bar");
      const bars = document.querySelectorAll(".bar-item");

      ScrollTrigger.create({
        trigger: chartContainerBar,
        start: "top 80%",
        onEnter: () => {
          // Animate height dari 0 ke target
          gsap.to(bars, {
            height: (i, target) => target.getAttribute("data-height"),
            duration: 1.5,
            stagger: 0.1,
            ease: "elastic.out(1, 0.5)",
          });
        },
        once: true,
      });
    </script>
  </body>
</html>

Penjelasan:

Langkah ini adalah inti visualisasi data yang paling kompleks namun menarik.

  1. Struktur: Dua grafik berbeda (Line/Area Chart untuk Revenue, Bar Chart untuk Engagement) dalam grid responsif.
  2. Morphing Logic (Area Chart):
    • Kita mendefinisikan 2 path SVG: d (bentuk awal/datar) dan data-d-to (bentuk akhir/bergelombang).
    • GSAP menginterpolasi atribut d dari bentuk awal ke bentuk akhir secara halus.
    • Teknik ini diterapkan pada dua layer: Stroke (garis solid) dan Fill (gradien area di bawahnya) secara bersamaan.
  3. Elastic Logic (Bar Chart):
    • Setiap bar dimulai dengan height: 0 atau height: 0%.
    • GSAP menganimasikan height ke target data yang ditentukan di atribut data-height.
    • Efek elastic.out membuat bar "membal" saat mencapai puncak, memberikan kesan organik.

Langkah 4: Interactive Data Cards dengan Mouse Movement

Animasi Data Card

Kartu data yang responsif terhadap pergerakan mouse menciptakan interaktivitas yang menarik.

File: datacards.html

<!DOCTYPE html>
<html lang="id">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Langkah 4: Interactive 3D Cards</title>
    
    <script src="<https://cdn.tailwindcss.com>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
    
    <style>
      @import url("<https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap>");
      body { font-family: "Inter", sans-serif; }

      /* Penting untuk efek 3D */
      .perspective-container {
        perspective: 1000px;
      }
    </style>
  </head>
  <body class="bg-[#020617] text-white overflow-x-hidden selection:bg-indigo-500/30">

    <!-- INTERACTIVE CARDS SECTION -->
    <section class="relative min-h-screen flex items-center justify-center py-20 px-6">
      
      <!-- Background Decoration -->
      <div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-4xl h-full max-h-[500px] bg-indigo-500/5 rounded-full blur-[120px] pointer-events-none"></div>

      <div class="container mx-auto max-w-6xl relative z-10 perspective-container">
        
        <div class="mb-16 text-center">
          <h2 class="text-4xl md:text-5xl font-black mb-4 text-white">
            Key <span class="text-emerald-400">Insights</span>
          </h2>
          <p class="text-slate-400 text-lg">
            Arahkan kursor Anda ke kartu untuk melihat interaksi detail.
          </p>
        </div>

        <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
          
          <!-- Card 1 (Indigo) -->
          <div class="interactive-card bg-slate-900/80 border border-white/10 rounded-3xl p-8 relative overflow-hidden group cursor-pointer backdrop-blur-xl h-[350px] flex flex-col justify-between transition-colors hover:border-indigo-500/50">
            <!-- Spotlight Gradient (Initially hidden) -->
            <div class="spotlight absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" 
                 style="background: radial-gradient(600px circle at var(--mouse-x) var(--mouse-y), rgba(99,102,241,0.15), transparent 40%);">
            </div>
            
            <div class="relative z-10">
              <div class="w-14 h-14 rounded-2xl bg-indigo-500/20 flex items-center justify-center text-indigo-400 mb-6">
                <svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>
              </div>
              <div class="text-5xl font-black text-white mb-2">+34%</div>
              <h3 class="text-xl font-bold text-indigo-200 mb-4">Revenue Growth</h3>
              <p class="text-slate-400 text-sm leading-relaxed">
                Pertumbuhan konsisten selama 3 kuartal berturut-turut dengan proyeksi stabil untuk tahun depan.
              </p>
            </div>
          </div>

          <!-- Card 2 (Cyan) -->
          <div class="interactive-card bg-slate-900/80 border border-white/10 rounded-3xl p-8 relative overflow-hidden group cursor-pointer backdrop-blur-xl h-[350px] flex flex-col justify-between transition-colors hover:border-cyan-500/50">
            <div class="spotlight absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" 
                 style="background: radial-gradient(600px circle at var(--mouse-x) var(--mouse-y), rgba(6,182,212,0.15), transparent 40%);">
            </div>
            
            <div class="relative z-10">
              <div class="w-14 h-14 rounded-2xl bg-cyan-500/20 flex items-center justify-center text-cyan-400 mb-6">
                <svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
              </div>
              <div class="text-5xl font-black text-white mb-2">2.5M</div>
              <h3 class="text-xl font-bold text-cyan-200 mb-4">Active Users</h3>
              <p class="text-slate-400 text-sm leading-relaxed">
                Basis pengguna aktif terus berkembang dengan retention rate 82% per bulan.
              </p>
            </div>
          </div>

          <!-- Card 3 (Emerald) -->
          <div class="interactive-card bg-slate-900/80 border border-white/10 rounded-3xl p-8 relative overflow-hidden group cursor-pointer backdrop-blur-xl h-[350px] flex flex-col justify-between transition-colors hover:border-emerald-500/50">
            <div class="spotlight absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" 
                 style="background: radial-gradient(600px circle at var(--mouse-x) var(--mouse-y), rgba(16,185,129,0.15), transparent 40%);">
            </div>
            
            <div class="relative z-10">
              <div class="w-14 h-14 rounded-2xl bg-emerald-500/20 flex items-center justify-center text-emerald-400 mb-6">
                <svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
              </div>
              <div class="text-5xl font-black text-white mb-2">99.9%</div>
              <h3 class="text-xl font-bold text-emerald-200 mb-4">System Uptime</h3>
              <p class="text-slate-400 text-sm leading-relaxed">
                Infrastruktur cloud kami menjamin ketersediaan maksimal dengan monitoring 24/7.
              </p>
            </div>
          </div>

        </div>
      </div>
    </section>

    <script>
      // --- LOGIC INTERACTIVE CARDS ---
      const cards = document.querySelectorAll(".interactive-card");

      cards.forEach((card) => {
        card.addEventListener("mousemove", (e) => {
          const rect = card.getBoundingClientRect();
          // Hitung posisi mouse relatif terhadap kartu (0,0 di kiri atas kartu)
          const x = e.clientX - rect.left;
          const y = e.clientY - rect.top;
          
          // Update CSS variables untuk gradient spotlight
          card.style.setProperty("--mouse-x", `${x}px`);
          card.style.setProperty("--mouse-y", `${y}px`);
          
          // Hitung rotasi 3D (Tilt Effect)
          // Range: -10deg sampai 10deg
          const xCenter = rect.width / 2;
          const yCenter = rect.height / 2;
          
          const rotateY = ((x - xCenter) / xCenter) * 10; // Kiri-Kanan
          const rotateX = ((y - yCenter) / yCenter) * -10; // Atas-Bawah (Inverted)

          gsap.to(card, {
            rotationY: rotateY,
            rotationX: rotateX,
            scale: 1.02, // Sedikit zoom
            transformPerspective: 1000,
            duration: 0.4,
            ease: "power2.out"
          });
        });

        // Reset saat mouse keluar
        card.addEventListener("mouseleave", () => {
          gsap.to(card, {
            rotationY: 0,
            rotationX: 0,
            scale: 1,
            duration: 0.7,
            ease: "elastic.out(1, 0.5)" // Efek membal saat reset
          });
        });
      });
    </script>
  </body>
</html>

Penjelasan:

Langkah ini menambahkan elemen interaksi pengguna yang menyenangkan (micro-interaction).

  1. Konsep "Spotlight": Kartu akan merespons posisi kursor mouse pengguna.
  2. Visual Effect:
    • Gradient Follow: Sebuah gradient radial redup akan bergerak mengikuti kursor di atas kartu (background: radial-gradient(...)).
    • 3D Tilt: Kartu akan sedikit miring (rotasi 3D) mengikuti arah mouse, memberikan efek kedalaman.
  3. Logika JS:
    • Event listener mousemove menghitung posisi mouse relatif terhadap kartu (e.clientX - rect.left).
    • GSAP digunakan untuk mengubah rotasi (rotationXrotationY) secara halus.
    • Event mouseleave mengembalikan kartu ke posisi semula.

Langkah 5: Number Formatter & Currency

Penting untuk memformat angka dengan benar agar mudah dibaca.

Formatting

file: formatter.html

<!DOCTYPE html>
<html lang="id">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Langkah 5: Smart Number Formatting</title>

    <script src="<https://cdn.tailwindcss.com>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js>"></script>

    <style>
      @import url("<https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap>");
      body {
        font-family: "Inter", sans-serif;
      }

      /* Grid Background Pattern (Konsisten dengan Langkah 1) */
      .bg-grid {
        background-size: 40px 40px;
        background-image: linear-gradient(
            to right,
            rgba(255, 255, 255, 0.03) 1px,
            transparent 1px
          ),
          linear-gradient(
            to bottom,
            rgba(255, 255, 255, 0.03) 1px,
            transparent 1px
          );
      }
    </style>
  </head>
  <body
    class="bg-[#020617] text-white overflow-x-hidden selection:bg-indigo-500/30"
  >
    <!-- SECTION CONTAINER -->
    <section
      class="relative min-h-screen flex items-center justify-center py-20 px-6"
    >
      <!-- Background Elements -->
      <div class="absolute inset-0 -z-10 overflow-hidden">
        <div class="absolute inset-0 bg-grid"></div>
        <div
          class="absolute top-0 right-0 w-[600px] h-[600px] bg-indigo-600/10 rounded-full blur-[120px] pointer-events-none"
        ></div>
        <div
          class="absolute bottom-0 left-0 w-[500px] h-[500px] bg-cyan-600/10 rounded-full blur-[120px] pointer-events-none"
        ></div>
      </div>

      <div class="container mx-auto max-w-5xl relative z-10">
        <!-- Header -->
        <div class="text-center mb-16">
          <div
            class="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-indigo-500/30 bg-indigo-500/10 text-indigo-300 text-xs font-medium mb-6"
          >
            <span>💎</span> Premium Data Formatting
          </div>
          <h2 class="text-4xl md:text-5xl font-black mb-4 text-white">
            Financial
            <span
              class="text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 to-cyan-400"
              >Overview</span
            >
          </h2>
          <p class="text-slate-400 text-lg max-w-2xl mx-auto">
            Teknik memformat raw data menjadi informasi yang mudah dibaca
            (Currency, Percent, & Compact) secara real-time.
          </p>
        </div>

        <!-- Cards Grid -->
        <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
          <!-- Card 1: CURRENCY (Net Profit) -->
          <div
            class="group bg-slate-900/60 border border-white/10 rounded-3xl p-8 backdrop-blur-xl hover:border-indigo-500/50 transition-all duration-500 hover:-translate-y-1"
          >
            <div class="flex items-center justify-between mb-6">
              <div
                class="w-12 h-12 rounded-2xl bg-indigo-500/20 flex items-center justify-center text-indigo-400 group-hover:scale-110 transition-transform duration-500"
              >
                <svg
                  class="w-6 h-6"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="2"
                    d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
                  ></path>
                </svg>
              </div>
              <span
                class="text-xs font-bold tracking-wider text-slate-500 uppercase"
                >Yearly</span
              >
            </div>

            <div class="space-y-1">
              <p class="text-slate-400 text-sm font-medium">Net Profit</p>
              <div
                class="text-3xl xl:text-4xl font-black text-white tracking-tight truncate counter"
                data-target="2540300"
                data-type="currency"
                title="$2,540,300"
              >
                $0
              </div>
            </div>

            <div
              class="mt-6 pt-6 border-t border-white/5 flex items-center gap-2 text-sm text-emerald-400"
            >
              <svg
                class="w-4 h-4"
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                <path
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  stroke-width="2"
                  d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
                ></path>
              </svg>
              <span>+18.2% vs target</span>
            </div>
          </div>

          <!-- Card 2: PERCENTAGE (Growth) -->
          <div
            class="group bg-slate-900/60 border border-white/10 rounded-3xl p-8 backdrop-blur-xl hover:border-cyan-500/50 transition-all duration-500 hover:-translate-y-1"
          >
            <div class="flex items-center justify-between mb-6">
              <div
                class="w-12 h-12 rounded-2xl bg-cyan-500/20 flex items-center justify-center text-cyan-400 group-hover:scale-110 transition-transform duration-500"
              >
                <svg
                  class="w-6 h-6"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="2"
                    d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
                  ></path>
                </svg>
              </div>
              <span
                class="text-xs font-bold tracking-wider text-slate-500 uppercase"
                >Q4 2025</span
              >
            </div>

            <div class="space-y-1">
              <p class="text-slate-400 text-sm font-medium">Growth Rate</p>
              <div
                class="text-4xl md:text-5xl font-black text-white tracking-tight counter"
                data-target="84.5"
                data-type="percent"
              >
                0%
              </div>
            </div>

            <div
              class="mt-6 pt-6 border-t border-white/5 flex items-center gap-2 text-sm text-cyan-400"
            >
              <svg
                class="w-4 h-4"
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                <path
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  stroke-width="2"
                  d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
                ></path>
              </svg>
              <span>Top 5% Industry</span>
            </div>
          </div>

          <!-- Card 3: COMPACT (Total Transactions) -->
          <div
            class="group bg-slate-900/60 border border-white/10 rounded-3xl p-8 backdrop-blur-xl hover:border-emerald-500/50 transition-all duration-500 hover:-translate-y-1"
          >
            <div class="flex items-center justify-between mb-6">
              <div
                class="w-12 h-12 rounded-2xl bg-emerald-500/20 flex items-center justify-center text-emerald-400 group-hover:scale-110 transition-transform duration-500"
              >
                <svg
                  class="w-6 h-6"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="2"
                    d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
                  ></path>
                </svg>
              </div>
              <span
                class="text-xs font-bold tracking-wider text-slate-500 uppercase"
                >Lifetime</span
              >
            </div>

            <div class="space-y-1">
              <p class="text-slate-400 text-sm font-medium">Transactions</p>
              <!-- Data Target: 1.45 Juta -->
              <div
                class="text-4xl md:text-5xl font-black text-white tracking-tight counter"
                data-target="1450000"
                data-type="compact"
              >
                0
              </div>
            </div>

            <div
              class="mt-6 pt-6 border-t border-white/5 flex items-center gap-2 text-sm text-slate-400"
            >
              <span>Volume tinggi</span>
              <span
                class="px-2 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-[10px] font-bold"
                >HIGH VOL</span
              >
            </div>
          </div>
        </div>
 <!-- Replay Button -->
        <div class="mt-16 text-center">
          <button
            onclick="runAnimations()"
            class="group relative px-8 py-3 rounded-full bg-white/5 border border-white/10 text-sm font-semibold hover:bg-white/10 transition-all hover:scale-105 active:scale-95"
          >
            <span class="relative z-10 flex items-center gap-2">
              <svg
                class="w-4 h-4 text-indigo-400 group-hover:rotate-180 transition-transform duration-500"
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                <path
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  stroke-width="2"
                  d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
                ></path>
              </svg>
              Replay Animation
            </span>
          </button>
        </div>
      </div>
    </section>

    <script>
      gsap.registerPlugin(ScrollTrigger);

      // --- UTILITY: ADVANCED FORMATTER ---
      function formatNumber(value, type) {
        switch (type) {
          case "currency":
            // Format Currency: $2,540,300
            return new Intl.NumberFormat("en-US", {
              style: "currency",
              currency: "USD",
              maximumFractionDigits: 0,
            }).format(value);

          case "percent":
            // Format Percent: 84.5% (Fixed 1 decimal)
            return value.toFixed(1) + "%";

          case "compact":
            // Format Compact: 1.4M (Short notation)
            return new Intl.NumberFormat("en-US", {
              notation: "compact",
              compactDisplay: "short",
              maximumFractionDigits: 1,
            }).format(value);

          default:
            return Math.floor(value).toLocaleString("en-US");
        }
      }

      // --- ANIMATION LOGIC ---
      function runAnimations() {
        const counters = document.querySelectorAll(".counter");

        counters.forEach((counter) => {
          const target = parseFloat(counter.getAttribute("data-target"));
          const type = counter.getAttribute("data-type");

          // Object proxy untuk animasi nilai mentah
          const proxy = { val: 0 };

          gsap.to(proxy, {
            val: target,
            duration: 2.5,
            ease: "power3.out", // Easing yang smooth (cepat di awal, pelan di akhir)
            onUpdate: function () {
              // Update textContent dengan nilai yang sudah diformat
              counter.textContent = formatNumber(this.targets()[0].val, type);
            },
          });
        });
      }

      // Start animation on load
      runAnimations();
    </script>
  </body>
</html>

Penjelasan:

Langkah ini fokus pada utilitas (utility) untuk memformat angka mentah menjadi format yang ramah pengguna (misal: 2500000 menjadi $2,500,000 atau 2.5M).

  1. Utility Function: Kita membuat fungsi formatNumber yang fleksibel menangani tipe currencypercent, dan compact (singkatan K/M/B).
  2. Implementasi: Kita menerapkan fungsi ini pada animasi GSAP onUpdate agar angka yang sedang berjalan tetap terformat rapi selama animasi berlangsung.

Dengan menggabungkan 5 teknik di atas, kita telah menciptakan dashboard yang tidak hanya informatif tetapi juga menyenangkan untuk dilihat dan diinteraksikan. Kunci kesuksesan adalah:

  1. Timing yang tepat: Tidak semua animasi harus bersamaan.
  2. Easing yang smooth: Gunakan power2.outback.out, dll. sesuai konteks.
  3. ScrollTrigger untuk context: Animasi hanya berjalan saat user scroll ke section tersebut.
  4. Interactive element: Responsivitas terhadap mouse membuat experience lebih engaging.

Data visualization dengan GSAP adalah seni yang menggabungkan storytelling, animasi, dan fungsi. Semoga tutorial ini membantu Anda membuat dashboard yang memukau!