use std::collections::HashMap;
use std::sync::Arc;
use crate::plot::diff::{diff_columns, diff_positional, KeyIndex};
use crate::plot::value::{DataColumn, Value};
use super::{empty_datacolumn_like, Channel, ChannelDecl, ExpectedOutput, Keys};
pub struct GeomState {
pub(crate) keys: Keys,
pub(crate) channels: HashMap<String, Channel>,
pub(crate) prev_keys: Keys,
pub(crate) prev_channels: HashMap<String, Channel>,
pub(crate) enter: Vec<usize>,
pub(crate) update: Vec<(usize, usize)>,
pub(crate) exit: Vec<Value>,
pub(crate) dirty: bool,
pub(crate) declared: Vec<ChannelDecl>,
}
impl GeomState {
pub fn from_builder(
keys_opt: Option<DataColumn>,
channels: HashMap<String, Channel>,
n: usize,
keys_strategy: KeysStrategy,
declared: Vec<ChannelDecl>,
) -> Self {
let keys = build_keys(keys_opt, n, keys_strategy);
let prev_keys = keys.empty_like();
let prev_channels = empty_channels_like(&channels);
Self {
keys,
channels,
prev_keys,
prev_channels,
enter: Vec::new(),
update: Vec::new(),
exit: Vec::new(),
dirty: true,
declared,
}
}
pub fn len(&self) -> usize {
self.keys.len()
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
pub fn set(&mut self, channel: impl Into<String>, value: impl Into<Channel>) {
let name: String = channel.into();
let value: Channel = value.into();
if let Some(len) = value.data_len() {
if len != self.keys.len() {
panic!(
"GeomState::set: \"{name}\" length {len} does not match row count {}",
self.keys.len()
);
}
}
self.channels.insert(name, value);
self.dirty = true;
}
pub fn replace_current_with(&mut self, new: GeomState) {
self.prev_keys = std::mem::replace(&mut self.keys, new.keys);
self.prev_channels = std::mem::replace(&mut self.channels, new.channels);
self.declared = new.declared;
self.dirty = true;
}
pub fn rebuild_diff_against_previous(&mut self) {
if !self.dirty {
return;
}
let (enter, update, exit) = match (&self.prev_keys, &self.keys) {
(Keys::Explicit(prev_col), Keys::Explicit(next_col)) => {
let idx = KeyIndex::build(prev_col);
diff_columns(prev_col, &idx, next_col)
}
_ => diff_positional(self.prev_keys.len(), self.keys.len()),
};
self.enter = enter;
self.update = update;
self.exit = exit;
self.prev_keys = self.keys.clone();
self.prev_channels = self.channels.clone();
self.dirty = false;
}
pub fn rebuild_grouped_diff(
&mut self,
prev_first_rows: &[usize],
next_first_rows: &[usize],
geom_name: &str,
) {
let (enter, update, exit) = match (&self.prev_keys, &self.keys) {
(Keys::Explicit(prev_col), Keys::Explicit(next_col)) => {
let prev_unique = super::marks::unique_values_at_first_rows(
prev_col,
prev_first_rows.iter().copied(),
geom_name,
);
let next_unique = super::marks::unique_values_at_first_rows(
next_col,
next_first_rows.iter().copied(),
geom_name,
);
let idx = KeyIndex::build(&prev_unique);
diff_columns(&prev_unique, &idx, &next_unique)
}
_ => diff_positional(prev_first_rows.len(), next_first_rows.len()),
};
self.enter = enter;
self.update = update;
self.exit = exit;
self.prev_keys = self.keys.clone();
self.prev_channels = self.channels.clone();
self.dirty = false;
}
}
#[derive(Clone, Copy, Debug)]
pub enum KeysStrategy {
PerRow,
OneMark,
}
fn build_keys(keys_opt: Option<DataColumn>, n: usize, strategy: KeysStrategy) -> Keys {
match (keys_opt, strategy) {
(Some(k), _) => {
if k.len() != n {
panic!(
"build_keys: keys length {} does not match row count {n}",
k.len()
);
}
Keys::Explicit(k)
}
(None, KeysStrategy::PerRow) => Keys::Positional(n),
(None, KeysStrategy::OneMark) => {
let placeholder: Arc<str> = Arc::from("_");
Keys::Explicit(DataColumn::String(vec![placeholder; n]))
}
}
}
pub fn require_data_column<'a>(
name: &str,
channels: &'a HashMap<String, Channel>,
geom_label: &str,
) -> &'a DataColumn {
match channels.get(name) {
Some(Channel::Data(c)) | Some(Channel::RawData(c)) => c,
Some(Channel::Constant(_)) | Some(Channel::RawConstant(_)) => panic!(
"{geom_label}::build: \"{name}\" must be data, not constant — positions vary per row"
),
None => panic!("{geom_label}::build: missing required channel \"{name}\""),
}
}
pub fn require_x_and_siblings(
channels: &HashMap<String, Channel>,
siblings: &[&str],
geom_label: &str,
) -> usize {
let n = require_data_column("x", channels, geom_label).len();
for &name in siblings {
let len = require_data_column(name, channels, geom_label).len();
if len != n {
panic!("{geom_label}::build: \"{name}\" length {len} does not match \"x\" length {n}");
}
}
n
}
pub fn validate_channel_lengths(channels: &HashMap<String, Channel>, n: usize, geom_label: &str) {
for (name, ch) in channels {
if let Some(len) = ch.data_len() {
if len != n {
panic!("{geom_label}::build: \"{name}\" length {len} does not match row count {n}");
}
}
}
}
pub fn validate_pick_id_channel(channels: &HashMap<String, Channel>, geom_label: &str) {
let ch = match channels.get("pick_id") {
Some(c) => c,
None => return,
};
let check = |v: &Value, where_: &str| {
match v.as_number() {
Some(n) if n.is_finite() && n >= 0.0 && n <= 0xFF_FFFF as f64 && n.trunc() == n => {}
Some(n) => panic!(
"{geom_label}::build: \"pick_id\" {where_} must be a non-negative integer ≤ 0xFFFFFF, got {n}"
),
None => panic!(
"{geom_label}::build: \"pick_id\" {where_} must be numeric (Number/Date/DateTime/Time/Duration), got {v:?}"
),
}
};
match ch {
Channel::Constant(v) | Channel::RawConstant(v) => check(v, "constant"),
Channel::Data(col) | Channel::RawData(col) => {
for i in 0..col.len() {
check(&col.get(i), "data column");
}
}
}
}
pub fn validate_known_channels(
channels: &HashMap<String, Channel>,
catalog: &[(&'static str, ExpectedOutput)],
geom_label: &str,
) {
let mut unknown: Vec<&str> = channels
.keys()
.map(String::as_str)
.filter(|name| !catalog.iter().any(|(known, _)| known == name))
.collect();
if unknown.is_empty() {
return;
}
unknown.sort_unstable();
let names: Vec<String> = unknown.iter().map(|n| format!("\"{n}\"")).collect();
let label = match unknown.len() {
1 => "channel",
_ => "channels",
};
panic!(
"{geom_label}::build: unknown {label} {} — not declared by this geom",
names.join(", ")
);
}
pub fn filter_declared(
channels: &HashMap<String, Channel>,
catalog: &[(&'static str, ExpectedOutput)],
) -> Vec<ChannelDecl> {
let mut out = Vec::with_capacity(catalog.len());
for (name, expected) in catalog {
if let Some(ch) = channels.get(*name) {
out.push(ChannelDecl {
name,
data_bound: ch.is_data(),
expected_output: *expected,
});
}
}
out.sort_by_key(|d| d.name);
out
}
pub fn finalize_state(
keys_opt: Option<DataColumn>,
channels: HashMap<String, Channel>,
n: usize,
strategy: KeysStrategy,
catalog: &[(&'static str, ExpectedOutput)],
geom_label: &str,
) -> GeomState {
validate_known_channels(&channels, catalog, geom_label);
validate_channel_lengths(&channels, n, geom_label);
validate_pick_id_channel(&channels, geom_label);
let declared = filter_declared(&channels, catalog);
GeomState::from_builder(keys_opt, channels, n, strategy, declared)
}
fn empty_channels_like(channels: &HashMap<String, Channel>) -> HashMap<String, Channel> {
channels
.iter()
.map(|(name, ch)| {
let prev = match ch {
Channel::Constant(v) => Channel::Constant(v.clone()),
Channel::Data(col) => Channel::Data(empty_datacolumn_like(col)),
Channel::RawConstant(v) => Channel::RawConstant(v.clone()),
Channel::RawData(col) => Channel::RawData(empty_datacolumn_like(col)),
};
(name.clone(), prev)
})
.collect()
}
#[macro_export]
#[doc(hidden)]
macro_rules! impl_geom_inherents {
($ty:ident) => {
impl $ty {
pub fn builder() -> $crate::plot::GeomBuilder<Self> {
$crate::plot::GeomBuilder::new()
}
pub fn len(&self) -> usize {
self.state.len()
}
pub fn is_empty(&self) -> bool {
self.state.is_empty()
}
pub fn has_explicit_keys(&self) -> bool {
self.state.keys.is_explicit()
}
pub fn set(
&mut self,
channel: impl Into<String>,
value: impl Into<$crate::plot::Channel>,
) {
self.state.set(channel, value);
}
pub fn update(&mut self, f: impl FnOnce(&mut $crate::plot::GeomBuilder<Self>)) {
let carry_keys = match &self.state.keys {
$crate::plot::Keys::Explicit(col) => Some(col.clone()),
$crate::plot::Keys::Positional(_) => None,
};
let mut b =
$crate::plot::GeomBuilder::from_parts(carry_keys, self.state.channels.clone());
f(&mut b);
let new = b.build();
self.state.replace_current_with(new.state);
<Self as $crate::plot::Geom>::invalidate_caches(self);
}
}
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! impl_geom_inherents_grouped {
($ty:ident) => {
impl $ty {
pub fn builder() -> $crate::plot::GeomBuilder<Self> {
$crate::plot::GeomBuilder::new()
}
pub fn len(&self) -> usize {
self.state.len()
}
pub fn is_empty(&self) -> bool {
self.state.is_empty()
}
pub fn set(
&mut self,
channel: impl Into<String>,
value: impl Into<$crate::plot::Channel>,
) {
self.state.set(channel, value);
}
pub fn update(&mut self, f: impl FnOnce(&mut $crate::plot::GeomBuilder<Self>)) {
let carry_keys = match &self.state.keys {
$crate::plot::Keys::Explicit(col) => Some(col.clone()),
$crate::plot::Keys::Positional(_) => None,
};
let mut b =
$crate::plot::GeomBuilder::from_parts(carry_keys, self.state.channels.clone());
f(&mut b);
let new = b.build();
self.state.replace_current_with(new.state);
<Self as $crate::plot::Geom>::invalidate_caches(self);
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plot::value::Value;
const CATALOG: &[(&str, ExpectedOutput)] = &[
("x", ExpectedOutput::Numbers),
("y", ExpectedOutput::Numbers),
("fill", ExpectedOutput::Colors),
("pick_id", ExpectedOutput::Numbers),
];
fn channels(entries: Vec<(&str, Channel)>) -> HashMap<String, Channel> {
entries
.into_iter()
.map(|(name, ch)| (name.to_string(), ch))
.collect()
}
fn xy(n: usize) -> HashMap<String, Channel> {
channels(vec![
("x", Channel::Data(DataColumn::F64(vec![0.0; n]))),
("y", Channel::Data(DataColumn::F64(vec![0.0; n]))),
])
}
fn finalize(ch: HashMap<String, Channel>, n: usize, strategy: KeysStrategy) -> GeomState {
finalize_state(None, ch, n, strategy, CATALOG, "TestGeom")
}
#[test]
#[should_panic(expected = "unknown channel \"colour\" — not declared by this geom")]
fn finalize_state_rejects_a_channel_the_geom_does_not_declare() {
let mut ch = xy(2);
ch.insert("colour".to_string(), Channel::Constant(Value::Number(1.0)));
finalize(ch, 2, KeysStrategy::PerRow);
}
#[test]
#[should_panic(expected = "unknown channels \"colour\", \"shape\"")]
fn finalize_state_lists_every_unknown_channel_sorted() {
let mut ch = xy(2);
ch.insert("shape".to_string(), Channel::Constant(Value::Number(1.0)));
ch.insert("colour".to_string(), Channel::Constant(Value::Number(1.0)));
finalize(ch, 2, KeysStrategy::PerRow);
}
#[test]
#[should_panic(expected = "\"y\" length 2 does not match row count 3")]
fn finalize_state_rejects_a_short_data_column() {
let ch = channels(vec![
("x", Channel::Data(DataColumn::F64(vec![0.0; 3]))),
("y", Channel::Data(DataColumn::F64(vec![0.0; 2]))),
]);
finalize(ch, 3, KeysStrategy::PerRow);
}
#[test]
#[should_panic(expected = "\"pick_id\" data column must be a non-negative integer")]
fn finalize_state_rejects_a_fractional_pick_id() {
let mut ch = xy(2);
ch.insert(
"pick_id".to_string(),
Channel::Data(DataColumn::F64(vec![1.0, 2.5])),
);
finalize(ch, 2, KeysStrategy::PerRow);
}
#[test]
#[should_panic(expected = "missing required channel \"x\"")]
fn require_data_column_reports_a_missing_channel() {
require_data_column("x", &HashMap::new(), "TestGeom");
}
#[test]
#[should_panic(expected = "\"x\" must be data, not constant")]
fn require_data_column_rejects_a_constant_where_a_column_is_needed() {
let ch = channels(vec![("x", Channel::Constant(Value::Number(1.0)))]);
require_data_column("x", &ch, "TestGeom");
}
#[test]
#[should_panic(expected = "\"y\" length 1 does not match \"x\" length 3")]
fn require_x_and_siblings_rejects_a_sibling_of_a_different_length() {
let ch = channels(vec![
("x", Channel::Data(DataColumn::F64(vec![0.0; 3]))),
("y", Channel::Data(DataColumn::F64(vec![0.0; 1]))),
]);
require_x_and_siblings(&ch, &["y"], "TestGeom");
}
#[test]
#[should_panic(expected = "keys length 2 does not match row count 3")]
fn from_builder_rejects_a_key_column_of_the_wrong_length() {
GeomState::from_builder(
Some(DataColumn::String(vec![Arc::from("a"), Arc::from("b")])),
xy(3),
3,
KeysStrategy::PerRow,
Vec::new(),
);
}
#[test]
#[should_panic(expected = "\"fill\" length 4 does not match row count 2")]
fn set_rejects_a_column_that_does_not_match_the_row_count() {
let mut state = finalize(xy(2), 2, KeysStrategy::PerRow);
state.set(
"fill",
Channel::Data(DataColumn::F64(vec![0.0, 1.0, 2.0, 3.0])),
);
}
#[test]
fn per_row_strategy_synthesises_positional_keys() {
let state = finalize(xy(3), 3, KeysStrategy::PerRow);
match &state.keys {
Keys::Positional(n) => assert_eq!(*n, 3),
Keys::Explicit(_) => panic!("expected positional keys"),
}
assert_eq!(state.len(), 3);
assert!(!state.is_empty());
}
#[test]
fn one_mark_strategy_synthesises_a_single_shared_key() {
let state = finalize(xy(3), 3, KeysStrategy::OneMark);
match &state.keys {
Keys::Explicit(col) => {
assert_eq!(col.len(), 3);
assert!(col.get(0).key_eq(&col.get(2)));
}
Keys::Positional(_) => panic!("expected an explicit placeholder column"),
}
assert_eq!(super::super::marks::build_marks(&state.keys).len(), 1);
}
#[test]
fn a_supplied_key_column_survives_either_strategy() {
for strategy in [KeysStrategy::PerRow, KeysStrategy::OneMark] {
let keys = DataColumn::String(vec![Arc::from("a"), Arc::from("b")]);
let state = GeomState::from_builder(Some(keys), xy(2), 2, strategy, Vec::new());
match &state.keys {
Keys::Explicit(col) => assert!(col.get(0).key_eq(&Value::String(Arc::from("a")))),
Keys::Positional(_) => panic!("supplied keys must not be replaced"),
}
}
}
#[test]
fn a_fresh_state_diffs_as_all_enter() {
let mut state = finalize(xy(3), 3, KeysStrategy::PerRow);
assert!(state.dirty);
state.rebuild_diff_against_previous();
assert_eq!(state.enter, vec![0, 1, 2]);
assert!(state.update.is_empty());
assert!(state.exit.is_empty());
assert!(!state.dirty);
}
#[test]
fn filter_declared_keeps_supplied_catalog_entries_in_name_order() {
let mut ch = xy(2);
ch.insert(
"fill".to_string(),
Channel::Constant(Value::Color(crate::color::rgb(1.0, 0.0, 0.0))),
);
let state = finalize(ch, 2, KeysStrategy::PerRow);
let names: Vec<&str> = state.declared.iter().map(|d| d.name).collect();
assert_eq!(names, vec!["fill", "x", "y"]);
let fill = state.declared.iter().find(|d| d.name == "fill").unwrap();
assert!(!fill.data_bound, "a constant channel is not data-bound");
assert_eq!(fill.expected_output, ExpectedOutput::Colors);
let x = state.declared.iter().find(|d| d.name == "x").unwrap();
assert!(x.data_bound);
}
}