• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nens / ThreeDiToolbox / #2590

23 Sep 2025 03:04PM UTC coverage: 34.904% (-0.2%) from 35.146%
#2590

push

coveralls-python

web-flow
Merge 98b7685a1 into f6f4be1e7

79 of 358 new or added lines in 42 files covered. (22.07%)

8 existing lines in 6 files now uncovered.

4875 of 13967 relevant lines covered (34.9%)

0.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

15.0
/tool_fraction_analysis/fraction_plot.py
1
import re
1✔
2
from qgis.PyQt.QtGui import QColor
1✔
3
import pyqtgraph as pg
1✔
4
from qgis.PyQt.QtCore import pyqtSignal
1✔
5

6
from threedi_results_analysis.utils.geo_utils import distance_to_polyline
1✔
7
from threedi_results_analysis.utils.color import reduce_saturation, increase_value
1✔
8

9
pg.setConfigOption("background", "w")
1✔
10
pg.setConfigOption("foreground", "k")
1✔
11
from qgis.PyQt.QtCore import Qt
1✔
12
from threedi_results_analysis.threedi_plugin_model import ThreeDiPluginModel
1✔
13
from threedi_results_analysis.tool_fraction_analysis.fraction_model import FractionModel
1✔
14

15

16
class FractionPlot(pg.PlotWidget):
1✔
17
    hover_plot = pyqtSignal(str)
1✔
18

19
    def __init__(self, parent, result_model: ThreeDiPluginModel, fraction_model: FractionModel):
1✔
20
        super().__init__(parent)
×
21
        self.showGrid(True, True, 0.5)
×
NEW
22
        self.scene().sigMouseMoved.connect(self.mouse_moved)
×
23
        self.fraction_model = fraction_model
×
24
        self.result_model = result_model
×
25
        # map from substance name to (list of) plot
26
        self.item_map = {}
×
27
        self.setLabel("bottom", "Time", "hrs")
×
28
        self.setLabel("left", "Concentration", "")
×
29
        self.getAxis("left").enableAutoSIPrefix(False)
×
NEW
30
        self.mouseLabel = pg.TextItem(text="", anchor=(1, 1), color=(0, 0, 0))
×
31

32
        # self.proxy = pg.SignalProxy(self.scene().sigMouseMoved, rateLimit=20, slot=self.mouse_moved)
33

34
    def clear_plot(self):
1✔
35
        self.clear()
×
36
        self.item_map.clear()
×
37
        self.setLabel("left", "Concentration", "")
×
NEW
38
        self.addItem(self.mouseLabel)
×
NEW
39
        self.mouseLabel.setText("")
×
40

41
    def item_checked(self, model_item):
1✔
NEW
42
        if not self.item_map:  # No plots yet
×
NEW
43
            return
×
44
        substance = model_item.data()
×
45
        for plot in self.item_map[substance]:
×
NEW
46
            plot.setVisible(model_item.checkState() == Qt.CheckState.Checked)
×
47

48
    def mouse_moved(self, pos):
1✔
49
        # Translate scene position to plot coordinates
NEW
50
        mouse_point = self.plotItem.vb.mapSceneToView(pos)
×
NEW
51
        x = mouse_point.x()
×
NEW
52
        y = mouse_point.y()
×
53

54
        # find closest plot (if any)
NEW
55
        min_dist = float("inf")
×
NEW
56
        closest_substance = None
×
NEW
57
        closest_data_point = None
×
NEW
58
        for item in self.plotItem.listDataItems():
×
NEW
59
            x_data, y_data = item.getData()
×
NEW
60
            dist, data_point = distance_to_polyline(x, y, x_data, y_data)
×
NEW
61
            if dist < 0.3:
×
NEW
62
                if dist < min_dist:
×
NEW
63
                    min_dist = dist
×
64
                    # Check which substance this plot corresponds to
NEW
65
                    for substance, plots in self.item_map.items():
×
NEW
66
                        for plot in plots:
×
NEW
67
                            if plot is item:
×
NEW
68
                                closest_substance = substance
×
NEW
69
                                closest_data_point = data_point
×
NEW
70
                                break
×
NEW
71
                assert closest_substance  # We should always find a plot
×
72

NEW
73
        if closest_substance is not None:
×
NEW
74
            self.mouseLabel.setText("(%0.1f, %0.1f)" % (closest_data_point[0], closest_data_point[1]))
×
NEW
75
            self.mouseLabel.setPos(x, y)
×
76
        else:
NEW
77
            self.mouseLabel.setText("")
×
78

NEW
79
        self.hover_plot.emit(closest_substance)
×
80

81
    def item_color_changed(self, color_model_item):
1✔
NEW
82
        if not self.item_map:  # No plots yet
×
NEW
83
            return
×
84

85
        # Retrieve the substance name from the model
NEW
86
        row = color_model_item.index().row()
×
NEW
87
        selected_model_item = color_model_item.model().item(row, 0)
×
NEW
88
        substance = selected_model_item.data()
×
89

NEW
90
        style, color, width = color_model_item.data()[0]
×
NEW
91
        pen = pg.mkPen(color=QColor(*color), width=width, style=style)
×
NEW
92
        self.item_map[substance][0].setPen(pen)
×
93

NEW
94
        if len(self.item_map[substance]) == 2:
×
95
            # there is a fill, also change that color
NEW
96
            fill_color = self.reduce_saturation(QColor(*color))
×
NEW
97
            self.item_map[substance][1].setBrush(pg.mkBrush(fill_color))
×
98

99
    def highlight_plot(self, row):
1✔
100

NEW
101
        if not self.item_map:  # No plots yet
×
NEW
102
            return
×
103

NEW
104
        self.unhighlight_plots()
×
105

NEW
106
        hovered_model_item = self.fraction_model.item(row, 0)
×
NEW
107
        substance = hovered_model_item.data()
×
108

NEW
109
        hovered_color_item = self.fraction_model.item(row, 1)
×
110
        # Original color
NEW
111
        style, color, width = hovered_color_item.data()[1]
×
NEW
112
        highlight_color = increase_value(QColor(*color))
×
NEW
113
        pen = pg.mkPen(color=highlight_color, width=width, style=style)
×
NEW
114
        self.item_map[substance][0].setPen(pen)
×
115
        # also set fill color
NEW
116
        if len(self.item_map[substance]) == 2:
×
NEW
117
            self.item_map[substance][1].setBrush(pg.mkBrush(highlight_color))
×
118

119
    def unhighlight_plots(self):
1✔
NEW
120
        if not self.item_map:  # No plots yet
×
NEW
121
            return
×
122

NEW
123
        for row in range(self.fraction_model.rowCount()):
×
NEW
124
            substance = self.fraction_model.item(row, 0).data()
×
125
            # original color
NEW
126
            style, color, width = self.fraction_model.item(row, 1).data()[1]
×
127

NEW
128
            pen = pg.mkPen(color=QColor(*color), width=width, style=style)
×
NEW
129
            self.item_map[substance][0].setPen(pen)
×
NEW
130
            if len(self.item_map[substance]) == 2:
×
NEW
131
                fill_color = reduce_saturation(QColor(*color))
×
NEW
132
                self.item_map[substance][1].setBrush(pg.mkBrush(fill_color))
×
133

134
    def fraction_selected(self, feature_id, substance_unit: str, time_unit: str, stacked: bool, volume: bool):
1✔
135
        """
136
        Retrieve info from model and create plots
137
        """
138
        self.clear_plot()
×
139

140
        substance_unit_conversion = 1.0
×
141

142
        if volume:
×
143
            # for known units, we apply basic conversion
144
            pattern = r"^(.*)/\s*(m3|l)\s*$"
×
145
            matches = re.findall(pattern, substance_unit, flags=re.IGNORECASE)
×
146
            if len(matches) == 1 and len(matches[0]) == 2:
×
147
                if matches[0][1].lower() == "l":
×
148
                    substance_unit_conversion = 1000.0
×
149
                    processed_substance_unit = matches[0][0].strip()
×
150
                    volume_label = "Load"
×
151
                elif matches[0][1].lower() == "m3":
×
152
                    substance_unit_conversion = 1.0
×
153
                    processed_substance_unit = matches[0][0].strip()
×
154
                    volume_label = "Load"
×
155
            elif substance_unit.strip() == "%":
×
156
                substance_unit_conversion = 1.0
×
157
                processed_substance_unit = "m<sup>3</sup>"
×
158
                volume_label = "Volume"
×
159
            else:  # unknown, take original unit as-is and append x m3
160
                substance_unit_conversion = 1.0
×
161
                processed_substance_unit = f"{substance_unit} ยท m<sup>3</sup>"
×
162
                volume_label = "Load"
×
163

164
        plots = self.fraction_model.create_plots(feature_id, time_unit, stacked, volume, substance_unit_conversion)
×
165
        prev_plot = None
×
NEW
166
        for substance, plot, visible in plots:
×
167
            self.item_map[substance] = [plot]
×
168
            plot.setZValue(100)
×
NEW
169
            plot.setVisible(visible)
×
UNCOV
170
            self.addItem(plot)
×
171

172
            if stacked:
×
173
                # Add fill between consecutive plots
174
                plot_color = plot.opts['pen'].color()
×
175
                # Reduce saturation for fill
176
                fill_color = self.reduce_saturation(plot_color)
×
177
                if not prev_plot:
×
178
                    # this is the first, just fill downward to axis
179
                    plot.setFillLevel(0)
×
180
                    plot.setFillBrush(pg.mkBrush(fill_color))
×
181
                else:
182
                    fill = pg.FillBetweenItem(plot, prev_plot, pg.mkBrush(fill_color))
×
183
                    fill.setZValue(20)
×
NEW
184
                    fill.setVisible(visible)
×
185
                    self.addItem(fill)
×
186
                    self.item_map[substance].append(fill)
×
187

188
            prev_plot = plot
×
189

190
        self.setLabel("left", volume_label if volume else "Concentration", processed_substance_unit if volume else substance_unit)
×
191
        self.plotItem.vb.menu.viewAll.triggered.emit()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc