use std::cell::RefCell;
use densa_chart::{Axis, ChartPainter, ChartPalette, Plot, Point, Rect};
use densa_draw::{Canvas, Instance, InstanceBuffer};
use densa_text::{AtlasConfig, Font, GlyphCache};
const NAMES: [&str; 4] = ["north", "south", "east", "west"];
fn drawn(draw: impl FnOnce(&mut Canvas<'_>)) -> Vec<Instance> {
let mut cache = GlyphCache::new(AtlasConfig::DEFAULT);
let mut out = InstanceBuffer::with_capacity(100_000);
let mut canvas = Canvas::begin(&mut cache, &mut out);
draw(&mut canvas);
assert_eq!(out.dropped(), 0);
out.as_slice().to_vec()
}
fn painter() -> ChartPainter {
ChartPainter::new(Font::bundled_mono().unwrap(), 13.0, ChartPalette::new())
}
fn four(painter: &mut ChartPainter) -> Plot {
let area = painter.plot_area(Rect::new(0.0, 0.0, 460.0, 300.0), true);
Plot::new(area, Axis::category(-0.5, 3.5), Axis::number(0.0, 10.0))
}
fn below(instances: &[Instance], plot: &Plot) -> Vec<[f32; 4]> {
let mut glyphs: Vec<[f32; 4]> = instances
.iter()
.filter(|i| i.is_glyph() && i.rect[1] >= plot.area().y1)
.map(|i| i.rect)
.collect();
glyphs.sort_by(|a, b| a[0].total_cmp(&b[0]));
glyphs
}
fn around(glyphs: &[[f32; 4]], x: f32, half: f32) -> usize {
glyphs
.iter()
.filter(|g| g[0] < x + half && g[0] + g[2] > x - half)
.inspect(|g| assert!(g[0] >= x - half && g[0] + g[2] <= x + half, "{g:?} by {x}"))
.count()
}
#[test]
fn each_place_is_labelled_with_its_name_under_it() {
let mut painter = painter();
let plot = four(&mut painter);
let names = |i: usize| NAMES.get(i).copied();
let instances = drawn(|canvas| painter.paint_axes_named(canvas, &plot, true, &names));
let glyphs = below(&instances, &plot);
assert_eq!(glyphs.len(), 18, "every letter of every name: {glyphs:?}");
let gap = plot.x_px(1.0) - plot.x_px(0.0);
for (place, name) in NAMES.iter().enumerate() {
let x = plot.x_px(place as f64);
assert_eq!(
around(&glyphs, x, gap / 2.0),
name.len(),
"{name} centred at {x}"
);
}
let unlabelled = drawn(|canvas| painter.paint_axes_named(canvas, &plot, false, &names));
assert!(
below(&unlabelled, &plot).is_empty(),
"no x labels asked for"
);
let unnamed = drawn(|canvas| painter.paint_axes(canvas, &plot, true));
assert!(below(&unnamed, &plot).is_empty(), "no names given");
}
#[test]
fn a_name_longer_than_its_room_is_cut_short_of_the_next() {
let mut painter = painter();
let plot = four(&mut painter);
let long = "a category name far too long to fit";
let names = |i: usize| (i < 4).then_some(long);
let instances = drawn(|canvas| painter.paint_axes_named(canvas, &plot, true, &names));
let glyphs = below(&instances, &plot);
let gap = plot.x_px(1.0) - plot.x_px(0.0);
let letters = long.chars().filter(|c| *c != ' ').count();
for place in 0..4 {
let x = plot.x_px(f64::from(place));
let shown = around(&glyphs, x, gap / 2.0);
assert!(
shown > 3 && shown < letters,
"cut at place {place}: {shown}"
);
}
let shown = around(&glyphs, plot.x_px(0.0), gap / 2.0);
assert!(
glyphs.len() >= 4 * shown - 4,
"each place gets about the same room"
);
}
#[test]
fn names_are_asked_for_places_on_the_axis_only() {
let mut painter = painter();
let area = painter.plot_area(Rect::new(0.0, 0.0, 800.0, 300.0), true);
let plot = Plot::new(area, Axis::category(-3.5, 3.5), Axis::number(0.0, 10.0));
let asked = RefCell::new(Vec::new());
let names = |i: usize| {
asked.borrow_mut().push(i);
NAMES.get(i).copied()
};
let instances = drawn(|canvas| painter.paint_axes_named(canvas, &plot, true, &names));
assert_eq!(
*asked.borrow(),
[0, 1, 2, 3],
"not for the ticks left of zero"
);
assert_eq!(below(&instances, &plot).len(), 18);
}
#[test]
fn many_places_are_labelled_every_few() {
let mut painter = painter();
let area = painter.plot_area(Rect::new(0.0, 0.0, 460.0, 300.0), true);
let plot = Plot::new(area, Axis::category(-0.5, 99.5), Axis::number(0.0, 10.0));
let asked = RefCell::new(Vec::new());
let names = |i: usize| {
asked.borrow_mut().push(i);
Some("x")
};
drawn(|canvas| painter.paint_axes_named(canvas, &plot, true, &names));
let asked = asked.into_inner();
assert!(asked.len() >= 2 && asked.len() < 12, "{asked:?}");
assert!(asked.windows(2).all(|w| w[1] - w[0] == asked[1] - asked[0]));
}
#[test]
fn the_crosshair_names_the_nearest_place() {
let mut painter = painter();
let palette = *painter.palette();
let plot = four(&mut painter);
let names = |i: usize| NAMES.get(i).copied();
let x = plot.x_px(2.2);
let at = Point::new(x, plot.area().y0 + 20.0);
let instances = drawn(|canvas| painter.paint_crosshair_named(canvas, &plot, at, true, &names));
let marker = palette.marker.to_array();
let boxes: Vec<&Instance> = instances
.iter()
.filter(|i| i.color == marker && i.rect[1] > plot.area().y1)
.collect();
assert_eq!(boxes.len(), 1, "a box below the plot");
let text = palette.marker_text.to_array();
let written: Vec<&Instance> = instances
.iter()
.filter(|i| i.is_glyph() && i.color == text && i.rect[1] > plot.area().y1)
.collect();
assert_eq!(written.len(), 4, "east");
let [left, _, width, _] = boxes[0].rect;
assert!(written
.iter()
.all(|g| g.rect[0] >= left && g.rect[0] + g.rect[2] <= left + width));
let past = Point::new(plot.area().x1 - 1.0, at.y);
let nothing = drawn(|canvas| {
painter.paint_crosshair_named(canvas, &plot, past, true, &|i| (i < 3).then_some("x"))
});
assert!(nothing.iter().all(|i| i.rect[1] <= plot.area().y1));
let unnamed = drawn(|canvas| painter.paint_crosshair(canvas, &plot, at, true));
assert!(unnamed.iter().all(|i| i.rect[1] <= plot.area().y1));
}
#[test]
fn a_name_wider_than_the_plot_keeps_its_crosshair_box_inside() {
let mut painter = painter();
let area = painter.plot_area(Rect::new(0.0, 0.0, 160.0, 100.0), true);
let plot = Plot::new(area, Axis::category(-0.5, 0.5), Axis::number(0.0, 1.0));
let long = "a category name far too long to fit under this plot";
let at = Point::new(plot.x_px(0.0), area.y0 + 10.0);
let instances =
drawn(|canvas| painter.paint_crosshair_named(canvas, &plot, at, true, &|_| Some(long)));
let under: Vec<&Instance> = instances.iter().filter(|i| i.rect[1] > area.y1).collect();
assert!(!under.is_empty());
assert!(under
.iter()
.all(|i| i.rect[0] >= area.x0 && i.rect[0] + i.rect[2] <= area.x1 + 0.5));
}
#[test]
fn labels_near_the_edges_keep_out_of_their_neighbours_room() {
let mut painter = painter();
let area = painter.plot_area(Rect::new(0.0, 0.0, 460.0, 300.0), true);
let plot = Plot::new(area, Axis::category(0.0, 3.01), Axis::number(0.0, 10.0));
let long = "a category name far too long to fit";
let instances = drawn(|canvas| painter.paint_axes_named(canvas, &plot, true, &|_| Some(long)));
let glyphs = below(&instances, &plot);
let slot = plot.x_px(1.0) - plot.x_px(0.0);
for place in 0..4 {
let x = plot.x_px(f64::from(place));
let mine = glyphs
.iter()
.filter(|g| (g[0] + g[2] / 2.0 - x).abs() < slot / 2.0);
for g in mine {
assert!(g[0] >= area.x0 && g[0] + g[2] <= area.x1 + 0.5, "{g:?}");
assert!(
g[0] >= x - slot / 2.0 && g[0] + g[2] <= x + slot / 2.0,
"{g:?} by {x}"
);
}
}
assert!(
glyphs.len() > 4 * 2,
"the edge ones still write something: {}",
glyphs.len()
);
}
#[test]
fn a_cut_label_is_centred_under_its_place() {
let mut painter = painter();
let plot = four(&mut painter);
let long = "a category name far too long to fit";
let instances = drawn(|canvas| painter.paint_axes_named(canvas, &plot, true, &|_| Some(long)));
let glyphs = below(&instances, &plot);
let x = plot.x_px(1.0);
let half = (plot.x_px(1.0) - plot.x_px(0.0)) / 2.0;
let mine: Vec<_> = glyphs.iter().filter(|g| (g[0] - x).abs() < half).collect();
let left = mine.first().unwrap()[0];
let right = mine.last().map(|g| g[0] + g[2]).unwrap();
let centre = (left + right) / 2.0;
assert!((centre - x).abs() < 5.0, "{centre} under {x}");
}
#[test]
fn line_breaks_and_tabs_in_a_name_are_not_drawn() {
let mut painter = painter();
let plot = four(&mut painter);
let plain = drawn(|canvas| painter.paint_axes_named(canvas, &plot, true, &|_| Some("ab")));
let broken =
drawn(|canvas| painter.paint_axes_named(canvas, &plot, true, &|_| Some("a\n\tb\r")));
assert_eq!(below(&broken, &plot), below(&plain, &plot));
}
#[test]
fn a_place_too_narrow_for_even_an_ellipsis_has_no_label() {
let mut painter = painter();
let area = painter.plot_area(Rect::new(0.0, 0.0, 460.0, 300.0), true);
let plot = Plot::new(area, Axis::category(-0.5, 99.5), Axis::number(0.0, 10.0));
let instances = drawn(|canvas| {
painter.paint_crosshair_named(
canvas,
&Plot::new(Rect::new(0.0, 0.0, 6.0, 50.0), plot.x(), plot.y()),
Point::new(3.0, 10.0),
true,
&|_| Some("north"),
)
});
assert!(
instances.iter().all(|i| !i.is_glyph() || i.rect[1] < 50.0),
"{instances:?}"
);
}
fn span_around(glyphs: &[[f32; 4]], x: f32, half: f32) -> (f32, f32) {
let mine: Vec<_> = glyphs
.iter()
.filter(|g| (g[0] + g[2] / 2.0 - x).abs() < half)
.collect();
let left = mine.iter().map(|g| g[0]).fold(f32::MAX, f32::min);
let right = mine.iter().map(|g| g[0] + g[2]).fold(f32::MIN, f32::max);
(left, right)
}
#[test]
fn a_short_name_is_centred_and_cut_names_leave_a_gap_between_them() {
let mut painter = painter();
let plot = four(&mut painter);
let slot = plot.x_px(1.0) - plot.x_px(0.0);
let short = drawn(|canvas| painter.paint_axes_named(canvas, &plot, true, &|_| Some("mmmm")));
let (left, right) = span_around(&below(&short, &plot), plot.x_px(1.0), slot / 2.0);
assert!(
((left + right) / 2.0 - plot.x_px(1.0)).abs() < 2.0,
"{left}..{right}"
);
let long = "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm";
let cut = drawn(|canvas| painter.paint_axes_named(canvas, &plot, true, &|_| Some(long)));
let glyphs = below(&cut, &plot);
let (_, first_ends) = span_around(&glyphs, plot.x_px(1.0), slot / 2.0);
let (second_starts, _) = span_around(&glyphs, plot.x_px(2.0), slot / 2.0);
assert!(
second_starts - first_ends >= 4.0,
"{first_ends} then {second_starts}"
);
}
#[test]
fn a_cut_name_ends_in_an_ellipsis() {
let mut painter = painter();
let plot = four(&mut painter);
let long = "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm";
let instances = drawn(|canvas| painter.paint_axes_named(canvas, &plot, true, &|_| Some(long)));
let glyphs = below(&instances, &plot);
let dots = glyphs.iter().filter(|g| g[3] <= g[2] / 2.0).count();
assert_eq!(dots, 4, "one at the end of each name: {glyphs:?}");
}
#[test]
fn the_crosshair_names_nothing_without_x_labels_and_its_box_holds_a_cut_name() {
let mut painter = painter();
let palette = *painter.palette();
let plot = four(&mut painter);
let at = Point::new(plot.x_px(1.0), plot.area().y0 + 20.0);
let names = |_| Some("north");
let unlabelled =
drawn(|canvas| painter.paint_crosshair_named(canvas, &plot, at, false, &names));
assert!(unlabelled.iter().all(|i| i.rect[1] <= plot.area().y1));
let narrow = Plot::new(Rect::new(0.0, 0.0, 40.0, 100.0), plot.x(), plot.y());
let at = Point::new(20.0, 10.0);
let long = |_| Some("mmmmmmmmmmmmmmmmmmmmmmmmmmmmmm");
let instances = drawn(|canvas| painter.paint_crosshair_named(canvas, &narrow, at, true, &long));
let marker = palette.marker.to_array();
let boxed = instances
.iter()
.find(|i| i.color == marker && i.rect[1] > 100.0)
.unwrap();
let [left, _, width, _] = boxed.rect;
let pad = 7.0;
let text: Vec<_> = instances
.iter()
.filter(|i| i.is_glyph() && i.rect[1] > 100.0)
.collect();
let end = text
.iter()
.map(|g| g.rect[0] + g.rect[2])
.fold(0.0, f32::max);
assert!(
end <= left + width && end >= left + width - pad - 4.0,
"{end} in {boxed:?}"
);
}