Skip to content

Interactive stat_bin with showSelected (#158 PoC) - #343

Open
ANAMASGARD wants to merge 6 commits into
masterfrom
interactive-stat-bin-158
Open

Interactive stat_bin with showSelected (#158 PoC)#343
ANAMASGARD wants to merge 6 commits into
masterfrom
interactive-stat-bin-158

Conversation

@ANAMASGARD

Copy link
Copy Markdown
Contributor

Summary

This PR starts work on #158: making stat_bin work with showSelected so histograms update when the user changes selection.

What in Commit 1

Added tests/testthat/test-renderer3-stat-bin-showSelected.R.

The test checks that a faceted histogram with showSelected="facet":

  • compiles without the current StatBin guard warning
  • renders in the browser
  • recalculates counts when facet selection changes
  • keeps bin positions stable

Why

Issue #158 asks whether stats should move out of compile-time processing. This PR defines the expected behavior for a small stat_bin + showSelected proof-of-concept before implementation.

How the fix will work (Commit 2 )

  1. R exports pre-stat data, showSelected columns, and panel x_breaks.
  2. JS filters by selection, recomputes bin counts, then draws bars.

Selection changes counts, not the bin grid.
Screenshot From 2026-08-12 21-06-44

Local run shows expected failure on master

image

Fails on master StatBin/showSelected guard; implementation follows.

Signed-off-by: Gaurav Chaudhary <chaudharygaurav2004@gmail.com>
ANAMASGARD and others added 2 commits August 19, 2026 21:38
R exports pre-stat rows and panel x_breaks; stat-bin.js rebins after
selection. Narrow StatBin guard for js_stat=bin. Fixes acceptance
test xpath and select_facet helper from Commit 1.
@tdhock

tdhock commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

this is interesting but lowest priority, let’s work on the other easier PRs first

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.19858% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.51%. Comparing base (8f009ed) to head (3310c34).

Files with missing lines Patch % Lines
R/plot-build.r 83.67% 8 Missing ⚠️
R/z_animint.R 86.66% 2 Missing ⚠️
R/z_knitr.R 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #343      +/-   ##
==========================================
+ Coverage   73.07%   78.51%   +5.43%     
==========================================
  Files         165      166       +1     
  Lines        8933     9071     +138     
  Branches        0      186     +186     
==========================================
+ Hits         6528     7122     +594     
+ Misses       2405     1949     -456     
Flag Coverage Δ
javascript 97.36% <100.00%> (+16.29%) ⬆️
r 69.22% <83.58%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Chromote only saw the last document; snapshot before refresh and
merge same-URL Istanbul hits so stat-bin.js is counted. Does not
close #158.
@ANAMASGARD
ANAMASGARD requested a review from tdhock August 23, 2026 16:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a proof-of-concept for browser-side stat_bin recalculation when showSelected changes, with stable bin boundaries.

Changes:

  • Exports pre-stat data and bin breaks from R.
  • Adds JavaScript histogram binning.
  • Integrates the new path into rendering and HTML/knitr output.
  • Updates tests, coverage handling, NEWS, and package metadata.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Changes and final review comments
v8-to-istanbul.js Merges coverage from multiple snapshots.
tests/testthat/test-renderer3-stat-bin.R Updates histogram rendering expectations.
tests/testthat/test-renderer3-stat-bin-showSelected.R Adds interactive histogram tests.
tests/testthat/helper-HTML.R Captures JavaScript coverage snapshots.
R/z_knitr.R Loads the new JavaScript dependency.
R/z_animint.R Routes interactive StatBin layers; moderate issue: unbuilt plot scales can leave mapped non-position aesthetics incorrectly serialized.
R/plot-build.r Critical issue: effective default closed handling rejects direct stat_bin() calls. Moderate issues: explicit breaks are ignored, and facet panel assignments can be overwritten.
R/geom-.r Moderate issue: coord_flip renames raw x before JavaScript binning, producing zero-height bins.
NEWS.md Documents the proof of concept.
inst/htmljs/stat-bin.js Moderate issues: weights are ignored, and generated rows discard styling and interaction metadata.
inst/htmljs/index.html Loads stat-bin.js.
inst/htmljs/animint.js Invokes browser-side binning.
DESCRIPTION Updates package metadata and version.
Suppressed comments (10)

R/geom-.r:341

  • Because this path is selected solely by StatBin, it rewrites a caller-supplied geom to rect. A valid call such as stat_bin(geom="line", position="identity", binwidth=1, showSelected=...) is therefore silently rendered as bars instead of honoring the documented geom override. Restrict the JS path to GeomBar/histograms or reject other geoms.
      g$geom <- "rect"

R/geom-.r:320

  • Bypassing the non-identity-stat guard here also permits combining this PoC with clickSelects. The browser aggregation emits no clickSelects field, while the existing click handler calls update_selector(..., d.clickSelects), so clicking a bar selects undefined instead of a value. Reject clickSelects for this path or define an unambiguous bin-level value.
    js_stat_bin <- identical(l$js_stat, "bin")
    if(!(js_stat_bin && stat.type == "StatBin")){
      checkForNonIdentityAndSS(stat.type, has.show, is.show, l,
                              g$classed, names(g.data), names(g$aes))

R/plot-build.r:363

  • These breaks are computed from the final built scales, which are retrained after StatBin on the generated xmin/xmax values when facet shrink is enabled. That range can be wider than the original pre-stat x range, causing the JS grid to gain or shift bins (often an extra empty edge bin) instead of matching the bins used by the R stat. Compute the breaks from the pre-stat panel x dimensions before the statistic is applied.
js_stat_bin_x_breaks <- function(built, binwidth) {
  layout <- built$panel$layout
  breaks_list <- list()
  for(panel_i in layout$PANEL) {
    scales <- panel_scales(built$panel, panel_i)
    bins <- bin_breaks_width(scales$x$dimension(), binwidth)
    breaks_list[[as.character(panel_i)]] <- bins$breaks

R/plot-build.r:315

  • The validator rejects an explicit boundary, but StatBin$setup_params also accepts the deprecated origin alias and converts it to a boundary. With origin=..., the initial R stat uses the requested alignment while js_stat_bin_x_breaks() always computes the default alignment, so bars are assigned to a different grid. Reject origin here as well or carry the normalized boundary into the JS parameters.
  if(!is.null(params$boundary)){
    stop("JS stat_bin does not support boundary=.", call.=FALSE)
  }

R/z_animint.R:394

  • CoordFlip is still allowed into this path, but Geom$export_animint() swaps serialized x/y column names for flipped coordinates while compute_stat_bin() always reads row.x and emits x bounds. Consequently a flipped interactive histogram has no usable x values for counting and renders zero-height or incorrectly oriented bars. Reject coord_flip() here or carry the coordinate orientation through the JavaScript binning and rendering path.
        L$stat_params$x_breaks <- js_stat_bin_x_breaks(
          ggplot.info$built, L$stat_params$binwidth)

R/z_animint.R:391

  • get_pre_stat_layer_data() has already assigned each row to its facet PANEL, but this overwrites that assignment by copying every row into every panel. A faceted histogram whose showSelected variable is not the facet variable (for example facet_grid(facet~.) + showSelected="year") will therefore count all facets in every panel instead of preserving the facet split. Retain the original panel assignment, or make any replication conditional on an explicitly defined facet-selection behavior.
        panel_ids <- ggplot.info$built$panel$layout$PANEL
        df <- js_stat_bin_all_panels(df, panel_ids)

inst/htmljs/stat-bin.js:53

  • For multiple selection, data can contain rows from several selected values, but d.group is computed by add_group() after showSelected* is added as an aesthetic; discrete showSelected columns therefore become part of the default group whenever no explicit group is mapped. The same visual group is consequently binned separately per selected value, producing overlapping per-value bars instead of counts over the combined selection. Use a stable visual grouping that excludes showSelected unless grouping by it was explicitly requested.
        groups = d3.nest().key(function(d) {
                return d.group;
        }).sortKeys(d3.ascending).entries(data);

inst/htmljs/stat-bin.js:65

  • ymax is always set to the raw count, regardless of the layer's calculated y mapping. For example, geom_histogram(aes(y=..density..), showSelected=...) is rendered as counts rather than density (and the other stat_bin computed variables are likewise ignored). Compute the requested stat value or reject non-count y mappings before selecting this JS implementation.
                                ymin: 0,
                                ymax: counts[bin_i],
                                fill: fill_val,

inst/htmljs/stat-bin.js:29

  • When CoordFlip is used, Geom$export_animint renames the pre-stat x column to y before this function runs. rows[row_i].x is then undefined, so every value is skipped and the histogram counts are zero; the generated bounds are not flipped either. Preserve the source x for binning and explicitly swap the generated coordinates, or reject this coordinate system.
                if (x_val === null || isNaN(x_val)) {
                        continue;
                }
                bin_i = bin_index_for_stat_bin(Number(x_val), breaks);

tests/testthat/helper-HTML.R:126

  • takePreciseCoverage() only reads the active session; it does not stop it. Calling startPreciseCoverage() again can fail with Precise coverage has already been started, and that warning will escape animint2HTML()'s expect_no_warning() checks in the JS_coverage suite. Stop the session before restarting it.
    remDr$Profiler$startPreciseCoverage(callCount = TRUE, detailed = TRUE)

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread R/geom-.r
Comment on lines +340 to +344
if(js_stat_bin){
g$geom <- "rect"
}else{
processed_values <- l$geom$pre_process(g, g.data, ranges)
g <- processed_values$g
Comment thread R/plot-build.r
Comment on lines +369 to +372
do.call(rbind, lapply(panel_ids, function(panel_i) {
out <- df
out$PANEL <- panel_i
out
Comment thread R/plot-build.r
Comment on lines +357 to +363
js_stat_bin_x_breaks <- function(built, binwidth) {
layout <- built$panel$layout
breaks_list <- list()
for(panel_i in layout$PANEL) {
scales <- panel_scales(built$panel, panel_i)
bins <- bin_breaks_width(scales$x$dimension(), binwidth)
breaks_list[[as.character(panel_i)]] <- bins$breaks
Comment thread R/plot-build.r
stop("JS stat_bin does not support pad=TRUE.", call.=FALSE)
}
closed <- params$closed
if(!is.null(closed) && !identical(closed, "right")){
Comment thread R/z_animint.R
validate_js_stat_bin_params(L)
pre_stat <- get_pre_stat_layer_data(ggplot.info$ggplot)
df <- pre_stat[[layer.i]]
npscales <- ggplot.info$ggplot$scales$non_position_scales()
Comment thread inst/htmljs/stat-bin.js
Comment on lines +24 to +31
for (row_i = 0; row_i < rows.length; row_i++) {
x_val = rows[row_i].x;
if (x_val === null || isNaN(x_val)) {
continue;
}
bin_i = bin_index_for_stat_bin(Number(x_val), breaks);
if (0 <= bin_i) {
counts[bin_i] += 1;
Comment thread inst/htmljs/stat-bin.js
Comment on lines +60 to +64
out.push({
xmin: breaks[bin_i],
xmax: breaks[bin_i + 1],
ymin: 0,
ymax: counts[bin_i],
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants