---
title: "Implementing Interactive Vega-Lite Charts on My Blog, Rooted in the Grammar of Graphics"
description: "I built a reusable component for rendering interactive Vega-Lite charts on my blog. This post explores the implementation, the power of the underlying Grammar of Graphics, and why this declarative approach is ideal for data-driven insights from my machine learning work."
pubDate: "2025-04-23"
categories:
  [
    "Data Visualization",
    "Vega-Lite",
    "Grammar of Graphics",
    "ggplot2",
    "JavaScript",
    "Web Development",
    "Astro",
    "Preact",
    "Machine Learning",
  ]
---

To better communicate findings from my machine learning projects on this blog, I needed a way to go beyond static plots. Static images show results, but often hide underlying patterns. I wanted readers to _explore_ the data themselves. Therefore, I implemented a system for embedding **interactive data visualizations** using **Vega-Lite**, rendered via a custom component built for my Astro-based blog.

This approach allows for interactions like tooltips, zooming, and linked selections directly within articles. The choice of Vega-Lite wasn't arbitrary; it stems from the powerful **Grammar of Graphics (GoG)** framework, a principled approach to visualization design that also underpins influential tools like `ggplot2`. This post outlines how I built this feature, why the GoG foundation is critical, and the benefits for technical communication.

Here is a chart that displays the same demonstration as the example on the official Vega-Lite website, while also supporting dark mode.

import Chart from "@/components/islands/common/Chart";

<Chart
  spec="/charts/spec/demo.json"
  height={400}
  client:visible
  ariaLabel="Demo chart"
/>

## The Need: Interactive, Performant Visualizations

My goal was clear: embed charts that were:

1. **Declarative:** Defined using Vega-Lite's JSON specifications based on data mappings.
2. **Performant:** Loaded efficiently without slowing down page loads, using lazy-loading.
3. **Responsive:** Adapted automatically to different screen sizes.
4. **Theme-Aware:** Matched the blog's light/dark mode seamlessly.
5. **Integrated:** Easily embedded within my Markdown (MDX) content.

Achieving this required both a smart frontend component and leveraging a robust visualization library built on solid principles.

## Why Vega-Lite? The Power of the Grammar of Graphics

Vega-Lite's strength comes from its foundation in the **Grammar of Graphics (GoG)**, a formal system for constructing statistical graphics first detailed by **Leland Wilkinson**. GoG decomposes charts into fundamental components:

- **Data:** The information being visualized.
- **Aesthetics (Mappings):** How data variables map to visual properties (position, color, size, shape).
- **Geometric Objects (Geoms):** The visual marks used (points, lines, bars).
- **Scales:** Functions translating data values to aesthetic values (pixels, colors).
- **Statistics (Stats):** Data transformations applied before mapping (binning, smoothing).
- **Coordinates:** The space where data is plotted (Cartesian, polar).
- **Faceting:** Creating subplots based on data subsets.

This modular approach offers immense **expressiveness** and **clarity**. It was popularized by **Hadley Wickham's `ggplot2`** package for R, which demonstrated GoG's practical power and became a dominant force in statistical graphics.

**Vega-Lite** adapts these GoG principles specifically for **interactive web visualizations**:

- It uses a **declarative JSON syntax** reflecting GoG components (`data`, `mark`, `encoding`).
- It natively supports **rich interactivity** (tooltips, zooming, panning, linked selections/brushing) crucial for data exploration.
- It compiles to **Vega**, a lower-level grammar, offering both ease of use and deep customization.
- Its JSON format integrates seamlessly with **web technologies**.

For my workflow, the synergy with **Python's `Altair` library** is key. I can define plots in Python using GoG concepts, export the Vega-Lite JSON, and reuse that _exact_ specification on the blog, ensuring perfect consistency between analysis and presentation.

## Implementation: A Custom `<Chart>` Component

I built a reusable Preact component (`Chart.tsx`) within my Astro project to handle rendering:

- **Core Rendering:** Uses the `useEffect` hook to dynamically import `vegaEmbed` and render the chart spec into a container element ref.
- **Lazy Loading (`client:visible`):** Astro's `client:visible` directive ensures the `vega-embed` library (which can be sizable) and rendering logic only load when the chart scrolls into view, improving initial page performance.
- **Responsiveness (`ResizeObserver`):** While Vega-Lite's `width: "container"` helps, `ResizeObserver` actively monitors the container element's size. If the layout changes _after_ the initial render (e.g., sidebar toggle), it triggers the Vega view's `resize()` method, ensuring the chart adapts correctly.
- **Theming:** Listens for a site-wide theme change event (or checks `document.documentElement` class) and updates the chart's theme using `vegaEmbed` options to support dark mode.
- **Accessibility:** Accepts an `ariaLabel` prop passed to the chart for screen reader users. Vega-Embed itself aids accessibility.

```d2 title="Interactive Chart Rendering Workflow"
direction: down
style: {
  fill: transparent
}

analysis: "Analysis (Python/Altair)" {
  shape: step
}
vlJson: "Vega-Lite JSON" {
  shape: document
}
chartComp: "<Chart> Component\n(Astro/Preact + client:visible)" {
  shape: package
}
vegaEmbed: "Lazy-loaded\nvega-embed" {
  shape: hexagon
  style: {
    stroke-dash: 3
  }
}
features: "Uses:\n- ResizeObserver\n- Theming" {
  shape: cloud
}
svgChart: "Accessible &\nInteractive SVG Chart" {
  shape: page
  style: {
    stroke: "#4CAF50"
    stroke-width: 2
  }
}

analysis -> vlJson
vlJson -> chartComp
chartComp -> vegaEmbed: "Renders via"
features -> vegaEmbed: "Acts on"
vegaEmbed -> svgChart: "Renders"
```

## Embedding Charts in Content

Using the component in my MDX posts is straightforward:

```jsx
import Chart from "@/components/islands/common/Chart";

### Exploring Model Hyperparameters

The interactive plot below shows validation accuracy across different parameters. Hover for details.

<Chart
  spec="/charts/spec/hyperparameter-scan.json" // Path to the Vega-Lite JSON
  height={400}
  client:visible // Enable lazy-loading
  ariaLabel="Interactive scatter plot of model accuracy vs hyperparameters"
/>

We can observe that optimal performance occurs...
```

This setup is ideal for visualizing various machine learning results:

- Hyperparameter scans (scatter plots with tooltips).
- Model comparisons (bar charts).
- Feature importance plots.
- Probability distributions (histograms, density plots, potentially linked).
- Time-series forecasts (zoomable line charts).

## Conclusion: Better Communication Through Principled Interaction

By implementing interactive Vega-Lite charts with a custom, performant component, I can now share data-driven insights from my machine learning work more effectively. This approach leverages the robust Grammar of Graphics principles, made practical by tools like ggplot2 and adapted for the interactive web by Vega-Lite. The streamlined workflow from Python analysis to blog post enables clearer communication and allows readers to engage directly with the data.
