Skip to content
This repository has been archived by the owner on Sep 7, 2020. It is now read-only.

Adding mixin with animation support #41

Merged
merged 16 commits into from
May 10, 2016
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules/
bundle.js
Copy link
Owner

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.

Copy link
Contributor Author

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.

14 changes: 14 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have all mixins in one? Shouldn't it be ReactFauxDOM.mixins.animation? I'm not sure on the convention here though. It would give the user more control, right? I just wouldn't want to accidentally pollute the scope of some component.

Copy link
Owner

Choose a reason for hiding this comment

The 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 :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 connectFauxDOM and drawFauxDOM into mixins/core, and the rest into mixins/animate?

Copy link
Owner

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 mixins: [core, anim]. Which doesn't matter much, I guess, just feels a bit unclean. My gut instinct is to make users explicitly load both mixins. What do you think?

Copy link
Owner

Choose a reason for hiding this comment

The 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.

26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Owner

Choose a reason for hiding this comment

The 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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency this should be [An example animated chart using the mixin][mixin-example] then add [mixin-example]: ./examples/animate-d3-with-mixin to the bottom of the document with the other links.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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][]

Expand Down
45 changes: 45 additions & 0 deletions examples/animate-d3-with-mixin/index.html
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>
156 changes: 156 additions & 0 deletions examples/animate-d3-with-mixin/index.js
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')
Copy link
Owner

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 this.connectedFauxDOM object, only the fauxnode.toReact() result is ever stored in state (that is, sent to this.setState). Or perhaps I'm misusing the label here? In my mind virtual DOM is the sketetal representative object structure mirroring the DOM, which is what fauxnode.toReact outputs, right?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, you're right. If you're storing the toReact result, it's virtual DOM. I didn't spot that.


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'))
18 changes: 18 additions & 0 deletions examples/animate-d3-with-mixin/package.json
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"
}
2 changes: 2 additions & 0 deletions lib/ReactFauxDOM.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
var Element = require('./Element')
var Window = require('./Window')
var mixin = require('./mixin')

var ReactFauxDOM = {
Element: Element,
defaultView: Window,
mixin: mixin,
createElement: function (nodeName) {
return new Element(nodeName)
},
Expand Down
44 changes: 44 additions & 0 deletions lib/mixin.js
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)
Copy link
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Owner

Choose a reason for hiding this comment

The 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 n D3 animations as long as they all finished within the timeout.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 connected elements. As is, you're animating them all or none. Could perhaps extend the signature like this: animateFauxDOM(duration [, nameofconnectednodetoanimate ])

Copy link
Owner

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're probably right. Connecting more than one element is probably an edge case as is, anyway. And the performance win would be very small since React makes the comparison awfully quick.

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
Loading