const n = 4; // The number of series const m = 58; // The number of values per series // The xz array has m elements, representing the x-values shared by all series // The yz array has n elements, representing the y-values of each of the n series // Each yz[i] is an array of m non-negative numbers representing a y-value for xz[i] // The x01z array has the same structure as yz, but with stacked [y₀, y₁] instead of y const xz = d3.range(n).map(() => bumps(m)); const yz = d3.range(m); const x01z = d3.stack().keys(d3.range(n))(d3.transpose(xz)); const xMaxGrouped = d3.max(xz, x => d3.max(x)); const xMaxStacked = d3.max(x01z, x => d3.max(x, d => d[1])); console.log('xz', xz); console.log('yz', yz); console.log('x01z', x01z); console.log('xMaxGrouped', xMaxGrouped); console.log('xMaxStacked', xMaxStacked); const svg = d3.select('svg'); const controlHeight = 50; const margin = {top: 10, right: 10, bottom: 20, left: 20}; const width = +svg.attr('width') - margin.left - margin.right; const height = +svg.attr('height') - controlHeight - margin.top - margin.bottom; const g = svg.append('g') .attr('transform', `translate(${margin.left},${margin.top})`); const x = d3.scaleLinear() .domain([0, xMaxStacked]) .range([0, width]); const y = d3.scaleBand() .domain(yz) .rangeRound([0, height]) .padding(0.08); const color = d3.scaleOrdinal() .domain(d3.range(n)) .range(d3.schemeCategory20c.slice(8, 12)); // greens const series = g.selectAll('.series') .data(x01z) .enter().append('g') .attr('fill', (d, i) => color(i)); const rect = series.selectAll('rect') .data(d => d) .enter().append('rect') .attr('x', 0) .attr('y', (d, i) => y(i)) .attr('width', 0) .attr('height', y.bandwidth()); rect.transition() .delay((d, i) => i * 10) .attr('x', d => x(d[0])) .attr('y', (d, i) => y(i)) .attr('width', d => x(d[1]) - x(d[0])); g.append('g') .attr('class', 'axis axis--y') .attr('transform', `translate(0,0)`) .call(d3.axisLeft(y) .tickSize(0) .tickPadding(6)); d3.selectAll('input') .on('change', changed); const timeout = d3.timeout(() => { d3.select('input[value=\'grouped\']') .property('checked', true) .dispatch('change'); }, 2000); function changed() { timeout.stop(); if (this.value === 'grouped') transitionGrouped(); else transitionStacked(); } function transitionGrouped() { x.domain([0, xMaxGrouped]); rect.transition() .duration(500) .delay((d, i) => i * 10) .attr('y', function(d, i) { return y(i) + y.bandwidth() / n * this.parentNode.__data__.key; }) .attr('height', y.bandwidth() / n) .transition() .attr('x', d => x(0)) .attr('width', d => x(0) + x(d[1] - d[0])); } function transitionStacked() { x.domain([0, xMaxStacked]); rect.transition() .duration(500) .delay((d, i) => i * 10) .attr('x', d => x(d[0])) .attr('width', d => x(d[1]) - x(d[0])) .transition() .attr('y', (d, i) => y(i)) .attr('height', y.bandwidth()); } // Returns an array of m psuedorandom, smoothly-varying non-negative numbers. // Inspired by Lee Byron’s test data generator. // http://leebyron.com/streamgraph/ function bumps(m) { const values = []; let i; let j; let w; let x; let y; let z; // Initialize with uniform random values in [0.1, 0.2). for (i = 0; i < m; ++i) { values[i] = 0.1 + 0.1 * Math.random(); } // Add five random bumps. for (j = 0; j < 5; ++j) { x = 1 / (0.1 + Math.random()); y = 2 * Math.random() - 0.5; z = 10 / (0.1 + Math.random()); for (i = 0; i < m; i++) { w = (i / m - y) * z; values[i] += x * Math.exp(-w * w); } } // Ensure all values are positive. for (i = 0; i < m; ++i) { values[i] = Math.max(0, values[i]); } return values; }