mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Merge PR #1146: Charts: bars draw their series' pattern fill and no fill
Conflicts: ROADMAP.md (union of progress-log rows)
This commit is contained in:
@@ -146,6 +146,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-22 | Shape Fill and Shape Outline format the members of a selected group, including nested groups, in one undo step. Fill transparency retains each member's effective colour (#1079) |
|
||||
| 2026-10-21 | 3-D contours, extrusion depths and bevels keep their size in points inside a group whose child coordinates scale its shapes: a 1 pt contour on a small extruded cube in a legacy group drew hundreds of points wide, a band across the slide |
|
||||
| 2026-10-20 | Chart number formats with explicit empty positive, negative or zero sections hide the corresponding labels, including all values with three semicolons; a blank whole format still uses General. Synthetic formatter regressions |
|
||||
| 2026-10-22 | Chart bars and their legend keys draw the series' own pattern fill (`a:pattFill`, now read and written) and draw nothing for `a:noFill`, with the series outline where it has one, as PowerPoint for Mac does (synthetic probe); they took the palette's colour before. Lines, areas and markers use a pattern's foreground colour |
|
||||
| 2026-10-18 | Native chart sidecar bytes move from JSON arrays into referenced ZIP entries, with shared media byte/item limits and bounded reads; older inline sidecars still load. Synthetic format regressions cover large workbooks and malformed members |
|
||||
| 2026-10-18 | Slide thumbnails and the Slide Sorter render only the slides in view: on a deck of more than 600 slides they used to evict their own thumbnails, so the pane flashed white and never finished (#969) |
|
||||
| 2026-10-18 | Resizing with the mouse snaps the edge being dragged to the slide, guides and other shapes: a fixed edge that already lined up used to take the snap and leave the dragged edge free, and a fixed edge near a line shifted the dragged one (#961) |
|
||||
@@ -276,6 +277,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-16 | trivial | Progress log: responsive mouse-wheel navigation on the fitted canvas |
|
||||
| 2026-10-21 | trivial | Progress log: theme `spDef` anchor no longer applied to a file's own shapes |
|
||||
| 2026-10-21 | trivial | Progress log: 3-D sizes in scaled groups stay in points |
|
||||
| 2026-10-22 | trivial | Progress log: chart series pattern and no fills |
|
||||
| 2026-10-14 | trivial | remote MCP checks reply IDs, envelopes and bounded newline framing (#718) |
|
||||
| 2026-10-14 | minor | Gray list markers on empty paragraphs while editing; caret keeps the list indent before typing |
|
||||
| 2026-10-14 | minor | Community PR wave folded: headline breadth 84% over 191 features (P0 92%, P1 82%, P2 55%) and the feature-area breadths from `cargo xtask parity`; one row per area; Upcoming item 2 merged; per-PR count notes dropped from the progress log. Ready stays ~55%, stage alpha |
|
||||
|
||||
@@ -5,7 +5,7 @@ use deckcraft_model::style::Fill;
|
||||
|
||||
use crate::xml::El;
|
||||
|
||||
/// Fills of chart parts never reference images we keep; read colour fills only.
|
||||
/// Fills of chart parts never reference images we keep; read colour and pattern fills only.
|
||||
fn simple_fill(sp: Option<&El>) -> Option<Fill> {
|
||||
let sp = sp?;
|
||||
for e in sp.elements() {
|
||||
@@ -13,6 +13,7 @@ fn simple_fill(sp: Option<&El>) -> Option<Fill> {
|
||||
"noFill" => return Some(Fill::None),
|
||||
"solidFill" => return super::dml::color(e).map(|color| Fill::Solid { color }),
|
||||
"gradFill" => return Some(Fill::Gradient(super::dml::gradient(e))),
|
||||
"pattFill" => return Some(Fill::Pattern(super::dml::pattern(e))),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -557,6 +558,29 @@ mod tests {
|
||||
assert!(!read_chart_xml(&crate::write::chart::chart_xml(&c).0).unwrap().minor_gridlines);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn series_pattern_fills_are_read_and_written_back() {
|
||||
// A pattern-filled series and an empty one with an outline, as PowerPoint writes them.
|
||||
let xml = br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><c:chart><c:plotArea><c:barChart><c:barDir val="col"/><c:ser><c:idx val="0"/><c:spPr><a:pattFill prst="openDmnd"><a:fgClr><a:schemeClr val="accent4"/></a:fgClr><a:bgClr><a:schemeClr val="bg1"/></a:bgClr></a:pattFill></c:spPr><c:val><c:numLit><c:ptCount val="1"/><c:pt idx="0"><c:v>5</c:v></c:pt></c:numLit></c:val></c:ser><c:ser><c:idx val="1"/><c:spPr><a:noFill/><a:ln w="19050"><a:solidFill><a:schemeClr val="accent4"/></a:solidFill></a:ln></c:spPr><c:val><c:numLit><c:ptCount val="1"/><c:pt idx="0"><c:v>3</c:v></c:pt></c:numLit></c:val></c:ser></c:barChart></c:plotArea></c:chart></c:chartSpace>"#;
|
||||
let c = read_chart_xml(xml).unwrap();
|
||||
let accent4 = deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Accent4);
|
||||
let pattern = deckcraft_model::style::PatternFill {
|
||||
preset: "openDmnd".into(),
|
||||
fg: accent4.clone(),
|
||||
bg: deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Bg1),
|
||||
};
|
||||
assert_eq!(c.series[0].fill, Some(Fill::Pattern(pattern.clone())));
|
||||
assert_eq!(c.series[1].fill, Some(Fill::None));
|
||||
assert_eq!(c.series[1].line.as_ref().and_then(|l| l.fill.clone()), Some(Fill::solid(accent4)));
|
||||
// Written out from the model (an edited chart), the pattern survives.
|
||||
let mut edited = c.clone();
|
||||
edited.raw = None;
|
||||
edited.title = Some("Edited".into());
|
||||
let back = read_chart_xml(&crate::write::chart::chart_xml(&edited).0).unwrap();
|
||||
assert_eq!(back.series[0].fill, Some(Fill::Pattern(pattern)));
|
||||
assert_eq!(back.series[1].fill, Some(Fill::None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_base_is_read() {
|
||||
let xml = br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea><c:lineChart><c:axId val="1"/><c:axId val="2"/></c:lineChart><c:catAx><c:axId val="1"/></c:catAx><c:valAx><c:axId val="2"/><c:scaling><c:logBase val="10"/><c:orientation val="minMax"/></c:scaling></c:valAx></c:plotArea></c:chart></c:chartSpace>"#;
|
||||
|
||||
@@ -86,16 +86,21 @@ pub fn fill_el(imp: &mut Imp, part: &Part, f: &El) -> Option<Fill> {
|
||||
Some(p) => Fill::Picture(p),
|
||||
None => Fill::None,
|
||||
},
|
||||
"pattFill" => Fill::Pattern(PatternFill {
|
||||
preset: f.attr("prst").unwrap_or("pct5").to_string(),
|
||||
fg: f.child("fgClr").and_then(color).unwrap_or(ColorRef::rgb(Rgba::BLACK)),
|
||||
bg: f.child("bgClr").and_then(color).unwrap_or(ColorRef::rgb(Rgba::WHITE)),
|
||||
}),
|
||||
"pattFill" => Fill::Pattern(pattern(f)),
|
||||
"grpFill" => Fill::Group,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// An `a:pattFill`: its preset (5 % dots when unset), foreground (black) and background (white).
|
||||
pub fn pattern(f: &El) -> PatternFill {
|
||||
PatternFill {
|
||||
preset: f.attr("prst").unwrap_or("pct5").to_string(),
|
||||
fg: f.child("fgClr").and_then(color).unwrap_or(ColorRef::rgb(Rgba::BLACK)),
|
||||
bg: f.child("bgClr").and_then(color).unwrap_or(ColorRef::rgb(Rgba::WHITE)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gradient(f: &El) -> Gradient {
|
||||
let mut stops: Vec<GradientStop> = f
|
||||
.child("gsLst")
|
||||
|
||||
@@ -30,7 +30,17 @@ fn simple_fill(w: &mut W, f: &Fill) {
|
||||
match f {
|
||||
Fill::None => w.empty0("a:noFill"),
|
||||
Fill::Solid { color } => super::dml::solid(w, color),
|
||||
Fill::Gradient(_) | Fill::Picture(_) | Fill::Pattern(_) | Fill::Group | Fill::Background => {
|
||||
Fill::Pattern(p) => {
|
||||
w.open("a:pattFill", A::new().a("prst", if p.preset.is_empty() { "pct5" } else { &p.preset }));
|
||||
w.open0("a:fgClr");
|
||||
super::dml::color(w, &p.fg);
|
||||
w.close("a:fgClr");
|
||||
w.open0("a:bgClr");
|
||||
super::dml::color(w, &p.bg);
|
||||
w.close("a:bgClr");
|
||||
w.close("a:pattFill");
|
||||
}
|
||||
Fill::Gradient(_) | Fill::Picture(_) | Fill::Group | Fill::Background => {
|
||||
if let Fill::Gradient(g) = f
|
||||
&& let Some(s) = g.stops.first()
|
||||
{
|
||||
|
||||
+88
-28
@@ -29,31 +29,70 @@ pub fn series_color(ctx: &Ctx, c: &Chart, i: usize) -> Rgba {
|
||||
}
|
||||
}
|
||||
|
||||
/// A series' one colour (lines, areas, markers): its solid fill, a pattern's foreground, else the
|
||||
/// palette's.
|
||||
pub(crate) fn series_fill(ctx: &Ctx, c: &Chart, i: usize, s: Option<&deckcraft_model::chart::Series>) -> Rgba {
|
||||
match s.and_then(|s| s.fill.as_ref()) {
|
||||
Some(Fill::Solid { color: cc }) => ctx.color(cc, None),
|
||||
Some(Fill::Pattern(p)) => ctx.color(&p.fg, None),
|
||||
_ => series_color(ctx, c, i),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)),
|
||||
/// How a bar, or its legend key, is painted.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Paint {
|
||||
Color(Rgba),
|
||||
/// A preset pattern: name, foreground, background.
|
||||
Pattern(String, Rgba, Rgba),
|
||||
/// `a:noFill`: only the outline, if any, shows.
|
||||
Nothing,
|
||||
}
|
||||
|
||||
/// The paint of fill `f` as a bar or legend key draws it; the palette's `fallback` when the file
|
||||
/// gives none (or one a bar can't show).
|
||||
fn paint_of(ctx: &Ctx, f: Option<&Fill>, fallback: Rgba) -> Paint {
|
||||
match f {
|
||||
Some(Fill::Solid { color: cc }) => Paint::Color(ctx.color(cc, None)),
|
||||
Some(Fill::Pattern(p)) => Paint::Pattern(p.preset.clone(), ctx.color(&p.fg, None), ctx.color(&p.bg, None)),
|
||||
Some(Fill::None) => Paint::Nothing,
|
||||
_ => Paint::Color(fallback),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill of one bar: a `<c:dPt>` fill wins, and a single-series chart that varies colours by point uses one accent per point
|
||||
/// (not waterfalls, which are never read from a `<c:barChart>`).
|
||||
fn bar_fill(ctx: &Ctx, c: &Chart, si: usize, pi: usize, s: &deckcraft_model::chart::Series) -> Rgba {
|
||||
if s.point_fills.iter().any(|(k, _)| *k == pi) {
|
||||
return point_fill(ctx, c, si, s, pi);
|
||||
/// Fill `r` (in the space `m` maps to the output) with `p`.
|
||||
fn fill_with(ctx: &mut RenderContext, p: &Paint, r: Rect, m: Affine) {
|
||||
match p {
|
||||
Paint::Color(c) => {
|
||||
ctx.set_transform(m);
|
||||
ctx.set_paint(color(*c, 1.0));
|
||||
ctx.fill_rect(&r);
|
||||
}
|
||||
Paint::Pattern(name, fg, bg) => crate::pattern::fill(ctx, name, *fg, *bg, 1.0, &r.to_path(0.1), m),
|
||||
Paint::Nothing => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// A bar series' outline (`c:spPr/a:ln`) where the file gives it a solid colour: colour and width.
|
||||
fn bar_outline(ctx: &Ctx, s: &deckcraft_model::chart::Series) -> Option<(Rgba, f64)> {
|
||||
let l = s.line.as_ref()?;
|
||||
match l.fill.as_ref()? {
|
||||
Fill::Solid { color: cc } => Some((ctx.color(cc, None), l.width.unwrap_or(0.75).clamp(0.0, 20.0))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint of one bar: a `<c:dPt>` fill wins, and a single-series chart that varies colours by point uses one accent per point
|
||||
/// (not waterfalls, which are never read from a `<c:barChart>`). Pattern fills draw as patterns and
|
||||
/// `a:noFill` leaves the bar empty, as in PowerPoint.
|
||||
fn bar_paint(ctx: &Ctx, c: &Chart, si: usize, pi: usize, s: &deckcraft_model::chart::Series) -> Paint {
|
||||
if let Some((_, f)) = s.point_fills.iter().find(|(k, _)| *k == pi) {
|
||||
return paint_of(ctx, Some(f), series_fill(ctx, c, si, Some(s)));
|
||||
}
|
||||
if c.vary_colors && c.series.len() == 1 && s.fill.is_none() && c.kind != ChartType::Waterfall {
|
||||
return series_color(ctx, c, pi);
|
||||
return Paint::Color(series_color(ctx, c, pi));
|
||||
}
|
||||
series_fill(ctx, c, si, Some(s))
|
||||
paint_of(ctx, s.fill.as_ref(), series_color(ctx, c, si))
|
||||
}
|
||||
|
||||
fn polyline(pts: &[Point]) -> BezPath {
|
||||
@@ -235,16 +274,17 @@ fn draw_inner(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64,
|
||||
let pie = matches!(c.kind, ChartType::Pie | ChartType::Doughnut);
|
||||
// Legend placed by `c:legendPos`.
|
||||
let waterfall = c.kind == ChartType::Waterfall;
|
||||
let legend_items: Vec<(String, Rgba)> = if waterfall {
|
||||
["Increase", "Decrease", "Total"].iter().enumerate().map(|(i, n)| (n.to_string(), series_color(rctx, c, i))).collect()
|
||||
// Each legend entry: its name, its key's paint and the key's outline.
|
||||
let legend_items: Vec<(String, Paint, Option<(Rgba, f64)>)> = if waterfall {
|
||||
["Increase", "Decrease", "Total"].iter().enumerate().map(|(i, n)| (n.to_string(), Paint::Color(series_color(rctx, c, i)), None)).collect()
|
||||
} else if matches!(c.kind, ChartType::Treemap | ChartType::Sunburst) {
|
||||
// A hierarchy's legend names its top-level groups.
|
||||
c.category_tree().iter().enumerate().map(|(i, n)| (n.name.clone(), series_color(rctx, c, i))).collect()
|
||||
c.category_tree().iter().enumerate().map(|(i, n)| (n.name.clone(), Paint::Color(series_color(rctx, c, i)), None)).collect()
|
||||
} else if matches!(c.kind, ChartType::Funnel | ChartType::RegionMap) {
|
||||
// A map draws its own colour scale.
|
||||
vec![]
|
||||
} else if pie {
|
||||
c.categories.iter().enumerate().map(|(i, n)| (n.clone(), series_color(rctx, c, i))).collect()
|
||||
c.categories.iter().enumerate().map(|(i, n)| (n.clone(), Paint::Color(series_color(rctx, c, i)), None)).collect()
|
||||
} else {
|
||||
c.series
|
||||
.iter()
|
||||
@@ -252,13 +292,24 @@ fn draw_inner(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64,
|
||||
.map(|(i, s)| {
|
||||
let k = c.series_kind(s);
|
||||
let line = !k.is_bar_like() && !matches!(k, ChartType::Area | ChartType::StackedArea | ChartType::FilledRadar);
|
||||
(s.name.clone(), if line { series_line(rctx, c, i, s) } else { series_fill(rctx, c, i, Some(s)) })
|
||||
if line {
|
||||
(s.name.clone(), Paint::Color(series_line(rctx, c, i, s)), None)
|
||||
} else if k.is_bar_like() {
|
||||
let paint = if c.vary_colors && c.series.len() == 1 && s.fill.is_none() {
|
||||
Paint::Color(series_color(rctx, c, i))
|
||||
} else {
|
||||
paint_of(rctx, s.fill.as_ref(), series_color(rctx, c, i))
|
||||
};
|
||||
(s.name.clone(), paint, bar_outline(rctx, s))
|
||||
} else {
|
||||
(s.name.clone(), Paint::Color(series_fill(rctx, c, i, Some(s))), None)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
if let (Some(pos), false) = (&c.legend, legend_items.is_empty()) {
|
||||
let vertical = matches!(pos.as_str(), "l" | "r" | "tr");
|
||||
let item_ws: Vec<f64> = legend_items.iter().map(|(n, _)| n.chars().count() as f64 * base * 0.55 + base * 1.6).collect();
|
||||
let item_ws: Vec<f64> = legend_items.iter().map(|(n, _, _)| n.chars().count() as f64 * base * 0.55 + base * 1.6).collect();
|
||||
let size = if vertical {
|
||||
(item_ws.iter().copied().fold(0.0, f64::max).min(w * 0.4), legend_items.len() as f64 * base * 1.5)
|
||||
} else {
|
||||
@@ -267,10 +318,15 @@ fn draw_inner(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64,
|
||||
let (origin, plot_area) = legend_layout(pos, size, area, base, c.legend_overlay);
|
||||
area = plot_area;
|
||||
let (mut x, mut y) = (origin.x, origin.y);
|
||||
for (name, col) in &legend_items {
|
||||
ctx.set_transform(m);
|
||||
ctx.set_paint(color(*col, 1.0));
|
||||
ctx.fill_rect(&Rect::new(x, y + base * 0.3, x + base * 0.7, y + base));
|
||||
for (name, paint, outline) in &legend_items {
|
||||
let key = Rect::new(x, y + base * 0.3, x + base * 0.7, y + base);
|
||||
fill_with(ctx, paint, key, m);
|
||||
if let Some((oc, ow)) = outline.filter(|o| o.1 > 0.0) {
|
||||
ctx.set_transform(m);
|
||||
ctx.set_paint(color(oc, 1.0));
|
||||
ctx.set_stroke(kurbo::Stroke::new(ow.min(base * 0.2)));
|
||||
ctx.stroke_path(&key.to_path(0.1));
|
||||
}
|
||||
let tw = name.chars().count() as f64 * base * 0.55 + base;
|
||||
label(ctx, rctx, m, name, Rect::new(x + base, y, x + base + tw, y + base * 1.3), base, Align::Left, Anchor::Middle, text_col, false);
|
||||
if vertical {
|
||||
@@ -625,7 +681,7 @@ fn draw_inner(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64,
|
||||
.abs();
|
||||
// A waterfall colours increases, decreases and totals, and joins each bar's end
|
||||
// to the next bar's start.
|
||||
let fill = if waterfall {
|
||||
let paint = if waterfall {
|
||||
let slot = if c.subtotals.contains(&i) {
|
||||
2
|
||||
} else if b < a {
|
||||
@@ -633,13 +689,17 @@ fn draw_inner(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64,
|
||||
} else {
|
||||
0
|
||||
};
|
||||
series_color(rctx, c, slot)
|
||||
Paint::Color(series_color(rctx, c, slot))
|
||||
} else {
|
||||
bar_fill(rctx, c, *si, i, s)
|
||||
bar_paint(rctx, c, *si, i, s)
|
||||
};
|
||||
ctx.set_transform(m);
|
||||
ctx.set_paint(color(fill, 1.0));
|
||||
ctx.fill_rect(&r.abs());
|
||||
fill_with(ctx, &paint, r.abs(), m);
|
||||
if !waterfall && let Some((oc, ow)) = bar_outline(rctx, s).filter(|o| o.1 > 0.0) {
|
||||
ctx.set_transform(m);
|
||||
ctx.set_paint(color(oc, 1.0));
|
||||
ctx.set_stroke(kurbo::Stroke::new(ow));
|
||||
ctx.stroke_path(&r.abs().to_path(0.1));
|
||||
}
|
||||
if waterfall && i + 1 < ncat && !horiz {
|
||||
let y = to_v(b);
|
||||
let mut j = BezPath::new();
|
||||
|
||||
@@ -340,6 +340,43 @@ fn chart_axis_titles_are_drawn() {
|
||||
assert!(ink(&titled, bottom) > ink(&bare, bottom) + 30, "category axis title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_series_draw_their_pattern_fill_and_no_fill_with_their_outline() {
|
||||
// Three column series, as PowerPoint draws them: a red-on-yellow pattern, no fill with a blue
|
||||
// outline, and no fill at all. None takes a palette colour; the legend keys match the bars.
|
||||
use deckcraft_model::style::{Fill, PatternFill};
|
||||
let rgb = |r, g, b| ColorRef::rgb(deckcraft_color::Rgba::rgb(r, g, b));
|
||||
let mut c = Chart::sample(ChartType::Column);
|
||||
c.title = None;
|
||||
c.series.truncate(3);
|
||||
c.categories = vec!["A".into()];
|
||||
for s in &mut c.series {
|
||||
s.values = vec![Some(10.0)];
|
||||
}
|
||||
c.series[0].fill = Some(Fill::Pattern(PatternFill { preset: "dkDnDiag".into(), fg: rgb(255, 0, 0), bg: rgb(255, 255, 0) }));
|
||||
c.series[1].fill = Some(Fill::None);
|
||||
c.series[1].line = Some(deckcraft_model::style::Line { width: Some(3.0), fill: Some(Fill::solid(rgb(0, 0, 255))), ..Default::default() });
|
||||
c.series[2].fill = Some(Fill::None);
|
||||
let sh =
|
||||
Shape { id: ShapeId(625), xfrm: Some(Xfrm::new(0.0, 0.0, 960.0, 540.0)), kind: ShapeKind::Chart(Box::new(c.clone())), ..Default::default() };
|
||||
let img = render_slide(&deck_with(vec![sh]), 0, &RenderOpts { scale: 1.0, ..Default::default() });
|
||||
let count =
|
||||
|f: &dyn Fn([u8; 4]) -> bool| (0..img.width).flat_map(|x| (0..img.height).map(move |y| (x, y))).filter(|(x, y)| f(img.pixel(*x, *y))).count();
|
||||
let red = count(&|p| p[0] > 220 && p[1] < 40 && p[2] < 40);
|
||||
let yellow = count(&|p| p[0] > 220 && p[1] > 220 && p[2] < 40);
|
||||
let blue = count(&|p| p[0] < 40 && p[1] < 40 && p[2] > 220);
|
||||
assert!(red > 1000 && yellow > 1000, "the pattern's two colours: {red} red, {yellow} yellow");
|
||||
assert!(blue > 300, "the empty series' outline: {blue}");
|
||||
// No palette colour: the theme's accents would show for any series drawn the old way.
|
||||
let p = deck_with(vec![]);
|
||||
let ctx = deckcraft_model::resolve::Ctx::for_slide(&p, &p.slides[0]).unwrap();
|
||||
for i in 0..3 {
|
||||
let a = crate::chart::series_color(&ctx, &c, i);
|
||||
let n = count(&|q| q[0].abs_diff(a.r) < 6 && q[1].abs_diff(a.g) < 6 && q[2].abs_diff(a.b) < 6);
|
||||
assert!(n < 50, "palette colour {i} drawn on {n} pixels");
|
||||
}
|
||||
}
|
||||
|
||||
#[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.
|
||||
|
||||
Reference in New Issue
Block a user