A library for reactive dataflow programming.
JavaScript
Clone or download
Permalink
Failed to load latest commit information.
.gitignore Begin restructuring Apr 4, 2016
.travis.yml
LICENSE Update LICENSE Apr 15, 2016
README.md Add link to Topologica.js. Closes #45 Feb 13, 2018
index.js Remove dated comment Jul 14, 2016
package.json 0.13.0 Jul 14, 2016
test.js Throw error when newly added property is already defined. Closes #40 Jul 14, 2016

README.md

reactive-model

A JavaScript library for dataflow programming.

See also Topologica.js, a minimalist rewrite of this library.

Build Status

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

Installing

You can include reactive-model in your HTML like this (will introduce a global variable ReactiveModel):

<script src="//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");

Examples

Bl.ocks

Full Name Greeting
Responding to Resize
Margin Convention
Margin Convention II
Responsive Axes
Baseball Scatter Plot
Responsive Axes with React

ABCs

AB

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.

ABC

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.

CDE

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.

Full Name

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.

Tricky Cases

Tricky Case I

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.

Tricky Case II

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.

API Reference

Models

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

Properties

# model(propertyName[, defaultValue])

Adds a property to the model. Returns the model to support chaining.

Arguments:

  • propertyName - The name of the property (a string).
  • defaultValue (optional) - The default value for this property.

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.

Data Flow

# model([output,] callback, inputs)

Adds a reactive function to this model.

Arguments:

  • output (optional) - The output property name.
  • callback - The reactive function callback. Arguments are values corresponding to inputs. May be of two forms:
    • callback(arguments…) For synchronous reactive functions. The returned value will be assigned to output.
    • callback(arguments…, done) For asynchronous reactive functions. The function done should be invoked asynchronously with the value to assign to output. The returned value is ignored.
  • inputs - The input property names. May be either
    • a comma-delimited list of input property names (e.g. "a, b"), or
    • an array of property name strings (e.g. ["a", "b"]).

The callback will be invoked:

  • when all input properties are defined,
  • after any input properties change,
  • during a digest.

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

  • the reactive function is initially set up, and
  • whenever its value is set.

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");

Configuration

# 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

  • keys are property names, and
  • values are current property values.

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

  • keys are property names, and
  • values are property values to be set.

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

Serialization

# 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:

Related Work