Charts: category axis orientation (maxMin) read, written and drawn; horizontal bars run bottom-up

This commit is contained in:
Nikita Rakitin
2026-10-14 03:16:35 +03:00
parent 8e124d32a8
commit 71b02327cf
6 changed files with 58 additions and 6 deletions
+8 -4
View File
@@ -382,10 +382,14 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h
}
let ncat = c.categories.len().max(c.series.iter().map(|s| s.values.len()).max().unwrap_or(0)).max(1);
let band = if horiz { plot.height() / ncat as f64 } else { plot.width() / ncat as f64 };
// Where category `i`'s band starts along its axis: left to right, or bottom to top on a
// horizontal bar chart, unless the axis is reversed.
let backwards = horiz != c.cat_reversed;
let slot = |i: usize| if backwards { ncat.saturating_sub(i + 1) as f64 } else { i as f64 };
// Category labels: wrapped onto two lines when they don't fit their band (long date ranges).
let wraps = !horiz && c.categories.iter().any(|n| n.chars().count() as f64 * base * 0.5 > band);
for (i, name) in c.categories.iter().enumerate() {
let mid = if horiz { plot.y0 + band * (i as f64 + 0.5) } else { plot.x0 + band * (i as f64 + 0.5) };
let mid = if horiz { plot.y0 + band * (slot(i) + 0.5) } else { plot.x0 + band * (slot(i) + 0.5) };
if horiz {
label(
ctx,
@@ -440,7 +444,7 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h
for (bi, (si, s)) in bars.iter().enumerate() {
let sec = c.on_secondary(s);
let v = s.values.get(i).copied().flatten().unwrap_or(0.0) / total;
let start = band * i as f64 + (band - group) / 2.0 + if stacked { 0.0 } else { step * bi as f64 };
let start = band * slot(i) + (band - group) / 2.0 + if stacked { 0.0 } else { step * bi as f64 };
let (a, b) = if stacked {
if v >= 0.0 {
let r = (pos_acc, pos_acc + v);
@@ -513,9 +517,9 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h
let xs: Vec<f64> = s.x_values.iter().flatten().copied().collect();
let (xl, xh) = (xs.iter().copied().fold(f64::MAX, f64::min), xs.iter().copied().fold(f64::MIN, f64::max));
let xv = s.x_values.get(i).copied().flatten().unwrap_or(i as f64);
if xh > xl { plot.x0 + (xv - xl) / (xh - xl) * plot.width() } else { plot.x0 + band * (i as f64 + 0.5) }
if xh > xl { plot.x0 + (xv - xl) / (xh - xl) * plot.width() } else { plot.x0 + band * (slot(i) + 0.5) }
}
_ => plot.x0 + band * (i as f64 + 0.5),
_ => plot.x0 + band * (slot(i) + 0.5),
};
pts.push(Point::new(x, to_axis(sec, v)));
}
+20
View File
@@ -198,6 +198,26 @@ fn column_points_take_their_own_fill_and_smooth_lines_curve() {
assert!(mid.y > mid.x, "the first half bows towards the peak instead of following the chord: {mid:?}");
}
#[test]
fn horizontal_bars_list_the_first_category_at_the_bottom_unless_reversed() {
let draw = |reversed: bool| {
let mut c = Chart::sample(ChartType::Bar);
c.title = None;
c.legend = None;
c.series.truncate(1);
c.categories = vec!["A".into(), "B".into(), "C".into()];
c.series[0].values = vec![Some(10.0), Some(10.0), Some(10.0)];
c.series[0].point_fills = vec![(0, deckcraft_model::style::Fill::solid(ColorRef::rgb(deckcraft_color::Rgba::rgb(255, 0, 0))))];
c.cat_reversed = reversed;
let sh = Shape { id: ShapeId(623), xfrm: Some(Xfrm::new(0.0, 0.0, 960.0, 540.0)), kind: ShapeKind::Chart(Box::new(c)), ..Default::default() };
let img = render_slide(&deck_with(vec![sh]), 0, &RenderOpts { scale: 1.0, ..Default::default() });
let rows: Vec<u32> = (0..540).filter(|y| (0..960).any(|x| img.pixel(x, *y)[..3] == [255, 0, 0])).collect();
(rows[0] + rows[rows.len() - 1]) / 2
};
assert!(draw(false) > 360, "first category at the bottom: {}", draw(false));
assert!(draw(true) < 180, "reversed: first category at the top: {}", draw(true));
}
#[test]
fn placeholder_prompts_only_in_edit_view() {
let p = Presentation::default();