Skip to content

Instantly share code, notes, and snippets.

@mwdchang
Created March 24, 2016 20:53
Show Gist options
  • Select an option

  • Save mwdchang/6bc303efc1feb7afcf7c to your computer and use it in GitHub Desktop.

Select an option

Save mwdchang/6bc303efc1feb7afcf7c to your computer and use it in GitHub Desktop.
Zoomable Calendar
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.12/d3.js"></script>
<style>
body {
font-family: Tahoma;
font-size: 0.7rem;
}
.month {
fill: none;
stroke: #000;
stroke-opacity: 0.6;
stroke-width: 1px;
}
.year {
fill: none;
fill-opacity: 0.3;
stroke: #F00;
stroke-width: 1.5px;
}
.day {
fill: #fff;
stroke: #BBB;
stroke-width: 0.5px;
}
.axis {
font-size: 0.7rem;
stroke-width: 1px;
stroke: #000;
}
.axis path,
.axis line {
fill:none;
stroke-width: 2px;
stroke: #000;
}
.axis text {
stroke: none;
font-size: 0.8rem;
}
body > p {
font-size: 1.0rem;
}
</style>
</head>
<body>
<p>Zoomable calendar. Pan and Zoom with the mouse, each calendar year is aligned with the time-scale. Note however the individual weeks
are not necessarily aligned as the months have different number of days.
</p>
<svg id="canvas" width="1600" height="500"></svg>
</body>
<script>
var svg = d3.select('svg');
var domainExtent = [new Date(2014, 0, 1), new Date(2018, 0, 1)];
var timeScale = d3.time.scale().domain(domainExtent).range([30, 1500]);
var yearOffsets = _getYearOffSet(2014, 2018);
var boundary = _computeBoundary(2014, 2018);
var lookup = {};
var dateRange = d3.time.days(new Date(2014, 0, 1), new Date(2019, 0, 1));
dateRange.forEach(function(d, i) {
lookup[d] = 0.8*Math.abs(
Math.cos(i * Math.PI / 180)
) + 0.2*Math.random();
});
var timeAxis = d3.svg.axis()
.scale(timeScale)
.orient('top')
.ticks(4)
.tickSize(5)
.tickPadding(5)
.outerTickSize(0);
var topAxis = svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0, 30)')
.call(timeAxis.orient('top'));
var bottomAxis = svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0, 230)')
.call(timeAxis.orient('bottom'));
var zoom = d3.behavior.zoom()
.scaleExtent([1, 105])
.x(this.timeScale)
.on('zoom', () => {
if (! d3.event['sourceEvent']) return;
d3.selectAll('.day, .month, .year').remove();
topAxis.call(timeAxis.orient('top'));
bottomAxis.call(timeAxis.orient('bottom'));
yearOffsets = _getYearOffSet(2014, 2018);
boundary = _computeBoundary(2014, 2018);
renderCalendarMonthDays(2014);
renderCalendarMonthDays(2015);
renderCalendarMonthDays(2016);
renderCalendarMonthDays(2017);
renderCalendarMonthOutline(2014);
renderCalendarMonthOutline(2015);
renderCalendarMonthOutline(2016);
renderCalendarMonthOutline(2017);
renderYearOutline(2014, 2018);
d3.selectAll('.day, .month, .year').style('pointer-events', 'none');
});
svg.append('rect')
.attr('transform', 'translate(0, 0)')
.attr('x', 0)
.attr('y', 0)
.attr('height', 250)
.attr('width', (1500))
.style('fill', '#FFF')
.style('fill-opacity', 0.4)
.style({
'stroke': 'none',
'touch-action': 'none',
'-ms-touch-action': 'none'
})
.call(zoom);
/** See: http://bl.ocks.org/mbostock/4063318 **/
var cellH = 25;
renderCalendarMonthDays(2014);
renderCalendarMonthDays(2015);
renderCalendarMonthDays(2016);
renderCalendarMonthDays(2017);
renderCalendarMonthOutline(2014);
renderCalendarMonthOutline(2015);
renderCalendarMonthOutline(2016);
renderCalendarMonthOutline(2017);
renderYearOutline(2014, 2018);
d3.selectAll('.day, .month, .year').style('pointer-events', 'none');
function renderYearOutline(yearStart, yearEnd) {
svg.selectAll('.year')
.data(
d3.time.years(
new Date(yearStart, 0, 1),
new Date(yearEnd, 0, 1)
)
)
.enter()
.append('path')
.attr('transform', function(d) {
var bound = boundary[d.getFullYear()];
return 'translate(' + bound.start + ', 40)';
})
.classed('year', true)
.attr('d', function(d) {
var bound = boundary[d.getFullYear()];
cellW = bound.cellW;
return yearPath(d);
});
}
function renderCalendarMonthOutline(year) {
var bound = boundary[year];
cellW = bound.cellW;
svg.selectAll('.month' + year)
.data(
d3.time.months(
new Date(year, 0, 1),
new Date(year+1, 0, 1)
)
)
.enter()
.append('path')
.attr('transform', 'translate(' + bound.start + ', 40)')
.classed('month', true)
.classed('month'+year, true)
.attr('d', monthPath);
}
function renderCalendarMonthDays(year) {
var bound = boundary[year];
cellW = bound.cellW;
var rect = svg.selectAll(".day" + year)
.data(function(d) {
return d3.time.days(new Date(year, 0, 1), new Date(year+1, 0, 1));
})
.enter()
.append("rect")
.attr('transform', function(d) {
return 'translate(' + bound.start + ', 40)';
})
.attr("class", "day " + "day"+year)
.attr("width", cellW)
.attr("height", cellH)
.attr("x", function(d) { return d3.time.weekOfYear(d) * cellW; })
.attr("y", function(d) { return d.getDay() * cellH; });
rect.style('fill-opacity', 0.75).style('fill', function(d, i) {
var val = lookup[d];
if (d.getMonth() === 2 && d.getDate() === 14) {
return d3.rgb(155, 28, 45);
} else {
return d3.rgb(255-(val*124), 120+(val*115), 50+val*65);
}
});
}
function _computeBoundary(startYear, endYear) {
var result = {};
for (var year = startYear; year <= endYear; year++) {
var _startYear = new Date(year, 0, 1);
var start = timeScale(new Date(year, 0, 1));
var end = timeScale(new Date(year+1, 0, 1));
var endWeek = d3.time.weekOfYear(new Date(year, 11, 31));
var endDay = (new Date(year, 11, 31)).getDay();
var cellW = (end - start) / endWeek;
var gap = +(timeScale(new Date(year, 0 ,2)) - timeScale(new Date(year, 0, 1)));
if (endDay === 6 && year > startYear) {
end -= cellW;
cellW = (end - start) / endWeek;
}
result[year] = {
cellW: cellW,
start: start
};
}
return result;
}
/**
* In a calendar view, because a year can end on any day of the week,
* we need to calculate and carry on any offsets to map it to the continuous scale
*/
function _getYearOffSet(startYear, endYear) {
var result = {};
var offset = 0;
for (var year = startYear; year <= endYear; year++) {
var _startYear = new Date(year, 0, 1);
var _endYear = new Date(year, 11, 31);
var start = timeScale(_startYear);
var end = timeScale(_endYear);
var endWeek = d3.time.weekOfYear(_endYear);
var startDay = _startYear.getDay();
var cellW = (end - start) / endWeek;
var gap = +(timeScale(new Date(year, 0 ,2)) - timeScale(new Date(year, 0, 1)));
if (startDay === 0 && year > startYear) {
//offset += cellW;
} else if (startDay !== 0 && year > startYear) {
// console.log('gap', year, gap);
offset -= cellW;
}
result[year] = offset;
}
return result;
}
/*
svg.selectAll('.month-label')
.data(
d3.time.months(
new Date(2015, 0, 1),
new Date(2016, 0, 1)
)
)
.enter()
.append('text')
.classed('month-label', true)
.attr('y', 35)
.attr('x', function(d) {
return 10 + d3.time.weekOfYear(d) * cellW + cellW;
})
.text(d3.time.format('%b-%y'));
*/
function yearPath(t0) {
var t1 = new Date(t0.getFullYear()+1, 0, 0),
d0 = t0.getDay(), w0 = d3.time.weekOfYear(t0),
d1 = t1.getDay(), w1 = d3.time.weekOfYear(t1);
return "M" + (w0 + 1) * cellW + "," + d0 * cellH
+ "H" + w0 * cellW + "V" + 7 * cellH
+ "H" + w1 * cellW + "V" + (d1 + 1) * cellH
+ "H" + (w1 + 1) * cellW + "V" + 0
+ "H" + (w0 + 1) * cellW + "Z";
}
function monthPath(t0) {
var t1 = new Date(t0.getFullYear(), t0.getMonth() + 1, 0),
d0 = t0.getDay(), w0 = d3.time.weekOfYear(t0),
d1 = t1.getDay(), w1 = d3.time.weekOfYear(t1);
return "M" + (w0 + 1) * cellW + "," + d0 * cellH
+ "H" + w0 * cellW + "V" + 7 * cellH
+ "H" + w1 * cellW + "V" + (d1 + 1) * cellH
+ "H" + (w1 + 1) * cellW + "V" + 0
+ "H" + (w0 + 1) * cellW + "Z";
}
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment