leptos_helios/
utils.rs

1//! Utility functions and helpers
2
3use std::collections::HashMap;
4
5/// Utility functions for data conversion and validation
6pub mod conversion {
7    use super::*;
8
9    /// Convert JavaScript data to Polars DataFrame
10    pub fn js_data_to_dataframe(
11        js_data: &serde_json::Value,
12    ) -> Result<crate::DataFrame, crate::data_processing::DataError> {
13        use polars::prelude::*;
14
15        if let Some(array) = js_data.as_array() {
16            if array.is_empty() {
17                return Ok(DataFrame::empty());
18            }
19
20            let mut columns = HashMap::new();
21
22            // Extract columns from first object
23            if let Some(first_obj) = array[0].as_object() {
24                for key in first_obj.keys() {
25                    columns.insert(key.clone(), Vec::new());
26                }
27            }
28
29            // Collect values for each column
30            for item in array {
31                if let Some(obj) = item.as_object() {
32                    for (key, value) in obj {
33                        if let Some(column_vec) = columns.get_mut(key) {
34                            column_vec.push(value.clone());
35                        }
36                    }
37                }
38            }
39
40            // Convert to Polars Series
41            let series: Vec<Series> = columns
42                .into_iter()
43                .map(|(name, values)| {
44                    // Convert JSON values to appropriate Polars types
45                    let polars_values: Vec<_> = values
46                        .iter()
47                        .map(|v| match v {
48                            serde_json::Value::Number(n) => {
49                                if n.is_f64() {
50                                    polars::prelude::AnyValue::Float64(n.as_f64().unwrap())
51                                } else {
52                                    polars::prelude::AnyValue::Int64(n.as_i64().unwrap())
53                                }
54                            }
55                            serde_json::Value::String(s) => polars::prelude::AnyValue::String(s),
56                            serde_json::Value::Bool(b) => polars::prelude::AnyValue::Boolean(*b),
57                            serde_json::Value::Null => polars::prelude::AnyValue::Null,
58                            _ => polars::prelude::AnyValue::String(Box::leak(
59                                v.to_string().into_boxed_str(),
60                            )),
61                        })
62                        .collect();
63
64                    Series::new((&name).into(), polars_values)
65                })
66                .collect();
67
68            Ok(DataFrame::new(
69                series.into_iter().map(|s| s.into()).collect(),
70            )?)
71        } else {
72            Err(crate::data_processing::DataError::Format(
73                "Expected JSON array".to_string(),
74            ))
75        }
76    }
77
78    /// Convert Polars DataFrame to JavaScript-compatible format
79    pub fn dataframe_to_js(
80        df: &crate::DataFrame,
81    ) -> Result<serde_json::Value, crate::data_processing::DataError> {
82        let mut result = Vec::new();
83
84        for row in 0..df.height() {
85            let mut obj = serde_json::Map::new();
86
87            for col_name in df.get_column_names() {
88                let series = df.column(col_name)?;
89                let value = series
90                    .get(row)
91                    .map_err(|e| crate::data_processing::DataError::Processing(e.to_string()))?;
92
93                let json_value = match value {
94                    polars::prelude::AnyValue::Int32(i) => serde_json::Value::Number(i.into()),
95                    polars::prelude::AnyValue::Int64(i) => serde_json::Value::Number(i.into()),
96                    polars::prelude::AnyValue::Float32(f) => {
97                        serde_json::Value::Number(serde_json::Number::from_f64(f as f64).unwrap())
98                    }
99                    polars::prelude::AnyValue::Float64(f) => {
100                        serde_json::Value::Number(serde_json::Number::from_f64(f).unwrap())
101                    }
102                    polars::prelude::AnyValue::String(s) => {
103                        serde_json::Value::String(s.to_string())
104                    }
105                    polars::prelude::AnyValue::Boolean(b) => serde_json::Value::Bool(b),
106                    polars::prelude::AnyValue::Null => serde_json::Value::Null,
107                    _ => serde_json::Value::String(value.to_string()),
108                };
109
110                obj.insert(col_name.to_string(), json_value);
111            }
112
113            result.push(serde_json::Value::Object(obj));
114        }
115
116        Ok(serde_json::Value::Array(result))
117    }
118}
119
120/// Performance measurement utilities
121pub mod performance {
122    use std::time::{Duration, Instant};
123
124    /// Measure execution time of a function
125    pub fn measure_time<F, R>(f: F) -> (R, Duration)
126    where
127        F: FnOnce() -> R,
128    {
129        let start = Instant::now();
130        let result = f();
131        let duration = start.elapsed();
132        (result, duration)
133    }
134
135    /// Benchmark a function multiple times
136    pub fn benchmark<F, R>(f: F, iterations: usize) -> BenchmarkResult
137    where
138        F: Fn() -> R,
139    {
140        let mut times = Vec::with_capacity(iterations);
141
142        for _ in 0..iterations {
143            let (_, duration) = measure_time(&f);
144            times.push(duration);
145        }
146
147        times.sort();
148
149        BenchmarkResult {
150            min: times[0],
151            max: times[iterations - 1],
152            mean: times.iter().sum::<Duration>() / iterations as u32,
153            median: times[iterations / 2],
154            p95: times[(iterations as f64 * 0.95) as usize],
155            p99: times[(iterations as f64 * 0.99) as usize],
156        }
157    }
158
159    /// Results from performance benchmarking
160    #[derive(Debug, Clone)]
161    pub struct BenchmarkResult {
162        /// Minimum execution time
163        pub min: Duration,
164        /// Maximum execution time
165        pub max: Duration,
166        /// Mean execution time
167        pub mean: Duration,
168        /// Median execution time
169        pub median: Duration,
170        /// 95th percentile execution time
171        pub p95: Duration,
172        /// 99th percentile execution time
173        pub p99: Duration,
174    }
175}
176
177/// Memory utilities
178pub mod memory {
179    use std::sync::atomic::{AtomicUsize, Ordering};
180
181    /// Global memory tracker
182    pub struct MemoryTracker {
183        allocated: AtomicUsize,
184        peak: AtomicUsize,
185    }
186
187    impl Default for MemoryTracker {
188        fn default() -> Self {
189            Self::new()
190        }
191    }
192
193    impl MemoryTracker {
194        /// Creates a new memory tracker
195        ///
196        /// # Returns
197        ///
198        /// Returns a new `MemoryTracker` instance
199        pub const fn new() -> Self {
200            Self {
201                allocated: AtomicUsize::new(0),
202                peak: AtomicUsize::new(0),
203            }
204        }
205
206        /// Returns the current allocated memory in bytes
207        ///
208        /// # Returns
209        ///
210        /// Returns the number of bytes currently allocated
211        pub fn allocated(&self) -> usize {
212            self.allocated.load(Ordering::Relaxed)
213        }
214
215        /// Returns the peak memory usage in bytes
216        ///
217        /// # Returns
218        ///
219        /// Returns the maximum number of bytes allocated at any point
220        pub fn peak(&self) -> usize {
221            self.peak.load(Ordering::Relaxed)
222        }
223
224        /// Records a memory allocation
225        ///
226        /// # Arguments
227        ///
228        /// * `size` - The size of the allocation in bytes
229        pub fn record_allocation(&self, size: usize) {
230            let current = self.allocated.fetch_add(size, Ordering::Relaxed);
231            let new_total = current + size;
232
233            let mut peak = self.peak.load(Ordering::Relaxed);
234            while peak < new_total {
235                match self.peak.compare_exchange_weak(
236                    peak,
237                    new_total,
238                    Ordering::Relaxed,
239                    Ordering::Relaxed,
240                ) {
241                    Ok(_) => break,
242                    Err(p) => peak = p,
243                }
244            }
245        }
246
247        /// Records a memory deallocation
248        ///
249        /// # Arguments
250        ///
251        /// * `size` - The size of the deallocation in bytes
252        pub fn record_deallocation(&self, size: usize) {
253            self.allocated.fetch_sub(size, Ordering::Relaxed);
254        }
255    }
256
257    /// Global memory tracker instance
258    pub static MEMORY_TRACKER: MemoryTracker = MemoryTracker::new();
259}
260
261/// Validation utilities
262pub mod validation {
263    use crate::chart::{ChartSpec, ValidationError};
264
265    /// Validate chart specification
266    pub fn validate_chart_spec(spec: &ChartSpec) -> Result<(), ValidationError> {
267        spec.validate()
268    }
269
270    /// Validate data types
271    pub fn validate_data_types(spec: &ChartSpec) -> Result<(), ValidationError> {
272        // Check that encoding data types match actual data
273        // Note: DataReference doesn't directly contain DataFrame, would need to load data
274        // For now, skip this validation
275        if false {
276            for (_field, _encoding) in spec.encoding.get_field_encodings() {
277                // Would need to load data from DataReference first
278                // For now, skip validation
279            }
280        }
281
282        Ok(())
283    }
284
285    fn types_compatible(
286        actual: &polars::prelude::DataType,
287        expected: &crate::chart::DataType,
288    ) -> bool {
289        matches!(
290            (actual, expected),
291            (
292                polars::prelude::DataType::Int32 | polars::prelude::DataType::Int64,
293                crate::chart::DataType::Quantitative,
294            ) | (
295                polars::prelude::DataType::Float32 | polars::prelude::DataType::Float64,
296                crate::chart::DataType::Quantitative,
297            ) | (
298                polars::prelude::DataType::String,
299                crate::chart::DataType::Nominal | crate::chart::DataType::Ordinal,
300            ) | (
301                polars::prelude::DataType::Boolean,
302                crate::chart::DataType::Nominal
303            ) | (
304                polars::prelude::DataType::Date | polars::prelude::DataType::Datetime(_, _),
305                crate::chart::DataType::Temporal,
306            )
307        )
308    }
309}
310
311/// Error handling utilities
312pub mod error {
313    use crate::HeliosError;
314
315    /// Convert error to user-friendly message
316    pub fn user_friendly_error(error: &HeliosError) -> String {
317        error.user_message()
318    }
319
320    /// Get suggested actions for an error
321    pub fn suggested_actions(error: &HeliosError) -> Vec<String> {
322        error.suggested_actions()
323    }
324
325    /// Check if error is recoverable
326    pub fn is_recoverable(error: &HeliosError) -> bool {
327        error.is_recoverable()
328    }
329}
330
331/// Testing utilities
332pub mod test_utils {
333    use polars::prelude::*;
334
335    /// Create test DataFrame
336    pub fn create_test_dataframe() -> DataFrame {
337        df! {
338            "x" => [1, 2, 3, 4, 5],
339            "y" => [2, 4, 1, 5, 3],
340            "category" => ["A", "B", "A", "C", "B"],
341        }
342        .unwrap()
343    }
344
345    /// Create large test DataFrame
346    pub fn create_large_test_dataframe(size: usize) -> DataFrame {
347        let x: Vec<i32> = (0..size as i32).collect();
348        let y: Vec<f64> = (0..size).map(|i| (i as f64).sin()).collect();
349        let category: Vec<String> = (0..size).map(|i| format!("Category_{}", i % 10)).collect();
350
351        df! {
352            "x" => x,
353            "y" => y,
354            "category" => category,
355        }
356        .unwrap()
357    }
358
359    /// Create test chart specification
360    pub fn create_test_chart_spec() -> crate::chart::ChartSpec {
361        crate::chart::ChartSpec {
362            data: crate::chart::DataReference {
363                source: "test".to_string(),
364                format: crate::chart::DataFormat::Inline,
365                schema: None,
366            },
367            mark: crate::chart::MarkType::Point {
368                size: Some(5.0),
369                shape: None,
370                opacity: None,
371            },
372            encoding: crate::chart::Encoding {
373                x: Some(crate::chart::PositionEncoding {
374                    field: "x".to_string(),
375                    data_type: crate::chart::DataType::Quantitative,
376                    scale: None,
377                    axis: None,
378                    legend: None,
379                    bin: None,
380                    aggregate: None,
381                    sort: None,
382                }),
383                y: Some(crate::chart::PositionEncoding {
384                    field: "y".to_string(),
385                    data_type: crate::chart::DataType::Quantitative,
386                    scale: None,
387                    axis: None,
388                    legend: None,
389                    bin: None,
390                    aggregate: None,
391                    sort: None,
392                }),
393                color: Some(crate::chart::ColorEncoding {
394                    field: "category".to_string(),
395                    data_type: crate::chart::DataType::Nominal,
396                    scale: None,
397                    axis: None,
398                    legend: None,
399                    bin: None,
400                    aggregate: None,
401                    sort: None,
402                }),
403                ..Default::default()
404            },
405            transform: Vec::new(),
406            selection: Vec::new(),
407            intelligence: None,
408            config: crate::chart::ChartConfig::default(),
409        }
410    }
411
412    /// Assert two DataFrames are approximately equal
413    pub fn assert_dataframes_approx_equal(df1: &DataFrame, df2: &DataFrame, tolerance: f64) {
414        assert_eq!(df1.height(), df2.height(), "DataFrame heights differ");
415        assert_eq!(df1.width(), df2.width(), "DataFrame widths differ");
416
417        for col_name in df1.get_column_names() {
418            let series1 = df1.column(col_name).unwrap();
419            let series2 = df2.column(col_name).unwrap();
420
421            assert_eq!(
422                series1.dtype(),
423                series2.dtype(),
424                "Column '{}' types differ",
425                col_name
426            );
427
428            for i in 0..series1.len() {
429                let val1 = series1.get(i).unwrap();
430                let val2 = series2.get(i).unwrap();
431
432                match (val1.clone(), val2.clone()) {
433                    (AnyValue::Float64(f1), AnyValue::Float64(f2)) => {
434                        assert!(
435                            (f1 - f2).abs() < tolerance,
436                            "Column '{}' row {}: {} != {} (tolerance: {})",
437                            col_name,
438                            i,
439                            f1,
440                            f2,
441                            tolerance
442                        );
443                    }
444                    (AnyValue::Float32(f1), AnyValue::Float32(f2)) => {
445                        assert!(
446                            (f1 - f2).abs() < tolerance as f32,
447                            "Column '{}' row {}: {} != {} (tolerance: {})",
448                            col_name,
449                            i,
450                            f1,
451                            f2,
452                            tolerance
453                        );
454                    }
455                    _ => assert_eq!(val1, val2, "Column '{}' row {} values differ", col_name, i),
456                }
457            }
458        }
459    }
460}
461
462// Add extension trait for Encoding to get field encodings
463impl crate::chart::Encoding {
464    /// Returns a vector of field encodings with their names
465    ///
466    /// # Returns
467    ///
468    /// Returns a vector of tuples containing field names and their position encodings
469    pub fn get_field_encodings(&self) -> Vec<(String, &crate::chart::PositionEncoding)> {
470        let mut encodings = Vec::new();
471
472        if let Some(ref x) = self.x {
473            encodings.push((x.field.clone(), x));
474        }
475        if let Some(ref y) = self.y {
476            encodings.push((y.field.clone(), y));
477        }
478
479        encodings
480    }
481}
482
483// Add extension trait for PositionEncoding to get data type
484impl crate::chart::PositionEncoding {
485    /// Returns the data type of the position encoding
486    ///
487    /// # Returns
488    ///
489    /// Returns a reference to the data type
490    pub fn data_type(&self) -> &crate::chart::DataType {
491        &self.data_type
492    }
493}