Density surfaces wear mean point color (LOD doc §2)#226
Open
masenf wants to merge 4 commits into
Open
Conversation
The Tier-2 density map colormapped the binned COUNT, ignoring the points' own colors — a colored scatter's aggregate view matched neither its marks nor its legend, and every density<->points zoom transition visibly recolored the chart. The surface now wears the data's colors (LOD doc §2): each cell carries the alpha-weighted mean of its binned points' resolved colors — one law for continuous, categorical, and direct-RGBA channels — averaged in linear light through an integer-only pipeline (checked-in sRGB<->linear u16 tables, u64 sums; bitwise deterministic across thread counts and platforms). The log-tone-mapped count drives only the alpha channel: more points, deeper color; fewer, lighter. Constant-color traces keep the compact count-only wire and a client tint (the mean of a constant is the constant). - Rust core: bin_2d_mean_color kernel, with a fused cell accumulator that fans out under a points-per-cell gate (<=4 workers, exact integer merge — output identical to the serial scan); pyramid mean-color planes (build_color/compose_color; count grids bit-identical to the count-only compose; colored pyramids refuse appends and are invalidated + rebuilt lazily). C ABI v40. - Python: channels.resolve_bin_colors maps every channel mode onto the kernel color source; initial emit + density_view (exact and pyramid paths) ship the RGBA plane as density.rgba with color_agg: "mean"; color is no longer listed in dropped_channels; the windowed-exact spatial tier is gated to colorless traces (its index is position-only, §27 — the upsampled colored-pyramid grid stays instead); SVG/PNG exporters follow the same color law; continuous-channel density scatters render their colorbar again. - Client: DENSITY_FS mean-color branch over a premultiplied RGBA8 texture (count tone curve baked at upload so bilinear filtering weights color by coverage); exposure easing re-encodes color grids; the standalone to_html re-bin worker aggregates the sample's colors under the same law; the density-tier gradient legend swatch is removed (count is alpha, so a gradient would claim color == density; a named density trace falls through to the plain marker swatch, matching the static exporters). - Handoff (§5): intensity-only. Hue is continuous by construction, so drilled points arrive in native colors, enter at their cell's count-alpha, and ease to native opacity; density_colormap left the points wire, and density_val (still u8 on the live wire, #221) now weights alpha instead of indexing a colormap LUT. The aggregate backdrop retires once a drill settles inside its window (T10 amended: a mean-color wash under exact marks reads as data) and eases back fast when the view leaves the window, a refinement goes pending, or the drill dies — zoom-outs never blank and interleaved replies never flash. - Spec: LOD doc §2 rules rewritten around the shipped mean-color law, pyramid §4 color planes and refused appends, T3/T10 amended, Phase 1/3 progress; dossier §5 Tier-2, F5 verdict, and constants table; wire protocol records density.rgba/color_agg, the u8 unit-scalar encodings, and the slimmer points message; chart-kind contract covers the aggregate kernel pairing and the legend rule; rust-engine inventory row. - Tests: kernel-vs-NumPy oracle suite (tests/test_density_mean_color, including a serial parallel-merge oracle and exporter pixel checks), wire and colorbar assertions, render-smoke probes for the mean-color surface (meancolor) and backdrop retirement (dretire). The pyramid color test asserts the direction the #153 area-weighted compose keeps invariant (nonzero count implies lit, unlit implies zero count) rather than exact mask equality, which boundary-bin count slivers can legitimately break.
The demo predated the kernel pyramid and carried two extra density implementations to stay interactive at 100M points: a 6144^2 integral-image overview server-side and ~350 lines of page JS (browser local re-bins, request parking/single-flight/timeout-retry, per-client staleness maps, pending-status heuristics). Both were count-only, so once density surfaces started wearing the mean point color, mid-zoom windows flipped back to a count-colormapped green wash, flashing old<->new across zoom levels. The engine has since absorbed every job they existed to do: Figure.density_view serves wide windows from the mean-color pyramid in O(visible cells) with drill bookkeeping and recorded binning, and the render client debounces requests, seq-drops stale replies, and keeps the best cached texture drawn while a reply is in flight. The module is now the pattern it demonstrates: build one figure (pyramid warmed at startup), serve payload + bundled client as HTML, and wire ChartView to a comm object that POSTs messages to one endpoint dispatching density_view/pick under a lock. Page JS drops from ~400 lines to ~60 (fetch + status badge), and the HTML route reuses the shared figure instead of regenerating 100M rows per GET. Round-trips ship as XYBF binary frames (wire-protocol §7): the reply message is the frame's compact JSON metadata and each numeric buffer rides raw and 8-byte aligned, so the browser decodes one xy.decodeFrame and hands the kernel zero-copy views with no base64 step on either side (~33% smaller replies on the grids and point buffers that dominate a drill). First paint still embeds its blob as base64 — inline HTML has no binary channel (§6) — and malformed requests still return a JSON error the client rejects on res.ok before decoding. tests/test_drilldown_pan_alignment.py is removed with its subject: it guarded the browser-local re-bin's window-clamp bug, and engine-served grids always report exactly the window they binned, so that failure mode is unreachable. The example test now decodes the XYBF frame and asserts on its message and raw buffers.
Serve the fastapi example's live drilldown scatter — same seed-11 chunked columns, same mark config, same XY_LIVE_POINTS override — from the reflex-xy adapter as one inline() token with zero transport code. The fastapi app wires this chart through a custom HTTP transport (a Starlette endpoint plus a comm bridge); the adapter's websocket namespace and the kernel's density tiers replace all of it, so behavior differences between the two hosts isolate what that custom code adds versus what is fundamental to the engine (density tiers, pyramid binning, drill-in). - examples/reflex: §6 section + drilldown_chart()/drilldown_view(), README run notes (import-time build, memory, XY_LIVE_POINTS override) - tests/test_example_apps.py: source markers for §6; the import test pins XY_LIVE_POINTS=50000 (same override the fastapi test uses) and asserts the inline token mints - scripts/reflex_ws_smoke.py: count the seventh live source, wait for the drilldown payload, and pixel-check its density surface - spec: reflex-integration §7 inventory + production-readiness example focus bullet name the deliberate cross-host overlap
Once a points reply has shipped its window exactly (reduction "none" — the subset IS every point in the window), any view contained in that window is already answered by the marks on the GPU: the smaller window's points are a subset of the shipped ones. Drilling deeper therefore no longer round-trips the kernel — _scheduleViewRequest skips the trace's pending markers and its density_view message (lodDrillServesView, re-checked at debounced send time), while still bumping seq so an in-flight reply for an older, wider view dies stale instead of yanking exact marks out from under a view it cannot improve. Three conditions re-arm the request: leaving the window, a drill that is dying or not exact (only reduction "none" arms the elision, recorded as drill.exact), and depth — the shipped geometry is f32, offset-encoded on the window midpoint (dossier §16), so once the view span drops below 1/256 of the window span on either axis one request goes out purely to re-center the encoding. Data changes cannot serve stale marks through the elision: streaming append and full payload updates rebuild the GPU trace, which drops the drill. Recorded as invariant T12 in spec/design/lod-architecture.md §5, in the wire protocol's density_view entry, and in dossier §16/§28; covered by a headless-Chromium probe (tests/test_drill_zoomin_elides_request.py) asserting the elision and each re-arm condition.
Greptile SummaryThis PR makes density surfaces reflect the colors of their source points. The main changes are:
Confidence Score: 5/5This looks safe to merge. No blocking issues found in the changed code.
What T-Rex did
Important Files Changed
Reviews (2): Last reviewed commit: "Elide density_view requests for zooms in..." | Re-trigger Greptile |
masenf
changed the base branch from
claude/density-mean-color-on-issue-153
to
main
July 23, 2026 06:12
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.
Tier-2 scatter aggregation now colors each density cell with the alpha-weighted mean of the point colors in that cell, averaged in linear light. The count still drives the alpha channel for opacity/coverage. This eliminates the visual lie of colormapping density counts and lets zoomed-out views truthfully represent the data's own colors.
Key changes
Rust core (
src/):bin_2d_mean_color_cellskernel that accumulates exact integer sums of alpha-weighted linear-light color alongside counts, enabling order-independent parallel reductionPyramid::build_colorpath that builds mean-color planes alongside count pyramids;reduce_color_levelmerges them during pyramid constructionSRGB_TO_LINEAR_U16,linear_u16_to_srgb_u8) for bitwise-deterministic color math independent of thread countABI_VERSIONto 40; addedxy_bin_2d_mean_colorandxy_pyramid_build_colorC exportsPython (
python/xy/):interaction.py: Channel-bearing traces now build mean-color pyramids; appends invalidate and rebuild (color domain changes require rescan)channels.py: Newquantize_unit_u8andcolormap_lut_rgba8helpers for wire quantization and LUT export_payload.py: Density updates now ship mean-color RGBA8 planes alongside counts; continuous channels quantize to u8 for wire efficiencymarks.py: Color channel survives aggregation as the density surface's per-cell mean; size/density_val channels are dropped (count is implicit in alpha)JavaScript (
js/src/):45_lod.ts:lodWriteGridTexturenow handles RGBA mean-color planes; premultiplies RGB by the log-tone-mapped alpha so bilinear filtering weights color by coverage54_kernel.ts: Zoom-in request elision (T12) — views inside an exact drill window skip the round-trip and use cached marks50_chartview.ts: Density traces no longer get a colormap gradient swatch in the legend (they wear the data's own colors, not a colormap)FastAPI example (
examples/fastapi/live_drilldown.py):Figure.density_viewandFigure.pickDensityOverviewclass and integral-image math; the pyramid now lives in the engineencode_framefromxy.channelfor XYBF binary transport (no base64)seqDocumentation & tests:
spec/design/lod-architecture.md§2: Documented mean-color aggregation law and per-channel rulestests/test_density_mean_color.py: Kernel correctness vs NumPy oracle, wire shape, static exporter color lawtests/test_drill_zoomin_elides_request.py: T12 elision (no re-request inside exact drill window)Implementation notes
color_levels: Option<Vec<Vec<[u16; 4]>>>inPyramid); count-only traces skip themdensity_colormapfield; the surface IS the data's colorsseqwithout sending a message, so stale replies from wider views are dropped correctlyhttps://claude.ai/code/session_01VYW3bncfneK8NbLoAFDMoo