diff --git a/crates/chart/src/render.rs b/crates/chart/src/render.rs index 76a1619e3..6a64558a1 100644 --- a/crates/chart/src/render.rs +++ b/crates/chart/src/render.rs @@ -1,9 +1,8 @@ //! Chart layout: title, legend, axes and every chart kind, drawn into [`Prim`]s. -use std::collections::HashSet; use std::f32::consts::{PI, TAU}; -use gridcraft_model::{Chart, ChartKind, LegendPos, LineDash, QuartileMethod, Theme, TrendlineKind}; +use gridcraft_model::{Chart, ChartKind, LineDash, QuartileMethod, Theme, TrendlineKind}; use crate::axis::{Fmt, Scale, clean, fixed_scale, nice_scale}; use crate::resolve::{MAX_POINTS, MAX_SERIES, MAX_TRENDLINES, series_color}; @@ -70,8 +69,7 @@ impl Ctx<'_> { } String::new() } - #[allow(clippy::too_many_arguments)] - fn text(&mut self, x: f32, y: f32, s: &str, size: f32, color: Rgba, align: HAlign, valign: VAlign) { + fn text(&mut self, x: f32, y: f32, s: &str, size: f32, color: Rgba, (align, valign): (HAlign, VAlign)) { if s.is_empty() { return; } @@ -135,7 +133,7 @@ pub fn render(chart: &Chart, data: &ChartData, w: f32, h: f32, m: &dyn Measure) if let Some(t) = chart_title(chart, &data) { let size = TITLE_SIZE.min(h * 0.12).max(4.0); let t = ctx.fit(&t, size, area.w); - ctx.text(w / 2.0, area.y, &t, size, TEXT, HAlign::Center, VAlign::Top); + ctx.text(w / 2.0, area.y, &t, size, TEXT, (HAlign::Center, VAlign::Top)); let used = size * 1.3 + 4.0; area.y += used; area.h -= used; diff --git a/crates/chart/src/render/cartesian.rs b/crates/chart/src/render/cartesian.rs index 85a577b08..d449498e7 100644 --- a/crates/chart/src/render/cartesian.rs +++ b/crates/chart/src/render/cartesian.rs @@ -2,7 +2,10 @@ use super::frame::{Bx, CatAxis, ValAxis, frame, on, value_range}; use super::legend::{cat_label, plot_kind}; use super::lines::{decimate, draw_markers, draw_runs}; use super::trend::{draw_trend_labels, draw_trendlines, max_backward, max_forward, max_forward_slots, supports_trendline}; -use super::*; +use super::{ + Chart, ChartData, ChartKind, Ctx, Fmt, HAlign, LABEL_SIZE, MAX_CATS, MAX_LABELS, MAX_LINE_PTS, MAX_MARKERS, Prim, Scale, SeriesData, TEXT, + VAlign, WHITE, clean, fixed_scale, nice_scale, +}; // ---------------------------------------------------------------- cartesian (column/bar/line/area/combo) @@ -18,6 +21,9 @@ enum Fam { Area100, } +/// Stack accumulator: (axis, family) key with positive and negative running sums. +type Stack = ((bool, Fam), Vec, Vec); + fn family(k: ChartKind) -> Fam { use ChartKind::*; match k { @@ -45,16 +51,7 @@ fn is_line(f: Fam) -> bool { matches!(f, Fam::Line | Fam::LineStack) } -pub(super) fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx, horizontal: bool) { - let ncat = data.categories.len().max(data.series.iter().map(|s| s.values.len()).max().unwrap_or(0)).min(MAX_CATS); - if ncat == 0 { - return; - } - let kinds: Vec = data.series.iter().map(|s| if horizontal { chart.kind } else { plot_kind(chart, s) }).collect(); - let fams: Vec = kinds.iter().map(|k| family(*k)).collect(); - let any_primary = data.series.iter().any(|s| !s.secondary); - let sec: Vec = data.series.iter().map(|s| s.secondary && any_primary && !horizontal).collect(); - +fn series_spans(data: &ChartData, fams: &[Fam], sec: &[bool], ncat: usize) -> Vec>> { // Totals for 100% stacking, per axis and family (100% columns and 100% areas stack apart). let mut totals: Vec<((bool, Fam), Vec)> = Vec::new(); for (i, s) in data.series.iter().enumerate() { @@ -81,7 +78,7 @@ pub(super) fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx if t > 0.0 { v / t } else { 0.0 } }; // Stack accumulators keyed by (axis, family): positive and negative running sums. - let mut stacks: Vec<((bool, Fam), Vec, Vec)> = Vec::new(); + let mut stacks: Vec = Vec::new(); let mut spans: Vec>> = Vec::with_capacity(data.series.len()); for (i, s) in data.series.iter().enumerate() { let fam = fams.get(i).copied().unwrap_or(Fam::Col); @@ -126,6 +123,10 @@ pub(super) fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx spans.push(row); } + spans +} + +fn axis_scales(data: &ChartData, fams: &[Fam], sec: &[bool], spans: &[Vec>]) -> Vec<(Scale, Fmt)> { // Axis scales. let mut axes: Vec<(Scale, Fmt)> = Vec::new(); for axis in [false, true] { @@ -151,6 +152,46 @@ pub(super) fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx break; } } + axes +} + +fn column_slots(fams: &[Fam], sec: &[bool]) -> (Vec, f32) { + // Column slots: each clustered series gets one, each stack group shares one. + let mut slot_keys: Vec<(bool, Fam, usize)> = Vec::new(); + let mut slot_of = vec![0usize; fams.len()]; + for (i, f) in fams.iter().enumerate() { + if !is_col(*f) { + continue; + } + let a = sec.get(i).copied().unwrap_or(false); + let key = if *f == Fam::Col { (a, *f, i) } else { (a, *f, usize::MAX) }; + let k = match slot_keys.iter().position(|k| *k == key) { + Some(p) => p, + None => { + slot_keys.push(key); + slot_keys.len() - 1 + } + }; + if let Some(s) = slot_of.get_mut(i) { + *s = k; + } + } + (slot_of, slot_keys.len().max(1) as f32) +} + +pub(super) fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx, horizontal: bool) { + let ncat = data.categories.len().max(data.series.iter().map(|s| s.values.len()).max().unwrap_or(0)).min(MAX_CATS); + if ncat == 0 { + return; + } + let kinds: Vec = data.series.iter().map(|s| if horizontal { chart.kind } else { plot_kind(chart, s) }).collect(); + let fams: Vec = kinds.iter().map(|k| family(*k)).collect(); + let any_primary = data.series.iter().any(|s| !s.secondary); + let sec: Vec = data.series.iter().map(|s| s.secondary && any_primary && !horizontal).collect(); + + let spans = series_spans(data, &fams, &sec, ncat); + + let axes = axis_scales(data, &fams, &sec, &spans); let Some((s1, f1)) = axes.first() else { return }; let y1 = ValAxis { scale: *s1, fmt: f1 }; let y2 = axes.get(1).map(|(s, f)| ValAxis { scale: *s, fmt: f }); @@ -177,27 +218,7 @@ pub(super) fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx let center = |j: usize| if horizontal { plot.bottom() - (j as f32 + 0.5) * cw } else { plot.x + (j as f32 + 0.5) * cw }; let vpos = |v: f64, sc: &Scale| if horizontal { plot.x + sc.frac(v) * plot.w } else { plot.bottom() - sc.frac(v) * plot.h }; - // Column slots: each clustered series gets one, each stack group shares one. - let mut slot_keys: Vec<(bool, Fam, usize)> = Vec::new(); - let mut slot_of = vec![0usize; data.series.len()]; - for (i, f) in fams.iter().enumerate() { - if !is_col(*f) { - continue; - } - let a = sec.get(i).copied().unwrap_or(false); - let key = if *f == Fam::Col { (a, *f, i) } else { (a, *f, usize::MAX) }; - let k = match slot_keys.iter().position(|k| *k == key) { - Some(p) => p, - None => { - slot_keys.push(key); - slot_keys.len() - 1 - } - }; - if let Some(s) = slot_of.get_mut(i) { - *s = k; - } - } - let nslots = slot_keys.len().max(1) as f32; + let (slot_of, nslots) = column_slots(&fams, &sec); let bw = cw / (nslots + 1.5); let labels_on = chart.data_labels && ncat <= MAX_LABELS; // Area labels wait until every area is filled, so a later (stacked) area can't paint over them. @@ -275,20 +296,20 @@ pub(super) fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx if horizontal { let y = c + nslots * bw / 2.0 - (k + 0.5) * bw; if stacked { - ctx.text(mid, y, &t, LABEL_SIZE, on(s.color), HAlign::Center, VAlign::Middle); + ctx.text(mid, y, &t, LABEL_SIZE, on(s.color), (HAlign::Center, VAlign::Middle)); } else if v >= 0.0 { - ctx.text(pb.max(pt) + 3.0, y, &t, LABEL_SIZE, TEXT, HAlign::Left, VAlign::Middle); + ctx.text(pb.max(pt) + 3.0, y, &t, LABEL_SIZE, TEXT, (HAlign::Left, VAlign::Middle)); } else { - ctx.text(lo - 3.0, y, &t, LABEL_SIZE, TEXT, HAlign::Right, VAlign::Middle); + ctx.text(lo - 3.0, y, &t, LABEL_SIZE, TEXT, (HAlign::Right, VAlign::Middle)); } } else { let x = c - nslots * bw / 2.0 + (k + 0.5) * bw; if stacked { - ctx.text(x, mid, &t, LABEL_SIZE, on(s.color), HAlign::Center, VAlign::Middle); + ctx.text(x, mid, &t, LABEL_SIZE, on(s.color), (HAlign::Center, VAlign::Middle)); } else if v >= 0.0 { - ctx.text(x, lo - 2.0, &t, LABEL_SIZE, TEXT, HAlign::Center, VAlign::Bottom); + ctx.text(x, lo - 2.0, &t, LABEL_SIZE, TEXT, (HAlign::Center, VAlign::Bottom)); } else { - ctx.text(x, lo + len + 2.0, &t, LABEL_SIZE, TEXT, HAlign::Center, VAlign::Top); + ctx.text(x, lo + len + 2.0, &t, LABEL_SIZE, TEXT, (HAlign::Center, VAlign::Top)); } } } @@ -304,7 +325,7 @@ pub(super) fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx if labels_on { for (j, p) in pts.iter().enumerate() { if let (Some(p), Some(v)) = (p, s.values.get(j).copied().flatten()) { - ctx.text(p[0], p[1] - 5.0, &fmt.format(v, None), LABEL_SIZE, TEXT, HAlign::Center, VAlign::Bottom); + ctx.text(p[0], p[1] - 5.0, &fmt.format(v, None), LABEL_SIZE, TEXT, (HAlign::Center, VAlign::Bottom)); } } } @@ -312,7 +333,7 @@ pub(super) fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx } } for (x, y, t) in area_labels.drain(..) { - ctx.text(x, y, &t, LABEL_SIZE, TEXT, HAlign::Center, VAlign::Bottom); + ctx.text(x, y, &t, LABEL_SIZE, TEXT, (HAlign::Center, VAlign::Bottom)); } } // Trendlines on top: x is the category number 1..n. @@ -412,7 +433,7 @@ pub(super) fn scatter(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx) if chart.data_labels && pts.len() <= MAX_LABELS { for (p, raw) in px.iter().zip(pts) { if let (Some(p), Some((_, y))) = (p, raw) { - ctx.text(p[0], p[1] - 6.0, &fy.format(*y, None), LABEL_SIZE, TEXT, HAlign::Center, VAlign::Bottom); + ctx.text(p[0], p[1] - 6.0, &fy.format(*y, None), LABEL_SIZE, TEXT, (HAlign::Center, VAlign::Bottom)); } } } diff --git a/crates/chart/src/render/frame.rs b/crates/chart/src/render/frame.rs index 7fa2a54a1..0117423b3 100644 --- a/crates/chart/src/render/frame.rs +++ b/crates/chart/src/render/frame.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{AXIS, AXIS_TITLE_SIZE, Chart, ChartData, Ctx, DARK_TEXT, Fmt, GRID, HAlign, LABEL_SIZE, PI, Prim, Rgba, Scale, TEXT, VAlign, WHITE}; #[derive(Clone, Copy, Debug)] pub(super) struct Bx { @@ -44,7 +44,7 @@ pub(super) fn axis_titles(ctx: &mut Ctx, chart: &Chart, mut area: Bx) -> Bx { let lh = AXIS_TITLE_SIZE * 1.4 + 2.0; if let Some(t) = chart.x_title.as_deref().map(str::trim).filter(|t| !t.is_empty()) { let t = ctx.fit(t, AXIS_TITLE_SIZE, area.w); - ctx.text(area.cx(), area.bottom(), &t, AXIS_TITLE_SIZE, TEXT, HAlign::Center, VAlign::Bottom); + ctx.text(area.cx(), area.bottom(), &t, AXIS_TITLE_SIZE, TEXT, (HAlign::Center, VAlign::Bottom)); area.h -= lh; } if let Some(t) = chart.y_title.as_deref().map(str::trim).filter(|t| !t.is_empty()) { @@ -123,12 +123,12 @@ pub(super) fn frame(ctx: &mut Ctx, area: Bx, cat: CatAxis, y: &ValAxis, y2: Opti if grid { ctx.hline(plot.x, plot.right(), yy, GRID, 0.75); } - ctx.text(plot.x - 4.0, yy, t, ls, TEXT, HAlign::Right, VAlign::Middle); + ctx.text(plot.x - 4.0, yy, t, ls, TEXT, (HAlign::Right, VAlign::Middle)); } if let (Some(a), Some(l)) = (y2, &y2labels) { for (v, t) in l { let yy = plot.bottom() - a.scale.frac(*v) * plot.h; - ctx.text(plot.right() + 4.0, yy, t, ls, TEXT, HAlign::Left, VAlign::Middle); + ctx.text(plot.right() + 4.0, yy, t, ls, TEXT, (HAlign::Left, VAlign::Middle)); } } let zero = if y.scale.min <= 0.0 && y.scale.max >= 0.0 { 0.0 } else { y.scale.min }; @@ -142,7 +142,7 @@ pub(super) fn frame(ctx: &mut Ctx, area: Bx, cat: CatAxis, y: &ValAxis, y2: Opti let k = (((lw + 6.0) / cw.max(1e-3)).ceil() as usize).clamp(1, n); for (j, t) in l.iter().enumerate().step_by(k) { let t = ctx.fit(t, ls, cw * k as f32 - 2.0); - ctx.text(plot.x + (j as f32 + 0.5) * cw, plot.bottom() + 3.0, &t, ls, TEXT, HAlign::Center, VAlign::Top); + ctx.text(plot.x + (j as f32 + 0.5) * cw, plot.bottom() + 3.0, &t, ls, TEXT, (HAlign::Center, VAlign::Top)); } // Outer levels, one row each below the labels: a group's name centred under its // categories, and a separator at each group boundary down through the rows. @@ -152,7 +152,7 @@ pub(super) fn frame(ctx: &mut Ctx, area: Bx, cat: CatAxis, y: &ValAxis, y2: Opti let (x0, x1) = (plot.x + first as f32 * cw, plot.x + end as f32 * cw); if let Some(t) = level.get(first).filter(|t| !t.is_empty()) { let t = ctx.fit(t, ls, x1 - x0 - 2.0); - ctx.text((x0 + x1) / 2.0, top, &t, ls, TEXT, HAlign::Center, VAlign::Top); + ctx.text((x0 + x1) / 2.0, top, &t, ls, TEXT, (HAlign::Center, VAlign::Top)); } if first > 0 { ctx.vline(x0, plot.bottom(), top + lh - 2.0, AXIS, 0.75); @@ -169,7 +169,7 @@ pub(super) fn frame(ctx: &mut Ctx, area: Bx, cat: CatAxis, y: &ValAxis, y2: Opti let k = (((lw + 6.0) / slot.max(1e-3)).ceil() as usize).max(1); for (v, t) in ticks.iter().zip(&labels).step_by(k) { let xx = plot.x + s.frac(*v) * plot.w; - ctx.text(xx, plot.bottom() + 3.0, t, ls, TEXT, HAlign::Center, VAlign::Top); + ctx.text(xx, plot.bottom() + 3.0, t, ls, TEXT, (HAlign::Center, VAlign::Top)); } if s.min <= 0.0 && s.max >= 0.0 { let xx = plot.x + s.frac(0.0) * plot.w; @@ -202,7 +202,7 @@ pub(super) fn frame(ctx: &mut Ctx, area: Bx, cat: CatAxis, y: &ValAxis, y2: Opti ctx.vline(xx, plot.y, plot.bottom(), GRID, 0.75); } if i % k == 0 { - ctx.text(xx, plot.bottom() + 3.0, t, ls, TEXT, HAlign::Center, VAlign::Top); + ctx.text(xx, plot.bottom() + 3.0, t, ls, TEXT, (HAlign::Center, VAlign::Top)); } } let zero = if y.scale.min <= 0.0 && y.scale.max >= 0.0 { 0.0 } else { y.scale.min }; @@ -213,7 +213,7 @@ pub(super) fn frame(ctx: &mut Ctx, area: Bx, cat: CatAxis, y: &ValAxis, y2: Opti let k = ((lh / ch.max(1e-3)).ceil() as usize).clamp(1, n); for (j, t) in labels.iter().enumerate().step_by(k) { let t = ctx.fit(t, ls, lw - 6.0); - ctx.text(plot.x - 4.0, plot.bottom() - (j as f32 + 0.5) * ch, &t, ls, TEXT, HAlign::Right, VAlign::Middle); + ctx.text(plot.x - 4.0, plot.bottom() - (j as f32 + 0.5) * ch, &t, ls, TEXT, (HAlign::Right, VAlign::Middle)); } // Outer levels: a group's name centred beside its categories, separators at the // boundaries running right to the axis. @@ -224,7 +224,7 @@ pub(super) fn frame(ctx: &mut Ctx, area: Bx, cat: CatAxis, y: &ValAxis, y2: Opti let (y0, y1) = (plot.bottom() - end as f32 * ch, plot.bottom() - first as f32 * ch); if let Some(t) = level.get(first).filter(|t| !t.is_empty()) { let t = ctx.fit(t, ls, w - 4.0); - ctx.text((x0 + x1) / 2.0, (y0 + y1) / 2.0, &t, ls, TEXT, HAlign::Center, VAlign::Middle); + ctx.text((x0 + x1) / 2.0, (y0 + y1) / 2.0, &t, ls, TEXT, (HAlign::Center, VAlign::Middle)); } if first > 0 { ctx.hline(x0, plot.x, y1, AXIS, 0.75); diff --git a/crates/chart/src/render/kinds.rs b/crates/chart/src/render/kinds.rs index 2ffbfa3dd..54ce6bec8 100644 --- a/crates/chart/src/render/kinds.rs +++ b/crates/chart/src/render/kinds.rs @@ -1,6 +1,9 @@ use super::frame::{Bx, CatAxis, ValAxis, frame, on, value_range}; use super::legend::{cat_label, point_color}; -use super::*; +use super::{ + AXIS, Chart, ChartData, Ctx, DOWN, Fmt, GRID, HAlign, LABEL_SIZE, MAX_CATS, MAX_LABELS, MAX_MARKERS, Prim, QuartileMethod, STOCK, SeriesData, + TAU, TEXT, UP, VAlign, WHITE, clean, nice_scale, +}; // ---------------------------------------------------------------- histogram @@ -47,7 +50,7 @@ pub(super) fn histogram(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx let x = plot.x + b as f32 * cw; ctx.rect(x, top, cw, plot.bottom() - top, s.color, Some((WHITE, 0.75))); if chart.data_labels { - ctx.text(x + cw / 2.0, top - 2.0, &c.to_string(), LABEL_SIZE, TEXT, HAlign::Center, VAlign::Bottom); + ctx.text(x + cw / 2.0, top - 2.0, &c.to_string(), LABEL_SIZE, TEXT, (HAlign::Center, VAlign::Bottom)); } } } @@ -110,7 +113,7 @@ pub(super) fn waterfall(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx } if chart.data_labels && ncat <= MAX_LABELS { let v = if *kind == 0 { *t } else { t - b }; - ctx.text(x + bw / 2.0, yb.min(yt) - 2.0, &f.format(v, None), LABEL_SIZE, TEXT, HAlign::Center, VAlign::Bottom); + ctx.text(x + bw / 2.0, yb.min(yt) - 2.0, &f.format(v, None), LABEL_SIZE, TEXT, (HAlign::Center, VAlign::Bottom)); } } } @@ -150,10 +153,10 @@ pub(super) fn funnel(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx) { if chart.data_labels { let t = f.format(s.values.get(j).copied().flatten().unwrap_or(0.0), None); let color = if ctx.tw(&t, LABEL_SIZE) + 4.0 <= bw { on(s.color) } else { TEXT }; - ctx.text(plot.cx(), yc, &t, LABEL_SIZE, color, HAlign::Center, VAlign::Middle); + ctx.text(plot.cx(), yc, &t, LABEL_SIZE, color, (HAlign::Center, VAlign::Middle)); } let l = ctx.fit(labels.get(j).map(String::as_str).unwrap_or(""), LABEL_SIZE, lw - 8.0); - ctx.text(plot.x - 6.0, yc, &l, LABEL_SIZE, TEXT, HAlign::Right, VAlign::Middle); + ctx.text(plot.x - 6.0, yc, &l, LABEL_SIZE, TEXT, (HAlign::Right, VAlign::Middle)); } } } @@ -194,7 +197,7 @@ pub(super) fn pie(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx, inne a += sweep; } for (x, y, t, c) in labels { - ctx.text(x, y, &t, LABEL_SIZE, c, HAlign::Center, VAlign::Middle); + ctx.text(x, y, &t, LABEL_SIZE, c, (HAlign::Center, VAlign::Middle)); } } @@ -281,11 +284,11 @@ pub(super) fn treemap(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx) ctx.rect(b.x, b.y, b.w, b.h, fill, Some((WHITE, 1.5))); if b.w > 24.0 && b.h > LABEL_SIZE * 1.6 { let name = ctx.fit(&cat_label(data, j), LABEL_SIZE, b.w - 8.0); - ctx.text(b.x + 4.0, b.y + 4.0, &name, LABEL_SIZE, on(fill), HAlign::Left, VAlign::Top); + ctx.text(b.x + 4.0, b.y + 4.0, &name, LABEL_SIZE, on(fill), (HAlign::Left, VAlign::Top)); if chart.data_labels && b.h > LABEL_SIZE * 3.2 { let v = items.iter().find(|x| x.0 == j).map(|x| x.1).unwrap_or(0.0); let t = ctx.fit(&f.format(v, None), LABEL_SIZE, b.w - 8.0); - ctx.text(b.x + 4.0, b.y + 4.0 + LABEL_SIZE * 1.4, &t, LABEL_SIZE, on(fill), HAlign::Left, VAlign::Top); + ctx.text(b.x + 4.0, b.y + 4.0 + LABEL_SIZE * 1.4, &t, LABEL_SIZE, on(fill), (HAlign::Left, VAlign::Top)); } } } @@ -325,7 +328,7 @@ pub(super) fn radar(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx) { ctx.line(ring, GRID, 0.75); } if ti % every == 0 { - ctx.text(cx - 3.0, cy - rr, &f.format(t, Some(sc.step)), LABEL_SIZE * 0.9, TEXT, HAlign::Right, VAlign::Middle); + ctx.text(cx - 3.0, cy - rr, &f.format(t, Some(sc.step)), LABEL_SIZE * 0.9, TEXT, (HAlign::Right, VAlign::Middle)); } } let step = ncat.div_ceil(72).max(1); @@ -349,7 +352,7 @@ pub(super) fn radar(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx) { VAlign::Middle }; let t = ctx.fit(labels.get(j).map(String::as_str).unwrap_or(""), LABEL_SIZE, lw.max(20.0)); - ctx.text(p[0], p[1], &t, LABEL_SIZE, TEXT, align, valign); + ctx.text(p[0], p[1], &t, LABEL_SIZE, TEXT, (align, valign)); } for s in &data.series { let mut pts: Vec<[f32; 2]> = (0..ncat).map(|j| pt(ang(j), sc.frac(s.values.get(j).copied().flatten().unwrap_or(sc.min)) * r)).collect(); diff --git a/crates/chart/src/render/legend.rs b/crates/chart/src/render/legend.rs index 95cb39d48..fec1e4549 100644 --- a/crates/chart/src/render/legend.rs +++ b/crates/chart/src/render/legend.rs @@ -1,6 +1,10 @@ use super::frame::Bx; use super::trend::supports_trendline; -use super::*; +use super::{ + Chart, ChartData, ChartKind, Ctx, DOWN, HAlign, LABEL_SIZE, MAX_CATS, MAX_SERIES, MAX_TRENDLINES, Prim, Rgba, SeriesData, TEXT, Theme, UP, + VAlign, auto_name, series_color, +}; +use gridcraft_model::LegendPos; /// How a legend entry's swatch is drawn. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -132,7 +136,7 @@ pub(super) fn draw_legend(ctx: &mut Ctx, entries: &[(String, Rgba, Swatch)], pos draw_swatch(ctx, e.2, x, yc, s, e.1); let sx = swatch_w(e.2, s) + gap; let t = ctx.fit(&e.0, size, (iw - sx).max(0.0) + 0.5); - ctx.text(x + sx, yc, &t, size, TEXT, HAlign::Left, VAlign::Middle); + ctx.text(x + sx, yc, &t, size, TEXT, (HAlign::Left, VAlign::Middle)); } x += iw + spacing; } @@ -152,7 +156,7 @@ pub(super) fn draw_legend(ctx: &mut Ctx, entries: &[(String, Rgba, Swatch)], pos draw_swatch(ctx, e.2, x0, yc, s, e.1); let sx = swatch_w(e.2, s) + gap; let t = ctx.fit(&e.0, size, (colw - sx).max(0.0) + 0.5); - ctx.text(x0 + sx, yc, &t, size, TEXT, HAlign::Left, VAlign::Middle); + ctx.text(x0 + sx, yc, &t, size, TEXT, (HAlign::Left, VAlign::Middle)); } let used = colw + 8.0; if pos == LegendPos::Right { Bx { w: area.w - used, ..area } } else { Bx { x: area.x + used, w: area.w - used, ..area } } diff --git a/crates/chart/src/render/lines.rs b/crates/chart/src/render/lines.rs index 9cef4608c..ae0229cfb 100644 --- a/crates/chart/src/render/lines.rs +++ b/crates/chart/src/render/lines.rs @@ -1,5 +1,6 @@ use super::frame::Bx; -use super::*; +use super::{Ctx, MAX_LINE_PTS, MAX_MARKERS, Prim, Rgba, WHITE}; +use std::collections::HashSet; // ---------------------------------------------------------------- helpers diff --git a/crates/chart/src/render/trend.rs b/crates/chart/src/render/trend.rs index 38e435a47..b642df262 100644 --- a/crates/chart/src/render/trend.rs +++ b/crates/chart/src/render/trend.rs @@ -1,6 +1,9 @@ use super::frame::{Bx, value_range}; use super::lines::decimate; -use super::*; +use super::{ + ChartKind, Ctx, HAlign, LABEL_SIZE, LineDash, MAX_CATS, MAX_LINE_PTS, MAX_TRENDLINES, Prim, SeriesData, TEXT, TrendlineKind, VAlign, + fit_trendline, moving_average, +}; // ---------------------------------------------------------------- trendlines @@ -36,10 +39,11 @@ pub(super) fn max_backward(s: &SeriesData) -> f64 { s.trendlines.iter().filter(|t| !matches!(t.spec.kind, TrendlineKind::MovingAverage(_))).map(|t| forecast(t.spec.backward)).fold(0.0, f64::max) } +type ClippedSegment = ((f64, f64), (f64, f64), bool, bool); + /// Liang–Barsky: the part of segment `a`–`b` inside the unit square, with whether the start and /// end were cut. -#[allow(clippy::type_complexity)] -fn clip_segment(a: (f64, f64), b: (f64, f64)) -> Option<((f64, f64), (f64, f64), bool, bool)> { +fn clip_segment(a: (f64, f64), b: (f64, f64)) -> Option { let (dx, dy) = (b.0 - a.0, b.1 - a.1); let (mut t0, mut t1) = (0.0f64, 1.0f64); for (p, q) in [(-dx, a.0), (dx, 1.0 - a.0), (-dy, a.1), (dy, 1.0 - a.1)] { @@ -161,6 +165,6 @@ pub(super) fn draw_trend_labels(ctx: &mut Ctx, plot: Bx, labels: &[String]) { break; } let t = ctx.fit(t, LABEL_SIZE, plot.w - 8.0); - ctx.text(plot.right() - 4.0, y, &t, LABEL_SIZE, TEXT, HAlign::Right, VAlign::Top); + ctx.text(plot.right() - 4.0, y, &t, LABEL_SIZE, TEXT, (HAlign::Right, VAlign::Top)); } }