Skip to content

Instantly share code, notes, and snippets.

@curran
Last active July 8, 2018 20:47
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save curran/5905182da50a4667dc00 to your computer and use it in GitHub Desktop.
Save curran/5905182da50a4667dc00 to your computer and use it in GitHub Desktop.
Reactive Flow Diagram
{"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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment