Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active September 12, 2016 10:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/fdb442d5ad3a04f62b9b8ecd5b893ec0 to your computer and use it in GitHub Desktop.
Save nitaku/fdb442d5ad3a04f62b9b8ecd5b893ec0 to your computer and use it in GitHub Desktop.
Path network editor
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)
.AppView {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.AppView .NetworkView {
flex-grow: 1;
height: 0;
}
// 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);
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
// 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);
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
// 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);
# 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
// 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);
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
// 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);
new AppView
parent: 'body'
body, html {
padding: 0;
margin: 0;
height: 100%;
}
<!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>
// Generated by CoffeeScript 1.10.0
(function() {
new AppView({
parent: 'body'
});
}).call(this);
# 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
// 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);
# 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
// 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);
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()
.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;
}
// 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);
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()
.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;
}
// 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