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

new(vx-react-spring): add package + AnimatedAxis #779

Merged
merged 11 commits into from
Aug 21, 2020
Merged
1 change: 1 addition & 0 deletions packages/vx-axis/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ An axis component consists of a line with ticks, tick labels, and an axis label
interpret your graph.

You can use one of the 4 pre-made axes, or you can create your own based on the `<Axis />` element.
Note that the `@vx/react-spring` package exports an `AnimatedAxis` variant with animated ticks.

## Installation

Expand Down
3 changes: 0 additions & 3 deletions packages/vx-axis/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@
"classnames": "^2.2.5",
"prop-types": "^15.6.0"
},
"devDependencies": {
"@vx/scale": "0.0.198"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is now a dep so don't need to duplicate it

},
"peerDependencies": {
"react": "^16.3.0-0"
},
Expand Down
25 changes: 13 additions & 12 deletions packages/vx-axis/src/axis/Axis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,21 @@ export default function Axis<Scale extends AxisScale>({
horizontal,
);

const ticks = (tickValues ?? getTicks(scale, numTicks))
const filteredTickValues = (tickValues ?? getTicks(scale, numTicks))
.map((value, index) => ({ value, index }))
.filter(({ value }) => !hideZero || (value !== 0 && value !== '0'))
.map(({ value, index }) => {
const scaledValue = coerceNumber(tickPosition(value));
.filter(({ value }) => !hideZero || (value !== 0 && value !== '0'));

return {
value,
index,
from: createPoint({ x: scaledValue, y: 0 }, horizontal),
to: createPoint({ x: scaledValue, y: tickLength * tickSign }, horizontal),
formattedValue: format(value, index),
};
});
const ticks = filteredTickValues.map(({ value, index }) => {
const scaledValue = coerceNumber(tickPosition(value));

return {
value,
index,
from: createPoint({ x: scaledValue, y: 0 }, horizontal),
to: createPoint({ x: scaledValue, y: tickLength * tickSign }, horizontal),
formattedValue: format(value, index, filteredTickValues),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have been wanting to add this for a while (will update the PR description): we now pass all ticks as a third argument (common d3 pattern) so that users can determine if a tick is first/last, etc. with index only you couldn't ever determine this because you may not know the exact # ticks unless you specify tickValues

};
});

return (
<Group className={cx('vx-axis', axisClassName)} top={top} left={left}>
Expand Down
13 changes: 8 additions & 5 deletions packages/vx-axis/src/axis/AxisBottom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import Axis from './Axis';
import Orientation from '../constants/orientation';
import { SharedAxisProps, AxisScale } from '../types';

export default function AxisBottom<Scale extends AxisScale>({
axisClassName,
labelOffset = 8,
tickLabelProps = (/** tickValue, index */) => ({
export const bottomTickLabelProps = (/** tickValue, index */) =>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exporting these so that they could be used outside the component. tick label props are the primary differences between axis configurations

({
dy: '0.25em',
fill: '#222',
fontFamily: 'Arial',
fontSize: 10,
textAnchor: 'middle',
}),
} as const);

export default function AxisBottom<Scale extends AxisScale>({
axisClassName,
labelOffset = 8,
tickLabelProps = bottomTickLabelProps,
tickLength = 8,
...restProps
}: SharedAxisProps<Scale>) {
Expand Down
13 changes: 8 additions & 5 deletions packages/vx-axis/src/axis/AxisLeft.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ import Axis from './Axis';
import Orientation from '../constants/orientation';
import { SharedAxisProps, AxisScale } from '../types';

export default function AxisLeft<Scale extends AxisScale>({
axisClassName,
labelOffset = 36,
tickLabelProps = (/** tickValue, index */) => ({
export const leftTickLabelProps = (/** tickValue, index */) =>
({
dx: '-0.25em',
dy: '0.25em',
fill: '#222',
fontFamily: 'Arial',
fontSize: 10,
textAnchor: 'end',
}),
} as const);

export default function AxisLeft<Scale extends AxisScale>({
axisClassName,
labelOffset = 36,
tickLabelProps = leftTickLabelProps,
tickLength = 8,
...restProps
}: SharedAxisProps<Scale>) {
Expand Down
58 changes: 21 additions & 37 deletions packages/vx-axis/src/axis/AxisRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React from 'react';
import cx from 'classnames';
import { Line } from '@vx/shape';
import { Group } from '@vx/group';
import { Text } from '@vx/text';

import { TextProps } from '@vx/text/lib/Text';
import Orientation from '../constants/orientation';
import getLabelTransform from '../utils/getLabelTransform';
import { AxisRendererProps, AxisScale } from '../types';
import Ticks from './Ticks';

const defaultTextProps: Partial<TextProps> = {
textAnchor: 'middle',
Expand All @@ -30,51 +29,36 @@ export default function AxisRenderer<Scale extends AxisScale>({
orientation,
scale,
stroke = '#222',
strokeWidth = 1,
strokeDasharray,
strokeWidth = 1,
tickClassName,
tickComponent,
tickLabelProps = (/** tickValue, index */) => defaultTextProps,
tickLength,
tickStroke = '#222',
tickTransform,
ticks,
ticksComponent = Ticks,
}: AxisRendererProps<Scale>) {
let tickLabelFontSize = 10; // track the max tick label size to compute label offset

// compute the max tick label size to compute label offset
const allTickLabelProps = ticks.map(({ value, index }) => tickLabelProps(value, index));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the jank logic I alluded to above. The label and ticks were previously coupled with this, so I updated the logic to pass an array of tickLabelProps to ticksComponent.

Might be worth a breaking change to remove this?

const maxTickLabelFontSize = Math.max(
10,
...allTickLabelProps.map(props => (typeof props.fontSize === 'number' ? props.fontSize : 0)),
);
return (
<>
{ticks.map(({ value, index, from, to, formattedValue }) => {
const tickLabelPropsObj = tickLabelProps(value, index);
tickLabelFontSize = Math.max(
tickLabelFontSize,
(typeof tickLabelPropsObj.fontSize === 'number' && tickLabelPropsObj.fontSize) || 0,
);

const tickYCoord =
to.y + (horizontal && orientation !== Orientation.top ? tickLabelFontSize : 0);

return (
<Group
key={`vx-tick-${value}-${index}`}
className={cx('vx-axis-tick', tickClassName)}
transform={tickTransform}
>
{!hideTicks && <Line from={from} to={to} stroke={tickStroke} strokeLinecap="square" />}
{tickComponent ? (
tickComponent({
...tickLabelPropsObj,
x: to.x,
y: tickYCoord,
formattedValue,
})
) : (
<Text x={to.x} y={tickYCoord} {...tickLabelPropsObj}>
{formattedValue}
</Text>
)}
</Group>
);
{ticksComponent({
hideTicks,
horizontal,
orientation,
scale,
tickClassName,
tickComponent,
tickLabelProps: allTickLabelProps,
tickStroke,
tickTransform,
ticks,
})}

{!hideAxisLine && (
Expand All @@ -96,7 +80,7 @@ export default function AxisRenderer<Scale extends AxisScale>({
labelProps,
orientation,
range: scale.range(),
tickLabelFontSize,
tickLabelFontSize: maxTickLabelFontSize,
tickLength,
})}
{...labelProps}
Expand Down
13 changes: 8 additions & 5 deletions packages/vx-axis/src/axis/AxisRight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ import { SharedAxisProps, AxisScale } from '../types';

export type AxisRightProps<Scale extends AxisScale> = SharedAxisProps<Scale>;

export default function AxisRight<Scale extends AxisScale>({
axisClassName,
labelOffset = 36,
tickLabelProps = (/** tickValue, index */) => ({
export const rightTickLabelProps = (/** tickValue, index */) =>
({
dx: '0.25em',
dy: '0.25em',
fill: '#222',
fontFamily: 'Arial',
fontSize: 10,
textAnchor: 'start',
}),
} as const);

export default function AxisRight<Scale extends AxisScale>({
axisClassName,
labelOffset = 36,
tickLabelProps = rightTickLabelProps,
tickLength = 8,
...restProps
}: AxisRightProps<Scale>) {
Expand Down
15 changes: 9 additions & 6 deletions packages/vx-axis/src/axis/AxisTop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ import { SharedAxisProps, AxisScale } from '../types';

export type AxisTopProps<Scale extends AxisScale> = SharedAxisProps<Scale>;

export default function AxisTop<Scale extends AxisScale>({
axisClassName,
labelOffset = 8,
tickLabelProps = (/** tickValue, index */) => ({
dy: '-0.25em',
export const topTickLabelProps = (/** tickValue, index */) =>
({
dy: '-0.75em',
fill: '#222',
fontFamily: 'Arial',
fontSize: 10,
textAnchor: 'middle',
}),
} as const);

export default function AxisTop<Scale extends AxisScale>({
axisClassName,
labelOffset = 8,
tickLabelProps = topTickLabelProps,
tickLength = 8,
...restProps
}: AxisTopProps<Scale>) {
Expand Down
53 changes: 53 additions & 0 deletions packages/vx-axis/src/axis/Ticks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import cx from 'classnames';
import { Line } from '@vx/shape';
import { Group } from '@vx/group';
import { Text } from '@vx/text';

import Orientation from '../constants/orientation';
import { TicksRendererProps, AxisScale } from '../types';

export default function Ticks<Scale extends AxisScale>({
hideTicks,
horizontal,
orientation,
tickClassName,
tickComponent,
tickLabelProps: allTickLabelProps,
tickStroke = '#222',
tickTransform,
ticks,
}: TicksRendererProps<Scale>) {
return ticks.map(({ value, index, from, to, formattedValue }) => {
const tickLabelProps = allTickLabelProps[index] ?? {};
const tickLabelFontSize = Math.max(
10,
(typeof tickLabelProps.fontSize === 'number' && tickLabelProps.fontSize) || 0,
);

const tickYCoord =
to.y + (horizontal && orientation !== Orientation.top ? tickLabelFontSize : 0);

return (
<Group
key={`vx-tick-${value}-${index}`}
className={cx('vx-axis-tick', tickClassName)}
transform={tickTransform}
>
{!hideTicks && <Line from={from} to={to} stroke={tickStroke} strokeLinecap="square" />}
{tickComponent ? (
tickComponent({
...tickLabelProps,
x: to.x,
y: tickYCoord,
formattedValue,
})
) : (
<Text x={to.x} y={tickYCoord} {...tickLabelProps}>
{formattedValue}
</Text>
)}
</Group>
);
});
}
41 changes: 32 additions & 9 deletions packages/vx-axis/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export type AxisScale<Output extends AxisScaleOutput = AxisScaleOutput> =

type FormattedValue = string | undefined;

export type TickFormatter<T> = (value: T, index: number) => FormattedValue;
export type TickFormatter<T> = (
value: T,
index: number,
values: { value: T; index: number }[],
) => FormattedValue;

export type TickLabelProps<T> = (value: T, index: number) => Partial<TextProps>;

Expand All @@ -23,6 +27,21 @@ export type TickRendererProps = Partial<TextProps> & {
formattedValue: FormattedValue;
};

export type TicksRendererProps<Scale extends AxisScale> = {
tickLabelProps: Partial<TextProps>[];
} & Pick<
AxisRendererProps<Scale>,
| 'hideTicks'
| 'horizontal'
| 'orientation'
| 'scale'
| 'tickClassName'
| 'tickComponent'
| 'tickStroke'
| 'tickTransform'
| 'ticks'
>;

interface CommonProps<Scale extends AxisScale> {
/** The class name applied to the axis line element. */
axisLineClassName?: string;
Expand Down Expand Up @@ -54,8 +73,10 @@ interface CommonProps<Scale extends AxisScale> {
strokeDasharray?: string;
/** The class name applied to each tick group. */
tickClassName?: string;
/** Override the component used to render tick labels (instead of <Text /> from @vx/text) */
/** Override the component used to render tick labels (instead of <Text /> from @vx/text). */
tickComponent?: (tickRendererProps: TickRendererProps) => React.ReactNode;
/** Override the component used to render all tick lines and labels. */
ticksComponent?: (tickRendererProps: TicksRendererProps<Scale>) => React.ReactNode;
/** A [d3 formatter](https://github.com/d3/d3-scale/blob/master/README.md#continuous_tickFormat) for the tick text. */
tickFormat: TickFormatter<ScaleInput<Scale>>;
/** A function that returns props for a given tick label. */
Expand All @@ -73,6 +94,14 @@ interface Point {
y: number;
}

export type ComputedTick<Scale extends AxisScale> = {
value: ScaleInput<Scale>;
index: number;
from: Point;
to: Point;
formattedValue: FormattedValue;
};

export type AxisRendererProps<Scale extends AxisScale> = CommonProps<Scale> & {
/** Start point of the axis line */
axisFromPoint: Point;
Expand All @@ -87,13 +116,7 @@ export type AxisRendererProps<Scale extends AxisScale> = CommonProps<Scale> & {
/** Axis coordinate sign, -1 for left or top orientation. */
tickSign: 1 | -1;
/** Computed ticks with positions and formatted value */
ticks: {
value: ScaleInput<Scale>;
index: number;
from: Point;
to: Point;
formattedValue: FormattedValue;
}[];
ticks: ComputedTick<Scale>[];
};

export type SharedAxisProps<Scale extends AxisScale> = Partial<CommonProps<Scale>> & {
Expand Down
1 change: 1 addition & 0 deletions packages/vx-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@vx/network": "0.0.198",
"@vx/pattern": "0.0.198",
"@vx/point": "0.0.198",
"@vx/react-spring": "0.0.198",
"@vx/responsive": "0.0.198",
"@vx/scale": "0.0.198",
"@vx/shape": "0.0.198",
Expand Down
Loading