Skip to content

Commit

Permalink
feat(stream): add stack tooltip on stream chart
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphaël Benitte committed Aug 12, 2017
1 parent 16b7f4d commit 2e67e44
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 18 deletions.
73 changes: 57 additions & 16 deletions src/components/charts/stream/Stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
*/
import React from 'react'
import PropTypes from 'prop-types'
import { merge, isEqual, min, max, range } from 'lodash'
import { stack as d3Stack, area as d3Area } from 'd3-shape'
import { min, max, range, sortBy } from 'lodash'
import { stack as d3Stack, area } from 'd3-shape'
import { scaleLinear, scalePoint } from 'd3-scale'
import compose from 'recompose/compose'
import pure from 'recompose/pure'
Expand All @@ -32,6 +32,7 @@ import Container from '../Container'
import Axes from '../../axes/Axes'
import Grid from '../../axes/Grid'
import StreamLayers from './StreamLayers'
import StreamSlices from './StreamSlices'

const stackMin = layers => min(layers.reduce((acc, layer) => [...acc, ...layer.map(d => d[0])], []))
const stackMax = layers => max(layers.reduce((acc, layer) => [...acc, ...layer.map(d => d[1])], []))
Expand All @@ -42,7 +43,7 @@ const Stream = ({

order,
offsetType,
curveInterpolator,
areaGenerator,

// dimensions
margin,
Expand Down Expand Up @@ -71,6 +72,9 @@ const Stream = ({

// interactivity
isInteractive,

// stack tooltip
enableStackTooltip,
}) => {
const stack = d3Stack()
.keys(keys)
Expand All @@ -85,17 +89,34 @@ const Stream = ({
const xScale = scalePoint().domain(range(data.length)).range([0, width])
const yScale = scaleLinear().domain([minValue, maxValue]).range([height, 0])

const area = d3Area()
.x((d, i) => xScale(i))
.y0(d => yScale(d[0]))
.y1(d => yScale(d[1]))
.curve(curveInterpolator)

const enhancedLayers = layers.map((layer, i) => ({
id: keys[i],
layer,
path: area(layer),
color: getColor(i),
const enhancedLayers = layers.map((points, i) => {
const layer = points.map(([y1, y2], i) => ({
index: i,
value: y2 - y1,
x: xScale(i),
y1: yScale(y1),
y2: yScale(y2),
}))

return {
id: keys[i],
layer,
path: areaGenerator(layer),
color: getColor(i),
}
})

const slices = range(data.length).map(i => ({
index: i,
x: enhancedLayers[0].layer[i].x,
stack: sortBy(
enhancedLayers.map(layer => ({
id: layer.id,
color: layer.color,
...layer.layer[i],
})),
'y2'
),
}))

const motionProps = {
Expand All @@ -118,7 +139,6 @@ const Stream = ({
/>
<StreamLayers
layers={enhancedLayers}
area={area}
fillOpacity={fillOpacity}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
Expand All @@ -136,6 +156,14 @@ const Stream = ({
left={axisLeft}
{...motionProps}
/>
{isInteractive &&
enableStackTooltip &&
<StreamSlices
slices={slices}
height={height}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
/>}
</SvgWrapper>}
</Container>
)
Expand All @@ -149,7 +177,7 @@ Stream.propTypes = {
order: stackOrderPropType.isRequired,
offsetType: stackOffsetPropType.isRequired,
curve: areaCurvePropType.isRequired,
curveInterpolator: PropTypes.func.isRequired,
areaGenerator: PropTypes.func.isRequired,

// dimensions
width: PropTypes.number.isRequired,
Expand Down Expand Up @@ -177,6 +205,9 @@ Stream.propTypes = {

// interactivity
isInteractive: PropTypes.bool,

// stack tooltip
enableStackTooltip: PropTypes.bool.isRequired,
}

export const StreamDefaultProps = {
Expand All @@ -201,13 +232,23 @@ export const StreamDefaultProps = {

// interactivity
isInteractive: true,

// stack tooltip
enableStackTooltip: true,
}

const enhance = compose(
defaultProps(StreamDefaultProps),
withTheme(),
withCurve(),
withMargin(),
withPropsOnChange(['curveInterpolator'], ({ curveInterpolator }) => ({
areaGenerator: area()
.x(({ x }) => x)
.y0(({ y1 }) => y1)
.y1(({ y2 }) => y2)
.curve(curveInterpolator),
})),
withPropsOnChange(['colors'], ({ colors }) => ({
getColor: getColorRange(colors),
})),
Expand Down
1 change: 0 additions & 1 deletion src/components/charts/stream/StreamLayers.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ const StreamLayers = ({
}

StreamLayers.propTypes = {
area: PropTypes.func.isRequired,
fillOpacity: PropTypes.number.isRequired,

// motion
Expand Down
46 changes: 46 additions & 0 deletions src/components/charts/stream/StreamSlices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* This file is part of the nivo project.
*
* Copyright 2016-present, Raphaël Benitte.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React from 'react'
import PropTypes from 'prop-types'
import pure from 'recompose/pure'
import StreamSlicesItem from './StreamSlicesItem'

const StreamSlices = ({ slices, height, showTooltip, hideTooltip }) =>
<g>
{slices.map(slice =>
<StreamSlicesItem
key={slice.index}
slice={slice}
height={height}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
/>
)}
</g>

StreamSlices.propTypes = {
slices: PropTypes.arrayOf(
PropTypes.shape({
index: PropTypes.number.isRequired,
x: PropTypes.number.isRequired,
stack: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
color: PropTypes.string.isRequired,
})
).isRequired,
})
).isRequired,
height: PropTypes.number.isRequired,
showTooltip: PropTypes.func.isRequired,
hideTooltip: PropTypes.func.isRequired,
}

export default pure(StreamSlices)
73 changes: 73 additions & 0 deletions src/components/charts/stream/StreamSlicesItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* This file is part of the nivo project.
*
* Copyright 2016-present, Raphaël Benitte.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React from 'react'
import PropTypes from 'prop-types'
import compose from 'recompose/compose'
import pure from 'recompose/pure'
import withState from 'recompose/withState'
import withHandlers from 'recompose/withHandlers'
import withPropsOnChange from 'recompose/withPropsOnChange'
import TableTooltip from '../../tooltip/TableTooltip'

const Chip = ({ color }) =>
<span style={{ display: 'block', width: '12px', height: '12px', background: color }} />

const StreamSlicesItem = ({ slice, height, showTooltip, hideTooltip, isHover }) =>
<g transform={`translate(${slice.x}, 0)`}>
{isHover &&
<line
x1={0}
x2={0}
y1={0}
y2={height}
stroke="#000"
strokeOpacity={0.35}
strokeWidth={1}
/>}
<rect
x={-20}
width={40}
height={height}
fill="#000"
fillOpacity={0}
onMouseEnter={showTooltip}
onMouseMove={showTooltip}
onMouseLeave={hideTooltip}
/>
</g>

StreamSlicesItem.propTypes = {
slice: PropTypes.object.isRequired,
height: PropTypes.number.isRequired,
showTooltip: PropTypes.func.isRequired,
hideTooltip: PropTypes.func.isRequired,
isHover: PropTypes.bool.isRequired,
}

const enhance = compose(
withState('isHover', 'setIsHover', false),
withPropsOnChange(['slice'], ({ slice }) => ({
tooltip: (
<TableTooltip rows={slice.stack.map(p => [<Chip color={p.color} />, p.id, p.value])} />
),
})),
withHandlers({
showTooltip: ({ showTooltip, setIsHover, tooltip }) => e => {
setIsHover(true)
showTooltip(tooltip, e)
},
hideTooltip: ({ hideTooltip, setIsHover }) => () => {
setIsHover(false)
hideTooltip()
},
}),
pure
)

export default enhance(StreamSlicesItem)
1 change: 0 additions & 1 deletion src/constants/directions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

export const DIRECTION_HORIZONTAL = 'horizontal'
export const DIRECTION_VERTICAL = 'vertical'
9 changes: 9 additions & 0 deletions src/constants/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* This file is part of the nivo project.
*
* Copyright 2016-present, Raphaël Benitte.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
export * from './directions'
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ export * from './components/charts/voronoi'
export * from './components/charts/stream'
export * from './components/axes'
export * from './components/markers'
export * from './constants'
export * from './props'

0 comments on commit 2e67e44

Please sign in to comment.