Skip to content

Commit

Permalink
Merge pull request #865 from airbnb/chris--xychart-stackedbar
Browse files Browse the repository at this point in the history
new(xychart): add BarStack series
  • Loading branch information
williaster authored Oct 15, 2020
2 parents 2d8c0a4 + 9612b6f commit bcae42d
Show file tree
Hide file tree
Showing 23 changed files with 764 additions and 78 deletions.
96 changes: 65 additions & 31 deletions packages/visx-demo/src/sandboxes/visx-xychart/Example.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import cityTemperature, { CityTemperature } from '@visx/mock-data/lib/mocks/cityTemperature';
import { CityTemperature } from '@visx/mock-data/lib/mocks/cityTemperature';
import {
AnimatedAxis,
AnimatedGrid,
DataProvider,
BarSeries,
BarStack,
LineSeries,
Tooltip,
XYChart,
Expand All @@ -17,20 +18,19 @@ type Props = {
height: number;
};

const xScaleConfig = { type: 'band', paddingInner: 0.3 } as const;
const yScaleConfig = { type: 'linear' } as const;
const numTicks = 4;
const data = cityTemperature.slice(150, 225);
const getDate = (d: CityTemperature) => d.date;
const getSfTemperature = (d: CityTemperature) => Number(d['San Francisco']);
const getNyTemperature = (d: CityTemperature) => Number(d['New York']);
type City = 'San Francisco' | 'New York' | 'Austin';

export default function Example({ height }: Props) {
return (
<ExampleControls>
{({
accessors,
animationTrajectory,
config,
data,
numTicks,
renderBarSeries,
renderBarStack,
renderHorizontally,
renderLineSeries,
sharedTooltip,
Expand All @@ -45,11 +45,7 @@ export default function Example({ height }: Props) {
xAxisOrientation,
yAxisOrientation,
}) => (
<DataProvider
theme={theme}
xScale={renderHorizontally ? yScaleConfig : xScaleConfig}
yScale={renderHorizontally ? xScaleConfig : yScaleConfig}
>
<DataProvider theme={theme} xScale={config.x} yScale={config.y}>
<XYChart height={Math.min(400, height)}>
<CustomChartBackground />
<AnimatedGrid
Expand All @@ -59,23 +55,56 @@ export default function Example({ height }: Props) {
animationTrajectory={animationTrajectory}
numTicks={numTicks}
/>
{renderBarStack && (
<g fillOpacity={renderLineSeries ? 0.5 : 1}>
<BarStack horizontal={renderHorizontally}>
<BarSeries
dataKey="New York"
data={data}
xAccessor={accessors.x['New York']}
yAccessor={accessors.y['New York']}
/>
<BarSeries
dataKey="San Francisco"
data={data}
xAccessor={accessors.x['San Francisco']}
yAccessor={accessors.y['San Francisco']}
/>
<BarSeries
dataKey="Austin"
data={data}
xAccessor={accessors.x.Austin}
yAccessor={accessors.y.Austin}
/>
</BarStack>
</g>
)}
{renderBarSeries && (
<BarSeries
dataKey="New York"
data={data}
xAccessor={renderHorizontally ? getNyTemperature : getDate}
yAccessor={renderHorizontally ? getDate : getNyTemperature}
xAccessor={accessors.x['New York']}
yAccessor={accessors.y['New York']}
horizontal={renderHorizontally}
/>
)}
{renderLineSeries && (
<LineSeries
dataKey="San Francisco"
data={data}
xAccessor={renderHorizontally ? getSfTemperature : getDate}
yAccessor={renderHorizontally ? getDate : getSfTemperature}
horizontal={!renderHorizontally}
/>
<>
<LineSeries
dataKey="San Francisco"
data={renderBarStack ? data : data}
xAccessor={accessors.x['San Francisco']}
yAccessor={accessors.y['San Francisco']}
horizontal={!renderHorizontally}
/>
<LineSeries
dataKey="Austin"
data={renderBarStack ? data : data}
xAccessor={accessors.x.Austin}
yAccessor={accessors.y.Austin}
horizontal={!renderHorizontally}
/>
</>
)}
<AnimatedAxis
key={`time-axis-${animationTrajectory}-${renderHorizontally}`}
Expand All @@ -101,27 +130,32 @@ export default function Example({ height }: Props) {
renderTooltip={({ tooltipData, colorScale }) => (
<>
{/** date */}
{tooltipData?.nearestDatum?.datum
? getDate(tooltipData?.nearestDatum?.datum)
: 'No date'}
{(tooltipData?.nearestDatum?.datum &&
accessors.date(tooltipData?.nearestDatum?.datum)) ||
'No date'}
<br />
<br />
{/** temperatures */}
{((sharedTooltip
? Object.keys(tooltipData?.datumByKey ?? {})
: [tooltipData?.nearestDatum?.key]
).filter(key => key) as string[]).map(key => (
<div key={key}>
).filter(city => city) as City[]).map(city => (
<div key={city}>
<em
style={{
color: colorScale?.(key),
color: colorScale?.(city),
textDecoration:
tooltipData?.nearestDatum?.key === key ? 'underline' : undefined,
tooltipData?.nearestDatum?.key === city ? 'underline' : undefined,
}}
>
{key}
{city}
</em>{' '}
{tooltipData?.datumByKey[key].datum[key as keyof CityTemperature]}° F
{tooltipData?.nearestDatum?.datum
? accessors[renderHorizontally ? 'x' : 'y'][city](
tooltipData?.nearestDatum?.datum,
)
: '–'}
° F
</div>
))}
</>
Expand Down
119 changes: 106 additions & 13 deletions packages/visx-demo/src/sandboxes/visx-xychart/ExampleControls.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,47 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { lightTheme, darkTheme, XYChartTheme } from '@visx/xychart';
import { AnimationTrajectory } from '@visx/react-spring/lib/types';
import cityTemperature, { CityTemperature } from '@visx/mock-data/lib/mocks/cityTemperature';
import customTheme from './customTheme';

const dateScaleConfig = { type: 'band', paddingInner: 0.3 } as const;
const temperatureScaleConfig = { type: 'linear' } as const;
const numTicks = 4;
const data = cityTemperature.slice(200, 275);
const dataSmall = data.slice(25);
const getDate = (d: CityTemperature) => d.date;
const getSfTemperature = (d: CityTemperature) => Number(d['San Francisco']);
const getNegativeSFTemperature = (d: CityTemperature) => -getSfTemperature(d);
const getNyTemperature = (d: CityTemperature) => Number(d['New York']);
const getAustinTemperature = (d: CityTemperature) => Number(d.Austin);

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

interface Accessors {
'San Francisco': Accessor;
'New York': Accessor;
Austin: Accessor;
}

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

type ProvidedProps = {
accessors: {
x: Accessors;
y: Accessors;
date: Accessor;
};
config: {
x: SimpleScaleConfig;
y: SimpleScaleConfig;
};
animationTrajectory: AnimationTrajectory;
data: CityTemperature[];
numTicks: number;
renderHorizontally: boolean;
renderBarSeries: boolean;
renderBarStack: boolean;
renderLineSeries: boolean;
sharedTooltip: boolean;
showGridColumns: boolean;
Expand All @@ -29,7 +63,7 @@ type ControlsProps = {
export default function ExampleControls({ children }: ControlsProps) {
const [theme, setTheme] = useState<XYChartTheme>(darkTheme);
const [animationTrajectory, setAnimationTrajectory] = useState<AnimationTrajectory>('center');
const [gridProps, setGridProps] = useState<[boolean, boolean]>([true, true]);
const [gridProps, setGridProps] = useState<[boolean, boolean]>([false, false]);
const [showGridRows, showGridColumns] = gridProps;
const [xAxisOrientation, setXAxisOrientation] = useState<'top' | 'bottom'>('bottom');
const [yAxisOrientation, setYAxisOrientation] = useState<'left' | 'right'>('right');
Expand All @@ -40,24 +74,63 @@ export default function ExampleControls({ children }: ControlsProps) {
const [snapTooltipToDatumX, setSnapTooltipToDatumX] = useState(true);
const [snapTooltipToDatumY, setSnapTooltipToDatumY] = useState(true);
const [sharedTooltip, setSharedTooltip] = useState(true);
const [renderBarSeries, setRenderBarSeries] = useState(true);
const [renderLineSeries, setRenderLineSeries] = useState(true);
const [renderBarOrBarStack, setRenderBarOrBarStack] = useState<'bar' | 'barstack'>('barstack');
const [renderLineSeries, setRenderLineSeries] = useState(false);
const [negativeValues, setNegativeValues] = useState(false);

const accessors = useMemo(
() => ({
x: {
'San Francisco': renderHorizontally
? negativeValues
? getNegativeSFTemperature
: getSfTemperature
: getDate,
'New York': renderHorizontally ? getNyTemperature : getDate,
Austin: renderHorizontally ? getAustinTemperature : getDate,
},
y: {
'San Francisco': renderHorizontally
? getDate
: negativeValues
? getNegativeSFTemperature
: getSfTemperature,
'New York': renderHorizontally ? getDate : getNyTemperature,
Austin: renderHorizontally ? getDate : getAustinTemperature,
},
date: getDate,
}),
[renderHorizontally, negativeValues],
);

const config = useMemo(
() => ({
x: renderHorizontally ? temperatureScaleConfig : dateScaleConfig,
y: renderHorizontally ? dateScaleConfig : temperatureScaleConfig,
}),
[renderHorizontally],
);

return (
<>
{children({
accessors,
animationTrajectory,
renderBarSeries,
config,
data: renderBarOrBarStack === 'bar' ? data : dataSmall,
numTicks,
renderBarSeries: renderBarOrBarStack === 'bar',
renderBarStack: renderBarOrBarStack === 'barstack',
renderHorizontally,
renderLineSeries,
renderLineSeries: renderBarOrBarStack === 'bar' && renderLineSeries,
sharedTooltip,
showGridColumns,
showGridRows,
showHorizontalCrosshair,
showTooltip,
showVerticalCrosshair,
snapTooltipToDatumX,
snapTooltipToDatumY,
snapTooltipToDatumX: renderBarOrBarStack === 'bar' && snapTooltipToDatumX,
snapTooltipToDatumY: renderBarOrBarStack === 'bar' && snapTooltipToDatumY,
theme,
xAxisOrientation,
yAxisOrientation,
Expand Down Expand Up @@ -237,7 +310,7 @@ export default function ExampleControls({ children }: ControlsProps) {
<label>
<input
type="checkbox"
disabled={!showTooltip}
disabled={!showTooltip || renderBarOrBarStack === 'barstack'}
onChange={() => setSnapTooltipToDatumX(!snapTooltipToDatumX)}
checked={showTooltip && snapTooltipToDatumX}
/>{' '}
Expand All @@ -246,7 +319,7 @@ export default function ExampleControls({ children }: ControlsProps) {
<label>
<input
type="checkbox"
disabled={!showTooltip}
disabled={!showTooltip || renderBarOrBarStack === 'barstack'}
onChange={() => setSnapTooltipToDatumY(!snapTooltipToDatumY)}
checked={showTooltip && snapTooltipToDatumY}
/>{' '}
Expand Down Expand Up @@ -293,12 +366,32 @@ export default function ExampleControls({ children }: ControlsProps) {
</label>
<label>
<input
type="checkbox"
onChange={() => setRenderBarSeries(!renderBarSeries)}
checked={renderBarSeries}
type="radio"
onChange={() => setRenderBarOrBarStack('bar')}
checked={renderBarOrBarStack === 'bar'}
/>{' '}
bar
</label>
<label>
<input
type="radio"
onChange={() => setRenderBarOrBarStack('barstack')}
checked={renderBarOrBarStack === 'barstack'}
/>{' '}
bar stack
</label>
</div>
{/** data */}
<div>
<strong>data</strong>
<label>
<input
type="checkbox"
onChange={() => setNegativeValues(!negativeValues)}
checked={negativeValues}
/>{' '}
negative values (SF)
</label>
</div>
</div>
<style jsx>{`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { buildChartTheme } from '@visx/xychart';

export default buildChartTheme({
backgroundColor: '#f09ae9',
colors: ['rgba(255,231,143,0.8)', '#6a097d', '#ffc1fa'],
colors: ['rgba(255,231,143,0.8)', '#6a097d', '#d6e0f0'],
gridColor: '#336d88',
gridColorDark: '#1d1b38',
labelStyles: { fill: '#1d1b38' },
Expand Down
3 changes: 2 additions & 1 deletion packages/visx-xychart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"@visx/tooltip": "1.0.0",
"@visx/voronoi": "1.0.0",
"classnames": "^2.2.5",
"d3-array": "2.6.0",
"d3-array": "^2.6.0",
"d3-shape":"^2.0.0",
"mitt": "^2.1.0",
"prop-types": "^15.6.2"
},
Expand Down
5 changes: 2 additions & 3 deletions packages/visx-xychart/src/components/series/BarSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import findNearestDatumY from '../../utils/findNearestDatumY';
import useEventEmitter, { HandlerParams } from '../../hooks/useEventEmitter';
import TooltipContext from '../../context/TooltipContext';

type BarSeriesProps<
export type BarSeriesProps<
XScale extends AxisScale,
YScale extends AxisScale,
Datum extends object
Expand Down Expand Up @@ -88,12 +88,11 @@ function BarSeries<XScale extends AxisScale, YScale extends AxisScale, Datum ext

const { showTooltip, hideTooltip } = useContext(TooltipContext) ?? {};
const handleMouseMove = useCallback(
(params: HandlerParams | undefined) => {
(params?: HandlerParams) => {
const { svgPoint } = params || {};
if (svgPoint && width && height && showTooltip) {
const datum = (horizontal ? findNearestDatumY : findNearestDatumX)({
point: svgPoint,
key: dataKey,
data,
xScale,
yScale,
Expand Down
Loading

0 comments on commit bcae42d

Please sign in to comment.