Skip to content

Instantly share code, notes, and snippets.

@itszero
Last active May 2, 2016 07:30
Show Gist options
  • Save itszero/15ba2916b25dd13f6300a297c0f18889 to your computer and use it in GitHub Desktop.
Save itszero/15ba2916b25dd13f6300a297c0f18889 to your computer and use it in GitHub Desktop.

Declarative d3 graph (Decl3?)

This is a work-in-progress very early stage project/proposal.

Example

Decl3 is a side project trying to create a composable/declarative d3-react bridge.

Motivation

In most React-D3 wrapper libraries, the flow of graph configs (scale, axis colors...) are either one of those:

  • pull-up to container and pass-down multiple times
  • provide a wrapper for each kind of charts, which does the passing down.

The problem with the first way is, obviously, duplicity. And for the second one is that you would have to create and maintain a wrapper object for each kind of graph.

Proposed Solution

Inspired by react-redux interface. We add a makeGlobalSettings(globalProps, ownProps) static method onto those components that might generate a reusable value. In the D3 container, we simply iterate through children, collecting those reusable values and reapply them to those children to be used in render time.

It's like a React context, but children to children.

Downside

  • We probably would not be able to recursively do this.
  • Key in the config would need to be consistent.

Questions

  • Do I miss some libraries that has already implemented in this way?
  • Is there some potential downside I have not think through?
  • Is this scalable to a large chart?

Some draft code follows.

import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom';
import d3 from 'd3';
import styles from './styles.css';
function getDefaultRange(axis, props) {
switch (axis) {
case 'x':
return [0, props.width];
case 'y':
return [props.height, 0];
default:
return [0, 1];
}
}
function getDefaultOrient(axis) {
switch (axis) {
case 'x':
return 'bottom';
case 'y':
return 'left';
default:
return 'top';
}
}
class Axis extends React.Component {
static makeGlobalProps(oldProps, ownProps) {
const scale = d3.scale.linear()
.domain(ownProps.domain)
.range(ownProps.range || getDefaultRange(ownProps.axis, oldProps));
const orient = ownProps.orient || getDefaultOrient(ownProps.axis);
const axis = d3.svg.axis()
.scale(scale)
.orient(orient);
return {
...oldProps,
axies: {
...(oldProps.axies || {}),
[ownProps.axis]: { scale, orient, axis }
}
};
}
componentDidMount() { this._renderAxis(); }
componentDidUpdate() { this._renderAxis(); }
render() {
const { margin, width, height } = this.props.config;
const { orient } = this.props.config.axies[this.props.axis];
const translateY = orient === 'bottom' ? height : 0;
const translateX = orient === 'right' ? width : 0;
return (
<g
className={styles.root}
transform={`translate(${translateX}, ${translateY})`}
/>
);
}
_renderAxis() {
const { axis } = this.props.config.axies[this.props.axis];
d3.select(ReactDOM.findDOMNode(this))
.call(axis);
}
}
export default Axis;
import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom';
import d3 from 'd3';
const DEFAULT_MARGIN = {
left: 20,
right: 20,
top: 20,
bottom: 20
};
class D3 extends React.Component {
static propTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
margin: PropTypes.object
};
render() {
const { children, height, margin: pMargin, width } = this.props;
const margin = {...DEFAULT_MARGIN, ...(pMargin || {})};
const aggregatedProps = this._collectProps(children);
const newChildren = this._applyProps(aggregatedProps, children);
return (
<svg
width={width + margin.left + margin.right}
height={height + margin.top + margin.bottom}
>
<g transform={`translate(${margin.left}, ${margin.top})`}>
{ newChildren }
</g>
</svg>
);
}
_collectProps(children) {
children = this._makeArray(children);
const { children: _, ...graphProps } = this.props;
return children.reduce((props, child) => (
child && child.type.makeGlobalProps ?
child.type.makeGlobalProps(props, child.props) :
props
), graphProps);
}
_applyProps(props, children) {
children = this._makeArray(children);
return children.map((child, i) => React.cloneElement(child, {
config: props,
key: `${i}`
}));
}
_makeArray(objs) {
return (Array.isArray(objs) ? objs : [objs]).filter((obj) => !!obj);
}
}
export default D3;
import React from 'react';
import D3 from '../D3';
import Axis from '../Axis';
import Line from '../Line';
const DATA ={
a: [30, 25, 23, 4, 8, 16, 32, 64, 128, 256],
b: [192, 64, 78, 50, 90, 100, 99, 40, 120, 50],
c: [256, 128, 64, 32, 16, 8, 4, 24, 122, 50]
};
const Home = () => (
<div className="container">
<D3 width={760} height={260} margin={{left: 50}}>
<Axis axis='x' domain={[0, 9]}/>
<Axis axis='y' domain={[0, 256]}/>
<Line data={DATA.a} color='red'/>
<Line data={DATA.b} color='blue'/>
<Line data={DATA.c} color/>
</D3>
</div>
);
export default Home;
import React, { PropTypes } from 'react';
import d3 from 'd3';
const svgLine = (axies) => {
return d3.svg.line()
.x((d, i) => axies.x.scale(d.x || i))
.y((d, i) => axies.y.scale(d.y || d));
};
const Line = ({ color, config, data, strokeWidth }) => (
<g>
<path
d={svgLine(config.axies)(data)}
fill='none'
stroke={color}
strokeWidth={strokeWidth}
/>
</g>
);
Line.propTypes = {
color: PropTypes.string,
strokeWidth: PropTypes.number
};
Line.defaultProps = {
color: 'black',
strokeWidth: 5
};
export default Line;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment