This program renders a network diagram for a ModelJS reactive flow.
The input data is generated by an experimental ModelJS branch that computes the reactive flow graph at runtime.
Based on a previous implementation from July 2014
This program renders a network diagram for a ModelJS reactive flow.
The input data is generated by an experimental ModelJS branch that computes the reactive flow graph at runtime.
Based on a previous implementation from July 2014
| {"nodes":[{"type":"lambda","fixed":1,"x":-108,"y":308},{"type":"property","property":"container","fixed":1,"x":-233,"y":306},{"type":"property","property":"svg","fixed":1,"x":-5,"y":309},{"type":"lambda","fixed":1,"x":214,"y":176},{"type":"property","property":"box","fixed":1,"x":-200,"y":257},{"type":"lambda","fixed":1,"x":80,"y":311},{"type":"property","property":"g","fixed":1,"x":165,"y":311},{"type":"lambda","fixed":1,"x":212,"y":264},{"type":"property","property":"margin","fixed":1,"x":-222,"y":202},{"type":"lambda","fixed":1,"x":231,"y":369},{"type":"property","property":"titleText","fixed":1,"x":388,"y":347},{"type":"lambda","fixed":1,"x":578,"y":391},{"type":"property","property":"titleOffset","fixed":1,"x":430,"y":401},{"type":"lambda","fixed":1,"x":214,"y":216},{"type":"property","property":"width","fixed":1,"x":497,"y":63},{"type":"property","property":"height","fixed":1,"x":485,"y":555},{"type":"lambda","fixed":1,"x":653,"y":123},{"type":"property","property":"xAxisG","fixed":1,"x":782,"y":65},{"type":"property","property":"xAxisText","fixed":1,"x":785,"y":119},{"type":"lambda","fixed":1,"x":963,"y":159},{"type":"property","property":"xAxisLabelOffset","fixed":1,"x":-292,"y":153},{"type":"lambda","fixed":1,"x":955,"y":53},{"type":"lambda","fixed":1,"x":963,"y":107},{"type":"lambda","fixed":1,"x":963,"y":221},{"type":"property","property":"xAxisLabel","fixed":1,"x":-257,"y":104},{"type":"lambda","fixed":1,"x":598,"y":332},{"type":"property","property":"yAxisG","fixed":1,"x":811,"y":442},{"type":"property","property":"yAxisText","fixed":1,"x":790,"y":390},{"type":"lambda","fixed":1,"x":946,"y":335},{"type":"property","property":"yAxisLabelOffset","fixed":1,"x":-286,"y":418},{"type":"lambda","fixed":1,"x":946,"y":463},{"type":"lambda","fixed":1,"x":944,"y":407},{"type":"property","property":"yAxisLabel","fixed":1,"x":-248,"y":466},{"type":"lambda","fixed":1,"x":674,"y":271},{"type":"property","property":"barsG","fixed":1,"x":1133,"y":312},{"type":"lambda","fixed":1,"x":589,"y":235},{"type":"lambda","fixed":true,"x":-87,"y":137},{"type":"property","property":"data","fixed":1,"x":-203,"y":365},{"type":"property","property":"xAttribute","fixed":1,"x":-242,"y":54},{"type":"property","property":"getX","fixed":1,"x":147,"y":92},{"type":"lambda","fixed":1,"x":17,"y":26},{"type":"property","property":"sortField","fixed":1,"x":-234,"y":2},{"type":"property","property":"sortOrder","fixed":1,"x":-235,"y":-51},{"type":"property","property":"sortedData","fixed":1,"x":157,"y":17},{"type":"lambda","fixed":1,"x":-70,"y":499},{"type":"property","property":"yAttribute","fixed":1,"x":-233,"y":521},{"type":"property","property":"getY","fixed":1,"x":61,"y":525},{"type":"lambda","fixed":1,"x":281,"y":585},{"type":"property","property":"yDomainMin","fixed":1,"x":-252,"y":573},{"type":"property","property":"yDomainMax","fixed":1,"x":-255,"y":625},{"type":"property","property":"yDomain","fixed":1,"x":475,"y":614},{"type":"lambda","fixed":1,"x":678,"y":566},{"type":"property","property":"yScale","fixed":1,"x":815,"y":565},{"type":"lambda","fixed":1,"x":1033,"y":516},{"type":"property","property":"getYScaled","fixed":1,"x":1243,"y":482},{"type":"lambda","fixed":1,"x":326,"y":4},{"type":"property","property":"xDomain","fixed":1,"x":498,"y":-11},{"type":"lambda","fixed":1,"x":952,"y":573},{"type":"lambda","fixed":1,"x":668,"y":-27},{"type":"property","property":"barPadding","fixed":1,"x":-248,"y":-101},{"type":"property","property":"xScale","fixed":1,"x":787,"y":0},{"type":"lambda","fixed":1,"x":1092,"y":96},{"type":"property","property":"getXScaled","fixed":1,"x":1233,"y":131},{"type":"lambda","fixed":1,"x":955,"y":-2},{"type":"lambda","fixed":1,"x":1378,"y":318}],"links":[{"source":1,"target":0},{"source":0,"target":2},{"source":2,"target":3},{"source":4,"target":3},{"source":2,"target":5},{"source":5,"target":6},{"source":6,"target":7},{"source":8,"target":7},{"source":6,"target":9},{"source":9,"target":10},{"source":10,"target":11},{"source":12,"target":11},{"source":4,"target":13},{"source":8,"target":13},{"source":13,"target":14},{"source":13,"target":15},{"source":6,"target":16},{"source":16,"target":17},{"source":16,"target":18},{"source":18,"target":19},{"source":20,"target":19},{"source":17,"target":21},{"source":15,"target":21},{"source":18,"target":22},{"source":14,"target":22},{"source":18,"target":23},{"source":24,"target":23},{"source":6,"target":25},{"source":25,"target":26},{"source":25,"target":27},{"source":27,"target":28},{"source":29,"target":28},{"source":27,"target":30},{"source":15,"target":30},{"source":27,"target":31},{"source":32,"target":31},{"source":6,"target":33},{"source":33,"target":34},{"source":10,"target":35},{"source":14,"target":35},{"source":37,"target":36},{"source":38,"target":36},{"source":36,"target":39},{"source":41,"target":40},{"source":42,"target":40},{"source":37,"target":40},{"source":40,"target":43},{"source":37,"target":44},{"source":45,"target":44},{"source":44,"target":46},{"source":37,"target":47},{"source":46,"target":47},{"source":48,"target":47},{"source":49,"target":47},{"source":47,"target":50},{"source":37,"target":51},{"source":50,"target":51},{"source":15,"target":51},{"source":51,"target":52},{"source":37,"target":53},{"source":52,"target":53},{"source":46,"target":53},{"source":53,"target":54},{"source":43,"target":55},{"source":39,"target":55},{"source":55,"target":56},{"source":26,"target":57},{"source":52,"target":57},{"source":56,"target":58},{"source":14,"target":58},{"source":59,"target":58},{"source":58,"target":60},{"source":37,"target":61},{"source":60,"target":61},{"source":39,"target":61},{"source":61,"target":62},{"source":17,"target":63},{"source":60,"target":63},{"source":34,"target":64},{"source":43,"target":64},{"source":62,"target":64},{"source":54,"target":64},{"source":60,"target":64},{"source":15,"target":64}],"scale":0.5332125839901604,"translate":[373.3250529749264,143.7733216449567]} |
| // A force directed graph visualization module. | |
| define(["d3", "model", "lodash"], function (d3, Model, _) { | |
| // The constructor function, accepting default values. | |
| return function ForceDirectedGraph(defaults) { | |
| // Create a Model. | |
| // This will serve as the public API for the visualization. | |
| var model = Model({ | |
| // Force directed layout parameters. | |
| charge: -200, | |
| linkDistance: 140, | |
| gravity: 0.03, | |
| // The color scale. | |
| color: d3.scale.ordinal() | |
| .domain(["property", "lambda"]) | |
| .range(["#FFD1B5", "white"]) | |
| }), | |
| force = d3.layout.force(), | |
| zoom = d3.behavior.zoom(), | |
| // The size of nodes and arrows | |
| nodeSize = 20, | |
| arrowWidth = 8; | |
| // Respond to zoom interactions. | |
| zoom.on("zoom", function (){ | |
| model.scale = zoom.scale(); | |
| model.translate = zoom.translate(); | |
| }); | |
| // Call onTick each frame of the force directed layout. | |
| force.on("tick", function(e) { onTick(e); }) | |
| // This function gets reassigned later, each time new data loads. | |
| function onTick(){} | |
| // Stop propagation of drag events here so that both dragging nodes and panning are possible. | |
| // Draws from http://stackoverflow.com/questions/17953106/why-does-d3-js-v3-break-my-force-graph-when-implementing-zooming-when-v2-doesnt/17976205#17976205 | |
| force.drag().on("dragstart", function () { | |
| d3.event.sourceEvent.stopPropagation(); | |
| }); | |
| // Fix node positions after the first time the user clicks and drags a node. | |
| force.drag().on("dragend", function (d) { | |
| // Stop the dragged node from moving. | |
| d.fixed = true; | |
| // Communicate this change to the outside world. | |
| serializeState(); | |
| }); | |
| // Create the SVG element from the container DOM element. | |
| model.when("container", function (container) { | |
| model.svg = d3.select(container).append("svg").call(zoom); | |
| }); | |
| // Adjust the size of the SVG based on the `box` property. | |
| model.when(["svg", "box"], function (svg, box) { | |
| svg.attr("width", box.width).attr("height", box.height); | |
| force.size([box.width, box.height]); | |
| }); | |
| // Create the SVG group that will contain the visualization. | |
| model.when("svg", function (svg) { | |
| model.g = svg.append("g"); | |
| // Arrowhead setup. | |
| // Draws from Mobile Patent Suits example: | |
| // http://bl.ocks.org/mbostock/1153292 | |
| svg.append("defs") | |
| .append("marker") | |
| .attr("id", "arrow") | |
| .attr("orient", "auto") | |
| .attr("preserveAspectRatio", "none") | |
| // See also http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute | |
| //.attr("viewBox", "0 -" + arrowWidth + " 10 " + (2 * arrowWidth)) | |
| .attr("viewBox", "0 -5 10 10") | |
| // See also http://www.w3.org/TR/SVG/painting.html#MarkerElementRefXAttribute | |
| .attr("refX", 10) | |
| .attr("refY", 0) | |
| .attr("markerWidth", 10) | |
| .attr("markerHeight", arrowWidth) | |
| .append("path") | |
| .attr("d", "M0,-5L10,0L0,5"); | |
| }); | |
| // These 3 groups exist for control of Z-ordering. | |
| model.when("g", function (g) { | |
| model.nodeG = g.append("g"); | |
| model.linkG = g.append("g"); | |
| model.arrowG = g.append("g"); | |
| }); | |
| // Update the force layout with configured properties. | |
| model.when(["charge"], force.charge, force); | |
| model.when(["linkDistance"], force.linkDistance, force); | |
| model.when(["gravity"], force.gravity, force); | |
| // Update zoom scale and translation. | |
| model.when(["scale", "translate", "g"], function (scale, translate, g) { | |
| // In the case the scale and translate were set externally, | |
| if(zoom.scale() !== scale){ | |
| // update the internal D3 zoom state. | |
| zoom.scale(scale); | |
| zoom.translate(translate); | |
| } | |
| // Transform the SVG group. | |
| g.attr("transform", "translate(" + translate + ")scale(" + scale + ")"); | |
| }); | |
| // "state" represents the serialized state of the graph. | |
| model.when("state", function(state){ | |
| // Extract the scale and translate. | |
| if(state.scale && model.scale !== state.scale){ | |
| model.scale = state.scale; | |
| } | |
| if(state.translate && model.translate !== state.translate){ | |
| model.translate = state.translate; | |
| } | |
| // Set the node and link data. | |
| var newData = _.cloneDeep(state); | |
| force.nodes(newData.nodes).links(newData.links).start(); | |
| model.data = newData; | |
| }); | |
| // Update the serialized state. | |
| model.when(["scale", "translate"], _.throttle(function(scale, translate){ | |
| serializeState(); | |
| }, 1000)); | |
| // Sets model.state to expose the serialized state. | |
| function serializeState(){ | |
| var data = model.data, | |
| scale = model.scale, | |
| translate = model.translate; | |
| model.state = { | |
| nodes: data.nodes.map(function(node){ | |
| return { | |
| type: node.type, | |
| property: node.property, | |
| fixed: node.fixed, | |
| // Keep size of JSON small, so it fits in a URL. | |
| x: Math.round(node.x), | |
| y: Math.round(node.y) | |
| }; | |
| }), | |
| links: data.links.map(function(link){ | |
| // Replaced link object references with indices for serialization. | |
| return { | |
| source: link.source.index, | |
| target: link.target.index | |
| }; | |
| }), | |
| scale: scale, | |
| translate: translate | |
| }; | |
| } | |
| model.when(["data", "color", "nodeG", "linkG", "arrowG"], | |
| function(data, color, nodeG, linkG, arrowG){ | |
| var node = nodeG.selectAll("g").data(data.nodes), | |
| nodeEnter = node.enter().append("g").call(force.drag); | |
| nodeEnter.append("rect").attr("class", "node") | |
| .attr("y", -nodeSize) | |
| .attr("height", nodeSize * 2) | |
| .attr("rx", nodeSize) | |
| .attr("ry", nodeSize); | |
| nodeEnter.append("text").attr("class", "nodeLabel"); | |
| node.select("g text") | |
| // Use the property name for property nodes, and λ for lambda nodes. | |
| .text(function(d) { | |
| return (d.type === "property" ? d.property : "λ"); | |
| }) | |
| //Center text vertically. | |
| .attr("dy", function(d) { | |
| if(d.type === "lambda"){ | |
| return "0.35em"; | |
| } else { | |
| return "0.3em"; | |
| } | |
| }) | |
| // Compute rectancle sizes based on text labels. | |
| .each(function (d) { | |
| var circleWidth = nodeSize * 2, | |
| textLength = this.getComputedTextLength(), | |
| textWidth = textLength + nodeSize; | |
| if(circleWidth > textWidth) { | |
| d.isCircle = true; | |
| d.rectX = -nodeSize; | |
| d.rectWidth = circleWidth; | |
| } else { | |
| d.isCircle = false; | |
| d.rectX = -(textLength + nodeSize) / 2; | |
| d.rectWidth = textWidth; | |
| d.textLength = textLength; | |
| } | |
| }); | |
| node.select("g rect") | |
| .attr("x", function(d) { return d.rectX; }) | |
| .style("foo", function(d) { return "test"; }) | |
| .attr("width", function(d) { return d.rectWidth; }) | |
| .style("fill", function(d) { return color(d.type); }); | |
| node.exit().remove(); | |
| var link = linkG.selectAll(".link").data(data.links); | |
| link.enter().append("line").attr("class", "link") | |
| link.exit().remove(); | |
| var arrow = arrowG.selectAll(".arrow").data(data.links); | |
| arrow.enter().append("line") | |
| .attr("class", "arrow") | |
| .attr("marker-end", function(d) { return "url(#arrow)" }); | |
| arrow.exit().remove(); | |
| // Run a modified version of force directed layout | |
| // to account for link direction going from left to right. | |
| onTick = function(e) { | |
| // Execute left-right constraints | |
| var k = 1 * e.alpha; | |
| force.links().forEach(function (link) { | |
| var a = link.source, | |
| b = link.target, | |
| dx = b.x - a.x, | |
| dy = b.y - a.y, | |
| d = Math.sqrt(dx * dx + dy * dy), | |
| x = (a.x + b.x) / 2; | |
| if(!a.fixed){ | |
| a.x += k * (x - d / 2 - a.x); | |
| } | |
| if(!b.fixed){ | |
| b.x += k * (x + d / 2 - b.x); | |
| } | |
| }); | |
| force.nodes().forEach(function (d) { | |
| if(d.isCircle){ | |
| d.leftX = d.rightX = d.x; | |
| } else { | |
| d.leftX = d.x - d.textLength / 2 + nodeSize / 2; | |
| d.rightX = d.x + d.textLength / 2 - nodeSize / 2; | |
| } | |
| }); | |
| link.call(edge); | |
| arrow.call(edge); | |
| node.attr("transform", function(d) { | |
| return "translate(" + d.x + "," + d.y + ")"; | |
| }); | |
| }; | |
| }); | |
| // Sets the (x1, y1, x2, y2) line properties for graph edges. | |
| function edge(selection){ | |
| selection | |
| .each(function (d) { | |
| var sourceX, targetX, dy, dy, angle; | |
| if( d.source.rightX < d.target.leftX ){ | |
| sourceX = d.source.rightX; | |
| targetX = d.target.leftX; | |
| } else if( d.target.rightX < d.source.leftX ){ | |
| targetX = d.target.rightX; | |
| sourceX = d.source.leftX; | |
| } else if (d.target.isCircle) { | |
| targetX = sourceX = d.target.x; | |
| } else if (d.source.isCircle) { | |
| targetX = sourceX = d.source.x; | |
| } else { | |
| targetX = sourceX = (d.source.x + d.target.x) / 2; | |
| } | |
| dx = targetX - sourceX; | |
| dy = d.target.y - d.source.y; | |
| angle = Math.atan2(dx, dy); | |
| d.sourceX = sourceX + Math.sin(angle) * nodeSize; | |
| d.targetX = targetX - Math.sin(angle) * nodeSize; | |
| d.sourceY = d.source.y + Math.cos(angle) * nodeSize; | |
| d.targetY = d.target.y - Math.cos(angle) * nodeSize; | |
| }) | |
| .attr("x1", function(d) { return d.sourceX; }) | |
| .attr("y1", function(d) { return d.sourceY; }) | |
| .attr("x2", function(d) { return d.targetX; }) | |
| .attr("y2", function(d) { return d.targetY; }); | |
| } | |
| model.set(defaults); | |
| return model; | |
| }; | |
| }); |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <!-- Use RequireJS for module loading. --> | |
| <script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.14/require.js"></script> | |
| <!-- Configure RequireJS paths for third party libraries. --> | |
| <script> | |
| requirejs.config({ | |
| paths: { | |
| d3: "//d3js.org/d3.v3.min", | |
| jquery: "//code.jquery.com/jquery-2.1.1.min", | |
| lodash: "//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.4.0/lodash.min", | |
| async: "//cdnjs.cloudflare.com/ajax/libs/async/0.9.0/async", | |
| crossfilter: "//cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.11/crossfilter.min" | |
| } | |
| }); | |
| </script> | |
| <!-- Include CSS that styles the visualization. --> | |
| <link rel="stylesheet" href="styles.css"> | |
| <title>Data Flow Diagram</title> | |
| </head> | |
| <body> | |
| <!-- The visualization will be injected into this div. --> | |
| <div id="container"></div> | |
| <!-- Run the main program. --> | |
| <script src="main.js"></script> | |
| </body> | |
| </html> |
| require(["d3", "forceDirectedGraph", "lodash"], function (d3, ForceDirectedGraph, lodash) { | |
| // Initialize the force directed graph. | |
| var container = d3.select("#container").node(), | |
| forceDirectedGraph = ForceDirectedGraph({ container: container }); | |
| // Initialize zoom based on client size. | |
| var scale = container.clientWidth * 1 / 800; | |
| forceDirectedGraph.scale = scale; | |
| forceDirectedGraph.translate = [ | |
| container.clientWidth / 2 * (1 - scale), | |
| container.clientHeight / 2 * (1 - scale) | |
| ]; | |
| // Set up default data. | |
| if(!location.hash){ | |
| location.hash = '{"nodes":[{"type":"lambda","fixed":0,"x":442,"y":250},{"type":"property","property":"firstName","fixed":1,"x":290,"y":212},{"type":"property","property":"lastName","fixed":1,"x":293,"y":294},{"type":"property","property":"fullName","fixed":0,"x":581,"y":247}],"links":[{"source":1,"target":0},{"source":2,"target":0},{"source":0,"target":3}],"scale":1.938287710980903,"translate":[-360.71751731834274,-241.583180104211]}'; | |
| } | |
| // Update the fragment identifier in response to user interactions. | |
| forceDirectedGraph.when(["state"], function(state){ | |
| location.hash = JSON.stringify(state); | |
| console.log(JSON.stringify(state)); | |
| }); | |
| // Sets the data on the graph visualization from the fragment identifier. | |
| // See https://github.com/curran/screencasts/blob/gh-pages/navigation/examples/code/snapshot11/main.js | |
| function navigate(){ | |
| if(location.hash){ | |
| var newState = JSON.parse(location.hash.substr(1)); | |
| if(JSON.stringify(newState) !== JSON.stringify(forceDirectedGraph.state)){ | |
| forceDirectedGraph.state = newState; | |
| } | |
| } | |
| } | |
| // Navigate once to the initial hash value. | |
| navigate(); | |
| // Navigate whenever the fragment identifier value changes. | |
| window.addEventListener("hashchange", navigate); | |
| // Sets the `box` model property | |
| // based on the size of the container, | |
| function computeBox(){ | |
| forceDirectedGraph.box = { | |
| width: container.clientWidth, | |
| height: container.clientHeight | |
| }; | |
| } | |
| // once to initialize `model.box`, and | |
| computeBox(); | |
| // whenever the browser window resizes in the future. | |
| window.addEventListener("resize", computeBox); | |
| }); |
| // Implements key-value models with a functional reactive `when` operator. | |
| // See also https://github.com/curran/model | |
| define([], function (){ | |
| // The constructor function, accepting default values. | |
| return function Model(defaults){ | |
| // The returned public API object. | |
| var model = {}, | |
| // The internal stored values for tracked properties. { property -> value } | |
| values = {}, | |
| // The listeners for each tracked property. { property -> [callback] } | |
| listeners = {}, | |
| // The set of tracked properties. { property -> true } | |
| trackedProperties = {}; | |
| // The functional reactive "when" operator. | |
| // | |
| // * `properties` An array of property names (can also be a single property string). | |
| // * `callback` A callback function that is called: | |
| // * with property values as arguments, ordered corresponding to the properties array, | |
| // * only if all specified properties have values, | |
| // * once for initialization, | |
| // * whenever one or more specified properties change, | |
| // * on the next tick of the JavaScript event loop after properties change, | |
| // * only once as a result of one or more synchronous changes to dependency properties. | |
| function when(properties, callback){ | |
| // This function will trigger the callback to be invoked. | |
| var triggerCallback = debounce(function (){ | |
| var args = properties.map(function(property){ | |
| return values[property]; | |
| }); | |
| if(allAreDefined(args)){ | |
| callback.apply(null, args); | |
| } | |
| }); | |
| // Handle either an array or a single string. | |
| properties = (properties instanceof Array) ? properties : [properties]; | |
| // Trigger the callback once for initialization. | |
| triggerCallback(); | |
| // Trigger the callback whenever specified properties change. | |
| properties.forEach(function(property){ | |
| on(property, triggerCallback); | |
| }); | |
| } | |
| // Returns a debounced version of the given function. | |
| // See http://underscorejs.org/#debounce | |
| function debounce(callback){ | |
| var queued = false; | |
| return function () { | |
| if(!queued){ | |
| queued = true; | |
| setTimeout(function () { | |
| queued = false; | |
| callback(); | |
| }, 0); | |
| } | |
| }; | |
| } | |
| // Returns true if all elements of the given array are defined, false otherwise. | |
| function allAreDefined(arr){ | |
| return !arr.some(function (d) { | |
| return typeof d === 'undefined' || d === null; | |
| }); | |
| } | |
| // Adds a change listener for a given property with Backbone-like behavior. | |
| // See http://backbonejs.org/#Events-on | |
| function on(property, callback){ | |
| getListeners(property).push(callback); | |
| track(property); | |
| }; | |
| // Gets or creates the array of listener functions for a given property. | |
| function getListeners(property){ | |
| return listeners[property] || (listeners[property] = []); | |
| } | |
| // Tracks a property if it is not already tracked. | |
| function track(property){ | |
| if(!(property in trackedProperties)){ | |
| trackedProperties[property] = true; | |
| values[property] = model[property]; | |
| Object.defineProperty(model, property, { | |
| get: function () { return values[property]; }, | |
| set: function(value) { | |
| values[property] = value; | |
| getListeners(property).forEach(function(callback){ | |
| callback(value); | |
| }); | |
| } | |
| }); | |
| } | |
| } | |
| // Sets all of the given values on the model. | |
| // Values is an object { property -> value }. | |
| function set(values){ | |
| for(property in values){ | |
| model[property] = values[property]; | |
| } | |
| } | |
| // Transfer defaults passed into the constructor to the model. | |
| set(defaults); | |
| // Expose the public API. | |
| model.when = when; | |
| model.on = on; | |
| model.set = set | |
| return model; | |
| } | |
| }); |
| /* Make the visualization container fill the page. */ | |
| #container { | |
| position: fixed; | |
| left: 0px; | |
| right: 0px; | |
| top: 0px; | |
| bottom: 0px; | |
| } | |
| /* Style the nodes of the graph. */ | |
| .node { | |
| stroke: black; | |
| stroke-width: 1.5; | |
| } | |
| .nodeLabel { | |
| font-size: 2em; | |
| /* Center text horizontally */ | |
| text-anchor: middle; | |
| } | |
| /* Style the links of the graph. */ | |
| .link { | |
| stroke: black; | |
| } | |
| /* Set the arrowhead size. */ | |
| .arrow { | |
| stroke-width: 1.5px; | |
| } |