Skip to content

Instantly share code, notes, and snippets.

@monkeycycle
Forked from cool-Blue/index.html
Created April 2, 2016 04:53
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 monkeycycle/6ca5a3b92f509a8e7dee1ca5d333f4d8 to your computer and use it in GitHub Desktop.
Save monkeycycle/6ca5a3b92f509a8e7dee1ca5d333f4d8 to your computer and use it in GitHub Desktop.
stacked bar chart with dynamic axes and labels
<!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>
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