Skip to content

Commit

Permalink
feat(tooltips): add support for configurable tooltips for bar charts …
Browse files Browse the repository at this point in the history
…and heat maps (#159)

* feat(bar): Support configurable tooltips for bar charts

* fix(bar): BarCanvas displayName exposes internals

* fix(heatmap): lint errors

* feat(heatmap): Support configurable tooltips
  • Loading branch information
bripkens authored and Raphaël Benitte committed May 18, 2018
1 parent 8d8374a commit 82473c1
Show file tree
Hide file tree
Showing 15 changed files with 180 additions and 48 deletions.
1 change: 1 addition & 0 deletions .storybook/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ setDefaults({

function loadStories() {
require('../packages/nivo-bar/stories/bar.stories')
require('../packages/nivo-bar/stories/barCanvas.stories')
require('../packages/nivo-chord/stories/chord.stories')
require('../packages/nivo-circle-packing/stories/bubble.stories')
require('../packages/nivo-heatmap/stories/heatmap.stories')
Expand Down
2 changes: 2 additions & 0 deletions packages/nivo-bar/src/Bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const Bar = ({
// interactivity
isInteractive,
tooltipFormat,
tooltip,
onClick,

legends,
Expand Down Expand Up @@ -180,6 +181,7 @@ const Bar = ({
onClick,
theme,
tooltipFormat,
tooltip,
}

let bars
Expand Down
10 changes: 8 additions & 2 deletions packages/nivo-bar/src/BarCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Container } from '@nivo/core'
import { BasicTooltip } from '@nivo/core'
import { BarPropTypes } from './props'
import enhance from './enhance'
import setDisplayName from 'recompose/setDisplayName'

const findNodeUnderCursor = (nodes, margin, x, y) =>
nodes.find(node =>
Expand Down Expand Up @@ -127,7 +128,7 @@ class BarCanvas extends Component {
handleMouseHover = (showTooltip, hideTooltip) => event => {
if (!this.bars) return

const { margin, theme } = this.props
const { margin, theme, tooltip } = this.props
const [x, y] = getRelativeCursor(this.surface, event)

const bar = findNodeUnderCursor(this.bars, margin, x, y)
Expand All @@ -140,6 +141,11 @@ class BarCanvas extends Component {
enableChip={true}
color={bar.color}
theme={theme}
renderContent={
typeof tooltip === 'function'
? tooltip.bind(null, { color: bar.color, ...bar.data })
: null
}
/>,
event
)
Expand Down Expand Up @@ -191,4 +197,4 @@ class BarCanvas extends Component {

BarCanvas.propTypes = BarPropTypes

export default enhance(BarCanvas)
export default setDisplayName('BarCanvas')(enhance(BarCanvas))
38 changes: 23 additions & 15 deletions packages/nivo-bar/src/BarItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,9 @@ const BarItem = ({
showTooltip,
hideTooltip,
onClick,
tooltipFormat,

theme,
tooltip,
}) => {
const handleTooltip = e =>
showTooltip(
<BasicTooltip
id={`${data.id} - ${data.indexValue}`}
value={data.value}
enableChip={true}
color={color}
theme={theme}
format={tooltipFormat}
/>,
e
)
const handleTooltip = e => showTooltip(tooltip, e)

return (
<g transform={`translate(${x}, ${y})`}>
Expand Down Expand Up @@ -106,6 +93,7 @@ BarItem.propTypes = {
showTooltip: PropTypes.func.isRequired,
hideTooltip: PropTypes.func.isRequired,
onClick: PropTypes.func,
tooltip: PropTypes.element.isRequired,

theme: PropTypes.shape({
tooltip: PropTypes.shape({}).isRequired,
Expand All @@ -116,6 +104,26 @@ const enhance = compose(
withPropsOnChange(['data', 'onClick'], ({ data, onClick }) => ({
onClick: event => onClick(data, event),
})),
withPropsOnChange(
['data', 'color', 'theme', 'tooltip', 'tooltipFormat'],
({ data, color, theme, tooltip, tooltipFormat }) => ({
tooltip: (
<BasicTooltip
id={`${data.id} - ${data.indexValue}`}
value={data.value}
enableChip={true}
color={color}
theme={theme}
format={tooltipFormat}
renderContent={
typeof tooltip === 'function'
? tooltip.bind(null, { color, ...data })
: null
}
/>
),
})
),
pure
)

Expand Down
1 change: 1 addition & 0 deletions packages/nivo-bar/src/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const BarPropTypes = {
isInteractive: PropTypes.bool,
onClick: PropTypes.func.isRequired,
tooltipFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
tooltip: PropTypes.func,

legends: PropTypes.arrayOf(
PropTypes.shape({
Expand Down
29 changes: 29 additions & 0 deletions packages/nivo-bar/stories/bar.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,32 @@ stories.add(
/>
))
)

stories.add(
'custom tooltip',
withInfo()(() => (
<Bar
{...commonProps}
axisLeft={{
format: value =>
Number(value).toLocaleString('ru-RU', {
minimumFractionDigits: 2,
}),
}}
tooltip={({ id, value, color }) => {
return (
<strong style={{ color }}>
{id}: {value}
</strong>
)
}}
theme={{
tooltip: {
container: {
background: 'gray',
},
},
}}
/>
))
)
44 changes: 44 additions & 0 deletions packages/nivo-bar/stories/barCanvas.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import { withInfo } from '@storybook/addon-info'
import { generateCountriesData } from '@nivo/generators'
import { BarCanvas } from '../index'

const keys = ['hot dogs', 'burgers', 'sandwich', 'kebab', 'fries', 'donut']
const commonProps = {
width: 1000,
height: 600,
margin: { top: 60, right: 80, bottom: 60, left: 80 },
data: generateCountriesData(keys, { size: 7 }),
indexBy: 'country',
keys,
padding: 0.2,
labelTextColor: 'inherit:darker(1.4)',
labelSkipWidth: 16,
labelSkipHeight: 16,
}

const stories = storiesOf('BarCanvas', module)

stories.add(
'custom tooltip',
withInfo()(() => (
<BarCanvas
{...commonProps}
tooltip={({ id, value, color }) => {
return (
<strong style={{ color }}>
{id}: {value}
</strong>
)
}}
theme={{
tooltip: {
container: {
background: 'gray',
},
},
}}
/>
))
)
25 changes: 15 additions & 10 deletions packages/nivo-core/src/components/tooltip/BasicTooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ import Chip from './Chip'
const chipStyle = { marginRight: 7 }

const BasicTooltip = props => {
const { id, value: _value, format, enableChip, color, theme } = props
const { id, value: _value, format, enableChip, color, theme, renderContent } = props

let value = _value
if (format !== undefined && value !== undefined) {
value = format(value)
}

return (
<div style={theme.tooltip.container}>
let content
if (typeof renderContent === 'function') {
content = renderContent()
} else {
let value = _value
if (format !== undefined && value !== undefined) {
value = format(value)
}
content = (
<div style={theme.tooltip.basic}>
{enableChip && <Chip color={color} style={chipStyle} />}
{value !== undefined ? (
Expand All @@ -37,8 +39,10 @@ const BasicTooltip = props => {
id
)}
</div>
</div>
)
)
}

return <div style={theme.tooltip.container}>{content}</div>
}

BasicTooltip.propTypes = {
Expand All @@ -47,6 +51,7 @@ BasicTooltip.propTypes = {
enableChip: PropTypes.bool.isRequired,
color: PropTypes.string,
format: PropTypes.func,
renderContent: PropTypes.func,

theme: PropTypes.shape({
tooltip: PropTypes.shape({
Expand Down
26 changes: 10 additions & 16 deletions packages/nivo-heatmap/src/HeatMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,21 @@ import HeatMapCellRect from './HeatMapCellRect'
import HeatMapCellCircle from './HeatMapCellCircle'
import HeatMapCellTooltip from './HeatMapCellTooltip'

import { scaleLinear } from 'd3-scale'

class HeatMap extends Component {
static propTypes = HeatMapPropTypes

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

handleNodeLeave = hideTooltip => {
Expand All @@ -42,8 +48,6 @@ class HeatMap extends Component {
yScale,
offsetX,
offsetY,
minValue,
maxValue,

margin,
width,
Expand All @@ -65,12 +69,10 @@ class HeatMap extends Component {
enableGridY,

// labels
enableLabels,
getLabelTextColor,

// theming
theme,
colorScale,

// motion
animate,
Expand Down Expand Up @@ -99,14 +101,6 @@ class HeatMap extends Component {
motionStiffness,
}

const legendItems = scaleLinear()
.domain([minValue, maxValue])
.ticks(4)
.map(i => ({
label: i,
fill: colorScale(i),
}))

return (
<Container isInteractive={isInteractive} theme={theme}>
{({ showTooltip, hideTooltip }) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/nivo-heatmap/src/HeatMapCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class HeatMapCanvas extends Component {

const [x, y] = getRelativeCursor(this.surface, event)

const { margin, offsetX, offsetY, theme, setCurrentNode } = this.props
const { margin, offsetX, offsetY, theme, setCurrentNode, tooltip } = this.props
const node = this.nodes.find(node =>
isCursorInRect(
node.x + margin.left + offsetX - node.width / 2,
Expand All @@ -114,7 +114,7 @@ class HeatMapCanvas extends Component {

if (node !== undefined) {
setCurrentNode(node)
showTooltip(<HeatMapCellTooltip node={node} theme={theme} />, event)
showTooltip(<HeatMapCellTooltip node={node} theme={theme} tooltip={tooltip} />, event)
} else {
setCurrentNode(null)
hideTooltip()
Expand Down
21 changes: 20 additions & 1 deletion packages/nivo-heatmap/src/HeatMapCellTooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,37 @@
* file that was distributed with this source code.
*/
import React from 'react'
import PropTypes from 'prop-types'
import pure from 'recompose/pure'
import { BasicTooltip } from '@nivo/core'

const HeatMapCellTooltip = ({ node, theme, format }) => (
const HeatMapCellTooltip = ({ node, theme, format, tooltip }) => (
<BasicTooltip
id={`${node.yKey} - ${node.xKey}`}
value={node.value}
enableChip={true}
color={node.color}
theme={theme}
format={format}
renderContent={typeof tooltip === 'function' ? tooltip.bind(null, { ...node }) : null}
/>
)

HeatMapCellTooltip.propTypes = {
node: PropTypes.shape({
xKey: PropTypes.string.isRequired,
yKey: PropTypes.string.isRequired,
value: PropTypes.number.isRequired,
color: PropTypes.string.isRequired,
}).isRequired,
format: PropTypes.func,
tooltip: PropTypes.func,
theme: PropTypes.shape({
tooltip: PropTypes.shape({
container: PropTypes.object.isRequired,
basic: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
}

export default pure(HeatMapCellTooltip)
4 changes: 3 additions & 1 deletion packages/nivo-heatmap/src/ResponsiveHeatMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import React from 'react'
import { ResponsiveWrapper } from '@nivo/core'
import HeatMap from './HeatMap'

export default props => (
const ResponsiveHeatMap = props => (
<ResponsiveWrapper>
{({ width, height }) => <HeatMap width={width} height={height} {...props} />}
</ResponsiveWrapper>
)

export default ResponsiveHeatMap
4 changes: 3 additions & 1 deletion packages/nivo-heatmap/src/ResponsiveHeatMapCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import React from 'react'
import { ResponsiveWrapper } from '@nivo/core'
import HeatMapCanvas from './HeatMapCanvas'

export default props => (
const ResponsiveHeatMapCanvas = props => (
<ResponsiveWrapper>
{({ width, height }) => <HeatMapCanvas width={width} height={height} {...props} />}
</ResponsiveWrapper>
)

export default ResponsiveHeatMapCanvas
Loading

0 comments on commit 82473c1

Please sign in to comment.