Skip to content

Instantly share code, notes, and snippets.

@timhall
Last active November 18, 2015 15:23
Show Gist options
  • Save timhall/d16594db294fc3c62b76 to your computer and use it in GitHub Desktop.
Save timhall/d16594db294fc3c62b76 to your computer and use it in GitHub Desktop.
d3.compose functional

Goals

Apply functional techniques to make d3.compose easier to reason about, more testable, and better match d3's standard approach

Implementation

Use a "smart"/"dumb" components approach (reference) with "smart" component handling logic, state, and context and "dumb" component handling rendering.

  • The "dumb" component is stateless and is a simple, idempotent function that takes in a d3.selection and properties and renders the chart.
  • The "smart" component provides the API for interacting with Compose, prepares properties for the "dumb" component, and handles any logic/state
// "smart" component
export default class Bars extends Chart {
prepare() {
// Map data, scale, etc to props for "dumb" component
return {
bars: this.props.data.map(d => {/* ... */})
};
}
render() {
const props = this.prepare();
draw(this.base, props);
}
}
// "dumb" component
export function draw(selection, props) {
// Standard d3 draw
// select, enter, update, exit
}
import Chart from './Chart';
import {createDraw, createPrepare} from './helpers';
import {prepareSeries} from './helpers/series';
import {prepareXY} from './helpers/xy';
import {prepareValues} from './helpers/values';
export default class Bars extends Chart {
prepare() {
prepare(this.base, this.props);
}
render() {
const props = this.prepare();
draw(this.base, props)
}
}
export const draw = createDraw({
select,
enter,
update,
merge,
exit
});
export function select(props) {
// "this" = selection
return this.selectAll('rect')
.data(props.bars, d => d.key);
}
export function enter(props) {
this.append('rect');
}
export function update(props) {
// ...
}
export function merge(props) {
this
.attr('width', d => d.width)
.attr('height', d => d.height);
}
export function exit(props) {
this.remove();
}
export const prepare = createPrepare(
(selection, props) => {
// defaults...
return props;
},
prepareSeries,
prepareXY,
prepareValues,
prepareBars
);
export function prepareBars(selection, props) {
return {
...props,
bars: props.data.map(d => {/* ... */})
};
}
// helpers.js
export function createDraw(steps) {
return (selection, props) => {
const {
select,
enter,
update,
merge,
exit,
transitions
} = prepareSteps(steps, props);
const selected = selection.call(select);
selected.exit().call(exit)
if (transitions.exit)
selected = selected.transition().call(transitions.exit);
selected.call(update)
if (transitions.update)
selected = selected.transition().call(transitions.update);
selected.enter().call(enter)
if (transitions.enter)
selected = selected.transition().call(transitions.enter);
selected.call(merge)
if (transitions.merge)
selected = selected.transition().call(transitions.merge);
};
}
export function createPrepare(steps...) {
return (selection, props) => {
return steps.reduce((memo, step) => step(selection, memo), props);
}
}
export function prepareSteps(steps, props) {
const {
select = function() { return this; },
enter = () => {},
update = () => {},
merge = () => {},
exit = function() { this.remove(); }
} = steps;
const transitions = {
enter: steps.enterTransition,
update: steps.updateTransition,
merge: steps.mergeTransition,
exit: steps.exitTransition
};
return {
select: curry(select, props),
enter: curry(enter, props),
update: curry(update, props),
merge: curry(merge, props),
exit: curry(exit, props),
transitions: prepareTransitions(transitions, props)
}
}
export function prepareTransitions(transitions, props) {
// ...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment