diff options
| author | Alon Levy <alon@pobox.com> | 2015-01-04 16:44:38 +0200 |
|---|---|---|
| committer | Alon Levy <alon@pobox.com> | 2015-01-05 13:23:57 +0200 |
| commit | ff5c984089c19467304dbec0b87b2aa019717b13 (patch) | |
| tree | 91a10398f03988516f44672078e3705b8d2555cb /src | |
| parent | d50ec127af2fe7c7276380e10fce00496580fd58 (diff) | |
initial graph_view introduction - mixing circle & force layouts
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/buttons.js | 12 | ||||
| -rw-r--r-- | src/client/main.js | 7 | ||||
| -rw-r--r-- | src/client/model/graph.js | 87 | ||||
| -rw-r--r-- | src/client/model/util.js | 5 | ||||
| -rw-r--r-- | src/client/rz_core.js | 529 | ||||
| -rw-r--r-- | src/client/textanalysis.js | 217 | ||||
| -rw-r--r-- | src/client/textanalysis.ui.js | 14 | ||||
| -rw-r--r-- | src/client/view/graph_view.js | 492 | ||||
| -rw-r--r-- | src/client/view/helpers.js | 6 | ||||
| -rw-r--r-- | src/client/view/selection.js | 8 |
10 files changed, 685 insertions, 692 deletions
diff --git a/src/client/buttons.js b/src/client/buttons.js index 860c8573..e29a1729 100644 --- a/src/client/buttons.js +++ b/src/client/buttons.js @@ -7,13 +7,13 @@ var key="#47989379"; $('.save a').click(function(){ - var json = rz_core.graph.save_to_json(); + var json = rz_core.main_graph.save_to_json(); console.log('saving to local storage ' + json.length + ' bytes'); localStorage.setItem(key, json); }); $('.export').click(function() { - var json = rz_core.graph.save_to_json(); + var json = rz_core.main_graph.save_to_json(); var filename = 'graph.json'; var blob = new Blob([json], {type: 'application/json'}); console.log('saving ' + json.length + ' bytes to ' + filename); @@ -21,14 +21,14 @@ $('.export').click(function() { }); $('.url-copy a').click(function() { - var json = rz_core.graph.save_to_json(); + var json = rz_core.main_graph.save_to_json(); // TODO use jquery BBQ $.param({json: json}); var encoded = document.location.origin + '/?json=' + encodeURIComponent(json); window.prompt('Copy to clipboard: Ctrl-C, Enter (or Cmd-C for Mac)', encoded); }); var really_load = function() { - if (!rz_core.graph.empty()) { + if (!rz_core.main_graph.empty()) { return confirm('All unsaved changes will be deleted, are you sure?'); } return true; @@ -73,10 +73,10 @@ logout_button.click(function() { }); $('a.save-history').click(function() { - if (rz_core.graph.history === undefined) { + if (rz_core.main_graph.history === undefined) { throw "History is undefined"; } - rz_core.graph.history.save_to_file(); + rz_core.main_graph.history.save_to_file(); }); return {'buttons': 'nothing here'}; }); // define diff --git a/src/client/main.js b/src/client/main.js index 47c9de1d..ca7625aa 100644 --- a/src/client/main.js +++ b/src/client/main.js @@ -45,7 +45,8 @@ function(textanalysis_ui, textanalysis, buttons, history, drag_n_drop, } if (util.getParameterByName('debug')) { $(document.body).addClass('debug'); - rz_core.graph.set_user('fakeuser'); + rz_core.edit_graph.set_user('fakeuser'); + rz_core.main_graph.set_user('fakeuser'); } document.body.onkeyup = function(e) { @@ -59,7 +60,7 @@ function(textanalysis_ui, textanalysis, buttons, history, drag_n_drop, search.focus(); } if (e.ctrlKey && key == 'z' && e.target.nodeName !== 'INPUT') { - // TODO: rz_core.graph.undo(); + // TODO: rz_core.main_graph.undo(); } }; // TODO: move me somewhere @@ -101,7 +102,7 @@ function(textanalysis_ui, textanalysis, buttons, history, drag_n_drop, // TODO: interaction between the hack above and this model_core.init(rz_config); - textanalysis.init(rz_core.graph); + textanalysis.init(rz_core.main_graph); } return { diff --git a/src/client/model/graph.js b/src/client/model/graph.js index 46b0359b..61797886 100644 --- a/src/client/model/graph.js +++ b/src/client/model/graph.js @@ -5,7 +5,7 @@ function (Bacon, consts, util, model_core, model_util, model_diff, rz_api_backen var debug = false; -function Graph() { +function Graph(temporary) { var id_to_node_map = {}, node_map = {}, @@ -38,9 +38,11 @@ function Graph() { * * @return node if node was actually added */ - this.addTempNode = function(spec) { - util.assert(spec.state === 'temp', "node is not a temp node"); - return this.__addNode(spec); + // FIXME: is this a good idea? i.e. changing API based on constructor? just in dynamic city + if (temporary) { + this.addTempNode = function(spec) { + return this.__addNode(spec); + } } var nodes_to_touched_links = function (node_id_set) { @@ -141,11 +143,7 @@ function Graph() { } else { node = model_core.create_node__set_random_id(spec); if (debug) { - if ('bubble' != node.type){ - console.log('__addNode: stamping node id: ' + node.id + ', name: \'' + node.name + '\' (bubble)'); - }else { - console.log('__addNode: stamping node id: ' + node.id + ', name: \'' + node.name + '\''); - } + console.log('__addNode: stamping node id: ' + node.id + ', name: \'' + node.name + '\''); } } } else { @@ -228,7 +226,7 @@ function Graph() { * * NOTE: chainlinks are treated specially, they don't count for distance. So all their decendants will be added. * - * NOTE: temp state nodes (n.state === 'temp') are ignored. + * NOTE: Doesn't handle inter graph links * * @return - { * 'node': [node] @@ -271,19 +269,14 @@ function Graph() { var adjacentnode; if (same(link.__src, n)) { adjacentnode = find_node__by_id(link.__dst.id); - if (adjacentnode.state !== "temp") { - addNode({type: 'exit', node: adjacentnode}); - } + addNode({type: 'exit', node: adjacentnode}); ret.links.push({type: 'exit', link: link}); if (link.__dst.type === "chainlink") { links_forEach(function(link2) { if (link.__dst.id === link2.__dst.id && - link2.__dst.type === "chainlink" && - link2.__dst.state !== "temp") { + link2.__dst.type === "chainlink") { adjacentnode = find_node__by_id(link2.__src.id); - if (adjacentnode.state !== "temp") { - addNode({type: 'enter', node: adjacentnode}); - } + addNode({type: 'enter', node: adjacentnode}); ret.links.push({type: 'enter', link: link2}); } }); @@ -291,9 +284,7 @@ function Graph() { } if (same(link.__dst, n)) { adjacentnode = find_node__by_id(link.__src.id); - if (adjacentnode.state !== "temp") { - addNode({type: 'enter', node: adjacentnode}); - } + addNode({type: 'enter', node: adjacentnode}); ret.links.push({type: 'enter', link: link}); } }); @@ -309,11 +300,7 @@ function Graph() { * a single node id change. false otherwise */ this.compareSubset = function(state, new_nodes, new_links) { - // Note: the nodes include a state=='temp', type=='bubble' node - // but it's ok since it exists both in new_nodes and in state_nodes - var state_nodes = findNodes(null, state).filter(function (nd) { - return nd.type !== 'bubble'; - }); + var state_nodes = findNodes(null, state); var state_links = findLinks(state).map(function(link) { return [link.__src.name, link.__dst.name]; }).sort(); @@ -388,7 +375,7 @@ function Graph() { return; } - util.assert(src.state === 'temp' && dst.state === 'temp' && state == "temp", + util.assert(temporary, "creation of link not through commit_and_tx that isn't temporary"); var link = model_core.create_link__set_random_id(src, dst, { name: name, @@ -416,9 +403,11 @@ function Graph() { existing_link.state = link.state; } } - this.addTempLink = function (link) { - util.assert(link.state === "temp", "link is not temporary"); - return __addLink(link); + // FIXME: good idea to add API based on constructor parameter? + if (temporary) { + this.addTempLink = function (link) { + return __addLink(link); + } } this.update_link = function(link, new_link_spec, on_success, on_error) { @@ -541,7 +530,7 @@ function Graph() { if (n_eq_id.name == new_name) { return; } - util.assert(n_eq_id.state === 'temp', 'editName should now only be used by textanalysis'); + util.assert(temporary, 'editName should now only be used on temporary graphs'); util.assert(n_eq_name === undefined); n_eq_id.name = new_name; @@ -574,6 +563,7 @@ function Graph() { if ((n === undefined)) { return false; } + console.debug("deprecated and broken probably"); // cannot assert state is temp since robot code still uses it if (state != 'temp') { console.log('warning: I hope robot is doing this not-through-backend property change'); @@ -613,10 +603,10 @@ function Graph() { this._remove_link_set = _remove_link_set; this.removeNodes = function(state) { - var temp_node_ids = get_nodes().filter(function (n) { return n.state == state; }) + var node_ids = get_nodes().filter(function (n) { return n.state == state; }) .map(function (n) { return n.id; }), topo_diff = model_diff.new_topo_diff({ - node_set_rm : temp_node_ids, + node_set_rm : node_ids, }); this.commit_diff__topo(topo_diff); @@ -659,14 +649,6 @@ function Graph() { } this.hasNodeByName = hasNodeByName; - var hasNodeByNameAndNotState = function(name, state) { - util.assert(state === 'temp', "this is deprecated and to be removed but anyway just for temp"); - return get_nodes().filter(function(n) { - return compareNames(n.name, name) && n.state !== state; - }).length > 0; - } - this.hasNodeByNameAndNotState = hasNodeByNameAndNotState; - /** * return node whose id matches the given id or undefined if no node was found */ @@ -707,8 +689,13 @@ function Graph() { } function clear() { - nodes.length = 0; - links.length = 0; + id_to_node_map = {}; + node_map = {} + id_to_link_map = {}; + link_map = {}; + invalidate_links = true; + invalidate_nodes = true; + // FIXME: push on diffBus? } this.clear = clear; @@ -764,8 +751,10 @@ function Graph() { }); diff.link_set_add.map(function (link_spec) { // resolve link ptr - var src = find_node__by_id(link_spec.__src_id), - dst = find_node__by_id(link_spec.__dst_id) || find_node__by_name(link_spec.__dst.name), + var src = find_node__by_id(link_spec.__src_id) + || (temporary && find_node__by_name(link_spec.__src.name)), + dst = find_node__by_id(link_spec.__dst_id) + || (temporary && find_node__by_name(link_spec.__dst.name)), link = model_core.create_link_from_spec(src, dst, link_spec); __addLink(link); @@ -926,9 +915,6 @@ function Graph() { for (i = 0 ; i < n_length; ++i) { node = nodes[i]; - if (node.state == 'temp') { - continue; - } if (node_visitor(node)) { selected.push(node); } @@ -940,7 +926,7 @@ function Graph() { removeRelated(); nodes_forEach(function (node) { names.forEach(function (name) { - if (compareNames(node.name, name) && node.state != 'temp') { + if (compareNames(node.name, name)) { node.state = 'related'; } }); @@ -949,8 +935,9 @@ function Graph() { this.markRelated = markRelated; function removeRelated() { + // FIXME: related should use separate variable, not overload 'state' (bad bad bad) nodes_forEach(function (node) { - if (node.state == 'related') { + if (node.state === 'related') { node.state = 'perm'; } }); diff --git a/src/client/model/util.js b/src/client/model/util.js index 25d419bf..20472fda 100644 --- a/src/client/model/util.js +++ b/src/client/model/util.js @@ -106,11 +106,6 @@ define([ 'jquery', 'model/diff' ], function($, model_diff) { var new_n_set = $.extend([], n_set); var new_l_set = $.extend([], l_set); - // filter out 'bubble' nodes - new_n_set = new_n_set.filter(function(n) { - return 'bubble' != n.type; - }); - new_n_set = $.map(new_n_set, function(n, _) { return adapt_format_write_node(n); }) diff --git a/src/client/rz_core.js b/src/client/rz_core.js index 36d8058e..bd1dc141 100644 --- a/src/client/rz_core.js +++ b/src/client/rz_core.js @@ -1,7 +1,7 @@ "use strict" -define(['jquery', 'd3', 'consts', 'rz_bus', 'util', 'model/graph', 'model/core', 'view/helpers', 'view/view', 'rz_observer', 'view/selection', 'rz_config', 'rz_mesh', 'model/diff'], -function($, d3, consts, rz_bus, util, model_graph, model_core, view_helpers, view, rz_observer, selection, rz_config, rz_mesh, model_diff) { +define(['jquery', 'd3', 'consts', 'rz_bus', 'util', 'model/graph', 'model/core', 'view/helpers', 'view/view', 'rz_observer', 'view/selection', 'rz_config', 'rz_mesh', 'model/diff', "view/graph_view"], +function($, d3, consts, rz_bus, util, model_graph, model_core, view_helpers, view, rz_observer, selection, rz_config, rz_mesh, model_diff, graph_view) { var addednodes = [], vis, @@ -10,9 +10,10 @@ var addednodes = [], deliverables = [], circle, // <-- should not be module globals. scrollValue = 0, - graph, - drag, - force; + main_graph, + edit_graph, + main_graph_view, + edit_graph_view; // "CSS" for SVG elements. Reused for editing elements. var node_text_dx = 15, @@ -76,11 +77,11 @@ var svgInput = (function() { d = jelement.data().d; if (e.which == 13 && newname != d.name) { if (d.hasOwnProperty('__src')) { - graph.update_link(d, {name: newname}); + main_graph.update_link(d, {name: newname}); } else { // TODO - display hourglass // TODO - use promises to make A follows B readable. - graph.update_node(d, {name: newname}); + main_graph.update_node(d, {name: newname}); } } hide(); @@ -175,57 +176,40 @@ var svgInput = (function() { }; }()); +var zoomProgress = false; +var zoomBus = new Bacon.Bus(); +var zoomProperty = zoomBus.toProperty(); +zoomBus.push(zoomProgress); // set initial value +function svg_click_handler(e) { + if (zoomProgress) { + zoomProgress = false; + return; + } + if (e.originalEvent.target.nodeName != 'svg') { + return; + } + svgInput.hide(); + selection.clear(); + view.hide(); + update_view__graph(false); +} function recenterZoom() { vis.attr("transform", "translate(0,0)scale(1)"); } -// zoom or drag -var zoomInProgress = false; +main_graph = new model_graph.Graph(false); +edit_graph = new model_graph.Graph(true); var initDrawingArea = function () { function zoom() { - zoomInProgress = true; + zoomProgress = true; vis.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); d3.event.sourceEvent.stopPropagation(); } - function dragstarted(d) { - d3.event.sourceEvent.stopPropagation(); - d3.select(this).classed("dragging", true); - d.dragstart = {clientX:d3.event.sourceEvent.clientX, clientY:d3.event.sourceEvent.clientY}; - force.stop(); - } - - function dragged(d) { - d.x = d3.event.x; - d.y = d3.event.y; - tick(); - } - - function dragended(d) { - d3.select(this).classed("dragging", false); - d3.select(this).classed("fixed", true); // TODO: this is broken since we override all the classes. Need to switch to class addition/removal (i.e. use classed for everything) or set class in one location (so here just set a value on the node, not the element) - if (d.dragstart.clientX - d3.event.sourceEvent.clientX != 0 || - d.dragstart.clientY - d3.event.sourceEvent.clientY != 0) { - tick(); - force.resume(); - } - } - - graph = new model_graph.Graph(); - graph.diffBus.onValue(function (diff) { - var relayout = (false == model_diff.is_attr_diff(diff)); - if (relayout) { - console.log('****** graph relayout *******'); - } else { - console.log('****** graph update *******'); - } - console.dir(diff); - update_view__graph(relayout); - }); // TODO: we are listening both on graph.diffBus and selection.selectionChangedBus, // but the relation is actually: // @@ -235,14 +219,16 @@ var initDrawingArea = function () { // we need to deduplicate this event // but there is no coordination, resulting in double updates. selection.selectionChangedBus.onValue( - function() { update_view__graph(false); }); + function() { update_view__graph(false); + }); var user_id = $('#user_id'), user = user_id.text(); if (user_id.length > 0) { console.log('found user ID: \'' + user + '\''); - graph.set_user(user); + main_graph.set_user(user); + edit_graph.set_user(user); } var el = document.body; @@ -271,54 +257,47 @@ var initDrawingArea = function () { * init zoom behavior */ var zoom_obj = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom); - zoom_obj(d3.select('#canvas_d3')) + zoom_obj(d3.select('#canvas_d3')); d3.select("svg").on("dblclick.zoom", null); // disable zoom on double click $('svg').click(svg_click_handler); - // SVG rendering order is last rendered on top, so to make sure - // all links are below the nodes we group them under a single g - vis.append("g").attr("id", "link-group"); - vis.append("g").attr("id", "selected-link-group"); - - drag = d3.behavior.drag() - .origin(function(d) { return d; }) - .on("dragstart", dragstarted) - .on("drag", dragged) - .on("dragend", dragended); + main_graph_view = graph_view.GraphView({ + vis: vis.append('g'), + graph_name: "main", + graph: main_graph, + zoomProperty: zoomProperty, + forceEnabled: true, + node_text_dx: node_text_dx, + node_text_dy: node_text_dy, + }); + edit_graph_view = graph_view.GraphView({ + vis: vis.append('g'), + graph_name: "edit", + graph: edit_graph, + zoomProperty: zoomProperty, + forceEnabled: false, + node_text_dx: node_text_dx, + node_text_dy: node_text_dy, + }); // $('#canvas_d3').dblclick(canvas_handler_dblclick); - see #138 - if (rz_config.backend_enabled){ - graph.load_from_backend(); + if (rz_config.backend_enabled) { + main_graph.load_from_backend(); } } -function init_force_layout(){ - var el = document.body; - var w = $(el).innerWidth(), - h = $(el).innerHeight(); - - force = d3.layout.force() - .distance(120) - .gravity(0.12) - .charge(-1800) - .size([w, h]) - .on("tick", tick) - .start(); -} - /** * Initialize backend websocket connection - this will have no effect if * rz_config.backend__maintain_ws_connection is set to 'false' */ function init_ws_connection(){ if (true == rz_config.backend__maintain_ws_connection){ - rz_mesh.init({graph: graph}); + rz_mesh.init({graph: main_graph}); } } initDrawingArea(); -init_force_layout(); init_ws_connection(); /** @@ -343,7 +322,7 @@ function canvas_handler_dblclick(){ var n = model_core.create_node__set_random_id(); n.name = ''; // will be set by user - graph.commit_and_tx_diff__topo(model_diff.new_topo({ + main_graph.commit_and_tx_diff__topo(model_diff.new_topo({ node_set_add : [n].map(model_util.adapt_format_write_node), })); @@ -365,380 +344,12 @@ function canvas_handler_dblclick(){ }); } -/** - * update view: graph - */ -function update_view__graph(relayout) { - var node, - link, - link_g, - linktext, - nodetext, - unselected_link_group = document.querySelector('#link-group'), - selected_link_group = document.querySelector('#selected-link-group'), - relayout = relayout || true; - - //return; - - link = vis.selectAll("g.link") - .data(graph.links(), function(d) { return d.id; }); - - link_g = link.enter().append('g') - .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping - .attr('class', 'link graph') - - link_g.append("path") - .attr("class", function(d) { - return d.state + ' link graph'; - }) - .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping - .attr("marker-end", "url(#end)"); - - // second path for larger click area - link_g.append("path") - .attr("class", "ghostlink") - .on("click", function(d, i) { - if (zoomInProgress) { - // don't disable zoomInProgress, it will be disabled by the svg_click_handler - // after this events bubbles to the svg element - return; - } - var that = this, - src = this.link.__src, - dst = this.link.__dst; - - view.edge_info.on_delete(function () { - view.edge_info.hide(); - graph.removeLink(that.link); - }); - view.edge_info.show(d); - selection.update([src, dst]); - }); - - link.attr("class", function(d, i){ - var temp_and = (d.name && d.name.replace(/ /g,"")=="and" && d.state==="temp") ? "temp_and" : ""; - - return ["graph link", temp_and, selection.selected_class(d)].join(' '); - }); - - link.selectAll('path.link') - .attr('class', function(d) { - return [d.state, selection.selected_class(d), "link graph"].join(' '); - }); - - link.exit().remove(); - - vis.selectAll('.ghostlink') - .data(graph.links()) - .each(function (d) { - this.link = d; - }); - - linktext = vis.selectAll(".linklabel") - .data(graph.links(), function(d) { return d.id; }); - linktext.enter() - .append("text") - .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping - .attr("class", function(d) { - return ["linklabel graph", selection.selected_class(d)].join(' '); - }) - .attr("text-anchor", "middle") - .on("click", function(d, i) { - if (d.state !== "temp") { - svgInput.enable(this, d); - } - }); - - linktext - .text(function(d) { - var name = d.name || ""; - if (!(d.__dst.state === "temp" || - d.__src.state === "chosen" || d.__dst.state === "chosen")) { - return ""; - } - if (name.length < 25 || d.__src.state === "chosen" || - d.__dst.state === "chosen" || d.state==="temp") { - return name; - } else { - return name.substring(0, 14) + "..."; - } - }); - - linktext.exit().remove(); - - node = vis.selectAll(".node") - .data(graph.nodes(), function(d) { - return d.id; - }); - console.log("update update update update"); - console.dir(node); - - var nodeEnter = node.enter() - .append("g") - .attr('id', function(d){ return d.id; }) // append node id to enable data->visual mapping - .attr('visibility', 'hidden') // made visible on first tick - .call(drag); - - // reorder nodes so selected are last, and so rendered last, and so on top. - (function () { - var ontop = [], - bubble; - - node.each(function (d) { - this.node = d; - }) - .attr('class', function(d) { - if (selection.node_selected(d)) { - if (d.type == 'bubble') { - bubble = this; - } else { - ontop.push(this); - } - } - return ['node', selection.selected_class(d)].join(' '); - }); - if (bubble === undefined) { - // nothing to do if there is no bubble - return; - } - function reparent(new_parent, element) { - if (element.parentNode == new_parent) { - return; - } - new_parent.appendChild(element); - } - // move link to correct group - // O(|links|*|ontop|) - link.each(function (d) { - if (ontop.some(function (node) { - var d_node = node.node; - return d.__src == d_node || d.__dst == d_node; - })) - { - reparent(selected_link_group, this); - } else { - reparent(unselected_link_group, this); - } - }); - linktext.each(function (d) { - if (selection.node_selected(d)) { - ontop.push(this); - } - }); - function moveToEnd(e) { - e.parentNode.appendChild(e); - } - moveToEnd(bubble); - ontop.reverse().forEach(function (e) { - moveToEnd(e); - }); - var count_links = function() { - return selected_link_group.childElementCount + unselected_link_group.childElementCount; - }; - // put back on top link group on top - selected_link_group.parentNode.insertBefore(selected_link_group, bubble.nextSibling); - })(); - - nodetext = nodeEnter.insert("text") - .attr("class", "nodetext graph") - .attr("dx", node_text_dx) - .attr("dy", node_text_dy) - .on("click", function(d, i) { - if (d3.event.defaultPrevented) { - // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on - return; - } - if (d.state !== "temp") { - svgInput.enable(this, d); - selection.update([d]); - showNodeInfo(this.parentNode.node, i); - } - d3.event.stopPropagation(); - }); - - node.select('g.node text') - .text(function(d) { - if (!d.name) { - return d.type == 'bubble' ? "" : "_"; - } - if (d.state === "temp" || d.state === 'chosen' - || d.state === "enter" || d.state === "exit") { - return d.name; - } else { - if (d.name.length < 28) { - return d.name; - } else { - return d.name.substring(0, 25) + "..."; - } - } - }); - - circle = nodeEnter.insert("circle"); - node.select('g.node circle') - .attr("class", function(d) { - return d.type + " " + d.state + " circle graph"; - }) - .attr("r", function(d) { - return view_helpers.customSize(d.type) - 2; - }) - .on("click", function(d, i) { - if (d3.event.defaultPrevented) { - // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on - return; - } - d3.event.stopPropagation(); - selection.update([d]); - if(d.state !== "temp") { - showNodeInfo(d, i); - } - }); - circle.append("svg:image") - .attr("class", "status graph") - .attr('x', -7) - .attr('y', -8) - .attr('width', 15) - .attr('height', 15) - .attr("xlink:href", function(d) { - switch (d.status) { - case "done": - return "res/img/check.png"; - break; - case "current": - return "res/img/wait.png"; - break; - case "waiting": - return "res/img/cross.png"; - break; - } - }); - - node.exit().remove(); - - //update deliverables - deliverables = []; - var nodes = graph.nodes(); - for (var i = 0; i < nodes.length; i++) { - var current = nodes[i]; - if (current.type === "third-internship-proposal") { - deliverables.push({ - "id": nodes[i].id, - "startdate": nodes[i].start, - "enddate": nodes[i].end - }); - } - //Do something - } - - force.nodes(graph.nodes()) - .links(graph.links()) - - if (relayout) { - force.alpha(0.1).start(); - } else { - // XXX If we are stopped we need to update the text of the links at least, - // and this is the simplest way - tick(); - } -} - - - -var debug_print = function(message) { - var element = $(".debug"); - if (element.length == 1) { - element.html(message); - } else { - console.log(message); - } -} - -function check_for_nan(x) { - if (Number.isNaN(x)) { - console.log('nan problem'); - force.stop(); - } - return Number.isNaN(x); -} - -var newnodes=1; -function tick(e) { - //console.log(e); - //$(".debug").html(force.alpha()); - var node = vis.selectAll(".node") - .data(graph.nodes(), function(d) { - return d.id; - }); - var link = vis.selectAll("path.link") - .data(graph.links(), function(d) { - return d.id; - }); - var linktext = vis.selectAll(".linklabel").data(graph.links()); - - function transform(d) { - if (check_for_nan(d.x) || check_for_nan(d.y)) { - return; - } - return "translate(" + d.x + "," + d.y + ")"; - } - - //circles animation - var tempcounter = 0, - temptotal = graph.nodes().filter(function(d){ - return d.state === "temp" && d.type !== "chainlink" && d.type !== "bubble"; - }).length; - if (temptotal !== newnodes) { - newnodes += temptotal / 15 / (newnodes * newnodes); - } - newnodes = Math.max(1, Math.min(newnodes, temptotal)); - graph.nodes().forEach(function(d, i) { - var r, a; - if (d.state === "temp") { - tempcounter++; - if (d.type==="chainlink" || d.type==="bubble") { - d.x = window.innerWidth / 2; - d.y = window.innerHeight / 2; - } else { - r = 60 + newnodes * 20; - a = -Math.PI + Math.PI * 2 * (tempcounter-1) / newnodes + 0.3; - d.x = window.innerWidth / 2 + r * Math.cos(a); - d.y = window.innerHeight / 2 + r * Math.sin(a); - } - check_for_nan(d.x); - check_for_nan(d.y); - } - }); - - link.attr("d", function(d, i) { - var d_val, - ghost; - - var dx = d.__dst.x - d.__src.x, - dy = d.__dst.y - d.__src.y, - dr = Math.sqrt(dx * dx + dy * dy); - d_val = "M" + d.__src.x + "," + d.__src.y + "L" + d.__dst.x + "," + d.__dst.y; - // update ghostlink position - ghost = $(this.nextElementSibling); - ghost.attr("d", d_val); - return d_val; - }); - - linktext.attr("transform", function(d) { - return "translate(" + (d.__src.x + d.__dst.x) / 2 + "," + (d.__src.y + d.__dst.y) / 2 + ")"; - }); - - node.attr("transform", transform); - - // After initial placement we can make the nodes visible. - //links.attr('visibility', 'visible'); - node.attr('visibility', 'visible'); -} - function showNodeInfo(node, i) { var closed = false; view.node_info.on_save(function(e, form_data) { closed = true; - graph.update_node(node, form_data, function() { + main_graph.update_node(node, form_data, function() { var old_type = node.type, new_type = form_data.type; @@ -748,7 +359,7 @@ function showNodeInfo(node, i) { }); // FIXME - attribute diff, ignore uninteresting diffs via filtering - graph.diffBus.onValue(function () { + main_graph.diffBus.onValue(function () { if (closed) { return Bacon.noMore; } @@ -762,31 +373,25 @@ function showNodeInfo(node, i) { console.log("closing node info"); closed = true; view.node_info.hide(); - graph.commit_and_tx_diff__topo(topo_diff); + main_graph.commit_and_tx_diff__topo(topo_diff); }); view.node_info.show(node); } -function svg_click_handler(e) { - if (zoomInProgress) { - zoomInProgress = false; - return; - } - if (e.originalEvent.target.nodeName != 'svg') { - return; - } - svgInput.hide(); - selection.clear(); - view.hide(); - update_view__graph(false); +function update_view__graph(relayout) +{ + main_graph_view.update_view(relayout); + edit_graph_view.update_view(relayout); } return { - graph: graph, - force: force, + main_graph: main_graph, + edit_graph: edit_graph, + main_graph_view: main_graph_view, + edit_graph_view: edit_graph_view, load_from_json: function(result) { - graph.load_from_json(result); + main_graph.load_from_json(result); recenterZoom(); update_view__graph(true); }, diff --git a/src/client/textanalysis.js b/src/client/textanalysis.js index ebfd3007..a34aac7f 100644 --- a/src/client/textanalysis.js +++ b/src/client/textanalysis.js @@ -70,7 +70,10 @@ var lastnode; var sugg_name = {}, id_to_name_map = {}, - suggestions_options = new Bacon.Bus(); // TODO: Property: same as bus, but with initial value + suggestions_bus = new Bacon.Bus(), + suggestions_options = suggestions_bus.toProperty(); + +suggestions_bus.push([]); var ANALYSIS_NODE_START = 'ANALYSIS_NODE_START'; var ANALYSIS_NODE = 'ANALYSIS_NODE' @@ -88,80 +91,24 @@ function selectedType() function auto_suggest__update_name(name, id) { if (id !== undefined && id_to_name_map[id] !== undefined) { - delete sugg[id_to_name_map[id]]; + delete sugg_name[id_to_name_map[id]]; id_to_name_map[id] = name; } /* note that name can contain spaces - this is ok. We might want to limit this though? */ - sugg[name] = 1; - suggestions_options.push(sugg); + sugg_name[name] = 1; + suggestions_bus.push(sugg_name); } function auto_suggest_remove_name(name, id) { - delete sugg[name]; - if (id !== undefined) { - delete sugg[id]; - } - suggestions_options.push(sugg); -} - -/* up_to_two_renames: - * - * allow one letter or 'new node' to anything changes */ -function up_to_two_renames(graph, old_name, new_name, link_set_add) -{ - var not_one_letter = false; - var k; - /* Allowed renames: - * no change - * s1 is substring of s2 - * older (s1) node being 'new node' - */ - function allowed_rename(s1, s2) - { - return (s1 == s2 || - s1 == 'new node' || - s1.substr(0, s2.length) == s2 || - s2.substr(0, s1.length) == s1); - } - - if (old_name.length != new_name.length) { - console.log('bug: up_to_two_renames: not equal inputs'); - return; + if (name !== undefined) { + delete sugg_name[name]; } - if (old_name.length > 2) { - console.log('bug: up_to_two_renames: input length 2 < ' + old_name.length); - return; - } - if (old_name.length == 2) { - if (allowed_rename(old_name[0], new_name[1]) && - allowed_rename(old_name[1], new_name[0])) { - old_name = [old_name[1], old_name[0]]; - } else { - if (!allowed_rename(old_name[0], new_name[0]) || - !allowed_rename(old_name[1], new_name[1])) { - not_one_letter = true; - } - } - } - if (not_one_letter) { - console.log('bug: up_to_two_renames: not one letter changes'); - console.log(old_name); - console.log(new_name); - return; - } - for (k = 0 ; k < old_name.length ; ++k) { - graph.editNameByName(old_name[k], new_name[k]); - // very disappointing. suicide threats are ok? - link_set_add.forEach(function (link) { - if (link.__src.name == old_name[k]) { - link.__src.name = new_name[k]; - } - if (link.__dst.name == old_name[k]) { - link.__dst.name = new_name[k]; - } - }); + if (id !== undefined) { + delete sugg_name[id_to_name_map[id]]; + delete id_to_name_map[id]; } + suggestions_bus.push(sugg_name); } // TODO: add escape char, i.e. r"bla\"bla" -> ['bla"bla'] @@ -233,7 +180,7 @@ var textAnalyser = function (newtext, finalize) { word, completeSentence, completeSentenceParts, - typesetter, starGraph, + starGraph, n, link_hash = {}, yell_bug = false, // TODO: fix both issues @@ -242,14 +189,14 @@ var textAnalyser = function (newtext, finalize) { START = "START", ret = model_diff.new_topo_diff(); - function __addNode(name, type, state) { + function __addNode(name, type) { if (type === undefined) { console.log('bug: textanalyser.addNode of type undefined'); } - var node = model_core.create_node_from_spec( - {'name':name, - 'type':type, - 'state':state}); + var node = model_core.create_node_from_spec({ + 'name':name, + 'type':type, + }); ret.node_set_add.push(node); } @@ -264,7 +211,7 @@ var textAnalyser = function (newtext, finalize) { return {'existing': name}; }; - function __addLink(src_name, dst_name, name, state) { + function __addLink(src_name, dst_name, name) { if (!src_name || !dst_name) { if (yell_bug) { console.log('bug - adding link (' + src_name + ', ' + dst_name + ')'); @@ -287,7 +234,6 @@ var textAnalyser = function (newtext, finalize) { '__src': src_name, '__dst': dst_name, 'name':name, - 'state':state }; ret.link_set_add.push(link); } @@ -376,19 +322,10 @@ var textAnalyser = function (newtext, finalize) { nodeindex = 0; //CHANGE TO PERMANENT STATE AND UPDATE SUGGESTIONLIST - typesetter = ""; if (finalize === true) { - typesetter = "perm"; for (n = 0; n < token_set_new_node_names.length; n++) { auto_suggest__update_name(token_set_new_node_names[n]); } - } else { - typesetter = "temp"; - } - - //ADD SURROUNDING BUBBLE - if (orderStack.length > 0) { - __addNode("", "bubble","temp"); } //0-N ORDER STACK @@ -400,11 +337,11 @@ var textAnalyser = function (newtext, finalize) { } break; case NODE: - __addNode(token_set_new_node_names[nodeindex], typeStack[nodeindex], typesetter); + __addNode(token_set_new_node_names[nodeindex], typeStack[nodeindex]); if (!starGraph && nodeindex > 0 && token_set_new_link_names[linkindex] !== undefined) { __addLink(token_set_new_node_names[nodeindex - 1], token_set_new_node_names[nodeindex], - token_set_new_link_names[linkindex], typesetter); + token_set_new_link_names[linkindex]); } nodeindex++; break; @@ -428,11 +365,11 @@ var textAnalyser = function (newtext, finalize) { break; case NODE: typeStack[nodeindex] = selectedType(); - __addNode(token_set_new_node_names[nodeindex], typeStack[nodeindex], typesetter); + __addNode(token_set_new_node_names[nodeindex], typeStack[nodeindex]); if (!starGraph && nodeindex > 0 && token_set_new_link_names[linkindex] !== undefined) { __addLink(token_set_new_node_names[nodeindex - 1], token_set_new_node_names[nodeindex], - token_set_new_link_names[linkindex], typesetter); + token_set_new_link_names[linkindex]); and_connect(token_set_new_node_names[nodeindex]); } ret.state = ANALYSIS_NODE_START; @@ -455,9 +392,10 @@ var textAnalyser = function (newtext, finalize) { if(token_set_new_link_names[x])if(token_set_new_link_names[x].replace(/ /g,"")!=="and"){ verb = token_set_new_link_names[x]; for(var y=0; y<x ;y++){ - __addLink(token_set_new_node_names[y], node, verb, typesetter); + __addLink(token_set_new_node_names[y], node, verb); for(var z=x; z<token_set_new_node_names.length ;z++){ - __addLink(token_set_new_node_names[y], token_set_new_node_names[z], verb, typesetter); + __addLink(token_set_new_node_names[y], + token_set_new_node_names[z], verb); } } } @@ -466,81 +404,57 @@ var textAnalyser = function (newtext, finalize) { //STAR CASE if (starGraph) { - __addNode(completeSentence, "chainlink", typesetter); + __addNode(completeSentence, "chainlink"); for (n = 0; n < token_set_new_node_names.length; n++) { - __addLink(token_set_new_node_names[n], completeSentence, "chained", typesetter); + __addLink(token_set_new_node_names[n], completeSentence, "chained"); } } ret.drop_conjugator_links = and_count < linkindex; - ret.applyToGraph = function(graph, backend_commit) { - window.ret = ret; + ret.applyToGraph = function(spec) { + var main_graph = spec.main_graph, + edit_graph = spec.edit_graph, + backend_commit = spec.backend_commit; - ret.link_set_add.forEach(function (link) { - if (ret.drop_conjugator_links && (link.name.replace(/ /g,"") === "and")) { - link.state = "temp"; - } - link.__src = graph.find_node__by_name(link.__src) || __sourceNodeFromName(link.__src); - link.__dst = graph.find_node__by_name(link.__dst) || __sourceNodeFromName(link.__dst); - if (link.__src.id !== undefined) { - link.__src_id = link.__src.id; - } - if (link.__dst.id !== undefined) { - link.__dst_id = link.__dst.id; - } - }); + util.assert(main_graph !== undefined && + edit_graph !== undefined && + backend_commit !== undefined, "missing inputs"); + window.ret = ret; - /* - * generate fitered node set who: - * - are not name-present in graph - * - are not of type 'bubble' - */ - var n_set = ret.node_set_add.filter(function(node) { - return false == graph.hasNodeByNameAndNotState(node.name, "temp") - && node.type !== 'bubble'; - }), - link_set = ret.link_set_add.map( - function (link) { - return [link.__src.name, link.__dst.name]; - }), - comp = graph.compareSubset('temp', n_set, link_set); + ret.link_set_add = ret.link_set_add + .filter(function (link) { + return !finalize || + (ret.drop_conjugator_links && + (link.name.replace(/ /g,"") === "and")); + }) + .map(function (link) { + link.__src = edit_graph.find_node__by_name(link.__src) || + __sourceNodeFromName(link.__src); + link.__dst = edit_graph.find_node__by_name(link.__dst) || + __sourceNodeFromName(link.__dst); + if (link.__src.id !== undefined) { + link.__src_id = link.__src.id; + } + if (link.__dst.id !== undefined) { + link.__dst_id = link.__dst.id; + } + return link; + }); - if (false == finalize && comp.graph_same) { - console.log('close enough'); - if (comp.old_name && comp.new_name) { - up_to_two_renames(graph, comp.old_name, comp.new_name, ret.link_set_add); - } - } else { - console.log('not close enough (or finalize)'); - // REINITIALISE GRAPH (DUMB BUT IT WORKS) - // - // FIXME: have temp nodes on a separete graph - // finalize - take temp graph and create a single topo_diff - // else - update temp graph with this topodiff, first clear it every time. - graph.removeNodes("temp"); - graph.removeLinks("temp"); - } + // REINITIALISE GRAPH (DUMB BUT IT WORKS) + edit_graph.clear(); if (!finalize) { - graph.markRelated(token_set_new_node_names); + main_graph.markRelated(token_set_new_node_names); } else { - graph.removeRelated(); + main_graph.removeRelated(); } if (finalize && backend_commit) { - // broadcast diff: - // - finalize? - // - broadcast_diff requested by caller - // drop bubble node - ret.node_set_add = ret.node_set_add.filter(function(n) { return n.type != 'bubble'; }); - // drop and links - ret.link_set_add = ret.link_set_add.filter(function(l) { return l.name !== 'and'; }); - graph.commit_and_tx_diff__topo(ret); + main_graph.commit_and_tx_diff__topo(ret); } else { - if (!comp.graph_same) { - graph.commit_diff__topo(ret); - } + edit_graph.commit_diff__topo(ret); } }; @@ -551,10 +465,10 @@ var textAnalyser = function (newtext, finalize) { return ret; }; -function init(graph) +function init(main_graph) { // deal with new nodes - graph.diffBus + main_graph.diffBus .filter(function (diff) { return diff.node_set_add && diff.node_set_add.length > 0; }) @@ -562,12 +476,13 @@ function init(graph) .flatMap(Bacon.fromArray) .map(".name") .map(function (name) { return name.toLowerCase(); }) + // FIXME - should be removal of existing name via id, we know the id too. .onValue(auto_suggest__update_name); // deal with renamed links /* // TODO renamed links - broken in server, DBO_attr_diff_commit doesn't return an Attr_Diff - graph.diffBus + main_graph.diffBus .filter(function (diff) { return diff && diff.link_set_rm && diff.link_set_rm.length > 0; }) diff --git a/src/client/textanalysis.ui.js b/src/client/textanalysis.ui.js index b06978c1..6e9709c0 100644 --- a/src/client/textanalysis.ui.js +++ b/src/client/textanalysis.ui.js @@ -65,7 +65,11 @@ function analyzeSentence(sentence, finalize) break; } - ret.applyToGraph(rz_core.graph, rz_config.backend_enabled); + ret.applyToGraph({ + main_graph: rz_core.main_graph, + edit_graph: rz_core.edit_graph, + backend_commit: rz_config.backend_enabled, + }); if (finalize || sentence.length == 0) { typeselection.hide(); @@ -99,13 +103,13 @@ function changeType(arg) { nodetype = (arg === 'up'? textanalysis.selected_type_next() : textanalysis.selected_type_prev()); if (arg === 'up') { - rz_core.graph.editType(id, null, nodetype); + rz_core.edit_graph.editType(id, null, nodetype); typeselection.showChosenType(nodetype); - rz_core.graph.findCoordinates(id); + rz_core.edit_graph.findCoordinates(id); } else { - rz_core.graph.editType(id, null, nodetype); + rz_core.edit_graph.editType(id, null, nodetype); typeselection.showChosenType(nodetype); - rz_core.graph.findCoordinates(id); + rz_core.edit_graph.findCoordinates(id); } } diff --git a/src/client/view/graph_view.js b/src/client/view/graph_view.js new file mode 100644 index 00000000..baa321be --- /dev/null +++ b/src/client/view/graph_view.js @@ -0,0 +1,492 @@ +/* view of a single graph in a graph tree. + * + * Usage is of a two graph system: + * + * <Master Graph> + * + * <Editing Graph> + * + * Nodes are drawn per graph. + * Edges connecting between the two graphs are drawn by the descendant graph. + * + * Updates are done using Bacon based events. + * + * TODO: fill in the event diagram here. + * TODO: do it via tests. + * + * Layout is either d3 force or custom. We use custom for the temporary graph. + * + * This avoids the need to compute similarity between two consecutive temporary graphs, + * i.e. + * #alon is #bore + * and + * #alon is #bored + * + * which resulted in overly complex (read: undefined/buggy) code. + */ + +define(['d3', 'Bacon', 'util', 'view/selection', 'view/helpers', 'model/diff'], +function(d3 , Bacon , util , selection , view_helpers, model_diff) { + +function GraphView(spec) { + var forceEnabled = spec.forceEnabled, + temporary = spec.forceEnabled, // graph wide variable, not per node/link + vis = spec.vis, + graph_name = spec.graph_name, + graph = spec.graph, + zoomProperty = spec.zoomProperty, + node_text_dx = spec.node_text_dx, + node_text_dy = spec.node_text_dy, + + zoomInProgress = false, + force, + drag; + + util.assert(vis !== undefined && graph_name !== undefined && + graph !== undefined && zoomProperty !== undefined && + (temporary !== undefined) && (forceEnabled !== undefined) && + node_text_dx !== undefined && node_text_dy !== undefined, + "missing spec variable"); + + zoomProperty.onValue(function (val) { + zoomInProgress = val; + }); + + graph.diffBus.onValue(function (diff) { + var relayout = !temporary && (false == model_diff.is_attr_diff(diff)); + if (relayout) { + console.log('****** ' + graph_name + ' graph relayout *******'); + } else { + console.log('****** ' + graph_name + ' graph update *******'); + } + console.dir(diff); + update_view(relayout); + }); + + function dragstarted(d) { + d3.event.sourceEvent.stopPropagation(); + d3.select(this).classed("dragging", true); + d.dragstart = {clientX:d3.event.sourceEvent.clientX, clientY:d3.event.sourceEvent.clientY}; + if (forceEnabled) { + force.stop(); + } + } + + function dragged(d) { + d.x = d3.event.x; + d.y = d3.event.y; + tick(); + } + + function dragended(d) { + d3.select(this).classed("dragging", false); + d3.select(this).classed("fixed", true); // TODO: this is broken since we override all the classes. Need to switch to class addition/removal (i.e. use classed for everything) or set class in one location (so here just set a value on the node, not the element) + if (d.dragstart.clientX - d3.event.sourceEvent.clientX != 0 || + d.dragstart.clientY - d3.event.sourceEvent.clientY != 0) { + tick(); + if (forceEnabled) { + force.resume(); + } + } + } + + + function update_view(relayout) { + var node, + link, + link_g, + linktext, + nodetext, + unselected_link_group = document.querySelector('#link-group'), + selected_link_group = document.querySelector('#selected-link-group'); + + relayout = relayout || true; + + link = vis.selectAll("g.link") + .data(graph.links(), function(d) { return d.id; }); + + link_g = link.enter().append('g') + .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping + .attr('class', 'link graph') + + link_g.append("path") + .attr("class", function(d) { + return d.state + ' link graph'; + }) + .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping + .attr("marker-end", "url(#end)"); + + // second path for larger click area + link_g.append("path") + .attr("class", "ghostlink") + .on("click", function(d, i) { + if (zoomInProgress) { + // don't disable zoomInProgress, it will be disabled by the svg_click_handler + // after this events bubbles to the svg element + return; + } + var that = this, + src = this.link.__src, + dst = this.link.__dst; + + view.edge_info.on_delete(function () { + view.edge_info.hide(); + graph.removeLink(that.link); + }); + view.edge_info.show(d); + selection.update([src, dst]); + }); + + link.attr("class", function(d, i){ + var temp_and = (d.name && d.name.replace(/ /g,"")=="and" && temporary) ? "temp_and" : ""; + + return ["graph link", temp_and, selection.selected_class(d)].join(' '); + }); + + link.selectAll('path.link') + .attr('class', function(d) { + return [d.state, selection.selected_class(d), "link graph"].join(' '); + }); + + link.exit().remove(); + + vis.selectAll('.ghostlink') + .data(graph.links()) + .each(function (d) { + this.link = d; + }); + + linktext = vis.selectAll(".linklabel") + .data(graph.links(), function(d) { return d.id; }); + linktext.enter() + .append("text") + .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping + .attr("class", function(d) { + return ["linklabel graph", selection.selected_class(d)].join(' '); + }) + .attr("text-anchor", "middle") + .on("click", function(d, i) { + if (!temporary) { // FIXME: if temporary don't even put a click handler + svgInput.enable(this, d); + } + }); + + linktext + .text(function(d) { + var name = d.name || ""; + if (!(temporary || + d.__src.state === "chosen" || d.__dst.state === "chosen")) { + return ""; + } + if (name.length < 25 || d.__src.state === "chosen" || + d.__dst.state === "chosen" || temporary) { + return name; + } else { + return name.substring(0, 14) + "..."; + } + }); + + linktext.exit().remove(); + + node = vis.selectAll(".node") + .data(graph.nodes(), function(d) { + return d.id; + }); + console.log("update update update update"); + console.dir(node); + + var nodeEnter = node.enter() + .append("g") + .attr('id', function(d){ return d.id; }) // append node id to enable data->visual mapping + .attr('visibility', 'hidden') // made visible on first tick + .call(drag); + + // reorder nodes so selected are last, and so rendered last, and so on top. + // FIXME: with b-ubble removed this is probably broken. actually also before. links are not correctly ordered. + (function () { + var ontop = []; + + node.each(function (d) { + this.node = d; + }) + .attr('class', function(d) { + if (selection.node_selected(d)) { + ontop.push(this); + } + return ['node', selection.selected_class(d)].join(' '); + }); + function reparent(new_parent, element) { + if (element.parentNode == new_parent) { + return; + } + new_parent.appendChild(element); + } + // move link to correct group + // O(|links|*|ontop|) + link.each(function (d) { + if (ontop.some(function (node) { + var d_node = node.node; + return d.__src == d_node || d.__dst == d_node; + })) + { + reparent(selected_link_group, this); + } else { + reparent(unselected_link_group, this); + } + }); + linktext.each(function (d) { + if (selection.node_selected(d)) { + ontop.push(this); + } + }); + function moveToEnd(e) { + e.parentNode.appendChild(e); + } + ontop.reverse().forEach(function (e) { + moveToEnd(e); + }); + var count_links = function() { + return selected_link_group.childElementCount + unselected_link_group.childElementCount; + }; + })(); + + nodetext = nodeEnter.insert("text") + .attr("class", "nodetext graph") + .attr("dx", node_text_dx) + .attr("dy", node_text_dy) + .on("click", function(d, i) { + if (d3.event.defaultPrevented) { + // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on + return; + } + if (!temporary) { + svgInput.enable(this, d); + selection.update([d]); + showNodeInfo(this.parentNode.node, i); + } + d3.event.stopPropagation(); + }); + + node.select('g.node text') + .text(function(d) { + if (!d.name) { + return "_"; + } + if (temporary || d.state === 'chosen' + || d.state === "enter" || d.state === "exit") { + return d.name; + } else { + if (d.name.length < 28) { + return d.name; + } else { + return d.name.substring(0, 25) + "..."; + } + } + }); + + circle = nodeEnter.insert("circle"); + node.select('g.node circle') + .attr("class", function(d) { + return d.type + " " + d.state + " circle graph"; + }) + .attr("r", function(d) { + return view_helpers.customSize(d.type) - 2; + }) + .on("click", function(d, i) { + if (d3.event.defaultPrevented) { + // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on + return; + } + d3.event.stopPropagation(); + selection.update([d]); + if(!temporary) { + showNodeInfo(d, i); + } + }); + circle.append("svg:image") + .attr("class", "status graph") + .attr('x', -7) + .attr('y', -8) + .attr('width', 15) + .attr('height', 15) + .attr("xlink:href", function(d) { + switch (d.status) { + case "done": + return "res/img/check.png"; + break; + case "current": + return "res/img/wait.png"; + break; + case "waiting": + return "res/img/cross.png"; + break; + } + }); + + node.exit().remove(); + + //update deliverables + deliverables = []; + var nodes = graph.nodes(); + for (var i = 0; i < nodes.length; i++) { + var current = nodes[i]; + if (current.type === "third-internship-proposal") { + deliverables.push({ + "id": nodes[i].id, + "startdate": nodes[i].start, + "enddate": nodes[i].end + }); + } + //Do something + } + + if (forceEnabled) { + force.nodes(graph.nodes()) + .links(graph.links()) + + if (relayout) { + force.alpha(0.1).start(); + } else { + // XXX If we are stopped we need to update the text of the links at least, + // and this is the simplest way + tick(); + } + } + } + + var debug_print = function(message) { + var element = $(".debug"); + if (element.length == 1) { + element.html(message); + } else { + console.log(message); + } + } + + function check_for_nan(x) { + if (Number.isNaN(x)) { + console.log('nan problem'); + if (forceEnabled) { + force.stop(); + } + } + return Number.isNaN(x); + } + + var newnodes=1; + + function temporary_set_positions() { + //circles animation + var tempcounter = 0, + temptotal = graph.nodes().filter(function(d){ + return temporary && d.type !== "chainlink"; + }).length; + + if (temptotal !== newnodes) { + newnodes += temptotal / 15 / (newnodes * newnodes); + } + newnodes = Math.max(1, Math.min(newnodes, temptotal)); + graph.nodes().forEach(function(d, i) { + var r, a; + tempcounter++; + if (d.type==="chainlink") { + d.x = window.innerWidth / 2; + d.y = window.innerHeight / 2; + } else { + r = 60 + newnodes * 20; + a = -Math.PI + Math.PI * 2 * (tempcounter-1) / newnodes + 0.3; + d.x = window.innerWidth / 2 + r * Math.cos(a); + d.y = window.innerHeight / 2 + r * Math.sin(a); + } + check_for_nan(d.x); + check_for_nan(d.y); + }); + } + + function tick(e) { + //console.log(e); + //$(".debug").html(force.alpha()); + var node = vis.selectAll(".node") + .data(graph.nodes(), function(d) { + return d.id; + }); + var link = vis.selectAll("path.link") + .data(graph.links(), function(d) { + return d.id; + }); + var linktext = vis.selectAll(".linklabel").data(graph.links()); + + function transform(d) { + if (check_for_nan(d.x) || check_for_nan(d.y)) { + return; + } + return "translate(" + d.x + "," + d.y + ")"; + } + + if (temporary) { + temporary_set_positions(); + } + + link.attr("d", function(d, i) { + var d_val, + ghost; + + var dx = d.__dst.x - d.__src.x, + dy = d.__dst.y - d.__src.y, + dr = Math.sqrt(dx * dx + dy * dy); + d_val = "M" + d.__src.x + "," + d.__src.y + "L" + d.__dst.x + "," + d.__dst.y; + // update ghostlink position + ghost = $(this.nextElementSibling); + ghost.attr("d", d_val); + return d_val; + }); + + linktext.attr("transform", function(d) { + return "translate(" + (d.__src.x + d.__dst.x) / 2 + "," + (d.__src.y + d.__dst.y) / 2 + ")"; + }); + + node.attr("transform", transform); + + // After initial placement we can make the nodes visible. + //links.attr('visibility', 'visible'); + node.attr('visibility', 'visible'); + } + + // SVG rendering order is last rendered on top, so to make sure + // all links are below the nodes we group them under a single g + vis.append("g").attr("id", "link-group"); + vis.append("g").attr("id", "selected-link-group"); + + drag = d3.behavior.drag() + .origin(function(d) { return d; }) + .on("dragstart", dragstarted) + .on("drag", dragged) + .on("dragend", dragended); + + function init_force_layout(){ + var el = document.body; + var w = $(el).innerWidth(), + h = $(el).innerHeight(); + + force = d3.layout.force() + .distance(120) + .gravity(0.12) + .charge(-1800) + .size([w, h]) + .on("tick", tick) + .start(); + } + + if (forceEnabled) { + init_force_layout(); + } + return { + update_view: update_view, + }; +} + +return { + GraphView: GraphView, +}; + +}); diff --git a/src/client/view/helpers.js b/src/client/view/helpers.js index ad11763a..f01f812f 100644 --- a/src/client/view/helpers.js +++ b/src/client/view/helpers.js @@ -31,9 +31,6 @@ function customColor(type) { case "chainlink": color = "#363636"; //dark-grey break; - case "bubble": - color = "rgba(255,255,255,0.2)"; // white, 0.2 opaque - break; default: console.log('bug: unknown type ' + type); color = '#d4d4d9'; //mid-light grey @@ -69,9 +66,6 @@ function customSize(type) { case "chainlink": size = 8; break; - case "bubble": - size = 180; - break; default: size = 9; break; diff --git a/src/client/view/selection.js b/src/client/view/selection.js index 9a87494a..a45f4b2b 100644 --- a/src/client/view/selection.js +++ b/src/client/view/selection.js @@ -6,7 +6,7 @@ function get_rz_core() // circular dependency on rz_core, so require.js cannot solve it. if (rz_core === undefined) { rz_core = require('rz_core'); - listen_on_diff_bus(rz_core.graph.diffBus); + listen_on_diff_bus(rz_core.main_graph.diffBus); } return rz_core; } @@ -59,14 +59,14 @@ function updateSelectedNodesBus(new_selected_nodes) } function byVisitors(node_selector, link_selector) { - var new_selected_nodes = get_rz_core().graph.findByVisitors(node_selector, link_selector); + var new_selected_nodes = get_rz_core().main_graph.findByVisitors(node_selector, link_selector); clear(); connectedComponent(new_selected_nodes); } function connectedComponent(nodes) { - var connected = get_rz_core().graph.getConnectedNodesAndLinks(nodes, 1), + var connected = get_rz_core().main_graph.getConnectedNodesAndLinks(nodes, 1), i, node, link, @@ -112,7 +112,7 @@ var selected_class = function(node) { var clear = function() { selected_nodes.length = 0; - get_rz_core().graph.setRegularState(); + get_rz_core().main_graph.setRegularState(); } function arr_compare(a1, a2) |
