mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-10 04:19:48 -06:00
Keep clustered hairline bars of each series in their own strip of the pixel column
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
95c3bb9b89
commit
494a7dbb5c
@@ -59,24 +59,32 @@ struct BarRun {
|
||||
a1: f32,
|
||||
lo: f32,
|
||||
hi: f32,
|
||||
/// More than one bar was merged.
|
||||
merged: bool,
|
||||
}
|
||||
|
||||
impl BarRun {
|
||||
fn new(pos: f32, lo: f32, hi: f32) -> Self {
|
||||
BarRun { bucket: pos.floor(), a0: pos, a1: pos, lo, hi }
|
||||
BarRun { bucket: pos.floor(), a0: pos, a1: pos, lo, hi, merged: false }
|
||||
}
|
||||
fn add(&mut self, pos: f32, lo: f32, hi: f32) {
|
||||
self.merged = true;
|
||||
self.a0 = self.a0.min(pos);
|
||||
self.a1 = self.a1.max(pos);
|
||||
self.lo = self.lo.min(lo);
|
||||
self.hi = self.hi.max(hi);
|
||||
}
|
||||
fn draw(&self, ctx: &mut Ctx, bw: f32, horizontal: bool, color: Rgba) {
|
||||
let along = self.a1 - self.a0 + bw;
|
||||
/// `band` is the part of the pixel column reserved for this series (`start`, `len`): in a
|
||||
/// clustered chart a merged run stays inside it, so the other series keep a visible strip.
|
||||
fn draw(&self, ctx: &mut Ctx, bw: f32, horizontal: bool, color: Rgba, band: Option<(f32, f32)>) {
|
||||
let (a0, along) = match band {
|
||||
Some((start, len)) if self.merged => (self.bucket + start, len),
|
||||
_ => (self.a0, self.a1 - self.a0 + bw),
|
||||
};
|
||||
if horizontal {
|
||||
ctx.rect(self.lo, self.a0, self.hi - self.lo, along, color, None);
|
||||
ctx.rect(self.lo, a0, self.hi - self.lo, along, color, None);
|
||||
} else {
|
||||
ctx.rect(self.a0, self.lo, along, self.hi - self.lo, color, None);
|
||||
ctx.rect(a0, self.lo, along, self.hi - self.lo, color, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,10 +113,19 @@ impl BarSink {
|
||||
fn pos(&self, c: f32) -> f32 {
|
||||
if self.horizontal { c + self.nslots * self.bw / 2.0 - (self.k + 1.0) * self.bw } else { c - self.nslots * self.bw / 2.0 + self.k * self.bw }
|
||||
}
|
||||
/// This series' strip of a pixel column (offset, length) when the columns hold several
|
||||
/// clustered series, in the direction the slots run.
|
||||
fn band(&self) -> Option<(f32, f32)> {
|
||||
(self.nslots > 1.0).then(|| {
|
||||
let idx = if self.horizontal { self.nslots - 1.0 - self.k } else { self.k };
|
||||
(idx / self.nslots, 1.0 / self.nslots)
|
||||
})
|
||||
}
|
||||
/// Draws the bar at category center `c` spanning `lo..lo + len` on the value axis, or, when
|
||||
/// `merge` is set, adds it to the current run.
|
||||
fn bar(&mut self, ctx: &mut Ctx, merge: bool, c: f32, lo: f32, len: f32) {
|
||||
let pos = self.pos(c);
|
||||
let band = self.band();
|
||||
if !merge {
|
||||
if self.horizontal {
|
||||
ctx.rect(lo, pos, len, self.bw, self.color, None);
|
||||
@@ -121,14 +138,15 @@ impl BarSink {
|
||||
Some(m) if m.bucket == pos.floor() => m.add(pos, lo, lo + len),
|
||||
run => {
|
||||
if let Some(m) = run.replace(BarRun::new(pos, lo, lo + len)) {
|
||||
m.draw(ctx, self.bw, self.horizontal, self.color);
|
||||
m.draw(ctx, self.bw, self.horizontal, self.color, band);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn finish(self, ctx: &mut Ctx) {
|
||||
let band = self.band();
|
||||
if let Some(m) = self.run {
|
||||
m.draw(ctx, self.bw, self.horizontal, self.color);
|
||||
m.draw(ctx, self.bw, self.horizontal, self.color, band);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,40 @@ fn hairline_columns_are_merged_and_wide_ones_are_not() {
|
||||
assert!((1..=320).contains(&n), "{n} bars");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clustered_hairline_series_keep_a_visible_strip() {
|
||||
// Four series of similar height, 20,000 categories in 400 px: every pixel column holds a
|
||||
// merged bar of each series, and they must not cover each other.
|
||||
for kind in [ChartKind::ColumnClustered, ChartKind::BarClustered] {
|
||||
let horizontal = kind == ChartKind::BarClustered;
|
||||
let mut c = chart(kind);
|
||||
c.data_labels = false;
|
||||
c.legend = LegendPos::None;
|
||||
let mut d = data_of(4, 20_000);
|
||||
for (s, series) in d.series.iter_mut().enumerate() {
|
||||
for (i, v) in series.values.iter_mut().enumerate() {
|
||||
*v = Some(50.0 + ((i + s) % 3) as f64);
|
||||
}
|
||||
}
|
||||
let prims = render(&c, &d, 400.0, 300.0, &ApproxMeasure);
|
||||
let along = |r: &(f32, f32, f32, f32)| if horizontal { (r.1, r.1 + r.3) } else { (r.0, r.0 + r.2) };
|
||||
let mut strips: Vec<(f32, f32, usize)> = Vec::new();
|
||||
for (s, series) in d.series.iter().enumerate() {
|
||||
strips.extend(series_rects(&prims, series.color).iter().map(|r| (along(r).0, along(r).1, s)));
|
||||
}
|
||||
strips.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
assert!(strips.len() > 400);
|
||||
// Rectangles of different series never overlap (more than rounding).
|
||||
for w in strips.windows(2) {
|
||||
assert!(w[0].2 == w[1].2 || w[1].0 >= w[0].1 - 1e-3, "series {} and {} overlap", w[0].2, w[1].2);
|
||||
}
|
||||
// And all four series show up in the same pixel column.
|
||||
let col = strips[strips.len() / 2].0.floor();
|
||||
let seen: std::collections::BTreeSet<usize> = strips.iter().filter(|s| s.0.floor() == col).map(|s| s.2).collect();
|
||||
assert_eq!(seen.len(), 4, "series visible in pixel column {col}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_data_is_rendered_without_a_copy() {
|
||||
let d = data_of(3, 50);
|
||||
|
||||
Reference in New Issue
Block a user