From f797073f64c1974623b009c7b2d92829834ce2df Mon Sep 17 00:00:00 2001 From: Raphael Benitte Date: Mon, 9 May 2016 16:04:11 +0900 Subject: [PATCH] feat(stack-filters): fix areas z-index + seamless areas transitions --- src/components/charts/stack/Stack.js | 95 +++++++++++++++++++++--- src/components/charts/stack/StackDots.js | 45 ++++++----- 2 files changed, 104 insertions(+), 36 deletions(-) diff --git a/src/components/charts/stack/Stack.js b/src/components/charts/stack/Stack.js index 1874a8445..54694cd64 100644 --- a/src/components/charts/stack/Stack.js +++ b/src/components/charts/stack/Stack.js @@ -19,6 +19,22 @@ import { margin as marginPropType } from '../../../PropTypes'; import decoratorsFromReactChildren from '../../../lib/decoratorsFromReactChildren'; +const findPrecedingLayer = (layers, index) => { + if (layers === null) { + return null; + } + + for (let i = layers.length - 1; i >= 0; i--) { + const layer = layers[i]; + if (layer.index < index) { + return layer; + } + } + + return null; +}; + + class Stack extends Component { constructor(props) { super(props); @@ -26,6 +42,8 @@ class Stack extends Component { this.state = { excludeLayers: [] }; + + this.previousData = null; } renderD3(props, state) { @@ -95,30 +113,70 @@ class Stack extends Component { .domain([0, d3.max(stacked, layer => d3.max(layer.values, d => (d.y0 + d.y)))]) ; + filteredData = filteredData.map(layer => { + return _.assign(layer, { + values: layer.values.map(v => ({ + value: v, + interpolated: { + x: xScale(v.x), + y0: yScale(v.y0), + y: yScale(v.y0 + v.y) + } + })) + }); + }); + const area = d3.svg.area() .interpolate(interpolation) - .x(d => xScale(d.x)) - .y0(d => yScale(d.y0)) - .y1(d => yScale(d.y0 + d.y)) + .x(d => d.x) + .y0(d => d.y0) + .y1(d => d.y) ; // ————————————————————————————————————————————————————————————————————————————————————————————————————————————— // Areas // ————————————————————————————————————————————————————————————————————————————————————————————————————————————— - let paths = wrapper.selectAll('.nivo_stack_area').data(stacked, d => d.index); + let paths = wrapper.select('.nivo_stack_areas').selectAll('.nivo_stack_area').data(stacked, d => d.index); // ENTER paths.enter().append('path') .attr('class', 'nivo_stack_area') - .attr('d', d => area(d.values)) + .attr('d', d => { + if (this.previousData === null) { + return area(d.values.map(p => ({ + x: p.interpolated.x, + y0: yScale.range()[0], + y: yScale.range()[0], + }))); + } + + const precedingLayer = findPrecedingLayer(this.previousData, d.index); + + if (precedingLayer !== null) { + return area(precedingLayer.values.map(p => ({ + x: p.interpolated.x, + y0: p.interpolated.y, + y: p.interpolated.y, + }))); + } + + return area(d.values.map(p => ({ + x: p.interpolated.x, + y0: p.interpolated.y0, + y: p.interpolated.y0, + }))); + }) .style('fill', d => d.color) ; // UPDATE paths .on('click', d => { - this.setState({ excludeLayers: excludeLayers.concat([d.index]) }); + // we cannot have no layer + if (filteredData.length > 1) { + this.setState({ excludeLayers: excludeLayers.concat([d.index]) }); + } }) .on('mouseover', function (d) { d3.select(this).style('fill', overColorFn); @@ -132,7 +190,7 @@ class Stack extends Component { .transition() .duration(transitionDuration) .ease(transitionEasing) - .attr('d', d => area(d.values)) + .attr('d', d => area(d.values.map(v => v.interpolated))) .style('fill', d => d.color) ; @@ -142,13 +200,22 @@ class Stack extends Component { .duration(transitionDuration) .ease(transitionEasing) .attr('d', d => { + const precedingLayer = findPrecedingLayer(filteredData, d.index); + + if (precedingLayer !== null) { + return area(precedingLayer.values.map(p => ({ + x: p.interpolated.x, + y0: p.interpolated.y, + y: p.interpolated.y, + }))); + } + return area(d.values.map(p => ({ - x: p.x, - y0: p.y0, - y: 0, + x: p.interpolated.x, + y0: p.interpolated.y0, + y: p.interpolated.y0, }))); }) - .style('opacity', 0) .remove() ; @@ -199,6 +266,8 @@ class Stack extends Component { this.decorators.forEach(decorator => { decorator(stackContext); }); + + this.previousData = filteredData; } shouldComponentUpdate(nextProps, nextState) { @@ -218,7 +287,9 @@ class Stack extends Component { render() { return ( - + + + ); } diff --git a/src/components/charts/stack/StackDots.js b/src/components/charts/stack/StackDots.js index c11be701d..57d4c671e 100644 --- a/src/components/charts/stack/StackDots.js +++ b/src/components/charts/stack/StackDots.js @@ -26,26 +26,27 @@ class StackDots extends Component { const colorFn = getColorGenerator(props.color); const borderColorFn = getColorGenerator(props.borderColor); const { showOnOver } = props; - const { onMouseOver, onMouseOut } = props; // Receive context from Parent Stack component return ({ - element, wrapper, + wrapper, stacked, width, height, - xScale, yScale, transitionDuration, transitionEasing }) => { const slices = []; stacked.forEach(layer => { layer.values.forEach((datum, i) => { if (!slices[i]) { - slices[i] = []; + slices[i] = { + x: datum.interpolated.x, + values: [], + }; } - slices[i].push(_.assign({}, datum, { + slices[i].values.push(_.assign({}, datum, { index: layer.index, - color: layer.color + color: layer.color, })); }); }); @@ -54,7 +55,7 @@ class StackDots extends Component { const newSlices = elements.enter().append('g') .attr('class', 'nivo_stack_slices') - .attr('transform', (d, i) => `translate(${xScale(i)},0)`) + .attr('transform', d => `translate(${d.x},0)`) .style('opacity', showOnOver ? 0 : 1) .style('pointer-events', 'all') ; @@ -96,19 +97,19 @@ class StackDots extends Component { .transition() .duration(transitionDuration) .ease(transitionEasing) - .attr('transform', (d, i) => `translate(${xScale(i)},0)`) + .attr('transform', d => `translate(${d.x},0)`) ; // ————————————————————————————————————————————————————————————————————————————————————————————————————————— // Lines // ————————————————————————————————————————————————————————————————————————————————————————————————————————— - const lines = elements.selectAll('line').data(d => d, d => d.index); + const lines = elements.selectAll('line').data(d => d.values, d => d.index); // ENTER lines.enter().append('line') - .attr('y1', d => yScale(d.y0 + d.y)) - .attr('y2', d => yScale(d.y0)) + .attr('y1', d => d.interpolated.y) + .attr('y2', d => d.interpolated.y0) .style('stroke-width', props.borderWidth) .style('stroke', borderColorFn) ; @@ -118,8 +119,8 @@ class StackDots extends Component { .transition() .duration(transitionDuration) .ease(transitionEasing) - .attr('y1', d => yScale(d.y0 + d.y)) - .attr('y2', d => yScale(d.y0)) + .attr('y1', d => d.interpolated.y) + .attr('y2', d => d.interpolated.y0) .style('stroke-width', props.borderWidth) .style('stroke', borderColorFn) ; @@ -129,8 +130,8 @@ class StackDots extends Component { .transition() .duration(transitionDuration) .ease(transitionEasing) - .attr('y1', d => yScale(d.y0)) - .attr('y2', d => yScale(d.y0)) + .attr('y1', d => d.interpolated.y0) + .attr('y2', d => d.interpolated.y0) .style('opacity', 0) .remove() ; @@ -139,15 +140,13 @@ class StackDots extends Component { // ————————————————————————————————————————————————————————————————————————————————————————————————————————— // Circles // ————————————————————————————————————————————————————————————————————————————————————————————————————————— - const circles = elements.selectAll('circle').data(d => d, d => d.index); + const circles = elements.selectAll('circle').data(d => d.values, d => d.index); // ENTER circles.enter().append('circle') .attr('r', 0.1) .style('cursor', 'pointer') - .attr('transform', d => { - return `translate(0,${yScale(d.y0 + d.y)})`; - }) + .attr('transform', d => `translate(0,${d.interpolated.y})`) .style('fill', colorFn) .style('stroke-width', props.borderWidth) .style('stroke', borderColorFn) @@ -155,7 +154,7 @@ class StackDots extends Component { // UPDATE circles - .on('mouseover', function (d) { + .on('mouseover', function () { d3.select(this) .transition() .duration(overTransitionDuration) @@ -163,7 +162,7 @@ class StackDots extends Component { .attr('r', props.radius * 2) ; }) - .on('mouseout', function (d) { + .on('mouseout', function () { d3.select(this) .transition() .duration(overTransitionDuration) @@ -175,9 +174,7 @@ class StackDots extends Component { .duration(transitionDuration) .ease(transitionEasing) .attr('r', props.radius) - .attr('transform', d => { - return `translate(0,${yScale(d.y0 + d.y)})`; - }) + .attr('transform', d => `translate(0,${d.interpolated.y})`) .style('fill', colorFn) .style('stroke-width', props.borderWidth) .style('stroke', borderColorFn)