From 84a2b2f01064375dee2164a8e676d90203b9296b Mon Sep 17 00:00:00 2001 From: s-ol Date: Tue, 26 Jan 2021 18:15:52 +0100 Subject: graphviz-ify --- src/index.js | 70 +++++-------- src/layout.js | 116 +++++++++++++++++++++ src/viz.es.js | 325 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 464 insertions(+), 47 deletions(-) create mode 100644 src/layout.js create mode 100644 src/viz.es.js (limited to 'src') 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 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: '\n\n' + }); + } + + 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; -- cgit v1.2.3