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.

Files changed (45) hide show
  1. docs/examples/00-Minimal Example.md +1 -1
  2. docs/examples/01-Basic Example.md +1 -1
  3. docs/examples/02-Complex Example.md +1 -1
  4. docs/examples/index.md +1 -1
  5. docs/faq/contribute.md +26 -14
  6. docs/faq/index.md +1 -1
  7. docs/javascripts/mathjax.js +1 -1
  8. docs/user-guide/Mathematical Notation/Bus.md +1 -1
  9. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +21 -21
  10. docs/user-guide/Mathematical Notation/Flow.md +3 -3
  11. docs/user-guide/Mathematical Notation/InvestParameters.md +3 -0
  12. docs/user-guide/Mathematical Notation/LinearConverter.md +5 -5
  13. docs/user-guide/Mathematical Notation/OnOffParameters.md +3 -0
  14. docs/user-guide/Mathematical Notation/Piecewise.md +1 -1
  15. docs/user-guide/Mathematical Notation/Storage.md +2 -2
  16. docs/user-guide/Mathematical Notation/index.md +1 -1
  17. docs/user-guide/Mathematical Notation/others.md +1 -1
  18. docs/user-guide/index.md +2 -2
  19. flixopt/__init__.py +4 -0
  20. flixopt/aggregation.py +33 -32
  21. flixopt/calculation.py +161 -65
  22. flixopt/components.py +687 -154
  23. flixopt/config.py +17 -8
  24. flixopt/core.py +69 -60
  25. flixopt/effects.py +146 -64
  26. flixopt/elements.py +297 -110
  27. flixopt/features.py +78 -71
  28. flixopt/flow_system.py +72 -50
  29. flixopt/interface.py +952 -113
  30. flixopt/io.py +15 -10
  31. flixopt/linear_converters.py +373 -81
  32. flixopt/network_app.py +445 -266
  33. flixopt/plotting.py +215 -87
  34. flixopt/results.py +382 -209
  35. flixopt/solvers.py +25 -21
  36. flixopt/structure.py +41 -39
  37. flixopt/utils.py +10 -7
  38. {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/METADATA +64 -53
  39. flixopt-2.1.8.dist-info/RECORD +56 -0
  40. scripts/extract_release_notes.py +5 -5
  41. scripts/gen_ref_pages.py +1 -1
  42. flixopt-2.1.6.dist-info/RECORD +0 -54
  43. {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/WHEEL +0 -0
  44. {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/licenses/LICENSE +0 -0
  45. {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 json
1
+ from __future__ import annotations
2
+
2
3
  import logging
3
4
  import socket
4
5
  import threading
5
- from typing import Any, Dict, List
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, Component, Flow
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', 'Source': '#4ECDC4', 'Sink': '#45B7D1',
43
- 'Storage': '#96CEB4', 'Converter': '#FFEAA7', 'Other': '#DDA0DD',
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', 'Source': '#34495E', 'Sink': '#7F8C8D',
47
- 'Storage': '#95A5A6', 'Converter': '#BDC3C7', 'Other': '#ECF0F1',
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', 'Source': '#BAFFC9', 'Sink': '#BAE1FF',
51
- 'Storage': '#FFFFBA', 'Converter': '#FFDFBA', 'Other': '#E0BBE4',
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
- def flow_graph(flow_system: FlowSystem) -> networkx.DiGraph:
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 = networkx.DiGraph()
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"Failed to add edge {edge}: {e}")
179
+ logger.error(f'Failed to add edge {edge}: {e}')
153
180
 
154
181
  return graph
155
182
 
156
- def make_cytoscape_elements(graph: networkx.DiGraph) -> List[Dict[str, Any]]:
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
- 'data': {
165
- 'id': node_id,
166
- 'label': node_id,
167
- 'color': node_data.get('color', '#7F8C8D'),
168
- 'shape': node_data.get('shape', 'rectangle'),
169
- 'element_type': node_data.get('element_type', 'Other'),
170
- 'parameters': node_data.get('parameters', ''),
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
- 'data': {
179
- 'source': u,
180
- 'target': v,
181
- 'id': f'{u}-{v}',
182
- 'label': edge_data.get('label', ''),
183
- 'parameters': edge_data.get('parameters', ''),
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
- html.Label(label, style={
193
- 'color': 'white', 'font-size': '12px', 'margin-bottom': '5px',
194
- 'display': 'block'
195
- }),
196
- daq.ColorPicker(
197
- id=input_id,
198
- label="",
199
- value={'hex': default_color},
200
- size=200,
201
- theme={'dark': True},
202
- style={'margin-bottom': '10px'}
203
- ),
204
- ])
205
-
206
- def create_style_section(title: str, children: List):
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
- html.H4(title, style={
210
- 'color': 'white', 'margin-bottom': '10px',
211
- 'border-bottom': '2px solid #3498DB', 'padding-bottom': '5px',
212
- }),
213
- html.Div(children, style={'margin-bottom': '20px'}),
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
- html.Div([
220
- html.H3('Style Controls', style={
221
- 'color': 'white', 'margin-bottom': '20px', 'text-align': 'center',
222
- 'border-bottom': '3px solid #9B59B6', 'padding-bottom': '10px',
223
- }),
224
-
225
- # Layout Section
226
- create_style_section('Layout', [
227
- dcc.Dropdown(
228
- id='layout-dropdown',
229
- options=[
230
- {'label': 'Klay (horizontal)', 'value': 'klay'},
231
- {'label': 'Dagre (vertical)', 'value': 'dagre'},
232
- {'label': 'Breadthfirst', 'value': 'breadthfirst'},
233
- {'label': 'Cose (force-directed)', 'value': 'cose'},
234
- {'label': 'Grid', 'value': 'grid'},
235
- {'label': 'Circle', 'value': 'circle'},
236
- ],
237
- value='klay',
238
- clearable=False,
239
- style={'width': '100%'},
240
- ),
241
- ]),
242
-
243
- # Color Scheme Section
244
- create_style_section('Color Scheme', [
245
- dcc.Dropdown(
246
- id='color-scheme-dropdown',
247
- options=[{'label': k, 'value': k} for k in VisualizationConfig.COLOR_PRESETS.keys()],
248
- value='Default',
249
- style={'width': '100%', 'margin-bottom': '10px'},
250
- ),
251
- ]),
252
-
253
- # Color Pickers Section
254
- create_style_section('Custom Colors', [
255
- create_color_picker_input('Bus', 'bus-color-picker', '#7F8C8D'),
256
- create_color_picker_input('Source', 'source-color-picker', '#F1C40F'),
257
- create_color_picker_input('Sink', 'sink-color-picker', '#F1C40F'),
258
- create_color_picker_input('Storage', 'storage-color-picker', '#2980B9'),
259
- create_color_picker_input('Converter', 'converter-color-picker', '#D35400'),
260
- create_color_picker_input('Edge', 'edge-color-picker', '#808080'),
261
- ]),
262
-
263
- # Node Settings
264
- create_style_section('Node Settings', [
265
- html.Label('Size', style={'color': 'white', 'font-size': '12px'}),
266
- dcc.Slider(
267
- id='node-size-slider', min=50, max=150, step=10, value=90,
268
- marks={i: {'label': str(i), 'style': {'color': 'white', 'font-size': '10px'}}
269
- for i in range(50, 151, 25)},
270
- tooltip={'placement': 'bottom', 'always_visible': True},
271
- ),
272
- html.Br(),
273
- html.Label('Font Size', style={'color': 'white', 'font-size': '12px'}),
274
- dcc.Slider(
275
- id='font-size-slider', min=8, max=20, step=1, value=10,
276
- marks={i: {'label': str(i), 'style': {'color': 'white', 'font-size': '10px'}}
277
- for i in range(8, 21, 2)},
278
- tooltip={'placement': 'bottom', 'always_visible': True},
279
- ),
280
- ]),
281
-
282
- # Reset Button
283
- html.Div([
284
- html.Button('Reset to Defaults', id='reset-btn', n_clicks=0, style={
285
- 'width': '100%', 'background-color': '#E74C3C', 'color': 'white',
286
- 'border': 'none', 'padding': '10px', 'border-radius': '5px',
287
- 'cursor': 'pointer', 'margin-top': '20px',
288
- }),
289
- ]),
290
- ], id='sidebar-content', style={
291
- 'width': '280px', 'height': '100vh', 'background-color': '#2C3E50',
292
- 'padding': '20px', 'position': 'fixed', 'left': '0', 'top': '0',
293
- 'overflow-y': 'auto', 'border-right': '3px solid #34495E',
294
- 'box-shadow': '2px 0 5px rgba(0,0,0,0.1)', 'z-index': '999',
295
- 'transform': 'translateX(-100%)', 'transition': 'transform 0.3s ease',
296
- })
297
- ])
298
-
299
- def shownetwork(graph: networkx.DiGraph):
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"Required packages not available: {VISUALIZATION_ERROR}")
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
- # Toggle button
315
- html.Button('☰', id='toggle-sidebar', n_clicks=0, style={
316
- 'position': 'fixed', 'top': '20px', 'left': '20px', 'z-index': '1000',
317
- 'background-color': '#3498DB', 'color': 'white', 'border': 'none',
318
- 'padding': '10px 15px', 'border-radius': '5px', 'cursor': 'pointer',
319
- 'font-size': '18px', 'box-shadow': '0 2px 5px rgba(0,0,0,0.3)',
320
- }),
321
-
322
- # Data storage
323
- dcc.Store(id='elements-store', data=elements),
324
-
325
- # Sidebar
326
- create_sidebar(),
327
-
328
- # Main content
329
- html.Div([
330
- # Header
331
- html.Div([
332
- html.H2('Network Visualization', style={
333
- 'color': 'white', 'margin': '0', 'text-align': 'center'
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
- # Info panel
355
- html.Div([
356
- html.H4('Element Information', style={
357
- 'color': 'white', 'margin': '0 0 10px 0',
358
- 'border-bottom': '2px solid #3498DB', 'padding-bottom': '5px',
359
- }),
360
- html.Div(id='info-panel', children=[
361
- html.P('Click on a node or edge to see details.',
362
- style={'color': '#95A5A6', 'font-style': 'italic'})
363
- ]),
364
- ], style={
365
- 'background-color': '#2C3E50', 'padding': '15px',
366
- 'height': '25vh', 'overflow-y': 'auto',
367
- 'border-top': '2px solid #34495E',
368
- }),
369
- ], id='main-content', style={
370
- 'margin-left': '0', 'background-color': '#1A252F',
371
- 'min-height': '100vh', 'transition': 'margin-left 0.3s ease',
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', 'height': '100vh', 'background-color': '#2C3E50',
387
- 'padding': '20px', 'position': 'fixed', 'left': '0', 'top': '0',
388
- 'overflow-y': 'auto', 'border-right': '3px solid #34495E',
389
- 'box-shadow': '2px 0 5px rgba(0,0,0,0.1)', 'z-index': '999',
390
- 'transform': sidebar_transform, 'transition': 'transform 0.3s ease',
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, 'background-color': '#1A252F',
395
- 'min-height': '100vh', 'transition': 'margin-left 0.3s ease',
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(color_scheme, bus_color, source_color, sink_color,
416
- storage_color, converter_color, edge_color,
417
- node_size, font_size, stored_elements):
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
- # Determine colors to use
422
- if any(picker for picker in [bus_color, source_color, sink_color,
423
- storage_color, converter_color, edge_color]):
424
- # Use custom colors from pickers
425
- colors = {
426
- 'Bus': bus_color.get('hex') if bus_color else '#7F8C8D',
427
- 'Source': source_color.get('hex') if source_color else '#F1C40F',
428
- 'Sink': sink_color.get('hex') if sink_color else '#F1C40F',
429
- 'Storage': storage_color.get('hex') if storage_color else '#2980B9',
430
- 'Converter': converter_color.get('hex') if converter_color else '#D35400',
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 [html.P('Click on a node or edge to see details.',
507
- style={'color': '#95A5A6', 'font-style': 'italic'})]
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(f"Node: {node_data.get('label', 'Unknown')}",
513
- style={'color': 'white', 'margin-bottom': '10px'}),
514
- html.P(f"Type: {node_data.get('element_type', 'Unknown')}",
515
- style={'color': '#BDC3C7'}),
516
- html.Pre(node_data.get('parameters', 'No parameters'),
517
- style={'color': '#BDC3C7', 'font-size': '11px',
518
- 'white-space': 'pre-wrap'})
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(f"Edge: {edge_data.get('label', 'Unknown')}",
523
- style={'color': 'white', 'margin-bottom': '10px'}),
524
- html.P(f"{edge_data.get('source', '')} → {edge_data.get('target', '')}",
525
- style={'color': '#E67E22'}),
526
- html.Pre(edge_data.get('parameters', 'No parameters'),
527
- style={'color': '#BDC3C7', 'font-size': '11px',
528
- 'white-space': 'pre-wrap'})
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': '#7F8C8D'}, # bus
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