Skip to content

Commit 61d2599

Browse files
Feat/add visualizer (#47)
* Add visualizer dependencies * Add visualizer code * Add visualize method to public api * Fix all imports * Add integration test * some cleanup (not done) * rename test file * decrease to 80vh * add highlighted color * search form on one sentence * Improve search form * Lots of styling * Add scaling, legenda and frontend layout * Adjust scaling steps * adjust view height * cleanup * remove uv.lock * add REUSE Compliance * make visualize import optional and DCO Remediation Commit for Thijs Baaijen <[email protected]> I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: 8523654 I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: 5426cc8 I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: 11b422a I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: 735e2e4 I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: e58a463 I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: f3041a7 I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: 8c460b3 I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: 32502fc I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: 0520977 I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: fb3aee2 I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: 78fd55f I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: b19ffa8 I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: ff38bb2 I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: dae8686 I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: fb7ff04 I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: 403bd30 I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: d47df1e I, Thijs Baaijen <[email protected]>, hereby add my Signed-off-by to this commit: 5e8475d Signed-off-by: Thijs Baaijen <[email protected]> * move visualizer import Signed-off-by: Thijs Baaijen <[email protected]> * cleanup code Signed-off-by: Thijs Baaijen <[email protected]> * install visualizer deps in CI Signed-off-by: Thijs Baaijen <[email protected]> * Add parser tests Signed-off-by: Thijs Baaijen <[email protected]> * Add REUSE compliance Signed-off-by: Thijs Baaijen <[email protected]> * Install visualizer in CI Signed-off-by: Thijs Baaijen <[email protected]> * cleanup Signed-off-by: Thijs Baaijen <[email protected]> * add visualizer dependencies to 'dev' group too Signed-off-by: Thijs Baaijen <[email protected]> * move CoordinatedNodeArray Signed-off-by: Thijs Baaijen <[email protected]> * Add documentation Signed-off-by: Thijs Baaijen <[email protected]> * Add port parameter Signed-off-by: Thijs Baaijen <[email protected]> * move vis-deps to correct group Signed-off-by: Thijs Baaijen <[email protected]> * Add reuse Signed-off-by: Thijs Baaijen <[email protected]> * rename main.py to app.py Signed-off-by: Thijs Baaijen <[email protected]> * add layout test Signed-off-by: Thijs Baaijen <[email protected]> * exclude visualizer from coverage Signed-off-by: Thijs Baaijen <[email protected]> * Update visualizer docs with disclaimer Signed-off-by: Thijs Baaijen <[email protected]> * re-enable visualizer tests Signed-off-by: Thijs Baaijen <[email protected]> * Update docs Signed-off-by: Thijs Baaijen <[email protected]> * Update pyproject.toml Co-authored-by: Jaap Schouten <[email protected]> Signed-off-by: Thijs Baaijen <[email protected]> * Add basic test for get_app_layout Signed-off-by: Thijs Baaijen <[email protected]> * clenaup Signed-off-by: Thijs Baaijen <[email protected]> * add type hints Signed-off-by: Thijs Baaijen <[email protected]> * add callback tests Signed-off-by: Thijs Baaijen <[email protected]> * use constants Signed-off-by: Thijs Baaijen <[email protected]> * bump minor version Signed-off-by: Thijs Baaijen <[email protected]> --------- Signed-off-by: Thijs Baaijen <[email protected]> Co-authored-by: Jaap Schouten <[email protected]>
1 parent ede0ad0 commit 61d2599

29 files changed

+890
-4
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.3
1+
1.3

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ demos/index
3535
model_structure
3636
model_interface
3737
examples/index
38+
visualizer
3839
advanced_documentation/index
3940
```
4041

docs/visualizer.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!--
2+
SPDX-FileCopyrightText: Contributors to the Power Grid Model project <[email protected]>
3+
4+
SPDX-License-Identifier: MPL-2.0
5+
-->
6+
7+
# Visualizer
8+
9+
## Features
10+
11+
- Based on [dash-cytoscape](https://github.com/plotly/dash-cytoscape).
12+
- Visualize small and large (10000+ nodes) networks
13+
- Explore attributes of nodes and branches
14+
- Highlight specific nodes and branches
15+
- Visualize various layouts, including hierarchical, force-directed and coordinate-based layouts
16+
17+
With Coordinates | Hierarchical | Force-Directed
18+
:------------------:|:------------:|:-------------:
19+
<img width="250" alt="Coordinates" src="https://github.com/user-attachments/assets/6f991cb1-08b4-4c4b-8adc-eed36f58db40" /> | <img width="250" alt="Hierarchical" src="https://github.com/user-attachments/assets/0cf5684d-fb7c-4920-92b8-1e49bc827a92" /> | <img width="250" alt="Force-Directed" src="https://github.com/user-attachments/assets/f0167ded-ceb4-4a31-a91e-e029dd6d7f13" />
20+
21+
-----
22+
## Quickstart
23+
#### Installation
24+
```bash
25+
pip install 'power-grid-model-ds[visualizer]' # quotes added for zsh compatibility
26+
```
27+
28+
#### Usage
29+
```python
30+
from power_grid_model_ds import Grid
31+
from power_grid_model_ds.visualizer import visualize
32+
from power_grid_model_ds.generators import RadialGridGenerator
33+
34+
grid = RadialGridGenerator(Grid).run()
35+
visualize(grid)
36+
```
37+
This will start a local web server at http://localhost:8050
38+
39+
#### Disclaimer
40+
Please note that the visualizer is still a work in progress and may not be fully functional or contain bugs.
41+
We welcome any feedback or suggestions for improvement.

pyproject.toml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ classifiers = [
2626
"Topic :: Scientific/Engineering :: Physics",
2727
]
2828
requires-python = ">=3.11,<3.14"
29-
dependencies = ["power-grid-model>=1.7", "rustworkx>= 0.15.1", "numpy>=1.21"]
29+
dependencies = [
30+
"power-grid-model>=1.7",
31+
"rustworkx>= 0.15.1",
32+
"numpy>=1.21",
33+
]
3034
dynamic = ["version"]
3135

3236
[project.optional-dependencies]
@@ -40,8 +44,19 @@ dev = [
4044
"isort>=5.13.2",
4145
"mypy>=1.9.0",
4246
"pre-commit>=4",
47+
# Visualization (make sure these stay equalivalent to the 'visualizer' group)
48+
"dash>=3.0.0",
49+
"dash-bootstrap-components>=2.0.0",
50+
"dash-cytoscape>=1.0.2",
51+
]
4352

53+
visualizer = [
54+
# Visualization (make sure these are also updated in the 'dev' group)
55+
"dash>=3.0.0",
56+
"dash-bootstrap-components>=2.0.0",
57+
"dash-cytoscape>=1.0.2",
4458
]
59+
4560
doc = [
4661
"sphinx",
4762
"myst-nb",
@@ -51,6 +66,7 @@ doc = [
5166
"numpydoc",
5267
]
5368

69+
5470
[project.urls]
5571
Home-page = "https://lfenergy.org/projects/power-grid-model/"
5672
GitHub = "https://github.com/PowerGridModel/power-grid-model-ds"
@@ -108,3 +124,8 @@ disable_error_code = ["assignment", "import-untyped"]
108124
[tool.coverage.run]
109125
relative_files = true
110126
branch = true
127+
128+
[tool.coverage.report]
129+
exclude_also = [
130+
'if TYPE_CHECKING:',
131+
]

src/power_grid_model_ds/_core/model/graphs/container.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from power_grid_model_ds._core.model.graphs.models import RustworkxGraphModel
1818
from power_grid_model_ds._core.model.graphs.models.base import BaseGraphModel
1919

20-
if TYPE_CHECKING: # pragma: no cover
20+
if TYPE_CHECKING:
2121
from power_grid_model_ds._core.model.grids.base import Grid
2222

2323

src/power_grid_model_ds/_core/model/graphs/models/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
NoPathBetweenNodes,
1818
)
1919

20-
if TYPE_CHECKING: # pragma: no cover
20+
if TYPE_CHECKING:
2121
from power_grid_model_ds._core.model.grids.base import Grid
2222

2323

src/power_grid_model_ds/_core/visualizer/__init__.py

Whitespace-only changes.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <[email protected]>
2+
#
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
import dash_bootstrap_components as dbc
6+
from dash import Dash, dcc, html
7+
from dash_bootstrap_components.icons import FONT_AWESOME
8+
9+
from power_grid_model_ds._core.model.grids.base import Grid
10+
from power_grid_model_ds._core.visualizer.callbacks import ( # noqa: F401 # pylint: disable=unused-import
11+
element_scaling,
12+
element_selection,
13+
layout_dropdown,
14+
search_form,
15+
)
16+
from power_grid_model_ds._core.visualizer.layout.cytoscape_html import get_cytoscape_html
17+
from power_grid_model_ds._core.visualizer.layout.header import HEADER_HTML
18+
from power_grid_model_ds._core.visualizer.layout.selection_output import SELECTION_OUTPUT_HTML
19+
from power_grid_model_ds._core.visualizer.parsers import parse_branches, parse_node_array
20+
from power_grid_model_ds.arrays import NodeArray
21+
22+
GOOGLE_FONTS = "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
23+
MDBOOTSTRAP = "https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/8.2.0/mdb.min.css"
24+
25+
26+
def visualize(grid: Grid, debug: bool = False, port: int = 8050) -> None:
27+
"""Visualize the Grid.
28+
29+
grid: Grid
30+
The grid to visualize.
31+
32+
layout: str
33+
The layout to use.
34+
35+
If 'layout' is not provided (""):
36+
And grid.node contains "x" and "y" columns:
37+
The layout will be set to "preset" which uses the x and y coordinates to place the nodes.
38+
Otherwise:
39+
The layout will be set to "breadthfirst", which is a hierarchical breadth-first-search (BFS) layout.
40+
Other options:
41+
- "random": A layout that places the nodes randomly.
42+
- "circle": A layout that places the nodes in a circle.
43+
- "concentric": A layout that places the nodes in concentric circles.
44+
- "grid": A layout that places the nodes in a grid matrix.
45+
- "cose": A layout that uses the CompoundSpring Embedder algorithm (force-directed layout)
46+
"""
47+
48+
app = Dash(
49+
external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.BOOTSTRAP, MDBOOTSTRAP, FONT_AWESOME, GOOGLE_FONTS]
50+
)
51+
app.layout = get_app_layout(grid)
52+
app.run(debug=debug, port=port)
53+
54+
55+
def _get_columns_store(grid: Grid) -> dcc.Store:
56+
return dcc.Store(
57+
id="columns-store",
58+
data={
59+
"node": grid.node.columns,
60+
"line": grid.line.columns,
61+
"link": grid.link.columns,
62+
"transformer": grid.transformer.columns,
63+
"branch": grid.branches.columns,
64+
},
65+
)
66+
67+
68+
def get_app_layout(grid: Grid) -> html.Div:
69+
"""Get the app layout."""
70+
columns_store = _get_columns_store(grid)
71+
graph_layout = _get_graph_layout(grid.node)
72+
elements = parse_node_array(grid.node) + parse_branches(grid)
73+
cytoscape_html = get_cytoscape_html(graph_layout, elements)
74+
75+
return html.Div(
76+
[
77+
columns_store,
78+
HEADER_HTML,
79+
html.Hr(style={"border-color": "white", "margin": "0"}),
80+
cytoscape_html,
81+
SELECTION_OUTPUT_HTML,
82+
],
83+
)
84+
85+
86+
def _get_graph_layout(nodes: NodeArray) -> str:
87+
"""Determine the graph layout"""
88+
if "x" in nodes.columns and "y" in nodes.columns:
89+
return "preset"
90+
return "breadthfirst"

src/power_grid_model_ds/_core/visualizer/callbacks/__init__.py

Whitespace-only changes.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <[email protected]>
2+
#
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
from copy import deepcopy
6+
from typing import Any
7+
8+
from dash import Input, Output, callback
9+
10+
from power_grid_model_ds._core.visualizer.layout.cytoscape_styling import BRANCH_WIDTH, DEFAULT_STYLESHEET, NODE_SIZE
11+
12+
13+
@callback(
14+
Output("cytoscape-graph", "stylesheet", allow_duplicate=True),
15+
Input("node-scale-input", "value"),
16+
Input("edge-scale-input", "value"),
17+
prevent_initial_call=True,
18+
)
19+
def scale_elements(node_scale: float, edge_scale: float) -> list[dict[str, Any]]:
20+
"""Callback to scale the elements of the graph."""
21+
new_stylesheet = deepcopy(DEFAULT_STYLESHEET)
22+
edge_style = {
23+
"selector": "edge",
24+
"style": {
25+
"width": BRANCH_WIDTH * edge_scale,
26+
},
27+
}
28+
new_stylesheet.append(edge_style)
29+
node_style = {
30+
"selector": "node",
31+
"style": {
32+
"height": NODE_SIZE * node_scale,
33+
"width": NODE_SIZE * node_scale,
34+
},
35+
}
36+
new_stylesheet.append(node_style)
37+
return new_stylesheet

0 commit comments

Comments
 (0)