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(xychart): add colorAccessor to relevant series #1005

Merged
merged 8 commits into from
Jan 11, 2021
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