| window.AppView = Backbone.D3View.extend | |
| initialize: () -> | |
| # retrieve the grammar from an external file | |
| d3.text 'minidot.peg.js', (grammar) => | |
| left = @d3el.append 'div' | |
| .attr | |
| class: 'left' | |
| editor = new Editor | |
| model: @model | |
| grammar: grammar | |
| left.node().appendChild(editor.el) | |
| editor.render() | |
| matrix = new Matrix | |
| model: @model | |
| left.node().appendChild(matrix.el) | |
| matrix.render() | |
| node_link = new NodeLink | |
| model: @model | |
| @el.appendChild(node_link.el) | |
| node_link.render() | |
| // Generated by CoffeeScript 1.10.0 | |
| (function() { | |
| window.AppView = Backbone.D3View.extend({ | |
| initialize: function() { | |
| return d3.text('minidot.peg.js', (function(_this) { | |
| return function(grammar) { | |
| var editor, left, matrix, node_link; | |
| left = _this.d3el.append('div').attr({ | |
| "class": 'left' | |
| }); | |
| editor = new Editor({ | |
| model: _this.model, | |
| grammar: grammar | |
| }); | |
| left.node().appendChild(editor.el); | |
| editor.render(); | |
| matrix = new Matrix({ | |
| model: _this.model | |
| }); | |
| left.node().appendChild(matrix.el); | |
| matrix.render(); | |
| node_link = new NodeLink({ | |
| model: _this.model | |
| }); | |
| _this.el.appendChild(node_link.el); | |
| return node_link.render(); | |
| }; | |
| })(this)); | |
| } | |
| }); | |
| }).call(this); |
| // Backbone.D3View.js 0.3.1 | |
| // --------------- | |
| // (c) 2015 Adam Krebs | |
| // Backbone.D3View may be freely distributed under the MIT license. | |
| // For all details and documentation: | |
| // https://github.com/akre54/Backbone.D3View | |
| (function (factory) { | |
| if (typeof define === 'function' && define.amd) { define(['backbone', 'd3'], factory); | |
| } else if (typeof exports === 'object') { module.exports = factory(require('backbone'), require('d3')); | |
| } else { factory(Backbone, d3); } | |
| }(function (Backbone, d3) { | |
| // Cached regex to match an opening '<' of an HTML tag, possibly left-padded | |
| // with whitespace. | |
| var paddedLt = /^\s*</; | |
| var ElementProto = (typeof Element !== 'undefined' && Element.prototype) || {}; | |
| var matchesSelector = ElementProto.matches || | |
| ElementProto.webkitMatchesSelector || | |
| ElementProto.mozMatchesSelector || | |
| ElementProto.msMatchesSelector || | |
| ElementProto.oMatchesSelector; | |
| Backbone.D3ViewMixin = { | |
| // A reference to the d3 selection backing the view. | |
| d3el: null, | |
| namespace: d3.ns.prefix.svg, | |
| $: function(selector) { | |
| return this.el.querySelectorAll(selector); | |
| }, | |
| $$: function(selector) { | |
| return this.d3el.selectAll(selector); | |
| }, | |
| _removeElement: function() { | |
| this.undelegateEvents(); | |
| this.d3el.remove(); | |
| }, | |
| _createElement: function(tagName) { | |
| var ns = typeof this.namespace === 'function' ? this.namespace() : this.namespace; | |
| return ns ? | |
| document.createElementNS(ns, tagName) : | |
| document.createElement(tagName); | |
| }, | |
| _setElement: function(element) { | |
| if (typeof element == 'string') { | |
| if (paddedLt.test(element)) { | |
| var el = document.createElement('div'); | |
| el.innerHTML = element; | |
| this.el = el.firstChild; | |
| } else { | |
| this.el = document.querySelector(element); | |
| } | |
| } else { | |
| this.el = element; | |
| } | |
| this.d3el = d3.select(this.el); | |
| }, | |
| _setAttributes: function(attributes) { | |
| this.d3el.attr(attributes); | |
| }, | |
| // `delegate` supports two- and three-arg forms. The `selector` is optional. | |
| delegate: function(eventName, selector, listener) { | |
| if (listener === undefined) { | |
| listener = selector; | |
| selector = null; | |
| } | |
| var view = this; | |
| var wrapped = function(event) { | |
| var node = event.target, | |
| idx = 0, | |
| o = d3.event; | |
| d3.event = event; | |
| // The `event` object is stored in `d3.event` but Backbone expects it as | |
| // the first argument to the listener. | |
| if (!selector) { | |
| listener.call(view, d3.event, node.__data__, idx++); | |
| d3.event = o; | |
| return; | |
| } | |
| while (node && node !== view.el) { | |
| if (matchesSelector.call(node, selector)) { | |
| listener.call(view, d3.event, node.__data__, idx++); | |
| } | |
| node = node.parentNode; | |
| } | |
| d3.event = o; | |
| }; | |
| var map = this._domEvents || (this._domEvents = {}); | |
| var handlers = map[eventName] || (map[eventName] = []); | |
| handlers.push({selector: selector, listener: listener, wrapped: wrapped}); | |
| this.el.addEventListener(eventName, wrapped, false); | |
| return this; | |
| }, | |
| undelegate: function(eventName, selector, listener) { | |
| if (!this._domEvents || !this._domEvents[eventName]) return; | |
| if (typeof selector !== 'string') { | |
| listener = selector; | |
| selector = null; | |
| } | |
| var handlers = this._domEvents[eventName].slice(); | |
| var i = handlers.length; | |
| while (i--) { | |
| var handler = handlers[i]; | |
| var match = (listener ? handler.listener === listener : true) && | |
| (selector ? handler.selector === selector : true); | |
| if (!match) continue; | |
| this.el.removeEventListener(eventName, handler.wrapped, false); | |
| this._domEvents[eventName].splice(i, 1); | |
| } | |
| }, | |
| undelegateEvents: function() { | |
| var map = this._domEvents, el = this.el; | |
| if (!el || !map) return; | |
| Object.keys(map).forEach(function(eventName) { | |
| map[eventName].forEach(function(handler) { | |
| el.removeEventListener(eventName, handler.wrapped, false); | |
| }); | |
| }); | |
| this._domEvents = {}; | |
| return this; | |
| } | |
| }; | |
| Backbone.D3View = Backbone.View.extend(Backbone.D3ViewMixin); | |
| return Backbone.D3View; | |
| })); |
| window.Editor = Backbone.D3View.extend | |
| namespace: null | |
| tagName: 'div' | |
| events: | |
| input: 'compile' | |
| initialize: (conf) -> | |
| @d3el.classed 'editor', true | |
| @textarea = @d3el.append 'textarea' | |
| @status_bar = @d3el.append 'div' | |
| .attr | |
| class: 'status_bar' | |
| @parser = PEG.buildParser conf.grammar | |
| # example code | |
| @textarea.node().value = ''' | |
| 0--1 | |
| 2--3--4--5--2 | |
| a->b->c->a | |
| Z | |
| foo<-bar,baz,bob | |
| One,Two,Three,Four,Five* | |
| Four--foo | |
| ''' | |
| @compile() | |
| compile: () -> | |
| @status_bar.text 'All ok.' | |
| @status_bar.classed 'error', false | |
| try | |
| graph = @parser.parse @textarea.node().value | |
| @model.update graph | |
| catch e | |
| @status_bar.text "Line #{e.location.start.line}: #{e.message}" | |
| @status_bar.classed 'error', true | |
| .editor { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .editor textarea { | |
| flex-grow: 1; | |
| height: 0; | |
| resize: none; | |
| border: 0; | |
| outline: 0; | |
| } | |
| .editor .status_bar { | |
| height: 22px; | |
| background: #DDD; | |
| border-top: 1px solid gray; | |
| font-family: sans-serif; | |
| font-size: 12px; | |
| padding: 4px; | |
| box-sizing: border-box; | |
| } | |
| .editor .error { | |
| background: #F77; | |
| } |
| // Generated by CoffeeScript 1.10.0 | |
| (function() { | |
| window.Editor = Backbone.D3View.extend({ | |
| namespace: null, | |
| tagName: 'div', | |
| events: { | |
| input: 'compile' | |
| }, | |
| initialize: function(conf) { | |
| this.d3el.classed('editor', true); | |
| this.textarea = this.d3el.append('textarea'); | |
| this.status_bar = this.d3el.append('div').attr({ | |
| "class": 'status_bar' | |
| }); | |
| this.parser = PEG.buildParser(conf.grammar); | |
| this.textarea.node().value = '0--1\n2--3--4--5--2\na->b->c->a\nZ\nfoo<-bar,baz,bob\nOne,Two,Three,Four,Five*\nFour--foo'; | |
| return this.compile(); | |
| }, | |
| compile: function() { | |
| var e, error, graph; | |
| this.status_bar.text('All ok.'); | |
| this.status_bar.classed('error', false); | |
| try { | |
| graph = this.parser.parse(this.textarea.node().value); | |
| return this.model.update(graph); | |
| } catch (error) { | |
| e = error; | |
| this.status_bar.text("Line " + e.location.start.line + ": " + e.message); | |
| return this.status_bar.classed('error', true); | |
| } | |
| } | |
| }); | |
| }).call(this); |
| window.Graph = Backbone.Model.extend | |
| defaults: | |
| graph: { | |
| nodes: [] | |
| links: [] | |
| } | |
| selected: null | |
| update: (graph) -> | |
| # objectify the graph | |
| index = {} | |
| graph.nodes.forEach (n) -> index[n.id] = n | |
| graph.links.forEach (l) -> | |
| l.id = l.source + (if l.directed then '->' else '--') + l.target | |
| l.source = index[l.source] | |
| l.target = index[l.target] | |
| # FIXME handle direction (e.g. link id should be in lexicographic order for undirected links) | |
| @set 'graph', graph | |
| select: (id) -> | |
| if id is @get 'selected' | |
| @set 'selected', null | |
| else | |
| @set 'selected', id | |
| // Generated by CoffeeScript 1.10.0 | |
| (function() { | |
| window.Graph = Backbone.Model.extend({ | |
| defaults: { | |
| graph: { | |
| nodes: [], | |
| links: [] | |
| }, | |
| selected: null | |
| }, | |
| update: function(graph) { | |
| var index; | |
| index = {}; | |
| graph.nodes.forEach(function(n) { | |
| return index[n.id] = n; | |
| }); | |
| graph.links.forEach(function(l) { | |
| l.id = l.source + (l.directed ? '->' : '--') + l.target; | |
| l.source = index[l.source]; | |
| return l.target = index[l.target]; | |
| }); | |
| return this.set('graph', graph); | |
| }, | |
| select: function(id) { | |
| if (id === this.get('selected')) { | |
| return this.set('selected', null); | |
| } else { | |
| return this.set('selected', id); | |
| } | |
| } | |
| }); | |
| }).call(this); |
| app = new AppView | |
| el: 'body' | |
| model: new Graph |
| html, body { | |
| margin: 0; | |
| padding: 0; | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| } | |
| body { | |
| display: flex; | |
| flex-direction: row; | |
| } | |
| .left { | |
| width: 300px; | |
| border-right: 2px solid gray; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .editor { | |
| height: 0; | |
| flex-grow: 1; | |
| } | |
| .node_link { | |
| width: 0; | |
| flex-grow: 2; | |
| } | |
| .matrix { | |
| height: 300px; | |
| border-top: 2px solid gray; | |
| } |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>Example graph DSL in PEG.js</title> | |
| <link rel="stylesheet" href="index.css"> | |
| <script src="http://d3js.org/d3.v3.min.js"></script> | |
| <script src="/webvis/tmp/peg-0.9.0.min.js"></script> | |
| <script src="http://underscorejs.org/underscore-min.js"></script> | |
| <script src="http://backbonejs.org/backbone-min.js"></script> | |
| <script src="backbone.d3view.js"></script> | |
| <script src="http://marvl.infotech.monash.edu/webcola/cola.v3.min.js"></script> | |
| <!-- your views go here --> | |
| <script src="AppView.js"></script> | |
| <script src="Editor.js"></script> | |
| <link rel="stylesheet" href="Editor.css"> | |
| <script src="NodeLink.js"></script> | |
| <link rel="stylesheet" href="NodeLink.css"> | |
| <script src="Matrix.js"></script> | |
| <link rel="stylesheet" href="Matrix.css"> | |
| <!-- your models go here --> | |
| <script src="Graph.js"></script> | |
| </head> | |
| <body> | |
| <script src="index.js"></script> | |
| </body> | |
| </html> |
| MARGIN = 4 | |
| window.Matrix = Backbone.D3View.extend | |
| tagName: 'svg' | |
| events: | |
| 'click .cell': (evt, d) -> @model.select d.id | |
| initialize: () -> | |
| @d3el.classed 'matrix', true | |
| @vis = @d3el.append 'g' | |
| .attr | |
| transform: "translate(#{MARGIN},#{MARGIN})" | |
| @listenTo @model, 'change:graph', @render | |
| @listenTo @model, 'change:selected', @focus | |
| render: () -> | |
| width = @el.getBoundingClientRect().width | |
| height = @el.getBoundingClientRect().height | |
| size = Math.min(width, height) - 2*MARGIN | |
| graph = @model.get 'graph' | |
| # store the node index within its data structure | |
| graph.nodes.forEach (d, i) -> d.i = i | |
| cell_size = size / graph.nodes.length | |
| cells = @vis.selectAll '.cell' | |
| .data graph.links, (d) -> d.id | |
| enter_cells = cells.enter().append 'rect' | |
| .attr | |
| class: 'cell' | |
| enter_cells.append 'title' | |
| .text (d) -> d.id | |
| cells | |
| .attr | |
| x: (d) -> d.target.i*cell_size | |
| y: (d) -> d.source.i*cell_size | |
| width: cell_size | |
| height: cell_size | |
| fill: (d) -> if d.directed then 'orange' else 'teal' | |
| cells.exit() | |
| .remove() | |
| # mirror cells for undirected links | |
| mirror_cells = @vis.selectAll '.mirror' | |
| .data graph.links.filter((d) -> not d.directed), (d) -> 'mirror_' + d.id | |
| enter_mirror_cells = mirror_cells.enter().append 'rect' | |
| .attr | |
| class: 'mirror cell' | |
| enter_mirror_cells.append 'title' | |
| .text (d) -> d.id | |
| mirror_cells | |
| .attr | |
| x: (d) -> d.source.i*cell_size | |
| y: (d) -> d.target.i*cell_size | |
| width: cell_size | |
| height: cell_size | |
| fill: (d) -> 'teal' | |
| mirror_cells.exit() | |
| .remove() | |
| focus: () -> | |
| id = @model.get 'selected' | |
| @vis.selectAll '.cell' | |
| .classed 'selected', (d) -> d.id is id | |
| .classed 'unselected', (d) -> if id is null then null else d.id isnt id |
| .matrix .cell { | |
| shape-rendering: crispEdges; | |
| opacity: 0.6; | |
| } | |
| .matrix .selected.cell, .matrix .cell:hover { | |
| opacity: 1; | |
| } | |
| .matrix .unselected.cell { | |
| opacity: 0.2; | |
| } |
| // Generated by CoffeeScript 1.10.0 | |
| (function() { | |
| var MARGIN; | |
| MARGIN = 4; | |
| window.Matrix = Backbone.D3View.extend({ | |
| tagName: 'svg', | |
| events: { | |
| 'click .cell': function(evt, d) { | |
| return this.model.select(d.id); | |
| } | |
| }, | |
| initialize: function() { | |
| this.d3el.classed('matrix', true); | |
| this.vis = this.d3el.append('g').attr({ | |
| transform: "translate(" + MARGIN + "," + MARGIN + ")" | |
| }); | |
| this.listenTo(this.model, 'change:graph', this.render); | |
| return this.listenTo(this.model, 'change:selected', this.focus); | |
| }, | |
| render: function() { | |
| var cell_size, cells, enter_cells, enter_mirror_cells, graph, height, mirror_cells, size, width; | |
| width = this.el.getBoundingClientRect().width; | |
| height = this.el.getBoundingClientRect().height; | |
| size = Math.min(width, height) - 2 * MARGIN; | |
| graph = this.model.get('graph'); | |
| graph.nodes.forEach(function(d, i) { | |
| return d.i = i; | |
| }); | |
| cell_size = size / graph.nodes.length; | |
| cells = this.vis.selectAll('.cell').data(graph.links, function(d) { | |
| return d.id; | |
| }); | |
| enter_cells = cells.enter().append('rect').attr({ | |
| "class": 'cell' | |
| }); | |
| enter_cells.append('title').text(function(d) { | |
| return d.id; | |
| }); | |
| cells.attr({ | |
| x: function(d) { | |
| return d.target.i * cell_size; | |
| }, | |
| y: function(d) { | |
| return d.source.i * cell_size; | |
| }, | |
| width: cell_size, | |
| height: cell_size, | |
| fill: function(d) { | |
| if (d.directed) { | |
| return 'orange'; | |
| } else { | |
| return 'teal'; | |
| } | |
| } | |
| }); | |
| cells.exit().remove(); | |
| mirror_cells = this.vis.selectAll('.mirror').data(graph.links.filter(function(d) { | |
| return !d.directed; | |
| }), function(d) { | |
| return 'mirror_' + d.id; | |
| }); | |
| enter_mirror_cells = mirror_cells.enter().append('rect').attr({ | |
| "class": 'mirror cell' | |
| }); | |
| enter_mirror_cells.append('title').text(function(d) { | |
| return d.id; | |
| }); | |
| mirror_cells.attr({ | |
| x: function(d) { | |
| return d.source.i * cell_size; | |
| }, | |
| y: function(d) { | |
| return d.target.i * cell_size; | |
| }, | |
| width: cell_size, | |
| height: cell_size, | |
| fill: function(d) { | |
| return 'teal'; | |
| } | |
| }); | |
| return mirror_cells.exit().remove(); | |
| }, | |
| focus: function() { | |
| var id; | |
| id = this.model.get('selected'); | |
| return this.vis.selectAll('.cell').classed('selected', function(d) { | |
| return d.id === id; | |
| }).classed('unselected', function(d) { | |
| if (id === null) { | |
| return null; | |
| } else { | |
| return d.id !== id; | |
| } | |
| }); | |
| } | |
| }); | |
| }).call(this); |
| // DOT-like syntax translated to d3.js data structure | |
| /* | |
| 1--2--3--4--1 | |
| 1--3 | |
| 2--4 | |
| */ | |
| { | |
| var node_index = {}; | |
| var graph = { | |
| nodes: [], | |
| links: [] | |
| }; | |
| } | |
| start | |
| = Def ('\n' Def)* { return graph; } | |
| Def 'definition' | |
| = CliqueDef | |
| / NodesLinksDef | |
| CliqueDef 'clique definition' | |
| = list:NodeList '*' { | |
| list.forEach(function(a){ | |
| list.forEach(function(b){ | |
| if(a !== b) { | |
| graph.links.push({ | |
| source: d3.min([a,b]), | |
| target: d3.max([a,b]) | |
| }); | |
| } | |
| }); | |
| }); | |
| } | |
| NodesLinksDef 'nodes-links definition' | |
| = first:NodeList rest:(_ Link _ NodeList)* { | |
| var node_lists = [first].concat(rest.map(function(d){ return d[3]; })); | |
| node_lists.forEach(function(list, i){ | |
| // add links between contiguous node lists | |
| if(i < node_lists.length-1) { | |
| var list_a = list; | |
| var list_b = node_lists[i+1]; | |
| var l = rest[i][1]; | |
| list_a.forEach(function(a){ | |
| list_b.forEach(function(b){ | |
| var link = {}; | |
| if(l == 'undirected') { | |
| link.source = d3.min([a,b]); | |
| link.target = d3.max([a,b]); | |
| } | |
| else if (l == 'ltr') { | |
| link.source = a; | |
| link.target = b; | |
| link.directed = true; | |
| } | |
| else if (l == 'rtl') { | |
| link.source = b; | |
| link.target = a; | |
| link.directed = true; | |
| }; | |
| graph.links.push(link); | |
| }); | |
| }); | |
| } | |
| }); | |
| } | |
| Link 'link specifier' | |
| = '--' { return 'undirected'; } | |
| / '->' { return 'ltr'; } | |
| / '<-' { return 'rtl'; } | |
| NodeList 'node list' | |
| = first:Node rest:(',' Node)* { | |
| var nodes = [first].concat(rest.map(function(d){ return d[1]; })); | |
| nodes.forEach(function(id) { | |
| // add a node if its ID was not previously found | |
| if(!(id in node_index)) { | |
| var node = {id: id}; | |
| node_index[id] = node; | |
| graph.nodes.push(node); | |
| } | |
| }); | |
| return nodes; | |
| } | |
| Node 'identifier' | |
| = [_a-zA-Z0-9]+ { return text(); } | |
| _ 'whitespace' | |
| = [ \t]* |
| R = 18 | |
| window.NodeLink = Backbone.D3View.extend | |
| tagName: 'svg' | |
| events: | |
| 'click .link': (evt, d) -> @model.select d.id | |
| initialize: () -> | |
| @d3el.classed 'node_link', true | |
| defs = @d3el.append 'defs' | |
| # define arrow markers for graph links | |
| defs.append 'marker' | |
| .attr | |
| id: 'end-arrow' | |
| viewBox: '0 0 10 10' | |
| refX: 4+R | |
| refY: 5 | |
| orient: 'auto' | |
| .append 'path' | |
| .attr | |
| d: 'M0,0 L0,10 L10,5 z' | |
| # append a group for zoomable content | |
| zoomable_layer = @d3el.append('g') | |
| zoom = d3.behavior.zoom() | |
| .scaleExtent([-Infinity,Infinity]) | |
| .on 'zoom', () -> | |
| zoomable_layer | |
| .attr | |
| transform: "translate(#{zoom.translate()})scale(#{zoom.scale()})" | |
| @d3el.call(zoom) | |
| # create two layers for nodes and links | |
| @links_layer = zoomable_layer.append 'g' | |
| @nodes_layer = zoomable_layer.append 'g' | |
| @listenTo @model, 'change:graph', @render | |
| @listenTo @model, 'change:selected', @focus | |
| render: () -> | |
| width = @el.getBoundingClientRect().width | |
| height = @el.getBoundingClientRect().height | |
| graph = @model.get 'graph' | |
| # draw nodes | |
| nodes = @nodes_layer.selectAll '.node' | |
| .data graph.nodes, (d) -> d.id | |
| enter_nodes = nodes.enter().append 'g' | |
| .attr | |
| class: 'node' | |
| enter_nodes.append 'circle' | |
| .attr | |
| r: R | |
| # draw the label | |
| enter_nodes.append 'text' | |
| .text (d) -> d.id | |
| .attr | |
| dy: '0.35em' | |
| nodes.exit() | |
| .remove() | |
| # draw links | |
| links = @links_layer.selectAll '.link' | |
| .data graph.links, (d) -> d.id | |
| links | |
| .enter().append 'line' | |
| .attr | |
| class: 'link' | |
| links | |
| .classed 'directed', (d) -> d.directed | |
| links.exit() | |
| .remove() | |
| ### cola layout ### | |
| graph.nodes.forEach (v) -> | |
| v.width = 3*R | |
| v.height = 3*R | |
| d3cola = cola.d3adaptor() | |
| .size([width, height]) | |
| .linkDistance(100) | |
| .avoidOverlaps(true) | |
| .nodes(graph.nodes) | |
| .links(graph.links) | |
| .on 'tick', () -> | |
| # update nodes and links | |
| nodes | |
| .attr('transform', (d) -> "translate(#{d.x},#{d.y})") | |
| links | |
| .attr('x1', (d) -> d.source.x) | |
| .attr('y1', (d) -> d.source.y) | |
| .attr('x2', (d) -> d.target.x) | |
| .attr('y2', (d) -> d.target.y) | |
| drag = d3cola.drag() | |
| drag.on 'dragstart', () -> | |
| # silence other listener | |
| d3.event.sourceEvent.stopPropagation() | |
| nodes | |
| .call(drag) | |
| d3cola.start(30,30,30) | |
| focus: () -> | |
| id = @model.get 'selected' | |
| @links_layer.selectAll '.link' | |
| .classed 'selected', (d) -> d.id is id | |
| .classed 'unselected', (d) -> if id is null then null else d.id isnt id | |
| .node_link .node > circle { | |
| fill: #dddddd; | |
| stroke: #777777; | |
| stroke-width: 2px; | |
| } | |
| .node_link .node > text { | |
| font-family: sans-serif; | |
| text-anchor: middle; | |
| pointer-events: none; | |
| -webkit-touch-callout: none; /* iOS Safari */ | |
| -webkit-user-select: none; /* Chrome/Safari/Opera */ | |
| -khtml-user-select: none; /* Konqueror */ | |
| -moz-user-select: none; /* Firefox */ | |
| -ms-user-select: none; /* IE/Edge */ | |
| user-select: none; /* non-prefixed version, currently | |
| not supported by any browser */ | |
| } | |
| .node_link .link { | |
| stroke: teal; | |
| stroke-width: 4px; | |
| opacity: 0.6; | |
| } | |
| .node_link .directed.link { | |
| stroke: orange; | |
| marker-end: url(#end-arrow); | |
| } | |
| .node_link #end-arrow { | |
| fill: orange; | |
| } | |
| .node_link .selected.link { | |
| opacity: 1; | |
| } | |
| .node_link .unselected.link { | |
| opacity: 0.2; | |
| } |
| // Generated by CoffeeScript 1.10.0 | |
| (function() { | |
| var R; | |
| R = 18; | |
| window.NodeLink = Backbone.D3View.extend({ | |
| tagName: 'svg', | |
| events: { | |
| 'click .link': function(evt, d) { | |
| return this.model.select(d.id); | |
| } | |
| }, | |
| initialize: function() { | |
| var defs, zoom, zoomable_layer; | |
| this.d3el.classed('node_link', true); | |
| defs = this.d3el.append('defs'); | |
| defs.append('marker').attr({ | |
| id: 'end-arrow', | |
| viewBox: '0 0 10 10', | |
| refX: 4 + R, | |
| refY: 5, | |
| orient: 'auto' | |
| }).append('path').attr({ | |
| d: 'M0,0 L0,10 L10,5 z' | |
| }); | |
| zoomable_layer = this.d3el.append('g'); | |
| zoom = d3.behavior.zoom().scaleExtent([-Infinity, Infinity]).on('zoom', function() { | |
| return zoomable_layer.attr({ | |
| transform: "translate(" + (zoom.translate()) + ")scale(" + (zoom.scale()) + ")" | |
| }); | |
| }); | |
| this.d3el.call(zoom); | |
| this.links_layer = zoomable_layer.append('g'); | |
| this.nodes_layer = zoomable_layer.append('g'); | |
| this.listenTo(this.model, 'change:graph', this.render); | |
| return this.listenTo(this.model, 'change:selected', this.focus); | |
| }, | |
| render: function() { | |
| var d3cola, drag, enter_nodes, graph, height, links, nodes, width; | |
| width = this.el.getBoundingClientRect().width; | |
| height = this.el.getBoundingClientRect().height; | |
| graph = this.model.get('graph'); | |
| nodes = this.nodes_layer.selectAll('.node').data(graph.nodes, function(d) { | |
| return d.id; | |
| }); | |
| enter_nodes = nodes.enter().append('g').attr({ | |
| "class": 'node' | |
| }); | |
| enter_nodes.append('circle').attr({ | |
| r: R | |
| }); | |
| enter_nodes.append('text').text(function(d) { | |
| return d.id; | |
| }).attr({ | |
| dy: '0.35em' | |
| }); | |
| nodes.exit().remove(); | |
| links = this.links_layer.selectAll('.link').data(graph.links, function(d) { | |
| return d.id; | |
| }); | |
| links.enter().append('line').attr({ | |
| "class": 'link' | |
| }); | |
| links.classed('directed', function(d) { | |
| return d.directed; | |
| }); | |
| links.exit().remove(); | |
| /* cola layout */ | |
| graph.nodes.forEach(function(v) { | |
| v.width = 3 * R; | |
| return v.height = 3 * R; | |
| }); | |
| d3cola = cola.d3adaptor().size([width, height]).linkDistance(100).avoidOverlaps(true).nodes(graph.nodes).links(graph.links).on('tick', function() { | |
| nodes.attr('transform', function(d) { | |
| return "translate(" + d.x + "," + d.y + ")"; | |
| }); | |
| return links.attr('x1', function(d) { | |
| return d.source.x; | |
| }).attr('y1', function(d) { | |
| return d.source.y; | |
| }).attr('x2', function(d) { | |
| return d.target.x; | |
| }).attr('y2', function(d) { | |
| return d.target.y; | |
| }); | |
| }); | |
| drag = d3cola.drag(); | |
| drag.on('dragstart', function() { | |
| return d3.event.sourceEvent.stopPropagation(); | |
| }); | |
| nodes.call(drag); | |
| return d3cola.start(30, 30, 30); | |
| }, | |
| focus: function() { | |
| var id; | |
| id = this.model.get('selected'); | |
| return this.links_layer.selectAll('.link').classed('selected', function(d) { | |
| return d.id === id; | |
| }).classed('unselected', function(d) { | |
| if (id === null) { | |
| return null; | |
| } else { | |
| return d.id !== id; | |
| } | |
| }); | |
| } | |
| }); | |
| }).call(this); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment