portfolio/src/js/portfolio.js

1296 lines
41 KiB
JavaScript

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 += `<span class='nodeYear'>${getNodeYear(relNode)}</span>`;
}
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 = `<div id='scalarbar'></div>`;
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 += `<span class='nodeYear'>${nodeYear}</span>`;
}
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 += `<dt>type</dt><dd>${nodeDatum['@type']}</dd>`;
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 += `<dt class='dt-${getDisplayAttr(attr)}' title='${attr}'>${getDisplayAttr(attr)}</dt><dd class='dd-${getDisplayAttr(attr)}'><a href='${nodeAttr[i]}'>${nodeAttr[i]}</a></dd>`;
} else if(attr == 'https://schema.org/embedUrl') {
listEl.innerHTML += `<dt class='dt-${getDisplayAttr(attr)}' title='${attr}'>${getDisplayAttr(attr)}</dt><dd class='dd-${getDisplayAttr(attr)}'><a href='${nodeAttr[i]}'>${nodeAttr[i]}</a></dd>`;
listEl.innerHTML += `<dd class='dd-embed'><embed src='${nodeAttr[i]}'></embed></dd>`;
} else if(attr == 'https://schema.org/contentUrl') {
listEl.innerHTML += `<dt class='dt-${getDisplayAttr(attr)}' title='${attr}'>${getDisplayAttr(attr)}</dt><dd class='dd-${getDisplayAttr(attr)}'><a href='${nodeAttr[i]}'>${nodeAttr[i]}</a></dd>`;
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 += `<dd class='dd-contentobject'><video controls ${poster} autoplay><source src='${nodeAttr[i]}' ${videoType}></video></dd>`;
} else{
listEl.innerHTML += `<dd class='dd-contentobject'><object data='${nodeAttr[i]}'></object></dd>`;
}
} else {
let valueHtml = nodeAttr[i].replace(/\n/g,"<br>");
listEl.innerHTML += `<dt class='dt-${getDisplayAttr(attr)}' title='${attr}'>${getDisplayAttr(attr)}</dt><dd class='dd-${getDisplayAttr(attr)}'>${valueHtml}</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 incomming <dl>
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 += `<video controls preload="none" ${poster}><source src='${rel['https://schema.org/contentUrl']}' ${videoType}></video>`;
} else{
ddEl.innerHTML = `<object data='${rel['https://schema.org/contentUrl']}'></object>`
}
relsEl.appendChild(ddEl);
}
}
}
// relationships / links outgoing <dl>
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 = `<object data='${rel['https://schema.org/contentUrl']}'></object>`
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();
}