flixopt 2.1.6__py3-none-any.whl → 2.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flixopt might be problematic. Click here for more details.
- docs/examples/00-Minimal Example.md +1 -1
- docs/examples/01-Basic Example.md +1 -1
- docs/examples/02-Complex Example.md +1 -1
- docs/examples/index.md +1 -1
- docs/faq/contribute.md +26 -14
- docs/faq/index.md +1 -1
- docs/javascripts/mathjax.js +1 -1
- docs/user-guide/Mathematical Notation/Bus.md +1 -1
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +21 -21
- docs/user-guide/Mathematical Notation/Flow.md +3 -3
- docs/user-guide/Mathematical Notation/InvestParameters.md +3 -0
- docs/user-guide/Mathematical Notation/LinearConverter.md +5 -5
- docs/user-guide/Mathematical Notation/OnOffParameters.md +3 -0
- docs/user-guide/Mathematical Notation/Piecewise.md +1 -1
- docs/user-guide/Mathematical Notation/Storage.md +2 -2
- docs/user-guide/Mathematical Notation/index.md +1 -1
- docs/user-guide/Mathematical Notation/others.md +1 -1
- docs/user-guide/index.md +2 -2
- flixopt/__init__.py +4 -0
- flixopt/aggregation.py +33 -32
- flixopt/calculation.py +161 -65
- flixopt/components.py +687 -154
- flixopt/config.py +17 -8
- flixopt/core.py +69 -60
- flixopt/effects.py +146 -64
- flixopt/elements.py +297 -110
- flixopt/features.py +78 -71
- flixopt/flow_system.py +72 -50
- flixopt/interface.py +952 -113
- flixopt/io.py +15 -10
- flixopt/linear_converters.py +373 -81
- flixopt/network_app.py +445 -266
- flixopt/plotting.py +215 -87
- flixopt/results.py +382 -209
- flixopt/solvers.py +25 -21
- flixopt/structure.py +41 -39
- flixopt/utils.py +10 -7
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/METADATA +64 -53
- flixopt-2.1.8.dist-info/RECORD +56 -0
- scripts/extract_release_notes.py +5 -5
- scripts/gen_ref_pages.py +1 -1
- flixopt-2.1.6.dist-info/RECORD +0 -54
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/WHEEL +0 -0
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/licenses/LICENSE +0 -0
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/top_level.txt +0 -0
flixopt/network_app.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import logging
|
|
3
4
|
import socket
|
|
4
5
|
import threading
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
6
7
|
|
|
7
8
|
try:
|
|
8
9
|
import dash_cytoscape as cyto
|
|
9
10
|
import dash_daq as daq
|
|
10
|
-
import networkx
|
|
11
|
+
import networkx as nx
|
|
11
12
|
from dash import Dash, Input, Output, State, callback_context, dcc, html, no_update
|
|
12
13
|
from werkzeug.serving import make_server
|
|
13
14
|
|
|
@@ -17,12 +18,17 @@ except ImportError as e:
|
|
|
17
18
|
DASH_CYTOSCAPE_AVAILABLE = False
|
|
18
19
|
VISUALIZATION_ERROR = str(e)
|
|
19
20
|
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .flow_system import FlowSystem
|
|
23
|
+
|
|
24
|
+
import networkx as nx
|
|
25
|
+
|
|
20
26
|
from .components import LinearConverter, Sink, Source, SourceAndSink, Storage
|
|
21
|
-
from .elements import Bus
|
|
22
|
-
from .flow_system import FlowSystem
|
|
27
|
+
from .elements import Bus
|
|
23
28
|
|
|
24
29
|
logger = logging.getLogger('flixopt')
|
|
25
30
|
|
|
31
|
+
|
|
26
32
|
# Configuration class for better organization
|
|
27
33
|
class VisualizationConfig:
|
|
28
34
|
"""Configuration constants for the visualization"""
|
|
@@ -39,16 +45,28 @@ class VisualizationConfig:
|
|
|
39
45
|
COLOR_PRESETS = {
|
|
40
46
|
'Default': DEFAULT_COLORS,
|
|
41
47
|
'Vibrant': {
|
|
42
|
-
'Bus': '#FF6B6B',
|
|
43
|
-
'
|
|
48
|
+
'Bus': '#FF6B6B',
|
|
49
|
+
'Source': '#4ECDC4',
|
|
50
|
+
'Sink': '#45B7D1',
|
|
51
|
+
'Storage': '#96CEB4',
|
|
52
|
+
'Converter': '#FFEAA7',
|
|
53
|
+
'Other': '#DDA0DD',
|
|
44
54
|
},
|
|
45
55
|
'Dark': {
|
|
46
|
-
'Bus': '#2C3E50',
|
|
47
|
-
'
|
|
56
|
+
'Bus': '#2C3E50',
|
|
57
|
+
'Source': '#34495E',
|
|
58
|
+
'Sink': '#7F8C8D',
|
|
59
|
+
'Storage': '#95A5A6',
|
|
60
|
+
'Converter': '#BDC3C7',
|
|
61
|
+
'Other': '#ECF0F1',
|
|
48
62
|
},
|
|
49
63
|
'Pastel': {
|
|
50
|
-
'Bus': '#FFB3BA',
|
|
51
|
-
'
|
|
64
|
+
'Bus': '#FFB3BA',
|
|
65
|
+
'Source': '#BAFFC9',
|
|
66
|
+
'Sink': '#BAE1FF',
|
|
67
|
+
'Storage': '#FFFFBA',
|
|
68
|
+
'Converter': '#FFDFBA',
|
|
69
|
+
'Other': '#E0BBE4',
|
|
52
70
|
},
|
|
53
71
|
}
|
|
54
72
|
|
|
@@ -96,8 +114,17 @@ class VisualizationConfig:
|
|
|
96
114
|
},
|
|
97
115
|
]
|
|
98
116
|
|
|
99
|
-
|
|
117
|
+
|
|
118
|
+
def flow_graph(flow_system: FlowSystem) -> nx.DiGraph:
|
|
100
119
|
"""Convert FlowSystem to NetworkX graph - simplified and more robust"""
|
|
120
|
+
if not DASH_CYTOSCAPE_AVAILABLE:
|
|
121
|
+
raise ImportError(
|
|
122
|
+
'Network visualization requires optional dependencies. '
|
|
123
|
+
'Install with: pip install flixopt[viz] or '
|
|
124
|
+
'pip install dash dash-cytoscape networkx werkzeug. '
|
|
125
|
+
f'Original error: {VISUALIZATION_ERROR}'
|
|
126
|
+
)
|
|
127
|
+
|
|
101
128
|
nodes = list(flow_system.components.values()) + list(flow_system.buses.values())
|
|
102
129
|
edges = list(flow_system.flows.values())
|
|
103
130
|
|
|
@@ -127,7 +154,7 @@ def flow_graph(flow_system: FlowSystem) -> networkx.DiGraph:
|
|
|
127
154
|
else:
|
|
128
155
|
return 'rectangle'
|
|
129
156
|
|
|
130
|
-
graph =
|
|
157
|
+
graph = nx.DiGraph()
|
|
131
158
|
|
|
132
159
|
# Add nodes with attributes
|
|
133
160
|
for node in nodes:
|
|
@@ -149,157 +176,225 @@ def flow_graph(flow_system: FlowSystem) -> networkx.DiGraph:
|
|
|
149
176
|
parameters=edge.__str__().replace(')', '\n)'),
|
|
150
177
|
)
|
|
151
178
|
except Exception as e:
|
|
152
|
-
logger.error(f
|
|
179
|
+
logger.error(f'Failed to add edge {edge}: {e}')
|
|
153
180
|
|
|
154
181
|
return graph
|
|
155
182
|
|
|
156
|
-
|
|
183
|
+
|
|
184
|
+
def make_cytoscape_elements(graph: nx.DiGraph) -> list[dict[str, Any]]:
|
|
157
185
|
"""Convert NetworkX graph to Cytoscape elements"""
|
|
158
186
|
elements = []
|
|
159
187
|
|
|
160
188
|
# Add nodes
|
|
161
189
|
for node_id in graph.nodes():
|
|
162
190
|
node_data = graph.nodes[node_id]
|
|
163
|
-
elements.append(
|
|
164
|
-
|
|
165
|
-
'
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
191
|
+
elements.append(
|
|
192
|
+
{
|
|
193
|
+
'data': {
|
|
194
|
+
'id': node_id,
|
|
195
|
+
'label': node_id,
|
|
196
|
+
'color': node_data.get('color', '#7F8C8D'),
|
|
197
|
+
'shape': node_data.get('shape', 'rectangle'),
|
|
198
|
+
'element_type': node_data.get('element_type', 'Other'),
|
|
199
|
+
'parameters': node_data.get('parameters', ''),
|
|
200
|
+
}
|
|
171
201
|
}
|
|
172
|
-
|
|
202
|
+
)
|
|
173
203
|
|
|
174
204
|
# Add edges
|
|
175
205
|
for u, v in graph.edges():
|
|
176
206
|
edge_data = graph.edges[u, v]
|
|
177
|
-
elements.append(
|
|
178
|
-
|
|
179
|
-
'
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
207
|
+
elements.append(
|
|
208
|
+
{
|
|
209
|
+
'data': {
|
|
210
|
+
'source': u,
|
|
211
|
+
'target': v,
|
|
212
|
+
'id': f'{u}-{v}',
|
|
213
|
+
'label': edge_data.get('label', ''),
|
|
214
|
+
'parameters': edge_data.get('parameters', ''),
|
|
215
|
+
}
|
|
184
216
|
}
|
|
185
|
-
|
|
217
|
+
)
|
|
186
218
|
|
|
187
219
|
return elements
|
|
188
220
|
|
|
221
|
+
|
|
189
222
|
def create_color_picker_input(label: str, input_id: str, default_color: str):
|
|
190
223
|
"""Create a compact color picker with DAQ ColorPicker"""
|
|
191
|
-
return html.Div(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
return html.Div(
|
|
225
|
+
[
|
|
226
|
+
html.Label(
|
|
227
|
+
label, style={'color': 'white', 'font-size': '12px', 'margin-bottom': '5px', 'display': 'block'}
|
|
228
|
+
),
|
|
229
|
+
daq.ColorPicker(
|
|
230
|
+
id=input_id,
|
|
231
|
+
label='',
|
|
232
|
+
value={'hex': default_color},
|
|
233
|
+
size=200,
|
|
234
|
+
theme={'dark': True},
|
|
235
|
+
style={'margin-bottom': '10px'},
|
|
236
|
+
),
|
|
237
|
+
]
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def create_style_section(title: str, children: list):
|
|
207
242
|
"""Create a collapsible section for organizing controls"""
|
|
208
|
-
return html.Div(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
243
|
+
return html.Div(
|
|
244
|
+
[
|
|
245
|
+
html.H4(
|
|
246
|
+
title,
|
|
247
|
+
style={
|
|
248
|
+
'color': 'white',
|
|
249
|
+
'margin-bottom': '10px',
|
|
250
|
+
'border-bottom': '2px solid #3498DB',
|
|
251
|
+
'padding-bottom': '5px',
|
|
252
|
+
},
|
|
253
|
+
),
|
|
254
|
+
html.Div(children, style={'margin-bottom': '20px'}),
|
|
255
|
+
]
|
|
256
|
+
)
|
|
257
|
+
|
|
215
258
|
|
|
216
259
|
def create_sidebar():
|
|
217
260
|
"""Create the main sidebar with improved organization"""
|
|
218
|
-
return html.Div(
|
|
219
|
-
|
|
220
|
-
html.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
261
|
+
return html.Div(
|
|
262
|
+
[
|
|
263
|
+
html.Div(
|
|
264
|
+
[
|
|
265
|
+
html.H3(
|
|
266
|
+
'Style Controls',
|
|
267
|
+
style={
|
|
268
|
+
'color': 'white',
|
|
269
|
+
'margin-bottom': '20px',
|
|
270
|
+
'text-align': 'center',
|
|
271
|
+
'border-bottom': '3px solid #9B59B6',
|
|
272
|
+
'padding-bottom': '10px',
|
|
273
|
+
},
|
|
274
|
+
),
|
|
275
|
+
# Layout Section
|
|
276
|
+
create_style_section(
|
|
277
|
+
'Layout',
|
|
278
|
+
[
|
|
279
|
+
dcc.Dropdown(
|
|
280
|
+
id='layout-dropdown',
|
|
281
|
+
options=[
|
|
282
|
+
{'label': 'Klay (horizontal)', 'value': 'klay'},
|
|
283
|
+
{'label': 'Dagre (vertical)', 'value': 'dagre'},
|
|
284
|
+
{'label': 'Breadthfirst', 'value': 'breadthfirst'},
|
|
285
|
+
{'label': 'Cose (force-directed)', 'value': 'cose'},
|
|
286
|
+
{'label': 'Grid', 'value': 'grid'},
|
|
287
|
+
{'label': 'Circle', 'value': 'circle'},
|
|
288
|
+
],
|
|
289
|
+
value='klay',
|
|
290
|
+
clearable=False,
|
|
291
|
+
style={'width': '100%'},
|
|
292
|
+
),
|
|
293
|
+
],
|
|
294
|
+
),
|
|
295
|
+
# Color Scheme Section
|
|
296
|
+
create_style_section(
|
|
297
|
+
'Color Scheme',
|
|
298
|
+
[
|
|
299
|
+
dcc.Dropdown(
|
|
300
|
+
id='color-scheme-dropdown',
|
|
301
|
+
options=[{'label': k, 'value': k} for k in VisualizationConfig.COLOR_PRESETS.keys()],
|
|
302
|
+
value='Default',
|
|
303
|
+
style={'width': '100%', 'margin-bottom': '10px'},
|
|
304
|
+
),
|
|
305
|
+
],
|
|
306
|
+
),
|
|
307
|
+
# Color Pickers Section
|
|
308
|
+
create_style_section(
|
|
309
|
+
'Custom Colors',
|
|
310
|
+
[
|
|
311
|
+
create_color_picker_input('Bus', 'bus-color-picker', '#7F8C8D'),
|
|
312
|
+
create_color_picker_input('Source', 'source-color-picker', '#F1C40F'),
|
|
313
|
+
create_color_picker_input('Sink', 'sink-color-picker', '#F1C40F'),
|
|
314
|
+
create_color_picker_input('Storage', 'storage-color-picker', '#2980B9'),
|
|
315
|
+
create_color_picker_input('Converter', 'converter-color-picker', '#D35400'),
|
|
316
|
+
create_color_picker_input('Edge', 'edge-color-picker', '#808080'),
|
|
317
|
+
],
|
|
318
|
+
),
|
|
319
|
+
# Node Settings
|
|
320
|
+
create_style_section(
|
|
321
|
+
'Node Settings',
|
|
322
|
+
[
|
|
323
|
+
html.Label('Size', style={'color': 'white', 'font-size': '12px'}),
|
|
324
|
+
dcc.Slider(
|
|
325
|
+
id='node-size-slider',
|
|
326
|
+
min=50,
|
|
327
|
+
max=150,
|
|
328
|
+
step=10,
|
|
329
|
+
value=90,
|
|
330
|
+
marks={
|
|
331
|
+
i: {'label': str(i), 'style': {'color': 'white', 'font-size': '10px'}}
|
|
332
|
+
for i in range(50, 151, 25)
|
|
333
|
+
},
|
|
334
|
+
tooltip={'placement': 'bottom', 'always_visible': True},
|
|
335
|
+
),
|
|
336
|
+
html.Br(),
|
|
337
|
+
html.Label('Font Size', style={'color': 'white', 'font-size': '12px'}),
|
|
338
|
+
dcc.Slider(
|
|
339
|
+
id='font-size-slider',
|
|
340
|
+
min=8,
|
|
341
|
+
max=20,
|
|
342
|
+
step=1,
|
|
343
|
+
value=10,
|
|
344
|
+
marks={
|
|
345
|
+
i: {'label': str(i), 'style': {'color': 'white', 'font-size': '10px'}}
|
|
346
|
+
for i in range(8, 21, 2)
|
|
347
|
+
},
|
|
348
|
+
tooltip={'placement': 'bottom', 'always_visible': True},
|
|
349
|
+
),
|
|
350
|
+
],
|
|
351
|
+
),
|
|
352
|
+
# Reset Button
|
|
353
|
+
html.Div(
|
|
354
|
+
[
|
|
355
|
+
html.Button(
|
|
356
|
+
'Reset to Defaults',
|
|
357
|
+
id='reset-btn',
|
|
358
|
+
n_clicks=0,
|
|
359
|
+
style={
|
|
360
|
+
'width': '100%',
|
|
361
|
+
'background-color': '#E74C3C',
|
|
362
|
+
'color': 'white',
|
|
363
|
+
'border': 'none',
|
|
364
|
+
'padding': '10px',
|
|
365
|
+
'border-radius': '5px',
|
|
366
|
+
'cursor': 'pointer',
|
|
367
|
+
'margin-top': '20px',
|
|
368
|
+
},
|
|
369
|
+
),
|
|
370
|
+
]
|
|
371
|
+
),
|
|
372
|
+
],
|
|
373
|
+
id='sidebar-content',
|
|
374
|
+
style={
|
|
375
|
+
'width': '280px',
|
|
376
|
+
'height': '100vh',
|
|
377
|
+
'background-color': '#2C3E50',
|
|
378
|
+
'padding': '20px',
|
|
379
|
+
'position': 'fixed',
|
|
380
|
+
'left': '0',
|
|
381
|
+
'top': '0',
|
|
382
|
+
'overflow-y': 'auto',
|
|
383
|
+
'border-right': '3px solid #34495E',
|
|
384
|
+
'box-shadow': '2px 0 5px rgba(0,0,0,0.1)',
|
|
385
|
+
'z-index': '999',
|
|
386
|
+
'transform': 'translateX(-100%)',
|
|
387
|
+
'transition': 'transform 0.3s ease',
|
|
388
|
+
},
|
|
389
|
+
)
|
|
390
|
+
]
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def shownetwork(graph: nx.DiGraph):
|
|
300
395
|
"""Main function to create and run the network visualization"""
|
|
301
396
|
if not DASH_CYTOSCAPE_AVAILABLE:
|
|
302
|
-
raise ImportError(f
|
|
397
|
+
raise ImportError(f'Required packages not available: {VISUALIZATION_ERROR}')
|
|
303
398
|
|
|
304
399
|
app = Dash(__name__, suppress_callback_exceptions=True)
|
|
305
400
|
|
|
@@ -310,72 +405,118 @@ def shownetwork(graph: networkx.DiGraph):
|
|
|
310
405
|
elements = make_cytoscape_elements(graph)
|
|
311
406
|
|
|
312
407
|
# App Layout
|
|
313
|
-
app.layout = html.Div(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}),
|
|
335
|
-
html.Button('Export PNG', id='export-btn', n_clicks=0, style={
|
|
336
|
-
'position': 'absolute', 'right': '20px', 'top': '15px',
|
|
337
|
-
'background-color': '#27AE60', 'color': 'white', 'border': 'none',
|
|
338
|
-
'padding': '10px 20px', 'border-radius': '5px', 'cursor': 'pointer',
|
|
339
|
-
}),
|
|
340
|
-
], style={
|
|
341
|
-
'background-color': '#34495E', 'padding': '15px 20px',
|
|
342
|
-
'position': 'relative', 'border-bottom': '2px solid #3498DB',
|
|
343
|
-
}),
|
|
344
|
-
|
|
345
|
-
# Cytoscape graph
|
|
346
|
-
cyto.Cytoscape(
|
|
347
|
-
id='cytoscape',
|
|
348
|
-
layout={'name': 'klay'},
|
|
349
|
-
style={'width': '100%', 'height': '70vh'},
|
|
350
|
-
elements=elements,
|
|
351
|
-
stylesheet=VisualizationConfig.DEFAULT_STYLESHEET,
|
|
408
|
+
app.layout = html.Div(
|
|
409
|
+
[
|
|
410
|
+
# Toggle button
|
|
411
|
+
html.Button(
|
|
412
|
+
'☰',
|
|
413
|
+
id='toggle-sidebar',
|
|
414
|
+
n_clicks=0,
|
|
415
|
+
style={
|
|
416
|
+
'position': 'fixed',
|
|
417
|
+
'top': '20px',
|
|
418
|
+
'left': '20px',
|
|
419
|
+
'z-index': '1000',
|
|
420
|
+
'background-color': '#3498DB',
|
|
421
|
+
'color': 'white',
|
|
422
|
+
'border': 'none',
|
|
423
|
+
'padding': '10px 15px',
|
|
424
|
+
'border-radius': '5px',
|
|
425
|
+
'cursor': 'pointer',
|
|
426
|
+
'font-size': '18px',
|
|
427
|
+
'box-shadow': '0 2px 5px rgba(0,0,0,0.3)',
|
|
428
|
+
},
|
|
352
429
|
),
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
html.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
430
|
+
# Data storage
|
|
431
|
+
dcc.Store(id='elements-store', data=elements),
|
|
432
|
+
# Sidebar
|
|
433
|
+
create_sidebar(),
|
|
434
|
+
# Main content
|
|
435
|
+
html.Div(
|
|
436
|
+
[
|
|
437
|
+
# Header
|
|
438
|
+
html.Div(
|
|
439
|
+
[
|
|
440
|
+
html.H2(
|
|
441
|
+
'Network Visualization', style={'color': 'white', 'margin': '0', 'text-align': 'center'}
|
|
442
|
+
),
|
|
443
|
+
html.Button(
|
|
444
|
+
'Export PNG',
|
|
445
|
+
id='export-btn',
|
|
446
|
+
n_clicks=0,
|
|
447
|
+
style={
|
|
448
|
+
'position': 'absolute',
|
|
449
|
+
'right': '20px',
|
|
450
|
+
'top': '15px',
|
|
451
|
+
'background-color': '#27AE60',
|
|
452
|
+
'color': 'white',
|
|
453
|
+
'border': 'none',
|
|
454
|
+
'padding': '10px 20px',
|
|
455
|
+
'border-radius': '5px',
|
|
456
|
+
'cursor': 'pointer',
|
|
457
|
+
},
|
|
458
|
+
),
|
|
459
|
+
],
|
|
460
|
+
style={
|
|
461
|
+
'background-color': '#34495E',
|
|
462
|
+
'padding': '15px 20px',
|
|
463
|
+
'position': 'relative',
|
|
464
|
+
'border-bottom': '2px solid #3498DB',
|
|
465
|
+
},
|
|
466
|
+
),
|
|
467
|
+
# Cytoscape graph
|
|
468
|
+
cyto.Cytoscape(
|
|
469
|
+
id='cytoscape',
|
|
470
|
+
layout={'name': 'klay'},
|
|
471
|
+
style={'width': '100%', 'height': '70vh'},
|
|
472
|
+
elements=elements,
|
|
473
|
+
stylesheet=VisualizationConfig.DEFAULT_STYLESHEET,
|
|
474
|
+
),
|
|
475
|
+
# Info panel
|
|
476
|
+
html.Div(
|
|
477
|
+
[
|
|
478
|
+
html.H4(
|
|
479
|
+
'Element Information',
|
|
480
|
+
style={
|
|
481
|
+
'color': 'white',
|
|
482
|
+
'margin': '0 0 10px 0',
|
|
483
|
+
'border-bottom': '2px solid #3498DB',
|
|
484
|
+
'padding-bottom': '5px',
|
|
485
|
+
},
|
|
486
|
+
),
|
|
487
|
+
html.Div(
|
|
488
|
+
id='info-panel',
|
|
489
|
+
children=[
|
|
490
|
+
html.P(
|
|
491
|
+
'Click on a node or edge to see details.',
|
|
492
|
+
style={'color': '#95A5A6', 'font-style': 'italic'},
|
|
493
|
+
)
|
|
494
|
+
],
|
|
495
|
+
),
|
|
496
|
+
],
|
|
497
|
+
style={
|
|
498
|
+
'background-color': '#2C3E50',
|
|
499
|
+
'padding': '15px',
|
|
500
|
+
'height': '25vh',
|
|
501
|
+
'overflow-y': 'auto',
|
|
502
|
+
'border-top': '2px solid #34495E',
|
|
503
|
+
},
|
|
504
|
+
),
|
|
505
|
+
],
|
|
506
|
+
id='main-content',
|
|
507
|
+
style={
|
|
508
|
+
'margin-left': '0',
|
|
509
|
+
'background-color': '#1A252F',
|
|
510
|
+
'min-height': '100vh',
|
|
511
|
+
'transition': 'margin-left 0.3s ease',
|
|
512
|
+
},
|
|
513
|
+
),
|
|
514
|
+
]
|
|
515
|
+
)
|
|
374
516
|
|
|
375
517
|
# Callbacks
|
|
376
518
|
@app.callback(
|
|
377
|
-
[Output('sidebar-content', 'style'), Output('main-content', 'style')],
|
|
378
|
-
[Input('toggle-sidebar', 'n_clicks')]
|
|
519
|
+
[Output('sidebar-content', 'style'), Output('main-content', 'style')], [Input('toggle-sidebar', 'n_clicks')]
|
|
379
520
|
)
|
|
380
521
|
def toggle_sidebar(n_clicks):
|
|
381
522
|
is_open = (n_clicks or 0) % 2 == 1
|
|
@@ -383,24 +524,70 @@ def shownetwork(graph: networkx.DiGraph):
|
|
|
383
524
|
main_margin = '280px' if is_open else '0'
|
|
384
525
|
|
|
385
526
|
sidebar_style = {
|
|
386
|
-
'width': '280px',
|
|
387
|
-
'
|
|
388
|
-
'
|
|
389
|
-
'
|
|
390
|
-
'
|
|
527
|
+
'width': '280px',
|
|
528
|
+
'height': '100vh',
|
|
529
|
+
'background-color': '#2C3E50',
|
|
530
|
+
'padding': '20px',
|
|
531
|
+
'position': 'fixed',
|
|
532
|
+
'left': '0',
|
|
533
|
+
'top': '0',
|
|
534
|
+
'overflow-y': 'auto',
|
|
535
|
+
'border-right': '3px solid #34495E',
|
|
536
|
+
'box-shadow': '2px 0 5px rgba(0,0,0,0.1)',
|
|
537
|
+
'z-index': '999',
|
|
538
|
+
'transform': sidebar_transform,
|
|
539
|
+
'transition': 'transform 0.3s ease',
|
|
391
540
|
}
|
|
392
541
|
|
|
393
542
|
main_style = {
|
|
394
|
-
'margin-left': main_margin,
|
|
395
|
-
'
|
|
543
|
+
'margin-left': main_margin,
|
|
544
|
+
'background-color': '#1A252F',
|
|
545
|
+
'min-height': '100vh',
|
|
546
|
+
'transition': 'margin-left 0.3s ease',
|
|
396
547
|
}
|
|
397
548
|
|
|
398
549
|
return sidebar_style, main_style
|
|
399
550
|
|
|
551
|
+
# Combined callback to handle both color scheme changes and reset
|
|
552
|
+
@app.callback(
|
|
553
|
+
[
|
|
554
|
+
Output('bus-color-picker', 'value'),
|
|
555
|
+
Output('source-color-picker', 'value'),
|
|
556
|
+
Output('sink-color-picker', 'value'),
|
|
557
|
+
Output('storage-color-picker', 'value'),
|
|
558
|
+
Output('converter-color-picker', 'value'),
|
|
559
|
+
],
|
|
560
|
+
[Input('color-scheme-dropdown', 'value'), Input('reset-btn', 'n_clicks')],
|
|
561
|
+
)
|
|
562
|
+
def update_color_pickers(color_scheme, reset_clicks):
|
|
563
|
+
"""Update color pickers when color scheme changes or reset is clicked"""
|
|
564
|
+
ctx = callback_context
|
|
565
|
+
|
|
566
|
+
# Determine which input triggered the callback
|
|
567
|
+
if ctx.triggered:
|
|
568
|
+
trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]
|
|
569
|
+
if trigger_id == 'reset-btn' and reset_clicks and reset_clicks > 0:
|
|
570
|
+
# Reset was clicked, use default colors
|
|
571
|
+
colors = VisualizationConfig.DEFAULT_COLORS
|
|
572
|
+
else:
|
|
573
|
+
# Color scheme changed
|
|
574
|
+
colors = VisualizationConfig.COLOR_PRESETS.get(color_scheme, VisualizationConfig.DEFAULT_COLORS)
|
|
575
|
+
else:
|
|
576
|
+
# Initial load
|
|
577
|
+
colors = VisualizationConfig.COLOR_PRESETS.get(color_scheme, VisualizationConfig.DEFAULT_COLORS)
|
|
578
|
+
|
|
579
|
+
return (
|
|
580
|
+
{'hex': colors['Bus']},
|
|
581
|
+
{'hex': colors['Source']},
|
|
582
|
+
{'hex': colors['Sink']},
|
|
583
|
+
{'hex': colors['Storage']},
|
|
584
|
+
{'hex': colors['Converter']},
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# Updated main visualization callback - simplified logic
|
|
400
588
|
@app.callback(
|
|
401
589
|
[Output('cytoscape', 'elements'), Output('cytoscape', 'stylesheet')],
|
|
402
590
|
[
|
|
403
|
-
Input('color-scheme-dropdown', 'value'),
|
|
404
591
|
Input('bus-color-picker', 'value'),
|
|
405
592
|
Input('source-color-picker', 'value'),
|
|
406
593
|
Input('sink-color-picker', 'value'),
|
|
@@ -410,30 +597,33 @@ def shownetwork(graph: networkx.DiGraph):
|
|
|
410
597
|
Input('node-size-slider', 'value'),
|
|
411
598
|
Input('font-size-slider', 'value'),
|
|
412
599
|
],
|
|
413
|
-
[State('elements-store', 'data')]
|
|
600
|
+
[State('elements-store', 'data')],
|
|
414
601
|
)
|
|
415
|
-
def update_visualization(
|
|
416
|
-
|
|
417
|
-
|
|
602
|
+
def update_visualization(
|
|
603
|
+
bus_color,
|
|
604
|
+
source_color,
|
|
605
|
+
sink_color,
|
|
606
|
+
storage_color,
|
|
607
|
+
converter_color,
|
|
608
|
+
edge_color,
|
|
609
|
+
node_size,
|
|
610
|
+
font_size,
|
|
611
|
+
stored_elements,
|
|
612
|
+
):
|
|
613
|
+
"""Update visualization based on current color picker values"""
|
|
418
614
|
if not stored_elements:
|
|
419
615
|
return no_update, no_update
|
|
420
616
|
|
|
421
|
-
#
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
'Other': '#27AE60',
|
|
432
|
-
}
|
|
433
|
-
else:
|
|
434
|
-
# Use preset scheme
|
|
435
|
-
colors = VisualizationConfig.COLOR_PRESETS.get(color_scheme,
|
|
436
|
-
VisualizationConfig.DEFAULT_COLORS)
|
|
617
|
+
# Use colors from pickers (which are now synced with scheme selection)
|
|
618
|
+
default_colors = VisualizationConfig.DEFAULT_COLORS
|
|
619
|
+
colors = {
|
|
620
|
+
'Bus': bus_color.get('hex') if bus_color else default_colors['Bus'],
|
|
621
|
+
'Source': source_color.get('hex') if source_color else default_colors['Source'],
|
|
622
|
+
'Sink': sink_color.get('hex') if sink_color else default_colors['Sink'],
|
|
623
|
+
'Storage': storage_color.get('hex') if storage_color else default_colors['Storage'],
|
|
624
|
+
'Converter': converter_color.get('hex') if converter_color else default_colors['Converter'],
|
|
625
|
+
'Other': default_colors['Other'],
|
|
626
|
+
}
|
|
437
627
|
|
|
438
628
|
# Update element colors
|
|
439
629
|
updated_elements = []
|
|
@@ -497,73 +687,62 @@ def shownetwork(graph: networkx.DiGraph):
|
|
|
497
687
|
return updated_elements, stylesheet
|
|
498
688
|
|
|
499
689
|
@app.callback(
|
|
500
|
-
Output('info-panel', 'children'),
|
|
501
|
-
[Input('cytoscape', 'tapNodeData'), Input('cytoscape', 'tapEdgeData')]
|
|
690
|
+
Output('info-panel', 'children'), [Input('cytoscape', 'tapNodeData'), Input('cytoscape', 'tapEdgeData')]
|
|
502
691
|
)
|
|
503
692
|
def display_element_info(node_data, edge_data):
|
|
504
693
|
ctx = callback_context
|
|
505
694
|
if not ctx.triggered:
|
|
506
|
-
return [
|
|
507
|
-
|
|
695
|
+
return [
|
|
696
|
+
html.P('Click on a node or edge to see details.', style={'color': '#95A5A6', 'font-style': 'italic'})
|
|
697
|
+
]
|
|
508
698
|
|
|
509
699
|
# Determine what was clicked
|
|
510
700
|
if ctx.triggered[0]['prop_id'] == 'cytoscape.tapNodeData' and node_data:
|
|
511
701
|
return [
|
|
512
|
-
html.H5(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
html.Pre(
|
|
517
|
-
|
|
518
|
-
|
|
702
|
+
html.H5(
|
|
703
|
+
f'Node: {node_data.get("label", "Unknown")}', style={'color': 'white', 'margin-bottom': '10px'}
|
|
704
|
+
),
|
|
705
|
+
html.P(f'Type: {node_data.get("element_type", "Unknown")}', style={'color': '#BDC3C7'}),
|
|
706
|
+
html.Pre(
|
|
707
|
+
node_data.get('parameters', 'No parameters'),
|
|
708
|
+
style={'color': '#BDC3C7', 'font-size': '11px', 'white-space': 'pre-wrap'},
|
|
709
|
+
),
|
|
519
710
|
]
|
|
520
711
|
elif ctx.triggered[0]['prop_id'] == 'cytoscape.tapEdgeData' and edge_data:
|
|
521
712
|
return [
|
|
522
|
-
html.H5(
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
html.Pre(
|
|
527
|
-
|
|
528
|
-
|
|
713
|
+
html.H5(
|
|
714
|
+
f'Edge: {edge_data.get("label", "Unknown")}', style={'color': 'white', 'margin-bottom': '10px'}
|
|
715
|
+
),
|
|
716
|
+
html.P(f'{edge_data.get("source", "")} → {edge_data.get("target", "")}', style={'color': '#E67E22'}),
|
|
717
|
+
html.Pre(
|
|
718
|
+
edge_data.get('parameters', 'No parameters'),
|
|
719
|
+
style={'color': '#BDC3C7', 'font-size': '11px', 'white-space': 'pre-wrap'},
|
|
720
|
+
),
|
|
529
721
|
]
|
|
530
722
|
|
|
531
|
-
return [html.P('Click on a node or edge to see details.',
|
|
532
|
-
style={'color': '#95A5A6', 'font-style': 'italic'})]
|
|
723
|
+
return [html.P('Click on a node or edge to see details.', style={'color': '#95A5A6', 'font-style': 'italic'})]
|
|
533
724
|
|
|
534
|
-
@app.callback(
|
|
535
|
-
Output('cytoscape', 'layout'),
|
|
536
|
-
Input('layout-dropdown', 'value')
|
|
537
|
-
)
|
|
725
|
+
@app.callback(Output('cytoscape', 'layout'), Input('layout-dropdown', 'value'))
|
|
538
726
|
def update_layout(selected_layout):
|
|
539
727
|
return {'name': selected_layout}
|
|
540
728
|
|
|
541
|
-
# Reset callback
|
|
729
|
+
# Reset callback for non-color-picker controls
|
|
542
730
|
@app.callback(
|
|
543
731
|
[
|
|
544
732
|
Output('color-scheme-dropdown', 'value'),
|
|
545
|
-
Output('bus-color-picker', 'value'),
|
|
546
|
-
Output('source-color-picker', 'value'),
|
|
547
|
-
Output('sink-color-picker', 'value'),
|
|
548
|
-
Output('storage-color-picker', 'value'),
|
|
549
|
-
Output('converter-color-picker', 'value'),
|
|
550
733
|
Output('edge-color-picker', 'value'),
|
|
551
734
|
Output('node-size-slider', 'value'),
|
|
552
735
|
Output('font-size-slider', 'value'),
|
|
553
736
|
Output('layout-dropdown', 'value'),
|
|
554
737
|
],
|
|
555
|
-
[Input('reset-btn', 'n_clicks')]
|
|
738
|
+
[Input('reset-btn', 'n_clicks')],
|
|
556
739
|
)
|
|
557
740
|
def reset_controls(n_clicks):
|
|
741
|
+
"""Reset all controls to defaults (color pickers handled separately)"""
|
|
558
742
|
if n_clicks and n_clicks > 0:
|
|
559
743
|
return (
|
|
560
|
-
'Default', # color scheme
|
|
561
|
-
{'hex': '#
|
|
562
|
-
{'hex': '#F1C40F'}, # source
|
|
563
|
-
{'hex': '#F1C40F'}, # sink
|
|
564
|
-
{'hex': '#2980B9'}, # storage
|
|
565
|
-
{'hex': '#D35400'}, # converter
|
|
566
|
-
{'hex': '#808080'}, # edge
|
|
744
|
+
'Default', # color scheme (will trigger color picker updates)
|
|
745
|
+
{'hex': '#808080'}, # edge color
|
|
567
746
|
90, # node size
|
|
568
747
|
10, # font size
|
|
569
748
|
'klay', # layout
|