Skip to content

Commit

Permalink
Merge pull request #787 from hshoff/chris--animated-grid
Browse files Browse the repository at this point in the history
new(vx-react-spring): add AnimatedGridRows/Columns + animationTrajectory
  • Loading branch information
williaster committed Aug 21, 2020
2 parents ef2db11 + 24f6caa commit 7d35015
Show file tree
Hide file tree
Showing 25 changed files with 711 additions and 279 deletions.
5 changes: 3 additions & 2 deletions packages/vx-axis/src/axis/AxisRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TextProps } from '@vx/text/lib/Text';
import getLabelTransform from '../utils/getLabelTransform';
import { AxisRendererProps, AxisScale } from '../types';
import Ticks from './Ticks';
import { Orientation } from '..';

const defaultTextProps: Partial<TextProps> = {
textAnchor: 'middle',
Expand All @@ -26,15 +27,15 @@ export default function AxisRenderer<Scale extends AxisScale>({
labelClassName,
labelOffset = 14,
labelProps = defaultTextProps,
orientation,
orientation = Orientation.bottom,
scale,
stroke = '#222',
strokeDasharray,
strokeWidth = 1,
tickClassName,
tickComponent,
tickLabelProps = (/** tickValue, index */) => defaultTextProps,
tickLength,
tickLength = 8,
tickStroke = '#222',
tickTransform,
ticks,
Expand Down
22 changes: 11 additions & 11 deletions packages/vx-axis/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ export type TicksRendererProps<Scale extends AxisScale> = {
| 'ticks'
>;

interface CommonProps<Scale extends AxisScale> {
export type CommonProps<Scale extends AxisScale> = {
/** The class name applied to the axis line element. */
axisLineClassName?: string;
/** If true, will hide the axis line. */
hideAxisLine: boolean;
hideAxisLine?: boolean;
/** If true, will hide the ticks (but not the tick labels). */
hideTicks: boolean;
hideTicks?: boolean;
/** If true, will hide the '0' value tick and tick label. */
hideZero: boolean;
hideZero?: boolean;
/** The text for the axis label. */
label?: string;
/** The class name applied to the axis label text element. */
Expand All @@ -60,11 +60,11 @@ interface CommonProps<Scale extends AxisScale> {
/** Props applied to the axis label component. */
labelProps?: Partial<TextProps>;
/** The number of ticks wanted for the axis (note this is approximate) */
numTicks: number;
numTicks?: number;
/** Placement of the axis */
orientation: Orientation;
orientation?: Orientation;
/** Pixel padding to apply to both sides of the axis. */
rangePadding: number;
rangePadding?: number;
/** The color for the stroke of the lines. */
stroke?: string;
/** The pixel value for the width of the lines. */
Expand All @@ -78,16 +78,16 @@ interface CommonProps<Scale extends AxisScale> {
/** 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>>;
tickFormat?: TickFormatter<ScaleInput<Scale>>;
/** A function that returns props for a given tick label. */
tickLabelProps?: TickLabelProps<ScaleInput<Scale>>;
/** The length of the tick lines. */
tickLength: number;
tickLength?: number;
/** The color for the tick's stroke value. */
tickStroke?: string;
/** A custom SVG transform value to be applied to each tick group. */
tickTransform?: string;
}
};

interface Point {
x: number;
Expand Down Expand Up @@ -119,7 +119,7 @@ export type AxisRendererProps<Scale extends AxisScale> = CommonProps<Scale> & {
ticks: ComputedTick<Scale>[];
};

export type SharedAxisProps<Scale extends AxisScale> = Partial<CommonProps<Scale>> & {
export type SharedAxisProps<Scale extends AxisScale> = CommonProps<Scale> & {
/** The class name applied to the outermost axis group element. */
axisClassName?: string;
/** A left pixel offset applied to the entire axis. */
Expand Down
1 change: 1 addition & 0 deletions packages/vx-demo/src/components/Gallery/AxisTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default function AxisTile() {
title="Axes & scales"
description="<Axis.AxisBottom />"
detailsStyles={detailsStyles}
detailsHeight={20}
exampleProps={exampleProps}
exampleRenderer={Axis}
exampleUrl="/axis"
Expand Down
4 changes: 3 additions & 1 deletion packages/vx-demo/src/pages/docs/react-spring.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from 'react';
import ReactSpringReadme from '!!raw-loader!../../../../vx-react-spring/README.md';
import AnimatedAxis from '../../../../vx-react-spring/src/axis/AnimatedAxis';
import AnimatedGridColumns from '../../../../vx-react-spring/src/grid/AnimatedGridColumns';
import AnimatedGridRows from '../../../../vx-react-spring/src/grid/AnimatedGridRows';
import DocPage from '../../components/DocPage';
import AxisTile from '../../components/Gallery/AxisTile';

const components = [AnimatedAxis];
const components = [AnimatedAxis, AnimatedGridColumns, AnimatedGridRows];

const examples = [AxisTile];

Expand Down
210 changes: 134 additions & 76 deletions packages/vx-demo/src/sandboxes/vx-axis/Example.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, { useState } from 'react';
import React, { useState, useMemo } 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, coerceNumber } from '@vx/scale';
import { Orientation, SharedAxisProps, AxisScale } from '@vx/axis';
import { AnimatedAxis } from '@vx/react-spring';
import { AnimatedAxis, AnimatedGridRows, AnimatedGridColumns } from '@vx/react-spring';
import { LinearGradient } from '@vx/gradient';
import { timeFormat } from 'd3-time-format';

Expand All @@ -13,14 +12,21 @@ const axisColor = '#fff';
const tickLabelColor = '#fff';
export const labelColor = '#340098';
const gridColor = '#6e0fca';
const numTickColumns = 5;
const margin = {
top: 40,
right: 150,
bottom: 50,
bottom: 20,
left: 50,
};

const tickLabelProps = () =>
({
fill: tickLabelColor,
fontSize: 12,
fontFamily: 'sans-serif',
textAnchor: 'middle',
} as const);

const getMinMax = (vals: (number | { valueOf(): number })[]) => {
const numericVals = vals.map(coerceNumber);
return [Math.min(...numericVals), Math.max(...numericVals)];
Expand All @@ -41,69 +47,74 @@ export default function Example({
const width = outerWidth - margin.left - margin.right;
const height = outerHeight - margin.top - margin.bottom;
const [dataToggle, setDataToggle] = useState(true);

if (width < 10) return null;
const [animationTrajectory, setAnimationTrajectory] = useState<
'outside' | 'center' | 'min' | 'max'
>('center');

interface AxisDemoProps<Scale extends AxisScale> extends SharedAxisProps<Scale> {
values: ScaleInput<Scale>[];
}

// 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<AxisScale<number>>[] = useMemo(() => {
// 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<AxisScale<number>>[] = [
{
scale: scaleLinear({
domain: getMinMax(linearValues),
range: [0, width],
}),
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: bandValues,
range: [0, width],
paddingOuter: 0,
paddingInner: 1,
}),
values: bandValues,
tickFormat: (v: string) => v,
label: 'categories',
},
{
scale: scaleUtc({
domain: getMinMax(timeValues),
range: [0, width],
}),
values: timeValues,
tickFormat: (v: Date, i: number) =>
i === 3 ? '🎉' : width > 400 || i % 2 === 0 ? timeFormat('%b %d')(v) : '',
label: 'time',
},
{
scale: scaleLog({
domain: getMinMax(logValues),
range: [0, width],
}),
values: logValues,
tickFormat: (v: number) => {
const asString = `${v}`;
// label only major ticks
return asString.match(/^[.01?[\]]*$/) ? asString : '';
return [
{
scale: scaleLinear({
domain: getMinMax(linearValues),
range: [0, width],
}),
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: bandValues,
range: [0, width],
paddingOuter: 0,
paddingInner: 1,
}),
values: bandValues,
tickFormat: (v: string) => v,
label: 'categories',
},
{
scale: scaleUtc({
domain: getMinMax(timeValues),
range: [0, width],
}),
values: timeValues,
tickFormat: (v: Date, i: number) =>
i === 3 ? '🎉' : width > 400 || i % 2 === 0 ? timeFormat('%b %d')(v) : '',
label: 'time',
},
{
scale: scaleLog({
domain: getMinMax(logValues),
range: [0, width],
}),
values: logValues,
tickFormat: (v: number) => {
const asString = `${v}`;
// label only major ticks
return asString.match(/^[.01?[\]]*$/) ? asString : '';
},
label: 'log',
},
label: 'log',
},
];
];
}, [dataToggle, width]);

const scalePadding = 50;
if (width < 10) return null;

const scalePadding = 40;
const scaleHeight = height / axes.length - scalePadding;

const yScale = scaleLinear({
Expand Down Expand Up @@ -131,9 +142,28 @@ export default function Example({
<g transform={`translate(${margin.left},${margin.top})`}>
{axes.map(({ scale, values, label, tickFormat }, i) => (
<g key={`scale-${i}`} transform={`translate(0, ${i * (scaleHeight + scalePadding)})`}>
<AnimatedGridRows
// force remount when this changes to see the animation difference
key={`gridrows-${animationTrajectory}`}
scale={yScale}
stroke={gridColor}
width={width}
numTicks={dataToggle ? 1 : 3}
animationTrajectory={animationTrajectory}
/>
<AnimatedGridColumns
// force remount when this changes to see the animation difference
key={`gridcolumns-${animationTrajectory}`}
scale={scale}
stroke={gridColor}
height={scaleHeight}
numTicks={dataToggle ? 5 : 2}
animationTrajectory={animationTrajectory}
/>
<AreaClosed
data={values.map(x => [
(scale(x) ?? 0) +
// offset point half of band width for band scales
('bandwidth' in scale && typeof scale!.bandwidth !== 'undefined'
? scale.bandwidth!() / 2
: 0),
Expand All @@ -144,28 +174,16 @@ export default function Example({
fill={gridColor}
fillOpacity={0.2}
/>
<Grid
xScale={scale}
yScale={yScale}
stroke={gridColor}
width={width}
height={scaleHeight}
numTicksRows={2}
numTicksColumns={numTickColumns}
/>
<AnimatedAxis
// force remount when this changes to see the animation difference
key={`axis-${animationTrajectory}`}
orientation={Orientation.bottom}
top={scaleHeight}
scale={scale}
tickFormat={tickFormat}
stroke={axisColor}
tickStroke={axisColor}
tickLabelProps={() => ({
fill: tickLabelColor,
fontSize: 12,
fontFamily: 'sans-serif',
textAnchor: 'middle',
})}
tickLabelProps={tickLabelProps}
tickValues={label === 'log' || label === 'time' ? undefined : values}
numTicks={label === 'time' ? 6 : undefined}
label={label}
Expand All @@ -180,12 +198,52 @@ export default function Example({
fontFamily: 'sans-serif',
textAnchor: 'start',
}}
animationTrajectory={animationTrajectory}
/>
</g>
))}
</g>
</svg>
{showControls && <button onClick={() => setDataToggle(!dataToggle)}>Update scales</button>}
{showControls && (
<>
<div style={{ fontSize: 11 }}>
<strong>animation trajectory</strong>
<label>
<input
type="radio"
onChange={() => setAnimationTrajectory('outside')}
checked={animationTrajectory === 'outside'}
/>{' '}
outside
</label>
<label>
<input
type="radio"
onChange={() => setAnimationTrajectory('center')}
checked={animationTrajectory === 'center'}
/>{' '}
center
</label>
<label>
<input
type="radio"
onChange={() => setAnimationTrajectory('min')}
checked={animationTrajectory === 'min'}
/>{' '}
min
</label>
<label>
<input
type="radio"
onChange={() => setAnimationTrajectory('max')}
checked={animationTrajectory === 'max'}
/>{' '}
max
</label>
</div>
<button onClick={() => setDataToggle(!dataToggle)}>Update scales</button>
</>
)}
</>
);
}
Loading

0 comments on commit 7d35015

Please sign in to comment.