Skip to content

Commit ceb4c11

Browse files
committed
zoom into histogram
1 parent 10d115a commit ceb4c11

10 files changed

Lines changed: 251 additions & 61 deletions

File tree

src/python/lfs_plugins/histogram_panel.py

Lines changed: 139 additions & 52 deletions
Large diffs are not rendered by default.

src/visualizer/gui/resources/locales/de.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1634,7 +1634,7 @@
16341634
"no_marked_range": "No marked range",
16351635
"gaussian_count": "{count} Gaussians",
16361636
"range_value": "{min} to {max}",
1637-
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom",
1637+
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom · double-click to fit",
16381638
"status_selection": "Marked range becomes the active Gaussian selection.",
16391639
"summary": "{metric} distribution across {count} Gaussians",
16401640
"bin_tooltip": "Bin {index}: {left} to {right} | {count} Gaussians",

src/visualizer/gui/resources/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1637,7 +1637,7 @@
16371637
"no_marked_range": "No marked range",
16381638
"gaussian_count": "{count} Gaussians",
16391639
"range_value": "{min} to {max}",
1640-
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom",
1640+
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom · double-click to fit",
16411641
"status_selection": "Marked range becomes the active Gaussian selection.",
16421642
"summary": "{metric} distribution across {count} Gaussians",
16431643
"bin_tooltip": "Bin {index}: {left} to {right} | {count} Gaussians",

src/visualizer/gui/resources/locales/fr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1634,7 +1634,7 @@
16341634
"no_marked_range": "No marked range",
16351635
"gaussian_count": "{count} Gaussians",
16361636
"range_value": "{min} to {max}",
1637-
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom",
1637+
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom · double-click to fit",
16381638
"status_selection": "Marked range becomes the active Gaussian selection.",
16391639
"summary": "{metric} distribution across {count} Gaussians",
16401640
"bin_tooltip": "Bin {index}: {left} to {right} | {count} Gaussians",

src/visualizer/gui/resources/locales/it.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1634,7 +1634,7 @@
16341634
"no_marked_range": "No marked range",
16351635
"gaussian_count": "{count} Gaussians",
16361636
"range_value": "{min} to {max}",
1637-
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom",
1637+
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom · double-click to fit",
16381638
"status_selection": "Marked range becomes the active Gaussian selection.",
16391639
"summary": "{metric} distribution across {count} Gaussians",
16401640
"bin_tooltip": "Bin {index}: {left} to {right} | {count} Gaussians",

src/visualizer/gui/resources/locales/ja.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1634,7 +1634,7 @@
16341634
"no_marked_range": "No marked range",
16351635
"gaussian_count": "{count} Gaussians",
16361636
"range_value": "{min} to {max}",
1637-
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom",
1637+
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom · double-click to fit",
16381638
"status_selection": "Marked range becomes the active Gaussian selection.",
16391639
"summary": "{metric} distribution across {count} Gaussians",
16401640
"bin_tooltip": "Bin {index}: {left} to {right} | {count} Gaussians",

src/visualizer/gui/resources/locales/ko.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1634,7 +1634,7 @@
16341634
"no_marked_range": "No marked range",
16351635
"gaussian_count": "{count} Gaussians",
16361636
"range_value": "{min} to {max}",
1637-
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom",
1637+
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom · double-click to fit",
16381638
"status_selection": "Marked range becomes the active Gaussian selection.",
16391639
"summary": "{metric} distribution across {count} Gaussians",
16401640
"bin_tooltip": "Bin {index}: {left} to {right} | {count} Gaussians",

src/visualizer/gui/resources/locales/nl.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1634,7 +1634,7 @@
16341634
"no_marked_range": "No marked range",
16351635
"gaussian_count": "{count} Gaussians",
16361636
"range_value": "{min} to {max}",
1637-
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom",
1637+
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom · double-click to fit",
16381638
"status_selection": "Marked range becomes the active Gaussian selection.",
16391639
"summary": "{metric} distribution across {count} Gaussians",
16401640
"bin_tooltip": "Bin {index}: {left} to {right} | {count} Gaussians",

src/visualizer/gui/resources/locales/pl.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1634,7 +1634,7 @@
16341634
"no_marked_range": "No marked range",
16351635
"gaussian_count": "{count} Gaussians",
16361636
"range_value": "{min} to {max}",
1637-
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom",
1637+
"status_drag_delete": "Left-drag to mark a range, then delete it · Ctrl+scroll to zoom · double-click to fit",
16381638
"status_selection": "Marked range becomes the active Gaussian selection.",
16391639
"summary": "{metric} distribution across {count} Gaussians",
16401640
"bin_tooltip": "Bin {index}: {left} to {right} | {count} Gaussians",

tests/python/test_histogram_panel.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,17 @@ class _UpdateHandleStub:
146146
def __init__(self):
147147
self.request_update_count = 0
148148
self.dirty_all_count = 0
149+
self.records = {}
149150

150151
def request_update(self):
151152
self.request_update_count += 1
152153

153154
def dirty_all(self):
154155
self.dirty_all_count += 1
155156

157+
def update_record_list(self, name, items):
158+
self.records[name] = list(items)
159+
156160

157161
def _translation_matrix(tx: float, ty: float, tz: float) -> list[list[float]]:
158162
return [
@@ -734,7 +738,7 @@ def get_action_for_scroll(mode, modifiers):
734738

735739
# A refresh would re-bin against the new custom range; mirror that so the view the
736740
# next scroll reads from tracks the committed range.
737-
def sync_view_from_custom_range():
741+
def sync_view_from_custom_range(view_only=False):
738742
panel._primary_histogram_min = (
739743
panel._auto_histogram_min
740744
if panel._custom_range_min_value is None
@@ -1197,3 +1201,102 @@ def _set_panel_space(panel_id, space):
11971201
finally:
11981202
lf.ui.get_panel = original_get_panel
11991203
lf.ui.set_panel_space = original_set_panel_space
1204+
1205+
1206+
# --- Snappiness / elite-UX regressions -------------------------------------------------
1207+
1208+
def test_wheel_zoom_magnitude_scales_with_delta(histogram_panel_module):
1209+
f = histogram_panel_module.HistogramPanel._wheel_zoom_magnitude
1210+
assert f(1.0) == 1.0
1211+
assert f(-1.0) == 1.0
1212+
assert f(120.0) == 1.0 # one HID notch
1213+
assert f(5.0) == 1.0 # small per-notch systems stay single-step
1214+
assert f(600.0) == pytest.approx(5.0) # 5-notch flick zooms 5x further
1215+
assert f(-100000.0) == 8.0 # clamped
1216+
1217+
1218+
def test_approx_equal_uses_relative_tolerance(histogram_panel_module):
1219+
f = histogram_panel_module.HistogramPanel._approx_equal
1220+
assert f(1.0, 1.0)
1221+
assert f(1_000_000.0, 1_000_000.5) # within 1e-6 relative
1222+
assert not f(1_000_000.0, 1_000_100.0) # outside relative tolerance
1223+
assert f(0.0, 0.0)
1224+
assert not f(0.0, 1e-3)
1225+
1226+
1227+
def test_cursor_zoom_magnitude_zooms_further(histogram_panel_module):
1228+
f = histogram_panel_module.HistogramPanel._cursor_zoom_bounds
1229+
one = f(0.0, 1.0, 0.5, 0.0, 1.0, zoom_in=True, magnitude=1.0)
1230+
five = f(0.0, 1.0, 0.5, 0.0, 1.0, zoom_in=True, magnitude=5.0)
1231+
assert one is not None and five is not None
1232+
assert (one[1] - one[0]) == pytest.approx(0.8)
1233+
assert (five[1] - five[0]) == pytest.approx(0.8 ** 5)
1234+
assert (five[1] - five[0]) < (one[1] - one[0])
1235+
1236+
1237+
def test_refresh_view_only_reuses_cache_without_extracting(histogram_panel_module, lf, numpy, monkeypatch):
1238+
panel = histogram_panel_module.HistogramPanel()
1239+
panel._handle = _UpdateHandleStub()
1240+
data = numpy.array([0.1, 0.5, 0.9], dtype=numpy.float32)
1241+
panel._primary_valid_values = lf.Tensor.from_numpy(data)
1242+
panel._primary_finite_values_cpu = lf.Tensor.from_numpy(data)
1243+
1244+
calls = {"extract": 0, "rebind": 0}
1245+
monkeypatch.setattr(panel, "_extract_metric_values",
1246+
lambda *a, **k: calls.__setitem__("extract", calls["extract"] + 1))
1247+
monkeypatch.setattr(panel, "_rebind_view_from_cache",
1248+
lambda: calls.__setitem__("rebind", calls["rebind"] + 1))
1249+
1250+
panel._refresh(view_only=True)
1251+
1252+
assert calls["rebind"] == 1 # re-binned from cache
1253+
assert calls["extract"] == 0 # never re-extracted the scene
1254+
assert panel._handle.dirty_all_count == 1 # exactly one repaint
1255+
1256+
1257+
def test_refresh_view_only_falls_back_when_cache_empty(histogram_panel_module, monkeypatch):
1258+
panel = histogram_panel_module.HistogramPanel()
1259+
panel._handle = _UpdateHandleStub()
1260+
panel._primary_finite_values_cpu = None
1261+
rebind = []
1262+
monkeypatch.setattr(panel, "_rebind_view_from_cache", lambda: rebind.append(1))
1263+
monkeypatch.setattr(histogram_panel_module.lf, "get_scene", lambda: None, raising=False)
1264+
1265+
panel._refresh(view_only=True)
1266+
1267+
assert rebind == [] # no cache -> falls through to the full (here: no-scene) path
1268+
1269+
1270+
def test_chart_dblclick_fits_and_clears_custom_range(histogram_panel_module, monkeypatch):
1271+
panel = histogram_panel_module.HistogramPanel()
1272+
panel._handle = _UpdateHandleStub()
1273+
panel._show_chart = True
1274+
panel._custom_range_min_value = 0.2
1275+
panel._custom_range_max_value = 0.8
1276+
refreshed = []
1277+
monkeypatch.setattr(panel, "_refresh_range_preserving_mark", lambda: refreshed.append(1))
1278+
1279+
event = _MouseEventStub(mouse_x=0.0)
1280+
panel._on_chart_dblclick(event)
1281+
1282+
assert panel._custom_range_min_value is None
1283+
assert panel._custom_range_max_value is None
1284+
assert refreshed == [1]
1285+
assert event.stopped is True
1286+
1287+
1288+
def test_mouseup_aborts_drag_when_scene_invalid(histogram_panel_module, monkeypatch):
1289+
panel = histogram_panel_module.HistogramPanel()
1290+
panel._handle = _UpdateHandleStub()
1291+
panel._dragging_mark = True
1292+
monkeypatch.setattr(histogram_panel_module.lf, "get_scene",
1293+
lambda: SimpleNamespace(is_valid=lambda: False), raising=False)
1294+
reset = []
1295+
monkeypatch.setattr(panel, "_reset_marked_state", lambda clear_scene=False: reset.append(clear_scene))
1296+
1297+
event = _MouseEventStub(mouse_x=0.0)
1298+
panel._on_document_mouseup(event)
1299+
1300+
assert panel._dragging_mark is False
1301+
assert reset == [False]
1302+
assert event.stopped is True

0 commit comments

Comments
 (0)