diff --git a/static/build.js b/static/build.js index 2f212ed..267b8f8 100644 --- a/static/build.js +++ b/static/build.js @@ -1,129 +1,223 @@ 'use strict'; -function attachEvents() { - node - .on('mouseover', function(d) { - if (!mousedown_node || d === mousedown_node) { - return; - } - // enlarge target node - d3.select(this) - .attr('transform', d3.select(this).attr('transform') + ' scale(1.1)'); - }) - .on('mouseout', function(d) { - if (!mousedown_node || d === mousedown_node) { - return; - } - // unenlarge target node - d3.select(this) - .attr('transform', - d3.select(this).attr('transform').replace(' scale(1.1)', '')); - }) - .on('mousedown', function(d) { - if (d3.event.ctrlKey) { - return; - } +function resetMenus() { + d3.select('#node-menu').style('display', 'none'); + d3.select('#link-menu').style('display', 'none'); +} - // select node - mousedown_node = d; - if (mousedown_node === selected_node) { - selected_node = null; - } - else selected_node = mousedown_node; +function closeEditMenu() { + selected_link = null; + selected_node = null; + editing = false; + resetMenus(); +} + +function selectElement(target) { + trigger_repaint_style(); + if (target === selected_link || target === selected_node) { + // de-select + closeEditMenu(); + } else { + editing = true; + if (target instanceof Node) { // selecting a node selected_link = null; + selected_node = target; + editNode(target); + } else { + selected_link = target; + selected_node = null; + editLink(target); + } + } +} - // reposition drag line - drag_line - .classed('hidden', false) - .attr({ - 'x1': mousedown_node.x, - 'y1': mousedown_node.y, - 'x2': mousedown_node.x, - 'y2': mousedown_node.y - }); - - restart(); - attachEvents() - }) - .on('mouseup', function(d) { - if (!mousedown_node) { - return; - } +function resetMouseVars() { + mousedown_node = null; +} - // needed by FF - drag_line - .classed('hidden', true) - .style('marker-end', ''); - - // check for drag-to-self - mouseup_node = d; - if (mouseup_node === mousedown_node) { - editing = true; - editNode(d); - resetMouseVars(); - return; - } +// d3 events +Link.addEvents({ + mousedown: function (l) { + if (d3.event.ctrlKey) { + return; + } - // unenlarge target node - d3.select(this).attr('transform', ''); + // select link + selectElement(l); + resetMouseVars(); + } +}); - // add link to graph (update if exists) - // NB: links are strictly source < target; arrows separately specified by booleans - var source, target, - source = mousedown_node; - target = mouseup_node; +Node.addEvents({ + mouseover: function (d) { + // enlarge target node for link + if (mousedown_node && d != mousedown_node) { + d.setScale(1.1); + } + + }, + mouseout: function (d) { + if (mousedown_node && d != mousedown_node) { + // unenlarge target node for link + d.setScale(1); + } + }, + mousedown: function (d) { + if (d3.event.ctrlKey) { + return; + } + + // save mousedown_node for later handling on mouseup and + mousedown_node = d; + + // reset drag line to be centered on mousedown_node + drag_line + .classed('hidden', false) + .attr({ + 'x1': mousedown_node.x, + 'y1': mousedown_node.y, + 'x2': mousedown_node.x, + 'y2': mousedown_node.y + }); + + }, + mouseup: function (d) { + if (!mousedown_node) { + return; + } + + // needed by FF + drag_line + .classed('hidden', true) + .style('marker-end', ''); - var link = addLink(source, target) + // are we making a new link or clicking on a new node? + if (d === mousedown_node) { + // we clicked on a node to edit it + selectElement(d); + } else { + // shrink down the node we just moused over + d.setScale(1); + // create and then select new link + selectElement(createLink(mousedown_node, d)); + } + + resetMouseVars(); + startGraphAnimation(); + } +}); - // select new link - selected_link = link; - selected_node = null; - restart(); - attachEvents(); - }); - path - .on('mousedown', function(d) { - if (d3.event.ctrlKey) { - return; +function editNode(d) { + // hide link menu + d3.select('#link-menu').style('display', 'none'); + var nodeMenu = d3.select('#node-menu'); + nodeMenu.style('display', 'block'); + // We wish we could use arrow functions here but they don't have a semantic this + nodeMenu.select('#edit-node-name') + .property('value', d.name) + .on('keyup', function () { d.name = this.value; }); + nodeMenu.select('#edit-node-r') + .property('value', d.r) + .on('input', function () { d.r = this.value; }); + nodeMenu.select('#edit-node-dashed') + .property('value', d.dashed) + .on('change', function () { d.dashed = d3.select(this).property('checked') }); + nodeMenu.select('#delete-node') + .on('click', function () { + if (selected_node) { + removeNode(selected_node); } + closeEditMenu(); + }); +} - // select link - mousedown_link = d; - if (mousedown_link === selected_link) { - selected_link = null; - } else { - selected_link = mousedown_link; +function editLink(d) { + d3.select('#node-menu').style('display', 'none'); + var linkMenu = d3.select('#link-menu'); + linkMenu.style('display', 'block'); + linkMenu.select('#link-name').text(d.name()); + linkMenu.select('#source-name').text(d.source.name); + linkMenu.select('#target-name').text(d.target.name); + linkMenu.select('#edit-center-text') + .property('value', d.centerText) + .on('keyup', function () { d.centerText = this.value; }); + linkMenu.select('#edit-source-text') + .property('value', d.sourceText) + .on('keyup', function () { d.sourceText = this.value; }); + linkMenu.select('#edit-target-text') + .property('value', d.targetText) + .on('keyup', function () { d.targetText = this.value; }); + linkMenu.select('#edit-strength') + .property('value', d.strength) + .on('input', function () { d.strength = this.value; }); + linkMenu.select('#edit-link-dashed') + .property('checked', d.dashed) + .on('click', function () { + d.dashed = d3.select(this).property('checked'); + }); + linkMenu.select('#delete-link') + .on('click', function () { + if (selected_link) { + removeLink(selected_link); } - selected_node = null; - editLink(d); - resetMouseVars(); - restart(); + closeEditMenu(); }); } -function resetMouseVars() { - mousedown_node = null; - mouseup_node = null; - mousedown_link = null; +// Node and Link creation and destruction methods + +function createNode(point) { + let node = style_repaint_on_set(new Node(++window.graph.lastId, point), node_props_updated_by_tick); + window.graph.nodes.push(node); + trigger_full_repaint(); + return node; } -function resetMenus() { - d3.select('#node-menu').style('display', 'none'); - d3.select('#link-menu').style('display', 'none'); +function createLink(source, target) { + let existing = Link.search_index(window.graph.links, source, target); + + if (existing) { + // Don't create dupliacte links + return existing; + } + + let link = style_repaint_on_set(new Link(source, target)) + window.graph.links.push(link); + trigger_full_repaint(); + return link; } -function writeGraph() { - d3.select('#graph-field').html(JSON.stringify(window.graph)); +function removeLink(link) { + let was_inserted = window.graph.links.indexOf(link); + if (was_inserted >= 0) { + window.graph.links.splice(was_inserted, 1); + } + + trigger_full_repaint(); + return link; } -function spliceLinksForNode(node) { - var toSplice = window.graph.links.filter(function(l) { - return (l.source === node || l.target === node); - }); - toSplice.map(function(l) { - window.graph.links.splice(window.graph.links.indexOf(l), 1); - }); +function removeNode(node) { + let was_inserted = window.graph.nodes.indexOf(node); + if (was_inserted >= 0) { + // remove from node list + window.graph.nodes.splice(was_inserted, 1); + // remove all references from links + window.graph.links + .filter((l) => (l.source === node || l.target === node)). + forEach((link) => removeLink(link)); + } + // alaways decrement, even if the node wasn't inserted for some reason + window.graph.lastId--; + + trigger_full_repaint(); + return node; +} + + +function writeGraph() { + d3.select('#graph-field').html(JSON.stringify(window.graph)); } function mousedown() { @@ -133,50 +227,13 @@ function mousedown() { // because :active only works in WebKit? svg.classed('active', true); - if (d3.event.ctrlKey || d3.event.target.nodeName !== 'svg') { + if (d3.event.ctrlKey || d3.event.target.nodeName !== 'svg' || mouse_over_link) { return; } // insert new node at point var point = d3.mouse(this) - addNode(point); - - restart(); - attachEvents(); -} - -function addNode(point) { - var newNode = { - id: ++window.graph.lastId, - name: 'New ' + window.graph.lastId, - x: width / 2, - y: height / 2, - r: 12 - }; - if (point) { - newNode.x = point[0]; - newNode.y = point[1]; - } - window.graph.nodes.push(newNode); - return newNode; -} - -function addLink(source, target) { - var link = window.graph.links.filter(function(l) { - return (l.source === source && l.target === target) || - (l.source === target && l.target === source); - })[0]; - - if (link) { - return link; - } else { - link = { - source: source, - target: target, - strength: 10 - }; - window.graph.links.push(link); - } + selectElement(createNode(point)); } function mousemove() { @@ -193,9 +250,6 @@ function mousemove() { 'x2': point[0], 'y2': point[1] }); - - attachEvents(); - restart(); } function mouseup() { @@ -226,7 +280,7 @@ function keydown() { lastKeyDown = d3.event.keyCode; // ctrl - if(d3.event.keyCode === 17) { + if (d3.event.keyCode === 17) { node.call(force.drag); svg.classed('ctrl', true); } @@ -244,124 +298,19 @@ function keyup() { } } -function editNode(d) { - d3.select('#link-menu').style('display', 'none'); - var nodeMenu = d3.select('#node-menu'); - nodeMenu.style('display', 'block'); - document.getElementById('edit-node-name').value = d.name; - nodeMenu.select('#edit-node-name') - .on('keyup', function() { - window.graph.nodes.filter(function(node) { - return node === d; - })[0].name = this.value; - restart(); - }); - document.getElementById('edit-node-r').value = d.r; - nodeMenu.select('#edit-node-r') - .on('input', function() { - window.graph.nodes.filter(function(node) { - return node.id === d.id; - })[0].r = this.value; - restart(); - }); - document.getElementById('edit-node-dashed').checked = d.dashed; - nodeMenu.select('#edit-node-dashed') - .on('change', function() { - window.graph.nodes.filter(function(link) { - return link === d; - })[0].dashed = d3.select(this).property('checked'); - restart(); - }); - nodeMenu.select('#delete-node') - .on('click', function() { - if (selected_node) { - window.graph.nodes - .splice(window.graph.nodes.indexOf(selected_node), 1); - spliceLinksForNode(selected_node); - } - selected_link = null; - selected_node = null; - restart(); - attachEvents(); - nodeMenu.style('display', 'none'); - }); -} - -function editLink(d) { - d3.select('#node-menu').style('display', 'none'); - var linkMenu = d3.select('#link-menu'); - linkMenu.style('display', 'block'); - linkMenu.select('#source-name').text(d.source.name); - linkMenu.select('#target-name').text(d.target.name); - linkMenu.select('#edit-center-text') - .attr('value', d.centerText ? d.centerText : '') - .on('keyup', function() { - window.graph.links.filter(function(link) { - return link === d; - })[0].centerText = this.value; - restart(); - }); - linkMenu.select('#edit-source-text') - .attr('value', d.sourceText ? d.sourceText : '') - .on('keyup', function() { - window.graph.links.filter(function(link) { - return link === d; - })[0].sourceText = this.value; - restart(); - }); - linkMenu.select('#edit-target-text') - .attr('value', d.targetText ? d.targetText : '') - .on('keyup', function() { - window.graph.links.filter(function(link) { - return link === d; - })[0].targetText = this.value; - restart(); - }); - linkMenu.select('#edit-strength') - .attr('value', d.strength) - .on('input', function() { - window.graph.links.filter(function(link) { - return link === d; - })[0].strength = this.value; - restart(); - }); - linkMenu.select('#edit-link-dashed') - .property('checked', d.dashed) - .on('change', function() { - window.graph.links.filter(function(link) { - return link === d; - })[0].dashed = d3.select(this).property('checked'); - restart(); - }); - linkMenu.select('#delete-link') - .on('click', function() { - if (selected_link) { - window.graph.links - .splice(window.graph.links.indexOf(selected_link), 1); - } - selected_link = null; - selected_node = null; - restart(); - attachEvents(); - linkMenu.style('display', 'none'); - }); -} function addTemplate(template) { var parts = template.split(';'); var nodes = parts[0].split(','); var links = parts[1].split(','); var builtNodes = {}; - nodes.forEach(function(d) { - builtNodes[d] = addNode(null); + nodes.forEach(function (d) { + builtNodes[d] = createNode(null); }); - links.forEach(function(d) { + links.forEach(function (d) { var linkParts = d.split('-'); - addLink(builtNodes[linkParts[0]], builtNodes[linkParts[1]]); + createLink(builtNodes[linkParts[0]], builtNodes[linkParts[1]]); }) - - restart(); - attachEvents(); } panel.on('mousedown', mousedown) @@ -370,8 +319,11 @@ panel.on('mousedown', mousedown) d3.select(window) .on('keydown', keydown) .on('keyup', keyup); -d3.select('.expand-help').on('click', function(e) { +d3.select('.expand-help').on('click', function (e) { d3.event.preventDefault(); var body = d3.select('.instructions .body'); body.classed('hidden', !body.classed('hidden')); }); + +// call again, which will disable dragging behavior +startGraphAnimation(); \ No newline at end of file diff --git a/static/polycule.js b/static/polycule.js index de199e8..48b182d 100644 --- a/static/polycule.js +++ b/static/polycule.js @@ -1,16 +1,249 @@ 'use strict'; +class D3EventObject { + /* + Do not define an events static member, because the static + variable would be inhereted and shared by the subclasses. + Instead, we define a events variable on each subclass. + */ + + static addEvent(name, func) { + this.events[name] = func + } + + static addEvents(obj) { + Object.entries(obj).forEach( + ([name, f]) => this.addEvent(name, f) + ) + } + + static attach_events(data_binding) { + Object.entries(this.events).forEach( + ([name, f]) => (data_binding.on(name, f)) + ) + } +} + +class Node extends D3EventObject { + static events = {} + + constructor(id, point) { + super(); + this.id = id; + this.name = 'New ' + id; + if (point) { + this.x = point[0]; + this.y = point[1]; + } else { + this.x = width / 2; + this.y = height / 2; + } + this.r = 12; + } + + static from(json) { + let node = Object.assign(new Node(0), json); + return node; + } + + selected() { return this === selected_node } + + under_label_point() { return [this.x, this.y + this.r * 2]; } + + toString() { return `${this.name}[${this.x},${this.y}]`; } + + setScale(amount) { + if (amount != 1) { + this.scale = amount; + } else { + delete this.scale; + } + } + + // Draw Functions + static set_radius(node) { return node.r; } + static set_stroke(node) { return node.selected() ? '#00FFFF' : '#888'; } + static set_style(node) { return node.dashed ? 'fill:#ccc!important' : null; } + static set_stroke_dash(node) { return node.dashed ? `${node.r / 4}, ${node.r / 4}` : null; } + + static set_text_y(node) { return - node.r - 2; } + static set_text(node) { return node.name; } + + static transform(node) { + let xform = `translate(${node.x}, ${node.y})`; + if (node.scale) { + xform += ` scale(${node.scale})`; + } + return xform; + } + + static update_svg(data_binding) { + data_binding.attr('transform', Node.transform) + data_binding.select('text') + .attr('y', Node.set_text_y) + .text(Node.set_text); + data_binding.select('circle') + .attr('r', Node.set_radius) + .attr('stroke', Node.set_stroke) + .attr('style', Node.set_style) + .attr('stroke-dasharray', Node.set_stroke_dash); + } + + static create_svg(data_binding) { + let node_elements = data_binding.enter() + .append('g') + .classed('node', true); + + // circle (node) group + node_elements.append('circle') + .attr('r', Node.set_radius) + .attr('stroke', Node.set_stroke) + .attr('style', Node.set_style) + .attr('stroke-dasharray', Node.set_stroke_dash); + + // show node IDs + node_elements.append('text') + .attr('class', 'node-name') + .attr('text-anchor', 'middle') + .attr('x', 0) + .attr('y', Node.set_text_y) + .text(Node.set_text); + + return node_elements; + } +} + +class Link extends D3EventObject { + static events = {} + + constructor(source, target) { + super(); + this.source = source; + this.target = target; + this.strength = 10; + this.dashed = false; + this.targetText = ""; + this.sourceText = ""; + this.centerText = ""; + } + + selected() { + return this == selected_link; + } + + name() { + return `[${this.source.name}]->[${this.target.name}]`; + } + + toString() { return this.name(); } + + static search_index(index, source, target) { + var link = index.filter(function (l) { + return (l.source === source && l.target === target) || + (l.source === target && l.target === source); + }); + + if (link.length) { + return link[0]; + } + + return null; + } + + static from(json) { + return Object.assign(new Link(null, null), json); + } + + // position setters + static source_x(link) { return link.source.x; } + static source_y(link) { return link.source.y; } + static target_x(link) { return link.target.x; } + static target_y(link) { return link.target.y; } + static midpoint_x(link) { return (link.source.x + ((link.target.x - link.source.x) / 2)); } + static midpoint_y(link) { return (link.source.y + ((link.target.y - link.source.y) / 2)); } + static source_text_x(link) { return link.source.under_label_point()[0]; } + static source_text_y(link) { return link.source.under_label_point()[1]; } + static target_text_x(link) { return link.target.under_label_point()[0]; } + static target_text_y(link) { return link.target.under_label_point()[1]; } + + // style setters + static stroke(link) { return link.selected() ? '#00FFFF' : 'rgba(0,0,0,0.25)'; } + static stroke_width(link) { return link.strength; } + static stroke_dash(link) { return link.dashed ? `${link.strength / 1.5}, ${link.strength / 1.5}` : null; } + + static update_line(data_binding) { + data_binding + .attr('x1', Link.source_x) + .attr('y1', Link.source_y) + .attr('x2', Link.target_x) + .attr('y2', Link.target_y) + .attr('stroke', Link.stroke) + .attr('stroke-width', Link.stroke_width) + .attr('stroke-dasharray', Link.stroke_dash); + } + + static update_text(data_binding) { + data_binding.select('.center-text') + .attr('dx', Link.midpoint_x) + .attr('dy', Link.midpoint_y) + .text(function (d) { return d.centerText; }); + data_binding.select('.source-text') + .attr('dx', Link.source_text_x) + .attr('dy', Link.source_text_y) + .text(function (d) { return d.sourceText; }); + data_binding.select('.target-text') + .attr('dx', Link.target_text_x) + .attr('dy', Link.target_text_y) + .text(function (d) { return d.targetText; }); + } + + static update_svg(data_binding) { + Link.update_line(data_binding.select('line')); + Link.update_text(data_binding); + } + + static create_svg(data_binding) { + var path_elements = data_binding.enter() + .append('g') + .classed('link', true); + + Link.update_line(path_elements.append('line')); + + path_elements.append('text').attr('class', 'center-text meaning hidden'); + path_elements.append('text').attr('class', 'source-text meaning hidden'); + path_elements.append('text').attr('class', 'target-text meaning hidden'); + + Link.update_text(path_elements); + } +} + +Link.addEvents({ + mouseover: function () { + d3.select(this).selectAll('.meaning').classed('hidden', false); + }, + mouseout: function () { + d3.select(this).selectAll('.meaning').classed('hidden', true); + } +}) + // set up SVG for D3 -var width = 960, - height = 500, - selected_node = null, - selected_link = null, - mousedown_link = null, - mousedown_node = null, - mouseup_node = null, - editing = false, - scale = window.graph.scale || 1, - translate = window.graph.translate || [0, 0]; +const repaint_delay = 0; +var width = 960, + height = 500, + repaint_soon = null, + full_repaint = true, + node_props_updated_by_tick = ['x', 'y', 'px', 'py'], + selected_node = null, + selected_link = null, + mousedown_link = null, + mousedown_node = null, + mouseup_node = null, + mouse_over_link = false, + editing = false, + scale = window.graph.scale || 1, + translate = window.graph.translate || [0, 0]; + +// View management functions var panel = d3.select('#panel') .attr('oncontextmenu', 'return false;') @@ -26,7 +259,7 @@ function zoom(newScale) { var oldscale = scale; scale += newScale; window.graph.scale = scale; - scaleContainer.attr('transform', 'scale(' + scale + ')'); + scaleContainer.attr('transform', 'scale(' + scale + ')'); translate = [ translate[0] + ((width * oldscale) - (width * scale)), @@ -56,206 +289,167 @@ function pan(vert, horiz) { // } } - - -window.graph.links.forEach(function(link) { - window.graph.nodes.forEach(function(node) { - if (node.id === link.source.id) { - link.source = node; - } - if (node.id === link.target.id) { - link.target = node; - } - }); -}); d3.select('#in') - .on('click', function() { + .on('click', function () { zoom(0.1); }); d3.select('#out') - .on('click', function() { + .on('click', function () { zoom(-0.1); }); d3.select('#up') - .on('click', function() { + .on('click', function () { pan(10, 0); }); d3.select('#down') - .on('click', function() { + .on('click', function () { pan(-10, 0); }); d3.select('#left') - .on('click', function() { + .on('click', function () { pan(0, 10); }); d3.select('#right') - .on('click', function() { + .on('click', function () { pan(0, -10); }); -// init D3 force layout -var force = d3.layout.force() - .nodes(window.graph.nodes) - .links(window.graph.links) - .size([width / scale, height / scale]) - .linkDistance(function(d) { return Math.log(3 / d.strength * 10) * 50; }) - .charge(-500) - .on('tick', tick) +// Drawing management functions +function simUpdate() { + Link.update_svg(path); + Node.update_svg(node); +} -// line displayed when dragging new nodes -var drag_line = svg.append('line') - .attr('class', 'link dragline hidden'); +// update force layout (called automatically each iteration) +function repaint() { + if (full_repaint) { + path = path.data(window.graph.links); + // NB: the function arg is crucial here! nodes are known by id, not by index! + node = node.data(window.graph.nodes, function (d) { return d.id; }); -// handles to link and node element groups -var path = svg.append('g').selectAll('.link'), - node = svg.append('g').selectAll('.node'); + // paint any new nodes + Link.create_svg(path); + Node.create_svg(node); -// update force layout (called automatically each iteration) -function tick() { - if (!drag_line.classed('hidden')) { - return; + // attach d3 events to nodes and links + Link.attach_events(path); + Node.attach_events(node); + + // remove old elements + node.exit().remove(); + path.exit().remove(); + + // save changes if needed + if (typeof writeGraph != 'undefined') { + writeGraph(); + } + // (re)start the graph moving + force.start(); + } else { + // d3js is running a physics 'sim' on the nodes and edges, + // but we need to issue the draw commands + Link.update_svg(path); + Node.update_svg(node); } - path.select('line') - .attr('x1', function(d) { return d.source.x; }) - .attr('y1', function(d) { return d.source.y; }) - .attr('x2', function(d) { return d.target.x; }) - .attr('y2', function(d) { return d.target.y; }) - path.select('.source-text') - .attr('dx', function(d) { return d.source.x}) - .attr('dy', function(d) { return d.source.y + d.source.r * 2}); - path.select('.target-text') - .attr('dx', function(d) { return d.target.x}) - .attr('dy', function(d) { return d.target.y + d.target.r * 2}); - path.select('.center-text') - .attr('dx', function(d) { - return (d.source.x + ((d.target.x - d.source.x) / 2)); - }) - .attr('dy', function(d) { - return (d.source.y + ((d.target.y - d.source.y) / 2)) - 10; - }); - node.attr('transform', function(d) { - return 'translate(' + d.x + ',' + d.y + ')'; - }); + + // mini-repaint housekeeping + full_repaint = false; + // avoid double calls + clearTimeout(repaint_soon); + repaint_soon = null; } -// update graph (called when needed) -function restart() { - // path (link) group - path = path.data(window.graph.links); - - // update existing links - path.classed('selected', function(d) { return d === selected_link; }); - - - // add new links - var pathG = path.enter() - .append('g') - .classed('link', true) - .classed('selected', function(d) { return d === selected_link; }); - pathG.append('line') - .attr('x1', function(d) { return d.source.x; }) - .attr('y1', function(d) { return d.source.y; }) - .attr('x2', function(d) { return d.target.x; }) - .attr('y2', function(d) { return d.target.y; }) - .attr('stroke-width', function(d) { return d.strength; }) - .attr('stroke-dasharray', function(d) { - if (d.dashed) { - return '' + [d.strength / 1.5, d.strength / 1.5]; - } - }); - pathG.append('text') - .attr('class', 'center-text meaning hidden'); - pathG.append('text') - .attr('class', 'source-text meaning hidden'); - pathG.append('text') - .attr('class', 'target-text meaning hidden'); - // remove old links - path.exit().remove(); - - path.select('line') - .attr('stroke-width', function(d) { return d.strength; }) - .attr('stroke-dasharray', function(d) { - if (d.dashed) { - return '' + [d.strength / 1.5, d.strength / 1.5]; - } - }); - path.select('.center-text') - .text(function(d) { return d.centerText; }); - path.select('.source-text') - .text(function(d) { return d.sourceText; }); - path.select('.target-text') - .text(function(d) { return d.targetText; }); - path.on('mouseover', function(d) { - d3.select(this).selectAll('.meaning') - .classed('hidden', false); - }) - .on('mouseout', function(d) { - d3.select(this).selectAll('.meaning') - .classed('hidden', true); - }); +function trigger_repaint_style() { + if (repaint_soon === null) { + repaint_soon = setTimeout(repaint, repaint_delay); + } +} - // circle (node) group - // NB: the function arg is crucial here! nodes are known by id, not by index! - node = node.data(window.graph.nodes, function(d) { return d.id; }); - - // add new nodes - var nodeG = node.enter() - .append('g') - .classed('node', true); - - nodeG.append('circle') - .attr('class', 'node') - .attr('r', function(d) { return d.r; }) - .attr('style', function(d) { - if (d.dashed) { - return 'fill:#ccc!important'; - } - }) - .attr('stroke-dasharray', function(d) { - if (d.dashed) { - return '' + [d.r / 4, d.r / 4]; - } - }); +function trigger_full_repaint() { + // called when we add or remove elements + full_repaint = true; + trigger_repaint_style(); +} + +// Use proxy objects to trigger a draw call if needed +function style_repaint_on_set(object, props_to_ignore) { + if (typeof props_to_ignore === 'undefined') { + props_to_ignore = []; + } - // show node IDs - nodeG.append('text') - .attr('x', 0) - .attr('y', function(d) { return -d.r - 2; }) - .attr('class', 'id') - .attr('text-anchor', 'middle') - .text(function(d) { return d.name; }); - - node.select('circle') - .attr('r', function(d) { return d.r; }) - .attr('style', function(d) { - if (d.dashed) { - return 'fill:#ccc!important'; + return new Proxy(object, { + set: function (obj, prop, value) { + if (!props_to_ignore.includes(prop)) { + trigger_repaint_style(); } - }) - .attr('stroke-dasharray', function(d) { - if (d.dashed) { - return '' + [d.r / 4, d.r / 4]; + obj[prop] = value; + return true; + }, + deleteProperty: function (o, k) { + delete o[k]; + if (!props_to_ignore.includes(prop)) { + trigger_repaint_style(); } - }); + return true; + } + }); +} + + +function loadNode(json) { return style_repaint_on_set(Node.from(json), node_props_updated_by_tick); } +function loadLink(json) { + // we're restaring from stored JSON + let link = Link.from(json); + + // stored version will have a deep copy, find proper references + window.graph.nodes.forEach(function (node) { + if (node.id === link.source.id) { + link.source = node; + } + if (node.id === link.target.id) { + link.target = node; + } + }); + + return style_repaint_on_set(link); +} + +window.graph.nodes = window.graph.nodes.map(loadNode); +window.graph.links = window.graph.links.map(loadLink); + +// init D3 force layout +var force = d3.layout.force() + .nodes(window.graph.nodes) + .links(window.graph.links) + .size([width / scale, height / scale]) + .linkDistance(function (d) { return Math.log(3 / d.strength * 10) * 50; }) + .charge(-500) + .on('tick', simUpdate); - node.select('.id') - .attr('y', function(d) { return -d.r - 2; }) - .text(function(d) { return d.name; }); +// line displayed when dragging new nodes +var drag_line = svg.append('line') + .attr('class', 'link dragline hidden'); + +// handles to link and node element groups +var path = svg.append('g').selectAll('.link'), + node = svg.append('g').selectAll('.node'); - // remove old nodes - node.exit().remove(); +function startGraphAnimation() { + // rebuild nodes and links + trigger_full_repaint(); + // don't wait + repaint(); - // set the graph in motion - force.start(); try { writeGraph(); - } catch(e) { + } catch (e) { + // Start dragging behavior if we are not editing the graph node.call(force.drag); } } - + function panzoom() { d3.event.preventDefault() switch (d3.event.key) { @@ -292,4 +486,4 @@ d3.select(window) .on('keydown', panzoom); // app starts here -restart(); +startGraphAnimation(); \ No newline at end of file diff --git a/static/style.css b/static/style.css index e6da7de..e7390bd 100644 --- a/static/style.css +++ b/static/style.css @@ -85,7 +85,6 @@ form { } #graph .link line { - stroke: rgba(0,0,0,0.25); cursor: pointer; } @@ -103,7 +102,6 @@ form { #graph .node circle { fill: #888; - stroke: #888; stroke-width: 1px; } diff --git a/templates/create_polycule.jinja2 b/templates/create_polycule.jinja2 index bfcae71..dcfb43e 100644 --- a/templates/create_polycule.jinja2 +++ b/templates/create_polycule.jinja2 @@ -35,6 +35,7 @@
Delete: