Skip to content

Instantly share code, notes, and snippets.

@chaitanyagurrapu
Last active December 19, 2015 19:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save chaitanyagurrapu/6007521 to your computer and use it in GitHub Desktop.
Save chaitanyagurrapu/6007521 to your computer and use it in GitHub Desktop.
A Google calendar like display for temporal data using D3.Js

A simple Google calendar like display to visualize temporal data. Use the forward and backwards buttons to move through the months.

Random data is being generated for each day and displayed in the appropriate cell of the grid. This examples shows pie charts for each day but a simpler and more common scenario might be to actually display text in each cell, such as appointments.

Days that fall in the previous or following month are shaded in a different color from the days that fall in the current month. Just like in the real Google calendar.

<!DOCTYPE html >
<head>
<title>D3 Sandbox</title>
</head>
<body>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<link href="Calendar.css" rel="stylesheet" type="text/css" />
<h3> D3 Calendar</h3>
<div>
<button id="back" name="back" ><</button>
<button id="forward" name="forward" >></button>
<label id="currentMonth">May 2013</label>
</div>
<div id="chart"></div>
<script type="text/javascript">
// Revealing module pattern to store some global data that will be shared between different functions.
var d3CalendarGlobals = function() {
var calendarWidth = 1380,
calendarHeight = 820,
gridXTranslation = 10,
gridYTranslation = 40,
cellColorForCurrentMonth = '#EAEAEA',
cellColorForPreviousMonth = '#FFFFFF',
counter = 0, // Counter is used to keep track of the number of "back" and "forward" button presses and to calculate the month to display.
currentMonth = new Date().getMonth(),
monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
datesGroup;
function publicCalendarWidth() { return calendarWidth; }
function publicCalendarHeight() { return calendarHeight; }
function publicGridXTranslation() { return gridXTranslation; }
function publicGridYTranslation() { return gridYTranslation; }
function publicGridWidth() { return calendarWidth - 10; }
function publicGridHeight() { return calendarHeight - 40; }
function publicCellWidth() { return publicGridWidth() / 7; }
function publicCellHeight() { return publicGridHeight() / 5; }
function publicGetDatesGroup() {
return datesGroup;
}
function publicSetDatesGroup(value) {
datesGroup = value;
}
function publicIncrementCounter() { counter = counter + 1; }
function publicDecrementCounter() { counter = counter - 1; }
function publicMonthToDisplay() {
var dateToDisplay = new Date();
// We use the counter that keep tracks of "back" and "forward" presses to get the month to display.
dateToDisplay.setMonth(currentMonth + counter);
return dateToDisplay.getMonth();
}
function publicMonthToDisplayAsText() { return monthNames[publicMonthToDisplay()]; }
function publicYearToDisplay() {
var dateToDisplay = new Date();
// We use the counter that keep tracks of "back" and "forward" presses to get the year to display.
dateToDisplay.setMonth(currentMonth + counter);
return dateToDisplay.getFullYear();
}
function publicGridCellPositions() {
// We store the top left positions of a 7 by 5 grid. These positions will be our reference points for drawing
// various objects such as the rectangular grids, the text indicating the date etc.
var cellPositions = [];
for (y = 0; y < 5; y++) {
for (x = 0; x < 7; x++) {
cellPositions.push([x * publicCellWidth(), y * publicCellHeight()]);
}
}
return cellPositions;
}
// This function generates all the days of the month. But since we have a 7 by 5 grid, we also need to get some of
// the days from the previous month and the next month. This way our grid will have all its cells filled. The days
// from the previous or the next month will have a different color though.
function publicDaysInMonth() {
var daysArray = [];
var firstDayOfTheWeek = new Date(publicYearToDisplay(), publicMonthToDisplay(), 1).getDay();
var daysInPreviousMonth = new Date(publicYearToDisplay(), publicMonthToDisplay(), 0).getDate();
// Lets say the first week of the current month is a Wednesday. Then we need to get 3 days from
// the end of the previous month. But we can't naively go from 29 - 31. We have to do it properly
// depending on whether the last month was one that had 31 days, 30 days or 28.
for (i = 1; i <= firstDayOfTheWeek; i++) {
daysArray.push([daysInPreviousMonth - firstDayOfTheWeek + i, cellColorForCurrentMonth]);
}
// These are all the days in the current month.
var daysInMonth = new Date(publicYearToDisplay(), publicMonthToDisplay() + 1, 0).getDate();
for (i = 1; i <= daysInMonth; i++) {
daysArray.push([i, cellColorForPreviousMonth]);
}
// Depending on how many days we have so far (from previous month and current), we will need
// to get some days from next month. We can do this naively though, since all months start on
// the 1st.
var daysRequiredFromNextMonth = 35 - daysArray.length;
for (i = 1; i <= daysRequiredFromNextMonth; i++) {
daysArray.push([i,cellColorForCurrentMonth]);
}
return daysArray.slice(0,35);
}
return {
calendarWidth: publicCalendarWidth(),
calendarHeight: publicCalendarHeight(),
gridXTranslation :publicGridXTranslation(),
gridYTranslation :publicGridYTranslation(),
gridWidth :publicGridWidth(),
gridHeight :publicGridHeight(),
cellWidth :publicCellWidth(),
cellHeight :publicCellHeight(),
getDatesGroup : publicGetDatesGroup,
setDatesGroup: publicSetDatesGroup,
incrementCounter : publicIncrementCounter,
decrementCounter : publicDecrementCounter,
monthToDisplay : publicMonthToDisplay(),
monthToDisplayAsText : publicMonthToDisplayAsText,
yearToDisplay: publicYearToDisplay,
gridCellPositions: publicGridCellPositions(),
daysInMonth : publicDaysInMonth
}
}();
$(document).ready( function (){
renderCalendarGrid();
renderDaysOfMonth();
$('#back').click(displayPreviousMonth);
$('#forward').click(displayNextMonth);
}
);
function displayPreviousMonth() {
// We keep track of user's "back" and "forward" presses in this counter
d3CalendarGlobals.decrementCounter();
renderDaysOfMonth();
}
function displayNextMonth(){
// We keep track of user's "back" and "forward" presses in this counter
d3CalendarGlobals.incrementCounter();
renderDaysOfMonth();
}
// This function is responsible for rendering the days of the month in the grid.
function renderDaysOfMonth(month, year) {
$('#currentMonth').text(d3CalendarGlobals.monthToDisplayAsText() + ' ' + d3CalendarGlobals.yearToDisplay());
// We get the days for the month we need to display based on the number of times the user has pressed
// the forward or backward button.
var daysInMonthToDisplay = d3CalendarGlobals.daysInMonth();
var cellPositions = d3CalendarGlobals.gridCellPositions;
// All text elements representing the dates in the month are grouped together in the "datesGroup" element by the initalizing
// function below. The initializing function is also responsible for drawing the rectangles that make up the grid.
d3CalendarGlobals.datesGroup
.selectAll("text")
.data(daysInMonthToDisplay)
.attr("x", function (d,i) { return cellPositions[i][0]; })
.attr("y", function (d,i) { return cellPositions[i][1]; })
.attr("dx", 20) // right padding
.attr("dy", 20) // vertical alignment : middle
.attr("transform", "translate(" + d3CalendarGlobals.gridXTranslation + "," + d3CalendarGlobals.gridYTranslation + ")")
.text(function (d) { return d[0]; }); // Render text for the day of the week
d3CalendarGlobals.calendar
.selectAll("rect")
.data(daysInMonthToDisplay)
// Here we change the color depending on whether the day is in the current month, the previous month or the next month.
// The function that generates the dates for any given month will also specify the colors for days that are not part of the
// current month. We just have to use it to fill the rectangle
.style("fill", function (d) { return d[1]; });
drawGraphsForMonthlyData();
}
function drawGraphsForMonthlyData() {
// Get some random data
var data = getDataForMonth();
// Set up variables required to draw a pie chart
var outerRadius = d3CalendarGlobals.cellWidth / 3;
var innerRadius = 0;
var pie = d3.layout.pie();
var color = d3.scale.category10();
var arc = d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius);
// We need to index and group the pie charts and slices generated so that they can be rendered in
// the appropriate cells. To do that, we call D3's 'pie' function of each of the data elements.
var indexedPieData = [];
for (i = 0; i < data.length; i++) {
var pieSlices = pie(data[i]);
// This loop is to store an index (j) for each of the slices of a given pie chart. Two different charts
// on two different days will have the the same set of numbers for slices (eg: 0,1,2). This will help us
// pick the same colors for the slices for two independent charts. Otherwise, the colors of the slices
// will be different each day.
for (j = 0; j < pieSlices.length; j++) {
indexedPieData.push([pieSlices[j], i, j]);
}
}
var cellPositions = d3CalendarGlobals.gridCellPositions;
d3CalendarGlobals.chartsGroup
.selectAll("g.arc")
.remove();
var arcs = d3CalendarGlobals.chartsGroup.selectAll("g.arc")
// use the indexed data so that each pie chart can be draw in a different cell and therefore for a different day
.data(indexedPieData)
.enter()
.append("g")
.attr("class", "arc")
.attr("transform", function (d) {
// This is where we use the index here to translate the pie chart and rendere it in the appropriate cell.
// Normally, the chart would be squashed up against the top left of the cell, obscuring the text that shows the day of the month.
// We use the gridXTranslation and gridYTranslation and multiply it by a factor to move it to the center of the cell. There is probably
// a better way of doing this though.
var currentDataIndex = d[1];
return "translate(" + (outerRadius + d3CalendarGlobals.gridXTranslation * 5 + cellPositions[currentDataIndex][0]) + ", " + (outerRadius + d3CalendarGlobals.gridYTranslation * 1.25 + cellPositions[currentDataIndex][1]) + ")";
});
arcs.append("path")
.attr("fill", function (d, i) {
// The color is generated using the second index. Each slice of the pie is given a fixed number. This applies to all charts (see the indexing loop above).
// This way, by using the index we can generate teh same colors for each of the slices for different charts on different days.
return color(d[2]);
})
.attr("d", function (d, i) {
// Standard functions for drawing a pie charts in D3.
return arc(d[0]);
});
arcs.append("text")
.attr("transform", function (d,i) {
// Standard functions for drawing a pie charts in D3.
return "translate(" + arc.centroid(d[0]) + ")";
})
.attr("text-anchor", "middle")
.text(function(d,i) {
return d[0].value;
});
}
// Generates some random data that can be used to draw pie charts.
function getDataForMonth() {
var randomData = [];
for (var i = 0; i < 35; i++) {
randomData.push([Math.floor(Math.random()*100),Math.floor(Math.random()*100),Math.floor(Math.random()*100)]);
}
return randomData;
}
// This is the initializing function. It adds an svg element, draws a set of rectangles to form the calendar grid,
// puts text in each cell representing the date and does the initial rendering of the pie charts.
function renderCalendarGrid(month, year) {
// Add the svg element.
d3CalendarGlobals.calendar = d3.select("#chart")
.append("svg")
.attr("class", "calendar")
.attr("width", d3CalendarGlobals.calendarWidth )
.attr("height", d3CalendarGlobals.calendarHeight)
.append("g");
// Cell positions are generated and stored globally because they are used by other functions as a reference to render different things.
var cellPositions = d3CalendarGlobals.gridCellPositions;
// Draw rectangles at the appropriate postions, starting from the top left corner. Since we want to leave some room for the heading and buttons,
// use the gridXTranslation and gridYTranslation variables.
d3CalendarGlobals.calendar.selectAll("rect")
.data(cellPositions)
.enter()
.append("rect")
.attr("x", function (d) { return d[0]; })
.attr("y", function (d) { return d[1]; })
.attr("width", d3CalendarGlobals.cellWidth)
.attr("height", d3CalendarGlobals.cellHeight)
.style("stroke", "#555")
.style("fill", "white")
.attr("transform", "translate(" + d3CalendarGlobals.gridXTranslation + "," + d3CalendarGlobals.gridYTranslation + ")");
var daysOfTheWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
// This adds the day of the week headings on top of the grid
d3CalendarGlobals.calendar.selectAll("headers")
.data([0, 1, 2, 3, 4, 5, 6])
.enter().append("text")
.attr("x", function (d) { return cellPositions[d][0]; })
.attr("y", function (d) { return cellPositions[d][1]; })
.attr("dx", d3CalendarGlobals.gridXTranslation + 5) // right padding
.attr("dy", 30) // vertical alignment : middle
.text(function (d) { return daysOfTheWeek[d] });
// The intial rendering of the dates for the current mont inside each of the cells in the grid. We create a named group ("datesGroup"),
// and add our dates to this group. This group is also stored globally. Later on, when the the user presses the back and forward buttons
// to navigate between the months, we clear and re add the new text elements to this group
d3CalendarGlobals.datesGroup = d3CalendarGlobals.calendar.append("svg:g");
var daysInMonthToDisplay = d3CalendarGlobals.daysInMonth();
d3CalendarGlobals.datesGroup
.selectAll("daysText")
.data(daysInMonthToDisplay)
.enter()
.append("text")
.attr("x", function (d, i) { return cellPositions[i][0]; })
.attr("y", function (d, i) { return cellPositions[i][1]; })
.attr("dx", 20) // right padding
.attr("dy", 20) // vertical alignment : middle
.attr("transform", "translate(" + d3CalendarGlobals.gridXTranslation + "," + d3CalendarGlobals.gridYTranslation + ")")
.text(function (d) { return d[0]; });
// Create a new svg group to store the chart elements and store it globally. Again, as the user navigates through the months by pressing
// the "back" and "forward" buttons on the page, we clear the chart elements from this group and re add them again.
d3CalendarGlobals.chartsGroup = d3CalendarGlobals.calendar.append("svg:g");
// Call the function to draw the charts in the cells. This will be called again each time the user presses the forward or backward buttons.
drawGraphsForMonthlyData();
}
</script>
</body>
</html>
@cfmonk
Copy link

cfmonk commented Sep 10, 2014

Great bit of code, thank you.

I might have been being dim but I'm implementing this using non-random data and when I was redoing the data filter on month change I couldn't get at an updated monthToDisplay, I hacked a solution because I couldn't work out what's wrong (it's late) by adding myMonthToDisplay: publicMonthToDisplay, to the return object in d3calendarGlobalVars.... and then calling that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment