-
Notifications
You must be signed in to change notification settings - Fork 79
Adding mixin with animation support #41
Changes from 10 commits
fbe5a86
2afe696
7a8c5b1
4ccf62e
0bdb9cd
64321b2
fcabe05
efb0d69
393347e
09a75f1
4fde2e9
f6d6cf4
cab8f95
57bb5d2
df75803
fce075d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
node_modules/ | ||
bundle.js | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -89,3 +89,17 @@ render () { | |
|
||
[jsdom]: https://github.com/tmpvar/jsdom | ||
[d3]: http://d3js.org/ | ||
|
||
## React component mixin | ||
|
||
You also have access to a mixin which makes it easy to render faux nodes, and to animate them while they're being mutated by a DOM library like for example D3. | ||
|
||
The mixin gives you the following methods: | ||
|
||
* **`connectFauxDOM(node,name)`**: This will store `node` in `this.connectedFauxDOM[name]`, and make an asynchronous call to `drawFauxDOM`. The node can be a faux element or a string, in which case a faux element is instantiated. The node is returned for convenience. A component can have multiple connected nodes. | ||
* **`drawFauxDOM()`**: This will update component state (causing a render) with virtual DOM (through `node.toReact()`) for all previously `connect`ed faux nodes. Each node's representation will be on `this.state[name]`, where `name` is the one used in the `connect` call. | ||
* **`animateFauxDOM(duration)`**: This will make a call to `drawFauxDOM` every 16 milliseconds until the duration has expired. | ||
* **`stopAnimatingFauxDOM()`**: Cancels eventual ongoing animation | ||
* **`isAnimatingFauxDOM()`**: Returns true or false depending on whether an animation is ongoing. | ||
|
||
The mixin will also take care of the necessary setup and teardown. To see the mixin in action, check out the `animate-d3-with-mixin` mini-app in the `examples` folder. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we have all mixins in one? Shouldn't it be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that a bunch of mixins is a good way to add custom support for things. We just need it generic so we can add more in the future :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, yeah, that makes sense! We might just want to use the mixin for convenient static rendering. So, put There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds great to me! I prefer the plural there too, naming wise. Keep all animation specific stuff in a namespace. Then documentation of mixins can explain how core gives you these things, animation these other things. Will they be dependant on each other? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now we have two mixins, although I haven't updated the docs yet. The anim one assumes you've already loaded the core one. We could of course include core in anim, but that would mean the users get duplicate life cycle hooks if they do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think including both explicitly is the way to go, so if you had core, anim and some other thing that depended on core you wouldn't duplicate it. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,32 +19,32 @@ return el.toReact() | |
// Yields: <div style='color: red;' className='box'></div> | ||
``` | ||
|
||
It supports a wide range of DOM operations and will fool most libraries but it isn't exhaustive (the full DOM API is ludicrously large). It supports enough to work with D3 but will require you to fork and add to the project if you encounter something that's missing. | ||
There's also a **mixin** with animation support: | ||
|
||
You can think of this as a bare bones [jsdom][] that's built to bridge the gap between the declarative React and the imperative JavaScript world. We just need to expand it as we go along since jsdom is a huge project that solves different problems. | ||
```javascript | ||
// inside componentWillMount | ||
var faux = this.connectFauxDOM('div','chart'); | ||
d3.doingAnAdvancedAnimationFor3secs(faux); | ||
this.animateFauxDOM(3500); // duration + margin | ||
|
||
I'm trying to keep it light so as not to slow down your render function. I want efficient, declarative and stateless code, but I don't want to throw away previous tools to get there. | ||
// inside render | ||
return {this.state.chart}; | ||
``` | ||
|
||
## Limitations | ||
|
||
It's great for... | ||
React-faux-DOM supports a wide range of DOM operations and will fool most libraries but it isn't exhaustive (the full DOM API is ludicrously large). It supports enough to work with D3 but will require you to fork and add to the project if you encounter something that's missing. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: If it's hyphenated keep it all lowercase and usually wrapped in backticks (for a code block). In this context I would write it as "ReactFauxDOM" or just "The faux DOM..." |
||
|
||
* Static D3 components or other such libraries (things like Backbone should work too!) | ||
* D3 components with simple state and event interaction, like tooltips on charts | ||
* D3 components such as progress bars that can be animated using [react-motion][], for example | ||
You can think of this as a bare bones [jsdom][] that's built to bridge the gap between the declarative React and the imperative JavaScript world. We just need to expand it as we go along since jsdom is a huge project that solves different problems. | ||
|
||
It's not so great for... | ||
I'm trying to keep it light so as not to slow down your render function. I want efficient, declarative and stateless code, but I don't want to throw away previous tools to get there. | ||
|
||
* Physics based D3 components or anything using a lot of DOM mutation and state | ||
* Linked to the previous one, brushing and filtering of selections using the built in stateful D3 tools | ||
* Essentially: Anything with a lot of DOM mutation from timers, events or internal state will be hard to use | ||
|
||
If you keep it stateless and React-ish then you'll be fine. Use tools like D3 to fluently build your charts / DOM, don't use it as an animation / physics / DOM mutation library, that doesn't work within React. See the state example linked below for a simple way to handle state, events and D3. | ||
|
||
## Usage | ||
|
||
* Full [documentation][] with current DOM API coverage | ||
* [An example static chart ][lab-chart] ([source][lab-chart-source]) | ||
* [An example animated chart using the mixin](./examples/animate-d3-with-mixin) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For consistency this should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whoops, of course, completely missed those! |
||
* [A simple example using state and events][lab-state] ([source][lab-state-source]) | ||
* [d3-react-sparkline][], a small component I built at [Qubit][] | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width"> | ||
<title>Mixin example</title> | ||
<style type="text/css"> | ||
|
||
button { | ||
position: absolute; | ||
margin: 5em; | ||
} | ||
|
||
/* styles below are from Mike Bostock's D3 chart used in the example | ||
https://bl.ocks.org/mbostock/3943967 */ | ||
|
||
body { | ||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; | ||
margin: auto; | ||
position: relative; | ||
} | ||
|
||
text { | ||
font: 10px sans-serif; | ||
} | ||
|
||
.axis path, | ||
.axis line { | ||
fill: none; | ||
stroke: #000; | ||
shape-rendering: crispEdges; | ||
} | ||
|
||
form { | ||
position: absolute; | ||
right: 10px; | ||
top: 10px; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<div id="container"></div> | ||
<script type="text/javascript" src="./bundle.js"></script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
var React = require('react') | ||
var ReactDOM = require('react-dom') | ||
var Faux = require('../../lib/ReactFauxDOM') | ||
var d3 = require('d3') | ||
|
||
var Chart = React.createClass({ | ||
mixins: [Faux.mixin], | ||
getInitialState: function () { | ||
return { look: 'stacked' } | ||
}, | ||
render: function () { | ||
return <div> | ||
<button onClick={this.toggle}>Toggle</button> | ||
{this.state.chart} | ||
</div> | ||
}, | ||
toggle: function () { | ||
if (this.state.look === 'stacked') { | ||
this.setState({ look: 'grouped' }) | ||
this.transitionGrouped() | ||
} else { | ||
this.setState({ look: 'stacked' }) | ||
this.transitionStacked() | ||
} | ||
}, | ||
componentDidMount: function () { | ||
// This will create a faux div and store its virtual DOM | ||
// in state.chart | ||
var faux = this.connectFauxDOM('div', 'chart') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By virtual DOM, you mean faux DOM, right? So we store the faux DOM so D3 has access to the nodes the whole way through the animation? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless I'm confusing myself, I do mean virtual DOM. Although the faux nodes are stored in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, you're right. If you're storing the |
||
|
||
var component = this | ||
|
||
/* | ||
D3 code below by Mike Bostock, https://bl.ocks.org/mbostock/3943967 | ||
The only changes made for this example are... | ||
|
||
1) feeding D3 the faux node created above | ||
2) calling this.animateFauxDOM(duration) after each animation kickoff | ||
3) attaching the radio button callbacks to the component | ||
4) deleting the radio button (as we do the toggling through the react button) | ||
|
||
*/ | ||
|
||
var n = 4 // number of layers | ||
var m = 58 // number of samples per layer | ||
var stack = d3.layout.stack() | ||
var layers = stack(d3.range(n).map(function () { return bumpLayer(m, 0.1) })) | ||
var yGroupMax = d3.max(layers, function (layer) { return d3.max(layer, function (d) { return d.y }) }) | ||
var yStackMax = d3.max(layers, function (layer) { return d3.max(layer, function (d) { return d.y0 + d.y }) }) | ||
|
||
var margin = {top: 40, right: 10, bottom: 20, left: 10} | ||
var width = 960 - margin.left - margin.right | ||
var height = 500 - margin.top - margin.bottom | ||
|
||
var x = d3.scale.ordinal() | ||
.domain(d3.range(m)) | ||
.rangeRoundBands([0, width], 0.08) | ||
|
||
var y = d3.scale.linear() | ||
.domain([0, yStackMax]) | ||
.range([height, 0]) | ||
|
||
var color = d3.scale.linear() | ||
.domain([0, n - 1]) | ||
.range(['#aad', '#556']) | ||
|
||
var xAxis = d3.svg.axis() | ||
.scale(x) | ||
.tickSize(0) | ||
.tickPadding(6) | ||
.orient('bottom') | ||
|
||
var svg = d3.select(faux).append('svg') | ||
.attr('width', width + margin.left + margin.right) | ||
.attr('height', height + margin.top + margin.bottom) | ||
.append('g') | ||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') | ||
|
||
var layer = svg.selectAll('.layer') | ||
.data(layers) | ||
.enter().append('g') | ||
.attr('class', 'layer') | ||
.style('fill', function (d, i) { return color(i) }) | ||
|
||
var rect = layer.selectAll('rect') | ||
.data(function (d) { return d }) | ||
.enter().append('rect') | ||
.attr('x', function (d) { return x(d.x) }) | ||
.attr('y', height) | ||
.attr('width', x.rangeBand()) | ||
.attr('height', 0) | ||
|
||
rect.transition() | ||
.delay(function (d, i) { return i * 10 }) | ||
.attr('y', function (d) { return y(d.y0 + d.y) }) | ||
.attr('height', function (d) { return y(d.y0) - y(d.y0 + d.y) }) | ||
|
||
this.animateFauxDOM(800) | ||
|
||
svg.append('g') | ||
.attr('class', 'x axis') | ||
.attr('transform', 'translate(0,' + height + ')') | ||
.call(xAxis) | ||
|
||
this.transitionGrouped = function () { | ||
y.domain([0, yGroupMax]) | ||
|
||
rect.transition() | ||
.duration(500) | ||
.delay(function (d, i) { return i * 10 }) | ||
.attr('x', function (d, i, j) { return x(d.x) + x.rangeBand() / n * j }) | ||
.attr('width', x.rangeBand() / n) | ||
.transition() | ||
.attr('y', function (d) { return y(d.y) }) | ||
.attr('height', function (d) { return height - y(d.y) }) | ||
|
||
component.animateFauxDOM(2000) | ||
} | ||
|
||
this.transitionStacked = function () { | ||
y.domain([0, yStackMax]) | ||
|
||
rect.transition() | ||
.duration(500) | ||
.delay(function (d, i) { return i * 10 }) | ||
.attr('y', function (d) { return y(d.y0 + d.y) }) | ||
.attr('height', function (d) { return y(d.y0) - y(d.y0 + d.y) }) | ||
.transition() | ||
.attr('x', function (d) { return x(d.x) }) | ||
.attr('width', x.rangeBand()) | ||
|
||
component.animateFauxDOM(2000) | ||
} | ||
|
||
// Inspired by Lee Byron's test data generator. | ||
function bumpLayer (n, o) { | ||
function bump (a) { | ||
var x = 1 / (0.1 + Math.random()) | ||
var y = 2 * Math.random() - 0.5 | ||
var z = 10 / (0.1 + Math.random()) | ||
for (var i = 0; i < n; i++) { | ||
var w = (i / n - y) * z | ||
a[i] += x * Math.exp(-w * w) | ||
} | ||
} | ||
|
||
var a = [] | ||
var i | ||
for (i = 0; i < n; ++i) a[i] = o + o * Math.random() | ||
for (i = 0; i < 5; ++i) bump(a) | ||
return a.map(function (d, i) { return {x: i, y: Math.max(0, d)} }) | ||
} | ||
} | ||
}) | ||
|
||
ReactDOM.render(<Chart/>, document.getElementById('container')) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"name": "react-faux-dom-example-mixin", | ||
"version": "0.0.1", | ||
"description": "An example of how to use the React-Faux-DOM mixin to animate D3", | ||
"main": "index.js", | ||
"devDependencies": { | ||
"browserify": "^13.0.0", | ||
"d3": "^3.5.16", | ||
"react": "^0.14.6", | ||
"react-dom": "^0.14.6", | ||
"reactify": "^1.1.1" | ||
}, | ||
"scripts": { | ||
"build": "browserify -t reactify index.js -o bundle.js" | ||
}, | ||
"author": "David Waller", | ||
"license": "Unlicense" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
var Element = require('./Element') | ||
var mapValues = require('./utils/mapValues') | ||
|
||
var mixin = { | ||
componentWillMount: function () { | ||
this.connectedFauxDOM = {} | ||
this.animateFauxDOMUntil = 0 | ||
}, | ||
connectFauxDOM: function (node, name) { | ||
this.connectedFauxDOM[name] = typeof node !== 'string' ? node : new Element(node) | ||
setTimeout(this.drawFauxDOM) | ||
return this.connectedFauxDOM[name] | ||
}, | ||
drawFauxDOM: function () { | ||
var virtualDOM = mapValues(this.connectedFauxDOM, function (n) { | ||
return n.toReact() | ||
}) | ||
this.setState(virtualDOM) | ||
}, | ||
animateFauxDOM: function (duration) { | ||
this.animateFauxDOMUntil = Math.max(Date.now() + duration, this.animateFauxDOMUntil) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The architecture only allows one animation at a time if I'm not mistaken with a locked framerate. Will that make some D3 animations fall over? I really like the mixin approach, it's a great way to plug stuff in and integrate the faux DOM with a component, but I'm not sure about 16ms intervals with hard coded lengths and only one animation at a time. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I suppose it's not one at a time, you could have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exactly. However I'm beginning to think we should support animating a subset of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, smells like complexity would creep in here though. Mixins are an elegant way to hook in, but these timeouts and manual tie ins to nodes and trees is beginning to feel a little bad to me. I don't know if there's a nicer way though. What if it was D3 specific, and named that way, would that allow you to hook in nicely? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're probably right. |
||
if (!this.fauxDOMAnimationInterval) { | ||
this.fauxDOMAnimationInterval = setInterval(function () { | ||
if (Date.now() < this.animateFauxDOMUntil) { | ||
this.drawFauxDOM() | ||
} else { | ||
this.stopAnimatingFauxDOM() | ||
} | ||
}.bind(this), 16) | ||
} | ||
}, | ||
stopAnimatingFauxDOM: function () { | ||
this.fauxDOMAnimationInterval = clearInterval(this.fauxDOMAnimationInterval) | ||
this.animateFauxDOMUntil = 0 | ||
}, | ||
isAnimatingFauxDOM: function () { | ||
return !!this.fauxDOMAnimationInterval | ||
}, | ||
componentWillUnmount: function () { | ||
this.stopAnimatingFauxDOM() | ||
} | ||
} | ||
|
||
module.exports = mixin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May as well move this to
examples/animate-d3-with-mixin/.gitignore
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes a ton of more sense. Good catch, will do.