"For scatter with such volumes, a high-performance engine is definitely needed. If we aim for that, we will need to add it to the arsenal. On the other hand, in exploratory data analysis, scatter is only one tool. Many other charts (for example, histogram) show the result of one or another statistical function. Here ggplot is good."
This is a profoundly accurate observation. It highlights the fundamental distinction between raw data geoms (like geom_point) and statistical geoms (like geom_histogram, geom_density, geom_boxplot). Let's explore how this distinction informs the architecture for high-performance charting.
Charts like histograms, density plots, boxplots, and violin plots are inherently statistical aggregations. They take raw data and apply a statistical function (binning, quantiles, kernel density estimation) before rendering. The output is already a reduced dataset:
Because these geoms are already statistical transformations, they are naturally LOD-friendly. The data is reduced to a manageable size (often O(screen pixels) or O(bins)) before it ever reaches the rendering layer. ggplot's strength lies in this declarative mapping: data + geom_histogram() implies "apply the histogram stat, then render the bars."
Scatter plots are different. geom_point(x, y) is a raw data geom. It implies a 1:1 mapping: one mark per row. At 100k points, this is fine. At 10M or 100M points, this creates an O(N) rendering cliff:
The creator is right: for scatter with such volumes, a high-performance engine is definitely needed. But the question is: how do we maintain the declarative Grammar of Graphics spirit while solving the rendering bottleneck?
The insight from the xy library is to treat viewport-driven density aggregation as a dynamic statistical function. Just as geom_histogram() applies a binning stat, a high-performance scatter engine applies a density surface stat when the visible point count exceeds a threshold.
This is implemented via a Render Tier system that sits between the declarative spec and the WebGL renderer:
| Tier | Name | Representation | Trigger Condition |
|---|---|---|---|
| 0 | Direct | every visible mark, exact points | visible_count <= 200,000 |
| 1 | Shape-preserving reduction | per-pixel-column aggregate (M4 for lines) | Lines/areas >10k points |
| 2 | Density / aggregate surface | mean-point-color texture (512x384 grid) | visible_count > 200,000 |
| 3 | Out-of-core tiles | Tier-2 pyramid (4→1 exact sums) | Zoom-out beyond Tier-2 resolution |
The user still declares xy.scatter(x, y, color=col, density=True) or geom_point(). The engine internally decides whether to render Tier-0 points, Tier-2 density surface, or Tier-3 pyramid tiles based on the viewport's visible_count. This preserves the declarative Grammar of Graphics API while achieving 100M-point performance.
geom_point(), geom_histogram(), etc. should remain the user-facing syntax.geom_point with N > 200k, automatically switch to a "density surface" geom internally: But keep the color/alpha semantics using mean-color compositing (alpha-weighted mean of resolved colors in linear light).bin_2d_mean_color).(v - offset) * scale and update only two vec2 uniforms per mark in the WebGL shader.When switching from Tier-0 (points) to Tier-2 (density surface), a critical challenge is preserving the user's color and alpha aesthetics. The xy library solves this with physical alpha compositing and alpha-weighted mean color in linear light:
a_pt, the displayed alpha is 1 − (1 − a_pt)^k. This saturates after a few points exactly like real overplotted marks do.