More changes
This commit is contained in:
parent
fd7d6e49cb
commit
d0b3be2209
3 changed files with 1012 additions and 895 deletions
221
portfolio.css
Normal file
221
portfolio.css
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
body{
|
||||||
|
margin:0;overflow: hidden;
|
||||||
|
font-family: sans-serif;
|
||||||
|
/* background: #fceabb;
|
||||||
|
background: -moz-linear-gradient(-45deg, #fceabb 0%, #fccd4d 50%, #f8b500 51%, #fbdf93 100%);
|
||||||
|
background: -webkit-linear-gradient(-45deg, #fceabb 0%,#fccd4d 50%,#f8b500 51%,#fbdf93 100%);
|
||||||
|
background: linear-gradient(135deg, #fceabb 0%,#fccd4d 50%,#f8b500 51%,#fbdf93 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fceabb', endColorstr='#fbdf93',GradientType=1 ); */
|
||||||
|
/*background: #f7fbfc;
|
||||||
|
background: -moz-linear-gradient(45deg, #f7fbfc 0%, #d9edf2 40%, #add9e4 100%);
|
||||||
|
background: -webkit-linear-gradient(45deg, #f7fbfc 0%,#d9edf2 40%,#add9e4 100%);
|
||||||
|
background: linear-gradient(45deg, #f7fbfc 0%,#d9edf2 40%,#add9e4 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f7fbfc', endColorstr='#add9e4',GradientType=1 );*/
|
||||||
|
/*background: #e2e2e2;
|
||||||
|
background: -moz-linear-gradient(45deg, #e2e2e2 0%, #dbdbdb 50%, #d1d1d1 51%, #fefefe 100%);
|
||||||
|
background: -webkit-linear-gradient(45deg, #e2e2e2 0%,#dbdbdb 50%,#d1d1d1 51%,#fefefe 100%);
|
||||||
|
background: linear-gradient(45deg, #e2e2e2 0%,#dbdbdb 50%,#d1d1d1 51%,#fefefe 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e2e2e2', endColorstr='#fefefe',GradientType=1 );*/
|
||||||
|
/*background: #d2dfed;
|
||||||
|
background: -moz-linear-gradient(45deg, #d2dfed 0%, #c8d7eb 26%, #bed0ea 51%, #a6c0e3 51%, #afc7e8 62%, #bad0ef 75%, #99b5db 88%, #799bc8 100%);
|
||||||
|
background: -webkit-linear-gradient(45deg, #d2dfed 0%,#c8d7eb 26%,#bed0ea 51%,#a6c0e3 51%,#afc7e8 62%,#bad0ef 75%,#99b5db 88%,#799bc8 100%);
|
||||||
|
background: linear-gradient(45deg, #d2dfed 0%,#c8d7eb 26%,#bed0ea 51%,#a6c0e3 51%,#afc7e8 62%,#bad0ef 75%,#99b5db 88%,#799bc8 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#d2dfed', endColorstr='#799bc8',GradientType=1 );*/
|
||||||
|
background: #d2dfed;
|
||||||
|
background: -moz-linear-gradient(45deg, #d2dfed 0%, #afc1d8 13%, #d5e0ef 28%, #bed0ea 51%, #a8c0dd 51%, #c0d0e5 63%, #bad0ef 75%, #a2bad8 88%, #799bc8 100%);
|
||||||
|
background: -webkit-linear-gradient(45deg, #d2dfed 0%,#afc1d8 13%,#d5e0ef 28%,#bed0ea 51%,#a8c0dd 51%,#c0d0e5 63%,#bad0ef 75%,#a2bad8 88%,#799bc8 100%);
|
||||||
|
background: linear-gradient(45deg, #d2dfed 0%,#afc1d8 13%,#d5e0ef 28%,#bed0ea 51%,#a8c0dd 51%,#c0d0e5 63%,#bad0ef 75%,#a2bad8 88%,#799bc8 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#d2dfed', endColorstr='#799bc8',GradientType=1 );
|
||||||
|
|
||||||
|
/*-moz-animation: bgShift 5s infinite;
|
||||||
|
background-size: 400% 400%;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@-moz-keyframes bgShift{
|
||||||
|
0%{background-position:0% 50%}
|
||||||
|
50%{background-position:100% 50%}
|
||||||
|
100%{background-position:0% 50%}
|
||||||
|
}*/
|
||||||
|
svg{
|
||||||
|
width:100vw;
|
||||||
|
height: calc(100vh - 40px);
|
||||||
|
cursor: dragging;
|
||||||
|
}
|
||||||
|
svg.dragging{
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
g.node{
|
||||||
|
cursor: pointer;
|
||||||
|
stroke: blue;
|
||||||
|
stroke-width: 0;
|
||||||
|
transition: stroke-width .5s;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 1s;
|
||||||
|
}
|
||||||
|
g.node.visibleNode/* , g.node.ImageObject */
|
||||||
|
{
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
g.node circle.highlightCircle{
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
g.node:hover circle.highlightCircle{
|
||||||
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
g.node.centeredNode circle.highlightCircle{
|
||||||
|
stroke-width:1px;
|
||||||
|
stroke: green;
|
||||||
|
stroke-dasharray: 3 2;
|
||||||
|
}
|
||||||
|
g.node.selectedNode circle.highlightCircle{
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
g.node.drag{
|
||||||
|
cursor:grabbing;
|
||||||
|
}
|
||||||
|
.node text{
|
||||||
|
text-anchor: start;
|
||||||
|
}
|
||||||
|
.relationship{
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
.relationship.visibleLink{
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
.relationship line{
|
||||||
|
fill:none;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 2px;\;
|
||||||
|
}
|
||||||
|
.relationship text{
|
||||||
|
fill:black;
|
||||||
|
/* text-transform: lowercase; */
|
||||||
|
font-size: 75%;
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
.relationship.activeLink text{
|
||||||
|
fill:white;
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
.relationship.activeLink line{
|
||||||
|
stroke: white;
|
||||||
|
}
|
||||||
|
circle.nodeBg{
|
||||||
|
fill: white;
|
||||||
|
/*stroke-width:.2em;*/
|
||||||
|
fill:url(#blueGrad);
|
||||||
|
}
|
||||||
|
text{
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag{
|
||||||
|
fill:#00f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MediaObject circle.nodeBg{
|
||||||
|
fill:url(#orangeGrad);
|
||||||
|
}
|
||||||
|
/* .ImageObject circle.nodeBg{
|
||||||
|
stroke:lightgreen;
|
||||||
|
}*/
|
||||||
|
.Person circle.nodeBg{
|
||||||
|
fill:url(#redGrad);
|
||||||
|
}
|
||||||
|
.PublicationEvent circle.nodeBg{
|
||||||
|
fill:url(#limeGrad);
|
||||||
|
}
|
||||||
|
/*.Place circle{
|
||||||
|
stroke:darkgreen;
|
||||||
|
}
|
||||||
|
.Country circle{
|
||||||
|
stroke:yellow;
|
||||||
|
}*/
|
||||||
|
.relationship.address line{
|
||||||
|
/* stroke:#90F7FE; */
|
||||||
|
}
|
||||||
|
.relationship.location line{
|
||||||
|
/* stroke:darkgreen; */
|
||||||
|
}
|
||||||
|
.relationship.contributor line{
|
||||||
|
/* stroke:orange; */
|
||||||
|
/* stroke-width:.4em; */
|
||||||
|
}
|
||||||
|
#nodeDetails{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -720px;
|
||||||
|
bottom: 10px;
|
||||||
|
width: 700px;
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
/* opacity: 0; */
|
||||||
|
transition: opacity 1s, right 1s;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
body.detailsOpen #nodeDetails{
|
||||||
|
/* opacity:1; */
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
svg#portfolioGraph {
|
||||||
|
position: relative;
|
||||||
|
right: 0;
|
||||||
|
transition: right 1s;
|
||||||
|
}
|
||||||
|
body.detailsOpen svg#portfolioGraph{
|
||||||
|
right: calc(720px / 2);
|
||||||
|
}
|
||||||
|
#nodeDetails .nodeTitle{
|
||||||
|
|
||||||
|
}
|
||||||
|
#nodeDetails .nodeType{
|
||||||
|
font-size:80%;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #999;
|
||||||
|
margin-left:10px;
|
||||||
|
}
|
||||||
|
#nodeDetails .nodeType:hover{
|
||||||
|
cursor:pointer;
|
||||||
|
color:blue;
|
||||||
|
}
|
||||||
|
/* #nodeDetails .nodeType::before{
|
||||||
|
content:"(";
|
||||||
|
}
|
||||||
|
#nodeDetails .nodeType::after{
|
||||||
|
content:")";
|
||||||
|
} */
|
||||||
|
#nodeDetails dt{
|
||||||
|
float:left;
|
||||||
|
width: 120px;
|
||||||
|
font-weight:bold;
|
||||||
|
}
|
||||||
|
#nodeDetails dd:not(.nodeTitleNr1) {
|
||||||
|
margin-left: 120px;
|
||||||
|
}
|
||||||
|
#nodeDetails dt.dt-description{
|
||||||
|
float:none;
|
||||||
|
}
|
||||||
|
#nodeDetails dd.dd-description{
|
||||||
|
margin-left:0;
|
||||||
|
}
|
||||||
|
#graphControls{
|
||||||
|
position:fixed;
|
||||||
|
left:0;
|
||||||
|
bottom:0;
|
||||||
|
right:0;
|
||||||
|
height:40px;
|
||||||
|
background:white;
|
||||||
|
}
|
||||||
|
#graphControls ul{
|
||||||
|
margin:0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#graphControls li{
|
||||||
|
list-style:none;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 9px 10px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
786
portfolio.js
Normal file
786
portfolio.js
Normal file
|
@ -0,0 +1,786 @@
|
||||||
|
var data;
|
||||||
|
|
||||||
|
function getTitleAttribute(node) {
|
||||||
|
if(typeof node['name'] !== "undefined"){
|
||||||
|
return 'name';
|
||||||
|
}
|
||||||
|
switch (node['type']) {
|
||||||
|
case "WebSite":
|
||||||
|
if(typeof node['url'] !== "undefined") {return 'url';}
|
||||||
|
break;
|
||||||
|
case "ImageObject":
|
||||||
|
if(typeof node['caption'] !== "undefined") {return 'caption';}
|
||||||
|
break;
|
||||||
|
break;
|
||||||
|
case "PostalAddress":
|
||||||
|
if(typeof node['addressLocality'] !== "undefined") {return 'addressLocality';}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 'id';
|
||||||
|
}
|
||||||
|
function getNodeTitle(node){
|
||||||
|
return node[getTitleAttribute(node)];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
Transform a flattened jsonld into a d3 compatible graph
|
||||||
|
@param Object data flattened jsonld data
|
||||||
|
@return Object graph has keys "nodes" and "links"
|
||||||
|
*/
|
||||||
|
function jsonLdToGraph(data){
|
||||||
|
let nodes = {};
|
||||||
|
let links = [];
|
||||||
|
// collect all nodes
|
||||||
|
for(nodeId in data){
|
||||||
|
data[nodeId]["type"][0] = data[nodeId]["type"][0];
|
||||||
|
nodes[data[nodeId]["id"]] = data[nodeId];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// collect all links (separate loop as we need to check nodes)
|
||||||
|
for(nodeId in data) {
|
||||||
|
let node = data[nodeId];
|
||||||
|
currentId = node["id"];
|
||||||
|
for(key in node){
|
||||||
|
let nodeAttr = Array.isArray(node[key]) ? node[key] : [node[key]];
|
||||||
|
// // relations should always be lists (eases assumptions)
|
||||||
|
// if(typeof node[key] !== "Array" && typeof node[key]['id'] !== "undefined") {
|
||||||
|
// node[key] = [node[key]];
|
||||||
|
// }
|
||||||
|
// every attribute is an Array after flatten(), loop them
|
||||||
|
for(i in nodeAttr) {
|
||||||
|
if(key !== "id" && typeof nodeAttr[i] === "string" && nodes[nodeAttr[i]]) {
|
||||||
|
links[links.length] = {
|
||||||
|
"source": currentId,
|
||||||
|
"target": nodeAttr[i],
|
||||||
|
"name": key
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if(typeof nodeAttr[i]["id"] !== "undefined") {
|
||||||
|
// if there is just one item, flatten/expand has turned urls in objects with just an id
|
||||||
|
// reverse this, as we don't want these separate for this project
|
||||||
|
if (Object.keys(nodeAttr[i]).length == 1 && typeof nodes[nodeAttr[i]["id"]] === "undefined") {
|
||||||
|
// skip
|
||||||
|
// nodeAttr = nodeAttr[i]["id"];
|
||||||
|
} else {
|
||||||
|
links[links.length] = {
|
||||||
|
"source": currentId,
|
||||||
|
"target": nodeAttr[i]["id"],
|
||||||
|
"name": key
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"nodes": Object.values(nodes),
|
||||||
|
"links": links
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var graph;
|
||||||
|
var nodeMap = {};
|
||||||
|
jsonld.flatten(window.location.protocol + "//" + window.location.host + "/rubenvandeven.jsonld", {"@context": "https://schema.org/"},(err, flattened)=> {
|
||||||
|
console.log(err);
|
||||||
|
data = flattened;
|
||||||
|
graph = jsonLdToGraph(flattened['@graph']);
|
||||||
|
// create a map of nodes by id.
|
||||||
|
for(let i in graph.nodes) {
|
||||||
|
nodeMap[graph.nodes[i]['id']] = graph.nodes[i];
|
||||||
|
}
|
||||||
|
// console.log(graph);
|
||||||
|
startGraph(graph);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function inCircle(dx, dy, r) {
|
||||||
|
// fastest check if in circle: https://stackoverflow.com/a/7227057
|
||||||
|
let dxAbs = Math.abs(dx);
|
||||||
|
let dyAbs = Math.abs(dy);
|
||||||
|
|
||||||
|
if(dxAbs > r || dyAbs > r) {
|
||||||
|
return false;
|
||||||
|
} else if(dxAbs + dyAbs <= r){
|
||||||
|
return true;
|
||||||
|
} else if( Math.pow(dx,2) + Math.pow(dy, 2) <= Math.pow(r,2)){
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGraph(graph){
|
||||||
|
|
||||||
|
|
||||||
|
// config
|
||||||
|
var nodeSize = 40;
|
||||||
|
var selectedNodeSize = 140;
|
||||||
|
|
||||||
|
// set some vars
|
||||||
|
var currentNodeIdx = 0;
|
||||||
|
var currentNodePositionRadius = 0;
|
||||||
|
var nodePositions = {};
|
||||||
|
var types = {};
|
||||||
|
for (nodeIdx in graph['nodes']) {
|
||||||
|
let type = graph['nodes'][nodeIdx]["type"];
|
||||||
|
if(typeof types[type] == 'undefined') {
|
||||||
|
types[type] = [];
|
||||||
|
}
|
||||||
|
types[type][types[type].length] = nodeIdx;
|
||||||
|
}
|
||||||
|
var graphControlsEl = document.getElementById('graphControls');
|
||||||
|
var typeLinksEl = document.getElementById('typeLinks');
|
||||||
|
var relLinksEl = document.getElementById('relLinks');
|
||||||
|
|
||||||
|
// make controls
|
||||||
|
for (let typeName in types) {
|
||||||
|
let typeLinkEl = document.createElement("li");
|
||||||
|
typeLinkEl.innerHTML = typeName;
|
||||||
|
typeLinkEl.addEventListener('click', function(){
|
||||||
|
centerByType(typeName);
|
||||||
|
// positionNodesInCenter(types[typeName]);
|
||||||
|
});
|
||||||
|
typeLinksEl.appendChild(typeLinkEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// make svg
|
||||||
|
var svg = d3.select("svg"),
|
||||||
|
width = +svg.attr("width"),
|
||||||
|
height = +svg.attr("height");
|
||||||
|
|
||||||
|
var simulation = d3.forceSimulation()
|
||||||
|
.force("link", d3.forceLink().id(function(d) { return d["id"]; }).strength(.005))
|
||||||
|
.force("charge", d3.forceManyBody()) // doesn't seem necessary?
|
||||||
|
.force("collision", d3.forceCollide(nodeSize * 1.1)) // avoid overlapping nodes
|
||||||
|
// .force("center", d3.forceCenter(width / 2, height / 2)) // position around center
|
||||||
|
|
||||||
|
// .force("x", d3.forceX())
|
||||||
|
// .force("y", d3.forceY())
|
||||||
|
// .force("y", d3.forceY())
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
var link = svg.append("g")
|
||||||
|
.attr("class", "links")
|
||||||
|
.selectAll(".relationship")
|
||||||
|
.data(graph['links'])
|
||||||
|
.enter().append("g")
|
||||||
|
.attr("class", function(d){return "relationship "+d.name;})
|
||||||
|
;
|
||||||
|
linkLine = link
|
||||||
|
// .append("line");
|
||||||
|
.append("line").attr("marker-end", "url(#arrowHead)")
|
||||||
|
;
|
||||||
|
linkText = link
|
||||||
|
.append("text")
|
||||||
|
.text(function(d){
|
||||||
|
return d.name;
|
||||||
|
// snake_case: return d.name.replace(/(?:^|\.?)([A-Z])/g, function (x,y){return "_" + y.toLowerCase()}).replace(/^_/, "");
|
||||||
|
})
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
|
var node = svg.append("g")
|
||||||
|
.attr("class", "nodes")
|
||||||
|
.selectAll(".node")
|
||||||
|
.data(graph.nodes)
|
||||||
|
.enter().append("g")
|
||||||
|
.attr("class", function(d) { return 'node ' + d['type']; })
|
||||||
|
;
|
||||||
|
var getViewbox = function() {
|
||||||
|
return svg.attr("viewBox").split(" ").map(parseFloat);
|
||||||
|
}
|
||||||
|
var positionNodesInCenter = function(idxs) {
|
||||||
|
let viewBox = getViewbox();
|
||||||
|
let cx = viewBox[0] + viewBox[2]/2;
|
||||||
|
let cy = viewBox[1] + viewBox[3]/2;
|
||||||
|
|
||||||
|
if(typeof idxs == "object" && idxs !== null && idxs.length == 1) {
|
||||||
|
idxs = idxs[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
nodePositions = {}; // reset
|
||||||
|
if(idxs === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if(typeof idxs == "object") {
|
||||||
|
// array or object -> each
|
||||||
|
// calculate grid:
|
||||||
|
// let itemsX = 4;
|
||||||
|
// let itemsY = Math.ceil(idxs.length/itemsX);
|
||||||
|
// console.log(itemsX,itemsY);
|
||||||
|
// let rowDiffX = viewBox[3] * (1/(itemsX+1));
|
||||||
|
// let rowDiffY = viewBox[2] * (1/(itemsY+1));
|
||||||
|
// console.log(rowDiffX, rowDiffY);
|
||||||
|
// for (var i = 0; i < idxs.length; i++) {
|
||||||
|
// nodePositions[idxs[i]] = [
|
||||||
|
// cx - itemsX/2*rowDiffX + rowDiffX * ((i % itemsX)),
|
||||||
|
// cy - itemsY/2*rowDiffY + rowDiffY * (Math.floor(i / itemsX))
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
positionNodesInCircle(idxs);
|
||||||
|
console.log(nodePositions);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
nodePositions[idxs] = [
|
||||||
|
cx,
|
||||||
|
cy
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
node.each(function(d,nIdx,nodeEls){
|
||||||
|
if(typeof nodePositions[nIdx] != 'undefined') {
|
||||||
|
nodeEls[nIdx].classList.add('centeredNode');
|
||||||
|
nodeEls[nIdx].classList.add('visibleNode');
|
||||||
|
} else {
|
||||||
|
nodeEls[nIdx].classList.remove('centeredNode');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// restart animation (they call that 'alpha' in d3 force)
|
||||||
|
simulation.alpha(1);
|
||||||
|
}
|
||||||
|
var positionNodesInCircle = function(idxs, r) {
|
||||||
|
let viewBox = getViewbox();
|
||||||
|
if(typeof r == 'undefined') {
|
||||||
|
if(idxs.length == 1) {
|
||||||
|
r = viewBox[2] / 6;
|
||||||
|
} else {
|
||||||
|
r = viewBox[2] / (4 + Math.max(0, 2.5 - idxs.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentNodePositionRadius = r;
|
||||||
|
let cx = viewBox[0] + viewBox[2]/2;
|
||||||
|
let cy = viewBox[1] + viewBox[3]/2;
|
||||||
|
|
||||||
|
stepSize = 2*Math.PI / idxs.length;
|
||||||
|
for (var i = 0; i < idxs.length; i++) {
|
||||||
|
nodePositions[idxs[i]] = [
|
||||||
|
cx + Math.sin(stepSize * i) * r,
|
||||||
|
cy + Math.cos(stepSize * i) * r
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// restart animation (they call that 'alpha' in d3 force)
|
||||||
|
simulation.alpha(1);
|
||||||
|
}
|
||||||
|
var centerByType = function(types) {
|
||||||
|
if(!Array.isArray(types)) {
|
||||||
|
types = [types];
|
||||||
|
}
|
||||||
|
let idxs = [];
|
||||||
|
for(let idx in graph.nodes) {
|
||||||
|
if(types.indexOf(graph.nodes[idx]['type']) > -1) {
|
||||||
|
idxs[idxs.length] = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deselectNode();
|
||||||
|
positionNodesInCenter(idxs.length ? idxs : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
d3Selection="";
|
||||||
|
var selectedNodeTransition = d3.transition()
|
||||||
|
.duration(750)
|
||||||
|
.ease(d3.easeLinear);
|
||||||
|
|
||||||
|
var nodeDetailEl = document.getElementById("nodeDetails");
|
||||||
|
|
||||||
|
var createRelationshipEl = function(relNode, i) {
|
||||||
|
let el = document.createElement("dd");
|
||||||
|
el.classList.add('relLink');
|
||||||
|
let titleEl = document.createElement('span');
|
||||||
|
titleEl.innerHTML = getNodeTitle(relNode);
|
||||||
|
titleEl.classList.add('nodeTitle');
|
||||||
|
titleEl.classList.add('nodeTitleNr'+i);
|
||||||
|
titleEl.addEventListener('click',function(e){
|
||||||
|
let idx = graph.nodes.indexOf(relNode);
|
||||||
|
selectNode(idx);
|
||||||
|
});
|
||||||
|
let typeEl = document.createElement('span');
|
||||||
|
typeEl.classList.add('nodeType')
|
||||||
|
typeEl.innerHTML = relNode['type']
|
||||||
|
typeEl.addEventListener('click',function(e){
|
||||||
|
centerByType(relNode['type']);
|
||||||
|
});
|
||||||
|
el.appendChild(titleEl);
|
||||||
|
el.appendChild(typeEl);
|
||||||
|
// el.innerHTML = `${getNodeTitle(relNode)} (${relNode['type']})`;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
var setDetails = function(nodeDatum, nodeIdx) {
|
||||||
|
document.body.classList.add("detailsOpen");
|
||||||
|
while (nodeDetailEl.hasChildNodes()) {
|
||||||
|
nodeDetailEl.removeChild(nodeDetailEl.lastChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let relUp = [];
|
||||||
|
let relDown = [];
|
||||||
|
let titleAttr = getTitleAttribute(nodeDatum);
|
||||||
|
let titleEl = document.createElement('h2');
|
||||||
|
titleEl.innerHTML = getNodeTitle(nodeDatum);
|
||||||
|
|
||||||
|
let typeEl = document.createElement('span');
|
||||||
|
typeEl.classList.add('nodeType')
|
||||||
|
typeEl.innerHTML = nodeDatum['type']
|
||||||
|
typeEl.addEventListener('click',function(e){
|
||||||
|
centerByType(nodeDatum['type']);
|
||||||
|
});
|
||||||
|
titleEl.appendChild(typeEl);
|
||||||
|
nodeDetailEl.appendChild(titleEl);
|
||||||
|
|
||||||
|
let listEl = document.createElement("dl");
|
||||||
|
// listEl.innerHTML += `<dt>type</dt><dd>${nodeDatum['type']}</dd>`;
|
||||||
|
for (let attr in nodeDatum) {
|
||||||
|
if([
|
||||||
|
'id','x','y','index','type','vy','vx','fx','fy','leftX','rightX', titleAttr
|
||||||
|
].indexOf(attr) != -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// approach all as array
|
||||||
|
let nodeAttr = Array.isArray(nodeDatum[attr]) ? nodeDatum[attr] : [nodeDatum[attr]];
|
||||||
|
for (let i in nodeAttr) {
|
||||||
|
// check if relationship:
|
||||||
|
if(typeof nodeAttr[i] === "string" && nodeMap[nodeAttr[i]]) {
|
||||||
|
continue;
|
||||||
|
} else if(typeof nodeAttr[i]['id'] !== 'undefined') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(attr == 'url') {
|
||||||
|
listEl.innerHTML += `<dt class='dt-${attr}'>${attr}</dt><dd class='dd-${attr}'><a href='${nodeAttr[i]}'>${nodeAttr[i]}</a></dd>`;
|
||||||
|
} else if(attr == 'contentUrl') {
|
||||||
|
listEl.innerHTML += `<dt class='dt-${attr}'>${attr}</dt><dd class='dd-${attr}'><object data='${nodeAttr[i]}'>${nodeAttr[i]}</object></dd>`;
|
||||||
|
} else {
|
||||||
|
listEl.innerHTML += `<dt class='dt-${attr}'>${attr}</dt><dd class='dd-${attr}'>${nodeAttr[i]}</dd>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodeDetailEl.appendChild(listEl);
|
||||||
|
|
||||||
|
let relTitleEl = document.createElement("h4");
|
||||||
|
relTitleEl.classList.add('linkTitle');
|
||||||
|
relTitleEl.innerHTML = "links";
|
||||||
|
nodeDetailEl.appendChild(relTitleEl);
|
||||||
|
|
||||||
|
let relsEl = document.createElement("dl");
|
||||||
|
// collect relationships
|
||||||
|
for (var i = 0; i < graph.links.length; i++) {
|
||||||
|
let link = graph.links[i];
|
||||||
|
if(link['source']['id'] == nodeDatum['id']) {
|
||||||
|
if(typeof relDown[link['name']] == "undefined") {
|
||||||
|
relDown[link['name']] = [];
|
||||||
|
}
|
||||||
|
relDown[link['name']][relDown[link['name']].length] = link['target'];
|
||||||
|
}
|
||||||
|
if(link['target']['id'] == nodeDatum['id']) {
|
||||||
|
if(typeof relUp[link['name']] == "undefined") {
|
||||||
|
relUp[link['name']] = [];
|
||||||
|
}
|
||||||
|
relUp[link['name']][relUp[link['name']].length] = link['source'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// relationships / links in <dl>
|
||||||
|
for(let attr in relDown) {
|
||||||
|
let attrEl = document.createElement("dt");
|
||||||
|
attrEl.innerHTML = attr;
|
||||||
|
relsEl.appendChild(attrEl);
|
||||||
|
for(let i in relDown[attr]) {
|
||||||
|
let rel = relDown[attr][i];
|
||||||
|
relsEl.appendChild(createRelationshipEl(rel));
|
||||||
|
// html += `<dd>${rel['type']}: ${rel['id']}</dd>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let attr in relUp) {
|
||||||
|
let attrEl = document.createElement("dt");
|
||||||
|
attrEl.innerHTML = attr;
|
||||||
|
relsEl.appendChild(attrEl);
|
||||||
|
for(let i in relUp[attr]) {
|
||||||
|
let rel = relUp[attr][i];
|
||||||
|
relsEl.appendChild(createRelationshipEl(rel, i));
|
||||||
|
// html += `<dd>${rel['type']}: ${rel['id']}</dd>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeDetailEl.appendChild(relsEl);
|
||||||
|
|
||||||
|
node.each(function(d,nIdx,nodeEls){
|
||||||
|
if(nIdx == nodeIdx) {
|
||||||
|
nodeEls[nIdx].classList.add('selectedNode');
|
||||||
|
} else {
|
||||||
|
nodeEls[nIdx].classList.remove('selectedNode');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var closeDetails = function() {
|
||||||
|
document.body.classList.remove("detailsOpen");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a node, and center it + show details
|
||||||
|
* @param int idx The index of the node in the graph.nodes array
|
||||||
|
* @param Element|null nodeEl Optional, provide node element, so loop doesn't have to be used to change the Element
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
var selectNode = function(idx){
|
||||||
|
let nodeEl = null;
|
||||||
|
let nodeDatum = null;
|
||||||
|
node.each(function(d,nIdx,nodeEls){
|
||||||
|
if(nIdx == idx) {
|
||||||
|
nodeEl = nodeEls[idx];
|
||||||
|
nodeDatum = d;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(!nodeEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set global var
|
||||||
|
positionNodesInCenter(idx);
|
||||||
|
|
||||||
|
/* DISABLED: Make selected nodes bigger
|
||||||
|
// update collision: the selected node should get plenty of space.
|
||||||
|
simulation.force("collision").radius(function(d, idx){
|
||||||
|
if(typeof nodePositions[idx] != 'undefined'){
|
||||||
|
return selectedNodeSize * 1.2;
|
||||||
|
}
|
||||||
|
return nodeSize * 1.1;
|
||||||
|
});
|
||||||
|
node.each(function(d, idx, nodeEls){
|
||||||
|
let nodeEl = nodeEls[idx];
|
||||||
|
let nodeD3 = d3.select(nodeEl);
|
||||||
|
let circleD3 = nodeD3.select('circle');
|
||||||
|
|
||||||
|
if(typeof nodePositions[idx] !== 'undefined') {
|
||||||
|
circleD3.transition(selectedNodeTransition).attr('r', selectedNodeSize);
|
||||||
|
// nodeEl.getElementsByTagName("circle")[0].attributes.r.value = selectedNodeSize;
|
||||||
|
} else {
|
||||||
|
circleD3.transition(selectedNodeTransition).attr('r', nodeSize);
|
||||||
|
// nodeEl.getElementsByTagName("circle")[0].attributes.r.value = nodeSize;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
linkedIdxs = [];
|
||||||
|
link.each(function(d,idx,linkEls,q){
|
||||||
|
if(d.source == nodeDatum || d.target == nodeDatum) {
|
||||||
|
linkEls[idx].classList.add('activeLink','visibleLink');
|
||||||
|
linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHeadSelected)");
|
||||||
|
node.filter(function(a, fnodeIdx){
|
||||||
|
let r = a.id == d.source.id || a.id == d.target.id; //connected node: true/false
|
||||||
|
if(r && linkedIdxs.indexOf(fnodeIdx) === -1){
|
||||||
|
linkedIdxs[linkedIdxs.length] = fnodeIdx;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}).classed('visibleNode', true);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
linkEls[idx].classList.remove('activeLink');
|
||||||
|
linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHead)");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
i = linkedIdxs.indexOf(idx);
|
||||||
|
|
||||||
|
if(i !== -1) {
|
||||||
|
linkedIdxs.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
positionNodesInCircle(linkedIdxs);
|
||||||
|
|
||||||
|
setDetails(nodeDatum ,idx);
|
||||||
|
}
|
||||||
|
var deselectNode = function() {
|
||||||
|
positionNodesInCenter(null);
|
||||||
|
link.each(function(d,idx,linkEls,q){
|
||||||
|
linkEls[idx].classList.remove('activeLink');
|
||||||
|
linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHead)")
|
||||||
|
});
|
||||||
|
closeDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
simulation.force('centerActive', function force(alpha) {
|
||||||
|
// let currentNode = node.selectAll('.detail');
|
||||||
|
// console.log(currentNode);
|
||||||
|
node.each(function(d, idx, nodes){
|
||||||
|
let n = d;
|
||||||
|
let k = alpha * 0.1;
|
||||||
|
if(typeof nodePositions[idx] != 'undefined') {
|
||||||
|
n.vx -= (n.x - nodePositions[idx][0]) * k * 5;
|
||||||
|
n.vy -= (n.y - nodePositions[idx][1]) * k * 5;
|
||||||
|
} else {
|
||||||
|
// if it's not positioned, move it out of the circle
|
||||||
|
if(currentNodePositionRadius < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewBox = getViewbox();
|
||||||
|
let cx = viewBox[0] + viewBox[2]/2;
|
||||||
|
let cy = viewBox[1] + viewBox[3]/2;
|
||||||
|
|
||||||
|
let dx = n.x - cx;
|
||||||
|
let dy = n.y - cy;
|
||||||
|
if(!inCircle(dx, dy, currentNodePositionRadius)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
n.vx += dx * k /5;
|
||||||
|
n.vy += dy * k /5;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//path to curve the tile
|
||||||
|
nodePath = node.append("path")
|
||||||
|
.attr("id", function(d,idx){return "nodePath"+idx;})
|
||||||
|
.attr("d", function(){
|
||||||
|
var r = nodeSize * 0.9;
|
||||||
|
var startX = nodeSize;
|
||||||
|
// M cx cy
|
||||||
|
// m -r, 0
|
||||||
|
// a r,r 0 1,0 (r * 2),0
|
||||||
|
// a r,r 0 1,0 -(r * 2),0
|
||||||
|
// return 'M' + nodeSize/2 + ' ' + nodeSize/2 + ' ' +
|
||||||
|
return 'M' + 0 + ' ' + 0 + ' ' +
|
||||||
|
'm -' + r + ', 0'+' ' +
|
||||||
|
'a ' + r +','+r+' 0 1,0 '+ (r*2) +',0 '+
|
||||||
|
'a ' + r +','+r+' 0 1,0 -'+ (r*2) +',0'
|
||||||
|
;
|
||||||
|
// return 'm' + startX + ',' + nodeSize + ' ' +
|
||||||
|
// 'a' + r + ',' + r + ' 0 0 0 ' + (2*r) + ',0';
|
||||||
|
})
|
||||||
|
;
|
||||||
|
|
||||||
|
node.call(d3.drag()
|
||||||
|
.on("start", dragstarted)
|
||||||
|
.on("drag", dragged)
|
||||||
|
.on("end", dragended))
|
||||||
|
.on("click", function(d, idx, nodes){
|
||||||
|
let node = nodes[idx];
|
||||||
|
// if(typeof nodePositions[idx] == 'undefined') {
|
||||||
|
selectNode(idx, node, d);
|
||||||
|
// } else {
|
||||||
|
// node.parentNode.classList.toggle('detail');
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.call(d3.drag()
|
||||||
|
.on("start", function(){
|
||||||
|
svg.node().classList.add("dragging");
|
||||||
|
})
|
||||||
|
.on("drag", function(){
|
||||||
|
moveViewboxPx(d3.event.dx, d3.event.dy);
|
||||||
|
})
|
||||||
|
.on("end", function(){
|
||||||
|
svg.node().classList.remove("dragging");
|
||||||
|
}));
|
||||||
|
|
||||||
|
node.append('circle')
|
||||||
|
.attr("r", nodeSize)
|
||||||
|
.attr("class", "nodeBg")
|
||||||
|
;
|
||||||
|
node.append('circle')
|
||||||
|
.attr("r", nodeSize * 1.08) // nodeSize + margin
|
||||||
|
.attr("class", "highlightCircle")
|
||||||
|
;
|
||||||
|
|
||||||
|
node.append('text')
|
||||||
|
.append("textPath")
|
||||||
|
.attr( "xlink:href",function(d, idx){return '#nodePath'+idx;})
|
||||||
|
.text(getNodeTitle);
|
||||||
|
|
||||||
|
node.each(function(d) {
|
||||||
|
if(!d.contentUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
d3.select(this).append('svg:image')
|
||||||
|
.attr("xlink:href", d.contentUrl)
|
||||||
|
.attr("width", nodeSize*2)
|
||||||
|
.attr("height", nodeSize*2)
|
||||||
|
.attr("transform","translate(-"+nodeSize+" -"+nodeSize+")")
|
||||||
|
.attr("clip-path","url(#clipNodeImage)")
|
||||||
|
.attr("preserveAspectRatio","xMidYMid slice")
|
||||||
|
;
|
||||||
|
});
|
||||||
|
|
||||||
|
// node.append("title")
|
||||||
|
// .text(function(d) { return d.id; });
|
||||||
|
|
||||||
|
simulation
|
||||||
|
.nodes(graph.nodes)
|
||||||
|
.on("tick", ticked);
|
||||||
|
|
||||||
|
simulation.force("link")
|
||||||
|
.links(graph.links)
|
||||||
|
.distance(function(l){
|
||||||
|
switch (l.name) {
|
||||||
|
case 'publishedAt':
|
||||||
|
return 400;
|
||||||
|
case 'image':
|
||||||
|
return 100;
|
||||||
|
default:
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
}) // distance between the nodes / link length
|
||||||
|
// .charge(-100)
|
||||||
|
;
|
||||||
|
|
||||||
|
// run on each draw
|
||||||
|
function ticked() {
|
||||||
|
graph.nodes.forEach(function (d, idx) {
|
||||||
|
d.leftX = d.rightX = d.x;
|
||||||
|
|
||||||
|
// fix first node on center
|
||||||
|
// if(idx === 0) {
|
||||||
|
// d.fx = width/2;
|
||||||
|
// d.fy = height/2;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
linkLine.each(function (d) {
|
||||||
|
var sourceX, targetX, midX, dy, dy, angle;
|
||||||
|
|
||||||
|
// This mess makes the arrows exactly perfect.
|
||||||
|
// thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4
|
||||||
|
if( d.source.rightX < d.target.leftX ){
|
||||||
|
sourceX = d.source.rightX;
|
||||||
|
targetX = d.target.leftX;
|
||||||
|
} else if( d.target.rightX < d.source.leftX ){
|
||||||
|
targetX = d.target.rightX;
|
||||||
|
sourceX = d.source.leftX;
|
||||||
|
} else if (d.target.isCircle) {
|
||||||
|
targetX = sourceX = d.target.x;
|
||||||
|
} else if (d.source.isCircle) {
|
||||||
|
targetX = sourceX = d.source.x;
|
||||||
|
} else {
|
||||||
|
midX = (d.source.x + d.target.x) / 2;
|
||||||
|
if(midX > d.target.rightX){
|
||||||
|
midX = d.target.rightX;
|
||||||
|
} else if(midX > d.source.rightX){
|
||||||
|
midX = d.source.rightX;
|
||||||
|
} else if(midX < d.target.leftX){
|
||||||
|
midX = d.target.leftX;
|
||||||
|
} else if(midX < d.source.leftX){
|
||||||
|
midX = d.source.leftX;
|
||||||
|
}
|
||||||
|
targetX = sourceX = midX;
|
||||||
|
}
|
||||||
|
|
||||||
|
dx = targetX - sourceX;
|
||||||
|
dy = d.target.y - d.source.y;
|
||||||
|
angle = Math.atan2(dx, dy);
|
||||||
|
|
||||||
|
/* DISABLED
|
||||||
|
srcSize = (typeof nodePositions[d.source.index] != 'undefined') ? selectedNodeSize : nodeSize;
|
||||||
|
tgtSize = (typeof nodePositions[d.target.index] != 'undefined') ? selectedNodeSize : nodeSize;
|
||||||
|
*/
|
||||||
|
srcSize = nodeSize;
|
||||||
|
tgtSize = nodeSize;
|
||||||
|
|
||||||
|
// Compute the line endpoint such that the arrow
|
||||||
|
// is touching the edge of the node rectangle perfectly.
|
||||||
|
d.sourceX = sourceX + Math.sin(angle) * srcSize;
|
||||||
|
d.targetX = targetX - Math.sin(angle) * tgtSize;
|
||||||
|
d.sourceY = d.source.y + Math.cos(angle) * srcSize;
|
||||||
|
d.targetY = d.target.y - Math.cos(angle) * tgtSize;
|
||||||
|
})
|
||||||
|
.attr("x1", function(d) { return d.sourceX; })
|
||||||
|
.attr("y1", function(d) { return d.sourceY; })
|
||||||
|
.attr("x2", function(d) { return d.targetX; })
|
||||||
|
.attr("y2", function(d) { return d.targetY; });
|
||||||
|
linkText.attr("transform", function(d){
|
||||||
|
let dx = (d.target.x - d.source.x) /2;
|
||||||
|
let dy = (d.target.y - d.source.y) /2;
|
||||||
|
let x = d.source.x + dx;
|
||||||
|
let y = d.source.y + dy;
|
||||||
|
let deg = Math.atan(dy / dx) * 180 / Math.PI;
|
||||||
|
return "translate("+x+" "+y+") rotate("+deg+") translate(0, -10)";
|
||||||
|
});
|
||||||
|
// linkPath.attr("d", function(d) {
|
||||||
|
// var x1 = d.source.x,
|
||||||
|
// y1 = d.source.y,
|
||||||
|
// x2 = d.target.x,
|
||||||
|
// y2 = d.target.y,
|
||||||
|
// dx = x2 - x1,
|
||||||
|
// dy = y2 - y1,
|
||||||
|
// dr = Math.sqrt(dx * dx + dy * dy),
|
||||||
|
|
||||||
|
// // Defaults for normal edge.
|
||||||
|
// drx = dr,
|
||||||
|
// dry = dr,
|
||||||
|
// xRotation = 0, // degrees
|
||||||
|
// largeArc = 0, // 1 or 0
|
||||||
|
// sweep = 1; // 1 or 0
|
||||||
|
|
||||||
|
// // Self edge.
|
||||||
|
// if ( x1 === x2 && y1 === y2 ) {
|
||||||
|
// // Fiddle with this angle to get loop oriented.
|
||||||
|
// xRotation = -45;
|
||||||
|
|
||||||
|
// // Needs to be 1.
|
||||||
|
// largeArc = 1;
|
||||||
|
|
||||||
|
// // Change sweep to change orientation of loop.
|
||||||
|
// //sweep = 0;
|
||||||
|
|
||||||
|
// // Make drx and dry different to get an ellipse
|
||||||
|
// // instead of a circle.
|
||||||
|
// drx = 30;
|
||||||
|
// dry = 20;
|
||||||
|
|
||||||
|
// // For whatever reason the arc collapses to a point if the beginning
|
||||||
|
// // and ending points of the arc are the same, so kludge it.
|
||||||
|
// x2 = x2 + 1;
|
||||||
|
// y2 = y2 + 1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return "M" + x1 + "," + y1 + "A" + drx + "," + dry + " " + xRotation + "," + largeArc + "," + sweep + " " + x2 + "," + y2;
|
||||||
|
// });
|
||||||
|
|
||||||
|
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragstarted(d,idx,nodes) {
|
||||||
|
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
|
||||||
|
let nodeEl = nodes[idx];
|
||||||
|
d.fx = d.x;
|
||||||
|
d.fy = d.y;
|
||||||
|
// nodeEl.style.fill = '#00f';
|
||||||
|
nodeEl.classList.add('drag');
|
||||||
|
}
|
||||||
|
|
||||||
|
// use to validate drag
|
||||||
|
// function validate(x, a, b) {
|
||||||
|
// if (x =< a) return a;
|
||||||
|
// return b;
|
||||||
|
// }
|
||||||
|
|
||||||
|
function dragged(d, idx) {
|
||||||
|
d.fx = d3.event.x;
|
||||||
|
d.fy = d3.event.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragended(d, idx, nodes) {
|
||||||
|
if (!d3.event.active) simulation.alphaTarget(0);
|
||||||
|
let nodeEl = nodes[idx];
|
||||||
|
d.fx = null;
|
||||||
|
d.fy = null;
|
||||||
|
nodeEl.classList.remove('drag');
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveViewboxPx(dx, dy){
|
||||||
|
let viewBox = svg.attr("viewBox").split(" ").map(parseFloat);
|
||||||
|
viewBox[0] -= dx * 1;
|
||||||
|
viewBox[1] -= dy * 1;
|
||||||
|
svg.attr("viewBox", viewBox.join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// start by selecting the first node :-)
|
||||||
|
// selectNode(currentNodeIdx+1);
|
||||||
|
// positionNodesInCenter(currentNodeIdx);
|
||||||
|
selectNode(currentNodeIdx); deselectNode();
|
||||||
|
// positionNodesInCenter(currentNodeIdx+1);
|
||||||
|
}
|
900
test3.html
900
test3.html
|
@ -3,219 +3,10 @@
|
||||||
<head>
|
<head>
|
||||||
<title></title>
|
<title></title>
|
||||||
<link href="rubenvandeven.jsonld" rel="alternate" type="application/ld+json" />
|
<link href="rubenvandeven.jsonld" rel="alternate" type="application/ld+json" />
|
||||||
<style type="text/css">
|
<link rel="stylesheet" href="/portfolio.css">
|
||||||
body{
|
|
||||||
margin:0;overflow: hidden;
|
|
||||||
font-family: sans-serif;
|
|
||||||
/* background: #fceabb;
|
|
||||||
background: -moz-linear-gradient(-45deg, #fceabb 0%, #fccd4d 50%, #f8b500 51%, #fbdf93 100%);
|
|
||||||
background: -webkit-linear-gradient(-45deg, #fceabb 0%,#fccd4d 50%,#f8b500 51%,#fbdf93 100%);
|
|
||||||
background: linear-gradient(135deg, #fceabb 0%,#fccd4d 50%,#f8b500 51%,#fbdf93 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fceabb', endColorstr='#fbdf93',GradientType=1 ); */
|
|
||||||
/*background: #f7fbfc;
|
|
||||||
background: -moz-linear-gradient(45deg, #f7fbfc 0%, #d9edf2 40%, #add9e4 100%);
|
|
||||||
background: -webkit-linear-gradient(45deg, #f7fbfc 0%,#d9edf2 40%,#add9e4 100%);
|
|
||||||
background: linear-gradient(45deg, #f7fbfc 0%,#d9edf2 40%,#add9e4 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f7fbfc', endColorstr='#add9e4',GradientType=1 );*/
|
|
||||||
/*background: #e2e2e2;
|
|
||||||
background: -moz-linear-gradient(45deg, #e2e2e2 0%, #dbdbdb 50%, #d1d1d1 51%, #fefefe 100%);
|
|
||||||
background: -webkit-linear-gradient(45deg, #e2e2e2 0%,#dbdbdb 50%,#d1d1d1 51%,#fefefe 100%);
|
|
||||||
background: linear-gradient(45deg, #e2e2e2 0%,#dbdbdb 50%,#d1d1d1 51%,#fefefe 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e2e2e2', endColorstr='#fefefe',GradientType=1 );*/
|
|
||||||
/*background: #d2dfed;
|
|
||||||
background: -moz-linear-gradient(45deg, #d2dfed 0%, #c8d7eb 26%, #bed0ea 51%, #a6c0e3 51%, #afc7e8 62%, #bad0ef 75%, #99b5db 88%, #799bc8 100%);
|
|
||||||
background: -webkit-linear-gradient(45deg, #d2dfed 0%,#c8d7eb 26%,#bed0ea 51%,#a6c0e3 51%,#afc7e8 62%,#bad0ef 75%,#99b5db 88%,#799bc8 100%);
|
|
||||||
background: linear-gradient(45deg, #d2dfed 0%,#c8d7eb 26%,#bed0ea 51%,#a6c0e3 51%,#afc7e8 62%,#bad0ef 75%,#99b5db 88%,#799bc8 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#d2dfed', endColorstr='#799bc8',GradientType=1 );*/
|
|
||||||
background: #d2dfed;
|
|
||||||
background: -moz-linear-gradient(45deg, #d2dfed 0%, #afc1d8 13%, #d5e0ef 28%, #bed0ea 51%, #a8c0dd 51%, #c0d0e5 63%, #bad0ef 75%, #a2bad8 88%, #799bc8 100%);
|
|
||||||
background: -webkit-linear-gradient(45deg, #d2dfed 0%,#afc1d8 13%,#d5e0ef 28%,#bed0ea 51%,#a8c0dd 51%,#c0d0e5 63%,#bad0ef 75%,#a2bad8 88%,#799bc8 100%);
|
|
||||||
background: linear-gradient(45deg, #d2dfed 0%,#afc1d8 13%,#d5e0ef 28%,#bed0ea 51%,#a8c0dd 51%,#c0d0e5 63%,#bad0ef 75%,#a2bad8 88%,#799bc8 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#d2dfed', endColorstr='#799bc8',GradientType=1 );
|
|
||||||
|
|
||||||
/*-moz-animation: bgShift 5s infinite;
|
|
||||||
background-size: 400% 400%;*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/*@-moz-keyframes bgShift{
|
|
||||||
0%{background-position:0% 50%}
|
|
||||||
50%{background-position:100% 50%}
|
|
||||||
100%{background-position:0% 50%}
|
|
||||||
}*/
|
|
||||||
svg{
|
|
||||||
width:100vw;
|
|
||||||
height: 100vh;
|
|
||||||
cursor: dragging;
|
|
||||||
}
|
|
||||||
svg.dragging{
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
g.node{
|
|
||||||
cursor: pointer;
|
|
||||||
stroke: blue;
|
|
||||||
stroke-width: 0;
|
|
||||||
transition: stroke-width .5s;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 1s;
|
|
||||||
}
|
|
||||||
g.node.visibleNode/* , g.node.ImageObject */
|
|
||||||
{
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
g.node circle.highlightCircle{
|
|
||||||
fill: none;
|
|
||||||
}
|
|
||||||
g.node:hover circle.highlightCircle{
|
|
||||||
stroke-width: 2px;
|
|
||||||
}
|
|
||||||
g.node.centeredNode circle.highlightCircle{
|
|
||||||
stroke-width:1px;
|
|
||||||
stroke: green;
|
|
||||||
stroke-dasharray: 3 2;
|
|
||||||
}
|
|
||||||
g.node.selectedNode circle.highlightCircle{
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
g.node.drag{
|
|
||||||
cursor:grabbing;
|
|
||||||
}
|
|
||||||
.node text{
|
|
||||||
text-anchor: start;
|
|
||||||
}
|
|
||||||
.relationship{
|
|
||||||
display:none;
|
|
||||||
}
|
|
||||||
.relationship.visibleLink{
|
|
||||||
display:block;
|
|
||||||
}
|
|
||||||
.relationship line{
|
|
||||||
fill:none;
|
|
||||||
stroke: #000;
|
|
||||||
stroke-width: 2px;\;
|
|
||||||
}
|
|
||||||
.relationship text{
|
|
||||||
fill:black;
|
|
||||||
/* text-transform: lowercase; */
|
|
||||||
font-size: 75%;
|
|
||||||
}
|
|
||||||
.relationship:hover line{
|
|
||||||
/*TODO: fancy color animation*/
|
|
||||||
stroke:red;
|
|
||||||
}
|
|
||||||
.relationship:hover text{
|
|
||||||
fill:red;
|
|
||||||
}
|
|
||||||
.relationship.activeLink text{
|
|
||||||
color:red;
|
|
||||||
}
|
|
||||||
.relationship.activeLink line{
|
|
||||||
stroke: red;
|
|
||||||
}
|
|
||||||
circle.nodeBg{
|
|
||||||
fill: white;
|
|
||||||
/*stroke-width:.2em;*/
|
|
||||||
fill:url(#blueGrad);
|
|
||||||
}
|
|
||||||
text{
|
|
||||||
text-anchor: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag{
|
|
||||||
fill:#00f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MediaObject circle.nodeBg{
|
|
||||||
fill:url(#orangeGrad);
|
|
||||||
}
|
|
||||||
/* .ImageObject circle.nodeBg{
|
|
||||||
stroke:lightgreen;
|
|
||||||
}*/
|
|
||||||
.Person circle.nodeBg{
|
|
||||||
fill:url(#redGrad);
|
|
||||||
}
|
|
||||||
.PublicationEvent circle.nodeBg{
|
|
||||||
fill:url(#limeGrad);
|
|
||||||
}
|
|
||||||
/*.Place circle{
|
|
||||||
stroke:darkgreen;
|
|
||||||
}
|
|
||||||
.Country circle{
|
|
||||||
stroke:yellow;
|
|
||||||
}*/
|
|
||||||
.relationship.address line{
|
|
||||||
/* stroke:#90F7FE; */
|
|
||||||
}
|
|
||||||
.relationship.location line{
|
|
||||||
/* stroke:darkgreen; */
|
|
||||||
}
|
|
||||||
.relationship.contributor line{
|
|
||||||
/* stroke:orange; */
|
|
||||||
/* stroke-width:.4em; */
|
|
||||||
}
|
|
||||||
#nodeDetails{
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
/* bottom: 10px; */
|
|
||||||
width: 450px;
|
|
||||||
background: white;
|
|
||||||
padding: 10px;
|
|
||||||
opacity:0;
|
|
||||||
transition:opacity 1s;
|
|
||||||
max-height: calc(100% - 20px);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
#nodeDetails.open{
|
|
||||||
opacity:1;
|
|
||||||
}
|
|
||||||
#nodeDetails .nodeTitle{
|
|
||||||
|
|
||||||
}
|
|
||||||
#nodeDetails .nodeType{
|
|
||||||
font-size:80%;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #999;
|
|
||||||
margin-left:10px;
|
|
||||||
}
|
|
||||||
#nodeDetails .nodeType:hover{
|
|
||||||
cursor:pointer;
|
|
||||||
color:blue;
|
|
||||||
}
|
|
||||||
/* #nodeDetails .nodeType::before{
|
|
||||||
content:"(";
|
|
||||||
}
|
|
||||||
#nodeDetails .nodeType::after{
|
|
||||||
content:")";
|
|
||||||
} */
|
|
||||||
#nodeDetails dt{
|
|
||||||
float:left;
|
|
||||||
width: 120px;
|
|
||||||
font-weight:bold;
|
|
||||||
}
|
|
||||||
#graphControls{
|
|
||||||
position:fixed;
|
|
||||||
left:0;
|
|
||||||
top:0;
|
|
||||||
right:0;
|
|
||||||
height:50px;
|
|
||||||
background:white;
|
|
||||||
}
|
|
||||||
#graphControls ul{
|
|
||||||
margin:0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
#graphControls li{
|
|
||||||
list-style:none;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<svg width="800" height="600" reserveAspectRatio="xMidYMid" viewBox="-400 -300 800 600" xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink">
|
<svg id="portfolioGraph" width="800" height="600" reserveAspectRatio="xMidYMid" viewBox="-400 -300 800 600" xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="orangeGrad" gradientTransform="rotate(66 .5 .5)" objectBoundingBox="gradientUnits">
|
<linearGradient id="orangeGrad" gradientTransform="rotate(66 .5 .5)" objectBoundingBox="gradientUnits">
|
||||||
<stop offset=".05" stop-color="#fceabb" />
|
<stop offset=".05" stop-color="#fceabb" />
|
||||||
|
@ -248,8 +39,8 @@ background-size: 400% 400%;*/
|
||||||
<stop offset=".51" stop-color="hsl(206, 0%, 48%)" />
|
<stop offset=".51" stop-color="hsl(206, 0%, 48%)" />
|
||||||
<stop offset=".99" stop-color="hsl(212, 0%, 78%)" />
|
<stop offset=".99" stop-color="hsl(212, 0%, 78%)" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 6 6" preserveAspectRatio="none" orient="auto" id="arrowHead"><path d="M0,-3L8,0L0,3"></path></marker>
|
<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 6 6" preserveAspectRatio="none" orient="auto" id="arrowHead" fill="#999"><path d="M0,-3L8,0L0,3"></path></marker>
|
||||||
<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 6 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelected"><path d="M0,-3L8,0L0,3" fill="red"></path></marker>
|
<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 6 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelected"><path d="M0,-3L8,0L0,3" fill="white"></path></marker>
|
||||||
<clipPath id="clipNodeImage">
|
<clipPath id="clipNodeImage">
|
||||||
<circle cx="40" cy="40" r="40" />
|
<circle cx="40" cy="40" r="40" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
|
@ -263,688 +54,7 @@ background-size: 400% 400%;*/
|
||||||
<ul id='relLinks'></ul>
|
<ul id='relLinks'></ul>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jsonld@0.5.21/dist/jsonld.js"></script>
|
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jsonld@0.5.21/dist/jsonld.js"></script>
|
||||||
<script type="text/javascript">
|
|
||||||
var data;
|
|
||||||
|
|
||||||
function getTitleAttribute(node) {
|
|
||||||
if(typeof node['name'] !== "undefined"){
|
|
||||||
return 'name';
|
|
||||||
}
|
|
||||||
switch (node['type']) {
|
|
||||||
case "WebSite":
|
|
||||||
if(typeof node['url'] !== "undefined") {return 'url';}
|
|
||||||
break;
|
|
||||||
case "ImageObject":
|
|
||||||
if(typeof node['caption'] !== "undefined") {return 'caption';}
|
|
||||||
break;
|
|
||||||
break;
|
|
||||||
case "PostalAddress":
|
|
||||||
if(typeof node['addressLocality'] !== "undefined") {return 'addressLocality';}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return 'id';
|
|
||||||
}
|
|
||||||
function getNodeTitle(node){
|
|
||||||
return node[getTitleAttribute(node)];
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
Transform a flattened jsonld into a d3 compatible graph
|
|
||||||
@param Object data flattened jsonld data
|
|
||||||
@return Object graph has keys "nodes" and "links"
|
|
||||||
*/
|
|
||||||
function jsonLdToGraph(data){
|
|
||||||
let nodes = {};
|
|
||||||
let links = [];
|
|
||||||
// collect all nodes
|
|
||||||
for(nodeId in data){
|
|
||||||
data[nodeId]["type"][0] = data[nodeId]["type"][0];
|
|
||||||
nodes[data[nodeId]["id"]] = data[nodeId];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// collect all links (separate loop as we need to check nodes)
|
|
||||||
for(nodeId in data) {
|
|
||||||
let node = data[nodeId];
|
|
||||||
currentId = node["id"];
|
|
||||||
for(key in node){
|
|
||||||
let nodeAttr = Array.isArray(node[key]) ? node[key] : [node[key]];
|
|
||||||
// // relations should always be lists (eases assumptions)
|
|
||||||
// if(typeof node[key] !== "Array" && typeof node[key]['id'] !== "undefined") {
|
|
||||||
// node[key] = [node[key]];
|
|
||||||
// }
|
|
||||||
// every attribute is an Array after flatten(), loop them
|
|
||||||
for(i in nodeAttr) {
|
|
||||||
if(key !== "id" && typeof nodeAttr[i] === "string" && nodes[nodeAttr[i]]) {
|
|
||||||
links[links.length] = {
|
|
||||||
"source": currentId,
|
|
||||||
"target": nodeAttr[i],
|
|
||||||
"name": key
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if(typeof nodeAttr[i]["id"] !== "undefined") {
|
|
||||||
// if there is just one item, flatten/expand has turned urls in objects with just an id
|
|
||||||
// reverse this, as we don't want these separate for this project
|
|
||||||
if (Object.keys(nodeAttr[i]).length == 1 && typeof nodes[nodeAttr[i]["id"]] === "undefined") {
|
|
||||||
// skip
|
|
||||||
// nodeAttr = nodeAttr[i]["id"];
|
|
||||||
} else {
|
|
||||||
links[links.length] = {
|
|
||||||
"source": currentId,
|
|
||||||
"target": nodeAttr[i]["id"],
|
|
||||||
"name": key
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"nodes": Object.values(nodes),
|
|
||||||
"links": links
|
|
||||||
};
|
|
||||||
}
|
|
||||||
var graph;
|
|
||||||
var nodeMap = {};
|
|
||||||
jsonld.flatten("http://localhost:8000/rubenvandeven.jsonld", {"@context": "https://schema.org/"},(err, flattened)=> {
|
|
||||||
data = flattened;
|
|
||||||
graph = jsonLdToGraph(flattened['@graph']);
|
|
||||||
// create a map of nodes by id.
|
|
||||||
for(let i in graph.nodes) {
|
|
||||||
nodeMap[graph.nodes[i]['id']] = graph.nodes[i];
|
|
||||||
}
|
|
||||||
// console.log(graph);
|
|
||||||
startGraph(graph);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="d3.min.js"></script>
|
<script type="text/javascript" src="d3.min.js"></script>
|
||||||
<script>
|
<script type="text/javascript" src="/portfolio.js"></script>
|
||||||
function startGraph(graph){
|
|
||||||
|
|
||||||
|
|
||||||
// config
|
|
||||||
var nodeSize = 40;
|
|
||||||
var selectedNodeSize = 140;
|
|
||||||
|
|
||||||
// set some vars
|
|
||||||
var currentNodeIdx = 0;
|
|
||||||
var nodePositions = {};
|
|
||||||
var types = {};
|
|
||||||
for (nodeIdx in graph['nodes']) {
|
|
||||||
let type = graph['nodes'][nodeIdx]["type"];
|
|
||||||
if(typeof types[type] == 'undefined') {
|
|
||||||
types[type] = [];
|
|
||||||
}
|
|
||||||
types[type][types[type].length] = nodeIdx;
|
|
||||||
}
|
|
||||||
var graphControlsEl = document.getElementById('graphControls');
|
|
||||||
var typeLinksEl = document.getElementById('typeLinks');
|
|
||||||
var relLinksEl = document.getElementById('relLinks');
|
|
||||||
|
|
||||||
// make controls
|
|
||||||
for (let typeName in types) {
|
|
||||||
let typeLinkEl = document.createElement("li");
|
|
||||||
typeLinkEl.innerHTML = typeName;
|
|
||||||
typeLinkEl.addEventListener('click', function(){
|
|
||||||
positionNodesInCenter(types[typeName]);
|
|
||||||
});
|
|
||||||
typeLinksEl.appendChild(typeLinkEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// make svg
|
|
||||||
var svg = d3.select("svg"),
|
|
||||||
width = +svg.attr("width"),
|
|
||||||
height = +svg.attr("height");
|
|
||||||
|
|
||||||
var simulation = d3.forceSimulation()
|
|
||||||
.force("link", d3.forceLink().id(function(d) { return d["id"]; }).strength(.005))
|
|
||||||
.force("charge", d3.forceManyBody()) // doesn't seem necessary?
|
|
||||||
.force("collision", d3.forceCollide(nodeSize * 1.1)) // avoid overlapping nodes
|
|
||||||
// .force("center", d3.forceCenter(width / 2, height / 2)) // position around center
|
|
||||||
|
|
||||||
// .force("x", d3.forceX())
|
|
||||||
// .force("y", d3.forceY())
|
|
||||||
// .force("y", d3.forceY())
|
|
||||||
;
|
|
||||||
|
|
||||||
|
|
||||||
var link = svg.append("g")
|
|
||||||
.attr("class", "links")
|
|
||||||
.selectAll(".relationship")
|
|
||||||
.data(graph['links'])
|
|
||||||
.enter().append("g")
|
|
||||||
.attr("class", function(d){return "relationship "+d.name;})
|
|
||||||
;
|
|
||||||
linkLine = link
|
|
||||||
// .append("line");
|
|
||||||
.append("line").attr("marker-end", "url(#arrowHead)")
|
|
||||||
;
|
|
||||||
linkText = link
|
|
||||||
.append("text")
|
|
||||||
.text(function(d){
|
|
||||||
return d.name;
|
|
||||||
// snake_case: return d.name.replace(/(?:^|\.?)([A-Z])/g, function (x,y){return "_" + y.toLowerCase()}).replace(/^_/, "");
|
|
||||||
})
|
|
||||||
|
|
||||||
;
|
|
||||||
|
|
||||||
var node = svg.append("g")
|
|
||||||
.attr("class", "nodes")
|
|
||||||
.selectAll(".node")
|
|
||||||
.data(graph.nodes)
|
|
||||||
.enter().append("g")
|
|
||||||
.attr("class", function(d) { return 'node ' + d['type']; })
|
|
||||||
;
|
|
||||||
|
|
||||||
var positionNodesInCenter = function(idxs) {
|
|
||||||
let viewBox = svg.attr("viewBox").split(" ").map(parseFloat);
|
|
||||||
let cx = viewBox[0] + viewBox[2]/2;
|
|
||||||
let cy = viewBox[1] + viewBox[3]/2;
|
|
||||||
|
|
||||||
nodePositions = {}; // reset
|
|
||||||
if(idxs === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if(typeof idxs == "object") {
|
|
||||||
// array or object -> each
|
|
||||||
// calculate grid:
|
|
||||||
let itemsX = 4;
|
|
||||||
let itemsY = Math.ceil(idxs.length/itemsX);
|
|
||||||
console.log(itemsX,itemsY);
|
|
||||||
let rowDiffX = viewBox[3] * (1/(itemsX+1));
|
|
||||||
let rowDiffY = viewBox[2] * (1/(itemsY+1));
|
|
||||||
console.log(rowDiffX, rowDiffY);
|
|
||||||
for (var i = 0; i < idxs.length; i++) {
|
|
||||||
nodePositions[idxs[i]] = [
|
|
||||||
cx - itemsX/2*rowDiffX + rowDiffX * ((i % itemsX)),
|
|
||||||
cy - itemsY/2*rowDiffY + rowDiffY * (Math.floor(i / itemsX))
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
nodePositions[idxs] = [
|
|
||||||
cx,
|
|
||||||
cy
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
node.each(function(d,nIdx,nodeEls){
|
|
||||||
if(typeof nodePositions[nIdx] != 'undefined') {
|
|
||||||
nodeEls[nIdx].classList.add('centeredNode');
|
|
||||||
} else {
|
|
||||||
nodeEls[nIdx].classList.remove('centeredNode');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// restart animation (they call that 'alpha' in d3 force)
|
|
||||||
simulation.alpha(1);
|
|
||||||
}
|
|
||||||
var centerByType = function(types) {
|
|
||||||
if(!Array.isArray(types)) {
|
|
||||||
types = [types];
|
|
||||||
}
|
|
||||||
let idxs = [];
|
|
||||||
for(let idx in graph.nodes) {
|
|
||||||
if(types.indexOf(graph.nodes[idx]['type']) > -1) {
|
|
||||||
idxs[idxs.length] = idx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
positionNodesInCenter(idxs.length ? idxs : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
d3Selection="";
|
|
||||||
var selectedNodeTransition = d3.transition()
|
|
||||||
.duration(750)
|
|
||||||
.ease(d3.easeLinear);
|
|
||||||
|
|
||||||
var nodeDetailEl = document.getElementById("nodeDetails");
|
|
||||||
|
|
||||||
var createRelationshipEl = function(relNode) {
|
|
||||||
let el = document.createElement("dd");
|
|
||||||
el.classList.add('relLink');
|
|
||||||
let titleEl = document.createElement('span');
|
|
||||||
titleEl.innerHTML = getNodeTitle(relNode);
|
|
||||||
titleEl.classList.add('nodeTitle')
|
|
||||||
titleEl.addEventListener('click',function(e){
|
|
||||||
let idx = graph.nodes.indexOf(relNode);
|
|
||||||
selectNode(idx);
|
|
||||||
});
|
|
||||||
let typeEl = document.createElement('span');
|
|
||||||
typeEl.classList.add('nodeType')
|
|
||||||
typeEl.innerHTML = relNode['type']
|
|
||||||
typeEl.addEventListener('click',function(e){
|
|
||||||
centerByType(relNode['type']);
|
|
||||||
});
|
|
||||||
el.appendChild(titleEl);
|
|
||||||
el.appendChild(typeEl);
|
|
||||||
// el.innerHTML = `${getNodeTitle(relNode)} (${relNode['type']})`;
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
var setDetails = function(nodeDatum, nodeIdx) {
|
|
||||||
nodeDetailEl.classList.add("open");
|
|
||||||
let relUp = [];
|
|
||||||
let relDown = [];
|
|
||||||
let html = `<h2>${getNodeTitle(nodeDatum)}</h2>`;
|
|
||||||
html += "<dl>";
|
|
||||||
html += `<dt>type</dt><dd>${nodeDatum['type']}</dd>`;
|
|
||||||
for (let attr in nodeDatum) {
|
|
||||||
if([
|
|
||||||
'id','x','y','index','type','vy','vx','fx','fy','leftX','rightX'
|
|
||||||
].indexOf(attr) != -1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// approach all as array
|
|
||||||
let nodeAttr = Array.isArray(nodeDatum[attr]) ? nodeDatum[attr] : [nodeDatum[attr]];
|
|
||||||
for (let i in nodeAttr) {
|
|
||||||
// check if relationship:
|
|
||||||
if(typeof nodeAttr[i] === "string" && nodeMap[nodeAttr[i]]) {
|
|
||||||
// if(typeof relationships[attr] == "undefined") {
|
|
||||||
// relationships[attr] = [];
|
|
||||||
// }
|
|
||||||
// relationships[attr][relationships[attr].length] = nodeMap[nodeAttr[i]];
|
|
||||||
continue;
|
|
||||||
} else if(typeof nodeAttr[i]['id'] !== 'undefined') {
|
|
||||||
// if(typeof relationships[attr] == "undefined") {
|
|
||||||
// relationships[attr] = [];
|
|
||||||
// }
|
|
||||||
// relationships[attr][relationships[attr].length] = nodeMap[nodeAttr[i]['id']];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
html += `<dt>${attr}</dt><dd>${nodeAttr[i]}</dd>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
html += "</dl>";
|
|
||||||
|
|
||||||
html += "<h4 class='linkTitle'>links</h4>";
|
|
||||||
let relsEl = document.createElement("dl");
|
|
||||||
// collect relationships
|
|
||||||
for (var i = 0; i < graph.links.length; i++) {
|
|
||||||
let link = graph.links[i];
|
|
||||||
if(link['source']['id'] == nodeDatum['id']) {
|
|
||||||
if(typeof relDown[link['name']] == "undefined") {
|
|
||||||
relDown[link['name']] = [];
|
|
||||||
}
|
|
||||||
relDown[link['name']][relDown[link['name']].length] = link['target'];
|
|
||||||
}
|
|
||||||
if(link['target']['id'] == nodeDatum['id']) {
|
|
||||||
if(typeof relUp[link['name']] == "undefined") {
|
|
||||||
relUp[link['name']] = [];
|
|
||||||
}
|
|
||||||
relUp[link['name']][relUp[link['name']].length] = link['source'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// relationships / links in <dl>
|
|
||||||
for(let attr in relDown) {
|
|
||||||
let attrEl = document.createElement("dt");
|
|
||||||
attrEl.innerHTML = attr;
|
|
||||||
relsEl.appendChild(attrEl);
|
|
||||||
for(let i in relDown[attr]) {
|
|
||||||
let rel = relDown[attr][i];
|
|
||||||
relsEl.appendChild(createRelationshipEl(rel));
|
|
||||||
// html += `<dd>${rel['type']}: ${rel['id']}</dd>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(let attr in relUp) {
|
|
||||||
let attrEl = document.createElement("dt");
|
|
||||||
attrEl.innerHTML = attr;
|
|
||||||
relsEl.appendChild(attrEl);
|
|
||||||
for(let i in relUp[attr]) {
|
|
||||||
let rel = relUp[attr][i];
|
|
||||||
relsEl.appendChild(createRelationshipEl(rel));
|
|
||||||
// html += `<dd>${rel['type']}: ${rel['id']}</dd>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nodeDetailEl.innerHTML = html;
|
|
||||||
nodeDetailEl.appendChild(relsEl);
|
|
||||||
|
|
||||||
node.each(function(d,nIdx,nodeEls){
|
|
||||||
if(nIdx == nodeIdx) {
|
|
||||||
nodeEls[nIdx].classList.add('selectedNode');
|
|
||||||
} else {
|
|
||||||
nodeEls[nIdx].classList.remove('selectedNode');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var closeDetails = function() {
|
|
||||||
nodeDetailEl.classList.remove("open");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a node, and center it + show details
|
|
||||||
* @param int idx The index of the node in the graph.nodes array
|
|
||||||
* @param Element|null nodeEl Optional, provide node element, so loop doesn't have to be used to change the Element
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
var selectNode = function(idx){
|
|
||||||
let nodeEl = null;
|
|
||||||
let nodeDatum = null;
|
|
||||||
node.each(function(d,nIdx,nodeEls){
|
|
||||||
if(nIdx == idx) {
|
|
||||||
nodeEl = nodeEls[idx];
|
|
||||||
nodeDatum = d;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if(!nodeEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set global var
|
|
||||||
positionNodesInCenter(idx);
|
|
||||||
|
|
||||||
/* DISABLED: Make selected nodes bigger
|
|
||||||
// update collision: the selected node should get plenty of space.
|
|
||||||
simulation.force("collision").radius(function(d, idx){
|
|
||||||
if(typeof nodePositions[idx] != 'undefined'){
|
|
||||||
return selectedNodeSize * 1.2;
|
|
||||||
}
|
|
||||||
return nodeSize * 1.1;
|
|
||||||
});
|
|
||||||
node.each(function(d, idx, nodeEls){
|
|
||||||
let nodeEl = nodeEls[idx];
|
|
||||||
let nodeD3 = d3.select(nodeEl);
|
|
||||||
let circleD3 = nodeD3.select('circle');
|
|
||||||
|
|
||||||
if(typeof nodePositions[idx] !== 'undefined') {
|
|
||||||
circleD3.transition(selectedNodeTransition).attr('r', selectedNodeSize);
|
|
||||||
// nodeEl.getElementsByTagName("circle")[0].attributes.r.value = selectedNodeSize;
|
|
||||||
} else {
|
|
||||||
circleD3.transition(selectedNodeTransition).attr('r', nodeSize);
|
|
||||||
// nodeEl.getElementsByTagName("circle")[0].attributes.r.value = nodeSize;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
link.each(function(d,idx,linkEls,q){
|
|
||||||
if(d.source == nodeDatum || d.target == nodeDatum) {
|
|
||||||
linkEls[idx].classList.add('activeLink','visibleLink');
|
|
||||||
linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHeadSelected)");
|
|
||||||
node.filter(function(e){
|
|
||||||
return e.id == d.source.id || e.id == d.target.id; //connected nodes
|
|
||||||
}).classed('visibleNode', true);
|
|
||||||
} else {
|
|
||||||
linkEls[idx].classList.remove('activeLink');
|
|
||||||
linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHead)");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setDetails(nodeDatum ,idx);
|
|
||||||
}
|
|
||||||
var deselectNode = function() {
|
|
||||||
positionNodesInCenter(null);
|
|
||||||
link.each(function(d,idx,linkEls,q){
|
|
||||||
linkEls[idx].classList.remove('activeLink');
|
|
||||||
linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHead)")
|
|
||||||
});
|
|
||||||
closeDetails();
|
|
||||||
}
|
|
||||||
|
|
||||||
simulation.force('centerActive', function force(alpha) {
|
|
||||||
// let currentNode = node.selectAll('.detail');
|
|
||||||
// console.log(currentNode);
|
|
||||||
node.each(function(d, idx, nodes){
|
|
||||||
if(typeof nodePositions[idx] != 'undefined') {
|
|
||||||
let n = d;
|
|
||||||
let k = alpha * 0.1;
|
|
||||||
n.vx -= (n.x - nodePositions[idx][0]) * k * 5;
|
|
||||||
n.vy -= (n.y - nodePositions[idx][1]) * k * 5;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//path to curve the tile
|
|
||||||
nodePath = node.append("path")
|
|
||||||
.attr("id", function(d,idx){return "nodePath"+idx;})
|
|
||||||
.attr("d", function(){
|
|
||||||
var r = nodeSize * 0.9;
|
|
||||||
var startX = nodeSize;
|
|
||||||
// M cx cy
|
|
||||||
// m -r, 0
|
|
||||||
// a r,r 0 1,0 (r * 2),0
|
|
||||||
// a r,r 0 1,0 -(r * 2),0
|
|
||||||
// return 'M' + nodeSize/2 + ' ' + nodeSize/2 + ' ' +
|
|
||||||
return 'M' + 0 + ' ' + 0 + ' ' +
|
|
||||||
'm -' + r + ', 0'+' ' +
|
|
||||||
'a ' + r +','+r+' 0 1,0 '+ (r*2) +',0 '+
|
|
||||||
'a ' + r +','+r+' 0 1,0 -'+ (r*2) +',0'
|
|
||||||
;
|
|
||||||
// return 'm' + startX + ',' + nodeSize + ' ' +
|
|
||||||
// 'a' + r + ',' + r + ' 0 0 0 ' + (2*r) + ',0';
|
|
||||||
})
|
|
||||||
;
|
|
||||||
|
|
||||||
node.call(d3.drag()
|
|
||||||
.on("start", dragstarted)
|
|
||||||
.on("drag", dragged)
|
|
||||||
.on("end", dragended))
|
|
||||||
.on("click", function(d, idx, nodes){
|
|
||||||
let node = nodes[idx];
|
|
||||||
if(typeof nodePositions[idx] == 'undefined') {
|
|
||||||
selectNode(idx, node, d);
|
|
||||||
} else {
|
|
||||||
node.parentNode.classList.toggle('detail');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
svg.call(d3.drag()
|
|
||||||
.on("start", function(){
|
|
||||||
svg.node().classList.add("dragging");
|
|
||||||
})
|
|
||||||
.on("drag", function(){
|
|
||||||
moveViewboxPx(d3.event.dx, d3.event.dy);
|
|
||||||
})
|
|
||||||
.on("end", function(){
|
|
||||||
svg.node().classList.remove("dragging");
|
|
||||||
}));
|
|
||||||
|
|
||||||
node.append('circle')
|
|
||||||
.attr("r", nodeSize)
|
|
||||||
.attr("class", "nodeBg")
|
|
||||||
;
|
|
||||||
node.append('circle')
|
|
||||||
.attr("r", nodeSize * 1.08) // nodeSize + margin
|
|
||||||
.attr("class", "highlightCircle")
|
|
||||||
;
|
|
||||||
|
|
||||||
node.append('text')
|
|
||||||
.append("textPath")
|
|
||||||
.attr( "xlink:href",function(d, idx){return '#nodePath'+idx;})
|
|
||||||
.text(getNodeTitle);
|
|
||||||
|
|
||||||
node.each(function(d) {
|
|
||||||
if(!d.contentUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
d3.select(this).append('svg:image')
|
|
||||||
.attr("xlink:href", d.contentUrl)
|
|
||||||
.attr("width", nodeSize*2)
|
|
||||||
.attr("height", nodeSize*2)
|
|
||||||
.attr("transform","translate(-"+nodeSize+" -"+nodeSize+")")
|
|
||||||
.attr("clip-path","url(#clipNodeImage)")
|
|
||||||
.attr("preserveAspectRatio","xMidYMid slice")
|
|
||||||
;
|
|
||||||
});
|
|
||||||
|
|
||||||
// node.append("title")
|
|
||||||
// .text(function(d) { return d.id; });
|
|
||||||
|
|
||||||
simulation
|
|
||||||
.nodes(graph.nodes)
|
|
||||||
.on("tick", ticked);
|
|
||||||
|
|
||||||
simulation.force("link")
|
|
||||||
.links(graph.links)
|
|
||||||
.distance(function(l){
|
|
||||||
switch (l.name) {
|
|
||||||
case 'publishedAt':
|
|
||||||
return 400;
|
|
||||||
case 'image':
|
|
||||||
return 100;
|
|
||||||
default:
|
|
||||||
return 200;
|
|
||||||
}
|
|
||||||
}) // distance between the nodes / link length
|
|
||||||
// .charge(-100)
|
|
||||||
;
|
|
||||||
|
|
||||||
// run on each draw
|
|
||||||
function ticked() {
|
|
||||||
graph.nodes.forEach(function (d, idx) {
|
|
||||||
d.leftX = d.rightX = d.x;
|
|
||||||
|
|
||||||
// fix first node on center
|
|
||||||
// if(idx === 0) {
|
|
||||||
// d.fx = width/2;
|
|
||||||
// d.fy = height/2;
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
|
|
||||||
linkLine.each(function (d) {
|
|
||||||
var sourceX, targetX, midX, dy, dy, angle;
|
|
||||||
|
|
||||||
// This mess makes the arrows exactly perfect.
|
|
||||||
// thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4
|
|
||||||
if( d.source.rightX < d.target.leftX ){
|
|
||||||
sourceX = d.source.rightX;
|
|
||||||
targetX = d.target.leftX;
|
|
||||||
} else if( d.target.rightX < d.source.leftX ){
|
|
||||||
targetX = d.target.rightX;
|
|
||||||
sourceX = d.source.leftX;
|
|
||||||
} else if (d.target.isCircle) {
|
|
||||||
targetX = sourceX = d.target.x;
|
|
||||||
} else if (d.source.isCircle) {
|
|
||||||
targetX = sourceX = d.source.x;
|
|
||||||
} else {
|
|
||||||
midX = (d.source.x + d.target.x) / 2;
|
|
||||||
if(midX > d.target.rightX){
|
|
||||||
midX = d.target.rightX;
|
|
||||||
} else if(midX > d.source.rightX){
|
|
||||||
midX = d.source.rightX;
|
|
||||||
} else if(midX < d.target.leftX){
|
|
||||||
midX = d.target.leftX;
|
|
||||||
} else if(midX < d.source.leftX){
|
|
||||||
midX = d.source.leftX;
|
|
||||||
}
|
|
||||||
targetX = sourceX = midX;
|
|
||||||
}
|
|
||||||
|
|
||||||
dx = targetX - sourceX;
|
|
||||||
dy = d.target.y - d.source.y;
|
|
||||||
angle = Math.atan2(dx, dy);
|
|
||||||
|
|
||||||
/* DISABLED
|
|
||||||
srcSize = (typeof nodePositions[d.source.index] != 'undefined') ? selectedNodeSize : nodeSize;
|
|
||||||
tgtSize = (typeof nodePositions[d.target.index] != 'undefined') ? selectedNodeSize : nodeSize;
|
|
||||||
*/
|
|
||||||
srcSize = nodeSize;
|
|
||||||
tgtSize = nodeSize;
|
|
||||||
|
|
||||||
// Compute the line endpoint such that the arrow
|
|
||||||
// is touching the edge of the node rectangle perfectly.
|
|
||||||
d.sourceX = sourceX + Math.sin(angle) * srcSize;
|
|
||||||
d.targetX = targetX - Math.sin(angle) * tgtSize;
|
|
||||||
d.sourceY = d.source.y + Math.cos(angle) * srcSize;
|
|
||||||
d.targetY = d.target.y - Math.cos(angle) * tgtSize;
|
|
||||||
})
|
|
||||||
.attr("x1", function(d) { return d.sourceX; })
|
|
||||||
.attr("y1", function(d) { return d.sourceY; })
|
|
||||||
.attr("x2", function(d) { return d.targetX; })
|
|
||||||
.attr("y2", function(d) { return d.targetY; });
|
|
||||||
linkText.attr("transform", function(d){
|
|
||||||
let dx = (d.target.x - d.source.x) /2;
|
|
||||||
let dy = (d.target.y - d.source.y) /2;
|
|
||||||
let x = d.source.x + dx;
|
|
||||||
let y = d.source.y + dy;
|
|
||||||
let deg = Math.atan(dy / dx) * 180 / Math.PI;
|
|
||||||
return "translate("+x+" "+y+") rotate("+deg+") translate(0, -10)";
|
|
||||||
});
|
|
||||||
// linkPath.attr("d", function(d) {
|
|
||||||
// var x1 = d.source.x,
|
|
||||||
// y1 = d.source.y,
|
|
||||||
// x2 = d.target.x,
|
|
||||||
// y2 = d.target.y,
|
|
||||||
// dx = x2 - x1,
|
|
||||||
// dy = y2 - y1,
|
|
||||||
// dr = Math.sqrt(dx * dx + dy * dy),
|
|
||||||
|
|
||||||
// // Defaults for normal edge.
|
|
||||||
// drx = dr,
|
|
||||||
// dry = dr,
|
|
||||||
// xRotation = 0, // degrees
|
|
||||||
// largeArc = 0, // 1 or 0
|
|
||||||
// sweep = 1; // 1 or 0
|
|
||||||
|
|
||||||
// // Self edge.
|
|
||||||
// if ( x1 === x2 && y1 === y2 ) {
|
|
||||||
// // Fiddle with this angle to get loop oriented.
|
|
||||||
// xRotation = -45;
|
|
||||||
|
|
||||||
// // Needs to be 1.
|
|
||||||
// largeArc = 1;
|
|
||||||
|
|
||||||
// // Change sweep to change orientation of loop.
|
|
||||||
// //sweep = 0;
|
|
||||||
|
|
||||||
// // Make drx and dry different to get an ellipse
|
|
||||||
// // instead of a circle.
|
|
||||||
// drx = 30;
|
|
||||||
// dry = 20;
|
|
||||||
|
|
||||||
// // For whatever reason the arc collapses to a point if the beginning
|
|
||||||
// // and ending points of the arc are the same, so kludge it.
|
|
||||||
// x2 = x2 + 1;
|
|
||||||
// y2 = y2 + 1;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return "M" + x1 + "," + y1 + "A" + drx + "," + dry + " " + xRotation + "," + largeArc + "," + sweep + " " + x2 + "," + y2;
|
|
||||||
// });
|
|
||||||
|
|
||||||
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragstarted(d,idx,nodes) {
|
|
||||||
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
|
|
||||||
let nodeEl = nodes[idx];
|
|
||||||
d.fx = d.x;
|
|
||||||
d.fy = d.y;
|
|
||||||
// nodeEl.style.fill = '#00f';
|
|
||||||
nodeEl.classList.add('drag');
|
|
||||||
}
|
|
||||||
|
|
||||||
// use to validate drag
|
|
||||||
// function validate(x, a, b) {
|
|
||||||
// if (x =< a) return a;
|
|
||||||
// return b;
|
|
||||||
// }
|
|
||||||
|
|
||||||
function dragged(d, idx) {
|
|
||||||
d.fx = d3.event.x;
|
|
||||||
d.fy = d3.event.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragended(d, idx, nodes) {
|
|
||||||
if (!d3.event.active) simulation.alphaTarget(0);
|
|
||||||
let nodeEl = nodes[idx];
|
|
||||||
d.fx = null;
|
|
||||||
d.fy = null;
|
|
||||||
nodeEl.classList.remove('drag');
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveViewboxPx(dx, dy){
|
|
||||||
let viewBox = svg.attr("viewBox").split(" ").map(parseFloat);
|
|
||||||
viewBox[0] -= dx * 1;
|
|
||||||
viewBox[1] -= dy * 1;
|
|
||||||
svg.attr("viewBox", viewBox.join(" "));
|
|
||||||
}
|
|
||||||
|
|
||||||
// start by selecting the first node :-)
|
|
||||||
// selectNode(currentNodeIdx+1);
|
|
||||||
selectNode(currentNodeIdx);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue