Skip to content

Commit 52ef4fc

Browse files
committed
Add customizable result shape and marker styles
Add four new settings to configure visual appearance of result shapes and markers through the Settings dialog (Visualization tab): - Shape style for signals/images: line, fill, symbol properties - Marker style for signals/images: cursor appearance and labels
1 parent a9b540f commit 52ef4fc

14 files changed

Lines changed: 764 additions & 80 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,24 @@ See DataLab [roadmap page](https://datalab-platform.com/en/contributing/roadmap.
198198
* Ideal for analyzing sensor data, temperature logs, and time-series measurements
199199
* This closes [Issue #258](https://github.com/DataLab-Platform/DataLab/issues/258) - Add support for CSV files with date/time X-axis
200200

201+
* **Customizable result shape and marker styles**: New settings to configure visual appearance of result shapes and markers
202+
* Added four new button controls in the Settings dialog (Visualization tab) to customize styles:
203+
* **Shape style for signals**: Configure line style, color, width, fill pattern, symbol appearance, and selection styles for annotation shapes on signal plots
204+
* **Marker style for signals**: Configure cursor marker appearance including line style, symbol, text labels, and background transparency for signal analysis results
205+
* **Shape style for images**: Configure annotation shape appearance for image plots optimized for image visualization
206+
* **Marker style for images**: Configure cursor marker styles for image analysis results
207+
* **Editable through PlotPy's shape parameter dialog**: Clicking each button opens an interactive editor with comprehensive style options including:
208+
* Line properties: style (solid, dashed, dotted), color, width
209+
* Fill properties: pattern, color, transparency
210+
* Symbol properties: marker type, size, edge and face colors
211+
* Selection appearance: different styles when shapes are selected
212+
* Text formatting for marker labels
213+
* **Persistent configuration**: All style settings are saved in the user configuration file and preserved across sessions
214+
* **Applied to all result shapes and markers**: Styles are applied to all result shapes and markers from analysis operations (peak detection, FWHM, centroid, statistics, etc.)
215+
* **Shape refresh on settings change**: When shape/marker parameters are modified in Settings, users are prompted to refresh existing result shapes on visible plots to apply the new styles
216+
* Improves workflow flexibility by allowing users to establish consistent visual styling for their analysis results
217+
* Particularly useful for creating presentation-ready plots with custom color schemes or for adapting to different display environments
218+
201219
* **Enhance metadata handling by adding function name context to results**:
202220
* When computation functions (e.g., `full_width_at_y`, `x_at_y`) create results with parameters, the function name is now stored
203221
* Analysis parameters tab displays function name as a comment above parameter information for better context

datalab/adapters_metadata/common.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,33 @@ def append(self, adapter: BaseResultAdapter, obj: SignalObj | ImageObj) -> None:
9595
self.short_ids.append(sid)
9696

9797

98+
def have_results(objs: list[SignalObj | ImageObj]) -> bool:
99+
"""Return True if any object has results
100+
101+
Args:
102+
objs: List of objects
103+
104+
Returns:
105+
True if any object has results, False otherwise
106+
"""
107+
return bool(
108+
any(GeometryAdapter.iterate_from_obj(obj) for obj in objs)
109+
or any(TableAdapter.iterate_from_obj(obj) for obj in objs)
110+
)
111+
112+
113+
def have_geometry_results(objs: list[SignalObj | ImageObj]) -> bool:
114+
"""Return True if any object has geometry results
115+
116+
Args:
117+
objs: List of objects
118+
119+
Returns:
120+
True if any object has geometry results, False otherwise
121+
"""
122+
return bool(any(GeometryAdapter.iterate_from_obj(obj) for obj in objs))
123+
124+
98125
def create_resultdata_dict(
99126
objs: list[SignalObj | ImageObj],
100127
) -> dict[str, ResultData]:

datalab/adapters_plotpy/base.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,23 @@
1010
from __future__ import annotations
1111

1212
import json
13-
from typing import (
14-
TYPE_CHECKING,
15-
)
13+
from typing import TYPE_CHECKING
1614

15+
import guidata.dataset as gds
1716
from guidata.io import JSONReader, JSONWriter
1817
from plotpy.io import load_items, save_items
1918
from plotpy.items import (
2019
AbstractLabelItem,
20+
AnnotatedPoint,
2121
AnnotatedSegment,
2222
AnnotatedShape,
2323
)
2424

25+
from datalab.config import Conf
26+
2527
if TYPE_CHECKING:
26-
from plotpy.items import (
27-
AbstractShape,
28-
)
29-
from plotpy.styles import AnnotationParam
28+
from plotpy.items import AbstractShape
29+
from plotpy.styles import AnnotationParam, ShapeParam
3030

3131

3232
def config_annotated_shape(

datalab/adapters_plotpy/objects/scalar.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from collections.abc import Iterable
1616
from typing import TYPE_CHECKING, Literal
1717

18+
import guidata.dataset as gds
1819
import numpy as np
1920
from guidata.configtools import get_font
2021
from plotpy.builder import make
@@ -91,14 +92,14 @@ def __init__(self, result_adapter: GeometryAdapter) -> None:
9192
super().__init__(result_adapter)
9293

9394
def iterate_shape_items(
94-
self, fmt: str, lbl: bool, option: Literal["s", "i"]
95+
self, fmt: str, lbl: bool, prefix: Literal["s", "i"]
9596
) -> Iterable:
9697
"""Iterate over metadata shape plot items.
9798
9899
Args:
99100
fmt: numeric format (e.g. "%.3f")
100101
lbl: if True, show shape labels
101-
option: shape style option ("s" for signal, "i" for image)
102+
prefix: "s" for signal, "i" for image
102103
103104
Yields:
104105
Plot item
@@ -110,7 +111,7 @@ def iterate_shape_items(
110111
for idx, coords in enumerate(self.result_adapter.result.coords):
111112
if idx >= max_shapes:
112113
break
113-
yield self.create_shape_item(coords, fmt, lbl, option)
114+
yield self.create_shape_item(coords, fmt, lbl, prefix)
114115

115116
# If shapes were truncated, create a warning label
116117
if total_coords > max_shapes:
@@ -129,7 +130,7 @@ def iterate_shape_items(
129130
yield warning_label
130131

131132
def create_shape_item(
132-
self, coords: np.ndarray, fmt: str, lbl: bool, option: Literal["s", "i"]
133+
self, coords: np.ndarray, fmt: str, lbl: bool, prefix: Literal["s", "i"]
133134
) -> (
134135
AnnotatedPoint
135136
| Marker
@@ -146,7 +147,7 @@ def create_shape_item(
146147
coords: coordinate array
147148
fmt: numeric format (e.g. "%.3f")
148149
lbl: if True, show shape labels
149-
option: shape style option ("s" for signal, "i" for image)
150+
prefix: "s" for signal, "i" for image
150151
151152
Returns:
152153
Plot item
@@ -209,8 +210,30 @@ def create_shape_item(
209210
raise NotImplementedError(
210211
f"Unsupported shape kind: {self.result_adapter.result.kind}"
211212
)
213+
212214
if isinstance(item, AnnotatedShape):
213-
config_annotated_shape(item, fmt, lbl, "results", option)
215+
config_annotated_shape(item, fmt, lbl, "results", f"{prefix}/annotation")
216+
# Apply settings for annotated shapes (except AnnotatedPoint)
217+
if not isinstance(item, AnnotatedPoint):
218+
if prefix == "s":
219+
config_param = Conf.view.sig_shape_param.get()
220+
else:
221+
config_param = Conf.view.ima_shape_param.get()
222+
shape_param: ShapeParam = item.shape.shapeparam
223+
gds.update_dataset(shape_param, config_param)
224+
shape_param.update_item(item.shape)
225+
226+
if isinstance(item, Marker):
227+
item.set_style("results", f"{prefix}/marker")
228+
# Apply cursor/marker settings from config
229+
if prefix == "s":
230+
config_param = Conf.view.sig_marker_param.get()
231+
else:
232+
config_param = Conf.view.ima_marker_param.get()
233+
param = item.markerparam
234+
gds.update_dataset(param, config_param)
235+
param.update_item(item)
236+
214237
set_plot_item_editable(item, False)
215238
return item
216239

@@ -241,20 +264,7 @@ def label(x, y): # pylint: disable=unused-argument
241264
def label(x, y):
242265
return txt % (x, y)
243266

244-
marker = make.marker(
245-
position=(x0, y0),
246-
markerstyle=mstyle,
247-
label_cb=label,
248-
linestyle="DashLine",
249-
color="#ffff00",
250-
)
251-
param = marker.markerparam
252-
param.symbol.marker = "Diamond"
253-
param.symbol.size = 9
254-
param.symbol.edgecolor = "#ffff00"
255-
param.symbol.facecolor = "#ffff00"
256-
param.symbol.alpha = 0.7
257-
param.update_item(marker)
267+
marker = make.marker(position=(x0, y0), markerstyle=mstyle, label_cb=label)
258268
return marker
259269

260270

0 commit comments

Comments
 (0)