Skip to content

Commit

Permalink
Merge 7640e5b into 7064863
Browse files Browse the repository at this point in the history
  • Loading branch information
williaster committed Sep 12, 2020
2 parents 7064863 + 7640e5b commit 9089297
Show file tree
Hide file tree
Showing 18 changed files with 575 additions and 27 deletions.
5 changes: 5 additions & 0 deletions package.json
Expand Up @@ -82,6 +82,11 @@
"react": true,
"next": true
},
"babel": {
"plugins": [
["@babel/plugin-proposal-private-methods", { "loose": false }]
]
},
"typescript": {
"compilerOptions": {
"emitDeclarationOnly": true
Expand Down
@@ -0,0 +1,40 @@
import React, { useContext } from 'react';
import { PatternLines } from '@vx/pattern';
import { DataContext } from '@vx/xychart';

const patternId = 'xy-chart-pattern';

export default function CustomChartBackground() {
const { theme, margin, width, height } = useContext(DataContext);
const textStyles = { ...theme?.axisStyles.x.bottom.axisLabel, textAnchor: 'start' };

// early return values not available in context
if (width == null || height == null || margin == null || theme == null) return null;

return (
<>
<PatternLines
id={patternId}
width={10}
height={10}
orientation={['diagonal']}
stroke={theme?.gridStyles?.stroke}
strokeWidth={1}
/>
<rect x={0} y={0} width={width} height={height} fill={theme?.backgroundColor ?? '#fff'} />
<rect
x={margin.left}
y={margin.top}
width={width - margin.left - margin.right}
height={height - margin.top - margin.bottom}
fill={`url(#${patternId})`}
/>
<text x={margin.left} y={height - margin.top + margin.bottom / 2} {...textStyles}>
width {width}px
</text>
<g transform={`translate(${margin.left / 1.3}, ${height - margin.bottom})rotate(-90)`}>
<text {...textStyles}>height {height}px</text>
</g>
</>
);
}
22 changes: 17 additions & 5 deletions packages/vx-demo/src/sandboxes/vx-xychart/Example.tsx
@@ -1,20 +1,32 @@
import React, { useState } from 'react';
import { XYChart, ThemeProvider, lightTheme, XYChartTheme } from '@vx/xychart';
import cityTemperature, { CityTemperature } from '@vx/mock-data/lib/mocks/cityTemperature';
import { XYChart, LineSeries, DataProvider, darkTheme, XYChartTheme } from '@vx/xychart';

import Controls from './Controls';
import CustomChartBackground from './CustomChartBackground';

type Props = {
width: number;
height: number;
};

const xScaleConfig = { type: 'linear' } as const;
const yScaleConfig = xScaleConfig;
const data = cityTemperature.slice(50, 80);
const getDate = (d: CityTemperature) => new Date(d.date);
const getSfTemperature = (d: CityTemperature) => Number(d['San Francisco']);

export default function Example(_: Props) {
const [theme, setTheme] = useState<XYChartTheme>(lightTheme);
const [theme, setTheme] = useState<XYChartTheme>(darkTheme);

return (
<>
<ThemeProvider theme={theme}>
<XYChart />
</ThemeProvider>
<DataProvider theme={theme} xScale={xScaleConfig} yScale={yScaleConfig}>
<XYChart height={400}>
<CustomChartBackground />
<LineSeries dataKey="line" data={data} xAccessor={getDate} yAccessor={getSfTemperature} />
</XYChart>
</DataProvider>
<Controls theme={theme} setTheme={setTheme} />
</>
);
Expand Down
2 changes: 2 additions & 0 deletions packages/vx-demo/src/sandboxes/vx-xychart/package.json
Expand Up @@ -7,6 +7,8 @@
"@babel/runtime": "^7.8.4",
"@types/react": "^16",
"@types/react-dom": "^16",
"@vx/mock-data": "latest",
"@vx/pattern": "latest",
"@vx/responsive": "latest",
"@vx/xychart": "latest",
"react": "^16",
Expand Down
5 changes: 5 additions & 0 deletions packages/vx-xychart/package.json
Expand Up @@ -42,6 +42,11 @@
"dependencies": {
"@types/classnames": "^2.2.9",
"@types/react": "*",
"@vx/axis": "0.0.199",
"@vx/responsive": "0.0.199",
"@vx/scale": "0.0.199",
"@vx/shape": "0.0.199",
"d3-array": "2.6.0",
"classnames": "^2.2.5",
"prop-types": "^15.6.2"
}
Expand Down
57 changes: 57 additions & 0 deletions packages/vx-xychart/src/classes/DataRegistry.ts
@@ -0,0 +1,57 @@
import { AxisScale } from '@vx/axis';
import { DataRegistryEntry } from '../types/data';

/** A class for holding data entries */
export default class DataRegistry<
XScale extends AxisScale,
YScale extends AxisScale,
Datum = unknown
> {
private registry: { [key: string]: DataRegistryEntry<XScale, YScale, Datum> } = {};

private registryKeys: string[] = [];

/** Add one or more entries to the registry. */
public registerData(
entryOrEntries:
| DataRegistryEntry<XScale, YScale, Datum>
| DataRegistryEntry<XScale, YScale, Datum>[],
) {
const entries = Array.isArray(entryOrEntries) ? entryOrEntries : [entryOrEntries];
entries.forEach(currEntry => {
if (currEntry.key in this.registry && this.registry[currEntry.key] != null) {
console.debug('Overriding data registry key', currEntry.key);
this.registryKeys = this.registryKeys.filter(key => key !== currEntry.key);
}
this.registry[currEntry.key] = currEntry;
this.registryKeys.push(currEntry.key);
});
}

/** Remove one or more entries to the registry. */
public unregisterData(keyOrKeys: string | string[]) {
const keys = Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys];
keys.forEach(currKey => {
delete this.registry[currKey];
this.registryKeys = this.registryKeys.filter(key => key !== currKey);
});
}

/** Returns all data registry entries. This value is not constant between calls. */
public entries() {
return Object.values(this.registry);
}

/** Returns a specific entity from the registry, if it exists. */
public get(key: string) {
return this.registry[key];
}

/**
* Returns the current registry keys.
* This value is constant between calls if the keys themselves have not changed.
*/
public keys() {
return this.registryKeys;
}
}
56 changes: 36 additions & 20 deletions packages/vx-xychart/src/components/XYChart.tsx
@@ -1,22 +1,38 @@
import React, { useContext } from 'react';
import ThemeContext from '../context/ThemeContext';
import React, { useContext, useEffect } from 'react';
import ParentSize from '@vx/responsive/lib/components/ParentSize';

export default function XYChart() {
const theme = useContext(ThemeContext);
return (
<div
style={{
width: '100%',
height: 400,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: theme.backgroundColor,
border: `1px solid ${theme?.gridStyles?.stroke}`,
...theme.htmlLabelStyles,
}}
>
XYChart
</div>
);
import DataContext from '../context/DataContext';
import { Margin } from '../types';

const DEFAULT_MARGIN = { top: 50, right: 50, bottom: 50, left: 50 };

type Props = {
events?: boolean;
width?: number;
height?: number;
margin?: Margin;
children: React.ReactNode;
};

export default function XYChart(props: Props) {
const { children, width, height, margin = DEFAULT_MARGIN } = props;
const { setDimensions } = useContext(DataContext);

// update dimensions in context
useEffect(() => {
if (setDimensions && width != null && height != null && width > 0 && height > 0) {
setDimensions({ width, height, margin });
}
}, [setDimensions, width, height, margin]);

// if width and height aren't both provided, wrap in auto-sizer + preserve passed dims
if (width == null || height == null) {
return <ParentSize>{dims => <XYChart {...dims} {...props} />}</ParentSize>;
}

return width > 0 && height > 0 ? (
<svg width={width} height={height}>
{children}
</svg>
) : null;
}
79 changes: 79 additions & 0 deletions packages/vx-xychart/src/components/series/LineSeries.tsx
@@ -0,0 +1,79 @@
import React, { useContext, useCallback, useEffect } from 'react';
import LinePath from '@vx/shape/lib/shapes/LinePath';
import { ScaleConfig, ScaleConfigToD3Scale, ScaleInput } from '@vx/scale';
import { AxisScaleOutput } from '@vx/axis';
import DataContext from '../../context/DataContext';
import isValidNumber from '../../typeguards/isValidNumber';

type LineSeriesProps<
XScaleConfig extends ScaleConfig<AxisScaleOutput>,
YScaleConfig extends ScaleConfig<AxisScaleOutput>,
Datum
> = {
dataKey: string;
data: Datum[];
xAccessor: (
d: Datum,
) => // eslint-disable-next-line @typescript-eslint/no-explicit-any
ScaleInput<ScaleConfigToD3Scale<XScaleConfig, AxisScaleOutput, any, any>>;
yAccessor: (
d: Datum,
) => // eslint-disable-next-line @typescript-eslint/no-explicit-any
ScaleInput<ScaleConfigToD3Scale<YScaleConfig, AxisScaleOutput, any, any>>;
};

export default function LineSeries<
XScaleConfig extends ScaleConfig<AxisScaleOutput>,
YScaleConfig extends ScaleConfig<AxisScaleOutput>,
Datum
>({
data,
xAccessor,
yAccessor,
dataKey,
...lineProps
}: LineSeriesProps<XScaleConfig, YScaleConfig, Datum>) {
const { xScale, yScale, colorScale, dataRegistry } = useContext(DataContext);

// register data on mount
// @TODO(chris) make this easier with HOC
useEffect(() => {
if (dataRegistry) dataRegistry.registerData({ key: dataKey, data, xAccessor, yAccessor });
return () => dataRegistry?.unregisterData(dataKey);
}, [dataRegistry, dataKey, data, xAccessor, yAccessor]);

const getScaledX = useCallback(
(d: Datum) => {
const x = xScale?.(xAccessor(d));
return isValidNumber(x)
? x + (xScale && 'bandwidth' in xScale ? xScale?.bandwidth?.() ?? 0 : 0) / 2
: NaN;
},
[xScale, xAccessor],
);

const getScaledY = useCallback(
(d: Datum) => {
const y = yScale?.(yAccessor(d));
return isValidNumber(y)
? y + (yScale && 'bandwidth' in yScale ? yScale?.bandwidth?.() ?? 0 : 0) / 2
: NaN;
},
[yScale, yAccessor],
);

if (!data || !xAccessor || !yAccessor) return null;

const color = colorScale?.(dataKey) ?? '#222';

return (
<LinePath
data={data}
x={getScaledX}
y={getScaledY}
stroke={color}
strokeWidth={2}
{...lineProps}
/>
);
}
37 changes: 37 additions & 0 deletions packages/vx-xychart/src/context/DataContext.tsx
@@ -0,0 +1,37 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import { AxisScale } from '@vx/axis';
import { DataContextType } from '../types';

type AnyDataContext = DataContextType<AxisScale, AxisScale, any>;

/** Utilities for infering context generics */
export type InferXScaleConfig<X extends AnyDataContext> = X extends DataContextType<
infer T,
any,
any
>
? T
: AxisScale;

export type InferYScaleConfig<X extends AnyDataContext> = X extends DataContextType<
any,
infer T,
any
>
? T
: AxisScale;

export type InferDatum<X extends AnyDataContext> = X extends DataContextType<any, any, infer T>
? T
: any;

export type InferDataContext<C extends AnyDataContext = AnyDataContext> = DataContextType<
InferXScaleConfig<C>,
InferYScaleConfig<C>,
InferDatum<C>
>;

const DataContext = React.createContext<Partial<InferDataContext>>({});

export default DataContext;
12 changes: 12 additions & 0 deletions packages/vx-xychart/src/hooks/useDataRegistry.ts
@@ -0,0 +1,12 @@
import { AxisScale } from '@vx/axis';
import { useMemo } from 'react';
import DataRegistry from '../classes/DataRegistry';

/** Hook that returns a constant instance of a DataRegistry. */
export default function useDataRegistry<
XScale extends AxisScale,
YScale extends AxisScale,
Datum = unknown
>() {
return useMemo(() => new DataRegistry<XScale, YScale, Datum>(), []);
}

0 comments on commit 9089297

Please sign in to comment.