From 368013f36a858a7a9f88ce999e8f1a3f04c49be1 Mon Sep 17 00:00:00 2001 From: Ivan Shubin Date: Fri, 24 Jan 2025 20:30:06 +0100 Subject: [PATCH] added decoding of sankey diagram from textarea --- assets/css/main.css | 7 + assets/templates/diagrams/sankey.json | 2 +- assets/templates/diagrams/sankey.yaml | 1 + assets/templates/diagrams/src/sankey.sch | 58 ++++- src/ui/components/editor/ArgumentsEditor.vue | 229 ++++++++++-------- .../editor/properties/TemplateProperties.vue | 10 +- src/ui/delayer.js | 21 +- 7 files changed, 214 insertions(+), 114 deletions(-) diff --git a/assets/css/main.css b/assets/css/main.css index 5ce9eb70a..07f9cbc3c 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -2141,6 +2141,13 @@ ul.button-group.disabled > li, ul.button-group > li.disabled { .properties-table .property-arg-binder { max-width: 44px; } +.properties-table textarea.property-textarea { + margin-top: 5px; + margin-bottom: 5px; + padding: 4px; + border-radius: 3px; + width: 100%; +} .property-arg-binder-icon { opacity: 0.6; } diff --git a/assets/templates/diagrams/sankey.json b/assets/templates/diagrams/sankey.json index d7440ae08..07990cc2c 100644 --- a/assets/templates/diagrams/sankey.json +++ b/assets/templates/diagrams/sankey.json @@ -1 +1 @@ -{"name": "Sankey diagram", "description": "", "args": {"nodes": {"type": "string", "value": "a1_1;x=0;y=0;|a1_2|a2", "name": "Nodes encoded", "hidden": true}, "connections": {"type": "string", "value": "a1_1-a2;s=a1_1;d=a2;v=100|a1_2-a2;s=a1_2;d=a2;v=30", "name": "Connections encoded", "hidden": true}}, "preview": "/assets/templates/previews/mind-map.svg", "defaultArea": {"x": 0, "y": 0, "w": 200, "h": 60}, "import": ["./src/item.sch", "./src/control.sch", "./src/sankey.sch"], "item": {"id": "root", "name": "Sankey diagram", "shape": "rect", "shapeProps": {"fill": {"type": "none"}, "strokeColor": "rgba(200,200,200,1)"}, "locked": false, "area": {"x": 0, "y": 0, "w": {"$-expr": "width"}, "h": {"$-expr": "height"}}, "childItems": [{"$-foreach": {"source": "nodeItems", "it": "it"}, "id": {"$-expr": "it.id"}, "name": {"$-expr": "it.name"}, "shape": {"$-expr": "it.shape"}, "shapeProps": {"$-expr": "toJSON(it.shapeProps)"}, "args": {"$-expr": "it.getArgs()"}, "locked": {"$-expr": "it.locked"}, "textSlots": {"$-expr": "toJSON(it.textSlots)"}, "area": {"x": {"$-expr": "it.x"}, "y": {"$-expr": "it.y"}, "w": {"$-expr": "it.w"}, "h": {"$-expr": "it.h"}}}, {"$-foreach": {"source": "connectorItems", "it": "it"}, "id": {"$-expr": "it.id"}, "name": {"$-expr": "it.name"}, "shape": {"$-expr": "it.shape"}, "shapeProps": {"$-expr": "toJSON(it.shapeProps)"}, "args": {"$-expr": "it.getArgs()"}, "locked": {"$-expr": "it.locked"}, "textSlots": {"$-expr": "toJSON(it.textSlots)"}, "area": {"x": {"$-expr": "it.x"}, "y": {"$-expr": "it.y"}, "w": {"$-expr": "it.w"}, "h": {"$-expr": "it.h"}}}]}, "init": "\nstruct Item {\n id: uid()\n name: ''\n shape: 'rect'\n x: 0\n y: 0\n w: 100\n h: 50\n shapeProps: Map()\n childItems: List()\n args: Map()\n locked: true\n textSlots: Map()\n\n\n traverse(callback) {\n this.childItems.forEach((childItem) => {\n childItem.traverse(callback)\n })\n callback(this)\n }\n\n getArgs() {\n toJSON(this.args)\n }\n\n setText(slotName, text) {\n if (!this.textSlots.has(slotName)) {\n this.textSlots.set(slotName, Map('text', text))\n } else {\n this.textSlots.get(slotName).set('text', text)\n }\n }\n\n toJSON() {\n childItems = this.childItems.map((childItem) => { childItem.toJSON() })\n result = toJSON(Map(\n 'id', this.id,\n 'childItems', childItems,\n 'name', this.name,\n 'shape', this.shape,\n 'area', Map('x', this.x, 'y', this.y, 'w', this.w, 'h', this.h, 'r', 0, 'sx', 1, 'sy', 1, 'px', 0.5, 'py', 0.5),\n 'shapeProps', this.shapeProps,\n 'args', this.args,\n 'locked', this.locked,\n 'textSlots', this.textSlots,\n ))\n\n result\n }\n}\n\n\n\nstruct Control {\n name: \"\"\n data: Map()\n click: \"log('control clicked')\"\n x: 0\n y: 0\n width: 20\n height: 20\n text: \"+\"\n placement: \"TL\"\n selectedItemId: \"\"\n type: 'button'\n}\n\ngapRatio = 0.40\nnodeWidth = 20\n\nstruct Node {\n id: uid()\n name: 'Unnamed'\n value: 0\n inValue: 0\n outValue: 0\n x: 0\n y: 0\n level: 0\n srcNodes: List()\n dstNodes: List()\n width: 0\n height: 0\n position: 0\n offset: 0\n unitSize: 1\n reservedIn: 0\n reservedOut: 0\n}\n\nfunc encodeNodes(nodes) {\n local result = ''\n\n nodes.forEach((n, i) => {\n if (i > 0) {\n result += '|'\n }\n result += `${n.id};v=${n.value};x=${n.x};y=${n.y}`\n })\n result\n}\n\nfunc decodeListOfObjects(text, constructorCallback, paramSetterCallback) {\n local nodes = List()\n splitString(text, '|').forEach(singleNodeText => {\n local node = constructorCallback()\n local parts = splitString(singleNodeText, ';')\n for (local i = 0; i < parts.size; i++) {\n if (i == 0) {\n node.id = parts.get(0)\n } else {\n local varValue = splitString(parts.get(i), '=')\n if (varValue.size == 2) {\n local name = varValue.get(0)\n local value = varValue.get(1)\n paramSetterCallback(node, name, value)\n }\n }\n }\n nodes.add(node)\n })\n nodes\n}\n\nfunc decodeNodes(text) {\n decodeListOfObjects(text, () => {\n Node()\n }, (node, name, value) => {\n if (name == 'x') {\n node.x = value\n } else if (name == 'y') {\n node.y = value\n }\n })\n}\n\n\nstruct Connection {\n id: uid()\n srcId: ''\n dstId: ''\n value: 0\n}\n\nfunc encodeConnections(connections) {\n local result = ''\n\n connections.forEach((c, i) => {\n if (i > 0) {\n result += '|'\n }\n result += `${c.id};s=${c.srcId};d=${c.dstId};v=${c.value}`\n })\n result\n}\n\nfunc decodeConnections(text) {\n decodeListOfObjects(text, () => {\n Connection()\n }, (node, name, value) => {\n if (name == 'v') {\n node.value = value\n } else if (name == 's') {\n node.srcId = value\n } else if (name == 'd') {\n node.dstId = value\n }\n })\n}\n\n// Performs a recursive tree iteration and updates the levels in nodes\n// maxVisitCount is used in order to prevent from infinite loop in case there is a cyclic dependency\nfunc updateLevels(node, maxVisitCount) {\n if (maxVisitCount >= 0) {\n node.dstNodes.forEach(dstNode => {\n local newLevel = node.level + 1\n if (dstNode.level < newLevel) {\n dstNode.level = newLevel\n updateLevels(dstNode, maxVisitCount - 1)\n }\n dstNode.level = node.level + 1\n })\n }\n}\n\nstruct Level {\n idx: 0\n nodes: List()\n nodesMap: Map()\n totalValue: 0\n height: 0\n}\n\nfunc buildLevels(allNodes, allConnections) {\n local nodesMap = Map()\n allNodes.forEach(node => {\n nodesMap.set(node.id, node)\n })\n\n allConnections.forEach(c => {\n if (c.srcId != c.dstId) {\n local srcNode = nodesMap.get(c.srcId)\n if (!srcNode) {\n srcNode = Node(c.srcId)\n nodesMap.set(c.srcId, srcNode)\n }\n local dstNode = nodesMap.get(c.dstId)\n if (!dstNode) {\n dstNode = Node(c.dstId)\n nodesMap.set(c.dstId, dstNode)\n }\n\n local value = abs(c.value)\n srcNode.outValue += value\n dstNode.inValue += value\n srcNode.dstNodes.add(dstNode)\n dstNode.srcNodes.add(srcNode)\n }\n })\n\n nodesMap.forEach(node => {\n if (node.srcNodes.size == 0) {\n node.level = 0\n updateLevels(node, nodesMap.size)\n }\n })\n\n local levels = Map()\n local maxLevel = 0\n nodesMap.forEach(node => {\n node.value = max(node.inValue, node.outValue)\n local level = levels.get(node.level)\n if (level) {\n level.nodes.add(node)\n } else {\n levels.set(node.level, Level(node.level, List(node), nodesMap))\n }\n if (maxLevel < node.level) {\n maxLevel = node.level\n }\n })\n\n local allLevels = List()\n for (local i = 0; i <= maxLevel; i++) {\n local level = levels.get(i)\n if (level) {\n level.nodes.sort((a, b) => {\n b.value - a.value\n })\n allLevels.add(level)\n level.totalValue = 0\n level.nodes.forEach(node => {\n level.totalValue += node.value\n })\n }\n }\n\n allLevels\n}\n\n\nfunc buildNodeItems(levels) {\n local maxLevelValue = 0\n local maxNodesPerLevel = 0\n levels.forEach(level => {\n if (maxLevelValue < level.totalValue) {\n maxLevelValue = level.totalValue\n }\n if (maxNodesPerLevel < level.nodes.size) {\n maxNodesPerLevel = level.nodes.size\n }\n })\n\n local nodeItems = List()\n\n if (maxLevelValue > 0 && maxNodesPerLevel > 0) {\n local unitSize = height * (1 - gapRatio) / maxLevelValue\n local singleGap = height * gapRatio / maxNodesPerLevel\n\n levels.forEach(level => {\n local levelPosition = level.idx * max(1, width - nodeWidth) / (levels.size - 1)\n local levelSize = level.totalValue * unitSize + singleGap * (level.nodes.size - 1)\n local levelOffset = height / 2 - levelSize / 2\n\n local currentY = levelOffset\n level.nodes.forEach(node => {\n node.unitSize = unitSize\n node.height = unitSize * node.value\n node.width = nodeWidth\n node.position = levelPosition\n node.offset = currentY\n\n local nodeItem = Item(node.id, node.name, 'rect')\n nodeItem.w = node.width\n nodeItem.h = node.height\n nodeItem.x = node.position\n nodeItem.y = node.offset\n nodeItems.add(nodeItem)\n\n currentY += nodeItem.h + singleGap\n })\n })\n }\n nodeItems\n}\n\nfunc buildConnectorItems(levels, allConnections) {\n local connectorItems = List()\n local connectionsBySource = Map()\n allConnections.forEach(c => {\n if (c.srcId != c.dstId) {\n local cs = connectionsBySource.get(c.srcId)\n if (cs) {\n cs.add(c)\n } else {\n connectionsBySource.set(c.srcId, List(c))\n }\n }\n })\n\n levels.forEach(level => {\n level.nodes.forEach(node => {\n local connections = connectionsBySource.get(node.id)\n if (connections) {\n connections.forEach(c => {\n local dstNode = level.nodesMap.get(c.dstId)\n if (dstNode) {\n connectorItems.add(buildSingleConnectorItem(c, node, dstNode))\n }\n })\n }\n })\n })\n\n connectorItems\n}\n\nstruct PathPoint {\n t: 'B'\n x: 0\n y: 0\n x1: 0\n y1: 0\n x2: 0\n y2: 0\n}\n\nfunc buildSingleConnectorItem(connector, srcNode, dstNode) {\n local item = Item(connector.id, 'Connection', 'path')\n local connectorSize = srcNode.unitSize * connector.value\n local xs = srcNode.position + srcNode.width\n local ys1 = srcNode.offset + srcNode.reservedOut\n local ys2 = ys1 + connectorSize\n srcNode.reservedOut += connectorSize\n\n local xd = dstNode.position\n local yd1 = dstNode.offset + dstNode.reservedIn\n local yd2 = yd1 + connectorSize\n dstNode.reservedIn += connectorSize\n\n local minX = min(xs, xd)\n local maxX = max(xs, xd)\n local minY = min(ys1, ys2, yd1, yd2)\n local maxY = max(ys1, ys2, yd1, yd2)\n\n local dx = max(0.001, maxX - minX)\n local dy = max(0.001, maxY - minY)\n\n // local rawPoints = List(\n // Vector(xs, ys1),\n // Vector(xd, yd1),\n // Vector(xd, yd2),\n // Vector(xs, ys2),\n // )\n\n // local points = rawPoints.map(p => {\n // PathPoint('L', 100 * (p.x - minX) / dx, 100 * (p.y - minY) / dy)\n // })\n\n local points = List(\n PathPoint('B', xs, ys1, 0, (ys2 - ys1) / 3, (xd - xs) / 3, 0),\n PathPoint('B', xd, yd1, (xs - xd) / 3, 0, 0, (yd2 - yd1) / 3),\n PathPoint('B', xd, yd2, 0, (yd1 - yd2) / 3, (xs - xd) / 3, 0),\n PathPoint('B', xs, ys2, (xd - xs) / 3, 0, 0, (ys1 - ys2) / 3),\n ).map(p => {\n PathPoint('B',\n 100 * (p.x - minX) / dx, 100 * (p.y - minY) / dy,\n 100 * p.x1 / dx, 100 * p.y1 / dy,\n 100 * p.x2 / dx, 100 * p.y2 / dy\n )\n })\n\n item.x = minX\n item.y = minY\n item.w = dx\n item.h = dy\n\n item.shapeProps.set('paths', List(Map(\n 'id', 'p-' + connector.id,\n 'closed', true,\n 'pos', 'relative',\n 'points', points\n )))\n item\n}\n\nallNodes = decodeNodes(nodes)\nallConnections = decodeConnections(connections)\n\nlocal levels = buildLevels(allNodes, allConnections)\n\nnodeItems = buildNodeItems(levels)\nconnectorItems = buildConnectorItems(levels, allConnections)\n\n\n"} \ No newline at end of file +{"name": "Sankey diagram", "description": "", "args": {"nodes": {"type": "string", "value": "a1_1;x=0;y=0;|a1_2|a2", "name": "Nodes encoded", "hidden": true}, "connections": {"type": "string", "value": "a1_1-a2;s=a1_1;d=a2;v=100|a1_2-a2;s=a1_2;d=a2;v=30", "name": "Connections encoded", "hidden": true}, "diagramCode": {"type": "string", "value": "Wages [2000] Budget\n //Comment\n\nOther [120] Budget\nInvalid line\n", "name": "Diagram", "textarea": true, "rows": 15}}, "preview": "/assets/templates/previews/mind-map.svg", "defaultArea": {"x": 0, "y": 0, "w": 200, "h": 60}, "import": ["./src/item.sch", "./src/control.sch", "./src/sankey.sch"], "item": {"id": "root", "name": "Sankey diagram", "shape": "rect", "shapeProps": {"fill": {"type": "none"}, "strokeColor": "rgba(200,200,200,1)"}, "locked": false, "area": {"x": 0, "y": 0, "w": {"$-expr": "width"}, "h": {"$-expr": "height"}}, "childItems": [{"$-foreach": {"source": "nodeItems", "it": "it"}, "id": {"$-expr": "it.id"}, "name": {"$-expr": "it.name"}, "shape": {"$-expr": "it.shape"}, "shapeProps": {"$-expr": "toJSON(it.shapeProps)"}, "args": {"$-expr": "it.getArgs()"}, "locked": {"$-expr": "it.locked"}, "textSlots": {"$-expr": "toJSON(it.textSlots)"}, "area": {"x": {"$-expr": "it.x"}, "y": {"$-expr": "it.y"}, "w": {"$-expr": "it.w"}, "h": {"$-expr": "it.h"}}}, {"$-foreach": {"source": "connectorItems", "it": "it"}, "id": {"$-expr": "it.id"}, "name": {"$-expr": "it.name"}, "shape": {"$-expr": "it.shape"}, "shapeProps": {"$-expr": "toJSON(it.shapeProps)"}, "args": {"$-expr": "it.getArgs()"}, "locked": {"$-expr": "it.locked"}, "textSlots": {"$-expr": "toJSON(it.textSlots)"}, "area": {"x": {"$-expr": "it.x"}, "y": {"$-expr": "it.y"}, "w": {"$-expr": "it.w"}, "h": {"$-expr": "it.h"}}}]}, "init": "\nstruct Item {\n id: uid()\n name: ''\n shape: 'rect'\n x: 0\n y: 0\n w: 100\n h: 50\n shapeProps: Map()\n childItems: List()\n args: Map()\n locked: true\n textSlots: Map()\n\n\n traverse(callback) {\n this.childItems.forEach((childItem) => {\n childItem.traverse(callback)\n })\n callback(this)\n }\n\n getArgs() {\n toJSON(this.args)\n }\n\n setText(slotName, text) {\n if (!this.textSlots.has(slotName)) {\n this.textSlots.set(slotName, Map('text', text))\n } else {\n this.textSlots.get(slotName).set('text', text)\n }\n }\n\n toJSON() {\n childItems = this.childItems.map((childItem) => { childItem.toJSON() })\n result = toJSON(Map(\n 'id', this.id,\n 'childItems', childItems,\n 'name', this.name,\n 'shape', this.shape,\n 'area', Map('x', this.x, 'y', this.y, 'w', this.w, 'h', this.h, 'r', 0, 'sx', 1, 'sy', 1, 'px', 0.5, 'py', 0.5),\n 'shapeProps', this.shapeProps,\n 'args', this.args,\n 'locked', this.locked,\n 'textSlots', this.textSlots,\n ))\n\n result\n }\n}\n\n\n\nstruct Control {\n name: \"\"\n data: Map()\n click: \"log('control clicked')\"\n x: 0\n y: 0\n width: 20\n height: 20\n text: \"+\"\n placement: \"TL\"\n selectedItemId: \"\"\n type: 'button'\n}\n\ngapRatio = 0.40\nnodeWidth = 20\n\nstruct Node {\n id: uid()\n name: 'Unnamed'\n value: 0\n inValue: 0\n outValue: 0\n x: 0\n y: 0\n level: 0\n srcNodes: List()\n dstNodes: List()\n width: 0\n height: 0\n position: 0\n offset: 0\n unitSize: 1\n reservedIn: 0\n reservedOut: 0\n}\n\nfunc encodeNodes(nodes) {\n local result = ''\n\n nodes.forEach((n, i) => {\n if (i > 0) {\n result += '|'\n }\n result += `${n.id};v=${n.value};x=${n.x};y=${n.y}`\n })\n result\n}\n\nfunc decodeListOfObjects(text, constructorCallback, paramSetterCallback) {\n local nodes = List()\n splitString(text, '|').forEach(singleNodeText => {\n local node = constructorCallback()\n local parts = splitString(singleNodeText, ';')\n for (local i = 0; i < parts.size; i++) {\n if (i == 0) {\n node.id = parts.get(0)\n } else {\n local varValue = splitString(parts.get(i), '=')\n if (varValue.size == 2) {\n local name = varValue.get(0)\n local value = varValue.get(1)\n paramSetterCallback(node, name, value)\n }\n }\n }\n nodes.add(node)\n })\n nodes\n}\n\nfunc decodeNodes(text) {\n decodeListOfObjects(text, () => {\n Node()\n }, (node, name, value) => {\n if (name == 'x') {\n node.x = value\n } else if (name == 'y') {\n node.y = value\n }\n })\n}\n\n\nstruct Connection {\n id: uid()\n srcId: ''\n dstId: ''\n value: 0\n}\n\nfunc encodeConnections(connections) {\n local result = ''\n\n connections.forEach((c, i) => {\n if (i > 0) {\n result += '|'\n }\n result += `${c.id};s=${c.srcId};d=${c.dstId};v=${c.value}`\n })\n result\n}\n\nfunc decodeConnections(text) {\n decodeListOfObjects(text, () => {\n Connection()\n }, (node, name, value) => {\n if (name == 'v') {\n node.value = value\n } else if (name == 's') {\n node.srcId = value\n } else if (name == 'd') {\n node.dstId = value\n }\n })\n}\n\n\nfunc parseConnection(line) {\n local s1 = line.indexOf('[')\n local s2 = line.indexOf(']')\n\n if (s1 > 0 && s2 > s1) {\n local nodeName1 = line.substring(0, s1).trim()\n local nodeName2 = line.substring(s2+1).trim()\n local valueText = line.substring(s1+1, s2)\n\n if (nodeName1 != '' && nodeName2 != '') {\n local value = parseFloat(valueText)\n local id = nodeName1 + '[]' + nodeName2\n Connection(id, nodeName1, nodeName2, value)\n } else {\n null\n }\n } else {\n null\n }\n}\n\nfunc parseConnections(text) {\n local connections = List()\n splitString(text, '\\n').forEach(line => {\n line = line.trim()\n if (line != '' && !line.startsWith('//')) {\n local c = parseConnection(line)\n if (c) {\n connections.add(c)\n }\n }\n })\n connections\n}\n\nfunc extractNodesFromConnections(connections) {\n local nodeIds = Set()\n\n connections.forEach(c => {\n nodeIds.add(c.srcId)\n nodeIds.add(c.dstId)\n })\n\n local list = List()\n\n nodeIds.forEach(id => {\n list.add(Node(id, id))\n })\n list\n}\n\n\n\n// Performs a recursive tree iteration and updates the levels in nodes\n// maxVisitCount is used in order to prevent from infinite loop in case there is a cyclic dependency\nfunc updateLevels(node, maxVisitCount) {\n if (maxVisitCount >= 0) {\n node.dstNodes.forEach(dstNode => {\n local newLevel = node.level + 1\n if (dstNode.level < newLevel) {\n dstNode.level = newLevel\n updateLevels(dstNode, maxVisitCount - 1)\n }\n dstNode.level = node.level + 1\n })\n }\n}\n\nstruct Level {\n idx: 0\n nodes: List()\n nodesMap: Map()\n totalValue: 0\n height: 0\n}\n\nfunc buildLevels(allNodes, allConnections) {\n local nodesMap = Map()\n allNodes.forEach(node => {\n nodesMap.set(node.id, node)\n })\n\n allConnections.forEach(c => {\n if (c.srcId != c.dstId) {\n local srcNode = nodesMap.get(c.srcId)\n if (!srcNode) {\n srcNode = Node(c.srcId)\n nodesMap.set(c.srcId, srcNode)\n }\n local dstNode = nodesMap.get(c.dstId)\n if (!dstNode) {\n dstNode = Node(c.dstId)\n nodesMap.set(c.dstId, dstNode)\n }\n\n local value = abs(c.value)\n srcNode.outValue += value\n dstNode.inValue += value\n srcNode.dstNodes.add(dstNode)\n dstNode.srcNodes.add(srcNode)\n }\n })\n\n nodesMap.forEach(node => {\n if (node.srcNodes.size == 0) {\n node.level = 0\n updateLevels(node, nodesMap.size)\n }\n })\n\n local levels = Map()\n local maxLevel = 0\n nodesMap.forEach(node => {\n node.value = max(node.inValue, node.outValue)\n local level = levels.get(node.level)\n if (level) {\n level.nodes.add(node)\n } else {\n levels.set(node.level, Level(node.level, List(node), nodesMap))\n }\n if (maxLevel < node.level) {\n maxLevel = node.level\n }\n })\n\n local allLevels = List()\n for (local i = 0; i <= maxLevel; i++) {\n local level = levels.get(i)\n if (level) {\n level.nodes.sort((a, b) => {\n b.value - a.value\n })\n allLevels.add(level)\n level.totalValue = 0\n level.nodes.forEach(node => {\n level.totalValue += node.value\n })\n }\n }\n\n allLevels\n}\n\n\nfunc buildNodeItems(levels) {\n local maxLevelValue = 0\n local maxNodesPerLevel = 0\n levels.forEach(level => {\n if (maxLevelValue < level.totalValue) {\n maxLevelValue = level.totalValue\n }\n if (maxNodesPerLevel < level.nodes.size) {\n maxNodesPerLevel = level.nodes.size\n }\n })\n\n local nodeItems = List()\n\n if (maxLevelValue > 0 && maxNodesPerLevel > 0) {\n local unitSize = height * (1 - gapRatio) / maxLevelValue\n local singleGap = height * gapRatio / maxNodesPerLevel\n\n levels.forEach(level => {\n local levelPosition = level.idx * max(1, width - nodeWidth) / (levels.size - 1)\n local levelSize = level.totalValue * unitSize + singleGap * (level.nodes.size - 1)\n local levelOffset = height / 2 - levelSize / 2\n\n local currentY = levelOffset\n level.nodes.forEach(node => {\n node.unitSize = unitSize\n node.height = unitSize * node.value\n node.width = nodeWidth\n node.position = levelPosition\n node.offset = currentY\n\n local nodeItem = Item(node.id, node.name, 'rect')\n nodeItem.w = node.width\n nodeItem.h = node.height\n nodeItem.x = node.position\n nodeItem.y = node.offset\n nodeItems.add(nodeItem)\n\n currentY += nodeItem.h + singleGap\n })\n })\n }\n nodeItems\n}\n\nfunc buildConnectorItems(levels, allConnections) {\n local connectorItems = List()\n local connectionsBySource = Map()\n allConnections.forEach(c => {\n if (c.srcId != c.dstId) {\n local cs = connectionsBySource.get(c.srcId)\n if (cs) {\n cs.add(c)\n } else {\n connectionsBySource.set(c.srcId, List(c))\n }\n }\n })\n\n levels.forEach(level => {\n level.nodes.forEach(node => {\n local connections = connectionsBySource.get(node.id)\n if (connections) {\n connections.forEach(c => {\n local dstNode = level.nodesMap.get(c.dstId)\n if (dstNode) {\n connectorItems.add(buildSingleConnectorItem(c, node, dstNode))\n }\n })\n }\n })\n })\n\n connectorItems\n}\n\nstruct PathPoint {\n t: 'B'\n x: 0\n y: 0\n x1: 0\n y1: 0\n x2: 0\n y2: 0\n}\n\nfunc buildSingleConnectorItem(connector, srcNode, dstNode) {\n local item = Item(connector.id, 'Connection', 'path')\n local connectorSize = srcNode.unitSize * connector.value\n local xs = srcNode.position + srcNode.width\n local ys1 = srcNode.offset + srcNode.reservedOut\n local ys2 = ys1 + connectorSize\n srcNode.reservedOut += connectorSize\n\n local xd = dstNode.position\n local yd1 = dstNode.offset + dstNode.reservedIn\n local yd2 = yd1 + connectorSize\n dstNode.reservedIn += connectorSize\n\n local minX = min(xs, xd)\n local maxX = max(xs, xd)\n local minY = min(ys1, ys2, yd1, yd2)\n local maxY = max(ys1, ys2, yd1, yd2)\n\n local dx = max(0.001, maxX - minX)\n local dy = max(0.001, maxY - minY)\n\n local points = List(\n PathPoint('B', xs, ys1, 0, (ys2 - ys1) / 3, (xd - xs) / 3, 0),\n PathPoint('B', xd, yd1, (xs - xd) / 3, 0, 0, (yd2 - yd1) / 3),\n PathPoint('B', xd, yd2, 0, (yd1 - yd2) / 3, (xs - xd) / 3, 0),\n PathPoint('B', xs, ys2, (xd - xs) / 3, 0, 0, (ys1 - ys2) / 3),\n ).map(p => {\n PathPoint('B',\n 100 * (p.x - minX) / dx, 100 * (p.y - minY) / dy,\n 100 * p.x1 / dx, 100 * p.y1 / dy,\n 100 * p.x2 / dx, 100 * p.y2 / dy\n )\n })\n\n item.x = minX\n item.y = minY\n item.w = dx\n item.h = dy\n\n item.shapeProps.set('paths', List(Map(\n 'id', 'p-' + connector.id,\n 'closed', true,\n 'pos', 'relative',\n 'points', points\n )))\n item\n}\n\n// allNodes = decodeNodes(nodes)\n// allConnections = decodeConnections(connections)\nallConnections = parseConnections(diagramCode)\nallNodes = extractNodesFromConnections(allConnections)\nlog('Parsed connections', allConnections)\n\nlocal levels = buildLevels(allNodes, allConnections)\n\nnodeItems = buildNodeItems(levels)\nconnectorItems = buildConnectorItems(levels, allConnections)\n\n\n"} \ No newline at end of file diff --git a/assets/templates/diagrams/sankey.yaml b/assets/templates/diagrams/sankey.yaml index e813debd3..5ccd3b894 100644 --- a/assets/templates/diagrams/sankey.yaml +++ b/assets/templates/diagrams/sankey.yaml @@ -3,6 +3,7 @@ description: "" args: nodes: {type: "string", value: "a1_1;x=0;y=0;|a1_2|a2", name: "Nodes encoded", hidden: true} connections: {type: "string", value: "a1_1-a2;s=a1_1;d=a2;v=100|a1_2-a2;s=a1_2;d=a2;v=30", name: "Connections encoded", hidden: true} + diagramCode: {type: "string", value: "Wages [2000] Budget\n //Comment\n\nOther [120] Budget\nInvalid line\n", name: "Diagram", textarea: true, rows: 15} preview: "/assets/templates/previews/mind-map.svg" defaultArea: {x: 0, y: 0, w: 200, h: 60} diff --git a/assets/templates/diagrams/src/sankey.sch b/assets/templates/diagrams/src/sankey.sch index ecdc367dc..a8c588077 100644 --- a/assets/templates/diagrams/src/sankey.sch +++ b/assets/templates/diagrams/src/sankey.sch @@ -101,6 +101,60 @@ func decodeConnections(text) { }) } + +func parseConnection(line) { + local s1 = line.indexOf('[') + local s2 = line.indexOf(']') + + if (s1 > 0 && s2 > s1) { + local nodeName1 = line.substring(0, s1).trim() + local nodeName2 = line.substring(s2+1).trim() + local valueText = line.substring(s1+1, s2) + + if (nodeName1 != '' && nodeName2 != '') { + local value = parseFloat(valueText) + local id = nodeName1 + '[]' + nodeName2 + Connection(id, nodeName1, nodeName2, value) + } else { + null + } + } else { + null + } +} + +func parseConnections(text) { + local connections = List() + splitString(text, '\n').forEach(line => { + line = line.trim() + if (line != '' && !line.startsWith('//')) { + local c = parseConnection(line) + if (c) { + connections.add(c) + } + } + }) + connections +} + +func extractNodesFromConnections(connections) { + local nodeIds = Set() + + connections.forEach(c => { + nodeIds.add(c.srcId) + nodeIds.add(c.dstId) + }) + + local list = List() + + nodeIds.forEach(id => { + list.add(Node(id, id)) + }) + list +} + + + // Performs a recursive tree iteration and updates the levels in nodes // maxVisitCount is used in order to prevent from infinite loop in case there is a cyclic dependency func updateLevels(node, maxVisitCount) { @@ -326,8 +380,8 @@ func buildSingleConnectorItem(connector, srcNode, dstNode) { item } -allNodes = decodeNodes(nodes) -allConnections = decodeConnections(connections) +allConnections = parseConnections(diagramCode) +allNodes = extractNodesFromConnections(allConnections) local levels = buildLevels(allNodes, allConnections) diff --git a/src/ui/components/editor/ArgumentsEditor.vue b/src/ui/components/editor/ArgumentsEditor.vue index f5ddf6797..464c02c21 100644 --- a/src/ui/components/editor/ArgumentsEditor.vue +++ b/src/ui/components/editor/ArgumentsEditor.vue @@ -4,104 +4,128 @@ @@ -175,6 +199,15 @@ export default { }, methods: { + isRegularArg(arg) { + if (arg.type === 'script') { + return false; + } else if (arg.type === 'string' && arg.textarea) { + return false; + } + return true; + }, + buildArgumentBindOptions(argName) { const options = []; if (this.argBinds && this.argBinds.hasOwnProperty(argName)) { diff --git a/src/ui/components/editor/properties/TemplateProperties.vue b/src/ui/components/editor/properties/TemplateProperties.vue index dcf85d52b..26d939ff6 100644 --- a/src/ui/components/editor/properties/TemplateProperties.vue +++ b/src/ui/components/editor/properties/TemplateProperties.vue @@ -59,6 +59,7 @@ import {forEach, forEachObject} from '../../../collections'; import EditorEventBus from '../EditorEventBus'; import ItemSvg from '../items/ItemSvg.vue'; import Panel from '../Panel.vue'; +import { createDelayer } from '../../../delayer'; export default { props: { @@ -76,6 +77,7 @@ export default { }, beforeDestroy() { + this.updateDelayer.destroy(); EditorEventBus.item.templateArgsUpdated.specific.$off(this.editorId, this.item.id, this.updateTemplateArgs); }, @@ -87,6 +89,10 @@ export default { templateNotFound: false, template: null, editorPanels: [], + lastChangedArgName: null, + updateDelayer: createDelayer(200, () => { + this.$emit('updated', this.item.id, this.template, this.args, this.lastChangedArgName); + }), args: this.item.args && this.item.args.templateArgs ? this.item.args.templateArgs : {} } }, @@ -161,8 +167,8 @@ export default { }); } this.args[name] = value; - this.$emit('updated', this.item.id, this.template, this.args, name); - + this.lastChangedArgName = name; + this.updateDelayer.trigger(); this.updateEditorPanels(); this.$forceUpdate(); }, diff --git a/src/ui/delayer.js b/src/ui/delayer.js index 225b7a4fd..18d99b9ca 100644 --- a/src/ui/delayer.js +++ b/src/ui/delayer.js @@ -3,24 +3,23 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ export function createDelayer(timeoutInMillis, callback) { - return { - timerId: null, - timeoutInMillis, + let timerId = null; + return { trigger() { - if (this.timerId) { - clearTimeout(this.timerId); + if (timerId) { + clearTimeout(timerId); } - this.timerId = setTimeout(() => { - this.timerId = null; + timerId = setTimeout(() => { + timerId = null; callback(); - }, this.timeoutInMillis); + }, timeoutInMillis); }, destroy() { - if (this.timerId) { - clearTimeout(this.timerId); - this.timerId = null; + if (timerId) { + clearTimeout(timerId); + timerId = null; } } };