Skip to main content

graphix_package_gui/widgets/chart/
dataset.rs

1use super::types::*;
2use anyhow::{bail, Context, Result};
3use arcstr::ArcStr;
4use graphix_rt::{GXExt, GXHandle, TRef};
5use log::error;
6use netidx::publisher::{FromValue, Value};
7use poolshark::local::LPooled;
8
9// ── Dataset types ───────────────────────────────────────────────────
10
11#[derive(Clone, Copy)]
12pub enum XYKind {
13    Line,
14    Scatter,
15    Area,
16}
17
18/// A compiled dataset with live reactive data refs.
19pub enum DatasetEntry<X: GXExt> {
20    XY { kind: XYKind, data: TRef<X, XYData>, style: SeriesStyleV },
21    DashedLine { data: TRef<X, XYData>, dash: f64, gap: f64, style: SeriesStyleV },
22    Bar { data: TRef<X, BarData>, style: BarStyleV },
23    Candlestick { data: TRef<X, OHLCData>, style: CandlestickStyleV },
24    ErrorBar { data: TRef<X, EBData>, style: SeriesStyleV },
25    Pie { data: TRef<X, BarData>, style: PieStyleV },
26    Scatter3D { data: TRef<X, XYZData>, style: SeriesStyleV },
27    Line3D { data: TRef<X, XYZData>, style: SeriesStyleV },
28    Surface { data: TRef<X, SurfaceData>, style: SurfaceStyleV },
29}
30
31impl<X: GXExt> DatasetEntry<X> {
32    pub fn label(&self) -> Option<&str> {
33        match self {
34            Self::XY { style, .. }
35            | Self::DashedLine { style, .. }
36            | Self::ErrorBar { style, .. }
37            | Self::Scatter3D { style, .. }
38            | Self::Line3D { style, .. } => style.label.as_deref(),
39            Self::Bar { style, .. } => style.label.as_deref(),
40            Self::Candlestick { style, .. } => style.label.as_deref(),
41            Self::Pie { .. } => None,
42            Self::Surface { style, .. } => style.label.as_deref(),
43        }
44    }
45}
46
47/// Dataset metadata parsed from the datasets array value before ref compilation.
48enum DatasetMeta {
49    XY { kind: XYKind, data_id: u64, style: SeriesStyleV },
50    DashedLine { data_id: u64, dash: f64, gap: f64, style: SeriesStyleV },
51    Bar { data_id: u64, style: BarStyleV },
52    Candlestick { data_id: u64, style: CandlestickStyleV },
53    ErrorBar { data_id: u64, style: SeriesStyleV },
54    Pie { data_id: u64, style: PieStyleV },
55    Scatter3D { data_id: u64, style: SeriesStyleV },
56    Line3D { data_id: u64, style: SeriesStyleV },
57    Surface { data_id: u64, style: SurfaceStyleV },
58}
59
60impl FromValue for DatasetMeta {
61    fn from_value(v: Value) -> Result<Self> {
62        let (tag, inner) = v.cast_to::<(ArcStr, Value)>()?;
63        match &*tag {
64            "Line" => {
65                let [(_, data), (_, style)] = inner.cast_to::<[(ArcStr, Value); 2]>()?;
66                let data_id = data.cast_to::<u64>()?;
67                let style = SeriesStyleV::from_value(style)?;
68                Ok(Self::XY { kind: XYKind::Line, data_id, style })
69            }
70            "Scatter" => {
71                let [(_, data), (_, style)] = inner.cast_to::<[(ArcStr, Value); 2]>()?;
72                let data_id = data.cast_to::<u64>()?;
73                let style = SeriesStyleV::from_value(style)?;
74                Ok(Self::XY { kind: XYKind::Scatter, data_id, style })
75            }
76            "Bar" => {
77                let [(_, data), (_, style)] = inner.cast_to::<[(ArcStr, Value); 2]>()?;
78                let data_id = data.cast_to::<u64>()?;
79                let style = BarStyleV::from_value(style)?;
80                Ok(Self::Bar { data_id, style })
81            }
82            "Area" => {
83                let [(_, data), (_, style)] = inner.cast_to::<[(ArcStr, Value); 2]>()?;
84                let data_id = data.cast_to::<u64>()?;
85                let style = SeriesStyleV::from_value(style)?;
86                Ok(Self::XY { kind: XYKind::Area, data_id, style })
87            }
88            "DashedLine" => {
89                let [(_, dash), (_, data), (_, gap), (_, style)] =
90                    inner.cast_to::<[(ArcStr, Value); 4]>()?;
91                let data_id = data.cast_to::<u64>()?;
92                let dash = dash.cast_to::<f64>()?;
93                let gap = gap.cast_to::<f64>()?;
94                let style = SeriesStyleV::from_value(style)?;
95                Ok(Self::DashedLine { data_id, dash, gap, style })
96            }
97            "Candlestick" => {
98                let [(_, data), (_, style)] = inner.cast_to::<[(ArcStr, Value); 2]>()?;
99                let data_id = data.cast_to::<u64>()?;
100                let style = CandlestickStyleV::from_value(style)?;
101                Ok(Self::Candlestick { data_id, style })
102            }
103            "ErrorBar" => {
104                let [(_, data), (_, style)] = inner.cast_to::<[(ArcStr, Value); 2]>()?;
105                let data_id = data.cast_to::<u64>()?;
106                let style = SeriesStyleV::from_value(style)?;
107                Ok(Self::ErrorBar { data_id, style })
108            }
109            "Pie" => {
110                let [(_, data), (_, style)] = inner.cast_to::<[(ArcStr, Value); 2]>()?;
111                let data_id = data.cast_to::<u64>()?;
112                let style = PieStyleV::from_value(style)?;
113                Ok(Self::Pie { data_id, style })
114            }
115            "Scatter3D" => {
116                let [(_, data), (_, style)] = inner.cast_to::<[(ArcStr, Value); 2]>()?;
117                let data_id = data.cast_to::<u64>()?;
118                let style = SeriesStyleV::from_value(style)?;
119                Ok(Self::Scatter3D { data_id, style })
120            }
121            "Line3D" => {
122                let [(_, data), (_, style)] = inner.cast_to::<[(ArcStr, Value); 2]>()?;
123                let data_id = data.cast_to::<u64>()?;
124                let style = SeriesStyleV::from_value(style)?;
125                Ok(Self::Line3D { data_id, style })
126            }
127            "Surface" => {
128                let [(_, data), (_, style)] = inner.cast_to::<[(ArcStr, Value); 2]>()?;
129                let data_id = data.cast_to::<u64>()?;
130                let style = SurfaceStyleV::from_value(style)?;
131                Ok(Self::Surface { data_id, style })
132            }
133            s => bail!("invalid dataset variant: {s}"),
134        }
135    }
136}
137
138/// Compile dataset metadata into live entries with data refs.
139pub async fn compile_datasets<X: GXExt>(
140    gx: &GXHandle<X>,
141    v: Value,
142) -> Result<LPooled<Vec<DatasetEntry<X>>>> {
143    let metas: Vec<DatasetMeta> = v
144        .cast_to::<Vec<Value>>()?
145        .into_iter()
146        .map(DatasetMeta::from_value)
147        .collect::<Result<_>>()?;
148    let mut entries: LPooled<Vec<DatasetEntry<X>>> = LPooled::take();
149    entries.reserve(metas.len());
150    for meta in metas {
151        match meta {
152            DatasetMeta::XY { kind, data_id, style } => {
153                let data_ref = gx.compile_ref(data_id).await?;
154                let data = TRef::new(data_ref).context("chart xy data")?;
155                entries.push(DatasetEntry::XY { kind, data, style });
156            }
157            DatasetMeta::DashedLine { data_id, dash, gap, style } => {
158                let data_ref = gx.compile_ref(data_id).await?;
159                let data = TRef::new(data_ref).context("chart dashed data")?;
160                entries.push(DatasetEntry::DashedLine { data, dash, gap, style });
161            }
162            DatasetMeta::Bar { data_id, style } => {
163                let data_ref = gx.compile_ref(data_id).await?;
164                let data = TRef::new(data_ref).context("chart bar data")?;
165                entries.push(DatasetEntry::Bar { data, style });
166            }
167            DatasetMeta::Candlestick { data_id, style } => {
168                let data_ref = gx.compile_ref(data_id).await?;
169                let data = TRef::new(data_ref).context("chart ohlc data")?;
170                entries.push(DatasetEntry::Candlestick { data, style });
171            }
172            DatasetMeta::ErrorBar { data_id, style } => {
173                let data_ref = gx.compile_ref(data_id).await?;
174                let data = TRef::new(data_ref).context("chart errorbar data")?;
175                entries.push(DatasetEntry::ErrorBar { data, style });
176            }
177            DatasetMeta::Pie { data_id, style } => {
178                let data_ref = gx.compile_ref(data_id).await?;
179                let data = TRef::new(data_ref).context("chart pie data")?;
180                entries.push(DatasetEntry::Pie { data, style });
181            }
182            DatasetMeta::Scatter3D { data_id, style } => {
183                let data_ref = gx.compile_ref(data_id).await?;
184                let data = TRef::new(data_ref).context("chart scatter3d data")?;
185                entries.push(DatasetEntry::Scatter3D { data, style });
186            }
187            DatasetMeta::Line3D { data_id, style } => {
188                let data_ref = gx.compile_ref(data_id).await?;
189                let data = TRef::new(data_ref).context("chart line3d data")?;
190                entries.push(DatasetEntry::Line3D { data, style });
191            }
192            DatasetMeta::Surface { data_id, style } => {
193                let data_ref = gx.compile_ref(data_id).await?;
194                let data = TRef::new(data_ref).context("chart surface data")?;
195                entries.push(DatasetEntry::Surface { data, style });
196            }
197        }
198    }
199    Ok(entries)
200}
201
202// ── Chart mode detection ────────────────────────────────────────────
203
204#[derive(Clone, Copy, PartialEq, Eq)]
205pub enum ChartMode {
206    Numeric,
207    TimeSeries,
208    Bar,
209    Pie,
210    ThreeD,
211    Empty,
212}
213
214pub fn chart_mode<X: GXExt>(datasets: &[DatasetEntry<X>]) -> ChartMode {
215    let mut has_bar = false;
216    let mut has_pie = false;
217    let mut has_3d = false;
218    let mut has_other = false;
219    for ds in datasets {
220        match ds {
221            DatasetEntry::XY { data, .. } | DatasetEntry::DashedLine { data, .. } => {
222                if let Some(d) = data.t.as_ref() {
223                    match d {
224                        XYData::DateTime(v) if !v.is_empty() => has_other = true,
225                        XYData::Numeric(v) if !v.is_empty() => has_other = true,
226                        _ => {}
227                    }
228                }
229            }
230            DatasetEntry::Bar { data, .. } => {
231                if let Some(d) = data.t.as_ref() {
232                    if !d.0.is_empty() {
233                        has_bar = true;
234                    }
235                }
236            }
237            DatasetEntry::Candlestick { data, .. } => {
238                if let Some(d) = data.t.as_ref() {
239                    match d {
240                        OHLCData::DateTime(v) if !v.is_empty() => has_other = true,
241                        OHLCData::Numeric(v) if !v.is_empty() => has_other = true,
242                        _ => {}
243                    }
244                }
245            }
246            DatasetEntry::ErrorBar { data, .. } => {
247                if let Some(d) = data.t.as_ref() {
248                    match d {
249                        EBData::DateTime(v) if !v.is_empty() => has_other = true,
250                        EBData::Numeric(v) if !v.is_empty() => has_other = true,
251                        _ => {}
252                    }
253                }
254            }
255            DatasetEntry::Pie { data, .. } => {
256                if let Some(d) = data.t.as_ref() {
257                    if !d.0.is_empty() {
258                        has_pie = true;
259                    }
260                }
261            }
262            DatasetEntry::Scatter3D { data, .. } | DatasetEntry::Line3D { data, .. } => {
263                if let Some(d) = data.t.as_ref() {
264                    if !d.0.is_empty() {
265                        has_3d = true;
266                    }
267                }
268            }
269            DatasetEntry::Surface { data, .. } => {
270                if let Some(d) = data.t.as_ref() {
271                    if !d.0.is_empty() {
272                        has_3d = true;
273                    }
274                }
275            }
276        }
277    }
278    let mode_count = has_bar as u8 + has_pie as u8 + has_3d as u8 + has_other as u8;
279    if mode_count > 1 {
280        error!("chart: cannot mix bar, pie, 3D, and XY/timeseries datasets");
281        return ChartMode::Empty;
282    }
283    if has_pie {
284        return ChartMode::Pie;
285    }
286    if has_bar {
287        return ChartMode::Bar;
288    }
289    if has_3d {
290        return ChartMode::ThreeD;
291    }
292    // Determine numeric vs timeseries from first non-empty dataset
293    for ds in datasets {
294        match ds {
295            DatasetEntry::XY { data, .. } | DatasetEntry::DashedLine { data, .. } => {
296                if let Some(d) = data.t.as_ref() {
297                    match d {
298                        XYData::DateTime(v) if !v.is_empty() => {
299                            return ChartMode::TimeSeries
300                        }
301                        XYData::Numeric(v) if !v.is_empty() => return ChartMode::Numeric,
302                        _ => {}
303                    }
304                }
305            }
306            DatasetEntry::Bar { .. }
307            | DatasetEntry::Pie { .. }
308            | DatasetEntry::Scatter3D { .. }
309            | DatasetEntry::Line3D { .. }
310            | DatasetEntry::Surface { .. } => {}
311            DatasetEntry::Candlestick { data, .. } => {
312                if let Some(d) = data.t.as_ref() {
313                    match d {
314                        OHLCData::DateTime(v) if !v.is_empty() => {
315                            return ChartMode::TimeSeries;
316                        }
317                        OHLCData::Numeric(v) if !v.is_empty() => {
318                            return ChartMode::Numeric
319                        }
320                        _ => {}
321                    }
322                }
323            }
324            DatasetEntry::ErrorBar { data, .. } => {
325                if let Some(d) = data.t.as_ref() {
326                    match d {
327                        EBData::DateTime(v) if !v.is_empty() => {
328                            return ChartMode::TimeSeries;
329                        }
330                        EBData::Numeric(v) if !v.is_empty() => return ChartMode::Numeric,
331                        _ => {}
332                    }
333                }
334            }
335        }
336    }
337    ChartMode::Empty
338}