Skip to content

Instantly share code, notes, and snippets.

@nanyaks
Last active April 20, 2016 11:32
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 nanyaks/5141ae12560e98862693542b1eb824e5 to your computer and use it in GitHub Desktop.
Save nanyaks/5141ae12560e98862693542b1eb824e5 to your computer and use it in GitHub Desktop.
Stacked group chart graph
license: none
"use strict";
/*global $, d3, _ */
var seriesNames = ["SO", "JP", "LO"],
numSeries = seriesNames.length,
projectTypes = [
"Permit/Fenc",
"Prelift",
"Lift",
"Demo",
"Footings",
"Pilings",
"Foundation",
"Sill Plates",
"Set",
"Reconnect",
"Desk Steps",
"Landscaping",
"Punchlist"
],
numSamples = projectTypes.length, // number of samples is the number of project stages we have all in all,
mainBarValues = [],
data = seriesNames.map(function (name) {
var _value = bumpLayer(numSamples, 1); // return the values for each of seriesNames (should have come from the DB somehow)
var _obj = {
name: name,
values: _value
};
mainBarValues.push(_obj);
return _obj;
}),
addressData = seriesNames.map(function (name, index) {
var _value = generateAddressBarLayer(mainBarValues[index].values);
return {
name: name,
values: _value
};
}),
stack = d3.layout.stack().values(function (d) { return d.values; }),
addressStack = d3.layout.stack().values(function (d) { return d.values; });
/*
* This was an attemp to sanitize the data and have values for y0
*
*/
var largestDetailArray = d3.max(addressData, function (d){
return d.values;
});
var maxAddressArrayLength = largestDetailArray.length;
// call sanitizeData
var sanitizedAddressData = sanitizeData(addressData, maxAddressArrayLength);
console.log(maxAddressArrayLength);
console.log(sanitizedAddressData);
stack(data);
addressStack(addressData);
//addressStack(data);
var chartMode = "stacked",
numEnabledSeries = numSeries,
lastHoveredBarIndex,
containerWidth = document.querySelector(".js-stacked-chart-container").clientWidth,
containerHeight = 500,
margin = {top: 150, right: 10, bottom: 20, left: 30},
width = containerWidth - margin.left - margin.right,
height = containerHeight - margin.top - margin.bottom,
widthPerStack = width / numSamples,
animationDuration = 400,
delayBetweenBarAnimation = 10,
numYAxisTicks = 8,
maxStackY = d3.max(data, function (series) { return d3.max(series.values, function (d) { return d.y0 + d.y; }); }),
paddingBetweenLegendSeries = 5,
// legend series boxes
legendSeriesBoxX = 0,
legendSeriesBoxY = 0,
legendSeriesBoxWidth = 15,
legendSeriesBoxHeight = 15,
legendSeriesHeight = legendSeriesBoxHeight + paddingBetweenLegendSeries,
legendSeriesLabelX = -5,
legendSeriesLabelY = legendSeriesBoxHeight / 2,
legendMargin = 10,
legendX = containerWidth - legendSeriesBoxWidth - legendMargin,
legendY = legendMargin,
overlayTopPadding = 20,
tooltipBottomMargin = 12;
var binsScale = d3.scale.ordinal()
.domain(d3.range(numSamples))
.rangeBands([0, width], 0.1, 0.05);
var xScale = d3.scale.linear()
.domain([0, numSamples])
.range([0, width]);
var yScale = d3.scale.linear()
.domain([0, maxStackY])
.range([height, 0]);
var heightScale = d3.scale.linear()
.domain([0, maxStackY])
.range([0, height]);
// Dummy data for the x-axis
var xAxisData = ['A', 'B', 'C', 'D', 'E'];
var xAxis = d3.svg.axis()
.scale(xScale) //binsScale)
.tickFormat(function(d) { return projectTypes[d]; })
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
var enabledSeries = function () { return _.reject(data, function (series) { return series.disabled; }); };
var seriesClass = function (seriesName) { return "series-" + seriesName.toLowerCase(); };
var layerClass = function (d) { return "layer " + seriesClass(d.name); };
var legendSeriesClass = function (d) { return "" + seriesClass(d); };
var barDelay = function (d, i) { return i * delayBetweenBarAnimation; };
var joinKey = function (d) { return d.name; };
var stackedBarX = function (d) { return binsScale(d.x); };
var stackedBarY = function (d) { return yScale(d.y0 + d.y); };
var stackedBarBaseY = function (d) { return yScale(d.y0); };
var stackedBarWidth = binsScale.rangeBand();
var groupedBarX = function (d, i, j) { return binsScale(d.x) + j * groupedBarWidth(); };
var groupedBarY = function (d) { return yScale(d.y); };
var groupedBarBaseY = height;
var groupedBarWidth = function () { return binsScale.rangeBand() / numEnabledSeries; };
var barHeight = function (d) { return heightScale(d.y); };
/**
* Function to perform trasnsition on the chart bars
*/
var transitionStackedBars = function (selection) {
selection.transition()
.duration(animationDuration)
.delay(barDelay)
.attr("y", stackedBarY)
.attr("height", barHeight);
};
// Function to handle the address bars
var transitionStackedAddressBars = function (selection) {
selection.transition()
.duration(animationDuration)
.delay(barDelay)
.attr("y", stackedSecondBarY)
.attr("height", barHeight);
};
var svg = d3.select(".js-stacked-chart")
.attr("width", containerWidth)
.attr("height", containerHeight);
var mainArea = svg.append("g")
.attr("class", "main-area")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var layersArea = mainArea.append("g")
.attr("class", "layers");
var addressLayer = mainArea.append("g")
.attr("class", "layers");
var addressTextLayer = mainArea.append("g")
.attr("class", "layers");
var layers = layersArea.selectAll(".layer").data(data)
.enter().append("g")
.attr("class", layerClass); // This line adds the different colors for the stacked chart
// The width of the second bar
var thinBarWidth = stackedBarWidth / 10.0;
// This is to displace the second bar to the right of the first
var stackedSecondBarX = function (d) { return binsScale(d.x) + thinBarWidth; }; // simple hack
var stackedSecondBarY = function (d) {
return yScale(d.y0 + d.y);
};
var stackedTextX = function (d) { return binsScale(d.x) + thinBarWidth + 40; }; // simple hack
var stackedTextY = function (d) {
var _yScale = yScale(d.y0 + d.y) + 20; // Not entirely accurate but it gets the work done
return _yScale;
};
var addressBarWidth = thinBarWidth * 9.0; // give the remainder 18th of the width to the addressBar
layers.selectAll("rect").data(function (d) { return d.values; })
.enter().append("rect")
.attr("x", stackedBarX)
.attr("y", height)
.attr("width", thinBarWidth)
.attr("height", 0)
.call(transitionStackedBars);
/**
* Start second bar definition
*/
var addressBar = addressLayer.selectAll(".layer").data(addressData)
.enter().append("g")
.attr("class", layerClass);
var addressText = addressTextLayer.selectAll(".layer").data(addressData)
.enter().append("g")
.attr("class", layerClass);
/**
* Address bar rectangles
*/
addressBar.selectAll("rect")
.data(function (d) {
return d.values;
})
.enter().append("rect")
.attr("x", stackedSecondBarX)
.attr("y", stackedSecondBarY)
.attr("width", addressBarWidth)
.attr("height", height)
.attr("class", "addressBar")
.call(transitionStackedAddressBars);
/**
* Address bar text
*/
addressText.selectAll("text")
.data(function (d) {
return d.values;
})
.enter().append("text")
.attr("x", stackedTextX)
.attr("y", stackedTextY)
.attr("fill", "#000")
.style("stroke-width", 1)
.style({"font-size":"10px","z-index":"999999999"})
.style("text-anchor", "middle")
.text(function (d) { return d.y0 ; });
// .text("123 Elm Street");
/*
* End second bar
*/
/*
* Add the onclick handler for individual rectangles
*/
layers.selectAll("rect")
.on("click", function (d, i) { console.log(d, i); });
mainArea.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + widthPerStack / 2 + ", -40)") // hack to provide a margin at the top - btw the x axis and the charts
.call(xAxis);
mainArea.append("g")
.attr("class", "y axis")
.call(yAxis);
var legendSeries = svg.append("g")
.attr("class", "legend")
.attr("transform", "translate(" + legendX + "," + legendY + ")")
.selectAll("g").data(seriesNames.reverse())
.enter().append("g")
.attr("class", legendSeriesClass)
.attr("transform", function (d, i) { return "translate(0," + (i * legendSeriesHeight) + ")"; })
.on("click", toggleSeries);
legendSeries.append("rect")
.attr("class", "series-box")
.attr("x", legendSeriesBoxX)
.attr("y", legendSeriesBoxY)
.attr("width", legendSeriesBoxWidth)
.attr("height", legendSeriesBoxHeight);
legendSeries.append("text")
.attr("class", "series-label")
.attr("x", legendSeriesLabelX)
.attr("y", legendSeriesLabelY)
.text(String);
d3.selectAll(".js-stacked-chart-container input").on("change", changeChartMode);
/**
* Generate data for the secondary stacked bar given the main stacked bar
*/
function generateAddressBarLayer(mainStackedBar) {
var sub = [];
for (var i = 0, len = mainStackedBar.length; i < len; i++) {
var _yVal = mainStackedBar[i].y;
if (!_yVal > 0) {
continue;
}
var smallArr = [];
var count = 0;
for (var j = 0; j < _yVal; ++j) {
var _valueObj = {};
_valueObj.x = +mainStackedBar[i].x;
_valueObj.y = 1;
sub.push(_valueObj);
}
}
return sub;
}
/**
* @function sanitizeData
* Sanitize the data for final rendering
*
*/
function sanitizeData (series, maxLength) {
var _b = series.map(function (obj, index){
var _arr = [];
var count = 0;
for (var i = 0; i < maxLength; i++) {
var o = {};
if (!obj.values[i]) {
o.x = 0;
o.y = 1;
o.y0 = 1;
} else {
o.x = +obj.values[i].x;
o.y = +obj.values[i].y;
o.y0 = count++;
}
_arr.push(o);
}
return {name: obj.name, values: _arr};
});
return _b
}
/**
* Toggles a certain series.
* @param {String} seriesName The name of the series to be toggled
*/
function toggleSeries (seriesName) {
var series,
isDisabling,
newData;
series = _.findWhere(data, { name: seriesName });
isDisabling = !series.disabled;
if (isDisabling === true && numEnabledSeries === 1) {
return;
}
d3.select(this).classed("disabled", isDisabling);
series.disabled = isDisabling;
newData = stack(enabledSeries());
numEnabledSeries = newData.length;
layers = layers.data(newData, joinKey);
if (isDisabling === true) {
removeSeries();
}
else {
addSeries();
}
}
/**
* Removes a certain series.
*/
function removeSeries () {
var layerToBeRemoved;
layerToBeRemoved = layers.exit();
if (chartMode === "stacked") {
removeStackedSeries(layerToBeRemoved);
}
else {
removeGroupedSeries(layerToBeRemoved);
}
}
/**
* Smoothly transitions and then removes a certain series when the chart is in `stacked` mode.
* @param {d3.selection} layerToBeRemoved The layer that contains the series' bars
*/
function removeStackedSeries (layerToBeRemoved) {
layerToBeRemoved.selectAll("rect").transition()
.duration(animationDuration)
.delay(barDelay)
.attr("y", stackedBarBaseY)
.attr("height", 0)
.call(endAll, function () {
layerToBeRemoved.remove();
});
transitionStackedBars(layers.selectAll("rect"));
}
/**
* Smoothly transitions and then removes a certain series when the chart is in `grouped` mode.
* @param {d3.selection} layerToBeRemoved The layer that contains the series' bars
*/
function removeGroupedSeries (layerToBeRemoved) {
layerToBeRemoved.selectAll("rect").transition()
.duration(animationDuration)
.delay(barDelay)
.attr("y", groupedBarBaseY)
.attr("height", 0)
.call(endAll, function () {
layerToBeRemoved.remove();
layers.selectAll("rect").transition()
.duration(animationDuration)
.delay(barDelay)
.attr("x", groupedBarX)
.attr("width", groupedBarWidth);
});
}
/**
* Adds a certain series.
*/
function addSeries () {
var newLayer;
newLayer = layers.enter().append("g")
.attr("class", layerClass);
if (chartMode === "stacked") {
addStackedSeries(newLayer);
}
else {
addGroupedSeries(newLayer);
}
}
/**
* Smoothly transitions and adds a certain series when the chart is in `stacked` mode.
* @param {d3.selection} newLayer The new layer to be added
*/
function addStackedSeries (newLayer) {
newLayer.selectAll("rect").data(function (d) { return d.values; })
.enter().append("rect")
.attr("x", stackedBarX)
.attr("y", stackedBarBaseY)
// .attr("width", stackedBarWidth)
.attr("width", thinBarWidth)
.attr("height", 0);
transitionStackedBars(layers.selectAll("rect"));
}
/**
* Smoothly transitions and adds a certain series when the chart is in `grouped` mode.
* @param {d3.selection} newLayer The new layer to be added
*/
function addGroupedSeries (newLayer) {
var newBars;
layers.selectAll("rect").transition()
.duration(animationDuration)
.delay(barDelay)
.attr("x", groupedBarX)
.attr("width", groupedBarWidth)
.call(endAll, function () {
newBars = newLayer.selectAll("rect").data(function (d) { return d.values; })
.enter().append("rect")
.attr("y", groupedBarBaseY)
.attr("width", groupedBarWidth)
.attr("height", 0);
layers.selectAll("rect").attr("x", groupedBarX);
newBars.transition()
.duration(animationDuration)
.delay(barDelay)
.attr("y", groupedBarY)
.attr("height", barHeight);
});
}
/**
* Changes the chart to the selected mode: `stacked` or `grouped`.
* In `stacked` mode, the bars of each bin are stacked together.
* In `grouped` mode, the bars of each bin are placed side by side.
*/
function changeChartMode() {
chartMode = this.value;
if (chartMode === "stacked") {
stackBars();
}
else {
groupBars();
}
}
/**
* Smoothly transitions the chart to `stacked` mode.
* In this mode, the bars of each bin are stacked together.
*/
function stackBars() {
layers.selectAll("rect").transition()
.duration(animationDuration)
.delay(barDelay)
.attr("y", stackedBarY)
.transition()
.duration(animationDuration)
.attr("x", stackedBarX)
.attr("width", stackedBarWidth);
}
/**
* Smoothly transitions the chart to `grouped` mode.
* In this mode, the bars of each bin are placed side by side.
*/
function groupBars() {
layers.selectAll("rect").transition()
.duration(animationDuration)
.delay(barDelay)
.attr("x", groupedBarX)
.attr("width", groupedBarWidth)
.transition()
.duration(animationDuration)
.attr("y", groupedBarY);
}
/**
* Shows the tooltip.
*/
function showTooltip() {
var hoveredBarIndex,
tooltip;
hoveredBarIndex = (d3.mouse(this)[0] / widthPerStack) | 0;
if (hoveredBarIndex === lastHoveredBarIndex) {
return;
}
lastHoveredBarIndex = hoveredBarIndex;
layers.selectAll("rect").classed("highlighted", function (d, i) { return (i === hoveredBarIndex); });
tooltip = $(".js-tooltip");
tooltip.find(".js-tooltip-table").html(tooltipContent());
tooltip.css({
top: margin.top + highestBinBarHeight() - tooltip.outerHeight() - tooltipBottomMargin,
left: margin.left + (hoveredBarIndex * widthPerStack) + (widthPerStack / 2) - (tooltip.outerWidth() / 2),
}).fadeIn();
}
function tooltipContent () {
var bars, template;
bars = [];
layers.each(function (d) {
bars.unshift({ name: d.name, value: d.values[lastHoveredBarIndex].y.toFixed(4) });
});
template = $(".js-tooltip-table-content").html();
return _.template(template, { bars: bars });
}
/**
* Hides the tooltip.
*/
function hideTooltip () {
$(".js-tooltip").stop().hide();
layers.selectAll("rect")
.filter(function (d, i) { return (i === lastHoveredBarIndex); })
.classed("highlighted", false);
lastHoveredBarIndex = undefined;
}
/**
* Calculates the height of the highest (not tallest) bar within a certain bin.
* @return {Number} The height, in pixels, of the highest bar within a certain bin
*/
function highestBinBarHeight() {
var bars,
highestGroupBar;
if (chartMode === "stacked") {
highestGroupBar = _.last(layers.data()).values[lastHoveredBarIndex];
return yScale(highestGroupBar.y0 + highestGroupBar.y);
}
else {
bars = _.map(layers.data(), function (series) { return series.values[lastHoveredBarIndex]; });
highestGroupBar = _.max(bars, function (bar) { return bar.y; });
return yScale(highestGroupBar.y);
}
}
/**
* Calls a function at the end of **all** transitions.
* @param {d3.transition} transition A D3 transition
* @param {Function} callback The function to be called at the end of **all** transitions
*/
function endAll (transition, callback) {
var n;
if (transition.empty()) {
callback();
}
else {
n = transition.size();
transition.each("end", function () {
n--;
if (n === 0) {
callback();
}
});
}
}
// Inspired by Lee Byron's test data generator.
function bumpLayer(n, o) {
function bump(a) {
var x = 1 / (.1 + Math.random()),
y = 2 * (Math.random() - .5),
z = 10 / (.1 + Math.random());
for (var i = 0; i < n; i++) {
var w = (i / n - y) * z;
a[i] += x * Math.exp(-w * w);
}
}
var a = [], i;
for (i = 0; i < n; ++i) a[i] = o + o * Math.random();
for (i = 0; i < 5; ++i) bump(a);
return a.map(function (d, i) { return {x: i, y: Math.max(0, Math.floor(d))}; });
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.stacked-chart-container {
position: relative;
}
.stacked-chart-container .controls {
position: absolute;
top: 24px;
left: 18px;
}
.stacked-chart .clickable {
cursor: pointer;
}
.stacked-chart-container .tooltip {
position: absolute;
font-size: 13px;
white-space: nowrap;
border: 1px solid black;
background-color: white;
pointer-events: none;
border-radius: 5px;
display: none;
}
.stacked-chart-container .tooltip-wrapper {
position: relative;
padding: 6px;
}
.stacked-chart-container .tooltip-wrapper:before {
content: "";
position: absolute;
width: 0;
height: 0;
bottom: -20px;
left: 50%;
transform: translateX(-50%);
border: 10px solid;
border-color: black transparent transparent transparent;
}
.stacked-chart-container .tooltip-wrapper:after {
content: "";
position: absolute;
width: 0;
height: 0;
bottom: -19px;
left: 50%;
transform: translateX(-50%);
border: 10px solid;
border-color: white transparent transparent transparent;
}
.stacked-chart-container .tooltip-table {
text-align: right;
}
.stacked-chart path,
.stacked-chart line,
.stacked-chart rect {
shape-rendering: crispEdges;
}
.stacked-chart text {
font: 10px sans-serif;
font-weight: 700;
}
.stacked-chart .axis path,
.stacked-chart .axis line {
fill: none;
/* stroke: #000; */
}
.stacked-chart .series-so {
fill: steelblue;
}
.stacked-chart .series-jp {
fill: #CCC;
}
.stacked-chart .series-lo {
fill: #CD4638;
}
.stacked-chart .grid-lines {
fill: none;
stroke: lightgrey;
}
.stacked-chart .layer rect {
opacity: 0.8;
transition: opacity 0.5s ease;
}
.stacked-chart .layer rect.highlighted {
opacity: 1;
}
.stacked-chart .overlay {
opacity: 0;
}
.stacked-chart .series-box {
stroke-width: 2px;
}
/* Series classes */
.stacked-chart .series-so .series-box {
stroke: steelblue;
}
.stacked-chart .series-jp .series-box {
stroke: #CCC;
}
.stacked-chart .series-lo .series-box {
stroke: #CD4638;
}
/* Stroke for the addressBar */
.stacked-chart .addressBar {
fill: #fff;
stroke: #ececec;
}
.stacked-chart .disabled .series-box {
fill-opacity: 0;
}
.stacked-chart .series-label {
fill: black;
text-anchor: end;
alignment-baseline: central;
}
.tick line {display: none;}
</style>
<body>
<div class="stacked-chart-container js-stacked-chart-container">
<!-- <form class="controls"> -->
<!-- <label><input type="radio" name="mode" value="stacked" checked>Stacked</label> -->
<!-- <label><input type="radio" name="mode" value="grouped">Grouped</label> -->
<!-- </form> -->
<svg class="stacked-chart js-stacked-chart"></svg>
<div class="tooltip js-tooltip">
<div class="tooltip-wrapper">
<table class="tooltip-table js-tooltip-table"></table>
</div>
</div>
</div>
<div class="js-legend-chart-container">
<svg class="legend-chart js-legend-chart"></svg>
</div>
<script type="text/x-underscore" class="js-tooltip-table-content">
<table>
<% _.each(bars, function (bar) { %>
<tr>
<td><%= bar.name %></td>
<td><%= bar.value %></td>
</tr>
<% }); %>
</table>
</script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.5.2/underscore-min.js"></script>
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="dynamic-stack-chart.js"></script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment