mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-01 14:37:05 -06:00
Charts: draw data-point fills on columns and bars, smoothed lines, and the file's chart text colour
This commit is contained in:
@@ -36,6 +36,43 @@ fn series_fill(ctx: &Ctx, c: &Chart, i: usize, s: Option<&deckcraft_model::chart
|
||||
}
|
||||
}
|
||||
|
||||
/// The fill of point `k` of a series: its own (`c:dPt`) where the file gives one, else the series'.
|
||||
fn point_fill(ctx: &Ctx, c: &Chart, i: usize, s: &deckcraft_model::chart::Series, k: usize) -> Rgba {
|
||||
match s.point_fills.iter().find(|(p, _)| *p == k).map(|(_, f)| f) {
|
||||
Some(Fill::Solid { color: cc }) => ctx.color(cc, None),
|
||||
_ => series_fill(ctx, c, i, Some(s)),
|
||||
}
|
||||
}
|
||||
|
||||
fn polyline(pts: &[Point]) -> BezPath {
|
||||
let mut p = BezPath::new();
|
||||
for (k, q) in pts.iter().enumerate() {
|
||||
if k == 0 {
|
||||
p.move_to(*q);
|
||||
} else {
|
||||
p.line_to(*q);
|
||||
}
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// A smoothed line (`c:smooth`) through `pts`: a Catmull-Rom spline as cubic Béziers, the curve
|
||||
/// PowerPoint draws, passing through every point.
|
||||
pub(crate) fn smooth_path(pts: &[Point]) -> BezPath {
|
||||
let mut p = BezPath::new();
|
||||
let Some(first) = pts.first() else { return p };
|
||||
p.move_to(*first);
|
||||
for k in 0..pts.len() - 1 {
|
||||
let p0 = pts[k.saturating_sub(1)];
|
||||
let (p1, p2) = (pts[k], pts[k + 1]);
|
||||
let p3 = pts[(k + 2).min(pts.len() - 1)];
|
||||
let c1 = p1 + (p2 - p0) / 6.0;
|
||||
let c2 = p2 - (p3 - p1) / 6.0;
|
||||
p.curve_to(c1, c2, p2);
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// A line series' colour: its line's own (where PowerPoint keeps it), else its fill's, else the
|
||||
/// palette's.
|
||||
fn series_line(ctx: &Ctx, c: &Chart, i: usize, s: &deckcraft_model::chart::Series) -> Rgba {
|
||||
@@ -418,7 +455,7 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h
|
||||
Rect::new(plot.x0 + start, to_axis(sec, b), plot.x0 + start + bar, to_axis(sec, a))
|
||||
};
|
||||
ctx.set_transform(m);
|
||||
ctx.set_paint(color(series_fill(rctx, c, *si, Some(s)), 1.0));
|
||||
ctx.set_paint(color(point_fill(rctx, c, *si, s, i), 1.0));
|
||||
ctx.fill_rect(&r.abs());
|
||||
if c.data_labels {
|
||||
let t = c.data_label(s, s.values.get(i).copied().flatten().unwrap_or(0.0));
|
||||
@@ -480,14 +517,7 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h
|
||||
if pts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut p = BezPath::new();
|
||||
for (k, q) in pts.iter().enumerate() {
|
||||
if k == 0 {
|
||||
p.move_to(*q);
|
||||
} else {
|
||||
p.line_to(*q);
|
||||
}
|
||||
}
|
||||
let p = if s.smooth && !area_kind && pts.len() > 2 { smooth_path(&pts) } else { polyline(&pts) };
|
||||
ctx.set_transform(m);
|
||||
if area_kind {
|
||||
let mut a = p.clone();
|
||||
|
||||
@@ -155,6 +155,30 @@ fn combo_chart_draws_columns_and_a_line_on_its_own_axis() {
|
||||
assert!(col[0] < 250 || col[1] < 250 || col[2] < 250, "no column: {col:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_points_take_their_own_fill_and_smooth_lines_curve() {
|
||||
// One column series where only the middle point is given a red c:dPt fill.
|
||||
let mut c = Chart::sample(ChartType::Column);
|
||||
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![(1, deckcraft_model::style::Fill::solid(ColorRef::rgb(deckcraft_color::Rgba::rgb(255, 0, 0))))];
|
||||
let sh = Shape { id: ShapeId(621), 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 red = |x: u32| (300..500).any(|y| img.pixel(x, y)[..3] == [255, 0, 0]);
|
||||
assert!(red(480), "the middle column takes its point's fill");
|
||||
assert!(!red(200) && !red(760), "the others keep the series colour");
|
||||
|
||||
// A smoothed line through a peak overshoots no point but leaves the straight chord.
|
||||
let pts = [kurbo::Point::new(0.0, 0.0), kurbo::Point::new(10.0, 10.0), kurbo::Point::new(20.0, 0.0)];
|
||||
let p = crate::chart::smooth_path(&pts);
|
||||
assert_eq!(p.elements().len(), 3, "a cubic per segment");
|
||||
let mid = kurbo::ParamCurve::eval(&p.segments().next().unwrap(), 0.5);
|
||||
assert!(mid.y > mid.x, "the first half bows towards the peak instead of following the chord: {mid:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_prompts_only_in_edit_view() {
|
||||
let p = Presentation::default();
|
||||
|
||||
Reference in New Issue
Block a user