aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authors-ol <s-ol@users.noreply.github.com>2021-01-26 17:15:52 +0000
committers-ol <s-ol@users.noreply.github.com>2021-01-26 17:56:21 +0000
commit84a2b2f01064375dee2164a8e676d90203b9296b (patch)
tree50285380a0ca150dfa8301e0001ee0c33a82a32c /src
parentadd README (diff)
downloadfedidag-84a2b2f01064375dee2164a8e676d90203b9296b.tar.gz
fedidag-84a2b2f01064375dee2164a8e676d90203b9296b.zip
graphviz-ify
Diffstat (limited to 'src')
-rw-r--r--src/index.js70
-rw-r--r--src/layout.js116
-rw-r--r--src/viz.es.js325
3 files changed, 464 insertions, 47 deletions
diff --git a/src/index.js b/src/index.js
index 7e9cdc6..2fbbcce 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,8 +1,8 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import * as jsonld from 'jsonld';
-import dagre from 'dagre';
import * as dom from './dom';
+import * as layout from './layout';
import { Note } from './ui';
dom.css`
@@ -50,26 +50,21 @@ const loadData = async () => {
const article = document.createElement('article');
document.body.appendChild(article);
-loadData().then(data => {
- const graph = new dagre.graphlib.Graph();
+loadData().then(async (data) => {
+ let state = layout.init();
article.textContent = '';
const canvas = document.createElement('canvas');
article.appendChild(canvas);
- graph.setGraph({
- rankSep: 20,
- nodeSep: 40,
- });
- graph.setDefaultEdgeLabel(() => ({}));
-
- let rootId;
-
const indexedData = {};
for (const data of data['@graph'])
indexedData[data.id] = data;
window.dagData = indexedData;
+ const graph = {};
+
+ let rootId;
for (const data of data['@graph']) {
if (data.inReplyTo.length === 0)
@@ -89,45 +84,26 @@ loadData().then(data => {
);
article.appendChild(dom);
- graph.setNode(data.id, {
+ const node = {
dom, data,
width: dom.offsetWidth,
height: dom.offsetHeight,
- });
-
- for (const reply of data.replies || []) {
- graph.setEdge(data.id, reply);
- }
- }
-
- dagre.layout(graph);
- const info = graph.graph();
-
- canvas.width = info.width;
- canvas.height = info.height;
-
- for (const id of graph.nodes()) {
- const node = graph.node(id);
- node.dom.style.left = `${node.x - node.width / 2}px`;
- node.dom.style.top = `${node.y - node.height / 2}px`;
- }
-
- const ctx = canvas.getContext('2d');
- ctx.lineWidth = 4;
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
- ctx.strokeStyle = '#696969';
-
- for (const eo of graph.edges()) {
- const edge = graph.edge(eo);
-
- ctx.beginPath();
- ctx.moveTo(edge.points[0].x, edge.points[0].y);
- for (const p of edge.points.slice(1))
- ctx.lineTo(p.x, p.y);
- ctx.stroke();
+ };
+ graph[data.id] = node;
+ state = layout.addNode(state, node);
}
- const rootNode = graph.node(rootId);
- window.scrollTo(rootNode.x - window.innerWidth / 2, rootNode.y);
+ await layout.render(state, canvas,
+ (id, x, y) => {
+ const dom = graph[id].dom;
+ dom.style.left = `${x - dom.offsetWidth/2}px`;
+ dom.style.top = `${y - dom.offsetHeight/2}px`;
+ },
+ (width, height) => {
+ canvas.width = width;
+ canvas.height = height;
+ },
+ );
+
+ graph[rootId].dom.scrollIntoView();
});
diff --git a/src/layout.js b/src/layout.js
new file mode 100644
index 0000000..4762f4a
--- /dev/null
+++ b/src/layout.js
@@ -0,0 +1,116 @@
+import Viz from './viz.es';
+
+const graphviz = new Viz({ workerURL: 'lib/lite.render.js' });
+
+export const init = () => {
+ return 'digraph { node [shape="rectangle"];\n';
+}
+
+export const addNode = (dot, node) => {
+ const id = node.data.id;
+ dot += ` "${id}" [fixedsize=true; width=${node.width / 72}; height=${node.height / 72}];\n`;
+
+ for (const replyId of node.data.replies || [])
+ dot += ` "${id}" -> "${replyId}";\n`;
+
+ return dot;
+};
+
+const toNumber = (i) => Number(i);
+
+export const render = async (dot, canvas, setPosition, setSize) => {
+ dot += '}';
+
+ console.log(dot);
+
+ const info = await graphviz.renderJSONObject(dot, { format: 'json0' });
+ window.info = info;
+
+ const [_x, _y, width, height] = info.bb.split(',').map(toNumber);
+
+ setSize(width, height);
+
+ 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';
+
+ 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.lineTo(...points[0]);
+ ctx.stroke();
+ }
+}
+
+/*
+import * as dagre from 'dagre';
+
+export const init = () => {
+ const graph = new dagre.graphlib.Graph();
+
+ graph.setGraph({
+ rankSep: 20,
+ nodeSep: 40,
+ });
+ graph.setDefaultEdgeLabel(() => ({}));
+
+ return graph;
+};
+
+export const addNode = (graph, node) => {
+ graph.setNode(node.data.id, node);
+
+ for (const reply of node.data.replies || []) {
+ graph.setEdge(node.data.id, reply);
+ }
+
+ return graph;
+};
+
+export const render = (graph, canvas, setPosition, setSize) => {
+ dagre.layout(graph);
+ const info = graph.graph();
+
+ setSize(info.width, info.height);
+
+ for (const id of graph.nodes()) {
+ const node = graph.node(id);
+ setPosition(node, node.x, node.y);
+ }
+
+ const ctx = canvas.getContext('2d');
+ ctx.lineWidth = 4;
+ ctx.lineCap = 'round';
+ ctx.lineJoin = 'round';
+ ctx.strokeStyle = '#696969';
+
+ for (const eo of graph.edges()) {
+ const edge = graph.edge(eo);
+
+ ctx.beginPath();
+ ctx.moveTo(edge.points[0].x, edge.points[0].y);
+ for (const p of edge.points.slice(1))
+ ctx.lineTo(p.x, p.y);
+ ctx.stroke();
+ }
+};
+*/
diff --git a/src/viz.es.js b/src/viz.es.js
new file mode 100644
index 0000000..3c63e69
--- /dev/null
+++ b/src/viz.es.js
@@ -0,0 +1,325 @@
+/*
+Viz.js 2.1.2 (Graphviz 2.40.1, Expat {{EXPAT_VERSION}}, Emscripten 1.37.36)
+Copyright (c) 2014-2018 Michael Daines
+Licensed under MIT license
+
+This distribution contains other software in object code form:
+
+Graphviz
+Licensed under Eclipse Public License - v 1.0
+http://www.graphviz.org
+
+Expat
+Copyright (c) 1998, 1999, 2000 Thai Open Source Software Center Ltd and Clark Cooper
+Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Expat maintainers.
+Licensed under MIT license
+http://www.libexpat.org
+
+zlib
+Copyright (C) 1995-2013 Jean-loup Gailly and Mark Adler
+http://www.zlib.net/zlib_license.html
+*/
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
+ return typeof obj;
+} : function (obj) {
+ return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+};
+
+var classCallCheck = function (instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+};
+
+var createClass = function () {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ return function (Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+}();
+
+var _extends = Object.assign || function (target) {
+ for (var i = 1; i < arguments.length; i++) {
+ var source = arguments[i];
+
+ for (var key in source) {
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
+ target[key] = source[key];
+ }
+ }
+ }
+
+ return target;
+};
+
+var WorkerWrapper = function () {
+ function WorkerWrapper(worker) {
+ var _this = this;
+
+ classCallCheck(this, WorkerWrapper);
+
+ this.worker = worker;
+ this.listeners = [];
+ this.nextId = 0;
+
+ this.worker.addEventListener('message', function (event) {
+ var id = event.data.id;
+ var error = event.data.error;
+ var result = event.data.result;
+
+ _this.listeners[id](error, result);
+ delete _this.listeners[id];
+ });
+ }
+
+ createClass(WorkerWrapper, [{
+ key: 'render',
+ value: function render(src, options) {
+ var _this2 = this;
+
+ return new Promise(function (resolve, reject) {
+ var id = _this2.nextId++;
+
+ _this2.listeners[id] = function (error, result) {
+ if (error) {
+ reject(new Error(error.message, error.fileName, error.lineNumber));
+ return;
+ }
+ resolve(result);
+ };
+
+ _this2.worker.postMessage({ id: id, src: src, options: options });
+ });
+ }
+ }]);
+ return WorkerWrapper;
+}();
+
+var ModuleWrapper = function ModuleWrapper(module, render) {
+ classCallCheck(this, ModuleWrapper);
+
+ var instance = module();
+ this.render = function (src, options) {
+ return new Promise(function (resolve, reject) {
+ try {
+ resolve(render(instance, src, options));
+ } catch (error) {
+ reject(error);
+ }
+ });
+ };
+};
+
+// 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] : {},
+ workerURL = _ref3.workerURL,
+ worker = _ref3.worker,
+ Module = _ref3.Module,
+ render = _ref3.render;
+
+ classCallCheck(this, Viz);
+
+ if (typeof workerURL !== 'undefined') {
+ this.wrapper = new WorkerWrapper(new Worker(workerURL));
+ } else if (typeof worker !== 'undefined') {
+ this.wrapper = new WorkerWrapper(worker);
+ } else if (typeof Module !== 'undefined' && typeof render !== 'undefined') {
+ this.wrapper = new ModuleWrapper(Module, render);
+ } else if (typeof Viz.Module !== 'undefined' && typeof Viz.render !== 'undefined') {
+ this.wrapper = new ModuleWrapper(Viz.Module, Viz.render);
+ } else {
+ throw new Error('Must specify workerURL or worker option, Module and render options, or include one of full.render.js or lite.render.js after viz.js.');
+ }
+ }
+
+ createClass(Viz, [{
+ key: 'renderString',
+ value: function renderString(src) {
+ var _ref4 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
+ _ref4$format = _ref4.format,
+ format = _ref4$format === undefined ? 'svg' : _ref4$format,
+ _ref4$engine = _ref4.engine,
+ engine = _ref4$engine === undefined ? 'dot' : _ref4$engine,
+ _ref4$files = _ref4.files,
+ files = _ref4$files === undefined ? [] : _ref4$files,
+ _ref4$images = _ref4.images,
+ images = _ref4$images === undefined ? [] : _ref4$images,
+ _ref4$yInvert = _ref4.yInvert,
+ yInvert = _ref4$yInvert === undefined ? false : _ref4$yInvert,
+ _ref4$nop = _ref4.nop,
+ nop = _ref4$nop === undefined ? 0 : _ref4$nop;
+
+ for (var i = 0; i < images.length; i++) {
+ files.push({
+ path: images[i].path,
+ data: '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n<svg width="' + images[i].width + '" height="' + images[i].height + '"></svg>'
+ });
+ }
+
+ 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] : {};
+ var format = options.format;
+
+
+ if (format !== 'json' || format !== 'json0') {
+ format = 'json';
+ }
+
+ return this.renderString(src, _extends({}, options, { format: format })).then(function (str) {
+ return JSON.parse(str);
+ });
+ }
+ }]);
+ return Viz;
+}();
+
+export default Viz;