use ordered_float::OrderedFloat;
use pango::FontDescription;
use quick_xml::{
- Reader, Writer,
+ DeError, Reader, Writer,
events::{BytesStart, Event},
};
use rs_stats::distributions::normal_distribution::normal_pdf;
/// Error parsing XML data: {0}
XmlError(#[from] serde_path_to_error::Error<quick_xml::DeError>),
- /// CSV data has unknown encoding {0}
- UnknownCsvEncoding(String),
-
/// Graph has no `coordinates` element.
NoCoordinates,
}
impl EmbeddedSource {
+ fn from_xml(xml: &[u8]) -> Result<Self, DeError> {
+ quick_xml::de::from_reader(Cursor::new(xml))
+ }
+
fn decode(&self) -> DataSource {
let mut series = (0..self.names.len())
.map(|i| DataSeries {
}
pub fn csv_data(&self) -> Result<Option<DataSource>, GraphError> {
- if let Some(csv) = &self.csv {
- let mut reader = csv::Reader::from_reader(Cursor::new(&csv));
- let headers = reader.headers().unwrap(); // XXX
- let mut variables = vec![DataSeries::default(); headers.len()];
- for row in reader.records() {
- let row = row.unwrap(); // XXX
- for (column, s) in row.iter().enumerate() {
- let value = match s.parse::<f64>() {
- Ok(number) => Datum::Number(Some(number)),
- Err(_) => Datum::String(s.into()),
- };
- variables[column].data.push(value);
- }
- }
- Ok(Some(DataSource(
- zip(
- reader.headers().unwrap().iter().map(|name| name.into()),
- variables,
- )
- .collect(),
- )))
- } else {
- Ok(None)
+ match &self.csv {
+ Some(csv) => Ok(Some(DataSource::from_csv_data(&csv).unwrap() /*XXX*/)),
+ _ => Ok(None),
}
}
visualization.decode(self.binary_data(&mut warn)?, self.csv_data()?, &mut warn)
}
- //pub fn to_spv(self) -> Self {}
+ /// Returns a version of this graph that is most suitable for including in
+ /// an SPV file. This reverses the transformation made by
+ /// [to_chart_xml](Self::to_chart_xml): while the output produced by that
+ /// function is valid and SPSS will display the graphs, an apparent bug in
+ /// some SPSS versions keeps it from editing them.
+ ///
+ /// Most graphs don't need changes; for those, this function returns the
+ /// original graph.
+ pub fn to_spv(&self) -> Cow<'_, crate::output::Graph> {
+ if self.data.is_some() {
+ return Cow::Borrowed(self);
+ }
+
+ let mut data = IndexMap::new();
+ let mut reader = Reader::from_reader(self.xml.as_slice());
+ let mut writer = Writer::new(Vec::new());
+ loop {
+ let start_position = reader.buffer_position() as usize;
+ let event = match reader.read_event() {
+ Ok(event) => event,
+ Err(_) => todo!(),
+ };
+
+ fn id_attribute<'a>(start: &'a BytesStart) -> Option<Cow<'a, str>> {
+ for attribute in start.attributes() {
+ if let Ok(attribute) = attribute
+ && attribute.key.local_name().as_ref() == b"id"
+ && let Ok(value) = attribute.unescape_value()
+ {
+ return Some(value);
+ }
+ }
+ None
+ }
+
+ match &event {
+ Event::Start(start) => {
+ let local_name = start.local_name();
+ if local_name.as_ref() == b"embeddedSource"
+ && let Some(id) = id_attribute(start)
+ {
+ reader.read_to_end(start.to_end().name()).expect("xxx");
+ let end_position = reader.buffer_position() as usize;
+ let data_source =
+ EmbeddedSource::from_xml(&self.xml[start_position..end_position])
+ .expect("xxx")
+ .decode();
+ writer
+ .create_element("userSource")
+ .with_attribute(("id", id.as_ref()))
+ .write_empty()
+ .expect("writing to a vector shouldn't fail");
+ data.insert(id.into_owned(), data_source);
+ } else {
+ writer
+ .write_event(event)
+ .expect("writing to a vector shouldn't fail");
+ }
+ }
+ Event::Eof => break,
+ _ => writer
+ .write_event(event)
+ .expect("writing to a vector shouldn't fail"),
+ }
+ }
+
+ if data.is_empty() {
+ Cow::Borrowed(self)
+ } else {
+ Cow::Owned(crate::output::Graph {
+ xml: writer.into_inner(),
+ data: Some(DataSources(data).to_legacy_bin()),
+ csv: self.csv.clone(),
+ })
+ }
+ }
+ /// Returns a version of this graph that can be written as a separate XML
+ /// file outside any SPV file, by embedding the data in the XML.
pub fn to_chart_xml<F>(&self, mut warn: F) -> Result<Vec<u8>, GraphError>
where
F: FnMut(GraphWarning),
use std::{
fmt::Display,
io::{Cursor, Read, Seek, SeekFrom, Write},
+ iter::zip,
};
use binrw::{BinRead, BinResult, BinWrite, Endian, binread, binwrite};
.count() as u32;
let mut labels = IndexMap::<&str, u32>::new();
if n_maps > 0 {
+ let mut count = 0;
n_maps.write_le(&mut bin).unwrap();
for ((source_name, source), variable_strings) in self.0.iter().zip(&n_strings) {
let Some((_n, last_string)) = variable_strings
else {
continue;
};
+ count += 1;
(SpvString(source_name), last_string)
.write_le(&mut bin)
.unwrap();
- for ((variable_name, series), n_strings) in source.0.iter().zip(variable_strings) {
+ for ((variable_name, series), n_strings) in source
+ .0
+ .iter()
+ .zip(variable_strings)
+ .take(last_string as usize)
+ {
(SpvString(variable_name), *n_strings as u32)
.write_le(&mut bin)
.unwrap();
}
}
}
+ assert_eq!(n_maps, count);
}
(labels.len() as u32).write_le(&mut bin).unwrap();
})?;
Ok(())
}
+
+ /// Parses `csv` as a `DataSource` in CSV format.
+ pub fn from_csv_data(csv: &[u8]) -> Result<Self, csv::Error> {
+ let mut reader = csv::Reader::from_reader(Cursor::new(&csv));
+ let headers = reader.headers()?;
+ let mut variables = vec![DataSeries::default(); headers.len()];
+ for row in reader.records() {
+ let row = row?;
+ for (column, s) in row.iter().enumerate() {
+ let value = match s.parse::<f64>() {
+ Ok(number) => Datum::Number(Some(number)),
+ Err(_) => Datum::String(s.into()),
+ };
+ variables[column].data.push(value);
+ }
+ }
+ Ok(DataSource(
+ zip(
+ reader.headers().unwrap().iter().map(|name| name.into()),
+ variables,
+ )
+ .collect(),
+ ))
+ }
}
/// A single column of data with an optional label.