Created
June 14, 2017 09:08
-
-
Save hardiksondagar/8b12ab93ecd3d5232b2cd97e576d4961 to your computer and use it in GitHub Desktop.
Angular D3 Heatmap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
'use strict'; | |
angular.module().directive('d3Heatmap', ['d3Service', function(d3Service) { | |
return { | |
restrict: 'EA', | |
scope: { | |
data: "=data", | |
onSelect: '&' | |
}, | |
link: function(scope, iElement, iAttrs) { | |
d3Service.d3().then(function(d3) { | |
//UI configuration | |
var width = iElement.parent().width(), | |
itemSize = (width - 45) / 48, | |
cellSize = itemSize - 0.1, | |
height = iElement.parent().height(), | |
margin = { | |
top: 20, | |
right: 20, | |
bottom: 20, | |
left: 25 | |
}; | |
//formats | |
var minuteFormat = d3.time.format('%M'), | |
hourFormat = d3.time.format('%H'), | |
dayFormat = d3.time.format('%j'), | |
yearFormat = d3.time.format('%Y'), | |
dateFormat = d3.time.format('%Y-%m-%d'), | |
timeFormat = d3.time.format('%Y-%m-%dT%H:%M:%S.%LZ'), | |
monthDayFormat = d3.time.format('%d'), | |
blockFormat = d3.time.format('%Y-%m-%d %H:%M'), | |
weekDayFormat = d3.time.format('%A'); | |
var colorSchemes = [ | |
[ | |
'#ffffcc', | |
'#ffeda0', | |
'#fed976', | |
'#feb24c', | |
'#fd8d3c', | |
'#fc4e2a', | |
'#e31a1c', | |
'#bd0026', | |
'#800026' | |
], | |
[ | |
'#1a9850', | |
'#66bd63', | |
'#a6d96a', | |
'#d9ef8b', | |
'#ffffbf', | |
'#fee08b', | |
'#fdae61', | |
'#f46d43', | |
'#d73027', | |
], | |
[ | |
'#2166ac', | |
'#4393c3', | |
'#92c5de', | |
'#d1e5f0', | |
'#f7f7f7', | |
'#fddbc7', | |
'#f4a582', | |
'#d6604d', | |
'#b2182b' | |
], | |
[ | |
'#fff7ec', | |
'#fee8c8', | |
'#fdd49e', | |
'#fdbb84', | |
'#fc8d59', | |
'#ef6548', | |
'#d7301f', | |
'#b30000', | |
'#7f0000' | |
] | |
]; | |
//data vars for rendering | |
var dateExtent = null, | |
data = null, | |
startDate = null, | |
dayOffset = 0, | |
colorCalibration = colorSchemes[1], | |
dailyValueExtent = {}; | |
//axises and scales | |
var axisWidth = itemSize * 48, | |
axisHeight = 0; | |
var xAxisScale = d3.scale.linear() | |
.range([0, axisWidth]) | |
.domain([0, 24]); | |
var xAxis = d3.svg.axis() | |
.ticks(12) | |
.tickFormat(d3.format('3d')) | |
.scale(xAxisScale) | |
.orient('top'); | |
var yAxisScale = d3.time.scale(); | |
var yAxis = d3.svg.axis() | |
.orient('left') | |
.ticks(d3.time.days, 3) | |
.tickFormat(monthDayFormat); | |
initCalibration(); | |
var svg = d3.select(iElement[0]).append('svg'); | |
var heatmap = svg | |
.attr('width', width) | |
.attr('height', height) | |
.append('g') | |
.attr('width', width - margin.left - margin.right) | |
.attr('height', height - margin.top - margin.bottom) | |
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); | |
var rect = null; | |
function initCalibration() { | |
d3.select('[role="calibration"] [role="example"]').select('svg') | |
.selectAll('rect').data(colorCalibration).enter() | |
.append('rect') | |
.attr('width', cellSize / 2) | |
.attr('height', cellSize / 2) | |
.attr('x', function(d, i) { | |
return i * itemSize / 2; | |
}) | |
.attr('fill', function(d) { | |
return d; | |
}); | |
} | |
function renderColor() { | |
var renderByCount = false; | |
var max, min; | |
min = d3.min(scope.data.data[0].timeSeries, function(d) { | |
if (d.value >= 0) { | |
return d.value; | |
} else { | |
return null; | |
} | |
}); | |
max = d3.max(scope.data.data[0].timeSeries, function(d) { | |
return d.value; | |
}); | |
rect | |
.filter(function(d) { | |
return (d.value >= 0); | |
}) | |
.transition() | |
.delay(function(d) { | |
return (utils.dayDiff(dateFormat(d.date), startDate)) * 15; | |
}) | |
.duration(500) | |
.attrTween('fill', function(d, i, a) { | |
//choose color dynamicly | |
var colorIndex = d3.scale.quantile() | |
.range(d3.range(colorCalibration.length)) | |
// .domain((renderByCount ? [0, 1200] : dailyValueExtent[d.day])); | |
.domain((renderByCount ? [min, max] : dailyValueExtent[d.day])); | |
return d3.interpolate(a, colorCalibration[colorIndex(d.value)]); | |
}); | |
} | |
//extend frame height in `http://bl.ocks.org/` | |
// d3.select(self.frameElement).style("height", "600px"); | |
var delay = (function() { | |
var timer = 0; | |
return function(callback, ms) { | |
clearTimeout(timer); | |
timer = setTimeout(callback, ms); | |
}; | |
})(); | |
// on window resize, re-render d3 canvas | |
window.onresize = function() { | |
delay(function() { | |
return scope.$apply(); | |
}, 500); | |
}; | |
scope.$watch(function() { | |
return angular.element(window)[0].innerWidth; | |
}, function() { | |
return scope.render(scope.data); | |
}); | |
// watch for data changes and re-render | |
scope.$watch('data', function(newVals, oldVals) { | |
delay(function() { | |
return scope.render(newVals); | |
}, 500); | |
}, true); | |
// define render function | |
scope.render = function(heatmapData) { | |
// remove all previous items before render | |
svg.selectAll("*").remove(); | |
if (!heatmapData || !heatmapData.data || !heatmapData.data[0] || !heatmapData.data[0].timeSeries) { | |
return; | |
} | |
width = iElement.parent().width(); | |
itemSize = width / 51.5; | |
var max, min; | |
min = d3.min(heatmapData.data[0].timeSeries, function(d) { | |
return d.timeStamp; | |
}); | |
max = d3.max(heatmapData.data[0].timeSeries, function(d) { | |
return d.timeStamp; | |
}); | |
var days = Math.ceil((max - min) / 86400); | |
height = (days + 6) * itemSize / 2; | |
cellSize = itemSize - 0.1; | |
axisWidth = itemSize * 48; | |
var xAxisScale = d3.scale.linear() | |
.range([0, axisWidth]) | |
.domain([0, 24]); | |
var xAxis = d3.svg.axis() | |
.ticks(12) | |
.tickFormat(d3.format('3d')) | |
.scale(xAxisScale) | |
.orient('top'); | |
var heatmap = svg | |
.attr('width', width) | |
.attr('height', height) | |
.append('g') | |
.attr('width', width - margin.left - margin.right) | |
.attr('height', height - margin.top - margin.bottom) | |
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); | |
data = heatmapData.data[0].timeSeries; | |
data.forEach(function(valueObj) { | |
var date = new Date(valueObj.timeStamp * 1000); | |
valueObj['date'] = date; | |
var day = valueObj['day'] = monthDayFormat(valueObj['date']); | |
var dayData = dailyValueExtent[day] = (dailyValueExtent[day] || [1000, -1]); | |
var pmValue = valueObj['value']; | |
dayData[0] = d3.min([dayData[0], pmValue]); | |
dayData[1] = d3.max([dayData[1], pmValue]); | |
}); | |
dateExtent = d3.extent(data, function(d) { | |
return d.date; | |
}); | |
var dayDiff = utils.dayDiff(dateFormat(dateExtent[1]), dateFormat(dateExtent[0])); | |
axisHeight = itemSize * (dayDiff + 1) / 2; | |
//render axises | |
yAxis.scale(yAxisScale.range([0, axisHeight]).domain([dateExtent[0], dateExtent[1]])); | |
var TimeTextTransform = parseInt(axisWidth) + parseInt(cellSize); | |
var DateTextTransform = parseInt(axisHeight) + parseInt(cellSize); | |
svg.append('g') | |
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') | |
.attr('class', 'x axis') | |
.call(xAxis) | |
.append('text') | |
.text('Time') | |
.attr('transform', 'translate(' + TimeTextTransform + ',-10)'); | |
svg.append('g') | |
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') | |
.attr('class', 'y axis') | |
.call(yAxis) | |
.append('text') | |
.text('Date') | |
.attr('transform', 'translate(-10,' + DateTextTransform + ')'); | |
//render heatmap rects | |
startDate = dateFormat(dateExtent[0]); | |
/* scale day into 15 minutes slot */ | |
var dateScale = d3.scale.linear() | |
.range([95, 0]) | |
.domain([1439, 0]); | |
rect = heatmap.selectAll('rect') | |
.data(data) | |
.enter().append('rect') | |
.attr('width', cellSize / 2) | |
.attr('height', cellSize / 2) | |
.attr('y', function(d) { | |
return itemSize * (utils.dayDiff(dateFormat(d.date), startDate)) / 2; | |
}) | |
.attr('x', function(d) { | |
var minutes = (hourFormat(d.date) * 60) + parseInt(minuteFormat(d.date)); | |
return (dateScale(minutes) * itemSize / 2); | |
}) | |
.attr('fill', '#fefefe'); | |
rect.filter(function(d) { | |
return true; | |
}) | |
.append('title') | |
.html(function(d) { | |
return 'Day : ' + weekDayFormat(d.date) + '<br>Date : ' + blockFormat(d.date) + '<br>kWh : ' + d.value; | |
}); | |
rect.on({ | |
'click': function(d, i, j) { | |
scope.onSelect({ | |
data: d | |
}); | |
scope.$apply(); | |
} | |
} | |
); | |
renderColor(); | |
}; | |
}); | |
} | |
}; | |
}]); | |
}()); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
@author Hardik Sondagar | |
@abstract Load d3 script dynamically. On injecting d3Service into directives, write all the dynamic code after d3Service.d3() promise resolveFor example check app/shared/d3/d3.directive.js | |
*/ | |
(function() { | |
'use strict'; | |
angular.module('d3', []).factory('d3Service', ['$document', '$q', '$rootScope', function($document, $q, $rootScope) { | |
var d = $q.defer(); | |
function onScriptLoad() { | |
// Load client in the browser | |
$rootScope.$apply(function() { | |
d.resolve(window.d3); | |
}); | |
} | |
/* Check if already loaded */ | |
if (typeof d3 != "undefined") { | |
d.resolve(window.d3); | |
} else { | |
// Create a script tag with d3 as the source | |
// and call our onScriptLoad callback when it | |
// has been loaded | |
var scriptTag = $document[0].createElement('script'); | |
scriptTag.type = 'text/javascript'; | |
scriptTag.async = true; | |
scriptTag.src = 'http://d3js.org/d3.v3.min.js'; | |
scriptTag.onreadystatechange = function() { | |
if (this.readyState == 'complete') onScriptLoad(); | |
}; | |
scriptTag.onload = onScriptLoad; | |
var s = $document[0].getElementsByTagName('body')[0]; | |
s.appendChild(scriptTag); | |
} | |
return { | |
d3: function() { | |
return d.promise; | |
} | |
}; | |
}]); | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment