use serde::{Deserialize, Serialize};
use super::{ChartSpec, DiagramOrientation, sequence::is_identifier, validate_text};
use crate::error::{ChartError, ChartWarning};
pub(crate) const MAX_NODES: usize = 150;
pub(crate) const MAX_DEPTH: usize = 12;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TreeSpec {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub nodes: Vec<TreeNodeSpec>,
#[serde(default, skip_serializing_if = "DiagramOrientation::is_auto")]
pub orientation: DiagramOrientation,
#[serde(default, skip_serializing_if = "TreeLayout::is_layered")]
pub layout: TreeLayout,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub distances: Option<DistancesSpec>,
#[serde(default, skip_serializing_if = "Linkage::is_average")]
pub linkage: Linkage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DistancesSpec {
pub labels: Vec<String>,
pub matrix: Vec<Vec<f64>>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Linkage {
#[default]
Average,
Complete,
Single,
}
impl Linkage {
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_average(&self) -> bool {
matches!(self, Self::Average)
}
}
pub(crate) const MAX_LEAVES: usize = 40;
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TreeLayout {
#[default]
Layered,
Radial,
}
impl TreeLayout {
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_layered(&self) -> bool {
matches!(self, Self::Layered)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TreeNodeSpec {
pub id: String,
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sublabel: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub link: Option<String>,
#[serde(default, skip_serializing_if = "TreeNodeKind::is_unit")]
pub kind: TreeNodeKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub partner: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub collapsed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parts: Vec<super::PartSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<super::DiagramRole>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TreeNodeKind {
#[default]
Unit,
Person,
External,
}
impl TreeNodeKind {
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_unit(&self) -> bool {
matches!(self, Self::Unit)
}
}
impl TreeSpec {
pub(crate) fn node(&self, id: &str) -> Option<usize> {
self.nodes.iter().position(|node| node.id == id)
}
pub(crate) fn head_of(&self, node: usize) -> Option<usize> {
self.nodes[node]
.partner
.as_deref()
.map(|id| self.node(id).expect("validated"))
}
pub(crate) fn partner_of(&self, node: usize) -> Option<usize> {
let id = self.nodes[node].id.as_str();
(0..self.nodes.len()).find(|other| self.nodes[*other].partner.as_deref() == Some(id))
}
pub(crate) fn root(&self) -> usize {
(0..self.nodes.len())
.find(|node| self.nodes[*node].parent.is_none() && self.nodes[*node].partner.is_none())
.expect("a validated tree has a root")
}
pub(crate) fn parent_of(&self, node: usize) -> Option<usize> {
let parent = self.node(self.nodes[node].parent.as_deref()?)?;
Some(self.head_of(parent).unwrap_or(parent))
}
pub(crate) fn children(&self, node: usize) -> Vec<usize> {
(0..self.nodes.len())
.filter(|child| self.parent_of(*child) == Some(node))
.collect()
}
pub(crate) fn depth(&self, node: usize) -> usize {
let mut depth = 0;
let mut at = self.head_of(node).unwrap_or(node);
while let Some(parent) = self.parent_of(at) {
depth += 1;
at = parent;
}
depth
}
fn below(&self, node: usize) -> Vec<usize> {
let head = self.head_of(node).unwrap_or(node);
let mut found = Vec::new();
let mut stack = self.children(head);
while let Some(next) = stack.pop() {
if found.contains(&next) {
continue;
}
found.push(next);
if let Some(partner) = self.partner_of(next) {
found.push(partner);
}
stack.extend(self.children(next));
}
found
}
pub(crate) fn hidden_below(&self, node: usize) -> usize {
if self.nodes[node].collapsed {
self.below(node).len()
} else {
0
}
}
pub(crate) fn collapsed_view(&self) -> Option<(TreeSpec, Vec<usize>)> {
if !self.nodes.iter().any(|node| node.collapsed) {
return None;
}
let mut hidden = vec![false; self.nodes.len()];
for node in 0..self.nodes.len() {
if self.nodes[node].collapsed && !hidden[node] {
for below in self.below(node) {
hidden[below] = true;
}
}
}
let kept: Vec<usize> = (0..self.nodes.len())
.filter(|node| !hidden[*node])
.collect();
let nodes = kept
.iter()
.map(|&node| {
let mut copy = self.nodes[node].clone();
let count = self.hidden_below(node);
if count > 0 {
copy.sublabel = Some(match ©.sublabel {
Some(sublabel) => format!("{sublabel} · +{count}"),
None => format!("+{count}"),
});
}
copy
})
.collect();
Some((
TreeSpec {
nodes,
orientation: self.orientation,
layout: self.layout,
value_label: self.value_label.clone(),
distances: None,
linkage: self.linkage,
},
kept,
))
}
pub(crate) fn reading_order(&self) -> Vec<usize> {
let mut order = Vec::with_capacity(self.nodes.len());
let mut stack = vec![self.root()];
while let Some(node) = stack.pop() {
order.push(node);
order.extend(self.partner_of(node));
stack.extend(self.children(node).into_iter().rev());
}
order
}
}
impl ChartSpec {
pub(super) fn validate_tree(&self) -> Result<Vec<ChartWarning>, ChartError> {
self.reject_map_options("a tree", "tree.nodes")?;
let Some(tree) = &self.tree else {
return Err(ChartError::new(
"missing_tree",
"/tree",
"a tree requires a tree block",
));
};
if let Some(distances) = &tree.distances {
tree.validate_dendrogram(distances)?;
return Ok(Vec::new());
}
if tree.linkage != Linkage::Average {
return Err(ChartError::new(
"option_not_supported",
"/tree/linkage",
"linkage belongs to a tree built from distances",
));
}
if tree.nodes.is_empty() {
return Err(ChartError::new(
"empty_data",
"/tree/nodes",
"provide at least one node",
));
}
if tree.nodes.len() > MAX_NODES {
return Err(ChartError::new(
"too_many_nodes",
"/tree/nodes",
format!("at most {MAX_NODES} nodes are supported"),
));
}
tree.validate_nodes()?;
tree.validate_structure()?;
if let Some(label) = &tree.value_label {
validate_text(label, "/tree/valueLabel", 40)?;
}
super::flow::validate_parts(
tree.nodes
.iter()
.enumerate()
.map(|(index, node)| (node.parts.as_slice(), format!("/tree/nodes/{index}/parts"))),
)?;
if tree.layout == TreeLayout::Radial {
tree.validate_radial()?;
}
let mut warnings = Vec::new();
for (index, node) in tree.nodes.iter().enumerate() {
if node.collapsed && tree.below(index).is_empty() {
warnings.push(ChartWarning::new(
"nothing_to_collapse",
format!("/tree/nodes/{index}/collapsed"),
"nothing hangs below this node, so collapsing it changes nothing",
));
}
}
Ok(warnings)
}
}
impl TreeSpec {
fn validate_dendrogram(&self, distances: &DistancesSpec) -> Result<(), ChartError> {
let refuse = |field: &str| {
Err(ChartError::new(
"option_not_supported",
format!("/tree/{field}"),
format!("a dendrogram builds its tree from the distances; remove {field}"),
))
};
if !self.nodes.is_empty() {
return refuse("nodes");
}
if self.layout != TreeLayout::Layered {
return refuse("layout");
}
if !self.orientation.is_auto() {
return refuse("orientation");
}
if self.value_label.is_some() {
return refuse("valueLabel");
}
let count = distances.labels.len();
if count < 2 {
return Err(ChartError::new(
"empty_data",
"/tree/distances/labels",
"a dendrogram joins at least two items",
));
}
if count > MAX_LEAVES {
return Err(ChartError::new(
"too_many_nodes",
"/tree/distances/labels",
format!("at most {MAX_LEAVES} items are supported"),
));
}
for (index, label) in distances.labels.iter().enumerate() {
let path = format!("/tree/distances/labels/{index}");
validate_text(label, &path, 60)?;
if distances.labels[..index].contains(label) {
return Err(ChartError::new(
"duplicate_id",
path,
format!("the label \"{label}\" is already taken by an earlier item"),
));
}
}
let invalid = |path: String, message: &str| {
Err(ChartError::new("invalid_matrix", path, message.to_owned()))
};
if distances.matrix.len() != count {
return invalid(
"/tree/distances/matrix".to_owned(),
"the matrix has one row per label",
);
}
for (row, values) in distances.matrix.iter().enumerate() {
if values.len() != count {
return invalid(
format!("/tree/distances/matrix/{row}"),
"every row has one distance per label",
);
}
}
for (row, values) in distances.matrix.iter().enumerate() {
for (column, value) in values.iter().enumerate() {
let path = format!("/tree/distances/matrix/{row}/{column}");
if row == column && *value != 0.0 {
return invalid(path, "the distance of an item to itself is 0");
}
if *value < 0.0 {
return invalid(path, "a distance is at least 0");
}
if (value - distances.matrix[column][row]).abs() > 1e-9 {
return invalid(
path,
"the distance from a to b is the distance from b to a; the matrix is symmetric",
);
}
}
}
Ok(())
}
fn validate_radial(&self) -> Result<(), ChartError> {
if !self.orientation.is_auto() {
return Err(ChartError::new(
"option_not_supported",
"/tree/orientation",
"a radial tree grows outward in every direction; remove orientation or use the layered layout",
));
}
for (index, node) in self.nodes.iter().enumerate() {
let refuse = |field: &str, message: &str| {
Err(ChartError::new(
"option_not_supported",
format!("/tree/nodes/{index}/{field}"),
message.to_owned(),
))
};
if node.partner.is_some() {
return refuse(
"partner",
"a radial tree has no room for couples side by side; use the layered layout",
);
}
if node.link.is_some() {
return refuse(
"link",
"a radial tree writes no labels on its spokes; put the text in a sublabel or use the layered layout",
);
}
}
Ok(())
}
fn validate_partner(&self, index: usize, partner: &str, path: &str) -> Result<(), ChartError> {
let node = &self.nodes[index];
let invalid = |message: String| {
Err(ChartError::new(
"invalid_partner",
format!("{path}/partner"),
message,
))
};
let Some(other) = self.node(partner) else {
return Err(ChartError::new(
"unknown_node",
format!("{path}/partner"),
format!("no node has the id \"{partner}\""),
));
};
if other == index {
return invalid("a node cannot be its own partner".to_owned());
}
if node.parent.is_some() {
return invalid("a partner stands beside its partner, not below a parent".to_owned());
}
if self.nodes[other].partner.is_some() {
return invalid(format!(
"\"{partner}\" is itself a partner; a couple has one head and one partner"
));
}
if self
.nodes
.iter()
.enumerate()
.any(|(at, other)| at != index && other.partner.as_deref() == Some(partner))
{
return invalid(format!(
"another node is already the partner of \"{partner}\""
));
}
Ok(())
}
fn validate_nodes(&self) -> Result<(), ChartError> {
let tree = self;
for (index, node) in tree.nodes.iter().enumerate() {
let path = format!("/tree/nodes/{index}");
if !is_identifier(&node.id) {
return Err(ChartError::new(
"invalid_id",
format!("{path}/id"),
"use 1–64 ASCII letters, digits, hyphens, or underscores, starting with a letter",
));
}
if tree.nodes[..index].iter().any(|other| other.id == node.id) {
return Err(ChartError::new(
"duplicate_id",
format!("{path}/id"),
format!("the id \"{}\" is already taken by an earlier node", node.id),
));
}
validate_text(&node.label, &format!("{path}/label"), 60)?;
if let Some(sublabel) = &node.sublabel {
validate_text(sublabel, &format!("{path}/sublabel"), 60)?;
}
if let Some(link) = &node.link {
validate_text(link, &format!("{path}/link"), 40)?;
if node.parent.is_none() {
return Err(ChartError::new(
"option_not_supported",
format!("{path}/link"),
"the root has no parent to be joined to; remove the link",
));
}
}
if let Some(partner) = &node.partner {
tree.validate_partner(index, partner, &path)?;
}
if let Some(parent) = &node.parent
&& tree.node(parent).is_none()
{
return Err(ChartError::new(
"unknown_node",
format!("{path}/parent"),
format!("no node has the id \"{parent}\""),
));
}
}
Ok(())
}
fn validate_structure(&self) -> Result<(), ChartError> {
let tree = self;
let roots: Vec<usize> = (0..tree.nodes.len())
.filter(|index| {
tree.nodes[*index].parent.is_none() && tree.nodes[*index].partner.is_none()
})
.collect();
if roots.len() != 1 {
let (path, message) = match roots.get(1) {
Some(second) => (
format!("/tree/nodes/{second}"),
"a tree has exactly one root; give this node a parent".to_owned(),
),
None => (
"/tree/nodes".to_owned(),
"a tree has exactly one root: leave the parent off one node".to_owned(),
),
};
return Err(ChartError::new("invalid_root", path, message));
}
for index in 0..tree.nodes.len() {
let mut at = index;
let mut steps = 0;
while let Some(parent) = tree.parent_of(at) {
at = parent;
steps += 1;
if steps > tree.nodes.len() {
return Err(ChartError::new(
"circular_parent",
format!("/tree/nodes/{index}/parent"),
"the parents of these nodes run in a circle",
));
}
}
if steps > MAX_DEPTH {
return Err(ChartError::new(
"tree_too_deep",
format!("/tree/nodes/{index}/parent"),
format!("at most {MAX_DEPTH} levels below the root are supported"),
));
}
}
Ok(())
}
}