Real-Time Data Visualization with D3.js and Vue.js.
Tags: typescript, d3, vue, web
Published: 2023-04-22 22:48:24

Many web applications and solutions are becoming more dynamic and responsive. This is, in part, due to high and scalable computing capacity. More web applications are deployed on various cloud platforms as a result of high reliability, availability and security.

To add to this, is the end users' continual demand for real time experience. As organizations becomes more data-driven, many are using data visualizations to either tell their stories or share real time feedback with their customers.

This tutorial will use [D3.js](https://d3js.org/), a JavaScript library for manipulating documents based on data and [Vue.js](https://vuejs.org/), a progressive JavaScript framework used for building user interfaces, to demonstrate real time visualization.

Firstly, we will create a vue.js project using [vite.js](https://vitejs.dev/), a build tool for modern web development that is designed to be fast and efficient. If you do not have vite already installed, run the following command `npm create vite@latest`.

Then, navigate to project directory and run `npm init vue@latest` and follow the prompt by picking a project name, selecting TypeScript and other relevant features.

```
✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes

Scaffolding project in ./<your-project-name>...
Done.
```

This will scaffold a vue.js project with similar project tree:

```
├── README.md
├── env.d.ts
├── index.html
├── node_modules
├── package-lock.json
├── package.json
├── public
│   └── favicon.ico
├── src
│   ├── App.vue
│   ├── assets
│   ├── components
│   ├── main.ts
│   ├── router
│   ├── types
│   └── views
├── tsconfig.app.json
├── tsconfig.config.json
├── tsconfig.json
├── tsconfig.vitest.json
└── vite.config.ts
```

Afterwards, confirm that your `package.json` file has the following dependencies, else copy and paste (more reliably install them individually if the latest version is your thing) them. Make sure that you are inside your project directory. Otherwise, change to your project directory and run `npm install`.

```
  "dependencies": {
    "@types/d3": "^7.4.0",
    "@types/d3-shape": "^3.1.0",
    "d3": "^7.6.1",
    "d3-shape": "^3.1.0",
    "vue": "^3.2.45",
    "vue-router": "^4.1.6",
    "vuex": "^4.1.0"
  },
  "devDependencies": {
    "@rushstack/eslint-patch": "^1.1.4",
    "@types/jsdom": "^20.0.1",
    "@types/node": "^18.11.12",
    "@vitejs/plugin-vue": "^4.0.0",
    "@vue/eslint-config-prettier": "^7.0.0",
    "@vue/eslint-config-typescript": "^11.0.0",
    "@vue/test-utils": "^2.2.6",
    "@vue/tsconfig": "^0.1.3",
    "eslint": "^8.22.0",
    "eslint-plugin-vue": "^9.3.0",
    "jsdom": "^20.0.3",
    "npm-run-all": "^4.1.5",
    "prettier": "^2.7.1",
    "typescript": "~4.7.4",
    "vite": "^4.0.0",
    "vitest": "^0.25.6",
    "vue-tsc": "^1.0.12"
  }
```

Navigate to the components directory and create the chart folder (Barchart for this example) and a BarChart.vue file as shown below:

```
├── BarChart
│   └── BarChart.vue
```

Open the `.vue` file and paste the following code:

```
<template>
  <div class="" ref="barChartRef"></div>
</template>
```
This is a vue.js code template with a `ref?: VNodeRef` property called `barChartRef`. We will define the `ref` inside the script section. Next create a script section and import the D3.js library and other Vue.js dependencies. 

For demonstration purposes, we will be skipping some codes here but are available in the repository.

```
<script setup lang="ts">
// @ is an alias to /src
import * as d3 from "d3";
import { onMounted, onBeforeUnmount, computed, ref, reactive, watch, } from "vue";
...

</script>
```

We can also define our data, data interface (schema) and other variables for the chart.

```
interface Input {
  label: string;
  value: number;
}

const barChartRef = ref<HTMLElement | null>(null);
const margin = reactive({ top: 30, right: 20, bottom: 30, left: 40 });
const height = ref(360);
const width = ref(
  (barChartRef?.value?.offsetWidth || 300) - margin.left - margin.right
);

const revenue = ref([
  {
    label: "2013-06-30",
    value: 660,
  },
  {
    label: "2014-12-31",
    value: 814,
  },
  {
    label: "2015-06-30",
    value: 1131,
  },
  {
    label: "2016-12-31",
    value: 1267,
  },
  {
    label: "2017-06-30",
    value: 1514,
  },
  {
    label: "2018-12-31",
    value: 1763,
  },
  {
    label: "2019-06-30",
    value: 2653,
  },
  {
    label: "2020-12-31",
    value: 6148,
  },
  {
    label: "2021-06-30",
    value: 12394,
  },
  {
    label: "2022-12-31",
    value: 2162,
  },
]);
```
Then, we will transform our data to the right format as below using a computed function:

```
const data = computed(() => {
  return revenue.value.map((d: Input) => {
    return {
      label: d.label,
      value: d.value,
    };
  });
});
```

We are going to use other Vue.js native API functions to control how our chart is rendered on the page.

```
onMounted(async () => {
  window.addEventListener("resize", handleResize);
  handleResize();
  handleDraw();
});

onBeforeUnmount(() => {
  window.removeEventListener("resize", handleResize);
});

watch(
  () => width.value,
  () => {
    remove();
    handleDraw();
  }
);
```

To avoid having multiple charts when the page re-renders, we can create a function that checks if the number of charts on the page exceeds one. If true, it will purge all except one.

```
const remove = () => {
  // TODO: Get svg reference
  const svg = d3.select(barChartRef.value).selectAll("svg");

  // check the number of existing elements, if greater than 0; remove all existing ones
  if (svg.size()) svg.remove().exit();
};
```
We can make the chart to be responsive for every device screen by checking the user's screen size and displaying a chart within the width of the screen.

```
const handleResize = () => {
  if (barChartRef?.value?.offsetWidth) {
    width.value = barChartRef.value.offsetWidth - margin.left - margin.right;
    return;
  }

  if (window.innerWidth < 400) {
    width.value = 300 - margin.left - margin.right;
    return;
  }

  width.value = 550 - margin.left - margin.right;
  return;
};
```
Finally, let us create a function that with generate our chart every time the page renders.

```
const handleDraw = async () => {
  // append the svg object to the body of the page
  const svg = d3
    .select(barChartRef.value)
    .append("svg")
    .attr("width", width.value + margin.left + margin.right)
    .attr("height", height.value + margin.top + margin.bottom)
    .append("g")
    .attr("transform", `translate(${margin.left},${margin.top})`);

  // Add X axis
  const x = d3
    .scaleBand()
    .domain(data.value.map((d) => d.label))
    .range([0, width.value])
    .padding(0.2);

  svg
    .append("g")
    .attr("transform", `translate(0, ${height.value})`)
    .call(d3.axisBottom(x))
    .selectAll("text")
    .attr("transform", "translate(-10,0)rotate(-45)")
    .style("text-anchor", "end");

  // Add Y axis
  const y = d3
    .scaleLinear()
    .domain([0, d3.max(data.value, (d): number => d.value)] as number[])
    .range([height.value, 0]);

  svg.append("g").call(d3.axisLeft(y));

  // Bars
  svg
    .selectAll("mybar")
    .data(data.value)
    .enter()
    .append("rect")
    .attr("x", (d) => x(d.label) as number)
    .attr("y", (d) => y(d.value))
    .attr("width", x.bandwidth())
    .attr("height", (d) => height.value - y(d.value))
    .attr("fill", "#4682b4");

  svg
    .append("text")
    .attr("class", "title")
    .attr("x", width.value / 2)
    .attr("y", 0 - margin.top / 2)
    .attr("text-anchor", "middle")
    .text("Company Revenue in USD ($)");
};
```

In the above code, we added both the `x-axis` and `y-axis` components. First, we defined both axes, then we formatted them to fit into our chart using the following code snippet:

```
  // Add X axis
  const x = d3
    .scaleBand()
    .domain(data.value.map((d) => d.label))
    .range([0, width.value])
    .padding(0.2);

  svg
    .append("g")
    .attr("transform", `translate(0, ${height.value})`)
    .call(d3.axisBottom(x))
    .selectAll("text")
    .attr("transform", "translate(-10,0)rotate(-45)")
    .style("text-anchor", "end");

  // Add Y axis
  const y = d3
    .scaleLinear()
    .domain([0, d3.max(data.value, (d): number => d.value)] as number[])
    .range([height.value, 0]);

  svg.append("g").call(d3.axisLeft(y));
```

A sample output should look like the image below:

![Vue graph](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8vhce1nz6pejklewvrb5.PNG)

That is all we need to do in order to create a real time responsive and dynamic visualization using D3.js and Vue.js. The repository for this tutorial is on [GitHub](https://github.com/callezenwaka/graphs/tree/main/vue-chart).

If you like the article, do like and share with friends.