Skip to content

Commit

Permalink
Misc improvements
Browse files Browse the repository at this point in the history
Also includes an interactive example in x.py
  • Loading branch information
gselzer committed Oct 31, 2024
1 parent d89fca2 commit 68e3f78
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 54 deletions.
2 changes: 2 additions & 0 deletions src/ndv/histogram/_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def __init__(self, histogram: VispyHistogramView) -> None:
self._layout.addWidget(self._vert)
self._layout.addWidget(self._log)

Check warning on line 39 in src/ndv/histogram/_qt.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_qt.py#L36-L39

Added lines #L36 - L39 were not covered by tests

# Viewbox controls

# -- Protocol methods -- #

def view(self) -> Any:
Expand Down
25 changes: 17 additions & 8 deletions src/ndv/histogram/_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ def view(self) -> Any:
class LutView(Protocol):

Check warning on line 21 in src/ndv/histogram/_view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_view.py#L21

Added line #L21 was not covered by tests
"""A view for LUT parameters."""

cmapChanged = Signal(cmap.Colormap)
gammaChanged = Signal(float)
climsChanged = Signal(tuple[float, float])
autoscaleChanged = Signal(object)
cmapChanged: Signal = Signal(cmap.Colormap)
gammaChanged: Signal = Signal(float)
climsChanged: Signal = Signal(tuple[float, float])
autoscaleChanged: Signal = Signal(object)

Check warning on line 27 in src/ndv/histogram/_view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_view.py#L24-L27

Added lines #L24 - L27 were not covered by tests

def set_visibility(self, visible: bool) -> None: ...
def set_cmap(self, lut: cmap.Colormap) -> None: ...
Expand All @@ -42,24 +42,33 @@ class HistogramView(StatsView, LutView):
"""A histogram-based view ."""

def set_domain(self, domain: tuple[float, float] | None) -> None:

Check warning on line 44 in src/ndv/histogram/_view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_view.py#L44

Added line #L44 was not covered by tests
"""
"""Sets the domain of the view.
If a tuple, sets the displayed extremes of the x axis to the passed values.
If None, sets them to the extent of the data instead.
"""
...

def set_range(self, range: tuple[float, float] | None) -> None:

Check warning on line 52 in src/ndv/histogram/_view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_view.py#L52

Added line #L52 was not covered by tests
"""
"""Sets the range of the view.
If a tuple, sets the displayed extremes of the y axis to the passed values.
If None, sets them to the extent of the data instead.
"""
...

def set_vertical(self, vertical: bool) -> None:

Check warning on line 60 in src/ndv/histogram/_view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_view.py#L60

Added line #L60 was not covered by tests
"""
"""Sets the axis of the domain.
If true, views the domain along the y axis and the range along the x axis.
Otherwise views the domain along the x axis and the range along the y axis.
"""
...

def enable_range_log(self, enabled: bool) -> None: ...
def enable_range_log(self, enabled: bool) -> None:

Check warning on line 68 in src/ndv/histogram/_view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_view.py#L68

Added line #L68 was not covered by tests
"""Sets the axis scale of the range.
If true, the range will be displayed with a logarithmic (base 10) scale.
If false, the range will be displayed with a linear scale.
"""
...
90 changes: 55 additions & 35 deletions src/ndv/histogram/_vispy.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,20 +271,22 @@ def set_range(
super().set_range(x, y, z, margin)

Check warning on line 271 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L271

Added line #L271 was not covered by tests


class VispyHistogramView(scene.SceneCanvas, HistogramView):
def __init__(self, parent: Any = None) -> None:
super().__init__(parent=parent)
self.unfreeze()
# FIXME? Initialize signals so VisPy is happy
self.gammaChanged
self.climsChanged
self.autoscaleChanged
self.cmapChanged
class VispyHistogramView(HistogramView):

Check warning on line 274 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L274

Added line #L274 was not covered by tests
"""A HistogramView on a VisPy SceneCanvas."""

def __init__(self) -> None:

Check warning on line 277 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L277

Added line #L277 was not covered by tests
# Canvas
self._canvas = scene.SceneCanvas()
self._canvas.unfreeze()
self._canvas.on_mouse_press = self.on_mouse_press
self._canvas.on_mouse_move = self.on_mouse_move
self._canvas.on_mouse_release = self.on_mouse_release
self._canvas.freeze()

Check warning on line 284 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L279-L284

Added lines #L279 - L284 were not covered by tests

# Plot
self.plot = PlotWidget()
self.plot.lock_axis("y")
self.central_widget.add_widget(self.plot)
self._canvas.central_widget.add_widget(self.plot)
self.node_tform = self.plot.node_transform(self.plot._view.scene)
self._vertical: bool = False

Check warning on line 291 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L287-L291

Added lines #L287 - L291 were not covered by tests
# The values of the left and right edges on the canvas (respectively)
Expand Down Expand Up @@ -316,6 +318,7 @@ def __init__(self, parent: Any = None) -> None:
face_color="b",
edge_width=1.0,
)
self.lut_line.visible = False
self.lut_line.order = -1

Check warning on line 322 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L321-L322

Added lines #L321 - L322 were not covered by tests

self._gamma: float = 1
Expand All @@ -324,8 +327,12 @@ def __init__(self, parent: Any = None) -> None:
size=6,
edge_width=0,
)
self._gamma_handle.visible = False
self._gamma_handle.order = -2
self._gamma_handle_position: float = 0.5
self._gamma_handle.transform = self.lut_line.transform = (

Check warning on line 332 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L330-L332

Added lines #L330 - L332 were not covered by tests
scene.transforms.STTransform()
)
self._gamma_handle_position: list[float] = [0.5, 0.5]
self._gamma_handle_grabbed: bool = False

Check warning on line 336 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L335-L336

Added lines #L335 - L336 were not covered by tests

self._clims: tuple[float, float] | None = None
Expand All @@ -335,7 +342,6 @@ def __init__(self, parent: Any = None) -> None:
self.plot._view.add(self._hist)
self.plot._view.add(self.lut_line)
self.plot._view.add(self._gamma_handle)

Check warning on line 344 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L342-L344

Added lines #L342 - L344 were not covered by tests
self.freeze()

# -- Protocol methods -- #

Expand All @@ -344,6 +350,8 @@ def set_histogram(self, values: np.ndarray, bin_edges: np.ndarray) -> None:
self._values = values
self._bin_edges = bin_edges
self._update_histogram()
if self._clims is None:
self.set_clims((self._bin_edges[0], self._bin_edges[-1]))
self._resize()

Check warning on line 355 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L350-L355

Added lines #L350 - L355 were not covered by tests

def set_std_dev(self, std_dev: float) -> None:

Check warning on line 357 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L357

Added line #L357 was not covered by tests
Expand All @@ -355,7 +363,7 @@ def set_average(self, average: float) -> None:
pass

def view(self) -> Any:
return self.native
return self._canvas.native

Check warning on line 366 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L365-L366

Added lines #L365 - L366 were not covered by tests

def set_visibility(self, visible: bool) -> None:
if self._hist is None:
Expand Down Expand Up @@ -391,15 +399,15 @@ def set_vertical(self, vertical: bool) -> None:
self._vertical = vertical
self._update_histogram()
self.plot.lock_axis("x" if vertical else "y")
self._resize()
self._update_lut_lines()
self._resize()

Check warning on line 403 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L398-L403

Added lines #L398 - L403 were not covered by tests

def enable_range_log(self, enabled: bool) -> None:
if enabled != self._log_y:
self._log_y = enabled
self._update_histogram()
self._resize()
self._update_lut_lines()
self._resize()

Check warning on line 410 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L405-L410

Added lines #L405 - L410 were not covered by tests

# -- Helper Methods -- #

Expand Down Expand Up @@ -447,23 +455,21 @@ def _update_lut_lines(self, npoints: int = 256) -> None:
X = np.empty(npoints + 4)
Y = np.empty(npoints + 4)
if self._vertical:

Check warning on line 457 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L455-L457

Added lines #L455 - L457 were not covered by tests
y1 = self.plot.xaxis.axis.domain[1] * 0.98
# clims lines
X[0:2], Y[0:2] = (y1, y1 / 2), self._clims[0]
X[-2:], Y[-2:] = (y1 / 2, 0), self._clims[1]
X[0:2], Y[0:2] = (1, 1 / 2), self._clims[0]
X[-2:], Y[-2:] = (1 / 2, 0), self._clims[1]

Check warning on line 460 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L459-L460

Added lines #L459 - L460 were not covered by tests
# gamma line
X[2:-2] = np.linspace(0, 1, npoints) ** self._gamma * y1
X[2:-2] = np.linspace(0, 1, npoints) ** self._gamma
Y[2:-2] = np.linspace(self._clims[0], self._clims[1], npoints)
midpoint = np.array([(y1 * 2**-self._gamma, np.mean(self._clims))])
midpoint = np.array([(2**-self._gamma, np.mean(self._clims))])

Check warning on line 464 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L462-L464

Added lines #L462 - L464 were not covered by tests
else:
y1 = self.plot.yaxis.axis.domain[1] * 0.98
# clims lines
X[0:2], Y[0:2] = self._clims[0], (y1, y1 / 2)
X[-2:], Y[-2:] = self._clims[1], (y1 / 2, 0)
X[0:2], Y[0:2] = self._clims[0], (1, 1 / 2)
X[-2:], Y[-2:] = self._clims[1], (1 / 2, 0)

Check warning on line 468 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L467-L468

Added lines #L467 - L468 were not covered by tests
# gamma line
X[2:-2] = np.linspace(self._clims[0], self._clims[1], npoints)
Y[2:-2] = np.linspace(0, 1, npoints) ** self._gamma * y1
midpoint = np.array([(np.mean(self._clims), y1 * 2**-self._gamma)])
Y[2:-2] = np.linspace(0, 1, npoints) ** self._gamma
midpoint = np.array([(np.mean(self._clims), 2**-self._gamma)])

Check warning on line 472 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L470-L472

Added lines #L470 - L472 were not covered by tests

# TODO: Move to self.edit_cmap
color = np.linspace(0.2, 0.8, npoints + 4).repeat(4).reshape(-1, 4)
Expand All @@ -472,9 +478,11 @@ def _update_lut_lines(self, npoints: int = 256) -> None:
color[-3:] = [c1, c2, c1]

Check warning on line 478 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L475-L478

Added lines #L475 - L478 were not covered by tests

self.lut_line.set_data((X, Y), marker_size=0, color=color)
self.lut_line.visible = True

Check warning on line 481 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L480-L481

Added lines #L480 - L481 were not covered by tests

self._gamma_handle_position = midpoint[0]
self._gamma_handle_position[:] = midpoint[0]
self._gamma_handle.set_data(pos=midpoint)
self._gamma_handle.visible = True

Check warning on line 485 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L483-L485

Added lines #L483 - L485 were not covered by tests

def on_mouse_press(self, event: SceneMouseEvent) -> None:
if event.pos is None:
Expand Down Expand Up @@ -507,8 +515,8 @@ def _pos_is_clim(self, event: SceneMouseEvent, tolerance: int = 3) -> int:
_, clim0 = self._to_window_coords((0, self._clims[0]))

Check warning on line 515 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L512-L515

Added lines #L512 - L515 were not covered by tests
else:
x = event.pos[0]
clim1, _ = self._to_window_coords((self._clims[1],))
clim0, _ = self._to_window_coords((self._clims[0],))
clim1, _ = self._to_window_coords((self._clims[1], 0))
clim0, _ = self._to_window_coords((self._clims[0], 0))
if abs(clim1 - x) < tolerance:
return 1
if abs(clim0 - x) < tolerance:
Expand All @@ -524,7 +532,9 @@ def _pos_is_gamma(self, event: SceneMouseEvent, tolerance: int = 4) -> bool:
"""
if self._gamma_handle_position is None:
return False
gx, gy = self._to_window_coords(self._gamma_handle_position)
gx, gy = self._to_window_coords(

Check warning on line 535 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L533-L535

Added lines #L533 - L535 were not covered by tests
self._gamma_handle.transform.map(self._gamma_handle_position)
)
x, y = event.pos
if abs(gx - x) < tolerance and abs(gy - y) < tolerance:
return True
Expand Down Expand Up @@ -566,32 +576,36 @@ def on_mouse_move(self, event: SceneMouseEvent) -> None:
self.gammaChanged.emit(-np.log2(y / y1))
return

Check warning on line 577 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L576-L577

Added lines #L576 - L577 were not covered by tests

self.native.unsetCursor()
self._canvas.native.unsetCursor()

Check warning on line 579 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L579

Added line #L579 was not covered by tests

if self._pos_is_clim(event) > -1:
if self._vertical:
cursor = Qt.CursorShape.SplitVCursor

Check warning on line 583 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L581-L583

Added lines #L581 - L583 were not covered by tests
else:
cursor = Qt.CursorShape.SplitHCursor
self.native.setCursor(cursor)
self._canvas.native.setCursor(cursor)
elif self._pos_is_gamma(event):
if self._vertical:
cursor = Qt.CursorShape.SplitHCursor

Check warning on line 589 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L585-L589

Added lines #L585 - L589 were not covered by tests
else:
cursor = Qt.CursorShape.SplitVCursor
self.native.setCursor(cursor)
self._canvas.native.setCursor(cursor)

Check warning on line 592 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L591-L592

Added lines #L591 - L592 were not covered by tests
else:
x, y = self._to_plot_coords(event.pos)
x1, x2 = self.plot.xaxis.axis.domain
y1, y2 = self.plot.yaxis.axis.domain
if (x1 < x <= x2) and (y1 <= y <= y2):
self.native.setCursor(Qt.CursorShape.SizeAllCursor)
self._canvas.native.setCursor(Qt.CursorShape.SizeAllCursor)
if self._vertical:
self._domain = self.plot.yaxis.axis.domain

Check warning on line 600 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L594-L600

Added lines #L594 - L600 were not covered by tests
else:
self._domain = self.plot.xaxis.axis.domain

Check warning on line 602 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L602

Added line #L602 was not covered by tests

def _to_window_coords(self, pos: tuple[float, float]) -> tuple[float, float]:
def _to_window_coords(self, pos: Sequence[float]) -> tuple[float, float]:
x, y, _, _ = self.node_tform.imap(pos)
return x, y

Check warning on line 606 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L604-L606

Added lines #L604 - L606 were not covered by tests

def _to_plot_coords(self, pos: tuple[float, float]) -> tuple[float, float]:
def _to_plot_coords(self, pos: Sequence[float]) -> tuple[float, float]:
x, y, _, _ = self.node_tform.map(pos)
return x, y

Check warning on line 610 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L608-L610

Added lines #L608 - L610 were not covered by tests

Expand All @@ -600,3 +614,9 @@ def _resize(self) -> None:
x=self._range if self._vertical else self._domain,
y=self._domain if self._vertical else self._range,
)
if self._vertical:
scale = 0.98 * self.plot.xaxis.axis.domain[1]
self.lut_line.transform.scale = (scale, 1)

Check warning on line 619 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L617-L619

Added lines #L617 - L619 were not covered by tests
else:
scale = 0.98 * self.plot.yaxis.axis.domain[1]
self.lut_line.transform.scale = (1, scale)

Check warning on line 622 in src/ndv/histogram/_vispy.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/histogram/_vispy.py#L621-L622

Added lines #L621 - L622 were not covered by tests
46 changes: 35 additions & 11 deletions x.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import numpy as np
from qtpy.QtWidgets import QApplication
from qtpy.QtCore import QTimer
from qtpy.QtWidgets import QApplication, QPushButton

from ndv.histogram._model import StatsModel
from ndv.histogram._qt import QtHistogramView
from ndv.histogram._vispy import VispyHistogramView

app = QApplication([])
Expand All @@ -10,31 +12,53 @@
view = VispyHistogramView()


def connection(data: tuple[np.ndarray, np.ndarray]) -> None:
def _connection(data: tuple[np.ndarray, np.ndarray]) -> None:
values, bins = data
view.set_histogram(values, bins)
view.set_clims((bins[0], bins[-1]))
view.set_gamma(1)
# view.set_clims((bins[0], bins[-1]))
# view.set_gamma(1)


stats.events.histogram.connect(connection)
stats.events.histogram.connect(_connection)


# TODO: Once we have a LutModel to play with, direct these view signals to the model
# The controller will then change its state to the signal value, which should then
# be hooked up to call edit_X.
def foo(gamma: float) -> None:
def _foo(gamma: float) -> None:
view.set_gamma(gamma)


def bar(clims: tuple[float, float]) -> None:
def _bar(clims: tuple[float, float]) -> None:
view.set_clims(clims)


view.gammaChanged.connect(foo)
view.climsChanged.connect(bar)
view.set_domain((-40, 60))
# view.set_range((0, 3500))
view.gammaChanged.connect(_foo)
view.climsChanged.connect(_bar)

biggerview = QtHistogramView(view)
biggerview.view().show()

data_btn = QPushButton("Change Data")
data_btn.setCheckable(True)
timer = QTimer()
timer.setInterval(10)
timer.blockSignals(True)


def _update_data() -> None:
"""Replaces the displayed data."""
stats.data = np.random.normal(10, 10, 10000)


timer.timeout.connect(_update_data)


biggerview._layout.addWidget(data_btn)
data_btn.toggled.connect(lambda toggle: timer.blockSignals(not toggle))
timer.start()

view.view().show()

stats.data = np.random.normal(10, 10, 10000)
app.exec()

0 comments on commit 68e3f78

Please sign in to comment.