Examples by package
{vxPackages.map(vxPackage => (
-
+
{`@vx/${vxPackage}`}
diff --git a/packages/vx-demo/src/components/PackageList.tsx b/packages/vx-demo/src/components/PackageList.tsx
index 073e977dc..ced0cffc6 100644
--- a/packages/vx-demo/src/components/PackageList.tsx
+++ b/packages/vx-demo/src/components/PackageList.tsx
@@ -114,6 +114,12 @@ export default function PackageList({
{!compact &&
Visualize nodes and links between them
}
+
+
+ @vx/react-spring
+
+ {!compact && Animated vx primitives
}
+
@vx/stats
diff --git a/packages/vx-demo/src/pages/docs/react-spring.tsx b/packages/vx-demo/src/pages/docs/react-spring.tsx
new file mode 100644
index 000000000..f8bb8721f
--- /dev/null
+++ b/packages/vx-demo/src/pages/docs/react-spring.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import ReactSpringReadme from '!!raw-loader!../../../../vx-react-spring/README.md';
+import AnimatedAxis from '../../../../vx-react-spring/src/axis/AnimatedAxis';
+import DocPage from '../../components/DocPage';
+import AxisTile from '../../components/Gallery/AxisTile';
+
+const components = [AnimatedAxis];
+
+const examples = [AxisTile];
+
+export default () => (
+
+);
diff --git a/packages/vx-demo/src/sandboxes/vx-axis/Example.tsx b/packages/vx-demo/src/sandboxes/vx-axis/Example.tsx
index 9ed6f95c7..58b729d75 100644
--- a/packages/vx-demo/src/sandboxes/vx-axis/Example.tsx
+++ b/packages/vx-demo/src/sandboxes/vx-axis/Example.tsx
@@ -1,9 +1,10 @@
-import React from 'react';
+import React, { useState } from 'react';
import AreaClosed from '@vx/shape/lib/shapes/AreaClosed';
import { Grid } from '@vx/grid';
import { curveMonotoneX } from '@vx/curve';
-import { scaleUtc, scaleLinear, scaleLog, scaleBand, ScaleInput } from '@vx/scale';
-import { AxisBottom, SharedAxisProps, AxisScale } from '@vx/axis';
+import { scaleUtc, scaleLinear, scaleLog, scaleBand, ScaleInput, coerceNumber } from '@vx/scale';
+import { Orientation, SharedAxisProps, AxisScale } from '@vx/axis';
+import { AnimatedAxis } from '@vx/react-spring';
import { LinearGradient } from '@vx/gradient';
import { timeFormat } from 'd3-time-format';
@@ -20,15 +21,26 @@ const margin = {
left: 50,
};
+const getMinMax = (vals: (number | { valueOf(): number })[]) => {
+ const numericVals = vals.map(coerceNumber);
+ return [Math.min(...numericVals), Math.max(...numericVals)];
+};
+
export type AxisProps = {
width: number;
height: number;
+ showControls?: boolean;
};
-export default function Example({ width: outerWidth = 800, height: outerHeight = 800 }: AxisProps) {
+export default function Example({
+ width: outerWidth = 800,
+ height: outerHeight = 800,
+ showControls = true,
+}: AxisProps) {
// in svg, margin is subtracted from total width/height
const width = outerWidth - margin.left - margin.right;
const height = outerHeight - margin.top - margin.bottom;
+ const [dataToggle, setDataToggle] = useState(true);
if (width < 10) return null;
@@ -36,44 +48,57 @@ export default function Example({ width: outerWidth = 800, height: outerHeight =
values: ScaleInput[];
}
+ // toggle between two value ranges to demo animation
+ const linearValues = dataToggle ? [0, 2, 4, 6, 8, 10] : [6, 8, 10, 12];
+ const bandValues = dataToggle ? ['a', 'b', 'c', 'd'] : ['d', 'c', 'b', 'a'];
+ const timeValues = dataToggle
+ ? [new Date('2020-01-01'), new Date('2020-02-01')]
+ : [new Date('2020-02-01'), new Date('2020-03-01')];
+ const logValues = dataToggle ? [1, 10, 100, 1000, 10000] : [0.0001, 0.001, 0.1, 1, 10, 100];
+
const axes: AxisDemoProps>[] = [
{
scale: scaleLinear({
- domain: [0, 10],
+ domain: getMinMax(linearValues),
range: [0, width],
}),
- values: [0, 2, 4, 6, 8, 10],
- tickFormat: (v: number) => (v === 10 ? 'last' : (v === 0 && 'first') || `${v}`),
+ values: linearValues,
+ tickFormat: (v: number, index: number, ticks: { value: number; index: number }[]) =>
+ index === 0 ? 'first' : index === ticks[ticks.length - 1].index ? 'last' : `${v}`,
label: 'linear',
},
{
scale: scaleBand({
- domain: ['a', 'b', 'c', 'd'],
+ domain: bandValues,
range: [0, width],
paddingOuter: 0,
paddingInner: 1,
}),
- values: ['a', 'b', 'c', 'd'],
+ values: bandValues,
tickFormat: (v: string) => v,
label: 'categories',
},
{
scale: scaleUtc({
- domain: [new Date('2020-01-01'), new Date('2020-03-01')],
+ domain: getMinMax(timeValues),
range: [0, width],
}),
- values: [new Date('2020-01-01'), new Date('2020-02-01'), new Date('2020-03-01')],
+ values: timeValues,
tickFormat: (v: Date, i: number) =>
- v.getDate() === 1 ? '🎉' : width > 400 || i % 2 === 0 ? timeFormat('%b %d')(v) : '',
+ i === 3 ? '🎉' : width > 400 || i % 2 === 0 ? timeFormat('%b %d')(v) : '',
label: 'time',
},
{
scale: scaleLog({
- domain: [1, 10000],
+ domain: getMinMax(logValues),
range: [0, width],
}),
- values: [1, 10, 100, 1000, 10000],
- tickFormat: (v: number) => (`${v}`[0] === '1' ? `${v}` : ''),
+ values: logValues,
+ tickFormat: (v: number) => {
+ const asString = `${v}`;
+ // label only major ticks
+ return asString.match(/^[.01?[\]]*$/) ? asString : '';
+ },
label: 'log',
},
];
@@ -87,76 +112,80 @@ export default function Example({ width: outerWidth = 800, height: outerHeight =
});
return (
-
+ {showControls && }
+ >
);
}
diff --git a/packages/vx-demo/src/sandboxes/vx-axis/package.json b/packages/vx-demo/src/sandboxes/vx-axis/package.json
index d7341ea03..ad88de0df 100644
--- a/packages/vx-demo/src/sandboxes/vx-axis/package.json
+++ b/packages/vx-demo/src/sandboxes/vx-axis/package.json
@@ -14,6 +14,7 @@
"@vx/grid": "latest",
"@vx/group": "latest",
"@vx/mock-data": "latest",
+ "@vx/react-spring": "latest",
"@vx/responsive": "latest",
"@vx/shape": "latest",
"@vx/scale": "latest",
diff --git a/packages/vx-demo/src/types/index.ts b/packages/vx-demo/src/types/index.ts
index bd400a275..fa453ce68 100644
--- a/packages/vx-demo/src/types/index.ts
+++ b/packages/vx-demo/src/types/index.ts
@@ -40,6 +40,7 @@ export type VxPackage =
| 'network'
| 'pattern'
| 'point'
+ | 'react-spring'
| 'responsive'
| 'scale'
| 'shape'
diff --git a/packages/vx-react-spring/README.md b/packages/vx-react-spring/README.md
new file mode 100644
index 000000000..cdcc64055
--- /dev/null
+++ b/packages/vx-react-spring/README.md
@@ -0,0 +1,21 @@
+# @vx/react-spring
+
+
+
+
+
+Although `vx` is largely unopinioned on animation, there is value in abstracting the complexity +
+boilerplate of building **animated** `vx` visualization components. This package requires
+`react-spring` as a `peerDependency` (to be compatible with any usage within your app) and exports
+all `vx` components that depend on `react-spring`.
+
+[`react-spring`](https://www.react-spring.io/) provides performant primitives for animating react
+components and is our recommended library for making animated charts. In order to minimize
+`react-spring` as a dependency across other `vx` packages, we've consolidated components which
+depend on it here.
+
+## Installation
+
+```
+npm install --save react-spring @vx/react-spring
+```
diff --git a/packages/vx-react-spring/package.json b/packages/vx-react-spring/package.json
new file mode 100644
index 000000000..47e4563ab
--- /dev/null
+++ b/packages/vx-react-spring/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@vx/react-spring",
+ "version": "0.0.198",
+ "description": "Vx primitives that rely on react-spring for animation",
+ "sideEffects": false,
+ "main": "lib/index.js",
+ "module": "esm/index.js",
+ "types": "lib/index.d.ts",
+ "files": [
+ "lib",
+ "esm"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/hshoff/vx.git"
+ },
+ "keywords": [
+ "vx",
+ "react",
+ "d3",
+ "visualizations",
+ "charts",
+ "animation",
+ "react-spring"
+ ],
+ "contributors": [
+ {
+ "name": "Chris Williams",
+ "url": "https://github.com/williaster"
+ }
+ ],
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/hshoff/vx/issues"
+ },
+ "homepage": "https://github.com/hshoff/vx#readme",
+ "publishConfig": {
+ "access": "public"
+ },
+ "peerDependencies": {
+ "react": "^16.3.0-0",
+ "react-spring": "^8.0.27"
+ },
+ "dependencies": {
+ "@types/classnames": "^2.2.9",
+ "@types/react": "*",
+ "@vx/axis": "0.0.198",
+ "@vx/scale": "0.0.198",
+ "@vx/text": "0.0.198",
+ "classnames": "^2.2.5",
+ "prop-types": "^15.6.2"
+ }
+}
diff --git a/packages/vx-react-spring/src/axis/AnimatedAxis.tsx b/packages/vx-react-spring/src/axis/AnimatedAxis.tsx
new file mode 100644
index 000000000..9a49f5e2c
--- /dev/null
+++ b/packages/vx-react-spring/src/axis/AnimatedAxis.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import Axis, { AxisProps } from '@vx/axis/lib/axis/Axis';
+import { AxisScale } from '@vx/axis/lib/types';
+import AnimatedTicks from './AnimatedTicks';
+
+export default function AnimatedAxis(
+ axisProps: Omit, 'ticksComponent'>,
+) {
+ return ;
+}
diff --git a/packages/vx-react-spring/src/axis/AnimatedTicks/index.tsx b/packages/vx-react-spring/src/axis/AnimatedTicks/index.tsx
new file mode 100644
index 000000000..128115d32
--- /dev/null
+++ b/packages/vx-react-spring/src/axis/AnimatedTicks/index.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { animated, useTransition, interpolate } from 'react-spring';
+import cx from 'classnames';
+import Orientation from '@vx/axis/lib/constants/orientation';
+import { TicksRendererProps, AxisScale } from '@vx/axis/lib/types';
+import { Text } from '@vx/text';
+
+import useTickTransitionConfig from './useTickTransitionConfig';
+
+export default function AnimatedTicks({
+ hideTicks,
+ horizontal,
+ orientation,
+ scale,
+ tickClassName,
+ tickLabelProps: allTickLabelProps,
+ tickStroke = '#222',
+ tickTransform,
+ ticks,
+}: TicksRendererProps) {
+ const transitionConfig = useTickTransitionConfig({ horizontal, scale });
+ const animatedTicks = useTransition(ticks, tick => `${tick.value}-${horizontal}`, {
+ unique: true,
+ ...transitionConfig,
+ });
+
+ return animatedTicks.map(({ item, key, props }, index) => {
+ // @ts-ignore react-spring types only include CSSProperties
+ const { fromX, toX, fromY, toY, opacity } = props;
+ const tickLabelProps = allTickLabelProps[index] ?? allTickLabelProps[0] ?? {};
+ return (
+
+ {!hideTicks && (
+
+ )}
+ {/** animate the group, not the Text */}
+
+ `translate(${interpolatedX},${interpolatedY +
+ (orientation === Orientation.bottom && typeof tickLabelProps.fontSize === 'number'
+ ? tickLabelProps.fontSize ?? 10
+ : 0)})`,
+ )}
+ opacity={opacity}
+ >
+ {item.formattedValue}
+
+
+ );
+ });
+}
diff --git a/packages/vx-react-spring/src/axis/AnimatedTicks/useTickTransitionConfig.ts b/packages/vx-react-spring/src/axis/AnimatedTicks/useTickTransitionConfig.ts
new file mode 100644
index 000000000..9a956607e
--- /dev/null
+++ b/packages/vx-react-spring/src/axis/AnimatedTicks/useTickTransitionConfig.ts
@@ -0,0 +1,54 @@
+import { useMemo } from 'react';
+import { coerceNumber } from '@vx/scale';
+import { AxisScale, ComputedTick, TicksRendererProps } from '@vx/axis/lib/types';
+
+function enterUpdate({ from, to }: ComputedTick) {
+ return {
+ fromX: from.x,
+ toX: to.x,
+ fromY: from.y,
+ toY: to.y,
+ opacity: 1,
+ };
+}
+
+export default function useTickTransitionConfig({
+ horizontal,
+ scale,
+}: Pick, 'scale' | 'horizontal'>) {
+ return useMemo(() => {
+ const [a, b] = scale.range();
+ const isDescending = b != null && a != null && b < a;
+ const [minPosition, maxPosition] = isDescending ? [b, a] : [a, b];
+ const scaleLength = b != null && a != null ? Math.abs(coerceNumber(b) - coerceNumber(a)) : 0;
+
+ const fromLeave = ({ from, to, value }: ComputedTick) => {
+ const scaledValue = scale(value) ?? 0;
+
+ return {
+ fromX: horizontal
+ ? // for top/bottom scales, enter from left or right based on value
+ scaledValue < scaleLength / 2
+ ? minPosition
+ : maxPosition
+ : // for left/right scales, don't animate x
+ from.x,
+ // same logic as above for the `to` Point
+ toX: horizontal ? (scaledValue < scaleLength / 2 ? minPosition : maxPosition) : to.x,
+ // for top/bottom scales, don't animate y
+ fromY: horizontal
+ ? // for top/bottom scales, don't animate y
+ from.y
+ : // for left/right scales, animate from top or bottom based on value
+ scaledValue < scaleLength / 2
+ ? minPosition
+ : maxPosition,
+ // same logic as above for the `to` Point
+ toY: horizontal ? to.y : scaledValue < scaleLength / 2 ? minPosition : maxPosition,
+ opacity: 0,
+ };
+ };
+
+ return { from: fromLeave, leave: fromLeave, enter: enterUpdate, update: enterUpdate };
+ }, [horizontal, scale]);
+}
diff --git a/packages/vx-react-spring/src/index.ts b/packages/vx-react-spring/src/index.ts
new file mode 100644
index 000000000..08ee44bf0
--- /dev/null
+++ b/packages/vx-react-spring/src/index.ts
@@ -0,0 +1,2 @@
+export { default as AnimatedTicks } from './axis/AnimatedTicks';
+export { default as AnimatedAxis } from './axis/AnimatedAxis';
diff --git a/packages/vx-react-spring/test/AnimatedAxis.test.tsx b/packages/vx-react-spring/test/AnimatedAxis.test.tsx
new file mode 100644
index 000000000..a64a832f4
--- /dev/null
+++ b/packages/vx-react-spring/test/AnimatedAxis.test.tsx
@@ -0,0 +1,7 @@
+import { AnimatedAxis } from '../src';
+
+describe('AnimatedAxis', () => {
+ it('should be defined', () => {
+ expect(AnimatedAxis).toBeDefined();
+ });
+});
diff --git a/packages/vx-react-spring/test/AnimatedTicks.test.tsx b/packages/vx-react-spring/test/AnimatedTicks.test.tsx
new file mode 100644
index 000000000..a9cfe08c1
--- /dev/null
+++ b/packages/vx-react-spring/test/AnimatedTicks.test.tsx
@@ -0,0 +1,7 @@
+import { AnimatedTicks } from '../src';
+
+describe('AnimatedTicks', () => {
+ it('should be defined', () => {
+ expect(AnimatedTicks).toBeDefined();
+ });
+});
diff --git a/packages/vx-react-spring/test/useTickTransitionConfig.test.tsx b/packages/vx-react-spring/test/useTickTransitionConfig.test.tsx
new file mode 100644
index 000000000..a70d9146c
--- /dev/null
+++ b/packages/vx-react-spring/test/useTickTransitionConfig.test.tsx
@@ -0,0 +1,7 @@
+import useTickTransitionConfig from '../src/axis/AnimatedTicks/useTickTransitionConfig';
+
+describe('useTickTransitionConfig', () => {
+ it('should be defined', () => {
+ expect(useTickTransitionConfig).toBeDefined();
+ });
+});