diff --git a/portfolio.css b/portfolio.css new file mode 100644 index 0000000..b3e04e0 --- /dev/null +++ b/portfolio.css @@ -0,0 +1,221 @@ +body{ + margin:0;overflow: hidden; + font-family: sans-serif; +/* background: #fceabb; +background: -moz-linear-gradient(-45deg, #fceabb 0%, #fccd4d 50%, #f8b500 51%, #fbdf93 100%); +background: -webkit-linear-gradient(-45deg, #fceabb 0%,#fccd4d 50%,#f8b500 51%,#fbdf93 100%); +background: linear-gradient(135deg, #fceabb 0%,#fccd4d 50%,#f8b500 51%,#fbdf93 100%); +filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fceabb', endColorstr='#fbdf93',GradientType=1 ); */ +/*background: #f7fbfc; +background: -moz-linear-gradient(45deg, #f7fbfc 0%, #d9edf2 40%, #add9e4 100%); +background: -webkit-linear-gradient(45deg, #f7fbfc 0%,#d9edf2 40%,#add9e4 100%); +background: linear-gradient(45deg, #f7fbfc 0%,#d9edf2 40%,#add9e4 100%); +filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f7fbfc', endColorstr='#add9e4',GradientType=1 );*/ +/*background: #e2e2e2; +background: -moz-linear-gradient(45deg, #e2e2e2 0%, #dbdbdb 50%, #d1d1d1 51%, #fefefe 100%); +background: -webkit-linear-gradient(45deg, #e2e2e2 0%,#dbdbdb 50%,#d1d1d1 51%,#fefefe 100%); +background: linear-gradient(45deg, #e2e2e2 0%,#dbdbdb 50%,#d1d1d1 51%,#fefefe 100%); +filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e2e2e2', endColorstr='#fefefe',GradientType=1 );*/ +/*background: #d2dfed; +background: -moz-linear-gradient(45deg, #d2dfed 0%, #c8d7eb 26%, #bed0ea 51%, #a6c0e3 51%, #afc7e8 62%, #bad0ef 75%, #99b5db 88%, #799bc8 100%); +background: -webkit-linear-gradient(45deg, #d2dfed 0%,#c8d7eb 26%,#bed0ea 51%,#a6c0e3 51%,#afc7e8 62%,#bad0ef 75%,#99b5db 88%,#799bc8 100%); +background: linear-gradient(45deg, #d2dfed 0%,#c8d7eb 26%,#bed0ea 51%,#a6c0e3 51%,#afc7e8 62%,#bad0ef 75%,#99b5db 88%,#799bc8 100%); +filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#d2dfed', endColorstr='#799bc8',GradientType=1 );*/ +background: #d2dfed; +background: -moz-linear-gradient(45deg, #d2dfed 0%, #afc1d8 13%, #d5e0ef 28%, #bed0ea 51%, #a8c0dd 51%, #c0d0e5 63%, #bad0ef 75%, #a2bad8 88%, #799bc8 100%); +background: -webkit-linear-gradient(45deg, #d2dfed 0%,#afc1d8 13%,#d5e0ef 28%,#bed0ea 51%,#a8c0dd 51%,#c0d0e5 63%,#bad0ef 75%,#a2bad8 88%,#799bc8 100%); +background: linear-gradient(45deg, #d2dfed 0%,#afc1d8 13%,#d5e0ef 28%,#bed0ea 51%,#a8c0dd 51%,#c0d0e5 63%,#bad0ef 75%,#a2bad8 88%,#799bc8 100%); +filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#d2dfed', endColorstr='#799bc8',GradientType=1 ); + +/*-moz-animation: bgShift 5s infinite; +background-size: 400% 400%;*/ +} + +/*@-moz-keyframes bgShift{ + 0%{background-position:0% 50%} + 50%{background-position:100% 50%} + 100%{background-position:0% 50%} +}*/ +svg{ + width:100vw; + height: calc(100vh - 40px); + cursor: dragging; +} +svg.dragging{ + cursor: grabbing; +} +g.node{ + cursor: pointer; + stroke: blue; + stroke-width: 0; + transition: stroke-width .5s; + opacity: 0; + pointer-events: none; + transition: opacity 1s; +} +g.node.visibleNode/* , g.node.ImageObject */ +{ + opacity: 1; + pointer-events: auto; +} +g.node circle.highlightCircle{ + fill: none; +} +g.node:hover circle.highlightCircle{ + stroke-width: 2px; +} +g.node.centeredNode circle.highlightCircle{ + stroke-width:1px; + stroke: green; + stroke-dasharray: 3 2; +} +g.node.selectedNode circle.highlightCircle{ + color: red; +} +g.node.drag{ + cursor:grabbing; +} +.node text{ + text-anchor: start; +} +.relationship{ + display:none; +} +.relationship.visibleLink{ + display:block; +} +.relationship line{ + fill:none; + stroke: #999; + stroke-width: 2px;\; +} +.relationship text{ + fill:black; + /* text-transform: lowercase; */ + font-size: 75%; + display:none; +} +.relationship.activeLink text{ + fill:white; + display:block; +} +.relationship.activeLink line{ + stroke: white; +} +circle.nodeBg{ + fill: white; + /*stroke-width:.2em;*/ + fill:url(#blueGrad); +} +text{ + text-anchor: middle; +} + +.drag{ + fill:#00f; +} + +.MediaObject circle.nodeBg{ + fill:url(#orangeGrad); +} +/* .ImageObject circle.nodeBg{ + stroke:lightgreen; +}*/ +.Person circle.nodeBg{ + fill:url(#redGrad); +} +.PublicationEvent circle.nodeBg{ + fill:url(#limeGrad); +} +/*.Place circle{ + stroke:darkgreen; +} +.Country circle{ + stroke:yellow; +}*/ +.relationship.address line{ + /* stroke:#90F7FE; */ +} +.relationship.location line{ + /* stroke:darkgreen; */ +} +.relationship.contributor line{ + /* stroke:orange; */ + /* stroke-width:.4em; */ +} +#nodeDetails{ + position: absolute; + top: 0; + right: -720px; + bottom: 10px; + width: 700px; + background: white; + padding: 20px; + /* opacity: 0; */ + transition: opacity 1s, right 1s; + max-height: 100%; + overflow-y: auto; +} +body.detailsOpen #nodeDetails{ + /* opacity:1; */ + right: 0px; +} +svg#portfolioGraph { + position: relative; + right: 0; + transition: right 1s; +} +body.detailsOpen svg#portfolioGraph{ + right: calc(720px / 2); +} +#nodeDetails .nodeTitle{ + +} +#nodeDetails .nodeType{ + font-size:80%; + text-transform: uppercase; + color: #999; + margin-left:10px; +} +#nodeDetails .nodeType:hover{ + cursor:pointer; + color:blue; +} +/* #nodeDetails .nodeType::before{ + content:"("; +} +#nodeDetails .nodeType::after{ + content:")"; +} */ +#nodeDetails dt{ + float:left; + width: 120px; + font-weight:bold; +} +#nodeDetails dd:not(.nodeTitleNr1) { + margin-left: 120px; +} +#nodeDetails dt.dt-description{ + float:none; +} +#nodeDetails dd.dd-description{ + margin-left:0; +} +#graphControls{ + position:fixed; + left:0; + bottom:0; + right:0; + height:40px; + background:white; +} +#graphControls ul{ + margin:0; + padding: 0; +} +#graphControls li{ + list-style:none; + display: inline-block; + margin: 9px 10px 0; + cursor: pointer; +} diff --git a/portfolio.js b/portfolio.js new file mode 100644 index 0000000..7a98372 --- /dev/null +++ b/portfolio.js @@ -0,0 +1,786 @@ +var data; + +function getTitleAttribute(node) { + if(typeof node['name'] !== "undefined"){ + return 'name'; + } + switch (node['type']) { + case "WebSite": + if(typeof node['url'] !== "undefined") {return 'url';} + break; + case "ImageObject": + if(typeof node['caption'] !== "undefined") {return 'caption';} + break; + break; + case "PostalAddress": + if(typeof node['addressLocality'] !== "undefined") {return 'addressLocality';} + break; + } + return 'id'; +} +function getNodeTitle(node){ + return node[getTitleAttribute(node)]; +} +/** +Transform a flattened jsonld into a d3 compatible graph +@param Object data flattened jsonld data +@return Object graph has keys "nodes" and "links" +*/ +function jsonLdToGraph(data){ + let nodes = {}; + let links = []; + // collect all nodes + for(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(nodeId in data) { + let node = data[nodeId]; + currentId = node["id"]; + for(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(i in nodeAttr) { + if(key !== "id" && typeof nodeAttr[i] === "string" && nodes[nodeAttr[i]]) { + links[links.length] = { + "source": currentId, + "target": nodeAttr[i], + "name": key + }; + } + else if(typeof nodeAttr[i]["id"] !== "undefined") { + // if there is just one item, flatten/expand has turned urls in objects with just an id + // reverse this, as we don't want these separate for this project + if (Object.keys(nodeAttr[i]).length == 1 && typeof nodes[nodeAttr[i]["id"]] === "undefined") { + // skip + // nodeAttr = nodeAttr[i]["id"]; + } else { + links[links.length] = { + "source": currentId, + "target": nodeAttr[i]["id"], + "name": key + }; + } + } + } + } + } + return { + "nodes": Object.values(nodes), + "links": links + }; +} +var graph; +var nodeMap = {}; +jsonld.flatten(window.location.protocol + "//" + window.location.host + "/rubenvandeven.jsonld", {"@context": "https://schema.org/"},(err, flattened)=> { + console.log(err); + data = flattened; + graph = jsonLdToGraph(flattened['@graph']); + // create a map of nodes by id. + for(let i in graph.nodes) { + nodeMap[graph.nodes[i]['id']] = graph.nodes[i]; + } + // console.log(graph); + startGraph(graph); +}); + + +function inCircle(dx, dy, r) { + // fastest check if in circle: https://stackoverflow.com/a/7227057 + let dxAbs = Math.abs(dx); + let dyAbs = Math.abs(dy); + + if(dxAbs > r || dyAbs > r) { + return false; + } else if(dxAbs + dyAbs <= r){ + return true; + } else if( Math.pow(dx,2) + Math.pow(dy, 2) <= Math.pow(r,2)){ + return true; + } else { + return false; + } +} + +function startGraph(graph){ + + + // config +var nodeSize = 40; +var selectedNodeSize = 140; + +// set some vars +var currentNodeIdx = 0; +var currentNodePositionRadius = 0; +var nodePositions = {}; +var types = {}; +for (nodeIdx in graph['nodes']) { + let type = graph['nodes'][nodeIdx]["type"]; + if(typeof types[type] == 'undefined') { + types[type] = []; + } + types[type][types[type].length] = nodeIdx; +} +var graphControlsEl = document.getElementById('graphControls'); +var typeLinksEl = document.getElementById('typeLinks'); +var relLinksEl = document.getElementById('relLinks'); + +// make controls +for (let typeName in types) { + let typeLinkEl = document.createElement("li"); + typeLinkEl.innerHTML = typeName; + typeLinkEl.addEventListener('click', function(){ + centerByType(typeName); + // positionNodesInCenter(types[typeName]); + }); + typeLinksEl.appendChild(typeLinkEl); +} + + +// make svg +var svg = d3.select("svg"), + width = +svg.attr("width"), + height = +svg.attr("height"); + +var simulation = d3.forceSimulation() + .force("link", d3.forceLink().id(function(d) { return d["id"]; }).strength(.005)) + .force("charge", d3.forceManyBody()) // doesn't seem necessary? + .force("collision", d3.forceCollide(nodeSize * 1.1)) // avoid overlapping nodes + // .force("center", d3.forceCenter(width / 2, height / 2)) // position around center + + // .force("x", d3.forceX()) + // .force("y", d3.forceY()) + // .force("y", d3.forceY()) + ; + + +var link = svg.append("g") + .attr("class", "links") + .selectAll(".relationship") + .data(graph['links']) + .enter().append("g") + .attr("class", function(d){return "relationship "+d.name;}) + ; +linkLine = link + // .append("line"); + .append("line").attr("marker-end", "url(#arrowHead)") + ; +linkText = link + .append("text") + .text(function(d){ + return d.name; + // snake_case: return d.name.replace(/(?:^|\.?)([A-Z])/g, function (x,y){return "_" + y.toLowerCase()}).replace(/^_/, ""); + }) + + ; + + var node = svg.append("g") + .attr("class", "nodes") + .selectAll(".node") + .data(graph.nodes) + .enter().append("g") + .attr("class", function(d) { return 'node ' + d['type']; }) + ; +var getViewbox = function() { + return svg.attr("viewBox").split(" ").map(parseFloat); +} +var positionNodesInCenter = function(idxs) { + let viewBox = getViewbox(); + let cx = viewBox[0] + viewBox[2]/2; + let cy = viewBox[1] + viewBox[3]/2; + + if(typeof idxs == "object" && idxs !== null && idxs.length == 1) { + idxs = idxs[0]; + } + + nodePositions = {}; // reset + if(idxs === null) { + return; + } + else if(typeof idxs == "object") { + // array or object -> each + // calculate grid: + // let itemsX = 4; + // let itemsY = Math.ceil(idxs.length/itemsX); + // console.log(itemsX,itemsY); + // let rowDiffX = viewBox[3] * (1/(itemsX+1)); + // let rowDiffY = viewBox[2] * (1/(itemsY+1)); + // console.log(rowDiffX, rowDiffY); + // for (var i = 0; i < idxs.length; i++) { + // nodePositions[idxs[i]] = [ + // cx - itemsX/2*rowDiffX + rowDiffX * ((i % itemsX)), + // cy - itemsY/2*rowDiffY + rowDiffY * (Math.floor(i / itemsX)) + // ]; + // } + positionNodesInCircle(idxs); + console.log(nodePositions); + } + else{ + nodePositions[idxs] = [ + cx, + cy + ]; + } + + node.each(function(d,nIdx,nodeEls){ + if(typeof nodePositions[nIdx] != 'undefined') { + nodeEls[nIdx].classList.add('centeredNode'); + nodeEls[nIdx].classList.add('visibleNode'); + } else { + nodeEls[nIdx].classList.remove('centeredNode'); + } + }); + + // restart animation (they call that 'alpha' in d3 force) + simulation.alpha(1); +} +var positionNodesInCircle = function(idxs, r) { + let viewBox = getViewbox(); + if(typeof r == 'undefined') { + if(idxs.length == 1) { + r = viewBox[2] / 6; + } else { + r = viewBox[2] / (4 + Math.max(0, 2.5 - idxs.length)); + } + } + currentNodePositionRadius = r; + let cx = viewBox[0] + viewBox[2]/2; + let cy = viewBox[1] + viewBox[3]/2; + + stepSize = 2*Math.PI / idxs.length; + for (var i = 0; i < idxs.length; i++) { + nodePositions[idxs[i]] = [ + cx + Math.sin(stepSize * i) * r, + cy + Math.cos(stepSize * i) * r + ]; + } + + // restart animation (they call that 'alpha' in d3 force) + simulation.alpha(1); +} +var centerByType = function(types) { + if(!Array.isArray(types)) { + types = [types]; + } + let idxs = []; + for(let idx in graph.nodes) { + if(types.indexOf(graph.nodes[idx]['type']) > -1) { + idxs[idxs.length] = idx; + } + } + deselectNode(); + positionNodesInCenter(idxs.length ? idxs : null); +} + +d3Selection=""; +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('span'); + titleEl.innerHTML = getNodeTitle(relNode); + titleEl.classList.add('nodeTitle'); + titleEl.classList.add('nodeTitleNr'+i); + titleEl.addEventListener('click',function(e){ + let idx = graph.nodes.indexOf(relNode); + selectNode(idx); + }); + let typeEl = document.createElement('span'); + typeEl.classList.add('nodeType') + typeEl.innerHTML = relNode['type'] + typeEl.addEventListener('click',function(e){ + centerByType(relNode['type']); + }); + el.appendChild(titleEl); + el.appendChild(typeEl); + // el.innerHTML = `${getNodeTitle(relNode)} (${relNode['type']})`; + return el; +} + +var setDetails = function(nodeDatum, nodeIdx) { + document.body.classList.add("detailsOpen"); + while (nodeDetailEl.hasChildNodes()) { + nodeDetailEl.removeChild(nodeDetailEl.lastChild); + } + + + let relUp = []; + let relDown = []; + let titleAttr = getTitleAttribute(nodeDatum); + let titleEl = document.createElement('h2'); + titleEl.innerHTML = getNodeTitle(nodeDatum); + + let typeEl = document.createElement('span'); + typeEl.classList.add('nodeType') + typeEl.innerHTML = nodeDatum['type'] + typeEl.addEventListener('click',function(e){ + centerByType(nodeDatum['type']); + }); + titleEl.appendChild(typeEl); + nodeDetailEl.appendChild(titleEl); + + let listEl = document.createElement("dl"); + // listEl.innerHTML += `