View on GitHub

GraphFellow

GraphFellow: directed graphs in JavaScript

Download this project as a .zip file Download this project as a tar.gz file

beta: GraphFellow is still in development!

GraphFellow example: gossip protocol

This diagram demonstrates the gossip protocol, which is one of many ways nodes can propagate information across a network. This one is sharing the highest value. Each node transmits its value and absorbs any higher value it receives. The highest value on the network is shown in red as it spreads.

The gossip is transmitted randomly, but you can click on a node to force it.

Just show the graph →


Behind the scenes

The gossip protocol uses the on_init event to set up the network graph when the graph is first rendered, and also when the reset button is clicked.

HTML setup

First things first… like all the examples, this page starts by loading the libraries GraphFellow needs:

<script src="../../vendor/pixi.min.js"></script>
<script src="../../vendor/greensock-js/TweenMax.min.js"></script>

The container for the graph links to the JSON file that contains the graph’s config (see below):

<div id="gossip-example" data-graph-src="gossip-protocol.json"></div>

Before defining custom functions, the GraphFellow library itself is loaded:

<script src="../../graphfellow.js"></script>

Config in the JSON

As the data-graph-src attribute shows, the graph is defined in gossip-protocol.json. You can click on that and inspect the whole file, but the rest of this page describes the JSON piece-by-piece.

The graph is initialised by calling create_graph() after the event handling functions have been defined (see gossip-protocol.json for the whole file):

  "vertices": [
    {"id": "A", "x": 100, "y": 100 },
    {"id": "B", "x": 220, "y": 230 },
    {"id": "C", "x": 400, "y": 200 },
    ...
    ...
    {"id": "S", "x": 930, "y": 540 },
    {"id": "T", "x": 800, "y": 430 },
    {"id": "U", "x": 900, "y": 130 }
  ],
  "edges": [
    {"from": "A", "to": "B"},
    {"from": "B", "to": "C"},
    {"from": "C", "to": "D"},
    ...
    ...
    {"from": "Q", "to": "T"},
    {"from": "T", "to": "S"},
    {"from": "R", "to": "S"},
    {"from": "P", "to": "U"}
  ],

There are no travellers defined in the config, because they are created (and destroyed) whenever a node decides to transmit — see functions described below.

By default, GraphFellow graphs are defined on a 1000 x 1000 grid. This gossip demo is not square, so the grid_height of the grid is truncated at 660. GraphFellow uses this (or the alternative aspect_ratio setting) to determine the scaled dimensions when it sacles and renders the graph to fit on your page.

The graph is populated by the on_init function, It makes a decision as to which nodes should transmit by randomly deciding for each one every “tick”. So the config defines the tick period (in seconds), and provides a function to run on the on_tick event that the graph triggers every fifth of a second:

"config": {
  "grid_height": 660,
  "on_init": "randomly_assign_payloads",
  "tick_period": 0.2,
  "on_tick": "selectively_transmit_gossip_from_vertices",
  ...

Config for all vertices includes setting the on_click event handler, which allows you to exlicitly trigger transmission. This config also forces the graph to start with empty payloads in all the vertices:

"vertices": {
  "on_click": "transmit_gossip_from_vertex"
  "payload": "",
  "has_id_as_payload": false,
  "radius": 30,
  "text_font_size": 28,
  "text_color": "0xffffff",
  "stroke_width": 4,
  "fill_color": "0x000033",
  "stroke_color": "0x9999ff",
  "pulse_color": "0xeeeeff",
  "is_pulse_blur": true,
  "is_pulse_yoyo": true,
},

The config for the edges is straightforward. Every journey time takes 2 seconds, which means a traveller will get from start to end in the same time regardless of how long the edge is. If you want to correc this, you can set the journey time for individual journeys to override the global value set here:

"edges": {
  "stroke_color": "0xccccff",
  "traveller_radius": 6,
  "journey_duration": 2,
  "is_arrow": false,
  "is_bidirectional": true
},

The travellers in this graph have a journey_lifespan of 1, which means they are destroyed after a single journey. This is important: when a vertex transmits its payload across the network, it does so by creating one or more travellers that will carry that value one hop and then expire.

The on_departure and on_arrival event handlers are used to carry the value of the payload from node to node (vertex to vertex). See below for the functions themselves.

"travellers": {
  "journey_lifespan": 1,
  "fill_color": "0x6666ff",
  "text_color": "0xffffff",
  "stroke_color": "0x9999ff",
  "radius": 18,
  "payload": 0,
  "is_displaying_payload": true,
  "is_above_vertices": false,
  "on_departure": "take_payload_from_vertex",
  "on_arrival": "give_payload_to_vertex"
}

Defining the custom functions

All the custom functions are all added with GraphFellow.add_function(). It’s essential that the names in the config (JSON) matches the name with which the functions are added.

The named functions are effectively hooks that (text-based) JSON can reference. This example also includes eternal functions that those named functions themselves call.

First set up some globals (some of these may be available for you to change via controls in the HTML):


// when transmiting gossip, send on one edge
// or all or all edges?
// (always sends on all when user clicks on a vertex)
let is_transmitting_all = false;

// probablility that a node will transmit gossip,
// tested every tick
let p_transmit_this_tick = 0.02;

// when populating the network at the start, should
// all values be unique (otherwise duplicates may occur)?
let is_using_unique_values = true;

// once a vertex has transmitted, wait this long (ms)
// before trying again
let min_time_between_transmissions = 3001;

// track max value, to display it differently
let max_payload_in_graph = 0;

const max_value_traveller_color = 0xcc0000,
      max_value_pulse_color = 0xff9999;

Next, a make_traveller function that sets the colour of the traveller to red if it’s about to set off from a vertex that already has a maximum value on the network (this is to make it easier to see them spread). Note how the graph object is being passed in as an argument, which provides access to the create_traveller().

function make_traveller(at_vertex, graph) {
  let v = at_vertex.payload.value;
  let t_config = {at_vertex: at_vertex};
  if ( v === max_payload_in_graph) {
    t_config.fill_color = max_value_traveller_color;
  }
  return graph.create_traveller(t_config);
}

The randomly_assign_payloads is the most complex of the functions for this graph, but that’s mainly because it’s been written to allow the reset button to call this.

Note that calling destroy() on elements within the travellers array does remove them from the array, hence the use of while to remove them.

The set-up of the payload value depends on the setting of is_using_unique_values. The values themselves are in the range of 1 to n, where n is the number of nodes.

The payloads themsleves are set using the payload.set() method, because this renders the diagram correctly — just setting the payload.value will not do this.

The custom_timestamp field being used on each vertex is not part of GraphFellow. It’s used to enforce a gap between transmissions from the same node.

GraphFellow.add_function("randomly_assign_payloads", function(e, graph){
  while (graph.travellers.length > 0) {
    graph.travellers[0].destroy();
  }
  max_payload_in_graph = 0;
  let values = new Array(graph.vertices.length);
  for (let i=0; i < values.length; i++) {
    values[i] = 1 + (is_using_unique_values? i : Math.floor(Math.random()*values.length));
    if (values[i] > max_payload_in_graph) {
      max_payload_in_graph = values[i];
    }
  }
  // shuffle code from
  // https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array#6274398
  let c = values.length;
  while (c > 0) {
    let i = Math.floor(Math.random() * c);
      c--;
      let temp = values[c];
      values[c] = values[i];
      values[i] = temp;
  }
  let now = new Date();
  for (let i=0; i < graph.vertices.length; i++) {
    graph.vertices[i].payload.set(values[i]);
    graph.vertices[i].custom_timestamp = now;
  }
});

The next two functions are the on_departure and on_arrival event handlers that are applied to every traveller. The first sets the traveller’s payload at the start of its journey, and the second passes it on to the node (vertex) it’s arrived at, assuming that it is of higher value than the vertex’s current payload. The vertex will pulse if the value is set. The colour of the pulse is normally defined by the config, but by passing in a non-null argument, its colour can be overridden.

  GraphFellow.add_function("take_payload_from_vertex", function(e, graph){
    this.payload.set(this.at_vertex.payload.value);
  });

  GraphFellow.add_function("give_payload_to_vertex", function(e, graph){
    if (this.payload.value > this.at_vertex.payload.value) {
      this.at_vertex.payload.set(this.payload.value);
      let color = this.payload.value === max_payload_in_graph? max_value_pulse_color : null;
      this.at_vertex.pulse(color);
    }
  });

The transmit_gossip_from_vertex function is the on_click handler for the vertices. When you click on a vertex, it will transmit to all its neighbours.

GraphFellow.add_function("transmit_gossip_from_vertex", function(e, graph){
  if (this.json_type === 'vertices' && this.edges_out.length > 0) {
    for (let i=0; i < this.edges_out.length; i++) {
      make_traveller(this, graph).travel(this.edges_out[i]);
    }
  }
});

This is the on_tick handler — this runs every tick_period seconds (from the config, that’s 0.2, or five time a second) and randmonly decides whether or not each vertex will transmit this tick. This also takes into account the custom_timestamp to enforce a gap between transmissions.

Note that the travellers are despatched with the .travel(edge) method:

GraphFellow.add_function("selectively_transmit_gossip_from_vertices", function(e, graph){
  let now = new Date();
  for (let i=0; i < graph.vertices.length; i++) {
    if ( Math.random() < p_transmit_this_tick &&
      (now - graph.vertices[i].custom_timestamp) > min_time_between_transmissions ) 
    {
      graph.vertices[i].custom_timestamp = now;
      let edges_to_follow = [];
      if (is_transmitting_all) {
        edges_to_follow = graph.vertices[i].edges_out;
      } else {
        edges_to_follow.push(graph.vertices[i].get_random_edge_out());
      }
      for (let j=0; j < edges_to_follow.length; j++) {
        let t = make_traveller(graph.vertices[i], graph);
        t.travel(edges_to_follow[j]);
      }
    }
  }
});

The reset button is wired to call the randomly_assign_payloads function, which is also the graph’s on_init function. The GraphFellow.call_function() method is used to run named functions (that is, those that were added with GraphFellow.add_function), but you must provide the caller — in this case the graph itself — as the first argument:

document.getElementById("gossip-reset")
.addEventListener("click", function(){
  GraphFellow.call_function(
    GraphFellow.graphs[0],
    "randomly_assign_payloads"
  )
});

All done: create the graph

Finally, the graph is initialised by an explicit call to create_graph():

GraphFellow.create_graph(document.getElementById("gossip-example"));

If the graph’s container had a class of graphfellow, it would be possible to simply use GraphFellow.init() instead. The primary advantage of using create_graph is that it allows you to directly specify the container to populate, and, optionally, define its config as a JavaScript object. In this example, as the JSON is defined in the data-graph-src attribute, create_graph is being used just to target the container.