Skip to content

Commit

Permalink
feat(parallel-coords): add suport for legends and layers
Browse files Browse the repository at this point in the history
  • Loading branch information
plouc committed May 8, 2023
1 parent 33ec8bf commit 8f804bc
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 77 deletions.
1 change: 1 addition & 0 deletions packages/parallel-coordinates/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@nivo/axes": "workspace:*",
"@nivo/colors": "workspace:*",
"@nivo/core": "workspace:*",
"@nivo/legends": "workspace:*",
"@nivo/scales": "workspace:*",
"@nivo/tooltip": "workspace:*",
"@react-spring/web": "9.4.5 || ^9.7.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useRef } from 'react'
import { Container, useDimensions, useTheme } from '@nivo/core'
import { renderAxisToCanvas } from '@nivo/axes'
import { renderLegendToCanvas } from '@nivo/legends'
import { useParallelCoordinates } from '../hooks'
import { BaseDatum, ParallelCoordinatesCanvasProps } from '../types'
import { canvasDefaultProps } from '../defaults'
Expand All @@ -22,6 +23,7 @@ export const InnerParallelCoordinatesCanvas = <D extends BaseDatum>({
lineOpacity = canvasDefaultProps.lineOpacity,
lineWidth = canvasDefaultProps.lineWidth,
axesTicksPosition = canvasDefaultProps.axesTicksPosition,
legends = canvasDefaultProps.legends,
role = canvasDefaultProps.role,
ariaLabel,
ariaLabelledBy,
Expand All @@ -36,7 +38,7 @@ export const InnerParallelCoordinatesCanvas = <D extends BaseDatum>({
partialMargin
)

const { variablesScale, variablesWithScale, computedData, lineGenerator } =
const { variablesScale, variablesWithScale, computedData, lineGenerator, legendData } =
useParallelCoordinates<D>({
width: innerWidth,
height: innerHeight,
Expand Down Expand Up @@ -89,6 +91,16 @@ export const InnerParallelCoordinatesCanvas = <D extends BaseDatum>({
theme,
})
})

legends.forEach(legend => {
renderLegendToCanvas(ctx, {
...legend,
data: legendData,
containerWidth: innerWidth,
containerHeight: innerHeight,
theme,
})
})
}, [
canvasEl,
outerWidth,
Expand All @@ -104,6 +116,8 @@ export const InnerParallelCoordinatesCanvas = <D extends BaseDatum>({
variablesWithScale,
layout,
axesTicksPosition,
legends,
legendData,
theme,
pixelRatio,
])
Expand Down
18 changes: 8 additions & 10 deletions packages/parallel-coordinates/src/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CommonProps, BaseDatum } from './types'
import {CommonProps, BaseDatum, LayerId} from './types'

export const commonDefaultProps: Omit<
CommonProps<BaseDatum>,
Expand All @@ -8,7 +8,7 @@ export const commonDefaultProps: Omit<
// | 'onMouseMove'
// | 'onMouseLeave'
// | 'onClick'
// | 'forwardLegendData'
| 'forwardLegendData'
| 'renderWrapper'
| 'ariaLabel'
| 'ariaLabelledBy'
Expand All @@ -19,14 +19,14 @@ export const commonDefaultProps: Omit<

role: 'img',

colors: { scheme: 'yellow_orange_red' },
colors: { scheme: 'category10' },
lineWidth: 2,
lineOpacity: 0.35,

layers: ['axes', 'lines'],
lineOpacity: 0.5,

axesTicksPosition: 'after',

legends: [],

animate: true,
motionConfig: 'gentle',

Expand All @@ -35,13 +35,11 @@ export const commonDefaultProps: Omit<

export const svgDefaultProps = {
...commonDefaultProps,
// layers: ['cells', 'areas', 'legends'] as WaffleSvgLayer<Datum>[],
// legends: [],
layers: ['axes', 'lines', 'legends'] as LayerId[],
}

export const canvasDefaultProps = {
...commonDefaultProps,
// layers
// legends: [],
layers: ['axes', 'lines', 'legends'] as LayerId[],
pixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio ?? 1 : 1,
}
16 changes: 14 additions & 2 deletions packages/parallel-coordinates/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { scaleLinear, scalePoint } from 'd3-scale'
import { curveFromProp } from '@nivo/core'
import { OrdinalColorScaleConfig, useOrdinalColorScale } from '@nivo/colors'
import { castPointScale, castLinearScale } from '@nivo/scales'
import { VariableSpec, CommonProps, ComputedDatum, BaseDatum } from './types'
import { VariableSpec, CommonProps, ComputedDatum, BaseDatum, LegendDatum } from './types'
import { commonDefaultProps } from './defaults'

const computeParallelCoordinatesLayout = <D extends BaseDatum>({
Expand Down Expand Up @@ -95,7 +95,7 @@ export const useParallelCoordinates = <D extends BaseDatum>({
curve: CommonProps<D>['curve']
colors: CommonProps<D>['colors']
}) => {
const getColor = useOrdinalColorScale(colors, 'index')
const getColor = useOrdinalColorScale(colors, 'id')

const lineGenerator = useMemo(
() => line<[number, number]>().curve(curveFromProp(curve)),
Expand All @@ -115,10 +115,22 @@ export const useParallelCoordinates = <D extends BaseDatum>({
[width, height, data, variables, layout, getColor]
)

const legendData: LegendDatum<D>[] = useMemo(
() =>
computedData.map(datum => ({
id: datum.id,
label: datum.id,
color: datum.color,
data: datum,
})),
[computedData]
)

return {
variablesScale,
variablesWithScale,
computedData,
lineGenerator,
legendData,
}
}
118 changes: 77 additions & 41 deletions packages/parallel-coordinates/src/svg/ParallelCoordinates.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { createElement, Fragment, ReactNode } from 'react'
import { Container, SvgWrapper, useDimensions } from '@nivo/core'
import { Axis } from '@nivo/axes'
import { BoxLegendSvg } from '@nivo/legends'
import { svgDefaultProps } from '../defaults'
import { useParallelCoordinates } from '../hooks'
import { ParallelCoordinatesProps, BaseDatum } from '../types'
import { ParallelCoordinatesProps, BaseDatum, LayerId } from '../types'
import { ParallelCoordinatesLine } from './ParallelCoordinatesLine'

type InnerParallelCoordinatesProps<D extends BaseDatum> = Omit<
Expand All @@ -22,6 +24,8 @@ const InnerParallelCoordinates = <D extends BaseDatum>({
lineWidth = svgDefaultProps.lineWidth,
lineOpacity = svgDefaultProps.lineOpacity,
colors = svgDefaultProps.colors,
layers = svgDefaultProps.layers,
legends = svgDefaultProps.legends,
role = svgDefaultProps.role,
ariaLabel,
ariaLabelledBy,
Expand All @@ -33,7 +37,7 @@ const InnerParallelCoordinates = <D extends BaseDatum>({
partialMargin
)

const { variablesScale, variablesWithScale, computedData, lineGenerator } =
const { variablesScale, variablesWithScale, computedData, lineGenerator, legendData } =
useParallelCoordinates<D>({
width: innerWidth,
height: innerHeight,
Expand All @@ -48,46 +52,73 @@ const InnerParallelCoordinates = <D extends BaseDatum>({
variablesScale,
variablesWithScale,
computedData,
legendData,
})

const axes = (
<>
{variablesWithScale.map(variable => (
<Axis
key={variable.id}
axis={layout === 'horizontal' ? 'y' : 'x'}
length={layout === 'horizontal' ? innerHeight : innerWidth}
x={layout === 'horizontal' ? variablesScale(variable.id) : 0}
y={layout === 'horizontal' ? 0 : variablesScale(variable.id)}
scale={variable.scale}
ticksPosition={variable.ticksPosition || axesTicksPosition}
tickValues={variable.tickValues}
tickSize={variable.tickSize}
tickPadding={variable.tickPadding}
tickRotation={variable.tickRotation}
format={variable.tickFormat}
legend={variable.label || variable.id}
legendPosition={variable.legendPosition}
legendOffset={variable.legendOffset}
/>
))}
</>
)
const layerById: Record<LayerId, ReactNode> = {
axes: null,
lines: null,
legends: null,
}

const lines = (
<>
{computedData.map(datum => (
<ParallelCoordinatesLine<D>
key={datum.id}
data={datum}
variables={variables}
lineGenerator={lineGenerator}
lineWidth={lineWidth}
opacity={lineOpacity}
/>
))}
</>
)
if (layers.includes('axes')) {
layerById.axes = (
<g key="axes">
{variablesWithScale.map(variable => (
<Axis
key={variable.id}
axis={layout === 'horizontal' ? 'y' : 'x'}
length={layout === 'horizontal' ? innerHeight : innerWidth}
x={layout === 'horizontal' ? variablesScale(variable.id) : 0}
y={layout === 'horizontal' ? 0 : variablesScale(variable.id)}
scale={variable.scale}
ticksPosition={variable.ticksPosition || axesTicksPosition}
tickValues={variable.tickValues}
tickSize={variable.tickSize}
tickPadding={variable.tickPadding}
tickRotation={variable.tickRotation}
format={variable.tickFormat}
legend={variable.label || variable.id}
legendPosition={variable.legendPosition}
legendOffset={variable.legendOffset}
/>
))}
</g>
)
}

if (layers.includes('lines')) {
layerById.lines = (
<g key="lines">
{computedData.map(datum => (
<ParallelCoordinatesLine<D>
key={datum.id}
data={datum}
variables={variables}
lineGenerator={lineGenerator}
lineWidth={lineWidth}
opacity={lineOpacity}
/>
))}
</g>
)
}

if (layers.includes('legends')) {
layerById.legends = (
<g key="legends">
{legends.map((legend, i) => (
<BoxLegendSvg
key={i}
{...legend}
containerWidth={innerWidth}
containerHeight={innerHeight}
data={legendData}
/>
))}
</g>
)
}

return (
<SvgWrapper
Expand All @@ -99,8 +130,13 @@ const InnerParallelCoordinates = <D extends BaseDatum>({
ariaLabelledBy={ariaLabelledBy}
ariaDescribedBy={ariaDescribedBy}
>
{axes}
{lines}
{layers.map((layer, i) => {
if (typeof layer === 'function') {
return <Fragment key={i}>{createElement(layer, {})}</Fragment>
}

return layerById?.[layer] ?? null
})}
</SvgWrapper>
)
}
Expand Down
24 changes: 17 additions & 7 deletions packages/parallel-coordinates/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AriaAttributes } from 'react'
import { Box, Dimensions, MotionProps, LineCurveFactoryId, Theme, ValueFormat } from '@nivo/core'
import { OrdinalColorScaleConfig } from '@nivo/colors'
import { AxisProps } from '@nivo/axes'
import { LegendProps } from '@nivo/legends'

type KeysForValues<D extends BaseDatum> = {
[K in keyof D]: D[K] extends number ? K : never
Expand Down Expand Up @@ -43,7 +44,14 @@ export interface ComputedDatum<D extends BaseDatum> {
points: [number, number][]
}

export type LayerId = 'axes' | 'lines'
export interface LegendDatum<D extends BaseDatum> {
id: D['id']
label: D['id']
color: string
data: ComputedDatum<D>
}

export type LayerId = 'axes' | 'lines' | 'legends'

// Most of those props are optional for the public API,
// but required internally, using defaults.
Expand All @@ -60,33 +68,35 @@ export interface CommonProps<D extends BaseDatum> extends MotionProps {

axesTicksPosition: 'before' | 'after'

layers: LayerId[]

isInteractive: boolean
// tooltip: TooltipComponent<D>

renderWrapper: boolean

// forwardLegendData: (data: LegendDatum<D>[]) => void
legends: LegendProps[]
forwardLegendData: (data: LegendDatum<D>[]) => void

role: string
ariaLabel: AriaAttributes['aria-label']
ariaLabelledBy: AriaAttributes['aria-labelledby']
ariaDescribedBy: AriaAttributes['aria-describedby']
}

type ParallelCoordinatesLayer = LayerId

export type ParallelCoordinatesProps<D extends BaseDatum> = DataProps<D> &
Dimensions &
Partial<CommonProps<D>> & {
// layers?: WaffleHtmlLayer<D>[]
// cellComponent?: CellComponent<D>
layers?: ParallelCoordinatesLayer[]
motionStagger?: number
testIdPrefix?: string
}

type ParallelCoordinatesCanvasLayer = LayerId

export type ParallelCoordinatesCanvasProps<D extends BaseDatum> = DataProps<D> &
Dimensions &
Partial<CommonProps<D>> & {
// legends?: LegendProps[]
layers: ParallelCoordinatesCanvasLayer[]
pixelRatio?: number
}
Loading

0 comments on commit 8f804bc

Please sign in to comment.