Skip to content

Commit 2d700b0

Browse files
committed
Restyle realization status indicators
The pastel fills with a pie-chart overlay and a "finished / total" caption are hard to scan at larger realization counts, and the pie competes with the status fill for the same shape. Replace it with a status dot plus a progress ring drawn only while running, dropping the caption the ring now conveys. Shrink the cells so more of the ensemble fits on screen.
1 parent 0dc49c7 commit 2d700b0

5 files changed

Lines changed: 145 additions & 102 deletions

File tree

src/ert/ensemble_evaluator/state.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from typing import Final
22

3-
COLOR_FAILED: Final = (255, 200, 200)
4-
COLOR_FINISHED: Final = (127, 201, 127)
5-
COLOR_PENDING: Final = (190, 174, 212)
6-
COLOR_RUNNING: Final = (255, 255, 153)
7-
COLOR_UNKNOWN: Final = (128, 128, 128)
8-
COLOR_WAITING: Final = (164, 200, 255)
9-
COLOR_CANCELLED: Final = (235, 242, 246)
3+
COLOR_QUEUED: Final = (183, 191, 199)
4+
COLOR_FAILED: Final = (207, 34, 46)
5+
COLOR_FINISHED: Final = (45, 164, 78)
6+
COLOR_PENDING: Final = COLOR_QUEUED
7+
COLOR_RUNNING: Final = (214, 178, 42)
8+
COLOR_UNKNOWN: Final = (140, 149, 159)
9+
COLOR_WAITING: Final = COLOR_QUEUED
10+
COLOR_CANCELLED: Final = (234, 238, 242)
1011
COLOR_WARNING: Final = (255, 103, 0)
1112

1213
ENSEMBLE_STATE_CANCELLED: Final = "Cancelled"

src/ert/gui/experiments/view/progress_widget.py

Lines changed: 67 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,79 +11,98 @@
1111
QVBoxLayout,
1212
)
1313

14-
from ert.ensemble_evaluator.state import ENSEMBLE_STATE_FAILED, REAL_STATE_TO_COLOR
14+
from ert.ensemble_evaluator.state import (
15+
ENSEMBLE_STATE_FAILED,
16+
REAL_STATE_TO_COLOR,
17+
REALIZATION_STATE_WAITING,
18+
)
1519

1620

1721
class ProgressWidget(QFrame):
1822
def __init__(self) -> None:
1923
super().__init__()
2024
self.setFixedHeight(70)
2125

22-
self._vertical_layout = QVBoxLayout(self)
23-
self._vertical_layout.setContentsMargins(0, 0, 0, 0)
24-
self._vertical_layout.setSpacing(0)
25-
self.setLayout(self._vertical_layout)
26+
self._main_layout = QVBoxLayout(self)
27+
self._main_layout.setContentsMargins(0, 0, 0, 0)
28+
self._main_layout.setSpacing(2)
29+
self.setLayout(self._main_layout)
2630

2731
self._waiting_progress_bar = QProgressBar(self)
2832
self._waiting_progress_bar.setRange(0, 0)
2933
self._waiting_progress_bar.setFixedHeight(30)
30-
self._vertical_layout.addWidget(self._waiting_progress_bar)
34+
self._main_layout.addWidget(self._waiting_progress_bar)
3135

3236
self._progress_frame = QFrame(self)
33-
self._vertical_layout.addWidget(self._progress_frame)
37+
self._main_layout.addWidget(self._progress_frame)
3438

3539
self._horizontal_layout = QHBoxLayout(self._progress_frame)
3640
self._horizontal_layout.setContentsMargins(0, 0, 0, 0)
3741
self._horizontal_layout.setSpacing(0)
3842
self._progress_frame.setLayout(self._horizontal_layout)
3943

4044
self._legend_frame = QFrame(self)
41-
self._vertical_layout.addWidget(self._legend_frame)
42-
self._legend_frame.setFixedHeight(30)
43-
self._horizontal_legend_layout = QHBoxLayout(self._legend_frame)
44-
self._horizontal_legend_layout.setContentsMargins(0, 0, 0, 0)
45-
self._horizontal_legend_layout.setSpacing(0)
45+
self._main_layout.addWidget(self._legend_frame)
46+
self._legend_frame.setFixedHeight(24)
47+
self._legend_layout = QHBoxLayout(self._legend_frame)
48+
self._legend_layout.setContentsMargins(0, 0, 0, 0)
49+
self._legend_layout.setSpacing(6)
4650

47-
self._status: dict[str, int] = {}
51+
self._status_counts: dict[str, int] = {}
4852
self._realization_count = 0
49-
self._progress_label_map: dict[str, QLabel] = {}
50-
self._legend_map_text = {}
51-
52-
for state, color in REAL_STATE_TO_COLOR.items():
53-
label = QLabel(self)
54-
label.setVisible(False)
55-
label.setObjectName(f"progress_{state}")
56-
label.setStyleSheet(f"background-color : {QColor(*color).name()}")
57-
self._progress_label_map[state] = label
58-
self._horizontal_layout.addWidget(label)
59-
60-
label = QLabel(self)
61-
label.setFixedSize(20, 20)
62-
label.setStyleSheet(
63-
f"background-color : {QColor(*color).name()}; border: 1px solid black;"
64-
)
65-
self._horizontal_legend_layout.addWidget(label)
66-
67-
label = QLabel(self)
68-
label.setObjectName(f"progress_label_text_{state}")
69-
label.setText(f" {state} ({0}/{0})")
70-
self._legend_map_text[state] = label
71-
self._horizontal_legend_layout.addWidget(label)
53+
self._progress_segments: dict[str, QLabel] = {}
54+
self._legend_labels = {}
55+
for realization_status, status_color_rgb in REAL_STATE_TO_COLOR.items():
56+
status_color_hex = QColor(*status_color_rgb).name()
57+
58+
progress_segment = QLabel(self)
59+
progress_segment.setVisible(False)
60+
progress_segment.setObjectName(f"progress_{realization_status}")
61+
progress_segment.setStyleSheet(f"background-color : {status_color_hex}")
62+
self._progress_segments[realization_status] = progress_segment
63+
self._horizontal_layout.addWidget(progress_segment)
64+
65+
if realization_status == REALIZATION_STATE_WAITING:
66+
marker_style = (
67+
"background-color: transparent;"
68+
"border-radius: 7px;"
69+
f"border: 2px solid {status_color_hex};"
70+
)
71+
else:
72+
marker_style = (
73+
f"background-color: {status_color_hex};border-radius: 7px;"
74+
)
75+
76+
legend_marker = QLabel(self)
77+
legend_marker.setFixedSize(14, 14)
78+
legend_marker.setStyleSheet(marker_style)
79+
self._legend_layout.addWidget(legend_marker)
80+
81+
legend_label = QLabel(self)
82+
legend_label.setObjectName(f"progress_label_text_{realization_status}")
83+
legend_label.setText(f"{realization_status} ({0}/{0})")
84+
self._legend_labels[realization_status] = legend_label
85+
self._legend_layout.addWidget(legend_label)
86+
self._legend_layout.addSpacing(16)
87+
88+
self._legend_layout.addStretch()
7289

7390
def repaint_components(self) -> None:
7491
if self._realization_count > 0:
7592
full_width = self.width()
7693
self.stop_waiting_progress_bar()
7794

78-
for state, label in self._progress_label_map.items():
79-
label.setVisible(True)
80-
count = self._status.get(state, 0)
95+
for realization_status, progress_segment in self._progress_segments.items():
96+
progress_segment.setVisible(True)
97+
count = self._status_counts.get(realization_status, 0)
8198
width = int((count / self._realization_count) * full_width)
82-
label.setFixedWidth(width)
99+
progress_segment.setFixedWidth(width)
83100

84-
for state, label in self._legend_map_text.items():
85-
label.setText(
86-
f" {state} ({self._status.get(state, 0)}/{self._realization_count})"
101+
for realization_status, legend_label in self._legend_labels.items():
102+
legend_label.setText(
103+
f"{realization_status} "
104+
f"({self._status_counts.get(realization_status, 0)}/"
105+
f"{self._realization_count})"
87106
)
88107

89108
def stop_waiting_progress_bar(self) -> None:
@@ -95,13 +114,13 @@ def start_waiting_progress_bar(self) -> None:
95114
def set_all_failed(self) -> None:
96115
self.stop_waiting_progress_bar()
97116
full_width = self.width()
98-
for state, label in self._progress_label_map.items():
99-
label.setVisible(True)
100-
width = full_width if state == ENSEMBLE_STATE_FAILED else 0
101-
label.setFixedWidth(width)
117+
for realization_status, progress_segment in self._progress_segments.items():
118+
progress_segment.setVisible(True)
119+
width = full_width if realization_status == ENSEMBLE_STATE_FAILED else 0
120+
progress_segment.setFixedWidth(width)
102121

103122
def update_progress(self, status: dict[str, int], realization_count: int) -> None:
104-
self._status = status
123+
self._status_counts = status
105124
self._realization_count = realization_count
106125
if status.get("Finished", 0) < self._realization_count:
107126
self.start_waiting_progress_bar()

src/ert/gui/experiments/view/realization.py

Lines changed: 68 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
QItemSelectionModel,
77
QModelIndex,
88
QObject,
9-
QPoint,
9+
QRect,
1010
QSize,
1111
Qt,
1212
)
@@ -23,12 +23,17 @@
2323
QWidget,
2424
)
2525

26+
from ert.ensemble_evaluator.state import (
27+
REALIZATION_STATE_RUNNING,
28+
REALIZATION_STATE_WAITING,
29+
)
2630
from ert.gui.model.real_list import RealListModel
2731
from ert.gui.model.snapshot import (
2832
CallbackStatusMessageRole,
2933
FMStepColorHint,
3034
MemoryUsageRole,
3135
RealIens,
36+
StatusRole,
3237
)
3338
from ert.shared.status.utils import byte_with_unit
3439

@@ -40,7 +45,7 @@ def __init__(self, it: int, parent: QWidget | None = None) -> None:
4045
super().__init__(parent)
4146

4247
self._iter = it
43-
self._delegate_size = QSize(90, 90)
48+
self._delegate_size = QSize(70, 70)
4449

4550
self._real_view = QListView(self)
4651
self._real_view.setViewMode(QListView.ViewMode.IconMode)
@@ -95,80 +100,96 @@ def refresh_current_selection(self) -> None:
95100

96101

97102
class RealizationDelegate(QStyledItemDelegate):
98-
def __init__(self, size: QSize, parent: QObject) -> None:
103+
_STATUS_DOT_DIAMETER = 32
104+
_PROGRESS_RING_MARGIN = 4
105+
_PROGRESS_RING_WIDTH = 5
106+
107+
def __init__(self, item_size: QSize, parent: QObject) -> None:
99108
super().__init__(parent)
100-
self._size = size
109+
self._item_size = item_size
101110
parent.installEventFilter(self)
102-
self.adjustment_point_for_job_rect_margin = QPoint(-20, -20)
103-
self._color_black = QColor(0, 0, 0, 180)
104-
self._color_progress = QColor(50, 173, 230, 200)
105-
self._color_lightgray = QColor("LightGray").lighter(120)
106-
self._pen_black = QPen(self._color_black, 2, Qt.PenStyle.SolidLine)
111+
self._progress_track_color = QColor(0, 0, 0, 35)
112+
self._status_dot_outline_pen = QPen(
113+
QColor(0, 0, 0, 45), 1, Qt.PenStyle.SolidLine
114+
)
107115

108116
@override
109117
def paint(
110118
self, painter: QPainter | None, option: QStyleOptionViewItem, index: QModelIndex
111119
) -> None:
112120
if painter is None:
113121
return
114-
text = index.data(RealIens)
115-
selected_color, finished_count, total_count = tuple(index.data(FMStepColorHint))
122+
realization_label = index.data(RealIens)
123+
realization_status = index.data(StatusRole)
124+
realization_status_color, completed_step_count, total_step_count = tuple(
125+
index.data(FMStepColorHint)
126+
)
116127

117128
painter.save()
118129
painter.setRenderHint(QPainter.RenderHint.TextAntialiasing, True)
119130
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
120131

121-
percentage_done = (
122-
100 if total_count < 1 else int((finished_count * 100.0) / total_count)
123-
)
124-
125-
painter.setPen(self._pen_black)
126-
adjusted_rect = option.rect.adjusted(2, 2, -2, -2)
127-
128-
painter.setBrush(
129-
self._color_progress if percentage_done == 100 else self._color_lightgray
132+
if option.state & QStyle.StateFlag.State_Selected:
133+
selection_color = QColor(option.palette.color(QPalette.ColorRole.Highlight))
134+
selection_color.setAlpha(60)
135+
painter.setPen(Qt.PenStyle.NoPen)
136+
painter.setBrush(selection_color)
137+
painter.drawRoundedRect(option.rect.adjusted(2, 2, -2, -2), 6, 6)
138+
139+
status_dot_rect = QRect(
140+
option.rect.center().x() - self._STATUS_DOT_DIAMETER // 2,
141+
option.rect.top() + 8,
142+
self._STATUS_DOT_DIAMETER,
143+
self._STATUS_DOT_DIAMETER,
130144
)
131-
painter.drawEllipse(adjusted_rect)
132-
133-
if 0 < percentage_done < 100:
134-
painter.setBrush(self._color_progress)
135-
painter.drawPie(adjusted_rect, 1440, -int(percentage_done * 57.6))
136145

137-
if option.state & QStyle.StateFlag.State_Selected:
138-
factor: int = (
139-
125
140-
if selected_color.lighter(125).getRgb() != (255, 255, 255, 255)
141-
else 110
146+
if realization_status == REALIZATION_STATE_RUNNING and total_step_count > 0:
147+
progress_ring_rect = status_dot_rect.adjusted(
148+
-self._PROGRESS_RING_MARGIN,
149+
-self._PROGRESS_RING_MARGIN,
150+
self._PROGRESS_RING_MARGIN,
151+
self._PROGRESS_RING_MARGIN,
152+
)
153+
painter.setBrush(Qt.BrushStyle.NoBrush)
154+
painter.setPen(QPen(self._progress_track_color, self._PROGRESS_RING_WIDTH))
155+
painter.drawEllipse(progress_ring_rect)
156+
progress_arc_pen = QPen(realization_status_color, self._PROGRESS_RING_WIDTH)
157+
progress_arc_pen.setCapStyle(Qt.PenCapStyle.RoundCap)
158+
painter.setPen(progress_arc_pen)
159+
painter.drawArc(
160+
progress_ring_rect,
161+
90 * 16,
162+
-int(360 * 16 * completed_step_count / total_step_count),
142163
)
143-
selected_color = selected_color.lighter(factor)
144-
145-
painter.setBrush(selected_color)
146-
adjusted_rect = option.rect.adjusted(7, 7, -7, -7)
147-
painter.drawEllipse(adjusted_rect)
148164

149-
font = painter.font()
150-
font.setBold(True)
151-
painter.setFont(font)
165+
painter.setPen(self._status_dot_outline_pen)
166+
if realization_status == REALIZATION_STATE_WAITING:
167+
painter.setBrush(Qt.BrushStyle.NoBrush)
168+
painter.setPen(QPen(realization_status_color, 2))
169+
painter.drawEllipse(status_dot_rect.adjusted(1, 1, -1, -1))
170+
else:
171+
painter.setBrush(realization_status_color)
172+
painter.drawEllipse(status_dot_rect)
152173

153-
adj_rect = option.rect.adjusted(0, 20, 0, 0)
154-
painter.drawText(adj_rect, Qt.AlignmentFlag.AlignHCenter, text)
155-
adj_rect = option.rect.adjusted(0, 45, 0, 0)
174+
painter.setPen(option.palette.color(QPalette.ColorRole.Text))
156175
painter.drawText(
157-
adj_rect, Qt.AlignmentFlag.AlignHCenter, f"{finished_count} / {total_count}"
176+
option.rect.adjusted(0, self._STATUS_DOT_DIAMETER + 14, 0, 0),
177+
Qt.AlignmentFlag.AlignHCenter,
178+
realization_label,
158179
)
159180

160181
painter.restore()
161182

162183
@override
163184
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
164-
return self._size
185+
return self._item_size
165186

166187
@override
167188
def eventFilter(self, object: QObject | None, event: QEvent | None) -> bool:
168189
if isinstance(event, QHelpEvent) and event.type() == QEvent.Type.ToolTip:
169-
mouse_pos = event.pos() + self.adjustment_point_for_job_rect_margin
170190
parent: RealizationWidget = cast(RealizationWidget, self.parent())
171191
view = parent._real_view
192+
mouse_pos = view.viewport().mapFrom(parent, event.pos())
172193
index = view.indexAt(mouse_pos)
173194
if index.isValid():
174195
tooltip_text = ""
@@ -182,7 +203,9 @@ def eventFilter(self, object: QObject | None, event: QEvent | None) -> bool:
182203
tooltip_text += callback_error_msg
183204
if tooltip_text:
184205
parent.triggeredTooltipTextDisplay.emit(tooltip_text)
185-
QToolTip.showText(view.mapToGlobal(mouse_pos), tooltip_text)
206+
QToolTip.showText(
207+
view.viewport().mapToGlobal(mouse_pos), tooltip_text
208+
)
186209
return True
187210

188211
return super().eventFilter(object, event)

tests/ert/ui_tests/gui/test_missing_runpath.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def inner():
143143
)
144144
assert not run_dialog._progress_widget._waiting_progress_bar.isVisible()
145145
assert (
146-
run_dialog._progress_widget._progress_label_map[
146+
run_dialog._progress_widget._progress_segments[
147147
ENSEMBLE_STATE_FAILED
148148
].width()
149149
== run_dialog._progress_widget.width()

tests/ert/unit_tests/gui/experiments/view/test_legend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def test_marker_label_text_correct(qtbot, status: dict[str, int]):
3131

3232
assert label_marker
3333
count = status.get(state, 0)
34-
assert f" {state} ({count}/{realization_count})" in label_marker.text()
34+
assert f"{state} ({count}/{realization_count})" in label_marker.text()
3535

3636

3737
@given(

0 commit comments

Comments
 (0)