glidepath 0.2.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.
- glidepath/__init__.py +3 -0
- glidepath/app/__init__.py +364 -0
- glidepath/app/backtest.py +281 -0
- glidepath/app/charts.py +759 -0
- glidepath/app/copy.py +174 -0
- glidepath/app/display.py +148 -0
- glidepath/app/drawdown.py +436 -0
- glidepath/app/example.py +66 -0
- glidepath/app/exports.py +487 -0
- glidepath/app/files.py +249 -0
- glidepath/app/firstrun.py +114 -0
- glidepath/app/forms.py +1750 -0
- glidepath/app/inspector.py +506 -0
- glidepath/app/labels.py +66 -0
- glidepath/app/montecarlo.py +399 -0
- glidepath/app/plan.py +354 -0
- glidepath/app/retirement.py +446 -0
- glidepath/app/scenarios.py +831 -0
- glidepath/app/shell.py +185 -0
- glidepath/app/tables.py +138 -0
- glidepath/core/__init__.py +390 -0
- glidepath/core/annuities.py +240 -0
- glidepath/core/backtest.py +514 -0
- glidepath/core/comparison.py +278 -0
- glidepath/core/config.py +82 -0
- glidepath/core/contributions.py +337 -0
- glidepath/core/engine.py +2811 -0
- glidepath/core/entities.py +264 -0
- glidepath/core/glide.py +289 -0
- glidepath/core/investments.py +175 -0
- glidepath/core/money.py +107 -0
- glidepath/core/montecarlo.py +609 -0
- glidepath/core/pensions.py +298 -0
- glidepath/core/periods.py +367 -0
- glidepath/core/provenance.py +271 -0
- glidepath/core/randomness.py +128 -0
- glidepath/core/region.py +46 -0
- glidepath/core/reporting.py +231 -0
- glidepath/core/results.py +504 -0
- glidepath/core/retirement.py +291 -0
- glidepath/core/returns.py +312 -0
- glidepath/core/scenarios.py +579 -0
- glidepath/core/state_pension.py +264 -0
- glidepath/core/tax.py +139 -0
- glidepath/core/withdrawals.py +461 -0
- glidepath/core/wrappers.py +278 -0
- glidepath/gui/__init__.py +6 -0
- glidepath/gui/assets/icon_128.png +0 -0
- glidepath/gui/assets/icon_16.png +0 -0
- glidepath/gui/assets/icon_24.png +0 -0
- glidepath/gui/assets/icon_256.png +0 -0
- glidepath/gui/assets/icon_32.png +0 -0
- glidepath/gui/assets/icon_48.png +0 -0
- glidepath/gui/assets/icon_64.png +0 -0
- glidepath/gui/assets/wordmark.png +0 -0
- glidepath/gui/charts.py +829 -0
- glidepath/gui/forms.py +359 -0
- glidepath/gui/inspector.py +186 -0
- glidepath/gui/main.py +51 -0
- glidepath/gui/scenarios.py +402 -0
- glidepath/gui/style.py +376 -0
- glidepath/gui/tableview.py +67 -0
- glidepath/gui/widgets.py +989 -0
- glidepath/persistence/__init__.py +48 -0
- glidepath/persistence/assumptions.py +112 -0
- glidepath/persistence/decode.py +747 -0
- glidepath/persistence/document.py +101 -0
- glidepath/persistence/encode.py +433 -0
- glidepath/persistence/migrations.py +158 -0
- glidepath/persistence/values.py +298 -0
- glidepath/py.typed +0 -0
- glidepath/regions/__init__.py +7 -0
- glidepath/regions/uk/__init__.py +189 -0
- glidepath/regions/uk/ages.py +156 -0
- glidepath/regions/uk/contributions.py +717 -0
- glidepath/regions/uk/data/age_rules.toml +78 -0
- glidepath/regions/uk/data/assumptions_default.toml +170 -0
- glidepath/regions/uk/data/returns_history.toml +150 -0
- glidepath/regions/uk/data/tax_year_2026_27.toml +98 -0
- glidepath/regions/uk/extension.py +479 -0
- glidepath/regions/uk/loader.py +704 -0
- glidepath/regions/uk/region.py +160 -0
- glidepath/regions/uk/schema.py +563 -0
- glidepath/regions/uk/state_pension.py +129 -0
- glidepath/regions/uk/tax.py +466 -0
- glidepath/regions/uk/wrappers.py +283 -0
- glidepath/regions/uk/years.py +92 -0
- glidepath-0.2.0.dist-info/METADATA +189 -0
- glidepath-0.2.0.dist-info/RECORD +93 -0
- glidepath-0.2.0.dist-info/WHEEL +4 -0
- glidepath-0.2.0.dist-info/entry_points.txt +3 -0
- glidepath-0.2.0.dist-info/licenses/LICENSE +21 -0
- glidepath-0.2.0.dist-info/licenses/LICENSE-DATA +28 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""The scenario manager widgets (§4.7, roadmap 8.5).
|
|
2
|
+
|
|
3
|
+
Scenario list, per-scenario override table (a scenario's overrides
|
|
4
|
+
*are* its diff, planning §4.3), and the comparison report as a table
|
|
5
|
+
and grouped bar chart. Thin by policy: every heading, prompt, status
|
|
6
|
+
line, and pre-formatted cell comes from the app layer's
|
|
7
|
+
:class:`~glidepath.app.ScenariosViewModel`; this pane binds them to
|
|
8
|
+
widgets and forwards raw user input back through its callbacks.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from PySide6.QtCharts import (
|
|
15
|
+
QBarCategoryAxis,
|
|
16
|
+
QBarSeries,
|
|
17
|
+
QChart,
|
|
18
|
+
QChartView,
|
|
19
|
+
QValueAxis,
|
|
20
|
+
)
|
|
21
|
+
from PySide6.QtCore import QSignalBlocker, Qt
|
|
22
|
+
from PySide6.QtGui import QPainter
|
|
23
|
+
from PySide6.QtWidgets import (
|
|
24
|
+
QComboBox,
|
|
25
|
+
QGroupBox,
|
|
26
|
+
QHBoxLayout,
|
|
27
|
+
QInputDialog,
|
|
28
|
+
QLabel,
|
|
29
|
+
QListWidget,
|
|
30
|
+
QPushButton,
|
|
31
|
+
QRadioButton,
|
|
32
|
+
QTabWidget,
|
|
33
|
+
QVBoxLayout,
|
|
34
|
+
QWidget,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from glidepath.gui.charts import tooltip_bar_set
|
|
38
|
+
from glidepath.gui.tableview import fill_table, read_only_table
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from collections.abc import Callable
|
|
42
|
+
|
|
43
|
+
from glidepath.app import ChartSpec, OverrideRow, ScenarioItem, ScenariosViewModel
|
|
44
|
+
|
|
45
|
+
_CATEGORY_LABEL_ANGLE = -90
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ScenariosPaneCallbacks:
|
|
50
|
+
"""The shell handlers a :class:`ScenariosPane` forwards actions to.
|
|
51
|
+
|
|
52
|
+
Handlers returning ``str | None`` report a rejection message for
|
|
53
|
+
the status line; the rest cannot fail.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
add_scenario: Callable[[str], str | None]
|
|
57
|
+
remove_scenario: Callable[[str], None]
|
|
58
|
+
set_override: Callable[[str, str, str], str | None]
|
|
59
|
+
remove_override: Callable[[str, str], None]
|
|
60
|
+
select_basis: Callable[[str], None]
|
|
61
|
+
select_metric: Callable[[str], None]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ScenariosPane(QWidget):
|
|
65
|
+
"""The scenarios tab: manager, override diff list, and comparison."""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self, callbacks: ScenariosPaneCallbacks, parent: QWidget | None = None
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Build the manager and comparison surfaces and wire callbacks."""
|
|
71
|
+
super().__init__(parent)
|
|
72
|
+
self._callbacks = callbacks
|
|
73
|
+
self._view_model: ScenariosViewModel | None = None
|
|
74
|
+
self._basis_buttons: dict[str, QRadioButton] = {}
|
|
75
|
+
self._override_rows: tuple[OverrideRow, ...] = ()
|
|
76
|
+
|
|
77
|
+
manager_row = QHBoxLayout()
|
|
78
|
+
manager_row.addWidget(self._built_scenarios_box(), 1)
|
|
79
|
+
manager_row.addWidget(self._built_overrides_box(), 2)
|
|
80
|
+
|
|
81
|
+
self.message_label = QLabel("", self)
|
|
82
|
+
self.message_label.setWordWrap(True)
|
|
83
|
+
self.status_label = QLabel("", self)
|
|
84
|
+
self.status_label.setWordWrap(True)
|
|
85
|
+
|
|
86
|
+
layout = QVBoxLayout(self)
|
|
87
|
+
layout.addLayout(manager_row, 1)
|
|
88
|
+
layout.addWidget(self._built_comparison_box(), 1)
|
|
89
|
+
layout.addWidget(self.message_label)
|
|
90
|
+
layout.addWidget(self.status_label)
|
|
91
|
+
|
|
92
|
+
def _built_scenarios_box(self) -> QGroupBox:
|
|
93
|
+
"""The scenario list with its add/remove buttons."""
|
|
94
|
+
self._scenarios_box = QGroupBox(self)
|
|
95
|
+
self.scenario_list = QListWidget(self._scenarios_box)
|
|
96
|
+
self.scenario_list.currentTextChanged.connect(self._render_selected_scenario)
|
|
97
|
+
self.add_scenario_button = QPushButton("", self._scenarios_box)
|
|
98
|
+
self.add_scenario_button.clicked.connect(self._add_scenario)
|
|
99
|
+
self.remove_scenario_button = QPushButton("", self._scenarios_box)
|
|
100
|
+
self.remove_scenario_button.clicked.connect(self._remove_scenario)
|
|
101
|
+
buttons = QHBoxLayout()
|
|
102
|
+
buttons.addWidget(self.add_scenario_button)
|
|
103
|
+
buttons.addWidget(self.remove_scenario_button)
|
|
104
|
+
layout = QVBoxLayout(self._scenarios_box)
|
|
105
|
+
layout.addWidget(self.scenario_list)
|
|
106
|
+
layout.addLayout(buttons)
|
|
107
|
+
return self._scenarios_box
|
|
108
|
+
|
|
109
|
+
def _built_overrides_box(self) -> QGroupBox:
|
|
110
|
+
"""The selected scenario's override table and its buttons."""
|
|
111
|
+
self._overrides_box = QGroupBox(self)
|
|
112
|
+
self.scenario_status_label = QLabel("", self._overrides_box)
|
|
113
|
+
self.scenario_status_label.setWordWrap(True)
|
|
114
|
+
self.overrides_table = read_only_table(self._overrides_box)
|
|
115
|
+
self.overrides_table.cellDoubleClicked.connect(self._edit_override)
|
|
116
|
+
self.add_override_button = QPushButton("", self._overrides_box)
|
|
117
|
+
self.add_override_button.clicked.connect(self._add_override)
|
|
118
|
+
self.remove_override_button = QPushButton("", self._overrides_box)
|
|
119
|
+
self.remove_override_button.clicked.connect(self._remove_override)
|
|
120
|
+
buttons = QHBoxLayout()
|
|
121
|
+
buttons.addWidget(self.add_override_button)
|
|
122
|
+
buttons.addWidget(self.remove_override_button)
|
|
123
|
+
layout = QVBoxLayout(self._overrides_box)
|
|
124
|
+
layout.addWidget(self.scenario_status_label)
|
|
125
|
+
layout.addWidget(self.overrides_table)
|
|
126
|
+
layout.addLayout(buttons)
|
|
127
|
+
return self._overrides_box
|
|
128
|
+
|
|
129
|
+
def _built_comparison_box(self) -> QGroupBox:
|
|
130
|
+
"""The comparison report: basis toggle, metric picker, table, chart."""
|
|
131
|
+
self._comparison_box = QGroupBox(self)
|
|
132
|
+
self._basis_box = QGroupBox(self._comparison_box)
|
|
133
|
+
self._basis_layout = QHBoxLayout(self._basis_box)
|
|
134
|
+
self.metric_label = QLabel("", self._comparison_box)
|
|
135
|
+
self.metric_combo = QComboBox(self._comparison_box)
|
|
136
|
+
self.metric_combo.activated.connect(self._metric_selected)
|
|
137
|
+
controls = QHBoxLayout()
|
|
138
|
+
controls.addWidget(self._basis_box)
|
|
139
|
+
controls.addWidget(self.metric_label)
|
|
140
|
+
controls.addWidget(self.metric_combo, 1)
|
|
141
|
+
self.comparison_tabs = QTabWidget(self._comparison_box)
|
|
142
|
+
self.comparison_table = read_only_table(self.comparison_tabs)
|
|
143
|
+
self._chart_container = QWidget(self.comparison_tabs)
|
|
144
|
+
self._chart_layout = QVBoxLayout(self._chart_container)
|
|
145
|
+
self.comparison_tabs.addTab(self.comparison_table, "")
|
|
146
|
+
self.comparison_tabs.addTab(self._chart_container, "")
|
|
147
|
+
layout = QVBoxLayout(self._comparison_box)
|
|
148
|
+
layout.addLayout(controls)
|
|
149
|
+
layout.addWidget(self.comparison_tabs, 1)
|
|
150
|
+
return self._comparison_box
|
|
151
|
+
|
|
152
|
+
def refresh(self, view_model: ScenariosViewModel) -> None:
|
|
153
|
+
"""Re-render the manager and comparison from a fresh view model.
|
|
154
|
+
|
|
155
|
+
The selected scenario and comparison sub-tab survive the
|
|
156
|
+
rebuild, so edits keep the user where they were.
|
|
157
|
+
"""
|
|
158
|
+
self._view_model = view_model
|
|
159
|
+
self._refresh_manager(view_model)
|
|
160
|
+
self._refresh_comparison(view_model)
|
|
161
|
+
self.message_label.setText(view_model.message)
|
|
162
|
+
self.message_label.setVisible(bool(view_model.message))
|
|
163
|
+
|
|
164
|
+
def selected_scenario_name(self) -> str:
|
|
165
|
+
"""The name of the scenario selected in the list, or blank."""
|
|
166
|
+
item = self.scenario_list.currentItem()
|
|
167
|
+
return item.text() if item is not None else ""
|
|
168
|
+
|
|
169
|
+
def _refresh_manager(self, view_model: ScenariosViewModel) -> None:
|
|
170
|
+
"""Re-render the scenario list and the selected override table."""
|
|
171
|
+
self._scenarios_box.setTitle(view_model.scenarios_heading)
|
|
172
|
+
self.add_scenario_button.setText(view_model.add_scenario_label)
|
|
173
|
+
self.remove_scenario_button.setText(view_model.remove_scenario_label)
|
|
174
|
+
self._overrides_box.setTitle(view_model.overrides_heading)
|
|
175
|
+
self.add_override_button.setText(view_model.add_override_label)
|
|
176
|
+
self.remove_override_button.setText(view_model.remove_override_label)
|
|
177
|
+
selected = self.selected_scenario_name()
|
|
178
|
+
with QSignalBlocker(self.scenario_list):
|
|
179
|
+
self.scenario_list.clear()
|
|
180
|
+
self.scenario_list.addItems([item.name for item in view_model.scenarios])
|
|
181
|
+
names = [item.name for item in view_model.scenarios]
|
|
182
|
+
if names:
|
|
183
|
+
row = names.index(selected) if selected in names else 0
|
|
184
|
+
self.scenario_list.setCurrentRow(row)
|
|
185
|
+
self._render_selected_scenario()
|
|
186
|
+
|
|
187
|
+
def _selected_item(self) -> ScenarioItem | None:
|
|
188
|
+
"""The view-model entry behind the selected scenario, if any."""
|
|
189
|
+
if self._view_model is None:
|
|
190
|
+
return None
|
|
191
|
+
name = self.selected_scenario_name()
|
|
192
|
+
for item in self._view_model.scenarios:
|
|
193
|
+
if item.name == name:
|
|
194
|
+
return item
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def _render_selected_scenario(self, _text: str = "") -> None:
|
|
198
|
+
"""Fill the override table for the scenario selected in the list."""
|
|
199
|
+
if self._view_model is None:
|
|
200
|
+
return
|
|
201
|
+
item = self._selected_item()
|
|
202
|
+
self._override_rows = item.overrides if item is not None else ()
|
|
203
|
+
self.scenario_status_label.setText(item.status if item is not None else "")
|
|
204
|
+
fill_table(
|
|
205
|
+
self.overrides_table,
|
|
206
|
+
self._view_model.overrides_columns,
|
|
207
|
+
[
|
|
208
|
+
(row.target_label, row.value, row.note, row.status)
|
|
209
|
+
for row in self._override_rows
|
|
210
|
+
],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def _refresh_comparison(self, view_model: ScenariosViewModel) -> None:
|
|
214
|
+
"""Re-render the basis toggle, metric picker, table, and chart."""
|
|
215
|
+
self._comparison_box.setTitle(view_model.comparison_heading)
|
|
216
|
+
self._basis_box.setTitle(view_model.basis_heading)
|
|
217
|
+
self._sync_basis_buttons(view_model)
|
|
218
|
+
self._sync_metric_combo(view_model)
|
|
219
|
+
self.comparison_tabs.setTabText(0, view_model.comparison_table_label)
|
|
220
|
+
self.comparison_tabs.setTabText(1, view_model.comparison_chart_label)
|
|
221
|
+
fill_table(
|
|
222
|
+
self.comparison_table,
|
|
223
|
+
view_model.comparison_columns,
|
|
224
|
+
list(view_model.comparison_rows),
|
|
225
|
+
)
|
|
226
|
+
while self._chart_layout.count():
|
|
227
|
+
entry = self._chart_layout.takeAt(0)
|
|
228
|
+
widget = entry.widget() if entry is not None else None
|
|
229
|
+
if widget is not None:
|
|
230
|
+
widget.deleteLater()
|
|
231
|
+
if view_model.chart is not None:
|
|
232
|
+
self._chart_layout.addWidget(
|
|
233
|
+
self._chart_view(view_model.chart, view_model.chart_categories)
|
|
234
|
+
)
|
|
235
|
+
self.comparison_tabs.setVisible(view_model.chart is not None)
|
|
236
|
+
|
|
237
|
+
def _sync_basis_buttons(self, view_model: ScenariosViewModel) -> None:
|
|
238
|
+
"""Create the basis radio buttons once; keep the selection bound."""
|
|
239
|
+
for option in view_model.basis_options:
|
|
240
|
+
button = self._basis_buttons.get(option.key)
|
|
241
|
+
if button is None:
|
|
242
|
+
button = QRadioButton(option.label, self._basis_box)
|
|
243
|
+
button.clicked.connect(
|
|
244
|
+
lambda _checked=False, key=option.key: self._callbacks.select_basis(
|
|
245
|
+
key
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
self._basis_layout.addWidget(button)
|
|
249
|
+
self._basis_buttons[option.key] = button
|
|
250
|
+
button.setText(option.label)
|
|
251
|
+
button.setChecked(option.key == view_model.selected_basis_key)
|
|
252
|
+
|
|
253
|
+
def _sync_metric_combo(self, view_model: ScenariosViewModel) -> None:
|
|
254
|
+
"""Rebuild the metric picker; keep the selection bound."""
|
|
255
|
+
self.metric_label.setText(view_model.metric_heading)
|
|
256
|
+
with QSignalBlocker(self.metric_combo):
|
|
257
|
+
self.metric_combo.clear()
|
|
258
|
+
for option in view_model.metric_options:
|
|
259
|
+
self.metric_combo.addItem(option.label, userData=option.key)
|
|
260
|
+
keys = [option.key for option in view_model.metric_options]
|
|
261
|
+
if view_model.selected_metric_key in keys:
|
|
262
|
+
self.metric_combo.setCurrentIndex(
|
|
263
|
+
keys.index(view_model.selected_metric_key)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def _metric_selected(self, index: int) -> None:
|
|
267
|
+
"""Forward a user-chosen metric's key to the shell."""
|
|
268
|
+
key = self.metric_combo.itemData(index)
|
|
269
|
+
if key is not None:
|
|
270
|
+
self._callbacks.select_metric(str(key))
|
|
271
|
+
|
|
272
|
+
def _add_scenario(self) -> None:
|
|
273
|
+
"""Prompt for a scenario name and forward it to the shell."""
|
|
274
|
+
if self._view_model is None: # pragma: no cover - buttons imply a model
|
|
275
|
+
return
|
|
276
|
+
text, accepted = QInputDialog.getText(
|
|
277
|
+
self, self._view_model.name_prompt_title, self._view_model.name_prompt
|
|
278
|
+
)
|
|
279
|
+
if not accepted:
|
|
280
|
+
return
|
|
281
|
+
error = self._callbacks.add_scenario(text)
|
|
282
|
+
self.status_label.setText(error or "")
|
|
283
|
+
|
|
284
|
+
def _remove_scenario(self) -> None:
|
|
285
|
+
"""Remove the selected scenario."""
|
|
286
|
+
name = self.selected_scenario_name()
|
|
287
|
+
if name:
|
|
288
|
+
self._callbacks.remove_scenario(name)
|
|
289
|
+
self.status_label.setText("")
|
|
290
|
+
|
|
291
|
+
def _add_override(self) -> None:
|
|
292
|
+
"""Pick a target from the whitelist, prompt for a value, forward."""
|
|
293
|
+
view_model = self._view_model
|
|
294
|
+
scenario = self.selected_scenario_name()
|
|
295
|
+
if view_model is None or not scenario or not view_model.target_options:
|
|
296
|
+
return
|
|
297
|
+
labels = [option.label for option in view_model.target_options]
|
|
298
|
+
label, accepted = QInputDialog.getItem(
|
|
299
|
+
self,
|
|
300
|
+
view_model.target_prompt_title,
|
|
301
|
+
view_model.target_prompt,
|
|
302
|
+
labels,
|
|
303
|
+
0,
|
|
304
|
+
editable=False,
|
|
305
|
+
)
|
|
306
|
+
if not accepted:
|
|
307
|
+
return
|
|
308
|
+
option = view_model.target_options[labels.index(label)]
|
|
309
|
+
text, accepted = self._value_prompt(
|
|
310
|
+
option.edit_text, structured=option.structured, choices=option.choices
|
|
311
|
+
)
|
|
312
|
+
if not accepted:
|
|
313
|
+
return
|
|
314
|
+
error = self._callbacks.set_override(scenario, option.key, text)
|
|
315
|
+
self.status_label.setText(error or "")
|
|
316
|
+
|
|
317
|
+
def _edit_override(self, row_index: int, _column: int) -> None:
|
|
318
|
+
"""Re-prompt the value of a double-clicked override row."""
|
|
319
|
+
scenario = self.selected_scenario_name()
|
|
320
|
+
if not scenario or row_index >= len(self._override_rows):
|
|
321
|
+
return
|
|
322
|
+
row = self._override_rows[row_index]
|
|
323
|
+
text, accepted = self._value_prompt(
|
|
324
|
+
row.edit_text, structured=row.structured, choices=row.choices
|
|
325
|
+
)
|
|
326
|
+
if not accepted:
|
|
327
|
+
return
|
|
328
|
+
error = self._callbacks.set_override(scenario, row.target_key, text)
|
|
329
|
+
self.status_label.setText(error or "")
|
|
330
|
+
|
|
331
|
+
def _remove_override(self) -> None:
|
|
332
|
+
"""Remove the override behind the selected table row."""
|
|
333
|
+
scenario = self.selected_scenario_name()
|
|
334
|
+
row_index = self.overrides_table.currentRow()
|
|
335
|
+
if not scenario or not 0 <= row_index < len(self._override_rows):
|
|
336
|
+
return
|
|
337
|
+
row = self._override_rows[row_index]
|
|
338
|
+
self._callbacks.remove_override(scenario, row.target_key)
|
|
339
|
+
self.status_label.setText("")
|
|
340
|
+
|
|
341
|
+
def _value_prompt(
|
|
342
|
+
self, edit_text: str, *, structured: bool, choices: tuple[str, ...]
|
|
343
|
+
) -> tuple[str, bool]:
|
|
344
|
+
"""The value dialog a target's shape calls for; text and accepted."""
|
|
345
|
+
view_model = self._view_model
|
|
346
|
+
if view_model is None: # pragma: no cover - prompts imply a model
|
|
347
|
+
return "", False
|
|
348
|
+
if choices:
|
|
349
|
+
current = choices.index(edit_text) if edit_text in choices else 0
|
|
350
|
+
return QInputDialog.getItem(
|
|
351
|
+
self,
|
|
352
|
+
view_model.value_prompt_title,
|
|
353
|
+
view_model.value_prompt,
|
|
354
|
+
list(choices),
|
|
355
|
+
current,
|
|
356
|
+
editable=False,
|
|
357
|
+
)
|
|
358
|
+
if structured:
|
|
359
|
+
return QInputDialog.getMultiLineText(
|
|
360
|
+
self,
|
|
361
|
+
view_model.value_prompt_title,
|
|
362
|
+
view_model.table_value_prompt,
|
|
363
|
+
edit_text,
|
|
364
|
+
)
|
|
365
|
+
return QInputDialog.getText(
|
|
366
|
+
self,
|
|
367
|
+
view_model.value_prompt_title,
|
|
368
|
+
view_model.value_prompt,
|
|
369
|
+
text=edit_text,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def _chart_view(self, chart: ChartSpec, categories: tuple[str, ...]) -> QChartView:
|
|
373
|
+
"""One grouped bar chart bound to a chart spec — runs side by side."""
|
|
374
|
+
series = QBarSeries()
|
|
375
|
+
for entry in chart.series:
|
|
376
|
+
series.append(tooltip_bar_set(entry, categories))
|
|
377
|
+
|
|
378
|
+
qchart = QChart()
|
|
379
|
+
qchart.addSeries(series)
|
|
380
|
+
|
|
381
|
+
x_axis = QBarCategoryAxis()
|
|
382
|
+
x_axis.append(list(categories))
|
|
383
|
+
x_axis.setLabelsAngle(_CATEGORY_LABEL_ANGLE)
|
|
384
|
+
qchart.addAxis(x_axis, Qt.AlignmentFlag.AlignBottom)
|
|
385
|
+
series.attachAxis(x_axis)
|
|
386
|
+
|
|
387
|
+
y_axis = QValueAxis()
|
|
388
|
+
y_axis.setTitleText(chart.y_axis_label)
|
|
389
|
+
y_axis.setRange(0.0, float(chart.y_axis_max))
|
|
390
|
+
y_axis.applyNiceNumbers()
|
|
391
|
+
qchart.addAxis(y_axis, Qt.AlignmentFlag.AlignLeft)
|
|
392
|
+
series.attachAxis(y_axis)
|
|
393
|
+
|
|
394
|
+
view = QChartView(qchart, self._chart_container)
|
|
395
|
+
view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
396
|
+
return view
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
__all__ = [
|
|
400
|
+
"ScenariosPane",
|
|
401
|
+
"ScenariosPaneCallbacks",
|
|
402
|
+
]
|