use super::{
LABEL_SIZE,
axis::format_value,
fit_text,
labels::{LabelSpace, label_box, warn_label_overlap},
timechart::TimeFrame,
};
use crate::{
error::ChartWarning,
metrics::TextMetrics,
scene::{Circle, Element, Polyline, Rect, Text, TextAnchor},
spec::{ChartSpec, LayerRef, LayerSpec, NumberStyle, Shape},
time::{Precision, TimeZone},
};
pub(crate) fn zone_extent(
spec: &ChartSpec,
layer: &LayerSpec,
zone: TimeZone,
style: NumberStyle,
) -> String {
let resolve = |time: &Option<crate::time::TimeValue>| {
time.as_ref().and_then(|time| time.resolve(zone).ok())
};
let (from, to) = (resolve(&layer.from), resolve(&layer.to));
let epochs: Vec<i64> = from.into_iter().chain(to).collect();
let times = if epochs.is_empty() {
None
} else {
let precision = Precision::of(epochs.into_iter(), zone).at_least(spec.time_precision(zone));
crate::text::zone_times(
spec.locale,
from.map(|epoch| precision.format(epoch, zone)).as_deref(),
to.map(|epoch| precision.format(epoch, zone)).as_deref(),
)
};
let show = |value: f64| format_value(value, style);
let values = crate::text::zone_values(
spec.locale,
layer.bottom.map(show).as_deref(),
layer.top.map(show).as_deref(),
);
[times, values]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(", ")
}
pub(crate) fn marker_position(
layer: &LayerSpec,
zone: TimeZone,
chart: Precision,
style: NumberStyle,
) -> String {
let epoch = layer
.time
.as_ref()
.and_then(|time| time.resolve(zone).ok())
.expect("validated point markers carry a time");
let value = layer.value.expect("validated point markers carry a value");
format!(
"{}, {}",
Precision::of(std::iter::once(epoch), zone)
.at_least(chart)
.format(epoch, zone),
format_value(value, style)
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn push_zone(
spec: &ChartSpec,
entry: LayerRef,
frame: &TimeFrame,
elements: &mut Vec<Element>,
labels: &mut Vec<Element>,
space: &mut LabelSpace,
warnings: &mut Vec<ChartWarning>,
metrics: &impl TextMetrics,
) {
let layer = entry.layer;
let label = layer
.label
.as_deref()
.expect("validated zones carry a label");
let plot = frame.plot;
let resolve = |time: &Option<crate::time::TimeValue>| {
time.as_ref().and_then(|time| time.resolve(frame.zone).ok())
};
let (start, end) = frame.edges();
let from = resolve(&layer.from).map_or(start, |epoch| frame.x(epoch));
let to = resolve(&layer.to).map_or(end, |epoch| frame.x_until(epoch));
let (left, right) = (from.min(to), from.max(to));
let top = layer.top.map_or(plot.top, |value| frame.y(value));
let bottom = layer
.bottom
.map_or(plot.top + plot.height, |value| frame.y(value));
elements.push(Element::Rect(Rect {
x: left,
y: top,
width: right - left,
height: bottom - top,
class: "chartlet-zone",
series_index: None,
style_index: layer.resolved_color().is_some().then_some(entry.global),
tooltip: Some(format!(
"{label}: {}",
zone_extent(spec, layer, frame.zone, frame.style)
)),
}));
let content = fit_text(
label,
plot.width * 0.4,
LABEL_SIZE,
metrics,
warnings,
&format!("/panes/{}/layers/{}/label", entry.pane, entry.local),
);
let width = metrics.width(&content, LABEL_SIZE);
let (x, anchor) = if left + 4.0 + width > plot.left + plot.width {
(right - 4.0, TextAnchor::End)
} else {
(left + 4.0, TextAnchor::Start)
};
let y = if bottom - top >= 20.0 {
top + 14.0
} else if top - 17.0 >= plot.top {
top - 5.0
} else {
bottom + 13.0
};
let text = Text {
x,
y,
class: "chartlet-rule-label",
anchor,
content,
};
space.place(&text, entry, true, metrics, warnings);
labels.push(Element::Text(text));
}
pub(super) fn push_marker(
entry: LayerRef,
frame: &TimeFrame,
radius: f64,
elements: &mut Vec<Element>,
space: &mut LabelSpace,
) {
let layer = entry.layer;
let (x, y) = marker_point(entry, frame);
let label = layer
.label
.as_deref()
.expect("validated point markers carry a label");
let style_index = layer.resolved_color().is_some().then_some(entry.global);
let tooltip = Some(format!(
"{label}: {}",
marker_position(layer, frame.zone, frame.precision, frame.style)
));
let reach = radius * 1.25;
space
.symbols
.push((x - reach, y - reach, x + reach, y + reach));
let outline = |corners: &[(f64, f64)]| -> Vec<(f64, f64)> {
let mut points: Vec<(f64, f64)> = corners
.iter()
.map(|(dx, dy)| (x + dx * radius, y + dy * radius))
.collect();
points.push(points[0]);
points
};
let points = match layer.marker_shape() {
Shape::Circle => {
elements.push(Element::Circle(Circle {
cx: x,
cy: y,
radius,
class: "chartlet-marker",
topic: None,
series_index: None,
style_index,
tooltip,
}));
return;
}
Shape::Square => outline(&[(-0.9, -0.9), (0.9, -0.9), (0.9, 0.9), (-0.9, 0.9)]),
Shape::Diamond => outline(&[(0.0, -1.25), (1.25, 0.0), (0.0, 1.25), (-1.25, 0.0)]),
Shape::TriangleUp => outline(&[(0.0, -1.25), (1.15, 0.8), (-1.15, 0.8)]),
Shape::TriangleDown => outline(&[(0.0, 1.25), (1.15, -0.8), (-1.15, -0.8)]),
};
elements.push(Element::Polyline(Polyline {
points,
class: "chartlet-marker",
topic: None,
series_index: None,
style_index,
tooltip,
}));
}
fn marker_point(entry: LayerRef, frame: &TimeFrame) -> (f64, f64) {
let layer = entry.layer;
let epoch = layer
.time
.as_ref()
.and_then(|time| time.resolve(frame.zone).ok())
.expect("validated point markers carry a time");
let value = layer.value.expect("validated point markers carry a value");
(frame.x(epoch), frame.y(value))
}
pub(super) fn push_marker_label(
entry: LayerRef,
frame: &TimeFrame,
radius: f64,
labels: &mut Vec<Element>,
space: &mut LabelSpace,
warnings: &mut Vec<ChartWarning>,
metrics: &impl TextMetrics,
) {
let (x, y) = marker_point(entry, frame);
let content = fit_text(
entry
.layer
.label
.as_deref()
.expect("validated point markers carry a label"),
frame.plot.width * 0.4,
LABEL_SIZE,
metrics,
warnings,
&format!("/panes/{}/layers/{}/label", entry.pane, entry.local),
);
let gap = radius * 1.25 + 4.0;
let candidates = [
(x + gap, y + 4.0, TextAnchor::Start),
(x - gap, y + 4.0, TextAnchor::End),
(x, y - gap - 3.0, TextAnchor::Middle),
(x, y + gap + 10.0, TextAnchor::Middle),
]
.map(|(x, y, anchor)| Text {
x,
y,
class: "chartlet-rule-label",
anchor,
content: content.clone(),
});
let boxes = candidates.each_ref().map(|text| label_box(text, metrics));
let clear = boxes
.iter()
.position(|label| space.collision(*label, true).is_none() && !space.hits_symbol(*label));
let readable = || {
boxes
.iter()
.position(|label| space.collision(*label, false).is_none())
};
let chosen = clear.or_else(readable);
let index = chosen.unwrap_or(0);
if chosen.is_none()
&& let Some(collision) = space.collision(boxes[0], false)
{
warn_label_overlap(entry, collision, warnings);
}
space.labels.push(boxes[index]);
let text = candidates
.into_iter()
.nth(index)
.expect("the index comes from the candidates");
labels.push(Element::Text(text));
}
pub(super) fn push_rule(
entry: LayerRef,
frame: &TimeFrame,
elements: &mut Vec<Element>,
labels: &mut Vec<Element>,
space: &mut LabelSpace,
warnings: &mut Vec<ChartWarning>,
metrics: &impl TextMetrics,
) {
let layer = entry.layer;
let label = layer
.label
.as_deref()
.expect("validated annotations carry a label");
let path = format!("/panes/{}/layers/{}/label", entry.pane, entry.local);
let plot = frame.plot;
let bottom = plot.top + plot.height;
let (points, text, tooltip) = if let Some(value) = layer.value {
let y = frame.y(value);
(
vec![(plot.left, y), (plot.left + plot.width, y)],
Text {
x: plot.left + 4.0,
y: y - 6.0,
class: "chartlet-rule-label",
anchor: TextAnchor::Start,
content: fit_text(
label,
plot.width / 2.0,
LABEL_SIZE,
metrics,
warnings,
&path,
),
},
format!("{label}: {}", format_value(value, frame.style)),
)
} else {
let epoch = layer
.time
.as_ref()
.and_then(|time| time.resolve(frame.zone).ok())
.expect("validated annotations carry a value or a time");
let x = frame.x(epoch);
let (label_x, anchor) = if x > plot.left + plot.width * 0.7 {
(x - 4.0, TextAnchor::End)
} else {
(x + 4.0, TextAnchor::Start)
};
let precision = Precision::of(std::iter::once(epoch), frame.zone).at_least(frame.precision);
(
vec![(x, plot.top), (x, bottom)],
Text {
x: label_x,
y: plot.top + 12.0,
class: "chartlet-rule-label",
anchor,
content: fit_text(
label,
plot.width * 0.4,
LABEL_SIZE,
metrics,
warnings,
&path,
),
},
format!("{label}: {}", precision.format(epoch, frame.zone)),
)
};
elements.push(Element::Polyline(Polyline {
points,
class: "chartlet-rule",
topic: None,
series_index: None,
style_index: layer.resolved_color().is_some().then_some(entry.global),
tooltip: Some(tooltip),
}));
space.place(&text, entry, true, metrics, warnings);
labels.push(Element::Text(text));
}