diff options
| author | Jules Laplace <julescarbon@gmail.com> | 2021-08-16 18:02:43 +0200 |
|---|---|---|
| committer | Jules Laplace <julescarbon@gmail.com> | 2021-08-16 18:02:43 +0200 |
| commit | 9ffba33ec3d0e1bc2340f10afba9af39cac095b9 (patch) | |
| tree | 6af61f342ab20e4327d012f0612abe3fcf06f7d3 | |
| parent | 6f29c59991925e8685a1911ce1b37c6097d7c517 (diff) | |
better viz
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | src/index.js | 144 | ||||
| -rw-r--r-- | src/utils/set_utils.js | 81 | ||||
| -rw-r--r-- | src/utils/stars.js | 53 | ||||
| -rw-r--r-- | yarn.lock | 7 |
5 files changed, 267 insertions, 20 deletions
diff --git a/package.json b/package.json index 39c691a..34aaa90 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "eslint-plugin-babel": "^5.3.1", "eslint-plugin-import": "^2.24.0", "fontfaceobserver": "^2.1.0", - "fsevents": "^2.3.2", "history": "^5.0.1", "node-fetch": "^2.6.1", "okcms": "git+ssh://git@ghghgh.us/~/okcms.git", @@ -55,6 +54,7 @@ "redux-thunk": "^2.3.0", "store2": "^2.12.0", "terser-webpack-plugin": "^5.1.4", + "three-spritetext": "^1.6.2", "uuid": "^8.3.2", "vimeo": "^2.1.1", "webpack": "^5.50.0", diff --git a/src/index.js b/src/index.js index 39b3c7d..4c09857 100644 --- a/src/index.js +++ b/src/index.js @@ -7,46 +7,154 @@ */ import ForceGraph3D from "3d-force-graph"; +import SpriteText from "three-spritetext"; +import { union } from "./utils/set_utils.js"; +import stars from "./utils/stars.js"; + +const colors = [ + "rgba(111,53,158,1.0)", + "rgba(220,188,253,1.0)", + "rgba(30,177,237,1.0)", + "rgba(148,206,88,1.0)", + "rgba(252,42,28,1.0)", + "rgba(255,253,56,1.0)", + "rgba(43,253,183,1.0)", + "rgba(252,76,252,1.0)", + "rgba(205,254,170,1.0)", + "rgba(254,205,195,1.0)", + "rgba(199,227,254,1.0)", + "rgba(253,191,45,1.0)", + "rgba(253,191,45,1.0)", +]; async function main() { const db = await loadDB(); - const trees = {}; + const groups = {}; + const linkable = {}; const data = { nodes: [], links: [] }; console.log(db); + /** + * load nodes and links + */ + db.page.forEach((item, index) => { - data.nodes.push({ + const node = { + title: item.title, id: index, - }); + groups: [], + }; + data.nodes.push(node); for (let tagIndex = 0; tagIndex < 8; tagIndex += 1) { - const value = item["tag_" + tagIndex]; - if (value) { - if (value in trees) { - data.links.push({ - source: choice(trees[value]), - target: index, - }); - // option: don't link to the root node more than once - if (window.location.hash === "#dense" && trees[value][0]) { - trees[value].push(index); - } else { - trees[value] = [index]; - } + const group = item["tag_" + tagIndex]; + if (!group) continue; + group -= 1; + node.groups.push(group); + if (group in groups) { + groups[group].push(index); + } else { + groups[group] = [index]; + } + if (group in linkable) { + data.links.push({ + // source: choice(linkable[group]), + source: linkable[group][0], + target: index, + }); + // option: don't link to the root node more than once + // if (window.location.hash === "#dense" && linkable[group][0]) { + if (linkable[group][0]) { + linkable[group].push(index); } else { - trees[value] = [index]; + linkable[group] = [index]; } + } else { + linkable[group] = [index]; } } }); + /** + * find common links + */ + + data.links.forEach((link) => { + const a = data.nodes[link.source]; + const b = data.nodes[link.target]; + + !a.links && (a.links = []); + !b.links && (b.links = []); + a.links.push(link); + b.links.push(link); + }); + + const highlightNodes = new Set(); + const highlightLinks = new Set(); + let selectedNode = null; + + /** build group */ + let graph = ForceGraph3D(); - graph(document.querySelector("#graph")).graphData(data); + graph(document.querySelector("#graph")) + .graphData(data) + .nodeLabel((node) => node.title) + .nodeThreeObject((node) => { + const sprite = new SpriteText( + node.title.split(/[ :]+/).slice(0, 3).join(" ") + ); + sprite.material.depthWrite = false; // make sprite background transparent + sprite.color = colors[node.groups[0]]; // node.groups.length - 1]]; + sprite.textHeight = 4; + return sprite; + }) + // .nodeColor((node) => + // highlightNodes.has(node.id) + // ? node === selectedNode + // ? colors[commonGroups(selectedNode, node)[0]] + // : colors[commonGroups(selectedNode, node)[0]].replace("1.0", "0.8") + // : colors[commonGroups(selectedNode, node)[0]].replace("1.0", "0.6") + // ) + .linkWidth((link) => (highlightLinks.has(link) ? 4 : 1)) + .onNodeClick((node) => { + // no state change + if (!node && !highlightNodes.size) return; + + selectedNode = selectedNode === node ? null : node; + + highlightNodes.clear(); + highlightLinks.clear(); + if (node) { + node.groups.forEach((group) => + groups[group].forEach((neighbor) => highlightNodes.add(neighbor)) + ); + node.links.forEach((link) => highlightLinks.add(link)); + } + + updateHighlight(); + }); + + function updateHighlight() { + graph.nodeColor(graph.nodeColor()).linkWidth(graph.linkWidth()); + } + + graph.d3Force("charge").strength(-150); + + // camera orbit + const distance = 250; + let angle = 0; + graph.cameraPosition({ + x: distance * Math.sin(angle), + z: distance * Math.cos(angle), + }); + + stars(); } const randint = (limit) => Math.floor(Math.random() * limit); const choice = (list) => list[randint(list.length)]; +const commonGroups = (a, b) => union(a.groups, b.groups); async function loadDB() { const request = await fetch("/db.json"); diff --git a/src/utils/set_utils.js b/src/utils/set_utils.js new file mode 100644 index 0000000..88e8fea --- /dev/null +++ b/src/utils/set_utils.js @@ -0,0 +1,81 @@ +/** + * Operations on sets. + * @module app/utils/set_utils + */ + +/** + * Determine if `set` contains `subset` + * @param {Set} set the superset + * @param {Set} subset the subset + * @return {Boolean} true if set contains subset + */ +export function isSuperset(set, subset) { + for (let elem of subset) { + if (!set.has(elem)) { + return false; + } + } + return true; +} + +/** + * Return the union (A or B) of two sets + * @param {Set} setA a set + * @param {Set} setB a set + * @return {Boolean} the union of the sets + */ +export function union(setA, setB) { + let _union = new Set(setA); + for (let elem of setB) { + _union.add(elem); + } + return _union; +} + +/** + * Return the intersection (A and B) of two sets + * @param {Set} setA a set + * @param {Set} setB a set + * @return {Boolean} the intersection of the sets + */ +export function intersection(setA, setB) { + let _intersection = new Set(); + for (let elem of setB) { + if (setA.has(elem)) { + _intersection.add(elem); + } + } + return _intersection; +} + +/** + * Return the symmetric difference (A xor B) of two sets + * @param {Set} setA a set + * @param {Set} setB a set + * @return {Boolean} the symmetric difference of the sets + */ +export function symmetricDifference(setA, setB) { + let _difference = new Set(setA); + for (let elem of setB) { + if (_difference.has(elem)) { + _difference.delete(elem); + } else { + _difference.add(elem); + } + } + return _difference; +} + +/** + * Return the difference (A not B) of two sets + * @param {Set} setA a set + * @param {Set} setB a set + * @return {Boolean} the difference of the sets + */ +export function difference(setA, setB) { + let _difference = new Set(setA); + for (let elem of setB) { + _difference.delete(elem); + } + return _difference; +} diff --git a/src/utils/stars.js b/src/utils/stars.js new file mode 100644 index 0000000..516a359 --- /dev/null +++ b/src/utils/stars.js @@ -0,0 +1,53 @@ +export default function stars() { + var canvas = document.createElement("canvas"), + ctx = canvas.getContext("2d"); + document.body.appendChild(canvas); + canvas.style.width = "100%"; + canvas.style.height = "100%"; + canvas.style.position = "absolute"; + canvas.style.top = "0px"; + canvas.style.left = "0px"; + canvas.style.zIndex = 1; + canvas.style.opacity = 0.6; + canvas.style.pointerEvents = "none"; + document.body.addEventListener("resize", go); + document.body.parentNode.style.backgroundColor = "black"; + ctx.strokeStyle = "white"; + var s = Math.sin, + c = Math.cos; + go(); + function ri(n) { + return Math.random() * n; + } + function rr(a, b) { + return (b - a) * Math.random() + a; + } + function go() { + var w = (canvas.width = window.innerWidth); + var h = (canvas.height = window.innerHeight); + ctx.clearRect(0, 0, w, h); + var n = Math.sqrt(w * h) | 0; + while (n--) { + var x = ri(w); + var y = ri(h); + var r0 = rr(0, 1); + var r1 = rr(0, 1); + var r2 = rr(0, 1); + var t0 = ri(2 * Math.PI); + var t1 = ri(2 * Math.PI); + var t2 = ri(2 * Math.PI); + var x0 = x + c(t0) * r0; + var y0 = y + s(t0) * r0; + var x1 = x + c(t1) * r1; + var y1 = y + s(t1) * r1; + var x2 = x + c(t2) * r2; + var y2 = y + s(t2) * r2; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.bezierCurveTo(x0, y0, x1, y1, x2, y2); + var color = rr(0, 255) | 0; + ctx.strokeStyle = "rgb(" + color + "," + color + "," + color + ")"; + ctx.stroke(); + } + } +} @@ -3531,7 +3531,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^2.3.2, fsevents@~2.3.1, fsevents@~2.3.2: +fsevents@~2.3.1, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -6917,6 +6917,11 @@ three-render-objects@^1.26: kapsule "^1.13" polished "4" +three-spritetext@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/three-spritetext/-/three-spritetext-1.6.2.tgz#824e96db2ffe2148d26a7002d7b63ec11d19d0cf" + integrity sha512-VALj40t81Z6x/fDnY/tts8QU+mBl77bxoynBbcn/DW4oxfzZSwjaOfkQOe0jYpLoK2vtP0bAULvGgwIYnsN6oQ== + "three@>=0.118 <1": version "0.131.3" resolved "https://registry.yarnpkg.com/three/-/three-0.131.3.tgz#406fd210c603ca9154937ae3582996fbfd3cb716" |
