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}