A JavaScript library for dataflow programming.
See also Topologica.js, a minimalist rewrite of this library.
This library provides an abstraction for reactive data flows. This means you can define so-called "reactive functions" in terms of their inputs and output, and the library will take care of executing these functions in the correct order. When input properties change, those changes are propagated through the data flow graph based on topological sorting.
The reactive-model stack for interactive data visualizations.
reactive-property |
graph-data-structure |
reactive-function |
D3
Table of Contents
You can include reactive-model in your HTML like this (will introduce a global variable ReactiveModel
):
<script src="https://github.com/datavis-tech/reactive-model/raw/master//datavis-tech.github.io/reactive-model/reactive-model-v0.12.0.min.js"></script>
If you are using NPM, install with npm install reactive-model
, then require the module in your code like this:
var ReactiveModel = require("reactive-model");
Full Name Greeting |
Responding to Resize |
Margin Convention |
Margin Convention II |
Responsive Axes |
Baseball Scatter Plot |
Responsive Axes with React |
Here is an example where b
gets set to a + 1
whenever a
changes:
var my = ReactiveModel()
("a") // Create the property "a" with no default value.
("b", function (a){
return a + 1;
}, "a");
When a changes, b gets updated.
The naming convention of my
pays homage to Towards Reusable Charts.
Here's an example that assign b = a + 1
and c = b + 1
.
function increment(x){ return x + 1; }
var my = ReactiveModel()
("a", 5) // Create the property "a" with a default value of 5.
("b", increment, "a")
("c", increment, "b");
Here, b is both an output and an input.
See also ABC in reactive-function.
Here's an example that shows a reactive function with two inputs, where e = c + d
.
function add(x, y){ return x + y; }
var my = ReactiveModel()
("c", 5)
("d", 10)
("e", add, ["c", "d"]);
A reactive function with two inputs.
Consider a Web application that greets a user. The user can enter his or her first name and last name, and the application will display a greeting using their full name. To start with, we can construct a ReactiveModel instance and add properties firstName
and lastName
(with no default values).
var my = ReactiveModel()
("firstName")
("lastName");
After properties are added, they are exposed as chainable getter-setters on my
. Here's how you can set their values.
my.firstName("Jane")
.lastName("Smith");
Next, we set up a reactive function that computes fullName
.
my("fullName", function (firstName, lastName){
return firstName + " " + lastName;
}, "firstName, lastName");
The data flow graph for the example code above.
Once we have fullName
defined, we can use it as an input to another reactive function that computes the greeting.
my("greeting", function (fullName){
return "Hello " + fullName + "!";
}, "fullName");
The updated data flow graph including the greeting.
When input properties are defined, the changes will automatically propagate on the next animation frame. If you don't want to wait until the next animation frame for changes to propagate, you can force synchronous propagation by invoking digest.
ReactiveModel.digest();
This ensures that the value of computed properties will be immediately available. We can access them like this.
console.log(my.fullName()); // Prints "Jane Smith"
console.log(my.greeting()); // Prints "Hello Jane Smith!"
Reactive functions that have side effects but no output value can be defined by omitting the output property name argument. This is useful for DOM manipulation, such as passing the greeting text into a DOM element using D3.
my(function (greeting){
d3.select("#greeting").text(greeting);
}, "greeting");
Reactive functions with no output property add unnamed nodes to the data flow graph.
Here's a complete working example that extends the above example code to interact with DOM elements.
Reactive functions can be combined to create arbitrarily complex data flow graphs. Here's an example that demonstrates why topological sorting is the correct algorithm for computing the order in which to execute reactive functions. In this graph, propagation using breadth-first search (which is what Model.js and some other libraries use) would cause e
to be set twice, and the first time it would be set with an inconsistent state. Using topological sorting for change propagation guarantees that e
will only be set once, and there will never be inconsistent states.
The tricky case, where breadth-first propagation fails.
function increment(x){ return x + 1; }
function add(x, y){ return x + y; }
var my = ReactiveModel()
("a", 5)
("b", increment, "a")
("c", increment, "b")
("d", increment, "a")
("e", add, "b, d");
See also Tricky Case in reactive-function.
Here's a similar case that reactive-model handles correctly. If breadth-first search were used in this case, then h
would get set 3 times, the first two times with an inconsistent state.
Another tricky case where breadth-first propagation fails.
function increment(x){ return x + 1; }
function add3(x, y, z){ return x + y + z; }
var my = ReactiveModel()
("a", 5)
("b", increment, "a")
("c", increment, "b")
("d", increment, "c")
("e", increment, "a")
("f", increment, "e")
("g", increment, "a")
("h", add3, "d, f, g");
For more detailed example code, have a look at the tests.
# ReactiveModel()
Constructs a new reactive model instance.
Example:
var model = ReactiveModel();
# model.destroy()
Cleans up resources allocated to this model. Invokes
You should invoke this function when finished using model instances in order to avoid memory leaks.
# model(propertyName[, defaultValue])
Adds a property to the model. Returns the model to support chaining.
Arguments:
After a property is added, it is exposed as an instance of reactive-property on the model object at model[propertyName]
.
Example:
var model = ReactiveModel();
// Add property "a" with a default value of 5.
model("a", 5);
// Acces the value of "a".
console.log(model.a()); // Prints 5.
// Set the value of "a".
model.a(10);
// Acces the default value of "a".
console.log(model.a.default()); // Prints 5.
See also reactive-property.
# model([output,] callback, inputs)
Adds a reactive function to this model.
Arguments:
"a, b"
), or["a", "b"]
).The callback will be invoked:
An input property is considered "defined" if it has any value other than undefined
(null
is considered defined).
An input property is considered "changed" when
Any input property for one reactive function may also be the output of another.
Here's an example of an asynchronous reactive function.
var model = ReactiveModel()
("a", 50)
("b", function (a, done){
setTimeout(function (){
done(a + 1);
}, 500);
}, "a");
See also ReactiveFunction.
# ReactiveModel.link(propertyA, propertyB)
Sets up one-way data binding from propertyA to propertyB. Returns an instance of ReactiveFunction.
This can be used to set up data flow between two different models. For example, a computed property on one model can be linked to a configurable input property of another model. This function enables model instances to be treated as data flow components, and allows them to be assembled into user-defined data flow graphs.
Arguments:
Example:
var model1 = ReactiveModel()
("someOutput", 5);
var model2 = ReactiveModel()
("someInput", 10);
var link = ReactiveModel.link(model1.someOutput, model2.someInput);
ReactiveModel.digest();
console.log(model2.someInput()); // Prints 5
model1.someOutput(500);
ReactiveModel.digest();
console.log(model2.someInput()); // Prints 500
// The link needs to be explicitly destroyed, independently from the models.
link.destroy();
This is the same function as ReactiveFunction.link.
# ReactiveModel.digest()
Synchronously evaluates the data flow graph.
This is the same function as ReactiveFunction.digest().
Example:
my
.width(100)
.height(200);
ReactiveModel.digest();
# model.digest()
Synchronously evaluates the data flow graph. Returns the model to support chaining.
This is the same function as ReactiveFunction.digest().
Example:
my
.width(100)
.height(200)
.digest();
# model.call(function[, arguments…])
Invokes the function, passing in model along with any optional arguments. Returns the model to support chaining.
Example:
function fullName(my, first, last) {
my
("firstName", first)
("lastName", last)
("fullName", function (firstName, lastName){
return firstName + " " + lastName;
}, "firstName, lastName");
}
The above function can be invoked like this:
var model = ReactiveModel()
.call(fullName, "Jane", "Smith");
This is equivalent to:
var model = ReactiveModel();
fullName(model, "Jane", "Smith");
# model.expose()
Exposes the previously added property to the configuration. Returns the model to support chaining.
The property to expose must have a default value defined.
Here's an example where two properties x
and y
are defined with default values and exposed to the configuration.
var model = new ReactiveModel()
("x", 5).expose()
("y", 6).expose();
# model()
Returns the configuration, an Object where
The configuration only contains exposed properties that have values other than their defaults.
Example:
var model = new ReactiveModel()
("x", 5).expose()
("y", 6).expose();
model.x(50);
var configuration = model();
The value of configuration
will be:
{ "x": 50 }
Note that y
is omitted, because it has its default value.
# model(configuration)
Sets the configuration.
The argument configuration is an Object where
Only exposed properties may be set via the configuration. Exposed properties whose values are not included in configuration will be set to their default values.
Example:
var model = new ReactiveModel()
("x", 5).expose()
("y", 6).expose();
model.x(50);
// Set the configuration.
model({ y: 60 });
console.log(model.x()); // Prints 5 (x was set back to its default value).
console.log(model.y()); // Prints 60.
# model.on(listener)
Listen for changes in configuration. Returns the listener function that can be used to stop listening for changes.
The argument listener is a function of the form listener(configuration), where configuration is the same object returned from model(). This function is invoked after exposed properties are changed.
# model.off(listener)
Stop listening for changes in configuration. The argument listener must be the value returned from on (not the function passed into on).
# ReactiveModel.serializeGraph()
Serializes the data flow graph. Returns an object with the following properties.
nodes
An array of objects, each with the following properties.
id
The node identifier string.propertyName
The property name. This is the empty string for output nodes of reactive functions with no output property.links
An array of objects representing edges, each with the following properties.
source
The node identifier string of the source node (u).target
The node identifier string of the target node (v).Example:
var my = ReactiveModel()
("firstName", "Jane")
("lastName", "Smith")
("fullName", function (firstName, lastName){
return firstName + " " + lastName;
}, "firstName, lastName");
var serialized = ReactiveModel.serializeGraph();
The value of serialized
will be:
{
"nodes": [
{ "id": "95", "propertyName": "fullName" },
{ "id": "96", "propertyName": "firstName" },
{ "id": "97", "propertyName": "lastName" }
],
"links": [
{ "source": "96", "target": "95" },
{ "source": "97", "target": "95" }
]
}
See also: