leptos_helios/chart/
validation.rs1use super::{
6 ChartSpec, DataReference, Encoding, Intelligence, MarkType, Selection,
7 Transform,
8};
9use thiserror::Error;
11
12#[derive(Debug, Error)]
14pub enum ValidationError {
15 #[error("Missing required field: {0}")]
16 MissingField(String),
17
18 #[error("Invalid field value: {0}")]
19 InvalidValue(String),
20
21 #[error("Incompatible encoding for mark type: {0}")]
22 IncompatibleEncoding(String),
23
24 #[error("Invalid data reference: {0}")]
25 InvalidDataReference(String),
26
27 #[error("Validation failed: {0}")]
28 ValidationFailed(String),
29}
30
31pub struct ChartValidator;
33
34impl ChartValidator {
35 pub fn validate_spec(spec: &ChartSpec) -> Result<(), ValidationError> {
37 spec.data.validate()?;
39
40 spec.encoding.validate_for_mark(&spec.mark)?;
42
43 for transform in &spec.transform {
45 transform.validate()?;
46 }
47
48 for selection in &spec.selection {
50 selection.validate()?;
51 }
52
53 if let Some(intelligence) = &spec.intelligence {
55 intelligence.validate()?;
56 }
57
58 Ok(())
59 }
60}
61
62impl DataReference {
63 pub fn validate(&self) -> Result<(), ValidationError> {
65 if self.source.is_empty() {
66 return Err(ValidationError::InvalidDataReference(
67 "Source cannot be empty".to_string(),
68 ));
69 }
70
71 Ok(())
72 }
73}
74
75impl Encoding {
76 pub fn validate_for_mark(&self, mark: &MarkType) -> Result<(), ValidationError> {
78 match mark {
79 MarkType::Line { .. } | MarkType::Area { .. } => {
80 if self.x.is_none() || self.y.is_none() {
81 return Err(ValidationError::IncompatibleEncoding(
82 "Line and area charts require both x and y encodings".to_string(),
83 ));
84 }
85 }
86 MarkType::Bar { .. } => {
87 if self.x.is_none() || self.y.is_none() {
88 return Err(ValidationError::IncompatibleEncoding(
89 "Bar charts require both x and y encodings".to_string(),
90 ));
91 }
92 }
93 MarkType::Point { .. } | MarkType::Scatter { .. } => {
94 if self.x.is_none() || self.y.is_none() {
95 return Err(ValidationError::IncompatibleEncoding(
96 "Point and scatter charts require both x and y encodings".to_string(),
97 ));
98 }
99 }
100 _ => {
101 }
103 }
104
105 Ok(())
106 }
107
108 pub fn complexity(&self) -> f64 {
110 let mut complexity = 0.0;
111
112 if self.x.is_some() {
113 complexity += 1.0;
114 }
115 if self.y.is_some() {
116 complexity += 1.0;
117 }
118 if self.color.is_some() {
119 complexity += 1.5;
120 }
121 if self.size.is_some() {
122 complexity += 1.0;
123 }
124 if self.shape.is_some() {
125 complexity += 1.0;
126 }
127 if self.opacity.is_some() {
128 complexity += 0.5;
129 }
130 if self.text.is_some() {
131 complexity += 1.0;
132 }
133 if self.tooltip.is_some() {
134 complexity += 0.5;
135 }
136 if self.detail.is_some() {
137 complexity += 1.0;
138 }
139 if self.order.is_some() {
140 complexity += 0.5;
141 }
142 if self.row.is_some() {
143 complexity += 1.5;
144 }
145 if self.column.is_some() {
146 complexity += 1.5;
147 }
148
149 complexity
150 }
151
152 pub fn optimize(self) -> Self {
154 self
156 }
157}
158
159impl Transform {
160 pub fn validate(&self) -> Result<(), ValidationError> {
162 Ok(())
164 }
165
166 pub fn optimize(self) -> Self {
168 self
170 }
171}
172
173impl Selection {
174 pub fn validate(&self) -> Result<(), ValidationError> {
176 Ok(())
178 }
179}
180
181impl Intelligence {
182 pub fn validate(&self) -> Result<(), ValidationError> {
184 Ok(())
186 }
187
188 pub fn complexity(&self) -> f64 {
190 1.0
192 }
193}
194
195impl MarkType {
196 pub fn complexity(&self) -> f64 {
198 match self {
199 MarkType::Point { .. } => 1.0,
200 MarkType::Line { .. } => 2.0,
201 MarkType::Bar { .. } => 1.5,
202 MarkType::Area { .. } => 3.0,
203 MarkType::Text { .. } => 0.5,
204 MarkType::Rect { .. } => 1.0,
205 MarkType::Scatter { .. } => 1.5,
206 MarkType::BoxPlot { .. } => 2.5,
207 MarkType::Violin { .. } => 3.0,
208 MarkType::Heatmap { .. } => 2.0,
209 MarkType::Histogram { .. } => 1.5,
210 MarkType::Density { .. } => 2.5,
211 MarkType::Contour { .. } => 3.5,
212 MarkType::Radar { .. } => 2.0,
213 MarkType::Sankey { .. } => 4.0,
214 MarkType::Treemap { .. } => 3.0,
215 MarkType::Composite(ref marks) => marks.iter().map(|m| m.complexity()).sum(),
216 MarkType::Point3D { .. } => 4.0,
218 MarkType::Surface3D { .. } => 5.0,
219 MarkType::Choropleth { .. } => 3.5,
220 MarkType::NetworkGraph { .. } => 4.5,
221 MarkType::DotMap { .. } => 3.0,
222 MarkType::FlowMap { .. } => 4.0,
223 }
224 }
225}