use super::{
LABEL_SIZE, PlotArea,
axis::{
NumericScale, add_bottom_category_title, format_value, label_step, warn_if_labels_thinned,
},
axis_gutter,
bar::{bar_tooltip, error_reach, push_error_bar},
base_elements, count, fit_text,
legend::{add_line_legend, line_legend_space},
plot_margin,
title::title_extra,
};
use crate::{
error::ChartWarning,
metrics::TextMetrics,
scene::{Circle, Element, Hook, Polyline, Scene, Text, TextAnchor},
spec::{ChartSpec, Dataset, MAX_SERIES},
};
const SERIES_LINE_CLASSES: [&str; MAX_SERIES] = [
"chartlet-line chartlet-line-series-1",
"chartlet-line chartlet-line-series-2 chartlet-line-dashed",
"chartlet-line chartlet-line-series-3 chartlet-line-dotted",
"chartlet-line chartlet-line-series-4 chartlet-line-thin",
];
const SERIES_POINT_CLASSES: [&str; MAX_SERIES] = [
"chartlet-point chartlet-point-series-1",
"chartlet-point chartlet-point-series-2",
"chartlet-point chartlet-point-series-3",
"chartlet-point chartlet-point-series-4",
];
pub(super) fn layout_line(
spec: &ChartSpec,
warnings: &mut Vec<ChartWarning>,
metrics: &impl TextMetrics,
) -> Scene {
let dataset = spec.dataset();
let several = dataset.series.len() > 1;
let width = f64::from(spec.width);
let height = f64::from(spec.height);
let left = f64::from(axis_gutter(spec.width));
let right = f64::from(plot_margin(spec.width));
let plot_width = width - left - right;
let head = title_extra(spec, plot_width, metrics);
let legend = if several {
line_legend_space(&dataset, plot_width, metrics)
} else {
0.0
};
let top = 78.0 + head + legend;
let bottom = if spec.category_axis.title.is_some() {
82.0
} else {
62.0
};
let plot = PlotArea {
left,
top,
width: plot_width,
height: super::plot_height(height - top - bottom, warnings),
vertical_bars: true,
};
let scale = NumericScale::for_axis(
dataset.values().chain(spec.error_limits()),
false,
&spec.value_axis,
);
let mut elements = base_elements(spec, &scale, plot, warnings, metrics);
elements.push(plot_hook(dataset.categories.len(), plot, &scale));
if several {
add_line_legend(
&dataset,
&SERIES_LINE_CLASSES,
plot,
head,
&mut elements,
warnings,
metrics,
);
}
add_line_data(
spec,
&dataset,
plot,
&scale,
&mut elements,
warnings,
metrics,
);
add_bottom_category_title(
spec,
left,
plot_width,
height,
&mut elements,
warnings,
metrics,
);
Scene {
width: spec.width,
height: spec.height,
elements,
}
}
fn category_x(plot: PlotArea, index: usize, categories: usize) -> f64 {
let last_index = categories.saturating_sub(1);
if last_index == 0 {
return plot.left + plot.width / 2.0;
}
let divisor = f64::from(u32::try_from(last_index).expect("data count is limited"));
let item_index = f64::from(u32::try_from(index).expect("data count is limited"));
plot.left + 6.0 + item_index / divisor * (plot.width - 12.0)
}
fn add_line_data(
spec: &ChartSpec,
dataset: &Dataset,
plot: PlotArea,
scale: &NumericScale,
elements: &mut Vec<Element>,
warnings: &mut Vec<ChartWarning>,
metrics: &impl TextMetrics,
) {
let categories = dataset.categories.len();
let several = dataset.series.len() > 1;
let spacing = if categories > 1 {
plot.width / f64::from(u32::try_from(categories - 1).expect("data count is limited"))
} else {
plot.width
};
let step = label_step(&dataset.categories, spacing, metrics);
let mut category_label = |elements: &mut Vec<Element>, index: usize| {
if !index.is_multiple_of(step) {
return;
}
let label = fit_text(
&dataset.categories[index],
(spacing * count(step) - 8.0).max(20.0),
LABEL_SIZE,
metrics,
warnings,
&dataset.category_path(index),
);
elements.push(Element::Text(Text {
x: category_x(plot, index, categories),
y: plot.top + plot.height + 24.0,
class: "chartlet-label",
anchor: TextAnchor::Middle,
content: label,
}));
};
for (series_index, series) in dataset.series.iter().enumerate() {
let (line_class, point_class) = if several {
(
SERIES_LINE_CLASSES[series_index],
SERIES_POINT_CLASSES[series_index],
)
} else {
("chartlet-line", "chartlet-point")
};
let series_mark = several.then_some(series_index);
let mut segment = Vec::new();
for (index, value) in series.values.iter().enumerate() {
let x = category_x(plot, index, categories);
let Some(value) = *value else {
flush_line_segment(elements, &mut segment, line_class, series_mark);
if !several {
category_label(elements, index);
}
continue;
};
let y = scale.map(value, plot.top + plot.height, plot.top);
segment.push((x, y));
push_error_bar(
elements,
spec,
(index, series_index),
true,
|bound| scale.map(bound, plot.top + plot.height, plot.top),
(x, 12.0),
);
elements.push(Element::Circle(Circle {
cx: x,
cy: y,
radius: 4.0,
class: point_class,
series_index: series_mark,
style_index: None,
tooltip: Some(bar_tooltip(
spec,
(index, series_index),
&dataset.categories[index],
value,
series.name.as_deref(),
)),
}));
if spec.show_values && !several {
elements.push(Element::Text(Text {
x,
y: scale.map(
error_reach(spec, (index, series_index), value),
plot.top + plot.height,
plot.top,
) - 10.0,
class: "chartlet-value",
anchor: TextAnchor::Middle,
content: format_value(value, spec.number_style()),
}));
}
if !several {
category_label(elements, index);
}
}
flush_line_segment(elements, &mut segment, line_class, series_mark);
}
if several {
for index in 0..categories {
category_label(elements, index);
}
}
warn_if_labels_thinned(spec, step, warnings);
}
fn plot_hook(count: usize, plot: PlotArea, scale: &NumericScale) -> Element {
let last = f64::from(u32::try_from(count.saturating_sub(1)).expect("data count is limited"));
let x = if count <= 1 {
vec![(0.0, plot.left + plot.width / 2.0)]
} else {
vec![(0.0, plot.left + 6.0), (last, plot.left + plot.width - 6.0)]
};
let (min, max) = scale.ends();
let y = |value| scale.map(value, plot.top + plot.height, plot.top);
Element::Hook(Hook::Plot {
pane: 0,
x,
y: [(min, y(min)), (max, y(max))],
log: scale.is_log(),
})
}
fn flush_line_segment(
elements: &mut Vec<Element>,
segment: &mut Vec<(f64, f64)>,
class: &'static str,
series_index: Option<usize>,
) {
if segment.len() >= 2 {
elements.push(Element::Polyline(Polyline {
points: std::mem::take(segment),
class,
series_index,
style_index: None,
tooltip: None,
}));
} else {
segment.clear();
}
}