Skip to content

RFC: Proposal for new xy-chart package #734

@williaster

Description

@williaster

Motivation

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- and y- 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 around vx
    • Bar, Line, Point, Area, AreaDifference, StackedArea, StackedBar, GroupedBar, BoxPlot, ViolinPlot, Interval
  • Exports CrossHair component for use with Tooltip
  • Exports XAxis and YAxis components from vx
  • Supports horizontal and vertical reference lines
  • Supports x- and y-gridlines
  • Supports brush functionality (pre @vx/brush)
  • Supports styles with LinearGradient, Patterns, and a chart theme via props
  • Wraps individual points in FocusBlur handlers that are a11y 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 a Portal to fix z-index stacking context problem
  • easy creation of Legends
  • better support for overlays / annotations (points + regions)
  • integration with @vx primitives like brush, zoom, and drag
  • canvas support – vx is currently mostly svg based (this likely requires updates in other vx packages)
  • animation – @data-ui does not support animation. while this may not need to be fully baked in we should expose hooks to animate xy-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

  1. 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 for DataSeries) currently require data, so this is more consistent with separate package APIs
  • Series can use any custom logic they need for computing the x- and y- extent from their data, and the Chart container can simply collect these
    • additionally this allows more advanced functionality like horizontal orientation to be pushed to the Series-level without requiring it to be implemented by all Series and the Chart container doesn't need to have any knowledge of it

Cons 👎

  • Chart container needs a way to access the data across all series

  1. Chart container level – ❌ disfavored
() => <Chart data={...}><DataSeries /></Chart>

Pros 👍

  • ultimately the Chart needs access to all data in order to provide x- and y- scales; this makes that easy.

Cons 👎

  • Series may require custom logic to compute x- and y- extent from their data (e.g., a bar stack) which the Chart needs to be aware of in this model
  • still requires key accessors at the Series-level for Series 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

  1. Standard mouse events – ✅ favored
    react-vis and @data-ui expose mouse and touch events at both the Chart and Series level; these use standard react events such as onClick, onTouchMove, etc.

  2. 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:

  1. Written in TypeScript
    @data-ui was written in JavaScript, but vx is now a TypeScript project and typings will be similarly useful for @data-ui.

  2. Use react context over cloning children
    @data-ui was implemented before the new / more robust 16.3 context API. Therefore chart styles and shared scales are passed via props + React.cloneElement. Combined with hooks, using context 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 chart width + margin
  • yScale that accounts for data range of all chart series and chart height + margin
  • tooltipData + tooltipCoords, when applicable

In addition to chart width + height, the Chart container must have knowledge of all data (+ annotation) values to compute scales 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>
)

cc @hshoff @techniq @kristw

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions