DEV Community

Cover image for Data visualizations with Vue and D3: counting stars (part 2)
Natalia Tepluhina
Natalia Tepluhina

Posted on

Data visualizations with Vue and D3: counting stars (part 2)

You can find the first part of this article here.

If you skipped part 1, here is the code to start with ๐Ÿ˜‰

What are we going to build?

In this article, we will improve our star example with D3 data scaling. Scale functions are JavaScript functions that:

  • take an input (usually a number, date or category) and
  • return a value (such as a coordinate, a color, a length or a radius).

Theyโ€™re typically used to transform (or map) data values into visual variables (such as position, length, and color). This way, we can quickly 'draw' any set of initial data as SVG.

So, we will map our star 'visuals' to specific data values. At the end of the article, our star will look this way:

Finished sample

Scaling star radius

The first obvious question is: why do we need to scale the radius? Can't we use the value without scaling, as we did for rays in the previous article?

The reason is we want to limit our star outer radius while input data value can vary in a vast range. D3 will calculate the ratio and scale our star radius proportionally.

For the first iteration, let's say our input value will scale from 10 to 1000, but we want to display a star with a max radius of 250px. To perform the calculation, we're going to use D3 scaleLinear method.

scaleLinear constructs a new continuous scale with the specified domain and range. Each range value y can be expressed as a function of the domain value x: y = mx + b. (D3 Wiki)

First, let's remove our hardcoded outerRadius from data and let's create a new computed property to calculate it. Instead, we will add a radius property to data; it will serve as our input value.

data() {
  return {
    radius: 300,
    rays: 8,
  };
},
computed: {
  outerRadius() {
    return this.radius
  },
  ...
},

So, for now outerRadius just returns what we put in radius. Now, let's add some scaling to it! First, we need to import scaleLinear method from D3:

import { lineRadial } from 'd3-shape';
import { scaleLinear } from 'd3-scale';

We need to define a scaling function using scaleLinear method and then we need to call this function with our input radius as a parameter:

computed: {
  outerRadius() {
    const scale = scaleLinear()
      .domain([10, 1000])
      .range([5, 250]);

    return this.radius
  },
  ...
},

What's happening here? We provided a domain to scaleLinear, and it's the range in which our input value is going to change. As for range, it's what we have as an output: we expect our outerRadius to change in the range between 5px and 250px.

Now we can pass radius to the scale function:

computed: {
  outerRadius() {
  const scale = scaleLinear()
    .domain([10, 1000])
    .range([5, 250]);

  return scale(this.radius)
  },
  ...
},

As a result, we will have something like this:

Star with scaled radius.

Try to change the radius property to 500 or 1000, and you will see how the star grows in size.

Now, similarly to rays, let's add an input to change star radius dynamically. In our template, let's add

<div class="range-input">
  <label for="radius">Radius</label>
  <input name="radius" type="range" min="10" max="1000" v-model="radius" />
</div>

This way, we have bound our radius data property to the input field value.

Scaling radius gif.

Styling the app

Now we can change star radius and rays amount, but it still keeps this dull green color. What if we can improve it? If our star were a real star, it would change its color proportionally to its temperature, from red to blue. Can we create a scale that will do the same? With D3, the answer is obvious ๐Ÿ˜Š.

But before we start working on it, let's style our application a bit. First, let's move sliders to the right side of our image. To do so, we need to change the template slightly:

<template>
  <section class="wrapper">
    <svg width="500" height="500">
      <path
        class="radial"
        :d="radialData"
        transform="translate(250, 250)"
        fill="green"
      ></path>
    </svg>
    <aside>
      <div class="range-input">
        <input name="rays" type="range" min="4" max="60" v-model="rays" />
        <label for="rays">Rays</label>
      </div>
      <div class="range-input">
        <input
          name="radius"
          type="range"
          min="10"
          max="1000"
          v-model="radius"
        />
        <label for="radius">Radius</label>
      </div>
    </aside>
  </section>
</template>

And let's add some styles to the style section:

<style lang="scss">
  .wrapper {
    display: flex;
    align-items: center;
  }
  aside {
    padding-left: 20px;

    .range-input {
      display: flex;
      align-items: center;
      margin-bottom: 10px;
      input {
        margin-right: 10px;
      }
    }
  }
</style>

Now our app looks more structured:

Styled app.

Also, we want our star to be lighter, like... well, like real stars! Let's give it a dark background with a deep blue rectangle and let's change star color to white for now.

<svg width="500" height="500">
  <rect width="100%" height="100%" fill="#0e0432" />
  <path
    class="radial"
    :d="radialData"
    transform="translate(250, 250)"
    fill="white"
  ></path>
</svg>

Star on dark background

Now we can play with changing star color!

Creating a custom scale for star color

While D3 has built-in color interpolators, we want to build our simple one. We will take an input value from 0 to 100, and we will define a color for 0, 25, 50, 75 and 100 marks. D3 is smart enough to interpolate the color for in-between values.

As a first step, let's create a new data property - heat and set it to be 5:

data() {
  return {
    radius: 300,
    rays: 8,
    heat: 5,
  };
},

Next, let's create a new computed property for star color calculation:

starColor() {
  return this.heat
},

And finally we're ready to create a new scale:

starColor() {
  const myColor = scaleLinear()
    .domain([0, 25, 50, 75, 100])
    .range(['#ff7665', '#ffb469', '#ffe876', '#fff', '#99cdff']);
  return myColor(this.heat);
},

Now, we have a star color ready to be added to our SVG path fill property instead of static white color. Let's create a dynamic binding there:

<path
  class="radial"
  :d="radialData"
  transform="translate(250, 250)"
  :fill="starColor"
></path>

Our star has changed the color!

Red star.

Let's try to change the heat data property to 90, and a star will become light blue. To make it look nicer, we can create a gradient from white to current star color:

<svg width="500" height="500">
  <defs>
    <radialGradient id="starGradient">
      <stop offset="2%" stop-color="white" />
      <stop offset="95%" :stop-color="starColor" />
    </radialGradient>
  </defs>
  ...
</svg>

And now we can fill our star with this gradient instead of solid color:

<svg width="500" height="500">
  <defs>
    <radialGradient id="starGradient">
      <stop offset="2%" stop-color="white" />
      <stop offset="95%" :stop-color="starColor" />
    </radialGradient>
  </defs>
  <rect width="100%" height="100%" fill="#0e0432" />
  <path
    class="radial"
    :d="radialData"
    transform="translate(250, 250)"
    fill="url(#starGradient)"
  ></path>
</svg>

Now our star is really nice-looking:

Gradient star

And the last thing left is to add the same input slider as we did previously for rays and radius:

<div class="range-input">
  <input name="heat" type="range" min="0" max="100" v-model="heat" />
  <label for="heat">Heat</label>
</div>

And we're ready to go! You can find the final code of the Chart component here.

What's next?

In part 3, we're going to create not a single star but the whole constellation! And we will visualize a real dataset of Vue repositories:

Vue constellation

Top comments (1)

Collapse
 
thehumblesikh profile image
TheHumbleSikh

Thank you! <3