Plot operator using ggrs
- Data load
- Streaming and chunking architecture
- Scatter plot with a single facet
- GITHUB Actions (CI)
- GITHUB Actions (Release)
- Plot saving
- Scatter plot with multiple facets (row/column/grid faceting with FreeY scales)
- Optimize bulk streaming for multi-facet (currently uses per-facet chunking)
- Add operator properties - Plot width/height with "auto", backend (cpu/gpu)
- Add support for continuous colors (numeric color factors with palette interpolation)
- Review and optimize dependencies
Note: Point size is hardcoded (4) - should come from crosstab model aesthetics.
- Use operator input specs to get projection information
- Dynamic point size
- Specify gRPC as communication protocol
- Add pages
- Add x axis support
- Add support for continuous color scale
- Add support for categorical colors (ColorLevels column)
- Add color scale legend
- Configurable textual elements in plot (axis labels, legend, title)
Note: Legend positioning still requires fine-tuning
- Add heatmap (basic tile rendering)
- Heatmap cell labels (category names from schema)
- Axis label overlap culling (automatic label hiding when dense)
- Legend colorbar uses actual Tercen palette (not hardcoded gradient)
- Tick label rotation (axis.x.tick.rotation, axis.y.tick.rotation)
- Add Test context structure
- Support for minimal and white themes
Heatmaps use a different coordinate system than scatter plots:
| Aspect | Scatter Plot | Heatmap |
|---|---|---|
| Position columns | .xs, .ys (quantized) |
.ci, .ri (grid indices) |
| Axis type | Continuous | Discrete (categorical labels) |
| Faceting | Yes (multiple panels) | No (grid IS the plot) |
| Scale expansion | 5% padding | 0.5 units (centers labels in tiles) |
Axis labels: Heatmaps display categorical labels (from facet group names) instead of numeric indices. Labels are centered within their tiles using a 0.5-unit expansion on the discrete scale, giving a range of (-0.5, n-0.5) that matches the tile extent.
Data behavior: When multiple data points exist at the same grid position, tiles overlap and the last-rendered color is visible. This matches ggplot2's geom_tile() behavior. For aggregated heatmaps (one value per cell), pre-aggregate data before plotting.
- Add bar plot
- Add line plot
- Add support for manual axis ranges
- Switching between GPU / CPU
- Further optimize bulk streaming for multi-facet
GGRS automatically hides overlapping axis labels to maintain visual clarity. This is implemented in ggrs-core/src/overlap.rs.
- Labels are processed in order (first-come-first-served)
- Each label's bounding box is checked against previously rendered labels
- If overlap detected (with 2px padding), the label is skipped
- Result: Only non-overlapping labels are displayed
| Component | Description |
|---|---|
BoundingBox |
Represents element bounds with rotation support |
OverlapCuller trait |
Common interface for overlap detection |
LinearCuller |
O(n) implementation for labels (< 1000 elements) |
GridCuller |
O(1) implementation for dense data points (future use) |
Axis labels can be rotated (e.g., 45° or 90°). The culling uses the axis-aligned bounding box (AABB) of the rotated rectangle:
Original (50×20): Rotated 45°: AABB of rotated:
┌──────────────┐ ╱╲ ┌─────────────────┐
│ Label │ → ╱ ╲ → │ │
└──────────────┘ ╲ ╱ │ (49×49) │
╲ ╱ │ │
╲╱ └─────────────────┘
let mut x_label_culler = LinearCuller::new(2.0); // 2px padding
for (&x_break, label) in x_breaks.iter().zip(x_labels.iter()) {
let (text_width, text_height) = estimate_text_size(label, font_size_px);
let bbox = BoundingBox::new(x_pixel, y_pixel, text_width, text_height);
if x_label_culler.should_render(bbox) {
root.draw(&Text::new(label, ...));
}
}The legend colorbar for continuous color scales now uses the actual Tercen palette instead of a hardcoded gradient.
TercenStreamGenerator::load_legend_scale()extracts color stops from the TercenColorPalette- Color stops are converted to
ggrs_core::legend::ColorStopand stored inLegendScale::Continuous draw_continuous_legend()usesLegendScale::interpolate_color()to sample the gradient- The legend gradient now matches the colors used in the actual plot
pub enum LegendScale {
Continuous {
min: f64,
max: f64,
aesthetic_name: String,
color_stops: Vec<ColorStop>, // From Tercen palette
},
Discrete { ... },
None,
}The interpolate_color(normalized) method:
- Takes a normalized value [0, 1]
- Converts to data space using min/max
- Finds surrounding color stops
- Performs linear RGB interpolation between stops
- Falls back to blue-red gradient if no stops provided
Axis tick labels can be rotated using operator properties.
| Property | Default | Description |
|---|---|---|
axis.x.tick.rotation |
0 |
X-axis tick label rotation in degrees |
axis.y.tick.rotation |
0 |
Y-axis tick label rotation in degrees |
Due to plotters library limitations, rotation is mapped to the nearest 90° increment:
| Input Range | Effective Rotation |
|---|---|
| -45° to 44° | 0° (horizontal) |
| 45° to 134° | 90° (vertical, clockwise) |
| 135° to 224° | 180° (upside down) |
| 225° to 314° | 270° (vertical, counter-clockwise) |
- Horizontal labels (default):
axis.x.tick.rotation = 0 - Vertical X labels:
axis.x.tick.rotation = 90- useful for long category names - Vertical Y labels:
axis.y.tick.rotation = 90- rarely needed
Alignment is automatically adjusted based on rotation:
- 0°: Center-Top (X), Right-Center (Y)
- 90°: Right-Center (X), Center-Bottom (Y)
- 180°: Center-Bottom (X), Left-Center (Y)
- 270°: Left-Center (X), Center-Top (Y)