Interactive stat_bin with showSelected (#158 PoC) - #343
Conversation
Fails on master StatBin/showSelected guard; implementation follows. Signed-off-by: Gaurav Chaudhary <chaudharygaurav2004@gmail.com>
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.
|
this is interesting but lowest priority, let’s work on the other easier PRs first |
Codecov Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
There was a problem hiding this comment.
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 torect. A valid call such asstat_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 toGeomBar/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 noclickSelectsfield, while the existing click handler callsupdate_selector(..., d.clickSelects), so clicking a bar selectsundefinedinstead 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
builtscales, which are retrained afterStatBinon the generatedxmin/xmaxvalues 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, butStatBin$setup_paramsalso accepts the deprecatedoriginalias and converts it to a boundary. Withorigin=..., the initial R stat uses the requested alignment whilejs_stat_bin_x_breaks()always computes the default alignment, so bars are assigned to a different grid. Rejectoriginhere 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
CoordFlipis still allowed into this path, butGeom$export_animint()swaps serialized x/y column names for flipped coordinates whilecompute_stat_bin()always readsrow.xand emits x bounds. Consequently a flipped interactive histogram has no usable x values for counting and renders zero-height or incorrectly oriented bars. Rejectcoord_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 facetPANEL, but this overwrites that assignment by copying every row into every panel. A faceted histogram whoseshowSelectedvariable is not the facet variable (for examplefacet_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,
datacan contain rows from several selected values, butd.groupis computed byadd_group()aftershowSelected*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
ymaxis 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 otherstat_bincomputed 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
CoordFlipis used,Geom$export_animintrenames the pre-statxcolumn toybefore this function runs.rows[row_i].xis 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. CallingstartPreciseCoverage()again can fail withPrecise coverage has already been started, and that warning will escapeanimint2HTML()'sexpect_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.
| if(js_stat_bin){ | ||
| g$geom <- "rect" | ||
| }else{ | ||
| processed_values <- l$geom$pre_process(g, g.data, ranges) | ||
| g <- processed_values$g |
| do.call(rbind, lapply(panel_ids, function(panel_i) { | ||
| out <- df | ||
| out$PANEL <- panel_i | ||
| out |
| 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 |
| stop("JS stat_bin does not support pad=TRUE.", call.=FALSE) | ||
| } | ||
| closed <- params$closed | ||
| if(!is.null(closed) && !identical(closed, "right")){ |
| 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() |
| 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; |
| out.push({ | ||
| xmin: breaks[bin_i], | ||
| xmax: breaks[bin_i + 1], | ||
| ymin: 0, | ||
| ymax: counts[bin_i], |
Summary
This PR starts work on #158: making
stat_binwork withshowSelectedso 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":Why
Issue #158 asks whether stats should move out of compile-time processing. This PR defines the expected behavior for a small
stat_bin + showSelectedproof-of-concept before implementation.How the fix will work (Commit 2 )
showSelectedcolumns, and panelx_breaks.Selection changes counts, not the bin grid.

Local run shows expected failure on master