Sfoglia il codice sorgente

Create project idea

Arkadiusz Ryś 2 anni fa
parent
commit
81dbae0f97

File diff suppressed because it is too large
+ 36 - 0
.editorconfig


+ 0 - 0
.gitignore


+ 10 - 2
README.md

@@ -1,2 +1,10 @@
-Graph View
-##########
+# Graph View
+
+# Install
+
+Open `index.html` in your favorite browser.
+
+If you need the styling you can generate it using sass.
+```shell
+sass sass/graph.scss js/graph.js 
+```

+ 0 - 0
css/graph.css


+ 9 - 0
img/favicon.svg

@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+  <style>
+    path { fill: #111; }
+    @media (prefers-color-scheme: dark) {
+      path { fill: #fff; }
+    }
+  </style>
+  <path fill-rule="evenodd" d="M0 0h16v16H0z"/>
+</svg>

+ 45 - 0
index.html

@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <meta name="keywords" content="">
+  <meta name="description" content="">
+  <meta name="author" content="">
+
+  <meta property="og:title" content=""/>
+  <meta property="og:type" content="website"/>
+  <meta property="og:url" content=""/>
+  <meta property="og:image" content=""/>
+
+  <!--  <style>css/inline.css</style>-->
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css"
+        integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls" crossorigin="anonymous">
+  <link href="https://unpkg.com/@triply/yasgui@4/build/yasgui.min.css" rel="stylesheet" type="text/css"/>
+  <link rel="stylesheet" href="css/graph.css">
+  <title></title>
+  <link rel="icon" href="img/favicon.svg">
+  <!--  <link rel="prerender" href="">-->
+</head>
+
+<body>
+<header>
+
+</header>
+
+<main>
+  <div id="yasgui"></div>
+  <div id="figure1"></div>
+  <div id="figure2"></div>
+  <div id="figure3"></div>
+  <div id="figure4"></div>
+</main>
+
+<footer>
+</footer>
+<script defer src="js/mousetrap.1.6.5.min.js"></script>
+<script src="https://unpkg.com/@triply/yasgui@4/build/yasgui.min.js"></script>
+<script src="js/graph.js" type="module" defer></script>
+</body>
+</html>

+ 158 - 0
js/d3graph.js

@@ -0,0 +1,158 @@
+import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
+
+function getOrCreateNode(id, name, nodes, dict) {
+    var node = dict[id];
+    if (!node) {
+        node = {}
+        node.id = id;
+        node.name = name;
+        nodes.push(node);
+        dict[id] = node;
+    }
+    return node;
+}
+
+function toGraphData(data) {
+    var graph = {};
+    var nodes = graph.nodes = [];
+    var links = graph.links = [];
+    var idToNode = [];
+    var columns = data.head.vars;
+    data.results.bindings.forEach(item => {
+        var srcID = item[columns[0]].value;
+        var trgtID = item[columns[1]].value;
+        var src = getOrCreateNode(srcID, srcID, nodes, idToNode);
+        var trgt = getOrCreateNode(trgtID, trgtID, nodes, idToNode);
+        var link = {};
+        link.source = src.id;
+        link.target = trgt.id;
+        links.push(link);
+    });
+    return graph;
+}
+
+
+function drawGraph(data, containerID, width = 500, height = 300) {
+    var graph = toGraphData(data)
+    var container = d3.select(containerID);
+    var svg = container.append('svg').attr("width", width).attr("height", height);
+    // arrow def
+    svg.append('defs').append('marker')
+        .attr('id', 'arrowhead')
+        .attr('viewBox', '-0 -5 10 10')
+        .attr('refX', 13)
+        .attr('refY', 0)
+        .attr('orient', 'auto')
+        .attr('markerWidth', 13)
+        .attr('markerHeight', 13)
+        .attr('xoverflow', 'visible')
+        .append('svg:path')
+        .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
+        .attr('fill', '#999')
+        .style('stroke', 'none');
+
+    var nodes = graph.nodes;
+    var links = graph.links;
+    var simulation = d3.forceSimulation()
+        .force("link", d3.forceLink().id(function (d) {
+            return d.id;
+        }).distance(80))
+        .force("charge", d3.forceManyBody().strength(-100))
+        .force("center", d3.forceCenter(width / 2, height / 2));
+
+    var link = svg.selectAll(".link")
+        .data(links)
+        .enter()
+        .append("line")
+        .attr("class", "link")
+        .attr('marker-end', 'url(#arrowhead)')
+
+    var node = svg.selectAll(".node")
+        .data(nodes)
+        .enter()
+        .append("g")
+        .attr("class", "node")
+        .call(d3.drag()
+            .on("start", dragstarted)
+            .on("drag", dragged)
+            .on("end", dragended));
+
+    node.append("circle")
+        .attr("r", 5)
+        .style("fill", "red")
+
+    node.append("title")
+        .text(function (d) {
+            return d.name;
+        });
+
+    node.append("text")
+        .attr("dy", -3)
+        .text(function (d) {
+            return d.id
+        });
+
+    simulation.nodes(nodes).on("tick", ticked);
+
+    simulation.force("link").links(links);
+
+    function ticked() {
+        link
+            .attr("x1", function (d) {
+                return getX(d.source.x);
+            })
+            .attr("y1", function (d) {
+                return getY(d.source.y);
+            })
+            .attr("x2", function (d) {
+                return getX(d.target.x);
+            })
+            .attr("y2", function (d) {
+                return getY(d.target.y);
+            });
+
+        node.attr("transform",
+            function (d) {
+                var dx = getX(d.x);
+                var dy = getY(d.y);
+                return "translate(" + dx + ", " + dy + ")";
+            }
+        );
+
+        function getX(x) {
+            var dx = x;
+            if (dx < 20) dx = 20;
+            if (dx > width - 20) dx = width - 20;
+            return dx;
+        }
+
+        function getY(y) {
+            var dy = y;
+            if (dy < 20) dy = 20;
+            if (dy > height - 20) dy = height - 20;
+            return dy;
+        }
+
+    }
+
+
+    function dragstarted(event) {
+        if (!event.active) simulation.alphaTarget(0.3).restart()
+        event.subject.fx = event.subject.x;
+        event.subject.fy = event.subject.y;
+    }
+
+    function dragged(event) {
+        event.subject.fx = event.x;
+        event.subject.fy = event.y;
+    }
+
+    function dragended(event) {
+        if (!event.active) simulation.alphaTarget(0);
+        event.subject.fx = null;
+        event.subject.fy = null;
+    }
+}
+
+export { drawGraph, toGraphData };
+export default drawGraph;

+ 42 - 0
js/d3table.js

@@ -0,0 +1,42 @@
+import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
+
+
+function tabulate(data, containerID) {
+	var container = d3.select(containerID);
+	var table = container.append('table');
+	table.attr("id","table");
+	var thead = table.append('thead');
+	var	tbody = table.append('tbody');
+	var columns = data.head.vars;
+
+	// append the header row
+	thead.append('tr')
+	  .selectAll('th')
+	  .data(columns).enter()
+	  .append('th')
+	  .text(function (column) {
+		return column.replace(/\b\w/g, (match) => match.toUpperCase()).replace(/([A-Z])/g, " $1");
+	});
+
+	// create a row for each object in the data
+	var rows = tbody.selectAll('tr')
+	  .data(data.results.bindings).enter()
+	  .append('tr');
+
+	// create a cell in each row for each column
+	var cells = rows.selectAll('td')
+	  .data(function (row) {
+	    return columns.map(function (column) {
+	      return {column: column, value: row[column] ? row[column].value : ""};
+	    });
+	  })
+	  .enter()
+	  .append('td')
+	  .text(function (d) { return d.value; });
+
+  return table;
+}
+
+
+export { tabulate };
+export default tabulate;

+ 205 - 0
js/d3tree.js

@@ -0,0 +1,205 @@
+import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
+
+
+function drawTree(data, containerID, root_name, unique=true, _width=960, _height=500, depthMultiplier=180) {
+
+	function getOrCreateNode(id, name, dict){
+		var node = dict[id];
+		if (!node){
+			node = {}
+			node.id  = id;
+			node.name = name;
+			dict[id] = node;
+		}
+		return node;
+	}
+
+	function addNode(parentid, id, name, dict){
+		var parent = dict[parentid];
+		var node = dict[id];
+		if (!node){
+			node = getOrCreateNode(id,name,dict);
+			node.parent = parent.id;
+			if (!parent.children){
+				parent.children = [];
+			}
+			parent.children.push(node);
+		}
+		return node;
+	}
+
+	function transform(data){
+		var idToNode = [];
+		var root = getOrCreateNode("-1",root_name,idToNode);
+		root.parent = "null";
+		var columns = data.head.vars;
+		data.results.bindings.forEach(item => {
+			var parent_id = "-1";
+			for (let i = 0; i < columns.length/2; i++) {
+				if (item[columns[2*i]]) {
+					var childId = (!unique ? parent_id+"." : "") +item[columns[2*i]].value;
+					addNode(parent_id, childId, item[columns[2*i+1]].value, idToNode);
+					parent_id = childId;
+				}
+			}
+		});
+		return root;
+	}
+
+	function getVal(d){
+		return d.value + (d.children ? d.data.value : 0);
+	}
+
+
+	var treeData = transform(data);
+
+	// ************** Generate the tree diagram	 *****************
+	var margin = {top: 20, right: 120, bottom: 20, left: 120},
+		width = _width - margin.right - margin.left,
+		height = _height - margin.top - margin.bottom;
+
+	var i = 0,
+		root;
+
+
+	var root = d3.hierarchy(treeData)
+			.sum(d => d.value)
+			.sort((a, b) => b.value - a.value)
+
+	var tree = d3.tree();
+	tree.size([height, width]);
+	var diagonal = d3.linkHorizontal().x(d => d.y).y(d => d.x)
+
+	var svg = d3.select(containerID).append("svg")
+		.attr("width", width + margin.right + margin.left)
+		.attr("height", height + margin.top + margin.bottom)
+	  .append("g")
+		.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+
+  	const gLink = svg.append("g")
+      .attr("fill", "none")
+      .attr("stroke", "#555")
+      .attr("stroke-opacity", 0.4)
+      .attr("stroke-width", 1.5);
+
+	root.x0 = height / 2;
+	root.y0 = 0;
+
+	function update(source) {
+	    const duration = d3.event && d3.event.altKey ? 2500 : 250;
+	    const nodes = root.descendants().reverse();
+	    const links = root.links();
+
+	    // Compute the new tree layout.
+	    tree(root);
+
+	    let left = root;
+	    let right = root;
+	    root.eachBefore(node => {
+	      if (node.x < left.x) left = node;
+	      if (node.x > right.x) right = node;
+	    });
+
+		// Normalize for fixed-depth.
+	    nodes.forEach(function(d) { d.y = d.depth * depthMultiplier; });
+
+
+	    const height = right.x - left.x + margin.top + margin.bottom;
+
+	    const transition = svg.transition()
+	        .duration(duration);
+
+	 	// Update the nodes…
+	  	var node = svg.selectAll("g.node")
+		  .data(nodes, function(d) { return d.id || (d.id = ++i); });
+
+
+	    // Enter any new nodes at the parent's previous position.
+	    const nodeEnter = node.enter().append("g")
+	        .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
+		  	.attr("class", "node")
+	        .on("click", click);
+
+	    nodeEnter.append("circle")
+ 		  .attr("r", 1e-6)
+		  .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
+
+
+	    nodeEnter.append("text")
+	        .attr("dy", "0.35em")
+	        .attr("x", function(d) { return d.children || d._children ? -13 : 13; })
+	        .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
+	        .text(function(d){
+				return d.data.name;
+			});
+
+	    // Transition nodes to their new position.
+	    const nodeUpdate = node.merge(nodeEnter).transition(transition)
+		  	.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
+
+		nodeUpdate.select("circle")
+		  .attr("r", 10)
+		  .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
+
+		nodeUpdate.select("text")
+		  .style("fill-opacity", 1);
+
+		// Transition exiting nodes to the parent's new position.
+	    const nodeExit = node.exit().transition(transition)
+		  .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
+		  .remove();
+
+		nodeExit.select("circle")
+		  .attr("r", 1e-6);
+
+	  	nodeExit.select("text")
+		  .style("fill-opacity", 1e-6);
+
+	    // Update the links…
+	    const link = gLink.selectAll("path")
+	      .data(links, d => d.target.id);
+
+	    // Enter any new links at the parent's previous position.
+	    const linkEnter = link.enter().append("path")
+			.attr("class", "link")
+	        .attr("d", d => {
+	          const o = {x: source.x0, y: source.y0};
+	          return diagonal({source: o, target: o});
+	        });
+
+	    // Transition links to their new position.
+	    link.merge(linkEnter).transition(transition)
+	        .attr("d", diagonal);
+
+	    // Transition exiting nodes to the parent's new position.
+	    link.exit().transition(transition).remove()
+	        .attr("d", d => {
+	          const o = {x: source.x, y: source.y};
+	          return diagonal({source: o, target: o});
+	        });
+
+	    // Stash the old positions for transition.
+	    root.eachBefore(d => {
+	      d.x0 = d.x;
+	      d.y0 = d.y;
+	    });
+	 }
+
+  	update(root);
+
+	// Toggle children on click.
+	function click(event, d) {
+	  if (d.children) {
+		d._children = d.children;
+		d.children = null;
+	  } else {
+		d.children = d._children;
+		d._children = null;
+	  }
+	  update(d);
+	}
+}
+
+export { drawTree };
+export default drawTree;

+ 170 - 0
js/d3treemap.js

@@ -0,0 +1,170 @@
+import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
+
+
+function drawTreeMap(data, containerID, nameSeperators = [],WIDTH = 800, HEIGHT=800) {
+
+	function getOrCreateNode(id, name, dict){
+		var node = dict[id];
+		if (!node){
+			node = {}
+			node.name = name;
+			dict[id] = node;
+		}
+		return node;
+	}
+
+	function addNode(parentid, id, name, dict){
+		var parent = dict[parentid];
+		var node = dict[id];
+		if (!node){
+			node = getOrCreateNode(id,name,dict);
+			if (!parent.children){
+				parent.children = [];
+			}
+			parent.children.push(node);
+		}
+		return node;
+	}
+
+	function getName(name, nameSeperators){
+		var newName = name;
+		for (const nameSeperator of nameSeperators){
+			var index = newName.lastIndexOf(nameSeperator);
+			if (index!=-1){
+				return name.substring(index+1);
+			}
+		}
+		return newName;
+	}
+
+	function transform(data,nameSeperators){
+		var idToNode = [];
+		var root = getOrCreateNode("-1","root",idToNode);
+		var columns = data.head.vars;
+		data.results.bindings.forEach(item => {
+			var containerID = item[columns[4]] ? item[columns[4]].value : "-1";
+			var containerName = getName(item[columns[3]] ? item[columns[3]].value : "root",nameSeperators) ;
+			var childID = item[columns[1]].value;
+			var childeName = getName(item[columns[0]] ? item[columns[0]].value : "",nameSeperators);
+			var mass = item[columns[2]]?item[columns[2]].value : "0";
+			addNode("-1",containerID,containerName,idToNode);
+			var child = addNode(containerID,childID,childeName,idToNode);
+			child.value = parseInt(mass);
+		});
+		return root;
+	}
+
+	function getVal(d){
+		return d.value + (d.children ? d.data.value : 0);
+	}
+
+	var treeData = transform(data, nameSeperators);
+
+	var margin = {top: 10, right: 10, bottom: 10, left: 10},
+  		width = WIDTH - margin.left - margin.right,
+  		height = HEIGHT - margin.top - margin.bottom;
+
+	// append the svg object to the container
+	var svg = d3.select(containerID).append("svg");
+
+	svg.style('font-family', 'sans-serif')
+	   .attr('width', width)
+       .attr('height', height)
+
+  	const g = svg.append('g').attr('class', 'treemap-container')
+
+	var root = d3.hierarchy(treeData)
+    			.sum(d => d.value)
+    			.sort((a, b) => b.value - a.value)
+
+	colorScale = d3.scaleOrdinal( d3.schemeSet2 )
+
+	d3.treemap()
+    .size([ width, height ])
+    .padding(2)
+    .paddingTop(10)
+    .round(true)(root);
+
+    // Place the labels for our Root elements (if they have children)
+   g.selectAll('text.title')
+    // The data is the first "generation" of children
+    .data( root.children.filter(function(d){return d.children!=null}) )
+    .join('text')
+      .attr('class', 'title')
+      // The rest is just placement/styling
+      .attr('x', d => d.x0)
+      .attr('y', d => d.y0)
+      .attr('dy', '0.6em')
+      .attr('dx', 3)
+      .style('font-size', 12)
+    // Remember, the data on the original node is available on node.data (d.data here)
+    .text(d => d.data.name + " (" +  getVal(d) + ")")
+
+ // Now, we place the groups for all of the leaf nodes
+  const leaf = g.selectAll('g.leaf')
+    // root.leaves() returns all of the leaf nodes
+    .data(root.leaves())
+    .join('g')
+      .attr('class', 'leaf')
+      // position each group at the top left corner of the rect
+      .attr('transform', d => `translate(${ d.x0 },${ d.y0 })`)
+      .style('font-size', 10)
+
+  // A title element tells the browser to display its text value
+  // in a popover when the cursor is held over a rect. This is a simple
+  // way to add some interactivity
+  leaf.append('title')
+    .text(d => `${ d.parent.data.name }-${ d.data.name }\n${ d.value.toLocaleString()}`)
+
+  // Now we append the rects. Nothing crazy here
+  leaf.append('rect')
+      .attr('fill', d => colorScale(d.parent.data.name))
+      .attr('opacity', 0.7)
+      // the width is the right edge position - the left edge position
+      .attr('width', d => d.x1 - d.x0)
+      // same for height, but bottom - top
+      .attr('height', d => d.y1 - d.y0)
+      // make corners rounded
+      .attr('rx', 3)
+      .attr('ry', 3)
+
+  // This next section checks the width and height of each rectangle
+  // If it's big enough, it places labels. If not, it doesn't.
+  leaf.each((d, i, arr) => {
+    // The current leaf element
+    const current = arr[i]
+
+    const left = d.x0,
+          right = d.x1,
+          // calculate its width from the data
+          width = right - left,
+          // calculate its height from the data
+          height = d.y1 - d.y0
+
+    // too small to show text
+    const tooSmall = width < 34 || height < 25
+
+    const text = d3.select( current ).append('text')
+        // If it's too small, don't show the text
+        .attr('opacity', tooSmall ? 0 : 0.9)
+      .selectAll('tspan')
+      .data(d => [ d.data.name, d.value.toLocaleString() ])
+      .join('tspan')
+        .attr('x', 3)
+        .attr('y', (d,i) => i ? '2.5em' : '1.15em')
+        .text(d => d)
+
+ 	 });
+
+	svg.call(d3.zoom()
+      .extent([[0, 0], [width, height]])
+      .scaleExtent([1, 8])
+      .on("zoom", zoomed));
+
+	function zoomed({transform}) {
+	   g.attr("transform", transform);
+	}
+}
+
+export { drawTreeMap };
+export default drawTreeMap;

+ 62 - 0
js/d3treetable.js

@@ -0,0 +1,62 @@
+import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
+
+function transform(tableData, data){
+	var transformed = {};
+	var columns = data.head.vars;
+	transformed.roots = [];
+	transformed.columns = columns;
+	var rootsMap = [];
+	tableData.forEach(item => {
+	  var parentID = item[columns[0]].value;
+	  var parentName = item[columns[1]].value;
+	  var childID = item[columns[2]].value;
+	  var childName = item[columns[3]].value;
+	  var parent = rootsMap[parentID];
+	  if (!parent){
+		parent = {}
+		parent[columns[0]] = parentID;
+		parent[columns[1]] = parentName;
+		transformed.roots.push(parent);
+		rootsMap[parentID] = parent;
+		parent.children = [];
+	  }
+	  var child = {};
+	  child[columns[2]] = childID;
+	  child[columns[3]] = childName;
+	  parent.children.push(child);
+	});
+	return transformed;
+}
+
+function treeTable(data, containerID) {
+	var treeData = transform(data.results.bindings, data);
+	var container = d3.select(containerID);
+	var mainDiv = container.append('div');
+	var columns = treeData.columns;
+
+	var sections = mainDiv
+		.selectAll('details').data(treeData.roots).enter().append('details')
+		.classed("collapsible", true)
+		.attr("open", true);
+
+	sections.append('summary').text(function (d) {
+		return d[columns[1]];
+	 });
+
+	groups = sections.selectAll('ul')
+		.data(function (row) {
+			return  [row];
+		}).enter().append('ul').classed("content",true);
+
+	content = groups.selectAll('li')
+		.data(function (row) {
+			return row.children;
+		}).enter().append('li').text(function (d) {
+			 return "        " + d[columns[3]];
+		}).classed("content",true);
+
+  	return table;
+}
+
+export { treeTable };
+export default treeTable;

+ 49 - 0
js/graph.js

@@ -0,0 +1,49 @@
+import { tabulate } from './d3table.js';
+import { drawGraph } from './d3graph.js';
+import { drawTree } from './d3tree.js';
+import { treeTable } from './d3treetable.js';
+import { data } from './results.js';
+import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
+
+const div = d3.selectAll("div");
+tabulate(data, "#figure1");
+drawGraph(data, "#figure2", 1280, 650);
+drawTree(data, "#figure3", "root", false, 1280, 650, 150);
+// treeTable(data, "#figure4");
+
+
+Yasqe.defaults.value = ""
+const queries_obj = ""
+
+const url = window.location.href.endsWith('/') ? window.location.href.slice(0, -1) : window.location.href;
+const endpointsList = [...new Set([url, ...Object.keys(queries_obj).map((label) => {
+    if (queries_obj[label]["endpoint"]) return queries_obj[label]["endpoint"]
+})])]
+const yasguiEndpoints = endpointsList.map((endpoint) => {
+    return {endpoint: endpoint}
+})
+
+const yasgui = new Yasgui(document.getElementById("yasgui"), {
+    requestConfig: {endpoint: url, copyEndpointOnNewTab: true,},
+    endpointCatalogueOptions: {
+        getData: function () {
+            return yasguiEndpoints
+        },
+        keys: [],
+    },
+});
+
+Object.keys(queries_obj).map((label) => {
+    const tabsLabel = Object.keys(yasgui._tabs).map(tab => yasgui._tabs[tab].persistentJson.name)
+    if (!tabsLabel.includes(label)) {
+        yasgui.addTab(
+            false,
+            {
+                ...Yasgui.Tab.getDefaults(),
+                name: label,
+                requestConfig: {endpoint: queries_obj[label]['endpoint']},
+                yasqe: {value: queries_obj[label]['query']}
+            }
+        );
+    }
+})

File diff suppressed because it is too large
+ 11 - 0
js/mousetrap.1.6.5.min.js


+ 69 - 0
js/results.js

@@ -0,0 +1,69 @@
+const data = { "head": {
+    "vars": [ "id1" , "full_name1" , "id2" , "full_name2" ]
+  } ,
+  "results": {
+    "bindings": [
+      {
+        "id1": { "type": "literal" , "value": "0" } ,
+        "full_name1": { "type": "literal" , "value": "Example 0" }
+      } ,
+      {
+        "id1": { "type": "literal" , "value": "2" } ,
+        "full_name1": { "type": "literal" , "value": "Example 2" } ,
+        "id2": { "type": "literal" , "value": "2A" } ,
+        "full_name2": { "type": "literal" , "value": "Example 2A" }
+      } ,
+      {
+        "id1": { "type": "literal" , "value": "2" } ,
+        "full_name1": { "type": "literal" , "value": "Example 2" } ,
+        "id2": { "type": "literal" , "value": "2B" } ,
+        "full_name2": { "type": "literal" , "value": "Example 2B" }
+      } ,
+      {
+        "id1": { "type": "literal" , "value": "2" } ,
+        "full_name1": { "type": "literal" , "value": "Example 2" } ,
+        "id2": { "type": "literal" , "value": "2C" } ,
+        "full_name2": { "type": "literal" , "value": "Example 2C" }
+      } ,
+      {
+        "id1": { "type": "literal" , "value": "2" } ,
+        "full_name1": { "type": "literal" , "value": "Example 2" } ,
+        "id2": { "type": "literal" , "value": "2D" } ,
+        "full_name2": { "type": "literal" , "value": "Example 2D" }
+      } ,
+      {
+        "id1": { "type": "literal" , "value": "2" } ,
+        "full_name1": { "type": "literal" , "value": "Example 2" } ,
+        "id2": { "type": "literal" , "value": "2E" } ,
+        "full_name2": { "type": "literal" , "value": "Example 2E" }
+      } ,
+      {
+        "id1": { "type": "literal" , "value": "2" } ,
+        "full_name1": { "type": "literal" , "value": "Example 2" } ,
+        "id2": { "type": "literal" , "value": "2F" } ,
+        "full_name2": { "type": "literal" , "value": "Example 2F" }
+      } ,
+      {
+        "id1": { "type": "literal" , "value": "2" } ,
+        "full_name1": { "type": "literal" , "value": "Example 2" } ,
+        "id2": { "type": "literal" , "value": "2G" } ,
+        "full_name2": { "type": "literal" , "value": "Example 2G" }
+      } ,
+      {
+        "id1": { "type": "literal" , "value": "2" } ,
+        "full_name1": { "type": "literal" , "value": "Example 2" } ,
+        "id2": { "type": "literal" , "value": "2H" } ,
+        "full_name2": { "type": "literal" , "value": "Example 2H" }
+      } ,
+      {
+        "id1": { "type": "literal" , "value": "2" } ,
+        "full_name1": { "type": "literal" , "value": "Example 2" } ,
+        "id2": { "type": "literal" , "value": "2I" } ,
+        "full_name2": { "type": "literal" , "value": "Example 2I" }
+      }
+    ]
+  }
+}
+
+export { data };
+export default data;

+ 45 - 0
js/rollup.js

@@ -0,0 +1,45 @@
+function rollup(data, columns) {
+    var id1 = columns[0];
+    var name1 = columns[1];
+    var id2 = columns[2];
+    var name2 = columns[3];
+    var value = columns[4];
+
+    const nodes = {};
+    data.results.bindings.forEach((item) => {
+        if (!nodes[item[id1].value]) {
+            nodes[item[id1].value] = {value: 0, parent: null, children: [] };
+        }
+        if (item[value]) {
+            nodes[item[id1].value].value = parseInt(item[value].value);
+        }
+        if (item[id2]) {
+            nodes[item[id2].value] = {value: 0, parent: null, children: [] };
+            nodes[item[id2].value].parent = nodes[item[id1].value];
+            nodes[item[id1].value].children.push(nodes[item[id2].value]);
+        }
+    });
+    Object.values(nodes).forEach((node) => {
+        if (node.parent == null) {
+            visit(node);
+          }
+    });
+    data.head.vars = data.head.vars.filter(item => item !== value);
+    data.results.bindings.forEach(item => {
+        if (item[value]) {
+            delete item[value];
+        }
+        item[name1].value += " ("+nodes[item[id1].value].value+")";
+        if (item[name2]) {
+            item[name2].value += " ("+nodes[item[id2].value].value+")";
+        }
+    });
+    return data;
+}
+
+function visit(node) {
+    if (node.children.length !== 0) {
+        node.children.forEach((child) => visit(child));
+        node.value = node.children.reduce((acc, child) => acc + child.value, 0);
+    }
+}

+ 7 - 0
sass/graph.scss

@@ -0,0 +1,7 @@
+@import "partials/base/inline/base";
+@import "partials/base/inline/footer";
+@import "partials/base/inline/header";
+@import "partials/base/inline/pure-reset";
+@import "partials/base/backgrounds";
+
+

+ 5 - 0
sass/modules/_all.scss

@@ -0,0 +1,5 @@
+@import "colors";
+@import "utility";
+@import "font";
+@import "query";
+@import "spacing";

+ 27 - 0
sass/modules/_colors.scss

@@ -0,0 +1,27 @@
+$color-primary                             : #00a8ff;
+$color-secondary                           : darken(desaturate($color-primary, 40), 21);
+$color-black                               : #000000;
+$color-background-dark                     : #141414;
+$color-background-grey                     : #282828;
+$color-background-blue                     : #2f383b;
+$color-background-hover                    : #545454;
+$color-text                                : #cccccc;
+$color-white                               : #ffffff;
+$color-main-background                     : #0e1111;
+$color-background-dark-transparent         : rgba(0, 0, 0, 0.85);
+$color-background-medium-transparent       : rgba(35, 44, 49, 0.5);
+$color-complement                          : lighten(desaturate(adjust-hue($color-primary, -160), 14.13), 13.92);
+$color-accent                              : $color-primary;
+$color-border                              : $color-text;
+$color-link                                : $color-secondary;
+$color-title                               : $color-primary;
+
+// Header
+$color-header-border                       : $color-white;
+$color-header-title                        : $color-text;
+$color-logo-lynx                           : $color-white;
+
+// Footer
+$color-footer-background                   : $color-main-background;
+$color-border-top-background               : $color-border;
+$color-footer-logo                         : #00b7ff24;

+ 7 - 0
sass/modules/_font.scss

@@ -0,0 +1,7 @@
+// Base Font
+$base-font-family         : "Monospace";
+$base-font-weight         : 500;
+$base-font-size           : 20px;
+
+// Header Title
+$header-title-font-family : "Arial";

+ 12 - 0
sass/modules/_query.scss

@@ -0,0 +1,12 @@
+// Match pure queries:
+//None 	None 	                                Always 	  .pure-u-*
+//sm 	  @media screen and (min-width: 35.5em) ≥ 568px 	.pure-u-sm-*
+//md 	  @media screen and (min-width: 48em) 	≥ 768px 	.pure-u-md-*
+//lg 	  @media screen and (min-width: 64em) 	≥ 1024px 	.pure-u-lg-*
+//xl 	  @media screen and (min-width: 80em) 	≥ 1280px 	.pure-u-xl-*
+$sm-query  : "screen and (min-width: 35.5em)";
+$md-query  : "screen and (min-width: 48em)";
+$lg-query  : "screen and (min-width: 64em)";
+$xl-query  : "screen and (min-width: 80em)";
+
+$max-width : 1010px;

+ 1 - 0
sass/modules/_spacing.scss

@@ -0,0 +1 @@
+$baseline: 1rem;

+ 24 - 0
sass/modules/_utility.scss

@@ -0,0 +1,24 @@
+$code-font-size : 0.9rem;
+
+@mixin selector($foreground-color, $background-color) {
+  ::selection {
+    background : $background-color;
+    color      : $foreground-color;
+  }
+}
+
+@mixin user-select($value) {
+  user-select : $value;
+}
+
+@mixin rounded-borders() {
+  border-radius : 5px;
+}
+
+@mixin top-rounded-borders() {
+  border-radius : 5px 5px 0 0;
+}
+
+@mixin bottom-rounded-borders() {
+  border-radius : 0 0 5px 5px;
+}

File diff suppressed because it is too large
+ 7 - 0
sass/partials/base/_backgrounds.scss


+ 55 - 0
sass/partials/base/inline/_base.scss

@@ -0,0 +1,55 @@
+@import "../../../modules/all";
+
+:root {
+  --accent-color : #{$color-accent};
+}
+
+@media (prefers-color-scheme : dark) {
+  :root {
+    --accent-color : #{$color-accent};
+  }
+}
+
+@media (prefers-color-scheme : light) {
+  :root {
+
+  }
+}
+
+html {
+  overflow-y      : scroll;
+  scrollbar-color : $color-secondary $color-background-dark;
+}
+
+body {
+  background-color : $color-main-background;
+  color            : $color-text;
+  display          : flex;
+  flex-direction   : column;
+  font-family      : $base-font-family;
+  font-size        : $base-font-size;
+  font-weight      : $base-font-weight;
+  height           : 100%;
+  min-height       : 100vh;
+  position         : relative;
+}
+
+//@include selector($color-selector-foreground, $color-selector-background);
+
+img {
+  @include user-select(none);
+
+  border : 0;
+}
+
+a,
+a:link,
+a:visited {
+  color           : $color-link;
+  outline         : 0;
+  text-decoration : none;
+}
+
+a:hover {
+  text-decoration : underline;
+}

+ 5 - 0
sass/partials/base/inline/_footer.scss

@@ -0,0 +1,5 @@
+@import "../../../modules/all";
+
+footer {
+
+}

+ 5 - 0
sass/partials/base/inline/_header.scss

@@ -0,0 +1,5 @@
+@import "../../../modules/all";
+
+header {
+
+}

+ 14 - 0
sass/partials/base/inline/_pure-reset.scss

@@ -0,0 +1,14 @@
+@import "../../../modules/all";
+
+html,
+button,
+input,
+select,
+textarea,
+.pure-g [class*="pure-u"] {
+  font-family : $base-font-family;
+}
+
+.pure-g {
+  font-family : $base-font-family;
+}