How to make a bar chart using Vue.js

Often the project you are working on requires us to add a simple bar chart and including a charting library like highcharts.js or charts.js is too costly and resource-consuming. In this post lets a make a simple re-usable Vue component for a bar chart.

Goals

We're going to create a small reusable bar chart component with Vue. We will go through a step-by-step process of making the bar chart

  • How to set up a Vue application
  • Learn about d3-scale
  • How to use SVG to create a bar chart

I've created a live demo and shared the source code for you to follow along,

Setup and Installation

First, we'll install Vue CLI.

# install with npm
npm i -g @vue/cli @vue/cli-service-global

# install with yarn
yarn global add @vue/cli @vue/cli-service-global

Now that we have Vue CLI installed globally, we can use the Vue command anywhere. We'll use vue create to start a new project.

vue create vue-app

Once that's done, you can move to the new app that's been created and serve to run the dev server.

cd vue-app
npm run serve
# or
yarn serve

Once that's done, you can navigate to http://localhost:8080/ to see the default page.

Creating a Component

Before creating a bar chart let's understand the components of a bar chart, let's identify the building blocks that are required to compose a bar chart,

Structure

Create a file called BarChart.vue in src/component. We are going to create a skeleton of a vue file.

src/components/BarChart.vue
<template> </template>
<script>
  export default {
    name: "BarChart",
  };
</script>

<style lang="scss"></style>

Now let's create an SVG element, and set up some initial height and width for the SVG tag. This SVG element will act as the container for our chart.

BarChart.vue
<template>
  <svg class="barchart" :width="width" :height="height"></svg>
</template>

<script>
  export default {
    name: "BarChart",
    data() {
      return {
        height: 200,
        width: 500,
      };
    },
  };
</script>

<style lang="scss"></style>

We'll need some seed data for the bar chart to show up, let's take an array of people and their ages. Which might look like this

let dataSet = [
  ["Bob", 33],
  ["Robin", 24],
  ["Mark", 22],
  ["Joe", 29],
  ["Eve", 38],
  ["Karen", 21],
  ["Kirsty", 25],
  ["Chris", 30],
];

Now pull to make the data available for your Vue component, for now, we will place the dataset in the data field.

BarChart.vue
<template>
  <svg class="barchart" :width="width" :height="height"></svg>
</template>

<script>
  export default {
    name: "BarChart",
    data() {
      return {
        height: 200,
        width: 500,
        dataset: [
          ["Bob", 33],
          ["Robin", 24],
          ["Mark", 22],
          ["Joe", 29],
          ["Eve", 38],
          ["Karen", 21],
          ["Kirsty", 25],
          ["Chris", 30],
        ],
      };
    },
  };
</script>

<style lang="scss"></style>

Now we'll have to prep our data so that we can render the bars in the SVG. We can create our own functions for scaling the values that are tedious and error-prone, we are going to use an awesome d3-scale standalone library to generate our scales.

To add d3-scale to your project run the following command on your terminal.

yarn add d3-scale

Once that is done we'll need to import the scaling functions to our components. d3-scale provides us with a lot of scaling functions for a bar chart only the linerScale and scaleBand functions are sufficient.

The linerScale function takes two chained functions .range will take an array of the height of the chart and 0, and the domain will take the 0 to max age in the data and will return a function. Let's see an example below

let scale = scaleLinear()
  .range([200, 0])
  .domain([0, 50]);

scale(23); // returns 108
scale(33); // returns 68
scale(11); // returns 156

Similarly, for the y axis, you will need to use the scaleBand function as this in this case x-axis is not a continuous scale. We'll add two computed properties x and y that will return a corresponding scaling function.

computed: {
  x() {
    return scaleBand()
      .range([0, this.width])
      .padding(0.3)
      .domain(this.data.map(e => e[0]));
  },
  y() {
    let values = this.data.map(e => e[1]);
    return scaleLinear()
      .range([this.height, 0])
      .domain([0, Math.max(...values)]);
  },
}

The next step is to uses our dataset to generate x, y, height and width values to render our bars using the <rect> tag. We can create another computed property called bars and loop though the dataSet array and compute new values.

computed: {
  x() {
    return scaleBand()
      .range([0, this.width])
      .padding(0.3)
      .domain(this.data.map(e => e[0]));
  },
  y() {
    let values = this.data.map(e => e[1]);
    return scaleLinear()
      .range([this.height, 0])
      .domain([0, Math.max(...values)]);
  },
  bars() {
    let bars = this.data.map(d => {
      return {
        xLabel: d[0],
        x: this.x(d[0]),
        y: this.y(d[1]),
        width: this.x.bandwidth(),
        height: this.height - this.y(d[1])
      };
    });
    return bars;
  }
}

Now we can loop through the bars object in our HTML and generate some bars.

<template>
  <svg class="barchart" :width="width" :height="height">
    <g class="bars" fill="none">
      <rect
        v-for="(bar, index) in bars"
        fill="pink"
        :key="index"
        :height="bar.height"
        :width="bar.width"
        :x="bar.x"
        :y="bar.y"
      ></rect>
    </g>
  </svg>
</template>

Let's place our component in the Home.vue by importing it and calling the <BarChart> tag in the file like below,

Home.vue
<template>
  <div class="home">
    <BarChart class="chart" />
  </div>
</template>

<script>
  import BarChart from "@/components/BarChart.vue";

  export default {
    name: "home",
    components: { BarChart },
  };
</script>

<style lang="scss" scoped>
  .chart {
    margin: 120px auto;
    display: block;
  }
</style>

Now if you open http://localhost:8080/ you will see a preview of bars arranged next to each other. You could inspect and take a look at how the SVG rect tag is rendered. Also you could try adjusting the data object and see how all the bars will re-adjust accordingly.

Preview

Our next step is to generate the x and y axises, lets tackle the x-axis first. x-axis is the horizontal axis and first off, we'll need to create the line. The x and y axises are placed bottom and right edges so we need to make some room to display them, so we'll need to add a padding to the svg elements. Add an additional 40px to both height and width and then move the whole content inside by 20px left and top using the transform property.

We'll be using a another group tag g to place our x-axis and move that to the bottom of the graph by using the height property. For the line itself we can use a path to generate the line below using the width variable. Putting all that together,

<template>
  <svg class="barchart" :width="width + 40" :height="height + 40">
    <g transform="translate(20, 20)">
      <g class="x-axis" fill="none" :transform="`translate(0, ${height})`">
        <path stroke="currentColor" :d="`M0.5,6V0.5H${width}.5V6`"></path>
      </g>
      <g class="bars" fill="none">
        <rect
          v-for="(bar, index) in bars"
          fill="pink"
          :key="index"
          :height="bar.height"
          :width="bar.width"
          :x="bar.x"
          :y="bar.y"
        ></rect>
      </g>
    </g>
  </svg>
</template>

This is will generate a line below the graph like below, you can adjust the tick height by changing the d property on the path tag.

Preview

Now we need to add the ticks to the x-axis, horizontal ticks are nothing but labels for the bars, and for that we can loop through the bars again and places the text label right under the bar. You can create another g tag and loop that through the bars and add a text tag inside the g tag along with a line tag to show the little tick.

<template>
  <svg class="barchart" :width="width + 40" :height="height + 40">
    <g transform="translate(20, 20)">
      <g class="x-axis" fill="none" :transform="`translate(0, ${height})`">
        <path
          class="domain"
          stroke="currentColor"
          :d="`M0.5,6V0.5H${width}.5V6`"
        ></path>
        <g
          class="tick"
          opacity="1"
          font-size="10"
          font-family="sans-serif"
          text-anchor="middle"
          v-for="(bar, index) in bars"
          :key="index"
          :transform="`translate(${bar.x + bar.width / 2}, 0)`"
        >
          <line stroke="currentColor" y2="6"></line>
          <text fill="currentColor" y="9" dy="0.71em">{{ bar.xLabel }}</text>
        </g>
      </g>
      <g class="bars" fill="none">
        <rect
          v-for="(bar, index) in bars"
          fill="pink"
          :key="index"
          :height="bar.height"
          :width="bar.width"
          :x="bar.x"
          :y="bar.y"
        ></rect>
      </g>
    </g>
  </svg>
</template>

The text-anchor attribute will make sure the text is placed in the middle of the bar, and we will place each group at its corresponding x position. That will produce a chart like the below,

Preview

Our next step is to add the y-axis, and for that we need an array of values in correct intervals split equally base on the height of the chart. We can use the d3s tick() function to generate these values. We can add a computed property called yTicks and use the y scale function to generate the number of ticks.

computed: {
  yTicks() {
    return this.y.ticks(5);
  },
  x() {
    return scaleBand()
      .range([0, this.width])
      .padding(0.3)
      .domain(this.data.map(e => e[0]));
  },
  y() {
    let values = this.data.map(e => e[1]);
    return scaleLinear()
      .range([this.height, 0])
      .domain([0, Math.max(...values)]);
  },
  bars() {
    let bars = this.data.map(d => {
      return {
        xLabel: d[0],
        x: this.x(d[0]),
        y: this.y(d[1]),
        width: this.x.bandwidth(),
        height: this.height - this.y(d[1])
      };
    });
    return bars;
  }
}

Now the yTicks property will contain an array values in equal intervals scaled according to the height of the container, similar to the x-axis we will create a path and then group that loops within each group we'll have a text and line tag to place the label and show the tick. And we'll use the scale y() function to generate the corresponding y value for given tick value.

<template>
  <svg class="barchart" :width="width + 40" :height="height + 40">
    <g transform="translate(20, 20)">
      <g class="x-axis" fill="none" :transform="`translate(0, ${height})`">
        <path
          class="domain"
          stroke="currentColor"
          :d="`M0.5,6V0.5H${width}.5V6`"
        ></path>
        <g
          class="tick"
          opacity="1"
          font-size="10"
          font-family="sans-serif"
          text-anchor="middle"
          v-for="(bar, index) in bars"
          :key="index"
          :transform="`translate(${bar.x + bar.width / 2}, 0)`"
        >
          <line stroke="currentColor" y2="6"></line>
          <text fill="currentColor" y="9" dy="0.71em">{{ bar.xLabel }}</text>
        </g>
      </g>
      <g class="y-axis" fill="none" :transform="`translate(0, 0)`">
        <path
          class="domain"
          stroke="currentColor"
          :d="`M0.5,${height}.5H0.5V0.5H-6`"
        ></path>
        <g
          class="tick"
          opacity="1"
          font-size="10"
          font-family="sans-serif"
          text-anchor="end"
          v-for="(tick, index) in yTicks"
          :key="index"
          :transform="`translate(0, ${y(tick) + 0.5})`"
        >
          <line stroke="currentColor" x2="-6"></line>
          <text fill="currentColor" x="-9" dy="0.32em">{{ tick }}</text>
        </g>
      </g>
      <g class="bars" fill="none">
        <rect
          v-for="(bar, index) in bars"
          fill="pink"
          :key="index"
          :height="bar.height"
          :width="bar.width"
          :x="bar.x"
          :y="bar.y"
        ></rect>
      </g>
    </g>
  </svg>
</template>

That will generate our final graph with both x and y axis like shown in the picture below.

Preview

In the next step we can remove all the hard-coded values from the graph and get those values props for the component call and style the graph using CSS and it looks pretty. Putting it all together we will have a file like this,

BarChart.vue
<template>
  <svg
    class="barchart"
    :width="width + marginLeft / 2"
    :height="height + marginTop"
  >
    <g :transform="`translate(${marginLeft / 2}, ${marginTop / 2})`">
      <g
        class="x-axis"
        fill="none"
        :transform="`translate(0, ${height})`"
        style="color: #888"
      >
        <path
          class="domain"
          stroke="currentColor"
          :d="`M0.5,6V0.5H${width}.5V6`"
        ></path>
        <g
          class="tick"
          opacity="1"
          font-size="10"
          font-family="sans-serif"
          text-anchor="middle"
          v-for="(bar, index) in bars"
          :key="index"
          :transform="`translate(${bar.x + bar.width / 2}, 0)`"
        >
          <line stroke="currentColor" y2="6"></line>
          <text fill="currentColor" y="9" dy="0.71em">{{ bar.xLabel }}</text>
        </g>
      </g>
      <g
        class="y-axis"
        fill="none"
        :transform="`translate(0, 0)`"
        style="color: #888"
      >
        <path
          class="domain"
          stroke="currentColor"
          :d="`M0.5,${height}.5H0.5V0.5H-6`"
        ></path>
        <g
          class="tick"
          opacity="1"
          font-size="10"
          font-family="sans-serif"
          text-anchor="end"
          v-for="(tick, index) in yTicks"
          :key="index"
          :transform="`translate(0, ${y(tick) + 0.5})`"
        >
          <line stroke="currentColor" x2="-6"></line>
          <text fill="currentColor" x="-9" dy="0.32em">{{ tick }}</text>
        </g>
      </g>
      <g class="bars" fill="none">
        <rect
          v-for="(bar, index) in bars"
          fill="#2196f3"
          :key="index"
          :height="bar.height"
          :width="bar.width"
          :x="bar.x"
          :y="bar.y"
        ></rect>
      </g>
    </g>
  </svg>
</template>

<script>
  import { scaleLinear, scaleBand } from "d3-scale";

  export default {
    name: "BarChart",
    props: {
      height: { default: 200 },
      width: { default: 500 },
      dataSet: { default: [] },
      marginLeft: { default: 40 },
      marginTop: { default: 40 },
      marginBottom: { default: 40 },
      marginRight: { default: 40 },
      tickCount: { default: 5 },
      barPadding: { default: 0.3 },
    },
    computed: {
      yTicks() {
        return this.y.ticks(this.tickCount);
      },
      x() {
        return scaleBand()
          .range([0, this.width])
          .padding(this.barPadding)
          .domain(this.dataSet.map((e) => e[0]));
      },
      y() {
        let values = this.dataSet.map((e) => e[1]);
        return scaleLinear()
          .range([this.height, 0])
          .domain([0, Math.max(...values)]);
      },
      bars() {
        let bars = this.dataSet.map((d) => {
          return {
            xLabel: d[0],
            x: this.x(d[0]),
            y: this.y(d[1]),
            width: this.x.bandwidth(),
            height: this.height - this.y(d[1]),
          };
        });

        return bars;
      },
    },
  };
</script>

<style lang="scss"></style>
Home.vue
<template>
  <div class="home">
    <BarChart
      class="chart"
      :data-set="data"
      :margin-left="40"
      :margin-top="40"
      :tick-count="5"
      :bar-padding="0.5"
    />
  </div>
</template>

<script>
  import BarChart from "@/components/BarChart.vue";

  export default {
    name: "home",
    data() {
      return {
        data: [
          ["Bob", 33],
          ["Robin", 24],
          ["Mark", 22],
          ["Joe", 29],
          ["Eve", 38],
          ["Karen", 21],
          ["Kirsty", 25],
          ["Chris", 30],
        ],
      };
    },
    components: {
      BarChart,
    },
  };
</script>

<style lang="scss" scoped>
  .chart {
    margin: 40px auto 0;
    display: block;
  }
</style>

Adding some details and moving all the hard-coded values to props will result in a beautiful chart like the one below.

Conclusion

To recap we created a re-usable bar chart from scratch using vue and d3-scale, and here is a TLDR; version of the post

  • Create a SVG element
  • Import linearScale and scaleBand from d3-scale
  • Loop thought the data and generate the array with x, y, height, width, and xLabel
  • In the template render the bars using the rect tag
  • Use the path tag to render the x-axis and y-axis
  • Loop the thought bar and use the text tag to render x-axis labels(names)
  • Generate the number of ticks you want by using the .ticks(n) function
  • Loop through the tricks and use the text tag to render the y-axis values(ages).

And here is the demo and source code

If you like the post please feel free to share it on Twitter or leave a comment below. If you found any errors in this article, please feel free to edit on GitHub