diff --git a/packages/vx-demo/src/sandboxes/vx-legend/Example.tsx b/packages/vx-demo/src/sandboxes/vx-legend/Example.tsx index 2a68493e1..1dffd2d2a 100644 --- a/packages/vx-demo/src/sandboxes/vx-legend/Example.tsx +++ b/packages/vx-demo/src/sandboxes/vx-legend/Example.tsx @@ -71,7 +71,7 @@ export default function Example({ events = false }: { events?: boolean }) { return (
- scale={sizeScale}> + {labels => labels.map(label => { const size = sizeScale(label.datum); @@ -96,7 +96,7 @@ export default function Example({ events = false }: { events?: boolean }) { - scale={quantileScale}> + {labels => labels.map((label, i) => ( - scale={shapeScale}> + {labels => (
{labels.map((label, i) => { diff --git a/packages/vx-legend/package.json b/packages/vx-legend/package.json index e32f7571d..2ddbe2730 100644 --- a/packages/vx-legend/package.json +++ b/packages/vx-legend/package.json @@ -35,13 +35,10 @@ }, "dependencies": { "@types/classnames": "^2.2.9", - "@types/d3-scale": "^2.1.1", "@types/react": "*", "@vx/group": "0.0.198", + "@vx/scale": "0.0.198", "classnames": "^2.2.5", "prop-types": "^15.5.10" - }, - "devDependencies": { - "@vx/scale": "0.0.198" } } diff --git a/packages/vx-legend/src/legends/Legend/index.tsx b/packages/vx-legend/src/legends/Legend/index.tsx index 247589511..7fae77b47 100644 --- a/packages/vx-legend/src/legends/Legend/index.tsx +++ b/packages/vx-legend/src/legends/Legend/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import cx from 'classnames'; +import { AnyD3Scale, ScaleInput } from '@vx/scale'; import LegendItem from './LegendItem'; import LegendLabel, { LegendLabelProps } from './LegendLabel'; import LegendShape from './LegendShape'; @@ -7,22 +8,21 @@ import valueOrIdentity, { valueOrIdentityString } from '../../util/valueOrIdenti import labelTransformFactory from '../../util/labelTransformFactory'; import { FlexDirection, - ScaleType, FormattedLabel, LabelFormatter, LabelFormatterFactory, LegendShape as LegendShapeType, } from '../../types'; -export type LegendProps> = { +export type LegendProps = { /** Optional render function override. */ - children?: (labels: FormattedLabel[]) => React.ReactNode; + children?: (labels: FormattedLabel, ReturnType>[]) => React.ReactNode; /** Classname to be applied to legend container. */ className?: string; /** Styles to be applied to the legend container. */ style?: React.CSSProperties; /** Legend domain. */ - domain?: Datum[]; + domain?: ScaleInput[]; /** Width of the legend shape. */ shapeWidth?: string | number; /** Height of the legend shape. */ @@ -44,17 +44,21 @@ export type LegendProps> = { /** Flex direction of legend items. */ itemDirection?: FlexDirection; /** Legend item fill accessor function. */ - fill?: (label: FormattedLabel) => string | number | undefined; + fill?: ( + label: FormattedLabel, ReturnType>, + ) => string | number | undefined; /** Legend item size accessor function. */ - size?: (label: FormattedLabel) => string | number | undefined; + size?: ( + label: FormattedLabel, ReturnType>, + ) => string | number | undefined; /** Legend shape string preset or Element or Component. */ - shape?: LegendShapeType; + shape?: LegendShapeType, ReturnType>; /** Styles applied to legend shapes. */ - shapeStyle?: (label: FormattedLabel) => React.CSSProperties; + shapeStyle?: (label: FormattedLabel, ReturnType>) => React.CSSProperties; /** Given a legend item and its index, returns an item label. */ - labelFormat?: LabelFormatter; + labelFormat?: LabelFormatter>; /** Given the legend scale and labelFormatter, returns a label with datum, index, value, and label. */ - labelTransform?: LabelFormatterFactory; + labelTransform?: LabelFormatterFactory; /** Additional props to be set on LegendLabel. */ legendLabelProps?: Partial; }; @@ -63,7 +67,7 @@ const defaultStyle = { display: 'flex', }; -export default function Legend>({ +export default function Legend({ className, style = defaultStyle, scale, @@ -86,13 +90,14 @@ export default function Legend>( legendLabelProps, children, ...legendItemProps -}: LegendProps) { +}: LegendProps) { // `Scale extends ScaleType` constraint is tricky // could consider removing `scale` altogether in the future and making `domain: Datum[]` required // @ts-ignore doesn't like `.domain()` const domain = inputDomain || (('domain' in scale ? scale.domain() : []) as Datum[]); const labelFormatter = labelTransform({ scale, labelFormat }); const labels = domain.map(labelFormatter); + // eslint-disable-next-line react/jsx-no-useless-fragment if (children) return <>{children(labels)}; return ( diff --git a/packages/vx-legend/src/legends/Linear.tsx b/packages/vx-legend/src/legends/Linear.tsx index f6a9caea0..0e0a6e2ff 100644 --- a/packages/vx-legend/src/legends/Linear.tsx +++ b/packages/vx-legend/src/legends/Linear.tsx @@ -1,39 +1,22 @@ import React from 'react'; +import { PickD3Scale } from '@vx/scale'; import Legend, { LegendProps } from './Legend'; -import { ScaleLinear } from '../types'; +import defaultDomain from '../util/defaultDomain'; -export type LegendLinearProps = { - steps?: number; -} & LegendProps>; - -export function defaultDomain({ - steps = 5, - scale, -}: Pick, 'steps' | 'scale'>) { - const domain = scale.domain(); - const start = domain[0]; - const end = domain[domain.length - 1]; - const step = (end - start) / (steps - 1); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyLinearScale = PickD3Scale<'linear', any>; - return new Array(steps).fill(1).reduce((acc, cur, i) => { - acc.push(start + i * step); - return acc; - }, []); -} +export type LegendLinearProps = { + steps?: number; +} & LegendProps; /** Linear scales map from continuous inputs to continuous outputs. */ -export default function Linear({ +export default function Linear({ scale, domain: inputDomain, steps = 5, ...restProps -}: LegendLinearProps) { +}: LegendLinearProps) { const domain = inputDomain || defaultDomain({ steps, scale }); - return ( - > - scale={scale} - domain={domain} - {...restProps} - /> - ); + return scale={scale} domain={domain} {...restProps} />; } diff --git a/packages/vx-legend/src/legends/Ordinal.tsx b/packages/vx-legend/src/legends/Ordinal.tsx index aa5e2e502..7856fc266 100644 --- a/packages/vx-legend/src/legends/Ordinal.tsx +++ b/packages/vx-legend/src/legends/Ordinal.tsx @@ -1,16 +1,13 @@ import React from 'react'; +import { PickD3Scale } from '@vx/scale'; import Legend, { LegendProps } from './Legend'; -import { ScaleOrdinal } from '../types'; -export type LegendOrdinalProps = LegendProps< - string, - Output, - ScaleOrdinal ->; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyOrdinalScale = PickD3Scale<'ordinal', any, any>; + +export type LegendOrdinalProps = LegendProps; /** Ordinal scales map from strings to an Output type. */ -export default function Ordinal( - props: LegendOrdinalProps, -) { - return > {...props} />; +export default function Ordinal(props: LegendOrdinalProps) { + return {...props} />; } diff --git a/packages/vx-legend/src/legends/Quantile.tsx b/packages/vx-legend/src/legends/Quantile.tsx index 1b8e47ae6..34cf980ef 100644 --- a/packages/vx-legend/src/legends/Quantile.tsx +++ b/packages/vx-legend/src/legends/Quantile.tsx @@ -1,22 +1,22 @@ import React from 'react'; - +import { PickD3Scale } from '@vx/scale'; import Legend, { LegendProps } from './Legend'; -import { LabelFormatterFactory, ScaleQuantile } from '../types'; +import { LabelFormatterFactory } from '../types'; +import identity from '../util/identity'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyQuantileScale = PickD3Scale<'quantile', any>; -export type LegendQuantileProps = { +type FactoryProps = { labelDelimiter?: string; - labelTransform?: LabelFormatterFactory>; - scale: ScaleQuantile; -} & Omit>, 'scale' | 'labelTransform'>; +}; + +export type LegendQuantileProps = LegendProps & FactoryProps; -function labelFormatterFactoryFactory({ +function labelFormatterFactoryFactory({ labelDelimiter, -}: Pick, 'labelDelimiter'>): LabelFormatterFactory< - number, - Output, - ScaleQuantile -> { - return ({ scale, labelFormat }) => (datum: number, index: number) => { +}: FactoryProps): LabelFormatterFactory { + return ({ scale, labelFormat }) => (datum, index) => { const [x0, x1] = scale.invertExtent(scale(datum)); return { extent: [x0, x1], @@ -29,21 +29,21 @@ function labelFormatterFactoryFactory({ } /** A Quantile scale takes a number input and returns an Output. */ -export default function Quantile({ +export default function Quantile({ domain: inputDomain, scale, - labelFormat = x => x, + labelFormat = identity, labelTransform: inputLabelTransform, labelDelimiter = '-', ...restProps -}: LegendQuantileProps) { +}: LegendQuantileProps) { // transform range into input values because it may contain more elements const domain = inputDomain || scale.range().map(output => scale.invertExtent(output)[0]); const labelTransform = - inputLabelTransform || labelFormatterFactoryFactory({ labelDelimiter }); + inputLabelTransform || labelFormatterFactoryFactory({ labelDelimiter }); return ( - > + scale={scale} domain={domain} labelFormat={labelFormat} diff --git a/packages/vx-legend/src/legends/Size.tsx b/packages/vx-legend/src/legends/Size.tsx index 8cf8581b3..c28e649c7 100644 --- a/packages/vx-legend/src/legends/Size.tsx +++ b/packages/vx-legend/src/legends/Size.tsx @@ -1,43 +1,29 @@ import React from 'react'; +import { D3Scale } from '@vx/scale'; import Legend, { LegendProps } from './Legend'; -import { ScaleType } from '../types'; import labelTransformFactory from '../util/labelTransformFactory'; +import defaultDomain from '../util/defaultDomain'; +import identity from '../util/identity'; -export type LegendSizeProps = { - steps?: number; -} & LegendProps>; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnySizeScale = D3Scale; -function defaultDomain({ - steps, - scale, -}: { - steps: number; - scale: ScaleType; -}) { - const domain = scale.domain(); - const start = domain[0]; - const end = domain[domain.length - 1]; - if (typeof start === 'number' && typeof end === 'number') { - const step = (end - start) / (steps - 1); - return new Array(steps).fill(1).reduce((acc, cur, i) => { - acc.push(start + i * step); - return acc; - }, []); - } - return []; -} +export type LegendSizeProps = { + steps?: number; +} & LegendProps; -export default function Size({ +export default function Size({ scale, domain: inputDomain, steps = 5, - labelFormat = x => x, + labelFormat = identity, labelTransform = labelTransformFactory, ...restProps -}: LegendSizeProps) { +}: LegendSizeProps) { const domain = inputDomain || defaultDomain({ steps, scale }); + return ( - > + scale={scale} domain={domain} labelFormat={labelFormat} diff --git a/packages/vx-legend/src/legends/Threshold.tsx b/packages/vx-legend/src/legends/Threshold.tsx index 2d88564ae..e73632359 100644 --- a/packages/vx-legend/src/legends/Threshold.tsx +++ b/packages/vx-legend/src/legends/Threshold.tsx @@ -1,35 +1,39 @@ import React from 'react'; +import { PickD3Scale, ScaleInput } from '@vx/scale'; import Legend, { LegendProps } from './Legend'; -import { StringNumberDate, ScaleThreshold, LabelFormatterFactory } from '../types'; +import { LabelFormatterFactory } from '../types'; +import identity from '../util/identity'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyThresholdScale = PickD3Scale<'threshold', any, any, any>; const formatZero = (label: unknown) => (label === 0 ? '0' : label || ''); -export type LegendThresholdProps = LegendProps< - Datum, - Output, - ScaleThreshold -> & { +type TransformProps = { labelDelimiter?: string; labelLower?: string; labelUpper?: string; - labelTransform?: LabelFormatterFactory>; }; +export type LegendThresholdProps = LegendProps & + TransformProps & { + labelTransform?: LabelFormatterFactory; + }; + /** Default transform implicitly assumes that Datum is of type number. */ -function defaultTransform({ +function defaultTransform({ labelDelimiter, labelLower, labelUpper, -}: Pick< - LegendThresholdProps, - 'labelDelimiter' | 'labelLower' | 'labelUpper' ->): LabelFormatterFactory> { +}: TransformProps): LabelFormatterFactory { return ({ scale, labelFormat }) => { const scaleRange = scale.range(); const scaleDomain = scale.domain(); + type Datum = ScaleInput; + return (d, i) => { - const [d0, d1]: [Datum | undefined, Datum | undefined] = + const [d0, d1] = scaleRange.length >= i ? scale.invertExtent(scaleRange[i]) : [undefined, undefined]; let delimiter = ` ${labelDelimiter} `; @@ -48,13 +52,13 @@ function defaultTransform({ } else if (typeof d0 === 'number' && d1 == null) { // upper threshold e.g., [number, undefined] delimiter = labelUpper || delimiter; - value = d0 + (scaleDomain[1] as number); // x0,x1 are from the domain, so the domain is numeric if d0 is + value = d0 + scaleDomain[1]; // x0,x1 are from the domain, so the domain is numeric if d0 is text = `${delimiter}${formatZero(labelFormat(d0, i))}`; } return { extent: [d0, d1], - value: scale((value as Datum) || d), + value: scale(value || d), text, datum: d, index: i, @@ -63,33 +67,32 @@ function defaultTransform({ }; } -export default function Threshold({ +export default function Threshold({ scale, domain: inputDomain, - labelFormat = (d: Datum) => d, + labelFormat = identity, labelTransform: inputLabelTransform, labelDelimiter = 'to', labelLower = 'Less than ', labelUpper = 'More than ', ...restProps -}: LegendThresholdProps) { +}: LegendThresholdProps) { // d3 docs specify that for n values in a domain, there should be n+1 values in the range // https://github.com/d3/d3-scale#threshold_domain // therefore if a domain is not specified we transform the range into input values // because it should contain more elements - const domain = - inputDomain || (scale.range().map(output => scale.invertExtent(output)[0]) as Datum[]); + const domain = inputDomain || scale.range().map(output => scale.invertExtent(output)[0]); const labelTransform = inputLabelTransform || - defaultTransform({ + defaultTransform({ labelDelimiter, labelLower, labelUpper, }); return ( - > + scale={scale} domain={domain} labelFormat={labelFormat} diff --git a/packages/vx-legend/src/types/index.ts b/packages/vx-legend/src/types/index.ts index 3cbe21db3..5301b1d5c 100644 --- a/packages/vx-legend/src/types/index.ts +++ b/packages/vx-legend/src/types/index.ts @@ -1,35 +1,9 @@ -// eslint doesn't know about @types/d3-scale -// eslint-disable-next-line import/no-extraneous-dependencies -import * as d3Scale from 'd3-scale'; +import { AnyD3Scale, ScaleInput } from '@vx/scale'; -export type StringNumberDate = string | number | Date; - -export type ScaleBand = d3Scale.ScaleBand; -export type ScaleLinear<_ImplicitNumberInput, Output> = d3Scale.ScaleLinear; -export type ScaleOrdinal = d3Scale.ScaleOrdinal; -export type ScaleQuantile<_ImplicitNumberInput, Output> = d3Scale.ScaleQuantile; -export type ScaleThreshold = d3Scale.ScaleThreshold< - Input, - Output ->; - -export type ScaleType = Input extends StringNumberDate - ? - | ScaleThreshold // StringNumberDate needed for ScaleThreshold only - | ScaleLinear - | ScaleOrdinal - | ScaleBand - | ScaleQuantile - : - | ScaleLinear - | ScaleOrdinal - | ScaleBand - | ScaleQuantile; - -export type LabelFormatterFactory> = (args: { +export type LabelFormatterFactory = (args: { scale: Scale; - labelFormat: LabelFormatter; -}) => ItemTransformer; + labelFormat: LabelFormatter>; +}) => ItemTransformer, ReturnType>; export type LabelFormatter = ( item: Datum, diff --git a/packages/vx-legend/src/util/defaultDomain.ts b/packages/vx-legend/src/util/defaultDomain.ts new file mode 100644 index 000000000..bb2ac5be0 --- /dev/null +++ b/packages/vx-legend/src/util/defaultDomain.ts @@ -0,0 +1,21 @@ +import { D3Scale } from '@vx/scale'; + +export default function defaultDomain>({ + steps = 5, + scale, +}: { + steps: number; + scale: Scale; +}) { + const domain = scale.domain(); + const start = domain[0]; + const end = domain[domain.length - 1]; + if (typeof start === 'number' && typeof end === 'number') { + const step = (end - start) / (steps - 1); + return new Array(steps).fill(1).reduce((acc, cur, i) => { + acc.push(start + i * step); + return acc; + }, []); + } + return []; +} diff --git a/packages/vx-legend/src/util/identity.ts b/packages/vx-legend/src/util/identity.ts new file mode 100644 index 000000000..5538f4717 --- /dev/null +++ b/packages/vx-legend/src/util/identity.ts @@ -0,0 +1,3 @@ +export default function identity(x: T) { + return x; +} diff --git a/packages/vx-legend/src/util/labelTransformFactory.ts b/packages/vx-legend/src/util/labelTransformFactory.ts index 199f747ba..3579344ed 100644 --- a/packages/vx-legend/src/util/labelTransformFactory.ts +++ b/packages/vx-legend/src/util/labelTransformFactory.ts @@ -1,13 +1,14 @@ -import { LabelFormatter, ScaleType, ItemTransformer } from '../types'; +import { LabelFormatter, ItemTransformer } from '../types'; +import { AnyD3Scale, ScaleInput } from '../../../vx-scale/lib'; /** Returns a function which takes a Datum and index as input, and returns a formatted label object. */ -export default function labelTransformFactory>({ +export default function labelTransformFactory({ scale, labelFormat, }: { scale: Scale; - labelFormat: LabelFormatter; -}): ItemTransformer { + labelFormat: LabelFormatter>; +}): ItemTransformer, ReturnType> { return (d, i) => ({ datum: d, index: i, diff --git a/packages/vx-legend/test/scales.test.tsx b/packages/vx-legend/test/scales.test.tsx index 374f2691a..539f0230c 100644 --- a/packages/vx-legend/test/scales.test.tsx +++ b/packages/vx-legend/test/scales.test.tsx @@ -10,7 +10,6 @@ import { LegendThreshold, LegendQuantile, } from '../src'; -import { ScaleBand, ScaleOrdinal, ScaleThreshold, ScaleQuantile } from '../src/types'; describe('Legend scales', () => { it('should render with scaleLinear', () => { @@ -20,7 +19,7 @@ describe('Legend scales', () => { }); expect(() => shallow()).not.toThrow(); - expect(() => shallow( scale={linearScale} />)).not.toThrow(); + expect(() => shallow()).not.toThrow(); expect(() => shallow()).not.toThrow(); }); @@ -31,9 +30,7 @@ describe('Legend scales', () => { }); expect(() => shallow()).not.toThrow(); - expect(() => - shallow(> scale={ordinalScale} />), - ).not.toThrow(); + expect(() => shallow()).not.toThrow(); }); it('should render with scaleBand', () => { @@ -42,9 +39,7 @@ describe('Legend scales', () => { range: [1, 10], }); - expect(() => - shallow(> scale={bandScale} />), - ).not.toThrow(); + expect(() => shallow()).not.toThrow(); }); it('should render with scaleThreshold', () => { @@ -54,9 +49,7 @@ describe('Legend scales', () => { }); expect(() => shallow()).not.toThrow(); - expect(() => - shallow(> scale={thresholdScale} />), - ).not.toThrow(); + expect(() => shallow()).not.toThrow(); }); it('should render with scaleQuantile', () => { @@ -66,8 +59,6 @@ describe('Legend scales', () => { }); expect(() => shallow()).not.toThrow(); - expect(() => - shallow(> scale={quantileScale} />), - ).not.toThrow(); + expect(() => shallow()).not.toThrow(); }); });