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';} if(typeof node['contentUrl'] !== "undefined") {return 'contentUrl';} 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(let 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(let nodeId in data) { let node = data[nodeId]; let currentId = node["id"]; for(let 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(let 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; } } var nodePositions = {}; function startGraph(graph){ // config var nodeSize = 40; var selectedNodeSize = 140; // set some vars var currentNodeIdx = 0; var currentNodePositionRadius = 0; var types = {}; for (let 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"); let typeLinkAEl = document.createElement("a"); typeLinkAEl.innerHTML = typeName; typeLinkAEl.addEventListener('click', function(){ centerByType(typeName); // positionNodesInCenter(types[typeName]); }); typeLinkEl.append(typeLinkAEl); 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;}) ; var linkLine = link // .append("line"); .append("line").attr("marker-end", "url(#arrowHead)") ; var 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); simulation.restart(); } 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; let 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) console.log("reset alpha"); simulation.alpha(1); simulation.restart(); } 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); } 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('a'); 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('a'); 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 += `
type
${nodeDatum['type']}
`; 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 += `
${attr}
${nodeAttr[i]}
`; } else if(attr == 'contentUrl') { listEl.innerHTML += `
${attr}
${nodeAttr[i]}
`; listEl.innerHTML += `
`; } else { listEl.innerHTML += `
${attr}
${nodeAttr[i]}
`; } } } 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
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)); if(typeof rel['contentUrl'] != 'undefined') { let ddEl = document.createElement('dd') ddEl.classList.add('dd-contentobject'); ddEl.innerHTML = `` relsEl.appendChild(ddEl); } } } 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)); if(typeof rel['contentUrl'] != 'undefined') { let ddEl = document.createElement('dd') ddEl.classList.add('dd-contentobject'); ddEl.innerHTML = `` relsEl.appendChild(ddEl); } } } 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; } }); */ let 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)"); } }); let 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 var 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, dx, 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; */ let srcSize = nodeSize; let 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(); closeDetails(); // positionNodesInCenter(currentNodeIdx+1); }