From 2e94f26b24aad2b093caa4a35e905bef3b326e04 Mon Sep 17 00:00:00 2001 From: tkonopka Date: Fri, 3 Jun 2022 11:18:04 +0100 Subject: [PATCH 1/6] added support for swarmplot legends and swarmplot log axis --- packages/swarmplot/src/SwarmPlot.tsx | 19 ++++++- packages/swarmplot/src/SwarmPlotCanvas.tsx | 20 +++++++- packages/swarmplot/src/SwarmPlotLegends.tsx | 23 +++++++++ packages/swarmplot/src/compute.ts | 27 +++++++++- packages/swarmplot/src/hooks.ts | 21 ++++++++ packages/swarmplot/src/props.ts | 3 +- packages/swarmplot/src/types.ts | 24 ++++++++- .../swarmplot/stories/SwarmPlot.stories.tsx | 49 ++++++++++++++++++ .../stories/SwarmPlotCanvas.stories.tsx | 50 +++++++++++++++++++ 9 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 packages/swarmplot/src/SwarmPlotLegends.tsx diff --git a/packages/swarmplot/src/SwarmPlot.tsx b/packages/swarmplot/src/SwarmPlot.tsx index 0694551333..c357e92a9c 100644 --- a/packages/swarmplot/src/SwarmPlot.tsx +++ b/packages/swarmplot/src/SwarmPlot.tsx @@ -10,6 +10,7 @@ import { useSwarmPlot, useSwarmPlotLayerContext, useNodeMouseHandlers } from './ import { Circles } from './Circles' import { CircleSvg } from './CircleSvg' import { SwarmPlotAnnotations } from './SwarmPlotAnnotations' +import { SwarmPlotLegends } from './SwarmPlotLegends' type InnerSwarmPlotProps = Partial< Omit< @@ -52,6 +53,8 @@ const InnerSwarmPlot = ({ axisRight = defaultProps.axisRight, axisBottom = defaultProps.axisBottom, axisLeft = defaultProps.axisLeft, + legendLabel, + legends = defaultProps.legends, isInteractive, onMouseEnter, onMouseMove, @@ -67,7 +70,7 @@ const InnerSwarmPlot = ({ partialMargin ) - const { nodes, ...props } = useSwarmPlot({ + const { nodes, legendsData, ...props } = useSwarmPlot({ width: innerWidth, height: innerHeight, data, @@ -85,6 +88,8 @@ const InnerSwarmPlot = ({ colorBy, forceStrength, simulationIterations, + legendLabel, + legends, }) const xScale = props.xScale as Exclude[]> @@ -105,6 +110,7 @@ const InnerSwarmPlot = ({ circles: null, annotations: null, mesh: null, + legends: null, } if (layers.includes('grid')) { @@ -165,6 +171,17 @@ const InnerSwarmPlot = ({ ) } + if (layers.includes('legends')) { + layerById.legends = ( + + ) + } + if (isInteractive && useMesh) { layerById.mesh = ( ( ctx: CanvasRenderingContext2D, @@ -78,6 +79,8 @@ export const InnerSwarmPlotCanvas = ({ axisRight = defaultProps.axisRight, axisBottom = defaultProps.axisBottom, axisLeft = defaultProps.axisLeft, + legendLabel, + legends = defaultProps.legends, isInteractive, onMouseMove, onClick, @@ -95,7 +98,7 @@ export const InnerSwarmPlotCanvas = ({ partialMargin ) - const { nodes, ...scales } = useSwarmPlot({ + const { nodes, legendsData, ...scales } = useSwarmPlot({ width: innerWidth, height: innerHeight, data, @@ -113,6 +116,8 @@ export const InnerSwarmPlotCanvas = ({ colorBy, forceStrength, simulationIterations, + legendLabel, + legends, }) const { xScale, yScale } = scales as Record<'xScale' | 'yScale', AnyScale> @@ -203,6 +208,18 @@ export const InnerSwarmPlotCanvas = ({ renderVoronoiCellToCanvas(ctx, voronoi, currentNode.index) } } + + if (layer === 'legends') { + legendsData.forEach(([legend, data]) => { + renderLegendToCanvas(ctx, { + ...legend, + data, + containerWidth: innerWidth, + containerHeight: innerHeight, + theme, + }) + }) + } }) }, [ canvasEl, @@ -231,6 +248,7 @@ export const InnerSwarmPlotCanvas = ({ renderCircle, getBorderWidth, getBorderColor, + legendsData, ]) const getNodeFromMouseEvent = useCallback( diff --git a/packages/swarmplot/src/SwarmPlotLegends.tsx b/packages/swarmplot/src/SwarmPlotLegends.tsx new file mode 100644 index 0000000000..fdfdf1dd56 --- /dev/null +++ b/packages/swarmplot/src/SwarmPlotLegends.tsx @@ -0,0 +1,23 @@ +import { BoxLegendSvg } from '@nivo/legends' +import { LegendProps } from '@nivo/legends' +import { SwarmPlotLegendData } from './types' + +interface SwarmPlotLegendsProps { + width: number + height: number + legends: [LegendProps, SwarmPlotLegendData[]][] +} + +export const SwarmPlotLegends = ({ width, height, legends }: SwarmPlotLegendsProps) => ( + <> + {legends.map(([legend, data], i) => ( + + ))} + +) diff --git a/packages/swarmplot/src/compute.ts b/packages/swarmplot/src/compute.ts index 393991af9e..1b31e0582f 100644 --- a/packages/swarmplot/src/compute.ts +++ b/packages/swarmplot/src/compute.ts @@ -1,6 +1,7 @@ import isNumber from 'lodash/isNumber' import isPlainObject from 'lodash/isPlainObject' import isString from 'lodash/isString' +import uniqBy from 'lodash/uniqBy' import get from 'lodash/get' import { scaleLinear, ScaleOrdinal, scaleOrdinal } from 'd3-scale' import { forceSimulation, forceX, forceY, forceCollide, ForceX, ForceY } from 'd3-force' @@ -13,7 +14,13 @@ import { ScaleTime, ScaleTimeSpec, } from '@nivo/scales' -import { ComputedDatum, PreSimulationDatum, SizeSpec, SimulationForces } from './types' +import { + ComputedDatum, + PreSimulationDatum, + SizeSpec, + SimulationForces, + SwarmPlotLegendData, +} from './types' const getParsedValue = (scaleSpec: ScaleLinearSpec | ScaleTimeSpec) => { if (scaleSpec.type === 'time' && scaleSpec.format !== 'native') { @@ -220,3 +227,21 @@ export const computeNodes = ({ nodes: simulation.nodes() as ComputedDatum[], } } + +export const getLegendData = ({ + nodes, + getLegendLabel, +}: { + nodes: ComputedDatum[] + getLegendLabel: (datum: RawDatum) => string +}) => { + const nodeData = nodes.map( + node => + ({ + id: node.group, + label: getLegendLabel(node.data), + color: node?.color, + } as SwarmPlotLegendData) + ) + return uniqBy(nodeData, ({ id }) => id) +} diff --git a/packages/swarmplot/src/hooks.ts b/packages/swarmplot/src/hooks.ts index 4d33692d38..b4f5b2a8d6 100644 --- a/packages/swarmplot/src/hooks.ts +++ b/packages/swarmplot/src/hooks.ts @@ -9,16 +9,19 @@ import { computeValueScale, computeOrdinalScale, getSizeGenerator, + getLegendData, computeForces, computeNodes, } from './compute' import { SwarmPlotCommonProps, + SwarmPlotLegendData, ComputedDatum, SizeSpec, SwarmPlotCustomLayerProps, MouseHandlers, } from './types' +import { LegendProps } from '@nivo/legends' export const useValueScale = ({ width, @@ -112,6 +115,8 @@ export const useSwarmPlot = ({ simulationIterations, colors, colorBy, + legendLabel, + legends, }: { data: RawDatum[] width: number @@ -130,6 +135,8 @@ export const useSwarmPlot = ({ simulationIterations: SwarmPlotCommonProps['simulationIterations'] colors: SwarmPlotCommonProps['colors'] colorBy: SwarmPlotCommonProps['colorBy'] + legendLabel: SwarmPlotCommonProps['legendLabel'] + legends: SwarmPlotCommonProps['legends'] }) => { const axis = layout === 'horizontal' ? 'x' : 'y' @@ -143,6 +150,7 @@ export const useSwarmPlot = ({ colors, getColorId ) + const getLegendLabel = usePropertyAccessor(legendLabel ?? groupBy) const valueScale = useValueScale({ width, @@ -209,11 +217,24 @@ export const useSwarmPlot = ({ [nodes, formatValue, getColor] ) + const legendsData: [LegendProps, SwarmPlotLegendData[]][] = useMemo( + () => + legends.map(legend => { + const data = getLegendData({ + nodes: augmentedNodes, + getLegendLabel, + }) + return [legend, data] + }), + [legends, augmentedNodes, getLegendLabel] + ) + return { nodes: augmentedNodes, xScale, yScale, getColor, + legendsData, } } diff --git a/packages/swarmplot/src/props.ts b/packages/swarmplot/src/props.ts index ed6b1da6e0..d3c166d3af 100644 --- a/packages/swarmplot/src/props.ts +++ b/packages/swarmplot/src/props.ts @@ -17,13 +17,14 @@ export const defaultProps = { colorBy: 'group', borderWidth: 0, borderColor: 'rgba(0, 0, 0, 0)', - layers: ['grid', 'axes', 'circles', 'annotations', 'mesh'] as SwarmPlotLayerId[], + layers: ['grid', 'axes', 'circles', 'annotations', 'mesh', 'legends'] as SwarmPlotLayerId[], enableGridX: true, enableGridY: true, axisTop: {}, axisRight: {}, axisBottom: {}, axisLeft: {}, + legends: [], isInteractive: true, useMesh: false, debugMesh: false, diff --git a/packages/swarmplot/src/types.ts b/packages/swarmplot/src/types.ts index 0b4521a830..98c5d69531 100644 --- a/packages/swarmplot/src/types.ts +++ b/packages/swarmplot/src/types.ts @@ -4,9 +4,17 @@ import { ForceX, ForceY, ForceCollide } from 'd3-force' import { PropertyAccessor, ValueFormat, Theme, ModernMotionProps, Box, Margin } from '@nivo/core' import { InheritedColorConfig, OrdinalColorScaleConfig } from '@nivo/colors' import { AxisProps, CanvasAxisProps } from '@nivo/axes' -import { ScaleLinear, ScaleLinearSpec, ScaleTime, ScaleTimeSpec, TicksSpec } from '@nivo/scales' +import { + ScaleLinear, + ScaleLinearSpec, + ScaleLog, + ScaleTime, + ScaleTimeSpec, + TicksSpec, +} from '@nivo/scales' import { AnnotationMatcher } from '@nivo/annotations' import { ScaleOrdinal } from 'd3-scale' +import { LegendProps } from '@nivo/legends' export interface ComputedDatum { id: string @@ -33,12 +41,13 @@ export type SimulationForces = { collision: ForceCollide> } -export type SwarmPlotLayerId = 'grid' | 'axes' | 'circles' | 'annotations' | 'mesh' +export type SwarmPlotLayerId = 'grid' | 'axes' | 'circles' | 'annotations' | 'mesh' | 'legends' export interface SwarmPlotCustomLayerProps< RawDatum, Scale extends | ScaleLinear + | ScaleLog | ScaleTime | ScaleOrdinal = ScaleLinear > { @@ -56,6 +65,7 @@ export type SwarmPlotCustomLayer< RawDatum, Scale extends | ScaleLinear + | ScaleLog | ScaleTime | ScaleOrdinal = ScaleLinear > = React.FC> @@ -64,6 +74,7 @@ export type SwarmPlotLayer< RawDatum, Scale extends | ScaleLinear + | ScaleLog | ScaleTime | ScaleOrdinal = ScaleLinear > = SwarmPlotLayerId | SwarmPlotCustomLayer @@ -91,6 +102,13 @@ export type MouseHandlers = { onMouseLeave?: MouseHandler } +// replace in future by a type from @nivo/legends +export type SwarmPlotLegendData = { + id: string | number + label: string | number + color: string +} + export type SwarmPlotCommonProps = { data: RawDatum[] width: number @@ -126,6 +144,8 @@ export type SwarmPlotCommonProps = { RawDatum, ScaleLinear | ScaleTime | ScaleOrdinal >[] + legendLabel?: PropertyAccessor + legends: LegendProps[] animate: boolean motionConfig: ModernMotionProps['motionConfig'] role: string diff --git a/packages/swarmplot/stories/SwarmPlot.stories.tsx b/packages/swarmplot/stories/SwarmPlot.stories.tsx index 192d95c64e..debc1f8d3f 100644 --- a/packages/swarmplot/stories/SwarmPlot.stories.tsx +++ b/packages/swarmplot/stories/SwarmPlot.stories.tsx @@ -3,6 +3,7 @@ import { generateSwarmPlotData } from '@nivo/generators' import { SwarmPlot } from '../src' import { SwarmPlotExtraLayers } from './SwarmPlotExtraLayers' import { SwarmPlotCustomCircle } from './SwarmPlotCustomCircle' +import { select } from '@storybook/addon-knobs' const commonProps = { width: 700, @@ -133,3 +134,51 @@ stories.add('using time scale', () => ( layout="horizontal" /> )) + +stories.add('using log scale', () => { + // ensure that dataset has price as a non-negative value + const data = commonProps.data.map(datum => ({ + ...datum, + price: Math.max(1, datum.price), + })) + return ( + + ) +}) diff --git a/packages/swarmplot/stories/SwarmPlotCanvas.stories.tsx b/packages/swarmplot/stories/SwarmPlotCanvas.stories.tsx index b72cea30e8..c84dbf5695 100644 --- a/packages/swarmplot/stories/SwarmPlotCanvas.stories.tsx +++ b/packages/swarmplot/stories/SwarmPlotCanvas.stories.tsx @@ -61,3 +61,53 @@ stories.add('using annotations', () => ( ]} /> )) + +stories.add('using log scale', () => { + // ensure that dataset has price as a non-negative value + const data = commonProps.data.map(datum => ({ + ...datum, + price: Math.max(1, datum.price), + })) + return ( + + ) +}) From 61845eca73aea05036341f23f34642bf11a09fd2 Mon Sep 17 00:00:00 2001 From: tkonopka Date: Fri, 3 Jun 2022 11:33:32 +0100 Subject: [PATCH 2/6] memoized an object in SwarmplotCanvas to prevent downstream depencies changing on every render --- packages/swarmplot/src/SwarmPlotCanvas.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/swarmplot/src/SwarmPlotCanvas.tsx b/packages/swarmplot/src/SwarmPlotCanvas.tsx index 835ee97cb7..df7e79abfe 100644 --- a/packages/swarmplot/src/SwarmPlotCanvas.tsx +++ b/packages/swarmplot/src/SwarmPlotCanvas.tsx @@ -1,4 +1,4 @@ -import { createElement, useCallback, useEffect, useRef, useState } from 'react' +import { createElement, useCallback, useEffect, useMemo, useRef, useState } from 'react' import * as React from 'react' import isNumber from 'lodash/isNumber' import { Container, getRelativeCursor, isCursorInRect, useDimensions, useTheme } from '@nivo/core' @@ -130,7 +130,8 @@ export const InnerSwarmPlotCanvas = ({ }) const getBorderColor = useInheritedColor(borderColor, theme) - const getBorderWidth = () => 1 + // memoize getBorderWidth to provide a stable object for the rendering + const getBorderWidth = useMemo(() => () => 1, []) useEffect(() => { if (!canvasEl.current) return From dd266adfbf022067c78d8a443dd71fa9f2367e10 Mon Sep 17 00:00:00 2001 From: tkonopka Date: Fri, 3 Jun 2022 11:42:09 +0100 Subject: [PATCH 3/6] tweaked stories --- packages/swarmplot/stories/SwarmPlot.stories.tsx | 1 + .../swarmplot/stories/SwarmPlotCanvas.stories.tsx | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/swarmplot/stories/SwarmPlot.stories.tsx b/packages/swarmplot/stories/SwarmPlot.stories.tsx index debc1f8d3f..0df1e4cf3f 100644 --- a/packages/swarmplot/stories/SwarmPlot.stories.tsx +++ b/packages/swarmplot/stories/SwarmPlot.stories.tsx @@ -150,6 +150,7 @@ stories.add('using log scale', () => { valueScale={{ type: 'log' as const, }} + size={8} axisBottom={null} axisRight={null} axisTop={{ diff --git a/packages/swarmplot/stories/SwarmPlotCanvas.stories.tsx b/packages/swarmplot/stories/SwarmPlotCanvas.stories.tsx index c84dbf5695..ab90ea765f 100644 --- a/packages/swarmplot/stories/SwarmPlotCanvas.stories.tsx +++ b/packages/swarmplot/stories/SwarmPlotCanvas.stories.tsx @@ -67,22 +67,25 @@ stories.add('using log scale', () => { const data = commonProps.data.map(datum => ({ ...datum, price: Math.max(1, datum.price), - })) + })).filter(datum => datum.group != "group G") return ( { legendOffset: -50, }} axisBottom={{ - tickSize: 10, + tickSize: 5, tickPadding: 5, tickRotation: 0, - legend: 'groups', + legend: '', legendPosition: 'middle', legendOffset: 40, }} From b81f72babf656e7eb0b48242db71a0f0ea26abb5 Mon Sep 17 00:00:00 2001 From: tkonopka Date: Fri, 3 Jun 2022 12:31:01 +0100 Subject: [PATCH 4/6] organized value scales and their spec with dedicated types --- packages/swarmplot/src/compute.ts | 32 ++++++++----------- packages/swarmplot/src/hooks.ts | 12 +++---- packages/swarmplot/src/types.ts | 30 ++++++----------- .../swarmplot/stories/SwarmPlot.stories.tsx | 11 +++++-- .../stories/SwarmPlotCanvas.stories.tsx | 12 ++++--- 5 files changed, 43 insertions(+), 54 deletions(-) diff --git a/packages/swarmplot/src/compute.ts b/packages/swarmplot/src/compute.ts index 1b31e0582f..e74201622c 100644 --- a/packages/swarmplot/src/compute.ts +++ b/packages/swarmplot/src/compute.ts @@ -5,24 +5,18 @@ import uniqBy from 'lodash/uniqBy' import get from 'lodash/get' import { scaleLinear, ScaleOrdinal, scaleOrdinal } from 'd3-scale' import { forceSimulation, forceX, forceY, forceCollide, ForceX, ForceY } from 'd3-force' -import { - computeScale, - createDateNormalizer, - generateSeriesAxis, - ScaleLinear, - ScaleLinearSpec, - ScaleTime, - ScaleTimeSpec, -} from '@nivo/scales' +import { computeScale, createDateNormalizer, generateSeriesAxis } from '@nivo/scales' import { ComputedDatum, PreSimulationDatum, SizeSpec, SimulationForces, SwarmPlotLegendData, + SwarmPlotValueScaleSpec, + SwarmPlotValueScale, } from './types' -const getParsedValue = (scaleSpec: ScaleLinearSpec | ScaleTimeSpec) => { +const getParsedValue = (scaleSpec: SwarmPlotValueScaleSpec) => { if (scaleSpec.type === 'time' && scaleSpec.format !== 'native') { return createDateNormalizer(scaleSpec) as (value: T) => T } @@ -73,9 +67,9 @@ export const computeValueScale = ({ height: number axis: 'x' | 'y' getValue: (datum: RawDatum) => number | Date - scale: ScaleLinearSpec | ScaleTimeSpec + scale: SwarmPlotValueScaleSpec data: RawDatum[] -}) => { +}): SwarmPlotValueScale => { const values = data.map(getValue) if (scale.type === 'time') { @@ -84,20 +78,20 @@ export const computeValueScale = ({ ] const axes = generateSeriesAxis(series, axis, scale) - return computeScale(scale, axes, axis === 'x' ? width : height, axis) as ScaleTime< - Date | string - > + return computeScale(scale, axes, axis === 'x' ? width : height, axis) as SwarmPlotValueScale } const min = Math.min(...(values as number[])) const max = Math.max(...(values as number[])) + // the value scale can map raw values to coordinate in a linear or nonlinear manner + // but here return computeScale( scale, { all: values, min, max }, axis === 'x' ? width : height, axis - ) as ScaleLinear + ) as SwarmPlotValueScale } export const getSizeGenerator = (size: SizeSpec) => { @@ -147,7 +141,7 @@ export const computeForces = ({ forceStrength, }: { axis: 'x' | 'y' - valueScale: ScaleLinear | ScaleTime + valueScale: SwarmPlotValueScale ordinalScale: ScaleOrdinal spacing: number forceStrength: number @@ -190,13 +184,13 @@ export const computeNodes = ({ getId: (datum: RawDatum) => string layout: 'vertical' | 'horizontal' getValue: (datum: RawDatum) => number | Date - valueScale: ScaleLinear | ScaleTime + valueScale: SwarmPlotValueScale getGroup: (datum: RawDatum) => string ordinalScale: ScaleOrdinal getSize: (datum: RawDatum) => number forces: SimulationForces simulationIterations: number - valueScaleConfig: ScaleLinearSpec | ScaleTimeSpec + valueScaleConfig: SwarmPlotValueScaleSpec }) => { const config = { horizontal: ['x', 'y'], diff --git a/packages/swarmplot/src/hooks.ts b/packages/swarmplot/src/hooks.ts index b4f5b2a8d6..23a4cb57cb 100644 --- a/packages/swarmplot/src/hooks.ts +++ b/packages/swarmplot/src/hooks.ts @@ -4,7 +4,6 @@ import { usePropertyAccessor, useValueFormatter } from '@nivo/core' import { useOrdinalColorScale } from '@nivo/colors' import { AnnotationMatcher, useAnnotations } from '@nivo/annotations' import { useTooltip } from '@nivo/tooltip' -import { ScaleLinear, ScaleLinearSpec, ScaleTime, ScaleTimeSpec } from '@nivo/scales' import { computeValueScale, computeOrdinalScale, @@ -20,6 +19,8 @@ import { SizeSpec, SwarmPlotCustomLayerProps, MouseHandlers, + SwarmPlotValueScale, + SwarmPlotValueScaleSpec, } from './types' import { LegendProps } from '@nivo/legends' @@ -35,7 +36,7 @@ export const useValueScale = ({ height: number axis: 'x' | 'y' getValue: (datum: RawDatum) => number | Date - scale: ScaleLinearSpec | ScaleTimeSpec + scale: SwarmPlotValueScaleSpec data: RawDatum[] }) => useMemo( @@ -80,7 +81,7 @@ export const useForces = ({ forceStrength, }: { axis: 'x' | 'y' - valueScale: ScaleLinear | ScaleTime + valueScale: SwarmPlotValueScale ordinalScale: ScaleOrdinal spacing: number forceStrength: number @@ -327,10 +328,7 @@ export const useSwarmPlotAnnotations = ( export const useSwarmPlotLayerContext = < RawDatum, - Scale extends - | ScaleLinear - | ScaleTime - | ScaleOrdinal + Scale extends SwarmPlotValueScale | ScaleOrdinal >({ nodes, xScale, diff --git a/packages/swarmplot/src/types.ts b/packages/swarmplot/src/types.ts index 98c5d69531..b7dc95bafe 100644 --- a/packages/swarmplot/src/types.ts +++ b/packages/swarmplot/src/types.ts @@ -8,6 +8,7 @@ import { ScaleLinear, ScaleLinearSpec, ScaleLog, + ScaleLogSpec, ScaleTime, ScaleTimeSpec, TicksSpec, @@ -45,11 +46,7 @@ export type SwarmPlotLayerId = 'grid' | 'axes' | 'circles' | 'annotations' | 'me export interface SwarmPlotCustomLayerProps< RawDatum, - Scale extends - | ScaleLinear - | ScaleLog - | ScaleTime - | ScaleOrdinal = ScaleLinear + Scale extends SwarmPlotValueScale | ScaleOrdinal > { nodes: ComputedDatum[] xScale: Scale @@ -63,20 +60,12 @@ export interface SwarmPlotCustomLayerProps< export type SwarmPlotCustomLayer< RawDatum, - Scale extends - | ScaleLinear - | ScaleLog - | ScaleTime - | ScaleOrdinal = ScaleLinear + Scale extends SwarmPlotValueScale | ScaleOrdinal > = React.FC> export type SwarmPlotLayer< RawDatum, - Scale extends - | ScaleLinear - | ScaleLog - | ScaleTime - | ScaleOrdinal = ScaleLinear + Scale extends SwarmPlotValueScale | ScaleOrdinal > = SwarmPlotLayerId | SwarmPlotCustomLayer export type SizeSpec = @@ -109,6 +98,10 @@ export type SwarmPlotLegendData = { color: string } +export type SwarmPlotValueScaleSpec = ScaleLinearSpec | ScaleTimeSpec | ScaleLogSpec + +export type SwarmPlotValueScale = ScaleLinear | ScaleTime | ScaleLog + export type SwarmPlotCommonProps = { data: RawDatum[] width: number @@ -117,7 +110,7 @@ export type SwarmPlotCommonProps = { groups: string[] id: PropertyAccessor value: PropertyAccessor - valueScale: ScaleLinearSpec | ScaleTimeSpec + valueScale: SwarmPlotValueScaleSpec valueFormat: ValueFormat groupBy: PropertyAccessor size: SizeSpec @@ -140,10 +133,7 @@ export type SwarmPlotCommonProps = { debugMesh: boolean tooltip: (props: ComputedDatum) => JSX.Element annotations: AnnotationMatcher>[] - layers: SwarmPlotLayer< - RawDatum, - ScaleLinear | ScaleTime | ScaleOrdinal - >[] + layers: SwarmPlotLayer>[] legendLabel?: PropertyAccessor legends: LegendProps[] animate: boolean diff --git a/packages/swarmplot/stories/SwarmPlot.stories.tsx b/packages/swarmplot/stories/SwarmPlot.stories.tsx index 0df1e4cf3f..b5cf216b2d 100644 --- a/packages/swarmplot/stories/SwarmPlot.stories.tsx +++ b/packages/swarmplot/stories/SwarmPlot.stories.tsx @@ -3,7 +3,6 @@ import { generateSwarmPlotData } from '@nivo/generators' import { SwarmPlot } from '../src' import { SwarmPlotExtraLayers } from './SwarmPlotExtraLayers' import { SwarmPlotCustomCircle } from './SwarmPlotCustomCircle' -import { select } from '@storybook/addon-knobs' const commonProps = { width: 700, @@ -141,6 +140,11 @@ stories.add('using log scale', () => { ...datum, price: Math.max(1, datum.price), })) + const customLegendLabels: { [key: string]: string } = { + 'group A': 'A', + 'group B': 'B', + 'group C': 'C', + } return ( { tickSize: 10, tickPadding: 5, tickRotation: 0, - legend: 'groups', + legend: '', legendPosition: 'middle', legendOffset: -76, }} layout="horizontal" + legendLabel={datum => customLegendLabels[datum.group as string]} legends={[ { anchor: 'bottom', direction: 'row', itemHeight: 20, - itemWidth: 80, + itemWidth: 60, translateY: 50, }, ]} diff --git a/packages/swarmplot/stories/SwarmPlotCanvas.stories.tsx b/packages/swarmplot/stories/SwarmPlotCanvas.stories.tsx index ab90ea765f..4384a45290 100644 --- a/packages/swarmplot/stories/SwarmPlotCanvas.stories.tsx +++ b/packages/swarmplot/stories/SwarmPlotCanvas.stories.tsx @@ -64,10 +64,12 @@ stories.add('using annotations', () => ( stories.add('using log scale', () => { // ensure that dataset has price as a non-negative value - const data = commonProps.data.map(datum => ({ - ...datum, - price: Math.max(1, datum.price), - })).filter(datum => datum.group != "group G") + const data = commonProps.data + .map(datum => ({ + ...datum, + price: Math.max(1, datum.price), + })) + .filter(datum => datum.group != 'group G') return ( { }} size={7} borderWidth={0.5} - borderColor={"#222222"} + borderColor={'#222222'} enableGridY={true} axisTop={null} axisRight={null} From 89523c4d8c58071789a5d7d9221a1ab7f0a4e7fa Mon Sep 17 00:00:00 2001 From: tkonopka Date: Fri, 3 Jun 2022 13:21:04 +0100 Subject: [PATCH 5/6] removed spurious comment --- packages/swarmplot/src/compute.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/swarmplot/src/compute.ts b/packages/swarmplot/src/compute.ts index e74201622c..96fc45e9fd 100644 --- a/packages/swarmplot/src/compute.ts +++ b/packages/swarmplot/src/compute.ts @@ -84,8 +84,6 @@ export const computeValueScale = ({ const min = Math.min(...(values as number[])) const max = Math.max(...(values as number[])) - // the value scale can map raw values to coordinate in a linear or nonlinear manner - // but here return computeScale( scale, { all: values, min, max }, From 124ac73d774fe239f2e9977685649fc82d7f6bf4 Mon Sep 17 00:00:00 2001 From: tkonopka Date: Fri, 3 Jun 2022 16:08:08 +0100 Subject: [PATCH 6/6] added support for completely custom legends (ids, colors, labels) --- packages/swarmplot/src/compute.ts | 2 +- packages/swarmplot/src/hooks.ts | 5 +++-- .../swarmplot/stories/SwarmPlot.stories.tsx | 21 ++++++++++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/swarmplot/src/compute.ts b/packages/swarmplot/src/compute.ts index 96fc45e9fd..7402cccb9b 100644 --- a/packages/swarmplot/src/compute.ts +++ b/packages/swarmplot/src/compute.ts @@ -220,7 +220,7 @@ export const computeNodes = ({ } } -export const getLegendData = ({ +export const getBaseLegendData = ({ nodes, getLegendLabel, }: { diff --git a/packages/swarmplot/src/hooks.ts b/packages/swarmplot/src/hooks.ts index 23a4cb57cb..5d076bc33d 100644 --- a/packages/swarmplot/src/hooks.ts +++ b/packages/swarmplot/src/hooks.ts @@ -8,7 +8,7 @@ import { computeValueScale, computeOrdinalScale, getSizeGenerator, - getLegendData, + getBaseLegendData, computeForces, computeNodes, } from './compute' @@ -221,7 +221,8 @@ export const useSwarmPlot = ({ const legendsData: [LegendProps, SwarmPlotLegendData[]][] = useMemo( () => legends.map(legend => { - const data = getLegendData({ + if (legend.data) return [legend, legend.data as SwarmPlotLegendData[]] + const data = getBaseLegendData({ nodes: augmentedNodes, getLegendLabel, }) diff --git a/packages/swarmplot/stories/SwarmPlot.stories.tsx b/packages/swarmplot/stories/SwarmPlot.stories.tsx index b5cf216b2d..14d91ddbce 100644 --- a/packages/swarmplot/stories/SwarmPlot.stories.tsx +++ b/packages/swarmplot/stories/SwarmPlot.stories.tsx @@ -178,12 +178,31 @@ stories.add('using log scale', () => { legendLabel={datum => customLegendLabels[datum.group as string]} legends={[ { - anchor: 'bottom', + anchor: 'bottom-left', direction: 'row', itemHeight: 20, itemWidth: 60, translateY: 50, }, + { + anchor: 'bottom-right', + direction: 'row', + itemHeight: 20, + itemWidth: 80, + translateY: 50, + data: [ + { + id: 'A', + label: 'custom', + color: '#000000', + }, + { + id: 'B', + label: 'legends', + color: '#888888', + }, + ], + }, ]} /> )