├── README.md
├── download-icon.png
├── graph-creator.css
├── graph-creator.js
├── index.html
├── thumbnail.png
├── trash-icon.png
└── upload-icon.png
/README.md:
--------------------------------------------------------------------------------
1 | directed-graph-creator
2 | ======================
3 |
4 | Interactive tool for creating directed graphs, created using d3.js.
5 |
6 | Demo: http://bl.ocks.org/cjrd/6863459
7 |
8 |
9 |
10 |
11 |
12 | Operation:
13 |
14 | * drag/scroll to translate/zoom the graph
15 | * shift-click on graph to create a node
16 | * shift-click on a node and then drag to another node to connect them with a directed edge
17 | * shift-click on a node to change its title
18 | * click on node or edge and press backspace/delete to delete
19 |
20 | Run:
21 |
22 | * `python -m SimpleHTTPServer 8000`
23 | * navigate to http://127.0.0.1:8000
24 |
25 | Github repo is at https://github.com/metacademy/directed-graph-creator
26 |
27 | License: MIT/X
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/download-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cjrd/directed-graph-creator/01c83e88b227ec8451798f6e5385f9b1294bfd36/download-icon.png
--------------------------------------------------------------------------------
/graph-creator.css:
--------------------------------------------------------------------------------
1 | body{
2 | margin: 0;
3 | padding: 0;
4 | overflow:hidden;
5 | }
6 |
7 | p{
8 | text-align: center;
9 | overflow: overlay;
10 | position: relative;
11 | }
12 |
13 | body{
14 | -webkit-touch-callout: none;
15 | -webkit-user-select: none;
16 | -khtml-user-select: none;
17 | -moz-user-select: none;
18 | -ms-user-select: none;
19 | user-select: none;
20 | background-color: rgb(248, 248, 248)
21 | }
22 |
23 | #toolbox{
24 | position: absolute;
25 | bottom: 0;
26 | left: 0;
27 | margin-bottom: 0.5em;
28 | margin-left: 1em;
29 | border: 2px solid #EEEEEE;
30 | border-radius: 5px;
31 | padding: 1em;
32 | z-index: 5;
33 | }
34 |
35 | #toolbox input{
36 | width: 30px;
37 | opacity: 0.4;
38 | }
39 | #toolbox input:hover{
40 | opacity: 1;
41 | cursor: pointer;
42 | }
43 |
44 | #hidden-file-upload{
45 | display: none;
46 | }
47 |
48 | #download-input{
49 | margin: 0 0.5em;
50 | }
51 |
52 | .conceptG text{
53 | pointer-events: none;
54 | }
55 |
56 | marker{
57 | fill: #333;
58 | }
59 |
60 | g.conceptG circle{
61 | fill: #F6FBFF;
62 | stroke: #333;
63 | stroke-width: 2px;
64 | }
65 |
66 | g.conceptG:hover circle{
67 | fill: rgb(200, 238, 241);
68 | }
69 |
70 | g.selected circle{
71 | fill: rgb(250, 232, 255);
72 | }
73 | g.selected:hover circle{
74 | fill: rgb(250, 232, 255);
75 | }
76 |
77 | path.link {
78 | fill: none;
79 | stroke: #333;
80 | stroke-width: 6px;
81 | cursor: default;
82 | }
83 |
84 | path.link:hover{
85 | stroke: rgb(94, 196, 204);
86 | }
87 |
88 | g.connect-node circle{
89 | fill: #BEFFFF;
90 | }
91 |
92 | path.link.hidden{
93 | stroke-width: 0;
94 | }
95 |
96 | path.link.selected {
97 | stroke: rgb(229, 172, 247);
98 | }
99 |
--------------------------------------------------------------------------------
/graph-creator.js:
--------------------------------------------------------------------------------
1 | document.onload = (function(d3, saveAs, Blob, undefined){
2 | "use strict";
3 |
4 | // TODO add user settings
5 | var consts = {
6 | defaultTitle: "random variable"
7 | };
8 | var settings = {
9 | appendElSpec: "#graph"
10 | };
11 | // define graphcreator object
12 | var GraphCreator = function(svg, nodes, edges){
13 | var thisGraph = this;
14 | thisGraph.idct = 0;
15 |
16 | thisGraph.nodes = nodes || [];
17 | thisGraph.edges = edges || [];
18 |
19 | thisGraph.state = {
20 | selectedNode: null,
21 | selectedEdge: null,
22 | mouseDownNode: null,
23 | mouseDownLink: null,
24 | justDragged: false,
25 | justScaleTransGraph: false,
26 | lastKeyDown: -1,
27 | shiftNodeDrag: false,
28 | selectedText: null
29 | };
30 |
31 | // define arrow markers for graph links
32 | var defs = svg.append('svg:defs');
33 | defs.append('svg:marker')
34 | .attr('id', 'end-arrow')
35 | .attr('viewBox', '0 -5 10 10')
36 | .attr('refX', "32")
37 | .attr('markerWidth', 3.5)
38 | .attr('markerHeight', 3.5)
39 | .attr('orient', 'auto')
40 | .append('svg:path')
41 | .attr('d', 'M0,-5L10,0L0,5');
42 |
43 | // define arrow markers for leading arrow
44 | defs.append('svg:marker')
45 | .attr('id', 'mark-end-arrow')
46 | .attr('viewBox', '0 -5 10 10')
47 | .attr('refX', 7)
48 | .attr('markerWidth', 3.5)
49 | .attr('markerHeight', 3.5)
50 | .attr('orient', 'auto')
51 | .append('svg:path')
52 | .attr('d', 'M0,-5L10,0L0,5');
53 |
54 | thisGraph.svg = svg;
55 | thisGraph.svgG = svg.append("g")
56 | .classed(thisGraph.consts.graphClass, true);
57 | var svgG = thisGraph.svgG;
58 |
59 | // displayed when dragging between nodes
60 | thisGraph.dragLine = svgG.append('svg:path')
61 | .attr('class', 'link dragline hidden')
62 | .attr('d', 'M0,0L0,0')
63 | .style('marker-end', 'url(#mark-end-arrow)');
64 |
65 | // svg nodes and edges
66 | thisGraph.paths = svgG.append("g").selectAll("g");
67 | thisGraph.circles = svgG.append("g").selectAll("g");
68 |
69 | thisGraph.drag = d3.behavior.drag()
70 | .origin(function(d){
71 | return {x: d.x, y: d.y};
72 | })
73 | .on("drag", function(args){
74 | thisGraph.state.justDragged = true;
75 | thisGraph.dragmove.call(thisGraph, args);
76 | })
77 | .on("dragend", function() {
78 | // todo check if edge-mode is selected
79 | });
80 |
81 | // listen for key events
82 | d3.select(window).on("keydown", function(){
83 | thisGraph.svgKeyDown.call(thisGraph);
84 | })
85 | .on("keyup", function(){
86 | thisGraph.svgKeyUp.call(thisGraph);
87 | });
88 | svg.on("mousedown", function(d){thisGraph.svgMouseDown.call(thisGraph, d);});
89 | svg.on("mouseup", function(d){thisGraph.svgMouseUp.call(thisGraph, d);});
90 |
91 | // listen for dragging
92 | var dragSvg = d3.behavior.zoom()
93 | .on("zoom", function(){
94 | if (d3.event.sourceEvent.shiftKey){
95 | // TODO the internal d3 state is still changing
96 | return false;
97 | } else{
98 | thisGraph.zoomed.call(thisGraph);
99 | }
100 | return true;
101 | })
102 | .on("zoomstart", function(){
103 | var ael = d3.select("#" + thisGraph.consts.activeEditId).node();
104 | if (ael){
105 | ael.blur();
106 | }
107 | if (!d3.event.sourceEvent.shiftKey) d3.select('body').style("cursor", "move");
108 | })
109 | .on("zoomend", function(){
110 | d3.select('body').style("cursor", "auto");
111 | });
112 |
113 | svg.call(dragSvg).on("dblclick.zoom", null);
114 |
115 | // listen for resize
116 | window.onresize = function(){thisGraph.updateWindow(svg);};
117 |
118 | // handle download data
119 | d3.select("#download-input").on("click", function(){
120 | var saveEdges = [];
121 | thisGraph.edges.forEach(function(val, i){
122 | saveEdges.push({source: val.source.id, target: val.target.id});
123 | });
124 | var blob = new Blob([window.JSON.stringify({"nodes": thisGraph.nodes, "edges": saveEdges})], {type: "text/plain;charset=utf-8"});
125 | saveAs(blob, "mydag.json");
126 | });
127 |
128 |
129 | // handle uploaded data
130 | d3.select("#upload-input").on("click", function(){
131 | document.getElementById("hidden-file-upload").click();
132 | });
133 | d3.select("#hidden-file-upload").on("change", function(){
134 | if (window.File && window.FileReader && window.FileList && window.Blob) {
135 | var uploadFile = this.files[0];
136 | var filereader = new window.FileReader();
137 |
138 | filereader.onload = function(){
139 | var txtRes = filereader.result;
140 | // TODO better error handling
141 | try{
142 | var jsonObj = JSON.parse(txtRes);
143 | thisGraph.deleteGraph(true);
144 | thisGraph.nodes = jsonObj.nodes;
145 | thisGraph.setIdCt(jsonObj.nodes.length + 1);
146 | var newEdges = jsonObj.edges;
147 | newEdges.forEach(function(e, i){
148 | newEdges[i] = {source: thisGraph.nodes.filter(function(n){return n.id == e.source;})[0],
149 | target: thisGraph.nodes.filter(function(n){return n.id == e.target;})[0]};
150 | });
151 | thisGraph.edges = newEdges;
152 | thisGraph.updateGraph();
153 | }catch(err){
154 | window.alert("Error parsing uploaded file\nerror message: " + err.message);
155 | return;
156 | }
157 | };
158 | filereader.readAsText(uploadFile);
159 |
160 | } else {
161 | alert("Your browser won't let you save this graph -- try upgrading your browser to IE 10+ or Chrome or Firefox.");
162 | }
163 |
164 | });
165 |
166 | // handle delete graph
167 | d3.select("#delete-graph").on("click", function(){
168 | thisGraph.deleteGraph(false);
169 | });
170 | };
171 |
172 | GraphCreator.prototype.setIdCt = function(idct){
173 | this.idct = idct;
174 | };
175 |
176 | GraphCreator.prototype.consts = {
177 | selectedClass: "selected",
178 | connectClass: "connect-node",
179 | circleGClass: "conceptG",
180 | graphClass: "graph",
181 | activeEditId: "active-editing",
182 | BACKSPACE_KEY: 8,
183 | DELETE_KEY: 46,
184 | ENTER_KEY: 13,
185 | nodeRadius: 50
186 | };
187 |
188 | /* PROTOTYPE FUNCTIONS */
189 |
190 | GraphCreator.prototype.dragmove = function(d) {
191 | var thisGraph = this;
192 | if (thisGraph.state.shiftNodeDrag){
193 | thisGraph.dragLine.attr('d', 'M' + d.x + ',' + d.y + 'L' + d3.mouse(thisGraph.svgG.node())[0] + ',' + d3.mouse(this.svgG.node())[1]);
194 | } else{
195 | d.x += d3.event.dx;
196 | d.y += d3.event.dy;
197 | thisGraph.updateGraph();
198 | }
199 | };
200 |
201 | GraphCreator.prototype.deleteGraph = function(skipPrompt){
202 | var thisGraph = this,
203 | doDelete = true;
204 | if (!skipPrompt){
205 | doDelete = window.confirm("Press OK to delete this graph");
206 | }
207 | if(doDelete){
208 | thisGraph.nodes = [];
209 | thisGraph.edges = [];
210 | thisGraph.updateGraph();
211 | }
212 | };
213 |
214 | /* select all text in element: taken from http://stackoverflow.com/questions/6139107/programatically-select-text-in-a-contenteditable-html-element */
215 | GraphCreator.prototype.selectElementContents = function(el) {
216 | var range = document.createRange();
217 | range.selectNodeContents(el);
218 | var sel = window.getSelection();
219 | sel.removeAllRanges();
220 | sel.addRange(range);
221 | };
222 |
223 |
224 | /* insert svg line breaks: taken from http://stackoverflow.com/questions/13241475/how-do-i-include-newlines-in-labels-in-d3-charts */
225 | GraphCreator.prototype.insertTitleLinebreaks = function (gEl, title) {
226 | var words = title.split(/\s+/g),
227 | nwords = words.length;
228 | var el = gEl.append("text")
229 | .attr("text-anchor","middle")
230 | .attr("dy", "-" + (nwords-1)*7.5);
231 |
232 | for (var i = 0; i < words.length; i++) {
233 | var tspan = el.append('tspan').text(words[i]);
234 | if (i > 0)
235 | tspan.attr('x', 0).attr('dy', '15');
236 | }
237 | };
238 |
239 |
240 | // remove edges associated with a node
241 | GraphCreator.prototype.spliceLinksForNode = function(node) {
242 | var thisGraph = this,
243 | toSplice = thisGraph.edges.filter(function(l) {
244 | return (l.source === node || l.target === node);
245 | });
246 | toSplice.map(function(l) {
247 | thisGraph.edges.splice(thisGraph.edges.indexOf(l), 1);
248 | });
249 | };
250 |
251 | GraphCreator.prototype.replaceSelectEdge = function(d3Path, edgeData){
252 | var thisGraph = this;
253 | d3Path.classed(thisGraph.consts.selectedClass, true);
254 | if (thisGraph.state.selectedEdge){
255 | thisGraph.removeSelectFromEdge();
256 | }
257 | thisGraph.state.selectedEdge = edgeData;
258 | };
259 |
260 | GraphCreator.prototype.replaceSelectNode = function(d3Node, nodeData){
261 | var thisGraph = this;
262 | d3Node.classed(this.consts.selectedClass, true);
263 | if (thisGraph.state.selectedNode){
264 | thisGraph.removeSelectFromNode();
265 | }
266 | thisGraph.state.selectedNode = nodeData;
267 | };
268 |
269 | GraphCreator.prototype.removeSelectFromNode = function(){
270 | var thisGraph = this;
271 | thisGraph.circles.filter(function(cd){
272 | return cd.id === thisGraph.state.selectedNode.id;
273 | }).classed(thisGraph.consts.selectedClass, false);
274 | thisGraph.state.selectedNode = null;
275 | };
276 |
277 | GraphCreator.prototype.removeSelectFromEdge = function(){
278 | var thisGraph = this;
279 | thisGraph.paths.filter(function(cd){
280 | return cd === thisGraph.state.selectedEdge;
281 | }).classed(thisGraph.consts.selectedClass, false);
282 | thisGraph.state.selectedEdge = null;
283 | };
284 |
285 | GraphCreator.prototype.pathMouseDown = function(d3path, d){
286 | var thisGraph = this,
287 | state = thisGraph.state;
288 | d3.event.stopPropagation();
289 | state.mouseDownLink = d;
290 |
291 | if (state.selectedNode){
292 | thisGraph.removeSelectFromNode();
293 | }
294 |
295 | var prevEdge = state.selectedEdge;
296 | if (!prevEdge || prevEdge !== d){
297 | thisGraph.replaceSelectEdge(d3path, d);
298 | } else{
299 | thisGraph.removeSelectFromEdge();
300 | }
301 | };
302 |
303 | // mousedown on node
304 | GraphCreator.prototype.circleMouseDown = function(d3node, d){
305 | var thisGraph = this,
306 | state = thisGraph.state;
307 | d3.event.stopPropagation();
308 | state.mouseDownNode = d;
309 | if (d3.event.shiftKey){
310 | state.shiftNodeDrag = d3.event.shiftKey;
311 | // reposition dragged directed edge
312 | thisGraph.dragLine.classed('hidden', false)
313 | .attr('d', 'M' + d.x + ',' + d.y + 'L' + d.x + ',' + d.y);
314 | return;
315 | }
316 | };
317 |
318 | /* place editable text on node in place of svg text */
319 | GraphCreator.prototype.changeTextOfNode = function(d3node, d){
320 | var thisGraph= this,
321 | consts = thisGraph.consts,
322 | htmlEl = d3node.node();
323 | d3node.selectAll("text").remove();
324 | var nodeBCR = htmlEl.getBoundingClientRect(),
325 | curScale = nodeBCR.width/consts.nodeRadius,
326 | placePad = 5*curScale,
327 | useHW = curScale > 1 ? nodeBCR.width*0.71 : consts.nodeRadius*1.42;
328 | // replace with editableconent text
329 | var d3txt = thisGraph.svg.selectAll("foreignObject")
330 | .data([d])
331 | .enter()
332 | .append("foreignObject")
333 | .attr("x", nodeBCR.left + placePad )
334 | .attr("y", nodeBCR.top + placePad)
335 | .attr("height", 2*useHW)
336 | .attr("width", useHW)
337 | .append("xhtml:p")
338 | .attr("id", consts.activeEditId)
339 | .attr("contentEditable", "true")
340 | .text(d.title)
341 | .on("mousedown", function(d){
342 | d3.event.stopPropagation();
343 | })
344 | .on("keydown", function(d){
345 | d3.event.stopPropagation();
346 | if (d3.event.keyCode == consts.ENTER_KEY && !d3.event.shiftKey){
347 | this.blur();
348 | }
349 | })
350 | .on("blur", function(d){
351 | d.title = this.textContent;
352 | thisGraph.insertTitleLinebreaks(d3node, d.title);
353 | d3.select(this.parentElement).remove();
354 | });
355 | return d3txt;
356 | };
357 |
358 | // mouseup on nodes
359 | GraphCreator.prototype.circleMouseUp = function(d3node, d){
360 | var thisGraph = this,
361 | state = thisGraph.state,
362 | consts = thisGraph.consts;
363 | // reset the states
364 | state.shiftNodeDrag = false;
365 | d3node.classed(consts.connectClass, false);
366 |
367 | var mouseDownNode = state.mouseDownNode;
368 |
369 | if (!mouseDownNode) return;
370 |
371 | thisGraph.dragLine.classed("hidden", true);
372 |
373 | if (mouseDownNode !== d){
374 | // we're in a different node: create new edge for mousedown edge and add to graph
375 | var newEdge = {source: mouseDownNode, target: d};
376 | var filtRes = thisGraph.paths.filter(function(d){
377 | if (d.source === newEdge.target && d.target === newEdge.source){
378 | thisGraph.edges.splice(thisGraph.edges.indexOf(d), 1);
379 | }
380 | return d.source === newEdge.source && d.target === newEdge.target;
381 | });
382 | if (!filtRes[0].length){
383 | thisGraph.edges.push(newEdge);
384 | thisGraph.updateGraph();
385 | }
386 | } else{
387 | // we're in the same node
388 | if (state.justDragged) {
389 | // dragged, not clicked
390 | state.justDragged = false;
391 | } else{
392 | // clicked, not dragged
393 | if (d3.event.shiftKey){
394 | // shift-clicked node: edit text content
395 | var d3txt = thisGraph.changeTextOfNode(d3node, d);
396 | var txtNode = d3txt.node();
397 | thisGraph.selectElementContents(txtNode);
398 | txtNode.focus();
399 | } else{
400 | if (state.selectedEdge){
401 | thisGraph.removeSelectFromEdge();
402 | }
403 | var prevNode = state.selectedNode;
404 |
405 | if (!prevNode || prevNode.id !== d.id){
406 | thisGraph.replaceSelectNode(d3node, d);
407 | } else{
408 | thisGraph.removeSelectFromNode();
409 | }
410 | }
411 | }
412 | }
413 | state.mouseDownNode = null;
414 | return;
415 |
416 | }; // end of circles mouseup
417 |
418 | // mousedown on main svg
419 | GraphCreator.prototype.svgMouseDown = function(){
420 | this.state.graphMouseDown = true;
421 | };
422 |
423 | // mouseup on main svg
424 | GraphCreator.prototype.svgMouseUp = function(){
425 | var thisGraph = this,
426 | state = thisGraph.state;
427 | if (state.justScaleTransGraph) {
428 | // dragged not clicked
429 | state.justScaleTransGraph = false;
430 | } else if (state.graphMouseDown && d3.event.shiftKey){
431 | // clicked not dragged from svg
432 | var xycoords = d3.mouse(thisGraph.svgG.node()),
433 | d = {id: thisGraph.idct++, title: consts.defaultTitle, x: xycoords[0], y: xycoords[1]};
434 | thisGraph.nodes.push(d);
435 | thisGraph.updateGraph();
436 | // make title of text immediently editable
437 | var d3txt = thisGraph.changeTextOfNode(thisGraph.circles.filter(function(dval){
438 | return dval.id === d.id;
439 | }), d),
440 | txtNode = d3txt.node();
441 | thisGraph.selectElementContents(txtNode);
442 | txtNode.focus();
443 | } else if (state.shiftNodeDrag){
444 | // dragged from node
445 | state.shiftNodeDrag = false;
446 | thisGraph.dragLine.classed("hidden", true);
447 | }
448 | state.graphMouseDown = false;
449 | };
450 |
451 | // keydown on main svg
452 | GraphCreator.prototype.svgKeyDown = function() {
453 | var thisGraph = this,
454 | state = thisGraph.state,
455 | consts = thisGraph.consts;
456 | // make sure repeated key presses don't register for each keydown
457 | if(state.lastKeyDown !== -1) return;
458 |
459 | state.lastKeyDown = d3.event.keyCode;
460 | var selectedNode = state.selectedNode,
461 | selectedEdge = state.selectedEdge;
462 |
463 | switch(d3.event.keyCode) {
464 | case consts.BACKSPACE_KEY:
465 | case consts.DELETE_KEY:
466 | d3.event.preventDefault();
467 | if (selectedNode){
468 | thisGraph.nodes.splice(thisGraph.nodes.indexOf(selectedNode), 1);
469 | thisGraph.spliceLinksForNode(selectedNode);
470 | state.selectedNode = null;
471 | thisGraph.updateGraph();
472 | } else if (selectedEdge){
473 | thisGraph.edges.splice(thisGraph.edges.indexOf(selectedEdge), 1);
474 | state.selectedEdge = null;
475 | thisGraph.updateGraph();
476 | }
477 | break;
478 | }
479 | };
480 |
481 | GraphCreator.prototype.svgKeyUp = function() {
482 | this.state.lastKeyDown = -1;
483 | };
484 |
485 | // call to propagate changes to graph
486 | GraphCreator.prototype.updateGraph = function(){
487 |
488 | var thisGraph = this,
489 | consts = thisGraph.consts,
490 | state = thisGraph.state;
491 |
492 | thisGraph.paths = thisGraph.paths.data(thisGraph.edges, function(d){
493 | return String(d.source.id) + "+" + String(d.target.id);
494 | });
495 | var paths = thisGraph.paths;
496 | // update existing paths
497 | paths.style('marker-end', 'url(#end-arrow)')
498 | .classed(consts.selectedClass, function(d){
499 | return d === state.selectedEdge;
500 | })
501 | .attr("d", function(d){
502 | return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y;
503 | });
504 |
505 | // add new paths
506 | paths.enter()
507 | .append("path")
508 | .style('marker-end','url(#end-arrow)')
509 | .classed("link", true)
510 | .attr("d", function(d){
511 | return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y;
512 | })
513 | .on("mousedown", function(d){
514 | thisGraph.pathMouseDown.call(thisGraph, d3.select(this), d);
515 | }
516 | )
517 | .on("mouseup", function(d){
518 | state.mouseDownLink = null;
519 | });
520 |
521 | // remove old links
522 | paths.exit().remove();
523 |
524 | // update existing nodes
525 | thisGraph.circles = thisGraph.circles.data(thisGraph.nodes, function(d){ return d.id;});
526 | thisGraph.circles.attr("transform", function(d){return "translate(" + d.x + "," + d.y + ")";});
527 |
528 | // add new nodes
529 | var newGs= thisGraph.circles.enter()
530 | .append("g");
531 |
532 | newGs.classed(consts.circleGClass, true)
533 | .attr("transform", function(d){return "translate(" + d.x + "," + d.y + ")";})
534 | .on("mouseover", function(d){
535 | if (state.shiftNodeDrag){
536 | d3.select(this).classed(consts.connectClass, true);
537 | }
538 | })
539 | .on("mouseout", function(d){
540 | d3.select(this).classed(consts.connectClass, false);
541 | })
542 | .on("mousedown", function(d){
543 | thisGraph.circleMouseDown.call(thisGraph, d3.select(this), d);
544 | })
545 | .on("mouseup", function(d){
546 | thisGraph.circleMouseUp.call(thisGraph, d3.select(this), d);
547 | })
548 | .call(thisGraph.drag);
549 |
550 | newGs.append("circle")
551 | .attr("r", String(consts.nodeRadius));
552 |
553 | newGs.each(function(d){
554 | thisGraph.insertTitleLinebreaks(d3.select(this), d.title);
555 | });
556 |
557 | // remove old nodes
558 | thisGraph.circles.exit().remove();
559 | };
560 |
561 | GraphCreator.prototype.zoomed = function(){
562 | this.state.justScaleTransGraph = true;
563 | d3.select("." + this.consts.graphClass)
564 | .attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
565 | };
566 |
567 | GraphCreator.prototype.updateWindow = function(svg){
568 | var docEl = document.documentElement,
569 | bodyEl = document.getElementsByTagName('body')[0];
570 | var x = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth;
571 | var y = window.innerHeight|| docEl.clientHeight|| bodyEl.clientHeight;
572 | svg.attr("width", x).attr("height", y);
573 | };
574 |
575 |
576 |
577 | /**** MAIN ****/
578 |
579 | // warn the user when leaving
580 | window.onbeforeunload = function(){
581 | return "Make sure to save your graph locally before leaving :-)";
582 | };
583 |
584 | var docEl = document.documentElement,
585 | bodyEl = document.getElementsByTagName('body')[0];
586 |
587 | var width = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth,
588 | height = window.innerHeight|| docEl.clientHeight|| bodyEl.clientHeight;
589 |
590 | var xLoc = width/2 - 25,
591 | yLoc = 100;
592 |
593 | // initial node data
594 | var nodes = [];
595 | var edges = [];
596 |
597 |
598 | /** MAIN SVG **/
599 | var svg = d3.select(settings.appendElSpec).append("svg")
600 | .attr("width", width)
601 | .attr("height", height);
602 | var graph = new GraphCreator(svg, nodes, edges);
603 | graph.setIdCt(2);
604 | graph.updateGraph();
605 | })(window.d3, window.saveAs, window.Blob);
606 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cjrd/directed-graph-creator/01c83e88b227ec8451798f6e5385f9b1294bfd36/thumbnail.png
--------------------------------------------------------------------------------
/trash-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cjrd/directed-graph-creator/01c83e88b227ec8451798f6e5385f9b1294bfd36/trash-icon.png
--------------------------------------------------------------------------------
/upload-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cjrd/directed-graph-creator/01c83e88b227ec8451798f6e5385f9b1294bfd36/upload-icon.png
--------------------------------------------------------------------------------