use super::marks;
use super::model::{Chart, SeriesKind};
use super::project::Plot;
use crate::layout::PlacedNode;
use crate::layout::prim;
use crate::resolve::{MarkerKind, ResolvedValue};
type Seg = ((f64, f64), (f64, f64));
const SIZE: f64 = 10.0;
const GAP: f64 = 7.0;
const PAD: f64 = 2.0;
pub(super) struct Req {
pub anchor: (f64, f64),
pub radius: f64,
pub text: String,
pub color: ResolvedValue,
pub forced: bool,
pub inside: Option<Inside>,
}
pub(super) struct Inside {
pub fit: f64,
pub color: ResolvedValue,
}
pub(super) fn collect_series(plot: &Plot, chart: &Chart, reqs: &mut Vec<Req>) {
for ser in &chart.series {
if ser.tags.is_empty() || !ser.tooltip.inline() {
continue;
}
let radius = match ser.kind {
SeriesKind::Dots => ser.dot.0 / 2.0,
_ if ser.marker != MarkerKind::None => {
marks::marker_diameter(ser.marker, ser.thickness) / 2.0
}
_ => 0.0,
};
for (((xd, yd), (xp, yp)), tag) in marks::samples(plot, chart, ser).iter().zip(&ser.tags) {
if tag.is_empty() || !marks::in_domain(chart, ser, *xd, *yd) {
continue;
}
reqs.push(Req {
anchor: (*xp, *yp),
radius,
text: tag.clone(),
color: ser.tag_color.clone(),
forced: ser.tooltip.forced(),
inside: None,
});
}
}
}
pub(super) fn series_lines(plot: &Plot, chart: &Chart) -> Vec<Seg> {
let mut segs = Vec::new();
for ser in &chart.series {
if !matches!(ser.kind, SeriesKind::Line | SeriesKind::Area) {
continue;
}
let pts: Vec<(f64, f64)> = marks::samples(plot, chart, ser)
.iter()
.map(|(_, p)| *p)
.collect();
for win in pts.windows(2) {
segs.push((win[0], win[1]));
}
}
segs
}
pub(super) fn place(reqs: &[Req], plot: &Plot, lines: &[Seg]) -> Vec<PlacedNode> {
let mut placed: Vec<Rect> = Vec::new();
let mut out = Vec::new();
for req in reqs {
let w = prim::text_width(&req.text, SIZE);
let h = prim::text_height(&req.text, SIZE);
let mut seats: Vec<((f64, f64), &ResolvedValue)> = Vec::new();
if let Some(ins) = &req.inside
&& w <= ins.fit
{
seats.push((req.anchor, &ins.color));
}
seats.extend(candidates(req.anchor, w, h, req.radius).map(|c| (c, &req.color)));
let pick: Option<((f64, f64), ResolvedValue)> = {
let clear = |c: (f64, f64)| {
let r = Rect::around(c, w, h);
r.within(plot)
&& placed.iter().all(|p| !p.hits(&r))
&& lines.iter().all(|&(a, b)| !seg_hits_rect(a, b, &r))
};
let chosen = seats.iter().find(|(c, _)| clear(*c)).or_else(|| {
req.forced
.then(|| {
seats
.iter()
.find(|(c, _)| Rect::around(*c, w, h).within(plot))
})
.flatten()
});
match chosen {
Some(&(c, col)) => Some((c, col.clone())),
None if req.forced => Some((req.anchor, req.color.clone())),
None => None, }
};
let Some((center, color)) = pick else {
continue;
};
placed.push(Rect::around(center, w, h));
let mut t = prim::text(&req.text, center.0, center.1, SIZE, Some(color), false);
t.type_chain.push("chart-label".to_string());
out.push(t);
}
out
}
fn candidates((ax, ay): (f64, f64), w: f64, h: f64, radius: f64) -> [(f64, f64); 8] {
let dx = w / 2.0 + GAP + radius;
let dy = h / 2.0 + GAP + radius;
[
(ax, ay - dy), (ax, ay + dy), (ax + dx, ay), (ax - dx, ay), (ax + dx, ay - dy), (ax - dx, ay - dy), (ax + dx, ay + dy), (ax - dx, ay + dy), ]
}
struct Rect {
x0: f64,
y0: f64,
x1: f64,
y1: f64,
}
impl Rect {
fn around((cx, cy): (f64, f64), w: f64, h: f64) -> Rect {
let (hw, hh) = (w / 2.0 + PAD, h / 2.0 + PAD);
Rect {
x0: cx - hw,
y0: cy - hh,
x1: cx + hw,
y1: cy + hh,
}
}
fn within(&self, plot: &Plot) -> bool {
self.x0 >= plot.x0 && self.x1 <= plot.x1 && self.y0 >= plot.y0 && self.y1 <= plot.y1
}
fn hits(&self, o: &Rect) -> bool {
self.x0 < o.x1 && o.x0 < self.x1 && self.y0 < o.y1 && o.y0 < self.y1
}
}
fn seg_hits_rect(p0: (f64, f64), p1: (f64, f64), r: &Rect) -> bool {
let (dx, dy) = (p1.0 - p0.0, p1.1 - p0.1);
let edges = [
(-dx, p0.0 - r.x0),
(dx, r.x1 - p0.0),
(-dy, p0.1 - r.y0),
(dy, r.y1 - p0.1),
];
let (mut t0, mut t1) = (0.0_f64, 1.0_f64);
for (p, q) in edges {
if p.abs() < 1e-9 {
if q < 0.0 {
return false; }
} else {
let t = q / p;
if p < 0.0 {
t0 = t0.max(t);
} else {
t1 = t1.min(t);
}
if t0 > t1 {
return false;
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
fn rect() -> Rect {
Rect {
x0: 0.0,
y0: 0.0,
x1: 10.0,
y1: 10.0,
}
}
#[test]
fn seg_hits_rect_crossing_and_inside() {
assert!(
seg_hits_rect((-5.0, 5.0), (15.0, 5.0), &rect()),
"a line crossing right through is a hit"
);
assert!(
seg_hits_rect((5.0, 5.0), (5.0, 5.0), &rect()),
"a point inside is a hit"
);
}
#[test]
fn seg_hits_rect_clear() {
assert!(
!seg_hits_rect((-5.0, -5.0), (-1.0, -1.0), &rect()),
"a segment wholly outside misses"
);
assert!(
!seg_hits_rect((20.0, 0.0), (20.0, 10.0), &rect()),
"a parallel segment past the edge misses"
);
}
}