Last active
September 12, 2016 10:12
-
-
Save nitaku/fdb442d5ad3a04f62b9b8ecd5b893ec0 to your computer and use it in GitHub Desktop.
Path network editor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
global class AppView extends View | |
constructor: (conf) -> | |
super(conf) | |
graph = new Graph | |
keyboard = new Keyboard | |
keyboard.on 'delete_down', () -> | |
graph.delete_selected() | |
nv = new NetworkView | |
graph: graph | |
parent: this | |
tool = new CurrentTool | |
graph: graph | |
view: nv | |
new Toolbar | |
current_tool: tool | |
parent: this | |
prepend: true | |
# default tool is arrow | |
tool.set 'Arrow' | |
a = graph.new_node(100,100) | |
b = graph.new_node(100,200) | |
graph.new_link(a,b) | |
c = graph.new_node(200,200) | |
graph.new_link(b,c) | |
d = graph.new_node(200,100) | |
graph.new_link(c,d) | |
graph.new_link(d,a) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.AppView { | |
display: flex; | |
flex-direction: column; | |
width: 100%; | |
height: 100%; | |
} | |
.AppView .NetworkView { | |
flex-grow: 1; | |
height: 0; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Generated by CoffeeScript 1.10.0 | |
(function() { | |
var AppView, | |
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, | |
hasProp = {}.hasOwnProperty; | |
global(AppView = (function(superClass) { | |
extend(AppView, superClass); | |
function AppView(conf) { | |
var a, b, c, d, graph, keyboard, nv, tool; | |
AppView.__super__.constructor.call(this, conf); | |
graph = new Graph; | |
keyboard = new Keyboard; | |
keyboard.on('delete_down', function() { | |
return graph.delete_selected(); | |
}); | |
nv = new NetworkView({ | |
graph: graph, | |
parent: this | |
}); | |
tool = new CurrentTool({ | |
graph: graph, | |
view: nv | |
}); | |
new Toolbar({ | |
current_tool: tool, | |
parent: this, | |
prepend: true | |
}); | |
tool.set('Arrow'); | |
a = graph.new_node(100, 100); | |
b = graph.new_node(100, 200); | |
graph.new_link(a, b); | |
c = graph.new_node(200, 200); | |
graph.new_link(b, c); | |
d = graph.new_node(200, 100); | |
graph.new_link(c, d); | |
graph.new_link(d, a); | |
} | |
return AppView; | |
})(View)); | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
global class ArrowTool | |
constructor: (conf) -> | |
@graph = conf.graph | |
@view = conf.view | |
@listeners = [] | |
@listeners.push @view.on 'action_on_node', @select | |
@listeners.push @view.on 'action_on_link', @select | |
destroy: () -> | |
# unbind all listeners | |
@listeners.forEach (l) => | |
@view.on l, null | |
select: (d) => | |
@graph.select d |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Generated by CoffeeScript 1.10.0 | |
(function() { | |
var ArrowTool, | |
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; | |
global(ArrowTool = (function() { | |
function ArrowTool(conf) { | |
this.select = bind(this.select, this); | |
this.graph = conf.graph; | |
this.view = conf.view; | |
this.listeners = []; | |
this.listeners.push(this.view.on('action_on_node', this.select)); | |
this.listeners.push(this.view.on('action_on_link', this.select)); | |
} | |
ArrowTool.prototype.destroy = function() { | |
return this.listeners.forEach((function(_this) { | |
return function(l) { | |
return _this.view.on(l, null); | |
}; | |
})(this)); | |
}; | |
ArrowTool.prototype.select = function(d) { | |
return this.graph.select(d); | |
}; | |
return ArrowTool; | |
})()); | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
global class CurrentTool extends EventSource | |
constructor: (conf) -> | |
super | |
events: ['change'] | |
@graph = conf.graph | |
@view = conf.view | |
set: (name) => | |
if name isnt @tool_name | |
@tool_name = name | |
# destroy old tool, if any | |
if @tool? | |
@tool.destroy() | |
@tool = new window[@tool_name+'Tool'] | |
graph: @graph | |
view: @view | |
@trigger 'change' | |
return this | |
get: () => | |
return @tool_name |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Generated by CoffeeScript 1.10.0 | |
(function() { | |
var CurrentTool, | |
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, | |
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, | |
hasProp = {}.hasOwnProperty; | |
global(CurrentTool = (function(superClass) { | |
extend(CurrentTool, superClass); | |
function CurrentTool(conf) { | |
this.get = bind(this.get, this); | |
this.set = bind(this.set, this); | |
CurrentTool.__super__.constructor.call(this, { | |
events: ['change'] | |
}); | |
this.graph = conf.graph; | |
this.view = conf.view; | |
} | |
CurrentTool.prototype.set = function(name) { | |
if (name !== this.tool_name) { | |
this.tool_name = name; | |
if (this.tool != null) { | |
this.tool.destroy(); | |
} | |
this.tool = new window[this.tool_name + 'Tool']({ | |
graph: this.graph, | |
view: this.view | |
}); | |
this.trigger('change'); | |
} | |
return this; | |
}; | |
CurrentTool.prototype.get = function() { | |
return this.tool_name; | |
}; | |
return CurrentTool; | |
})(EventSource)); | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# make a function global | |
window.global = (f) -> window[f.name] = f | |
# object able to trigger events | |
global class EventSource | |
# instantiate a new EventSource with the given event types | |
constructor: (config) -> | |
@_dispatcher = d3.dispatch(config.events...) | |
# auto incrementing ids are used to avoid overwriting listeners | |
@next_id = 0 | |
# register a callback on the given event type, with an optional namespace | |
# syntax: event_type.namespace | |
# null as callback unbinds the listener | |
on: (event_type_ns, callback) -> | |
splitted_event_type_ns = event_type_ns.split('.') | |
event_type = splitted_event_type_ns[0] | |
if splitted_event_type_ns.length > 1 | |
namespace = splitted_event_type_ns[1] | |
else | |
# use an automatic ID | |
namespace = @next_id | |
@next_id += 1 | |
event_type_full = event_type + '.' + namespace | |
@_dispatcher.on event_type_full, callback | |
# callers who want to unbind or overwrite the created listener have to store the full event type | |
return event_type_full | |
# trigger an event of the given type and pass the given args | |
trigger: (event_type, args...) -> | |
@_dispatcher.apply(event_type, this, args) | |
return this | |
# GUI component | |
global class View extends EventSource | |
constructor: (conf) -> | |
super(conf) | |
# div is default | |
if not conf.tag? | |
conf.tag = 'div' | |
# both DOM and d3 element references | |
@el = document.createElement(conf.tag) | |
@d3el = d3.select(@el) | |
# set a CSS class equal to the class name of the view | |
# WARNING this is not supported on IE | |
# if needed, this polyfill can be used: | |
# https://github.com/JamesMGreene/Function.name | |
@d3el.classed this.constructor.name, true | |
# automatically append this view to the given parent, if any | |
if conf.parent? | |
@append_to(conf.parent, conf.prepend) | |
# append this view to a parent view or node | |
append_to: (parent, prepend) -> | |
if parent.el? # Backbone-style view | |
p_el = parent.el | |
else # selector or d3 selection or dom node | |
p_el = d3.select(parent).node() | |
if prepend | |
p_el.insertBefore @el, p_el.firstChild | |
else | |
p_el.appendChild @el | |
# recompute width and height | |
recompute_size: () -> | |
@width = @el.getBoundingClientRect().width | |
@height = @el.getBoundingClientRect().height | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Generated by CoffeeScript 1.10.0 | |
(function() { | |
var EventSource, View, | |
slice = [].slice, | |
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, | |
hasProp = {}.hasOwnProperty; | |
window.global = function(f) { | |
return window[f.name] = f; | |
}; | |
global(EventSource = (function() { | |
function EventSource(config) { | |
this._dispatcher = d3.dispatch.apply(d3, config.events); | |
this.next_id = 0; | |
} | |
EventSource.prototype.on = function(event_type_ns, callback) { | |
var event_type, event_type_full, namespace, splitted_event_type_ns; | |
splitted_event_type_ns = event_type_ns.split('.'); | |
event_type = splitted_event_type_ns[0]; | |
if (splitted_event_type_ns.length > 1) { | |
namespace = splitted_event_type_ns[1]; | |
} else { | |
namespace = this.next_id; | |
this.next_id += 1; | |
} | |
event_type_full = event_type + '.' + namespace; | |
this._dispatcher.on(event_type_full, callback); | |
return event_type_full; | |
}; | |
EventSource.prototype.trigger = function() { | |
var args, event_type; | |
event_type = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; | |
this._dispatcher.apply(event_type, this, args); | |
return this; | |
}; | |
return EventSource; | |
})()); | |
global(View = (function(superClass) { | |
extend(View, superClass); | |
function View(conf) { | |
View.__super__.constructor.call(this, conf); | |
if (conf.tag == null) { | |
conf.tag = 'div'; | |
} | |
this.el = document.createElement(conf.tag); | |
this.d3el = d3.select(this.el); | |
this.d3el.classed(this.constructor.name, true); | |
if (conf.parent != null) { | |
this.append_to(conf.parent, conf.prepend); | |
} | |
} | |
View.prototype.append_to = function(parent, prepend) { | |
var p_el; | |
if (parent.el != null) { | |
p_el = parent.el; | |
} else { | |
p_el = d3.select(parent).node(); | |
} | |
if (prepend) { | |
return p_el.insertBefore(this.el, p_el.firstChild); | |
} else { | |
return p_el.appendChild(this.el); | |
} | |
}; | |
View.prototype.recompute_size = function() { | |
this.width = this.el.getBoundingClientRect().width; | |
return this.height = this.el.getBoundingClientRect().height; | |
}; | |
return View; | |
})(EventSource)); | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
global class Graph extends EventSource | |
constructor: (conf) -> | |
super | |
events: ['change'] | |
@nodes = {} | |
@links = {} | |
@next_node_id = 0 | |
@next_link_id = 0 | |
@selection = null | |
get_nodes: () -> | |
return Object.keys(@nodes).map (k) => @nodes[k] | |
get_links: () -> | |
return Object.keys(@links).map (k) => @links[k] | |
new_node: (x,y) -> | |
n = { | |
type: 'node', | |
id: @next_node_id, | |
x: x, | |
y: y, | |
out_links: {}, | |
in_links: {} | |
} | |
@nodes[n.id] = n | |
@next_node_id += 1 | |
@trigger 'change' | |
return n | |
new_link: (source, target) -> | |
l = { | |
type: 'link', | |
id: @next_link_id, | |
source: source, | |
target: target, | |
d: Math.sqrt( Math.pow(target.x-source.x, 2) + Math.pow(target.y-source.y, 2) ) | |
} | |
@links[l.id] = l | |
@next_link_id += 1 | |
# reverse pointers from nodes | |
l.source.out_links[l.id] = l | |
l.target.in_links[l.id] = l | |
@trigger 'change' | |
return l | |
select: (d) -> | |
if @selection? | |
delete @selection.selected | |
@selection = null | |
d.selected = true | |
@selection = d | |
@trigger 'change' | |
return this | |
delete_node_id: (id, silent) -> | |
d = @nodes[id] | |
@delete_node d | |
if not silent | |
@trigger 'change' | |
delete_node: (d, silent) -> | |
Object.keys(d.out_links).forEach (k) => @delete_link_id(k, true) | |
Object.keys(d.in_links).forEach (k) => @delete_link_id(k, true) | |
delete @nodes[d.id] | |
if not silent | |
@trigger 'change' | |
delete_link_id: (id, silent) -> | |
l = @links[id] | |
@delete_link l | |
if not silent | |
@trigger 'change' | |
delete_link: (l, silent) -> | |
delete @links[l.id] | |
delete l.source.out_links[l.id] | |
delete l.target.in_links[l.id] | |
if not silent | |
@trigger 'change' | |
delete_selected: () -> | |
if not @selection? | |
return false | |
if @selection.type is 'node' and @selection.id of @nodes | |
@delete_node_id @selection.id, true | |
else if @selection.type is 'link' and @selection.id of @links | |
@delete_link_id @selection.id, true | |
@trigger 'change' | |
return true | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Generated by CoffeeScript 1.10.0 | |
(function() { | |
var Graph, | |
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, | |
hasProp = {}.hasOwnProperty; | |
global(Graph = (function(superClass) { | |
extend(Graph, superClass); | |
function Graph(conf) { | |
Graph.__super__.constructor.call(this, { | |
events: ['change'] | |
}); | |
this.nodes = {}; | |
this.links = {}; | |
this.next_node_id = 0; | |
this.next_link_id = 0; | |
this.selection = null; | |
} | |
Graph.prototype.get_nodes = function() { | |
return Object.keys(this.nodes).map((function(_this) { | |
return function(k) { | |
return _this.nodes[k]; | |
}; | |
})(this)); | |
}; | |
Graph.prototype.get_links = function() { | |
return Object.keys(this.links).map((function(_this) { | |
return function(k) { | |
return _this.links[k]; | |
}; | |
})(this)); | |
}; | |
Graph.prototype.new_node = function(x, y) { | |
var n; | |
n = { | |
type: 'node', | |
id: this.next_node_id, | |
x: x, | |
y: y, | |
out_links: {}, | |
in_links: {} | |
}; | |
this.nodes[n.id] = n; | |
this.next_node_id += 1; | |
this.trigger('change'); | |
return n; | |
}; | |
Graph.prototype.new_link = function(source, target) { | |
var l; | |
l = { | |
type: 'link', | |
id: this.next_link_id, | |
source: source, | |
target: target, | |
d: Math.sqrt(Math.pow(target.x - source.x, 2) + Math.pow(target.y - source.y, 2)) | |
}; | |
this.links[l.id] = l; | |
this.next_link_id += 1; | |
l.source.out_links[l.id] = l; | |
l.target.in_links[l.id] = l; | |
this.trigger('change'); | |
return l; | |
}; | |
Graph.prototype.select = function(d) { | |
if (this.selection != null) { | |
delete this.selection.selected; | |
this.selection = null; | |
} | |
d.selected = true; | |
this.selection = d; | |
this.trigger('change'); | |
return this; | |
}; | |
Graph.prototype.delete_node_id = function(id, silent) { | |
var d; | |
d = this.nodes[id]; | |
this.delete_node(d); | |
if (!silent) { | |
return this.trigger('change'); | |
} | |
}; | |
Graph.prototype.delete_node = function(d, silent) { | |
Object.keys(d.out_links).forEach((function(_this) { | |
return function(k) { | |
return _this.delete_link_id(k, true); | |
}; | |
})(this)); | |
Object.keys(d.in_links).forEach((function(_this) { | |
return function(k) { | |
return _this.delete_link_id(k, true); | |
}; | |
})(this)); | |
delete this.nodes[d.id]; | |
if (!silent) { | |
return this.trigger('change'); | |
} | |
}; | |
Graph.prototype.delete_link_id = function(id, silent) { | |
var l; | |
l = this.links[id]; | |
this.delete_link(l); | |
if (!silent) { | |
return this.trigger('change'); | |
} | |
}; | |
Graph.prototype.delete_link = function(l, silent) { | |
delete this.links[l.id]; | |
delete l.source.out_links[l.id]; | |
delete l.target.in_links[l.id]; | |
if (!silent) { | |
return this.trigger('change'); | |
} | |
}; | |
Graph.prototype.delete_selected = function() { | |
if (this.selection == null) { | |
return false; | |
} | |
if (this.selection.type === 'node' && this.selection.id in this.nodes) { | |
this.delete_node_id(this.selection.id, true); | |
} else if (this.selection.type === 'link' && this.selection.id in this.links) { | |
this.delete_link_id(this.selection.id, true); | |
} | |
this.trigger('change'); | |
return true; | |
}; | |
return Graph; | |
})(EventSource)); | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
new AppView | |
parent: 'body' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body, html { | |
padding: 0; | |
margin: 0; | |
height: 100%; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Path Network Editor</title> | |
<link type="text/css" href="index.css" rel="stylesheet"/> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script src="https://d3js.org/d3-selection-multi.v0.4.min.js"></script> | |
<script src="esv.js"></script> | |
<script src="Graph.js"></script> | |
<script src="CurrentTool.js"></script> | |
<script src="Keyboard.js"></script> | |
<script src="AppView.js"></script> | |
<link type="text/css" href="AppView.css" rel="stylesheet"/> | |
<script src="Toolbar.js"></script> | |
<link type="text/css" href="Toolbar.css" rel="stylesheet"/> | |
<script src="NetworkView.js"></script> | |
<link type="text/css" href="NetworkView.css" rel="stylesheet"/> | |
<script src="MultilineTool.js"></script> | |
<script src="ArrowTool.js"></script> | |
</head> | |
<body> | |
<script src="index.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Generated by CoffeeScript 1.10.0 | |
(function() { | |
new AppView({ | |
parent: 'body' | |
}); | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# event-emitting keyboard with no repeating keypresses | |
global class Keyboard extends EventSource | |
constructor: () -> | |
# each key has a 'down' and an 'up' event | |
keys = ['delete','ctrl','shift'] | |
events = [] | |
keys.forEach (d) -> | |
events.push "#{d}_down" | |
events.push "#{d}_up" | |
super | |
events: events | |
pressed_keys = {} | |
document.onkeydown = (e) => | |
# console.log e.keyCode | |
key = @keyname(e.keyCode) | |
if key? and key not of pressed_keys | |
@trigger key + '_down' | |
pressed_keys[key] = true | |
document.onkeyup = (e) => | |
# console.log e.keyCode | |
key = @keyname(e.keyCode) | |
if key? and key of pressed_keys | |
@trigger key + '_up' | |
delete pressed_keys[key] | |
keyname: (keyCode) -> | |
switch keyCode | |
when 46 then 'delete' | |
when 17 then 'ctrl' | |
when 16 then 'shift' | |
else null | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Generated by CoffeeScript 1.10.0 | |
(function() { | |
var Keyboard, | |
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, | |
hasProp = {}.hasOwnProperty; | |
global(Keyboard = (function(superClass) { | |
extend(Keyboard, superClass); | |
function Keyboard() { | |
var events, keys, pressed_keys; | |
keys = ['delete', 'ctrl', 'shift']; | |
events = []; | |
keys.forEach(function(d) { | |
events.push(d + "_down"); | |
return events.push(d + "_up"); | |
}); | |
Keyboard.__super__.constructor.call(this, { | |
events: events | |
}); | |
pressed_keys = {}; | |
document.onkeydown = (function(_this) { | |
return function(e) { | |
var key; | |
key = _this.keyname(e.keyCode); | |
if ((key != null) && !(key in pressed_keys)) { | |
_this.trigger(key + '_down'); | |
return pressed_keys[key] = true; | |
} | |
}; | |
})(this); | |
document.onkeyup = (function(_this) { | |
return function(e) { | |
var key; | |
key = _this.keyname(e.keyCode); | |
if ((key != null) && key in pressed_keys) { | |
_this.trigger(key + '_up'); | |
return delete pressed_keys[key]; | |
} | |
}; | |
})(this); | |
} | |
Keyboard.prototype.keyname = function(keyCode) { | |
switch (keyCode) { | |
case 46: | |
return 'delete'; | |
case 17: | |
return 'ctrl'; | |
case 16: | |
return 'shift'; | |
default: | |
return null; | |
} | |
}; | |
return Keyboard; | |
})(EventSource)); | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# FIXME if the source node of a preview line is deleted, this goes into an inconsistent state | |
# maybe its better to have a boolean for the drawing state and methods to activate/deactivate it | |
# we could then register a listener on a change event of the graph to run a check for the active node and deactivate the drawing mode if it is not found | |
global class MultilineTool | |
constructor: (conf) -> | |
@graph = conf.graph | |
@view = conf.view | |
@last_point = null | |
@listeners = [] | |
@listeners.push @view.on 'hover_on_point', @hover_on_point | |
@listeners.push @view.on 'action_on_point', @new_point | |
@listeners.push @view.on 'action_on_node', @touch_node | |
@listeners.push @view.on 'action_on_link', @touch_link | |
destroy: () -> | |
# unbind all listeners | |
@listeners.forEach (l) => | |
@view.on l, null | |
# clean the preview line | |
if @preview_line? | |
@preview_line.remove() | |
new_point: (x, y) => | |
nn = @graph.new_node x, y | |
# continue the path, if any | |
if @last_point? | |
@graph.new_link @last_point, nn | |
@last_point = nn | |
return nn | |
touch_node: (n, x, y) => | |
# touching an already existing node closes the path onto it | |
if @last_point? | |
if @last_point isnt n | |
# avoid linking a node to itself | |
@graph.new_link @last_point, n | |
@last_point = null | |
else | |
@last_point = n | |
touch_link: (l, x, y) => | |
# create a new point on the link using the closest point to the click | |
A = {x: l.source.x, y: l.source.y} | |
B = {x: l.target.x, y: l.target.y} | |
P = {x: x, y: y} | |
CP = get_closest_point(A, B, P) | |
must_close = @last_point? | |
nn = @new_point CP.x, CP.y | |
# close the path, if any | |
if must_close | |
@last_point = null | |
# split the link | |
@graph.delete_link l | |
@graph.new_link l.source, nn | |
@graph.new_link nn, l.target | |
hover_on_point: (x, y) => | |
# FIXME this can be implemented in a better way | |
if @last_point? | |
if not @preview_line? | |
@preview_line = @view.tools_overlay.append 'line' | |
@preview_line | |
.attrs | |
class: 'preview_line' | |
x1: (d) => @last_point.x | |
y1: (d) => @last_point.y | |
x2: x | |
y2: y | |
else | |
if @preview_line? | |
@preview_line.remove() | |
@preview_line = null | |
# private function to find the closest point on a segment | |
get_closest_point = (A, B, P) -> | |
a_to_p = [P.x - A.x, P.y - A.y] # vector A->P | |
a_to_b = [B.x - A.x, B.y - A.y] # vector A->B | |
atb2 = a_to_b[0]*a_to_b[0] + a_to_b[1]*a_to_b[1] # squared magnitude of a_to_b | |
atp_dot_atb = a_to_p[0]*a_to_b[0] + a_to_p[1]*a_to_b[1] # The dot product of a_to_p and a_to_b | |
t = atp_dot_atb / atb2 # The normalized "distance" from a to your closest point | |
C = {x: A.x + a_to_b[0]*t, y: A.y + a_to_b[1]*t} # Add the distance to A, moving towards B | |
return C |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Generated by CoffeeScript 1.10.0 | |
(function() { | |
var MultilineTool, get_closest_point, | |
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; | |
global(MultilineTool = (function() { | |
function MultilineTool(conf) { | |
this.hover_on_point = bind(this.hover_on_point, this); | |
this.touch_link = bind(this.touch_link, this); | |
this.touch_node = bind(this.touch_node, this); | |
this.new_point = bind(this.new_point, this); | |
this.graph = conf.graph; | |
this.view = conf.view; | |
this.last_point = null; | |
this.listeners = []; | |
this.listeners.push(this.view.on('hover_on_point', this.hover_on_point)); | |
this.listeners.push(this.view.on('action_on_point', this.new_point)); | |
this.listeners.push(this.view.on('action_on_node', this.touch_node)); | |
this.listeners.push(this.view.on('action_on_link', this.touch_link)); | |
} | |
MultilineTool.prototype.destroy = function() { | |
this.listeners.forEach((function(_this) { | |
return function(l) { | |
return _this.view.on(l, null); | |
}; | |
})(this)); | |
if (this.preview_line != null) { | |
return this.preview_line.remove(); | |
} | |
}; | |
MultilineTool.prototype.new_point = function(x, y) { | |
var nn; | |
nn = this.graph.new_node(x, y); | |
if (this.last_point != null) { | |
this.graph.new_link(this.last_point, nn); | |
} | |
this.last_point = nn; | |
return nn; | |
}; | |
MultilineTool.prototype.touch_node = function(n, x, y) { | |
if (this.last_point != null) { | |
if (this.last_point !== n) { | |
this.graph.new_link(this.last_point, n); | |
} | |
return this.last_point = null; | |
} else { | |
return this.last_point = n; | |
} | |
}; | |
MultilineTool.prototype.touch_link = function(l, x, y) { | |
var A, B, CP, P, must_close, nn; | |
A = { | |
x: l.source.x, | |
y: l.source.y | |
}; | |
B = { | |
x: l.target.x, | |
y: l.target.y | |
}; | |
P = { | |
x: x, | |
y: y | |
}; | |
CP = get_closest_point(A, B, P); | |
must_close = this.last_point != null; | |
nn = this.new_point(CP.x, CP.y); | |
if (must_close) { | |
this.last_point = null; | |
} | |
this.graph.delete_link(l); | |
this.graph.new_link(l.source, nn); | |
return this.graph.new_link(nn, l.target); | |
}; | |
MultilineTool.prototype.hover_on_point = function(x, y) { | |
if (this.last_point != null) { | |
if (this.preview_line == null) { | |
this.preview_line = this.view.tools_overlay.append('line'); | |
} | |
return this.preview_line.attrs({ | |
"class": 'preview_line', | |
x1: (function(_this) { | |
return function(d) { | |
return _this.last_point.x; | |
}; | |
})(this), | |
y1: (function(_this) { | |
return function(d) { | |
return _this.last_point.y; | |
}; | |
})(this), | |
x2: x, | |
y2: y | |
}); | |
} else { | |
if (this.preview_line != null) { | |
this.preview_line.remove(); | |
return this.preview_line = null; | |
} | |
} | |
}; | |
return MultilineTool; | |
})()); | |
get_closest_point = function(A, B, P) { | |
var C, a_to_b, a_to_p, atb2, atp_dot_atb, t; | |
a_to_p = [P.x - A.x, P.y - A.y]; | |
a_to_b = [B.x - A.x, B.y - A.y]; | |
atb2 = a_to_b[0] * a_to_b[0] + a_to_b[1] * a_to_b[1]; | |
atp_dot_atb = a_to_p[0] * a_to_b[0] + a_to_p[1] * a_to_b[1]; | |
t = atp_dot_atb / atb2; | |
C = { | |
x: A.x + a_to_b[0] * t, | |
y: A.y + a_to_b[1] * t | |
}; | |
return C; | |
}; | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
global class NetworkView extends View | |
constructor: (conf) -> | |
conf = {} if not conf? | |
conf.events = [ | |
'action_on_point', | |
'action_on_node', | |
'action_on_link', | |
'hover_on_point' | |
] | |
super(conf) | |
@graph = conf.graph | |
svg = @d3el.append 'svg' | |
# append a group for zoomable content | |
@zoomable_layer = svg.append 'g' | |
zoom = d3.zoom() | |
.scaleExtent([-Infinity,Infinity]) | |
.on 'zoom', () => | |
@current_zoom_t = d3.event.transform | |
@zoomable_layer | |
.attrs | |
transform: @current_zoom_t | |
@nodes_layer.selectAll '.node > *' | |
.attrs | |
transform: "scale(#{1/@current_zoom_t.k})" | |
@links_layer.selectAll '.link .label > *' | |
.attrs | |
transform: "scale(#{1/@current_zoom_t.k})" | |
svg.call zoom | |
@current_zoom_t = d3.zoomTransform svg | |
@links_layer = @zoomable_layer.append 'g' | |
@nodes_layer = @zoomable_layer.append 'g' | |
@tools_overlay = @zoomable_layer.append 'g' | |
@graph.on 'change', @redraw | |
svg.on 'click', () => | |
[x, y] = d3.mouse(@zoomable_layer.node()) | |
@trigger 'action_on_point', x, y | |
svg.on 'mousemove', () => | |
[x, y] = d3.mouse(@zoomable_layer.node()) | |
@trigger 'hover_on_point', x, y | |
redraw: () => | |
# nodes | |
nodes = @nodes_layer.selectAll '.node' | |
.data @graph.get_nodes(), (d) -> d.id | |
enter_nodes = nodes.enter().append 'g' | |
.attrs | |
class: 'node' | |
.on 'click', (d) => | |
[x, y] = d3.mouse(@zoomable_layer.node()) | |
@trigger 'action_on_node', d, x, y | |
d3.event.stopPropagation() | |
enter_nodes.append 'circle' | |
enter_nodes.append 'text' | |
all_nodes = enter_nodes.merge(nodes) | |
all_nodes.select 'circle' | |
.attrs | |
r: (d) -> 4 | |
transform: "scale(#{1/@current_zoom_t.k})" | |
all_nodes.select 'text' | |
.text (d) -> "(#{d3.format('.2f')(d.x)}, #{d3.format('.2f')(d.y)})" | |
.attrs | |
y: 16 | |
transform: "scale(#{1/@current_zoom_t.k})" | |
all_nodes | |
.classed 'selected', (d) -> d.selected | |
.attrs | |
transform: (d) -> "translate(#{d.x},#{d.y})" | |
nodes.exit() | |
.remove() | |
# links | |
links = @links_layer.selectAll '.link' | |
.data @graph.get_links(), (d) -> d.id | |
enter_links = links.enter().append 'g' | |
.attrs | |
class: 'link' | |
.on 'click', (d) => | |
[x, y] = d3.mouse(@zoomable_layer.node()) | |
@trigger 'action_on_link', d, x, y | |
d3.event.stopPropagation() | |
enter_links.append 'line' | |
.attrs | |
class: 'background' | |
enter_links.append 'line' | |
.attrs | |
class: 'foreground' | |
enter_labels = enter_links.append 'g' | |
.attrs | |
class: 'label' | |
enter_labels.append 'text' | |
all_links = enter_links.merge(links) | |
all_links.selectAll 'line' | |
.attrs | |
x1: (d) -> d.source.x | |
y1: (d) -> d.source.y | |
x2: (d) -> d.target.x | |
y2: (d) -> d.target.y | |
all_links.selectAll '.label' | |
.attrs | |
transform: (d) -> "translate(#{(d.source.x+d.target.x)/2},#{(d.source.y+d.target.y)/2})" | |
all_links.selectAll '.label text' | |
.text (d) -> d3.format('.2f')(d.d) | |
.attrs | |
dy: '0.35em' | |
transform: "scale(#{1/@current_zoom_t.k})" | |
all_links | |
.classed 'selected', (d) -> d.selected | |
links.exit() | |
.remove() | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.NetworkView { | |
display: flex; | |
flex-direction: column; | |
} | |
.NetworkView svg { | |
background: white; | |
flex-grow: 1; | |
height: 0; | |
} | |
.NetworkView .node { | |
/* help with node selection */ | |
stroke-width: 6; | |
stroke: transparent; | |
} | |
.NetworkView .node text, .NetworkView .link text { | |
font-family: sans-serif; | |
font-size: 10px; | |
text-anchor: middle; | |
stroke: none; | |
display: none; | |
text-shadow: -1px -1px white, -1px 1px white, 1px 1px white, 1px -1px white, -1px 0 white, 0 1px white, 1px 0 white, 0 -1px white; | |
pointer-events: none; | |
} | |
.NetworkView .selected.node text, .NetworkView .selected.link text { | |
display: inline; | |
} | |
.NetworkView .link *, .NetworkView .preview_line { | |
vector-effect: non-scaling-stroke; | |
} | |
.NetworkView .link .foreground { | |
stroke: black; | |
opacity: 0.3; | |
stroke-width: 2; | |
fill: none; | |
} | |
.NetworkView .link .background { | |
/* help with node selection */ | |
stroke: transparent; | |
stroke-width: 6; | |
fill: none; | |
} | |
.NetworkView .preview_line { | |
stroke: red; | |
opacity: 0.4; | |
stroke-width: 2; | |
fill: none; | |
pointer-events: none; | |
} | |
.NetworkView .selected.node { | |
fill: blue; | |
stroke: rgba(0,0,255,0.1); | |
} | |
.NetworkView .selected.link .foreground { | |
stroke: blue; | |
} | |
.NetworkView .selected.link .background { | |
stroke: rgba(0,0,255,0.1); | |
} | |
.NetworkView .selected.link text { | |
fill: blue; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Generated by CoffeeScript 1.10.0 | |
(function() { | |
var NetworkView, | |
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, | |
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, | |
hasProp = {}.hasOwnProperty; | |
global(NetworkView = (function(superClass) { | |
extend(NetworkView, superClass); | |
function NetworkView(conf) { | |
this.redraw = bind(this.redraw, this); | |
var svg, zoom; | |
if (conf == null) { | |
conf = {}; | |
} | |
conf.events = ['action_on_point', 'action_on_node', 'action_on_link', 'hover_on_point']; | |
NetworkView.__super__.constructor.call(this, conf); | |
this.graph = conf.graph; | |
svg = this.d3el.append('svg'); | |
this.zoomable_layer = svg.append('g'); | |
zoom = d3.zoom().scaleExtent([-Infinity, Infinity]).on('zoom', (function(_this) { | |
return function() { | |
_this.current_zoom_t = d3.event.transform; | |
_this.zoomable_layer.attrs({ | |
transform: _this.current_zoom_t | |
}); | |
_this.nodes_layer.selectAll('.node > *').attrs({ | |
transform: "scale(" + (1 / _this.current_zoom_t.k) + ")" | |
}); | |
return _this.links_layer.selectAll('.link .label > *').attrs({ | |
transform: "scale(" + (1 / _this.current_zoom_t.k) + ")" | |
}); | |
}; | |
})(this)); | |
svg.call(zoom); | |
this.current_zoom_t = d3.zoomTransform(svg); | |
this.links_layer = this.zoomable_layer.append('g'); | |
this.nodes_layer = this.zoomable_layer.append('g'); | |
this.tools_overlay = this.zoomable_layer.append('g'); | |
this.graph.on('change', this.redraw); | |
svg.on('click', (function(_this) { | |
return function() { | |
var ref, x, y; | |
ref = d3.mouse(_this.zoomable_layer.node()), x = ref[0], y = ref[1]; | |
return _this.trigger('action_on_point', x, y); | |
}; | |
})(this)); | |
svg.on('mousemove', (function(_this) { | |
return function() { | |
var ref, x, y; | |
ref = d3.mouse(_this.zoomable_layer.node()), x = ref[0], y = ref[1]; | |
return _this.trigger('hover_on_point', x, y); | |
}; | |
})(this)); | |
} | |
NetworkView.prototype.redraw = function() { | |
var all_links, all_nodes, enter_labels, enter_links, enter_nodes, links, nodes; | |
nodes = this.nodes_layer.selectAll('.node').data(this.graph.get_nodes(), function(d) { | |
return d.id; | |
}); | |
enter_nodes = nodes.enter().append('g').attrs({ | |
"class": 'node' | |
}).on('click', (function(_this) { | |
return function(d) { | |
var ref, x, y; | |
ref = d3.mouse(_this.zoomable_layer.node()), x = ref[0], y = ref[1]; | |
_this.trigger('action_on_node', d, x, y); | |
return d3.event.stopPropagation(); | |
}; | |
})(this)); | |
enter_nodes.append('circle'); | |
enter_nodes.append('text'); | |
all_nodes = enter_nodes.merge(nodes); | |
all_nodes.select('circle').attrs({ | |
r: function(d) { | |
return 4; | |
}, | |
transform: "scale(" + (1 / this.current_zoom_t.k) + ")" | |
}); | |
all_nodes.select('text').text(function(d) { | |
return "(" + (d3.format('.2f')(d.x)) + ", " + (d3.format('.2f')(d.y)) + ")"; | |
}).attrs({ | |
y: 16, | |
transform: "scale(" + (1 / this.current_zoom_t.k) + ")" | |
}); | |
all_nodes.classed('selected', function(d) { | |
return d.selected; | |
}).attrs({ | |
transform: function(d) { | |
return "translate(" + d.x + "," + d.y + ")"; | |
} | |
}); | |
nodes.exit().remove(); | |
links = this.links_layer.selectAll('.link').data(this.graph.get_links(), function(d) { | |
return d.id; | |
}); | |
enter_links = links.enter().append('g').attrs({ | |
"class": 'link' | |
}).on('click', (function(_this) { | |
return function(d) { | |
var ref, x, y; | |
ref = d3.mouse(_this.zoomable_layer.node()), x = ref[0], y = ref[1]; | |
_this.trigger('action_on_link', d, x, y); | |
return d3.event.stopPropagation(); | |
}; | |
})(this)); | |
enter_links.append('line').attrs({ | |
"class": 'background' | |
}); | |
enter_links.append('line').attrs({ | |
"class": 'foreground' | |
}); | |
enter_labels = enter_links.append('g').attrs({ | |
"class": 'label' | |
}); | |
enter_labels.append('text'); | |
all_links = enter_links.merge(links); | |
all_links.selectAll('line').attrs({ | |
x1: function(d) { | |
return d.source.x; | |
}, | |
y1: function(d) { | |
return d.source.y; | |
}, | |
x2: function(d) { | |
return d.target.x; | |
}, | |
y2: function(d) { | |
return d.target.y; | |
} | |
}); | |
all_links.selectAll('.label').attrs({ | |
transform: function(d) { | |
return "translate(" + ((d.source.x + d.target.x) / 2) + "," + ((d.source.y + d.target.y) / 2) + ")"; | |
} | |
}); | |
all_links.selectAll('.label text').text(function(d) { | |
return d3.format('.2f')(d.d); | |
}).attrs({ | |
dy: '0.35em', | |
transform: "scale(" + (1 / this.current_zoom_t.k) + ")" | |
}); | |
all_links.classed('selected', function(d) { | |
return d.selected; | |
}); | |
return links.exit().remove(); | |
}; | |
return NetworkView; | |
})(View)); | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
global class Toolbar extends View | |
constructor: (conf) -> | |
super(conf) | |
@current_tool = conf.current_tool | |
@tool_btns = {} | |
@tool_btns['Arrow'] = @d3el.append 'button' | |
.text 'Arrow tool' | |
.on 'click', () => | |
@current_tool.set 'Arrow' | |
@tool_btns['Multiline'] = @d3el.append 'button' | |
.text 'Multiline tool' | |
.on 'click', () => | |
@current_tool.set 'Multiline' | |
@current_tool.on 'change', @redraw | |
redraw: () => | |
Object.keys(@tool_btns).forEach (tool_name) => | |
@tool_btns[tool_name].classed 'selected', tool_name is @current_tool.get() | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.Toolbar { | |
padding: 4px; | |
background: #EEE; | |
border-bottom: 1px solid gray; | |
} | |
.Toolbar > *:not(:first-child) { | |
margin-left: 4px; | |
} | |
.Toolbar button { | |
border: 1px solid gray; | |
padding: 4px; | |
background: #CCC; | |
} | |
.Toolbar button.selected { | |
background: #ffb16f; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Generated by CoffeeScript 1.10.0 | |
(function() { | |
var Toolbar, | |
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, | |
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, | |
hasProp = {}.hasOwnProperty; | |
global(Toolbar = (function(superClass) { | |
extend(Toolbar, superClass); | |
function Toolbar(conf) { | |
this.redraw = bind(this.redraw, this); | |
Toolbar.__super__.constructor.call(this, conf); | |
this.current_tool = conf.current_tool; | |
this.tool_btns = {}; | |
this.tool_btns['Arrow'] = this.d3el.append('button').text('Arrow tool').on('click', (function(_this) { | |
return function() { | |
return _this.current_tool.set('Arrow'); | |
}; | |
})(this)); | |
this.tool_btns['Multiline'] = this.d3el.append('button').text('Multiline tool').on('click', (function(_this) { | |
return function() { | |
return _this.current_tool.set('Multiline'); | |
}; | |
})(this)); | |
this.current_tool.on('change', this.redraw); | |
} | |
Toolbar.prototype.redraw = function() { | |
return Object.keys(this.tool_btns).forEach((function(_this) { | |
return function(tool_name) { | |
return _this.tool_btns[tool_name].classed('selected', tool_name === _this.current_tool.get()); | |
}; | |
})(this)); | |
}; | |
return Toolbar; | |
})(View)); | |
}).call(this); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment