forked from guilhermesimoes's block: D3.js: Animating Stacked-to-Grouped Bars
forked from vikkya's block: D3.js: Animating Stacked-to-Grouped Bars
forked from Thanaporn-sk's block: D3.js: Animating Stacked-to-Grouped Bars
| license: mit |
forked from guilhermesimoes's block: D3.js: Animating Stacked-to-Grouped Bars
forked from vikkya's block: D3.js: Animating Stacked-to-Grouped Bars
forked from Thanaporn-sk's block: D3.js: Animating Stacked-to-Grouped Bars
| <!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; | |
| } | |
| .stacked-chart .axis path, | |
| .stacked-chart .axis line { | |
| fill: none; | |
| stroke: #000; | |
| } | |
| .stacked-chart .series-yes { | |
| fill: steelblue; | |
| } | |
| .stacked-chart .series-no { | |
| fill: #CCC; | |
| } | |
| .stacked-chart .series-maybe { | |
| 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; | |
| } | |
| .stacked-chart .series-yes .series-box { | |
| stroke: steelblue; | |
| } | |
| .stacked-chart .series-no .series-box { | |
| stroke: #CCC; | |
| } | |
| .stacked-chart .series-maybe .series-box { | |
| stroke: #CD4638; | |
| } | |
| .stacked-chart .disabled .series-box { | |
| fill-opacity: 0; | |
| } | |
| .stacked-chart .series-label { | |
| fill: black; | |
| text-anchor: end; | |
| alignment-baseline: central; | |
| } | |
| </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> | |
| <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="https://d3js.org/d3.v3.min.js" integrity="sha384-N8EP0Yml0jN7e0DcXlZ6rt+iqKU9Ck6f1ZQ+j2puxatnBq4k9E8Q6vqBcY34LNbn" crossorigin="anonymous"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js" integrity="sha384-FZY+KSLVXVyc1qAlqH9oCx1JEOlQh6iXfw3o2n3Iy32qGjXmUPWT9I0Z9e9wxYe3" crossorigin="anonymous"></script> | |
| <script src="https://code.jquery.com/jquery-3.1.0.min.js" integrity="sha384-nrOSfDHtoPMzJHjVTdCopGqIqeYETSXhZDFyniQ8ZHcVy08QesyHcnOUpMpqnmWq" crossorigin="anonymous"></script> | |
| <script type="text/javascript"> | |
| "use strict"; | |
| /*global $, d3, _ */ | |
| var seriesNames = ["Maybe", "No", "Yes"], | |
| numSamples = 22, | |
| numSeries = seriesNames.length, | |
| data = seriesNames.map(function (name) { | |
| return { | |
| name: name, | |
| values: bumpLayer(numSamples, 0.1) | |
| }; | |
| }), | |
| //stack = d3.layout.stack().values(function (d) { return d.values; }); | |
| console.log(data); | |
| stack(data); | |
| var chartMode = "stacked", | |
| numEnabledSeries = numSeries, | |
| lastHoveredBarIndex, | |
| containerWidth = document.querySelector(".js-stacked-chart-container").clientWidth, | |
| containerHeight = 500, | |
| margin = {top: 80, right: 30, 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, | |
| legendSeriesBoxX = 0, | |
| legendSeriesBoxY = 0, | |
| legendSeriesBoxWidth = 15, | |
| legendSeriesBoxHeight = 15, | |
| legendSeriesHeight = legendSeriesBoxHeight + paddingBetweenLegendSeries, | |
| legendSeriesLabelX = -5, | |
| legendSeriesLabelY = legendSeriesBoxHeight / 2, | |
| legendMargin = 20, | |
| legendX = containerWidth - legendSeriesBoxWidth - legendMargin, | |
| legendY = legendMargin, | |
| tooltipTemplate = _.template(document.querySelector(".js-tooltip-table-content").innerHTML), | |
| 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]); | |
| var xAxis = d3.svg.axis() | |
| .scale(xScale) //binsScale) | |
| .ticks(numSamples) | |
| .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 "clickable " + 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); }; | |
| var transitionStackedBars = function (selection) { | |
| selection.transition() | |
| .duration(animationDuration) | |
| .delay(barDelay) | |
| .attr("y", stackedBarY) | |
| .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 + ")"); | |
| mainArea.append("g") | |
| .attr("class", "grid-lines") | |
| .selectAll(".grid-line").data(yScale.ticks(numYAxisTicks)) | |
| .enter().append("line") | |
| .attr("class", "grid-line") | |
| .attr("x1", 0) | |
| .attr("x2", width) | |
| .attr("y1", yScale) | |
| .attr("y2", yScale); | |
| var layersArea = mainArea.append("g") | |
| .attr("class", "layers"); | |
| var layers = layersArea.selectAll(".layer").data(data) | |
| .enter().append("g") | |
| .attr("class", layerClass); | |
| layers.selectAll("rect").data(function (d) { return d.values; }) | |
| .enter().append("rect") | |
| .attr("x", stackedBarX) | |
| .attr("y", height) | |
| .attr("width", stackedBarWidth) | |
| .attr("height", 0) | |
| .call(transitionStackedBars); | |
| mainArea.append("g") | |
| .attr("class", "x axis") | |
| .attr("transform", "translate(0," + height + ")") | |
| .call(xAxis); | |
| mainArea.append("g") | |
| .attr("class", "y axis") | |
| .call(yAxis); | |
| svg.append("rect") | |
| .attr("class", "overlay") | |
| .attr("transform", "translate(" + margin.left + "," + margin.top + ")") | |
| .attr("x", 0) | |
| .attr("y", 0) | |
| .attr("width", width) | |
| .attr("height", height) | |
| .on("mousemove", showTooltip) | |
| .on("mouseout", hideTooltip); | |
| 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); | |
| /** | |
| * 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("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; | |
| bars = []; | |
| layers.each(function (d) { | |
| bars.unshift({ name: d.name, value: d.values[lastHoveredBarIndex].y.toFixed(4) }); | |
| }); | |
| return tooltipTemplate({ 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, d)}; }); | |
| } | |
| </script> | |
| </body> |