Skip to content

Commit

Permalink
Merge pull request #777 from kristw/kristw--legend
Browse files Browse the repository at this point in the history
feat(legend): update scale types for vx/legend
  • Loading branch information
hshoff committed Aug 11, 2020
2 parents 2fb2aef + 4e9e122 commit 474ec7e
Show file tree
Hide file tree
Showing 13 changed files with 132 additions and 171 deletions.
6 changes: 3 additions & 3 deletions packages/vx-demo/src/sandboxes/vx-legend/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export default function Example({ events = false }: { events?: boolean }) {
return (
<div className="legends">
<LegendDemo title="Size">
<LegendSize<number> scale={sizeScale}>
<LegendSize scale={sizeScale}>
{labels =>
labels.map(label => {
const size = sizeScale(label.datum);
Expand All @@ -96,7 +96,7 @@ export default function Example({ events = false }: { events?: boolean }) {
</LegendSize>
</LegendDemo>
<LegendDemo title="Quantile">
<LegendQuantile<string> scale={quantileScale}>
<LegendQuantile scale={quantileScale}>
{labels =>
labels.map((label, i) => (
<LegendItem
Expand Down Expand Up @@ -197,7 +197,7 @@ export default function Example({ events = false }: { events?: boolean }) {
</LegendOrdinal>
</LegendDemo>
<LegendDemo title="Custom Legend">
<Legend<string, React.FC | React.ReactNode, typeof shapeScale> scale={shapeScale}>
<Legend scale={shapeScale}>
{labels => (
<div style={{ display: 'flex', flexDirection: 'row' }}>
{labels.map((label, i) => {
Expand Down
5 changes: 1 addition & 4 deletions packages/vx-legend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
29 changes: 17 additions & 12 deletions packages/vx-legend/src/legends/Legend/index.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
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';
import valueOrIdentity, { valueOrIdentityString } from '../../util/valueOrIdentity';
import labelTransformFactory from '../../util/labelTransformFactory';
import {
FlexDirection,
ScaleType,
FormattedLabel,
LabelFormatter,
LabelFormatterFactory,
LegendShape as LegendShapeType,
} from '../../types';

export type LegendProps<Datum, Output, Scale = ScaleType<Datum, Output>> = {
export type LegendProps<Scale extends AnyD3Scale> = {
/** Optional render function override. */
children?: (labels: FormattedLabel<Datum, Output>[]) => React.ReactNode;
children?: (labels: FormattedLabel<ScaleInput<Scale>, ReturnType<Scale>>[]) => 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<Scale>[];
/** Width of the legend shape. */
shapeWidth?: string | number;
/** Height of the legend shape. */
Expand All @@ -44,17 +44,21 @@ export type LegendProps<Datum, Output, Scale = ScaleType<Datum, Output>> = {
/** Flex direction of legend items. */
itemDirection?: FlexDirection;
/** Legend item fill accessor function. */
fill?: (label: FormattedLabel<Datum, Output>) => string | number | undefined;
fill?: (
label: FormattedLabel<ScaleInput<Scale>, ReturnType<Scale>>,
) => string | number | undefined;
/** Legend item size accessor function. */
size?: (label: FormattedLabel<Datum, Output>) => string | number | undefined;
size?: (
label: FormattedLabel<ScaleInput<Scale>, ReturnType<Scale>>,
) => string | number | undefined;
/** Legend shape string preset or Element or Component. */
shape?: LegendShapeType<Datum, Output>;
shape?: LegendShapeType<ScaleInput<Scale>, ReturnType<Scale>>;
/** Styles applied to legend shapes. */
shapeStyle?: (label: FormattedLabel<Datum, Output>) => React.CSSProperties;
shapeStyle?: (label: FormattedLabel<ScaleInput<Scale>, ReturnType<Scale>>) => React.CSSProperties;
/** Given a legend item and its index, returns an item label. */
labelFormat?: LabelFormatter<Datum>;
labelFormat?: LabelFormatter<ScaleInput<Scale>>;
/** Given the legend scale and labelFormatter, returns a label with datum, index, value, and label. */
labelTransform?: LabelFormatterFactory<Datum, Output, Scale>;
labelTransform?: LabelFormatterFactory<Scale>;
/** Additional props to be set on LegendLabel. */
legendLabelProps?: Partial<LegendLabelProps>;
};
Expand All @@ -63,7 +67,7 @@ const defaultStyle = {
display: 'flex',
};

export default function Legend<Datum, Output, Scale = ScaleType<Datum, Output>>({
export default function Legend<Scale extends AnyD3Scale>({
className,
style = defaultStyle,
scale,
Expand All @@ -86,13 +90,14 @@ export default function Legend<Datum, Output, Scale = ScaleType<Datum, Output>>(
legendLabelProps,
children,
...legendItemProps
}: LegendProps<Datum, Output, Scale>) {
}: LegendProps<Scale>) {
// `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 (
Expand Down
37 changes: 10 additions & 27 deletions packages/vx-legend/src/legends/Linear.tsx
Original file line number Diff line number Diff line change
@@ -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<Output> = {
steps?: number;
} & LegendProps<number, Output, ScaleLinear<number, Output>>;

export function defaultDomain<Output>({
steps = 5,
scale,
}: Pick<LegendLinearProps<Output>, '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<Scale extends AnyLinearScale> = {
steps?: number;
} & LegendProps<Scale>;

/** Linear scales map from continuous inputs to continuous outputs. */
export default function Linear<Output>({
export default function Linear<Scale extends AnyLinearScale>({
scale,
domain: inputDomain,
steps = 5,
...restProps
}: LegendLinearProps<Output>) {
}: LegendLinearProps<Scale>) {
const domain = inputDomain || defaultDomain({ steps, scale });
return (
<Legend<number, Output, ScaleLinear<number, Output>>
scale={scale}
domain={domain}
{...restProps}
/>
);
return <Legend<Scale> scale={scale} domain={domain} {...restProps} />;
}
17 changes: 7 additions & 10 deletions packages/vx-legend/src/legends/Ordinal.tsx
Original file line number Diff line number Diff line change
@@ -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<Input extends { toString(): string }, Output> = LegendProps<
string,
Output,
ScaleOrdinal<Input, Output>
>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyOrdinalScale = PickD3Scale<'ordinal', any, any>;

export type LegendOrdinalProps<Scale extends AnyOrdinalScale> = LegendProps<Scale>;

/** Ordinal scales map from strings to an Output type. */
export default function Ordinal<Input extends { toString(): string }, Output>(
props: LegendOrdinalProps<Input, Output>,
) {
return <Legend<string, Output, ScaleOrdinal<Input, Output>> {...props} />;
export default function Ordinal<Scale extends AnyOrdinalScale>(props: LegendOrdinalProps<Scale>) {
return <Legend<Scale> {...props} />;
}
36 changes: 18 additions & 18 deletions packages/vx-legend/src/legends/Quantile.tsx
Original file line number Diff line number Diff line change
@@ -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<Output> = {
type FactoryProps = {
labelDelimiter?: string;
labelTransform?: LabelFormatterFactory<number, Output, ScaleQuantile<number, Output>>;
scale: ScaleQuantile<number, Output>;
} & Omit<LegendProps<number, Output, ScaleQuantile<number, Output>>, 'scale' | 'labelTransform'>;
};

export type LegendQuantileProps<Scale extends AnyQuantileScale> = LegendProps<Scale> & FactoryProps;

function labelFormatterFactoryFactory<Output>({
function labelFormatterFactoryFactory<Scale extends AnyQuantileScale>({
labelDelimiter,
}: Pick<LegendQuantileProps<Output>, 'labelDelimiter'>): LabelFormatterFactory<
number,
Output,
ScaleQuantile<number, Output>
> {
return ({ scale, labelFormat }) => (datum: number, index: number) => {
}: FactoryProps): LabelFormatterFactory<Scale> {
return ({ scale, labelFormat }) => (datum, index) => {
const [x0, x1] = scale.invertExtent(scale(datum));
return {
extent: [x0, x1],
Expand All @@ -29,21 +29,21 @@ function labelFormatterFactoryFactory<Output>({
}

/** A Quantile scale takes a number input and returns an Output. */
export default function Quantile<Output>({
export default function Quantile<Scale extends AnyQuantileScale>({
domain: inputDomain,
scale,
labelFormat = x => x,
labelFormat = identity,
labelTransform: inputLabelTransform,
labelDelimiter = '-',
...restProps
}: LegendQuantileProps<Output>) {
}: LegendQuantileProps<Scale>) {
// 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<Output>({ labelDelimiter });
inputLabelTransform || labelFormatterFactoryFactory<Scale>({ labelDelimiter });

return (
<Legend<number, Output, ScaleQuantile<number, Output>>
<Legend<Scale>
scale={scale}
domain={domain}
labelFormat={labelFormat}
Expand Down
40 changes: 13 additions & 27 deletions packages/vx-legend/src/legends/Size.tsx
Original file line number Diff line number Diff line change
@@ -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<Datum> = {
steps?: number;
} & LegendProps<Datum, number, ScaleType<Datum, number>>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnySizeScale = D3Scale<number, any, any>;

function defaultDomain<Datum>({
steps,
scale,
}: {
steps: number;
scale: ScaleType<Datum, number>;
}) {
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<Scale extends AnySizeScale> = {
steps?: number;
} & LegendProps<Scale>;

export default function Size<Datum>({
export default function Size<Scale extends AnySizeScale>({
scale,
domain: inputDomain,
steps = 5,
labelFormat = x => x,
labelFormat = identity,
labelTransform = labelTransformFactory,
...restProps
}: LegendSizeProps<Datum>) {
}: LegendSizeProps<Scale>) {
const domain = inputDomain || defaultDomain({ steps, scale });

return (
<Legend<Datum, number, ScaleType<Datum, number>>
<Legend<Scale>
scale={scale}
domain={domain}
labelFormat={labelFormat}
Expand Down
Loading

0 comments on commit 474ec7e

Please sign in to comment.