--- /dev/null
+// PSPP - a program for statistical analysis.
+// Copyright (C) 2025 Free Software Foundation, Inc.
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+// details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see <http://www.gnu.org/licenses/>.
+
+#![allow(dead_code)]
+use std::{
+ cell::{Cell, RefCell},
+ collections::{BTreeMap, HashMap},
+ fmt::Debug,
+ marker::PhantomData,
+ num::NonZeroUsize,
+};
+
+use displaydoc::Display;
+use enum_map::Enum;
+use indexmap::IndexMap;
+use ordered_float::OrderedFloat;
+use serde::Deserialize;
+
+use crate::{
+ data::Datum,
+ format::{F8_0, Type, UncheckedFormat},
+ output::pivot::{
+ CategoryLocator, Length, PivotTable,
+ look::{Color, VertAlign},
+ },
+};
+
+#[derive(Debug)]
+struct Ref<T> {
+ references: String,
+ _phantom: PhantomData<T>,
+}
+
+impl<T> Ref<T> {
+ fn get<'a>(&self, table: &HashMap<&str, &'a T>) -> Option<&'a T> {
+ table.get(self.references.as_str()).map(|v| &**v)
+ }
+}
+
+impl<'de, T> Deserialize<'de> for Ref<T> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ Ok(Self {
+ references: String::deserialize(deserializer)?,
+ _phantom: PhantomData,
+ })
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+struct Map(HashMap<OrderedFloat<f64>, Datum<String>>);
+
+impl Map {
+ fn apply(&self, data: &mut Vec<Datum<String>>) {
+ for value in data {
+ if let Datum::Number(Some(number)) = value
+ && let Some(to) = self.0.get(&OrderedFloat(*number))
+ {
+ *value = to.clone();
+ }
+ }
+ }
+}
+
+/// A warning decoding a legacy XML member.
+#[derive(Clone, Debug, Display, thiserror::Error)]
+pub enum GraphWarning {
+ /// Table has no data.
+ MissingData,
+
+ /// Table has variables with unresolved dependencies: {0:?}.
+ UnresolvedDependencies(Vec<String>),
+
+ /// Derived variable {variable:?} has unsupported value expression {value:?}.
+ UnsupportedValue {
+ /// Name of derived variable.
+ variable: String,
+ /// Value expression.
+ value: String,
+ },
+
+ /// Unsupported applyToConverse.
+ UnsupportedApplyToConverse,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct Visualization {
+ // Locale used for output, e.g. `en-US`.
+ #[serde(rename = "@lang")]
+ _lang: String,
+
+ /// Base style for the graph.
+ #[serde(rename = "@style")]
+ _style: Ref<Style>,
+
+ #[serde(rename = "$value")]
+ children: Vec<VisChild>,
+}
+
+impl Visualization {
+ pub fn decode_series(
+ &self,
+ data: IndexMap<String, IndexMap<String, Vec<Datum<String>>>>,
+ warn: &mut dyn FnMut(GraphWarning),
+ ) -> BTreeMap<&str, Series> {
+ let mut variables: Vec<&dyn Variable> = self
+ .children
+ .iter()
+ .filter_map(|child| child.variable())
+ .collect();
+ let mut series = BTreeMap::<&str, Series>::new();
+ while !variables.is_empty() {
+ let n = variables.len();
+ variables.retain(|variable| !variable.decode(&data, &mut series, warn));
+ if n == variables.len() {
+ warn(GraphWarning::UnresolvedDependencies(
+ variables
+ .iter()
+ .map(|variable| variable.name().into())
+ .collect(),
+ ));
+ break;
+ }
+ }
+ series
+ }
+
+ fn graph(&self) -> Result<&Graph, super::Error> {
+ for child in &self.children {
+ match child {
+ VisChild::Graph(g) => return Ok(g),
+ _ => (),
+ }
+ }
+ Err(super::Error::LegacyMissingGraph)
+ }
+
+ pub fn decode(
+ &self,
+ binary_data: IndexMap<String, IndexMap<String, Vec<Datum<String>>>>,
+ warn: &mut dyn FnMut(GraphWarning),
+ ) -> Result<PivotTable, super::Error> {
+ dbg!(self);
+ let graph = self.graph()?;
+ let series = self.decode_series(binary_data, warn);
+ let styles = self
+ .children
+ .iter()
+ .filter_map(|child| child.style())
+ .filter_map(|style| style.id.as_ref().map(|id| (id.as_str(), style)))
+ .collect::<HashMap<_, _>>();
+
+ todo!()
+ }
+}
+
+pub struct Series {
+ pub name: String,
+ label: Option<String>,
+ categories: Vec<Option<usize>>,
+ pub values: Vec<Datum<String>>,
+ affixes: Vec<Affix>,
+ coordinate_to_index: RefCell<BTreeMap<usize, CategoryLocator>>,
+ dimension_index: Cell<Option<usize>>,
+}
+
+impl Debug for Series {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Series")
+ .field("name", &self.name)
+ .finish_non_exhaustive()
+ }
+}
+
+impl Series {
+ fn new(name: String, categories: Vec<Option<usize>>, values: Vec<Datum<String>>) -> Self {
+ Self {
+ name,
+ label: None,
+ categories,
+ values,
+ affixes: Vec::new(),
+ coordinate_to_index: Default::default(),
+ dimension_index: Default::default(),
+ }
+ }
+ fn with_label(self, label: Option<String>) -> Self {
+ Self { label, ..self }
+ }
+ fn with_affixes(self, affixes: Vec<Affix>) -> Self {
+ Self { affixes, ..self }
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum VisChild {
+ SourceVariable(SourceVariable),
+ DerivedVariable(DerivedVariable),
+ CategoricalDomain(CategoricalDomain),
+ Graph(Graph),
+ LabelFrame(LabelFrame),
+ Container(Container),
+ Style(Style),
+ #[serde(other)]
+ Other,
+}
+
+impl VisChild {
+ fn variable(&self) -> Option<&dyn Variable> {
+ match self {
+ VisChild::SourceVariable(source_variable) => Some(source_variable),
+ VisChild::DerivedVariable(derived_variable) => Some(derived_variable),
+ _ => None,
+ }
+ }
+ fn style(&self) -> Option<&Style> {
+ match self {
+ Self::Style(style) => Some(style),
+ _ => None,
+ }
+ }
+}
+
+trait Variable {
+ fn decode<'a>(
+ &'a self,
+ data: &IndexMap<String, IndexMap<String, Vec<Datum<String>>>>,
+ series: &mut BTreeMap<&'a str, Series>,
+ warn: &mut dyn FnMut(GraphWarning),
+ ) -> bool;
+ fn name(&self) -> &str;
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SourceVariable {
+ #[serde(rename = "@id")]
+ id: String,
+
+ /// The `source-name` in the `tableData.bin` member.
+ #[serde(rename = "@source")]
+ source: String,
+
+ /// The name of a variable within the source, corresponding to the
+ /// `variable-name` in the `tableData.bin` member.
+ #[serde(rename = "@sourceName")]
+ variable: String,
+
+ /// Variable label, if any.
+ #[serde(rename = "@label")]
+ label: Option<String>,
+
+ /// A variable whose string values correspond one-to-one with the values of
+ /// this variable and are suitable as value labels.
+ #[serde(rename = "@labelVariable")]
+ label_variable: Option<Ref<SourceVariable>>,
+
+ format: Option<Format>,
+ string_format: Option<StringFormat>,
+}
+
+impl SourceVariable {
+ fn affixes(&self) -> &[Affix] {
+ if let Some(format) = &self.format {
+ &format.affixes
+ } else if let Some(string_format) = &self.string_format {
+ &string_format.affixes
+ } else {
+ &[]
+ }
+ }
+}
+impl Variable for SourceVariable {
+ fn decode<'a>(
+ &'a self,
+ data: &IndexMap<String, IndexMap<String, Vec<Datum<String>>>>,
+ series: &mut BTreeMap<&'a str, Series>,
+ _warn: &mut dyn FnMut(GraphWarning),
+ ) -> bool {
+ let label_series = if let Some(label_variable) = &self.label_variable {
+ if let Some(label_series) = series.get(label_variable.references.as_str()) {
+ Some(label_series)
+ } else {
+ return false;
+ }
+ } else {
+ None
+ };
+
+ let (categories, mut data) = if let Some(source) = data.get(&self.source)
+ && let Some(values) = source.get(&self.variable)
+ {
+ let categories = values
+ .iter()
+ .map(|value| {
+ value
+ .as_number()
+ .flatten()
+ .and_then(|v| (v >= 0.0 && v < usize::MAX as f64).then_some(v as usize))
+ })
+ .collect();
+ (categories, values.clone())
+ } else {
+ (Vec::new(), Vec::new())
+ };
+ if let Some(format) = &self.format {
+ format.mapping().apply(&mut data);
+ } else if let Some(string_format) = &self.string_format {
+ string_format.mapping().apply(&mut data);
+ };
+ if let Some(label_series) = label_series {
+ let format = self.format.as_ref().map_or(F8_0, |f| f.decode());
+ data = label_series
+ .values
+ .iter()
+ .map(|label| {
+ if label.is_number() {
+ Datum::String(label.display(format).with_stretch().to_string())
+ } else {
+ label.clone()
+ }
+ })
+ .collect();
+ }
+ series.insert(
+ &self.id,
+ Series::new(self.id.clone(), categories, data)
+ .with_affixes(Vec::from(self.affixes()))
+ .with_label(self.label.clone()),
+ );
+ true
+ }
+ fn name(&self) -> &str {
+ &self.variable
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct DerivedVariable {
+ #[serde(rename = "@id")]
+ id: String,
+
+ /// An expression that defines the variable's value.
+ #[serde(rename = "@value")]
+ value: String,
+ format: Option<Format>,
+ string_format: Option<StringFormat>,
+ #[serde(default, rename = "valueMapEntry")]
+ value_map: Vec<ValueMapEntry>,
+}
+
+impl DerivedVariable {
+ fn mapping(&self) -> Map {
+ Map(self
+ .value_map
+ .iter()
+ .flat_map(|vme| {
+ vme.from
+ .split(';')
+ .filter_map(|from| from.trim().parse::<f64>().ok())
+ .map(|from| {
+ (
+ OrderedFloat(from),
+ if let Ok(to) = vme.to.trim().parse::<f64>() {
+ Datum::Number(Some(to))
+ } else {
+ Datum::String(vme.to.clone())
+ },
+ )
+ })
+ })
+ .collect())
+ }
+}
+
+impl Variable for DerivedVariable {
+ fn decode<'a>(
+ &'a self,
+ _data: &IndexMap<String, IndexMap<String, Vec<Datum<String>>>>,
+ series: &mut BTreeMap<&'a str, Series>,
+ warn: &mut dyn FnMut(GraphWarning),
+ ) -> bool {
+ let mut values = if self.id == "column" || self.id == "row" {
+ vec![]
+ } else if let Some(rest) = self.id.strip_prefix("dimension")
+ && rest.parse::<usize>().is_ok()
+ {
+ vec![]
+ } else if self.value == "constant(0)" {
+ /// All the series have the same length, except for series with
+ /// length zero. This returns the length of the first nonempty
+ /// series, or `None` if there isn't one yet.
+ fn series_len(series: &mut BTreeMap<&str, Series>) -> Option<usize> {
+ series.values().find_map(|series| {
+ if !series.values.is_empty() {
+ Some(series.values.len())
+ } else {
+ None
+ }
+ })
+ }
+
+ if let Some(n_values) = series_len(series) {
+ (0..n_values).map(|_| Datum::Number(Some(0.0))).collect()
+ } else {
+ return false;
+ }
+ } else if self.value.starts_with("constant") {
+ vec![]
+ } else if let Some(rest) = self.value.strip_prefix("map(")
+ && let Some(var_name) = rest.strip_suffix(")")
+ {
+ let Some(dependency) = series.get(var_name) else {
+ return false;
+ };
+ dependency.values.clone()
+ } else {
+ warn(GraphWarning::UnsupportedValue {
+ variable: self.id.clone(),
+ value: self.value.clone(),
+ });
+ vec![]
+ };
+ self.mapping().apply(&mut values);
+ if let Some(format) = &self.format {
+ format.mapping().apply(&mut values);
+ } else if let Some(string_format) = &self.string_format {
+ string_format.mapping().apply(&mut values);
+ };
+ series.insert(
+ &self.id,
+ Series::new(
+ self.id.clone(),
+ (0..values.len()).map(|_| Some(0)).collect(),
+ values,
+ ),
+ );
+ true
+ }
+ fn name(&self) -> &str {
+ &self.id
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct CategoricalDomain {
+ #[serde(rename = "@id")]
+ id: Option<String>,
+
+ variable_reference: VariableReference,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct IntervalDomain {
+ range: IntervalRange,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct IntervalRange {
+ #[serde(rename = "@min")]
+ min: f64,
+
+ #[serde(rename = "@max")]
+ max: f64,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct VariableReference {
+ #[serde(rename = "@ref")]
+ reference: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct StringFormat {
+ #[serde(default, rename = "relabel")]
+ relabels: Vec<Relabel>,
+ #[serde(default, rename = "affix")]
+ affixes: Vec<Affix>,
+}
+
+impl StringFormat {
+ fn mapping(&self) -> Map {
+ Map(self
+ .relabels
+ .iter()
+ .map(|relabel| {
+ (
+ OrderedFloat(relabel.from),
+ Datum::String(relabel.to.clone()),
+ )
+ })
+ .collect())
+ }
+}
+
+#[derive(Deserialize, Debug, Default)]
+#[serde(rename_all = "camelCase")]
+struct Format {
+ #[serde(rename = "@baseFormat")]
+ base_format: Option<BaseFormat>,
+ #[serde(rename = "@mdyOrder")]
+ mdy_order: Option<MdyOrder>,
+ #[serde(rename = "@showQuarter")]
+ show_quarter: Option<bool>,
+ #[serde(rename = "@quarterPrefix")]
+ year_abbreviation: Option<bool>,
+ #[serde(rename = "@monthFormat")]
+ month_format: Option<MonthFormat>,
+ #[serde(rename = "@showWeek")]
+ show_week: Option<bool>,
+ #[serde(rename = "@showDay")]
+ show_day: Option<bool>,
+ #[serde(rename = "@showHour")]
+ show_hour: Option<bool>,
+ #[serde(rename = "@showSecond")]
+ show_second: Option<bool>,
+ #[serde(rename = "@showMillis")]
+ show_millis: Option<bool>,
+ #[serde(rename = "@maximumFractionDigits")]
+ maximum_fraction_digits: Option<i64>,
+ #[serde(rename = "@useGrouping")]
+ use_grouping: Option<bool>,
+ #[serde(rename = "@scientific")]
+ scientific: Option<Scientific>,
+ #[serde(default, rename = "@prefix")]
+ prefix: String,
+ #[serde(default, rename = "@suffix")]
+ suffix: String,
+ #[serde(rename = "@tryStringsAsNumbers", default)]
+ try_strings_as_numbers: bool,
+ #[serde(default, rename = "relabel")]
+ relabels: Vec<Relabel>,
+ #[serde(default, rename = "affix")]
+ affixes: Vec<Affix>,
+}
+
+impl Format {
+ fn mapping(&self) -> Map {
+ let format = self.decode();
+ Map(self
+ .relabels
+ .iter()
+ .map(|relabel| {
+ let value = match relabel.to.trim().parse::<f64>().ok() {
+ Some(to) if self.try_strings_as_numbers => Datum::Number(Some(to)),
+ Some(to) => Datum::String(
+ Datum::<String>::Number(Some(to))
+ .display(format)
+ .with_stretch()
+ .to_string(),
+ ),
+ None => Datum::String(relabel.to.clone()),
+ };
+ (OrderedFloat(relabel.from), value)
+ })
+ .collect())
+ }
+
+ fn decode(&self) -> crate::format::Format {
+ if self.base_format.is_some() {
+ SignificantDateTimeFormat::from(self).decode()
+ } else {
+ SignificantNumberFormat::from(self).decode()
+ }
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct NumberFormat {
+ #[serde(rename = "@maximumFractionDigits")]
+ maximum_fraction_digits: Option<i64>,
+ #[serde(rename = "@useGrouping")]
+ use_grouping: Option<bool>,
+ #[serde(rename = "@scientific")]
+ scientific: Option<Scientific>,
+ #[serde(default, rename = "@prefix")]
+ prefix: String,
+ #[serde(default, rename = "@suffix")]
+ suffix: String,
+ #[serde(default, rename = "affix")]
+ affixes: Vec<Affix>,
+}
+
+struct SignificantNumberFormat<'a> {
+ scientific: Option<Scientific>,
+ prefix: &'a str,
+ suffix: &'a str,
+ use_grouping: Option<bool>,
+ maximum_fraction_digits: Option<i64>,
+}
+
+impl<'a> From<&'a NumberFormat> for SignificantNumberFormat<'a> {
+ fn from(value: &'a NumberFormat) -> Self {
+ Self {
+ scientific: value.scientific,
+ prefix: &value.prefix,
+ suffix: &value.suffix,
+ use_grouping: value.use_grouping,
+ maximum_fraction_digits: value.maximum_fraction_digits,
+ }
+ }
+}
+
+impl<'a> From<&'a Format> for SignificantNumberFormat<'a> {
+ fn from(value: &'a Format) -> Self {
+ Self {
+ scientific: value.scientific,
+ prefix: &value.prefix,
+ suffix: &value.suffix,
+ use_grouping: value.use_grouping,
+ maximum_fraction_digits: value.maximum_fraction_digits,
+ }
+ }
+}
+
+impl<'a> SignificantNumberFormat<'a> {
+ fn decode(&self) -> crate::format::Format {
+ let type_ = if self.scientific == Some(Scientific::True) {
+ Type::E
+ } else if self.prefix == "$" {
+ Type::Dollar
+ } else if self.suffix == "%" {
+ Type::Pct
+ } else if self.use_grouping == Some(true) {
+ Type::Comma
+ } else {
+ Type::F
+ };
+ let d = match self.maximum_fraction_digits {
+ Some(d) if (0..=15).contains(&d) => d,
+ _ => 2,
+ };
+ UncheckedFormat {
+ type_,
+ w: 40,
+ d: d as u8,
+ }
+ .fix()
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct DateTimeFormat {
+ #[serde(rename = "@baseFormat")]
+ base_format: BaseFormat,
+ #[serde(rename = "@mdyOrder")]
+ mdy_order: Option<MdyOrder>,
+ #[serde(rename = "@showQuarter")]
+ show_quarter: Option<bool>,
+ #[serde(rename = "@yearAbbreviation")]
+ year_abbreviation: Option<bool>,
+ #[serde(rename = "@monthFormat")]
+ month_format: Option<MonthFormat>,
+ #[serde(rename = "@showWeek")]
+ show_week: Option<bool>,
+ #[serde(rename = "@showDay")]
+ show_day: Option<bool>,
+ #[serde(rename = "@showHour")]
+ show_hour: Option<bool>,
+ #[serde(rename = "@showSecond")]
+ show_second: Option<bool>,
+ #[serde(rename = "@showMillis")]
+ show_millis: Option<bool>,
+ #[serde(default, rename = "affix")]
+ affixes: Vec<Affix>,
+}
+
+struct SignificantDateTimeFormat {
+ base_format: Option<BaseFormat>,
+ show_quarter: Option<bool>,
+ show_week: Option<bool>,
+ show_day: Option<bool>,
+ show_hour: Option<bool>,
+ show_second: Option<bool>,
+ show_millis: Option<bool>,
+ mdy_order: Option<MdyOrder>,
+ month_format: Option<MonthFormat>,
+ year_abbreviation: Option<bool>,
+}
+
+impl From<&Format> for SignificantDateTimeFormat {
+ fn from(value: &Format) -> Self {
+ Self {
+ base_format: value.base_format,
+ show_quarter: value.show_quarter,
+ show_week: value.show_week,
+ show_day: value.show_day,
+ show_hour: value.show_hour,
+ show_second: value.show_second,
+ show_millis: value.show_millis,
+ mdy_order: value.mdy_order,
+ month_format: value.month_format,
+ year_abbreviation: value.year_abbreviation,
+ }
+ }
+}
+impl From<&DateTimeFormat> for SignificantDateTimeFormat {
+ fn from(value: &DateTimeFormat) -> Self {
+ Self {
+ base_format: Some(value.base_format),
+ show_quarter: value.show_quarter,
+ show_week: value.show_week,
+ show_day: value.show_day,
+ show_hour: value.show_hour,
+ show_second: value.show_second,
+ show_millis: value.show_millis,
+ mdy_order: value.mdy_order,
+ month_format: value.month_format,
+ year_abbreviation: value.year_abbreviation,
+ }
+ }
+}
+impl SignificantDateTimeFormat {
+ fn decode(&self) -> crate::format::Format {
+ let type_ = match self.base_format {
+ Some(BaseFormat::Date) => {
+ let type_ = if self.show_quarter == Some(true) {
+ Type::QYr
+ } else if self.show_week == Some(true) {
+ Type::WkYr
+ } else {
+ match (self.mdy_order, self.month_format) {
+ (Some(MdyOrder::DayMonthYear), Some(MonthFormat::Number)) => Type::EDate,
+ (Some(MdyOrder::DayMonthYear), Some(MonthFormat::PaddedNumber)) => {
+ Type::EDate
+ }
+ (Some(MdyOrder::DayMonthYear), _) => Type::Date,
+ (Some(MdyOrder::YearMonthDay), _) => Type::SDate,
+ _ => Type::ADate,
+ }
+ };
+ let mut w = type_.min_width();
+ if self.year_abbreviation != Some(true) {
+ w += 2;
+ };
+ return UncheckedFormat { type_, w, d: 0 }.try_into().unwrap();
+ }
+ Some(BaseFormat::DateTime) => {
+ if self.mdy_order == Some(MdyOrder::YearMonthDay) {
+ Type::YmdHms
+ } else {
+ Type::DateTime
+ }
+ }
+ _ => {
+ if self.show_day == Some(true) {
+ Type::DTime
+ } else if self.show_hour == Some(true) {
+ Type::Time
+ } else {
+ Type::MTime
+ }
+ }
+ };
+ date_time_format(type_, self.show_second, self.show_millis)
+ }
+}
+
+fn date_time_format(
+ type_: Type,
+ show_second: Option<bool>,
+ show_millis: Option<bool>,
+) -> crate::format::Format {
+ let mut w = type_.min_width();
+ let mut d = 0;
+ if show_second == Some(true) {
+ w += 3;
+ if show_millis == Some(true) {
+ d = 3;
+ w += d as u16 + 1;
+ }
+ }
+ UncheckedFormat { type_, w, d }.try_into().unwrap()
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct ElapsedTimeFormat {
+ #[serde(rename = "@showDay")]
+ show_day: Option<bool>,
+ #[serde(rename = "@showHour")]
+ show_hour: Option<bool>,
+ #[serde(rename = "@showSecond")]
+ show_second: Option<bool>,
+ #[serde(rename = "@showMillis")]
+ show_millis: Option<bool>,
+ #[serde(default, rename = "affix")]
+ affixes: Vec<Affix>,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum BaseFormat {
+ Date,
+ Time,
+ DateTime,
+ ElapsedTime,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum MdyOrder {
+ DayMonthYear,
+ MonthDayYear,
+ YearMonthDay,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum MonthFormat {
+ Long,
+ Short,
+ Number,
+ PaddedNumber,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum Scientific {
+ OnlyForSmall,
+ WhenNeeded,
+ True,
+ False,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct Affix {
+ /// A reference to a footnote with the given 1-based index.
+ #[serde(rename = "@definesReference")]
+ defines_reference: NonZeroUsize,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Relabel {
+ #[serde(rename = "@from")]
+ from: f64,
+ #[serde(rename = "@to")]
+ to: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct ValueMapEntry {
+ #[serde(rename = "@from")]
+ from: String,
+ #[serde(rename = "@to")]
+ to: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Style {
+ #[serde(rename = "@id")]
+ id: Option<String>,
+
+ /// The text color or, in some cases, background color.
+ #[serde(rename = "@color")]
+ color: Option<Color>,
+
+ #[serde(rename = "@font-size")]
+ font_size: Option<String>,
+
+ #[serde(rename = "@font-weight")]
+ font_weight: Option<FontWeight>,
+
+ #[serde(rename = "@font-style")]
+ font_style: Option<FontStyle>,
+
+ #[serde(rename = "@font-underline")]
+ font_underline: Option<FontUnderline>,
+
+ #[serde(rename = "@textAlignment")]
+ text_alignment: Option<TextAlignment>,
+
+ #[serde(rename = "@labelLocationVertical")]
+ label_location_vertical: Option<LabelLocation>,
+
+ #[serde(rename = "@visible")]
+ visible: Option<bool>,
+
+ #[serde(rename = "@decimal-offset")]
+ decimal_offset: Option<Length>,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum FontWeight {
+ Regular,
+ Bold,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum FontStyle {
+ Regular,
+ Italic,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum FontUnderline {
+ None,
+ Underline,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum TextAlignment {
+ Left,
+ Right,
+ Center,
+ Decimal,
+ Mixed,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum LabelLocation {
+ Positive,
+ Negative,
+ Center,
+}
+
+impl From<LabelLocation> for VertAlign {
+ fn from(value: LabelLocation) -> Self {
+ match value {
+ LabelLocation::Positive => VertAlign::Top,
+ LabelLocation::Negative => VertAlign::Bottom,
+ LabelLocation::Center => VertAlign::Middle,
+ }
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Graph {
+ faceting: Option<Faceting>,
+ facet_layout: Option<FacetLayout>,
+ interval: Interval,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Faceting {
+ #[serde(default, rename = "$value")]
+ children: Vec<FacetingChild>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum FacetingChild {
+ Cross(Cross),
+ Layer(Layer),
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Cross {
+ #[serde(rename = "$value")]
+ children: [CrossChild; 2],
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum CrossChild {
+ /// No dimensions along this axis.
+ Unity,
+ /// Dimensions along this axis.
+ Nest(Nest),
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Nest {
+ #[serde(rename = "variableReference")]
+ variable_references: Vec<VariableReference>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Layer {
+ #[serde(rename = "@variable")]
+ variable: String,
+
+ #[serde(rename = "@value")]
+ value: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct FacetLayout {
+ #[serde(rename = "$value")]
+ children: Vec<FacetLayoutChild>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum FacetLayoutChild {
+ SetCellProperties(SetCellProperties),
+ FacetLevel(FacetLevel),
+ #[serde(other)]
+ Other,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SetCellProperties {
+ #[serde(rename = "@applyToConverse", default)]
+ apply_to_converse: bool,
+
+ #[serde(rename = "$value")]
+ sets: Vec<Set>,
+
+ #[serde(rename = "union")]
+ union_: Option<Union>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Union {
+ #[serde(default, rename = "intersect")]
+ intersects: Vec<Intersect>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Intersect {
+ #[serde(default, rename = "$value")]
+ children: Vec<IntersectChild>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum IntersectChild {
+ Where(Where),
+ #[serde(other)]
+ Other,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Where {
+ #[serde(rename = "@variable")]
+ variable: String,
+ #[serde(rename = "@include")]
+ include: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum Set {
+ SetFormat(SetFormat),
+ SetStyle(SetStyle),
+ SetFrameStyle(SetFrameStyle),
+ #[serde(other)]
+ Other,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SetStyle {
+ #[serde(rename = "@target")]
+ target: String,
+
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SetFrameStyle {
+ #[serde(rename = "@target")]
+ target: String,
+
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SetFormat {
+ #[serde(rename = "@target")]
+ target: String,
+
+ #[serde(rename = "$value")]
+ child: Option<SetFormatChild>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum SetFormatChild {
+ Format(Format),
+ NumberFormat(NumberFormat),
+ StringFormat(Vec<StringFormat>),
+ DateTimeFormat(DateTimeFormat),
+ ElapsedTimeFormat(ElapsedTimeFormat),
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Interval {}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Labeling {
+ #[serde(rename = "$value", default)]
+ children: Vec<LabelingChild>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum LabelingChild {
+ Formatting(Formatting),
+ Footnotes(Footnotes),
+ #[serde(other)]
+ Other,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Formatting {
+ #[serde(rename = "@variable")]
+ variable: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Footnotes {
+ #[serde(rename = "@variable")]
+ variable: String,
+
+ #[serde(default, rename = "@superscript")]
+ superscript: bool,
+
+ #[serde(default, rename = "footnoteMapping")]
+ mappings: Vec<FootnoteMapping>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct FootnoteMapping {
+ #[serde(rename = "@definesReference")]
+ defines_reference: Option<NonZeroUsize>,
+
+ #[serde(rename = "@from")]
+ from: NonZeroUsize,
+
+ #[serde(rename = "@to")]
+ to: String,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct FacetLevel {
+ #[serde(rename = "@level")]
+ level: usize,
+
+ axis: Axis,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Axis {
+ label: Option<Label>,
+ major_ticks: MajorTicks,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct MajorTicks {
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Label {
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
+
+ #[serde(rename = "@textFrameStyle")]
+ text_frame_style: Option<Ref<Style>>,
+
+ #[serde(rename = "@purpose")]
+ purpose: Option<Purpose>,
+
+ #[serde(rename = "$value")]
+ child: LabelChild,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Enum)]
+#[serde(rename_all = "camelCase")]
+enum Purpose {
+ Title,
+ SubTitle,
+ SubSubTitle,
+ Layer,
+ Footnote,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+enum LabelChild {
+ Text(Vec<Text>),
+ #[serde(other)]
+ Other,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Text {
+ /// If this is present, then `text` defines the content or the marker for a
+ /// footnote.
+ #[serde(rename = "@usesReference")]
+ uses_reference: Option<NonZeroUsize>,
+
+ /// If this is present, then
+ #[serde(rename = "@definesReference")]
+ defines_reference: Option<NonZeroUsize>,
+
+ #[serde(default, rename = "$text")]
+ text: String,
+}
+
+enum DecodedText<'a> {
+ /// Defines footnote `index` content or marker as `text`.
+ FootnoteDefinition { index: usize, text: &'a str },
+
+ /// Adds a reference to footnote `index`.
+ FootnoteReference { index: usize },
+
+ /// Text content.
+ Text { text: &'a str },
+}
+
+impl Text {
+ fn decode(&self) -> DecodedText<'_> {
+ if let Some(uses_reference) = self.uses_reference {
+ DecodedText::FootnoteDefinition {
+ index: uses_reference.get() - 1,
+ text: &self.text,
+ }
+ } else if let Some(defines_reference) = self.defines_reference {
+ DecodedText::FootnoteReference {
+ index: defines_reference.get() - 1,
+ }
+ } else {
+ DecodedText::Text { text: &self.text }
+ }
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct LabelFrame {
+ label: Option<Label>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Container {
+ #[serde(rename = "labelFrame")]
+ #[serde(default)]
+ label_frames: Vec<LabelFrame>,
+}