Skip to main content

poincare_lib/
table_data.rs

1use std::collections::HashMap;
2
3use crate::graph_spec::{
4    OptionalColumn, TableColumnMapping, TableDelimiter, TableImportDefinition, TablePlotTarget,
5};
6use crate::{CurveInterpolation, DataBounds};
7use serde::{Deserialize, Serialize};
8
9impl TableDelimiter {
10    pub const ALL: [Self; 5] = [
11        Self::Comma,
12        Self::Semicolon,
13        Self::Tab,
14        Self::Space,
15        Self::Pipe,
16    ];
17
18    pub fn label(self) -> &'static str {
19        match self {
20            Self::Comma => "Comma",
21            Self::Semicolon => "Semicolon",
22            Self::Tab => "Tab",
23            Self::Space => "Space",
24            Self::Pipe => "Pipe",
25        }
26    }
27
28    fn split_line(self, line: &str) -> Vec<String> {
29        match self {
30            Self::Comma => line.split(',').map(|cell| cell.trim().to_string()).collect(),
31            Self::Semicolon => line.split(';').map(|cell| cell.trim().to_string()).collect(),
32            Self::Tab => line.split('\t').map(|cell| cell.trim().to_string()).collect(),
33            Self::Space => line
34                .split_whitespace()
35                .map(|cell| cell.trim().to_string())
36                .collect(),
37            Self::Pipe => line.split('|').map(|cell| cell.trim().to_string()).collect(),
38        }
39    }
40}
41
42impl TablePlotTarget {
43    pub fn label(self) -> &'static str {
44        match self {
45            Self::SurfaceGrid => "Surface Grid",
46            Self::Curve => "Curve",
47            Self::Scatter => "Scatter",
48            Self::VectorField => "Vector Field",
49        }
50    }
51}
52
53impl OptionalColumn {
54    pub fn into_option(self) -> Option<usize> {
55        match self {
56            Self::None => None,
57            Self::Column(index) => Some(index),
58        }
59    }
60}
61
62impl TableColumnMapping {
63    pub fn default_for(target: TablePlotTarget) -> Self {
64        match target {
65            TablePlotTarget::SurfaceGrid => Self::SurfaceGrid { x: 0, y: 1, z: 2 },
66            TablePlotTarget::Curve => Self::Curve {
67                x: 0,
68                y: 1,
69                z: OptionalColumn::Column(2),
70                label: OptionalColumn::None,
71                group: OptionalColumn::None,
72            },
73            TablePlotTarget::Scatter => Self::Scatter {
74                x: 0,
75                y: 1,
76                z: OptionalColumn::Column(2),
77                scalar: OptionalColumn::Column(3),
78                label: OptionalColumn::None,
79                group: OptionalColumn::None,
80            },
81            TablePlotTarget::VectorField => Self::VectorField {
82                x: 0,
83                y: 1,
84                z: OptionalColumn::Column(2),
85                vx: 3,
86                vy: 4,
87                vz: OptionalColumn::Column(5),
88                scalar: OptionalColumn::None,
89                label: OptionalColumn::None,
90                group: OptionalColumn::None,
91            },
92        }
93    }
94}
95
96impl TableImportDefinition {
97    pub fn empty(target: TablePlotTarget) -> Self {
98        Self {
99            source_path: None,
100            raw_text: String::new(),
101            delimiter: TableDelimiter::Comma,
102            header_row: false,
103            target,
104            mapping: TableColumnMapping::default_for(target),
105        }
106    }
107
108    pub fn auto_configure(&mut self) {
109        self.delimiter = detect_delimiter(&self.raw_text);
110        self.header_row = detect_header_row(&self.raw_text, self.delimiter);
111    }
112
113    pub fn set_target(&mut self, target: TablePlotTarget) {
114        if self.target != target {
115            self.target = target;
116            self.mapping = TableColumnMapping::default_for(target);
117        }
118    }
119
120    pub fn preview(&self) -> TablePreview {
121        parse_table_preview(&self.raw_text, self.delimiter, self.header_row)
122    }
123
124    pub fn validate(&self) -> Result<TableDataSet, Vec<TableValidationError>> {
125        build_dataset(self)
126    }
127
128    pub fn source_summary(&self) -> String {
129        match &self.source_path {
130            Some(path) => format!("Linked file: {path}"),
131            None => "Embedded table".to_string(),
132        }
133    }
134}
135
136#[derive(Clone, Debug, Serialize, Deserialize)]
137pub struct TablePreview {
138    pub headers: Vec<String>,
139    pub rows: Vec<TableRow>,
140    pub column_count: usize,
141}
142
143#[derive(Clone, Debug, Serialize, Deserialize)]
144pub struct TableRow {
145    pub source_row: usize,
146    pub cells: Vec<String>,
147}
148
149#[derive(Clone, Debug, Serialize, Deserialize)]
150pub struct TableValidationError {
151    pub row: Option<usize>,
152    pub column: Option<usize>,
153    pub message: String,
154}
155
156impl TableValidationError {
157    fn general(message: impl Into<String>) -> Self {
158        Self {
159            row: None,
160            column: None,
161            message: message.into(),
162        }
163    }
164
165    fn at(row: usize, column: usize, message: impl Into<String>) -> Self {
166        Self {
167            row: Some(row),
168            column: Some(column),
169            message: message.into(),
170        }
171    }
172
173    pub fn display(&self) -> String {
174        match (self.row, self.column) {
175            (Some(row), Some(column)) => format!("row {row}, col {column}: {}", self.message),
176            (Some(row), None) => format!("row {row}: {}", self.message),
177            _ => self.message.clone(),
178        }
179    }
180}
181
182#[derive(Clone, Debug)]
183pub enum TableDataSet {
184    SurfaceGrid {
185        xs: Vec<f64>,
186        ys: Vec<f64>,
187        zs: Vec<f64>,
188    },
189    Curve {
190        groups: Vec<Vec<glam::Vec3>>,
191        bounds: DataBounds,
192    },
193    Scatter {
194        points: Vec<glam::Vec3>,
195        scalars: Option<Vec<f32>>,
196        bounds: DataBounds,
197    },
198    VectorField {
199        samples: Vec<TableVectorSample>,
200        bounds: DataBounds,
201    },
202}
203
204#[derive(Clone, Debug)]
205pub struct TableVectorSample {
206    pub position: glam::Vec3,
207    pub vector: glam::Vec3,
208}
209
210fn parse_table_preview(raw_text: &str, delimiter: TableDelimiter, header_row: bool) -> TablePreview {
211    let parsed_rows = raw_text
212        .lines()
213        .enumerate()
214        .filter_map(|(index, line)| {
215            let trimmed = line.trim();
216            if trimmed.is_empty() {
217                None
218            } else {
219                Some(TableRow {
220                    source_row: index + 1,
221                    cells: delimiter.split_line(trimmed),
222                })
223            }
224        })
225        .collect::<Vec<_>>();
226    let column_count = parsed_rows.iter().map(|row| row.cells.len()).max().unwrap_or(0);
227    let mut rows = parsed_rows;
228    let headers = if header_row && !rows.is_empty() {
229        let header = rows.remove(0);
230        normalize_headers(header.cells, column_count)
231    } else {
232        (0..column_count).map(|index| format!("Column {}", index + 1)).collect()
233    };
234    TablePreview {
235        headers,
236        rows,
237        column_count,
238    }
239}
240
241fn normalize_headers(mut cells: Vec<String>, column_count: usize) -> Vec<String> {
242    cells.resize_with(column_count, String::new);
243    cells.into_iter()
244        .enumerate()
245        .map(|(index, header)| {
246            let trimmed = header.trim();
247            if trimmed.is_empty() {
248                format!("Column {}", index + 1)
249            } else {
250                trimmed.to_string()
251            }
252        })
253        .collect()
254}
255
256fn detect_delimiter(raw_text: &str) -> TableDelimiter {
257    let sample_lines = raw_text
258        .lines()
259        .map(str::trim)
260        .filter(|line| !line.is_empty())
261        .take(8)
262        .collect::<Vec<_>>();
263    let candidates = [
264        (TableDelimiter::Comma, ','),
265        (TableDelimiter::Semicolon, ';'),
266        (TableDelimiter::Tab, '\t'),
267        (TableDelimiter::Pipe, '|'),
268    ];
269    let mut best = TableDelimiter::Comma;
270    let mut best_score = 0usize;
271    for (delimiter, marker) in candidates {
272        let score = sample_lines
273            .iter()
274            .map(|line| line.matches(marker).count())
275            .sum::<usize>();
276        if score > best_score {
277            best = delimiter;
278            best_score = score;
279        }
280    }
281    if best_score == 0 && sample_lines.iter().any(|line| line.split_whitespace().count() > 1) {
282        TableDelimiter::Space
283    } else {
284        best
285    }
286}
287
288fn detect_header_row(raw_text: &str, delimiter: TableDelimiter) -> bool {
289    let mut rows = raw_text
290        .lines()
291        .map(str::trim)
292        .filter(|line| !line.is_empty())
293        .map(|line| delimiter.split_line(line))
294        .take(2);
295    let Some(first) = rows.next() else {
296        return false;
297    };
298    let second = rows.next();
299    let first_numeric = first.iter().filter(|cell| cell.parse::<f64>().is_ok()).count();
300    let second_numeric = second
301        .as_ref()
302        .map(|row| row.iter().filter(|cell| cell.parse::<f64>().is_ok()).count())
303        .unwrap_or(0);
304    first_numeric < first.len().saturating_div(2) && second_numeric >= first_numeric
305}
306
307fn build_dataset(definition: &TableImportDefinition) -> Result<TableDataSet, Vec<TableValidationError>> {
308    let preview = definition.preview();
309    if preview.column_count == 0 {
310        return Err(vec![TableValidationError::general("table is empty")]);
311    }
312    if preview.rows.is_empty() {
313        return Err(vec![TableValidationError::general(
314            "table has no data rows after applying header settings",
315        )]);
316    }
317    match (&definition.target, &definition.mapping) {
318        (TablePlotTarget::SurfaceGrid, TableColumnMapping::SurfaceGrid { x, y, z }) => {
319            build_surface_grid(&preview, *x, *y, *z)
320        }
321        (
322            TablePlotTarget::Curve,
323            TableColumnMapping::Curve {
324                x,
325                y,
326                z,
327                label: _,
328                group,
329            },
330        ) => build_curve_data(&preview, *x, *y, *z, *group),
331        (
332            TablePlotTarget::Scatter,
333            TableColumnMapping::Scatter {
334                x,
335                y,
336                z,
337                scalar,
338                label: _,
339                group: _,
340            },
341        ) => build_scatter_data(&preview, *x, *y, *z, *scalar),
342        (
343            TablePlotTarget::VectorField,
344            TableColumnMapping::VectorField {
345                x,
346                y,
347                z,
348                vx,
349                vy,
350                vz,
351                scalar: _,
352                label: _,
353                group: _,
354            },
355        ) => build_vector_field_data(&preview, *x, *y, *z, *vx, *vy, *vz),
356        _ => Err(vec![TableValidationError::general(
357            "table mapping does not match the selected plot type",
358        )]),
359    }
360}
361
362fn build_surface_grid(
363    preview: &TablePreview,
364    x_col: usize,
365    y_col: usize,
366    z_col: usize,
367) -> Result<TableDataSet, Vec<TableValidationError>> {
368    let mut errors = Vec::new();
369    let mut x_values = Vec::<(String, f64)>::new();
370    let mut y_values = Vec::<(String, f64)>::new();
371    let mut x_lookup = HashMap::<String, usize>::new();
372    let mut y_lookup = HashMap::<String, usize>::new();
373    let mut rows = Vec::new();
374    for row in &preview.rows {
375        let x_raw = required_cell(preview, row, x_col, "x", &mut errors);
376        let y_raw = required_cell(preview, row, y_col, "y", &mut errors);
377        let z_raw = required_cell(preview, row, z_col, "z", &mut errors);
378        if let (Some(x_raw), Some(y_raw), Some(z_raw)) = (x_raw, y_raw, z_raw) {
379            let x = parse_f64(row.source_row, x_col, "x", x_raw, &mut errors);
380            let y = parse_f64(row.source_row, y_col, "y", y_raw, &mut errors);
381            let z = parse_f64(row.source_row, z_col, "z", z_raw, &mut errors);
382            if let (Some(x), Some(y), Some(z)) = (x, y, z) {
383                let x_index = *x_lookup.entry(x_raw.to_string()).or_insert_with(|| {
384                    x_values.push((x_raw.to_string(), x));
385                    x_values.len() - 1
386                });
387                let y_index = *y_lookup.entry(y_raw.to_string()).or_insert_with(|| {
388                    y_values.push((y_raw.to_string(), y));
389                    y_values.len() - 1
390                });
391                rows.push((row.source_row, x_index, y_index, z));
392            }
393        }
394    }
395    if !errors.is_empty() {
396        return Err(errors);
397    }
398    let width = x_values.len();
399    let height = y_values.len();
400    if width == 0 || height == 0 {
401        return Err(vec![TableValidationError::general(
402            "surface grid needs at least one x column and one y column",
403        )]);
404    }
405    let mut zs = vec![None; width * height];
406    for (source_row, x_index, y_index, z) in rows {
407        let slot = &mut zs[y_index * width + x_index];
408        if slot.is_some() {
409            errors.push(TableValidationError::at(
410                source_row,
411                z_col + 1,
412                "duplicate surface sample for the same x/y pair",
413            ));
414        } else {
415            *slot = Some(z);
416        }
417    }
418    if !errors.is_empty() {
419        return Err(errors);
420    }
421    if zs.iter().any(Option::is_none) {
422        return Err(vec![TableValidationError::general(
423            "surface grid is incomplete; every x/y combination must be present",
424        )]);
425    }
426    Ok(TableDataSet::SurfaceGrid {
427        xs: x_values.into_iter().map(|(_, value)| value).collect(),
428        ys: y_values.into_iter().map(|(_, value)| value).collect(),
429        zs: zs.into_iter().flatten().collect(),
430    })
431}
432
433fn build_curve_data(
434    preview: &TablePreview,
435    x_col: usize,
436    y_col: usize,
437    z_col: OptionalColumn,
438    group_col: OptionalColumn,
439) -> Result<TableDataSet, Vec<TableValidationError>> {
440    let mut errors = Vec::new();
441    let mut groups = HashMap::<String, Vec<glam::Vec3>>::new();
442    let mut order = Vec::<String>::new();
443    let mut bounds = BoundsAccumulator::default();
444    for row in &preview.rows {
445        let x = parse_required_number(preview, row, x_col, "x", &mut errors);
446        let y = parse_required_number(preview, row, y_col, "y", &mut errors);
447        let z = parse_optional_number(preview, row, z_col.into_option(), "z", &mut errors)
448            .unwrap_or(0.0);
449        if let (Some(x), Some(y)) = (x, y) {
450            let point = glam::Vec3::new(x as f32, y as f32, z as f32);
451            let group_key = optional_cell(preview, row, group_col.into_option())
452                .unwrap_or("")
453                .to_string();
454            if !groups.contains_key(&group_key) {
455                order.push(group_key.clone());
456            }
457            groups.entry(group_key).or_default().push(point);
458            bounds.add(point);
459        }
460    }
461    if !errors.is_empty() {
462        return Err(errors);
463    }
464    let grouped_points = order
465        .into_iter()
466        .filter_map(|key| groups.remove(&key))
467        .filter(|points| !points.is_empty())
468        .collect::<Vec<_>>();
469    let Some(bounds) = bounds.finish() else {
470        return Err(vec![TableValidationError::general("curve table has no valid points")]);
471    };
472    Ok(TableDataSet::Curve {
473        groups: grouped_points,
474        bounds,
475    })
476}
477
478fn build_scatter_data(
479    preview: &TablePreview,
480    x_col: usize,
481    y_col: usize,
482    z_col: OptionalColumn,
483    scalar_col: OptionalColumn,
484) -> Result<TableDataSet, Vec<TableValidationError>> {
485    let mut errors = Vec::new();
486    let mut points = Vec::new();
487    let mut scalars = Vec::new();
488    let mut has_scalar = false;
489    let mut bounds = BoundsAccumulator::default();
490    for row in &preview.rows {
491        let x = parse_required_number(preview, row, x_col, "x", &mut errors);
492        let y = parse_required_number(preview, row, y_col, "y", &mut errors);
493        let z = parse_optional_number(preview, row, z_col.into_option(), "z", &mut errors)
494            .unwrap_or(0.0);
495        let scalar =
496            parse_optional_number(preview, row, scalar_col.into_option(), "scalar", &mut errors);
497        if let (Some(x), Some(y)) = (x, y) {
498            let point = glam::Vec3::new(x as f32, y as f32, z as f32);
499            points.push(point);
500            bounds.add(point);
501            if let Some(value) = scalar {
502                scalars.push(value as f32);
503                has_scalar = true;
504            } else {
505                scalars.push(z as f32);
506            }
507        }
508    }
509    if !errors.is_empty() {
510        return Err(errors);
511    }
512    let Some(bounds) = bounds.finish() else {
513        return Err(vec![TableValidationError::general("scatter table has no valid points")]);
514    };
515    Ok(TableDataSet::Scatter {
516        points,
517        scalars: has_scalar.then_some(scalars),
518        bounds,
519    })
520}
521
522fn build_vector_field_data(
523    preview: &TablePreview,
524    x_col: usize,
525    y_col: usize,
526    z_col: OptionalColumn,
527    vx_col: usize,
528    vy_col: usize,
529    vz_col: OptionalColumn,
530) -> Result<TableDataSet, Vec<TableValidationError>> {
531    let mut errors = Vec::new();
532    let mut samples = Vec::new();
533    let mut bounds = BoundsAccumulator::default();
534    for row in &preview.rows {
535        let x = parse_required_number(preview, row, x_col, "x", &mut errors);
536        let y = parse_required_number(preview, row, y_col, "y", &mut errors);
537        let z = parse_optional_number(preview, row, z_col.into_option(), "z", &mut errors)
538            .unwrap_or(0.0);
539        let vx = parse_required_number(preview, row, vx_col, "vx", &mut errors);
540        let vy = parse_required_number(preview, row, vy_col, "vy", &mut errors);
541        let vz = parse_optional_number(preview, row, vz_col.into_option(), "vz", &mut errors)
542            .unwrap_or(0.0);
543        if let (Some(x), Some(y), Some(vx), Some(vy)) = (x, y, vx, vy) {
544            let position = glam::Vec3::new(x as f32, y as f32, z as f32);
545            let vector = glam::Vec3::new(vx as f32, vy as f32, vz as f32);
546            samples.push(TableVectorSample { position, vector });
547            bounds.add(position);
548        }
549    }
550    if !errors.is_empty() {
551        return Err(errors);
552    }
553    let Some(bounds) = bounds.finish() else {
554        return Err(vec![TableValidationError::general(
555            "vector field table has no valid samples",
556        )]);
557    };
558    Ok(TableDataSet::VectorField { samples, bounds })
559}
560
561fn required_cell<'a>(
562    preview: &TablePreview,
563    row: &'a TableRow,
564    column: usize,
565    label: &str,
566    errors: &mut Vec<TableValidationError>,
567) -> Option<&'a str> {
568    let value = optional_cell(preview, row, Some(column));
569    match value {
570        Some(value) if !value.trim().is_empty() => Some(value),
571        _ => {
572            if column >= preview.column_count {
573                errors.push(TableValidationError::general(format!(
574                    "{label} column {} is outside the table width",
575                    column + 1
576                )));
577            } else {
578                errors.push(TableValidationError::at(
579                    row.source_row,
580                    column + 1,
581                    format!("missing {label} value"),
582                ));
583            }
584            None
585        }
586    }
587}
588
589fn optional_cell<'a>(preview: &TablePreview, row: &'a TableRow, column: Option<usize>) -> Option<&'a str> {
590    let column = column?;
591    if column >= preview.column_count {
592        return None;
593    }
594    row.cells.get(column).map(String::as_str)
595}
596
597fn parse_required_number(
598    preview: &TablePreview,
599    row: &TableRow,
600    column: usize,
601    label: &str,
602    errors: &mut Vec<TableValidationError>,
603) -> Option<f64> {
604    let value = required_cell(preview, row, column, label, errors)?;
605    parse_f64(row.source_row, column, label, value, errors)
606}
607
608fn parse_optional_number(
609    preview: &TablePreview,
610    row: &TableRow,
611    column: Option<usize>,
612    label: &str,
613    errors: &mut Vec<TableValidationError>,
614) -> Option<f64> {
615    let Some(column) = column else {
616        return None;
617    };
618    let Some(value) = optional_cell(preview, row, Some(column)) else {
619        return None;
620    };
621    if value.trim().is_empty() {
622        return None;
623    }
624    parse_f64(row.source_row, column, label, value, errors)
625}
626
627fn parse_f64(
628    row: usize,
629    column: usize,
630    label: &str,
631    value: &str,
632    errors: &mut Vec<TableValidationError>,
633) -> Option<f64> {
634    match value.parse::<f64>() {
635        Ok(number) => Some(number),
636        Err(_) => {
637            errors.push(TableValidationError::at(
638                row,
639                column + 1,
640                format!("{label} is not a number"),
641            ));
642            None
643        }
644    }
645}
646
647#[derive(Default)]
648struct BoundsAccumulator {
649    min: Option<glam::Vec3>,
650    max: Option<glam::Vec3>,
651}
652
653impl BoundsAccumulator {
654    fn add(&mut self, point: glam::Vec3) {
655        self.min = Some(self.min.map(|value| value.min(point)).unwrap_or(point));
656        self.max = Some(self.max.map(|value| value.max(point)).unwrap_or(point));
657    }
658
659    fn finish(self) -> Option<DataBounds> {
660        let min = self.min?;
661        let max = self.max?;
662        Some(DataBounds {
663            x: min.x as f64..=max.x as f64,
664            y: min.y as f64..=max.y as f64,
665            z: min.z as f64..=max.z as f64,
666        })
667    }
668}
669
670pub fn build_curve_piecewise(groups: &[Vec<glam::Vec3>], style: crate::PlotStyle) -> crate::PiecewisePlot {
671    build_curve_piecewise_with_interpolation(groups, style, CurveInterpolation::default())
672}
673
674pub fn build_curve_piecewise_with_interpolation(
675    groups: &[Vec<glam::Vec3>],
676    style: crate::PlotStyle,
677    interpolation: CurveInterpolation,
678) -> crate::PiecewisePlot {
679    let mut plot = crate::PiecewisePlot::new();
680    for points in groups {
681        if points.is_empty() {
682            continue;
683        }
684        let bounds = bounds_for_points(points);
685        plot.add_piece(
686            bounds,
687            crate::Curve3D::from_points_interpolated(points, interpolation).with_style(style.clone()),
688        );
689    }
690    plot
691}
692
693fn bounds_for_points(points: &[glam::Vec3]) -> crate::Domain {
694    let mut min = glam::Vec3::splat(f32::INFINITY);
695    let mut max = glam::Vec3::splat(f32::NEG_INFINITY);
696    for &point in points {
697        min = min.min(point);
698        max = max.max(point);
699    }
700    crate::Domain {
701        x: min.x as f64..=max.x as f64,
702        y: min.y as f64..=max.y as f64,
703        z: min.z as f64..=max.z as f64,
704    }
705}
706
707#[cfg(test)]
708mod tests {
709    use super::*;
710
711    #[test]
712    fn detects_semicolon_and_header_row() {
713        let raw = "x;y;z\n1;2;3\n4;5;6\n";
714        assert_eq!(detect_delimiter(raw), TableDelimiter::Semicolon);
715        assert!(detect_header_row(raw, TableDelimiter::Semicolon));
716    }
717
718    #[test]
719    fn validates_surface_grid_completeness() {
720        let table = TableImportDefinition {
721            source_path: None,
722            raw_text: "x,y,z\n0,0,1\n1,0,2\n".to_string(),
723            delimiter: TableDelimiter::Comma,
724            header_row: true,
725            target: TablePlotTarget::SurfaceGrid,
726            mapping: TableColumnMapping::SurfaceGrid { x: 0, y: 1, z: 2 },
727        };
728        let result = table.validate();
729        assert!(result.is_ok());
730    }
731}