Last active
April 2, 2016 04:53
-
-
Save cool-Blue/c2dcbd3e6be86be18fdd to your computer and use it in GitHub Desktop.
stacked bar chart with dynamic axes and labels
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
<!DOCTYPE html> | |
<html> | |
<head lang="en"> | |
<meta charset="UTF-8"> | |
<title>http://stackoverflow.com/questions/32057842/d3-js-highlighting-stacked-bar-and-getting-selected-values/32079517#32079517</title> | |
<style> | |
body { | |
position: relative; | |
} | |
#vis { | |
margin: 100px; | |
position: relative; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
.fly-in { | |
font-size: 8px; | |
} | |
.axis .tick line { | |
stroke: #ccc; | |
/*opacity: 0.5;*/ | |
pointer-events: none; | |
} | |
/*.axis .minor line{*/ | |
/*stroke: red;*/ | |
/*}*/ | |
.highlight { | |
font-weight: bold ; | |
} | |
svg { | |
overflow: visible; | |
} | |
</style> | |
</head> | |
<body> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="UTF-8"></script> | |
<!--<script src="https://rawgit.com/cool-Blue/d3-lib/516508b6aa8d9ae724ceb194257226aa29d48fb7/inputs/select/select.js"></script>--> | |
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/inputs/select/select.js" charset="UTF-8"></script> | |
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/plot-transform-2.0.0/plot-transform.js" charset="UTF-8"></script> | |
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/transitions/end-all/endAll.js" charset="UTF-8"></script> | |
<script src="script.js"></script> | |
</body> | |
</html> |
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
var width = 760, | |
height = 300, | |
padding = {left: 50, right: 200, top: 20, bottom: 30}, | |
xRangeWidth = width - padding.left - padding.right, | |
yRangeHeight = height - padding.top - padding.bottom; | |
var vis = d3.select("body").append("div").attr({ | |
margin: "auto", | |
id: "vis" | |
}), | |
svg = vis | |
.append("svg") | |
.attr("width", width) | |
.attr("height", height) | |
.append("g") | |
.attr("transform", "translate(" + [padding.left, padding.top] + ")"); | |
var dataSet1 = [ | |
{ | |
name: "PC", | |
sales: [{year: 2005, profit: 3000}, | |
{year: 2006, profit: 1300}, | |
{year: 2007, profit: 3700}, | |
{year: 2008, profit: 4900}, | |
{year: 2009, profit: 700}] | |
}, | |
{ | |
name: "SmartPhone", | |
sales: [{year: 2005, profit: 2000}, | |
{year: 2006, profit: 4000}, | |
{year: 2007, profit: 1810}, | |
{year: 2008, profit: 6540}, | |
{year: 2009, profit: 2820}] | |
}, | |
{ | |
name: "Software", | |
sales: [{year: 2005, profit: 1100}, | |
{year: 2006, profit: 1700}, | |
{year: 2007, profit: 1680}, | |
{year: 2008, profit: 4000}, | |
{year: 2009, profit: 4900}] | |
} | |
]; | |
var offsetSelect = d3.ui.select({ | |
base: vis, | |
before: "svg", | |
style: {position: "absolute", left: width - padding.right + 15 + "px", top: yRangeHeight + "px"}, | |
onchange: function() { | |
update(dataSet1) | |
}, | |
data: ["wiggle", "zero", "expand", "silhouette"] | |
}), | |
orderSelect = d3.ui.select({ | |
base: vis, | |
before: "svg", | |
style: {position: "absolute", left: width - padding.right + 15 + "px", top: yRangeHeight - 20 + "px"}, | |
onchange: function() { | |
update(dataSet1) | |
}, | |
data: ["inside-out", "default", "reverse"] | |
}), | |
stack = d3.layout.stack() | |
.values(function(d) { return d.sales; }) | |
.x(function(d) { return d.year; }) | |
.y(function(d) { return d.profit; }) | |
.out(function out(d, y0, y) { | |
d.p0 = y0; | |
d.y = y; | |
} | |
); | |
// x Axis | |
var xPadding = {inner: 0.1, outer: 0.3}, | |
xScale = d3.scale.ordinal() | |
.rangeBands([0, xRangeWidth], xPadding.inner, xPadding.outer), | |
xAxis = d3.cbPlot.d3Axis() | |
.scale(xScale) | |
.orient("bottom"), | |
gX = svg.append("g") | |
.attr("class", "x axis") | |
.attr("transform", "translate(0," + yRangeHeight + ")"); | |
// y Axis | |
var yAxisScale = d3.scale.linear() | |
.range([yRangeHeight, 0]), | |
yAxis = d3.cbPlot.d3Axis() | |
.scale(yAxisScale) | |
.orient("left") | |
.tickSubdivide(2), | |
gY = svg.append("g") | |
.attr("class", "y axis") | |
.style({"pointer-events": "none", "font-size": "12px"}), | |
yAxisTransition = 1000; | |
var yPlotScale = d3.scale.linear() | |
.range([0, yRangeHeight]); | |
var color = d3.scale.category10(); | |
function update(dataSet) { | |
// create an array of normalised layers and | |
// add the normalised values onto the data | |
var normData = stack.offset("expand")(dataSet) | |
.map(stack.values()) | |
.map(function(s) { | |
return s.map(function(p) {return p.yNorm = p.y}) | |
}), | |
stackedData = stack.offset(offsetSelect.value()) | |
.order(orderSelect.value())(dataSet), | |
maxY = d3.max(stackedData, function(d) { | |
return d3.max(d.sales, function(s) { | |
return s.profit + s.p0 | |
}) | |
}), | |
years = stackedData[0].sales.map(stack.x()), | |
yearlyTotals = years.reduce(function(t, y) { | |
return (t[y] = d3.sum(stackedData, function(o) { | |
return o.sales.filter(function(s) { | |
return s.year == y | |
})[0].profit | |
}), t) | |
}, {}); | |
xScale.domain(years); | |
yAxisScale.reset = function(){ | |
this.domain([0, offsetSelect.value() == "expand" ? 1 : maxY]) | |
.range([yRangeHeight, 0]) | |
.ticks(10) | |
}; | |
yAxisScale.reset(); | |
yPlotScale.domain(yAxisScale.domain()); | |
// plotArea | |
// (svg) -> (g.plotArea)[stackedData] | |
// apply a transform to map screen space to cartesian space | |
// this removes all confusion and mess when plotting data! | |
var plotArea = svg.selectAll(".plotArea") | |
.data([stackedData]); | |
plotArea.enter().insert("g", ".axis") | |
.attr(d3.cbPlot.transplot(yRangeHeight)) | |
.attr("class", "plotArea"); | |
/* | |
plotArea.series | |
(g.plotArea)[stackedData] xF transPlot | |
?data d | |
\ | |
`+-> (g.plotArea.series)[stackedData[0]] | |
: | |
: | |
\ | |
`+-> (g.plotArea.series)[stackedData[m]] | |
*/ | |
plotArea.series = plotArea.selectAll(".series") | |
.data(ID); | |
plotArea.series.enter() | |
.append("g") | |
.attr("class", "series"); | |
plotArea.series.style("fill", function(d, i) { | |
return color(i); | |
}); | |
plotArea.series.exit().remove(); | |
Object.defineProperties(plotArea.series, d3._CB_selection_destructure); | |
/* | |
plotArea.series.components | |
(g.series)[stackedData[0]] | |
?data d3.entries(d) | |
\ | |
`+-> (g.name)[stackedData[0].name] | |
+-> (g.sales)[stackedData[0].sales] | |
: | |
: | |
(g.series)[stackedData[m]] | |
?data d3.entries(d) | |
\ | |
`+-> (g.name)[stackedData[m].name] | |
+-> (g.sales)[stackedData[m].sales] | |
*/ | |
plotArea.series.components = plotArea.series.selectAll(".components") | |
.data(function(d) { | |
return d3.entries(d); | |
}); | |
plotArea.series.components.enter().append("g") | |
.attr("class", function(d){return d.key}) | |
.classed("components", true); | |
plotArea.series.components.exit().remove(); | |
/* | |
plotArea.series.components.values | |
(g.series)[stackedData[0]] | |
\ | |
`+-> (g.sales)[stackedData[0].sales] | |
: | |
: | |
(g.series)[stackedData[m]] | |
\ | |
`+-> (g.sales)[stackedData[m].sales] | |
*/ | |
plotArea.series.components.values = plotArea.series.components.filter(function(d){ | |
return d.key == "sales" | |
}); | |
Object.defineProperties(plotArea.series.components.values, d3._CB_selection_destructure); | |
/* | |
plotArea.series.components.labels | |
(g.series)[stackedData[0]] | |
\ | |
`+-> (g.name)[stackedData[0].name] xF transPlot | |
: | |
: | |
(g.series)[stackedData[m]] | |
\ | |
`+-> (g.name)[stackedData[m].name] xF transPlot | |
*/ | |
plotArea.series.components.labels = plotArea.series.components.filter(function(d){ | |
return d.key == "name" | |
}) | |
// reverse the plotArea transform (it is it's own inverse) | |
.attr(d3.cbPlot.transplot(yRangeHeight)); | |
Object.defineProperties(plotArea.series.components.labels, d3._CB_selection_destructure); | |
var s = xScale.rangeBand(), | |
w = s - xPadding.inner, | |
drag = d3.behavior.drag() | |
.on("dragstart", mouseOver), | |
/* | |
plotArea.series.components.values.points | |
(g.sales)[stackedData[0].sales] * on mouseover; * on mouseout; * bH drag | |
?data d.value | |
\ | |
`+-> (rect.point)[stackedData[0].sales.value[0] | |
: | |
+-> (rect.point)[stackedData[0].sales.value[n] | |
: | |
: | |
(g.sales)[stackedData[m].sales] * on mouseover; * on mouseout; * bH drag | |
?data d.value | |
\ | |
`+-> (rect.point)[stackedData[m].sales.value[0] | |
: | |
+-> (rect.point)[stackedData[m].sales.value[n] | |
*/ | |
points = plotArea.series.components.values.points = plotArea.series.components.values.selectAll("rect") | |
.data(function(d){ | |
return d.value | |
}); | |
points.enter() | |
.append("rect") | |
.attr({width: w, class: "point"}) | |
.on("mouseover", mouseOver) | |
.on("mouseout", mouseOut) | |
.call(drag); | |
points.transition() | |
.attr("x", function(d) { | |
return xScale(d.year); | |
}) | |
.attr("y", function(d) { | |
return yPlotScale(d.p0); | |
}) | |
.attr("height", function(d) { | |
return yPlotScale(d.y); | |
}) | |
.attr("stroke", "white"); | |
points.exit().remove(); | |
Object.defineProperties(plotArea.series.components.values.points, d3._CB_selection_destructure); | |
gX.transition().call(xAxis); | |
gY.transition().call(yAxis); | |
function mouseOver(pointData, pointIndex, groupIndex) { | |
console.log(["in", pointIndex].join("\t")); | |
var selectedYear = pointData.year, | |
// wrap the node in a selection with the proper parent | |
plotData = plotArea.series.components.values.data, | |
seriesData = plotData[groupIndex], | |
currentYear = d3.transpose(plotData)[pointIndex], | |
point = plotArea.series.components.values.points.nodes[groupIndex][pointIndex]; | |
// if the plot is not normalised, fly-in the axis on the selected year | |
if(offsetSelect.value() != "expand") { | |
yAxisScale.reset(); | |
// get the zero offset for the fly-in axis | |
var pMin = d3.min(currentYear, function(s) { | |
return s.p0 | |
}), | |
refP0 = seriesData[pointIndex].p0, | |
selectedGroupHeight = d3.sum(currentYear, function(d) {return d.y}), | |
// set the range and domain height for the selected year | |
localDomain = [0, selectedGroupHeight].map(function(d){return d + pMin - refP0}), | |
localRange = [0, selectedGroupHeight].map(function(d) {return yAxisScale(d + pMin)}); | |
console.log(yAxisScale(pMin)); | |
yAxisScale | |
.domain(localDomain) | |
.range(localRange); | |
// apply the changes to the y axis and manage the ticks | |
gY.transition("axis") | |
.duration(yAxisTransition) | |
.call(yAxis.ticks(+(Math.abs(localRange[0] - localRange[1]) / 15).toFixed())) | |
.attr("transform", "translate(" + point.attr("x") + ",0)") | |
.style({"font-size": "8px"}) | |
.call(function(t) {d3.select(t.node()).classed("fly-in", true)}); | |
// align the selected series across all years | |
points.transition("points") | |
.attr("y", alignY(seriesData[pointIndex].p0, groupIndex)) | |
.call(endAll, toolTip) | |
} else window.setTimeout(toolTip, 0); // if not expand | |
// manage the highlighting | |
// points highlighting | |
plotArea.series.transition("fade") | |
.attr("opacity", function(d, i) { | |
return i == groupIndex ? 1 : 0.5; | |
}); | |
// x axis highlighting | |
d3.selectAll(".x.axis .tick") | |
.filter(function(d) { | |
return d == selectedYear | |
}) | |
.classed("highlight", true); | |
// move the selected element to the front | |
d3.select(this.parentNode) | |
.moveToFront(); | |
gX.moveToFront(); | |
legendText(groupIndex); | |
// Tooltip | |
function toolTip() { | |
plotArea.series | |
.append("g") | |
.attr("class", "tooltip") | |
.attr("transform", "translate(" + [point.attr("x"), point.attr("y")] + ")") | |
.append("text") | |
.attr(d3.cbPlot.transflip()) | |
.text(d3.format(">8.0%")(pointData.yNorm)) | |
.attr({x: "1em", y: -point.attr("height") / 2, dy: ".35em", opacity: 0}) | |
.transition("tooltip").attr("opacity", 1) | |
.style({fill: "black", "pointer-events": "none"}) | |
} | |
} | |
function mouseOut(d, nodeIndex, groupIndex) { | |
console.log(["out", nodeIndex].join("\t")); | |
var year = d.year; | |
d3.selectAll(".x.axis .tick") | |
.filter(function(d) { | |
return d == year | |
}) | |
.classed("highlight", false); | |
plotArea.series.transition("fade") | |
.attr({opacity: 1}); | |
var g = plotArea.series.components.labels.nodes[groupIndex][0].select("text"); | |
g.classed("highlight", false); | |
g.text(g.text().split(":")[0]) | |
yAxisScale.reset(); | |
gY.selectAll(".minor").remove(); | |
gY.transition("axis").call(yAxis) | |
.attr("transform", "translate(0,0)") | |
.style({"font-size": "12px"}) | |
.call(function(t) {d3.select(t.node()).classed("fly-in", false)}); | |
plotArea.series.selectAll(".tooltip") | |
.transition("tooltip") | |
.attr({opacity: 0}) | |
.remove(); | |
points.transition("points").attr("y", function(d) { | |
return yPlotScale(d.p0); | |
}) | |
}; | |
/* | |
plotArea.series.components.labels | |
(g.name)[stackedData[0].name] | |
?data | |
\ | |
`+-> (g.label)[stackedData[0].name.value xF transPlot | |
: | |
: | |
(g.name)[stackedData[m].name] | |
?data | |
\ | |
`+-> (g.label)[stackedData[m].name.value xF transPlot | |
*/ | |
// Add the legend inside the series containers | |
// The series legend is wrapped in another g so that the | |
// plot transform can be reversed. Otherwise the text would be mirrored | |
var labHeight = 40, | |
labRadius = 10; | |
/* | |
plotArea.series.components.labels.circles | |
(g.name)[stackedData[0].name] xF transPlot | |
?data [d.value] | |
\ | |
`+-> (circle)[stackedData[0].name.value] | |
: | |
: | |
(g.name)[stackedData[m].name] xF transPlot | |
?data [d.value] | |
\ | |
`+-> (circle)[stackedData[0].name.value] | |
*/ | |
// add the marker and the legend text to the normalised container | |
// push the stackedData (name) down to them | |
var labelCircle = plotArea.series.components.labels.selectAll("circle") | |
.data(function(d){return [d.value]}), | |
// take a moment to get the series order delivered by stack | |
orders = stackedData.map(function(d) { // simplify the form | |
return {name: d.name, base: d.sales[0].p0} | |
}).sort(function(a, b) { // get a copy, sorted by p0 | |
return a.base - b.base | |
}).map(function(d) { // convert to index permutations | |
return stackedData.map(function(p) { | |
return p.name | |
}).indexOf(d.name) | |
}).reverse(); // convert to screen y ordinate | |
labelCircle.enter().append("circle") | |
.on("mouseover", function(pointData, pointIndex, groupIndex) { | |
var node = this, | |
typicalP0 = d3.median(plotArea.series.components.values.data[groupIndex], | |
function(d){return d.p0}); | |
plotArea.series.components.values.points.transition("points") | |
.attr("y", alignY(typicalP0, groupIndex)); | |
plotArea.series.transition("fade") | |
.attr("opacity", function(d) { | |
return d === d3.select(node.parentNode.parentNode).datum() ? 1 : 0.5; | |
}); | |
legendText(groupIndex); | |
}) | |
.on("mouseout", function(pointData, pointIndex, groupIndex) { | |
plotArea.series.transition("fade") | |
.attr({opacity: 1}); | |
plotArea.series.components.values.points.transition("points").attr("y", function(d) { | |
return yPlotScale(d.p0); | |
}) | |
}); | |
labelCircle.attr("cx", xRangeWidth + 20) | |
.attr("cy", function(d, i, j) { | |
return labHeight * orders[j]; | |
}) | |
.attr("r", labRadius); | |
/* | |
plotArea.series.components.labels.text | |
(g.name)[stackedData[0].name] xF transPlot | |
?data [d.value] | |
\ | |
`+-> (text)[stackedData[0].name.value] | |
: | |
: | |
(g.name)[stackedData[m].name] xF transPlot | |
?data [d.value] | |
\ | |
`+-> (text)[stackedData[0].name.value] | |
*/ | |
var labelText = plotArea.series.components.labels.selectAll("text") | |
.data(function(d){return [d.value]}); | |
labelText.enter().append("text"); | |
labelText.attr("x", xRangeWidth + 40) | |
.attr("y", function(d, i, j) { | |
return labHeight * orders[j]; | |
}) | |
.attr("dy", labRadius / 2) | |
.text(function(d) { | |
return d; | |
}); | |
function legendText(groupIndex){ | |
// Legend text | |
// add the value for the moused over item to the legend text and | |
// highlight it | |
var labelText = plotArea.series.components.labels.nodes[groupIndex][0].select("text"), | |
seriesData = plotArea.series.components.values.data[groupIndex], | |
fmt = [">8,.0f", ">8.0%"][(offsetSelect.value() == "expand") * 1]; | |
labelText.classed("highlight", true); | |
labelText.text(labelText.datum().value + ": " + d3.format(fmt)( | |
offsetSelect.value() != "expand" ? | |
d3.sum(seriesData, stack.y()) : | |
d3.sum(seriesData, function(s) { | |
var totalSales = d3.sum(d3.values(yearlyTotals)); | |
return s.y * yearlyTotals[s.year] / totalSales | |
}) | |
)); | |
} | |
function alignY(p0, series) { | |
var offsets = plotArea.series.components.values.data[series].map(function(d) { | |
return p0 - d.p0; | |
}); | |
return function(d, i) { | |
return yPlotScale(d.p0 + offsets[i]); | |
} | |
} | |
function aID(d) { | |
return [d]; | |
} | |
function ID(d) { | |
return d; | |
} | |
} | |
d3.selection.prototype.moveToFront = function() { | |
return this.each(function() { | |
this.parentNode.appendChild(this); | |
}); | |
}; | |
d3._CB_selection_destructure = { | |
"nodes": { | |
get: function() { | |
return this.map(function(g) { | |
return g.map(function(n) { | |
return d3.select(n) | |
}) | |
}) | |
} | |
}, | |
data: { | |
get: function() { | |
return this.map(function(g) { | |
return d3.select(g[0]).datum().value | |
}) | |
} | |
} | |
}; | |
update(dataSet1); | |
window.setTimeout(function(){ | |
update(dataSet1.map(function(d) { | |
return { | |
name: d.name, sales: d.sales.map(function(y) { | |
return {year: y.year, profit: y.profit / 2} | |
}) | |
} | |
}) | |
) | |
},1000) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment