Repository navigation
fix(cube): brush binned sources on their own bin grid - #134
Merged
Merged
Conversation
Merging this PR will not alter performance
Performance Changes
Comparing |
jvdd
force-pushed
the
fix/cube-source-grid
branch
from
September 30, 2026 13:18
a8e5eb9 to
f415455
Compare
jvdd
force-pushed
the
fix/cube-source-grid
branch
2 times, most recently
from
October 1, 2026 05:43
cb31aaa to
682c636
Compare
This was referenced Oct 1, 2026
…es to the nearest edge
jvdd
force-pushed
the
fix/cube-source-grid
branch
from
October 1, 2026 08:17
682c636 to
0b51914
Compare
This was referenced Oct 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #133
A histogram or histogram2d source brushed the cube on a fixed grid (P = 2048, or 128 per axis) that ignored its bars and cells. With this PR the brushed trace owns the free-axis grid.
What changed
1.
fix(hist): drop the upper-bound pad, the top clamp covers it_HIST_BIN_EPSILON(1e-10 on the upper bin bound of a histogram) is removed from the display grid, the cube target dims and the plugin tests. The kernels' top clamp already puts a value athiin the last bin, as for a histogram2d.(hi, hi + 1e-10]that the display did not count.TargetDimSpec.bin_variantonly selected that pad and is removed.2.
fix(hist): draw a Date bar at its bin centerDatecolumn emits its bin centers asDatetime("ms"). Before, each center was rounded to a whole day. Plotly draws and highlights a bar at its center, so a bar sat up to half a day off its bin, and bars narrower than a day stacked on one day. The hover of such a bar now shows the time of day of its center.3.
fix(cube): bin range free axes like the display kernel and snap brushes to the nearest edge_fixed_hist_bin_expr, the display kernel's rule (1e-9 round epsilon, top clamp top - 1). The natural floor and the degenerate top binPare gone. The cube content key versionvis 2.fixed_line_envelope2dbins its free axis with the same rule (hist2d_axis_scale, round epsilon, clamp top - 1) and has no extra top free bin. Its x buckets do not change. The line-envelope build passes it the raw free value.snap_brush(flexviz/cube.py) andfvCubeSnap(runtime/cube.js) select each bin whose center the brush covers, ends included, which is Plotly's highlight rule for bars. The centers are the ones the display draws,(i + 0.5) * step + lo: an analytic estimate is settled on them, so a brush end on a float-noisy center keeps that bar. A zero span (a constant column) draws every bar onlo: a brush overloselects all bins, a brush beside it selects none.lo, an inner edge is the first double that the kernel puts in bink(found by bracketing and bisecting the doubles around the floatlo + (k - 1e-9) * stepalong the kernel's rulefloor((v - lo) * scale + eps), at most about 128 steps; near a zero crossing billions of doubles share one value ofv - lo), and a brush that reaches the top bin ends at the domain top withclosed="both". The ceil of an edge is then the first whole unit of a temporal bin.day_gridis removed for all sources, with the header fieldswandp_eff,_day_free_bin_expr,fvCubeDayGrid,fvCubeSnapDayandfvCubeSnap2d.4.
fix(cube): brush a histogram source on its own barsHistogram.get_cube_source_specreturnsp = binsand, when zoomed, the viewport snapped to the bar lattice (snapped_domain, shared with the target spec)._histogram_domain_cols_by_uid, as for targets), because the source's bars span that union.free.p,free.domain,free.unit).fvCubeHeaderMatchesKeyrequiresporp + 1bins on a zoomed axis and, on a numeric axis, a domain that covers the viewport._fvCubeSourceConstraint: when a histogram and a non-binned trace share the brushed column in one figure, the histogram is the source._fvCubeSourcePis the one place that maps a source trace to its bin count.5.
fix(cube): brush a histogram2d source on its own cellsHistogram2D.get_cube_source_spectakesy_rangeand returnsp = (x_bins, y_bins), each zoomed axis snapped to its lattice. The engine passes one viewport per select anchor. The composite free bin isbin_y * nx + bin_x.FreeAxisSpec.pisint | tuple[int, int]and is validated per kind._locate_box2d_axis,box2d_composite_strideand_FV_CUBE_BOX2D_Pare removed.decodeFVCubesizes the dense bin index (4 × (nx × ny + 1)bytes) before it allocates it. An entry over the client budget is refused without that allocation.6.
fix(trace): round Float32 range bounds without moving a value across them_typed_range_boundsrounds a bound on aFloat32column like a bound on an integer column: a closed bound toward the interior of the range, an open bound away from it (_float32_toward). Before, the nearestFloat32could land on the far side of a value next to the bound. A whole-number bound arrives from JSON as an int and rounds too. This applies to committed selections and to zoomed viewport masks.7.
fix(predicates): round temporal selection bounds without moving a value across them_temporal_bound_toward: a selection bound finer than aDateorDatetime("ms")column rounds to a whole unit by the same rule. Before,_typed_temporal_littruncated it, so a range from Jan 1 13:00 kept Jan 1 (at 00:00), and[Jan 1 22:30, Jan 4 07:30)kept Jan 1 to 3 instead of Jan 2 to 4. This was a bug for every selection on such a column, not only the cube.8.
fix(cube): commit Date bar edges so a restored selection box keeps them_fvCubeCommitEdgescommits a Date edge as a µs string of the exact bar edge, and the server rounds it to whole days (commit 7). A ms or µs edge stays a whole unit. The stored selection box is the committed range, and_fvCubeBoxEdgesis removed./sharekeeps only the model fields of a selection, so a restore rebuilds the box from the predicate. Before, a Date box on bars narrower than two days came back on whole days: a box over bars 2 to 10 of 7 days in 16 bars came back as Jan 2 to Jan 6, so Plotly highlighted other bars than the rows.Why
Measured on
mainbefore this PR:A 200 x 200 hist2d source: a brush strictly inside one cell committed a box 1.56 cells wide per axis. Linked figures filtered 27 rows, the cell holds 12.
The default 20 x 20 grid: a brush inside cell
[30, 35)committedx = [31.25, 34.37). Linked figures showed 471 rows instead of 964.Cube size with
build_cube+encode_fvcubeon 2M rows, 20-bar histogram source (P = 2048 before, P = 20 after):A 20 x 20 hist2d source over a 20-bin hist target, 2M uniform rows: 326,930 cells (3.92 MB) at 128 x 128 against 8,000 cells (96.5 KB) on its own cells. A hist2d source finer than 128 x 128 gives a larger cube than before.
Found in review of the first version of this PR:
Datetime("us")edges one µs low when the span is not a round number. A row on that µs moved to another bar after a restore.(hi, hi + 1e-10]was in the cube but not in the display.Found in the follow-up review:
v - 1 === v. A 9999-12-31 sentinel in aDatetime("us")column puts even the first bar's upper edge there, so a brush over that bar froze the tab.Float32column: bins 0 and 3 of a 7-bin example showed 2 rows but committed 1. The cause is the nearest-Float32rounding of each bound, which predates this PR.Float64value within a few ulps oflo + (k - 1e-9) * stepcould be on the other side of the committed edge than in the kernel.Float64,Float32andInt64. After: 0 for each.Found in the third follow-up review:
The previous version binned the line-envelope free axis twice: Python computed each row's bin, turned it into a midpoint and filtered the rows, and the kernel binned the midpoints again with its old rule. The build was about 30 % slower than on
main. Now the kernel bins the free axis once. Release plugin, 4 threads, a 2048-bin free axis and 500 x buckets, medians of 39 alternating runs:mainA filter on the free domain before the collect saves time and memory on a zoomed source, but it costs a pass on every unzoomed build, so this PR leaves it out, as
maindoes: Line-envelope cube: filter a zoomed free domain before the collect #140.Found in the second follow-up review:
[-1, 1]in 20 bins, brush[0.02, 0.08]): a brush on an ordinary domain across zero froze the tab. The bisection returns in under 1 ms.Float32bound above 2^24 arrives from JSON as an int and skipped the rounding: 2 rows committed where the kernel counts 1.0.07500000000000001) dropped that bar.Contract
p = bins, the snapped viewport zoomed (binsorbins + 1bars), unzoomed the full domain unioned over its sibling histograms. Histogram2d:p = (x_bins, y_bins), each axis snapped. Line and box keepp = 2048._fixed_hist_bin_expron every free axis (and the same rule in the envelope kernel's free axis), the kernels' top clamp on every upper bound, no pad.snap_brush=fvCubeSnap, a bin is selected when the brush covers its drawn center, ends included; a zero span selects all bins or none. Each committed edge is the first double the kernel puts in its bin, found by a bounded search. A ms or µs edge is its ceil; a Date edge keeps its time of day. On the server aFloat32, integer or finer-than-column temporal bound rounds without moving a value across it.day_gridis gone for all sources.porp + 1bins, and on a numeric axis a domain that covers the viewport.Dateaxis.Release
flexviz-polarsmust ship with the nextflexvizrelease, and theflexviz-polarspin must rise: commit 3 changes the free-axis rule offixed_line_envelope2d, andcube.pyrelies on it.Known limits
Int64edge there is exact only to the float's step. The commit still returns.fvPhysicalToTemporalonmain): Cube brush on a time-zone-aware source (not UTC) commits its edges in the wrong zone #139.Tests
ruff format,ruff check flexviz testsandcargo fmt --check: clean.Every commit, with the release plugin built from commit 3 on. Non-browser is
pytest testswith the default markers, plusflexviz_polars/testsfrom commit 3 on, where the kernel changes. Browser ispytest tests -m browser -p no:randomly:The machine ran at load 4 to 8 from other work. Browser runs with 4 or 6 workers hit page-load timeouts in a different set of tests each run. Each of those tests passed when run again, and each commit then passed a full run with 3 workers. The one exception is commit 5:
test_cached_source_reset_issues_no_dashboard_update, which counts requests after a fixed 500 ms wait, failed in both full runs of that commit. It passed 5 of 5 runs on its own at commit 5 and at HEAD, and it passed in the full runs of commits 6 to 8.make test-perfon HEAD: 7 passed.mkdocs build --strictpassed.benchmarks/test_codspeed_engine.py -k build: 2 passed, including the newtest_build_line_cubes.TestDateHistSource,TestCubeGridGuardsandTestTemporalSourceCube, 5 runs: all passed.The edge sweep (values packed around every bin edge, domains across zero included): 0 of 6000 brushes wrong for
Float64,Float32andInt64, two seeds.Each new regression test fails on the code without its fix:
test_zoomed_hist_target_keeps_the_display_rows,test_date_bars_sit_on_their_bin_centers,test_zero_span_snap_covers_every_bar_or_none,test_brush_end_on_a_bar_center_includes_that_bar,test_committed_edges_keep_the_kernel_rows(Float64,Float32,Int64),test_temporal_commit_edges_are_the_kernel_bin_starts,test_constant_column_brush_selects_every_row,test_brush_highlights_the_rows_it_commits(both cases),test_sibling_hist_source_bins_on_its_display_grid,test_index_over_budget_is_sized_not_built,test_float32_bounds_keep_the_real_membership(float and whole-number bounds),test_float32_whole_number_bound_from_json_rounds,TestTemporalBoundRounding(the sub-unit cases),test_box_on_bar_edges_highlights_the_committed_bars(the restored box),test_free_value_on_an_edge_lands_in_the_display_bin(plugin), andtest_hover_lookup_puts_edge_values_in_the_server_binwithBIN_EPS = 0.test_edge_next_to_zero_is_found_in_few_stepsandtest_commit_edges_past_exact_float_integers_returnhang on the old code.fvCubeSnapcosts about 2 µs per call (Node, 20,000 calls).