Skip to content

Commit

Permalink
added dragging of nodes in sankey diagram
Browse files Browse the repository at this point in the history
  • Loading branch information
ishubin committed Jan 28, 2025
1 parent 20bf06d commit 9b03fea
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 68 deletions.
2 changes: 1 addition & 1 deletion assets/templates/diagrams/sankey.json

Large diffs are not rendered by default.

18 changes: 16 additions & 2 deletions assets/templates/diagrams/sankey.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
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}
nodesData: {type: "string", value: "a1_1;x=0;y=0;|a1_2|a2", name: "Nodes encoded", hidden: true}
diagramCode: {type: "string", value: "Wages [2000] Budget\nOther [120] Budget\nBudget [1000] Housing\nBudget [450] Taxes\n", name: "Diagram", textarea: true, rows: 15}
font: {type: font, value: Arial, name: Font}
colorTheme: {type: "choice", value: "default", options: ["default", "light", "dark"], name: "Color theme"}
Expand All @@ -11,6 +10,7 @@ args:
fontSize: {type: number, value: 14, name: "Font size", min: 1}
labelColor: {type: color, value: '#222222', name: 'Label color'}
magnify: {type: number, value: 0, name: "Magnify value", min: -50, max: 50}
connectorOpacity: {type: number, value: 60, name: "Connector opacity", min: 0, max: 100}

preview: "/assets/templates/previews/mind-map.svg"
defaultArea: {x: 0, y: 0, w: 200, h: 60}
Expand All @@ -21,6 +21,9 @@ import:
- ./src/sankey.sch


handlers:
area: onAreaUpdate(itemId, item, area)

item:
id: root
name: Sankey diagram
Expand All @@ -38,12 +41,14 @@ item:
childItems:
- $-foreach: {source: "connectorItems", it: "it"}
id: {$-expr: "it.id"}
tags: ['sankey-connector']
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)"}
opacity: {$-expr: connectorOpacity}
area:
x: {$-expr: "it.x"}
y: {$-expr: "it.y"}
Expand All @@ -52,6 +57,7 @@ item:

- $-foreach: {source: "nodeItems", it: "it"}
id: {$-expr: "it.id"}
tags: ['sankey-node']
name: {$-expr: "it.name"}
shape: {$-expr: "it.shape"}
shapeProps: {$-expr: "toJSON(it.shapeProps)"}
Expand All @@ -63,6 +69,13 @@ item:
y: {$-expr: "it.y"}
w: {$-expr: "it.w"}
h: {$-expr: "it.h"}
# behavior:
# events:
# - id: click
# event: clicked
# actions:
# - id: a1


- $-foreach: {source: "nodeLabels", it: "it"}
id: {$-expr: "it.id"}
Expand All @@ -79,3 +92,4 @@ item:
h: {$-expr: "it.h"}



132 changes: 67 additions & 65 deletions assets/templates/diagrams/src/sankey.sch
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
gapRatio = 0.40
gapRatio = 0.4
nodeWidth = 20
labelPadding = 5

labelFontSize = max(1, round(fontSize * (100 - magnify) / 100))
valueFontSize = max(1, round(fontSize * (100 + magnify) / 100))

local nodesById = Map()

colorThemes = Map(
'default', List('#F16161', '#F1A261', '#F1EB61', '#71EB57', '#57EBB1', '#57C2EB', '#576BEB', '#A557EB', '#EB57C8', '#EB578E'),
Expand Down Expand Up @@ -37,19 +38,19 @@ struct Node {
func encodeNodes(nodes) {
local result = ''

nodes.forEach((n, i) => {
if (i > 0) {
nodes.forEach(n => {
if (result != '') {
result += '|'
}
result += `${n.id};v=${n.value};x=${n.x};y=${n.y}`
result += `${n.id};x=${n.x};y=${n.y}`
})
result
}

func decodeListOfObjects(text, constructorCallback, paramSetterCallback) {
local nodes = List()
func decodeNodesData(text) {
local nodesById = Map()
splitString(text, '|').forEach(singleNodeText => {
local node = constructorCallback()
local node = Node()
local parts = splitString(singleNodeText, ';')
for (local i = 0; i < parts.size; i++) {
if (i == 0) {
Expand All @@ -59,25 +60,17 @@ func decodeListOfObjects(text, constructorCallback, paramSetterCallback) {
if (varValue.size == 2) {
local name = varValue.get(0)
local value = varValue.get(1)
paramSetterCallback(node, name, value)
if (name == 'x') {
node.x = value
} else if (name == 'y') {
node.y = value
}
}
}
}
nodes.add(node)
})
nodes
}

func decodeNodes(text) {
decodeListOfObjects(text, () => {
Node()
}, (node, name, value) => {
if (name == 'x') {
node.x = value
} else if (name == 'y') {
node.y = value
}
nodesById.set(node.id, node)
})
nodesById
}


Expand All @@ -90,32 +83,6 @@ struct Connection {
dstNode: null
}

func encodeConnections(connections) {
local result = ''

connections.forEach((c, i) => {
if (i > 0) {
result += '|'
}
result += `${c.id};s=${c.srcId};d=${c.dstId};v=${c.value}`
})
result
}

func decodeConnections(text) {
decodeListOfObjects(text, () => {
Connection()
}, (node, name, value) => {
if (name == 'v') {
node.value = value
} else if (name == 's') {
node.srcId = value
} else if (name == 'd') {
node.dstId = value
}
})
}


func parseConnection(line) {
local s1 = line.indexOf('[')
Expand All @@ -138,15 +105,18 @@ func parseConnection(line) {
}
}

func parseConnections(text) {
local nodesById = Map()

func parseConnections(text, nodesData) {
getOrCreateNode = (id) => {
local node = nodesById.get(id)
if (!node) {
node = Node(id, id)
nodesById.set(id, node)
}
local nData = nodesData.get(id)
if (nData) {
node.x = nData.x
node.y = nData.y
}
node
}

Expand All @@ -155,11 +125,12 @@ func parseConnections(text) {
line = line.trim()
if (line != '' && !line.startsWith('//')) {
local c = parseConnection(line)

c.srcNode = getOrCreateNode(c.srcId)
c.dstNode = getOrCreateNode(c.dstId)
if (c) {
connections.add(c)
c.srcNode = getOrCreateNode(c.srcId)
c.dstNode = getOrCreateNode(c.dstId)
if (c) {
connections.add(c)
}
}
}
})
Expand Down Expand Up @@ -196,7 +167,6 @@ func updateLevels(node, maxVisitCount) {
dstNode.level = newLevel
updateLevels(dstNode, maxVisitCount - 1)
}
dstNode.level = node.level + 1
})
}
}
Expand Down Expand Up @@ -322,12 +292,16 @@ func buildNodeItems(levels) {
local nodeItem = Item('n-' + node.id, node.name, 'rect')
nodeItem.w = node.width
nodeItem.h = node.height
nodeItem.x = node.position
nodeItem.y = node.offset
nodeItem.x = node.position + node.x * width
nodeItem.y = node.offset + node.y * height
nodeItem.shapeProps.set('strokeSize', 1)
nodeItem.shapeProps.set('strokeColor', '#ffffff')
nodeItem.shapeProps.set('cornerRadius', 2)
nodeItem.shapeProps.set('fill', Fill.solid(node.color))
nodeItem.args.set('tplArea', 'controlled')
nodeItem.args.set('tplConnector', 'off')
nodeItem.args.set('tplRotation', 'off')
nodeItem.locked = false
nodeItems.add(nodeItem)

currentY += nodeItem.h + singleGap
Expand All @@ -353,6 +327,8 @@ func buildConnectorItems(levels, allConnections, allNodes) {
}
})

local cs = List()

levels.forEach(level => {
level.nodes.forEach(node => {
local connections = connectionsBySource.get(node.id)
Expand All @@ -363,13 +339,21 @@ func buildConnectorItems(levels, allConnections, allNodes) {
connections.forEach(c => {
local dstNode = level.nodesMap.get(c.dstId)
if (dstNode) {
connectorItems.add(buildSingleConnectorItem(c, node, dstNode))
cs.add(c)
}
})
}
})
})

cs.sort((a, b) => {
a.srcNode.offset - b.srcNode.offset
})

cs.forEach(c => {
connectorItems.add(buildSingleConnectorItem(c, c.srcNode, c.dstNode))
})

connectorItems
}

Expand All @@ -386,13 +370,14 @@ struct PathPoint {
func buildSingleConnectorItem(connector, srcNode, dstNode) {
local item = Item(connector.id, 'Connection', 'path')
local connectorSize = srcNode.unitSize * connector.value
local xs = srcNode.position + srcNode.width
local ys1 = srcNode.offset + srcNode.reservedOut

local xs = srcNode.position + srcNode.x * width + srcNode.width
local ys1 = srcNode.offset + srcNode.y * height + srcNode.reservedOut
local ys2 = ys1 + connectorSize
srcNode.reservedOut += connectorSize

local xd = dstNode.position
local yd1 = dstNode.offset + dstNode.reservedIn
local xd = dstNode.position + dstNode.x * width
local yd1 = dstNode.offset + dstNode.y * height + dstNode.reservedIn
local yd2 = yd1 + connectorSize
dstNode.reservedIn += connectorSize

Expand Down Expand Up @@ -474,12 +459,14 @@ func buildNodeLabels(nodes) {
local item = buildLabel('ln-' + node.id, node.name, font, labelFontSize, halign, 'bottom')
item.w = textSize.w + 4
item.h = textSize.h * 1.8 + 4

if (isLeft) {
item.x = node.position - item.w - labelPadding
} else {
item.x = node.position + node.width + labelPadding
}
item.y = node.offset + node.height / 2 - totalHeight / 2
item.x += node.x * width
item.y = node.offset + node.y * height + node.height / 2 - totalHeight / 2

labelItems.add(item)

Expand All @@ -491,6 +478,7 @@ func buildNodeLabels(nodes) {
} else {
valueLabel.x = node.position + node.width + labelPadding
}
valueLabel.x += node.x * width
valueLabel.y = item.y + item.h

labelItems.add(valueLabel)
Expand All @@ -499,7 +487,21 @@ func buildNodeLabels(nodes) {
}


allConnections = parseConnections(diagramCode)
func onAreaUpdate(itemId, item, area) {
local node = null
if (itemId.startsWith('n-')) {
node = nodesById.get(itemId.substring(2))
}
if (node) {
node.x = (area.x - node.position) / max(1, width)
node.y = (area.y - node.offset) / max(1, height)

nodesData = encodeNodes(nodesById)
}
}

local nodesDataById = decodeNodesData(nodesData)
allConnections = parseConnections(diagramCode, nodesDataById)
allNodes = extractNodesFromConnections(allConnections)

local levels = buildLevels(allNodes, allConnections)
Expand Down

0 comments on commit 9b03fea

Please sign in to comment.