Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
tests
  • Loading branch information
kushalkolar committed Jul 23, 2026
commit feb0a445644556603a7ad138c6debd3318fa5092
40 changes: 32 additions & 8 deletions fastplotlib/graphics/features/_positions.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,12 @@ def __init__(
def buffer(self) -> pygfx.Buffer:
return self._vertex_colors.buffer

@property
def value(self) -> np.ndarray:
# mirror the managed colors feature, whose length is the number of color entries
# (this is per-line, not per-vertex, for an InfLineColors)
return self._vertex_colors.value

@block_reentrance
def __setitem__(self, key: slice, cmap_name):
if not isinstance(key, slice):
Expand Down Expand Up @@ -552,7 +558,10 @@ def set_value(self, graphic, value):
# number of lines changed, allocate a new buffer
self._fpl_buffer = pygfx.Buffer(fixed)
graphic.world_object.geometry.positions = self._fpl_buffer
self._emit_event(self._property_name, slice(None), value)
# emit the [n_lines, 2, 3] form to match `value` and the in-place path
self._emit_event(
self._property_name, slice(None), fixed.reshape(-1, 2, 3)
)
return
self[:] = fixed.reshape(len(self), 2, 3)
return
Expand All @@ -572,8 +581,12 @@ def set_value(self, graphic, value):

@block_reentrance
def __setitem__(self, key, value):
# key indexes lines
line_indices = np.atleast_1d(np.arange(len(self))[key])
# for axis=None, `value` is [n_lines, 2, 3] so the line index is the first
# element of a multi-dimensional endpoint/coordinate key
line_key = key[0] if (self._axis is None and isinstance(key, tuple)) else key
line_indices = np.atleast_1d(np.arange(len(self))[line_key])
if line_indices.size == 0:
return

if self._axis is None:
self.buffer.data.reshape(len(self), 2, 3)[key] = value
Expand Down Expand Up @@ -636,12 +649,23 @@ def set_value(self, graphic, value):

@block_reentrance
def __setitem__(self, key, value):
# key indexes lines; write each line's color to both of its vertices
line_indices = np.atleast_1d(np.arange(len(self))[key])
colors = parse_colors(value, line_indices.size)
# the line index is the first element of a multi-dimensional (per-channel) key
line_key = key[0] if isinstance(key, tuple) else key
line_indices = np.atleast_1d(np.arange(len(self))[line_key])
if line_indices.size == 0:
return

self.buffer.data[2 * line_indices] = colors
self.buffer.data[2 * line_indices + 1] = colors
if isinstance(key, tuple):
# channel-level write, e.g. colors[i, :3]; set the value directly, no color parsing
colors = value
rest = key[1:]
self.buffer.data[(2 * line_indices, *rest)] = value
self.buffer.data[(2 * line_indices + 1, *rest)] = value
else:
# one color per selected line, written to both of the line's vertices
colors = parse_colors(value, line_indices.size)
self.buffer.data[2 * line_indices] = colors
self.buffer.data[2 * line_indices + 1] = colors

offset = 2 * int(line_indices.min())
size = 2 * (int(line_indices.max()) - int(line_indices.min()) + 1)
Expand Down
5 changes: 5 additions & 0 deletions fastplotlib/graphics/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ def dash_pattern(self) -> str | tuple | list:

@dash_pattern.setter
def dash_pattern(self, value: str | tuple | list):
if self._thin and parse_dash_pattern(value):
warn(
"`dash_pattern` is ignored when `thin=True`; the thin line material does not "
"support dashing"
)
self._dash_pattern.set_value(self, value)

@property
Expand Down
3 changes: 3 additions & 0 deletions tests/test_common_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ def make_graphic(kind: str, **kwargs):
return fpl.ImageGraphic(np.random.rand(10, 10), **kwargs)
case "line":
return fpl.LineGraphic(np.random.rand(10), **kwargs)
case "inf_line":
return fpl.InfLineGraphic(np.random.rand(10), axis="x", **kwargs)
case "scatter":
return fpl.ScatterGraphic(
np.column_stack([np.random.rand(10), np.random.rand(10)]), **kwargs
Expand All @@ -30,6 +32,7 @@ def make_graphic(kind: str, **kwargs):
graphic_kinds = [
"image",
"line",
"inf_line",
"scatter",
"text",
]
Expand Down
71 changes: 67 additions & 4 deletions tests/test_positions_graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,11 +389,74 @@ def test_thickness(thickness):
assert graphic.thickness == thickness
assert graphic.world_object.material.thickness == thickness

if thickness == 0.5:
assert isinstance(graphic.world_object.material, pygfx.LineThinMaterial)
# the thin line material is selected via the `thin` flag, not the thickness value
assert not graphic.thin
assert isinstance(graphic.world_object.material, pygfx.LineMaterial)
assert not isinstance(graphic.world_object.material, pygfx.LineThinMaterial)

else:
assert isinstance(graphic.world_object.material, pygfx.LineMaterial)

@pytest.mark.parametrize(
"pattern,expected",
[
("--", (5, 5)),
("dashed", (5, 5)),
(":", (0, 2)),
("-.", (5, 2, 1, 2)),
((2, 3), (2, 3)),
],
)
def test_dash_pattern(pattern, expected):
fig = fpl.Figure()
data = generate_positions_spiral_data("xy")

graphic = fig[0, 0].add_line(data=data, dash_pattern=pattern)

# value returns the user input verbatim, the material receives the parsed tuple
assert graphic.dash_pattern == pattern
assert tuple(graphic.world_object.material.dash_pattern) == expected

# can be changed after creation
graphic.dash_pattern = "solid"
assert tuple(graphic.world_object.material.dash_pattern) == ()


def test_thin():
fig = fpl.Figure()
data = generate_positions_spiral_data("xy")

# non-thin by default
graphic = fig[0, 0].add_line(data=data, thickness=5.0)
assert graphic.thin is False
assert not isinstance(graphic.world_object.material, pygfx.LineThinMaterial)

# the material is swapped when toggling `thin` after creation, keeping the geometry
geometry = graphic.world_object.geometry
graphic.thin = True
assert graphic.thin is True
assert isinstance(graphic.world_object.material, pygfx.LineThinMaterial)
assert graphic.world_object.geometry is geometry

graphic.thin = False
assert not isinstance(graphic.world_object.material, pygfx.LineThinMaterial)
assert isinstance(graphic.world_object.material, pygfx.LineMaterial)

# can also be set at construction
thin_graphic = fig[0, 0].add_line(data=data, thin=True)
assert isinstance(thin_graphic.world_object.material, pygfx.LineThinMaterial)


def test_thin_ignores_dash_pattern_warns():
fig = fpl.Figure()
data = generate_positions_spiral_data("xy")

# constructing a thin line with a dash pattern warns that dashing is ignored
with pytest.warns(UserWarning, match="dash_pattern.*ignored"):
fig[0, 0].add_line(data=data, thin=True, dash_pattern="--")

# setting the dash pattern on a thin line also warns
graphic = fig[0, 0].add_line(data=data, thin=True)
with pytest.warns(UserWarning, match="dash_pattern.*ignored"):
graphic.dash_pattern = "--"


@pytest.mark.parametrize("graphic_type", ["line", "scatter"])
Expand Down