Interactive Data Visualization with Vega | by Déborah Mesquita | Towards Data Science

I’m always learning new visualization tools because this helps me identify the right one for the task at hand. When it comes to Data Visualization, d3 is usually the go-to choice, but recently I've been playing with Vega and I'm loving it.
Vega introduces a visualization grammar. A grammar is basically a set of rules that dictate how to use a language, so we can think of Vega as a tool that defines a set of rules of how to build and manipulate visual elements.
As my experience with data visualization grows, I’m finding more and more that constraints are a good thing. By introducing a visualization grammar, Vega gives us some constraints to work with. The best thing about it is that these constraints can make us feel very productive while building data visualizations.
There is also Vega-Lite, a high-level grammar that focuses on rapid creation of common statistical graphics, but today we'll stick with Vega which is a more general purpose tool.
Ok, enough of introductions, let’s get an overview about how Vega works.
We can deploy Vega on the web, but in this tutorial we’ll simply use the Vega Editor (which is another great thing about Vega).
While working with Vega we define the visualization in a JSON object. Let’s start building a Bar Chart.
If we break down this chart we have:
🗂 The data (the categories and amount for each data point)
📊 The x-axis, where each category is accommodated (we’ll need a scale to say were each category should be placed)
📊 The y-axis, where the amount for each data point is displayed (we’ll need a scale to say were each amount should be placed)
✍🏾 The rectangles
And this is how we define the above using Vega:
{  "$schema": "https://vega.github.io/schema/vega/v5.json",  "width": 400,  "height": 200,  "padding": 5,  "data": [    {      "name": "our_data",      "values": [        {          "category": "A",          "amount": 28        },        {          "category": "B",          "amount": 55        },        {          "category": "C",          "amount": 43        }      ]    }  ],  "scales": [    {      "name": "xscale",      "type": "band",      "domain": {        "data": "our_data",        "field": "category"      },      "range": "width",      "padding": 0.05    },    {      "name": "yscale",      "domain": {        "data": "our_data",        "field": "amount"      },      "range": "height"    }  ],  "axes": [    {      "orient": "bottom",      "scale": "xscale"    },    {      "orient": "left",      "scale": "yscale"    }  ],  "marks": [    {      "type": "rect",      "from": {        "data": "our_data"      },      "encode": {        "enter": {          "x": {            "scale": "xscale",            "field": "category"          },          "width": {            "scale": "xscale",            "band": 1          },          "y": {            "scale": "yscale",            "field": "amount"          },          "y2": {            "scale": "yscale",            "value": 0          },          "fill": {            "value": "steelblue"          }        }      }    }  ]}
You can try it live here.
Let’s go through each of these definitions. I’ll explain them briefly here but there are a lot more properties we can use to customize things (it’s a good idea to check the Docs when using each of them).
We can define data directly in a specification (like we are doing using the "values" property) or load data from an external file (json or csv for example) using the "url" property.
Vega scales are provided by the d3-scale library. We specify the type of scale with the "type" keyword (default is linear). Scale domains can be specified in multiple ways:
A data reference object that specifies field values in one or more data sets, like we are doing with {"data": "our_data", "field": "amount"}. Vega computes the [min,max] array of the amount key from the dataset
As a literal array of domain values
A signal reference that resolves to a domain value array. For example, {"signal": "myDomain"} (don’t worry, I’ll talk about Signals later on)
Here we need to specify the orientation and the scale that should be used to create the axis. There are a lot of properties we can use to customize them.
We use marks to encode data using geometric primitives (rectangles, circles, lines and so on). In this bar chart we are using the Rect mark. They need a given position, width, and height. We also need to specify what data should be used to build the marks (the "from" property).
"from": {"data":"our_data"}
All the definitions for things like "x", "y" and "width" will come from this dataset. Vega Types might look a little confusing at first, so let’s go through the ones we are using here:
"x": {"scale": "xscale", "field": "category"}
The "x" property for the rects will be set by the passing the values from "category" field to the "xscale".
"y": {"scale": "xscale", "band": 1}
The "y" property for each rect will be the range band width of the band scale xscale.
"fill": {"value": "steelblue"}
The "fill" color of the rects will be steelblue. To define constant values we use the "value" property.
Vega uses the same enter, update, exit pattern that d3 uses:
“The enter properties are evaluated when data is processed for the first time and a mark instance is newly added to a scene. The update properties are evaluated for all existing (non-exiting) mark instances. The exit properties are evaluated when the data backing a mark is removed, and so the mark is leaving the visual scene.” — Vega docs
We use the pattern inside the "encode" property. In this bar chart we are placing the elements when the data is processed:
"encode": {    "enter": {        "x": {"scale": "xscale", "field": "category"},        "width": {"scale": "xscale", "band": 1},        "y": {"scale": "yscale", "field": "amount"},        "y2": {"scale": "yscale", "value": 0},        "fill": {"value": "steelblue"}     }}
And this was the Vega 101 tour! To get a better sense of what Vega is capable of let’s build a timeline chart.
Besides loading the data we can also filter, calculate new fields or derive new data streams using Vega Transforms. We can sort the items by name using the collect transform:
"data": [    {      "name": "libraries",      "format": {        "type": "json",        "parse": {          "release": "date:'%Y'"        }      },      "values": [        {          "name": "vega",          "release": "2013",          "license": "BSD 3-Clause",          "description": "Vega is a visualization grammar, a declarative language for creating, saving, and sharing interactive visualization designs"        },        {          "name": "d3",          "release": "2011",          "license": "BSD 3-Clause",          "description": "D3 (Data-Driven Documents or D3.js) is a JavaScript library for visualizing data using web standards"        },        {          "name": "plotly",          "release": "2012",          "license": "MIT",          "description": "Plotly.js is an open source JavaScript library for creating graphs and dashboards"        }      ],      "transform": [        {          "type": "collect",          "sort": {            "field": "name"          }        }      ]    }  ]
Another great thing about Vega is that it's possible the inspect the contents of all the data we use to build the visualizations:
We'll need a time scale for the x-axis and an ordinal scale to color the rectangles:
"scales": [    {      "name": "xScale",      "type": "time",      "domain": {        "data": "libraries",        "field": "release"      },      "range": "width",      "nice": "year"    },    {      "name": "colorScale",      "type": "ordinal",      "domain": {        "data": "libraries",        "field": "license"      },      "range": {        "scheme": "dark2"      }    }  ]
Let's place an axis in the bottom and show the year in the labels:
"axes": [    {      "scale": "xScale",      "orient": "bottom",      "format": "%Y"    }  ]
There are three marks: the rectangles, the text inside the rectangles and the line from each rectangle to the axis. We'll use "rect", "text" and "rule" marks to define each of them.
But first let's introduce an important Vega property: Signals.
Signals are dynamic variables. As the documentation says, the signal values are reactive: they can update in response to input event streams, external API calls, or changes to upstream signals. Here we'll use them with initial values, but their power comes from being able to update them (we'll see how to do that another time).
"signals": [    {      "name": "rectWidth",      "value": 50    },    {      "name": "rectHeight",      "value": 40    },    {      "name": "rectY",      "value": 85    },    {      "name": "rectCenter",      "init": "[rectWidth/2,rectY+rectHeight/2]"    }  ]
Now that we have the signals we can use them to place the marks. Signals can also hold Vega expressions. A very used one is the scale:
scale(name, value[, group])Applies the named scale transform (or projection) to the specified value. The optional group argument takes a scenegraph group mark item to indicate the specific scope in which to look up the scale or projection.
In this example we'll use an expression to place the rectangles in the middle of each year with this expression:
"signal": "scale('xScale',datum.release)-rectWidth/2"         //scale(name, value[,group]
As we saw earlier, we need to specify what data should be used to build the marks with the "from" property. Vega is so great that we can specify the data from another mark itself! In this case, we'll use the data from the rect marks so we can get the center for each rectangle and place the text in the middle. To access the data points we use "datum" inside the expression.
"marks": [    {      "type": "rect",      "name": "rectangles",      "from": {        "data": "libraries"      },      "encode": {        "enter": {          "width": {            "signal": "rectWidth"          },          "height": {            "signal": "rectHeight"          },          "x": {            "signal": "scale('xScale',datum.release)-rectWidth/2"          },          "y": {            "signal": "rectY"          },          "fill": {            "signal": "scale('colorScale', datum.license)"          },          "tooltip": {            "signal": "{'Description': datum.description}"          }        },        "update": {          "fillOpacity": {            "value": 1          }        },        "hover": {          "fillOpacity": {            "value": 0.5          }        }      }    },    {      "type": "text",      "name": "labels",      "from": {        "data": "rectangles" // ⬅️cool      },      "encode": {        "enter": {          "text": {            "signal": "datum.datum.name"          },          "x": {            "signal": "datum.x+rectCenter[0]" //datum.x is from rect          },          "y": {            "signal": "rectCenter[1]"          },          "align": {            "value": "center"          },          "baseline": {            "value": "middle"          },          "fontWeight": {            "value": "bold"          },          "fill": {            "value": "black"          }        }      },      "interactive": false    },    {      "type": "rule",      "from": {        "data": "labels" // ⬅️cool      },      "encode": {        "enter": {          "x": {            "signal": "datum.x"          },          "x2": {            "signal": "datum.x"          },          "y": {            "signal": "datum.y+rectCenter[0]-5"          },          "y2": {            "signal": "height"          },          "strokeWidth": {            "value": 2          }        }      }    }  ]
Legends definitions are similar to mark definitions. To customize is the addressable elements are:
legend for the legend group mark,
title for the title text mark,
labels for label text marks,
symbols for legend symbol marks,
entries for symbol legend group marks containing a symbol / label pair, and
gradient for a gradient rect marks: one rect with gradient fill for continuous gradient legends, multiple rect marks with solid fill for discrete gradient legends.
Here we'll only set the "x" position for the legend (the whole group) and set the fontSize for the title and the labels.
"legends": [    {      "title": "License",      "fill": "colorScale",      "orient": "none",      "encode": {        "title": {          "update": {            "fontSize": {              "value": 15            }          }        },        "labels": {          "update": {            "fontSize": {              "value": 12            }          }        },        "legend": {          "update": {            "x": {              "value": 500            }          }        }      }    }  ]
The config object defines default visual values to set a visualization’s theme. Here we are setting the typefaces for the text of the graph. The title directive adds a descriptive title to a chart.
"config": {    "text": {      "font": "Ideal Sans, Avenir Next, Helvetica"    },    "title": {      "font": "Ideal Sans, Avenir Next, Helvetica",      "fontWeight": 500,      "fontSize": 17,      "limit": -1    },    "axis": {      "labelFont": "Ideal Sans, Avenir Next, Helvetica",      "labelFontSize": 12    }  },"title": {    "text": "Data visualization tools release dates",    "orient": "top",    "anchor": "start",    "frame": "group",    "encode": {      "update": {        "dx": {          "value": -1        }      }    }  }
And we are done! You can see the code here.
There are some other cool Vega features that we didn't see in this tutorial:
Triggers: Modify data sets or mark properties in response to signal values
Projections: Cartographic projections to map (longitude, latitude) data
Event Streams: Define input event streams to specify interactions
Layout: Perform grid layout for a collection of group marks
Today I use Vega in my workflow to prototype and test assumptions about the choices of my data visualizations. If after that I identify that I need something more customized I’ll then change gears and use d3 for that.
You can check more Vega examples here: https://vega.github.io/vega/examples/. We can view all the examples in the Online Vega Editor, which is awesome.
And that's it! Thanks for reading! 😄