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!

This is Bulbasaur and Vue, two great things we’ll learn about today

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.

Three relevant files, although we won’t discuss main.js

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:

https://gist.github.com/trainorpj/58d98954887c398d9bf8e34840a23cf2#file-example-data-js

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:

  1. Load in CustomPlot, a component from vue-custom-plot
  2. Load in json data from bulbasaur.json , and call it bulbasaur
  3. Make the App component, so we can export it to main.js
  4. Create the data object, which contains pokeData , and store the data bulbasaur in it
  5. Add w and h to the data object, which stand for “width” and “height,” respectively
https://gist.github.com/trainorpj/58d98954887c398d9bf8e34840a23cf2#file-full-file-0-vue

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.

https://gist.github.com/trainorpj/58d98954887c398d9bf8e34840a23cf2#file-template-0-html

The next step is an important one — we’re going to call CustomPlot in our template! We use it like so:

https://gist.github.com/trainorpj/58d98954887c398d9bf8e34840a23cf2#file-template-custom-plot-html
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):

  1. xy-data, the data we are going to render, which is pokeData
  2. width, the width of our visualization, which is w
  3. height, the height of our visualization, which is h

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:

  1. computedData, an array of objects, much like pokeData, but it has information on how to render our data to the svg
  2. xScale, a function that maps our x-values in pokeData to their actual values on <svg>
  3. yScale, a function that maps the y-values in pokeData 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 Adam Wathan.
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 .

https://gist.github.com/trainorpj/58d98954887c398d9bf8e34840a23cf2#file-computeddata-example-js

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 same c value we had in pokeData

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:

https://gist.github.com/trainorpj/58d98954887c398d9bf8e34840a23cf2#file-scale-example-js

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 use yScale(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:

https://gist.github.com/trainorpj/58d98954887c398d9bf8e34840a23cf2#file-upside-down-bulbasaur-html

And now, we have our beautiful bulbasaur:

Whoops… it’s upside-down

… 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:

  1. Access it in the scoped slot: slot-scope="{computedData, xScale, yScale, svg}"
  2. 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:

https://gist.github.com/trainorpj/58d98954887c398d9bf8e34840a23cf2#file-correct-bulbasaur-html
A wild Bulbasaur appears!

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 dawn 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>
Like what you read? Give Pj Trainor a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.