use std::fmt;
use arrow_array::cast::AsArray;
use arrow_array::types::{
ArrowPrimitiveType, Date32Type, Date64Type, Decimal128Type, Float32Type, Float64Type,
Int16Type, Int32Type, Int64Type, Int8Type, TimestampMicrosecondType, TimestampMillisecondType,
TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
};
use arrow_array::{Array, ArrayRef};
use arrow_schema::{DataType, TimeUnit};
use densa_chart::Points;
use densa_data::{LiveTable, View, ViewId};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SeriesError {
NoSuchColumn(String),
NotNumeric(String),
}
impl fmt::Display for SeriesError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoSuchColumn(name) => write!(f, "no column named {name:?}"),
Self::NotNumeric(name) => write!(f, "column {name:?} is not numeric"),
}
}
}
impl std::error::Error for SeriesError {}
type NumberFn = fn(&dyn Array, usize) -> f64;
#[derive(Debug, Clone, Copy)]
struct Column {
index: usize,
read: NumberFn,
}
#[derive(Debug, Clone)]
pub struct LiveSeries {
names: (String, String),
columns: Option<(Column, Column)>,
arrays: Vec<(ArrayRef, ArrayRef)>,
table: (u64, u64),
}
impl LiveSeries {
pub fn new(table: &LiveTable, x: &str, y: &str) -> Result<Self, SeriesError> {
let columns = (column(table, x)?, column(table, y)?);
let mut series = Self {
names: (x.to_owned(), y.to_owned()),
columns: Some(columns),
arrays: Vec::new(),
table: (table.instance(), table.generation()),
};
series.sync(table);
Ok(series)
}
pub fn sync(&mut self, table: &LiveTable) {
let current = (table.instance(), table.generation());
if current != self.table {
if self.table.0 != current.0 {
self.columns = column(table, &self.names.0)
.and_then(|x| Ok((x, column(table, &self.names.1)?)))
.ok();
}
self.arrays.clear();
self.table = current;
}
let Some((x, y)) = self.columns else {
return;
};
let new = table.batches().get(self.arrays.len()..).unwrap_or_default();
for batch in new {
self.arrays
.push((batch.column(x.index).clone(), batch.column(y.index).clone()));
}
}
pub fn x_name(&self) -> &str {
&self.names.0
}
pub fn y_name(&self) -> &str {
&self.names.1
}
pub fn is_resolved(&self) -> bool {
self.columns.is_some()
}
pub fn points<'a>(&'a self, table: &'a LiveTable, view: ViewId) -> Option<SeriesPoints<'a>> {
let (x, y) = self.columns?;
let synced = self.table == (table.instance(), table.generation())
&& self.arrays.len() == table.batches().len();
if !synced {
return None;
}
Some(SeriesPoints {
arrays: &self.arrays,
x: x.read,
y: y.read,
rows: table.view(view)?,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct SeriesPoints<'a> {
arrays: &'a [(ArrayRef, ArrayRef)],
x: NumberFn,
y: NumberFn,
rows: View<'a>,
}
impl SeriesPoints<'_> {
pub fn x_extent(&self) -> Option<(f64, f64)> {
let first = self.x(0);
if !first.is_finite() {
return None;
}
let (mut low, mut high) = (0, self.rows.len());
while low < high {
let middle = low + (high - low) / 2;
if self.x(middle).is_finite() {
low = middle + 1;
} else {
high = middle;
}
}
let last = self.x(low - 1);
Some((first.min(last), first.max(last)))
}
fn read(
&self,
index: usize,
pick: fn(&(ArrayRef, ArrayRef)) -> &ArrayRef,
read: NumberFn,
) -> f64 {
self.rows
.row(index)
.and_then(|at| Some((self.arrays.get(at.batch)?, at.row)))
.map_or(f64::NAN, |(arrays, row)| read(pick(arrays).as_ref(), row))
}
}
impl Points for SeriesPoints<'_> {
fn len(&self) -> usize {
self.rows.len()
}
fn x(&self, index: usize) -> f64 {
self.read(index, |arrays| &arrays.0, self.x)
}
fn y(&self, index: usize) -> f64 {
self.read(index, |arrays| &arrays.1, self.y)
}
}
fn column(table: &LiveTable, name: &str) -> Result<Column, SeriesError> {
let schema = table.schema();
let (index, field) = schema
.column_with_name(name)
.ok_or_else(|| SeriesError::NoSuchColumn(name.to_owned()))?;
let read =
number_reader(field.data_type()).ok_or_else(|| SeriesError::NotNumeric(name.to_owned()))?;
Ok(Column { index, read })
}
fn number_reader(data_type: &DataType) -> Option<NumberFn> {
Some(match data_type {
DataType::Int8 => read::<Int8Type>,
DataType::Int16 => read::<Int16Type>,
DataType::Int32 => read::<Int32Type>,
DataType::Int64 => read::<Int64Type>,
DataType::UInt8 => read::<UInt8Type>,
DataType::UInt16 => read::<UInt16Type>,
DataType::UInt32 => read::<UInt32Type>,
DataType::UInt64 => read::<UInt64Type>,
DataType::Float32 => read::<Float32Type>,
DataType::Float64 => read::<Float64Type>,
DataType::Decimal128(_, _) => read_decimal,
DataType::Timestamp(TimeUnit::Second, _) => read::<TimestampSecondType>,
DataType::Timestamp(TimeUnit::Millisecond, _) => read_scaled::<TimestampMillisecondType, 3>,
DataType::Timestamp(TimeUnit::Microsecond, _) => read_scaled::<TimestampMicrosecondType, 6>,
DataType::Timestamp(TimeUnit::Nanosecond, _) => read_scaled::<TimestampNanosecondType, 9>,
DataType::Date32 => read_days,
DataType::Date64 => read_scaled::<Date64Type, 3>,
_ => return None,
})
}
trait ToNumber {
fn to_number(self) -> f64;
}
macro_rules! to_number {
($($t:ty),*) => {$(
impl ToNumber for $t {
fn to_number(self) -> f64 {
self as f64
}
}
)*};
}
to_number!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
fn read<T>(array: &dyn Array, row: usize) -> f64
where
T: ArrowPrimitiveType,
T::Native: ToNumber,
{
match array.as_primitive_opt::<T>() {
Some(values) if row < values.len() && values.is_valid(row) => values.value(row).to_number(),
_ => f64::NAN,
}
}
fn read_scaled<T, const DIGITS: i32>(array: &dyn Array, row: usize) -> f64
where
T: ArrowPrimitiveType,
T::Native: ToNumber,
{
read::<T>(array, row) / 10f64.powi(DIGITS)
}
fn read_days(array: &dyn Array, row: usize) -> f64 {
read::<Date32Type>(array, row) * 86_400.0
}
fn read_decimal(array: &dyn Array, row: usize) -> f64 {
match array.as_primitive_opt::<Decimal128Type>() {
Some(values) if row < values.len() && values.is_valid(row) => {
let value = values.value(row) as f64;
let scale = i32::from(values.scale());
if scale >= 0 {
value / 10f64.powi(scale)
} else {
value * 10f64.powi(-scale)
}
}
_ => f64::NAN,
}
}