RFC: Proposal for new `xy-chart` package
See original GitHub issueMotivation
vx
packages are (by design) low-level and modular which maximizes their flexibility, but also requires more work to create even simple charts. My library @data-ui/xy-chart
is built on top of vx
and was designed to make it easier to create common charts with less work. To solve this use-case within the vx
ecosystem, and consolidate these efforts, @data-ui/xy-chart
is being deprecated and we plan to port its functionality to a new vx
package @vx/xy-chart
.
Goals of this RFC
- align on near- and mid-term features
- align on an
XYChart
API - align on a general direction for implementation
Features
To have feature parity with @data-ui/xychart
, the new package should support the following:
- Computes and provides
x-
andy- scales
across all data series - Handles mouse events for the
XYChart
across all data series- Positions tooltips and provides tooltip data
- Supports programmatic tooltip control
- Mouse events can be defined at chart or series level, can be disabled at series level
- Supports the following
*Series
which are mostly wrappers aroundvx
Bar
,Line
,Point
,Area
,AreaDifference
,StackedArea
,StackedBar
,GroupedBar
,BoxPlot
,ViolinPlot
,Interval
- Exports
CrossHair
component for use withTooltip
- Exports
XAxis
andYAxis
components fromvx
- Supports
horizontal
andvertical
reference lines
- Supports
x-
andy-gridlines
- Supports
brush
functionality (pre@vx/brush
) - Supports styles with
LinearGradient
,Patterns
, and a chart theme viaprops
- Wraps individual points in
FocusBlur
handlers that area11y
accessible / tab-able
We would also like to add additional support for the following:
New near-term features
- re-written in
TypeScript
- responsive by default
- supports arbitrary
datum
shape +x
/y
data accessors (currently requires{ x, y }
datum
shape) - first-class
hooks
support
New mid-term features
- optionally render
Tooltip
in aPortal
to fix z-index stacking context problem - easy creation of
Legend
s - better support for overlays / annotations (points + regions)
- integration with
@vx
primitives likebrush
,zoom
, anddrag
canvas
support –vx
is currently mostlysvg
based (this likely requires updates in othervx
packages)- animation –
@data-ui
does not support animation. while this may not need to be fully baked in we should expose hooks to animatexy-chart
API
@techniq has done some great research on declarative react
chart APIs here. Generally they are composable:
<Chart {...}>
<Axis {...} />
<Gridlines {...} />
<Legend />
<DataSeries {...} />
<DataSeries {...} />
</Chart>
However there are some key differences:
data
provided at the DataSeries
level vs the chart
container level
DataSeries
level – ✅ favored
() => <Chart><DataSeries data={...} /></Chart>
Pros 👍
- it’s more “natural” to directly link
data
to the series that will visually represent it @vx/shape
’s (the basis forDataSeries
) currently require data, so this is more consistent with separate package APIs- Series can use any custom logic they need for computing the
x-
andy-
extent from theirdata
, and theChart
container can simply collect these- additionally this allows more advanced functionality like
horizontal
orientation to be pushed to theSeries
-level without requiring it to be implemented by allSeries
and theChart
container doesn’t need to have any knowledge of it
- additionally this allows more advanced functionality like
Cons 👎
Chart
container needs a way to access the data across all series
Chart
container level – ❌ disfavored
() => <Chart data={...}><DataSeries /></Chart>
Pros 👍
- ultimately the
Chart
needs access to all data in order to providex-
andy-
scales; this makes that easy.
Cons 👎
Series
may require custom logic to computex-
andy-
extent from theirdata
(e.g., a bar stack) which theChart
needs to be aware of in this model- still requires
key
accessors at theSeries
-level forSeries
data - forces all
DataSeries
to have the same data length (or be filled with empty values)
Mouse and Touch events
Mouse and touch events are handled in several ways
-
Standard mouse events – ✅ favored
react-vis
and@data-ui
expose mouse and touch events at both theChart
andSeries
level; these use standardreact
events such asonClick
,onTouchMove
, etc. -
Custom event syntax – ❌ disfavored Some libraries like
Victory
have their own custom event system with non-standard syntax and selection language.
react
hooks
I’ve not been able to find any react
vis libraries which expose hooks
. Feels like an opportunity on top of a render / component API 😏
Implementation
We’d like to improve upon the following limitations of @data-ui
v1
implementation:
-
Written in TypeScript
@data-ui
was written in JavaScript, butvx
is now a TypeScript project and typings will be similarly useful for@data-ui
. -
Use
react
context
over cloning children@data-ui
was implemented before the new / more robust16.3
context
API. Therefore chartstyles
and sharedscales
are passed viaprop
s +React.cloneElement
. Combined withhooks
, usingcontext
should open up a whole new set of API possibilities (see below).
What is kept in context
The function of the XYChart
wrapper largely equates to managing shared state across the elements of a chart, which components can leverage as needed. . This includes
- chart
theme
+styles
xScale
that accounts for data range of all chart series and chartwidth
+margin
yScale
that accounts for data range of all chart series and chartheight
+margin
tooltipData
+tooltipCoords
, when applicable
In addition to chart width
+ height
, the Chart
container must have knowledge of all data
(+ annotation
) values to compute scale
s appropriately. Rather than having the Chart
introspect props from child
DataSeries
or Annotations
(which can get gnarly) we propose that DataSeries
and Annotations
register their data
, xValues
, and yValues
in context
.
This pushes the logic of data <> x/y extent
mapping to DataSeries
rather than the Chart
, and allows the Chart
to leverage these values to compute scales properly. It could look something like
// in e.g., <LineSeries {...props} />
const { key, data, xAccessor, yAccessor } = props;
const { registerData } = useContext(XYChart);
registerData({
dataKey: key,
xValues: data.map(d => xAccessor(d)),
yValues: data.map(d => yAccessor(d)),
// other interesting things to do at this level
mouseEvents: false,
legendItemRenderer,
});
Unknowns
I’m unsure if there are major performance implications of using hooks ⚡ 🐌
Proposed API
// all of these items have access to the same Chart `context` which includes
// theme, dataRegistry, xScale, yScale, colorScale, tooltipData, tooltipCoords
const { ChartProvider, XYChart, Legend, Tooltip } = useChart({ theme, scaleConfig, ... }));
() => (
{/* context provider */}
<ChartProvider>
{/**
* Chart renders `svg` container and computes `x-` and `y-scale`s using the
* data registry context. It is either passed `width`/`height`, or it uses
* `@vx/responsive` for auto-sizing
*/}
<XYChart>
{/**
* DataSeries register their data on mount. When `x-` and `y-scale`s
* are computed and available in context they render data.
*/}
<LineSeries key={dataRegistryKey} data={...} />
{/**
* Axes use `x-` or `y-scale`s from context based on orientation.
*/}
<Axis orientation="left" />
{/* Custom axis component could use `scale`s from context */}
<CustomAxis />
</XYChart>
{/**
* Tooltip is `html`-based so should be rendered outside the chart `svg`
* It has access to `tooltipData` and `tooltipCoords` from context.
*/}
<Tooltip renderInPortal={boolean} />
{/**
* Legend is `html`-based so should be rendered outside the chart `svg`.
* It has access to all series via `dataRegistry` from context,
* or we could add a legend renderer registry.
*/}
<Legend />
</ChartProvider>
)
The same functionality could be provided in a component
API:
import { ChartProvider, XYChart, Legend, Tooltip } from '@vx/xy-chart`
() => (
<ChartProvider theme={...} {...scaleConfig} >
<XYChart {...} />
<Legend {...} />
<Tooltip {...} />
</ChartProvider>
)
Issue Analytics
- State:
- Created 3 years ago
- Reactions:8
- Comments:25
Top GitHub Comments
Okay finally! (sorry there were some problems with other
vx
packages that I fixed locally, that then wouldn’t work in the sandbox)Here’s the PR #745 🎉 Have a lot of things working (love the dark mode + animation!), really curious about your feedback on the
react-spring
approach. I tried this approach and youruseAnimatedScale
variant but couldn’t actually get that to interpolate/tween values correctly. Can share that exploration if you want.Comment-3
Now the
XYChart
magically computes and setsdomain
for the entire chart based on all given data. There might be times when developers wish to override thedomain
computation, such as to add min/max bounds, zeroing, manually specify domain. Could we add one more optional hook to theXYChart
props which sets the default value to the current behavior.