DEV Community

Callis Ezenwaka
Callis Ezenwaka

Posted on • Updated on

Real-Time Data Visualization with D3.js and Vue.js.

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, a JavaScript library for manipulating documents based on data and Vue.js, 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, 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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Open the .vue file and paste the following code:

<template>
  <div class="" ref="barChartRef"></div>
</template>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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,
  },
]);
Enter fullscreen mode Exit fullscreen mode

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,
    };
  });
});
Enter fullscreen mode Exit fullscreen mode

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();
  }
);
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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 ($)");
};
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

A sample output should look like the image below:

Vue graph

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.

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

Top comments (2)

Collapse
 
michaelsynan profile image
Michael Synan

Will be checking d3js out soon. Thank you!

Collapse
 
callezenwaka profile image
Callis Ezenwaka

Thanks @michaelsynan. Do check it out and always reach out to me if you need help.