Created
September 2, 2018 19:10
-
-
Save ghamarian/45b474bd5adaafb210ee75a54ba5259e to your computer and use it in GitHub Desktop.
force layout
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| document.onload = (function (d3, saveAs, Blob, undefined) { | |
| "use strict"; | |
| // define graphcreator object | |
| let GraphCreator = function (svg, nodes, edges) { | |
| let thisGraph = this; | |
| thisGraph.idct = 0; | |
| thisGraph.nodes = nodes || []; | |
| thisGraph.edges = edges || []; | |
| thisGraph.state = { | |
| selectedNode: null, | |
| selectedEdge: null, | |
| mouseDownNode: null, | |
| mouseEnterNode: null, | |
| mouseDownLink: null, | |
| justDragged: false, | |
| justScaleTransGraph: false, | |
| lastKeyDown: -1, | |
| shiftNodeDrag: false, | |
| selectedText: null | |
| }; | |
| // define arrow markers for graph links | |
| let defs = svg.append('svg:defs'); | |
| defs.append('svg:marker') | |
| .attr('id', 'end-arrow') | |
| .attr('viewBox', '0 -5 10 10') | |
| .attr('refX', "32") | |
| .attr('markerWidth', 3.5) | |
| .attr('markerHeight', 3.5) | |
| .attr('orient', 'auto') | |
| .append('svg:path') | |
| .attr('d', 'M0,-5L10,0L0,5'); | |
| // define arrow markers for leading arrow | |
| defs.append('svg:marker') | |
| .attr('id', 'mark-end-arrow') | |
| .attr('viewBox', '0 -5 10 10') | |
| .attr('refX', 7) | |
| .attr('markerWidth', 3.5) | |
| .attr('markerHeight', 3.5) | |
| .attr('orient', 'auto') | |
| .append('svg:path') | |
| .attr('d', 'M0,-5L10,0L0,5'); | |
| thisGraph.svg = svg; | |
| thisGraph.svgG = svg.append("g") | |
| .classed(thisGraph.consts.graphClass, true); | |
| let svgG = thisGraph.svgG; | |
| // displayed when dragging between nodes | |
| thisGraph.dragLine = svgG.append('svg:path') | |
| .attr('class', 'link dragline hidden') | |
| .attr('d', 'M0,0L0,0') | |
| .style('marker-end', 'url(#mark-end-arrow)'); | |
| // svg nodes and edges | |
| thisGraph.paths = svgG.append("g").selectAll("g"); | |
| thisGraph.circles = svgG.append("g").selectAll("g"); | |
| thisGraph.drag = d3.drag() | |
| .subject(function (d) { | |
| return {x: d.x, y: d.y}; | |
| }) | |
| .on("drag", function (args) { | |
| thisGraph.state.justDragged = true; | |
| thisGraph.dragmove.call(thisGraph, args); | |
| }) | |
| .on("end", function (d) { | |
| // todo check if edge-mode is selected | |
| var mouse = d3.mouse(this); | |
| var elem = document.elementFromPoint(mouse[0], mouse[1]); | |
| if (thisGraph.state.shiftNodeDrag) { | |
| thisGraph.dragEnd.call(thisGraph, d3.select(this), thisGraph.state.mouseEnterNode) | |
| } | |
| }); | |
| // listen for key events | |
| d3.select(window).on("keydown", function () { | |
| thisGraph.svgKeyDown.call(thisGraph); | |
| }) | |
| .on("keyup", function () { | |
| thisGraph.svgKeyUp.call(thisGraph); | |
| }); | |
| svg.on("mousedown", function (d) { | |
| thisGraph.svgMouseDown.call(thisGraph, d); | |
| if (d3.event.shiftKey) { | |
| d3.event.stopImmediatePropagation(); | |
| $('body').css('cursor', 'cell'); | |
| } | |
| }); | |
| svg.on("mouseup", function (d) { | |
| thisGraph.svgMouseUp.call(thisGraph, d); | |
| }); | |
| // listen for dragging | |
| let dragSvg = d3.zoom() | |
| .on("zoom", function () { | |
| if (d3.event.sourceEvent.shiftKey) { | |
| // TODO the internal d3 state is still changing | |
| return false; | |
| } else { | |
| thisGraph.zoomed.call(thisGraph); | |
| } | |
| return true; | |
| }) | |
| .on("start", function () { | |
| var ael = d3.select("#" + thisGraph.consts.activeEditId).node(); | |
| if (ael) { | |
| ael.blur(); | |
| } | |
| if (!d3.event.sourceEvent.shiftKey) d3.select('body').style("cursor", "move"); | |
| }) | |
| .on("end", function () { | |
| d3.select('body').style("cursor", "auto"); | |
| }); | |
| svg.call(dragSvg).on("dblclick.zoom", null); | |
| // listen for resize | |
| window.onresize = function () { | |
| thisGraph.updateWindow(svg); | |
| }; | |
| // handle download data | |
| d3.select("#download-input").on("click", function () { | |
| let saveEdges = []; | |
| thisGraph.edges.forEach(function (val, i) { | |
| saveEdges.push({source: val.source.id, target: val.target.id}); | |
| }); | |
| let blob = new Blob([window.JSON.stringify({ | |
| "nodes": thisGraph.nodes, | |
| "edges": saveEdges | |
| })], {type: "text/plain;charset=utf-8"}); | |
| saveAs(blob, "mydag.json"); | |
| }); | |
| // handle uploaded data | |
| d3.select("#upload-input").on("click", function () { | |
| document.getElementById("hidden-file-upload").click(); | |
| }); | |
| d3.select("#hidden-file-upload").on("change", function () { | |
| if (window.File && window.FileReader && window.FileList && window.Blob) { | |
| let uploadFile = this.files[0]; | |
| let filereader = new window.FileReader(); | |
| filereader.onload = function () { | |
| let txtRes = filereader.result; | |
| // TODO better error handling | |
| try { | |
| let jsonObj = JSON.parse(txtRes); | |
| thisGraph.deleteGraph(true); | |
| thisGraph.nodes = jsonObj.nodes; | |
| thisGraph.setIdCt(jsonObj.nodes.length + 1); | |
| let newEdges = jsonObj.edges; | |
| newEdges.forEach(function (e, i) { | |
| newEdges[i] = { | |
| source: thisGraph.nodes.filter(function (n) { | |
| return n.id === e.source; | |
| })[0], | |
| target: thisGraph.nodes.filter(function (n) { | |
| return n.id === e.target; | |
| })[0] | |
| }; | |
| }); | |
| thisGraph.edges = newEdges; | |
| thisGraph.updateGraph(); | |
| } catch (err) { | |
| window.alert("Error parsing uploaded file\nerror message: " + err.message); | |
| return; | |
| } | |
| }; | |
| filereader.readAsText(uploadFile); | |
| } else { | |
| alert("Your browser won't let you save this graph -- try upgrading your browser to IE 10+ or Chrome or Firefox."); | |
| } | |
| }); | |
| // handle delete graph | |
| d3.select("#delete-graph").on("click", function () { | |
| thisGraph.deleteGraph(false); | |
| }); | |
| d3.select("div#chartId") | |
| .append("div") | |
| .classed("svg-container", true) //container class to make it responsive | |
| .append("svg") | |
| //responsive SVG needs these 2 attributes and no width and height attr | |
| .attr("preserveAspectRatio", "xMinYMin meet") | |
| .attr("viewBox", "0 0 600 400") | |
| //class to make it responsive | |
| .classed("svg-content-responsive", true); | |
| // thisGraph.d3cola = cola.d3adaptor() | |
| // .linkDistance(30) | |
| // .size([width, height]); | |
| // init D3 force layout | |
| thisGraph.force = d3.forceSimulation() | |
| .force('charge', d3.forceManyBody().strength(-500)) | |
| .force('link', d3.forceLink().id((d) => d.id).distance(500)) | |
| // .force('x', d3.forceX(width / 2)) | |
| // .force('y', d3.forceY(height / 2)) | |
| .on('tick', function(){return thisGraph.tick();}); | |
| }; | |
| GraphCreator.prototype.setIdCt = function (idct) { | |
| this.idct = idct; | |
| }; | |
| GraphCreator.prototype.consts = { | |
| selectedClass: "selected", | |
| connectClass: "connect-node", | |
| circleGClass: "conceptG", | |
| graphClass: "graph", | |
| activeEditId: "active-editing", | |
| BACKSPACE_KEY: 8, | |
| DELETE_KEY: 46, | |
| ENTER_KEY: 13, | |
| nodeRadius: 50 | |
| }; | |
| /* PROTOTYPE FUNCTIONS */ | |
| GraphCreator.prototype.dragmove = function (d) { | |
| let thisGraph = this; | |
| if (thisGraph.state.shiftNodeDrag) { | |
| thisGraph.dragLine.attr('d', 'M' + d.x + ',' + d.y + 'L' + d3.mouse(thisGraph.svgG.node())[0] + ',' + d3.mouse(this.svgG.node())[1]); | |
| } else { | |
| d.x += d3.event.dx; | |
| d.y += d3.event.dy; | |
| thisGraph.updateGraph(); | |
| } | |
| }; | |
| GraphCreator.prototype.deleteGraph = function (skipPrompt) { | |
| let thisGraph = this, | |
| doDelete = true; | |
| if (!skipPrompt) { | |
| doDelete = window.confirm("Press OK to delete this graph"); | |
| } | |
| if (doDelete) { | |
| thisGraph.nodes = []; | |
| thisGraph.edges = []; | |
| thisGraph.updateGraph(); | |
| } | |
| }; | |
| /* select all text in element: taken from http://stackoverflow.com/questions/6139107/programatically-select-text-in-a-contenteditable-html-element */ | |
| GraphCreator.prototype.selectElementContents = function (el) { | |
| let range = document.createRange(); | |
| range.selectNodeContents(el); | |
| let sel = window.getSelection(); | |
| sel.removeAllRanges(); | |
| sel.addRange(range); | |
| }; | |
| /* insert svg line breaks: taken from http://stackoverflow.com/questions/13241475/how-do-i-include-newlines-in-labels-in-d3-charts */ | |
| GraphCreator.prototype.insertTitleLinebreaks = function (gEl, title) { | |
| let words = title.split(/\s+/g), | |
| nwords = words.length; | |
| let el = gEl.append("text") | |
| .attr("text-anchor", "middle") | |
| .attr("dy", "-" + (nwords - 1) * 7.5); | |
| for (let i = 0; i < words.length; i++) { | |
| let tspan = el.append('tspan').text(words[i]); | |
| if (i > 0) | |
| tspan.attr('x', 0).attr('dy', '15'); | |
| } | |
| }; | |
| // remove edges associated with a node | |
| GraphCreator.prototype.spliceLinksForNode = function (node) { | |
| let thisGraph = this, | |
| toSplice = thisGraph.edges.filter(function (l) { | |
| return (l.source === node || l.target === node); | |
| }); | |
| toSplice.map(function (l) { | |
| thisGraph.edges.splice(thisGraph.edges.indexOf(l), 1); | |
| }); | |
| }; | |
| GraphCreator.prototype.replaceSelectEdge = function (d3Path, edgeData) { | |
| let thisGraph = this; | |
| d3Path.classed(thisGraph.consts.selectedClass, true); | |
| if (thisGraph.state.selectedEdge) { | |
| thisGraph.removeSelectFromEdge(); | |
| } | |
| thisGraph.state.selectedEdge = edgeData; | |
| }; | |
| GraphCreator.prototype.replaceSelectNode = function (d3Node, nodeData) { | |
| let thisGraph = this; | |
| d3Node.classed(this.consts.selectedClass, true); | |
| if (thisGraph.state.selectedNode) { | |
| thisGraph.removeSelectFromNode(); | |
| } | |
| thisGraph.state.selectedNode = nodeData; | |
| // TODO | |
| clear_properties(); | |
| thisGraph.print_inputs(nodeData); | |
| // $('.properties label').text(JSON.stringify(nodeData)); | |
| }; | |
| GraphCreator.prototype.removeSelectFromNode = function () { | |
| let thisGraph = this; | |
| thisGraph.circles.filter(function (cd) { | |
| return cd.id === thisGraph.state.selectedNode.id; | |
| }).classed(thisGraph.consts.selectedClass, false); | |
| thisGraph.state.selectedNode = null; | |
| }; | |
| GraphCreator.prototype.removeSelectFromEdge = function () { | |
| let thisGraph = this; | |
| thisGraph.paths.filter(function (cd) { | |
| return cd === thisGraph.state.selectedEdge; | |
| }).classed(thisGraph.consts.selectedClass, false); | |
| thisGraph.state.selectedEdge = null; | |
| }; | |
| GraphCreator.prototype.pathMouseDown = function (d3path, d) { | |
| let thisGraph = this, | |
| state = thisGraph.state; | |
| // d3.event.stopPropagation(); | |
| state.mouseDownLink = d; | |
| if (state.selectedNode) { | |
| thisGraph.removeSelectFromNode(); | |
| } | |
| let prevEdge = state.selectedEdge; | |
| if (!prevEdge || prevEdge !== d) { | |
| thisGraph.replaceSelectEdge(d3path, d); | |
| } else { | |
| thisGraph.removeSelectFromEdge(); | |
| } | |
| }; | |
| // mousedown on node | |
| GraphCreator.prototype.circleMouseDown = function (d3node, d) { | |
| let thisGraph = this, | |
| state = thisGraph.state; | |
| // d3.event.stopPropagation(); | |
| state.mouseDownNode = d; | |
| if (d3.event.shiftKey) { | |
| state.shiftNodeDrag = d3.event.shiftKey; | |
| // reposition dragged directed edge | |
| thisGraph.dragLine.classed('hidden', false) | |
| .attr('d', 'M' + d.x + ',' + d.y + 'L' + d.x + ',' + d.y); | |
| return; | |
| } | |
| }; | |
| /* place editable text on node in place of svg text */ | |
| GraphCreator.prototype.changeTextOfNode = function (d3node, d) { | |
| let thisGraph = this, | |
| consts = thisGraph.consts, | |
| htmlEl = d3node.node(); | |
| d3node.selectAll("text").remove(); | |
| let nodeBCR = htmlEl.getBoundingClientRect(), | |
| curScale = nodeBCR.width / consts.nodeRadius, | |
| placePad = 5 * curScale, | |
| useHW = curScale > 1 ? nodeBCR.width * 0.71 : consts.nodeRadius * 1.42; | |
| // replace with editableconent text | |
| let d3txt = thisGraph.svg.selectAll("foreignObject") | |
| .data([d]) | |
| .enter() | |
| .append("foreignObject") | |
| .attr("x", nodeBCR.left + placePad) | |
| .attr("y", nodeBCR.top + placePad) | |
| .attr("height", 2 * useHW) | |
| .attr("width", useHW) | |
| .append("xhtml:p") | |
| .attr("id", consts.activeEditId) | |
| .attr("contentEditable", "true") | |
| .text(d.title) | |
| .on("mousedown", function (d) { | |
| d3.event.stopPropagation(); | |
| }) | |
| .on("keydown", function (d) { | |
| d3.event.stopPropagation(); | |
| if (d3.event.keyCode == consts.ENTER_KEY && !d3.event.shiftKey) { | |
| this.blur(); | |
| } | |
| }) | |
| .on("blur", function (d) { | |
| d.title = this.textContent; | |
| thisGraph.insertTitleLinebreaks(d3node, d.title); | |
| d3.select(this.parentElement).remove(); | |
| }); | |
| return d3txt; | |
| }; | |
| GraphCreator.prototype.dragEnd = function (d3node, d) { | |
| let thisGraph = this, | |
| state = thisGraph.state, | |
| consts = thisGraph.consts; | |
| // reset the states | |
| state.shiftNodeDrag = false; | |
| d3node.classed(consts.connectClass, false); | |
| let mouseDownNode = state.mouseDownNode; | |
| let mouseEnterNode = state.mouseEnterNode; | |
| if (state.justDragged) { | |
| // dragged, not clicked | |
| state.justDragged = false; | |
| } | |
| thisGraph.dragLine.classed("hidden", true); | |
| if (!mouseDownNode || !mouseEnterNode) return; | |
| if (mouseDownNode !== d) { | |
| // we're in a different node: create new edge for mousedown edge and add to graph | |
| let newEdge = {source: mouseDownNode, target: d}; | |
| let filtRes = thisGraph.paths.filter(function (d) { | |
| if (d.source === newEdge.target && d.target === newEdge.source) { | |
| thisGraph.edges.splice(thisGraph.edges.indexOf(d), 1); | |
| } | |
| return d.source === newEdge.source && d.target === newEdge.target; | |
| }); | |
| if (!filtRes || !filtRes[0] || !filtRes[0].length) { | |
| thisGraph.edges.push(newEdge); | |
| thisGraph.updateGraph(); | |
| } | |
| } | |
| state.mouseDownNode = null; | |
| state.mouseEnterNode = null; | |
| return; | |
| }; | |
| GraphCreator.prototype.print_inputs = function (nodeData) { | |
| let thisGraph = this, | |
| state = thisGraph.state, | |
| consts = thisGraph.consts; | |
| for (let key in nodeData['parameters']) { | |
| let node_params = nodeData['parameters'][key]; | |
| switch (node_params['type']) { | |
| case 'select': | |
| add_input_select(key, node_params['options'], node_params['value']); | |
| break; | |
| case 'boolean': | |
| add_input_select(key, ['true', 'false'], node_params['value']); | |
| break; | |
| case "integer" : | |
| add_input_number(key, node_params, '1'); | |
| break; | |
| case "float" : | |
| add_input_number(key, node_params, 'any'); | |
| break; | |
| default: | |
| add_input(key, node_params['value'], node_params['type']); | |
| } | |
| } | |
| // TODO fix | |
| d3.selectAll("#properties select").on("change", function () { | |
| for (let i in thisGraph.nodes) { | |
| if (thisGraph.nodes[i].id === thisGraph.state.selectedNode.id) { | |
| thisGraph.nodes[i].parameters[this.id].value = this.value; | |
| } | |
| } | |
| }); | |
| d3.selectAll("#properties input").on("change", function () { | |
| for (let i in thisGraph.nodes) { | |
| if (thisGraph.nodes[i].id === thisGraph.state.selectedNode.id) { | |
| thisGraph.nodes[i].parameters[this.id].value = this.value; | |
| } | |
| } | |
| } | |
| ); | |
| } | |
| ; | |
| // mouseup on nodes | |
| GraphCreator.prototype.circleMouseUp = function (d3node, d) { | |
| let thisGraph = this, | |
| state = thisGraph.state, | |
| consts = thisGraph.consts; | |
| // reset the states | |
| state.shiftNodeDrag = false; | |
| d3node.classed(consts.connectClass, false); | |
| if (d3.event.shiftKey) { | |
| // shift-clicked node: edit text content | |
| let d3txt = thisGraph.changeTextOfNode(d3node, d); | |
| let txtNode = d3txt.node(); | |
| thisGraph.selectElementContents(txtNode); | |
| txtNode.focus(); | |
| } else { | |
| if (state.selectedEdge) { | |
| thisGraph.removeSelectFromEdge(); | |
| } | |
| } | |
| let prevNode = state.selectedNode; | |
| if (!prevNode || prevNode.id !== d.id) { | |
| thisGraph.replaceSelectNode(d3node, d); | |
| } else { | |
| thisGraph.removeSelectFromNode(); | |
| } | |
| }; // end of circles mouseup | |
| // mousedown on main svg | |
| GraphCreator.prototype.svgMouseDown = function () { | |
| this.state.graphMouseDown = true; | |
| }; | |
| // mouseup on main svg | |
| GraphCreator.prototype.svgMouseUp = function () { | |
| let thisGraph = this, | |
| state = thisGraph.state; | |
| if (state.justScaleTransGraph) { | |
| // dragged not clicked | |
| state.justScaleTransGraph = false; | |
| } else if (state.graphMouseDown && d3.event.shiftKey) { | |
| // clicked not dragged from svg | |
| // #TODO | |
| console.log('adding ' + d3.select('input[name="node"]:checked').node().value); | |
| let type = d3.select('input[name="node"]:checked').node().value; | |
| let xycoords = d3.mouse(thisGraph.svgG.node()), | |
| selected_item = d3.select('input[name="node"]:checked').node().value, | |
| d = { | |
| id: thisGraph.idct++, | |
| title: selected_item, | |
| x: xycoords[0], | |
| y: xycoords[1], | |
| parameters: $.extend(true, {}, core_layers[type]) | |
| }; | |
| console.log(d); | |
| thisGraph.nodes.push(d); | |
| thisGraph.updateGraph(); | |
| // make title of text immediently editable | |
| let d3txt = thisGraph.changeTextOfNode(thisGraph.circles.filter(function (dval) { | |
| return dval.id === d.id; | |
| }), d), | |
| txtNode = d3txt.node(); | |
| thisGraph.selectElementContents(txtNode); | |
| txtNode.focus(); | |
| } else if (state.shiftNodeDrag) { | |
| // dragged from node | |
| state.shiftNodeDrag = false; | |
| thisGraph.dragLine.classed("hidden", true); | |
| } | |
| state.graphMouseDown = false; | |
| }; | |
| // keydown on main svg | |
| GraphCreator.prototype.svgKeyDown = function () { | |
| let thisGraph = this, | |
| state = thisGraph.state, | |
| consts = thisGraph.consts; | |
| // make sure repeated key presses don't register for each keydown | |
| if (state.lastKeyDown !== -1) return; | |
| state.lastKeyDown = d3.event.keyCode; | |
| let selectedNode = state.selectedNode, | |
| selectedEdge = state.selectedEdge; | |
| switch (d3.event.keyCode) { | |
| case consts.BACKSPACE_KEY: | |
| case consts.DELETE_KEY: | |
| if ($(d3.event.srcElement).is("input")) { | |
| break; | |
| } | |
| d3.event.preventDefault(); | |
| if (selectedNode) { | |
| thisGraph.nodes.splice(thisGraph.nodes.indexOf(selectedNode), 1); | |
| thisGraph.spliceLinksForNode(selectedNode); | |
| state.selectedNode = null; | |
| thisGraph.updateGraph(); | |
| } else if (selectedEdge) { | |
| thisGraph.edges.splice(thisGraph.edges.indexOf(selectedEdge), 1); | |
| state.selectedEdge = null; | |
| thisGraph.updateGraph(); | |
| } | |
| break; | |
| } | |
| }; | |
| GraphCreator.prototype.svgKeyUp = function () { | |
| this.state.lastKeyDown = -1; | |
| }; | |
| // call to propagate changes to graph | |
| GraphCreator.prototype.updateGraph = function () { | |
| let thisGraph = this, | |
| consts = thisGraph.consts, | |
| state = thisGraph.state; | |
| thisGraph.paths = thisGraph.paths.data(thisGraph.edges, function (d) { | |
| return String(d.source.id) + "+" + String(d.target.id); | |
| }); | |
| let paths = thisGraph.paths; | |
| // update existing paths | |
| paths.style('marker-end', 'url(#end-arrow)') | |
| .classed(consts.selectedClass, function (d) { | |
| return d === state.selectedEdge; | |
| }) | |
| // .attr("d", line([d.source.x, d.source.y, d.target.x, d.target.y])); | |
| .attr("d", function (d) { | |
| var da = []; | |
| var middle = {x: (d.source.x + d.target.x) / 2, y: (d.source.y + d.target.y) / 1.7}; | |
| if (d.source.x < d.target.x) | |
| da = [{x: d.source.x, y: d.source.y}, {x: (d.target.x - consts.nodeRadius), y: d.target.y}]; | |
| else { | |
| da = [{x: d.source.x, y: d.source.y}, {x: (d.target.x + consts.nodeRadius), y: d.target.y}]; | |
| } | |
| // if (Math.abs(d.source.x - d.target.x) > 300) { | |
| // da.splice(1, 0, middle); | |
| // } | |
| var myline = d3.line().curve(d3.curveMonotoneX).x(d => d.x).y(d => d.y); | |
| return myline(da); | |
| }); | |
| // remove old links | |
| paths.exit().remove(); | |
| // add new paths | |
| paths = paths.enter() | |
| .append("path") | |
| .style('marker-end', 'url(#end-arrow)') | |
| .classed("link", true) | |
| .attr("d", function (d) { | |
| var da = []; | |
| var middle = {x: (d.source.x + d.target.x) / 2, y: (d.source.y + d.target.y) / 1.7}; | |
| if (d.source.x < d.target.x) | |
| da = [{x: d.source.x, y: d.source.y}, {x: (d.target.x - consts.nodeRadius), y: d.target.y}]; | |
| else { | |
| da = [{x: d.source.x, y: d.source.y}, {x: (d.target.x + consts.nodeRadius), y: d.target.y}]; | |
| } | |
| // if (Math.abs(d.source.x - d.target.x) > 300) { | |
| // da.splice(1, 0, middle); | |
| // } | |
| var myline = d3.line().curve(d3.curveMonotoneX).x(d => d.x).y(d => d.y); | |
| return myline(da); | |
| }) | |
| .merge(paths) | |
| .on("mouseup", function (d) { | |
| // state.mouseDownLink = null; | |
| }) | |
| .on("mousedown", function (d) { | |
| thisGraph.pathMouseDown.call(thisGraph, d3.select(this), d); | |
| } | |
| ); | |
| thisGraph.paths = paths; | |
| // update existing nodes | |
| thisGraph.circles = thisGraph.circles.data(thisGraph.nodes, function (d) { | |
| return d.id; | |
| }); | |
| // remove old nodes | |
| thisGraph.circles.exit().remove(); | |
| thisGraph.circles.attr("transform", function (d) { | |
| return "translate(" + d.x + "," + d.y + ")"; | |
| }); | |
| // add new nodes | |
| let newGs = thisGraph.circles.enter() | |
| .append("g").merge(thisGraph.circles); | |
| newGs.classed(consts.circleGClass, true) | |
| .attr("transform", function (d) { | |
| return "translate(" + d.x + "," + d.y + ")"; | |
| }) | |
| .on("mouseover", function (d) { | |
| state.mouseEnterNode = d; | |
| if (state.shiftNodeDrag) { | |
| d3.select(this).classed(consts.connectClass, true); | |
| } | |
| }) | |
| .on("mouseout", function (d) { | |
| state.mouseEnterNode = null; | |
| d3.select(this).classed(consts.connectClass, false); | |
| }) | |
| .on("mousedown", function (d) { | |
| thisGraph.circleMouseDown.call(thisGraph, d3.select(this), d); | |
| }) | |
| .call(thisGraph.drag) | |
| .on("click", function (d) { | |
| thisGraph.circleMouseUp.call(thisGraph, d3.select(this), d); | |
| }); | |
| thisGraph.circles = newGs; | |
| // newGs.append("circle") | |
| // .attr("r", String(consts.nodeRadius)); | |
| newGs.append('rect') | |
| .attr("rx", 6) | |
| .attr("ry", 6) | |
| .attr("x", d => -2 * consts.nodeRadius) | |
| .attr("y", d => -consts.nodeRadius) | |
| .attr("width", String(4 * consts.nodeRadius)) | |
| .attr("height", String(2 * consts.nodeRadius)); | |
| newGs.each(function (d) { | |
| thisGraph.insertTitleLinebreaks(d3.select(this), d.title); | |
| }); | |
| thisGraph.force.nodes(thisGraph.nodes) | |
| .force('link').links(thisGraph.edges); | |
| thisGraph.force.alphaTarget(0.3).restart(); | |
| }; | |
| GraphCreator.prototype.tick = function () { | |
| // handles to link and node element groups | |
| let thisGraph = this; | |
| thisGraph.paths.attr('d', (d) => { | |
| const deltaX = d.target.x - d.source.x; | |
| const deltaY = d.target.y - d.source.y; | |
| const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY); | |
| const normX = deltaX / dist; | |
| const normY = deltaY / dist; | |
| const sourcePadding = d.left ? 17 : 12; | |
| const targetPadding = d.right ? 17 : 12; | |
| const sourceX = d.source.x + (sourcePadding * normX); | |
| const sourceY = d.source.y + (sourcePadding * normY); | |
| const targetX = d.target.x - (targetPadding * normX); | |
| const targetY = d.target.y - (targetPadding * normY); | |
| return `M${sourceX},${sourceY}L${targetX},${targetY}`; | |
| }); | |
| thisGraph.circles.attr('transform', (d) => `translate(${d.x},${d.y})`); | |
| }; | |
| GraphCreator.prototype.zoomed = function () { | |
| this.state.justScaleTransGraph = true; | |
| d3.select("." + this.consts.graphClass) | |
| // .attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")"); | |
| .attr("transform", d3.event.transform); | |
| }; | |
| GraphCreator.prototype.updateWindow = function (svg) { | |
| let docEl = document.documentElement, | |
| bodyEl = document.getElementsByTagName('body')[0]; | |
| let x = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth; | |
| let y = window.innerHeight || docEl.clientHeight || bodyEl.clientHeight; | |
| svg.attr("width", x).attr("height", y); | |
| }; | |
| /**** MAIN ****/ | |
| // warn the user when leaving | |
| window.onbeforeunload = function () { | |
| return "Make sure to save your graph locally before leaving :-)"; | |
| }; | |
| let docEl = document.documentElement, | |
| bodyEl = document.getElementsByTagName('body')[0]; | |
| let width = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth, | |
| height = window.innerHeight || docEl.clientHeight || bodyEl.clientHeight; | |
| let xLoc = width / 2 - 25, | |
| yLoc = 100; | |
| // initial node data | |
| let nodes = [{title: "new concept", id: 0, x: xLoc, y: yLoc}, | |
| {title: "new concept", id: 1, x: xLoc, y: yLoc + 200}]; | |
| let edges = [{source: nodes[1], target: nodes[0]}]; | |
| /** MAIN SVG **/ | |
| let svg = d3.select(".svg-container").append("svg") | |
| .attr('viewBox', `87 0 ${width - 300} ${height}`) | |
| .attr("width", width - 300) | |
| .attr("height", height); | |
| let graph = new GraphCreator(svg, nodes, edges); | |
| var core_layers; | |
| graph.setIdCt(2); | |
| graph.updateGraph(); | |
| var promise = d3.json('./keraslayers'); | |
| promise.then(function (data) { | |
| core_layers = data; | |
| // TODO change to d3 | |
| for (var key in core_layers) { | |
| var $label = $('<label for="' + key + '">').text(core_layers[key]['class_name']); | |
| var $input = $('<input type="radio">').attr({id: key, name: 'node', value: key}); | |
| $('#form_radiob').append($input); | |
| $('#form_radiob').append($label); | |
| } | |
| }, | |
| function (error) { | |
| console.log('error loading core layers file'); | |
| }); | |
| function clear_properties() { | |
| let myNode = document.getElementsByClassName("properties")[0]; | |
| while (myNode.firstChild) { | |
| myNode.removeChild(myNode.firstChild); | |
| } | |
| } | |
| function add_input(label_name, default_value, type_input) { | |
| if (label_name != "class_name") { | |
| add_label(label_name); | |
| let in_t = d3.select(".properties").append("input") | |
| .attr("type", type_input) | |
| .attr("id", label_name) | |
| .attr("name", label_name) | |
| .attr("value", default_value); | |
| } | |
| } | |
| function add_input_number(label_name, node_params, step) { | |
| add_label(label_name); | |
| let in_n = d3.select(".properties").append("input") | |
| .attr("type", "number") | |
| .attr("id", label_name) | |
| .attr("name", label_name) | |
| .attr("value", node_params['value']) | |
| .attr("min", node_params['min']) | |
| .attr("step", step); | |
| let lab_err = d3.select(".properties").append("span") | |
| .attr("classList", "validity"); | |
| } | |
| function add_input_select(label_name, data, value) { | |
| add_label(label_name); | |
| var select = d3.select('.properties') | |
| .append('select') | |
| .attr('id', label_name) | |
| .attr('class', 'select') | |
| .on('change', onchange); | |
| var options = select | |
| .selectAll('option') | |
| .data(data).enter() | |
| .append('option') | |
| .text(function (d) { | |
| return d; | |
| }); | |
| d3.select('#' + label_name).property('value', value); | |
| function onchange() { | |
| var selectValue = d3.select('select').property('value'); | |
| }; | |
| } | |
| function add_label(label_name) { | |
| let label = document.createTextNode(label_name); | |
| document.getElementsByClassName("properties")[0].appendChild(label); | |
| } | |
| }) | |
| (window.d3, window.saveAs, window.Blob); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment