Skip to content

Instantly share code, notes, and snippets.

@Golodhros
Last active August 1, 2017 22:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Golodhros/b08748182e62c40b089d0c3934b48974 to your computer and use it in GitHub Desktop.
Save Golodhros/b08748182e62c40b089d0c3934b48974 to your computer and use it in GitHub Desktop.
Britecharts Heatmap MVP
license: mit
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
.container {
text-align: center;
}
.container div {
margin: 0 auto;
}
.info {
font-size: 1.2em;
}
</style>
</head>
<body>
<div class="container">
<div class="js-heatmap-chart-container"></div>
<div class="js-info info"></div>
</div>
<script>
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const url = "https://api.github.com/repos/Golodhros/britecharts/stats/punch_card";
/**
* @typedef HeatmapData
* @type {Array[]}
* @property {Number[]} values
* {Number} values[0] weekday (0: Monday, 6: Sunday)
* {Number} values[1] hour
* {Number} values[2] quantity
*
* @example
* [
* [0, 1, 3],
* [1, 2, 4]
* ]
*/
/**
* Heatmap reusable API class that renders a
* simple and configurable heatmap.
*
* @module Heatmap
* @tutorial heatmap
* @requires d3-array, d3-axis, d3-dispatch, d3-scale, d3-selection
*
* @example
* var heatmapChart = heatmap();
*
* heatmapChart
* .height(500)
* .width(800);
*
* d3.select('.css-selector')
* .datum(dataset)
* .call(heatmapChart);
*
*/
function HeatMap() {
let margin = {
top: 10,
right: 10,
bottom: 10,
left: 10
},
width = 900,
height = 300,
chartWidth, chartHeight,
svg,
data,
boxes,
boxSize = 35,
colorSchema = [
'#ffd8d4',
'#ffb5b0',
'#ff938c',
'#ff766c',
'#ff584c',
'#f04b42',
'#e03d38',
'#be2e29',
'#9c1e19'
],
colorScale,
// Dispatcher object to broadcast the mouse events
// Ref: https://github.com/mbostock/d3/wiki/Internals#d3_dispatch
dispatcher = d3.dispatch('customMouseOver', 'customMouseOut');
/**
* This function creates the graph using the selection as container
* @param {d3} _selection A d3 selection that represents
* the container(s) where the chart(s) will be rendered
* @param {HeatmapData} _data The data to attach and generate the chart
*/
function exports(_selection) {
_selection.each(function(_data) {
chartWidth = width - margin.left - margin.right;
chartHeight = height - margin.top - margin.bottom;
data = cleanData(_data);
buildScales();
// buildAxis();
buildSVG(this);
drawBoxes();
// drawAxis();
});
}
/**
* Creates the d3 x and y axis, setting orientations
* @private
*/
function buildAxis() {
if (isHorizontal) {
xAxis = d3.axisBottom(xScale)
.ticks(numOfHorizontalTicks, valueLabelFormat)
.tickSizeInner([-chartHeight]);
yAxis = d3Axis.axisLeft(yScale);
} else {
xAxis = d3Axis.axisBottom(xScale);
yAxis = d3Axis.axisLeft(yScale)
.ticks(numOfVerticalTicks, valueLabelFormat)
}
}
/**
* Builds containers for the chart, the axis and a wrapper for all of them
* Also applies the Margin convention
* @private
*/
function buildContainerGroups() {
let container = svg
.append('g')
.classed('container-group', true)
.attr('transform', `translate(${margin.left}, ${margin.top})`);
container
.append('g').classed('chart-group', true);
container
.append('g').classed('x-axis-group axis', true);
container
.append('g').classed('y-axis-group axis', true);
container
.append('g').classed('metadata-group', true);
}
/**
* Builds the SVG element that will contain the chart
* @param {HTMLElement} container DOM element that will work as the container of the graph
* @private
*/
function buildSVG(container) {
if (!svg) {
svg = d3.select(container)
.append('svg')
.classed('britechart heatmap-chart', true);
buildContainerGroups();
}
svg
.attr('width', width)
.attr('height', height);
}
/**
* Creates the x and y scales of the graph
* @private
*/
function buildScales() {
colorScale = d3.scaleLinear()
.range([colorSchema[0], colorSchema[colorSchema.length - 1]])
.domain(d3.extent(data, function (d) { return d[2] }))
.interpolate(d3.interpolateHcl);
}
/**
* Cleaning data adding the proper format
* @param {HeatmapData} originalData Data
* @private
*/
function cleanData(originalData) {
let data = originalData.map((d) => {
return [ +d[0], +d[1], +d[2]];
});
return data;
}
/**
* Custom OnMouseOut event handler
* @return {void}
* @private
*/
function customOnMouseOut(e, d, chartWidth, chartHeight) {
dispatcher.call('customMouseOut', e, d, d3.mouse(e), [chartWidth, chartHeight]);
}
/**
* Custom OnMouseOver event handler
* @return {void}
* @private
*/
function customOnMouseOver(e, d, chartWidth, chartHeight) {
dispatcher.call('customMouseOver', e, d, d3.mouse(e), [chartWidth, chartHeight]);
}
/**
* Draws the boxes that form the heatmap
* @private
*/
function drawBoxes() {
boxes = svg.select('.chart-group').selectAll('.box')
.data(data);
boxes.enter()
.append('rect')
.attr('x', function (d) { return d[1] * boxSize; })
.attr('y', function (d) { return d[0] * boxSize; })
.attr('width', boxSize)
.attr('height', boxSize)
.style('fill', function (d) { return colorScale(d[2]); })
.classed('box', true)
.on('mouseover', function (d) {
customOnMouseOver(this, d, chartWidth, chartHeight);
})
.on('mouseout', function (d) {
customOnMouseOut(this, d, chartWidth, chartHeight);
});
}
// API
/**
* Gets or Sets the height of the chart
* @param {number} _x Desired width for the graph
* @return { height | module} Current height or Heatmap Chart module to chain calls
* @public
*/
exports.height = function(_x) {
if (!arguments.length) {
return height;
}
height = _x;
return this;
};
/**
* Gets or Sets the margin of the chart
* @param {object} _x Margin object to get/set
* @return { margin | module} Current margin or Heatmap Chart module to chain calls
* @public
*/
exports.margin = function(_x) {
if (!arguments.length) {
return margin;
}
margin = _x;
return this;
};
/**
* Exposes an 'on' method that acts as a bridge with the event dispatcher
* We are going to expose this events:
* customMouseOver and customMouseOut
*
* @return {module} Heatmap Chart
* @public
*/
exports.on = function() {
let value = dispatcher.on.apply(dispatcher, arguments);
return value === dispatcher ? exports : value;
};
/**
* Gets or Sets the width of the chart
* @param {number} _x Desired width for the graph
* @return { width | module} Current width or Heatmap Chart module to chain calls
* @public
*/
exports.width = function(_x) {
if (!arguments.length) {
return width;
}
width = _x;
return this;
};
return exports;
};
d3.json(url, function (data) {
let heatmapChart = HeatMap(),
heatmapContainer = d3.select('.js-heatmap-chart-container');
heatmapChart
.width(840)
.height(280)
.on('customMouseOver', function(d) {
let message = d[2] + ' commits between ' + d[1] + ':00 and ' + d[1] + ':59 at ' + days[d[0]];
d3.select('.js-info').text(message);
});
heatmapContainer.datum(data).call(heatmapChart);
});
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment