Skip to content

Commit

Permalink
feat(heatmap): add tooltip support for HeatMap
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphaël Benitte committed Aug 30, 2017
1 parent 37974a9 commit 422d4c2
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 227 deletions.
1 change: 1 addition & 0 deletions src/components/charts/Container.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const containerStyle = {
}

const tooltipStyle = {
pointerEvents: 'none',
position: 'absolute',
zIndex: 10,
top: 0,
Expand Down
360 changes: 183 additions & 177 deletions src/components/charts/heatmap/HeatMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,203 +6,209 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React from 'react'
import { min, max, isEqual } from 'lodash'
import { TransitionMotion, spring } from 'react-motion'
import React, { Component } from 'react'
import { partial } from 'lodash'
import { TransitionMotion } from 'react-motion'
import { colorMotionSpring, getInterpolatedColor } from '../../../lib/colors'
import { HeatMapPropTypes } from './props'
import { computeNodes } from '../../../lib/charts/heatmap'
import enhance from './enhance'
import Container from '../Container'
import SvgWrapper from '../SvgWrapper'
import Grid from '../../axes/Grid'
import Axes from '../../axes/Axes'
import HeatMapCellRect from './HeatMapCellRect'
import HeatMapCellCircle from './HeatMapCellCircle'
import HeatMapCellTooltip from './HeatMapCellTooltip'

const HeatMap = ({
data,
getIndex,
keys,

cellWidth,
cellHeight,
sizeScale,
xScale,
yScale,
offsetX,
offsetY,

margin,
width,
height,
outerWidth,
outerHeight,

// cells
cellShape,
cellOpacity,
cellBorderWidth,
getCellBorderColor,

// axes & grid
axisTop,
axisRight,
axisBottom,
axisLeft,
enableGridX,
enableGridY,

// labels
getLabelTextColor,

// theming
theme,
colorScale,

// motion
animate,
motionStiffness,
motionDamping,

// interactivity
isInteractive,
}) => {
let Cell
if (cellShape === 'rect') {
Cell = HeatMapCellRect
} else if (cellShape === 'circle') {
Cell = HeatMapCellCircle
} else {
Cell = cellShape
class HeatMap extends Component {
static propTypes = HeatMapPropTypes

handleNodeHover = (showTooltip, node, event) => {
const { setCurrentNode, theme } = this.props
setCurrentNode(node)
showTooltip(<HeatMapCellTooltip node={node} theme={theme} />, event)
}

const nodes = data.reduce((acc, d) => {
keys.forEach(key => {
acc.push({
key: `${key}.${getIndex(d)}`,
xKey: key,
yKey: getIndex(d),
x: xScale(key),
y: yScale(getIndex(d)),
width: sizeScale ? Math.min(sizeScale(d[key]) * cellWidth, cellWidth) : cellWidth,
height: sizeScale
? Math.min(sizeScale(d[key]) * cellHeight, cellHeight)
: cellHeight,
value: d[key],
color: colorScale(d[key]),
})
})

return acc
}, [])

const motionProps = {
animate,
motionDamping,
motionStiffness,
handleNodeLeave = hideTooltip => {
this.props.setCurrentNode(null)
hideTooltip()
}

let cells
if (animate === true) {
cells = (
<TransitionMotion
styles={nodes.map(node => {
return {
key: node.key,
data: node,
style: {
x: spring(node.x, motionProps),
y: spring(node.y, motionProps),
width: spring(node.width, motionProps),
height: spring(node.height, motionProps),
...colorMotionSpring(node.color, motionProps),
},
}
})}
>
{interpolatedStyles => {
render() {
const {
xScale,
yScale,
offsetX,
offsetY,

margin,
width,
height,
outerWidth,
outerHeight,

// cells
cellShape,
cellBorderWidth,
getCellBorderColor,

// axes & grid
axisTop,
axisRight,
axisBottom,
axisLeft,
enableGridX,
enableGridY,

// labels
enableLabels,
getLabelTextColor,

// theming
theme,

// motion
animate,
motionStiffness,
motionDamping,
boundSpring,

// interactivity
isInteractive,
} = this.props

let Cell
if (cellShape === 'rect') {
Cell = HeatMapCellRect
} else if (cellShape === 'circle') {
Cell = HeatMapCellCircle
} else {
Cell = cellShape
}

const nodes = computeNodes(this.props)

const motionProps = {
animate,
motionDamping,
motionStiffness,
}

return (
<Container isInteractive={isInteractive} theme={theme}>
{({ showTooltip, hideTooltip }) => {
const onHover = partial(this.handleNodeHover, showTooltip)
const onLeave = partial(this.handleNodeLeave, hideTooltip)

return (
<g>
{interpolatedStyles.map(({ key, style, data: node }) => {
const color = getInterpolatedColor(style)

return React.createElement(Cell, {
key,
value: node.value,
x: style.x,
y: style.y,
width: Math.max(style.width, 0),
height: Math.max(style.height, 0),
color,
opacity: cellOpacity,
borderWidth: cellBorderWidth,
borderColor: getCellBorderColor({ ...node, color }),
textColor: getLabelTextColor({ ...node, color }),
})
<SvgWrapper
width={outerWidth}
height={outerHeight}
margin={Object.assign({}, margin, {
top: margin.top + offsetY,
left: margin.left + offsetX,
})}
</g>
>
<Grid
theme={theme}
width={width - offsetX * 2}
height={height - offsetY * 2}
xScale={enableGridX ? xScale : null}
yScale={enableGridY ? yScale : null}
{...motionProps}
/>
<Axes
xScale={xScale}
yScale={yScale}
width={width}
height={height}
theme={theme}
top={axisTop}
right={axisRight}
bottom={axisBottom}
left={axisLeft}
{...motionProps}
/>
{!animate &&
nodes.map(node =>
React.createElement(Cell, {
key: node.key,
value: node.value,
x: node.x,
y: node.y,
width: node.width,
height: node.height,
color: node.color,
opacity: node.opacity,
borderWidth: cellBorderWidth,
borderColor: getCellBorderColor(node),
textColor: getLabelTextColor(node),
onHover: partial(onHover, node),
onLeave,
})
)}

{animate === true &&
<TransitionMotion
styles={nodes.map(node => {
return {
key: node.key,
data: node,
style: {
x: boundSpring(node.x),
y: boundSpring(node.y),
width: boundSpring(node.width),
height: boundSpring(node.height),
opacity: boundSpring(node.opacity),
...colorMotionSpring(node.color, {
damping: motionDamping,
stiffness: motionStiffness,
}),
},
}
})}
>
{interpolatedStyles => {
return (
<g>
{interpolatedStyles.map(
({ key, style, data: node }) => {
const color = getInterpolatedColor(style)

return React.createElement(Cell, {
key,
value: node.value,
x: style.x,
y: style.y,
width: Math.max(style.width, 0),
height: Math.max(style.height, 0),
color,
opacity: style.opacity,
borderWidth: cellBorderWidth,
borderColor: getCellBorderColor({
...node,
color,
}),
textColor: getLabelTextColor({
...node,
color,
}),
onHover: partial(onHover, node),
onLeave,
})
}
)}
</g>
)
}}
</TransitionMotion>}
</SvgWrapper>
)
}}
</TransitionMotion>
</Container>
)
} else {
cells = nodes.map(node => {
return React.createElement(Cell, {
key: node.key,
value: node.value,
x: node.x,
y: node.y,
width: node.width,
height: node.height,
color: node.color,
opacity: cellOpacity,
borderWidth: cellBorderWidth,
borderColor: getCellBorderColor(node),
textColor: getLabelTextColor(node),
})
})
}

return (
<Container isInteractive={isInteractive} theme={theme}>
{({ showTooltip, hideTooltip }) => {
return (
<SvgWrapper
width={outerWidth}
height={outerHeight}
margin={Object.assign({}, margin, {
top: margin.top + offsetY,
left: margin.left + offsetX,
})}
>
<Grid
theme={theme}
width={width - offsetX * 2}
height={height - offsetY * 2}
xScale={enableGridX ? xScale : null}
yScale={enableGridY ? yScale : null}
{...motionProps}
/>
<Axes
xScale={xScale}
yScale={yScale}
width={width}
height={height}
theme={theme}
top={axisTop}
right={axisRight}
bottom={axisBottom}
left={axisLeft}
{...motionProps}
/>
{cells}
</SvgWrapper>
)
}}
</Container>
)
}

HeatMap.propTypes = HeatMapPropTypes

export default enhance(HeatMap)

0 comments on commit 422d4c2

Please sign in to comment.