use std::collections::BTreeMap;
use crate::{
diverging,
error::ChartWarning,
layout::{count, format_value, push_title, title_extra},
metrics::TextMetrics,
scene::{Element, Rect, Scene, Text, TextAnchor},
spec::{CalendarLayout, CalendarSpec, ChartSpec, Diverging, NumberStyle, calendar_date},
text::{self, Words},
time::{civil_from_days, days_from_civil, days_in_month},
};
const GUTTER: f64 = 44.0;
const MARGIN: f64 = 16.0;
const GAP: f64 = 2.0;
const KEY_SPACE: f64 = 48.0;
fn values_by_day(calendar: &CalendarSpec) -> (BTreeMap<i64, f64>, i64) {
let year = i64::from(calendar.year);
let start = days_from_civil(year, 1, 1);
let length = days_from_civil(year + 1, 1, 1) - start;
let values = calendar
.days
.iter()
.filter_map(|day| {
let (_, month, day_of_month) = calendar_date(&day.date)?;
Some((
days_from_civil(year, month, day_of_month) - start,
day.value?,
))
})
.collect();
(values, length)
}
pub(crate) fn layout(
spec: &ChartSpec,
warnings: &mut Vec<ChartWarning>,
metrics: &impl TextMetrics,
) -> Scene {
let calendar = spec
.calendar
.as_ref()
.expect("validated calendar charts carry a calendar block");
let width = f64::from(spec.width);
let height = f64::from(spec.height);
let mut elements = Vec::new();
push_title(
&mut elements,
&spec.title,
MARGIN,
width - 2.0 * MARGIN,
metrics,
warnings,
);
let top = 72.0 + title_extra(&spec.title, width - 2.0 * MARGIN, metrics);
let (values, length) = values_by_day(calendar);
let year = i64::from(calendar.year);
let cells = Cells {
grid: Grid {
left: MARGIN + GUTTER,
top,
width: width - 2.0 * MARGIN - GUTTER,
height: height - top - KEY_SPACE,
},
scale: calendar.diverging(),
values,
year,
start: days_from_civil(year, 1, 1),
style: spec.number_style(),
words: spec.locale.words(),
};
let labels = match calendar.layout {
CalendarLayout::Months => cells.push_months(&mut elements),
CalendarLayout::Weeks => cells.push_weeks(&mut elements, length),
};
elements.extend(labels.into_iter().map(Element::Text));
diverging::push_key(
&mut elements,
cells.grid.left + cells.grid.width - diverging::key_width(),
height - KEY_SPACE + 12.0,
&cells.scale,
cells.style,
);
Scene {
width: spec.width,
height: spec.height,
elements,
}
}
struct Cells {
grid: Grid,
scale: Diverging,
values: BTreeMap<i64, f64>,
year: i64,
start: i64,
style: NumberStyle,
words: &'static Words,
}
impl Cells {
fn push(&self, elements: &mut Vec<Element>, at: (i64, i64), size: (i64, i64), day: i64) {
let size_x = self.grid.width / small(size.0);
let size_y = self.grid.height / small(size.1);
let (_, month, day_of_month) = civil_from_days(self.start + day);
let date = format!("{:04}-{month:02}-{day_of_month:02}", self.year);
let value = self.values.get(&day).copied();
elements.push(Element::Rect(Rect {
x: self.grid.left + size_x * small(at.0) + GAP / 2.0,
y: self.grid.top + size_y * small(at.1) + GAP / 2.0,
width: size_x - GAP,
height: size_y - GAP,
class: value.map_or("chartlet-calendar-empty", |value| {
diverging::CLASSES[self.scale.step(value)]
}),
series_index: None,
style_index: None,
tooltip: Some(value.map_or_else(
|| format!("{date}: {}", self.words.no_value),
|value| format!("{date}: {}", format_value(value, self.style)),
)),
}));
}
fn push_months(&self, elements: &mut Vec<Element>) -> Vec<Text> {
for month in 1..=12_u32 {
for day_of_month in 1..=days_in_month(self.year, month) {
let day = days_from_civil(self.year, month, day_of_month) - self.start;
self.push(
elements,
(i64::from(day_of_month) - 1, i64::from(month) - 1),
(31, 12),
day,
);
}
}
let size_x = self.grid.width / 31.0;
let size_y = self.grid.height / 12.0;
let mut labels: Vec<Text> = self
.words
.months
.iter()
.enumerate()
.map(|(index, name)| Text {
x: self.grid.left - 8.0,
y: self.grid.top + size_y * (count(index) + 0.5) + 4.0,
class: "chartlet-label",
anchor: TextAnchor::End,
content: (*name).to_owned(),
})
.collect();
labels.extend([1_u32, 5, 10, 15, 20, 25, 31].map(|day_of_month| Text {
x: self.grid.left + size_x * (f64::from(day_of_month) - 0.5),
y: self.grid.top - 8.0,
class: "chartlet-tick",
anchor: TextAnchor::Middle,
content: day_of_month.to_string(),
}));
labels
}
fn push_weeks(&self, elements: &mut Vec<Element>, length: i64) -> Vec<Text> {
let first_weekday = (self.start + 3).rem_euclid(7);
let columns = (length - 1 + first_weekday) / 7 + 1;
for day in 0..length {
let slot = day + first_weekday;
self.push(elements, (slot / 7, slot % 7), (columns, 7), day);
}
let size_x = self.grid.width / small(columns);
let size_y = self.grid.height / 7.0;
let mut labels: Vec<Text> = self
.words
.weekdays
.iter()
.enumerate()
.step_by(2)
.map(|(index, name)| Text {
x: self.grid.left - 8.0,
y: self.grid.top + size_y * (count(index) + 0.5) + 4.0,
class: "chartlet-label",
anchor: TextAnchor::End,
content: (*name).to_owned(),
})
.collect();
labels.extend(self.words.months.iter().enumerate().map(|(index, name)| {
let month = u32::try_from(index + 1).expect("twelve months");
let column = (days_from_civil(self.year, month, 1) - self.start + first_weekday) / 7;
Text {
x: self.grid.left + size_x * small(column),
y: self.grid.top - 8.0,
class: "chartlet-tick",
anchor: TextAnchor::Start,
content: (*name).to_owned(),
}
}));
labels
}
}
fn small(value: i64) -> f64 {
count(usize::try_from(value).expect("grid coordinates are small and positive"))
}
struct Grid {
left: f64,
top: f64,
width: f64,
height: f64,
}
pub(crate) fn description(spec: &ChartSpec) -> String {
let calendar = spec
.calendar
.as_ref()
.expect("validated calendar charts carry a calendar block");
let scale = calendar.diverging();
let show = |value| format_value(value, spec.number_style());
let words = spec.locale.words();
let (values, length) = values_by_day(calendar);
let days: Vec<(&str, f64)> = calendar
.sorted_days()
.into_iter()
.filter_map(|day| day.value.map(|value| (day.date.as_str(), value)))
.collect();
let lowest = days
.iter()
.min_by(|a, b| a.1.total_cmp(&b.1))
.expect("validated calendars hold a value");
let highest = days
.iter()
.max_by(|a, b| a.1.total_cmp(&b.1))
.expect("validated calendars hold a value");
let missing = length - i64::try_from(values.len()).expect("a year has at most 366 days");
let scale = text::diverging_scale(
spec.locale,
&show(scale.reference),
&show(scale.min),
&show(scale.max),
);
format!(
"{} {}: {} ({}). {}: {} ({}).",
text::calendar_opening(
spec.locale,
calendar.year,
calendar.layout == CalendarLayout::Weeks,
&scale,
usize::try_from(length - missing).expect("a year has at most 366 days"),
length,
),
words.lowest,
show(lowest.1),
lowest.0,
words.highest,
show(highest.1),
highest.0,
)
}