Skip to content

Commit 365ecad

Browse files
committed
Downsampling: added error bar support, tests, ...
1 parent c62d872 commit 365ecad

18 files changed

Lines changed: 177 additions & 2893 deletions

File tree

doc/features/tools/overview.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ The `tools` module provides the following tools:
5252
* :py:class:`.tools.HRangeTool`
5353
* :py:class:`.tools.DummySeparatorTool`
5454
* :py:class:`.tools.AntiAliasingTool`
55+
* :py:class:`.tools.DownSamplingTool`
5556
* :py:class:`.tools.DisplayCoordsTool`
5657
* :py:class:`.tools.ReverseYAxisTool`
5758
* :py:class:`.tools.AspectRatioTool`

doc/features/tools/reference.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ Reference
5353
:members:
5454
.. autoclass:: plotpy.tools.AntiAliasingTool
5555
:members:
56+
.. autoclass:: plotpy.tools.DownSamplingTool
57+
:members:
5658
.. autoclass:: plotpy.tools.DisplayCoordsTool
5759
:members:
5860
.. autoclass:: plotpy.tools.ReverseYAxisTool

plotpy/builder/curvemarker.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ def __set_param(
121121
shade: bool | None = None,
122122
curvestyle: str | None = None,
123123
baseline: float | None = None,
124+
dsamp_factor: int | None = None,
125+
use_dsamp: bool | None = None,
124126
) -> None:
125127
"""Apply parameters to a :py:class:`.CurveParam` instance"""
126128
self.__set_baseparam(
@@ -141,6 +143,10 @@ def __set_param(
141143
param.curvestyle = curvestyle
142144
if baseline is not None:
143145
param.baseline = baseline
146+
if dsamp_factor is not None:
147+
param.dsamp_factor = dsamp_factor
148+
if use_dsamp is not None:
149+
param.use_dsamp = use_dsamp
144150

145151
def __get_arg_triple_plot(self, args):
146152
"""Convert MATLAB-like arguments into x, y, style"""
@@ -232,7 +238,7 @@ def mcurve(self, *args, **kwargs) -> CurveItem | list[CurveItem]:
232238
args: x, y, style
233239
kwargs: title, color, linestyle, linewidth, marker, markersize,
234240
markerfacecolor, markeredgecolor, shade, curvestyle, baseline,
235-
downsampling_factor, use_downsampling
241+
dsamp_factor, use_dsamp
236242
237243
Returns:
238244
:py:class:`.CurveItem` object
@@ -257,10 +263,10 @@ def mcurve(self, *args, **kwargs) -> CurveItem | list[CurveItem]:
257263
global CURVE_COUNT
258264
CURVE_COUNT += 1
259265
param.label = make_title(basename, CURVE_COUNT)
260-
if "downsampling_factor" in kwargs:
261-
param.downsampling_factor = kwargs.pop("downsampling_factor")
262-
if "use_downsampling" in kwargs:
263-
param.use_downsampling = kwargs.pop("use_downsampling")
266+
if "dsamp_factor" in kwargs:
267+
param.dsamp_factor = kwargs.pop("dsamp_factor")
268+
if "use_dsamp" in kwargs:
269+
param.use_dsamp = kwargs.pop("use_dsamp")
264270
update_style_attr(stylei, param)
265271
curves.append(self.pcurve(x, yi, param, **kwargs))
266272
if len(curves) == 1:
@@ -314,6 +320,8 @@ def curve(
314320
baseline: float | None = None,
315321
xaxis: str = "bottom",
316322
yaxis: str = "left",
323+
dsamp_factor: int | None = None,
324+
use_dsamp: bool | None = None,
317325
dx: numpy.ndarray | None = None,
318326
dy: numpy.ndarray | None = None,
319327
errorbarwidth: int | None = None,
@@ -345,6 +353,8 @@ def curve(
345353
baseline: baseline value. Default is None
346354
xaxis: x axis name. Default is 'bottom'
347355
yaxis: y axis name. Default is 'left'
356+
dsamp_factor: downsampling factor. Default is None
357+
use_dsamp: use downsampling. Default is None
348358
dx: x error data. Default is None
349359
dy: y error data. Default is None
350360
errorbarwidth: error bar width (pixels). Default is None
@@ -388,6 +398,8 @@ def curve(
388398
baseline=baseline,
389399
xaxis=xaxis,
390400
yaxis=yaxis,
401+
dsamp_factor=dsamp_factor,
402+
use_dsamp=use_dsamp,
391403
)
392404

393405
basename = _("Curve")
@@ -409,6 +421,8 @@ def curve(
409421
shade,
410422
curvestyle,
411423
baseline,
424+
dsamp_factor,
425+
use_dsamp,
412426
)
413427
return self.pcurve(x, y, param, xaxis, yaxis)
414428

@@ -501,6 +515,8 @@ def error(
501515
baseline: float | None = None,
502516
xaxis: str = "bottom",
503517
yaxis: str = "left",
518+
dsamp_factor: int | None = None,
519+
use_dsamp: bool | None = None,
504520
) -> ErrorBarCurveItem:
505521
"""Make an errorbar curve `plot item`
506522
@@ -538,6 +554,8 @@ def error(
538554
baseline: baseline value. Default is None
539555
xaxis: x axis name. Default is 'bottom'
540556
yaxis: y axis name. Default is 'left'
557+
dsamp_factor: downsampling factor. Default is None
558+
use_dsamp: use downsampling. Default is None
541559
542560
Returns:
543561
:py:class:`.ErrorBarCurveItem` object
@@ -567,6 +585,8 @@ def error(
567585
shade,
568586
curvestyle,
569587
baseline,
588+
dsamp_factor,
589+
use_dsamp,
570590
)
571591
errorbarparam.color = curveparam.line.color
572592
if errorbarwidth is not None:

plotpy/items/curve/base.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -365,14 +365,22 @@ def update_data(self):
365365
if isinstance(self._x, np.ndarray) and isinstance(self._y, np.ndarray):
366366
self._setData(self._x, self._y)
367367

368+
def dsamp(self, data: np.ndarray) -> np.ndarray:
369+
"""Downsample data
370+
371+
Args:
372+
data: Data to downsample
373+
374+
Returns:
375+
Downsampled data
376+
"""
377+
if self.param.use_dsamp and self.param.dsamp_factor > 1:
378+
return data[:: self.param.dsamp_factor]
379+
return data
380+
368381
def _setData(self, x: np.ndarray, y: np.ndarray):
369382
"""Wrapper around QwtPlotCurve.setData() to handle downsampling"""
370-
if not self.param.use_downsampling or self.param.downsampling_factor == 1:
371-
return super().setData(x, y)
372-
return super().setData(
373-
x[:: self.param.downsampling_factor],
374-
y[:: self.param.downsampling_factor],
375-
)
383+
return super().setData(self.dsamp(x), self.dsamp(y))
376384

377385
def set_data(self, x: np.ndarray, y: np.ndarray) -> None:
378386
"""Set curve data
@@ -449,9 +457,7 @@ def hit_test(self, pos: QC.QPointF) -> tuple[float, float, bool, None]:
449457
p1x = plot.transform(ax, self._x[i + 1])
450458
p1y = plot.transform(ay, self._y[i + 1])
451459
distance = seg_dist(QC.QPointF(pos), QC.QPointF(p0x, p0y), QC.QPointF(p1x, p1y))
452-
final_index = i // (
453-
int(not self.param.use_downsampling) or self.param.downsampling_factor
454-
)
460+
final_index = i // (int(not self.param.use_dsamp) or self.param.dsamp_factor)
455461
return distance, final_index, False, None
456462

457463
def get_closest_coordinates(self, x: float, y: float) -> tuple[float, float]:

plotpy/items/curve/errorbar.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def __init__(
7272
super().__init__(curveparam)
7373
self._dx = None
7474
self._dy = None
75-
self._minmaxarrays: dict[bool, tuple[float, float, float, float]] = {}
75+
self.__minmaxarrays: dict[bool, tuple[float, float, float, float]] = {}
7676
self.setIcon(get_icon("errorbar.png"))
7777

7878
def serialize(
@@ -157,7 +157,7 @@ def set_data(
157157
dy = None
158158
self._dx = dx
159159
self._dy = dy
160-
self._minmaxarrays = {}
160+
self.__minmaxarrays = {}
161161

162162
def get_minmax_arrays(self, all_values: bool = True) -> tuple[float, ...]:
163163
"""Get min/max arrays
@@ -168,11 +168,11 @@ def get_minmax_arrays(self, all_values: bool = True) -> tuple[float, ...]:
168168
Returns:
169169
tuple[float, ...]: Min/max arrays
170170
"""
171-
if self._minmaxarrays.get(all_values) is None:
172-
x = self._x
173-
y = self._y
174-
dx = self._dx
175-
dy = self._dy
171+
if self.__minmaxarrays.get(all_values) is None:
172+
x = self.dsamp(self._x)
173+
y = self.dsamp(self._y)
174+
dx = self.dsamp(self._dx)
175+
dy = self.dsamp(self._dy)
176176
if all_values:
177177
if dx is None:
178178
xmin = xmax = x
@@ -182,7 +182,7 @@ def get_minmax_arrays(self, all_values: bool = True) -> tuple[float, ...]:
182182
ymin = ymax = y
183183
else:
184184
ymin, ymax = y - dy, y + dy
185-
self._minmaxarrays.setdefault(all_values, (xmin, xmax, ymin, ymax))
185+
self.__minmaxarrays.setdefault(all_values, (xmin, xmax, ymin, ymax))
186186
else:
187187
isf = np.logical_and(np.isfinite(x), np.isfinite(y))
188188
if dx is not None:
@@ -197,10 +197,10 @@ def get_minmax_arrays(self, all_values: bool = True) -> tuple[float, ...]:
197197
ymin = ymax = y[isf]
198198
else:
199199
ymin, ymax = y[isf] - dy[isf], y[isf] + dy[isf]
200-
self._minmaxarrays.setdefault(
200+
self.__minmaxarrays.setdefault(
201201
all_values, (x[isf], y[isf], xmin, xmax, ymin, ymax)
202202
)
203-
return self._minmaxarrays[all_values]
203+
return self.__minmaxarrays[all_values]
204204

205205
def get_closest_coordinates(self, x: float, y: float) -> tuple[float, float]:
206206
"""
@@ -218,8 +218,8 @@ def get_closest_coordinates(self, x: float, y: float) -> tuple[float, float]:
218218
xc = plot.transform(self.xAxis(), x)
219219
yc = plot.transform(self.yAxis(), y)
220220
_distance, i, _inside, _other = self.hit_test(QC.QPointF(xc, yc))
221-
xi = self._x[i]
222-
yi = self._y[i]
221+
xi = self.dsamp(self._x)[i]
222+
yi = self.dsamp(self._y)[i]
223223
xmin, xmax, ymin, ymax = self.get_minmax_arrays()
224224
if abs(y - y) > abs(y - ymin[i]):
225225
yi = ymin[i]
@@ -343,6 +343,11 @@ def draw(
343343
def update_params(self):
344344
"""Update object properties from item parameters"""
345345
self.errorbarparam.update_item(self)
346+
347+
# In case the downsampling factor/state has changed,
348+
# we need to invalidate cached data
349+
self.__minmaxarrays = {}
350+
346351
CurveItem.update_params(self)
347352

348353
def update_item_parameters(self) -> None:

plotpy/locale/fr/LC_MESSAGES/plotpy.po

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,17 +1420,25 @@ msgstr "Marqueurs"
14201420
msgid "MAX resolution"
14211421
msgstr "Résolution MAX"
14221422

1423-
#: plotpy\tests\tools\test_downsample_curve.py:32
1424-
#: plotpy\tests\tools\test_edit_point.py:51
1425-
#: plotpy\tests\tools\test_get_point.py:31
1426-
#: plotpy\tests\tools\test_get_points.py:32
1427-
msgid "Select one point then press OK to accept"
1428-
msgstr "Sélectionner un point puis cliquer sur OK pour valider"
1423+
#: plotpy\tests\tools\test_downsample_curve.py:35
1424+
msgid "Right-click on the curve to enable/disable downsampling"
1425+
msgstr "Clic droit sur la courbe pour activer/désactiver le sous-échantillonnage"
14291426

1430-
#: plotpy\tests\tools\test_edit_point.py:45 plotpy\tools\curve.py:814
1427+
#: plotpy\tests\tools\test_edit_point.py:47 plotpy\tools\curve.py:818
14311428
msgid "Insert point"
14321429
msgstr "Insérer un point"
14331430

1431+
#: plotpy\tests\tools\test_edit_point.py:65
1432+
#: plotpy\tests\tools\test_get_point.py:36
1433+
msgid "Select one point then press OK to accept"
1434+
msgstr "Sélectionner un point puis cliquer sur OK pour valider"
1435+
1436+
#: plotpy\tests\tools\test_get_points.py:40
1437+
msgid ""
1438+
"Select up to %s points then press OK to accept (hold Ctrl to select multiple "
1439+
"points)"
1440+
msgstr "Sélectionner jusqu'à %s points puis cliquer sur OK pour valider (maintenir Ctrl pour sélectionner plusieurs points)"
1441+
14341442
#: plotpy\tests\widgets\test_simple_dialog.py:26
14351443
#: plotpy\tests\widgets\test_simple_window.py:47
14361444
msgid "Image width (pixels)"
@@ -1623,10 +1631,9 @@ msgstr "Avant d'insérer un nouveau point, sélectionner un point existant."
16231631
msgid "Insert new value"
16241632
msgstr "Insérer une nouvelle valeur"
16251633

1626-
#: plotpy\tools\curve.py:853
1627-
#, fuzzy
1628-
msgid "Downsample curves"
1629-
msgstr "Sous-échantillonnage (courbes)"
1634+
#: plotpy\tools\curve.py:1073
1635+
msgid "Downsample"
1636+
msgstr "Sous-échantillonner"
16301637

16311638
#: plotpy\tools\curve.py:893 plotpy\tools\curve.py:905
16321639
msgid "Export"

plotpy/plot/manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
DeleteItemTool,
3636
DisplayCoordsTool,
3737
DoAutoscaleTool,
38+
DownSamplingTool,
3839
DummySeparatorTool,
3940
EditItemDataTool,
4041
ExportItemDataTool,
@@ -575,6 +576,7 @@ def register_curve_tools(self) -> None:
575576
self.add_tool(CurveStatsTool)
576577
self.add_tool(AntiAliasingTool)
577578
self.add_tool(AxisScaleTool)
579+
self.add_tool(DownSamplingTool)
578580

579581
def register_image_tools(self) -> None:
580582
"""

plotpy/styles/curve.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ class CurveParam(DataSet):
4141
shade = FloatItem(_("Shadow"), default=0, min=0, max=1)
4242
curvestyle = ImageChoiceItem(_("Curve style"), CURVESTYLE_CHOICES, default="Lines")
4343
baseline = FloatItem(_("Baseline"), default=0.0)
44-
_downsampling_prop = GetAttrProp("use_downsampling")
45-
use_downsampling = BoolItem(_("Use downsampling"), default=False).set_prop(
46-
"display", store=_downsampling_prop
44+
_use_dsamp_prop = GetAttrProp("use_dsamp")
45+
use_dsamp = BoolItem(_("Use downsampling"), default=False).set_prop(
46+
"display", store=_use_dsamp_prop
4747
)
48-
downsampling_factor = IntItem(_("Downsampling factor"), default=10, min=1).set_prop(
49-
"display", active=_downsampling_prop
48+
dsamp_factor = IntItem(_("Downsampling factor"), default=10, min=1).set_prop(
49+
"display", active=_use_dsamp_prop
5050
)
5151

5252
def update_param(self, curve: CurveItem | PolygonMapItem):
@@ -88,6 +88,8 @@ def update_item(self, curve: CurveItem | PolygonMapItem):
8888
# Curve style, type and baseline
8989
curve.setStyle(getattr(QwtPlotCurve, self.curvestyle))
9090
curve.setBaseline(self.baseline)
91+
# Downsampling
92+
curve.update_data()
9193
if plot is not None:
9294
plot.blockSignals(False)
9395

0 commit comments

Comments
 (0)