Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 154 additions & 174 deletions flexmeasures/ui/templates/_macros.html
Original file line number Diff line number Diff line change
@@ -1,185 +1,165 @@
{% macro show_tree(assets, current_asset_name) %}
<div class="container">
<div id="view"></div>
<div class="container pb-4">
<div id="view" style="width: 100%; height: 75vh; min-height: 500px; overflow: hidden; border-radius: 8px; border: 1px solid #e2e8f0; background-color: #f8fafc; box-shadow: inset 0 2px 4px 0 rgba(0,0,0,0.06);"></div>
</div>

<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
<script type="text/javascript">
window.onload = function() {
let assets = {{ assets | safe }};
let currentAssetName = '{{ current_asset_name | safe }}';

<script type="text/javascript">
window.onload = function(){

let assets = {{ assets | safe }};
let currentAssetName = '{{ current_asset_name | safe}}';

{# Vega Spec adapted from https://stackoverflow.com/a/76300309 #}


treeSpecs = {
"$schema": "https://vega.github.io/schema/vega/v5.json",
"width": 1000,
"height": 650,
"padding": 0,
"autosize": {"type": "fit", "resize": false},
"signals": [
{"name": "nodeWidth", "value": 240},
{"name": "nodeHeight", "value": 40},
{"name": "verticalNodeGap", "value": 10},
{"name": "horizontalNodeGap", "value": 60},
{"name": "currentAssetName", "value": currentAssetName},
{
"name": "scaledNodeHeight",
"update": "abs(nodeHeight/ max(span(ydom),height))*height"
},
{"name": "scaledNodeWidth", "update": "(nodeWidth / span(xdom)) * width"},
{"name": "xrange", "update": "[0, width]"},
{"name": "yrange", "update": "[0, height]"},
{"name": "scaledFont13", "update": "(30/ span(xdom))*width"},
{"name": "scaledLimit", "update": "(30/ span(xdom))*width"},
{
"name": "xdom",
"update": "slice(xext)",
},
{
"name": "ydom",
"update": "slice(yext)",
},
],
"data": [
{
"name": "tree",
"values": assets,
"transform": [
{"type": "stratify", "key": "id", "parentKey": "parent"},
{
"type": "tree",
"method": "tidy",
"size": [{"signal": "nodeHeight * 0.1"},
{"signal": "width"}
],
"separation": {"signal": "true"},
"nodeSize" : [
{"signal": "nodeHeight+verticalNodeGap"},
{"signal": "nodeWidth+horizontalNodeGap"}

],
"as": ["y", "x", "depth", "children"],
},
{"type": "extent", "field": "x", "signal": "xext"},
{"type": "extent", "field": "y", "signal": "yext"}
]
},
{
"name": "links",
"source": "tree",
"transform": [
{"type": "treelinks", "signal": "upstream"},
{
"type": "linkpath",
"orient": "horizontal",
"shape": {"signal": "'diagonal'"},
"sourceY": {"expr": "scale('yscale', datum.source.y)"},
"sourceX": {"expr": "scale('xscale', datum.source.x)"},
"targetY": {"expr": "scale('yscale', datum.target.y)"},
"targetX": {"expr": "scale('xscale', datum.target.x)"}
// 1. Build hierarchy from flat list
let assetMap = {};
let rootNodes = [];

assets.forEach(asset => {
assetMap[asset.id] = {
name: asset.name,
...asset,
children: []
};
});

assets.forEach(asset => {
if (asset.parent === null || asset.parent === undefined) {
rootNodes.push(assetMap[asset.id]);
} else {
if (assetMap[asset.parent]) {
assetMap[asset.parent].children.push(assetMap[asset.id]);
} else {
rootNodes.push(assetMap[asset.id]);
}
}
]
}
],
"scales": [
{
"name": "xscale",
"zero": false,
"domain": {"signal": "xdom"},
"range": {"signal": "xrange"}
},
{
"name": "yscale",
"zero": false,
"domain": {"signal": "ydom"},
"range": {"signal": "yrange"}
}
],
"marks": [
{
"type": "path",
"from": {"data": "links"},
"encode": {
"update": {"path": {"field": "path"}, "stroke": {"value": "#aaa"}}
}
},
{
"name": "node",
"description": "The Parent Node",
"type": "group",
"clip": false,
"from": {"data": "tree"},
"encode": {
"update": {
"x": {"field": "x", "scale": "xscale"},
"width": {
"signal": "datum.name === currentAssetName ? scaledNodeWidth * 1.1 : scaledNodeWidth"
},
"yc": {"field": "y", "scale": "yscale"},
"height": {
"signal": "datum.name === currentAssetName ? scaledNodeHeight * 1.1 : scaledNodeHeight"
},
"fill": {
"signal": "datum.name === currentAssetName ? 'gold' : datum.name === 'Add asset' ? 'green' : 'lightblue'"
},
"stroke": {
"signal": "datum.name === currentAssetName ? 'darkorange' : 'black'"
},
"strokeWidth": {
"signal": "datum.name === currentAssetName ? 4 : 1"
},
"cornerRadius": {"value": 5},
"href": {"signal": "datum.link"},
"tooltip": {"signal": "datum.tooltip"}
});

var chartDom = document.getElementById('view');
var myChart = echarts.init(chartDom);

// Function to get responsive sizes
function getResponsiveConfig() {
let width = window.innerWidth;
if (width < 576) {
return { boxSize: 85, iconSize: 28, fontSize: 11 };
} else if (width < 768) {
return { boxSize: 105, iconSize: 36, fontSize: 13 };
} else {
return { boxSize: 130, iconSize: 44, fontSize: 14 };
}
},
"marks": [
{
"type": "text",
"interactive": false,
"name": "name",
"encode": {
"update": {
"x": {"signal": "(5/ span(xdom))*width + (scaledNodeWidth * 0.2)"},
"y": {"signal": "(5/ span(xdom))*width + (scaledNodeHeight * 0.15)"},
"fontWeight": {"value": "bold"},
"baseline": {"value": "line-top"},
"text": {"signal": "parent.name"},
"fontSize": {
"signal": "datum.name === currentAssetName ? scaledFont13 * 1.4 : scaledFont13"
},
"fill": {
"signal": "parent.name === currentAssetName ? 'black' : parent.name === 'Add asset' ? 'white' : 'darkblue'"
},
"limit": {"signal": "scaledNodeWidth-scaledLimit * 0.9 - (scaledNodeWidth * 0.2)"},
"font": {"value": "Calibri"},
"href": {"signal": "datum.link"},
"tooltip": {"signal": "parent.tooltip"}
}

function updateChart() {
let config = getResponsiveConfig();

// Enhance nodes with styles
function enrichNodeData(node) {
let isCurrent = node.name === currentAssetName;
let isAdd = node.name === 'Add asset';

node.symbol = 'roundRect';
node.symbolSize = isCurrent ? [config.boxSize * 1.08, config.boxSize * 1.08] : [config.boxSize, config.boxSize];

node.label = {
position: 'inside',
color: isCurrent ? '#000000' : (isAdd ? '#ffffff' : '#1e293b'),
fontSize: isCurrent ? config.fontSize + 1 : config.fontSize,
fontWeight: isCurrent ? 'bold' : '600',
fontFamily: 'system-ui, -apple-system, sans-serif',
formatter: function(params) {
if (node.icon) {
return `{icon_${node.id}|}\n{text|${node.name}}`;
}
return `{text|${node.name}}`;
},
rich: {
text: {
width: config.boxSize - 16,
overflow: 'truncate',
align: 'center',
padding: [8, 0, 0, 0]
}
}
};

if (node.icon) {
node.label.rich[`icon_${node.id}`] = {
backgroundColor: {
image: node.icon
},
height: config.iconSize,
width: config.iconSize,
align: 'center'
};
}
},
{
"type": "image",
"encode": {
"update": {
"x": {"signal": "-(scaledNodeWidth * 0.001)"},
"y": {"signal": "-(scaledNodeHeight * 0.001)"},
"width": {"signal": "scaledNodeWidth * 0.002"},
"height": {"signal": "scaledNodeHeight * 0.002"},
"baseline": {"value": "line-top"},
"url": {"signal": "parent.icon"}
}

node.itemStyle = {
color: isCurrent ? '#fcd34d' : (isAdd ? '#10b981' : '#ffffff'),
borderColor: isCurrent ? '#f59e0b' : (isAdd ? '#047857' : '#cbd5e1'),
borderWidth: isCurrent ? 3 : 1,
borderRadius: 8,
shadowColor: 'rgba(0, 0, 0, 0.2)',
shadowBlur: 10,
shadowOffsetX: 2,
shadowOffsetY: 6
};

if (node.children && node.children.length > 0) {
node.children.forEach(enrichNodeData);
}
},
]
}
]
}
vegaEmbed("#view", treeSpecs, { renderer: "svg" })
}
}

rootNodes.forEach(enrichNodeData);

var option = {
tooltip: {
trigger: 'item',
triggerOn: 'mousemove',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#e2e8f0',
borderWidth: 1,
textStyle: { color: '#334155' },
formatter: function(params) {
return params.data.tooltip || params.name;
}
},
series: [
{
type: 'tree',
data: rootNodes,
top: '10%',
left: '10%',
bottom: '15%',
right: '10%',
expandAndCollapse: true,
animationDuration: 550,
animationDurationUpdate: 750,
roam: true,
initialTreeDepth: -1,
lineStyle: {
color: '#94a3b8',
width: 2,
curveness: 0.5
}
}
]
};

myChart.setOption(option);
}

updateChart();

window.addEventListener('resize', function() {
myChart.resize();
updateChart();
});

myChart.on('click', function(params) {
if (params.data.link) {
window.location.href = params.data.link;
}
});
};
</script>
{% endmacro %}

Expand All @@ -201,5 +181,5 @@
{% endif %}
{% endfor %}
</tbody>
</table>
</table>
{% endmacro %}
Loading