Let’s Make a Bulbasaur: Data Visualization with Vue
I have always wanted my own Bulbasaur. If you’re not familiar with Pokemon, Bulbasaur is one of the first creatures you encounter. They’re adorable and make great companions. I’m to make one with javascript, if you’d like to join me!
Before moving on, you should also know that we’ll be using Vue, a javascript framework for making user interfaces. Furthermore, this will be an exercise in data visualization, so we’ll make use of svg.
Note: svg stands for “Scalable Vector Graphics.” You can keep going on if you don’t know much about svg. If you’d like to learn more after reading about bulbasaur, check out this introduction!
In this article we’ll learn about using Vue for data visualization. We’ll make use of a component I wrote called vue-custom-plot, which has us usingsome amazing features of Vue to bring bulbasaur to life.
You can follow along in your browser with codesandbox.io! I’ve provided a link below:
Note: The sandbox may be a bit slow to load, so please be patient 🙂
Setting Up Our Files
We’re only working with three files in our src
directory: main.js
, App.vue
, and bulbasaur.json
. We won’t discuss main.js
in this article, but feel free to explore it.
First, let’s consider out data, which is stored in bulbasaur.json
. When we load it into our component, the data will be an array of objects. Those objects will contain an x-coordinate, a y-coordinate, and a color (as a hexadecimal code), like this:
Next, we’ll take a look at our App.vue
file, where we’ll spend the rest of our time. We have a few more steps to set things up:
- Load in
CustomPlot
, a component fromvue-custom-plot
- Load in json data from
bulbasaur.json
, and call itbulbasaur
- Make the
App
component, so we can export it tomain.js
- Create the data object, which contains
pokeData
, and store the databulbasaur
in it - Add
w
andh
to the data object, which stand for “width” and “height,” respectively
With these two important steps (understanding the data, and creating the component), we’re ready to build our Bulbasaur!
Note: The remaining code snippets will only be in the
<template>
tags, since that’s the only code we’ll be changing.
Using CustomPlot
We need an svg element to actually render our points. This is the first step in any data visualization. Any <svg>
tag needs a width and a height — we’ll use the w
and h
variables we set up earlier.
The next step is an important one — we’re going to call CustomPlot
in our template! We use it like so:
Note: The
<g>
tag is a generic element for svg, kind of like how we use<div>
. It’s just a container for other elements.
We’ll pass in three props to CustomPlot
(it accepts more props, but we’ll just use these three for now):
xy-data
, the data we are going to render, which ispokeData
width
, the width of our visualization, which isw
height
, the height of our visualization, which ish
When we pass these into CustomPlot
, the component gives us access to some additional data, which we can access through a scoped slot.
If you haven’t heard of scoped slots, that’s okay. They’re a powerful tool in Vue that gives parent components access to child data.
In this example, App
(the parent) has access to data from CustomPlot
(the child) via a scoped slot. Specifically, CustomPlot
exposes the following:
computedData
, an array of objects, much likepokeData
, but it has information on how to render our data to the svgxScale
, a function that maps our x-values inpokeData
to their actual values on<svg>
yScale
, a function that maps the y-values inpokeData
to their actual values on<svg>
The most important takeaway is:
We pass pokeData
into CustomPlot
, then CustomPlot
decides how that pokeData
is supposed to render to <svg>
. A scoped slot gives us access to the data that CustomPlot
generates, which is called computedData
.
Note: If you’d like to learn more about scoped slots, I highly recommend Renderless Components by .
You can also check out the official documentation.
Time to Make a Bulbasaur
We’re going to use computedData
to tell our pixels where to render, and what color they should be. The image below shows that computedData
has a slightly more complicated structure than our original pokeData
.
It’s an array of objects, each containing the values we’ll need to draw Bulbasaur:
svgx
, the x-coordinate of a pixel on our<svg>
svgy
, the y-coordinate of a pixel on our<svg>
attrs.c
, the color of the pixel, which is the samec
value we had inpokeData
If we’re drawing pixels, we have the x and y-coordinates to put them, and we have the color for each pixel, but we still need the width and height of each pixel to actually draw them!
Width and height are not stored in computedData
, but it’s readily available with xScale
and yScale
, which we retrieved in our scoped slot.
Both xScale
and yScale
are functions that accept values from our original data, and return the corresponding svg coordinates. If it’s not clear, see the example below:
Fortunately, xScale
and yScale
give us a convenient way of calculating the width and height of a pixel. The distance from the first x-coordinate to the next x-coordinate is xScale(1) — xScale(0)
, which is the width between the two.
Furthermore, the pixels all have the same width, so this xScale(1) — xScale(0)
is the width for every pixel for our Bulbasaur!
Note: svg keeps track of y-coordinates from top to bottom, which means that
yScale(0) — yScale(1)
is our height. If you useyScale(1) — yScale(0)
, you’ll get a negative value for height, which is not allowed in svg
Here’s a recap for how we’re going to draw a pixels:
- x-coordinate: the
svgx
value in an item incomputedData
- y-coordinate: the
svgy
value in an item incomputedData
- width:
xScale(1) — xScale(0)
- height:
yScale(0) — yScale(1)
- color: the
attrs.c
value in an item incomputedData
Now it’s time to put this into Vue! We’ll use the <v-for>
directive to loop through the elements of computedData
. Here’s what our template will look like:
And now, we have our beautiful bulbasaur:
… Okay, some of the more experienced developers/designers/trainers may have noticed that this Bulbasaur is actually upside-down. That is not how Bulbasaurs usually appear, so this picture is not quite accurate.
We should probably do something about this.
Note: For those who are learning English, I was making a joke. Sometimes my sense of humor is hard to understand 😊. We’ll spend the rest of the article fixing the picture of Bulbasaur.
Flipping Bulbasaur
Why is this happening? Well, as I mentioned in one of the notes, yScale
maps data points from the top of the <svg>
to the bottom. If you’re familiar with graphs in math, science, or statistics, the y-coordinates usually start at the bottom and go up. It’s just the opposite 🙃 in svg.
This is a common obstacle for first time learners of data visualization. Fortunately, CustomPlot
offers several ways to fix this — we’ll return to our scoped slot.
CustomPlot
actually exposes even more than computedData
, xScale
, and yScale
— it also provides an object called svg
, which stores the dimensions of our data visualization. For example, svg.height
is the height of the data visualization.
We use it like so:
- Access it in the scoped slot:
slot-scope="{computedData, xScale, yScale, svg}"
- Use it to properly render the y-coordinate:
:y="svg.height — pixel.svgy"
Lastly, for good measure, let’s throw in a line of code to get ride of that ugly black background.
If we pass a value of 0 to color, it will render the pixel as black (that’s the default behavior). We can set
:fill="pixel.attrs.c !== 0 ? pixel.attrs.c : 'none'"
This works like so: “If pixels.attrs.c
is not equal to 0, then return the value pixel.attrs.c
, otherwise, return 'none'
, so that no color renders.”
This is the resulting code and image:
Closing Remarks
We went through a lot in this guide, and that’s because data visualization takes a lot of thought and code. Hopefully this was a good introduction to the craft!
Here’s what we went over:
- Using Vue’s scoped slots for poweful rendering
- Using components like vue-custom-plot to make data visualization easier
- How to use scales to render your data to an svg
To finish, I’ll post the code so you can copy/paste it, and I’ll include the link to the codesandbox!
Thank you for reading!
Special thanks to for the beautiful code snippets
Here’s the codesandbox.
<template><div id="app"><h2>A wild bulbasaur appears!</h2><!-- set up our initial svg --><svg :width="w" :height="h"> <!-- pass data and dimensions into CustomPlot --> <CustomPlot :xy-data="pokeData" :width="w" :height="h" > <g slot-scope="{computedData, xScale, yScale, svg}"> <rect v-for="p in computedData" :key="p.key" :x="p.svgx" :y="svg.height - p.svgy" :width="xScale(1) - xScale(0)" :height="yScale(0) - yScale(1)" :fill="p.attrs.c !== 0 ? p.attrs.c : 'none'"> </rect> </g></CustomPlot></svg></div></template><script>import { CustomPlot } from "vue-custom-plot";import bulbasaur from "./bulbasaur.json";export default { name: "App", components: { CustomPlot }, data() { return { pokeData: bulbasaur, w: 500, h: 500 }; }};</script>