diff options
| author | s-ol <s-ol@users.noreply.github.com> | 2021-01-26 17:15:52 +0000 |
|---|---|---|
| committer | s-ol <s-ol@users.noreply.github.com> | 2021-01-26 17:56:21 +0000 |
| commit | 84a2b2f01064375dee2164a8e676d90203b9296b (patch) | |
| tree | 50285380a0ca150dfa8301e0001ee0c33a82a32c /src | |
| parent | add README (diff) | |
| download | fedidag-84a2b2f01064375dee2164a8e676d90203b9296b.tar.gz fedidag-84a2b2f01064375dee2164a8e676d90203b9296b.zip | |
graphviz-ify
Diffstat (limited to 'src')
| -rw-r--r-- | src/index.js | 70 | ||||
| -rw-r--r-- | src/layout.js | 116 | ||||
| -rw-r--r-- | src/viz.es.js | 325 |
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; |
