1446 lines
No EOL
49 KiB
JavaScript
1446 lines
No EOL
49 KiB
JavaScript
'use strict';
|
|
|
|
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
|
|
|
|
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;
|
|
case "https://schema.org/OrganizationRole":
|
|
if (typeof node['https://schema.org/roleName'] !== "undefined") {
|
|
return 'https://schema.org/roleName';
|
|
}
|
|
break;
|
|
}
|
|
return '@id';
|
|
}
|
|
function getNodeLabel(node) {
|
|
var labelAttr = getLabelAttribute(node);
|
|
var 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') {
|
|
if (n['https://schema.org/dateCreated'].length == 9) {
|
|
return n['https://schema.org/dateCreated'];
|
|
}
|
|
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']);
|
|
var year = n['https://schema.org/startDate'].substr(0, 4);
|
|
return year;
|
|
}
|
|
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) {
|
|
var nodes = {};
|
|
var links = [];
|
|
|
|
// collect all nodes
|
|
for (var 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 (var _nodeId in data) {
|
|
var node = data[_nodeId];
|
|
var currentId = node["@id"];
|
|
for (var key in node) {
|
|
var 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 (var 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
|
|
var requestPromise = fetch('/assets/js/rubenvandeven.jsonld').then(function (r) {
|
|
return r.json();
|
|
});
|
|
var rankingPromise = fetch('/assets/js/ranking.json').then(function (r) {
|
|
return r.json();
|
|
});
|
|
|
|
Promise.all([requestPromise, rankingPromise]).then(function (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 (var 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
|
|
var dxAbs = Math.abs(dx);
|
|
var 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) {
|
|
var linkMap = {};
|
|
var _iteratorNormalCompletion = true;
|
|
var _didIteratorError = false;
|
|
var _iteratorError = undefined;
|
|
|
|
try {
|
|
for (var _iterator = graph['links'][Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
|
|
var link = _step.value;
|
|
|
|
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'] };
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError = true;
|
|
_iteratorError = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion && _iterator.return) {
|
|
_iterator.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError) {
|
|
throw _iteratorError;
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
var crumbs = {};
|
|
|
|
var createBreadcrumbLayer = function createBreadcrumbLayer(srcId) {
|
|
var path = crumbs[srcId];
|
|
var newPath = path.slice();
|
|
newPath.push(srcId);
|
|
|
|
var nextSrcIds = [];
|
|
var _iteratorNormalCompletion2 = true;
|
|
var _didIteratorError2 = false;
|
|
var _iteratorError2 = undefined;
|
|
|
|
try {
|
|
for (var _iterator2 = linkMap[srcId][Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
|
|
var link = _step2.value;
|
|
|
|
if (typeof crumbs[link['id']] !== 'undefined') continue;
|
|
crumbs[link['id']] = newPath;
|
|
nextSrcIds.push(link['id']);
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError2 = true;
|
|
_iteratorError2 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion2 && _iterator2.return) {
|
|
_iterator2.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError2) {
|
|
throw _iteratorError2;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nextSrcIds;
|
|
};
|
|
crumbs[srcId] = [];
|
|
var nextIds = [srcId];
|
|
while (nextIds.length > 0) {
|
|
var newNextIds = [];
|
|
var _iteratorNormalCompletion3 = true;
|
|
var _didIteratorError3 = false;
|
|
var _iteratorError3 = undefined;
|
|
|
|
try {
|
|
for (var _iterator3 = nextIds[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
|
|
var nextId = _step3.value;
|
|
|
|
var r = createBreadcrumbLayer(nextId);
|
|
newNextIds = newNextIds.concat(r);
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError3 = true;
|
|
_iteratorError3 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion3 && _iterator3.return) {
|
|
_iterator3.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError3) {
|
|
throw _iteratorError3;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (var nodeIdx in graph['nodes']) {
|
|
var 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
|
|
var i = 0;
|
|
|
|
var _loop = function _loop(typeCountIdx) {
|
|
var typeName = typeCounts[typeCountIdx][0];
|
|
var typeLinkEl = document.createElement("li");
|
|
var typeLinkAEl = document.createElement("a");
|
|
var 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 () {
|
|
var typeNodeEls = document.getElementsByClassName(typeName);
|
|
var _iteratorNormalCompletion7 = true;
|
|
var _didIteratorError7 = false;
|
|
var _iteratorError7 = undefined;
|
|
|
|
try {
|
|
for (var _iterator7 = typeNodeEls[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) {
|
|
var typeNodeEl = _step7.value;
|
|
|
|
typeNodeEl.classList.add('typeHighlight');
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError7 = true;
|
|
_iteratorError7 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion7 && _iterator7.return) {
|
|
_iterator7.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError7) {
|
|
throw _iteratorError7;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
typeLinkAEl.addEventListener('mouseout', function () {
|
|
var typeNodeEls = document.getElementsByClassName(typeName);
|
|
var _iteratorNormalCompletion8 = true;
|
|
var _didIteratorError8 = false;
|
|
var _iteratorError8 = undefined;
|
|
|
|
try {
|
|
for (var _iterator8 = typeNodeEls[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) {
|
|
var typeNodeEl = _step8.value;
|
|
|
|
typeNodeEl.classList.remove('typeHighlight');
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError8 = true;
|
|
_iteratorError8 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion8 && _iterator8.return) {
|
|
_iterator8.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError8) {
|
|
throw _iteratorError8;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
typeLinkEl.append(typeLinkAEl);
|
|
typeLinkEl.append(typeLinkCountEl);
|
|
(i < 5 ? typeLinksEl : moreTypeLinksEl).appendChild(typeLinkEl);
|
|
i++;
|
|
// typeLinksEl.appendChild(typeLinkEl);
|
|
};
|
|
|
|
for (var typeCountIdx in typeCounts) {
|
|
_loop(typeCountIdx);
|
|
}
|
|
|
|
showMoreTypeLinksEl.addEventListener('click', function () {
|
|
document.body.classList.add('showMoreLinks');
|
|
var hideMoreTypeLinks = function hideMoreTypeLinks(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) {
|
|
var baseClasses = 'node ' + d['@type'];
|
|
if (d['@type']) {
|
|
var slashpos = d['@type'].lastIndexOf('/');
|
|
if (slashpos > -1) {
|
|
baseClasses += ' ' + d['@type'].substr(slashpos + 1);
|
|
}
|
|
}
|
|
return baseClasses;
|
|
});
|
|
var getViewbox = function getViewbox() {
|
|
return svg.attr("viewBox").split(" ").map(parseFloat);
|
|
};
|
|
var positionNodesInCenter = function positionNodesInCenter(idxs) {
|
|
setViewboxForceCenter(); // sets forceCx & forceCy
|
|
if ((typeof idxs === 'undefined' ? 'undefined' : _typeof(idxs)) == "object" && idxs !== null && idxs.length == 1) {
|
|
idxs = idxs[0];
|
|
}
|
|
|
|
nodePositions = {}; // reset
|
|
if (idxs === null) {
|
|
return;
|
|
} else if ((typeof idxs === 'undefined' ? 'undefined' : _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 positionNodesInCircle(idxs, r) {
|
|
var viewBox = getViewbox();
|
|
var 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;
|
|
var forceCx = viewBox[0] + viewBox[2] / 2 - zoom['dx'];
|
|
var forceCy = viewBox[1] + viewBox[3] / 2 - zoom['dy'];
|
|
|
|
var 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 centerByType(types, updateHistory) {
|
|
if (typeof updateHistory == 'undefined') {
|
|
updateHistory = true;
|
|
}
|
|
if (!Array.isArray(types)) {
|
|
types = [types];
|
|
}
|
|
var idxs = [];
|
|
for (var 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 createRelationshipEl(relNode, i) {
|
|
var el = document.createElement("dd");
|
|
el.classList.add('relLink');
|
|
var titleEl = document.createElement('a');
|
|
titleEl.innerHTML = getNodeLabel(relNode);
|
|
var 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) {
|
|
var idx = graph.nodes.indexOf(relNode);
|
|
selectNode(idx);
|
|
});
|
|
var 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 setDetails(nodeDatum, nodeIdx) {
|
|
document.body.classList.add("detailsOpen");
|
|
scrollToY(0, 4000);
|
|
while (nodeDetailEl.hasChildNodes()) {
|
|
nodeDetailEl.removeChild(nodeDetailEl.lastChild);
|
|
}
|
|
|
|
// TODO: replace relUp & relDown with linkMap
|
|
var relUp = [];
|
|
var relDown = [];
|
|
var pageTitles = [];
|
|
var nodeDetailScalerEl = document.createElement('div');
|
|
// nodeDetailScalerEl.innerHTML = `<div id='scalarbar'></div>`;
|
|
nodeDetailScalerEl.id = 'nodeDetailsScaler';
|
|
nodeDetailScalerEl.addEventListener('mousedown', function (e) {
|
|
// console.log('go');
|
|
var drag = function drag(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);
|
|
|
|
var breadcrumbsEl = document.createElement('ul');
|
|
breadcrumbsEl.classList.add('breadcrumbs');
|
|
|
|
var _loop2 = function _loop2(crumbNodeId) {
|
|
var crumbWrapEl = document.createElement('li');
|
|
var crumbEl = document.createElement('span');
|
|
crumbEl.classList.add('crumb');
|
|
crumbEl.addEventListener('click', function (e) {
|
|
var idx = graph.nodes.indexOf(nodeMap[crumbNodeId]);
|
|
selectNode(idx);
|
|
});
|
|
crumbEl.innerHTML = '' + getNodeLabel(nodeMap[crumbNodeId]);
|
|
var 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]));
|
|
};
|
|
|
|
var _iteratorNormalCompletion4 = true;
|
|
var _didIteratorError4 = false;
|
|
var _iteratorError4 = undefined;
|
|
|
|
try {
|
|
for (var _iterator4 = breadcrumbs[nodeDatum['@id']][Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
|
|
var crumbNodeId = _step4.value;
|
|
|
|
_loop2(crumbNodeId);
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError4 = true;
|
|
_iteratorError4 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion4 && _iterator4.return) {
|
|
_iterator4.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError4) {
|
|
throw _iteratorError4;
|
|
}
|
|
}
|
|
}
|
|
|
|
nodeDetailEl.appendChild(breadcrumbsEl);
|
|
pageTitles.push(getNodeLabel(nodeDatum));
|
|
|
|
var titleAttr = getLabelAttribute(nodeDatum);
|
|
var titleEl = document.createElement('h2');
|
|
titleEl.innerHTML = getNodeLabel(nodeDatum);
|
|
|
|
var 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);
|
|
|
|
var listEl = document.createElement("dl");
|
|
// listEl.innerHTML += `<dt>type</dt><dd>${nodeDatum['@type']}</dd>`;
|
|
|
|
var skipNodeAttributes = ['@id', 'x', 'y', 'index', '@type', 'vy', 'vx', 'fx', 'fy', 'leftX', 'rightX'];
|
|
if (titleAttr !== 'https://schema.org/contentUrl') {
|
|
skipNodeAttributes[skipNodeAttributes.length] = titleAttr;
|
|
}
|
|
for (var attr in nodeDatum) {
|
|
if (skipNodeAttributes.indexOf(attr) != -1) {
|
|
continue;
|
|
}
|
|
|
|
// approach all as array
|
|
var nodeAttr = Array.isArray(nodeDatum[attr]) ? nodeDatum[attr] : [nodeDatum[attr]];
|
|
for (var _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 == 'https://schema.org/identifier' || 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') {
|
|
// console.log(nodeDatum, nodeAttr);
|
|
// let videoType = nodeDatum['https://schema.org/encodingFormat'] ? `type='${nodeDatum['https://schema.org/encodingFormat']}'`: "";
|
|
var videoType = "";
|
|
var poster = nodeDatum['https://schema.org/thumbnailUrl'] ? 'poster=\'' + nodeDatum['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 {
|
|
var 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);
|
|
|
|
var relsEl = document.createElement("dl");
|
|
// collect relationships
|
|
for (var i = 0; i < graph.links.length; i++) {
|
|
var _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 (var _attr in relDown) {
|
|
var attrEl = document.createElement("dt");
|
|
attrEl.innerHTML = getDisplayAttr(_attr);
|
|
relsEl.appendChild(attrEl);
|
|
|
|
// highest pagerank first:
|
|
relDown[_attr].sort(function (a, b) {
|
|
return weights[b['@id']] - weights[a['@id']];
|
|
});
|
|
|
|
for (var _i2 in relDown[_attr]) {
|
|
var rel = relDown[_attr][_i2];
|
|
relsEl.appendChild(createRelationshipEl(rel));
|
|
if (typeof rel['https://schema.org/contentUrl'] != 'undefined') {
|
|
var ddEl = document.createElement('dd');
|
|
ddEl.classList.add('dd-contentobject');
|
|
if (rel['@type'] == 'https://schema.org/VideoObject') {
|
|
var _videoType = rel['https://schema.org/encodingFormat'] ? 'type=\'' + rel['https://schema.org/encodingFormat'] + '\'' : "";
|
|
var _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 (var _attr2 in relUp) {
|
|
var _attrEl = document.createElement("dt");
|
|
_attrEl.innerHTML = getDisplayAttr(_attr2);
|
|
relsEl.appendChild(_attrEl);
|
|
|
|
// highest pagerank first:
|
|
relUp[_attr2].sort(function (a, b) {
|
|
return weights[b['@id']] - weights[a['@id']];
|
|
});
|
|
|
|
for (var _i3 in relUp[_attr2]) {
|
|
var _rel = relUp[_attr2][_i3];
|
|
relsEl.appendChild(createRelationshipEl(_rel, _i3));
|
|
if (typeof _rel['https://schema.org/contentUrl'] != 'undefined') {
|
|
var _ddEl = document.createElement('dd');
|
|
_ddEl.classList.add('dd-contentobject');
|
|
if (_rel['@type'] == 'https://schema.org/VideoObject') {
|
|
var _videoType2 = _rel['https://schema.org/encodingFormat'] ? 'type=\'' + _rel['https://schema.org/encodingFormat'] + '\'' : "";
|
|
var _poster2 = _rel['https://schema.org/thumbnailUrl'] ? 'poster=\'' + _rel['https://schema.org/thumbnailUrl'] + '\'' : "";
|
|
_ddEl.innerHTML += '<video controls preload="none" ' + _poster2 + '><source src=\'' + _rel['https://schema.org/contentUrl'] + '\' ' + _videoType2 + '></video>';
|
|
} else {
|
|
_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 closeDetails() {
|
|
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 selectNode(idx, updateHistory) {
|
|
if (typeof updateHistory == 'undefined') {
|
|
updateHistory = true;
|
|
}
|
|
|
|
var nodeEl = null;
|
|
var 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'
|
|
var 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);
|
|
|
|
var currentCrumbs = breadcrumbs[nodeDatum['@id']].slice();
|
|
currentCrumbs[currentCrumbs.length] = nodeDatum['@id'];
|
|
|
|
// set active links.
|
|
var 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) {
|
|
var 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
|
|
var posSrc = currentCrumbs.indexOf(d.source['@id']);
|
|
var 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');
|
|
}
|
|
});
|
|
|
|
var i = linkedIdxs.indexOf(idx);
|
|
|
|
if (i !== -1) {
|
|
linkedIdxs.splice(i, 1);
|
|
}
|
|
|
|
positionNodesInCircle(linkedIdxs);
|
|
|
|
setDetails(nodeDatum, idx);
|
|
};
|
|
var deselectNode = function deselectNode() {
|
|
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)
|
|
var firstNode = graph['nodes'].find(function (n) {
|
|
return n['@id'] === firstNodeId;
|
|
});
|
|
selectNode(graph['nodes'].indexOf(firstNode), false);
|
|
}
|
|
});
|
|
|
|
var forceCx, forceCy;
|
|
var setViewboxForceCenter = function setViewboxForceCenter() {
|
|
var viewBox = getViewbox();
|
|
var zoom = getZoomValues();
|
|
forceCx = viewBox[0] + viewBox[2] / 2 - zoom['dx'];
|
|
forceCy = viewBox[1] + viewBox[3] / 2 - zoom['dy'];
|
|
};
|
|
|
|
var getZoomValues = function getZoomValues() {
|
|
var zoomContainer = document.getElementById("container");
|
|
var dx = 0,
|
|
dy = 0,
|
|
scale = 1;
|
|
if (zoomContainer.transform.baseVal.length > 0) {
|
|
var _iteratorNormalCompletion5 = true;
|
|
var _didIteratorError5 = false;
|
|
var _iteratorError5 = undefined;
|
|
|
|
try {
|
|
for (var _iterator5 = zoomContainer.transform.baseVal[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
|
|
var transform = _step5.value;
|
|
|
|
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
|
|
}
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError5 = true;
|
|
_iteratorError5 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion5 && _iterator5.return) {
|
|
_iterator5.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError5) {
|
|
throw _iteratorError5;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
var n = d;
|
|
var 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;
|
|
}
|
|
|
|
var dx = n.x - forceCx;
|
|
var 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) {
|
|
var 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 () {
|
|
var 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", function (d) {
|
|
return getSizeForNode(d);
|
|
}).attr("class", "nodeBg");
|
|
node.append('circle').attr("r", function (d) {
|
|
return 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);
|
|
});
|
|
var splitText = function splitText(text) {
|
|
var characters = [" ", "-", '\xAD'];
|
|
var charSplitPos = {};
|
|
var mid = Math.floor(text.length / 2);
|
|
var splitPos = false;
|
|
var splitPosChar = false;
|
|
// split sentences
|
|
var _iteratorNormalCompletion6 = true;
|
|
var _didIteratorError6 = false;
|
|
var _iteratorError6 = undefined;
|
|
|
|
try {
|
|
for (var _iterator6 = characters[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
|
|
var char = _step6.value;
|
|
|
|
if (text.indexOf(char) < 0) {
|
|
continue;
|
|
}
|
|
var 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)
|
|
|
|
} catch (err) {
|
|
_didIteratorError6 = true;
|
|
_iteratorError6 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion6 && _iterator6.return) {
|
|
_iterator6.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError6) {
|
|
throw _iteratorError6;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (splitPos === false) {
|
|
return false;
|
|
}
|
|
|
|
var text1 = text.substr(0, splitPos).trim();
|
|
var text2 = text.substr(splitPos).trim();
|
|
|
|
if (splitPosChar == '\xAD') {
|
|
text1 += "-";
|
|
}
|
|
|
|
// find most equal split
|
|
return [text1, text2];
|
|
};
|
|
var 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) {
|
|
var textLength = void 0;
|
|
var self = d3.select(this);
|
|
var titleText = getNodeLabel(node);
|
|
var titleTexts = false;
|
|
if (titleText.length > 20) {
|
|
titleTexts = splitText(titleText);
|
|
}
|
|
if (titleTexts !== false) {
|
|
var tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "-10").attr("x", "0");
|
|
var tspan = self.append("tspan").text(titleTexts[1]).attr("y", "10").attr("x", "0");
|
|
var textLength1 = tspan.node().getComputedTextLength();
|
|
var 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", function (d) {
|
|
return getSizeForNode(d) * 2;
|
|
}).attr("height", function (d) {
|
|
return getSizeForNode(d) * 2;
|
|
}).attr("transform", function (d) {
|
|
return "translate(-" + getSizeForNode(d) + " -" + getSizeForNode(d) + ")";
|
|
}).attr("clip-path", "url(#clipNodeImage)").attr("preserveAspectRatio", "xMidYMid slice");
|
|
});
|
|
node.each(function (d) {
|
|
if (d['@type'] !== 'https://schema.org/VideoObject') {
|
|
return;
|
|
}
|
|
var size = getSizeForNode(d);
|
|
d3.select(this).append('svg:polygon').attr('points', "-10,-10, -10,10, 10,0").attr("class", "play");
|
|
});
|
|
|
|
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;
|
|
*/
|
|
var srcSize = getSizeForNode(d.source) + 3.2;
|
|
var 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) {
|
|
var dx = (d.target.x - d.source.x) / 2;
|
|
var dy = (d.target.y - d.source.y) / 2;
|
|
var x = d.source.x + dx;
|
|
var y = d.source.y + dy;
|
|
var 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();
|
|
var 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);
|
|
var nodeEl = nodes[idx];
|
|
d.fx = null;
|
|
d.fy = null;
|
|
nodeEl.classList.remove('drag');
|
|
}
|
|
|
|
function moveViewboxPx(dx, dy) {
|
|
var 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 (var t in types) {
|
|
if (getDisplayAttr(t) == location.pathname.substr(7)) {
|
|
centerByType(t, false);
|
|
}
|
|
}
|
|
} else {
|
|
var startNodeId = location.search.startsWith("?id=") ? location.search.substr(4) : 'https://rubenvandeven.com' + location.pathname;
|
|
var firstNode = graph['nodes'].find(function (n) {
|
|
return 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);
|
|
|
|
var 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
|
|
var 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
|
|
var PI_D2 = Math.PI / 2,
|
|
easingEquations = {
|
|
easeOutSine: function easeOutSine(pos) {
|
|
return Math.sin(pos * (Math.PI / 2));
|
|
},
|
|
easeInOutSine: function easeInOutSine(pos) {
|
|
return -0.5 * (Math.cos(Math.PI * pos) - 1);
|
|
},
|
|
easeInOutQuint: function easeInOutQuint(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();
|
|
} |