diff options
| author | LV-426 <lv-426@taproot.org.il> | 2014-11-13 17:41:57 +0200 |
|---|---|---|
| committer | LV-426 <lv-426@taproot.org.il> | 2014-11-13 17:41:57 +0200 |
| commit | 76d1de512a58f023ae348ae8500100490fba9915 (patch) | |
| tree | 87a3df058d3aaf1a3f2c408a784ffe63209cdaac /src | |
| parent | 4a303d5a7a7e1f8327bb18a2ef3e3093a30a50b6 (diff) | |
rename scripts/ -> src/
Diffstat (limited to 'src')
| -rw-r--r-- | src/ScrollTo.js | 17 | ||||
| -rw-r--r-- | src/app.js | 30 | ||||
| -rw-r--r-- | src/buttons.js | 84 | ||||
| -rw-r--r-- | src/consts.js | 10 | ||||
| -rw-r--r-- | src/drag_n_drop.js | 31 | ||||
| -rw-r--r-- | src/history.js | 135 | ||||
| -rw-r--r-- | src/main.js | 24 | ||||
| -rw-r--r-- | src/model/core.js | 80 | ||||
| -rw-r--r-- | src/model/diff.js | 144 | ||||
| -rw-r--r-- | src/model/graph.js | 649 | ||||
| -rw-r--r-- | src/model/util.js | 117 | ||||
| -rw-r--r-- | src/robot.js | 70 | ||||
| -rw-r--r-- | src/rz_api_backend.js | 220 | ||||
| -rw-r--r-- | src/rz_api_mesh.js | 22 | ||||
| -rw-r--r-- | src/rz_core.js | 744 | ||||
| -rw-r--r-- | src/rz_observer.js | 118 | ||||
| -rw-r--r-- | src/signal.js | 22 | ||||
| -rw-r--r-- | src/textanalysis.js | 459 | ||||
| -rw-r--r-- | src/textanalysis.ui.js | 164 | ||||
| -rw-r--r-- | src/util.js | 52 | ||||
| -rw-r--r-- | src/view/edge_info.js | 36 | ||||
| -rw-r--r-- | src/view/helpers.js | 78 | ||||
| -rw-r--r-- | src/view/internal.js | 14 | ||||
| -rw-r--r-- | src/view/node_info.js | 63 | ||||
| -rw-r--r-- | src/view/tab.js | 57 | ||||
| -rw-r--r-- | src/view/timeline.js | 183 | ||||
| -rw-r--r-- | src/view/view.js | 12 |
27 files changed, 3635 insertions, 0 deletions
diff --git a/src/ScrollTo.js b/src/ScrollTo.js new file mode 100644 index 00000000..a015f08c --- /dev/null +++ b/src/ScrollTo.js @@ -0,0 +1,17 @@ +$.fn.scrollTo = function( target, options, callback ){ + if(typeof options == 'function' && arguments.length == 2){ callback = options; options = target; } + var settings = $.extend({ + scrollTarget : target, + offsetTop : 50, + duration : 500, + easing : 'swing' + }, options); + return this.each(function(){ + var scrollPane = $(this); + var scrollTarget = (typeof settings.scrollTarget == "number") ? settings.scrollTarget : $(settings.scrollTarget); + var scrollY = (typeof scrollTarget == "number") ? scrollTarget : scrollTarget.offset().top + scrollPane.scrollTop() - parseInt(settings.offsetTop); + scrollPane.animate({scrollTop : scrollY }, parseInt(settings.duration), settings.easing, function(){ + if (typeof callback == 'function') { callback.call(this); } + }); + }); +}
\ No newline at end of file diff --git a/src/app.js b/src/app.js new file mode 100644 index 00000000..db2b87ab --- /dev/null +++ b/src/app.js @@ -0,0 +1,30 @@ +(function() { + var lib_path = '../lib/'; + var config = { + paths: { + jquery: lib_path + 'jquery', + 'jquery-ui': lib_path + 'jquery-ui', + 'd3': lib_path + 'd3/d3', + FileSaver: lib_path + 'FileSaver', + caret: lib_path + 'caret', + autocomplete: lib_path + 'autocomplete', + } + } + + config.urlArgs = (typeof local_config != 'undefined') && local_config.urlArgs; + +if (window.is_node) { + // Testing path only + console.log('app: running under node'); + config.baseUrl = '../src/'; + window.rhizi_require_config = config; +} else { + // Main app path + require.config(config); + + requirejs(['main'], function(main) { + console.log('starting rhizi logic'); + main.main(); + }); +} +}()); diff --git a/src/buttons.js b/src/buttons.js new file mode 100644 index 00000000..500d56fc --- /dev/null +++ b/src/buttons.js @@ -0,0 +1,84 @@ +"use strict" + +define(['jquery', 'FileSaver', 'rz_core'], function ($, saveAs, rz_core) { +$('.tutorial').click(function(){}); + +var key="#47989379"; + + +$('.save a').click(function(){ + var json = rz_core.graph.save_to_json(); + console.log('saving to local storage ' + json.length + ' bytes'); + localStorage.setItem(key, json); +}); + +$('.export a').click(function() { + var json = rz_core.graph.save_to_json(); + var filename = 'graph.json'; + var blob = new Blob([json], {type: 'application/json'}); + console.log('saving ' + json.length + ' bytes to ' + filename); + saveAs(blob, filename); +}); + +var really_load = function() { + if (!rz_core.graph.empty()) { + return confirm('All unsaved changes will be deleted, are you sure?'); + } + return true; +} + +$('.file-import').on('change', function(event) { + var file = event.target.files[0]; + var reader; + + if (!really_load()) { + return; + } + if (file === undefined) { + return; + } + console.log(file); + reader = new FileReader(); + reader.onload = (function(theFile) { + return function(e) { + var result = e.target.result; + if (e.target.readyState === FileReader.DONE) { + console.log('done reading ' + theFile.name); + console.log('got #' + result.length + ' bytes in ' + typeof(result)); + rz_core.load_from_json(result); + } + } + })(file); + reader.readAsText(file, "text/javascript"); +}); + +$('.local-storage-load a').click(function(){ + if (!really_load()) { + return; + } + var json_blob = localStorage.getItem(key) + rz_core.load_from_json(json_blob); +}); + +$('a.set-user').click(function() { + $('.set-user').hide(); + $('.set-user-form').show(); + $('.set-user-form').submit(function() { + var user = $('.set-user-input').val(); + rz_core.graph.set_user(user); + $('.set-user').html('user: ' + user); + $('.set-user').show(); + $('.set-user-form').hide(); + $('.save-history').show(); + return false; + }) +}); + +$('a.save-history').click(function() { + if (rz_core.graph.history === undefined) { + throw "History is undefined"; + } + rz_core.graph.history.save_to_file(); +}); +return {'buttons': 'nothing here'}; +}); // define diff --git a/src/consts.js b/src/consts.js new file mode 100644 index 00000000..0c2af53b --- /dev/null +++ b/src/consts.js @@ -0,0 +1,10 @@ +define(function() { + // TODO: enums, sometime + return { + APPLIED_GRAPH_DIFF: 'graph_diff', + KEYSTROKES: 'keystrokes', + KEYSTROKE_WHERE_EDIT_NODE: 'keystroke_where_edit_node', + KEYSTROKE_WHERE_DOCUMENT: 'keystroke_where_document', + KEYSTROKE_WHERE_TEXTANALYSIS: 'keystroke_where_textanalysis', + }; +}); diff --git a/src/drag_n_drop.js b/src/drag_n_drop.js new file mode 100644 index 00000000..da166ad4 --- /dev/null +++ b/src/drag_n_drop.js @@ -0,0 +1,31 @@ +define(['jquery', 'rz_core'], function($, rz_core) { + +function init() { +console.log('rhizi: init drag-n-drop'); +$(document).on('drop', function(e) { + e.stopPropagation(); + e.preventDefault(); + var files = e.originalEvent.dataTransfer.files; + var file = files[files.length - 1]; + var fr = new FileReader(); + fr.onload = function() { + if (fr.readyState != 2) { + console.log('drop: error: reading from file failed'); + } else { + console.log('loading dropped file'); + rz_core.load_from_json(fr.result); + } + } + fr.readAsText(file); + return false; +}); +$(document).on('dragover', function (e) +{ + e.stopPropagation(); + e.preventDefault(); + return false; +}); +}; +return {'init': init }; + +}); // define diff --git a/src/history.js b/src/history.js new file mode 100644 index 00000000..b46dd260 --- /dev/null +++ b/src/history.js @@ -0,0 +1,135 @@ +// Once upon a time we shall have a versioned property graph from which history +// will be one extractable aspect, much like a git for graphs. Now we just have +// a plain list of events for a specific user. + +// Enums and chrome don't play along well. Object.freeze I guess? actually rhizi code cuases +// exceptions but that shouldn't break the console, as evidenced by the '__commandLineAPI is not defined' +// error below. +// +// Uncaught TypeError: Can't add property addednodes, object is not extensible rz_core.js:4 +// Uncaught TypeError: Can't add property text, object is not extensible textanalysis.js:3 +// Uncaught TypeError: Can't add property key, object is not extensible buttons.js:5 +// Uncaught ReferenceError: sentence is not defined robot.js:11 +// Resource interpreted as Font but transferred with MIME type application/font-sfnt: "http://localhost:8000/external/Lato300.ttf". jquery.js:2 +// > document +// ReferenceError: __commandLineAPI is not defined +//var ActionEnum = Enum(); + +define(['jquery', 'FileSaver', 'consts', 'signal'], + function($, saveAs, consts, signal) { + +/* user - username (string) + * svg - svg element for catching zoom events (jquery DOMNode wrapper) + */ +function History(user, transform_element) { + var that = this; + this.records = []; + this.user = user; + this.transform_element = transform_element; + signal.slot(consts.APPLIED_GRAPH_DIFF, function(obj) { + return that.record_graph_diff(obj) + }); + signal.slot(consts.KEYSTROKES, function(obj) { + return that.record_keystrokes(obj); + }); + // XXX create zoom behavior - then proof to event name change + $(window).on('wheel.history', function(obj) { + that.record_zoom(obj); + return true; + }); +} + +var ACTION_KEYSTROKES = 'ACTION_KEYSTROKES'; +var ACTION_GRAPH_DIFF = 'ACTION_GRAPH_DIFF'; +var ACTION_ZOOM = 'ACTION_ZOOM'; + +var KEYSTROKE_WHERE_TEXTANALYSIS = 'KEYSTROKE_WHERE_TEXTANALYSIS'; +var KEYSTROKE_WHERE_DOCUMENT = 'KEYSTROKE_WHERE_DOCUMENT'; +var KEYSTROKE_WHERE_EDIT_NODE = 'KEYSTROKE_WHERE_EDIT_NODE'; + +History.prototype.record = function(action, d) +{ + if (d === undefined || action === undefined) { + throw "Invalid arguments"; + } + d['action'] = action; + d['user'] = this.user; + d['timestamp'] = new Date(); + this.records.push(d); + $('.history-timeline').html('<pre>' + JSON.stringify(d) + '</pre>'); +}; + +function svg_extract_translate_and_scale(e) +{ + // See: http://stackoverflow.com/questions/10349811/how-to-manipulate-translate-transforms-on-a-svg-element-with-javascript-in-chrom + // Using the regexp option right now, did only firefox testing 36 + var str = e.attributes['transform'].value; + var parts = /translate\(\s*([^\s,)]+)[ ,]([^\s,)]+)/.exec(str); + var scale = /scale\(\s*([^\s)]+)\)/.exec(str); + if (scale) { + var x = parts[1], y = parts[2]; + return {scale:+scale[1], translate: [+x, +y]}; + } else { + return {scale:1.0, translate: [0.0, 0.0]}; + } +} + +History.prototype.record_zoom = function(d) +{ + var transform = svg_extract_translate_and_scale(this.transform_element); + + if (transform === undefined) { + console.log('record_zoom: bug: transform_element has no transform attribute'); + return; + } + this.record(ACTION_ZOOM, {transform: transform}); +} + +History.prototype.save_to_file = function() +{ + var json = JSON.stringify(this.records, function (k, v) { return v; }, 2); + + saveAs(new Blob([json], {type: 'application/json'}), 'history.json'); +}; + +History.prototype.clear_history = function() +{ + this.records = []; +} + +History.prototype.record_graph_diff = function(obj) +{ + this.record(ACTION_GRAPH_DIFF, { + nodes: {add: obj.nodes && obj.nodes.add, remove: obj.nodes && obj.nodes.remove, + change: obj.nodes && obj.nodes.change}, + links: {add: obj.links && obj.links.add, remove: obj.links && obj.links.remove, + change: obj.links && obj.links.change}, + }); +} + +History.prototype.record_keystrokes = function(obj) +{ + var where = obj.where, + keys = obj.keys; + + if (where === undefined || keys === undefined || keys.length === undefined || + keys.length <= 0) { + throw "Invalid arguments"; + } + keys = keys.filter(function(k) { return k !== undefined; }); + if (keys.length == 0) { + return; + } + this.record(ACTION_KEYSTROKES, { + 'keys': keys, + 'where': where + }); +} + +return { + History:History, + KEYSTROKE_WHERE_TEXTANALYSIS:KEYSTROKE_WHERE_TEXTANALYSIS, + KEYSTROKE_WHERE_DOCUMENT:KEYSTROKE_WHERE_DOCUMENT, + KEYSTROKE_WHERE_EDIT_NODE:KEYSTROKE_WHERE_EDIT_NODE +}; +}); // define diff --git a/src/main.js b/src/main.js new file mode 100644 index 00000000..67213056 --- /dev/null +++ b/src/main.js @@ -0,0 +1,24 @@ +define(['textanalysis.ui', 'buttons', 'history', 'drag_n_drop', 'robot'], +function(textanalysis_ui, buttons, history, drag_n_drop, robot) { + + function expand(obj){ + if (!obj.savesize) { + obj.savesize = obj.size; + } + obj.size = Math.max(obj.savesize, obj.value.length); + } + + this.main = function() { + console.log('Rhizi main started'); + drag_n_drop.init(); + $('#editname').onkeyup = function() { expand(this); }; + $('#editlinkname').onkeyup = function() { expand(this); }; + $('#textanalyser').onkeyup = function() { expand(this); }; + + textanalysis_ui.main(); + } + + return { + main: main }; + } +); diff --git a/src/model/core.js b/src/model/core.js new file mode 100644 index 00000000..43a87b5d --- /dev/null +++ b/src/model/core.js @@ -0,0 +1,80 @@ +"use strict" + +/** + * core model module - currently unused + */ +define([], function() { + + /** + * return a random id + */ + var random_id = function() { + return Math.random().toString(36).substring(2); + } + + function random_node_name() { + return Math.random().toString(36).substring(2, 10); + } + + function Node() { + } + + function create_node__set_random_id(node_spec) { + if (undefined == node_spec) { + node_spec = {}; + } + + var ret = new Node(); + ret.id = random_id(); + + ret.name = node_spec.name; + ret.type = node_spec.type; + ret.state = node_spec.state; + ret.status = node_spec.status; + ret.url = node_spec.url; + + ret.start = node_spec.start; + ret.end = node_spec.end; + + return ret; + } + + function create_link__set_random_id(link_spec) { + var ret = new Link(); + ret.id = random_id(); + } + + /** + * determine if nodes are equal by ID + * + * @param other_node + * @returns {Boolean} + */ + Node.prototype.equlas_by_id = function(other) { + ret = this.id.toLowerCase() == other.id.toLowerCase(); + if (false == ret) { + console.debug(this.id + ' != ' + other.id); + } + return ret; + } + + function Link() { + + } + + /** + * determine if links are equal by ID + * + * @param other_node + * @returns {Boolean} + */ + Link.prototype.equlas_by_id = function(other) { + return this.id.toLowerCase() == other.id.toLowerCase(); + } + + return { + random_node_name : random_node_name, + create_node__set_random_id : create_node__set_random_id, + create_link__set_random_id : create_link__set_random_id, + }; +}); diff --git a/src/model/diff.js b/src/model/diff.js new file mode 100644 index 00000000..1bcfc5a2 --- /dev/null +++ b/src/model/diff.js @@ -0,0 +1,144 @@ +"use strict" + +/** + * Diff module + */ +define([], + function() { + + /** + * A set of diff objects + */ + function Diff_Set(obj_spec) { + this.__diff_set_topo = []; + this.__diff_set_attr = []; + this.__diff_set_vis = []; + } + Diff_Set.prototype.add_diff_obj = function(diff_obj) { + if (diff_obj instanceof Topo_Diff) { + this.__diff_set_topo.push(diff_obj); + } + if (diff_obj instanceof Attr_Diff) { + this.__diff_set_attr.push(diff_obj); + } + if (diff_obj instanceof Vis_Diff) { + this.__diff_set_vis.push(diff_obj); + } + } + + /** + * Topological diff object + */ + function Topo_Diff(obj_spec) { + + this.link_set_rm = obj_spec.link_set_rm; + this.node_set_rm = obj_spec.node_set_rm; + this.node_set_add = obj_spec.node_set_add; + this.link_set_add = obj_spec.link_set_add; + } + + /** + * Attribute diff object, organized by type, where currently + * node,link types are supported + */ + function Attr_Diff(obj_spec) { + this.__type_node = {} + this.__type_link = {} + } + + Attr_Diff.prototype.init_attr_diff = function(type, id) { + + if ('node' != type && 'link' != type) { + console + .error('attempt to init attribute diff for unsupported type: ' + + type); + return; + } + + var type_field = '__type_' + type; + this[type_field][id] = { + '__attr_write' : {}, + '__attr_remove' : [] + }; + + return this; + } + + Attr_Diff.prototype.init_attr_diff_node = function(id) { + return this.init_attr_diff('node', id); + } + + Attr_Diff.prototype.init_attr_diff_link = function(id) { + return this.init_attr_diff('link', id); + } + + Attr_Diff.prototype.add_attr_write_node = function(n_id, attr_name, + attr_val) { + + if (undefined == this.__type_node[n_id]) { + this.init_attr_diff_node(n_id); + } + this.__type_node[n_id].__attr_write[attr_name] = attr_val; + return this; + } + + Attr_Diff.prototype.add_attr_rm_node = function(n_id, attr_name) { + if (undefined == this[n_id]) { + this.init_attr_diff(n_id); + } + this.__type_node[n_id].__attr_remove.push(attr_name); + return this; + } + + /** + * Visual diff object expressing any visual change to the state of a + * particular visualization type + */ + function Vis_Diff(obj_spec) { + } + + function new_topo_diff(obj_spec) { + /* + * validate obj_spec + */ + // TODO + var ret = new Topo_Diff(obj_spec); + return ret; + } + + function new_attr_diff(obj_spec) { + /* + * validate obj_spec + */ + // TODO + var ret = new Attr_Diff(obj_spec); + ret.__type_node = {}; // id-to-obj map + ret.__type_link = {}; // id-to-obj map + return ret; + } + + function new_vis_diff(obj_spec) { + /* + * validate obj_spec + */ + // TODO + var ret = new Vis_Diff(obj_spec); + return ret; + } + + function new_diff_set(obj_spec) { + /* + * validate obj_spec + */ + // TODO + var ret = new Diff_Set(obj_spec); + return ret; + } + + return { + new_topo_diff : new_topo_diff, + new_attr_diff : new_attr_diff, + new_vis_diff : new_vis_diff, + new_diff_set : new_diff_set, + } + });
\ No newline at end of file diff --git a/src/model/graph.js b/src/model/graph.js new file mode 100644 index 00000000..820daef0 --- /dev/null +++ b/src/model/graph.js @@ -0,0 +1,649 @@ +"use strict" + +define(['signal', 'consts', 'util', 'textanalysis', 'model/core', 'model/util', 'rz_api_backend', 'rz_api_mesh'], +function (signal, consts, util, textanalysis, model_core, model_util, rz_api_backend, rz_api_mesh) { + +function Graph(el) { + + var nodes = [], + links = []; + + function id_generator_generator() { + var id = 0; + function get_next() { + var next = id; + id += 1; + return next; + } + return get_next; + } + var id_generator = id_generator_generator(); + + ///FUNCTIONS + this.addNode = function(name, type, state) { + if (type === undefined) { + console.log('bug: adding undefined type'); + } + var new_node = this._addNodeNoHistory( + {name:name, + type:type, + state:state, + start:0, + end:0, + status:"unknown"}); + if (new_node) { + signal.signal(consts.APPLIED_GRAPH_DIFF, [{ + nodes: {add: [new_node]}}]); + return new_node; + } + } + + this._addNodeNoHistory = function(spec) { + // No history recorded - this is a helper for loading from files / constant graphs + var node; + if (spec.id === undefined) { + node = findNodeByName(spec.name, null); + } else { + if (spec.id !== undefined) { + node = findNode(spec.id, null); + } + } + if (node === undefined) { + node = { + "id": spec.id || id_generator(), + "name": spec.name, + "type": spec.type, + "state": spec.state, + "start": spec.start, + "end": spec.end, + "status": spec.status, + 'url': spec.url, + 'x': spec.x, + 'y': spec.y, + }; + nodes.push(node); + } + return node; + } + + this.removeNode = function(id, state) { + var i = 0; + var n = findNode(id, state); + while (i < links.length) { + if ((links[i]['source'] === n) || (links[i]['target'] == n)) links.splice(i, 1); + else i++; + } + var index = findNodeIndex(id, state); + if (index !== undefined) { + nodes.splice(index, 1); + } + signal.signal(consts.APPLIED_GRAPH_DIFF, [{nodes: {removed: [id]}}]); + } + + this.removeNodes = function(state) { + var id = null; + var ns = findNodes(null, state); + for (var j = 0; j < ns.length; j++) { + var n = ns[j]; + var i = 0; + while (i < links.length) { + if ((links[i]['source'] === n) || (links[i]['target'] == n)) links.splice(i, 1); + else i++; + } + var index = findNodeIndex(id, state); + if (index !== undefined) { + nodes.splice(index, 1); + } + } + if (ns.length > 0) { + signal.signal(consts.APPLIED_GRAPH_DIFF, [{nodes: {removed: ns.map(function(n) { return n.id; })}}]); + } + } + + /** + * + * getConnectedNodesAndLinks + * + * @id + * @state - defines the starting node (must have id and state) + * @d - depth defining connected component. If -1 returns the entire connected component. (can be the whole 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. + * + * @return - { + * 'node': [node] + * 'link': [link] + * } + * + * TODO: rewrite using efficient data structure. Right now iterates over everything + * TODO: implement for d !== 1 + * + */ + this.getConnectedNodesAndLinks = function(n, d) { + var i = 0, + j = 0, + adjacentnode, + link, + link2, + ret = {'nodes':[], 'links':[]}; + + $(".debug").html(n.state); + + if (n === undefined) { + console.log('getConnectedNodesAndLinks: bug: called with undefined node'); + return; + } + if (d !== 1) { + console.log('getConnectedNodesAndLinks: bug: not implemented for d == ' + d); + } + d = d || 1; + + while (i < links.length) { + link = links[i]; + // XXX: using name comparison because n might be stale + if (compareNames(link.source.name, n.name)) { + adjacentnode = findNode(link.target.id, null); + if (adjacentnode.state !== "temp") { + ret.nodes.push({type: 'exit', node: adjacentnode}); + } + ret.links.push({type: 'exit', link: link}); + + if (link.target.type === "chainlink") { + while (j < links.length) { + link2 = links[j]; + if (link.target.id === link2.target.id && + link2.target.type === "chainlink" && + link2.target.state !== "temp") { + adjacentnode = findNode(link2.source.id, null); + if (adjacentnode.state !== "temp") { + ret.nodes.push({type: 'enter', node: adjacentnode}); + } + ret.links.push({type: 'enter', link: link2}); + } + j++; + } + } + j=0; + } + if (compareNames(links[i].target.name, n.name)) { + adjacentnode = findNode(links[i].source.id, null); + if (adjacentnode.state !== "temp") adjacentnode.state = "enter"; + links[i].state = "enter"; + } + i++; + } + return ret; + } + + /* compareSubset: + * state: one of the optional states that defines a subgraph + * new_nodes: array of objects with name + * new_links: array of length two arrays [source_name, target_name] + * returns: true if current and new graph are homomorphic up to + * 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_links = findLinks(state).map(function(link) { + return [link.source.name, link.target.name]; + }).sort(); + var k; + var state_source, state_target, new_source, new_target; + var changed_nodes; + var verbose = false; // XXX should be global. + var set_old_name, set_new_name; + + new_nodes.map(function (f) { + if (!f.name) { + console.log('missing name on node. node follows'); + console.log(f); + } + }); + new_nodes.sort(); + new_links.sort(); + if (new_nodes.length != state_nodes.length || new_links.length != state_links.length) { + if (verbose) { + console.log('not same size: new/old ' + new_nodes.length + ' / ' + state_nodes.length + '; ' + + new_links.length + ' / ' + state_links.length); + } + return {graph_same: false}; + } + changed_nodes = util.set_diff(util.set_from_array(state_nodes.map(function(d) { return d.name; })), + util.set_from_array(new_nodes.map(function (f) { return f.name; }))); + // we allow any number of changed nodes as long as we it is 1 or 2 :) + if (changed_nodes.a_b.length > 2) { + if (verbose) { + console.log('changed too many nodes'); + console.log(changed_nodes); + } + return {graph_same: false}; + } + set_old_name = util.set_from_array(changed_nodes.a_b); + set_new_name = util.set_from_array(changed_nodes.b_a); + for (k = 0 ; k < state_links.length ; ++k) { + state_source = state_links[k][0]; + state_target = state_links[k][1]; + new_source = new_links[k][0]; + new_target = new_links[k][1]; + if ((state_source !== new_source && + !(state_source in set_old_name && new_source in set_new_name)) + || + (state_target !== new_target && + !(state_target in set_old_name && new_target in set_new_name))) { + if (verbose) { + console.log('not same link: ' + + state_source + '->' + state_target + ' != ' + + new_source + '->' + new_target); + console.log('state_source === new_source: ' + String(state_source === new_source)); + console.log('state_target === new_target: ' + String(state_target === new_target)); + console.log(set_old_name); + console.log(set_new_name); + } + return {graph_same: false}; + } + } + return {graph_same: true, old_name: changed_nodes.a_b, new_name: changed_nodes.b_a}; + } + + this.addLinkByName = function(sourceName, targetName, name, state, drop_conjugator_links) { + var source = findNodeByName(sourceName, null), + target = findNodeByName(targetName, null), + sourceId = source ? source.id : null, + targetId = target ? target.id : null; + + if (sourceId === null || targetId === null) { + console.log('error: link of missing nodes: ' + sourceName + ' (' + sourceId + ') -> ' + + targetName + ' (' + targetId + ')'); + return; + } + this.addLink(sourceId, targetId, name, state, drop_conjugator_links); + } + + this.addLink = function(sourceId, targetId, name, state, drop_conjugator_links) { + var sourceNode = findNode(sourceId, null); + var targetNode = findNode(targetId, null); + var found = findLink(sourceId,targetId,name); + + if (drop_conjugator_links && name && (name.replace(/ /g,"") === "and")) { + state = "temp"; + } + if (sourceNode === undefined || targetNode === undefined) { + return; + } + if (!found) { + var id = id_generator(), + link = { + source: sourceNode, + target: targetNode, + name: name, + state: state, + id: id, + }; + links.push(link); + signal.signal(consts.APPLIED_GRAPH_DIFF, [{links: {add: [link]}}]); + } else { + found.name = name; + found.state = state; + } + } + + this.editLink = function(sourceId, targetId, newname, newstate) { + var link = findLink(sourceId, targetId, newname); + if (link !== undefined) { + link.name = newname; + if (newstate !== undefined) { + link.state = newstate; + } + } + } + + this.editLinkTarget = function(sourceId, targetId, newTarget) { + var link = findLink(sourceId, targetId, null); + if (link !== undefined) { + link.target = findNode(newTarget, null); + + } else { + + } + } + + this.editNameByName = function(old_name, new_name) { + var node = findNodeByName(old_name, null); + + if (node === undefined) { + console.log('editNameByName: error: cannot find node with name ' + old_name); + return; + } + return this.editName(node.id, new_name); // TODO: introduce Node class (yes Amir, I'm now down with that). + } + + this.editName = function(id, new_name) { + var index2 = findNodeByName(new_name, null); + var index = findNode(id, null); + var acceptReplace=true; + + if ((index !== undefined)) { + if (index2 !== undefined) { + acceptReplace = confirm('"' + index2.name + '" will replace "' + index.name + '", are you sure?'); + if (acceptReplace){ + for (var i = 0; i < links.length; i++) { + if (links[i].source === index) { + links[i].source = index2; + } + if (links[i].target === index) { + links[i].target = index2; + } + } + this.removeNode(index.id,null); + } + } else { + index.name = new_name; + } + } + } + + this.editDates = function(id, type, start, end) { + var index = findNode(id, type); + if ((index !== undefined)) { + index.start = start; + index.end = end; + } + } + + /** + * editType: + * + * @return true if type changed + */ + this.editType = function(id, state, newtype) { + var index = findNode(id, state); + if ((index !== undefined)) { + index.type = newtype; + } + } + + this.editURL = function(id, state, url) { + var index = findNode(id, state); + if ((index === undefined)) return; + index.url = url; + } + + this.editState = function(id, state, newstate) { + var index = findNode(id, state); + + if ((index !== undefined)) { + index.state = newstate; + } + } + + this.findCoordinates = function(id, type) { + var index = findNode(id, type); + if ((index !== undefined)) { + $('.typeselection').css('top', index.y - 90); + $('.typeselection').css('left', index.x - 230); + } + } + + this.removeLink = function(link) { + var link, + i; + + for ( i = 0 ; i < links.length; ++i ) { + if (link.id !== undefined) { + if (link.id === links[i].id) { + links.splice(i, 1); + return; + } + } else { + if (link.sourceId === links[i].sourceId && link.targetId === links[i].targetId) { + links.splice(i, 1); + return; + } + } + } + console.log('bug: attempt to remove non existant link'); + } + + this.removeLinks = function(state) { + var id = null; + var ls = findLinks(state); + for (var j = 0; j < ls.length; j++) { + var l = ls[j]; + var i = 0; + while (i < links.length) { + if (links[i] === l) links.splice(i, 1); + else i++; + } + } + } + + var findLink = function(sourceId, targetId, name) { + for (var i = 0; i < links.length; i++) { + if (links[i].source.id === sourceId && links[i].target.id === targetId) { + return links[i]; + } + } + } + + var findLinks = function(state) { + var foundLinks = []; + for (var i = 0; i < links.length; i++) { + if (links[i].state == state) { + foundLinks.push(links[i]); + } + } + return foundLinks; + } + + var compareNames = function(name1, name2) { + return name1.toLowerCase() === name2.toLowerCase(); + }; + + var hasNodeByName = function(name, state) { + var i; + + for (i = 0 ; i < nodes.length; ++i) { + if (compareNames(nodes[i].name, name) && nodes[i].state === state) { + return true; + } + } + return false; + } + this.hasNodeByName = hasNodeByName; + + var hasNode = function(id, state) { + var i; + + for (i = 0 ; i < nodes.length; ++i) { + if (nodes[i].id === id && nodes[i].state === state) { + return true; + } + } + return false; + } + this.hasNode = hasNode; + + var findNode = function(id, state) { + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].id === id || nodes[i].state === state) + return nodes[i]; + }; + } + + var findNodeByName = function(name, state) { + for (var i = 0 ; i < nodes.length ; ++i) { + if (compareNames(nodes[i].name, name) || nodes[i].state === state) { + return nodes[i]; + } + } + } + + var findNodes = function(id, state) { + // id=id.toLowerCase(); + var foundNodes = []; + for (var i = 0; i < nodes.length; i++) { + if ((id && nodes[i].id === id) || (state && nodes[i].state === state)) + foundNodes.push(nodes[i]); + } + return foundNodes; + } + + var findNodeIndex = function(id, state) { + for (var i = 0; i < nodes.length; i++) { + if ((id && nodes[i].id === id) || (state && nodes[i].state === state)) + return i; + }; + } + + function clear() { + nodes.length = 0; + links.length = 0; + } + this.clear = clear; + + function empty() { + return nodes.length == 0 && links.length == 0; + } + this.empty = empty; + + // @ajax-trans + this.commit_diff_set = function (diff_set) { + + function on_success(data){ + // TODO impl + } + + rz_api_mesh.broadcast_possible_next_diff_block(diff_set); + } + + /** + * perform initial DB load from backend + */ + // @ajax-trans + function load_from_backend() { + + function on_success(data){ + var n_set = []; + var l_set = []; + var len; + + len = data['node_set'].length; + for (var i = 0; i < len; i++) { + var n_raw = data['node_set'][i]; + var n = model_util.adapt_format_read_node(n_raw); + n_set.push(n); + } + + len = data['link_set'].length; + for (var i = 0; i < len; i++) { + var l_raw = data['link_set'][i]; + var l = model_util.adapt_format_read_link(l_raw); + l_set.push(l); + } + + len = n_set.length + for (var i = 0; i < len; i++) { + var n = n_set[i]; + graph.addNode(n.id, n.type, n.state) + } + + len = l_set.length + for (var i = 0; i < len; i++) { + var l = l_set[i]; + graph.addLink_byIds(l.sourceId, l.targetId, l.name, "perm") + } + } + + rz_api_backend.clone(0, on_success); + } + + this.load_from_json = function(json) { + var data = JSON.parse(json); + var i, node, link; + + clear(); + if (data == null) { + console.log('load callback: no data to load'); + return; + } + for(i = 0; i < data["nodes"].length; i++){ + node = data.nodes[i]; + this._addNodeNoHistory({id:node.id, name:node.name ? node.name : node.id, + type:node.type,state:"perm", + start:new Date(node.start), + end:new Date(node.end), + status:node.status, + url:node.url, + x: node.x, + y: node.y, + }); + textanalysis.autoSuggestAddName(node.name.toLowerCase()); + } + for(i = 0; i < data["links"].length; i++){ + link = data.links[i]; + this.addLink(link.source, link.target, link.name, "perm"); + } + this.clear_history(); + } + + this.save_to_json = function() { + var d = {"nodes":[], "links":[]}; + for(var i = 0 ; i < nodes.length ; i++){ + var node = nodes[i]; + d['nodes'].push({ + "id": node.id, + "name": node.name, + "type":node.type, + "state":"perm", + "start":node.start, + "end":node.end, + "status": node.status, + "url": node.url, + "x": node.x, + "y": node.y, + }); + } + for(var j=0 ; j < links.length ; j++){ + var link = links[j]; + d['links'].push({ + "source":link.source.id, + "target":link.target.id, + "name":link.name + }); + } + return JSON.stringify(d); + } + + this.set_user = function(user) { + this.user = user; + this.history = new History(this.user, $('svg g.zoom')[0]); + console.log('new user: ' + user); + } + + function clear_history() { + if (this.history !== undefined) { + this.history.clear(); + } + } + + this.clear_history = clear_history; + + var get_nodes = function() { return nodes; }; + this.nodes = get_nodes; + + var get_links = function() { return links; }; + this.links = get_links; + +} + +return { + Graph: Graph, +}; + +}); diff --git a/src/model/util.js b/src/model/util.js new file mode 100644 index 00000000..43fa66db --- /dev/null +++ b/src/model/util.js @@ -0,0 +1,117 @@ +"use strict" + +/** + * model utility functions: + * - convert from/to client/backend data representations + */ +define(['jquery', 'model/diff'], +function($, model_diff) { + + /** + * read by adapting from backend format + */ + function adapt_format_read_node(n_raw) { + var ret; + + ret = $.extend({ + 'type' : n_raw['__type'].toLowerCase(), // discard all + // labels except + 'state' : 'temp', + }, n_raw); + + delete ret.__label_set; + + return ret; + } + + /** + * write by adapting to backend format + */ + function adapt_format_write_node(n_raw) { + var ret = $.extend({ + '__type' : n_raw.type, + }, n_raw); + + delete ret.type; + delete ret.state; + + return ret + } + + /** + * read by adapting from backend format + */ + function adapt_format_read_link(l_raw) { + var ret; + + ret = $.extend({ + 'sourceId' : l_raw['__src'], + 'targetId' : l_raw['__dst'], + 'type' : l_raw['__type'].toLowerCase(), // discard all + // labels except + 'state' : 'temp', + }, l_raw); + + ret['name'] = ret['type']; + + delete ret.__src; + delete ret.__dst; + delete ret.__type; + + return ret; + } + + /** + * write by adapting to backend format + */ + function adapt_format_write_link(l_raw) { + var ret = $.extend({ + '__src' : l_raw.sourceId, + '__dst' : l_raw.targetId, + '__type' : 'textual_link', + }, l_raw); + + delete ret.source; + delete ret.target; + delete ret.state; + + return ret; + } + + /** + * write adapt diff from node set, link set. sets may be passed by reference + * as they are cloned + */ + function adapt_format_write_topo_diff(n_set, l_set) { + + 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); + }) + + new_l_set = $.map(new_l_set, function(l, _) { + return adapt_format_write_link(l); + }) + + var topo_diff = new model_diff.new_topo_diff({ + node_set_add : new_n_set, + link_set_add : new_l_set + }); + return topo_diff; + } + + return { + adapt_format_read_node : adapt_format_read_node, + adapt_format_read_link : adapt_format_read_link, + adapt_format_write_node : adapt_format_write_node, + adapt_format_write_link : adapt_format_write_link, + adapt_format_write_topo_diff : adapt_format_write_topo_diff, + } +});
\ No newline at end of file diff --git a/src/robot.js b/src/robot.js new file mode 100644 index 00000000..700e41b6 --- /dev/null +++ b/src/robot.js @@ -0,0 +1,70 @@ +define(['jquery'], function($) { + +var sentence=""; +/*sentence+=" #Rhizibot is showing you a #tutorial|"; +sentence+="#Rhizi visualizes data with #Graphs and #relationships|"; +sentence+="#links and #nodes have #context and #meaning|"; + +sentence+="#entities and #concepts are #nodes|"; +sentence+="We built this entire graph in 30 seconds, try it out yourself!";*/ + +sentence+='##Rhizi is a tool for creating interactive #Networks|'; +sentence+='Create #Networks by writing #Sentences|'; +sentence+='like a #Tweet you can put a #Hashtag in your #Sentences|'; +sentence+='#Hello|'; +sentence+='#John|'; +sentence+='Put your text between #Commas to use #"Multiple words"|'; +sentence+='#"John Smith"|'; +sentence+='#"Beauty and the beast"|'; +sentence+='Use the word #And to #"Connect multiple things together"|'; +sentence+='#John likes #Apples and #Oranges and #Pistachio|'; +//sentence+='Choose the #"node type" by using the #"TAB key"|'; +sentence+='#Click on any #Node to change and modify it|'; +sentence+='#Play around and have #Fun!|'; + +var robot = function (element, sentence) { + var r = { + speed: 1, + counter: 0, + sentence: sentence, + sentencecounter: 0, + element: element || $('#textanalyser')}; + r.next_event = function () { + if(r.counter <= r.sentence.length) { + var text = r.element.val(); + r.counter++; + if (r.sentence.charAt(r.counter) !== "|") { + r.element.val(text + r.sentence.charAt(r.counter)); + if ('oninput' in document.documentElement) { + r.element.trigger('input', {}); + } + //if(Math.random()>0.9)graph.editType("x","temp",nodetypes[Math.round(Math.random()*4)]); + if (r.sentence.charAt(r.counter)==="#") { + r.sentencecounter++; + r.timeout_id = window.setTimeout( r.next_event, 30/r.speed+Math.random()*160/r.speed ); + } else { + r.timeout_id = window.setTimeout( r.next_event, 50/r.speed+Math.round(Math.random()*100/r.speed) ); + } + } else { + var e = jQuery.Event("keypress"); + e.which = 13; + e.keyCode = 13; + $("#textanalyser").trigger(e); + window.setTimeout( r.next_event, 650/r.speed ); + } + } else { + window.clearInterval(r.timeout_id); + } + } + return r; +} + +$('.logo').click(function(){ + setTimeout( robot(undefined, sentence).next_event, 1000 ); +}); + +/*var answer = confirm ("Would you like a tutorial?") +if (answer) +setTimeout( Robot, 100 );*/ + +}); // define diff --git a/src/rz_api_backend.js b/src/rz_api_backend.js new file mode 100644 index 00000000..df162225 --- /dev/null +++ b/src/rz_api_backend.js @@ -0,0 +1,220 @@ +"use strict"; + +/** + * API calls designed to execute against a local backend service + */ +define([], function() { + + function RZ_API_Backend() { + + /** + * issue rhizi server ajax call + */ + var ajax_rs = function(path, req_opts, on_success, on_error) { + + function on_error_wrapper(xhr, err_text, err_thrown) { + // log wrap callback + console.error('error: \'' + err_text + '\''); + if (on_error && typeof (on_error) === "function") { + on_error(err_type, err_text); + } + } + + function on_success_wrapper(xhr, text) { + // log wrap callback + var ret_data = xhr.data; + console.log('success: ' + JSON.stringify(ret_data)); + + if (on_success) { + on_success(ret_data); + } + } + + /* + * add common request options + */ + req_opts.dataType = "json"; + req_opts.contentType = "application/json; charset=utf-8"; + req_opts.error = on_error_wrapper; + req_opts.success = on_success_wrapper; + req_opts.headers = {}; + req_opts.timeout = 8000; // ms + req_opts.crossDomain = true; + + $.ajax('http://127.0.0.1:3000' + path, req_opts); + } + + /** + * common attr_diff + */ + this.commit_diff_attr = function(attr_diff, on_success, on_error) { + + var post_dict = { + 'attr_diff' : attr_diff + } + + var req_opts = { + type : 'POST', + data : JSON.stringify(post_dict), + }; + + return ajax_rs('/graph/attr-diff-commit', req_opts, on_success, + on_error); + } + + /** + * commit topo_diff + */ + this.commit_diff_topo = function(topo_diff, on_success, on_error) { + + var post_dict = { + 'topo_diff' : topo_diff + } + + var req_opts = { + type : 'POST', + data : JSON.stringify(post_dict), + }; + + return ajax_rs('/graph/diff-commit-topo', req_opts, on_success, + on_error); + } + + /** + * commit vis_diff + */ + this.commit_diff_vis = function(vis_diff, on_success, on_error) { + // TODO impl + } + + /** + * commit a diff_set + */ + this.commit_diff_set = function(diff_set, on_success, on_error) { + + var post_dict = { + 'diff_set' : diff_set + } + + var req_opts = { + type : 'POST', + data : JSON.stringify(post_dict), + }; + + return ajax_rs('/graph/diff-commit-set', req_opts, on_success, + on_error); + } + + /** + * clone rhizi repo + */ + this.clone = function(depth, on_success, on_error) { + + // prep request + var req_opts = { + type : 'POST', + }; + + ajax_rs('/graph/clone', req_opts, on_success, on_error); + } + + /** + * load node-set by id attribute + * + * @param on_complete_cb + * will be called with the returned json data on successful + * invocation + * @param on_error + * error callback + */ + this.load_node_set = function(id_set, on_success, on_error) { + + // prep request data + var post_dict = { + 'id_set' : id_set + } + + // prep request + var req_opts = { + type : 'POST', + data : JSON.stringify(post_dict), + }; + + return ajax_rs('/load/node-set-by-id', req_opts, on_success, + on_error); + } + + /** + * load link set by src / dst id + */ + this.load_link_set = function(link_ptr_set, on_success, on_error) { + + // prep request data + var post_dict = { + 'link_ptr_set' : link_ptr_set + } + + // prep request + var req_opts = { + type : 'POST', + data : JSON.stringify(post_dict), + }; + + return ajax_rs('/load/link-set/by_link_ptr_set', req_opts, + on_success, on_error); + } + + /** + * add a node set + */ + this.add_node_set = function(n_set, on_success, on_error) { + var topo_diff = new Topo_Diff(); + topo_diff.node_set_add = n_set; + + return this.topo_diff_commit(topo_diff, on_success, on_error); + } + + /** + * add a link set + */ + this.add_link_set = function(l_set, on_success, on_error) { + var topo_diff = new Topo_Diff(); + topo_diff.link_set_add = l_set; + + return this.topo_diff_commit(topo_diff); + } + + /** + * remove node set + */ + this.remove_node_set = function() { + var topo_diff = null; + return this.topo_diff_commit(topo_diff); + } + + /** + * remove link set + */ + this.remove_link_set = function() { + var topo_diff = null; + return this.topo_diff_commit(topo_diff); + } + + /** + * update node set + */ + this.update_node_set = function(attr_diff, on_success, on_error) { + return this.attr_diff_commit(attr_diff, on_success, on_error); + } + + /** + * update link set + */ + this.update_link_set = function() { + var attr_diff = null; + return this.attr_diff_commit(null); + } + } + + return new RZ_API_Backend(); +}); diff --git a/src/rz_api_mesh.js b/src/rz_api_mesh.js new file mode 100644 index 00000000..9014dbac --- /dev/null +++ b/src/rz_api_mesh.js @@ -0,0 +1,22 @@ +/** + * API calls designed to execute in decentralized fashion + */ +define(['rz_api_backend'], +function(rz_api_backend) { + function RZ_API_Mesh() { + + /** + * suggest diff block and await commit/reject consensus + */ + // @ajax-trans + this.broadcast_possible_next_diff_block = function (diff_set) { + + function on_success(data){ + // TODO impl + } + + rz_api_backend.commit_diff_set(diff_set); + } + } + return new RZ_API_Mesh(); +}); diff --git a/src/rz_core.js b/src/rz_core.js new file mode 100644 index 00000000..7b56d41b --- /dev/null +++ b/src/rz_core.js @@ -0,0 +1,744 @@ +"use strict" + +define(['jquery', 'd3', 'consts', 'signal', 'util', 'model/graph', 'model/core', 'view/helpers', 'view/view', 'rz_observer'], +function($, d3, consts, signal, util, model_graph, model_core, view_helpers, view, rz_observer) { + +var addednodes = []; + +var vis; + +var graphstate = "GRAPH"; +var graphinterval = 0; + +var ganttTimer = 0; + +var deliverables = []; + +var circle; // <-- should not be module globals. + +var scrollValue = 0, + zoomObject; + +var graph; + +var drag; + +var force; + +var state_to_link_class = { + enter:'enterlink graph', + exit:'exitlink graph', +}; + +function recenterZoom() { + vis.attr("transform", "translate(0,0)scale(1)"); +} + +var initDrawingArea = function () { + + function zoom() { + if (graphstate === "GRAPH") { + vis.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); + } + if (graphstate === "GANTT") { + vis.attr("transform", "translate(0,0)scale(1)"); + } + } + + 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) { + d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y); + tick(); + } + + function dragended(d) { + d3.select(this).classed("dragging", false); + d3.select(this).classed("fixed", true); + d3.select(this).attr("dx", d3.event.x).attr("dy", d3.event.y); + if (d.dragstart.clientX - d3.event.sourceEvent.clientX != 0 || + d.dragstart.clientY - d3.event.sourceEvent.clientY != 0) { + tick(); + force.resume(); + } + } + + var el = document.body; + + graph = new model_graph.Graph(el); + + //Zoom scale behavior in zoom.js + zoomObject = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom); + + vis = d3.select(el).append("svg:svg") + .attr('id', 'canvas_d3') + .attr("width", '100%') + .attr("height", '100%') + .attr("pointer-events", "all") + .call(zoomObject) + .append("g") + .attr("class", "zoom"); + + // TODO: why do we need this huge overlay (hugeness also not constant) + vis.append("rect") + .attr("class", "overlay graph") + .attr("width", $(el).innerWidth() * 12) + .attr("height", $(el).innerHeight() * 12) + .attr("x", -$(el).innerWidth() * 5) + .attr("y", -$(el).innerHeight() * 5); + $('.overlay').click(mousedown); + + // 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"); + + drag = d3.behavior.drag() + .origin(function(d) { return d; }) + .on("dragstart", dragstarted) + .on("drag", dragged) + .on("dragend", dragended); + + 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(); + + graph.update = update; + $('#canvas_d3').dblclick(canvas_handler_dblclick); +} + +initDrawingArea(); + +/** + * find the visual element counterpart of a given model object. This relies on + * the visual element having an id attribute equal to the object's id. + * + * @return null if visual element is not found + */ +function locate_visual_element(model_obj){ + var id_sel = $('#' + model_obj.id); + if (0 == id_sel.length){ + console.warn('unable to find visual element for model object: object id: ' + model_obj.id.toString()) + return null; + } + return id_sel[0]; +} + +/** + * add node on canvas double click + */ +function canvas_handler_dblclick(){ + var n = model_core.create_node__set_random_id(); + n.name = ''; // will be set by user + + graph._addNodeNoHistory(n); // FIXME: clean once node creation functions consolidate + graph.update(); + + var n_ve = locate_visual_element(n); // locate visual element + + var on_slowdown_cb = function(){ + var set_focus = true; + editNode(n_ve, n, set_focus); + observer.disconnect(); + } + var mutation_handler = rz_observer.new_Mutation_Handler__on_dxy_slowdown(on_slowdown_cb); + var observer = rz_observer.new_MutationObserver(mutation_handler); + mutation_handler.on_slowdown_threshold_reached; + + observer.observe(n_ve, { + subtree: false, + childList : false, + attributes: true, + attributeOldValue : true, + }); +} + +function update(no_relayout) { + var node, + link, + link_g, + linktext, + nodetext, + link_group; + + link_group = vis.select('#link-group'); + link = link_group.selectAll("g.link") + .data(graph.links()); + + link_g = link.enter().append('g') + .attr('class', 'link graph'); + + link_g.append("svg:defs").selectAll("marker") + .data(["end"]) // Different link/path types can be defined here + .append("svg:marker") // This section adds in the arrows + .attr("id", String) + .attr("viewBox", "0 -5 10 10") + .attr("refX", 22) + .attr("refY", -1.5) + .attr("markerWidth", 4) + .attr("markerHeight", 4) + .attr("orient", "auto") + .attr("class", "graph") + .style("fill", function(d){ + if (d.state==="enter" || d.state==="exit") { + return "EDE275"; + } else { + return "#aaa"; + } + }) + .append("svg:path") + .attr("d", "M0,-5L10,0L0,5"); + + link_g.append("path") + .attr("class", function(d) { + return state_to_link_class[d.state] || 'link graph'; + }) + .attr("marker-end", "url(#end)"); + + // second path for larger click area + link_g.append("path") + .attr("class", "ghostlink") + .on("click", function(d, i) { + var that = this, + source = this.link.source, + target = this.link.target; + + view.edge_info.on_delete(function () { + graph.removeLink(that.link); + graph.update(true); + view.edge_info.hide(); + }); + view.edge_info.show(d); + highlight(source); + highlight(target); + source.state = 'chosen'; + target.state = 'chosen'; + graph.update(true); + }); + + link.style("stroke-dasharray", function(d,i){ + if(d.name && d.name.replace(/ /g,"")=="and" && d.state==="temp") + return "3,3"; + else + return "0,0"; + }); + + link.selectAll('path.link') + .attr('stroke-width', function(d) { + if (d.state === 'exit' || d.state === 'enter') { + return "4px"; + } + return "2.0px"; + }); + + link.exit().remove(); + + link_group.selectAll('.ghostlink') + .data(graph.links()) + .each(function (d) { + this.link = d; + }); + + linktext = vis.selectAll(".linklabel").data(graph.links()); + linktext.enter() + .append("text") + .attr("class", "linklabel graph") + .attr("text-anchor", "middle") + .on("click", function(d, i) { + if (d.state !== "temp") { + editLink(this, d, i); + } + }); + + linktext + .text(function(d) { + var name = d.name || ""; + if (!(d.target.state === "temp" || + d.source.state === "chosen" || d.target.state === "chosen")) { + return ""; + } + if (name.length < 25 || d.source.state === "chosen" || + d.target.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; + }); + + var nodeEnter = node.enter() + .append("g").attr('class', 'node') + .attr('id', function(d){ return d.id; }) // append node id to enable data->visual mapping + .attr('visibility', 'hidden') // made visible on first tick + .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"){ + editNode(this, d, i); + showInfo(this.node, i); + } + }) + .call(drag); + + node.each(function (d) { + this.node = d; + }); + + nodetext = nodeEnter.insert("text") + .attr("class", "nodetext graph") + .attr("dx", 15) + .attr("dy", ".30em"); + + node.select('g.node text') + .text(function(d) { + if (!d.name) { + return ""; + } + 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", "circle graph") + .attr("r", function(d) { + return view_helpers.customSize(d.type) - 2; + }) + .style("fill", function(d) { + return view_helpers.customColor(d.type); + }) + .style("stroke", function(d) { + if (d.state === "chosen") return "#EDE275"; + if (d.state === "enter") return "#EDE275"; + if (d.type === "bubble") return "#101010"; + if (d.state === "exit") return "#EDE275"; + if (d.type === "chainlink") return "#AAA"; + + return "#fff"; + }) + .style("stroke-width", function(d) { + if (d.state === "temp" && d.type !== "empty" || d.state === "chosen") return "3px"; + else return "1.5px"; + }) + .style("box-shadow", function(d) { + if (d.state === "temp") return "0 0 40px #FFFF8F"; + else return "0 0 0px #FFFF8F"; + }) + .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(); + if(d.state!=="temp") { + showInfo(d, i); + } else { + removeHighlight(); + } + update(true); + }); + + //if(graphstate==="GANTT"){ + nodeEnter.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; + } + }) + .on("click", function(d, i) { + if(d.state!=="temp")showInfo(d, i); + }); + //} + + 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 === "deliverable") { + deliverables.push({ + "id": nodes[i].id, + "startdate": nodes[i].start, + "enddate": nodes[i].end + }); + } + //Do something + } + + force.nodes(graph.nodes()) + .links(graph.links()) + + if (no_relayout) { + // XXX If we are stopped we need to update the text of the links at least, + // and this is the simplest way + tick(); + } else { + force.alpha(0.1).start(); + } +} + + + +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(force.nodes(), function(d) { + return d.id; + }); + var link = vis.select("#link-group").selectAll("path.link") + .data(graph.links()); + var linktext = vis.selectAll(".linklabel").data(graph.links()); + + function transform(d) { + if (graphstate === "GRAPH" || d.type === "deliverable") { + if (check_for_nan(d.x) || check_for_nan(d.y)) { + return; + } + if (d.state === "temp") { + return "translate(" + d.x + "," + d.y + ")"; + } else { + return "translate(" + d.x + "," + d.y + ")"; + } + } else { + return "translate(0,0)"; + } + return "translate(" + d.x + "," + d.y + ")"; + } + + if (graphstate === "GANTT") { + var k = 20 * e.alpha; + var today = new Date(); + var missingcounter = 0; + + graph.nodes().forEach(function(d, i) { + if ((d.start === 0 || d.end === 0)) { + d.x = 450 + missingcounter * 100; + d.y = window.innerWidth / 2; + if (missingcounter >= 6) { + d.x = 450 + (missingcounter - 6) * 100; + d.y = window.innerWidth / 2 + 50; + } + missingcounter++; + } else { + //var min= 150+graphinterval*Math.ceil(Math.abs(d.start.getTime() - today.getTime()) / (1000 * 3600 * 24)) - $('.gantbox').scrollLeft(); + //var max= 150+graphinterval*Math.ceil(Math.abs(d.end.getTime() - d.start.getTime()) / (1000 * 3600 * 24)) - $('.gantbox').scrollLeft(); + //d.x = min+Math.sin(today.getTime()/1000*Math.PI*2/10)*max; + ganttTimer++; + if (ganttTimer < 3000) { + d.x = 150 + graphinterval * Math.ceil(Math.abs(d.start.getTime() - today.getTime()) / (1000 * 3600 * 24)) * ganttTimer / 3000; + d.y = 150 + d.start.getHours() * 17; + } else { + d.x = 150 + graphinterval * Math.ceil(Math.abs(d.start.getTime() - today.getTime()) / (1000 * 3600 * 24)); + d.y = 150 + d.start.getHours() * 17; + } + } + if (d.state === "chosen") { + scrollValue = d.x; + } + }); + } else { + //circles animation + var tempcounter = 0; + var temptotal = 0; + graph.nodes().forEach(function(d, i) { + if (d.state === "temp" && d.type!=="chainlink" && d.type!=="bubble") { + temptotal++; + } + }); + if(temptotal!==newnodes){ + newnodes+=temptotal/15/(newnodes*newnodes); + } + if(newnodes>=temptotal){ + newnodes=temptotal; + } + if(newnodes<1)newnodes=1; + graph.nodes().forEach(function(d, i) { + if (d.state === "temp") { + tempcounter++; + if(d.type==="chainlink" || d.type==="bubble"){ + d.x = window.innerWidth / 2; + d.y = window.innerHeight / 2; + } else { + d.x = window.innerWidth / 2 + (60+newnodes*20) * Math.cos(-Math.PI+Math.PI * 2 * (tempcounter-1) / newnodes+0.3); + d.y = window.innerHeight / 2 + (60+newnodes*20) * Math.sin(-Math.PI+Math.PI * 2 * (tempcounter-1) / newnodes+0.3); + } + check_for_nan(d.x); + check_for_nan(d.y); + } + }); + } + + link.attr("d", function(d, i) { + var d_val, + ghost; + + if (graphstate === "GRAPH") { + var dx = d.target.x - d.source.x, + dy = d.target.y - d.source.y, + dr = Math.sqrt(dx * dx + dy * dy); + d_val = "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; + } else if (graphstate === "GANTT") { + if (d.state === "enter" || d.state === "exit") { + var dx = d.target.x - d.source.x, + dy = d.target.y - d.source.y, + dr = Math.sqrt(dx * dx + dy * dy) * 5; + d_val = "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; + } else { + var dx = d.target.x - d.source.x, + dy = d.target.y - d.source.y, + dr = Math.sqrt(dx * dx + dy * dy) * 5; + + d_val = "M" + 0 + "," + 0 + "A" + dr + "," + dr + " 0 0,1 " + 0 + "," + 0; + } + } + // update ghostlink position + ghost = $(this.nextElementSibling); + ghost.attr("d", d_val); + return d_val; + }); + + + linktext.attr("transform", function(d) { + if (graphstate === "GRAPH") { + return "translate(" + (d.source.x + d.target.x) / 2 + "," + (d.source.y + d.target.y) / 2 + ")"; + } else { + return "translate(0,0)"; + } + }); + + node.attr("transform", transform); + + // After initial placement we can make the nodes visible. + //links.attr('visibility', 'visible'); + node.attr('visibility', 'visible'); +} + +function removeHighlight() { + // TODO: stop manipulating state + var nodes = graph.nodes(), + links = graph.links(), + k = 0, j = 0; + + while (k < nodes.length) { + if (nodes[k]['state'] === "enter" || nodes[k]['state'] === "exit" || nodes[k]['state'] === "chosen") { + nodes[k]['state'] = "perm"; + } + k++; + } + while (j < links.length) { + links[j]['state'] = "perm"; + j++; + } +} + +function highlight(n) +{ + var n, + connected = graph.getConnectedNodesAndLinks(n, 1), + i, + node, + link, + data; + + n.state = 'chosen'; + + for (i = 0 ; i < connected.nodes.length ; ++i) { + data = connected.nodes[i]; + node = data.node; + switch (data.type) { + case 'exit': + node.state = 'exit'; + break; + case 'enter': + node.state = 'enter'; + break; + }; + } + for (i = 0 ; i < connected.links.length ; ++i) { + data = connected.links[i]; + link = data.link; + switch (data.type) { + case 'exit': + link.state = 'exit'; + break; + case 'enter': + link.state = 'enter'; + break; + }; + } +} + +function showInfo(d, i) { + if (d.state !== "chosen" && d.state !== 'temp') { + highlight(d); + view.node_info.show(d); + view.node_info.on_submit(function() { + if (d.type === "deliverable") { + graph.editDates(d.id, null, new Date($("#editstartdate").val()), new Date($("#editenddate").val())); + } + graph.editType(d.id,d.type,$('#edittype').val()); + graph.editURL(d.id, d.type, $('#editurl').val()); + graph.update(true); + return false; + }); + view.node_info.on_delete(function() { + if (confirm('This node and all its connections will be deleted, are you sure?')) { + graph.removeNode(d.id, null); + graph.update(false); + view.node_info.hide(); + } + }); + } else { + removeHighlight(); + view.node_info.hide(); + } + graph.update(true); +} + +function mousedown() { + $('.editinfo').css('top', -100); + $('.editinfo').css('left', 0); + $('.editlinkinfo').css('top', -100); + $('.editlinkinfo').css('left', 0); + removeHighlight(); + view.hide(); + graph.update(true); +} + +function AddedUnique(newnode) { + truth = true; + for (var p = 0; p < addednodes.length; p++) { + if (addednodes[p] === newnode) { + truth = false; + } + } + return truth; +} + + +$('#editform').keypress(function(e) { + signal.signal(consts.KEYSTROKES, [{where: consts.KEYSTROKE_WHERE_EDIT_NODE, keys: [e.which]}]); + if (e.which == 13) { + $('.editinfo').css('top', -100); + $('.editinfo').css('left', 0); + var element = $('#editname'); + var newname = element.val(); + var d = element.data().d; + graph.editName(d.id, newname); + graph.update(true); + return false; + } +}); + + +/** + * @param e visual node element + * @param n node model object + */ +function editNode(e, n, set_focus) { + var oldname = n.name; + var en_element = $('#editname'); + var offset = $(e).find('.nodetext').offset(); + + $('.editinfo').css('top', offset.top); + $('.editinfo').css('left', offset.left); + en_element.val(oldname); + en_element.data().d = n; + + if (set_focus){ + en_element.focus(); + } +} + +function editLink(link, d, i) { + var offset = $(link).offset(), + oldname = d.name; + + $('.editlinkinfo').css('top', offset.top); + $('.editlinkinfo').css('left', offset.left); + $('#editlinkname').val(oldname); + + // TODO: handle escape as well to quit without changes (enter does submit) + $('#editlinkform').submit(function() { + graph.editLink(d.source.id, d.target.id, $('#editlinkname').val()); + $('.editlinkinfo').css('top', -100); + $('.editlinkinfo').css('left', 0); + graph.update(true); + + return false; + }); + + graph.update(true); +} + +return { + graph: graph, + force: force, + load_from_json: function(result) { + graph.load_from_json(result); + recenterZoom(); + update(false); + } +} +}); /* close define call */ diff --git a/src/rz_observer.js b/src/rz_observer.js new file mode 100644 index 00000000..4fe6385f --- /dev/null +++ b/src/rz_observer.js @@ -0,0 +1,118 @@ +"use strict" + +/** + * DOM object observers + */ +define( + [], + function() { + + var MutationObserver = window.MutationObserver; + // more portable version: var MutationObserver = + // window.MutationObserver || + // window.WebKitMutationObserver; + + /** + * observe SVG object's translate attribute (x,y values), measure + * change rate & invoke on_slowdown_threshold_reached() upon + * reaching change slowdown threshold. + * + * Caller is responsible to disconnect observer. + */ + function Mutation_Handler__on_dxy_slowdown( + on_slowdown_threshold_reached) { + + var x_cur, y_cur, d, dx, dy, avg_d = 0; + + this.on_slowdown_threshold_reached = on_slowdown_threshold_reached; + + /** + * handle observer mutation + */ + this.handle_mutation = function(m) { + if ('transform' != m.attributeName || null == m.oldValue) { + return; + } + + /* + * parsed txt example: + * "translate(173.6007550157428,275.43228723527193)" + */ + var rgx_m = m.oldValue.match(/\((\d+.\d+),(\d+.\d+)\)/); + x_cur = rgx_m[1]; + y_cur = rgx_m[2]; + if (undefined == this.x_prv) { + this.x_prv = x_cur; + this.y_prv = y_cur; + return; + } + dx = x_cur - this.x_prv; + dy = y_cur - this.y_prv; + + var d = Math.sqrt(dx * dx + dy * dy); + // average across last samples + avg_d = (1.0 - this.most_recent_sample_weight) * avg_d + + this.most_recent_sample_weight * d; + + // debug + // console.log({ + // 'dx' : dx, + // 'dy' : dy, + // 'd' : d, + // 'avg_d' : avg_d, + // }); + + if (avg_d < this.slowdown_threshold) { + // slowdown threshold reached + if (this.on_slowdown_threshold_reached) { + this.on_slowdown_threshold_reached(); + } + } + + this.x_prv = x_cur; + this.y_prv = y_cur; + } + } + + function new_Mutation_Handler__on_dxy_slowdown( + on_slowdown_threshold_reached, slowdown_threshold, + most_recent_sample_weight) { + if (undefined == slowdown_threshold) { + slowdown_threshold = 0.07; + } + if (undefined == most_recent_sample_weight) { + most_recent_sample_weight = 0.3; + } + + var ret = new Mutation_Handler__on_dxy_slowdown( + on_slowdown_threshold_reached); + ret.slowdown_threshold = slowdown_threshold; + ret.most_recent_sample_weight = most_recent_sample_weight; + return ret; + } + + function new_MutationObserver(handler) { + /* + * FIXME implemente with inheritence - currently triggers + * 'illegal invocation', possibly due to some interaction with + * requirejs + */ + // var o = new + // window.MutationObserver(Mutation_Observer.handler); + // var ret = Object.create(o); + // ret. = undefined; + // return ret; + var for_each_mutaion = function(m_set) { + m_set.forEach(function(m) { + handler.handle_mutation(m); + }) + }; + + return new MutationObserver(for_each_mutaion); + } + + return { + new_MutationObserver : new_MutationObserver, + new_Mutation_Handler__on_dxy_slowdown : new_Mutation_Handler__on_dxy_slowdown, + }; + }); diff --git a/src/signal.js b/src/signal.js new file mode 100644 index 00000000..e5888b4c --- /dev/null +++ b/src/signal.js @@ -0,0 +1,22 @@ +/* + * Signal/slot (i.e. blackboard pattern) for rhizi. + * + * This is a thin wrapper over jquery right now. + * But just keeping it here to make any future change of implementation slightly + * easier. + */ + +define(['jquery'], function($) { + function slot(name, handler) { + $(window).on(name, function(e, args) { + handler(args); + }); + }; + function signal(name, obj) { + $(window).trigger(name, obj); + } + return { + 'slot': slot, + 'signal': signal + }; +}) diff --git a/src/textanalysis.js b/src/textanalysis.js new file mode 100644 index 00000000..4cff49bc --- /dev/null +++ b/src/textanalysis.js @@ -0,0 +1,459 @@ +"use strict"; + +define(['rz_core', 'model/util', 'model/diff'], +function(rz_core, model_util, model_diff) { + +var typeindex = 0; +var nodetypes = ["person", "project", "skill", "deliverable", "objective"]; +var typeStack = []; + +var lastnode; + +var sugg = {}; // suggestions for autocompletion of node names + +var ANALYSIS_NODE_START = 'ANALYSIS_NODE_START'; +var ANALYSIS_LINK = 'ANALYSIS_LINK'; + +function selectedType() +{ + return nodetypes[typeindex]; +} + +function autoSuggestAddName(name) +{ + /* note that name can contain spaces - this is ok. We might want to limit this though? */ + if(name.split(" ").length > 1) { + sugg['"' + name + '"'] = 1; + } else { + sugg[name] = 1; + } +} + +function autocompleteCallback(request, response_callback) +{ + var ret = []; + if (request.term === "" || request.term) { + for (var name in sugg) { + if (name.toLowerCase().indexOf(request.term.toLowerCase()) === 0) { + ret.push(name); + } + } + } + response_callback(ret); +} + +/* up_to_two_renames: + * + * allow one letter or 'new node' to anything changes */ +function up_to_two_renames(graph, old_name, new_name) +{ + 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 (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]); + } +} + +// TODO: add escape char, i.e. r"bla\"bla" -> ['bla"bla'] +function tokenize(text, node_token, quote) +{ + var segment = [], + subsegment = [], + sentence = [], + quoteword; + var j; + + segment = text.split(node_token); + for (j = 0; j < segment.length; j++) { + if (j !== 0) sentence.push(node_token); + subsegment = segment[j].split(" "); + if (subsegment.length === 0) { + sentence.push(" "); + } + for (var k = 0; k < subsegment.length; k++) { + if (subsegment[k] !== " " && subsegment[k] !== "") { + if (subsegment[k].charAt(0) === quote) { + quoteword = ""; + do { + quoteword += subsegment[k] + ' '; + if(subsegment[k].charAt(subsegment[k].length-1) !== quote)k++; + } while (k < subsegment.length && subsegment[k].charAt(subsegment[k].length - 1) !== quote); + if (subsegment[k] && subsegment[k]!==quoteword.replace(/ /g, "")) { + quoteword += subsegment[k]; + } + sentence.push(quoteword.replace(new RegExp(quote, 'g'), "")); + } else { + sentence.push(subsegment[k]); + } + } + } + } + return sentence; +} + +/* + * textAnalyser + * + * Input: + * @newtext - new sentence + * @finalize - is this an intermediate editing state or are we editing the graph + * + * Output: + * none + * + * Side effect: + * updating graph (global) + * + * Implementation notes: + * There is no well defined grammer. The translation goes from obvious to not + * so much for more complex sentences involving more then two nodes (two '#' + * marks). + * + */ +var textAnalyser = function (newtext, finalize) { + var sentence, + newlinks = [], + newnodes = [], + linkindex = 0, + nodeindex = 0, + orderStack = [], + and_count = 0, + prefix = "", + ret = {'nodes': [], 'links': []}, + m, + word, + completeSentence, + typesetter, starGraph, + n, + link_hash = {}, + yell_bug = false, // TODO: fix both issues + NODE = "NODE", + LINK = "LINK", + START = "START"; + + function addNode(name, type, state) { + if (type === undefined) { + console.log('bug: textanalyser.addNode of type undefined'); + } + ret.nodes.push({'name':name, 'type':type, 'state':state}); + } + function addLink(src, dst, name, state) { + if (!src || !dst) { + if (yell_bug) { + console.log('bug - adding link (' + src + ', ' + dst + ')'); + } + return; + } + if (link_hash[src] && link_hash[src][dst]) { + if (yell_bug) { + console.log('bug - adding link twice (' + src + ', ' + dst + ')'); + } + return; + } + if (!link_hash[src]) { + link_hash[src] = {}; + } + link_hash[src][dst] = 1; + ret.links.push({'sourceName':src, 'targetName':dst, 'name':name ? name.trim() : "", 'state':state}); + } + + //Sentence Sequencing + //Build the words and cuts the main elements + sentence = tokenize(newtext, '#', '"'); + + //BUILD NEW NODE AND LINK ARRAYS WITH ORDER OF APPEARENCE + for (m = 0; m < sentence.length; m++) { + switch (sentence[m]) { + case "#": + orderStack.push(START); + break; + case "and": + case "+": + case ",": + case "&": + sentence[m] = "and"; + and_count++; + //orderStack.push("AND"); + default: + if (orderStack[orderStack.length - 1] === START) { + orderStack.push(NODE); + newnodes.push(sentence[m]); + linkindex++; + } else if (orderStack[orderStack.length - 1] === NODE) { + orderStack.push(LINK); + if (!newlinks[linkindex]) { + newlinks[linkindex] = sentence[m] + " "; + } else { + newlinks[linkindex] += sentence[m] + " "; + } + } else { + if (!newlinks[linkindex]) { + newlinks[linkindex] = sentence[m] + " "; + } else { + newlinks[linkindex] += sentence[m] + " "; + } + } + if (newnodes.length === 0) { + prefix += (prefix.length > 0 ? ' ' : '') + sentence[m]; + } + break; + } + } + + starGraph = (newlinks.length - and_count) >= 3 || + ((newlinks.length - and_count >= 1) && + newlinks.length > 2 && + orderStack.length > 1 && + orderStack[orderStack.length - 1] != NODE); + + //PREFIX not null case - put complete sentence in first link. + if (prefix && !starGraph) { + newlinks[1] = prefix + " " + newnodes[0] + + (newlinks[1] !== undefined || newnodes[1] !== undefined ? + " " : "") + + (newlinks[1] !== undefined ? newlinks[1] : "") + + (newnodes[1] !== undefined ? newnodes[1] : ""); + } + + //WRITE COMPLETE SENTENCE + linkindex = 0; + nodeindex = 0; + word = ""; + completeSentence = prefix.length > 0 ? String(prefix) + " " : ""; + for (m = 0; m < orderStack.length; m++) { + if (orderStack[m] === NODE) { + word += " (" + newnodes[nodeindex] + ") "; + completeSentence += newnodes[nodeindex] + " "; + nodeindex++; + } else if (orderStack[m] === LINK) { + word += " -->" + newlinks[nodeindex] + " --> "; + completeSentence += newlinks[nodeindex]; + } + } + completeSentence = completeSentence.trim(); + + //REBUILD GRAPH + linkindex = 0; + nodeindex = 0; + + //CHANGE TO PERMANENT STATE AND UPDATE SUGGESTIONLIST + typesetter = ""; + if (finalize === true) { + typesetter = "perm"; + for (n = 0; n < newnodes.length; n++) { + autoSuggestAddName(newnodes[n]); + } + } else { + typesetter = "temp"; + } + + //ADD SURROUNDING BUBBLE + if (orderStack.length > 0) { + addNode("", "bubble","temp"); + } + + //0-N ORDER STACK + for (m = 0; m < orderStack.length - 1; m++) { + switch (orderStack[m]) { + case START: + if (!typeStack[nodeindex]) { + typeStack[nodeindex] = selectedType(); + } + break; + case NODE: + addNode(newnodes[nodeindex], typeStack[nodeindex], typesetter); + if (!starGraph) { + addLink(newnodes[nodeindex - 1], newnodes[nodeindex], + newlinks[linkindex], typesetter); + } + nodeindex++; + break; + case LINK: + linkindex++; + break; + } + } + + //FINAL N ORDER + switch (orderStack[orderStack.length - 1]) { + case START: + typeStack[nodeindex] = selectedType(); + addNode("new node", typeStack[nodeindex], "temp"); + if (!starGraph) { + addLink(newnodes[nodeindex - 1], "new node", newlinks[linkindex], "temp"); + and_connect("new node"); + } + ret.state = ANALYSIS_NODE_START; + break; + case NODE: + typeStack[nodeindex] = selectedType(); + addNode(newnodes[nodeindex], typeStack[nodeindex], typesetter); + if (!starGraph) { + addLink(newnodes[nodeindex - 1], newnodes[nodeindex], newlinks[linkindex], typesetter); + and_connect(newnodes[nodeindex]); + } + break; + case LINK: + linkindex++; + addNode("new node", selectedType(), "temp"); + if (!starGraph) { + addLink(newnodes[nodeindex - 1], "new node", newlinks[linkindex], "temp"); + and_connect("new node"); + } + ret.state = ANALYSIS_LINK; + break; + } + + //EXTERNAL AND CONNECTION CHECKING + function and_connect(node) { + var verb; + for(var x=0;x<newlinks.length;x++){ + if(newlinks[x])if(newlinks[x].replace(/ /g,"")!=="and"){ + verb = newlinks[x]; + for(var y=0; y<x ;y++){ + addLink(newnodes[y], node, verb, typesetter); + for(var z=x; z<newnodes.length ;z++){ + addLink(newnodes[y], newnodes[z], verb, typesetter); + } + } + } + } + } + + /*console.log(sentence); + console.log(completeSentence); + console.log(orderStack);*/ + + //STAR CASE + if (starGraph) { + addNode(completeSentence, "chainlink", typesetter); + for (n = 0; n < newnodes.length; n++) { + addLink(newnodes[n], completeSentence, "", typesetter); + } + } + + ret.drop_conjugator_links = and_count < linkindex; + + ret.applyToGraph = function(graph, backend_commit) { + window.ret = ret; + var comp = graph.compareSubset('temp', + ret.nodes.filter( + function(node) { + return !graph.hasNodeByName(node.name, "perm") + && node.type !== 'bubble'; + }).map(function (node) { + return {name: node.name}; + }), + ret.links.map( + function (link) { + return [link.sourceName, link.targetName]; + })); + + var k, n, l; + if (comp.graph_same && !finalize) { + if (comp.old_name && comp.new_name) { + up_to_two_renames(graph, comp.old_name, comp.new_name); + } + for (k in ret.links) { + l = ret.links[k]; + graph.addLinkByName(l.sourceName, l.targetName, l.name, l.state, ret.drop_conjugator_links); + } + } else { + // REINITIALISE GRAPH (DUMB BUT IT WORKS) + graph.removeNodes("temp"); + graph.removeLinks("temp"); + for (k in ret.nodes) { + n = ret.nodes[k]; + if (n.state == 'temp' && finalize) { + console.log('bug: temp node creation on finalize'); + } else { + lastnode = graph.addNode(n.name, n.type, n.state); + } + } + for (k in ret.links) { + l = ret.links[k]; + if (!finalize || l.name !== 'and') { + graph.addLinkByName(l.sourceName, l.targetName, l.name, l.state, ret.drop_conjugator_links); + } + } + } + + if (finalize && backend_commit) { + // broadcast diff: + // - finalize? + // - broadcast_diff requested by caller + var topo_diff = model_util.adapt_format_write_topo_diff(ret.nodes, ret.links); + var diff_set = model_diff.new_diff_set(); + diff_set.add_diff_obj(topo_diff); + graph.commit_diff_set(diff_set); + } + + // UPDATE GRAPH ONCE + graph.update(!finalize && comp.graph_same); + }; + + if (finalize) { + typeStack = []; + } + + return ret; +}; + +return { + autocompleteCallback:autocompleteCallback, + textAnalyser:textAnalyser, + autoSuggestAddName:autoSuggestAddName, + ANALYSIS_NODE_START:ANALYSIS_NODE_START, + ANALYSIS_LINK:ANALYSIS_LINK, + + //for the external arrow-type changer + lastnode: function() { return lastnode; }, + + selected_type_next: function() { + typeindex = (typeindex + 1) % 5; + return selectedType(); + }, + selected_type_prev: function() { + typeindex = (typeindex + 4) % 5; + return selectedType(); + } +}; +}); diff --git a/src/textanalysis.ui.js b/src/textanalysis.ui.js new file mode 100644 index 00000000..0be87f64 --- /dev/null +++ b/src/textanalysis.ui.js @@ -0,0 +1,164 @@ +"use strict" + +define(['autocomplete', 'rz_core', 'textanalysis', 'signal', 'consts'], +function(autocomplete, rz_core, textanalysis, signal, consts) { +var text = ""; // Last text of sentence +var element_name = '#textanalyser'; +var element = $(element_name); +var suggestionChange; + +function analyzeSentence(sentence, finalize) +{ + var ret = textanalysis.textAnalyser(sentence, finalize); + + switch (ret.state) { + case textanalysis.ANALYSIS_NODE_START: + $('.typeselection').css({top:window.innerHeight/2-115,left:window.innerWidth/2-325}); + $('.typeselection').html('<table><tr><td style="height:28px"></td></tr><tr><td>Use [TAB] key to pick a type</td></tr></table>'); + break; + case textanalysis.ANALYSIS_LINK: + $('.typeselection').css('top', -300); + $('.typeselection').css('left', 0); + break; + } + + var backend_commit = false; + ret.applyToGraph(rz_core.graph, backend_commit); + + if (finalize || sentence.length == 0) { + $('.typeselection').css('top', -300); + $('.typeselection').css('left', 0); + $('span.ui-helper-hidden-accessible').hide(); + } else { + $('span.ui-helper-hidden-accessible').show(); + } +} + +function textSelect(inp, s, e) { + e = e || s; + if (inp.createTextRange) { + var r = inp.createTextRange(); + r.collapse(true); + r.moveEnd('character', e); + r.moveStart('character', s); + r.select(); + }else if(inp.setSelectionRange) { + inp.focus(); + inp.setSelectionRange(s, e); + } +} + +function changeType(arg) { + var lastnode = textanalysis.lastnode(), + nodetype, + id; + + if(!lastnode) { + id = "new node"; + } else { + id = lastnode.id; + } + nodetype = (arg === 'up'? textanalysis.selected_type_next() : textanalysis.selected_type_prev()); + + if (arg === 'up') { + rz_core.graph.editType(id, null, nodetype); + $('.typeselection').html('<table><tr><td style="height:28px"></td></tr><tr><td>' + "Chosen Type: " + nodetype + '</td></tr></table>'); + rz_core.graph.findCoordinates(id, null); + } else { + rz_core.graph.editType(id, null, nodetype); + $('.typeselection').html('<table><tr><td style="height:28px"></td></tr><tr><td>' + "Chosen Type: " + nodetype + '</td></tr></table>'); + rz_core.graph.findCoordinates(id, null); + } + rz_core.graph.update(true); +} + +return { + analyzeSentence: analyzeSentence, + main:function () { + if (element.length != 1) { + return; + } + + element.autocompleteTrigger({ + triggerStart: '#', + triggerEnd: '', + source: textanalysis.autocompleteCallback, + open: function() { + $('.ui-autocomplete').css('width', '10px'); + }, + }); + + $(document).keydown(function(e) { + signal.signal(consts.KEYSTROKES, [{where: consts.KEYSTROKE_WHERE_DOCUMENT, keys: [e.keyCode]}]); + if (e.keyCode == 9) {//TAB + e.preventDefault(); + changeType(e.shiftKey ? "up" : "down", textanalysis.lastnode()); + return false; + } + + if (e.keyCode == 37) {//UP + $('html, body').scrollLeft(0); + } + if (e.keyCode == 39) {//DOWN + $('html, body').scrollLeft(0); + } + + if (e.keyCode == 38) {//UP + suggestionChange = true; + } + if (e.keyCode == 40) {//DOWN + suggestionChange = true; + } + + if (e.keyCode == 9) {//TAB + return false; + } + }); + + element.keypress(function(e) { + signal.signal(consts.KEYSTROKES, [{where: consts.KEYSTROKE_WHERE_TEXTANALYSIS, keys:[e.which]}]); + if (e.which == 13) { + if(!suggestionChange) { + text = element.val(); + element.val(""); + analyzeSentence(text, true); + } else { + suggestionChange = false; + } + return false; + } + + if (e.which == 37) {//RIGHT + $('body').scrollLeft(0); + e.stopPropagation(); + return false; + } + if (e.which == 39) { //LEFT + $('body').scrollLeft(0); + e.stopPropagation(); + return false; + } + }); + + if ('oninput' in document.documentElement) { + element.on('input', function(e) { + text = element.val(); + analyzeSentence(text, false); + }); + } else { + console.log('textanalysis.ui: fallback to polling'); + window.setInterval(function() { + if (element.val() != text) { + if (text.length * 8 > 500) { + element.css('width', text.length * 8 + 20); + } + // text changed + text = element.val(); + analyzeSentence(text, false); + suggestionChange = false; + } + }, 50); + } + } +}; +}); // define diff --git a/src/util.js b/src/util.js new file mode 100644 index 00000000..3113474b --- /dev/null +++ b/src/util.js @@ -0,0 +1,52 @@ +"use strict" + +define(function() { + + function set_from_array(a) { + var ret = {}; + for (var k = 0; k < a.length; ++k) { + ret[a[k]] = 1; + } + return ret; + } + + function set_from_object(o) { + var ret = {} + for ( var k in o) { + ret[k] = 1; + } + return ret; + } + + function set_diff(sa, sb) { + var ret = { + a_b : [], + b_a : [] + }; + var i; + for (i in sa) { + if (!(i in sb)) { + ret.a_b.push(i); + } + } + for (i in sb) { + if (!(i in sa)) { + ret.b_a.push(i); + } + } + return ret; + } + + function array_diff(aa, ab) { + var sa = set_from_array(aa); + var sb = set_from_array(ab); + return set_diff(sa, sb); + } + + return { + set_from_array : set_from_array, + set_from_object : set_from_object, + set_diff : set_diff, + array_diff : array_diff + }; +}); diff --git a/src/view/edge_info.js b/src/view/edge_info.js new file mode 100644 index 00000000..9652839c --- /dev/null +++ b/src/view/edge_info.js @@ -0,0 +1,36 @@ + +define(['view/internal'], +function(internal) { + +var delete_button = internal.edit_tab.get('edge', '#deleteedge'), + delete_callback = undefined; + +delete_button.on('click', function() { + if (delete_callback) { + delete_callback(); + } +}); + +function show(link) +{ + internal.edit_tab.show('edge'); + internal.edit_tab.get('edge', '#edgetitle').html(link.name); +} + +function hide() +{ + internal.edit_tab.hide(); +} + +function on_delete(f) +{ + delete_callback = f; +} + +return { + show: show, + hide: hide, + on_delete: on_delete, +}; +}); +"use strict" diff --git a/src/view/helpers.js b/src/view/helpers.js new file mode 100644 index 00000000..be0e289e --- /dev/null +++ b/src/view/helpers.js @@ -0,0 +1,78 @@ +"use strict" + +define(function() { +function customColor(type) { + var color; + switch (type) { + case "person": + color = '#FCB924'; + break; + case "project": + color = '#009DDC'; + break; + case "skill": + color = '#62BB47'; + break; + case "deliverable": + color = '#202020'; + break; + case "objective": + color = '#933E99'; + break; + case "empty": + color = "#080808"; + break; + case "chainlink": + color = "#fff"; + break; + case "bubble": + color = "rgba(0,0,0,0.2)"; + break; + default: + console.log('bug: unknown type ' + type); + color = '#080808'; + break; + } + return color; +} + +function customSize(type) { + var size; + switch (type) { + case "person": + size = 12; + break; + case "project": + size = 12; + break; + case "skill": + size = 12; + break; + case "deliverable": + size = 12; + break; + case "objective": + size = 12; + break; + case "empty": + size = 9; + break; + case "chainlink": + size = 8; + break; + case "bubble": + size = 180; + break; + default: + size = 9; + break; + } + return size; +} + + +return { + customSize: customSize, + customColor: customColor, +}; +}); diff --git a/src/view/internal.js b/src/view/internal.js new file mode 100644 index 00000000..bc18bd75 --- /dev/null +++ b/src/view/internal.js @@ -0,0 +1,14 @@ +define(['view/tab'], +function(tab) { + +var EDGE_INFO_SELECTOR = '.edge_info', + NODE_INFO_SELECTOR = '.info', + edit_tab = new tab.Tab({edge: EDGE_INFO_SELECTOR, node: NODE_INFO_SELECTOR}); + +return { + EDGE_INFO_SELECTOR: EDGE_INFO_SELECTOR, + NODE_INFO_SELECTOR: NODE_INFO_SELECTOR, + edit_tab: edit_tab, +}; + +}); diff --git a/src/view/node_info.js b/src/view/node_info.js new file mode 100644 index 00000000..d455b366 --- /dev/null +++ b/src/view/node_info.js @@ -0,0 +1,63 @@ +define(['jquery', 'view/helpers', 'view/internal'], +function($, view_helpers, internal) { + +function show(d) { + internal.edit_tab.show('node'); + + if (d.type === "deliverable") { + $('.info').html('Name: ' + d.id + '<br/><form id="editbox"><label>Type:</label><select id="edittype"><option value="person">Person</option><option value="project">Project</option><option value="skill">Skill</option><option value="deliverable">Deliverable</option><option value="objective">Objective</option></select><br/><label>Status</label><select id="editstatus"><option value="waiting">Waiting</option><option value="current">Current</option><option value="done">Done</option></select><br/><label>Start date:</label><input id="editstartdate"/></br><label>End date:</label><input id="editenddate"/></br><button>Save</button><button id="deletenode">Delete</button></form>'); + } else if(d.type=== "chainlink"){ + $('.info').html('Name: ' + d.id + '<br/><form id="editbox"><button>Save</button><button id="deletenode">Delete</button></form>'); + }else{ + $('.info').html('Name: ' + d.id + '<br/><form id="editbox"><label>Type:</label><select id="edittype"><option value="person">Person</option><option value="project">Project</option><option value="skill">Skill</option><option value="deliverable">Deliverable</option><option value="objective">Objective</option></select><br/><label>URL:</label><input id="editurl"/><br/><button>Save</button><button id="deletenode">Delete</button></form>'); + } + + $('.info').css("border-color", view_helpers.customColor(d.type)); + + $("#editenddate").datepicker({ + inline: true, + showOtherMonths: true, + dayNamesMin: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + }); + + $("#editstartdate").datepicker({ + inline: true, + showOtherMonths: true, + dayNamesMin: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + }); + + $('#editdescription').val(d.type); + + $('#edittype').val(d.type); + + $('#editurl').val(d.url); + + if (d.type === "deliverable") { + $('#editstartdate').val(d.start); + $('#editenddate').val(d.end); + } +} + +function hide() +{ + internal.edit_tab.hide(); +} + +function on_submit(f) +{ + internal.edit_tab.get('node', "#editbox").submit(f); +} + +function on_delete(f) +{ + internal.edit_tab.get('node', "#deletenode").click(f); +} + +return { + show: show, + hide: hide, + on_submit: on_submit, + on_delete: on_delete, +}; + +}); diff --git a/src/view/tab.js b/src/view/tab.js new file mode 100644 index 00000000..97ba1351 --- /dev/null +++ b/src/view/tab.js @@ -0,0 +1,57 @@ +"use strict" + +define(['jquery'], +function($) { + +function Tab(dict) { + var k, + selector = {}, + name = []; + + for (k in dict) { + if (dict.hasOwnProperty(k) == false) { + continue; + } + selector[k] = dict[k]; + name.push(k); + } + this._selector = selector; + this._name = name; +} + +Tab.prototype.show = function(shown_name) { + var i, + name, + element; + + for (i = 0 ; i < this._name.length ; ++i) { + name = this._name[i]; + element = $(this._selector[name]); + if (name === shown_name) { + element.fadeIn(300); + } else { + element.hide(); + } + } +} + +Tab.prototype.hide = function() { + var i; + + for (i = 0 ; i < this._name.length ; ++i) { + $(this._selector[this._name[i]]).fadeOut(300); + } +} + +Tab.prototype.get = function(name, sel) { + // selector concatenation + var e = $(this._selector[name] + ' ' + sel); + + return e; +} + +return { + Tab: Tab +}; + +}); diff --git a/src/view/timeline.js b/src/view/timeline.js new file mode 100644 index 00000000..b7443551 --- /dev/null +++ b/src/view/timeline.js @@ -0,0 +1,183 @@ +"use strict" + +function checkSwitch(checkswitch) { + + if (checkswitch.checked) { + vis.selectAll(".gantt").remove(); + $('.missingdates').fadeOut(300); + scrollValue = $('body').scrollLeft(); + + $('body').scrollLeft(0); + graphstate = "GRAPH"; + graph.update(); + + $('.status').fadeOut(600); + + //boxedin=false; + + } else { + ganttTimer=0; + $('.missingdates').fadeIn(300); + + graph.recenterZoom(); + + $('body').scrollLeft(scrollValue); + + graphstate = "GANTT"; + + graph.update(); + + $('.status').fadeIn(600); + + + initAxis(); + + //boxedin=true; + } + +} + +///For some reason JQuery's $('body').scroll never worked, so I found something else. +$('body').bind('DOMMouseScroll', function(e){ + if(graphstate==="GANTT"){ + if(e.originalEvent.detail !== 0) { + $('.overlay').hide(); + }else{ + $('.overlay').show(); + } + }else{ + return false; + } + }); + + //IE, Opera, Safari + $('body').bind('mousewheel', function(e){ + if(graphstate==="GANTT"){ + if(e.originalEvent.wheelDelta !== 0) { + $('.overlay').hide(); + }else{ + $('.overlay').show(); + var left = $('body').offset().left; + $('.overlay').scrollLeft(left); + } + }else{ + return false; + } + }); + + + + +function initAxis() { + var bar_height = 20; + var row_height = bar_height + 10; + var vertical_padding = 150 + var bar_start_offset = 40; + var h = 15 * row_height + vertical_padding; + + + var min = deliverables[0].startdate; + var max = deliverables[0].enddate; + + for (var i = 0; i < deliverables.length; i++) { + var deliv = deliverables[i]; + if (min > deliv.startdate) min = deliv.startdate; + if (end < deliv.enddate) end = deliv.enddate; + } + min = new Date(min); ///with min + max = new Date(max); + console.log("min "+min+" max "+max); + + ///update interval + var timeDiff = Math.abs(max.getTime() - min.getTime()); + var diffDays = timeDiff / (1000 * 3600 * 24); + var w = Math.round(diffDays * 15 - 100); + graphinterval = w / diffDays; + + + + var svg=vis; + vis.attr("width",w); + + var paddingLeft = 150; + var paddingTop = 120; + + var xScale = d3.time.scale() + .domain([min, max]).nice() + .range([paddingLeft, w]); + + var xAxis = d3.svg.axis() + .scale(xScale) + .orient("bottom"); + + // Lines + var line = svg.append("g") + .selectAll("line") + .data(xScale.ticks(40)) + .enter().append("line") + .attr("x1", xScale) + .attr("x2", xScale) + .attr("y1", paddingTop + 30) + .attr("y2", h - 50) + .attr("class", "gantt") + .style("stroke", "#ccc"); + + var y = function (i) { + return i * row_height + paddingTop + bar_start_offset; + } + + var labelY = function (i) { + return i * row_height + paddingTop + bar_start_offset + 13; + } + + // Company bars + var bar = svg.selectAll("rect") + .data(function (d) { + return Math.random() * 5; + }); + + bar.enter().append("rect") + .attr("y", -100) + .attr("x", -1000) + .attr("width", 100) + .attr("height", bar_height) + .attr("class", "company-bar gantt") + .on("mouseover", function (d) { + d3.select(this).style("fill", "#F5AF00"); + getCompanyData(String(d.uid)) + }) + .on("mouseout", function () { + d3.select(this).style("fill", "#fc0"); + }); + + + + var label = svg.selectAll("text") + .data(deliverables, function (key) { + return key.id + });; + + label.enter().append("text") + .attr("class", "bar-label gantt") + // .attr("text-anchor","end") + .attr("x", paddingLeft - 10) + .attr("y", function (d, i) { + return labelY(i); + }) + .text(function (d) { + }); + + + + // Bottom Axis + var btmAxis = svg.append("g") + .attr("transform", "translate(0," + (h - 25) + ")") + .attr("class", "axis gantt") + .call(xAxis); + + // Top Axis + var topAxis = svg.append("g") + .attr("transform", "translate(0," + paddingTop + ")") + .attr("class", "axis gantt") + .call(xAxis); +} diff --git a/src/view/view.js b/src/view/view.js new file mode 100644 index 00000000..95bfc09b --- /dev/null +++ b/src/view/view.js @@ -0,0 +1,12 @@ +"use strict" + +define(['view/node_info', 'view/edge_info', 'view/internal'], +function(view_node_info, view_edge_info, view_internal) { +return { + 'node_info': view_node_info, + 'edge_info': view_edge_info, + 'hide': function() { + view_internal.edit_tab.hide(); + }, +}; +}); |
