Skip to content

Commit

Permalink
feat(radar): add support for custom slice tooltip
Browse files Browse the repository at this point in the history
  • Loading branch information
plouc committed Sep 7, 2021
1 parent 2e69633 commit bb81efb
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 55 deletions.
6 changes: 3 additions & 3 deletions packages/core/index.d.ts
Expand Up @@ -385,9 +385,9 @@ declare module '@nivo/core' {
export type ValueFormat<Value, Context = void> =
| string // d3 formatter
// explicit formatting function
| Context extends void
? (value: Value) => string
: (value: Value, context: Context) => string
| (Context extends void
? (value: Value) => string
: (value: Value, context: Context) => string)
export function getValueFormatter<Value, Context = void>(
format?: ValueFormat<Value, Context>
): Context extends void ? (value: Value) => string : (value: Value, context: Context) => string
Expand Down
35 changes: 21 additions & 14 deletions packages/radar/src/Radar.tsx
Expand Up @@ -3,7 +3,7 @@ import { Container, useDimensions, SvgWrapper } from '@nivo/core'
import { BoxLegendSvg } from '@nivo/legends'
import { RadarShapes } from './RadarShapes'
import { RadarGrid } from './RadarGrid'
import { RadarTooltip } from './RadarTooltip'
import { RadarSlices } from './RadarSlices'
import { RadarDots } from './RadarDots'
import { svgDefaultProps } from './props'
import { RadarLayerId, RadarSvgProps } from './types'
Expand Down Expand Up @@ -44,6 +44,7 @@ const InnerRadar = <D extends Record<string, unknown>>({
fillOpacity = svgDefaultProps.fillOpacity,
blendMode = svgDefaultProps.blendMode,
isInteractive = svgDefaultProps.isInteractive,
sliceTooltip = svgDefaultProps.sliceTooltip,
legends = svgDefaultProps.legends,
role,
ariaLabel,
Expand Down Expand Up @@ -83,6 +84,7 @@ const InnerRadar = <D extends Record<string, unknown>>({
const layerById: Record<RadarLayerId, ReactNode> = {
grid: null,
shapes: null,
slices: null,
dots: null,
legends: null,
}
Expand Down Expand Up @@ -125,6 +127,23 @@ const InnerRadar = <D extends Record<string, unknown>>({
)
}

if (layers.includes('slices') && isInteractive) {
layerById.slices = (
<g key="slices" transform={`translate(${centerX}, ${centerY})`}>
<RadarSlices<D>
data={data}
keys={keys}
getIndex={getIndex}
formatValue={formatValue}
colorByKey={colorByKey}
radius={radius}
angleStep={angleStep}
tooltip={sliceTooltip}
/>
</g>
)
}

if (layers.includes('dots') && enableDots) {
layerById.dots = (
<g key="dots" transform={`translate(${centerX}, ${centerY})`}>
Expand Down Expand Up @@ -177,19 +196,7 @@ const InnerRadar = <D extends Record<string, unknown>>({
>
{layerById.grid}
{layerById.shapes}
{isInteractive && (
<g transform={`translate(${centerX}, ${centerY})`}>
<RadarTooltip<D>
data={data}
keys={keys}
getIndex={getIndex}
formatValue={formatValue}
colorByKey={colorByKey}
radius={radius}
angleStep={angleStep}
/>
</g>
)}
{layerById.slices}
{layerById.dots}
{layerById.legends}
</SvgWrapper>
Expand Down
4 changes: 2 additions & 2 deletions packages/radar/src/RadarGrid.tsx
Expand Up @@ -34,7 +34,7 @@ export const RadarGrid = ({
}, [indices, levels, radius, angleStep])

return (
<g>
<>
{angles.map((angle, i) => {
const position = positionFromAngle(angle, radius)
return (
Expand Down Expand Up @@ -64,6 +64,6 @@ export const RadarGrid = ({
labelOffset={labelOffset}
label={label}
/>
</g>
</>
)
}
@@ -1,10 +1,10 @@
import { useMemo, useState, useCallback, ReactNode } from 'react'
import { useMemo, useState, useCallback, createElement } from 'react'
import { Arc } from 'd3-shape'
import { positionFromAngle, useTheme } from '@nivo/core'
import { TableTooltip, Chip, useTooltip } from '@nivo/tooltip'
import { RadarDataProps } from './types'
import { useTooltip } from '@nivo/tooltip'
import { RadarCommonProps, RadarDataProps, RadarSliceTooltipDatum } from './types'

interface RadarTooltipItemProps<D extends Record<string, unknown>> {
interface RadarSliceProps<D extends Record<string, unknown>> {
datum: D
keys: RadarDataProps<D>['keys']
index: string | number
Expand All @@ -14,11 +14,10 @@ interface RadarTooltipItemProps<D extends Record<string, unknown>> {
endAngle: number
radius: number
arcGenerator: Arc<void, { startAngle: number; endAngle: number }>
tooltip: RadarCommonProps['sliceTooltip']
}

type TooltipRow = [ReactNode, string, number | string]

export const RadarTooltipItem = <D extends Record<string, unknown>>({
export const RadarSlice = <D extends Record<string, unknown>>({
datum,
keys,
index,
Expand All @@ -28,35 +27,39 @@ export const RadarTooltipItem = <D extends Record<string, unknown>>({
startAngle,
endAngle,
arcGenerator,
}: RadarTooltipItemProps<D>) => {
tooltip,
}: RadarSliceProps<D>) => {
const [isHover, setIsHover] = useState(false)
const theme = useTheme()
const { showTooltipFromEvent, hideTooltip } = useTooltip()

const tooltip = useMemo(() => {
// first use number values to be able to sort
const rows: TooltipRow[] = keys.map(key => [
<Chip key={key} color={colorByKey[key]} />,
key,
datum[key] as number,
])
rows.sort((a, b) => (a[2] as number) - (b[2] as number))
rows.reverse()
const tooltipData = useMemo(() => {
const data: RadarSliceTooltipDatum[] = keys.map(key => ({
color: colorByKey[key],
id: key,
value: datum[key] as number,
formattedValue: formatValue(datum[key] as number, key),
}))
data.sort((a, b) => a.value - b.value)
data.reverse()

// then replace with formatted values
rows.forEach(row => {
row[2] = formatValue(row[2] as number, row[1])
})
return data
}, [datum, keys, formatValue, colorByKey])

return <TableTooltip title={<strong>{index}</strong>} rows={rows} />
}, [datum, keys, index, formatValue, colorByKey])
const showItemTooltip = useCallback(
event => {
setIsHover(true)
showTooltipFromEvent(tooltip, event)
showTooltipFromEvent(
createElement(tooltip, {
index,
data: tooltipData,
}),
event
)
},
[showTooltipFromEvent, tooltip]
[showTooltipFromEvent, tooltip, index, tooltipData]
)

const hideItemTooltip = useCallback(() => {
setIsHover(false)
hideTooltip()
Expand Down
17 changes: 17 additions & 0 deletions packages/radar/src/RadarSliceTooltip.tsx
@@ -0,0 +1,17 @@
import { useMemo } from 'react'
import { TableTooltip, Chip } from '@nivo/tooltip'
import { RadarSliceTooltipProps } from './types'

export const RadarSliceTooltip = ({ index, data }: RadarSliceTooltipProps) => {
const rows = useMemo(
() =>
data.map(datum => [
<Chip key={datum.id} color={datum.color} />,
datum.id,
datum.formattedValue,
]),
[data]
)

return <TableTooltip title={<strong>{index}</strong>} rows={rows} />
}
@@ -1,33 +1,35 @@
import { arc as d3Arc } from 'd3-shape'
import { RadarTooltipItem } from './RadarTooltipItem'
import { RadarColorMapping, RadarDataProps } from './types'
import { RadarSlice } from './RadarSlice'
import { RadarColorMapping, RadarCommonProps, RadarDataProps } from './types'

interface RadarTooltipProps<D extends Record<string, unknown>> {
interface RadarSlicesProps<D extends Record<string, unknown>> {
data: RadarDataProps<D>['data']
keys: RadarDataProps<D>['keys']
getIndex: (d: D) => string | number
formatValue: (value: number, context: string) => string
colorByKey: RadarColorMapping
radius: number
angleStep: number
tooltip: RadarCommonProps['sliceTooltip']
}

export const RadarTooltip = <D extends Record<string, unknown>>({
export const RadarSlices = <D extends Record<string, unknown>>({
data,
keys,
getIndex,
formatValue,
colorByKey,
radius,
angleStep,
}: RadarTooltipProps<D>) => {
tooltip,
}: RadarSlicesProps<D>) => {
const arc = d3Arc<{ startAngle: number; endAngle: number }>().outerRadius(radius).innerRadius(0)

const halfAngleStep = angleStep * 0.5
let rootStartAngle = -halfAngleStep

return (
<g>
<>
{data.map(d => {
const index = getIndex(d)
const startAngle = rootStartAngle
Expand All @@ -36,7 +38,7 @@ export const RadarTooltip = <D extends Record<string, unknown>>({
rootStartAngle += angleStep

return (
<RadarTooltipItem
<RadarSlice
key={index}
datum={d}
keys={keys}
Expand All @@ -47,9 +49,10 @@ export const RadarTooltip = <D extends Record<string, unknown>>({
endAngle={endAngle}
radius={radius}
arcGenerator={arc}
tooltip={tooltip}
/>
)
})}
</g>
</>
)
}
4 changes: 3 additions & 1 deletion packages/radar/src/props.ts
@@ -1,8 +1,9 @@
import { RadarGridLabel } from './RadarGridLabel'
import { RadarSliceTooltip } from './RadarSliceTooltip'
import { RadarLayerId } from './types'

export const svgDefaultProps = {
layers: ['grid', 'shapes', 'dots', 'legends'] as RadarLayerId[],
layers: ['grid', 'shapes', 'slices', 'dots', 'legends'] as RadarLayerId[],

maxValue: 'auto' as const,

Expand Down Expand Up @@ -30,6 +31,7 @@ export const svgDefaultProps = {
blendMode: 'normal' as const,

isInteractive: true,
sliceTooltip: RadarSliceTooltip,

legends: [],
role: 'img',
Expand Down
18 changes: 16 additions & 2 deletions packages/radar/src/types.ts
Expand Up @@ -52,12 +52,26 @@ export interface PointProps {
}
}

export type RadarLayerId = 'grid' | 'shapes' | 'dots' | 'legends'
export interface RadarSliceTooltipDatum {
color: string
id: string
value: number
formattedValue: string
}

export interface RadarSliceTooltipProps {
index: string | number
data: RadarSliceTooltipDatum[]
}
export type RadarSliceTooltipComponent = FunctionComponent<RadarSliceTooltipProps>

export type RadarLayerId = 'grid' | 'shapes' | 'slices' | 'dots' | 'legends'

export type RadarColorMapping = Record<string, string>

export interface RadarCommonProps {
maxValue: number | 'auto'
// second argument passed to the formatter is the key
valueFormat: ValueFormat<number, string>

layers: RadarLayerId[]
Expand Down Expand Up @@ -90,7 +104,7 @@ export interface RadarCommonProps {
borderColor: InheritedColorConfig<{ key: string; color: string }>

isInteractive: boolean
tooltipFormat: ValueFormat<number>
sliceTooltip: RadarSliceTooltipComponent

renderWrapper: boolean

Expand Down
5 changes: 5 additions & 0 deletions packages/radar/tests/.eslintrc.yml
@@ -0,0 +1,5 @@
env:
jest: true

rules:
react/prop-types: 0

0 comments on commit bb81efb

Please sign in to comment.