Skip to content

Instantly share code, notes, and snippets.

@hardiksondagar
Created June 14, 2017 09:08
Show Gist options
  • Save hardiksondagar/8b12ab93ecd3d5232b2cd97e576d4961 to your computer and use it in GitHub Desktop.
Save hardiksondagar/8b12ab93ecd3d5232b2cd97e576d4961 to your computer and use it in GitHub Desktop.
Angular D3 Heatmap
(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();
};
});
}
};
}]);
}());
/*
@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