use super::{LABEL_SIZE, PlotArea, timechart::TimeFrame};
use crate::{
error::ChartWarning,
metrics::TextMetrics,
scene::{Text, TextAnchor},
spec::LayerRef,
};
pub(super) type LabelBox = (f64, f64, f64, f64);
pub(super) struct LabelSpace {
plot: PlotArea,
pub(super) labels: Vec<LabelBox>,
lines: Vec<Vec<(f64, f64)>>,
pub(super) symbols: Vec<LabelBox>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Collision {
Outside,
Label,
Line,
}
impl LabelSpace {
pub(super) fn new(frame: &TimeFrame, entries: &[LayerRef]) -> Self {
let annotated = entries.iter().any(|entry| !entry.layer.is_data());
let lines = if annotated {
let wicks = entries.iter().flat_map(|entry| {
entry.layer.resolved_candles(frame.zone).into_iter().map(
|(epoch, [_, high, low, _])| {
let x = frame.x(epoch);
vec![(x, frame.y(high)), (x, frame.y(low))]
},
)
});
entries
.iter()
.filter(|entry| entry.layer.is_data())
.flat_map(|entry| entry.layer.resolved_segments(frame.zone))
.map(|segment| {
segment
.iter()
.map(|(epoch, value)| (frame.x(*epoch), frame.y(*value)))
.collect()
})
.chain(wicks)
.collect()
} else {
Vec::new()
};
Self {
plot: frame.plot,
labels: Vec::new(),
lines,
symbols: Vec::new(),
}
}
fn outside(&self, label: LabelBox) -> bool {
let plot = self.plot;
label.0 < plot.left
|| label.2 > plot.left + plot.width
|| label.1 < plot.top
|| label.3 > plot.top + plot.height
}
fn hits_label(&self, label: LabelBox) -> bool {
self.labels.iter().any(|other| boxes_overlap(label, *other))
}
fn hits_line(&self, label: LabelBox) -> bool {
self.lines.iter().any(|line| {
line.windows(2)
.any(|pair| segment_hits_box(pair[0], pair[1], label))
})
}
pub(super) fn hits_symbol(&self, label: LabelBox) -> bool {
self.symbols
.iter()
.any(|other| boxes_overlap(label, *other))
}
pub(super) fn collision(&self, label: LabelBox, lines: bool) -> Option<Collision> {
if self.outside(label) {
Some(Collision::Outside)
} else if self.hits_label(label) {
Some(Collision::Label)
} else if lines && self.hits_line(label) {
Some(Collision::Line)
} else {
None
}
}
pub(super) fn place(
&mut self,
text: &Text,
entry: LayerRef,
lines: bool,
metrics: &impl TextMetrics,
warnings: &mut Vec<ChartWarning>,
) {
let label = label_box(text, metrics);
if let Some(collision) = self.collision(label, lines) {
warn_label_overlap(entry, collision, warnings);
}
self.labels.push(label);
}
}
pub(super) fn label_box(text: &Text, metrics: &impl TextMetrics) -> LabelBox {
let width = metrics.width(&text.content, LABEL_SIZE);
let left = match text.anchor {
TextAnchor::Start => text.x,
TextAnchor::Middle => text.x - width / 2.0,
TextAnchor::End => text.x - width,
};
(left, text.y - 9.0, left + width, text.y + 3.0)
}
const FALLBACK_RESERVE: f64 = 1.08;
pub(crate) struct WithReserve<'a, M>(pub &'a M);
impl<M: TextMetrics> TextMetrics for WithReserve<'_, M> {
fn width(&self, text: &str, font_size: f64) -> f64 {
self.0.width(text, font_size) * FALLBACK_RESERVE
}
}
pub(super) fn boxes_overlap(a: LabelBox, b: LabelBox) -> bool {
a.0 < b.2 && b.0 < a.2 && a.1 < b.3 && b.1 < a.3
}
fn segment_hits_box(a: (f64, f64), b: (f64, f64), area: LabelBox) -> bool {
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let (mut start, mut end) = (0.0_f64, 1.0_f64);
for (p, q) in [
(-dx, a.0 - area.0),
(dx, area.2 - a.0),
(-dy, a.1 - area.1),
(dy, area.3 - a.1),
] {
if p == 0.0 {
if q < 0.0 {
return false;
}
} else {
let t = q / p;
if p < 0.0 {
start = start.max(t);
} else {
end = end.min(t);
}
if start > end {
return false;
}
}
}
true
}
pub(super) fn warn_label_overlap(
entry: LayerRef,
collision: Collision,
warnings: &mut Vec<ChartWarning>,
) {
let message = match collision {
Collision::Outside => {
"the label reaches outside the plot area; shorten it or move the annotation"
}
Collision::Label => {
"the label overlaps the label of another annotation; shorten one of them or move it"
}
Collision::Line => "the label crosses a data line; shorten it or move the reference line",
};
warnings.push(ChartWarning::new(
"label_overlap",
format!("/panes/{}/layers/{}", entry.pane, entry.local),
message,
));
}