Skip to content

Commit

Permalink
implemented update of sankey connection values from edit box
Browse files Browse the repository at this point in the history
  • Loading branch information
ishubin committed Feb 2, 2025
1 parent 2742465 commit 04b93d4
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 46 deletions.
8 changes: 8 additions & 0 deletions assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -1289,6 +1289,14 @@ ul.pagination li a.current {
.svg-editor-plot .item-control-point:hover {
opacity: 0.7;
}
.svg-editor-plot .item-control-point-textfield {
display: block;
width: 100%;
background: none !important;
border: none;
height: 100%;
color: white !important;
}
.svg-editor-plot rect.boundary-box {
stroke-width: 1;
fill: rgba(255, 255, 255, 0.01);
Expand Down
2 changes: 1 addition & 1 deletion assets/templates/diagrams/sankey.json

Large diffs are not rendered by default.

23 changes: 20 additions & 3 deletions assets/templates/diagrams/sankey.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ args:
colorTheme: {group: "Theme & Colors", type: "choice", value: "default", options: ["default", "light", "dark"], name: "Color theme"}
conColorType: {group: "Theme & Colors", type: choice, value: source, options: ['source', 'destination', 'gradient', 'custom'], name: 'Connection color type'}
conColor: {group: "Theme & Colors", type: advanced-color, value: {type: solid, color: '#aaaaaa'}, name: 'Connection color', depends: {conColorType: 'custom'}}
labelColor: {group: "Theme & Colors", type: color, value: '#222222', name: 'Label color'}
labelColor: {group: "Theme & Colors", type: color, value: '#222222', name: 'Text color'}

nodeWidth: {group: "Nodes", type: number, value: 20, name: "Node width", min: 1}
nodeSpacing: {group: "Nodes", type: number, value: 40, name: "Nodes empty space (%)", min: 0, max: 90}
Expand All @@ -34,8 +34,8 @@ args:
magnify: {group: "Labels & Text", type: number, value: 0, name: "Magnify value", min: -50, max: 50}
conLabel: {group: "Labels & Text", type: boolean, value: true, name: 'Connection labels enabled', description: 'Displays the value of the connection'}
showLabelFill: {group: "Labels & Text", type: boolean, value: true, name: 'Show label fill'}
labelFill: {group: "Labels & Text", type: advanced-color, value: {type: solid, color: 'rgba(255, 255, 255, 0.3)'}, name: "Background", depends: {showLabelFill: true}}
labelStroke: {group: "Labels & Text", type: color, value: 'rgba(120,120,120,0.6)', name: 'Label stroke', depends: {showLabelFill: true}}
labelFill: {group: "Labels & Text", type: advanced-color, value: {type: solid, color: '#FCE6AC82'}, name: "Background", depends: {showLabelFill: true}}
labelStroke: {group: "Labels & Text", type: color, value: '#97654299', name: 'Label stroke', depends: {showLabelFill: true}}
labelStrokeSize: {group: "Labels & Text", type: number, value: 1, name: 'Label stroke size', depends: {showLabelFill: true}}
labelCornerRadius: {group: "Labels & Text", type: number, value: 4, name: 'Label corner radius', depends: {showLabelFill: true}}
labelPadding: {group: "Labels & Text", type: number, value: 5, name: 'Label padding', depends: {showLabelFill: true}}
Expand All @@ -57,6 +57,23 @@ import:
handlers:
area: onAreaUpdate(itemId, item, area)

controls:
- $-foreach: {source: "allConnections", it: "c"}
data:
connectionId: {$-expr: "c.id"}
selectedItemId: {$-expr: "c.id"}
name: insertSlide
type: textfield
text: {$-str: "${c.value}"}
placement: BL
x: { $-expr: "c.item.x" }
y: { $-expr: "c.item.y" }
width: 180
height: 30
input:
- onConnectionValueInput(control.data.connectionId, value)


item:
id: root
name: Sankey diagram
Expand Down
58 changes: 43 additions & 15 deletions assets/templates/diagrams/src/sankey.sch
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ struct Connection {
value: 0
srcNode: null
dstNode: null
item: null
}

struct CodeLine {
text: ''
connection: null
}


Expand All @@ -103,7 +109,7 @@ func parseConnection(line) {
}

func parseConnections(text, nodesData) {
getOrCreateNode = (id) => {
local getOrCreateNode = (id) => {
local node = nodesById.get(id)
if (!node) {
node = Node(id, id)
Expand All @@ -117,21 +123,22 @@ func parseConnections(text, nodesData) {
node
}

local connections = List()
splitString(text, '\n').forEach(line => {
line = line.trim()
local lines = List()
splitString(text, '\n').forEach(rawLine => {
local line = rawLine.trim()
local c = null
if (line != '' && !line.startsWith('//')) {
local c = parseConnection(line)
if (c) {
c.srcNode = getOrCreateNode(c.srcId)
c.dstNode = getOrCreateNode(c.dstId)
if (c) {
connections.add(c)
}
}
c = parseConnection(line)
}
if (c) {
c.srcNode = getOrCreateNode(c.srcId)
c.dstNode = getOrCreateNode(c.dstId)
lines.add(CodeLine(rawLine, c))
} else {
lines.add(CodeLine(rawLine, null))
}
})
connections
lines
}

func extractNodesFromConnections(connections) {
Expand Down Expand Up @@ -447,6 +454,7 @@ func buildSingleConnectorItem(connector, srcNode, dstNode) {
if (conLabel) {
item.childItems.add(buildConnectorLabel(connector, item.w, item.h))
}
connector.item = item
item
}

Expand Down Expand Up @@ -582,7 +590,6 @@ func buildNodeLabels(nodes) {




func onAreaUpdate(itemId, item, area) {
local node = null
if (itemId.startsWith('n-')) {
Expand All @@ -596,8 +603,29 @@ func onAreaUpdate(itemId, item, area) {
}
}


func onConnectionValueInput(connectionId, value) {
local code = ''
codeLines.forEach((line, idx) => {
if (idx > 0) {
code += '\n'
}
if (line.connection) {
local cVal = if (line.connection.id == connectionId) { value } else { line.connection.value }
code += line.connection.srcId + ' [' + cVal + '] ' + line.connection.dstId
} else {
code += line.text
}
})

diagramCode = code
}


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

codeLines = parseConnections(diagramCode, nodesDataById)
allConnections = codeLines.filter(cl => { cl.connection != null }).map(cl => { cl.connection })
allNodes = extractNodesFromConnections(allConnections)

local levels = buildLevels(allNodes, allConnections)
Expand Down
5 changes: 5 additions & 0 deletions src/ui/components/SchemeEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
:controlPointsColor="schemeContainer.scheme.style.controlPointsColor"
@custom-control-clicked="onEditBoxCustomControlClicked"
@template-rebuild-requested="onEditBoxTemplateRebuildRequested"
@template-properties-updated-requested="onEditBoxTemplatePropertiesUpdateRequested"
/>

<EditBox v-if="state === 'cropImage' && cropImage.editBox"
Expand Down Expand Up @@ -2302,6 +2303,10 @@ export default {
this.schemeContainer.updateEditBox();
},

onEditBoxTemplatePropertiesUpdateRequested() {
this.templatePropertiesKey += 1;
},

onTemplatePropertiesUpdated(originItemId, template, templateArgs, changedArgName) {
this.rebuildTemplate(originItemId, template, templateArgs);
// delaying full reindex to optimize performance, when user changes color in color picker we don't need to reindex all items right away
Expand Down
62 changes: 52 additions & 10 deletions src/ui/components/editor/EditBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
</g>

<g v-for="(control,idx) in templateControls">
<g v-if="control.type === 'button'" >
<template v-if="control.type === 'button'" >
<rect
class="item-control-point"
:x="control.x - control.xc * control.width/safeZoom"
Expand Down Expand Up @@ -177,7 +177,32 @@
data-type="edit-box-template-control"
@click="onTemplateControlClick(idx)"
/>
</g>
</template>
<template v-else-if="control.type === 'textfield'">
<rect
class="item-control-point"
:x="control.x - control.xc * control.width/safeZoom"
:y="control.y - control.yc * control.height/safeZoom"
:width="control.width/safeZoom"
:height="control.height/safeZoom"
:fill="controlPointsColor"
:rx="2/safeZoom"
/>
<foreignObject :x="control.x - control.xc * control.width/safeZoom" :y="control.y - control.yc * control.height/safeZoom" :width="control.width/safeZoom" :height="control.height/safeZoom">
<div xmlns="http://www.w3.org/1999/xhtml"
style="color: white; display: table-cell; white-space: nowrap; text-align: center; vertical-align: middle"
:style="{width: `${Math.round(control.width/safeZoom)}px`, height: `${Math.round(control.height/safeZoom)}px`}"
>
<input class="item-control-point-textfield"
type="text"
:value="control.text"
:style="{'font-size': `${14/safeZoom}px`, 'padding-left': `${7/safeZoom}px`}"
@blur="submitTemplateControlText(idx, $event.target.value)"
@keydown.enter="submitTemplateControlText(idx, $event.target.value)"
/>
</div>
</foreignObject>
</template>
</g>
</g>
</g>
Expand Down Expand Up @@ -726,6 +751,29 @@ export default {
control.x = lp.x;
control.y = lp.y;
});
this.$forceUpdate();
},


submitTemplateControlText(idx, text) {
const item = this.editBox.templateItemRoot;
const originArgs = utils.clone(item.args.templateArgs);
const updatedArgs = this.templateControls[idx].input(item, text);

const diff = jsonDiff(originArgs, updatedArgs);
if (diff.changes.length > 0) {
EditorEventBus.schemeChangeCommitted.$emit(this.editorId);
}

if (item.args && item.args.templateArgs) {
for (let key in item.args.templateArgs) {
if (updatedArgs.hasOwnProperty(key)) {
item.args.templateArgs[key] = updatedArgs[key];
}
}
}
this.$emit('template-rebuild-requested', this.editBox.templateItemRoot.id, this.template, item.args.templateArgs);
this.$emit('template-properties-updated-requested');
},

onTemplateControlClick(idx) {
Expand All @@ -746,6 +794,7 @@ export default {
}
}
this.$emit('template-rebuild-requested', this.editBox.templateItemRoot.id, this.template, item.args.templateArgs);
this.$emit('template-properties-updated-requested');
},

onColorControlToggled(expanded) {
Expand Down Expand Up @@ -828,14 +877,7 @@ export default {
},

shouldShowControlPoints() {
if (this.editBox.items.length === 1) {
const item = this.editBox.items[0];
if (item.shape === 'path') {
return false;
}
return true;
}
return false;
return this.editBox.items.length === 1;
},

selectedConnectorPath() {
Expand Down
5 changes: 5 additions & 0 deletions src/ui/components/editor/SvgEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,11 @@ export default {
},

mouseDown(event) {
// ignoring textfield control for templates so that it does not deselect the item
if (event.target.closest('.item-control-point-textfield')) {
return;
}

let newClickTime = performance.now();
// implementing own double click event hanlding
// as for some reason the native dblclick event is not reliable
Expand Down
46 changes: 36 additions & 10 deletions src/ui/components/editor/items/ItemTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,28 +278,54 @@ export function compileItemTemplate(editorId, template, templateRef) {
context: new TemplateContext(ContextPhases.EVENT, 'control', '')
}).controls.map(control => {

const controlExpressions = [].concat(initBlock).concat(toExpressionBlock(control.click));
const fullScript = controlExpressions.join('\n');
let inputHandler = 'click';
if (control.type === 'textfield') {
inputHandler = 'input';
}

const clickExecutor = buildTemplateExpression(fullScript);
return {
...control,
const fullScript = [].concat(initBlock).concat(toExpressionBlock(control[inputHandler])).join('\n');
const eventExecutor = buildTemplateExpression(fullScript);

let eventCallback = null;
if (control.type === 'textfield') {
/**
* @param {Item} item
* @param {Item} item - the root template item
* @param {String} value - the text from the input textfield, which was changed by user
* @returns {Object} updated data object which can be used to update the template args.
* Keep in mind that this object contains not only template args,
* but everything that was declared in the global scope of the template script
*/
click: (item) => {
return clickExecutor({
eventCallback = (item, value) => {
return eventExecutor({
...createTemplateFunctions(editorId, item),
...args, width, height,
context: new TemplateContext(ContextPhases.EVENT, 'control', control.id),
control,
value,
});
};
} else {
/**
* @param {Item} item - the root template item
* @returns {Object} updated data object which can be used to update the template args.
* Keep in mind that this object contains not only template args,
* but everything that was declared in the global scope of the template script
*/
eventCallback = (item) => {
return eventExecutor({
...createTemplateFunctions(editorId, item),
...args, width, height,
context: new TemplateContext(ContextPhases.EVENT, 'control', control.id)
context: new TemplateContext(ContextPhases.EVENT, 'control', control.id),
control,
});
}
};
}

const enrichedControl = {
...control,
};
enrichedControl[inputHandler] = eventCallback;
return enrichedControl;
});
},

Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/editor/properties/TemplateProperties.vue
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export default {
template: null,
editorPanels: [],
lastChangedArgName: null,
updateDelayer: createDelayer(100, () => {
updateDelayer: createDelayer(250, () => {
this.$emit('updated', this.item.id, this.template, this.args, this.lastChangedArgName);
}),
args: this.item.args && this.item.args.templateArgs ? this.item.args.templateArgs : {}
Expand Down
19 changes: 13 additions & 6 deletions src/ui/delayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ export function createDelayer(timeoutInMillis, callback) {
let timerId = null;

return {
lastUpdatedTime: -1,

trigger() {
if (timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(() => {
timerId = null;
if (this.lastUpdatedTime < 0 || (performance.now() - this.lastUpdatedTime) > timeoutInMillis) {
this.lastUpdatedTime = performance.now();
callback();
}, timeoutInMillis);
} else {
if (!timerId) {
timerId = setTimeout(() => {
timerId = null;
this.lastUpdatedTime = performance.now();
callback();
}, timeoutInMillis);
}
}
},

destroy() {
Expand Down

0 comments on commit 04b93d4

Please sign in to comment.