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)