var data; function getLabelAttribute(node) { if(typeof node['https://schema.org/name'] !== "undefined"){ return 'https://schema.org/name'; } switch (node['@type']) { case "https://schema.org/WebSite": if(typeof node['https://schema.org/url'] !== "undefined") {return 'https://schema.org/url';} break; case "https://schema.org/ImageObject": if(typeof node['https://schema.org/caption'] !== "undefined") {return 'https://schema.org/caption';} if(typeof node['https://schema.org/contentUrl'] !== "undefined") {return 'https://schema.org/contentUrl';} break; case "https://schema.org/PostalAddress": if(typeof node['https://schema.org/addressLocality'] !== "undefined") {return 'https://schema.org/addressLocality';} break; } return '@id'; } function getNodeLabel(node){ let labelAttr = getLabelAttribute(node); let label = node[labelAttr]; if(typeof label == "undefined") label = node["@id"]; if(typeof label == "undefined") label = ""; return label; } function getNodeYear(n){ if(typeof n['https://schema.org/dateCreated'] !== 'undefined') { return n['https://schema.org/dateCreated'].substr(0,4); } if(typeof n['https://schema.org/datePublished'] !== 'undefined') { return n['https://schema.org/datePublished'].substr(0,4); } if(typeof n['https://schema.org/startDate'] !== 'undefined') { // console.log(n['https://schema.org/startDate']); return n['https://schema.org/startDate'].substr(0,4); } if(typeof n['https://schema.org/endDate'] !== 'undefined') { return n['https://schema.org/endDate'].substr(0,4); } if(typeof n['https://schema.org/foundingDate'] !== 'undefined') { return n['https://schema.org/foundingDate'].substr(0,4); } if(typeof n['https://schema.org/temporalCoverage'] !== 'undefined') { if(n['https://schema.org/temporalCoverage'].match(/\d{4}-\d{4}/)) { return n['https://schema.org/temporalCoverage'].substr(5,4); } } return null; } function getDisplayAttr(attr) { return attr.replace(/.*[#|\/]/, ""); } /** 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; // map nodes to their ID var nodeMap = {}; var linkMap = {}; var breadcrumbs = {}; var weights = {}; // load the flattened jsonld file const requestPromise = fetch('/assets/js/rubenvandeven.jsonld').then(r => r.json()); const rankingPromise = fetch('/assets/js/ranking.json').then(r => r.json()); Promise.all([requestPromise, rankingPromise]) .then(values => { if(values[0].hasOwnProperty('@graph')) { data = values[0]; weights = values[1]; } else { data = values[1]; weights = values[0]; } graph = jsonLdToGraph(data['@graph']); // create a map of nodes by id. for(let i in graph.nodes) { nodeMap[graph.nodes[i]['@id']] = graph.nodes[i]; } 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 createLinkMap(graph) { let linkMap = {}; for(let link of graph['links']){ if(typeof linkMap[link['source']] == 'undefined') { linkMap[link['source']] = []; } linkMap[link['source']][linkMap[link['source']].length] = {'id': link['target'], 'name': link['name']}; if(typeof linkMap[link['target']] == 'undefined') { linkMap[link['target']] = []; } linkMap[link['target']][linkMap[link['target']].length] = {'id': link['source'], 'name': link['name']}; } return linkMap; } // config var nodeSize = 40; var selectedNodeSize = 140; var firstNodeId = "https://rubenvandeven.com/"; function getSizeForNode(node) { if(node.hasOwnProperty('https://schema.org/thumbnailUrl')) return nodeSize; if(weights[node['@id']]) return nodeSize * weights[node['@id']]; if(node['@id'] == firstNodeId) return nodeSize*1.2; // everynode has at least one link. these should equal 1 return nodeSize * (.7 + Math.min(20, linkMap[node['@id']].length) / 40) return nodeSize; } // TODO: make sure, 'shortest' path is favoured. function createBreadcrumbs(linkMap, srcId) { let crumbs = {}; let createBreadcrumbLayer = function(srcId) { let path = crumbs[srcId]; let newPath = path.slice(); newPath.push(srcId); let nextSrcIds = []; for (let link of linkMap[srcId]) { if(typeof crumbs[link['id']] !== 'undefined') continue; crumbs[link['id']] = newPath; nextSrcIds.push(link['id']); } return nextSrcIds; } crumbs[srcId] = []; let nextIds = [srcId]; while(nextIds.length > 0) { let newNextIds = []; for (let nextId of nextIds) { let r = createBreadcrumbLayer(nextId); newNextIds = newNextIds.concat(r); } nextIds = newNextIds; } return crumbs; } var nodePositions = {}; function startGraph(graph){ // set some vars var currentNodeIdx = 0; var currentNodePositionRadius = 0; var types = {}; linkMap = createLinkMap(graph); breadcrumbs = createBreadcrumbs(linkMap, firstNodeId); for (let nodeIdx in graph['nodes']) { let type = graph['nodes'][nodeIdx]["@type"]; if(typeof types[type] == 'undefined') { types[type] = []; } types[type].push(nodeIdx); } var graphControlsEl = document.getElementById('graphControls'); var typeLinksEl = document.getElementById('typeLinks'); var showMoreTypeLinksEl = document.getElementById('showMoreTypeLinks'); var moreTypeLinksEl = document.getElementById('moreTypeLinks'); var relLinksEl = document.getElementById('relLinks'); // sort types by count: var typeCounts = Object.keys(types).map(function(key) { return [key, types[key].length]; }); typeCounts.sort(function(first, second) { return second[1] - first[1]; }); // make controls let i = 0; for (let typeCountIdx in typeCounts) { let typeName = typeCounts[typeCountIdx][0]; let typeLinkEl = document.createElement("li"); let typeLinkAEl = document.createElement("a"); let typeLinkCountEl = document.createElement("span"); typeLinkCountEl.innerHTML = typeCounts[typeCountIdx][1]; typeLinkCountEl.classList.add('typeCount'); typeLinkAEl.innerHTML = getDisplayAttr(typeName); typeLinkAEl.title = typeName; typeLinkAEl.addEventListener('click', function(){ centerByType(typeName); // positionNodesInCenter(types[typeName]); }); typeLinkAEl.addEventListener('mouseover', function() { let typeNodeEls = document.getElementsByClassName(typeName); for(let typeNodeEl of typeNodeEls) { typeNodeEl.classList.add('typeHighlight'); } }); typeLinkAEl.addEventListener('mouseout', function() { let typeNodeEls = document.getElementsByClassName(typeName); for(let typeNodeEl of typeNodeEls) { typeNodeEl.classList.remove('typeHighlight'); } }); typeLinkEl.append(typeLinkAEl); typeLinkEl.append(typeLinkCountEl); (i < 5 ? typeLinksEl: moreTypeLinksEl).appendChild(typeLinkEl); i++; // typeLinksEl.appendChild(typeLinkEl); } showMoreTypeLinksEl.addEventListener('click', function () { document.body.classList.add('showMoreLinks'); var hideMoreTypeLinks = function(e) { e.preventDefault(); e.stopPropagation(); document.body.removeEventListener('mouseup', hideMoreTypeLinks, true); document.body.classList.remove('showMoreLinks'); } document.body.addEventListener('mouseup', hideMoreTypeLinks, true); }, false) // make svg var svg = d3.select("svg"), width = +svg.attr("width"), height = +svg.attr("height"); var container = svg.append("g") .attr("id", "container") ; 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(function(d){ return getSizeForNode(d) * 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 = container.append("g") .attr("class", "links") .selectAll(".relationship") .data(graph['links']) .enter().append("g") .attr("class", function(l){return "relationship "+l.name;}) ; var linkLine = link // .append("line"); .append("line").attr("marker-end", "url(#arrowHead)") ; var linkText = link .append("text") .text(function(l){ // l == Object { source: "https://rubenvandeven.com/#codesandmodes", target: "_:b34", name: "https://schema.org/location" } return getDisplayAttr(l.name); }) ; var node = container.append("g") .attr("class", "nodes") .selectAll(".node") .data(graph.nodes) .enter().append("g") .attr("class", function(d) { let baseClasses = 'node ' + d['@type']; if(d['@type']) { let slashpos = d['@type'].lastIndexOf('/'); if(slashpos > -1) { baseClasses += ' ' + d['@type'].substr(slashpos + 1); } } return baseClasses; }) ; var getViewbox = function() { return svg.attr("viewBox").split(" ").map(parseFloat); } var positionNodesInCenter = function(idxs) { setViewboxForceCenter(); // sets forceCx & forceCy 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] = [ forceCx, forceCy ]; // console.log("singleNode", idxs, nodePositions); } 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'); nodeEls[nIdx].classList.remove('visibleNode'); } }); // restart animation (they call that 'alpha' in d3 force) simulation.alpha(1); simulation.restart(); } var positionNodesInCircle = function(idxs, r) { let viewBox = getViewbox(); let zoom = getZoomValues(); setViewboxForceCenter(); // sets forceCx & forceCy 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 forceCx = viewBox[0] + viewBox[2]/2 - zoom['dx']; let forceCy = viewBox[1] + viewBox[3]/2 - zoom['dy']; let stepSize = 2*Math.PI / idxs.length; for (var i = 0; i < idxs.length; i++) { nodePositions[idxs[i]] = [ forceCx + Math.sin(stepSize * i) * r, forceCy + Math.cos(stepSize * i) * r ]; } // restart animation (they call that 'alpha' in d3 force) simulation.alpha(1); simulation.restart(); } var centerByType = function(types, updateHistory) { if(typeof updateHistory == 'undefined') { updateHistory = true; } 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(); if(updateHistory) { // TODO: working // console.log(types[0], getDisplayAttr(types[0]),types.map(getDisplayAttr)); history.pushState({types: types}, "", "/@type/"+(types.map(getDisplayAttr).join("+"))); } else { history.replaceState({types: types}, "", "/@type/"+(types.map(getDisplayAttr).join("+"))); } 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 = getNodeLabel(relNode) let year = getNodeYear(relNode); if(year !== null) { titleEl.innerHTML += `${getNodeYear(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 = getDisplayAttr(relNode['@type']); typeEl.title = relNode['@type']; typeEl.addEventListener('click',function(e){ centerByType(relNode['@type']); }); el.appendChild(titleEl); el.appendChild(typeEl); return el; } var setDetails = function(nodeDatum, nodeIdx) { document.body.classList.add("detailsOpen"); scrollToY(0, 4000); while (nodeDetailEl.hasChildNodes()) { nodeDetailEl.removeChild(nodeDetailEl.lastChild); } // TODO: replace relUp & relDown with linkMap let relUp = []; let relDown = []; let pageTitles = []; let nodeDetailScalerEl = document.createElement('div'); // nodeDetailScalerEl.innerHTML = `
`; nodeDetailScalerEl.id = 'nodeDetailsScaler'; nodeDetailScalerEl.addEventListener('mousedown', function(e){ // console.log('go'); let drag = function(e) { // 5px for padding nodeDetailEl.style.width = (window.innerWidth - e.clientX + 5) +'px'; } document.body.addEventListener('mousemove', drag); document.body.addEventListener('mouseup', function(){ document.body.removeEventListener('mousemove', drag); }); }); nodeDetails.appendChild(nodeDetailScalerEl); let breadcrumbsEl = document.createElement('ul'); breadcrumbsEl.classList.add('breadcrumbs'); for(let crumbNodeId of breadcrumbs[nodeDatum['@id']]) { let crumbWrapEl = document.createElement('li'); let crumbEl = document.createElement('span'); crumbEl.classList.add('crumb'); crumbEl.addEventListener('click', function(e){ let idx = graph.nodes.indexOf(nodeMap[crumbNodeId]); selectNode(idx); }); crumbEl.innerHTML = `${getNodeLabel(nodeMap[crumbNodeId])}`; let nodeYear = getNodeYear(nodeMap[crumbNodeId]); if(nodeYear !== null) { crumbEl.innerHTML += `${nodeYear}`; } crumbWrapEl.appendChild(crumbEl); breadcrumbsEl.appendChild(crumbWrapEl); pageTitles.push(getNodeLabel(nodeMap[crumbNodeId])); } nodeDetailEl.appendChild(breadcrumbsEl); pageTitles.push(getNodeLabel(nodeDatum)); let titleAttr = getLabelAttribute(nodeDatum); let titleEl = document.createElement('h2'); titleEl.innerHTML = getNodeLabel(nodeDatum); let typeEl = document.createElement('span'); typeEl.classList.add('nodeType') typeEl.innerHTML = getDisplayAttr(nodeDatum['@type']); typeEl.title = 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']}
`; let skipNodeAttributes = [ '@id','x','y','index','@type','vy','vx','fx','fy','leftX','rightX' ]; if(titleAttr !== 'https://schema.org/contentUrl') { skipNodeAttributes[skipNodeAttributes.length] = titleAttr; } for (let attr in nodeDatum) { if(skipNodeAttributes.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 == 'https://schema.org/url' || attr == 'http://www.w3.org/2000/01/rdf-schema#seeAlso') { listEl.innerHTML += `
${getDisplayAttr(attr)}
${nodeAttr[i]}
`; } else if(attr == 'https://schema.org/embedUrl') { listEl.innerHTML += `
${getDisplayAttr(attr)}
${nodeAttr[i]}
`; listEl.innerHTML += `
`; } else if(attr == 'https://schema.org/contentUrl') { listEl.innerHTML += `
${getDisplayAttr(attr)}
${nodeAttr[i]}
`; if(nodeDatum['@type'] == 'https://schema.org/VideoObject') { let videoType = nodeAttr['https://schema.org/encodingFormat'] ? `type='${nodeAttr['https://schema.org/encodingFormat']}'`: ""; let poster = nodeAttr['https://schema.org/thumbnailUrl'] ? `poster='${nodeAttr['https://schema.org/thumbnailUrl']}'`: ""; listEl.innerHTML += `
`; } else{ listEl.innerHTML += `
`; } } else { let valueHtml = nodeAttr[i].replace(/\n/g,"
"); listEl.innerHTML += `
${getDisplayAttr(attr)}
${valueHtml}
`; } } } 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 incomming
for(let attr in relDown) { let attrEl = document.createElement("dt"); attrEl.innerHTML = getDisplayAttr(attr); relsEl.appendChild(attrEl); // highest pagerank first: relDown[attr].sort((a,b) => weights[b['@id']] - weights[a['@id']]); for(let i in relDown[attr]) { let rel = relDown[attr][i]; relsEl.appendChild(createRelationshipEl(rel)); if(typeof rel['https://schema.org/contentUrl'] != 'undefined') { let ddEl = document.createElement('dd') ddEl.classList.add('dd-contentobject'); if(rel['@type'] == 'https://schema.org/VideoObject') { let videoType = rel['https://schema.org/encodingFormat'] ? `type='${rel['https://schema.org/encodingFormat']}'`: ""; let poster = rel['https://schema.org/thumbnailUrl'] ? `poster='${rel['https://schema.org/thumbnailUrl']}'`: ""; ddEl.innerHTML += ``; } else{ ddEl.innerHTML = `` } relsEl.appendChild(ddEl); } } } // relationships / links outgoing
for(let attr in relUp) { let attrEl = document.createElement("dt"); attrEl.innerHTML = getDisplayAttr(attr); relsEl.appendChild(attrEl); // highest pagerank first: relUp[attr].sort((a,b) => weights[b['@id']] - weights[a['@id']]); for(let i in relUp[attr]) { let rel = relUp[attr][i]; relsEl.appendChild(createRelationshipEl(rel, i)); if(typeof rel['https://schema.org/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'); } }); // TODO: update history & title document.title = pageTitles.join(" :: "); }; var closeDetails = function() { document.body.classList.remove("detailsOpen"); scrollToY(0, 4000); // for mobile } /** * 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, updateHistory){ if(typeof updateHistory == 'undefined') { updateHistory = true; } let nodeEl = null; let nodeDatum = null; node.each(function(d,nIdx,nodeEls){ if(nIdx == idx) { nodeEl = nodeEls[idx]; nodeDatum = d; } }); if(!nodeEl) { return; } if(true) { // always set history state, but replace instead of update on 'updatehistory' let id = null; if(nodeDatum['@id'].startsWith(/*location.origin*/'https://rubenvandeven.com/')){ id = nodeDatum['@id'].substr(26); } else { id = '?id=' + nodeDatum['@id']; } if(updateHistory) { history.pushState({node: idx}, getNodeLabel(nodeDatum), "/"+id); } else { history.replaceState({node: idx}, getNodeLabel(nodeDatum), "/"+id); } } // set global var positionNodesInCenter(idx); let currentCrumbs = breadcrumbs[nodeDatum['@id']].slice(); currentCrumbs[currentCrumbs.length] = nodeDatum['@id']; // set active links. let linkedIdxs = []; link.each(function(d,idx,linkEls,q){ // set nodes 'visible'/highlighted when linked to active node 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)"); } // check if link is part of breadcrumb trail let posSrc = currentCrumbs.indexOf(d.source['@id']); let posTrg = currentCrumbs.indexOf(d.target['@id']); if(posSrc > -1 && posTrg > -1 && Math.abs(posSrc - posTrg) == 1) { linkEls[idx].classList.add('breadcrumbLink'); linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHeadCrumbTrail)"); } else { linkEls[idx].classList.remove('breadcrumbLink'); } }); 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].classList.remove('breadcrumbLink'); linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHead)") }); closeDetails(); } window.addEventListener('popstate', function(event) { if(event.state.hasOwnProperty('node')) { selectNode(event.state['node'], false); } else { // if not sure what to do, fall back to first node (also used to return to opening page) let firstNode = graph['nodes'].find(n => n['@id'] === firstNodeId); selectNode(graph['nodes'].indexOf(firstNode), false); } }); var forceCx, forceCy; var setViewboxForceCenter = function() { let viewBox = getViewbox(); let zoom = getZoomValues(); forceCx = viewBox[0] + viewBox[2]/2 - zoom['dx']; forceCy = viewBox[1] + viewBox[3]/2 - zoom['dy']; } var getZoomValues = function(){ let zoomContainer = document.getElementById("container"); let dx = 0, dy = 0, scale = 1; if(zoomContainer.transform.baseVal.length > 0) { for(let transform of zoomContainer.transform.baseVal) { if(transform.type == SVGTransform.SVG_TRANSFORM_TRANSLATE) { dx += transform.matrix.e; dy += transform.matrix.f; } else if (transform.type == SVGTransform.SVG_TRANSFORM_SCALE) { scale *= transform.matrix.a; // assume simple scale } } } return {'dx': dx, 'dy': dy, 'scale': scale}; } setViewboxForceCenter(); // sets forceCx & forceCy var graphInitialised = false; simulation.force('centerActive', function force(alpha) { // let currentNode = node.selectAll('.detail'); // console.log(currentNode); // console.log(forceCx, forceCy); node.each(function(d, idx, nodes){ let n = d; let k = alpha * 0.1; n.fx = null; n.fy = null; if(typeof nodePositions[idx] != 'undefined') { if(graphInitialised == false) { n.x = nodePositions[idx][0]; n.y = nodePositions[idx][1]; n.vx = 0; n.vy = 0; } else { 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 dx = n.x - forceCx; let dy = n.y - forceCy; if(!inCircle(dx, dy, currentNodePositionRadius)) { return; } if(graphInitialised == false) { // on init, fixate items outside of circle n.fx = n.x + dx * (2+Math.random()); n.fy = n.y + dy * (2+Math.random()); } else { // if initialised, gradually move them outwards n.vx += dx * k*4; n.vy += dy * k*4; } } }); }); //path to curve the tile var nodePath = node.append("path") .attr("id", function(d,idx){return "nodePath"+idx;}) .attr("d", function(d){ var r = getSizeForNode(d) * 0.9; var startX = getSizeForNode(d); // 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]; selectNode(idx, node, d); }) .on('mouseover', function(n, nIdx){ link.each(function(l,idx,linkEls,q){ // set nodes 'visible'/highlighted when linked to active node if(l.source == n || l.target == n) { linkEls[idx].classList.add('hoverLink'); } }); }) .on('mouseout', function(){ let hoverLinkEls = document.getElementsByClassName('hoverLink'); while(hoverLinkEls.length > 0){ hoverLinkEls[0].classList.remove('hoverLink'); } }); // svg.call(d3.drag() // .on("start", function(d){ // if(d3.event.sourceEvent.type == 'touchstart' && d3.event.sourceEvent.touches.length > 1) { // } else { // d3.event.sourceEvent.stopPropagation(); // svg.node().classList.add("dragging"); // } // }) // .on("drag", function(){ // moveViewboxPx(d3.event.dx, d3.event.dy); // }) // .on("end", function(){ // svg.node().classList.remove("dragging"); // })); svg.call(d3.zoom() .scaleExtent([0.3,3]) .on("start", function(){ svg.node().classList.add("dragging"); }) .on("end", function(){ svg.node().classList.remove("dragging"); }) .on("zoom", function(a,b,c){ container.attr("transform", d3.event.transform); }) ); // svg.call(d3.zoom.transform, d3.zoomIdentity); node.append('circle') .attr("r", (d) => getSizeForNode(d)) .attr("class", "nodeBg") ; node.append('circle') .attr("r", (d) => getSizeForNode(d) * 1.08) // nodeSize + margin .attr("class", "highlightCircle") ; node.append('text') .attr("class", "nodeType") .text(function(n){ return n['@type']; }) node.append('text') .attr("class", "nodeYear") .attr("y", "22") .text(function(n){ return getNodeYear(n); }) ; let splitText = function(text){ let characters = [" ","-","\u00AD"]; let charSplitPos = {}; let mid = Math.floor(text.length / 2); let splitPos = false; let splitPosChar = false; // split sentences for(let char of characters) { if(text.indexOf(char) < 0) { continue; } let tmid = text.substr(0,mid).lastIndexOf(char); if(tmid === -1) { tmid = text.indexOf(char); } tmid += 1; // we want to cut _after_ the character // console.log("Char", char, tmid); if(splitPos === false || Math.abs(tmid-mid) < Math.abs(splitPos - mid)){ // console.log("least!"); splitPos = tmid; splitPosChar = char; } } // console.log("pos",splitPos) if(splitPos === false) { return false; } let text1 = text.substr(0, splitPos).trim(); let text2 = text.substr(splitPos).trim(); if(splitPosChar == "\u00AD") { text1 += "-"; } // find most equal split return [text1, text2]; } let nodeTitle = node.append('text') .attr("class", "nodeTitle") .attr("y", "5") ; nodeTitle // .append("textPath") // .attr( "xlink:href",function(d, idx){return '#nodePath'+idx;}) // .text(getNodeLabel) .each(function(node, nodes){ let textLength; let self = d3.select(this); let titleText = getNodeLabel(node); let titleTexts = false; if(titleText.length > 20) { titleTexts = splitText(titleText); } if(titleTexts !== false) { let tspan1 = self.append("tspan") .text(titleTexts[0]) .attr("y", "-10") .attr("x", "0") ; let tspan = self.append("tspan") .text(titleTexts[1]) .attr("y", "10") .attr("x", "0") ; let textLength1 = tspan.node().getComputedTextLength(); let textLength2 = tspan.node().getComputedTextLength(); textLength = Math.max(textLength1, textLength2); } else { self.text(titleText); textLength = self.node().getComputedTextLength(); } // scale according to text length: if(textLength > getSizeForNode(node) * 2) { self.attr('transform', `scale(${(getSizeForNode(node) * 2) / textLength / 1.05})`); } }) ; node.each(function(d) { if(!d['https://schema.org/thumbnailUrl']) { return; } d3.select(this).append('svg:image') .attr("xlink:href", d['https://schema.org/thumbnailUrl']) .attr("width", (d) => getSizeForNode(d)*2) .attr("height", (d) => getSizeForNode(d)* 2) .attr("transform",(d) => "translate(-"+getSizeForNode(d)+" -"+getSizeForNode(d)+")") .attr("clip-path","url(#clipNodeImage)") .attr("preserveAspectRatio","xMidYMid slice") ; }); simulation .nodes(graph.nodes) .on("tick", ticked); simulation.force("link") .links(graph.links) .distance(function(l){ switch (l.name) { // case 'publishedAt': // return 200; // case 'image': // return 200; default: return 300; } }) // 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 = getSizeForNode(d.source)+3.2; let tgtSize = getSizeForNode(d.target)+3.2; // 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; // if dx/dy == 0/0 -> deg == NaN if(isNaN(deg)) { return ""; } return "translate("+x+" "+y+") rotate("+deg+") translate(0, -10)"; }); 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); if(location.pathname.startsWith('/@type/')) { for(let t in types) { if(getDisplayAttr(t) == location.pathname.substr(7)) { centerByType(t, false); } } } else{ let startNodeId = location.search.startsWith("?id=") ? location.search.substr(4) : 'https://rubenvandeven.com'+location.pathname; let firstNode = graph['nodes'].find(n => n['@id'] === startNodeId); selectNode(graph['nodes'].indexOf(firstNode), false); } // closeDetails(); // hide details at first // positionNodesInCenter(currentNodeIdx+1); // setTimeout(function(){ // document.body.classList.add('graphInitialised'); // }, 10); let initPlaceholder = document.getElementById('initPlaceholder'); svg.node().removeChild(initPlaceholder); setTimeout(function(){ graphInitialised = true; document.body.classList.add('graphInitialised'); }, 500); } // Detect request animation frame var reqAnimFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || // IE Fallback, you can even fallback to onscroll function(callback){ window.setTimeout(callback, 1000/60) }; // all credits go to https://stackoverflow.com/a/26798337 function scrollToY(scrollTargetY, speed, easing, finishFunction) { // scrollTargetY: the target scrollY property of the window // speed: time in pixels per second // easing: easing equation to use var scrollY = window.scrollY, scrollTargetY = scrollTargetY || 0, speed = speed || 2000, easing = easing || 'easeOutSine', currentTime = 0, finishFunction = finishFunction || false; // min time .1, max time .8 seconds let time = Math.max(.1, Math.min(Math.abs(scrollY - scrollTargetY) / speed, .8)); // easing equations from https://github.com/danro/easing-js/blob/master/easing.js let PI_D2 = Math.PI / 2, easingEquations = { easeOutSine: function (pos) { return Math.sin(pos * (Math.PI / 2)); }, easeInOutSine: function (pos) { return (-0.5 * (Math.cos(Math.PI * pos) - 1)); }, easeInOutQuint: function (pos) { if ((pos /= 0.5) < 1) { return 0.5 * Math.pow(pos, 5); } return 0.5 * (Math.pow((pos - 2), 5) + 2); } }; // add animation loop function tick() { currentTime += 1 / 60; var p = currentTime / time; var t = easingEquations[easing](p); if (p < 1) { reqAnimFrame(tick); window.scrollTo(0, scrollY + ((scrollTargetY - scrollY) * t)); } else { window.scrollTo(0, scrollTargetY); if(finishFunction) { finishFunction(); } } } // call it once to get started tick(); }