1use std::fmt;
4
5use arrow_array::cast::AsArray;
6use arrow_array::types::{
7 ArrowPrimitiveType, Date32Type, Date64Type, Decimal128Type, Float32Type, Float64Type,
8 Int16Type, Int32Type, Int64Type, Int8Type, TimestampMicrosecondType, TimestampMillisecondType,
9 TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
10};
11use arrow_array::{Array, ArrayRef};
12use arrow_schema::{DataType, TimeUnit};
13use densa_chart::Points;
14use densa_data::{LiveTable, View, ViewId};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum SeriesError {
20 NoSuchColumn(String),
22 NotNumeric(String),
24}
25
26impl fmt::Display for SeriesError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Self::NoSuchColumn(name) => write!(f, "no column named {name:?}"),
30 Self::NotNumeric(name) => write!(
31 f,
32 "column {name:?} is not numeric: a series reads integers, f32 and f64 floats, \
33 decimals, timestamps and dates"
34 ),
35 }
36 }
37}
38
39impl std::error::Error for SeriesError {}
40
41type NumberFn = fn(&dyn Array, usize) -> f64;
43
44#[derive(Debug, Clone, Copy)]
46struct Column {
47 index: usize,
48 read: NumberFn,
49}
50
51#[derive(Debug, Clone)]
71pub struct LiveSeries {
72 names: (String, String),
73 columns: Option<(Column, Column)>,
75 arrays: Vec<(ArrayRef, ArrayRef)>,
77 table: (u64, u64),
79}
80
81impl LiveSeries {
82 pub fn new(table: &LiveTable, x: &str, y: &str) -> Result<Self, SeriesError> {
87 let columns = (column(table, x)?, column(table, y)?);
88 let mut series = Self {
89 names: (x.to_owned(), y.to_owned()),
90 columns: Some(columns),
91 arrays: Vec::new(),
92 table: (table.instance(), table.generation()),
93 };
94 series.sync(table);
95 Ok(series)
96 }
97
98 pub fn sync(&mut self, table: &LiveTable) {
102 let current = (table.instance(), table.generation());
103 if current != self.table {
104 if self.table.0 != current.0 {
105 self.columns = column(table, &self.names.0)
106 .and_then(|x| Ok((x, column(table, &self.names.1)?)))
107 .ok();
108 }
109 self.arrays.clear();
110 self.table = current;
111 }
112 let Some((x, y)) = self.columns else {
113 return;
114 };
115 let new = table.batches().get(self.arrays.len()..).unwrap_or_default();
116 for batch in new {
117 self.arrays
118 .push((batch.column(x.index).clone(), batch.column(y.index).clone()));
119 }
120 }
121
122 pub fn x_name(&self) -> &str {
124 &self.names.0
125 }
126
127 pub fn y_name(&self) -> &str {
129 &self.names.1
130 }
131
132 pub fn is_resolved(&self) -> bool {
135 self.columns.is_some()
136 }
137
138 pub fn points<'a>(&'a self, table: &'a LiveTable, view: ViewId) -> Option<SeriesPoints<'a>> {
142 let (x, y) = self.columns?;
143 let synced = self.table == (table.instance(), table.generation())
144 && self.arrays.len() == table.batches().len();
145 if !synced {
146 return None;
147 }
148 Some(SeriesPoints {
149 arrays: &self.arrays,
150 x: x.read,
151 y: y.read,
152 rows: table.view(view)?,
153 })
154 }
155}
156
157#[derive(Debug, Clone, Copy)]
160pub struct SeriesPoints<'a> {
161 arrays: &'a [(ArrayRef, ArrayRef)],
162 x: NumberFn,
163 y: NumberFn,
164 rows: View<'a>,
165}
166
167impl SeriesPoints<'_> {
168 pub fn x_extent(&self) -> Option<(f64, f64)> {
171 let first = self.x(0);
172 if !first.is_finite() {
173 return None;
174 }
175 let (mut low, mut high) = (0, self.rows.len());
177 while low < high {
178 let middle = low + (high - low) / 2;
179 if self.x(middle).is_finite() {
180 low = middle + 1;
181 } else {
182 high = middle;
183 }
184 }
185 let last = self.x(low - 1);
186 Some((first.min(last), first.max(last)))
187 }
188
189 fn read(
191 &self,
192 index: usize,
193 pick: fn(&(ArrayRef, ArrayRef)) -> &ArrayRef,
194 read: NumberFn,
195 ) -> f64 {
196 self.rows
197 .row(index)
198 .and_then(|at| Some((self.arrays.get(at.batch)?, at.row)))
199 .map_or(f64::NAN, |(arrays, row)| read(pick(arrays).as_ref(), row))
200 }
201}
202
203impl Points for SeriesPoints<'_> {
204 fn len(&self) -> usize {
205 self.rows.len()
206 }
207
208 fn x(&self, index: usize) -> f64 {
209 self.read(index, |arrays| &arrays.0, self.x)
210 }
211
212 fn y(&self, index: usize) -> f64 {
213 self.read(index, |arrays| &arrays.1, self.y)
214 }
215}
216
217fn column(table: &LiveTable, name: &str) -> Result<Column, SeriesError> {
219 let schema = table.schema();
220 let (index, field) = schema
221 .column_with_name(name)
222 .ok_or_else(|| SeriesError::NoSuchColumn(name.to_owned()))?;
223 let read =
224 number_reader(field.data_type()).ok_or_else(|| SeriesError::NotNumeric(name.to_owned()))?;
225 Ok(Column { index, read })
226}
227
228fn number_reader(data_type: &DataType) -> Option<NumberFn> {
230 Some(match data_type {
231 DataType::Int8 => read::<Int8Type>,
232 DataType::Int16 => read::<Int16Type>,
233 DataType::Int32 => read::<Int32Type>,
234 DataType::Int64 => read::<Int64Type>,
235 DataType::UInt8 => read::<UInt8Type>,
236 DataType::UInt16 => read::<UInt16Type>,
237 DataType::UInt32 => read::<UInt32Type>,
238 DataType::UInt64 => read::<UInt64Type>,
239 DataType::Float32 => read::<Float32Type>,
240 DataType::Float64 => read::<Float64Type>,
241 DataType::Decimal128(_, _) => read_decimal,
242 DataType::Timestamp(TimeUnit::Second, _) => read::<TimestampSecondType>,
243 DataType::Timestamp(TimeUnit::Millisecond, _) => read_scaled::<TimestampMillisecondType, 3>,
244 DataType::Timestamp(TimeUnit::Microsecond, _) => read_scaled::<TimestampMicrosecondType, 6>,
245 DataType::Timestamp(TimeUnit::Nanosecond, _) => read_scaled::<TimestampNanosecondType, 9>,
246 DataType::Date32 => read_days,
247 DataType::Date64 => read_scaled::<Date64Type, 3>,
248 _ => return None,
249 })
250}
251
252trait ToNumber {
254 fn to_number(self) -> f64;
255}
256
257macro_rules! to_number {
258 ($($t:ty),*) => {$(
259 impl ToNumber for $t {
260 fn to_number(self) -> f64 {
261 self as f64
263 }
264 }
265 )*};
266}
267
268to_number!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
269
270fn read<T>(array: &dyn Array, row: usize) -> f64
272where
273 T: ArrowPrimitiveType,
274 T::Native: ToNumber,
275{
276 match array.as_primitive_opt::<T>() {
277 Some(values) if row < values.len() && values.is_valid(row) => values.value(row).to_number(),
278 _ => f64::NAN,
279 }
280}
281
282fn read_scaled<T, const DIGITS: i32>(array: &dyn Array, row: usize) -> f64
284where
285 T: ArrowPrimitiveType,
286 T::Native: ToNumber,
287{
288 read::<T>(array, row) / 10f64.powi(DIGITS)
289}
290
291fn read_days(array: &dyn Array, row: usize) -> f64 {
293 read::<Date32Type>(array, row) * 86_400.0
294}
295
296fn read_decimal(array: &dyn Array, row: usize) -> f64 {
298 match array.as_primitive_opt::<Decimal128Type>() {
299 Some(values) if row < values.len() && values.is_valid(row) => {
300 let value = values.value(row) as f64;
302 let scale = i32::from(values.scale());
303 if scale >= 0 {
306 value / 10f64.powi(scale)
307 } else {
308 value * 10f64.powi(-scale)
309 }
310 }
311 _ => f64::NAN,
312 }
313}