Based on the Vega-Lite “Getting Started with Vega-Lite” and Exploring Data” tutorials, adapted for this course.

D3, Vega, and Vega-Lite

So far in this course you’ve been writing charts with D3. D3 is a low-level library: it doesn’t know anything about “charts” at all. It gives you a set of primitives for binding data to DOM elements, computing scales, and generating shapes like arcs and lines, and it leaves it entirely up to you to combine those primitives into an axis, a bar, a legend, or a tooltip. That’s what makes D3 so flexible — there is essentially no visualization you can’t build with it — but it also means that even a simple bar chart takes a page of code, and every project tends to reinvent its own conventions for scales, axes, and interaction.

Vega sits one level of abstraction above D3. It’s a declarative visualization grammar: instead of writing imperative code that manipulates the DOM step by step, you write a JSON specification that describes the data, the marks (visual shapes), and how data fields map to visual properties like position, color, and size. A Vega runtime reads that specification and takes care of generating scales, axes, legends, and SVG/Canvas output for you. This is much less code than D3, but a full Vega specification is still fairly verbose — you have to explicitly define scales, axes, and layout, even though most charts use very similar, predictable versions of them.

Vega-Lite sits one level above Vega. It’s a higher-level grammar — really a concise, “opinionated” way of writing Vega specifications. In Vega-Lite you mostly specify what you want to see (which fields go on which encoding channels, and what type each field is — nominal, ordinal, quantitative, or temporal), and Vega-Lite automatically infers sensible defaults for scales, axes, legends, and even the mark type. A chart that would take dozens of lines in Vega, or a page of D3, is often five or six lines of Vega-Lite. Under the hood, every Vega-Lite specification is compiled into a full Vega specification, which is then rendered — you can always “eject” to Vega if you need more control than Vega-Lite exposes.

  D3 Vega Vega-Lite
Level Low-level (DOM/SVG primitives) Mid-level (declarative grammar) High-level (concise grammar)
You write Imperative JavaScript A JSON visualization spec A short JSON spec
Scales/axes/legends You build them yourself You declare them explicitly Inferred automatically
Flexibility Maximum — anything is possible High Good for common statistical charts
Typical chart 50-200+ lines of JS 30-80 lines of JSON 5-20 lines of JSON
Best for Custom, bespoke, highly interactive visualizations Reusable visualization components/libraries Fast exploration and standard charts

A good rule of thumb: reach for Vega-Lite when you’re exploring a dataset or building a standard statistical chart (bar, line, scatter, histogram) and want it to look good with minimal effort; reach for Vega when you need a reusable component with custom interaction that Vega-Lite doesn’t expose; reach for D3 when you’re building something truly custom that doesn’t look like a “chart” at all, or when you need full control over every pixel.

This tutorial focuses on Vega-Lite, and is split into two parts: first, Getting Started, which introduces the anatomy of a Vega-Lite specification using a tiny toy dataset; and second, Exploring Data, which uses those building blocks to explore a real dataset of Seattle weather.

Part 1: Getting Started with Vega-Lite

You can follow along interactively in the Vega-Lite Online Editor as well as in the runnable examples below.

The Data

We’ll start with a tiny toy dataset: a table with a categorical field a and a numeric field b.

a b
C 2
C 7
C 4
D 1
D 2
D 6
E 8
E 4
E 7

In a Vega-Lite spec, this table is provided as an array of JSON objects — one object per row — under data.values:

"data": {
  "values": [
    {"a": "C", "b": 2}, {"a": "C", "b": 7}, {"a": "C", "b": 4},
    {"a": "D", "b": 1}, {"a": "D", "b": 2}, {"a": "D", "b": 6},
    {"a": "E", "b": 8}, {"a": "E", "b": 4}, {"a": "E", "b": 7}
  ]
}

Encoding Data with Marks

Every Vega-Lite spec has two more essential ingredients besides data:

  • mark — the geometric shape used to visualize each row, e.g. "point", "bar", "line", "area", "tick", "circle".
  • encoding — an object that maps data fields to visual channels (x, y, color, size, shape, …). Each channel entry names a field and its type: "nominal" (unordered categories), "ordinal" (ordered categories), "quantitative" (numbers), or "temporal" (dates/times).

Putting a on x (nominal) and b on y (quantitative) with a point mark gives us a simple scatterplot — and notice that Vega-Lite has automatically drawn an axis, gridlines, and picked a reasonable scale for us, none of which we had to ask for:

See output in new page.

Data Transformation: Aggregation

Because a only has three distinct values, it might be more informative to summarize the three b values for each category. Vega-Lite lets you compute statistics directly on an encoding channel using aggregate, without writing any transform code yourself. Combined with switching the mark to "bar", and swapping which channel gets which field, we get a bar chart of the average of b per category:

"encoding": {
  "y": {"field": "a", "type": "nominal"},
  "x": {"aggregate": "average", "field": "b", "type": "quantitative"}
}

Customize Your Visualization

Vega-Lite’s defaults are good, but you can override almost anything — axis titles, colors, sizes — by adding more properties to an encoding channel. Here we give the aggregated axis a clearer title:

See output in new page.

Compare how little changed in the spec (one mark, a swapped pair of channels, and one aggregate keyword) to how much changed in the resulting chart. This is the core productivity argument for a grammar like Vega-Lite: small, composable changes to the specification produce large, predictable changes to the chart.

Publish Your Visualization Online

A Vega-Lite spec by itself is just JSON — to turn it into a chart on a web page you use the Vega-Embed library, which depends on Vega and Vega-Lite themselves:

See output in new page.

With those three scripts loaded, embedding a chart is just:

<div id="vis"></div>
<script>
  var spec = { /* ... your Vega-Lite spec ... */ };
  vegaEmbed('#vis', spec);
</script>

vegaEmbed compiles your Vega-Lite spec down to Vega, renders it (as SVG by default) into the element you point it at, and by default also attaches export/”open in editor” action links. Every runnable example on this page — including the two charts above — is a self-contained HTML file that does exactly this; click “See output in new page” under any example to view its full source.

Part 2: Exploring Data

Now let’s use the same building blocks — data, mark, and encoding — to explore a real dataset: daily weather observations for Seattle from NOAA, covering 2012-2015, with columns for date, precipitation (mm), max/min temperature (°C), wind speed (m/s), and a categorical weather label (sun, fog, drizzle, rain, snow).

Unlike our toy dataset, this one lives in a CSV file, so instead of data.values we point at a data.url:

"data": {"url": "data/seattle-weather.csv"}

Vega-Lite infers the CSV’s column types automatically (you can always override them by setting type explicitly on an encoding channel, as we’ve been doing all along).

Looking at a Single Field: Ticks and Histograms

To get a feel for how precipitation is distributed, we can put it on x with a tick mark — one small tick per day:

See output in new page.

There are a lot of days, and a lot of overlapping ticks near zero. A histogram — binning the quantitative field and counting how many rows fall in each bin — usually communicates a distribution more clearly. In Vega-Lite, binning is just another encoding property, "bin": true, and counting is another aggregate, "count", which (unlike "average") doesn’t need a field:

See output in new page.

The histogram makes it obvious that precipitation is heavily skewed: most days have little or no rain, with a long tail of wetter days.

Aggregating Over Time

Dates are special: often you don’t want to plot every single date, but rather group observations by month, by year, or by year-and-month together. Vega-Lite supports this with the timeUnit property on a temporal field, which discretizes a date into a coarser unit before it’s encoded (or aggregated). Here we group every observation by its month (ignoring the year) and take the mean precipitation, which reveals the seasonal pattern:

See output in new page.

Precipitation in the winter months is, on average, much higher than in summer. Note that "timeUnit": "month" collapses all four years onto the twelve months — Jan 2012, Jan 2013, Jan 2014, and Jan 2015 are all treated as “January.”

If instead we want to see a trend across years rather than collapsing them, we use "timeUnit": "yearmonth", which keeps year and month together. Here’s the maximum temperature reached in each year-month:

See output in new page.

Deriving New Fields

Sometimes the field you want to plot isn’t in the data yet — it needs to be computed from other fields. Vega-Lite specs can include a transform array, applied before encoding. The calculate transform evaluates a small expression (using the datum keyword to refer to the current row) and stores the result as a new field name. Here we derive each day’s temperature range, and plot it over the year, colored by weather condition:

"transform": [
  {"calculate": "datum.temp_max - datum.temp_min", "as": "temp_range"}
]
See output in new page.

Putting It Together: A Stacked Bar Chart

Finally, let’s combine several of these ideas — a temporal timeUnit, an aggregate count, and a color encoding driven by a nominal field — plus a custom color scale (mapping specific weather categories to specific colors, rather than letting Vega-Lite pick) to show how the mix of weather conditions changes across the year:

See output in new page.

By default, when you add a color encoding to a bar mark that shares an axis with other bars, Vega-Lite automatically stacks the bars — you didn’t have to ask for that either. Notice how much of what we’ve built over this whole tutorial — inference of marks, scales, axes, legends, aggregation, binning, time units, derived fields, and stacking — comes from a spec that never exceeds about fifteen lines of JSON. That’s the productivity trade Vega-Lite is making: less control than D3 or Vega, in exchange for dramatically less code for the (very common) case of standard statistical charts.

Next Steps