Last active
September 30, 2017 00:58
-
-
Save ofersadgat/382c2c96ae8a83317c4f4aa1dd39fd8c to your computer and use it in GitHub Desktop.
Computed Updates
This file contains 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
import Ember from 'ember'; | |
import GraphCreator from '../lib/GraphCreator'; | |
const style = ` | |
body{ | |
margin: 0; | |
padding: 0; | |
overflow:hidden; | |
} | |
p{ | |
text-align: center; | |
overflow: overlay; | |
position: relative; | |
} | |
body{ | |
-webkit-touch-callout: none; | |
-webkit-user-select: none; | |
-khtml-user-select: none; | |
-moz-user-select: none; | |
-ms-user-select: none; | |
user-select: none; | |
background-color: rgb(248, 248, 248) | |
} | |
#toolbox{ | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
margin-bottom: 0.5em; | |
margin-left: 1em; | |
border: 2px solid #EEEEEE; | |
border-radius: 5px; | |
padding: 1em; | |
z-index: 5; | |
} | |
#toolbox input{ | |
width: 30px; | |
opacity: 0.4; | |
} | |
#toolbox input:hover{ | |
opacity: 1; | |
cursor: pointer; | |
} | |
#hidden-file-upload{ | |
display: none; | |
} | |
#download-input{ | |
margin: 0 0.5em; | |
} | |
.conceptG text{ | |
pointer-events: none; | |
} | |
marker{ | |
fill: #333; | |
} | |
g.conceptG circle{ | |
fill: #F6FBFF; | |
stroke: #333; | |
stroke-width: 2px; | |
} | |
g.conceptG:hover circle{ | |
fill: rgb(200, 238, 241); | |
} | |
g.selected circle{ | |
fill: rgb(250, 232, 255); | |
} | |
g.selected:hover circle{ | |
fill: rgb(250, 232, 255); | |
} | |
path.link { | |
fill: none; | |
stroke: #333; | |
stroke-width: 6px; | |
cursor: default; | |
} | |
path.link:hover{ | |
stroke: rgb(94, 196, 204); | |
} | |
g.connect-node circle{ | |
fill: #BEFFFF; | |
} | |
path.link.hidden{ | |
stroke-width: 0; | |
} | |
path.link.selected { | |
stroke: rgb(229, 172, 247); | |
} | |
`; | |
export default Ember.Component.extend({ | |
classNames: ['graph-creator-component'], | |
attributeBindings: ['edgeCount'], | |
didInsertElement() { | |
this._super(...arguments); | |
console.log('didInsertElement'); | |
Ember.$('head').append(`<style>${style}</style>`); | |
this.renderGraph(); | |
}, | |
edgeCount: Ember.computed('edges.[]', function(){ | |
return (this.get('edges') || []).length; | |
}), | |
renderGraph: function(){ | |
console.log('rendering fn'); | |
var docEl = document.documentElement, | |
bodyEl = document.getElementsByTagName('body')[0]; | |
var width = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth, | |
height = window.innerHeight|| docEl.clientHeight|| bodyEl.clientHeight; | |
var xLoc = width/2 - 25, | |
yLoc = 100; | |
var svg = d3.select(".graph-creator-component").append("svg") | |
.attr("width", width) | |
.attr("height", height); | |
var nodes = this.get('nodes'); | |
var edges = this.get('edges'); | |
console.log('nodes: ', nodes, 'edges:', edges); | |
var graph = new GraphCreator(svg, nodes, edges); | |
graph.setIdCt(2); | |
graph.updateGraph(); | |
this.graph = graph; | |
}, | |
onNodesChanged: Ember.observer('nodes', 'edges', function(){ | |
Ember.run.once(this, function(){ | |
if (this.isDestroying || this.isDestroyed || !this.graph){ | |
return; | |
} | |
console.log('rendering'); | |
this.renderGraph(); | |
return; | |
var nodes = this.get('nodes'); | |
var edges = this.get('edges'); | |
this.graph.nodes = nodes; | |
this.graph.edges = edges; | |
this.graph.updateGraph(); | |
}); | |
}), | |
}); |
This file contains 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
import Ember from 'ember'; | |
String.prototype.hashCode = function() { | |
var hash = 0, i, chr; | |
if (this.length === 0) return hash; | |
for (i = 0; i < this.length; i++) { | |
chr = this.charCodeAt(i); | |
hash = ((hash << 5) - hash) + chr; | |
hash |= 0; // Convert to 32bit integer | |
} | |
return hash; | |
}; | |
const Data = Ember.Object.extend({ | |
DataA: 0, | |
DataB: 0, | |
DataC: 0, | |
ComputedPropertyA: Ember.computed('DataA', function(){ | |
return this.get('DataA') + 1; | |
}), | |
ComputedPropertyB: Ember.computed('DataB', 'ComputedPropertyA', function(){ | |
return this.get('ComputedPropertyA') + this.get('DataB'); | |
}), | |
ComputedArrayA: Ember.computed('ComputedPropertyB', function(){ | |
return [this.get('ComputedPropertyB')]; | |
}), | |
ComputedPropertyC: Ember.computed('ComputedPropertyA', 'DataC', function(){ | |
return this.get('ComputedPropertyA') + this.get('DataB'); | |
}), | |
ComputedPropertyD: Ember.computed('ComputedPropertyC', function(){ | |
return this.get('ComputedPropertyC') + 1; | |
}), | |
}); | |
const ComputedData = Ember.Object.extend({ | |
dirty: true, | |
dependentKeys: [], | |
references: [], | |
children: [], | |
shouldUpdate: Ember.computed('dirty', '[email protected]', function(){ | |
var references = this.get('references'); | |
var i; | |
for (i = 0; i < references.length; i++){ | |
if (references[i].get('dirty')){ | |
return false; | |
} | |
} | |
return this.get('dirty'); | |
}), | |
}); | |
const VersionedData = Ember.Mixin.create({ | |
init: function() { | |
this._super.apply(this, arguments); | |
var prop, i; | |
var data = Ember.Object.create({}); | |
var computedProperties = {}; | |
for (prop in this) { | |
var value = this[prop]; | |
if (typeof value !== 'object'){ | |
continue; | |
} | |
if (value.constructor === Ember.ComputedProperty){ | |
data[prop] = Ember.computed(function(){}); | |
data[prop]._getter = value._getter; | |
var computed = ComputedData.extend({ | |
value: Ember.computed.alias('object.' + prop), | |
}).create({ | |
object: data, | |
property: prop, | |
dependentKeys: value._dependentKeys || [], | |
}); | |
computedProperties[prop] = computed; | |
} | |
} | |
Object.keys(computedProperties).forEach(function(computedName){ | |
var computed = computedProperties[computedName]; | |
computed.dependentKeys.forEach(function(key){ | |
var obj = computedProperties[key]; | |
if (obj){ | |
obj.children.pushObject(computed); | |
computed.references.pushObject(obj); | |
} | |
}); | |
}); | |
this.set('computedProperties', computedProperties); | |
}, | |
}); | |
export default Ember.Controller.extend({ | |
appName: 'Ember Twiddle', | |
data: Data.extend(VersionedData).create({}), | |
nodes: Ember.computed('data.computedProperties', function(){ | |
var props = Object.values(this.get('data.computedProperties') || {}); | |
var computeds = props.map(function(prop){ | |
return { | |
title: prop.get('property'), | |
id: prop.get('property').hashCode(), | |
datum: prop, | |
x: 0, | |
y: 0, | |
}; | |
}); | |
props.forEach(function(prop){ | |
(Ember.get(prop, 'dependentKeys') || []).forEach(function(dependentKey){ | |
var dependent = computeds.findBy('title', dependentKey); | |
if (!dependent){ | |
computeds.pushObject({ | |
title: dependentKey, | |
id: dependentKey.hashCode(), | |
x: 0, | |
y: 0, | |
}); | |
} | |
}); | |
}); | |
return computeds; | |
}), | |
edges: Ember.computed('nodes', function(){ | |
var nodes = this.get('nodes') || []; | |
var result = []; | |
console.log('edges'); | |
nodes.forEach(function(node){ | |
if (!node.datum){ | |
return; | |
} | |
(Ember.get(node.datum, 'dependentKeys') || []).forEach(function(dependentKey){ | |
result.pushObject({ | |
source: node, | |
target: nodes.findBy('title', dependentKey), | |
}); | |
}); | |
}); | |
return result; | |
}), | |
onNodeSelected: Ember.observer('selectedNode', function(){ | |
console.log('selected:', this.get('selectedNode')); | |
}), | |
actions: { | |
step: function(){ | |
this.set('step', this.get('step') + 1); | |
} | |
} | |
}); |
This file contains 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
// define graphcreator object | |
var GraphCreator = function(svg, nodes, edges){ | |
var thisGraph = this; | |
thisGraph.idct = 0; | |
thisGraph.nodes = nodes || []; | |
thisGraph.edges = edges || []; | |
thisGraph.state = { | |
selectedNode: null, | |
selectedEdge: null, | |
mouseDownNode: null, | |
mouseDownLink: null, | |
justDragged: false, | |
justScaleTransGraph: false, | |
lastKeyDown: -1, | |
shiftNodeDrag: false, | |
selectedText: null | |
}; | |
// define arrow markers for graph links | |
var 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); | |
var 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.behavior.drag() | |
.origin(function(d){ | |
return {x: d.x, y: d.y}; | |
}) | |
.on("drag", function(args){ | |
thisGraph.state.justDragged = true; | |
thisGraph.dragmove.call(thisGraph, args); | |
}) | |
.on("dragend", function() { | |
// todo check if edge-mode is selected | |
}); | |
// 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);}); | |
svg.on("mouseup", function(d){thisGraph.svgMouseUp.call(thisGraph, d);}); | |
// listen for dragging | |
var dragSvg = d3.behavior.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("zoomstart", 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("zoomend", 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(){ | |
var saveEdges = []; | |
thisGraph.edges.forEach(function(val, i){ | |
saveEdges.push({source: val.source.id, target: val.target.id}); | |
}); | |
var 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) { | |
var uploadFile = this.files[0]; | |
var filereader = new window.FileReader(); | |
filereader.onload = function(){ | |
var txtRes = filereader.result; | |
// TODO better error handling | |
try{ | |
var jsonObj = JSON.parse(txtRes); | |
thisGraph.deleteGraph(true); | |
thisGraph.nodes = jsonObj.nodes; | |
thisGraph.setIdCt(jsonObj.nodes.length + 1); | |
var 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); | |
}); | |
}; | |
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) { | |
var 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){ | |
var 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) { | |
var range = document.createRange(); | |
range.selectNodeContents(el); | |
var 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) { | |
var words = title.split(/\s+/g), | |
nwords = words.length; | |
var el = gEl.append("text") | |
.attr("text-anchor","middle") | |
.attr("dy", "-" + (nwords-1)*7.5); | |
for (var i = 0; i < words.length; i++) { | |
var 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) { | |
var 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){ | |
var thisGraph = this; | |
d3Path.classed(thisGraph.consts.selectedClass, true); | |
if (thisGraph.state.selectedEdge){ | |
thisGraph.removeSelectFromEdge(); | |
} | |
thisGraph.state.selectedEdge = edgeData; | |
}; | |
GraphCreator.prototype.replaceSelectNode = function(d3Node, nodeData){ | |
var thisGraph = this; | |
d3Node.classed(this.consts.selectedClass, true); | |
if (thisGraph.state.selectedNode){ | |
thisGraph.removeSelectFromNode(); | |
} | |
thisGraph.state.selectedNode = nodeData; | |
}; | |
GraphCreator.prototype.removeSelectFromNode = function(){ | |
var 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(){ | |
var 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){ | |
var thisGraph = this, | |
state = thisGraph.state; | |
d3.event.stopPropagation(); | |
state.mouseDownLink = d; | |
if (state.selectedNode){ | |
thisGraph.removeSelectFromNode(); | |
} | |
var prevEdge = state.selectedEdge; | |
if (!prevEdge || prevEdge !== d){ | |
thisGraph.replaceSelectEdge(d3path, d); | |
} else{ | |
thisGraph.removeSelectFromEdge(); | |
} | |
}; | |
// mousedown on node | |
GraphCreator.prototype.circleMouseDown = function(d3node, d){ | |
var 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){ | |
var thisGraph= this, | |
consts = thisGraph.consts, | |
htmlEl = d3node.node(); | |
d3node.selectAll("text").remove(); | |
var 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 | |
var 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; | |
}; | |
// mouseup on nodes | |
GraphCreator.prototype.circleMouseUp = function(d3node, d){ | |
var thisGraph = this, | |
state = thisGraph.state, | |
consts = thisGraph.consts; | |
// reset the states | |
state.shiftNodeDrag = false; | |
d3node.classed(consts.connectClass, false); | |
var mouseDownNode = state.mouseDownNode; | |
if (!mouseDownNode) return; | |
thisGraph.dragLine.classed("hidden", true); | |
if (mouseDownNode !== d){ | |
// we're in a different node: create new edge for mousedown edge and add to graph | |
var newEdge = {source: mouseDownNode, target: d}; | |
var 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[0].length){ | |
thisGraph.edges.push(newEdge); | |
thisGraph.updateGraph(); | |
} | |
} else{ | |
// we're in the same node | |
if (state.justDragged) { | |
// dragged, not clicked | |
state.justDragged = false; | |
} else{ | |
// clicked, not dragged | |
if (d3.event.shiftKey){ | |
// shift-clicked node: edit text content | |
var d3txt = thisGraph.changeTextOfNode(d3node, d); | |
var txtNode = d3txt.node(); | |
thisGraph.selectElementContents(txtNode); | |
txtNode.focus(); | |
} else{ | |
if (state.selectedEdge){ | |
thisGraph.removeSelectFromEdge(); | |
} | |
var prevNode = state.selectedNode; | |
if (!prevNode || prevNode.id !== d.id){ | |
thisGraph.replaceSelectNode(d3node, d); | |
} else{ | |
thisGraph.removeSelectFromNode(); | |
} | |
} | |
} | |
} | |
state.mouseDownNode = null; | |
return; | |
}; // end of circles mouseup | |
// mousedown on main svg | |
GraphCreator.prototype.svgMouseDown = function(){ | |
this.state.graphMouseDown = true; | |
}; | |
// mouseup on main svg | |
GraphCreator.prototype.svgMouseUp = function(){ | |
var 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 | |
var xycoords = d3.mouse(thisGraph.svgG.node()), | |
d = {id: thisGraph.idct++, title: consts.defaultTitle, x: xycoords[0], y: xycoords[1]}; | |
thisGraph.nodes.push(d); | |
thisGraph.updateGraph(); | |
// make title of text immediently editable | |
var 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() { | |
var 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; | |
var selectedNode = state.selectedNode, | |
selectedEdge = state.selectedEdge; | |
switch(d3.event.keyCode) { | |
case consts.BACKSPACE_KEY: | |
case consts.DELETE_KEY: | |
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(){ | |
this.layout(); | |
var 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); | |
}); | |
var paths = thisGraph.paths; | |
// update existing paths | |
paths.style('marker-end', 'url(#end-arrow)') | |
.classed(consts.selectedClass, function(d){ | |
return d === state.selectedEdge; | |
}) | |
.attr("d", function(d){ | |
return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y; | |
}); | |
// add new paths | |
paths.enter() | |
.append("path") | |
.style('marker-end','url(#end-arrow)') | |
.classed("link", true) | |
.attr("d", function(d){ | |
return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y; | |
}) | |
.on("mousedown", function(d){ | |
thisGraph.pathMouseDown.call(thisGraph, d3.select(this), d); | |
} | |
) | |
.on("mouseup", function(d){ | |
state.mouseDownLink = null; | |
}); | |
// remove old links | |
paths.exit().remove(); | |
// update existing nodes | |
thisGraph.circles = thisGraph.circles.data(thisGraph.nodes, function(d){ return d.id;}); | |
thisGraph.circles.attr("transform", function(d){return "translate(" + d.x + "," + d.y + ")";}); | |
// add new nodes | |
var newGs= thisGraph.circles.enter() | |
.append("g"); | |
newGs.classed(consts.circleGClass, true) | |
.attr("transform", function(d){return "translate(" + d.x + "," + d.y + ")";}) | |
.on("mouseover", function(d){ | |
if (state.shiftNodeDrag){ | |
d3.select(this).classed(consts.connectClass, true); | |
} | |
}) | |
.on("mouseout", function(d){ | |
d3.select(this).classed(consts.connectClass, false); | |
}) | |
.on("mousedown", function(d){ | |
thisGraph.circleMouseDown.call(thisGraph, d3.select(this), d); | |
}) | |
.on("mouseup", function(d){ | |
thisGraph.circleMouseUp.call(thisGraph, d3.select(this), d); | |
}) | |
.call(thisGraph.drag); | |
newGs.append("circle") | |
.attr("r", String(consts.nodeRadius)); | |
newGs.each(function(d){ | |
thisGraph.insertTitleLinebreaks(d3.select(this), d.title); | |
}); | |
// remove old nodes | |
thisGraph.circles.exit().remove(); | |
}; | |
GraphCreator.prototype.zoomed = function(){ | |
this.state.justScaleTransGraph = true; | |
d3.select("." + this.consts.graphClass) | |
.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")"); | |
}; | |
GraphCreator.prototype.updateWindow = function(svg){ | |
var docEl = document.documentElement, | |
bodyEl = document.getElementsByTagName('body')[0]; | |
var x = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth; | |
var y = window.innerHeight|| docEl.clientHeight|| bodyEl.clientHeight; | |
svg.attr("width", x).attr("height", y); | |
}; | |
GraphCreator.prototype.layout = function(){ | |
var depends = new Map(); | |
var dependencies = new Map(); | |
this.edges.forEach(function(edge){ | |
if (!depends.has(edge.source)){ | |
depends.set(edge.source, []); | |
} | |
if (!dependencies.has(edge.target)){ | |
dependencies.set(edge.target, []); | |
} | |
var list = depends.get(edge.source); | |
if (!list.includes(edge.target)){ | |
list.push(edge.target); | |
} | |
list = dependencies.get(edge.target); | |
if (!list.includes(edge.source)){ | |
list.push(edge.source); | |
} | |
}); | |
var nodes = this.nodes; | |
var added = true; | |
var positions = new Map(); | |
while (added){ | |
added = false; | |
nodes.forEach(function(node){ | |
if (positions.has(node)){ | |
return; | |
} | |
if (!dependencies.has(node)){ | |
console.log('solved: ', node); | |
positions.set(node, {width: 1, deps: 0, node: node}); | |
added = true; | |
} else { | |
var nodeDependencies = dependencies.get(node).map(function(dependency){ | |
return positions.get(dependency); | |
}); | |
var width = 0; | |
var deps = 0; | |
var hasAll = true; | |
nodeDependencies.forEach(function(dependency){ | |
if (!dependency){ | |
hasAll = false; | |
return; | |
} | |
width = width + dependency.width; | |
deps = deps < dependency.deps ? dependency.deps : deps; | |
}); | |
if (!hasAll){ | |
return; | |
} | |
console.log('solved: ', node); | |
positions.set(node, {width: width, deps: 1 + deps, node: node}); | |
added = true; | |
} | |
}); | |
} | |
const itemWidth = 200; | |
const itemHeight = 200; | |
var allPositions = Array.from(positions.values()); | |
allPositions.sort(function(a, b){ | |
return b.deps - a.deps; | |
}); | |
const maxHeight = allPositions[0].deps; | |
allPositions.forEach(function(a){ | |
console.log(a); | |
a.node.y = (maxHeight - a.deps) * itemHeight; | |
var info = allPositions.filter(function(b){ | |
return b.deps === a.deps; | |
}).map(function(item){ | |
if (!depends.has(item.node)){ | |
return {level: item, avg: -1}; | |
} | |
var itemDependencies = depends.get(item.node); | |
var sum = 0; | |
itemDependencies.forEach(function(itemDependency){ | |
sum += itemDependency.x; | |
}); | |
return {level: item, avg: sum / itemDependencies.length}; | |
}).sort(function(a,b){ | |
return a.avg - b.avg; | |
}); | |
var levels = []; | |
var waiting = []; | |
info.forEach(function(i){ | |
if (i.avg === -1){ | |
waiting.push(i.level); | |
} else { | |
var avg = Math.round(i.avg / itemWidth); | |
while (levels.length < avg){ | |
if (waiting.length){ | |
levels.push(waiting.shift()); | |
} else { | |
levels.push(null); | |
} | |
} | |
levels.push(i.level); | |
} | |
}); | |
levels = levels.concat(waiting); | |
console.log('info:', info); | |
console.log('levels:', levels); | |
if (levels.length === 1){ | |
a.node.x = (a.width / 2) * itemWidth; | |
} else { | |
a.node.x = levels.indexOf(a) * itemWidth; | |
} | |
}); | |
} | |
export default GraphCreator; |
This file contains 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
{ | |
"version": "0.12.1", | |
"EmberENV": { | |
"FEATURES": {} | |
}, | |
"options": { | |
"use_pods": false, | |
"enable-testing": false | |
}, | |
"dependencies": { | |
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js", | |
"d3": "https://d3js.org/d3.v3.min.js", | |
"ember": "2.12.0", | |
"ember-template-compiler": "2.12.0", | |
"ember-testing": "2.12.0" | |
}, | |
"addons": { | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment