use super::{
MULTIPLES_MARGIN,
axes::{fitted_unit, push_time_ticks, push_value_grid},
frame::{Detail, TimeFrame, time_axis, time_scale},
marks::draw_pane,
time_tick_spacing,
};
use crate::{
error::ChartWarning,
layout::{
LABEL_SIZE, LEGEND_HEIGHT, LEGEND_ROW, PANEL_GUTTER, PANEL_MARGIN, PlotArea, count,
fit_text,
legend::{add_layer_legend, legend_rows},
panel_plot_pixels,
title::{push_title, title_extra},
},
metrics::TextMetrics,
scene::{Element, Scene, Text, TextAnchor},
spec::ChartSpec,
time::{Precision, TimeZone},
};
pub(in crate::layout) fn multiples_header(
spec: &ChartSpec,
elements: &mut Vec<Element>,
warnings: &mut Vec<ChartWarning>,
metrics: &impl TextMetrics,
) -> f64 {
let width = f64::from(spec.width);
push_title(
elements,
spec,
MULTIPLES_MARGIN,
width - 2.0 * MULTIPLES_MARGIN,
metrics,
warnings,
);
let head = title_extra(spec, width - 2.0 * MULTIPLES_MARGIN, metrics);
let mut cursor = LEGEND_ROW + head;
if spec.series_names().len() > 1 {
add_layer_legend(
spec,
LEGEND_ROW + head,
MULTIPLES_MARGIN,
width - 2.0 * MULTIPLES_MARGIN,
elements,
warnings,
metrics,
);
cursor += count(legend_rows(spec, width - 2.0 * MULTIPLES_MARGIN, metrics)) * LEGEND_HEIGHT;
}
if let Some(axis_title) = &spec.value_axis.title {
elements.push(Element::Text(Text {
x: MULTIPLES_MARGIN,
y: cursor + 10.0,
class: "chartlet-axis-title",
anchor: TextAnchor::Start,
content: fit_text(
axis_title,
width - 2.0 * MULTIPLES_MARGIN,
LABEL_SIZE,
metrics,
warnings,
"/valueAxis/title",
),
}));
cursor += 20.0;
}
cursor + 8.0
}
fn panel_lines(
text: &str,
width: f64,
size: f64,
metrics: &impl TextMetrics,
warnings: &mut Vec<ChartWarning>,
path: &str,
) -> Vec<String> {
match super::title::two_lines(text, width, size, metrics) {
Some((first, rest)) => vec![
first.to_owned(),
fit_text(rest, width, size, metrics, warnings, path),
],
None => vec![fit_text(text, width, size, metrics, warnings, path)],
}
}
pub(in crate::layout) type PanelHead = (Vec<String>, Vec<String>, &'static str);
const PANEL_TITLE_LINE: f64 = 16.0;
const PANEL_NOTE_LINE: f64 = 15.0;
pub(in crate::layout) fn panel_heads(
spec: &ChartSpec,
width: f64,
warnings: &mut Vec<ChartWarning>,
metrics: &impl TextMetrics,
) -> (Vec<PanelHead>, f64, usize) {
let heads: Vec<PanelHead> = spec
.panes
.iter()
.enumerate()
.map(|(index, pane)| {
let title = panel_lines(
pane.title.as_deref().unwrap_or_default(),
width,
13.0,
metrics,
warnings,
&format!("/panes/{index}/title"),
);
let note = pane.note.as_deref().map_or_else(Vec::new, |note| {
panel_lines(
note,
width,
LABEL_SIZE,
metrics,
warnings,
&format!("/panes/{index}/note"),
)
});
let class = if pane.note_emphasis {
"chartlet-panel-note chartlet-panel-note-strong"
} else {
"chartlet-panel-note"
};
(title, note, class)
})
.collect();
let title_lines = heads
.iter()
.map(|(title, ..)| title.len())
.max()
.unwrap_or(1);
let note_lines = heads
.iter()
.map(|(_, note, _)| note.len())
.max()
.unwrap_or(0);
let height = 14.0 + PANEL_TITLE_LINE * count(title_lines) + PANEL_NOTE_LINE * count(note_lines);
(heads, height, title_lines)
}
pub(in crate::layout) fn push_panel_head(
(title, note, note_class): &PanelHead,
title_lines: usize,
plot: PlotArea,
cell_top: f64,
elements: &mut Vec<Element>,
) {
for (index, line) in title.iter().enumerate() {
elements.push(Element::Text(Text {
x: plot.left,
y: cell_top + 18.0 + PANEL_TITLE_LINE * count(index),
class: "chartlet-panel-title",
anchor: TextAnchor::Start,
content: line.clone(),
}));
}
let below = cell_top + 18.0 + PANEL_TITLE_LINE * count(title_lines - 1);
for (index, line) in note.iter().enumerate() {
elements.push(Element::Text(Text {
x: plot.left,
y: below + PANEL_NOTE_LINE * count(index + 1),
class: note_class,
anchor: TextAnchor::Start,
content: line.clone(),
}));
}
}
fn push_multiples_time_title(
spec: &ChartSpec,
elements: &mut Vec<Element>,
warnings: &mut Vec<ChartWarning>,
metrics: &impl TextMetrics,
) {
let (width, height) = (f64::from(spec.width), f64::from(spec.height));
if let Some(axis_title) = &spec.time_axis.title {
elements.push(Element::Text(Text {
x: width / 2.0,
y: height - 14.0,
class: "chartlet-axis-title",
anchor: TextAnchor::Middle,
content: fit_text(
axis_title,
width - 2.0 * MULTIPLES_MARGIN,
LABEL_SIZE,
metrics,
warnings,
"/timeAxis/title",
),
}));
}
}
pub(in crate::layout) fn layout_multiples(
spec: &ChartSpec,
warnings: &mut Vec<ChartWarning>,
metrics: &impl TextMetrics,
) -> Scene {
let zone = spec.time_zone().unwrap_or_default();
let width = f64::from(spec.width);
let height = f64::from(spec.height);
let columns = spec.multiples_columns();
let panels = spec.panes.len();
let rows = panels.div_ceil(usize::try_from(columns).expect("columns are limited"));
let mut elements = Vec::new();
let grid_top = multiples_header(spec, &mut elements, warnings, metrics);
let grid_bottom = height
- if spec.time_axis.title.is_some() {
36.0
} else {
12.0
};
let cell_width = (width - 2.0 * MULTIPLES_MARGIN) / f64::from(columns);
let cell_height = (grid_bottom - grid_top) / count(rows);
let panel_gutter = multiples_gutter(spec, zone, warnings, metrics);
let (heads, head_height, title_lines) = panel_heads(
spec,
cell_width - panel_gutter - f64::from(PANEL_MARGIN),
warnings,
metrics,
);
let (span, slots) = time_axis(spec, zone);
let precision = spec.time_precision(zone);
let shared = time_scale(spec, zone, None);
let max_ticks = usize::try_from(
(panel_plot_pixels(spec.width, columns) / time_tick_spacing(spec.observed_precision(zone)))
.max(2),
)
.expect("a usize is at least 32 bits wide");
for (pane_index, head) in heads.iter().enumerate() {
let column = pane_index % usize::try_from(columns).expect("columns are limited");
let row = pane_index / usize::try_from(columns).expect("columns are limited");
let cell_left = MULTIPLES_MARGIN + cell_width * count(column);
let cell_top = grid_top + cell_height * count(row);
let plot = PlotArea {
left: cell_left + panel_gutter,
top: cell_top + head_height,
width: cell_width - panel_gutter - f64::from(PANEL_MARGIN),
height: cell_height - head_height - 28.0,
vertical_bars: true,
};
let frame = TimeFrame {
plot,
span,
slots: slots.clone(),
zone,
precision,
tick_step: spec.time_axis.step,
sub_day: spec.observed_precision(zone) == Precision::Minute,
unit: spec.axis_unit(pane_index).map(str::to_owned),
scale: if spec.independent_axes {
time_scale(spec, zone, Some(pane_index))
} else {
shared
},
style: spec.number_style(),
reversed: spec.time_axis.reverse,
};
push_panel_head(head, title_lines, plot, cell_top, &mut elements);
let unit = fitted_unit(
spec.axis_unit(pane_index),
&frame,
cell_left,
"/valueAxis/unit",
warnings,
metrics,
);
push_value_grid(&frame, unit.as_deref(), &mut elements);
push_time_ticks(&frame, max_ticks, true, width, metrics, &mut elements);
elements.push(frame.hook(pane_index));
draw_pane(
spec,
pane_index,
&frame,
Detail::Compact,
&mut elements,
warnings,
metrics,
);
}
if cell_height - head_height - 28.0 < 60.0 {
warnings.push(ChartWarning::new(
"dense_chart",
"/height",
"the panels are less than 60 pixels tall; raise height or use more columns",
));
}
push_multiples_time_title(spec, &mut elements, warnings, metrics);
Scene {
width: spec.width,
height: spec.height,
elements,
}
}
fn multiples_gutter(
spec: &ChartSpec,
zone: TimeZone,
warnings: &mut Vec<ChartWarning>,
metrics: &impl TextMetrics,
) -> f64 {
let gutter = f64::from(PANEL_GUTTER);
let style = spec.number_style();
let panes: Vec<Option<usize>> = if spec.independent_axes {
(0..spec.panes.len()).map(Some).collect()
} else {
vec![None]
};
let labels: Vec<String> = panes
.into_iter()
.flat_map(|pane| {
let scale = time_scale(spec, zone, pane);
scale
.ticks()
.map(move |value| scale.tick_label(value, style))
.collect::<Vec<_>>()
})
.collect();
let width = |text: &str| super::WithReserve(metrics).width(text, LABEL_SIZE);
let plain = labels.iter().map(|label| width(label)).fold(0.0, f64::max);
let with_unit = spec.value_axis.unit.as_deref().map_or(plain, |unit| {
labels
.iter()
.map(|label| width(&format!("{label} {unit}")))
.fold(0.0, f64::max)
});
let cell =
(f64::from(spec.width) - 2.0 * MULTIPLES_MARGIN) / f64::from(spec.multiples_columns());
let most = (cell * 0.4).max(gutter);
if plain + 16.0 > most {
warnings.push(ChartWarning::new(
"dense_chart",
"/columns",
"the value tick labels need more room than the panels leave them; use fewer columns or a wider chart",
));
}
gutter.max(with_unit + 16.0).min(most)
}