Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active November 8, 2019 06:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nitaku/2f4e1d27ce589847a003 to your computer and use it in GitHub Desktop.
Save nitaku/2f4e1d27ce589847a003 to your computer and use it in GitHub Desktop.
Example graph DSL (PEG.js)
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>
// Generated by CoffeeScript 1.10.0
(function() {
var app;
app = new AppView({
el: 'body',
model: new Graph
});
}).call(this);
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]*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment