angular-calendar-heatmap
Version:
Angular directive for d3.js calendar heatmap graph.
1,346 lines (1,197 loc) • 60.6 kB
JavaScript
'use strict';
/* globals d3 */
angular.module('g1b.calendar-heatmap', []).
directive('calendarHeatmap', ['$window', function ($window) {
return {
restrict: 'E',
scope: {
data: '=',
color: '=?',
overview: '=?',
handler: '=?'
},
replace: true,
template: '<div class="calendar-heatmap"></div>',
link: function (scope, element) {
// Defaults
var gutter = 5;
var item_gutter = 1;
var width = 1000;
var height = 200;
var item_size = 10;
var label_padding = 40;
var max_block_height = 20;
var transition_duration = 500;
var in_transition = false;
// Tooltip defaults
var tooltip_width = 250;
var tooltip_padding = 15;
// Initialize current overview type and history
scope.overview = scope.overview || 'global';
scope.history = ['global'];
scope.selected = {};
// Initialize svg element
var svg = d3.select(element[0])
.append('svg')
.attr('class', 'svg');
// Initialize main svg elements
var items = svg.append('g');
var labels = svg.append('g');
var buttons = svg.append('g');
// Add tooltip to the same element as main svg
var tooltip = d3.select(element[0]).append('div')
.attr('class', 'heatmap-tooltip')
.style('opacity', 0);
var getNumberOfWeeks = function () {
var dayIndex = Math.round((moment() - moment().subtract(1, 'year').startOf('week')) / 86400000);
var colIndex = Math.trunc(dayIndex / 7);
var numWeeks = colIndex + 1;
return numWeeks;
}
scope.$watch(function () {
return element[0].clientWidth;
}, function ( w ) {
if ( !w ) { return; }
width = w < 1000 ? 1000 : w;
item_size = ((width - label_padding) / getNumberOfWeeks() - gutter);
height = label_padding + 7 * (item_size + gutter);
svg.attr({'width': width, 'height': height});
if ( !!scope.data && !!scope.data[0].summary ) {
scope.drawChart();
}
});
angular.element($window).bind('resize', function () {
scope.$apply();
});
// Watch for data availability
scope.$watch('data', function (data) {
if ( !data ) { return; }
// Get daily summary if that was not provided
if ( !data[0].summary ) {
data.map(function (d) {
var summary = d.details.reduce( function(uniques, project) {
if ( !uniques[project.name] ) {
uniques[project.name] = {
'value': project.value
};
} else {
uniques[project.name].value += project.value;
}
return uniques;
}, {});
var unsorted_summary = Object.keys(summary).map(function (key) {
return {
'name': key,
'value': summary[key].value
};
});
d.summary = unsorted_summary.sort(function (a, b) {
return b.value - a.value;
});
return d;
});
}
// Draw the chart
scope.drawChart();
});
/**
* Draw the chart based on the current overview type
*/
scope.drawChart = function () {
if ( !scope.data ) { return; }
if ( scope.overview === 'global' ) {
scope.drawGlobalOverview();
} else if ( scope.overview === 'year' ) {
scope.drawYearOverview();
} else if ( scope.overview === 'month' ) {
scope.drawMonthOverview();
} else if ( scope.overview === 'week' ) {
scope.drawWeekOverview();
} else if ( scope.overview === 'day' ) {
scope.drawDayOverview();
}
};
/**
* Draw global overview (multiple years)
*/
scope.drawGlobalOverview = function () {
// Add current overview to the history
if ( scope.history[scope.history.length-1] !== scope.overview ) {
scope.history.push(scope.overview);
}
// Define start and end of the dataset
var start = moment(scope.data[0].date).startOf('year');
var end = moment(scope.data[scope.data.length-1].date).endOf('year');
// Define array of years and total values
var year_data = d3.time.years(start, end).map(function (d) {
var date = moment(d);
return {
'date': date,
'total': scope.data.reduce(function (prev, current) {
if ( moment(current.date).year() === date.year() ) {
prev += current.total;
}
return prev;
}, 0),
'summary': function () {
var summary = scope.data.reduce(function (summary, d) {
if ( moment(d.date).year() === date.year() ) {
for ( var i = 0; i < d.summary.length; i++ ) {
if ( !summary[d.summary[i].name] ) {
summary[d.summary[i].name] = {
'value': d.summary[i].value,
};
} else {
summary[d.summary[i].name].value += d.summary[i].value;
}
}
}
return summary;
}, {});
var unsorted_summary = Object.keys(summary).map(function (key) {
return {
'name': key,
'value': summary[key].value
};
});
return unsorted_summary.sort(function (a, b) {
return b.value - a.value;
});
}(),
};
});
// Calculate max value of all the years in the dataset
var max_value = d3.max(year_data, function (d) {
return d.total;
});
// Define year labels and axis
var year_labels = d3.time.years(start, end).map(function (d) {
return moment(d);
});
var yearScale = d3.scale.ordinal()
.rangeRoundBands([0, width], 0.05)
.domain(year_labels.map(function(d) {
return d.year();
}));
// Add global data items to the overview
items.selectAll('.item-block-year').remove();
var item_block = items.selectAll('.item-block-year')
.data(year_data)
.enter()
.append('rect')
.attr('class', 'item item-block-year')
.attr('width', function () {
return (width - label_padding) / year_labels.length - gutter * 5;
})
.attr('height', function () {
return height - label_padding;
})
.attr('transform', function (d) {
return 'translate(' + yearScale(d.date.year()) + ',' + tooltip_padding * 2 + ')';
})
.attr('fill', function (d) {
var color = d3.scale.linear()
.range(['#ffffff', scope.color || '#ff4500'])
.domain([-0.15 * max_value, max_value]);
return color(d.total) || '#ff4500';
})
.on('click', function (d) {
if ( scope.in_transition ) { return; }
// Set in_transition flag
scope.in_transition = true;
// Set selected date to the one clicked on
scope.selected = d;
// Hide tooltip
scope.hideTooltip();
// Remove all global overview related items and labels
scope.removeGlobalOverview();
// Redraw the chart
scope.overview = 'year';
scope.drawChart();
})
.style('opacity', 0)
.on('mouseover', function(d) {
if ( scope.in_transition ) { return; }
// Construct tooltip
var tooltip_html = '';
tooltip_html += '<div><span><strong>Total time tracked:</strong></span>';
var sec = parseInt(d.total, 10);
var days = Math.floor(sec / 86400);
if ( days > 0 ) {
tooltip_html += '<span>' + (days === 1 ? '1 day' : days + ' days') + '</span></div>';
}
var hours = Math.floor((sec - (days * 86400)) / 3600);
if ( hours > 0 ) {
if ( days > 0 ) {
tooltip_html += '<div><span></span><span>' + (hours === 1 ? '1 hour' : hours + ' hours') + '</span></div>';
} else {
tooltip_html += '<span>' + (hours === 1 ? '1 hour' : hours + ' hours') + '</span></div>';
}
}
var minutes = Math.floor((sec - (days * 86400) - (hours * 3600)) / 60);
if ( minutes > 0 ) {
if ( days > 0 || hours > 0 ) {
tooltip_html += '<div><span></span><span>' + (minutes === 1 ? '1 minute' : minutes + ' minutes') + '</span></div>';
} else {
tooltip_html += '<span>' + (minutes === 1 ? '1 minute' : minutes + ' minutes') + '</span></div>';
}
}
tooltip_html += '<br />';
// Add summary to the tooltip
if ( d.summary.length <= 5 ) {
for ( var i = 0; i < d.summary.length; i++ ) {
tooltip_html += '<div><span><strong>' + d.summary[i].name + '</strong></span>';
tooltip_html += '<span>' + scope.formatTime(d.summary[i].value) + '</span></div>';
};
} else {
for ( var i = 0; i < 5; i++ ) {
tooltip_html += '<div><span><strong>' + d.summary[i].name + '</strong></span>';
tooltip_html += '<span>' + scope.formatTime(d.summary[i].value) + '</span></div>';
};
tooltip_html += '<br />';
var other_projects_sum = 0;
for ( var i = 5; i < d.summary.length; i++ ) {
other_projects_sum =+ d.summary[i].value;
};
tooltip_html += '<div><span><strong>Other:</strong></span>';
tooltip_html += '<span>' + scope.formatTime(other_projects_sum) + '</span></div>';
}
// Calculate tooltip position
var x = yearScale(d.date.year()) + tooltip_padding * 2;
while ( width - x < (tooltip_width + tooltip_padding * 5) ) {
x -= 10;
}
var y = tooltip_padding * 3;
// Show tooltip
tooltip.html(tooltip_html)
.style('left', x + 'px')
.style('top', y + 'px')
.transition()
.duration(transition_duration / 2)
.ease('ease-in')
.style('opacity', 1);
})
.on('mouseout', function () {
if ( scope.in_transition ) { return; }
scope.hideTooltip();
})
.transition()
.delay(function (d, i) {
return transition_duration * (i + 1) / 10;
})
.duration(function () {
return transition_duration;
})
.ease('ease-in')
.style('opacity', 1)
.call(function (transition, callback) {
if ( transition.empty() ) {
callback();
}
var n = 0;
transition
.each(function() { ++n; })
.each('end', function() {
if ( !--n ) {
callback.apply(this, arguments);
}
});
}, function() {
scope.in_transition = false;
});
// Add year labels
labels.selectAll('.label-year').remove();
labels.selectAll('.label-year')
.data(year_labels)
.enter()
.append('text')
.attr('class', 'label label-year')
.attr('font-size', function () {
return Math.floor(label_padding / 3) + 'px';
})
.text(function (d) {
return d.year();
})
.attr('x', function (d) {
return yearScale(d.year());
})
.attr('y', label_padding / 2)
.on('mouseenter', function (year_label) {
if ( scope.in_transition ) { return; }
items.selectAll('.item-block-year')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', function (d) {
return ( moment(d.date).year() === year_label.year() ) ? 1 : 0.1;
});
})
.on('mouseout', function () {
if ( scope.in_transition ) { return; }
items.selectAll('.item-block-year')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', 1);
})
.on('click', function (d) {
if ( scope.in_transition ) { return; }
// Set in_transition flag
scope.in_transition = true;
// Set selected year to the one clicked on
scope.selected = { date: d };
// Hide tooltip
scope.hideTooltip();
// Remove all global overview related items and labels
scope.removeGlobalOverview();
// Redraw the chart
scope.overview = 'year';
scope.drawChart();
});
},
/**
* Draw year overview
*/
scope.drawYearOverview = function () {
// Add current overview to the history
if ( scope.history[scope.history.length-1] !== scope.overview ) {
scope.history.push(scope.overview);
}
var year_ago = moment().startOf('day').subtract(1, 'year');
var max_value = d3.max(scope.data, function (d) {
return d.total;
});
// Define start and end date of the selected year
var start_of_year = moment(scope.selected.date).startOf('year');
var end_of_year = moment(scope.selected.date).endOf('year');
// Filter data down to the selected year
var year_data = scope.data.filter(function (d) {
return start_of_year <= moment(d.date) && moment(d.date) < end_of_year;
});
// Calculate max value of the year data
var max_value = d3.max(year_data, function (d) {
return d.total;
});
var color = d3.scale.linear()
.range(['#ffffff', scope.color || '#ff4500'])
.domain([-0.15 * max_value, max_value]);
var calcItemX = function (d) {
var date = moment(d.date);
var dayIndex = Math.round((date - moment(start_of_year).startOf('week')) / 86400000);
var colIndex = Math.trunc(dayIndex / 7);
return colIndex * (item_size + gutter) + label_padding;
};
var calcItemY = function (d) {
return label_padding + moment(d.date).weekday() * (item_size + gutter);
};
var calcItemSize = function (d) {
if ( max_value <= 0 ) { return item_size; }
return item_size * 0.75 + (item_size * d.total / max_value) * 0.25;
};
items.selectAll('.item-circle').remove();
items.selectAll('.item-circle')
.data(year_data)
.enter()
.append('rect')
.attr('class', 'item item-circle')
.style('opacity', 0)
.attr('x', function (d) {
return calcItemX(d) + (item_size - calcItemSize(d)) / 2;
})
.attr('y', function (d) {
return calcItemY(d) + (item_size - calcItemSize(d)) / 2;
})
.attr('rx', function (d) {
return calcItemSize(d);
})
.attr('ry', function (d) {
return calcItemSize(d);
})
.attr('width', function (d) {
return calcItemSize(d);
})
.attr('height', function (d) {
return calcItemSize(d);
})
.attr('fill', function (d) {
return ( d.total > 0 ) ? color(d.total) : 'transparent';
})
.on('click', function (d) {
if ( in_transition ) { return; }
// Don't transition if there is no data to show
if ( d.total === 0 ) { return; }
in_transition = true;
// Set selected date to the one clicked on
scope.selected = d;
// Hide tooltip
scope.hideTooltip();
// Remove all year overview related items and labels
scope.removeYearOverview();
// Redraw the chart
scope.overview = 'day';
scope.drawChart();
})
.on('mouseover', function (d) {
if ( in_transition ) { return; }
// Pulsating animation
var circle = d3.select(this);
(function repeat() {
circle = circle.transition()
.duration(transition_duration)
.ease('ease-in')
.attr('x', function (d) {
return calcItemX(d) - (item_size * 1.1 - item_size) / 2;
})
.attr('y', function (d) {
return calcItemY(d) - (item_size * 1.1 - item_size) / 2;
})
.attr('width', item_size * 1.1)
.attr('height', item_size * 1.1)
.transition()
.duration(transition_duration)
.ease('ease-in')
.attr('x', function (d) {
return calcItemX(d) + (item_size - calcItemSize(d)) / 2;
})
.attr('y', function (d) {
return calcItemY(d) + (item_size - calcItemSize(d)) / 2;
})
.attr('width', function (d) {
return calcItemSize(d);
})
.attr('height', function (d) {
return calcItemSize(d);
})
.each('end', repeat);
})();
// Construct tooltip
var tooltip_html = '';
tooltip_html += '<div class="header"><strong>' + (d.total ? scope.formatTime(d.total) : 'No time') + ' tracked</strong></div>';
tooltip_html += '<div>on ' + moment(d.date).format('dddd, MMM Do YYYY') + '</div><br>';
// Add summary to the tooltip
angular.forEach(d.summary, function (d) {
tooltip_html += '<div><span><strong>' + d.name + '</strong></span>';
tooltip_html += '<span>' + scope.formatTime(d.value) + '</span></div>';
});
// Calculate tooltip position
var x = calcItemX(d) + item_size;
if ( width - x < (tooltip_width + tooltip_padding * 3) ) {
x -= tooltip_width + tooltip_padding * 2;
}
var y = calcItemY(d) + item_size;
// Show tooltip
tooltip.html(tooltip_html)
.style('left', x + 'px')
.style('top', y + 'px')
.transition()
.duration(transition_duration / 2)
.ease('ease-in')
.style('opacity', 1);
})
.on('mouseout', function () {
if ( in_transition ) { return; }
// Set circle radius back to what it's supposed to be
d3.select(this).transition()
.duration(transition_duration / 2)
.ease('ease-in')
.attr('x', function (d) {
return calcItemX(d) + (item_size - calcItemSize(d)) / 2;
})
.attr('y', function (d) {
return calcItemY(d) + (item_size - calcItemSize(d)) / 2;
})
.attr('width', function (d) {
return calcItemSize(d);
})
.attr('height', function (d) {
return calcItemSize(d);
});
// Hide tooltip
scope.hideTooltip();
})
.transition()
.delay( function () {
return (Math.cos(Math.PI * Math.random()) + 1) * transition_duration;
})
.duration(function () {
return transition_duration;
})
.ease('ease-in')
.style('opacity', 1)
.call(function (transition, callback) {
if ( transition.empty() ) {
callback();
}
var n = 0;
transition
.each(function() { ++n; })
.each('end', function() {
if ( !--n ) {
callback.apply(this, arguments);
}
});
}, function() {
in_transition = false;
});
// Add month labels
var month_labels = d3.time.months(start_of_year, end_of_year);
var monthScale = d3.scale.linear()
.range([0, width])
.domain([0, month_labels.length]);
labels.selectAll('.label-month').remove();
labels.selectAll('.label-month')
.data(month_labels)
.enter()
.append('text')
.attr('class', 'label label-month')
.attr('font-size', function () {
return Math.floor(label_padding / 3) + 'px';
})
.text(function (d) {
return d.toLocaleDateString('en-us', {month: 'short'});
})
.attr('x', function (d, i) {
return monthScale(i) + (monthScale(i) - monthScale(i-1)) / 2;
})
.attr('y', label_padding / 2)
.on('mouseenter', function (d) {
if ( in_transition ) { return; }
var selected_month = moment(d);
items.selectAll('.item-circle')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', function (d) {
return moment(d.date).isSame(selected_month, 'month') ? 1 : 0.1;
});
})
.on('mouseout', function () {
if ( in_transition ) { return; }
items.selectAll('.item-circle')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', 1);
})
.on('click', function (d) {
if ( in_transition ) { return; }
// Check month data
var month_data = scope.data.filter(function (e) {
return moment(d).startOf('month') <= moment(e.date) && moment(e.date) < moment(d).endOf('month');
});
// Don't transition if there is no data to show
if ( !month_data.length ) { return; }
// Set selected month to the one clicked on
scope.selected = {date: d};
in_transition = true;
// Hide tooltip
scope.hideTooltip();
// Remove all year overview related items and labels
scope.removeYearOverview();
// Redraw the chart
scope.overview = 'month';
scope.drawChart();
});
// Add day labels
var day_labels = d3.time.days(moment().startOf('week'), moment().endOf('week'));
var dayScale = d3.scale.ordinal()
.rangeRoundBands([label_padding, height])
.domain(day_labels.map(function (d) {
return moment(d).weekday();
}));
labels.selectAll('.label-day').remove();
labels.selectAll('.label-day')
.data(day_labels)
.enter()
.append('text')
.attr('class', 'label label-day')
.attr('x', label_padding / 3)
.attr('y', function (d, i) {
return dayScale(i) + dayScale.rangeBand() / 1.75;
})
.style('text-anchor', 'left')
.attr('font-size', function () {
return Math.floor(label_padding / 3) + 'px';
})
.text(function (d) {
return moment(d).format('dddd')[0];
})
.on('mouseenter', function (d) {
if ( in_transition ) { return; }
var selected_day = moment(d);
items.selectAll('.item-circle')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', function (d) {
return (moment(d.date).day() === selected_day.day()) ? 1 : 0.1;
});
})
.on('mouseout', function () {
if ( in_transition ) { return; }
items.selectAll('.item-circle')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', 1);
});
// Add button to switch back to previous overview
scope.drawButton();
};
/**
* Draw month overview
*/
scope.drawMonthOverview = function () {
// Add current overview to the history
if ( scope.history[scope.history.length-1] !== scope.overview ) {
scope.history.push(scope.overview);
}
// Define beginning and end of the month
var start_of_month = moment(scope.selected.date).startOf('month');
var end_of_month = moment(scope.selected.date).endOf('month');
// Filter data down to the selected month
var month_data = scope.data.filter(function (d) {
return start_of_month <= moment(d.date) && moment(d.date) < end_of_month;
});
var max_value = d3.max(month_data, function (d) {
return d3.max(d.summary, function (d) {
return d.value;
});
});
// Define day labels and axis
var day_labels = d3.time.days(moment().startOf('week'), moment().endOf('week'));
var dayScale = d3.scale.ordinal()
.rangeRoundBands([label_padding, height])
.domain(day_labels.map(function (d) {
return moment(d).weekday();
}));
// Define week labels and axis
var week_labels = [start_of_month.clone()];
while ( start_of_month.week() !== end_of_month.week() ) {
week_labels.push(start_of_month.add(1, 'week').clone());
}
var weekScale = d3.scale.ordinal()
.rangeRoundBands([label_padding, width], 0.05)
.domain(week_labels.map(function(weekday) {
return weekday.week();
}));
// Add month data items to the overview
items.selectAll('.item-block-month').remove();
var item_block = items.selectAll('.item-block-month')
.data(month_data)
.enter()
.append('g')
.attr('class', 'item item-block-month')
.attr('width', function () {
return (width - label_padding) / week_labels.length - gutter * 5;
})
.attr('height', function () {
return Math.min(dayScale.rangeBand(), max_block_height);
})
.attr('transform', function (d) {
return 'translate(' + weekScale(moment(d.date).week()) + ',' + ((dayScale(moment(d.date).weekday()) + dayScale.rangeBand() / 1.75) - 15)+ ')';
})
.attr('total', function (d) {
return d.total;
})
.attr('date', function (d) {
return d.date;
})
.attr('offset', 0)
.on('click', function (d) {
if ( in_transition ) { return; }
// Don't transition if there is no data to show
if ( d.total === 0 ) { return; }
in_transition = true;
// Set selected date to the one clicked on
scope.selected = d;
// Hide tooltip
scope.hideTooltip();
// Remove all month overview related items and labels
scope.removeMonthOverview();
// Redraw the chart
scope.overview = 'day';
scope.drawChart();
});
var item_width = (width - label_padding) / week_labels.length - gutter * 5;
var itemScale = d3.scale.linear()
.rangeRound([0, item_width]);
item_block.selectAll('.item-block-rect')
.data(function (d) {
return d.summary;
})
.enter()
.append('rect')
.attr('class', 'item item-block-rect')
.attr('x', function (d) {
var total = parseInt(d3.select(this.parentNode).attr('total'));
var offset = parseInt(d3.select(this.parentNode).attr('offset'));
itemScale.domain([0, total]);
d3.select(this.parentNode).attr('offset', offset + itemScale(d.value));
return offset;
})
.attr('width', function (d) {
var total = parseInt(d3.select(this.parentNode).attr('total'));
itemScale.domain([0, total]);
return Math.max((itemScale(d.value) - item_gutter), 1)
})
.attr('height', function () {
return Math.min(dayScale.rangeBand(), max_block_height);
})
.attr('fill', function (d) {
var color = d3.scale.linear()
.range(['#ffffff', scope.color || '#ff4500'])
.domain([-0.15 * max_value, max_value]);
return color(d.value) || '#ff4500';
})
.style('opacity', 0)
.on('mouseover', function(d) {
if ( in_transition ) { return; }
// Get date from the parent node
var date = new Date(d3.select(this.parentNode).attr('date'));
// Construct tooltip
var tooltip_html = '';
tooltip_html += '<div class="header"><strong>' + d.name + '</strong></div><br>';
tooltip_html += '<div><strong>' + (d.value ? scope.formatTime(d.value) : 'No time') + ' tracked</strong></div>';
tooltip_html += '<div>on ' + moment(date).format('dddd, MMM Do YYYY') + '</div>';
// Calculate tooltip position
var x = weekScale(moment(date).week()) + tooltip_padding;
while ( width - x < (tooltip_width + tooltip_padding * 3) ) {
x -= 10;
}
var y = dayScale(moment(date).weekday()) + tooltip_padding * 2;
// Show tooltip
tooltip.html(tooltip_html)
.style('left', x + 'px')
.style('top', y + 'px')
.transition()
.duration(transition_duration / 2)
.ease('ease-in')
.style('opacity', 1);
})
.on('mouseout', function () {
if ( in_transition ) { return; }
scope.hideTooltip();
})
.transition()
.delay(function () {
return (Math.cos(Math.PI * Math.random()) + 1) * transition_duration;
})
.duration(function () {
return transition_duration;
})
.ease('ease-in')
.style('opacity', 1)
.call(function (transition, callback) {
if ( transition.empty() ) {
callback();
}
var n = 0;
transition
.each(function() { ++n; })
.each('end', function() {
if ( !--n ) {
callback.apply(this, arguments);
}
});
}, function() {
in_transition = false;
});
// Add week labels
labels.selectAll('.label-week').remove();
labels.selectAll('.label-week')
.data(week_labels)
.enter()
.append('text')
.attr('class', 'label label-week')
.attr('font-size', function () {
return Math.floor(label_padding / 3) + 'px';
})
.text(function (d) {
return 'Week ' + d.week();
})
.attr('x', function (d) {
return weekScale(d.week());
})
.attr('y', label_padding / 2)
.on('mouseenter', function (weekday) {
if ( in_transition ) { return; }
items.selectAll('.item-block-month')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', function (d) {
return ( moment(d.date).week() === weekday.week() ) ? 1 : 0.1;
});
})
.on('mouseout', function () {
if ( in_transition ) { return; }
items.selectAll('.item-block-month')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', 1);
})
.on('click', function (d) {
if ( in_transition ) { return; }
// Check week data
var week_data = scope.data.filter(function (e) {
return d.startOf('week') <= moment(e.date) && moment(e.date) < d.endOf('week');
});
// Don't transition if there is no data to show
if ( !week_data.length ) { return; }
in_transition = true;
// Set selected month to the one clicked on
scope.selected = { date: d };
// Hide tooltip
scope.hideTooltip();
// Remove all year overview related items and labels
scope.removeMonthOverview();
// Redraw the chart
scope.overview = 'week';
scope.drawChart();
});
// Add day labels
labels.selectAll('.label-day').remove();
labels.selectAll('.label-day')
.data(day_labels)
.enter()
.append('text')
.attr('class', 'label label-day')
.attr('x', label_padding / 3)
.attr('y', function (d, i) {
return dayScale(i) + dayScale.rangeBand() / 1.75;
})
.style('text-anchor', 'left')
.attr('font-size', function () {
return Math.floor(label_padding / 3) + 'px';
})
.text(function (d) {
return moment(d).format('dddd')[0];
})
.on('mouseenter', function (d) {
if ( in_transition ) { return; }
var selected_day = moment(d);
items.selectAll('.item-block-month')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', function (d) {
return (moment(d.date).day() === selected_day.day()) ? 1 : 0.1;
});
})
.on('mouseout', function () {
if ( in_transition ) { return; }
items.selectAll('.item-block-month')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', 1);
});
// Add button to switch back to previous overview
scope.drawButton();
};
/**
* Draw week overview
*/
scope.drawWeekOverview = function () {
// Add current overview to the history
if ( scope.history[scope.history.length-1] !== scope.overview ) {
scope.history.push(scope.overview);
}
// Define beginning and end of the week
var start_of_week = moment(scope.selected.date).startOf('week');
var end_of_week = moment(scope.selected.date).endOf('week');
// Filter data down to the selected week
var week_data = scope.data.filter(function (d) {
return start_of_week <= moment(d.date) && moment(d.date) < end_of_week;
});
var max_value = d3.max(week_data, function (d) {
return d3.max(d.summary, function (d) {
return d.value;
});
});
// Define day labels and axis
var day_labels = d3.time.days(moment().startOf('week'), moment().endOf('week'));
var dayScale = d3.scale.ordinal()
.rangeRoundBands([label_padding, height])
.domain(day_labels.map(function (d) {
return moment(d).weekday();
}));
// Define week labels and axis
var week_labels = [start_of_week];
var weekScale = d3.scale.ordinal()
.rangeRoundBands([label_padding, width], 0.01)
.domain(week_labels.map(function (weekday) {
return weekday.week();
}));
// Add week data items to the overview
items.selectAll('.item-block-week').remove();
var item_block = items.selectAll('.item-block-week')
.data(week_data)
.enter()
.append('g')
.attr('class', 'item item-block-week')
.attr('width', function () {
return (width - label_padding) / week_labels.length - gutter * 5;
})
.attr('height', function () {
return Math.min(dayScale.rangeBand(), max_block_height);
})
.attr('transform', function (d) {
return 'translate(' + weekScale(moment(d.date).week()) + ',' + ((dayScale(moment(d.date).weekday()) + dayScale.rangeBand() / 1.75) - 15)+ ')';
})
.attr('total', function (d) {
return d.total;
})
.attr('date', function (d) {
return d.date;
})
.attr('offset', 0)
.on('click', function (d) {
if ( in_transition ) { return; }
// Don't transition if there is no data to show
if ( d.total === 0 ) { return; }
in_transition = true;
// Set selected date to the one clicked on
scope.selected = d;
// Hide tooltip
scope.hideTooltip();
// Remove all week overview related items and labels
scope.removeWeekOverview();
// Redraw the chart
scope.overview = 'day';
scope.drawChart();
});
var item_width = (width - label_padding) / week_labels.length - gutter * 5;
var itemScale = d3.scale.linear()
.rangeRound([0, item_width]);
item_block.selectAll('.item-block-rect')
.data(function (d) {
return d.summary;
})
.enter()
.append('rect')
.attr('class', 'item item-block-rect')
.attr('x', function (d) {
var total = parseInt(d3.select(this.parentNode).attr('total'));
var offset = parseInt(d3.select(this.parentNode).attr('offset'));
itemScale.domain([0, total]);
d3.select(this.parentNode).attr('offset', offset + itemScale(d.value));
return offset;
})
.attr('width', function (d) {
var total = parseInt(d3.select(this.parentNode).attr('total'));
itemScale.domain([0, total]);
return Math.max((itemScale(d.value) - item_gutter), 1)
})
.attr('height', function () {
return Math.min(dayScale.rangeBand(), max_block_height);
})
.attr('fill', function (d) {
var color = d3.scale.linear()
.range(['#ffffff', scope.color || '#ff4500'])
.domain([-0.15 * max_value, max_value]);
return color(d.value) || '#ff4500';
})
.style('opacity', 0)
.on('mouseover', function(d) {
if ( in_transition ) { return; }
// Get date from the parent node
var date = new Date(d3.select(this.parentNode).attr('date'));
// Construct tooltip
var tooltip_html = '';
tooltip_html += '<div class="header"><strong>' + d.name + '</strong></div><br>';
tooltip_html += '<div><strong>' + (d.value ? scope.formatTime(d.value) : 'No time') + ' tracked</strong></div>';
tooltip_html += '<div>on ' + moment(date).format('dddd, MMM Do YYYY') + '</div>';
// Calculate tooltip position
var total = parseInt(d3.select(this.parentNode).attr('total'));
itemScale.domain([0, total]);
var x = parseInt(d3.select(this).attr('x')) + itemScale(d.value) / 4 + tooltip_width / 4;
while ( width - x < (tooltip_width + tooltip_padding * 3) ) {
x -= 10;
}
var y = dayScale(moment(date).weekday()) + tooltip_padding * 1.5;
// Show tooltip
tooltip.html(tooltip_html)
.style('left', x + 'px')
.style('top', y + 'px')
.transition()
.duration(transition_duration / 2)
.ease('ease-in')
.style('opacity', 1);
})
.on('mouseout', function () {
if ( in_transition ) { return; }
scope.hideTooltip();
})
.transition()
.delay(function () {
return (Math.cos(Math.PI * Math.random()) + 1) * transition_duration;
})
.duration(function () {
return transition_duration;
})
.ease('ease-in')
.style('opacity', 1)
.call(function (transition, callback) {
if ( transition.empty() ) {
callback();
}
var n = 0;
transition
.each(function() { ++n; })
.each('end', function() {
if ( !--n ) {
callback.apply(this, arguments);
}
});
}, function() {
in_transition = false;
});
// Add week labels
labels.selectAll('.label-week').remove();
labels.selectAll('.label-week')
.data(week_labels)
.enter()
.append('text')
.attr('class', 'label label-week')
.attr('font-size', function () {
return Math.floor(label_padding / 3) + 'px';
})
.text(function (d) {
return 'Week ' + d.week();
})
.attr('x', function (d) {
return weekScale(d.week());
})
.attr('y', label_padding / 2)
.on('mouseenter', function (weekday) {
if ( in_transition ) { return; }
items.selectAll('.item-block-week')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', function (d) {
return ( moment(d.date).week() === weekday.week() ) ? 1 : 0.1;
});
})
.on('mouseout', function () {
if ( in_transition ) { return; }
items.selectAll('.item-block-week')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', 1);
});
// Add day labels
labels.selectAll('.label-day').remove();
labels.selectAll('.label-day')
.data(day_labels)
.enter()
.append('text')
.attr('class', 'label label-day')
.attr('x', label_padding / 3)
.attr('y', function (d, i) {
return dayScale(i) + dayScale.rangeBand() / 1.75;
})
.style('text-anchor', 'left')
.attr('font-size', function () {
return Math.floor(label_padding / 3) + 'px';
})
.text(function (d) {
return moment(d).format('dddd')[0];
})
.on('mouseenter', function (d) {
if ( in_transition ) { return; }
var selected_day = moment(d);
items.selectAll('.item-block-week')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', function (d) {
return (moment(d.date).day() === selected_day.day()) ? 1 : 0.1;
});
})
.on('mouseout', function () {
if ( in_transition ) { return; }
items.selectAll('.item-block-week')
.transition()
.duration(transition_duration)
.ease('ease-in')
.style('opacity', 1);
});
// Add button to switch back to previous overview
scope.drawButton();
};
/**
* Draw day overview
*/
scope.drawDayOverview = function () {
// Add current overview to the history
if ( scope.history[scope.history.length-1] !== scope.overview ) {
scope.history.push(scope.overview);
}
// Initialize selected date to today if it was not set
if ( !Object.keys(scope.selected).length ) {
scope.selected = scope.data[scope.data.length - 1];
}
var project_labels = scope.selected.summary.map(function (project) {
return project.name;
});
var projectScale = d3.scale.ordinal()
.rangeRoundBands([label_padding, height])
.domain(project_labels);
var itemScale = d3.time.scale()
.range([label_padding*2, width])
.domain([moment(scope.selected.date).startOf('day'), moment(scope.selected.date).endOf('day')]);
items.selectAll('.item-block').remove();
items.selectAll('.item-block')
.data(scope.selected.details)
.enter()
.append('rect')
.attr('class', 'item item-block')
.attr('x', function (d) {
return itemScale(moment(d.date));
})
.attr('y', function (d) {
return (projectScale(d.name) + projectScale.rangeBand() / 2) - 15;
})
.attr('width', function (d) {
var end = itemScale(d3.time.second.offset(moment(d.date), d.value));
return Math.max((end - itemScale(moment(d.date))), 1);
})
.attr('height', function () {
return Math.min(projectScale.rangeBand(), max_block_height);
})
.attr('fill', function () {
return scope.color || '#ff4500';
})
.style('opacity', 0)
.on('mouseover', function(d) {
if ( in_transition ) { return; }
// Construct tooltip
var tooltip_html = '';
tooltip_html += '<div class="header"><strong>' + d.name + '</strong><div><br>';
tooltip_html += '<div><strong>' + (d.value ? scope.formatTime(d.value) : 'No time') + ' tracked</strong></div>';
tooltip_html += '<div>on ' + moment(d.date).format('dddd, MMM Do YYYY HH:mm') + '</div>';
// Calculate tooltip position
var x = d.value * 100 / (60 * 60 * 24) + itemScale(moment(d.date));
while ( width - x < (tooltip_width + tooltip_padding * 3) ) {
x -= 10;
}
var y = projectScale(d.name) + projectScale.rangeBand() / 2 + tooltip_padding / 2;
// Show tooltip
tooltip.html(tooltip_html)
.style('left', x + 'px')
.style('top', y + 'px')
.transition()
.duration(transition_duration / 2)
.ease('ease-in')
.style('opacity', 1);
})
.on('mouseout', function () {
if ( in_transition ) { return; }
scope.hideTooltip();
})
.on(