Skip to content

Commit

Permalink
feat(funnel): add support for annotations to Funnel component
Browse files Browse the repository at this point in the history
  • Loading branch information
plouc committed Jun 17, 2020
1 parent a69780f commit 9fca13c
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 34 deletions.
1 change: 1 addition & 0 deletions packages/funnel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dist/"
],
"dependencies": {
"@nivo/annotations": "0.62.0",
"@nivo/colors": "0.62.0",
"@nivo/core": "0.62.0",
"@nivo/tooltip": "0.62.0",
Expand Down
22 changes: 21 additions & 1 deletion packages/funnel/src/Funnel.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useFunnel } from './hooks'
import { Parts } from './Parts'
import { PartLabels } from './PartLabels'
import { Separators } from './Separators'
import { FunnelAnnotations } from './FunnelAnnotations'

const Funnel = props => {
const {
Expand Down Expand Up @@ -46,8 +47,11 @@ const Funnel = props => {

layers,

annotations,

isInteractive,
currentPartSizeExtension,
currentBorderWidth,
onMouseEnter,
onMouseMove,
onMouseLeave,
Expand Down Expand Up @@ -86,11 +90,19 @@ const Funnel = props => {
borderColor,
borderOpacity,
labelColor,
enableBeforeSeparators,
beforeSeparatorLength,
beforeSeparatorOffset,
enableAfterSeparators,
afterSeparatorLength,
afterSeparatorOffset,
isInteractive,
currentPartSizeExtension,
currentBorderWidth,
onMouseEnter,
onMouseMove,
onMouseLeave,
onClick,
})

const layerById = {
Expand All @@ -108,7 +120,15 @@ const Funnel = props => {
areaGenerator={areaGenerator}
borderGenerator={borderGenerator}
enableLabel={enableLabel}
setCurrentPartId={setCurrentPartId}
/>
),
annotations: (
<FunnelAnnotations
key="annotations"
parts={parts}
annotations={annotations}
widh={innerWidth}
height={innerHeight}
/>
),
labels: null,
Expand Down
13 changes: 13 additions & 0 deletions packages/funnel/src/FunnelAnnotations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react'
import { Annotation } from '@nivo/annotations'
import { useFunnelAnnotations } from './hooks'

export const FunnelAnnotations = ({ parts, annotations, width, height }) => {
const boundAnnotations = useFunnelAnnotations(parts, annotations)

return boundAnnotations.map((annotation, i) => (
<Annotation key={i} {...annotation} containerWidth={width} containerHeight={height} />
))
}

FunnelAnnotations.propTypes = {}
157 changes: 127 additions & 30 deletions packages/funnel/src/hooks.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useMemo, useState, useCallback } from 'react'
import { useMemo, useState } from 'react'
import { line, area, curveBasis, curveLinear } from 'd3-shape'
import { scaleLinear } from 'd3-scale'
import { useInheritedColor, useOrdinalColorScale } from '@nivo/colors'
import { useTheme, useValueFormatter } from '@nivo/core'
import { useAnnotations } from '@nivo/annotations'
import { FunnelDefaultProps as defaults } from './props'

export const computeShapeGenerators = (interpolation, direction) => {
Expand Down Expand Up @@ -69,7 +70,9 @@ export const computeSeparators = ({
width,
height,
spacing,
enableBeforeSeparators,
beforeSeparatorOffset,
enableAfterSeparators,
afterSeparatorOffset,
}) => {
const beforeSeparators = []
Expand All @@ -80,35 +83,43 @@ export const computeSeparators = ({
parts.forEach(part => {
const y = part.y0 - spacing / 2

if (enableBeforeSeparators === true) {
beforeSeparators.push({
partId: part.data.id,
x0: 0,
x1: part.x0 - beforeSeparatorOffset,
y0: y,
y1: y,
})
}
if (enableAfterSeparators === true) {
afterSeparators.push({
partId: part.data.id,
x0: part.x1 + afterSeparatorOffset,
x1: width,
y0: y,
y1: y,
})
}
})

const y = lastPart.y1
if (enableBeforeSeparators === true) {
beforeSeparators.push({
partId: part.data.id,
x0: 0,
x1: part.x0 - beforeSeparatorOffset,
...beforeSeparators[beforeSeparators.length - 1],
partId: 'none',
y0: y,
y1: y,
})
}
if (enableAfterSeparators === true) {
afterSeparators.push({
partId: part.data.id,
x0: part.x1 + afterSeparatorOffset,
x1: width,
...afterSeparators[afterSeparators.length - 1],
partId: 'none',
y0: y,
y1: y,
})
})

const y = lastPart.y1
beforeSeparators.push({
...beforeSeparators[beforeSeparators.length - 1],
partId: 'none',
y0: y,
y1: y,
})
afterSeparators.push({
...afterSeparators[afterSeparators.length - 1],
partId: 'none',
y0: y,
y1: y,
})
}
} else if (direction === 'horizontal') {
parts.forEach(part => {
const x = part.x0 - spacing / 2
Expand Down Expand Up @@ -147,6 +158,52 @@ export const computeSeparators = ({
return [beforeSeparators, afterSeparators]
}

export const computePartsHandlers = ({
parts,
setCurrentPartId,
isInteractive,
onMouseEnter,
onMouseLeave,
onMouseMove,
onClick,
}) => {
if (!isInteractive) return parts

return parts.map(part => {
const boundOnMouseEnter = event => {
setCurrentPartId(part.data.id)
onMouseEnter !== undefined && onMouseEnter(part, event)
}

const boundOnMouseLeave = event => {
setCurrentPartId(null)
onMouseLeave !== undefined && onMouseLeave(part, event)
}

const boundOnMouseMove =
onMouseMove !== undefined
? event => {
onMouseMove(part, event)
}
: undefined

const boundOnClick =
onClick !== undefined
? event => {
onClick(part, event)
}
: undefined

return {
...part,
onMouseEnter: boundOnMouseEnter,
onMouseLeave: boundOnMouseLeave,
onMouseMove: boundOnMouseMove,
onClick: boundOnClick,
}
})
}

/**
* Creates required layout to generate a funnel chart,
* it uses almost the same parameters as the Funnel component.
Expand All @@ -169,11 +226,19 @@ export const useFunnel = ({
borderColor = defaults.borderColor,
borderOpacity = defaults.borderOpacity,
labelColor = defaults.labelColor,
enableBeforeSeparators = defaults.enableBeforeSeparators,
beforeSeparatorLength = defaults.beforeSeparatorLength,
beforeSeparatorOffset = defaults.beforeSeparatorOffset,
enableAfterSeparators = defaults.enableAfterSeparators,
afterSeparatorLength = defaults.afterSeparatorLength,
afterSeparatorOffset = defaults.afterSeparatorOffset,
isInteractive = defaults.isInteractive,
currentPartSizeExtension = defaults.currentPartSizeExtension,
currentBorderWidth,
onMouseEnter,
onMouseMove,
onMouseLeave,
onClick,
}) => {
const theme = useTheme()
const getColor = useOrdinalColorScale(colors, 'id')
Expand All @@ -189,8 +254,8 @@ export const useFunnel = ({

let innerWidth
let innerHeight
const paddingBefore = beforeSeparatorLength + beforeSeparatorOffset
const paddingAfter = afterSeparatorLength + afterSeparatorOffset
const paddingBefore = enableBeforeSeparators ? beforeSeparatorLength + beforeSeparatorOffset : 0
const paddingAfter = enableAfterSeparators ? afterSeparatorLength + afterSeparatorOffset : 0
if (direction === 'vertical') {
innerWidth = width - paddingBefore - paddingAfter
innerHeight = height
Expand Down Expand Up @@ -244,7 +309,10 @@ export const useFunnel = ({
height: partHeight,
color: getColor(datum),
fillOpacity,
borderWidth,
borderWidth:
isCurrent && currentBorderWidth !== undefined
? currentBorderWidth
: borderWidth,
borderOpacity,
formattedValue: formatValue(datum.value),
isCurrent,
Expand Down Expand Up @@ -393,6 +461,20 @@ export const useFunnel = ({
currentPartId,
])

const partsWithHandlers = useMemo(
() =>
computePartsHandlers({
parts,
setCurrentPartId,
isInteractive,
onMouseEnter,
onMouseLeave,
onMouseMove,
onClick,
}),
[parts, setCurrentPartId, isInteractive, onMouseEnter, onMouseLeave, onMouseMove, onClick]
)

const [beforeSeparators, afterSeparators] = useMemo(
() =>
computeSeparators({
Expand All @@ -401,26 +483,26 @@ export const useFunnel = ({
width,
height,
spacing,
enableBeforeSeparators,
beforeSeparatorOffset,
beforeSeparatorLength,
enableAfterSeparators,
afterSeparatorOffset,
afterSeparatorLength,
}),
[
parts,
direction,
width,
height,
spacing,
enableBeforeSeparators,
beforeSeparatorOffset,
beforeSeparatorLength,
enableAfterSeparators,
afterSeparatorOffset,
afterSeparatorLength,
]
)

return {
parts,
parts: partsWithHandlers,
areaGenerator,
borderGenerator,
beforeSeparators,
Expand All @@ -429,3 +511,18 @@ export const useFunnel = ({
currentPartId,
}
}

export const useFunnelAnnotations = (parts, annotations) => {
if (annotations.length === 0) return []

return useAnnotations({
items: parts,
annotations,
getDimensions: (part, offset) => {
const width = part.width + offset * 2
const height = part.height + offset * 2

return { size: Math.max(width, height), width, height }
},
})
}
2 changes: 1 addition & 1 deletion website/src/data/components/funnel/generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const generateLightDataSet = () => {
lastValue = Math.round(lastValue * random(0.6, 0.95))

return {
id: `stage_${id}`,
id: `step_${id}`,
value: lastValue,
label: startCase(id),
}
Expand Down
6 changes: 4 additions & 2 deletions website/src/data/components/funnel/meta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ Funnel:
- experimental
- svg
- isomorphic
stories: []
stories:
- label: Displaying steps sub-groups by combining with other chart types
link: funnel-sub-clustering--demo
description: |
A funnel chart.
This component also provides a React hook which can be used in *headless mode*:
`useFunnel()`, meaning that you can compute the chart but handle the rendering
by yourself.
by yourself, this hook supports almost the same properties as the chart.
The responsive alternative of this component is `ResponsiveFunnel`.

0 comments on commit 9fca13c

Please sign in to comment.