Brady Dowling Family first. People before programs. Basketball too. Find me on YouTube at ReadWriteExercise.

Using D3 data visualization to create a calendar app

6 min read 1798

Introduction

Building a clone of a well-known application or site is a great way to learn a new technology or level up knowledge you already have. So, while D3 might not be the first tool you’d think to reach for to build a calendar app, we can learn a lot about the library by using it to create a Google Calendar clone.

D3 demo project setup

For simplicity, we’ll create a directory and then scaffold our app using Snowpack. From the command line, run the following (you can also run the yarn equivalents if that’s your thing):

mkdir calendar-clone
cd calendar-clone
npm init -y
npm install --save-dev snowpack
npm install --save d3

Before we get anything running, we should first create an HTML file (index.html ) in the root of our calendar-clone directory with the following contents:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="Starter Snowpack App" />
    <title>D3 Calendar App</title>
  </head>
  <body>
    <h1>D3 Calendar</h1>
    <script type="module" src="/index.js"></script>
  </body>
</html>

We’ll also create an index.js file in the same directory (which where we’ll be doing the rest of our work).

Just to be safe, let’s add a console log statement to make sure it’s running correctly:

console.log("Let's build a calendar app!");

From here, you can start up your dev server and run the following command from your terminal:

npx snowpack dev

This will start your dev server and open a browser window where it’s running.

Once you open the console in your browser devtools and find the log we added above, we’re ready to start building our app!

Demo: build a Google Calendar app clone with D3

D3 is most often used for pulling in, parsing, and visualizing data. In this tutorial, we will bypass the data manipulation that would take place in most data visualization projects by creating our own JSON object.

Import statements and data declaration

To begin building our app, our first step is to clear out the previous test code we added to our index.js file. With our index.js file blank, we will now import D3, declare some calendar events, and create a dates array which will be used later:

import * as d3 from 'd3';
const calendarEvents = [
  {
    timeFrom: '2020-11-11T05:00:00.000Z',
    timeTo: '2020-11-11T12:00:00.000Z',
    title: 'Sleep',
    background: '#616161'
  },
  {
    timeFrom: '2020-11-11T16:00:00.000Z',
    timeTo: '2020-11-11T17:30:00.000Z',
    title: 'Business meeting',
    background: '#33B779'
  },
  {
    timeFrom: '2020-11-12T00:00:00.000Z',
    timeTo: '2020-11-12T05:00:00.000Z',
    title: 'Wind down time',
    background: '#616161'
  }
];
// Make an array of dates to use for our yScale later on
const dates = [
  ...calendarEvents.map(d => new Date(d.timeFrom)),
  ...calendarEvents.map(d => new Date(d.timeTo))
];

Add standard D3 variables

We will design more later, but to start, we can declare a few variables that are typically part of D3 projects (margin, height, width) and a few others that are specific to ours (barWidth, nowColor):

const margin = { top: 30, right: 30, bottom: 30, left: 50 }; // Gives space for axes and other margins
const height = 1500;
const width = 900;
const barWidth = 600;
const nowColor = '#EA4335';
const barStyle = {
  background: '#616161',
  textColor: 'white',
  width: barWidth,
  startPadding: 2,
  endPadding: 3,
  radius: 3
};

Verify DOM updates with browser devtools

Before we continue, we’ll get something to display on the page so we can see that everything is working:

// Create the SVG element
const svg = d3
  .create('svg')
  .attr('width', width)
  .attr('height', height);
// All further code additions will go just below this line

// Actually add the element to the page
document.body.append(svg.node());
// This part ^ always goes at the end of our index.js

When you open up the browser, you may not notice anything at first, but rest assured, there is a big SVG element on the page. You can confirm this by opening your browser devtools and inspecting the page elements.

This is a great way of debugging issues with D3 as your code will change SVG elements and their properties on your page.

Add scale functions

Next, we will add in our scale functions, which are often called x or xScale and y or yScale. These functions are used to map points in our data to a pixel value on our visualization.

For example, if we had data points for 1 through 100 mapped along the y-axis, then the y function would map 82 to be toward the top of our SVG object (82% the way up, to be exact).

In many cases, time would be mapped on the x-scale, but for our purposes, we are going to plot time vertically on the y-scale. For added simplicity, we are only going to build a single-day calendar, so we will not need to include an x-scale at all:

const yScale = d3
  .scaleTime()
  .domain([d3.min(dates), d3.max(dates)])
  .range([margin.top, height - margin.bottom]);

Notice that we pass our data range to domain and our pixel range to range. Also notice that for the range function, we first pass the margin.top, rather than the bottom. This is because SVG are drawn from top to bottom, so the 0 y-coordinate will be at the top.

Draw y-axis

We’ll use the scale mapping we just created later on, but first, let’s draw the actual y-axis itself:

const yAxis = d3
  .axisLeft()
  .ticks(24)
  .scale(yScale);
// We'll be using this svg variable throughout to append other elements to it
svg
  .append('g')
  .attr('transform', `translate(${margin.left},0)`)
  .attr('opacity', 0.5)
  .call(yAxis);

Custom tick styling

Then, since we are only showing one day for now, we can style the first and last ticks to just appear as midnight (12AM), with the intention of displaying the actual date somewhere else in the app later.

svg
  .selectAll('g.tick')
  .filter((d, i, ticks) => i === 0 || i === ticks.length - 1)
  .select('text')
  .text('12 AM');

Checking in …

So far, we’ve created an axis, specified that it will be on the left side of our chart, specified that we want 24 tick marks on the axis, and applied our scale. We then used our append function to actually put the axis into a g element within our svg element.



To confirm that this is all set up correctly, inspect the elements you have so far in your browser devtools. The app itself will look like this:

Add grid lines

Since this is a calendar after all, we’re going to add some grid lines for design.

In our previous steps, we set up our yAxis and set the ticks to be 24, one for each hour of the day. To set up our grid lines, we will do the same, using axisRight so that the ticks appear on the right side:

const gridLines = d3
  .axisRight()
  .ticks(24)
  .tickSize(barStyle.width) // even though they're "ticks" we've set them to be full-width
  .tickFormat('')
  .scale(yScale);

svg
  .append('g')
  .attr('transform', `translate(${margin.left},0)`)
  .attr('opacity', 0.3)
  .call(gridLines);

As with our earlier approach, we’ve placed our axis within a g element and appended that to our svg element. Check your browser devtools to see the DOM structure.

Use calendarEvents array

With our axis and gridlines in place, we can use the calendarEvents array we created to add in some calendar events.

To add events, we will use the [join](https://observablehq.com/@d3/selection-join) method to add g elements to our svg element. To do this, we will select all existing g elements that have the class of barGroup (hint: none exist yet) and join a new g element with that class for each calendarEvents item that exists.

Here is the code we will add to make that happen:

const barGroups = svg
  .selectAll('g.barGroup')
  .data(calendarEvents)
  .join('g')
    .attr('class', 'barGroup');

Now, when you look at the browser devtools, you should be able to see that we’ve got 3 empty g items (our events list) within our svg element.

Append elements to items

With our g items created and calendar event-bound, we are now able to append other elements to them.

For our demo, we will use .append and rect to create colored rectangles for our calendar events:

barGroups
  .append('rect')
  .attr('fill', d => d.background || barStyle.background)
  .attr('x', margin.left)
  .attr('y', d => yScale(new Date(d.timeFrom)) + barStyle.startPadding)
  .attr('height', d => {
    const startPoint = yScale(new Date(d.timeFrom));
    const endPoint = yScale(new Date(d.timeTo));
    return (
      endPoint - startPoint - barStyle.endPadding - barStyle.startPadding
    );
  })
  .attr('width', barStyle.width)
  .attr('rx', barStyle.radius);

In the example above, we’ve done some math to figure out where the rectangle should start vertically and how tall it should be. If you change the math, you’ll see how that affects the app. Just remember: if you do change the math, don’t forget to change it back.

Track current time

Using a similar approach, we can now add in a line for tracking the current time which will allow users can see where “now” is on the calendar:

// Since we've hardcoded all our events to be on November 11 of 2020, we'll do the same thing for the "now" date
const currentTimeDate = new Date(new Date(new Date().setDate(11)).setMonth(10)).setFullYear(2020);

barGroups
  .append('rect')
  .attr('fill', nowColor)
  .attr('x', margin.left)
  .attr('y', yScale(currentTimeDate) + barStyle.startPadding)
  .attr('height', 2)
  .attr('width', barStyle.width);

Note that we’ve once again used the margin variable that we declared at the start to make sure that our rectangles aren’t going to overlap our axis.

Label events

Finally, we will add labels for the events we have on our calendar. We’ll again use the append method, but this time we’ll add a text element to our barGroups variable:

barGroups
  .append('text')
  .attr('font-family', 'Roboto')
  .attr('font-size', 12)
  .attr('font-weight', 500)
  .attr('text-anchor', 'start')
  .attr('fill', barStyle.textColor)
  .attr('x', margin.left + 10)
  .attr('y', d => yScale(new Date(d.timeFrom)) + 20)
  .text(d => d.title);

D3 final product

Conclusion

And just like that, we’ve created our own calendar and events app using D3.js! Of course, this is only the beginning of the D3 calendar experiment. You could continue to make improvements from here, like expanding it to a weekly or monthly calendar, or allowing dynamic event creation.

While you may not typically reach for D3 to build a calendar app, this exercise has given us some insight into how scales, axes, and shapes can be drawn using D3. For future experimentation, use browser devtools to see how our changes to our D3 code affects, changes, and modifies SVG elements in the app.

Are you adding new JS libraries to improve performance or build new features? What if they’re doing the opposite?

There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.

LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.

https://logrocket.com/signup/

LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Build confidently — .

Brady Dowling Family first. People before programs. Basketball too. Find me on YouTube at ReadWriteExercise.

Leave a Reply