├── 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 | Metacademy Logo 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 |
9 |
10 | 11 |
12 |
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 --------------------------------------------------------------------------------