CSNW / d3.compose

Compose complex, data-driven visualizations from reusable charts and components with d3
http://CSNW.github.io/d3.compose/
MIT License
697 stars 24 forks source link

d3.compose vNext #43

Closed timhall closed 8 years ago

timhall commented 8 years ago

Goals

The interaction of property get/set, transform, layer, and initialize made chart and components difficult to debug and maintain. The updated architecture uses a simple "stateless" function for drawing charts and components that more closely matches the standard d3 approach.

function Circles(selection, props) {
  var circles = selection.selectAll('circle')
    .data(props.data, (d) => d.key);

  circles.enter()
    .append('circle');

  circles
    .attr('cx', d => d.x)
    .attr('cy', d => d.y)
    .attr('r', d => d.r);
}

function LinesAndCircles(selection, props) {
  // "Layered" chart calls stateless functions for each layer
  Lines(getLayer(selection, 'lines'), props.lines);
  Circles(getLayer(selection, 'circles'), props.circles);
}

Helpers

In order to keep some of the convenience of d3.chart, helpers have been added for composing stateless draw functions.

const Bars = createSeriesDraw({
  select(props) {
    return this.selectAll('rect')
      .data(props.data, props.key);
  },

  enter(props) {
    const {yValue, yScale} = props;
    this.append('rect')
      .attr('y', (d, i) => bar0(yValue, yScale))
      .attr('height', 0);
  },

  merge(props) {
    const {xValue, yValue, xScale, yScale, offset, className, style, transition} = props;
    this
      .attr('x', (d, i, j) => barX(xValue, xScale, d, i, j))
      .attr('y', (d, i) => barY(yValue, yScale, offset, d, i))
      .attr('width', barWidth(xScale))
      .attr('height', (d, i) => barHeight(yValue, yScale, offset, d, i))
      .attr('class', className)
      .style(style)
  }
})

Updated render

Dynamic charts became difficult to test and reason about as they became more complex. The new system takes a chart "description" and renders to the given container.

function barChart(props) {
  const xScale = d3.scale.ordinal()
    .domain(getOrdinalDomain(props.data, d => d.x));

  return [
    d3c.title({text: props.title}),
    d3c.bars({data: props.data, xScale}),
    d3c.axis({scale: xScale})
  ];
}

const container = new Compose(d3.select('#chart'));

container.draw(barChart({data: [1, 2, 3], title: 'Bar Chart'}))

Connect and Subscribe

There are two ways that charts/components interact with Compose: subscribing to top-level events (e.g. mouse events) and connecting and sharing information with other charts. Currently both of these flow through the central pub-sub system, but connecting with other charts is brittle and requires each chart/component handle events properly. The new system separate these concerns into two parts: a one-way events system that charts can subscribe to and a central state system that charts can read from and dispatch actions to change. Additionally, in order to work with stateless charts and components, two wrappers will be used to subscribe and connect. This area is developing, but is inspired by redux and react-redux.

TODO

Lots of work left to do, but it's primarily just translating into the new system.

Architecture

Charts

Components

timhall commented 8 years ago

Ok, the first huge push for vNext is finished. There are still many things left to do to bring it up to parity with v0.15 (including all documentation), but that work is ready to be done on master. Merging!