Skip to content

Commit

Permalink
new(xychart): add colorAccessor to relevant series (#1005)
Browse files Browse the repository at this point in the history
* new(xychart): add colorAccessor to BaseBarGroup/Stack/Series and BaseGlyphSeries

* new(xychart/util): add cleanColorString + tests

* fix(xychart): fix tweening of url-containing color strings

* new(xychart): include index in colorAccessor signature

* new(demo/xychart): demo colorAccessor

* test(xychart): add colorAccessor tests

* style(demo/xychart): improve visibility of glyph patterns

* type(xychart): fix colorAccessor generics in BarSeries
  • Loading branch information
williaster authored Jan 11, 2021
1 parent 460a0db commit ad43f17
Show file tree
Hide file tree
Showing 16 changed files with 206 additions and 28 deletions.
6 changes: 6 additions & 0 deletions packages/visx-demo/src/sandboxes/visx-xychart/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default function Example({ height }: XYChartProps) {
annotationDatum,
annotationLabelPosition,
annotationType,
colorAccessorFactory,
config,
curve,
data,
Expand Down Expand Up @@ -113,18 +114,21 @@ export default function Example({ height }: XYChartProps) {
data={data}
xAccessor={accessors.x['New York']}
yAccessor={accessors.y['New York']}
colorAccessor={colorAccessorFactory('New York')}
/>
<AnimatedBarSeries
dataKey="San Francisco"
data={data}
xAccessor={accessors.x['San Francisco']}
yAccessor={accessors.y['San Francisco']}
colorAccessor={colorAccessorFactory('San Francisco')}
/>
<AnimatedBarSeries
dataKey="Austin"
data={data}
xAccessor={accessors.x.Austin}
yAccessor={accessors.y.Austin}
colorAccessor={colorAccessorFactory('Austin')}
/>
</AnimatedBarGroup>
)}
Expand All @@ -134,6 +138,7 @@ export default function Example({ height }: XYChartProps) {
data={data}
xAccessor={accessors.x['New York']}
yAccessor={accessors.y['New York']}
colorAccessor={colorAccessorFactory('New York')}
/>
)}
{renderAreaSeries && (
Expand Down Expand Up @@ -200,6 +205,7 @@ export default function Example({ height }: XYChartProps) {
xAccessor={accessors.x['San Francisco']}
yAccessor={accessors.y['San Francisco']}
renderGlyph={renderGlyph}
colorAccessor={colorAccessorFactory('San Francisco')}
/>
)}
<AnimatedAxis
Expand Down
39 changes: 32 additions & 7 deletions packages/visx-demo/src/sandboxes/visx-xychart/ExampleControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import React, { useCallback, useMemo, useState } from 'react';
import { lightTheme, darkTheme, XYChartTheme } from '@visx/xychart';
import { PatternLines } from '@visx/pattern';
import { GlyphProps } from '@visx/xychart/lib/types';
import { AnimationTrajectory } from '@visx/react-spring/lib/types';
import cityTemperature, { CityTemperature } from '@visx/mock-data/lib/mocks/cityTemperature';
Expand All @@ -26,6 +27,7 @@ const getNegativeSfTemperature = (d: CityTemperature) => -getSfTemperature(d);
const getNyTemperature = (d: CityTemperature) => Number(d['New York']);
const getAustinTemperature = (d: CityTemperature) => Number(d.Austin);
const defaultAnnotationDataIndex = 13;
const selectedDatumPatternId = 'xychart-selected-datum';

type Accessor = (d: CityTemperature) => number | string;

Expand All @@ -35,6 +37,8 @@ interface Accessors {
Austin: Accessor;
}

type DataKey = keyof Accessors;

type SimpleScaleConfig = { type: 'band' | 'linear'; paddingInner?: number };

type ProvidedProps = {
Expand All @@ -44,10 +48,11 @@ type ProvidedProps = {
date: Accessor;
};
animationTrajectory: AnimationTrajectory;
annotationDataKey: keyof Accessors | null;
annotationDataKey: DataKey | null;
annotationDatum?: CityTemperature;
annotationLabelPosition: { dx: number; dy: number };
annotationType?: 'line' | 'circle';
colorAccessorFactory: (key: DataKey) => (d: CityTemperature) => string | null;
config: {
x: SimpleScaleConfig;
y: SimpleScaleConfig;
Expand All @@ -57,7 +62,7 @@ type ProvidedProps = {
editAnnotationLabelPosition: boolean;
numTicks: number;
setAnnotationDataIndex: (index: number) => void;
setAnnotationDataKey: (key: keyof Accessors | null) => void;
setAnnotationDataKey: (key: DataKey | null) => void;
setAnnotationLabelPosition: (position: { dx: number; dy: number }) => void;
renderAreaSeries: boolean;
renderBarGroup: boolean;
Expand Down Expand Up @@ -117,26 +122,34 @@ export default function ExampleControls({ children }: ControlsProps) {
const [missingValues, setMissingValues] = useState(false);
const [glyphComponent, setGlyphComponent] = useState<'star' | 'cross' | 'circle' | '🍍'>('star');
const [curveType, setCurveType] = useState<'linear' | 'cardinal' | 'step'>('linear');
const themeBackground = theme.backgroundColor;
const glyphOutline = theme.gridStyles.stroke;
const renderGlyph = useCallback(
({ size, color, onPointerMove, onPointerOut, onPointerUp }: GlyphProps<CityTemperature>) => {
const handlers = { onPointerMove, onPointerOut, onPointerUp };
if (glyphComponent === 'star') {
return <GlyphStar stroke={themeBackground} fill={color} size={size * 8} {...handlers} />;
return <GlyphStar stroke={glyphOutline} fill={color} size={size * 10} {...handlers} />;
}
if (glyphComponent === 'circle') {
return <GlyphDot stroke={themeBackground} fill={color} r={size / 2} {...handlers} />;
return <GlyphDot stroke={glyphOutline} fill={color} r={size / 2} {...handlers} />;
}
if (glyphComponent === 'cross') {
return <GlyphCross stroke={themeBackground} fill={color} size={size * 8} {...handlers} />;
return <GlyphCross stroke={glyphOutline} fill={color} size={size * 10} {...handlers} />;
}
return (
<text dx="-0.75em" dy="0.25em" fontSize={14} {...handlers}>
🍍
</text>
);
},
[glyphComponent, themeBackground],
[glyphComponent, glyphOutline],
);
// for series that support it, return a colorAccessor which returns a custom color if the datum is selected
const colorAccessorFactory = useCallback(
(dataKey: DataKey) => (d: CityTemperature) =>
annotationDataKey === dataKey && d === data[annotationDataIndex]
? `url(#${selectedDatumPatternId})`
: null,
[annotationDataIndex, annotationDataKey],
);

const accessors = useMemo(
Expand Down Expand Up @@ -183,6 +196,7 @@ export default function ExampleControls({ children }: ControlsProps) {
annotationDatum: data[annotationDataIndex],
annotationLabelPosition,
annotationType,
colorAccessorFactory,
config,
curve:
(curveType === 'cardinal' && curveCardinal) ||
Expand Down Expand Up @@ -220,6 +234,17 @@ export default function ExampleControls({ children }: ControlsProps) {
xAxisOrientation,
yAxisOrientation,
})}
{/** This style is used for annotated elements via colorAccessor. */}
<svg>
<PatternLines
id={selectedDatumPatternId}
width={6}
height={6}
orientation={['diagonalRightToLeft']}
stroke={theme?.axisStyles.x.bottom.axisLine.stroke}
strokeWidth={1.5}
/>
</svg>
<div className="controls">
{/** data */}
<div>
Expand Down
12 changes: 10 additions & 2 deletions packages/visx-xychart/src/components/series/AnimatedBarSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ export default function AnimatedBarSeries<
XScale extends AxisScale,
YScale extends AxisScale,
Datum extends object
>({ ...props }: Omit<BaseBarSeriesProps<XScale, YScale, Datum>, 'BarsComponent'>) {
return <BaseBarSeries<XScale, YScale, Datum> {...props} BarsComponent={AnimatedBars} />;
>({ colorAccessor, ...props }: Omit<BaseBarSeriesProps<XScale, YScale, Datum>, 'BarsComponent'>) {
return (
<BaseBarSeries<XScale, YScale, Datum>
{...props}
// @TODO currently generics for non-SeriesProps are not passed correctly in
// withRegisteredData HOC
colorAccessor={colorAccessor as BaseBarSeriesProps<XScale, YScale, object>['colorAccessor']}
BarsComponent={AnimatedBars}
/>
);
}
17 changes: 13 additions & 4 deletions packages/visx-xychart/src/components/series/BarSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ import React from 'react';
import BaseBarSeries, { BaseBarSeriesProps } from './private/BaseBarSeries';
import Bars from './private/Bars';

function BarSeries<XScale extends AxisScale, YScale extends AxisScale, Datum extends object>(
props: Omit<BaseBarSeriesProps<XScale, YScale, Datum>, 'BarsComponent'>,
) {
return <BaseBarSeries<XScale, YScale, Datum> {...props} BarsComponent={Bars} />;
function BarSeries<XScale extends AxisScale, YScale extends AxisScale, Datum extends object>({
colorAccessor,
...props
}: Omit<BaseBarSeriesProps<XScale, YScale, Datum>, 'BarsComponent'>) {
return (
<BaseBarSeries<XScale, YScale, Datum>
{...props}
// @TODO currently generics for non-SeriesProps are not passed correctly in
// withRegisteredData HOC
colorAccessor={colorAccessor as BaseBarSeriesProps<XScale, YScale, object>['colorAccessor']}
BarsComponent={Bars}
/>
);
}

export default BarSeries;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AxisScale } from '@visx/axis';
import React, { useMemo } from 'react';
import { animated, useTransition } from 'react-spring';
import { Bar, BarsProps } from '../../../types';
import { cleanColor, colorHasUrl } from '../../../utils/cleanColorString';
import getScaleBaseline from '../../../utils/getScaleBaseline';

function enterUpdate({ x, y, width, height, fill }: Bar) {
Expand All @@ -10,7 +11,7 @@ function enterUpdate({ x, y, width, height, fill }: Bar) {
y,
width,
height,
fill,
fill: cleanColor(fill),
opacity: 1,
};
}
Expand All @@ -34,7 +35,7 @@ function useBarTransitionConfig<Scale extends AxisScale>({
y: shouldAnimateX ? y : scaleBaseline ?? 0,
width: shouldAnimateX ? 0 : width,
height: shouldAnimateX ? height : 0,
fill,
fill: cleanColor(fill),
opacity: 0,
};
}
Expand Down Expand Up @@ -72,7 +73,8 @@ export default function AnimatedBars<XScale extends AxisScale, YScale extends Ax
y={y}
width={width}
height={height}
fill={fill}
// use the item's fill directly if it's not animate-able
fill={colorHasUrl(item.fill) ? item.fill : fill}
opacity={opacity}
{...rectProps}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { useMemo } from 'react';
import { useTransition, animated, interpolate } from 'react-spring';
import getScaleBaseline from '../../../utils/getScaleBaseline';
import { GlyphProps, GlyphsProps } from '../../../types';
import { cleanColor, colorHasUrl } from '../../../utils/cleanColorString';

type ConfigKeys = 'enter' | 'update' | 'from' | 'leave';

Expand Down Expand Up @@ -30,17 +31,17 @@ export function useAnimatedGlyphsConfig<
from: ({ x, y, color }) => ({
x: horizontal ? xScaleBaseline : x,
y: horizontal ? y : yScaleBaseline,
color,
color: cleanColor(color),
opacity: 0,
}),
leave: ({ x, y, color }) => ({
x: horizontal ? xScaleBaseline : x,
y: horizontal ? y : yScaleBaseline,
color,
color: cleanColor(color),
opacity: 0,
}),
enter: ({ x, y, color }) => ({ x, y, color, opacity: 1 }),
update: ({ x, y, color }) => ({ x, y, color, opacity: 1 }),
enter: ({ x, y, color }) => ({ x, y, color: cleanColor(color), opacity: 1 }),
update: ({ x, y, color }) => ({ x, y, color: cleanColor(color), opacity: 1 }),
}),
[xScaleBaseline, yScaleBaseline, horizontal],
);
Expand Down Expand Up @@ -91,7 +92,9 @@ export default function AnimatedGlyphs<
x: 0,
y: 0,
size: item.size,
color: 'currentColor', // allows us to animate the color of the <g /> element
// currentColor doesn't work with url-based colors (pattern, gradient)
// otherwise currentColor allows us to animate the color of the <g /> element
color: colorHasUrl(item.color) ? item.color : 'currentColor',
onBlur,
onFocus,
onPointerMove,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ export default function BaseBarGroup<

const getWidth = horizontal ? (d: Datum) => Math.abs(getLength(d)) : () => barThickness;
const getHeight = horizontal ? () => barThickness : (d: Datum) => Math.abs(getLength(d));
const colorAccessor = barSeriesChildren.find(child => child.props.dataKey === key)?.props
?.colorAccessor;

return data
.map((bar, index) => {
Expand All @@ -171,7 +173,7 @@ export default function BaseBarGroup<
y: barY,
width: barWidth,
height: barHeight,
fill: colorScale(key),
fill: colorAccessor?.(bar, index) ?? colorScale(key),
};
})
.filter(bar => bar) as Bar[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export type BaseBarSeriesProps<
* Accepted values are [0, 1], 0 = no padding, 1 = no bar, defaults to 0.1.
*/
barPadding?: number;
/** Given a Datum, returns its color. Falls back to theme color if unspecified or if a null-ish value is returned. */
colorAccessor?: (d: Datum, index: number) => string | null | undefined;
};

// Fallback bandwidth estimate assumes no missing data values (divides chart space by # datum)
Expand All @@ -32,6 +34,7 @@ const getFallbackBandwidth = (fullBarWidth: number, barPadding: number) =>
function BaseBarSeries<XScale extends AxisScale, YScale extends AxisScale, Datum extends object>({
BarsComponent,
barPadding = 0.1,
colorAccessor,
data,
dataKey,
onBlur,
Expand Down Expand Up @@ -78,11 +81,21 @@ function BaseBarSeries<XScale extends AxisScale, YScale extends AxisScale, Datum
y: horizontal ? y : yZeroPosition + Math.min(0, barLength),
width: horizontal ? Math.abs(barLength) : barThickness,
height: horizontal ? barThickness : Math.abs(barLength),
fill: color, // @TODO allow prop overriding
fill: colorAccessor?.(datum, index) ?? color,
};
})
.filter(bar => bar) as Bar[];
}, [barThickness, color, data, getScaledX, getScaledY, horizontal, xZeroPosition, yZeroPosition]);
}, [
barThickness,
color,
colorAccessor,
data,
getScaledX,
getScaledY,
horizontal,
xZeroPosition,
yZeroPosition,
]);

const ownEventSourceKey = `${BARSERIES_EVENT_SOURCE}-${dataKey}`;
const eventEmitters = useSeriesEvents<XScale, YScale, Datum>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ function BaseBarStack<
const entry = dataRegistry.get(barStack.key);
if (!entry) return null;

// get colorAccessor from child BarSeries, if available
const barSeries:
| React.ReactElement<BaseBarSeriesProps<XScale, YScale, Datum>>
| undefined = barSeriesChildren.find(child => child.props.dataKey === barStack.key);
const colorAccessor = barSeries?.props?.colorAccessor;

return barStack.map((bar, index) => {
const barX = getX(bar);
if (!isValidNumber(barX)) return null;
Expand All @@ -207,13 +213,18 @@ function BaseBarStack<
const barHeight = getHeight(bar);
if (!isValidNumber(barHeight)) return null;

const barSeriesDatum = colorAccessor ? barSeries?.props?.data[index] : null;

return {
key: `${stackIndex}-${barStack.key}-${index}`,
x: barX,
y: barY,
width: barWidth,
height: barHeight,
fill: colorScale(barStack.key),
fill:
barSeriesDatum && colorAccessor
? colorAccessor(barSeriesDatum, index)
: colorScale(barStack.key),
};
});
})
Expand Down
Loading

0 comments on commit ad43f17

Please sign in to comment.