restructure menu & create breadcrumbs

This commit is contained in:
Ruben van de Ven 2018-09-29 12:54:28 +02:00
parent 14bae34193
commit a0a66bcc65
3 changed files with 324 additions and 69 deletions

View file

@ -41,6 +41,7 @@
</linearGradient> </linearGradient>
<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 6 6" preserveAspectRatio="none" orient="auto" id="arrowHead" fill="#999"><path d="M0,-3L8,0L0,3"></path></marker> <marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 6 6" preserveAspectRatio="none" orient="auto" id="arrowHead" fill="#999"><path d="M0,-3L8,0L0,3"></path></marker>
<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 6 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelected"><path d="M0,-3L8,0L0,3" fill="white"></path></marker> <marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 6 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelected"><path d="M0,-3L8,0L0,3" fill="white"></path></marker>
<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 6 6" preserveAspectRatio="none" orient="auto" id="arrowHeadCrumbTrail"><path d="M0,-3L8,0L0,3" fill="yellow"></path></marker>
<clipPath id="clipNodeImage"> <clipPath id="clipNodeImage">
<circle cx="40" cy="40" r="40" /> <circle cx="40" cy="40" r="40" />
</clipPath> </clipPath>
@ -54,9 +55,9 @@
</div> </div>
</svg> </svg>
<div id="graphControls"> <div id="graphControls">
<span class="typeJump">Show:</span> <span class="typeJump">Types:</span>
<ul id='typeLinks'></ul> <ul id='typeLinks'></ul>
<span class="showMoreTypeLinks">...</span> <a id="showMoreTypeLinks"></a>
<ul id='moreTypeLinks'></ul> <ul id='moreTypeLinks'></ul>
<!-- <ul id='relLinks'></ul> --> <!-- <ul id='relLinks'></ul> -->
</div> </div>

View file

@ -21,6 +21,12 @@ function getTitleAttribute(node) {
function getNodeTitle(node){ function getNodeTitle(node){
return node[getTitleAttribute(node)]; return node[getTitleAttribute(node)];
} }
function getNodeYear(n){
if(typeof n['dateCreated'] !== 'undefined') {
return n['dateCreated'].substr(0,4);
}
return null;
}
/** /**
Transform a flattened jsonld into a d3 compatible graph Transform a flattened jsonld into a d3 compatible graph
@param Object data flattened jsonld data @param Object data flattened jsonld data
@ -81,9 +87,7 @@ function jsonLdToGraph(data){
var graph; var graph;
// map nodes to their ID // map nodes to their ID
var nodeMap = {}; var nodeMap = {};
// TODO: map node IDs to their linked node IDs
var linkMap = {}; var linkMap = {};
// TODO: use linkMap to create breadcrumbs per node
var breadcrumbs = {}; var breadcrumbs = {};
// load the flattened jsonld file // load the flattened jsonld file
const requestPromise = fetch('/assets/js/rubenvandeven.jsonld') const requestPromise = fetch('/assets/js/rubenvandeven.jsonld')
@ -133,20 +137,32 @@ function createLinkMap(graph) {
// TODO: make sure, 'shortest' path is favoured. // TODO: make sure, 'shortest' path is favoured.
function createBreadcrumbs(linkMap, srcId) { function createBreadcrumbs(linkMap, srcId) {
let crumbs = {}; let crumbs = {};
let path = [];
let collectLinks = function(srcId, path){ let createBreadcrumbLayer = function(srcId) {
if(typeof crumbs[srcId] !== 'undefined') { let path = crumbs[srcId];
return; let newPath = path.slice();
} newPath.push(srcId);
crumbs[srcId] = path.slice();
path[path.length] = srcId; let nextSrcIds = [];
let links = linkMap[srcId];
// collect links, append given list & skip srcId for (let link of linkMap[srcId]) {
for(let link of links) { if(typeof crumbs[link['id']] !== 'undefined') continue;
collectLinks(link['id'], path.slice()); 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;
} }
collectLinks(srcId, path);
return crumbs; return crumbs;
} }
@ -172,25 +188,48 @@ for (let nodeIdx in graph['nodes']) {
if(typeof types[type] == 'undefined') { if(typeof types[type] == 'undefined') {
types[type] = []; types[type] = [];
} }
types[type][types[type].length] = nodeIdx; types[type].push(nodeIdx);
} }
var graphControlsEl = document.getElementById('graphControls'); var graphControlsEl = document.getElementById('graphControls');
var typeLinksEl = document.getElementById('typeLinks'); var typeLinksEl = document.getElementById('typeLinks');
var showMoreTypeLinksEl = document.getElementById('showMoreTypeLinks');
var moreTypeLinksEl = document.getElementById('moreTypeLinks'); var moreTypeLinksEl = document.getElementById('moreTypeLinks');
var relLinksEl = document.getElementById('relLinks'); 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 // make controls
let i = 0; let i = 0;
for (let typeName in types) { for (let typeCountIdx in typeCounts) {
let typeName = typeCounts[typeCountIdx][0];
let typeLinkEl = document.createElement("li"); let typeLinkEl = document.createElement("li");
let typeLinkAEl = document.createElement("a"); let typeLinkAEl = document.createElement("a");
let typeLinkCountEl = document.createElement("span"); let typeLinkCountEl = document.createElement("span");
typeLinkCountEl.innerHTML = types[typeName].length; typeLinkCountEl.innerHTML = typeCounts[typeCountIdx][1];
typeLinkCountEl.classList.add('typeCount');
typeLinkAEl.innerHTML = typeName; typeLinkAEl.innerHTML = typeName;
typeLinkAEl.addEventListener('click', function(){ typeLinkAEl.addEventListener('click', function(){
centerByType(typeName); centerByType(typeName);
// positionNodesInCenter(types[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(typeLinkAEl);
typeLinkEl.append(typeLinkCountEl); typeLinkEl.append(typeLinkCountEl);
(i < 5 ? typeLinksEl: moreTypeLinksEl).appendChild(typeLinkEl); (i < 5 ? typeLinksEl: moreTypeLinksEl).appendChild(typeLinkEl);
@ -198,6 +237,19 @@ for (let typeName in types) {
// typeLinksEl.appendChild(typeLinkEl); // typeLinksEl.appendChild(typeLinkEl);
} }
showMoreTypeLinksEl.addEventListener('click', function () {
console.log('showMore');
document.body.classList.add('showMoreLinks');
var hideMoreTypeLinks = function(e) {
console.log('removeMore');
e.preventDefault();
e.stopPropagation();
document.body.removeEventListener('mouseup', hideMoreTypeLinks, true);
document.body.classList.remove('showMoreLinks');
}
document.body.addEventListener('mouseup', hideMoreTypeLinks, true);
}, false)
// make svg // make svg
var svg = d3.select("svg"), var svg = d3.select("svg"),
@ -247,9 +299,7 @@ var getViewbox = function() {
return svg.attr("viewBox").split(" ").map(parseFloat); return svg.attr("viewBox").split(" ").map(parseFloat);
} }
var positionNodesInCenter = function(idxs) { var positionNodesInCenter = function(idxs) {
let viewBox = getViewbox(); setViewboxForceCenter(); // sets forceCx & forceCy
let cx = viewBox[0] + viewBox[2]/2;
let cy = viewBox[1] + viewBox[3]/2;
if(typeof idxs == "object" && idxs !== null && idxs.length == 1) { if(typeof idxs == "object" && idxs !== null && idxs.length == 1) {
idxs = idxs[0]; idxs = idxs[0];
@ -279,8 +329,8 @@ var positionNodesInCenter = function(idxs) {
} }
else{ else{
nodePositions[idxs] = [ nodePositions[idxs] = [
cx, forceCx,
cy forceCy
]; ];
} }
@ -300,6 +350,7 @@ var positionNodesInCenter = function(idxs) {
} }
var positionNodesInCircle = function(idxs, r) { var positionNodesInCircle = function(idxs, r) {
let viewBox = getViewbox(); let viewBox = getViewbox();
setViewboxForceCenter(); // sets forceCx & forceCy
if(typeof r == 'undefined') { if(typeof r == 'undefined') {
if(idxs.length == 1) { if(idxs.length == 1) {
r = viewBox[2] / 6; r = viewBox[2] / 6;
@ -308,14 +359,14 @@ var positionNodesInCircle = function(idxs, r) {
} }
} }
currentNodePositionRadius = r; currentNodePositionRadius = r;
let cx = viewBox[0] + viewBox[2]/2; let forceCx = viewBox[0] + viewBox[2]/2;
let cy = viewBox[1] + viewBox[3]/2; let forceCy = viewBox[1] + viewBox[3]/2;
let stepSize = 2*Math.PI / idxs.length; let stepSize = 2*Math.PI / idxs.length;
for (var i = 0; i < idxs.length; i++) { for (var i = 0; i < idxs.length; i++) {
nodePositions[idxs[i]] = [ nodePositions[idxs[i]] = [
cx + Math.sin(stepSize * i) * r, forceCx + Math.sin(stepSize * i) * r,
cy + Math.cos(stepSize * i) * r forceCy + Math.cos(stepSize * i) * r
]; ];
} }
@ -348,7 +399,11 @@ var createRelationshipEl = function(relNode, i) {
let el = document.createElement("dd"); let el = document.createElement("dd");
el.classList.add('relLink'); el.classList.add('relLink');
let titleEl = document.createElement('a'); let titleEl = document.createElement('a');
titleEl.innerHTML = getNodeTitle(relNode); titleEl.innerHTML = getNodeTitle(relNode)
let year = getNodeYear(relNode);
if(year !== null) {
titleEl.innerHTML += `<span class='nodeYear'>${getNodeYear(relNode)}</span>`;
}
titleEl.classList.add('nodeTitle'); titleEl.classList.add('nodeTitle');
titleEl.classList.add('nodeTitleNr'+i); titleEl.classList.add('nodeTitleNr'+i);
titleEl.addEventListener('click',function(e){ titleEl.addEventListener('click',function(e){
@ -376,10 +431,39 @@ var setDetails = function(nodeDatum, nodeIdx) {
// TODO: replace relUp & relDown with linkMap // TODO: replace relUp & relDown with linkMap
let relUp = []; let relUp = [];
let relDown = []; let relDown = [];
let breadcrumbsEl = document.createElement('div'); 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'); breadcrumbsEl.classList.add('breadcrumbs');
for(let crumbNodeId of breadcrumbs[nodeDatum['id']]) { for(let crumbNodeId of breadcrumbs[nodeDatum['id']]) {
breadcrumbsEl.innerHTML += ` <span class='crumb'>${getNodeTitle(nodeMap[crumbNodeId])}</span>`; 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 = `${getNodeTitle(nodeMap[crumbNodeId])}`;
let nodeYear = getNodeYear(nodeMap[crumbNodeId]);
if(nodeYear !== null) {
crumbEl.innerHTML += `<span class='nodeYear'>${nodeYear}</span>`;
}
crumbWrapEl.appendChild(crumbEl);
breadcrumbsEl.appendChild(crumbWrapEl);
} }
nodeDetailEl.appendChild(breadcrumbsEl); nodeDetailEl.appendChild(breadcrumbsEl);
@ -398,10 +482,15 @@ var setDetails = function(nodeDatum, nodeIdx) {
let listEl = document.createElement("dl"); let listEl = document.createElement("dl");
// listEl.innerHTML += `<dt>type</dt><dd>${nodeDatum['type']}</dd>`; // listEl.innerHTML += `<dt>type</dt><dd>${nodeDatum['type']}</dd>`;
let skipNodeAttributes = [
'id','x','y','index','type','vy','vx','fx','fy','leftX','rightX'
];
if(titleAttr !== 'contentUrl') {
skipNodeAttributes[skipNodeAttributes.length] = titleAttr;
}
for (let attr in nodeDatum) { for (let attr in nodeDatum) {
if([ if(skipNodeAttributes.indexOf(attr) != -1) {
'id','x','y','index','type','vy','vx','fx','fy','leftX','rightX', titleAttr
].indexOf(attr) != -1) {
continue; continue;
} }
@ -417,6 +506,7 @@ var setDetails = function(nodeDatum, nodeIdx) {
if(attr == 'url') { if(attr == 'url') {
listEl.innerHTML += `<dt class='dt-${attr}'>${attr}</dt><dd class='dd-${attr}'><a href='${nodeAttr[i]}'>${nodeAttr[i]}</a></dd>`; listEl.innerHTML += `<dt class='dt-${attr}'>${attr}</dt><dd class='dd-${attr}'><a href='${nodeAttr[i]}'>${nodeAttr[i]}</a></dd>`;
} else if(attr == 'contentUrl') { } else if(attr == 'contentUrl') {
console.log('test', attr);
listEl.innerHTML += `<dt class='dt-${attr}'>${attr}</dt><dd class='dd-${attr}'><a href='${nodeAttr[i]}'>${nodeAttr[i]}</a></dd>`; listEl.innerHTML += `<dt class='dt-${attr}'>${attr}</dt><dd class='dd-${attr}'><a href='${nodeAttr[i]}'>${nodeAttr[i]}</a></dd>`;
listEl.innerHTML += `<dd class='dd-contentobject'><object data='${nodeAttr[i]}'></object></dd>`; listEl.innerHTML += `<dd class='dd-contentobject'><object data='${nodeAttr[i]}'></object></dd>`;
} else { } else {
@ -545,7 +635,7 @@ var selectNode = function(idx){
let posTrg = currentCrumbs.indexOf(d.target['id']); let posTrg = currentCrumbs.indexOf(d.target['id']);
if(posSrc > -1 && posTrg > -1 && Math.abs(posSrc - posTrg) == 1) { if(posSrc > -1 && posTrg > -1 && Math.abs(posSrc - posTrg) == 1) {
linkEls[idx].classList.add('breadcrumbLink'); linkEls[idx].classList.add('breadcrumbLink');
// linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHeadSelected)"); linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHeadCrumbTrail)");
} else { } else {
linkEls[idx].classList.remove('breadcrumbLink'); linkEls[idx].classList.remove('breadcrumbLink');
} }
@ -570,9 +660,18 @@ var deselectNode = function() {
closeDetails(); closeDetails();
} }
var forceCx, forceCy;
var setViewboxForceCenter = function() {
let viewBox = getViewbox();
forceCx = viewBox[0] + viewBox[2]/2;
forceCy = viewBox[1] + viewBox[3]/2;
}
setViewboxForceCenter(); // sets forceCx & forceCy
simulation.force('centerActive', function force(alpha) { simulation.force('centerActive', function force(alpha) {
// let currentNode = node.selectAll('.detail'); // let currentNode = node.selectAll('.detail');
// console.log(currentNode); // console.log(currentNode);
// console.log(forceCx, forceCy);
node.each(function(d, idx, nodes){ node.each(function(d, idx, nodes){
let n = d; let n = d;
let k = alpha * 0.1; let k = alpha * 0.1;
@ -585,12 +684,10 @@ simulation.force('centerActive', function force(alpha) {
return; return;
} }
let viewBox = getViewbox();
let cx = viewBox[0] + viewBox[2]/2;
let cy = viewBox[1] + viewBox[3]/2;
let dx = n.x - cx;
let dy = n.y - cy; let dx = n.x - forceCx;
let dy = n.y - forceCy;
if(!inCircle(dx, dy, currentNodePositionRadius)) { if(!inCircle(dx, dy, currentNodePositionRadius)) {
return; return;
} }
@ -656,9 +753,33 @@ node.append('circle')
; ;
node.append('text') node.append('text')
.append("textPath") .attr("class", "nodeType")
.attr( "xlink:href",function(d, idx){return '#nodePath'+idx;}) .text(function(n){
.text(getNodeTitle); return n['type'];
})
node.append('text')
.attr("class", "nodeYear")
.attr("y", "22")
.text(function(n){
return getNodeYear(n);
})
;
node.append('text')
.attr("class", "nodeTitle")
.attr("y", "5")
// .append("textPath")
// .attr( "xlink:href",function(d, idx){return '#nodePath'+idx;})
.text(getNodeTitle)
.each(function(){
let self = d3.select(this),
textLength = self.node().getComputedTextLength()
;
if(textLength > nodeSize * 2) {
self.attr('transform', `scale(${(nodeSize * 2) / textLength})`);
}
})
;
node.each(function(d) { node.each(function(d) {
if(!d.contentUrl) { if(!d.contentUrl) {
@ -685,12 +806,12 @@ simulation.force("link")
.links(graph.links) .links(graph.links)
.distance(function(l){ .distance(function(l){
switch (l.name) { switch (l.name) {
case 'publishedAt': // case 'publishedAt':
return 400; // return 200;
case 'image': // case 'image':
return 100; // return 200;
default: default:
return 200; return 300;
} }
}) // distance between the nodes / link length }) // distance between the nodes / link length
// .charge(-100) // .charge(-100)

View file

@ -1,6 +1,6 @@
$detailsPadding: 20px; $detailsPadding: 20px;
$detailsWidth: 700px; $detailsWidth: 740px;
$detailSlide: -1 * ($detailsWidth + 2 * $detailsPadding); $detailSlide: -1 * ($detailsWidth);
$detailSlideMobile: -30vh; $detailSlideMobile: -30vh;
@ -64,17 +64,41 @@ g.node{
stroke-dasharray: 3 2; stroke-dasharray: 3 2;
} }
&:hover .highlightCircle{ &.typeHighlight {
stroke-width: 1px; .highlightCircle{
stroke: yellow; stroke-width: 1px;
stroke: yellow;
}
}
&:hover{
.highlightCircle{
stroke-width: 1px;
stroke: yellow;
}
.nodeBg{
fill: yellow;
}
} }
&.drag{ &.drag{
cursor: grabbing; cursor: grabbing;
} }
text{ text.nodeType{
text-anchor: start; text-anchor: middle;
font-size: 10pt;
display:none;
}
text.nodeYear{
transition: transform .5s;
text-anchor: middle;
font-size: 8pt;
}
text.nodeTitle{
text-anchor: middle;
// text-anchor: start;
font-family: "CMU Bright", sans-serif; font-family: "CMU Bright", sans-serif;
font-size: 10pt; font-size: 10pt;
} }
@ -183,6 +207,24 @@ text{
transition: opacity 1s, right 1s; transition: opacity 1s, right 1s;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
box-sizing: border-box; // Because of the scaler: we can just set width now
#nodeDetailsScaler{
position:absolute;
top:0;
left:0;
bottom:0;
width: 20px;
cursor: col-resize;
// background: #ccc;
padding: 5px;
#scalarbar{
height:100%;
border-right:solid 1px black;
border-left:solid 1px #333;
width:0;
}
}
.nodeTitle{ .nodeTitle{
@ -199,10 +241,55 @@ text{
color:blue; color:blue;
} }
} }
ul.breadcrumbs{
list-style: none;
margin:0;
padding: 0;
li{
display:inline-block;
&:not(:first-child)::before{
content: "::";
color: black;
text-decoration: none;
margin: 0 10px;
}
}
.crumb{
cursor: pointer;
color: blue;
&:hover{
text-decoration: underline;
}
}
}
span.nodeYear{
margin-left:15px;
&::before{
content:'(';
}
&::after{
content:')';
}
}
h4{
border-top: solid 1px black;
padding-top: 40px;
font-size: 120%;
}
dt{ dt{
float:left; float:left;
width: 120px; width: 120px;
font-weight:bold; font-weight:bold;
min-height:25px;
}
dd{
min-height:25px;
} }
dd:not(.nodeTitleNr1) { dd:not(.nodeTitleNr1) {
margin-left: 130px; margin-left: 130px;
@ -244,21 +331,67 @@ svg#portfolioGraph {
height: auto; height: auto;
background: white; background: white;
padding: 10px; padding: 10px;
.typeCount::before{
content: "(";
padding-left: 5px;
}
.typeCount::after{
content: ")";
}
ul#typeLinks{
margin:0;
padding: 0;
display:inline-block;
li{
list-style:none;
display: inline-block;
margin: 10px 10px;
cursor: pointer;
}
}
#showMoreTypeLinks{
display:inline-block;
width:20px;
text-align: right;
cursor: pointer;
&::before{
content:"...";
}
&:hover{
text-decoration:unline;
}
.showMoreLinks & {
pointer-events:none;
&::before{
content:"x";
}
}
}
#moreTypeLinks {
position: absolute;
right: 0;
background: white;
list-style: none;
padding: 20px 30px;
text-align: left;
margin: 0;
display:none;
.showMoreLinks & {
display: block;
}
}
.typeJump{
font-weight:bold;
}
} }
#graphControls .typeJump{
font-weight:bold;
}
#graphControls ul{
margin:0;
padding: 0;
display:inline-block;
}
#graphControls li{
list-style:none;
display: inline-block;
margin: 10px 10px;
cursor: pointer;
}
@media (max-width: 1000px) { @media (max-width: 1000px) {
body{ body{