@visx/xychart
Version:
Composable cartesian coordinate chart built with visx primitives
410 lines (319 loc) • 14.6 kB
Markdown
# /xychart
<a title="@visx/xychart npm downloads" href="https://www.npmjs.com/package/@visx/xychart">
<img src="https://img.shields.io/npm/dm/@visx/xychart.svg?style=flat-square" />
</a>
In contrast to other `visx` packages which are low-level, this package seeks to abstract some of the
complexity of common visualization engineering, and exposes a **high-level** x,y (cartesian
coordinate) chart API. However, it is implemented using modularized `React.context` layers for
theme, canvas dimensions, x/y/color scales, data, events, and tooltips which allows for more
expressivity and advanced use cases.
Out of the box it supports the following:
- \* many common `<*Series />` types (animated or not) such as lines, bars, etc.
- \* `<Axis />` (animated or not)
- \* `<Grid />` (animated or not)
- \* `<Annotation />` (animated or not)
- \* `<Tooltip />`
- \* `theme`ing
The following illustrates basic usage to create an animated line chart with a bottom `Axis`, `Grid`,
and `Tooltip`:
```tsx
import {
AnimatedAxis, // any of these can be non-animated equivalents
AnimatedGrid,
AnimatedLineSeries,
XYChart,
Tooltip,
} from '/xychart';
const data1 = [
{ x: '2020-01-01', y: 50 },
{ x: '2020-01-02', y: 10 },
{ x: '2020-01-03', y: 20 },
];
const data2 = [
{ x: '2020-01-01', y: 30 },
{ x: '2020-01-02', y: 40 },
{ x: '2020-01-03', y: 80 },
];
const accessors = {
xAccessor: (d) => d.x,
yAccessor: (d) => d.y,
};
const render = () => (
<XYChart height={300} xScale={{ type: 'band' }} yScale={{ type: 'linear' }}>
<AnimatedAxis orientation="bottom" />
<AnimatedGrid columns={false} numTicks={4} />
<AnimatedLineSeries dataKey="Line 1" data={data1} {...accessors} />
<AnimatedLineSeries dataKey="Line 2" data={data2} {...accessors} />
<Tooltip
snapTooltipToDatumX
snapTooltipToDatumY
showVerticalCrosshair
showSeriesGlyphs
renderTooltip={({ tooltipData, colorScale }) => (
<div>
<div style={{ color: colorScale(tooltipData.nearestDatum.key) }}>
{tooltipData.nearestDatum.key}
</div>
{accessors.xAccessor(tooltipData.nearestDatum.datum)}
{', '}
{accessors.yAccessor(tooltipData.nearestDatum.datum)}
</div>
)}
/>
</XYChart>
);
```
See sections below for more detailed guidance and advanced usage, or explore the comprehensive API
below.
<hr />
## Basic usage
<details>
<summary>Installation</summary>
```
npm install --save /xychart react-spring
```
Note: `react-spring` is a required `peerDependency` for importing `Animated*` components.
</details>
<details>
<summary>Series types</summary>
The following `Series` types are currently supported and we are happy to review or consider
additional Series types in the future.
| Component name | Description | Usage |
| --------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------- | --- |
| (Animated)AreaSeries | Connect data points with a `<path />`, with a color fill to the zero baseline | `<AreaSeries />` |
| (Animated)BarSeries | Render a `<rect />` for each data point | `<BarSeries />` |
| (Animated)BarGroup | Group multiple child `<BarSeries />` values together | `<BarGroup><BarSeries /><BarSeries />...</BarGroup>` |
| (Animated)BarStack | Stack multiple child `<BarSeries />` values together | `<BarStack><BarSeries /><BarSeries />...</BarStack>` | |
| (Animated)GlyphSeries | Render a `Glyph` (any shape, defaults to `<circle />`) for each data point, e.g., a scatter plot | `<GlyphSeries renderGlyph={() => ...} />` |
| (Animated)LineSeries | Connect data points with a `<path>` | `<GlyphSeries />` |
All `Series` have animated and non-animated variants to give you more control over your bundle size,
support missing (`null`) data, and can be rendered vertically or horizontally.
</details>
<details>
<summary>Theming</summary>
Default `lightTheme` and `darkTheme` themes are exported from `/xychart` and the utility
`buildChartTheme` is exported to support easy creation of custom themes.
```ts
import { buildChartTheme, XYChart } from '/xychart';
import { TextProps as SVGTextProps } from '/text/lib/Text'; // just for types
const customTheme = buildChartTheme({
// colors
backgroundColor: string; // used by Tooltip, Annotation
colors: string[]; // categorical colors, mapped to series via `dataKey`s
// labels
svgLabelBig?: SVGTextProps;
svgLabelSmall?: SVGTextProps;
htmlLabel?: HTMLTextStyles;
// lines
xAxisLineStyles?: LineStyles;
yAxisLineStyles?: LineStyles;
xTickLineStyles?: LineStyles;
yTickLineStyles?: LineStyles;
tickLength: number;
// grid
gridColor: string;
gridColorDark: string; // used for axis baseline if x/yxAxisLineStyles not set
gridStyles?: CSSProperties;
});
() => <XYChart theme={customTheme} />
```
</details>
<details>
<summary>Tooltips</summary>
`/tooltip` `Tooltip`s are integrated into `@visx/xychart`, and should be rendered as a child of
`XYChart` (or a child where `TooltipContext` is provided).
**`Tooltip` positioning** is handled by the `Tooltip` itself, based on `TooltipContext`. `Tooltip`
is rendered inside a `Portal`, avoiding clipping by parent DOM elements with higher z-index
contexts. See the API below for a full list of `props` to support additional behavior, such as
snapping to data point positions and rendering cross-hairs.
**`Tooltip` content** is controlled by the specified `prop.renderTooltip` which has access to:
- `tooltipData.nearestDatum` – the globally closest `Datum`, **across all** `Series`'s `dataKey`s
- `tooltipData.datumByKey` – the closest `Datum` **for each** `Series`'s `dataKey`; this enables
"shared tooltips" where you can render the nearest data point for each `Series`.
- a shared `colorScale` which maps `Series`'s `dataKey`s to `theme` colors
</details>
<details>
<summary>Event handlers</summary>
The following `PointerEvent`s (handling both `MouseEvent`s and `TouchEvent`s) are currently
supported. They may be set on individual `Series` components (e.g.,
`<BarSeries onPointerMove={() => ...} />`), or at the chart level (e.g.,
`<XYChart onPointerMove={() => {}} />`) in which case they are invoked once for _every_ `*Series`.
To **disable** event emitting for any `Series` set `<*Series enableEvents=false />`. The
`onFocus/onBlur` handlers enable you to make your chart events and `Tooltip`s accessible via
keyboard interaction. Note that the current implementation requires your target browser to support
the `SVG 2.0` spec for `tabIndex` on `SVG` elements.
Below, `HandlerParms` has the following type signature:
```ts
type EventHandlerParams<Datum> = {
datum: Datum; // nearest Datum to event, for Series with `dataKey=key`
distanceX: number; // x distance between event and Datum, in px
distanceY;: number; // y distance between event and Datum, in px
event: React.PointerEvent | React.FocusEvent; // the event
index: number; // index of Datum in Series `data` array
key: string; // `dataKey` of Series to which `Datum` belongs
svgPoint: { x: number; y: number }; // event position in svg-coordinates
};
```
| Prop name | Signature | `XYChart` support | `*Series` support |
| --------------- | --------------------------------------------- | ----------------- | ----------------- |
| `onPointerMove` | `(params: EventHandlerParams<Datum>) => void` | ✅ | ✅ |
| `onPointerOut` | `(event: React.PointerEvent) => void` | ✅ | ✅ |
| `onPointerUp` | `(params: EventHandlerParams<Datum>) => void` | ✅ | ✅ |
| `onPointerDown` | `(params: EventHandlerParams<Datum>) => void` | ✅ | ✅ |
| `onFocus` | `(params: EventHandlerParams<Datum>) => void` | ❌ | ✅ |
| `onBlur` | `(event: React.TouchEvent) => void` | ❌ | ✅ |
</details>
<details>
<summary>Annotations</summary>
Composable `/annotations` annotations are integrated into `@visx/xychart` and use its theme and
dimension context. These components allow for annotation of individual points using
`AnnotationCircleSubject`, or x- or y-thresholds using `AnnotationLineSubject`.
[CodeSandbox](https://codesandbox.io/s/annotations-8npmf?file=/Example.tsx)
```tsx
import React from 'react';
import {
Annotation,
AnnotationLabel,
AnnotationConnector,
AnnotationCircleSubject,
Grid,
LineSeries,
XYChart,
} from '/xychart';
const data = [
{ x: '2020-01-01', y: 50 },
{ x: '2020-01-02', y: 10 },
{ x: '2020-01-03', y: 20 },
{ x: '2020-01-04', y: 5 },
];
const labelXOffset = -40;
const labelYOffset = -50;
const chartConfig = {
xScale: { type: 'band' },
yScale: { type: 'linear' },
height: 300,
margin: { top: 10, right: 10, bottom: 10, left: 10 },
};
export default () => (
<XYChart {...chartConfig}>
<Grid numTicks={3} />
<LineSeries dataKey="line" data={data} xAccessor={d => d.x} yAccessor={d => d.y} />
<Annotation
dataKey="line" // use this Series's accessor functions, alternatively specify x/yAccessor here
datum={data[2]}
dx={labelXOffset}
dy={labelYOffset}
>
{/** Text label */}
<AnnotationLabel
title="Title"
subtitle="Subtitle deets"
showAnchorLine={false}
backgroundFill="rgba(0,150,150,0.1)"
/>
{/** Draw circle around point */}
<AnnotationCircleSubject />
{/** Connect label to CircleSubject */}
<AnnotationConnector />
</AnimatedAnnotation>
</XYChart>
);
```
</details>
<hr />
##### ⚠️ `ResizeObserver` dependency
Responsive `XYChart`s, `Tooltip`, and `AnnotationLabel` components rely on
[`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)s. If your
browser target needs a polyfill, you can either pollute the `window` object or inject it cleanly
using the `resizeObserverPolyfill` prop for these components. A polyfill passed to `XYChart` will be
accessible to child `Tooltip` and `AnnotationLabel` components.
<details>
<summary>Examples ✅ ❌</summary>
❌ `Error: This browser does not support ResizeObserver out of the box`
```tsx
// no polyfill, no browser support
() => <XYChart {...} />
() => <XYChart {...}><Tooltip /></XYChart>
```
✅ No errors
```tsx
// no polyfill, target browser supports ResizeObserver
() => <XYChart {...} />
() => <XYChart {...}><Tooltip /></XYChart>
// import the polyfill in the needed module, or set it on `window` object
import ResizeObserver from 'resize-observer-polyfill';
() => <XYChart {...}><Tooltip /></XYChart> // 😎
// cleanly pass polyfill to component that needs it
import ResizeObserver from 'resize-observer-polyfill';
() => (
<XYChart resizeObserverPolyfill={ResizeObserver} {...}>
<Tooltip />
</XYChart>
)
```
</details>
<hr />
## Advanced usage
<details>
<summary>Examples</summary>
`XYChart` is implemented using modularized `React.context` layers for scales, canvas dimensions,
data, events, and tooltips which enables more advanced usage than many other chart-level
abstractions.
By default `XYChart` renders all context providers if a given context is not available, but you can
share context across multiple `XYChart`s to implement functionality such as linked tooltips, shared
themes, or shared data.
- [`ThemeProvider` + custom theme chart background example](https://codesandbox.io/s/themeprovider-sbdvz?file=/Example.tsx)
- [`DataProvider/EventEmitterProvider` example of linked tooltips / small multiples](https://codesandbox.io/s/linked-tooltips-7s0jz?file=/Example.tsx)
- [`TooltipProvider` example of programmatic + keyboard tooltip triggering](https://codesandbox.io/s/programmatic-tooltips-hh7ly?file=/Example.tsx)
</details>
<details>
<summary>DataContext</summary>
This context provides chart canvas dimensions (`width`, `height`, and `margin`), x/y/color scales,
and a data registry. The data registry includes data from all child `*Series`, and x/y/color scales
are updated accordingly accounting for canvas dimensions.
</details>
<details>
<summary>ThemeContext</summary>
This context provides an `XYChart` theme, its used by all visual elements that compose a chart, and
can be used to render custom visual elements that are on theme.
</details>
<details>
<summary>EventEmitterContext</summary>
This context provides an event publishing / subscription object which can be used via the
`useEventEmitter` hook. `Series` and `XYChart` events, including tooltip updates, are emitted and
handled with through this context.
[CodeSandbox](https://codesandbox.io/s/eventemitterprovider-w8jhl?file=/Example.tsx)
```tsx
import React, { useState } from 'react';
import { useEventEmitter, EventEmitterProvider } from '/xychart';
const eventSourceId = 'optional-source-id-filter';
const EmitEvent = () => {
const emit = useEventEmitter();
return (
<button onPointerUp={(event) => emit('pointerup', event, eventSourceId)}>emit event</button>
);
};
const SubscribeToEvent = () => {
const [clickCount, setClickCount] = useState(0);
const allowedEventSources = [eventSourceId];
useEventEmitter('pointerup', () => setClickCount(clickCount + 1), allowedEventSources);
return <div>Emitted {clickCount} events</div>;
};
export default function Example() {
return (
<EventEmitterProvider>
<EmitEvent />
<SubscribeToEvent />
</EventEmitterProvider>
);
}
```
</details>
<details>
<summary>TooltipContext</summary>
This context provides access to `/tooltip`s `useTooltip` state, including whether the tooltip
is visible (`tooltipOpen`), tooltlip position (`tooltipLeft`, `tooltipTop`),
`tooltipData: { nearestDatum, datumByKey }` described above, and functions to update context
(`hideTooltip`, `showTooltip`, and `updateTooltip`).
</details>
<hr />