Skip to content

Commit 3b8fde1

Browse files
committedAug 17, 2021
feat(stream): add support for custom tooltip and stack tooltip
1 parent a7f56db commit 3b8fde1

File tree

11 files changed

+196
-50
lines changed

11 files changed

+196
-50
lines changed
 

‎packages/stream/src/LayerTooltip.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { BasicTooltip } from '@nivo/tooltip'
2+
import { TooltipProps } from './types'
3+
4+
export const LayerTooltip = ({ layer }: TooltipProps) => (
5+
<BasicTooltip id={layer.label} enableChip={true} color={layer.color} />
6+
)

‎packages/stream/src/StackTooltip.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useMemo } from 'react'
2+
import { TableTooltip, Chip } from '@nivo/tooltip'
3+
import { StackTooltipProps } from './types'
4+
5+
export const StackTooltip = ({ slice }: StackTooltipProps) => {
6+
const rows = useMemo(
7+
() =>
8+
slice.stack.map(p => [
9+
<Chip key={p.layerId} color={p.color} />,
10+
p.layerLabel,
11+
p.formattedValue,
12+
]),
13+
[slice]
14+
)
15+
16+
return <TableTooltip rows={rows} />
17+
}

‎packages/stream/src/Stream.tsx

+12-2
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ const InnerStream = <RawDatum extends StreamDatum>({
5959
dotBorderColor,
6060

6161
isInteractive = svgDefaultProps.isInteractive,
62+
tooltip = svgDefaultProps.tooltip,
6263
enableStackTooltip = svgDefaultProps.enableStackTooltip,
64+
stackTooltip = svgDefaultProps.stackTooltip,
6365

6466
legends = svgDefaultProps.legends,
6567

@@ -144,13 +146,14 @@ const InnerStream = <RawDatum extends StreamDatum>({
144146

145147
if (chartLayers.includes('layers')) {
146148
layerById.layers = (
147-
<StreamLayers
149+
<StreamLayers<RawDatum>
148150
key="layers"
149151
layers={layers}
150152
fillOpacity={fillOpacity}
151153
borderWidth={borderWidth}
152154
getBorderColor={getBorderColor}
153155
isInteractive={isInteractive}
156+
tooltip={tooltip}
154157
/>
155158
)
156159
}
@@ -177,7 +180,14 @@ const InnerStream = <RawDatum extends StreamDatum>({
177180
}
178181

179182
if (chartLayers.includes('slices') && isInteractive && enableStackTooltip) {
180-
layerById.slices = <StreamSlices key="slices" slices={slices} height={innerHeight} />
183+
layerById.slices = (
184+
<StreamSlices<RawDatum>
185+
key="slices"
186+
slices={slices}
187+
height={innerHeight}
188+
tooltip={stackTooltip}
189+
/>
190+
)
181191
}
182192

183193
if (chartLayers.includes('legends')) {

‎packages/stream/src/StreamLayer.tsx

+9-11
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,31 @@
1-
import { useCallback } from 'react'
1+
import { useCallback, createElement } from 'react'
22
import { useSpring, animated } from '@react-spring/web'
33
import { useAnimatedPath, useMotionConfig } from '@nivo/core'
44
import { InheritedColorConfigCustomFunction } from '@nivo/colors'
5-
import { BasicTooltip, useTooltip } from '@nivo/tooltip'
6-
import { StreamLayerData } from './types'
5+
import { useTooltip } from '@nivo/tooltip'
6+
import { StreamCommonProps, StreamDatum, StreamLayerData } from './types'
77

8-
interface StreamLayerProps {
8+
interface StreamLayerProps<RawDatum extends StreamDatum> {
99
layer: StreamLayerData
1010
fillOpacity: number
1111
borderWidth: number
1212
getBorderColor: InheritedColorConfigCustomFunction<StreamLayerData>
1313
isInteractive: boolean
14+
tooltip: StreamCommonProps<RawDatum>['tooltip']
1415
}
1516

16-
export const StreamLayer = ({
17+
export const StreamLayer = <RawDatum extends StreamDatum>({
1718
layer,
1819
fillOpacity,
1920
borderWidth,
2021
getBorderColor,
2122
isInteractive,
22-
}: StreamLayerProps) => {
23+
tooltip,
24+
}: StreamLayerProps<RawDatum>) => {
2325
const { showTooltipFromEvent, hideTooltip } = useTooltip()
2426
const handleMouseHover = useCallback(
2527
event => {
26-
showTooltipFromEvent(
27-
<BasicTooltip id={layer.label} enableChip={true} color={layer.color} />,
28-
event,
29-
'left'
30-
)
28+
showTooltipFromEvent(createElement(tooltip, { layer }), event, 'left')
3129
},
3230
[showTooltipFromEvent, layer]
3331
)

‎packages/stream/src/StreamLayers.tsx

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,34 @@
11
import { InheritedColorConfigCustomFunction } from '@nivo/colors'
22
import { StreamLayer } from './StreamLayer'
3-
import { StreamLayerData } from './types'
3+
import { StreamCommonProps, StreamLayerData, StreamDatum } from './types'
44

5-
interface StreamLayersProps {
5+
interface StreamLayersProps<RawDatum extends StreamDatum> {
66
layers: StreamLayerData[]
77
fillOpacity: number
88
borderWidth: number
99
getBorderColor: InheritedColorConfigCustomFunction<StreamLayerData>
1010
isInteractive: boolean
11+
tooltip: StreamCommonProps<RawDatum>['tooltip']
1112
}
1213

13-
export const StreamLayers = ({
14+
export const StreamLayers = <RawDatum extends StreamDatum>({
1415
layers,
1516
fillOpacity,
1617
borderWidth,
1718
getBorderColor,
1819
isInteractive,
19-
}: StreamLayersProps) => (
20+
tooltip,
21+
}: StreamLayersProps<RawDatum>) => (
2022
<g>
2123
{layers.map((layer, i) => (
22-
<StreamLayer
24+
<StreamLayer<RawDatum>
2325
key={i}
2426
layer={layer}
2527
getBorderColor={getBorderColor}
2628
borderWidth={borderWidth}
2729
fillOpacity={fillOpacity}
2830
isInteractive={isInteractive}
31+
tooltip={tooltip}
2932
/>
3033
))}
3134
</g>

‎packages/stream/src/StreamSlices.tsx

+14-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
1-
import { StreamSliceData } from './types'
1+
import { StreamSliceData, StreamDatum, StreamCommonProps } from './types'
22
import { StreamSlicesItem } from './StreamSlicesItem'
33

4-
interface StreamSlicesProps {
4+
interface StreamSlicesProps<RawDatum extends StreamDatum> {
55
slices: StreamSliceData[]
66
height: number
7+
tooltip: StreamCommonProps<RawDatum>['stackTooltip']
78
}
89

9-
export const StreamSlices = ({ slices, height }: StreamSlicesProps) => (
10+
export const StreamSlices = <RawDatum extends StreamDatum>({
11+
slices,
12+
height,
13+
tooltip,
14+
}: StreamSlicesProps<RawDatum>) => (
1015
<g>
1116
{slices.map(slice => (
12-
<StreamSlicesItem key={slice.index} slice={slice} height={height} />
17+
<StreamSlicesItem<RawDatum>
18+
key={slice.index}
19+
slice={slice}
20+
height={height}
21+
tooltip={tooltip}
22+
/>
1323
))}
1424
</g>
1525
)

‎packages/stream/src/StreamSlicesItem.tsx

+11-17
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,27 @@
1-
import { useCallback, useMemo, useState } from 'react'
2-
import { TableTooltip, Chip } from '@nivo/tooltip'
1+
import { createElement, useCallback, useState } from 'react'
32
import { useTooltip } from '@nivo/tooltip'
4-
import { StreamSliceData } from './types'
3+
import { StreamCommonProps, StreamDatum, StreamSliceData } from './types'
54

6-
interface StreamSlicesItemProps {
5+
interface StreamSlicesItemProps<RawDatum extends StreamDatum> {
76
slice: StreamSliceData
87
height: number
8+
tooltip: StreamCommonProps<RawDatum>['stackTooltip']
99
}
1010

11-
export const StreamSlicesItem = ({ slice, height }: StreamSlicesItemProps) => {
11+
export const StreamSlicesItem = <RawDatum extends StreamDatum>({
12+
slice,
13+
height,
14+
tooltip,
15+
}: StreamSlicesItemProps<RawDatum>) => {
1216
const [isHover, setIsHover] = useState(false)
1317
const { showTooltipFromEvent, hideTooltip } = useTooltip()
1418

15-
const rows = useMemo(
16-
() =>
17-
slice.stack.map(p => [
18-
<Chip key={p.layerId} color={p.color} />,
19-
p.layerLabel,
20-
p.formattedValue,
21-
]),
22-
[slice]
23-
)
24-
2519
const handleMouseHover = useCallback(
2620
event => {
2721
setIsHover(true)
28-
showTooltipFromEvent(<TableTooltip rows={rows} />, event, 'left')
22+
showTooltipFromEvent(createElement(tooltip, { slice }), event, 'left')
2923
},
30-
[setIsHover, showTooltipFromEvent, rows]
24+
[setIsHover, showTooltipFromEvent, tooltip, slice]
3125
)
3226

3327
const handleMouseLeave = useCallback(() => {

‎packages/stream/src/props.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { StackOrder, StackOffset, AreaCurve } from '@nivo/core'
22
import { StreamCommonProps, StreamLayerId } from './types'
33
import { StreamDotsItem } from './StreamDotsItem'
4+
import { LayerTooltip } from './LayerTooltip'
5+
import { StackTooltip } from './StackTooltip'
46

57
export const defaultProps = {
68
label: 'id',
@@ -30,8 +32,9 @@ export const defaultProps = {
3032
dotBorderColor: { from: 'color' },
3133

3234
isInteractive: true,
33-
tooltipLabel: 'id',
35+
tooltip: LayerTooltip,
3436
enableStackTooltip: true,
37+
stackTooltip: StackTooltip,
3538

3639
legends: [],
3740
legendLabel: 'id',

‎packages/stream/src/types.ts

+16-5
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,17 @@ export interface StreamLayerDatum {
6060
y2: number
6161
}
6262

63-
export type DotComponent = React.FC<{
63+
export interface TooltipProps {
64+
layer: StreamLayerData
65+
}
66+
export type Tooltip = FunctionComponent<TooltipProps>
67+
68+
export interface StackTooltipProps {
69+
slice: StreamSliceData
70+
}
71+
export type StackTooltip = FunctionComponent<StackTooltipProps>
72+
73+
export type DotComponent = FunctionComponent<{
6474
datum: StreamLayerDatum
6575
x: number
6676
y: number
@@ -103,10 +113,6 @@ export type StreamCommonProps<RawDatum extends StreamDatum> = {
103113
enableGridY: boolean
104114
gridYValues: GridValues<number>
105115

106-
isInteractive: boolean
107-
tooltipLabel: PropertyAccessor<StreamLayerData, string>
108-
enableStackTooltip: boolean
109-
110116
theme: Theme
111117
colors: OrdinalColorScaleConfig<Omit<StreamLayerData, 'label' | 'color' | 'data'>>
112118
fillOpacity: number
@@ -121,6 +127,11 @@ export type StreamCommonProps<RawDatum extends StreamDatum> = {
121127
dotBorderWidth: ((datum: StreamLayerDatum) => number) | number
122128
dotBorderColor: InheritedColorConfig<StreamLayerDatum>
123129

130+
isInteractive: boolean
131+
tooltip: Tooltip
132+
enableStackTooltip: boolean
133+
stackTooltip: StackTooltip
134+
124135
legends: LegendProps[]
125136

126137
renderWrapper: boolean

‎packages/stream/tests/Stream.test.tsx

+60-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { mount } from 'enzyme'
2-
import { Stream, StreamLayerDatum, StreamSvgProps } from '../src'
2+
import { Stream, StreamLayerData, StreamLayerDatum, StreamSliceData, StreamSvgProps } from '../src'
33

44
type TestDatum = {
55
A: number
@@ -31,10 +31,7 @@ describe('layers', () => {
3131

3232
it('should support custom layers', () => {
3333
const wrapper = mount(
34-
<Stream<TestDatum>
35-
{...commonProps}
36-
layers={['grid', 'axes', 'layers', CustomLayer]}
37-
/>
34+
<Stream<TestDatum> {...commonProps} layers={['grid', 'axes', 'layers', CustomLayer]} />
3835
)
3936

4037
const customLayer = wrapper.find(CustomLayer)
@@ -92,6 +89,64 @@ describe('dots', () => {
9289
})
9390
})
9491

92+
describe('tooltip', () => {
93+
it('should show a tooltip when hovering a layer', () => {
94+
const wrapper = mount(<Stream<TestDatum> {...commonProps} />)
95+
96+
expect(wrapper.find('LayerTooltip').exists()).toBe(false)
97+
98+
wrapper.find('StreamLayer').at(0).find('path').simulate('mouseEnter')
99+
const tooltip = wrapper.find('LayerTooltip')
100+
expect(tooltip.exists()).toBe(true)
101+
102+
const layerData = tooltip.prop<StreamLayerData>('layer')
103+
expect(layerData.id).toBe(commonProps.keys[0])
104+
})
105+
106+
it('should have stack tooltip enabled by default', () => {
107+
const wrapper = mount(<Stream<TestDatum> {...commonProps} />)
108+
109+
const slices = wrapper.find('StreamSlices')
110+
expect(slices.exists()).toBe(true)
111+
112+
const sliceRect = slices.find('StreamSlicesItem').at(0).find('rect')
113+
expect(sliceRect.exists()).toBe(true)
114+
115+
sliceRect.simulate('mouseEnter')
116+
const tooltip = wrapper.find('StackTooltip')
117+
expect(tooltip.exists()).toBe(true)
118+
119+
const sliceData = tooltip.prop<StreamSliceData>('slice')
120+
expect(sliceData.index).toBe(0)
121+
expect(sliceData.x).toBe(0)
122+
123+
const expectedData = [
124+
{ layerId: 'C', value: 30 },
125+
{ layerId: 'B', value: 20 },
126+
{ layerId: 'A', value: 10 },
127+
]
128+
expectedData.forEach((expectedDatum, index) => {
129+
expect(sliceData.stack[index].layerId).toBe(expectedDatum.layerId)
130+
expect(sliceData.stack[index].value).toBe(expectedDatum.value)
131+
})
132+
})
133+
134+
it('should allow to disable stack tooltip', () => {
135+
const wrapper = mount(<Stream<TestDatum> {...commonProps} enableStackTooltip={false} />)
136+
137+
expect(wrapper.find('StreamSlices').exists()).toBe(false)
138+
})
139+
140+
it('should disable tooltip and stack tooltip when `isInteractive` is false', () => {
141+
const wrapper = mount(<Stream<TestDatum> {...commonProps} isInteractive={false} />)
142+
143+
wrapper.find('StreamLayer').at(0).find('path').simulate('mouseEnter')
144+
expect(wrapper.find('LayerTooltip').exists()).toBe(false)
145+
146+
expect(wrapper.find('StreamSlices').exists()).toBe(false)
147+
})
148+
})
149+
95150
describe('accessibility', () => {
96151
it('should forward root aria properties to the SVG element', () => {
97152
const wrapper = mount(

‎website/src/data/components/stream/props.js

+39
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,37 @@ const props = [
315315
controlType: 'switch',
316316
group: 'Interactivity',
317317
},
318+
{
319+
key: 'tooltip',
320+
flavors: ['svg'],
321+
help: `Tooltip custom component.`,
322+
type: 'FunctionComponent',
323+
required: false,
324+
group: 'Interactivity',
325+
description: `
326+
Allows complete tooltip customisation, it must return
327+
a valid HTML element and will receive the following data:
328+
329+
\`\`\`
330+
{
331+
layer: {
332+
id: string | number
333+
label: string | number
334+
color: string
335+
// populated when using patterns/gradients
336+
fill?: string
337+
path: string
338+
// computed data for each data point for this
339+
// specific layer
340+
data: StreamLayerDatum[]
341+
}
342+
}
343+
\`\`\`
344+
345+
You can also customize the style of the tooltip
346+
using the \`theme.tooltip\` object.
347+
`,
348+
},
318349
{
319350
key: 'enableStackTooltip',
320351
flavors: ['svg'],
@@ -325,6 +356,14 @@ const props = [
325356
controlType: 'switch',
326357
group: 'Interactivity',
327358
},
359+
{
360+
key: 'stackTooltip',
361+
flavors: ['svg'],
362+
help: `Stack tooltip custom component.`,
363+
type: 'FunctionComponent',
364+
required: false,
365+
group: 'Interactivity',
366+
},
328367
...motionProperties(['svg'], defaultProps, 'react-spring'),
329368
{
330369
key: 'ariaLabel',

0 commit comments

Comments
 (0)
Please sign in to comment.