From 8f804bc1be32ca01880b1ba347dfd14f7988ecde Mon Sep 17 00:00:00 2001 From: plouc Date: Mon, 8 May 2023 22:07:50 +0900 Subject: [PATCH] feat(parallel-coords): add suport for legends and layers --- packages/parallel-coordinates/package.json | 1 + .../src/canvas/ParallelCoordinatesCanvas.tsx | 16 ++- packages/parallel-coordinates/src/defaults.ts | 18 ++- packages/parallel-coordinates/src/hooks.ts | 16 ++- .../src/svg/ParallelCoordinates.tsx | 118 ++++++++++++------ packages/parallel-coordinates/src/types.ts | 24 ++-- pnpm-lock.yaml | 21 ++-- .../components/parallel-coordinates/props.ts | 47 ++++++- .../src/pages/parallel-coordinates/canvas.tsx | 18 ++- .../src/pages/parallel-coordinates/index.tsx | 47 ++++++- 10 files changed, 249 insertions(+), 77 deletions(-) diff --git a/packages/parallel-coordinates/package.json b/packages/parallel-coordinates/package.json index a7e1df7ce..2af228d8a 100644 --- a/packages/parallel-coordinates/package.json +++ b/packages/parallel-coordinates/package.json @@ -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", diff --git a/packages/parallel-coordinates/src/canvas/ParallelCoordinatesCanvas.tsx b/packages/parallel-coordinates/src/canvas/ParallelCoordinatesCanvas.tsx index 88818d930..f3f1e7e44 100644 --- a/packages/parallel-coordinates/src/canvas/ParallelCoordinatesCanvas.tsx +++ b/packages/parallel-coordinates/src/canvas/ParallelCoordinatesCanvas.tsx @@ -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' @@ -22,6 +23,7 @@ export const InnerParallelCoordinatesCanvas = ({ lineOpacity = canvasDefaultProps.lineOpacity, lineWidth = canvasDefaultProps.lineWidth, axesTicksPosition = canvasDefaultProps.axesTicksPosition, + legends = canvasDefaultProps.legends, role = canvasDefaultProps.role, ariaLabel, ariaLabelledBy, @@ -36,7 +38,7 @@ export const InnerParallelCoordinatesCanvas = ({ partialMargin ) - const { variablesScale, variablesWithScale, computedData, lineGenerator } = + const { variablesScale, variablesWithScale, computedData, lineGenerator, legendData } = useParallelCoordinates({ width: innerWidth, height: innerHeight, @@ -89,6 +91,16 @@ export const InnerParallelCoordinatesCanvas = ({ theme, }) }) + + legends.forEach(legend => { + renderLegendToCanvas(ctx, { + ...legend, + data: legendData, + containerWidth: innerWidth, + containerHeight: innerHeight, + theme, + }) + }) }, [ canvasEl, outerWidth, @@ -104,6 +116,8 @@ export const InnerParallelCoordinatesCanvas = ({ variablesWithScale, layout, axesTicksPosition, + legends, + legendData, theme, pixelRatio, ]) diff --git a/packages/parallel-coordinates/src/defaults.ts b/packages/parallel-coordinates/src/defaults.ts index 8ec35ea25..5b4a76655 100644 --- a/packages/parallel-coordinates/src/defaults.ts +++ b/packages/parallel-coordinates/src/defaults.ts @@ -1,4 +1,4 @@ -import { CommonProps, BaseDatum } from './types' +import {CommonProps, BaseDatum, LayerId} from './types' export const commonDefaultProps: Omit< CommonProps, @@ -8,7 +8,7 @@ export const commonDefaultProps: Omit< // | 'onMouseMove' // | 'onMouseLeave' // | 'onClick' - // | 'forwardLegendData' + | 'forwardLegendData' | 'renderWrapper' | 'ariaLabel' | 'ariaLabelledBy' @@ -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', @@ -35,13 +35,11 @@ export const commonDefaultProps: Omit< export const svgDefaultProps = { ...commonDefaultProps, - // layers: ['cells', 'areas', 'legends'] as WaffleSvgLayer[], - // 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, } diff --git a/packages/parallel-coordinates/src/hooks.ts b/packages/parallel-coordinates/src/hooks.ts index 8d155f722..ad7f306f7 100644 --- a/packages/parallel-coordinates/src/hooks.ts +++ b/packages/parallel-coordinates/src/hooks.ts @@ -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 = ({ @@ -95,7 +95,7 @@ export const useParallelCoordinates = ({ curve: CommonProps['curve'] colors: CommonProps['colors'] }) => { - const getColor = useOrdinalColorScale(colors, 'index') + const getColor = useOrdinalColorScale(colors, 'id') const lineGenerator = useMemo( () => line<[number, number]>().curve(curveFromProp(curve)), @@ -115,10 +115,22 @@ export const useParallelCoordinates = ({ [width, height, data, variables, layout, getColor] ) + const legendData: LegendDatum[] = useMemo( + () => + computedData.map(datum => ({ + id: datum.id, + label: datum.id, + color: datum.color, + data: datum, + })), + [computedData] + ) + return { variablesScale, variablesWithScale, computedData, lineGenerator, + legendData, } } diff --git a/packages/parallel-coordinates/src/svg/ParallelCoordinates.tsx b/packages/parallel-coordinates/src/svg/ParallelCoordinates.tsx index c614d55b9..10c305f36 100644 --- a/packages/parallel-coordinates/src/svg/ParallelCoordinates.tsx +++ b/packages/parallel-coordinates/src/svg/ParallelCoordinates.tsx @@ -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 = Omit< @@ -22,6 +24,8 @@ const InnerParallelCoordinates = ({ lineWidth = svgDefaultProps.lineWidth, lineOpacity = svgDefaultProps.lineOpacity, colors = svgDefaultProps.colors, + layers = svgDefaultProps.layers, + legends = svgDefaultProps.legends, role = svgDefaultProps.role, ariaLabel, ariaLabelledBy, @@ -33,7 +37,7 @@ const InnerParallelCoordinates = ({ partialMargin ) - const { variablesScale, variablesWithScale, computedData, lineGenerator } = + const { variablesScale, variablesWithScale, computedData, lineGenerator, legendData } = useParallelCoordinates({ width: innerWidth, height: innerHeight, @@ -48,46 +52,73 @@ const InnerParallelCoordinates = ({ variablesScale, variablesWithScale, computedData, + legendData, }) - const axes = ( - <> - {variablesWithScale.map(variable => ( - - ))} - - ) + const layerById: Record = { + axes: null, + lines: null, + legends: null, + } - const lines = ( - <> - {computedData.map(datum => ( - - key={datum.id} - data={datum} - variables={variables} - lineGenerator={lineGenerator} - lineWidth={lineWidth} - opacity={lineOpacity} - /> - ))} - - ) + if (layers.includes('axes')) { + layerById.axes = ( + + {variablesWithScale.map(variable => ( + + ))} + + ) + } + + if (layers.includes('lines')) { + layerById.lines = ( + + {computedData.map(datum => ( + + key={datum.id} + data={datum} + variables={variables} + lineGenerator={lineGenerator} + lineWidth={lineWidth} + opacity={lineOpacity} + /> + ))} + + ) + } + + if (layers.includes('legends')) { + layerById.legends = ( + + {legends.map((legend, i) => ( + + ))} + + ) + } return ( ({ ariaLabelledBy={ariaLabelledBy} ariaDescribedBy={ariaDescribedBy} > - {axes} - {lines} + {layers.map((layer, i) => { + if (typeof layer === 'function') { + return {createElement(layer, {})} + } + + return layerById?.[layer] ?? null + })} ) } diff --git a/packages/parallel-coordinates/src/types.ts b/packages/parallel-coordinates/src/types.ts index 7b09f5f38..fa4fb1bbf 100644 --- a/packages/parallel-coordinates/src/types.ts +++ b/packages/parallel-coordinates/src/types.ts @@ -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 = { [K in keyof D]: D[K] extends number ? K : never @@ -43,7 +44,14 @@ export interface ComputedDatum { points: [number, number][] } -export type LayerId = 'axes' | 'lines' +export interface LegendDatum { + id: D['id'] + label: D['id'] + color: string + data: ComputedDatum +} + +export type LayerId = 'axes' | 'lines' | 'legends' // Most of those props are optional for the public API, // but required internally, using defaults. @@ -60,14 +68,13 @@ export interface CommonProps extends MotionProps { axesTicksPosition: 'before' | 'after' - layers: LayerId[] - isInteractive: boolean // tooltip: TooltipComponent renderWrapper: boolean - // forwardLegendData: (data: LegendDatum[]) => void + legends: LegendProps[] + forwardLegendData: (data: LegendDatum[]) => void role: string ariaLabel: AriaAttributes['aria-label'] @@ -75,18 +82,21 @@ export interface CommonProps extends MotionProps { ariaDescribedBy: AriaAttributes['aria-describedby'] } +type ParallelCoordinatesLayer = LayerId + export type ParallelCoordinatesProps = DataProps & Dimensions & Partial> & { - // layers?: WaffleHtmlLayer[] - // cellComponent?: CellComponent + layers?: ParallelCoordinatesLayer[] motionStagger?: number testIdPrefix?: string } +type ParallelCoordinatesCanvasLayer = LayerId + export type ParallelCoordinatesCanvasProps = DataProps & Dimensions & Partial> & { - // legends?: LegendProps[] + layers: ParallelCoordinatesCanvasLayer[] pixelRatio?: number } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 88b2b6b90..14777e396 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1037,6 +1037,9 @@ importers: '@nivo/core': specifier: workspace:* version: link:../core + '@nivo/legends': + specifier: workspace:* + version: link:../legends '@nivo/scales': specifier: workspace:* version: link:../scales @@ -11186,6 +11189,7 @@ packages: dependencies: ms: 2.1.3 supports-color: 5.5.0 + dev: true /debug@3.2.7(supports-color@8.1.1): resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -11197,7 +11201,6 @@ packages: dependencies: ms: 2.1.3 supports-color: 8.1.1 - dev: true /debug@4.3.4(supports-color@5.5.0): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} @@ -11475,7 +11478,7 @@ packages: '@types/tmp': 0.0.33 application-config-path: 0.1.0 command-exists: 1.2.9 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) eol: 0.9.1 get-port: 3.2.0 glob: 7.2.3 @@ -12231,7 +12234,7 @@ packages: /eslint-import-resolver-node@0.3.6: resolution: {integrity: sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==} dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) resolve: 1.22.2 transitivePeerDependencies: - supports-color @@ -12240,7 +12243,7 @@ packages: /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) is-core-module: 2.12.0 resolve: 1.22.2 transitivePeerDependencies: @@ -12266,7 +12269,7 @@ packages: optional: true dependencies: '@typescript-eslint/parser': 5.59.1(eslint@8.39.0)(typescript@4.9.5) - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) eslint-import-resolver-node: 0.3.6 find-up: 2.1.0 pkg-dir: 2.0.0 @@ -12296,7 +12299,7 @@ packages: optional: true dependencies: '@typescript-eslint/parser': 5.59.1(eslint@8.39.0)(typescript@4.9.5) - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) eslint: 8.39.0 eslint-import-resolver-node: 0.3.7 transitivePeerDependencies: @@ -12374,7 +12377,7 @@ packages: array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 eslint: 8.39.0 eslint-import-resolver-node: 0.3.7 @@ -12918,7 +12921,7 @@ packages: resolution: {integrity: sha512-/l77JHcOUrDUX8V67E287VEUQT0lbm71gdGVoodnlWBziarYKgMcpqT7xvh/HM8Jv52phw8Bd8tY+a7QjOr7Yg==} engines: {node: '>=6.0.0'} dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) es6-promise: 4.2.8 raw-body: 2.4.3 transitivePeerDependencies: @@ -18542,7 +18545,7 @@ packages: engines: {node: '>= 4.4.x'} hasBin: true dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) iconv-lite: 0.4.24 sax: 1.2.4 transitivePeerDependencies: diff --git a/website/src/data/components/parallel-coordinates/props.ts b/website/src/data/components/parallel-coordinates/props.ts index 26a577d7d..44d119343 100644 --- a/website/src/data/components/parallel-coordinates/props.ts +++ b/website/src/data/components/parallel-coordinates/props.ts @@ -1,7 +1,12 @@ // @ts-ignore import { lineCurvePropKeys } from '@nivo/core' import { commonDefaultProps as defaults } from '@nivo/parallel-coordinates' -import { themeProperty, motionProperties, groupProperties } from '../../../lib/componentProperties' +import { + themeProperty, + motionProperties, + groupProperties, + getLegendsProps, +} from '../../../lib/componentProperties' import { chartDimensions, commonAccessibilityProps, @@ -181,6 +186,46 @@ const props: ChartProperty[] = [ control: { type: 'opacity' }, group: 'Style', }, + { + key: 'legends', + group: 'Legends', + type: 'LegendDatum[]', + required: false, + help: `Optional chart's legends.`, + flavors: allFlavors, + control: { + type: 'array', + props: getLegendsProps(allFlavors), + shouldCreate: true, + addLabel: 'add legend', + shouldRemove: true, + defaults: { + anchor: 'left', + direction: 'column', + justify: false, + translateX: -100, + translateY: 0, + itemWidth: 100, + itemHeight: 20, + itemsSpacing: 4, + symbolSize: 20, + itemDirection: 'left-to-right', + itemTextColor: '#777', + onClick: (data: any) => { + console.log(JSON.stringify(data, null, ' ')) + }, + effects: [ + { + on: 'hover', + style: { + itemTextColor: '#000', + itemBackground: '#f7fafb', + }, + }, + ], + }, + }, + }, ...motionProperties(allFlavors, defaults), ...commonAccessibilityProps(allFlavors), ] diff --git a/website/src/pages/parallel-coordinates/canvas.tsx b/website/src/pages/parallel-coordinates/canvas.tsx index d1c9c5fd5..2da79e06e 100644 --- a/website/src/pages/parallel-coordinates/canvas.tsx +++ b/website/src/pages/parallel-coordinates/canvas.tsx @@ -15,18 +15,32 @@ const initialProperties = { variables, margin: { top: 50, - right: 60, + right: 120, bottom: 50, left: 60, }, layout: commonDefaultProps.layout, curve: commonDefaultProps.curve, colors: commonDefaultProps.colors, - colorBy: commonDefaultProps.colorBy, lineWidth: 1, lineOpacity: 0.2, axesPlan: commonDefaultProps.axesPlan, axesTicksPosition: commonDefaultProps.axesTicksPosition, + legends: [ + { + anchor: 'bottom-right', + direction: 'column', + justify: false, + translateX: 140, + translateY: 0, + itemsSpacing: 2, + itemWidth: 100, + itemHeight: 20, + itemDirection: 'left-to-right', + itemOpacity: 0.85, + symbolSize: 20, + }, + ], animate: commonDefaultProps.animate, motionConfig: commonDefaultProps.motionConfig, pixelRatio: diff --git a/website/src/pages/parallel-coordinates/index.tsx b/website/src/pages/parallel-coordinates/index.tsx index 1c3138d76..2a92fcbd8 100644 --- a/website/src/pages/parallel-coordinates/index.tsx +++ b/website/src/pages/parallel-coordinates/index.tsx @@ -13,22 +13,61 @@ const initialProperties = { variables, margin: { top: 50, - right: 60, + right: 120, bottom: 50, left: 60, }, layout: commonDefaultProps.layout, curve: commonDefaultProps.curve, colors: commonDefaultProps.colors, - colorBy: commonDefaultProps.colorBy, - lineWidth: commonDefaultProps.lineWidth, + lineWidth: 3, lineOpacity: commonDefaultProps.lineOpacity, axesTicksPosition: commonDefaultProps.axesTicksPosition, animate: commonDefaultProps.animate, motionConfig: commonDefaultProps.motionConfig, + legends: [ + { + anchor: 'right', + direction: 'column', + justify: false, + translateX: 100, + translateY: 0, + itemsSpacing: 2, + itemWidth: 60, + itemHeight: 20, + itemDirection: 'left-to-right', + itemOpacity: 0.85, + symbolSize: 20, + onClick: (data: any) => { + alert(JSON.stringify(data, null, ' ')) + }, + effects: [ + { + on: 'hover', + style: { + itemOpacity: 1, + }, + }, + ], + }, + ], } -const generateData = () => generateParallelCoordinatesData() +const generateData = () => + generateParallelCoordinatesData({ + ids: [ + { id: 'A' }, + { id: 'B' }, + { id: 'C' }, + { id: 'D' }, + { id: 'E' }, + { id: 'F' }, + { id: 'G' }, + { id: 'H' }, + { id: 'I' }, + { id: 'J' }, + ], + }) const ParallelCoordinates = () => { const {