pypsa-explorer 0.1.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.
- pypsa_explorer/__init__.py +20 -0
- pypsa_explorer/app.py +127 -0
- pypsa_explorer/callbacks/__init__.py +36 -0
- pypsa_explorer/callbacks/data_explorer.py +227 -0
- pypsa_explorer/callbacks/filters.py +40 -0
- pypsa_explorer/callbacks/navigation.py +55 -0
- pypsa_explorer/callbacks/network.py +434 -0
- pypsa_explorer/callbacks/theme.py +25 -0
- pypsa_explorer/callbacks/visualizations.py +521 -0
- pypsa_explorer/cli.py +162 -0
- pypsa_explorer/config.py +1579 -0
- pypsa_explorer/layouts/__init__.py +33 -0
- pypsa_explorer/layouts/components.py +506 -0
- pypsa_explorer/layouts/dashboard.py +158 -0
- pypsa_explorer/layouts/tabs.py +165 -0
- pypsa_explorer/layouts/welcome.py +325 -0
- pypsa_explorer/py.typed +1 -0
- pypsa_explorer/utils/__init__.py +17 -0
- pypsa_explorer/utils/data_table.py +129 -0
- pypsa_explorer/utils/helpers.py +208 -0
- pypsa_explorer/utils/network_loader.py +102 -0
- pypsa_explorer-0.1.0.dist-info/METADATA +355 -0
- pypsa_explorer-0.1.0.dist-info/RECORD +27 -0
- pypsa_explorer-0.1.0.dist-info/WHEEL +5 -0
- pypsa_explorer-0.1.0.dist-info/entry_points.txt +2 -0
- pypsa_explorer-0.1.0.dist-info/licenses/LICENSE +21 -0
- pypsa_explorer-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyPSA Explorer - Interactive dashboard for visualizing and analyzing PyPSA energy system networks.
|
|
3
|
+
|
|
4
|
+
This package provides a comprehensive web-based dashboard for exploring PyPSA networks
|
|
5
|
+
with interactive visualizations, filtering capabilities, and multi-network support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__author__ = "Open Energy Transition"
|
|
10
|
+
__email__ = "info@openenergytransition.org"
|
|
11
|
+
|
|
12
|
+
from pypsa_explorer.app import create_app, run_dashboard
|
|
13
|
+
from pypsa_explorer.utils.network_loader import load_networks
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"create_app",
|
|
17
|
+
"run_dashboard",
|
|
18
|
+
"load_networks",
|
|
19
|
+
"__version__",
|
|
20
|
+
]
|
pypsa_explorer/app.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Main application module for PyPSA Explorer dashboard."""
|
|
2
|
+
|
|
3
|
+
import dash
|
|
4
|
+
import dash_bootstrap_components as dbc
|
|
5
|
+
import pypsa
|
|
6
|
+
import pypsa.consistency
|
|
7
|
+
|
|
8
|
+
from pypsa_explorer.callbacks import register_all_callbacks
|
|
9
|
+
from pypsa_explorer.config import get_html_template, setup_plotly_theme
|
|
10
|
+
from pypsa_explorer.layouts.dashboard import create_dashboard_layout
|
|
11
|
+
from pypsa_explorer.utils.helpers import resolve_default_network_path
|
|
12
|
+
from pypsa_explorer.utils.network_loader import load_networks
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_app(
|
|
16
|
+
networks_input: dict[str, pypsa.Network | str] | str | None = None,
|
|
17
|
+
title: str = "PyPSA Explorer",
|
|
18
|
+
debug: bool = False, # noqa: ARG001
|
|
19
|
+
*,
|
|
20
|
+
load_default_on_start: bool = True,
|
|
21
|
+
default_network_path: str = "demo-network.nc",
|
|
22
|
+
) -> dash.Dash:
|
|
23
|
+
"""
|
|
24
|
+
Create and configure the Dash application.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
networks_input : dict, str, or None
|
|
29
|
+
Networks to load. Can be:
|
|
30
|
+
- dict: {label: network_object_or_path}
|
|
31
|
+
- str: Path to a single network file
|
|
32
|
+
- None: Behaviour depends on ``load_default_on_start``
|
|
33
|
+
title : str
|
|
34
|
+
Dashboard title
|
|
35
|
+
debug : bool
|
|
36
|
+
Whether to run in debug mode
|
|
37
|
+
load_default_on_start : bool
|
|
38
|
+
When ``True`` (default) load the bundled demo network if ``networks_input`` is ``None``.
|
|
39
|
+
When ``False`` start without networks and rely on runtime uploads or sample loading.
|
|
40
|
+
default_network_path : str
|
|
41
|
+
Filesystem path to the bundled demo network used when loading the example network.
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
dash.Dash
|
|
46
|
+
Configured Dash application instance
|
|
47
|
+
"""
|
|
48
|
+
# Setup Plotly theme
|
|
49
|
+
setup_plotly_theme()
|
|
50
|
+
|
|
51
|
+
resolved_default_path = resolve_default_network_path(default_network_path)
|
|
52
|
+
default_path_str = str(resolved_default_path) if resolved_default_path else default_network_path
|
|
53
|
+
|
|
54
|
+
# Load networks while allowing empty start states
|
|
55
|
+
if networks_input is None and not load_default_on_start:
|
|
56
|
+
networks: dict[str, pypsa.Network] = {}
|
|
57
|
+
else:
|
|
58
|
+
networks = load_networks(networks_input, default_network_path=default_path_str)
|
|
59
|
+
|
|
60
|
+
# Get the first network as the active network initially (if any)
|
|
61
|
+
network_labels = list(networks.keys())
|
|
62
|
+
active_network_label = network_labels[0] if network_labels else None
|
|
63
|
+
|
|
64
|
+
# Initialize Dash app
|
|
65
|
+
app = dash.Dash(
|
|
66
|
+
__name__,
|
|
67
|
+
external_stylesheets=[dbc.themes.BOOTSTRAP],
|
|
68
|
+
title=title,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Set custom HTML template with embedded CSS
|
|
72
|
+
app.index_string = get_html_template()
|
|
73
|
+
|
|
74
|
+
# Create layout
|
|
75
|
+
app.layout = create_dashboard_layout(
|
|
76
|
+
networks,
|
|
77
|
+
active_network_label,
|
|
78
|
+
default_network_path=default_path_str,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Register all callbacks
|
|
82
|
+
register_all_callbacks(app, networks, default_network_path=default_path_str)
|
|
83
|
+
|
|
84
|
+
return app
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_dashboard(
|
|
88
|
+
networks_input: dict[str, pypsa.Network | str] | str | None = None,
|
|
89
|
+
debug: bool = True,
|
|
90
|
+
host: str = "127.0.0.1",
|
|
91
|
+
port: int = 8050,
|
|
92
|
+
*,
|
|
93
|
+
load_default_on_start: bool = True,
|
|
94
|
+
default_network_path: str = "demo-network.nc",
|
|
95
|
+
) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Run the PyPSA Explorer dashboard.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
networks_input : dict, str, or None
|
|
102
|
+
Networks to load. Can be:
|
|
103
|
+
- dict: {label: network_object_or_path} mapping labels to Network objects or file paths
|
|
104
|
+
- str: Single path to a network file
|
|
105
|
+
- None: Behaviour depends on ``load_default_on_start``
|
|
106
|
+
debug : bool
|
|
107
|
+
Whether to run in debug mode
|
|
108
|
+
host : str
|
|
109
|
+
Host to run the server on
|
|
110
|
+
port : int
|
|
111
|
+
Port to run the server on
|
|
112
|
+
load_default_on_start : bool
|
|
113
|
+
Controls whether the bundled demo network loads automatically when ``networks_input`` is ``None``.
|
|
114
|
+
default_network_path : str
|
|
115
|
+
Filesystem path to the bundled demo network used for the sample loader.
|
|
116
|
+
"""
|
|
117
|
+
app = create_app(
|
|
118
|
+
networks_input,
|
|
119
|
+
debug=debug,
|
|
120
|
+
load_default_on_start=load_default_on_start,
|
|
121
|
+
default_network_path=default_network_path,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
print(f"Starting PyPSA Explorer Dashboard on http://{host}:{port}")
|
|
125
|
+
print("Press Ctrl+C to stop the server")
|
|
126
|
+
|
|
127
|
+
app.run(debug=debug, host=host, port=port)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Callback functions for PyPSA Explorer dashboard interactivity."""
|
|
2
|
+
|
|
3
|
+
from pypsa_explorer.callbacks.data_explorer import register_data_explorer_callbacks
|
|
4
|
+
from pypsa_explorer.callbacks.filters import register_filter_callbacks
|
|
5
|
+
from pypsa_explorer.callbacks.navigation import register_navigation_callbacks
|
|
6
|
+
from pypsa_explorer.callbacks.network import register_network_callbacks
|
|
7
|
+
from pypsa_explorer.callbacks.theme import register_theme_callbacks
|
|
8
|
+
from pypsa_explorer.callbacks.visualizations import register_visualization_callbacks
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"register_data_explorer_callbacks",
|
|
12
|
+
"register_filter_callbacks",
|
|
13
|
+
"register_navigation_callbacks",
|
|
14
|
+
"register_network_callbacks",
|
|
15
|
+
"register_theme_callbacks",
|
|
16
|
+
"register_visualization_callbacks",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def register_all_callbacks(app, networks: dict, *, default_network_path: str) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Register all dashboard callbacks.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
app : dash.Dash
|
|
27
|
+
The Dash application instance
|
|
28
|
+
networks : dict
|
|
29
|
+
Dictionary of loaded PyPSA networks
|
|
30
|
+
"""
|
|
31
|
+
register_filter_callbacks(app)
|
|
32
|
+
register_navigation_callbacks(app)
|
|
33
|
+
register_network_callbacks(app, networks, default_network_path=default_network_path)
|
|
34
|
+
register_visualization_callbacks(app, networks)
|
|
35
|
+
register_data_explorer_callbacks(app, networks)
|
|
36
|
+
register_theme_callbacks(app)
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Data explorer callbacks for interactive component dataframe viewing."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import dash
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import pypsa
|
|
8
|
+
from dash import Input, Output, State, ctx, no_update
|
|
9
|
+
|
|
10
|
+
from pypsa_explorer.utils.data_table import dataframe_to_datatable, get_timeseries_attributes
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# Maximum number of rows to display in tables
|
|
15
|
+
MAX_TABLE_ROWS = 5000
|
|
16
|
+
|
|
17
|
+
# Mapping of KPI card IDs to component names
|
|
18
|
+
KPI_COMPONENT_MAP = {
|
|
19
|
+
"kpi-card-buses": "buses",
|
|
20
|
+
"kpi-card-generators": "generators",
|
|
21
|
+
"kpi-card-lines": "lines",
|
|
22
|
+
"kpi-card-links": "links",
|
|
23
|
+
"kpi-card-storage_units": "storage_units",
|
|
24
|
+
"kpi-card-stores": "stores",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Human-readable labels for components
|
|
28
|
+
COMPONENT_LABELS = {
|
|
29
|
+
"buses": "Nodes",
|
|
30
|
+
"generators": "Generators",
|
|
31
|
+
"lines": "Lines",
|
|
32
|
+
"links": "Links",
|
|
33
|
+
"storage_units": "Storage Units",
|
|
34
|
+
"stores": "Stores",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def register_data_explorer_callbacks(app: dash.Dash, networks: dict[str, pypsa.Network]) -> None:
|
|
39
|
+
"""Register callbacks for the data explorer modal."""
|
|
40
|
+
|
|
41
|
+
@app.callback(
|
|
42
|
+
[
|
|
43
|
+
Output("data-explorer-modal", "is_open"),
|
|
44
|
+
Output("data-explorer-modal-title", "children"),
|
|
45
|
+
Output("static-data-table", "data"),
|
|
46
|
+
Output("static-data-table", "columns"),
|
|
47
|
+
Output("timeseries-attribute-selector", "options"),
|
|
48
|
+
Output("timeseries-attribute-selector", "value"),
|
|
49
|
+
Output("active-component-store", "data"),
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
Input("kpi-card-buses", "n_clicks"),
|
|
53
|
+
Input("kpi-card-generators", "n_clicks"),
|
|
54
|
+
Input("kpi-card-lines", "n_clicks"),
|
|
55
|
+
Input("kpi-card-links", "n_clicks"),
|
|
56
|
+
Input("kpi-card-storage_units", "n_clicks"),
|
|
57
|
+
Input("kpi-card-stores", "n_clicks"),
|
|
58
|
+
Input("close-data-explorer-modal", "n_clicks"),
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
State("network-selector", "data"),
|
|
62
|
+
State("data-explorer-modal", "is_open"),
|
|
63
|
+
],
|
|
64
|
+
)
|
|
65
|
+
def toggle_modal_and_load_data(
|
|
66
|
+
buses_clicks: int,
|
|
67
|
+
generators_clicks: int,
|
|
68
|
+
lines_clicks: int,
|
|
69
|
+
links_clicks: int,
|
|
70
|
+
storage_units_clicks: int,
|
|
71
|
+
stores_clicks: int,
|
|
72
|
+
close_clicks: int, # noqa: ARG001
|
|
73
|
+
network_label: str | None,
|
|
74
|
+
is_open: bool, # noqa: ARG001
|
|
75
|
+
) -> tuple[bool, str, list[dict], list[dict], list[dict], str | None, str]:
|
|
76
|
+
"""Toggle modal and load component data when KPI card is clicked."""
|
|
77
|
+
# Get the ID of the component that triggered the callback
|
|
78
|
+
triggered_id = ctx.triggered_id
|
|
79
|
+
|
|
80
|
+
# If close button was clicked, close the modal
|
|
81
|
+
if triggered_id == "close-data-explorer-modal":
|
|
82
|
+
return False, no_update, no_update, no_update, no_update, no_update, no_update # type: ignore[return-value]
|
|
83
|
+
|
|
84
|
+
# If no KPI card was clicked, don't update
|
|
85
|
+
if triggered_id not in KPI_COMPONENT_MAP:
|
|
86
|
+
return no_update, no_update, no_update, no_update, no_update, no_update, no_update # type: ignore[return-value]
|
|
87
|
+
|
|
88
|
+
# Check if n_clicks is greater than 0 (actual click, not component recreation)
|
|
89
|
+
# When KPI cards are recreated during network switch, they reset to n_clicks=0
|
|
90
|
+
# This check prevents the modal from opening on network changes
|
|
91
|
+
click_counts = {
|
|
92
|
+
"kpi-card-buses": buses_clicks,
|
|
93
|
+
"kpi-card-generators": generators_clicks,
|
|
94
|
+
"kpi-card-lines": lines_clicks,
|
|
95
|
+
"kpi-card-links": links_clicks,
|
|
96
|
+
"kpi-card-storage_units": storage_units_clicks,
|
|
97
|
+
"kpi-card-stores": stores_clicks,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
triggered_clicks = click_counts.get(triggered_id, 0)
|
|
101
|
+
|
|
102
|
+
# Only proceed if there was an actual click (n_clicks > 0)
|
|
103
|
+
if triggered_clicks == 0:
|
|
104
|
+
return no_update, no_update, no_update, no_update, no_update, no_update, no_update # type: ignore[return-value]
|
|
105
|
+
|
|
106
|
+
# Get the component name from the triggered card
|
|
107
|
+
component_name = KPI_COMPONENT_MAP[triggered_id]
|
|
108
|
+
component_label = COMPONENT_LABELS[component_name]
|
|
109
|
+
|
|
110
|
+
if not network_label or network_label not in networks:
|
|
111
|
+
return no_update, no_update, no_update, no_update, no_update, no_update, no_update # type: ignore[return-value]
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Get the active network
|
|
115
|
+
n = networks[network_label]
|
|
116
|
+
|
|
117
|
+
# Get the component dataframe
|
|
118
|
+
if not hasattr(n, component_name):
|
|
119
|
+
logger.warning(f"Component '{component_name}' not found in network '{network_label}'")
|
|
120
|
+
return (
|
|
121
|
+
True,
|
|
122
|
+
f"{component_label} - No Data Available",
|
|
123
|
+
[],
|
|
124
|
+
[],
|
|
125
|
+
[],
|
|
126
|
+
None,
|
|
127
|
+
component_name,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
component_df = getattr(n, component_name)
|
|
131
|
+
|
|
132
|
+
# Convert dataframe to dict for DataTable using utility
|
|
133
|
+
if isinstance(component_df, pd.DataFrame):
|
|
134
|
+
data, columns = dataframe_to_datatable(component_df)
|
|
135
|
+
else:
|
|
136
|
+
logger.warning(f"Component '{component_name}' is not a DataFrame")
|
|
137
|
+
data = []
|
|
138
|
+
columns = []
|
|
139
|
+
|
|
140
|
+
# Get available time-series attributes using efficient utility
|
|
141
|
+
timeseries_attrs = get_timeseries_attributes(n, component_name)
|
|
142
|
+
timeseries_options = [{"label": attr, "value": attr} for attr in timeseries_attrs]
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
True, # Open modal
|
|
146
|
+
f"{component_label} Data ({len(component_df):,} records)",
|
|
147
|
+
data,
|
|
148
|
+
columns,
|
|
149
|
+
timeseries_options,
|
|
150
|
+
timeseries_options[0]["value"] if timeseries_options else None,
|
|
151
|
+
component_name, # Store component name for efficient lookup
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"Error loading component data for '{component_name}': {e}")
|
|
156
|
+
return (
|
|
157
|
+
True,
|
|
158
|
+
f"{component_label} - Error Loading Data",
|
|
159
|
+
[],
|
|
160
|
+
[],
|
|
161
|
+
[],
|
|
162
|
+
None,
|
|
163
|
+
component_name,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@app.callback(
|
|
167
|
+
[
|
|
168
|
+
Output("timeseries-data-table", "data"),
|
|
169
|
+
Output("timeseries-data-table", "columns"),
|
|
170
|
+
],
|
|
171
|
+
[
|
|
172
|
+
Input("timeseries-attribute-selector", "value"),
|
|
173
|
+
],
|
|
174
|
+
[
|
|
175
|
+
State("active-component-store", "data"),
|
|
176
|
+
State("network-selector", "data"),
|
|
177
|
+
],
|
|
178
|
+
)
|
|
179
|
+
def update_timeseries_data(
|
|
180
|
+
selected_attribute: str | None,
|
|
181
|
+
component_name: str | None,
|
|
182
|
+
network_label: str | None,
|
|
183
|
+
) -> tuple[list[dict], list[dict]]:
|
|
184
|
+
"""Update time-series data table when attribute is selected."""
|
|
185
|
+
if not selected_attribute or not component_name:
|
|
186
|
+
return [], []
|
|
187
|
+
|
|
188
|
+
if not network_label or network_label not in networks:
|
|
189
|
+
return [], []
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
# Get the active network
|
|
193
|
+
n = networks[network_label]
|
|
194
|
+
|
|
195
|
+
# Get time-series data
|
|
196
|
+
timeseries_component = f"{component_name}_t"
|
|
197
|
+
if not hasattr(n, timeseries_component):
|
|
198
|
+
logger.warning(f"Time-series component '{timeseries_component}' not found in network '{network_label}'")
|
|
199
|
+
return [], []
|
|
200
|
+
|
|
201
|
+
ts_obj = getattr(n, timeseries_component)
|
|
202
|
+
if not hasattr(ts_obj, selected_attribute):
|
|
203
|
+
logger.warning(
|
|
204
|
+
f"Attribute '{selected_attribute}' not found in time-series component '{timeseries_component}'"
|
|
205
|
+
)
|
|
206
|
+
return [], []
|
|
207
|
+
|
|
208
|
+
ts_df = getattr(ts_obj, selected_attribute)
|
|
209
|
+
|
|
210
|
+
if isinstance(ts_df, pd.DataFrame):
|
|
211
|
+
# Check if dataframe exceeds maximum rows
|
|
212
|
+
if len(ts_df) > MAX_TABLE_ROWS:
|
|
213
|
+
logger.warning(
|
|
214
|
+
f"Time-series data for '{selected_attribute}' has {len(ts_df):,} rows, "
|
|
215
|
+
f"limiting to first {MAX_TABLE_ROWS:,} rows for display"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Use utility function for efficient conversion with uniform sampling
|
|
219
|
+
data, columns = dataframe_to_datatable(ts_df, max_rows=MAX_TABLE_ROWS)
|
|
220
|
+
return data, columns
|
|
221
|
+
|
|
222
|
+
logger.warning(f"Time-series attribute '{selected_attribute}' is not a DataFrame")
|
|
223
|
+
return [], []
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Error loading time-series data for '{selected_attribute}': {e}")
|
|
227
|
+
return [], []
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Filter-related callbacks for PyPSA Explorer dashboard."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from dash import Input, Output
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register_filter_callbacks(app) -> None:
|
|
9
|
+
"""Register filter-related callbacks."""
|
|
10
|
+
|
|
11
|
+
@app.callback(
|
|
12
|
+
[
|
|
13
|
+
Output("global-country-selector", "disabled"),
|
|
14
|
+
Output("global-country-selector", "value"),
|
|
15
|
+
],
|
|
16
|
+
[Input("global-country-mode", "value")],
|
|
17
|
+
)
|
|
18
|
+
def toggle_global_country_selector(mode: str) -> tuple[bool, list[Any]]:
|
|
19
|
+
"""Enable/disable country selector based on mode."""
|
|
20
|
+
if mode == "All":
|
|
21
|
+
return True, []
|
|
22
|
+
else:
|
|
23
|
+
return False, []
|
|
24
|
+
|
|
25
|
+
@app.callback(
|
|
26
|
+
[
|
|
27
|
+
Output("global-carrier-selector-container", "style"),
|
|
28
|
+
Output("carrier-not-applicable-text", "style"),
|
|
29
|
+
Output("active-tab-store", "data"),
|
|
30
|
+
],
|
|
31
|
+
[Input("tabs", "value")],
|
|
32
|
+
)
|
|
33
|
+
def handle_tab_specific_ui(active_tab: str) -> tuple[dict[str, str], dict[str, str], str]:
|
|
34
|
+
"""Handle UI elements based on the active tab."""
|
|
35
|
+
if active_tab in ["capex", "opex"]:
|
|
36
|
+
# Hide carrier selector for CAPEX/OPEX tabs
|
|
37
|
+
return {"display": "none"}, {"display": "block"}, active_tab
|
|
38
|
+
else:
|
|
39
|
+
# Show carrier selector for other tabs
|
|
40
|
+
return {"display": "block"}, {"display": "none"}, active_tab
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Navigation callbacks for PyPSA Explorer dashboard."""
|
|
2
|
+
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from dash import Input, Output, State, no_update
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register_navigation_callbacks(app) -> None:
|
|
9
|
+
"""Register navigation-related callbacks."""
|
|
10
|
+
|
|
11
|
+
@app.callback(
|
|
12
|
+
[
|
|
13
|
+
Output("welcome-content", "style"),
|
|
14
|
+
Output("dashboard-content", "style"),
|
|
15
|
+
Output("page-state", "data"),
|
|
16
|
+
Output("top-bar-network-selector", "style"),
|
|
17
|
+
],
|
|
18
|
+
[Input("enter-dashboard-btn", "n_clicks")],
|
|
19
|
+
[
|
|
20
|
+
State("page-state", "data"),
|
|
21
|
+
State("network-registry", "data"),
|
|
22
|
+
],
|
|
23
|
+
)
|
|
24
|
+
def navigate_pages(
|
|
25
|
+
n_clicks: int | None,
|
|
26
|
+
page_state: dict[str, str],
|
|
27
|
+
registry: dict[str, list[str]] | None = None,
|
|
28
|
+
) -> tuple[dict[str, str], dict[str, str], dict[str, str], dict[str, str]]:
|
|
29
|
+
"""Manage navigation between welcome page and dashboard."""
|
|
30
|
+
page_state = page_state or {}
|
|
31
|
+
has_network = bool(registry and registry.get("order"))
|
|
32
|
+
current_page = page_state.get("current_page")
|
|
33
|
+
|
|
34
|
+
if n_clicks and current_page == "welcome" and has_network:
|
|
35
|
+
return (
|
|
36
|
+
{"display": "none"},
|
|
37
|
+
{"display": "block"},
|
|
38
|
+
{"current_page": "dashboard"},
|
|
39
|
+
{"display": "flex"},
|
|
40
|
+
)
|
|
41
|
+
if current_page == "dashboard" and has_network:
|
|
42
|
+
return (
|
|
43
|
+
cast(dict[str, str], no_update),
|
|
44
|
+
cast(dict[str, str], no_update),
|
|
45
|
+
cast(dict[str, str], no_update),
|
|
46
|
+
{"display": "flex"},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# No networks or still on welcome page keeps selector hidden
|
|
50
|
+
return (
|
|
51
|
+
cast(dict[str, str], no_update),
|
|
52
|
+
cast(dict[str, str], no_update),
|
|
53
|
+
cast(dict[str, str], no_update),
|
|
54
|
+
{"display": "none"},
|
|
55
|
+
)
|