Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(legend): update scale types for vx/legend #777

Merged
merged 2 commits into from
Aug 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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