Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions datalab/adapters_plotpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

__all__ = [
"CURVESTYLES",
"LINE_STYLE_KEYS",
"CircularROIPlotPyAdapter",
"CurveStyles",
"GeometryPlotPyAdapter",
"ImageObjPlotPyAdapter",
"PolygonalROIPlotPyAdapter",
Expand Down Expand Up @@ -51,6 +53,8 @@
from .factories import DataLabPlotPyAdapterFactory
from .objects.adapters import (
CURVESTYLES,
LINE_STYLE_KEYS,
CurveStyles,
ImageObjPlotPyAdapter,
SignalObjPlotPyAdapter,
TypePlotItem,
Expand Down
2 changes: 2 additions & 0 deletions datalab/adapters_plotpy/objects/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from sigimax.adapters_plotpy.objects.image import get_obj_coords
from sigimax.adapters_plotpy.objects.signal import (
CURVESTYLES,
LINE_STYLE_KEYS,
CurveStyles,
apply_downsampling,
apply_line_width,
Expand All @@ -31,6 +32,7 @@

__all__ = [
"CURVESTYLES",
"LINE_STYLE_KEYS",
"BaseObjPlotPyAdapter",
"CurveStyles",
"ImageObjPlotPyAdapter",
Expand Down
8 changes: 5 additions & 3 deletions datalab/gui/actionhandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1510,12 +1510,14 @@ def create_last_actions(self):
antialiasing_action.setChecked(Conf.sig_antialiasing.get(True))
self.new_action(
_("Reset curve styles"),
select_condition=SelectCond.always,
select_condition=SelectCond.at_least_one_group_or_one_object,
icon_name="reset_curve_styles.svg",
triggered=self.panel.reset_curve_styles,
tip=_(
"Curve styles are looped over a list of predefined styles.\n"
"This action resets the list to its initial state."
"Curves are given automatic styles according to their "
"position on the plot.\n"
"This action removes the colors and line styles chosen for "
"the selected signals, which are given automatic styles again."
),
toolbar_pos=-1,
)
Expand Down
21 changes: 19 additions & 2 deletions datalab/gui/panel/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from sigima.io.signal import SignalIORegistry
from sigima.objects import SignalObj, SignalROI

from datalab.adapters_plotpy import CURVESTYLES
from datalab.adapters_plotpy import CURVESTYLES, LINE_STYLE_KEYS
from datalab.config import Conf, _
from datalab.gui import roieditor
from datalab.gui.actionhandler import SignalActionHandler
Expand Down Expand Up @@ -173,5 +173,22 @@ def toggle_anti_aliasing(self, state: bool) -> None:
self.plothandler.toggle_anti_aliasing(state)

def reset_curve_styles(self) -> None:
"""Reset curve styles"""
"""Reset the curve styles of the selected signals

Colors and line styles chosen by the user are removed from the metadata of
the selected signals, whose curves are given automatic styles again.
"""
CURVESTYLES.reset_styles()
modified = False
for obj in self.objview.get_sel_objects(include_groups=True):
for key in LINE_STYLE_KEYS:
if obj.metadata.pop(key, None) is not None:
modified = True
item = self.plothandler.get(get_uuid(obj))
if item is not None and not CURVESTYLES.has_automatic_style(item):
CURVESTYLES.restore_automatic_style(item)
item.param.update_item(item)
modified = True
self.refresh_plot("selected", update_items=False)
if modified:
self.SIG_OBJECT_MODIFIED.emit()
43 changes: 41 additions & 2 deletions datalab/gui/plothandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@
from sigima.objects import ImageObj, SignalObj, TypeObj

from datalab.adapters_metadata import GeometryAdapter, TableAdapter
from datalab.adapters_plotpy import TypePlotItem, create_adapter_from_object
from datalab.adapters_plotpy import (
CURVESTYLES,
CurveStyles,
TypePlotItem,
create_adapter_from_object,
)
from datalab.adapters_plotpy.objects.scalar import MergedResultPlotPyAdapter
from datalab.config import Conf, _
from datalab.utils.qthelpers import block_signals, create_progress_bar
Expand Down Expand Up @@ -297,7 +302,10 @@ def __add_item_to_plot(self, oid: str) -> TypePlotItem:
"""
obj = self.panel.objmodel[oid]
self.__cached_hashes[obj] = calc_data_hash(obj)
item: TypePlotItem = create_adapter_from_object(obj).make_item()
# Automatic curve styles are given by `update_automatic_styles`, according
# to the position of the curves on the plot: the style cycle is not used
with CURVESTYLES.suspend():
item: TypePlotItem = create_adapter_from_object(obj).make_item()
item.param.label = self.panel.mainwindow.render_object_title(obj.title)
item.param.update_item(item)
item.set_readonly(True)
Expand Down Expand Up @@ -412,10 +420,17 @@ def refresh_plot(
only_visible=only_visible,
only_existing=only_existing,
)
self.update_automatic_styles()
finally:
self.plot.setAutoReplot(auto_replot)
self.plot.replot()

def update_automatic_styles(self) -> None:
"""Update the automatic styles of the visible plot items

This base implementation does nothing.
"""

def __refresh_plot(
self,
what: str,
Expand Down Expand Up @@ -620,6 +635,30 @@ def get_plot_options(self) -> PlotOptions:
options.curve_antialiasing = self.plot.antialiased
return options

def update_automatic_styles(self) -> None:
"""Give the visible curves the automatic style of their position

Visible curves are numbered in the order of their signals in the panel.
Each curve whose color and line style were not chosen by the user is given
the automatic style of its position (see :py:meth:`CurveStyles.style_at`),
so that the first curves shown are always given the first styles.
"""
index = 0
for oid in self.panel.objmodel.get_object_ids():
item = self.get(oid)
if item is None or not item.isVisible():
continue
style = CurveStyles.style_at(index)
index += 1
if (
CURVESTYLES.has_automatic_style(item)
and CURVESTYLES.get_automatic_style(item) != style
):
CURVESTYLES.set_automatic_style(item, *style)
item.param.update_item(item)
if item.selected:
item.select()

def refresh_plot(
self,
what: str,
Expand Down
8 changes: 4 additions & 4 deletions datalab/locale/fr/LC_MESSAGES/datalab.po
Original file line number Diff line number Diff line change
Expand Up @@ -874,11 +874,11 @@ msgid "Reset curve styles"
msgstr "Réinitialiser les styles de courbes"

msgid ""
"Curve styles are looped over a list of predefined styles.\n"
"This action resets the list to its initial state."
"Curves are given automatic styles according to their position on the plot.\n"
"This action removes the colors and line styles chosen for the selected signals, which are given automatic styles again."
msgstr ""
"Les styles de courbes sont parcourus dans une liste de styles prédéfinis.\n"
"Cette action réinitialise la liste à son état initial."
"Les courbes reçoivent des styles automatiques selon leur position dans le graphique.\n"
"Cette action supprime les couleurs et les styles de trait choisis pour les signaux sélectionnés, qui reçoivent à nouveau des styles automatiques."

msgid "image"
msgstr "image"
Expand Down
172 changes: 172 additions & 0 deletions datalab/tests/features/signal/curve_styles_unit_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
Curve style unit tests
----------------------

Automatic curve styles depend on the position of the curves on the plot, are not
stored in signal metadata, and are not shifted by dialogs showing signals. Curve
styles chosen by the user are kept.
"""

# guitest: show

from __future__ import annotations

import os.path as osp

import numpy as np
from plotpy.items import CurveItem
from qtpy import QtGui as QG
from sigima.objects import SignalObj, create_signal

from datalab.adapters_plotpy import (
CURVESTYLES,
LINE_STYLE_KEYS,
CurveStyles,
create_adapter_from_object,
)
from datalab.gui.panel.signal import SignalPanel
from datalab.objectmodel import get_uuid
from datalab.tests import datalab_test_app_context, helpers


def _create_signals(count: int) -> list[SignalObj]:
"""Create test signals"""
x = np.linspace(0.0, 1.0, 50)
return [create_signal(f"Signal {i}", x, x + i) for i in range(count)]


def _item(panel: SignalPanel, obj: SignalObj) -> CurveItem:
"""Return the curve item of a signal"""
return panel.plothandler[get_uuid(obj)]


def _style(item: CurveItem) -> tuple[str, str]:
"""Return the color and the line style of a curve item"""
return QG.QColor(item.param.line.color).name(), item.param.line.style


def _set_user_style(panel: SignalPanel, item: CurveItem, color: str) -> None:
"""Set a curve color as the user does with the curve parameters dialog"""
item.param.line.color = color
item.param.update_item(item)
panel.plot_item_parameters_changed(item)


def test_styles_follow_curve_positions() -> None:
"""Visible curves are given the automatic styles of their positions"""
with datalab_test_app_context(console=False, exec_loop=False) as win:
panel = win.signalpanel
signals = _create_signals(12)
panel.add_objects(signals, set_current=False)
panel.objview.select_objects(signals)
for index, obj in enumerate(signals):
assert _style(_item(panel, obj)) == CurveStyles.style_at(index)
assert _style(_item(panel, signals[11]))[1] == "DashLine"

# Whatever the signals shown, the first curves get the first styles
panel.objview.select_objects([signals[7], signals[4]])
assert _style(_item(panel, signals[4])) == CurveStyles.style_at(0)
assert _style(_item(panel, signals[7])) == CurveStyles.style_at(1)


def test_style_cycle_not_used_by_panel_and_dialogs() -> None:
"""Showing signals, on the panel or in dialogs, does not shift the style cycle"""
with datalab_test_app_context(console=False, exec_loop=False) as win:
panel = win.signalpanel
signals = _create_signals(3)
panel.add_objects(signals, set_current=False)
CURVESTYLES.reset_styles()
panel.objview.select_objects(signals)
# As done by the ROI editor or "View in a new window":
for obj in signals:
item = _item(panel, obj)
copy = create_adapter_from_object(obj).make_item(update_from=item)
assert _style(copy) == _style(item)
new_item = create_adapter_from_object(signals[0]).make_item()
assert _style(new_item) == CurveStyles.style_at(0)


def test_user_styles_are_kept() -> None:
"""Curve styles chosen by the user are kept and stored in metadata"""
with helpers.WorkdirRestoringTempDir() as tmpdir:
with datalab_test_app_context(console=False, exec_loop=False) as win:
panel = win.signalpanel
signals = _create_signals(3)
panel.add_objects(signals, set_current=False)
panel.objview.select_objects(signals)
_set_user_style(panel, _item(panel, signals[1]), "#000000")
assert signals[1].metadata["color"] == "#000000"
for obj in (signals[0], signals[2]):
for key in LINE_STYLE_KEYS:
assert key not in obj.metadata

# The curve keeps its color and its position in the style cycle
panel.objview.select_objects([signals[2], signals[1]])
assert _style(_item(panel, signals[1]))[0] == "#000000"
assert _style(_item(panel, signals[2])) == CurveStyles.style_at(1)

# Workspace round trip: only the user-defined style is stored
fname = osp.join(tmpdir, "curve_styles.h5")
win.save_h5_workspace(fname)
panel.remove_all_objects()
win.load_h5_workspace([fname], reset_all=True)
loaded = panel.objmodel.get_all_objects()
assert loaded[1].metadata["color"] == "#000000"
for obj in (loaded[0], loaded[2]):
for key in LINE_STYLE_KEYS:
assert key not in obj.metadata
panel.objview.select_objects(loaded)
assert _style(_item(panel, loaded[0])) == CurveStyles.style_at(0)
assert _style(_item(panel, loaded[1]))[0] == "#000000"
assert _style(_item(panel, loaded[2])) == CurveStyles.style_at(2)


def test_legacy_styles_are_automatic() -> None:
"""Styles stored by earlier versions from their style cycle are automatic"""
with helpers.WorkdirRestoringTempDir() as tmpdir:
with datalab_test_app_context(console=False, exec_loop=False) as win:
panel = win.signalpanel
signals = _create_signals(2)
for obj in signals:
obj.metadata["color"] = CurveStyles.LEGACY_COLORS[2]
obj.metadata["style"] = "DashDotDotLine"
panel.add_objects(signals, set_current=False)
panel.objview.select_objects(signals)
for index, obj in enumerate(signals):
assert _style(_item(panel, obj)) == CurveStyles.style_at(index)
win.save_h5_workspace(osp.join(tmpdir, "legacy_styles.h5"))
for obj in signals:
for key in LINE_STYLE_KEYS:
assert key not in obj.metadata


def test_reset_curve_styles() -> None:
"""Resetting curve styles gives the selected curves automatic styles again"""
with datalab_test_app_context(console=False, exec_loop=False) as win:
panel = win.signalpanel
signals = _create_signals(3)
panel.add_objects(signals, set_current=False)
panel.objview.select_objects(signals)
for obj in signals:
_set_user_style(panel, _item(panel, obj), "#000000")
signals[0].metadata["style"] = "DotLine"

panel.objview.select_objects(signals[:2])
panel.reset_curve_styles()
for index, obj in enumerate(signals[:2]):
for key in LINE_STYLE_KEYS:
assert key not in obj.metadata
assert _style(_item(panel, obj)) == CurveStyles.style_at(index)
# Signals which are not selected keep their style
assert signals[2].metadata["color"] == "#000000"
assert _style(_item(panel, signals[2]))[0] == "#000000"


if __name__ == "__main__":
test_styles_follow_curve_positions()
test_style_cycle_not_used_by_panel_and_dialogs()
test_user_styles_are_kept()
test_legacy_styles_are_automatic()
test_reset_curve_styles()
4 changes: 3 additions & 1 deletion doc/features/common/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,9 @@ The Signals sub-tab contains settings specific to signal visualizations:
Default line width for curves representing signals. This setting affects all signal
visualizations unless overridden individually. Note: for signals exceeding the line
width performance threshold (see below), the line width is automatically clamped to
1.0 for optimal rendering performance.
1.0 for optimal rendering performance. Curves of width 1.0 or less are drawn one
screen pixel wide, also on high-DPI screens (e.g. with a 150% display scaling),
which keeps their rendering fast.

**Line width performance threshold**
For signals with more than this number of points (default: 1,000), line width is
Expand Down
23 changes: 17 additions & 6 deletions doc/features/signal/menu_view.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,23 @@ Anti-aliasing makes the curves look smoother, but it may also make them look les
Reset curve styles
^^^^^^^^^^^^^^^^^^

Reset the curve style cycle to the beginning.
Give the selected signals automatic curve styles again.

When plotting curves, DataLab automatically assigns a color and a line style to
each curve. Both parameters are chosen from a predefined list of colors and
line styles, and are assigned in a round-robin fashion.
each curve, according to its position among the curves shown on the plot (in the
order of the signals in the panel): the first curves are drawn with solid lines
in distinct colors, readable on both light and dark backgrounds, and the next
ones with dashed lines in the same colors. The first curve shown is thus always
assigned the first color, whatever the signals shown.

This menu entry allows you to reset the curve styles, so that the next time
you plot curves, the first curve will be assigned the first color and the first
line style of the predefined lists, and the loop will start again from there.
Automatic styles are not saved with the signals. If you choose another color or
line style for a curve (with the "Parameters" option of its context menu), this
style is saved in the metadata of the signal and is kept afterwards.

This menu entry removes the colors and line styles chosen for the selected signals,
so that their curves are assigned automatic styles again.

.. note::
Workspaces saved with DataLab 1.3 or earlier store the automatic style of
each curve: these styles are recognized as automatic styles, and are replaced
by the current ones.
Loading
Loading