Skip to content

Commit

Permalink
Merge pull request #779 from hshoff/chris--animated-axis
Browse files Browse the repository at this point in the history
new(vx-react-spring): add package + AnimatedAxis
  • Loading branch information
williaster committed Aug 21, 2020
2 parents 01d2a2e + 0af9877 commit ef2db11
Show file tree
Hide file tree
Showing 27 changed files with 527 additions and 169 deletions.
1 change: 1 addition & 0 deletions packages/vx-axis/Readme.md
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
Expand Up @@ -38,9 +38,6 @@
"classnames": "^2.2.5",
"prop-types": "^15.6.0"
},
"devDependencies": {
"@vx/scale": "0.0.198"
},
"peerDependencies": {
"react": "^16.3.0-0"
},
Expand Down
25 changes: 13 additions & 12 deletions packages/vx-axis/src/axis/Axis.tsx
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),
};
});

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
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 */) =>
({
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
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
@@ -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));
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
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
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
@@ -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
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
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

0 comments on commit ef2db11

Please sign in to comment.