flixopt 2.2.0b0__py3-none-any.whl → 3.0.0__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.
- flixopt/__init__.py +35 -1
- flixopt/aggregation.py +60 -81
- flixopt/calculation.py +381 -196
- flixopt/components.py +1022 -359
- flixopt/config.py +553 -191
- flixopt/core.py +475 -1315
- flixopt/effects.py +477 -214
- flixopt/elements.py +591 -344
- flixopt/features.py +403 -957
- flixopt/flow_system.py +781 -293
- flixopt/interface.py +1159 -189
- flixopt/io.py +50 -55
- flixopt/linear_converters.py +384 -92
- flixopt/modeling.py +759 -0
- flixopt/network_app.py +789 -0
- flixopt/plotting.py +273 -135
- flixopt/results.py +639 -383
- flixopt/solvers.py +25 -21
- flixopt/structure.py +928 -442
- flixopt/utils.py +34 -5
- flixopt-3.0.0.dist-info/METADATA +209 -0
- flixopt-3.0.0.dist-info/RECORD +26 -0
- {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/WHEEL +1 -1
- flixopt-3.0.0.dist-info/top_level.txt +1 -0
- docs/examples/00-Minimal Example.md +0 -5
- docs/examples/01-Basic Example.md +0 -5
- docs/examples/02-Complex Example.md +0 -10
- docs/examples/03-Calculation Modes.md +0 -5
- docs/examples/index.md +0 -5
- docs/faq/contribute.md +0 -49
- docs/faq/index.md +0 -3
- docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
- docs/images/architecture_flixOpt.png +0 -0
- docs/images/flixopt-icon.svg +0 -1
- docs/javascripts/mathjax.js +0 -18
- docs/release-notes/_template.txt +0 -32
- docs/release-notes/index.md +0 -7
- docs/release-notes/v2.0.0.md +0 -93
- docs/release-notes/v2.0.1.md +0 -12
- docs/release-notes/v2.1.0.md +0 -31
- docs/release-notes/v2.2.0.md +0 -55
- docs/user-guide/Mathematical Notation/Bus.md +0 -33
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
- docs/user-guide/Mathematical Notation/Flow.md +0 -26
- docs/user-guide/Mathematical Notation/Investment.md +0 -115
- docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
- docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
- docs/user-guide/Mathematical Notation/Storage.md +0 -44
- docs/user-guide/Mathematical Notation/index.md +0 -22
- docs/user-guide/Mathematical Notation/others.md +0 -3
- docs/user-guide/index.md +0 -124
- flixopt/config.yaml +0 -10
- flixopt-2.2.0b0.dist-info/METADATA +0 -146
- flixopt-2.2.0b0.dist-info/RECORD +0 -59
- flixopt-2.2.0b0.dist-info/top_level.txt +0 -5
- pics/architecture_flixOpt-pre2.0.0.png +0 -0
- pics/architecture_flixOpt.png +0 -0
- pics/flixOpt_plotting.jpg +0 -0
- pics/flixopt-icon.svg +0 -1
- pics/pics.pptx +0 -0
- scripts/gen_ref_pages.py +0 -54
- tests/ressources/Zeitreihen2020.csv +0 -35137
- {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/licenses/LICENSE +0 -0
flixopt/network_app.py
ADDED
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import socket
|
|
5
|
+
import threading
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import dash_cytoscape as cyto
|
|
10
|
+
import dash_daq as daq
|
|
11
|
+
import networkx as nx
|
|
12
|
+
from dash import Dash, Input, Output, State, callback_context, dcc, html, no_update
|
|
13
|
+
from werkzeug.serving import make_server
|
|
14
|
+
|
|
15
|
+
DASH_CYTOSCAPE_AVAILABLE = True
|
|
16
|
+
VISUALIZATION_ERROR = None
|
|
17
|
+
except ImportError as e:
|
|
18
|
+
DASH_CYTOSCAPE_AVAILABLE = False
|
|
19
|
+
VISUALIZATION_ERROR = str(e)
|
|
20
|
+
|
|
21
|
+
from .components import LinearConverter, Sink, Source, SourceAndSink, Storage
|
|
22
|
+
from .elements import Bus
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from .flow_system import FlowSystem
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger('flixopt')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Configuration class for better organization
|
|
31
|
+
class VisualizationConfig:
|
|
32
|
+
"""Configuration constants for the visualization"""
|
|
33
|
+
|
|
34
|
+
DEFAULT_COLORS = {
|
|
35
|
+
'Bus': '#7F8C8D',
|
|
36
|
+
'Source': '#F1C40F',
|
|
37
|
+
'Sink': '#F1C40F',
|
|
38
|
+
'Storage': '#2980B9',
|
|
39
|
+
'Converter': '#D35400',
|
|
40
|
+
'Other': '#27AE60',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
COLOR_PRESETS = {
|
|
44
|
+
'Default': DEFAULT_COLORS,
|
|
45
|
+
'Vibrant': {
|
|
46
|
+
'Bus': '#FF6B6B',
|
|
47
|
+
'Source': '#4ECDC4',
|
|
48
|
+
'Sink': '#45B7D1',
|
|
49
|
+
'Storage': '#96CEB4',
|
|
50
|
+
'Converter': '#FFEAA7',
|
|
51
|
+
'Other': '#DDA0DD',
|
|
52
|
+
},
|
|
53
|
+
'Dark': {
|
|
54
|
+
'Bus': '#2C3E50',
|
|
55
|
+
'Source': '#34495E',
|
|
56
|
+
'Sink': '#7F8C8D',
|
|
57
|
+
'Storage': '#95A5A6',
|
|
58
|
+
'Converter': '#BDC3C7',
|
|
59
|
+
'Other': '#ECF0F1',
|
|
60
|
+
},
|
|
61
|
+
'Pastel': {
|
|
62
|
+
'Bus': '#FFB3BA',
|
|
63
|
+
'Source': '#BAFFC9',
|
|
64
|
+
'Sink': '#BAE1FF',
|
|
65
|
+
'Storage': '#FFFFBA',
|
|
66
|
+
'Converter': '#FFDFBA',
|
|
67
|
+
'Other': '#E0BBE4',
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
DEFAULT_STYLESHEET = [
|
|
72
|
+
{
|
|
73
|
+
'selector': 'node',
|
|
74
|
+
'style': {
|
|
75
|
+
'content': 'data(label)',
|
|
76
|
+
'background-color': 'data(color)',
|
|
77
|
+
'font-size': 10,
|
|
78
|
+
'color': 'white',
|
|
79
|
+
'text-valign': 'center',
|
|
80
|
+
'text-halign': 'center',
|
|
81
|
+
'width': '90px',
|
|
82
|
+
'height': '70px',
|
|
83
|
+
'shape': 'data(shape)',
|
|
84
|
+
'text-outline-color': 'black',
|
|
85
|
+
'text-outline-width': 0.5,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
'selector': '[shape = "custom-source"]',
|
|
90
|
+
'style': {
|
|
91
|
+
'shape': 'polygon',
|
|
92
|
+
'shape-polygon-points': '-0.5 0.5, 0.5 0.5, 1 -0.5, -1 -0.5',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
'selector': '[shape = "custom-sink"]',
|
|
97
|
+
'style': {
|
|
98
|
+
'shape': 'polygon',
|
|
99
|
+
'shape-polygon-points': '-0.5 -0.5, 0.5 -0.5, 1 0.5, -1 0.5',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
'selector': 'edge',
|
|
104
|
+
'style': {
|
|
105
|
+
'curve-style': 'straight',
|
|
106
|
+
'width': 2,
|
|
107
|
+
'line-color': 'gray',
|
|
108
|
+
'target-arrow-color': 'gray',
|
|
109
|
+
'target-arrow-shape': 'triangle',
|
|
110
|
+
'arrow-scale': 2,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def flow_graph(flow_system: FlowSystem) -> nx.DiGraph:
|
|
117
|
+
"""Convert FlowSystem to NetworkX graph - simplified and more robust"""
|
|
118
|
+
if not DASH_CYTOSCAPE_AVAILABLE:
|
|
119
|
+
raise ImportError(
|
|
120
|
+
'Network visualization requires optional dependencies. '
|
|
121
|
+
'Install with: pip install flixopt[viz] or '
|
|
122
|
+
'pip install dash dash-cytoscape networkx werkzeug. '
|
|
123
|
+
f'Original error: {VISUALIZATION_ERROR}'
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
nodes = list(flow_system.components.values()) + list(flow_system.buses.values())
|
|
127
|
+
edges = list(flow_system.flows.values())
|
|
128
|
+
|
|
129
|
+
def get_element_type(element):
|
|
130
|
+
"""Determine element type for coloring"""
|
|
131
|
+
if isinstance(element, Bus):
|
|
132
|
+
return 'Bus'
|
|
133
|
+
elif isinstance(element, Source):
|
|
134
|
+
return 'Source'
|
|
135
|
+
elif isinstance(element, (Sink, SourceAndSink)):
|
|
136
|
+
return 'Sink'
|
|
137
|
+
elif isinstance(element, Storage):
|
|
138
|
+
return 'Storage'
|
|
139
|
+
elif isinstance(element, LinearConverter):
|
|
140
|
+
return 'Converter'
|
|
141
|
+
else:
|
|
142
|
+
return 'Other'
|
|
143
|
+
|
|
144
|
+
def get_shape(element):
|
|
145
|
+
"""Determine node shape"""
|
|
146
|
+
if isinstance(element, Bus):
|
|
147
|
+
return 'ellipse'
|
|
148
|
+
elif isinstance(element, Source):
|
|
149
|
+
return 'custom-source'
|
|
150
|
+
elif isinstance(element, (Sink, SourceAndSink)):
|
|
151
|
+
return 'custom-sink'
|
|
152
|
+
else:
|
|
153
|
+
return 'rectangle'
|
|
154
|
+
|
|
155
|
+
graph = nx.DiGraph()
|
|
156
|
+
|
|
157
|
+
# Add nodes with attributes
|
|
158
|
+
for node in nodes:
|
|
159
|
+
graph.add_node(
|
|
160
|
+
node.label_full,
|
|
161
|
+
color=VisualizationConfig.DEFAULT_COLORS[get_element_type(node)],
|
|
162
|
+
shape=get_shape(node),
|
|
163
|
+
element_type=get_element_type(node),
|
|
164
|
+
parameters=str(node),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Add edges
|
|
168
|
+
for edge in edges:
|
|
169
|
+
try:
|
|
170
|
+
graph.add_edge(
|
|
171
|
+
u_of_edge=edge.bus if edge.is_input_in_component else edge.component,
|
|
172
|
+
v_of_edge=edge.component if edge.is_input_in_component else edge.bus,
|
|
173
|
+
label=edge.label_full,
|
|
174
|
+
parameters=edge.__str__().replace(')', '\n)'),
|
|
175
|
+
)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f'Failed to add edge {edge}: {e}')
|
|
178
|
+
|
|
179
|
+
return graph
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def make_cytoscape_elements(graph: nx.DiGraph) -> list[dict[str, Any]]:
|
|
183
|
+
"""Convert NetworkX graph to Cytoscape elements"""
|
|
184
|
+
elements = []
|
|
185
|
+
|
|
186
|
+
# Add nodes
|
|
187
|
+
for node_id in graph.nodes():
|
|
188
|
+
node_data = graph.nodes[node_id]
|
|
189
|
+
elements.append(
|
|
190
|
+
{
|
|
191
|
+
'data': {
|
|
192
|
+
'id': node_id,
|
|
193
|
+
'label': node_id,
|
|
194
|
+
'color': node_data.get('color', '#7F8C8D'),
|
|
195
|
+
'shape': node_data.get('shape', 'rectangle'),
|
|
196
|
+
'element_type': node_data.get('element_type', 'Other'),
|
|
197
|
+
'parameters': node_data.get('parameters', ''),
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Add edges
|
|
203
|
+
for u, v in graph.edges():
|
|
204
|
+
edge_data = graph.edges[u, v]
|
|
205
|
+
elements.append(
|
|
206
|
+
{
|
|
207
|
+
'data': {
|
|
208
|
+
'source': u,
|
|
209
|
+
'target': v,
|
|
210
|
+
'id': f'{u}-{v}',
|
|
211
|
+
'label': edge_data.get('label', ''),
|
|
212
|
+
'parameters': edge_data.get('parameters', ''),
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return elements
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def create_color_picker_input(label: str, input_id: str, default_color: str):
|
|
221
|
+
"""Create a compact color picker with DAQ ColorPicker"""
|
|
222
|
+
return html.Div(
|
|
223
|
+
[
|
|
224
|
+
html.Label(
|
|
225
|
+
label, style={'color': 'white', 'font-size': '12px', 'margin-bottom': '5px', 'display': 'block'}
|
|
226
|
+
),
|
|
227
|
+
daq.ColorPicker(
|
|
228
|
+
id=input_id,
|
|
229
|
+
label='',
|
|
230
|
+
value={'hex': default_color},
|
|
231
|
+
size=200,
|
|
232
|
+
theme={'dark': True},
|
|
233
|
+
style={'margin-bottom': '10px'},
|
|
234
|
+
),
|
|
235
|
+
]
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def create_style_section(title: str, children: list):
|
|
240
|
+
"""Create a collapsible section for organizing controls"""
|
|
241
|
+
return html.Div(
|
|
242
|
+
[
|
|
243
|
+
html.H4(
|
|
244
|
+
title,
|
|
245
|
+
style={
|
|
246
|
+
'color': 'white',
|
|
247
|
+
'margin-bottom': '10px',
|
|
248
|
+
'border-bottom': '2px solid #3498DB',
|
|
249
|
+
'padding-bottom': '5px',
|
|
250
|
+
},
|
|
251
|
+
),
|
|
252
|
+
html.Div(children, style={'margin-bottom': '20px'}),
|
|
253
|
+
]
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def create_sidebar():
|
|
258
|
+
"""Create the main sidebar with improved organization"""
|
|
259
|
+
return html.Div(
|
|
260
|
+
[
|
|
261
|
+
html.Div(
|
|
262
|
+
[
|
|
263
|
+
html.H3(
|
|
264
|
+
'Style Controls',
|
|
265
|
+
style={
|
|
266
|
+
'color': 'white',
|
|
267
|
+
'margin-bottom': '20px',
|
|
268
|
+
'text-align': 'center',
|
|
269
|
+
'border-bottom': '3px solid #9B59B6',
|
|
270
|
+
'padding-bottom': '10px',
|
|
271
|
+
},
|
|
272
|
+
),
|
|
273
|
+
# Layout Section
|
|
274
|
+
create_style_section(
|
|
275
|
+
'Layout',
|
|
276
|
+
[
|
|
277
|
+
dcc.Dropdown(
|
|
278
|
+
id='layout-dropdown',
|
|
279
|
+
options=[
|
|
280
|
+
{'label': 'Klay (horizontal)', 'value': 'klay'},
|
|
281
|
+
{'label': 'Dagre (vertical)', 'value': 'dagre'},
|
|
282
|
+
{'label': 'Breadthfirst', 'value': 'breadthfirst'},
|
|
283
|
+
{'label': 'Cose (force-directed)', 'value': 'cose'},
|
|
284
|
+
{'label': 'Grid', 'value': 'grid'},
|
|
285
|
+
{'label': 'Circle', 'value': 'circle'},
|
|
286
|
+
],
|
|
287
|
+
value='klay',
|
|
288
|
+
clearable=False,
|
|
289
|
+
style={'width': '100%'},
|
|
290
|
+
),
|
|
291
|
+
],
|
|
292
|
+
),
|
|
293
|
+
# Color Scheme Section
|
|
294
|
+
create_style_section(
|
|
295
|
+
'Color Scheme',
|
|
296
|
+
[
|
|
297
|
+
dcc.Dropdown(
|
|
298
|
+
id='color-scheme-dropdown',
|
|
299
|
+
options=[{'label': k, 'value': k} for k in VisualizationConfig.COLOR_PRESETS.keys()],
|
|
300
|
+
value='Default',
|
|
301
|
+
style={'width': '100%', 'margin-bottom': '10px'},
|
|
302
|
+
),
|
|
303
|
+
],
|
|
304
|
+
),
|
|
305
|
+
# Color Pickers Section
|
|
306
|
+
create_style_section(
|
|
307
|
+
'Custom Colors',
|
|
308
|
+
[
|
|
309
|
+
create_color_picker_input('Bus', 'bus-color-picker', '#7F8C8D'),
|
|
310
|
+
create_color_picker_input('Source', 'source-color-picker', '#F1C40F'),
|
|
311
|
+
create_color_picker_input('Sink', 'sink-color-picker', '#F1C40F'),
|
|
312
|
+
create_color_picker_input('Storage', 'storage-color-picker', '#2980B9'),
|
|
313
|
+
create_color_picker_input('Converter', 'converter-color-picker', '#D35400'),
|
|
314
|
+
create_color_picker_input('Edge', 'edge-color-picker', '#808080'),
|
|
315
|
+
],
|
|
316
|
+
),
|
|
317
|
+
# Node Settings
|
|
318
|
+
create_style_section(
|
|
319
|
+
'Node Settings',
|
|
320
|
+
[
|
|
321
|
+
html.Label('Size', style={'color': 'white', 'font-size': '12px'}),
|
|
322
|
+
dcc.Slider(
|
|
323
|
+
id='node-size-slider',
|
|
324
|
+
min=50,
|
|
325
|
+
max=150,
|
|
326
|
+
step=10,
|
|
327
|
+
value=90,
|
|
328
|
+
marks={
|
|
329
|
+
i: {'label': str(i), 'style': {'color': 'white', 'font-size': '10px'}}
|
|
330
|
+
for i in range(50, 151, 25)
|
|
331
|
+
},
|
|
332
|
+
tooltip={'placement': 'bottom', 'always_visible': True},
|
|
333
|
+
),
|
|
334
|
+
html.Br(),
|
|
335
|
+
html.Label('Font Size', style={'color': 'white', 'font-size': '12px'}),
|
|
336
|
+
dcc.Slider(
|
|
337
|
+
id='font-size-slider',
|
|
338
|
+
min=8,
|
|
339
|
+
max=20,
|
|
340
|
+
step=1,
|
|
341
|
+
value=10,
|
|
342
|
+
marks={
|
|
343
|
+
i: {'label': str(i), 'style': {'color': 'white', 'font-size': '10px'}}
|
|
344
|
+
for i in range(8, 21, 2)
|
|
345
|
+
},
|
|
346
|
+
tooltip={'placement': 'bottom', 'always_visible': True},
|
|
347
|
+
),
|
|
348
|
+
],
|
|
349
|
+
),
|
|
350
|
+
# Reset Button
|
|
351
|
+
html.Div(
|
|
352
|
+
[
|
|
353
|
+
html.Button(
|
|
354
|
+
'Reset to Defaults',
|
|
355
|
+
id='reset-btn',
|
|
356
|
+
n_clicks=0,
|
|
357
|
+
style={
|
|
358
|
+
'width': '100%',
|
|
359
|
+
'background-color': '#E74C3C',
|
|
360
|
+
'color': 'white',
|
|
361
|
+
'border': 'none',
|
|
362
|
+
'padding': '10px',
|
|
363
|
+
'border-radius': '5px',
|
|
364
|
+
'cursor': 'pointer',
|
|
365
|
+
'margin-top': '20px',
|
|
366
|
+
},
|
|
367
|
+
),
|
|
368
|
+
]
|
|
369
|
+
),
|
|
370
|
+
],
|
|
371
|
+
id='sidebar-content',
|
|
372
|
+
style={
|
|
373
|
+
'width': '280px',
|
|
374
|
+
'height': '100vh',
|
|
375
|
+
'background-color': '#2C3E50',
|
|
376
|
+
'padding': '20px',
|
|
377
|
+
'position': 'fixed',
|
|
378
|
+
'left': '0',
|
|
379
|
+
'top': '0',
|
|
380
|
+
'overflow-y': 'auto',
|
|
381
|
+
'border-right': '3px solid #34495E',
|
|
382
|
+
'box-shadow': '2px 0 5px rgba(0,0,0,0.1)',
|
|
383
|
+
'z-index': '999',
|
|
384
|
+
'transform': 'translateX(-100%)',
|
|
385
|
+
'transition': 'transform 0.3s ease',
|
|
386
|
+
},
|
|
387
|
+
)
|
|
388
|
+
]
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def shownetwork(graph: nx.DiGraph):
|
|
393
|
+
"""Main function to create and run the network visualization"""
|
|
394
|
+
if not DASH_CYTOSCAPE_AVAILABLE:
|
|
395
|
+
raise ImportError(f'Required packages not available: {VISUALIZATION_ERROR}')
|
|
396
|
+
|
|
397
|
+
app = Dash(__name__, suppress_callback_exceptions=True)
|
|
398
|
+
|
|
399
|
+
# Load extra layouts
|
|
400
|
+
cyto.load_extra_layouts()
|
|
401
|
+
|
|
402
|
+
# Create initial elements
|
|
403
|
+
elements = make_cytoscape_elements(graph)
|
|
404
|
+
|
|
405
|
+
# App Layout
|
|
406
|
+
app.layout = html.Div(
|
|
407
|
+
[
|
|
408
|
+
# Toggle button
|
|
409
|
+
html.Button(
|
|
410
|
+
'☰',
|
|
411
|
+
id='toggle-sidebar',
|
|
412
|
+
n_clicks=0,
|
|
413
|
+
style={
|
|
414
|
+
'position': 'fixed',
|
|
415
|
+
'top': '20px',
|
|
416
|
+
'left': '20px',
|
|
417
|
+
'z-index': '1000',
|
|
418
|
+
'background-color': '#3498DB',
|
|
419
|
+
'color': 'white',
|
|
420
|
+
'border': 'none',
|
|
421
|
+
'padding': '10px 15px',
|
|
422
|
+
'border-radius': '5px',
|
|
423
|
+
'cursor': 'pointer',
|
|
424
|
+
'font-size': '18px',
|
|
425
|
+
'box-shadow': '0 2px 5px rgba(0,0,0,0.3)',
|
|
426
|
+
},
|
|
427
|
+
),
|
|
428
|
+
# Data storage
|
|
429
|
+
dcc.Store(id='elements-store', data=elements),
|
|
430
|
+
# Sidebar
|
|
431
|
+
create_sidebar(),
|
|
432
|
+
# Main content
|
|
433
|
+
html.Div(
|
|
434
|
+
[
|
|
435
|
+
# Header
|
|
436
|
+
html.Div(
|
|
437
|
+
[
|
|
438
|
+
html.H2(
|
|
439
|
+
'Network Visualization', style={'color': 'white', 'margin': '0', 'text-align': 'center'}
|
|
440
|
+
),
|
|
441
|
+
html.Button(
|
|
442
|
+
'Export PNG',
|
|
443
|
+
id='export-btn',
|
|
444
|
+
n_clicks=0,
|
|
445
|
+
style={
|
|
446
|
+
'position': 'absolute',
|
|
447
|
+
'right': '20px',
|
|
448
|
+
'top': '15px',
|
|
449
|
+
'background-color': '#27AE60',
|
|
450
|
+
'color': 'white',
|
|
451
|
+
'border': 'none',
|
|
452
|
+
'padding': '10px 20px',
|
|
453
|
+
'border-radius': '5px',
|
|
454
|
+
'cursor': 'pointer',
|
|
455
|
+
},
|
|
456
|
+
),
|
|
457
|
+
],
|
|
458
|
+
style={
|
|
459
|
+
'background-color': '#34495E',
|
|
460
|
+
'padding': '15px 20px',
|
|
461
|
+
'position': 'relative',
|
|
462
|
+
'border-bottom': '2px solid #3498DB',
|
|
463
|
+
},
|
|
464
|
+
),
|
|
465
|
+
# Cytoscape graph
|
|
466
|
+
cyto.Cytoscape(
|
|
467
|
+
id='cytoscape',
|
|
468
|
+
layout={'name': 'klay'},
|
|
469
|
+
style={'width': '100%', 'height': '70vh'},
|
|
470
|
+
elements=elements,
|
|
471
|
+
stylesheet=VisualizationConfig.DEFAULT_STYLESHEET,
|
|
472
|
+
),
|
|
473
|
+
# Info panel
|
|
474
|
+
html.Div(
|
|
475
|
+
[
|
|
476
|
+
html.H4(
|
|
477
|
+
'Element Information',
|
|
478
|
+
style={
|
|
479
|
+
'color': 'white',
|
|
480
|
+
'margin': '0 0 10px 0',
|
|
481
|
+
'border-bottom': '2px solid #3498DB',
|
|
482
|
+
'padding-bottom': '5px',
|
|
483
|
+
},
|
|
484
|
+
),
|
|
485
|
+
html.Div(
|
|
486
|
+
id='info-panel',
|
|
487
|
+
children=[
|
|
488
|
+
html.P(
|
|
489
|
+
'Click on a node or edge to see details.',
|
|
490
|
+
style={'color': '#95A5A6', 'font-style': 'italic'},
|
|
491
|
+
)
|
|
492
|
+
],
|
|
493
|
+
),
|
|
494
|
+
],
|
|
495
|
+
style={
|
|
496
|
+
'background-color': '#2C3E50',
|
|
497
|
+
'padding': '15px',
|
|
498
|
+
'height': '25vh',
|
|
499
|
+
'overflow-y': 'auto',
|
|
500
|
+
'border-top': '2px solid #34495E',
|
|
501
|
+
},
|
|
502
|
+
),
|
|
503
|
+
],
|
|
504
|
+
id='main-content',
|
|
505
|
+
style={
|
|
506
|
+
'margin-left': '0',
|
|
507
|
+
'background-color': '#1A252F',
|
|
508
|
+
'min-height': '100vh',
|
|
509
|
+
'transition': 'margin-left 0.3s ease',
|
|
510
|
+
},
|
|
511
|
+
),
|
|
512
|
+
]
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Callbacks
|
|
516
|
+
@app.callback(
|
|
517
|
+
[Output('sidebar-content', 'style'), Output('main-content', 'style')], [Input('toggle-sidebar', 'n_clicks')]
|
|
518
|
+
)
|
|
519
|
+
def toggle_sidebar(n_clicks):
|
|
520
|
+
is_open = (n_clicks or 0) % 2 == 1
|
|
521
|
+
sidebar_transform = 'translateX(0)' if is_open else 'translateX(-100%)'
|
|
522
|
+
main_margin = '280px' if is_open else '0'
|
|
523
|
+
|
|
524
|
+
sidebar_style = {
|
|
525
|
+
'width': '280px',
|
|
526
|
+
'height': '100vh',
|
|
527
|
+
'background-color': '#2C3E50',
|
|
528
|
+
'padding': '20px',
|
|
529
|
+
'position': 'fixed',
|
|
530
|
+
'left': '0',
|
|
531
|
+
'top': '0',
|
|
532
|
+
'overflow-y': 'auto',
|
|
533
|
+
'border-right': '3px solid #34495E',
|
|
534
|
+
'box-shadow': '2px 0 5px rgba(0,0,0,0.1)',
|
|
535
|
+
'z-index': '999',
|
|
536
|
+
'transform': sidebar_transform,
|
|
537
|
+
'transition': 'transform 0.3s ease',
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
main_style = {
|
|
541
|
+
'margin-left': main_margin,
|
|
542
|
+
'background-color': '#1A252F',
|
|
543
|
+
'min-height': '100vh',
|
|
544
|
+
'transition': 'margin-left 0.3s ease',
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return sidebar_style, main_style
|
|
548
|
+
|
|
549
|
+
# Combined callback to handle both color scheme changes and reset
|
|
550
|
+
@app.callback(
|
|
551
|
+
[
|
|
552
|
+
Output('bus-color-picker', 'value'),
|
|
553
|
+
Output('source-color-picker', 'value'),
|
|
554
|
+
Output('sink-color-picker', 'value'),
|
|
555
|
+
Output('storage-color-picker', 'value'),
|
|
556
|
+
Output('converter-color-picker', 'value'),
|
|
557
|
+
],
|
|
558
|
+
[Input('color-scheme-dropdown', 'value'), Input('reset-btn', 'n_clicks')],
|
|
559
|
+
)
|
|
560
|
+
def update_color_pickers(color_scheme, reset_clicks):
|
|
561
|
+
"""Update color pickers when color scheme changes or reset is clicked"""
|
|
562
|
+
ctx = callback_context
|
|
563
|
+
|
|
564
|
+
# Determine which input triggered the callback
|
|
565
|
+
if ctx.triggered:
|
|
566
|
+
trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]
|
|
567
|
+
if trigger_id == 'reset-btn' and reset_clicks and reset_clicks > 0:
|
|
568
|
+
# Reset was clicked, use default colors
|
|
569
|
+
colors = VisualizationConfig.DEFAULT_COLORS
|
|
570
|
+
else:
|
|
571
|
+
# Color scheme changed
|
|
572
|
+
colors = VisualizationConfig.COLOR_PRESETS.get(color_scheme, VisualizationConfig.DEFAULT_COLORS)
|
|
573
|
+
else:
|
|
574
|
+
# Initial load
|
|
575
|
+
colors = VisualizationConfig.COLOR_PRESETS.get(color_scheme, VisualizationConfig.DEFAULT_COLORS)
|
|
576
|
+
|
|
577
|
+
return (
|
|
578
|
+
{'hex': colors['Bus']},
|
|
579
|
+
{'hex': colors['Source']},
|
|
580
|
+
{'hex': colors['Sink']},
|
|
581
|
+
{'hex': colors['Storage']},
|
|
582
|
+
{'hex': colors['Converter']},
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# Updated main visualization callback - simplified logic
|
|
586
|
+
@app.callback(
|
|
587
|
+
[Output('cytoscape', 'elements'), Output('cytoscape', 'stylesheet')],
|
|
588
|
+
[
|
|
589
|
+
Input('bus-color-picker', 'value'),
|
|
590
|
+
Input('source-color-picker', 'value'),
|
|
591
|
+
Input('sink-color-picker', 'value'),
|
|
592
|
+
Input('storage-color-picker', 'value'),
|
|
593
|
+
Input('converter-color-picker', 'value'),
|
|
594
|
+
Input('edge-color-picker', 'value'),
|
|
595
|
+
Input('node-size-slider', 'value'),
|
|
596
|
+
Input('font-size-slider', 'value'),
|
|
597
|
+
],
|
|
598
|
+
[State('elements-store', 'data')],
|
|
599
|
+
)
|
|
600
|
+
def update_visualization(
|
|
601
|
+
bus_color,
|
|
602
|
+
source_color,
|
|
603
|
+
sink_color,
|
|
604
|
+
storage_color,
|
|
605
|
+
converter_color,
|
|
606
|
+
edge_color,
|
|
607
|
+
node_size,
|
|
608
|
+
font_size,
|
|
609
|
+
stored_elements,
|
|
610
|
+
):
|
|
611
|
+
"""Update visualization based on current color picker values"""
|
|
612
|
+
if not stored_elements:
|
|
613
|
+
return no_update, no_update
|
|
614
|
+
|
|
615
|
+
# Use colors from pickers (which are now synced with scheme selection)
|
|
616
|
+
default_colors = VisualizationConfig.DEFAULT_COLORS
|
|
617
|
+
colors = {
|
|
618
|
+
'Bus': bus_color.get('hex') if bus_color else default_colors['Bus'],
|
|
619
|
+
'Source': source_color.get('hex') if source_color else default_colors['Source'],
|
|
620
|
+
'Sink': sink_color.get('hex') if sink_color else default_colors['Sink'],
|
|
621
|
+
'Storage': storage_color.get('hex') if storage_color else default_colors['Storage'],
|
|
622
|
+
'Converter': converter_color.get('hex') if converter_color else default_colors['Converter'],
|
|
623
|
+
'Other': default_colors['Other'],
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
# Update element colors
|
|
627
|
+
updated_elements = []
|
|
628
|
+
for element in stored_elements:
|
|
629
|
+
if 'data' in element and 'element_type' in element['data']:
|
|
630
|
+
element_copy = element.copy()
|
|
631
|
+
element_copy['data'] = element['data'].copy()
|
|
632
|
+
element_type = element_copy['data']['element_type']
|
|
633
|
+
if element_type in colors:
|
|
634
|
+
element_copy['data']['color'] = colors[element_type]
|
|
635
|
+
updated_elements.append(element_copy)
|
|
636
|
+
else:
|
|
637
|
+
updated_elements.append(element)
|
|
638
|
+
|
|
639
|
+
# Create stylesheet
|
|
640
|
+
edge_color_hex = edge_color.get('hex') if edge_color else 'gray'
|
|
641
|
+
stylesheet = [
|
|
642
|
+
{
|
|
643
|
+
'selector': 'node',
|
|
644
|
+
'style': {
|
|
645
|
+
'content': 'data(label)',
|
|
646
|
+
'background-color': 'data(color)',
|
|
647
|
+
'font-size': font_size or 10,
|
|
648
|
+
'color': 'white',
|
|
649
|
+
'text-valign': 'center',
|
|
650
|
+
'text-halign': 'center',
|
|
651
|
+
'width': f'{node_size or 90}px',
|
|
652
|
+
'height': f'{int((node_size or 90) * 0.8)}px',
|
|
653
|
+
'shape': 'data(shape)',
|
|
654
|
+
'text-outline-color': 'black',
|
|
655
|
+
'text-outline-width': 0.5,
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
'selector': '[shape = "custom-source"]',
|
|
660
|
+
'style': {
|
|
661
|
+
'shape': 'polygon',
|
|
662
|
+
'shape-polygon-points': '-0.5 0.5, 0.5 0.5, 1 -0.5, -1 -0.5',
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
'selector': '[shape = "custom-sink"]',
|
|
667
|
+
'style': {
|
|
668
|
+
'shape': 'polygon',
|
|
669
|
+
'shape-polygon-points': '-0.5 -0.5, 0.5 -0.5, 1 0.5, -1 0.5',
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
'selector': 'edge',
|
|
674
|
+
'style': {
|
|
675
|
+
'curve-style': 'straight',
|
|
676
|
+
'width': 2,
|
|
677
|
+
'line-color': edge_color_hex,
|
|
678
|
+
'target-arrow-color': edge_color_hex,
|
|
679
|
+
'target-arrow-shape': 'triangle',
|
|
680
|
+
'arrow-scale': 2,
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
]
|
|
684
|
+
|
|
685
|
+
return updated_elements, stylesheet
|
|
686
|
+
|
|
687
|
+
@app.callback(
|
|
688
|
+
Output('info-panel', 'children'), [Input('cytoscape', 'tapNodeData'), Input('cytoscape', 'tapEdgeData')]
|
|
689
|
+
)
|
|
690
|
+
def display_element_info(node_data, edge_data):
|
|
691
|
+
ctx = callback_context
|
|
692
|
+
if not ctx.triggered:
|
|
693
|
+
return [
|
|
694
|
+
html.P('Click on a node or edge to see details.', style={'color': '#95A5A6', 'font-style': 'italic'})
|
|
695
|
+
]
|
|
696
|
+
|
|
697
|
+
# Determine what was clicked
|
|
698
|
+
if ctx.triggered[0]['prop_id'] == 'cytoscape.tapNodeData' and node_data:
|
|
699
|
+
return [
|
|
700
|
+
html.H5(
|
|
701
|
+
f'Node: {node_data.get("label", "Unknown")}', style={'color': 'white', 'margin-bottom': '10px'}
|
|
702
|
+
),
|
|
703
|
+
html.P(f'Type: {node_data.get("element_type", "Unknown")}', style={'color': '#BDC3C7'}),
|
|
704
|
+
html.Pre(
|
|
705
|
+
node_data.get('parameters', 'No parameters'),
|
|
706
|
+
style={'color': '#BDC3C7', 'font-size': '11px', 'white-space': 'pre-wrap'},
|
|
707
|
+
),
|
|
708
|
+
]
|
|
709
|
+
elif ctx.triggered[0]['prop_id'] == 'cytoscape.tapEdgeData' and edge_data:
|
|
710
|
+
return [
|
|
711
|
+
html.H5(
|
|
712
|
+
f'Edge: {edge_data.get("label", "Unknown")}', style={'color': 'white', 'margin-bottom': '10px'}
|
|
713
|
+
),
|
|
714
|
+
html.P(f'{edge_data.get("source", "")} → {edge_data.get("target", "")}', style={'color': '#E67E22'}),
|
|
715
|
+
html.Pre(
|
|
716
|
+
edge_data.get('parameters', 'No parameters'),
|
|
717
|
+
style={'color': '#BDC3C7', 'font-size': '11px', 'white-space': 'pre-wrap'},
|
|
718
|
+
),
|
|
719
|
+
]
|
|
720
|
+
|
|
721
|
+
return [html.P('Click on a node or edge to see details.', style={'color': '#95A5A6', 'font-style': 'italic'})]
|
|
722
|
+
|
|
723
|
+
@app.callback(Output('cytoscape', 'layout'), Input('layout-dropdown', 'value'))
|
|
724
|
+
def update_layout(selected_layout):
|
|
725
|
+
return {'name': selected_layout}
|
|
726
|
+
|
|
727
|
+
# Reset callback for non-color-picker controls
|
|
728
|
+
@app.callback(
|
|
729
|
+
[
|
|
730
|
+
Output('color-scheme-dropdown', 'value'),
|
|
731
|
+
Output('edge-color-picker', 'value'),
|
|
732
|
+
Output('node-size-slider', 'value'),
|
|
733
|
+
Output('font-size-slider', 'value'),
|
|
734
|
+
Output('layout-dropdown', 'value'),
|
|
735
|
+
],
|
|
736
|
+
[Input('reset-btn', 'n_clicks')],
|
|
737
|
+
)
|
|
738
|
+
def reset_controls(n_clicks):
|
|
739
|
+
"""Reset all controls to defaults (color pickers handled separately)"""
|
|
740
|
+
if n_clicks and n_clicks > 0:
|
|
741
|
+
return (
|
|
742
|
+
'Default', # color scheme (will trigger color picker updates)
|
|
743
|
+
{'hex': '#808080'}, # edge color
|
|
744
|
+
90, # node size
|
|
745
|
+
10, # font size
|
|
746
|
+
'klay', # layout
|
|
747
|
+
)
|
|
748
|
+
return no_update
|
|
749
|
+
|
|
750
|
+
# Export functionality
|
|
751
|
+
app.clientside_callback(
|
|
752
|
+
"""
|
|
753
|
+
function(n_clicks) {
|
|
754
|
+
if (n_clicks > 0 && window.cy) {
|
|
755
|
+
var png64 = window.cy.png({scale: 3, full: true});
|
|
756
|
+
var a = document.createElement('a');
|
|
757
|
+
a.href = png64;
|
|
758
|
+
a.download = 'network_visualization.png';
|
|
759
|
+
a.click();
|
|
760
|
+
}
|
|
761
|
+
return 'Export PNG';
|
|
762
|
+
}
|
|
763
|
+
""",
|
|
764
|
+
Output('export-btn', 'children'),
|
|
765
|
+
Input('export-btn', 'n_clicks'),
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
# Start server
|
|
769
|
+
def find_free_port(start_port=8050, end_port=8100):
|
|
770
|
+
for port in range(start_port, end_port):
|
|
771
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
772
|
+
if s.connect_ex(('localhost', port)) != 0:
|
|
773
|
+
return port
|
|
774
|
+
raise Exception('No free port found')
|
|
775
|
+
|
|
776
|
+
port = find_free_port()
|
|
777
|
+
server = make_server('127.0.0.1', port, app.server)
|
|
778
|
+
|
|
779
|
+
# Start server in background thread
|
|
780
|
+
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
781
|
+
server_thread.start()
|
|
782
|
+
|
|
783
|
+
print(f'Network visualization started on http://127.0.0.1:{port}/')
|
|
784
|
+
|
|
785
|
+
# Store server reference for cleanup
|
|
786
|
+
app.server_instance = server
|
|
787
|
+
app.port = port
|
|
788
|
+
|
|
789
|
+
return app
|