aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authors-ol <s-ol@users.noreply.github.com>2021-01-27 14:33:22 +0000
committers-ol <s-ol@users.noreply.github.com>2021-01-27 14:41:09 +0000
commit52f5c716fb09d0be315338703caf22c6261690ba (patch)
treef98cea42d411c8fd48cca00c093401d797e44495 /src
parentnode collapsing (w/o relayouting) (diff)
downloadfedidag-52f5c716fb09d0be315338703caf22c6261690ba.tar.gz
fedidag-52f5c716fb09d0be315338703caf22c6261690ba.zip
actually visually collapse nodes
Diffstat (limited to 'src')
-rw-r--r--src/graph.json1
-rw-r--r--src/index.js56
-rw-r--r--src/layout.js118
-rw-r--r--src/ui.js11
-rw-r--r--src/viz.es.js131
5 files changed, 100 insertions, 217 deletions
diff --git a/src/graph.json b/src/graph.json
index 5c3fcbf..a1f67d5 100644
--- a/src/graph.json
+++ b/src/graph.json
@@ -3,6 +3,7 @@
"id": "https://dag.s-ol.nu/discussions/1",
"type": "Document",
"name": "DiscDAG's Future",
+ "first": "https://dag.s-ol.nu/users/ColinWright/notes/20200510151146a",
"items": [
{
"id": "https://dag.s-ol.nu/users/ColinWright/notes/20200510151146a",
diff --git a/src/index.js b/src/index.js
index 111a7f6..6a20a0c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,7 +2,7 @@ import 'core-js/stable';
import 'regenerator-runtime/runtime';
import * as jsonld from 'jsonld';
import * as dom from './dom';
-import * as layout from './layout';
+import Layout from './layout';
import { Note } from './ui';
window.jsonld = jsonld;
@@ -62,6 +62,7 @@ const loadData = async () => {
{
'@context': context,
type: 'Document',
+ first: { '@embed': '@never' },
items: {
context: { '@embed': '@never' },
replies: { '@embed': '@never' },
@@ -103,12 +104,12 @@ const createNotes = (graph, discussion) => {
};
const layoutNodes = async (graph) => {
- let state = layout.init();
+ const layout = new Layout();
for (const node of Object.values(graph))
- state = layout.addNode(state, node);
+ layout.addNode(graph, node);
- await layout.render(state, canvas,
+ await layout.render(canvas,
(id, x, y) => {
const dom = graph[id].dom;
dom.style.left = `${x - dom.offsetWidth/2}px`;
@@ -128,22 +129,15 @@ const update = async (graph) => {
createNotes(graph, discussion);
await layoutNodes(graph);
-};
-const scrollToRoot = (graph) => {
- for (const node of Object.values(graph)) {
- if (node.data.inReplyTo.length === 0) {
- node.dom.scrollIntoView({ block: 'nearest', inline: 'nearest' });
- return;
- }
- }
-}
+ const root = graph[discussion.first];
+ root.dom.scrollIntoView({ block: 'nearest', inline: 'nearest' });
+};
// update collapsed nodes starting with id
// returns list of (potentially) affected nodes
-const updateCollapsed = (graph, id, updated=[]) => {
- updated.push(id);
- const node = graph[id];
+const updateCollapsed = (graph, node, updated=[]) => {
+ updated.push(node.data.id);
if (node.collapsed !== 'explicit') {
node.collapsed =
@@ -151,8 +145,8 @@ const updateCollapsed = (graph, id, updated=[]) => {
&& node.data.inReplyTo.every(p => graph[p.id].collapsed);
}
- for (const child of graph[id].data.replies)
- updateCollapsed(graph, child.id, updated);
+ for (const child of node.data.replies)
+ updateCollapsed(graph, graph[child.id], updated);
return updated;
};
@@ -161,20 +155,26 @@ const graph = {};
window.graph = graph;
update(graph)
- .then(() => scrollToRoot(graph))
.catch(err => console.error(err));
const hideNote = (e) => {
- const node = graph[e.currentTarget.dataset.id];
-
- node.collapsed = node.collapsed ? false : 'explicit';
-
- for (const id of updateCollapsed(graph, node.data.id)) {
- graph[id].dom.style.opacity = graph[id].collapsed ? 0.2 : 1.0;
+ const clicked = graph[e.currentTarget.dataset.id];
+ clicked.collapsed = clicked.collapsed ? false : 'explicit';
+
+ for (const id of updateCollapsed(graph, clicked)) {
+ const node = graph[id];
+
+ if (node.collapsed) {
+ const noParents = node.data.inReplyTo.every(p => graph[p.id].collapsed);
+ const noChildren = node.data.replies.every(c => graph[c.id].collapsed);
+ node.dom.className = noParents && noChildren ? 'hidden' : 'collapsed';
+ } else {
+ node.dom.className = '';
+ }
}
- // layoutNodes(graph)
- // .then(() => scrollToRoot(graph))
- // .catch(err => console.error(err));
+ layoutNodes(graph)
+ .then(() => clicked.dom.scrollIntoView({ block: 'nearest', inline: 'nearest' }))
+ .catch(err => console.error(err));
};
diff --git a/src/layout.js b/src/layout.js
index e256681..bda2886 100644
--- a/src/layout.js
+++ b/src/layout.js
@@ -1,74 +1,76 @@
import Viz from './viz.es';
-const graphviz = new Viz({ workerURL: 'lib/lite.render.js' });
-
-export const init = () => {
- return 'digraph { node [shape="rectangle"];\n';
-}
+const toNumber = (i) => Number(i);
+const gv = new Viz({ workerURL: 'lib/lite.render.js' });
-export const addNode = (dot, node) => {
- if (node.collapsed)
- return dot;
+export default class Layout {
+ constructor() {
+ this.src = 'digraph { node [shape="rectangle"];\n';
+ }
- const id = node.data.id;
- dot += ` "${id}" [fixedsize=true; width=${node.dom.offsetWidth / 72}; height=${node.dom.offsetHeight / 72}];\n`;
+ addNode(graph, node) {
+ if (node.dom.className === 'hidden')
+ return;
- for (const reply of node.data.replies)
- dot += ` "${id}" -> "${reply.id}";\n`;
+ const id = node.data.id;
+ this.src += ` "${id}" [fixedsize=true; width=${node.dom.offsetWidth / 72}; height=${node.dom.offsetHeight / 72}];\n`;
- return dot;
-};
+ for (const reply of node.data.replies) {
+ if (node.collapsed && graph[reply.id].collapsed)
+ continue;
-const toNumber = (i) => Number(i);
+ this.src += ` "${id}" -> "${reply.id}";\n`;
+ }
+ }
-export const render = async (dot, canvas, setPosition, setSize) => {
- dot += '}';
+ async render(canvas, setPosition, setSize) {
+ this.src += '}';
- const info = await graphviz.renderJSONObject(dot, { format: 'json0' });
- window.info = info;
+ const info = await gv.renderJSONObject(this.src, { format: 'json0' });
- const [_x, _y, width, height] = info.bb.split(',').map(toNumber);
+ const [_x, _y, width, height] = info.bb.split(',').map(toNumber);
- setSize(width, height);
+ setSize(width, height);
- for (const object of info.objects) {
- const [x, y] = object.pos.split(',').map(toNumber);
- setPosition(object.name, x, height - y);
- }
+ for (const object of info.objects) {
+ const [x, y] = object.pos.split(',').map(toNumber);
+ setPosition(object.name, x, height - y);
+ }
- const ctx = canvas.getContext('2d');
- ctx.lineWidth = 4;
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
- ctx.strokeStyle = '#696969';
- ctx.fillStyle = '#696969';
-
- for (const edge of info.edges) {
- const pos = edge.pos.slice(2);
- const points = pos.split(' ').map(p => {
- const [x, y] = p.split(',');
- return [toNumber(x), height - toNumber(y)];
- });
-
- ctx.beginPath();
- ctx.moveTo(...points[1]);
- for (let i = 4; i < points.length; i += 3) {
- const [ax, ay] = points[i - 2];
- const [bx, by] = points[i - 1];
- const [x, y] = points[i];
- ctx.bezierCurveTo(ax, ay, bx, by, x, y);
+ const ctx = canvas.getContext('2d');
+ ctx.lineWidth = 4;
+ ctx.lineCap = 'round';
+ ctx.lineJoin = 'round';
+ ctx.strokeStyle = '#696969';
+ ctx.fillStyle = '#696969';
+
+ for (const edge of info.edges) {
+ const pos = edge.pos.slice(2);
+ const points = pos.split(' ').map(p => {
+ const [x, y] = p.split(',');
+ return [toNumber(x), height - toNumber(y)];
+ });
+
+ ctx.beginPath();
+ ctx.moveTo(...points[1]);
+ for (let i = 4; i < points.length; i += 3) {
+ const [ax, ay] = points[i - 2];
+ const [bx, by] = points[i - 1];
+ const [x, y] = points[i];
+ ctx.bezierCurveTo(ax, ay, bx, by, x, y);
+ }
+ ctx.stroke();
+
+ const [lx, ly] = points[points.length - 1];
+ const [tx, ty] = points[0];
+ const [dx, dy] = [tx - lx, ty - ly];
+ const [hx, hy] = [dy*0.7, dx*-0.7];
+
+ ctx.beginPath();
+ ctx.moveTo(lx + dx*0.8, ly + dy*0.8);
+ ctx.lineTo(lx - dx*0.5 + hx, ly - dy*0.5 + hy);
+ ctx.lineTo(lx - dx*0.5 - hx, ly - dy*0.5 - hy);
+ ctx.fill();
}
- ctx.stroke();
-
- const [lx, ly] = points[points.length - 1];
- const [tx, ty] = points[0];
- const [dx, dy] = [tx - lx, ty - ly];
- const [hx, hy] = [dy*0.7, dx*-0.7];
-
- ctx.beginPath();
- ctx.moveTo(lx + dx*0.8, ly + dy*0.8);
- ctx.lineTo(lx - dx*0.5 + hx, ly - dy*0.5 + hy);
- ctx.lineTo(lx - dx*0.5 - hx, ly - dy*0.5 - hy);
- ctx.fill();
}
}
diff --git a/src/ui.js b/src/ui.js
index 88e5b10..c7f1090 100644
--- a/src/ui.js
+++ b/src/ui.js
@@ -96,6 +96,17 @@ section > main {
section.tombstone > main {
font-family: inherit;
}
+
+section.hidden {
+ display: none;
+}
+
+section.collapsed {
+ width: auto;
+}
+section.collapsed > main {
+ display: none;
+}
`;
const colors = new ColorHash({ lightness: 0.8 });
diff --git a/src/viz.es.js b/src/viz.es.js
index 3c63e69..56f6a98 100644
--- a/src/viz.es.js
+++ b/src/viz.es.js
@@ -123,110 +123,6 @@ var ModuleWrapper = function ModuleWrapper(module, render) {
// https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
-
-function b64EncodeUnicode(str) {
- return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
- return String.fromCharCode('0x' + p1);
- }));
-}
-
-function defaultScale() {
- if ('devicePixelRatio' in window && window.devicePixelRatio > 1) {
- return window.devicePixelRatio;
- } else {
- return 1;
- }
-}
-
-function svgXmlToImageElement(svgXml) {
- var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
- _ref$scale = _ref.scale,
- scale = _ref$scale === undefined ? defaultScale() : _ref$scale,
- _ref$mimeType = _ref.mimeType,
- mimeType = _ref$mimeType === undefined ? "image/png" : _ref$mimeType,
- _ref$quality = _ref.quality,
- quality = _ref$quality === undefined ? 1 : _ref$quality;
-
- return new Promise(function (resolve, reject) {
- var svgImage = new Image();
-
- svgImage.onload = function () {
- var canvas = document.createElement('canvas');
- canvas.width = svgImage.width * scale;
- canvas.height = svgImage.height * scale;
-
- var context = canvas.getContext("2d");
- context.drawImage(svgImage, 0, 0, canvas.width, canvas.height);
-
- canvas.toBlob(function (blob) {
- var image = new Image();
- image.src = URL.createObjectURL(blob);
- image.width = svgImage.width;
- image.height = svgImage.height;
-
- resolve(image);
- }, mimeType, quality);
- };
-
- svgImage.onerror = function (e) {
- var error;
-
- if ('error' in e) {
- error = e.error;
- } else {
- error = new Error('Error loading SVG');
- }
-
- reject(error);
- };
-
- svgImage.src = 'data:image/svg+xml;base64,' + b64EncodeUnicode(svgXml);
- });
-}
-
-function svgXmlToImageElementFabric(svgXml) {
- var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
- _ref2$scale = _ref2.scale,
- scale = _ref2$scale === undefined ? defaultScale() : _ref2$scale,
- _ref2$mimeType = _ref2.mimeType,
- mimeType = _ref2$mimeType === undefined ? 'image/png' : _ref2$mimeType,
- _ref2$quality = _ref2.quality,
- quality = _ref2$quality === undefined ? 1 : _ref2$quality;
-
- var multiplier = scale;
-
- var format = void 0;
- if (mimeType == 'image/jpeg') {
- format = 'jpeg';
- } else if (mimeType == 'image/png') {
- format = 'png';
- }
-
- return new Promise(function (resolve, reject) {
- fabric.loadSVGFromString(svgXml, function (objects, options) {
- // If there's something wrong with the SVG, Fabric may return an empty array of objects. Graphviz appears to give us at least one <g> element back even given an empty graph, so we will assume an error in this case.
- if (objects.length == 0) {
- reject(new Error('Error loading SVG with Fabric'));
- }
-
- var element = document.createElement("canvas");
- element.width = options.width;
- element.height = options.height;
-
- var canvas = new fabric.Canvas(element, { enableRetinaScaling: false });
- var obj = fabric.util.groupSVGElements(objects, options);
- canvas.add(obj).renderAll();
-
- var image = new Image();
- image.src = canvas.toDataURL({ format: format, multiplier: multiplier, quality: quality });
- image.width = options.width;
- image.height = options.height;
-
- resolve(image);
- });
- });
-}
-
var Viz = function () {
function Viz() {
var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
@@ -277,33 +173,6 @@ var Viz = function () {
return this.wrapper.render(src, { format: format, engine: engine, files: files, images: images, yInvert: yInvert, nop: nop });
}
}, {
- key: 'renderSVGElement',
- value: function renderSVGElement(src) {
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
-
- return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) {
- var parser = new DOMParser();
- return parser.parseFromString(str, 'image/svg+xml').documentElement;
- });
- }
- }, {
- key: 'renderImageElement',
- value: function renderImageElement(src) {
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
- var scale = options.scale,
- mimeType = options.mimeType,
- quality = options.quality;
-
-
- return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) {
- if ((typeof fabric === 'undefined' ? 'undefined' : _typeof(fabric)) === "object" && fabric.loadSVGFromString) {
- return svgXmlToImageElementFabric(str, { scale: scale, mimeType: mimeType, quality: quality });
- } else {
- return svgXmlToImageElement(str, { scale: scale, mimeType: mimeType, quality: quality });
- }
- });
- }
- }, {
key: 'renderJSONObject',
value: function renderJSONObject(src) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};