use std::borrow::Cow;
use polars::prelude::Schema;
use crate::chart::chart_data::{
self, BarData, BoxPlotData, ChartXRangeResult, HeatmapData, HistogramData, KdeData, RowsRead,
XAxisTemporalKind,
};
use crate::chart::chart_modal::{Aggregate, ChartModal, ChartSpec, Mark};
use crate::numfmt;
use crate::widgets::axis_numbers::AxisNumbers;
#[derive(Debug, Clone)]
pub enum PlotData {
Lines(LinesData),
XRange(ChartXRangeResult),
Histogram(HistogramData),
Box(BoxPlotData),
Kde(KdeData),
Heatmap(HeatmapData),
Bars(BarData),
}
#[derive(Debug, Clone, Default)]
pub struct LinesData {
pub names: Vec<String>,
pub series: Vec<Vec<(f64, f64)>>,
pub series_log: Option<Vec<Vec<(f64, f64)>>>,
pub breaks: Vec<Vec<usize>>,
pub xs: Vec<f64>,
pub bounds: Option<[f64; 4]>,
pub x_axis_kind: XAxisTemporalKind,
pub rows: RowsRead,
pub rows_note: Option<String>,
pub other: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct Drawn<'a> {
pub index: usize,
pub name: &'a str,
pub points: &'a [(f64, f64)],
pub breaks: &'a [usize],
pub other: bool,
}
impl LinesData {
pub fn new(grouped: chart_data::GroupedSeries, rows_note: Option<String>) -> Self {
Self {
series_log: None,
xs: crate::widgets::crosshair::xs(&grouped.series),
bounds: extent(&grouped.series),
names: grouped.names,
series: grouped.series,
breaks: grouped.breaks,
x_axis_kind: grouped.x_axis_kind,
rows: grouped.rows,
rows_note,
other: grouped.other,
}
}
pub fn keep_log(&mut self, log: bool) {
match (log, self.series_log.is_some()) {
(true, false) => self.series_log = Some(log_series(&self.series)),
(false, true) => self.series_log = None,
_ => {}
}
}
pub fn shown(&self, log: bool) -> &[Vec<(f64, f64)>] {
match (&self.series_log, log) {
(Some(logged), true) => logged,
_ => &self.series,
}
}
pub fn shown_bounds(&self, log: bool) -> Option<[f64; 4]> {
match self.bounds {
Some([x0, x1, y0, y1]) if log && self.series_log.is_some() => {
Some([x0, x1, log_y(y0), log_y(y1)])
}
Some(bounds) => Some(bounds),
None => extent(self.shown(log)),
}
}
pub fn drawn(&self, log: bool) -> impl Iterator<Item = Drawn<'_>> {
let last = self.names.len().saturating_sub(1);
self.shown(log)
.iter()
.zip(self.names.iter())
.enumerate()
.filter(|(_, (points, _))| !points.is_empty())
.map(move |(index, (points, name))| Drawn {
index,
name,
points,
breaks: self.breaks.get(index).map_or(&[][..], Vec::as_slice),
other: self.other && index == last,
})
}
}
pub fn extent(series: &[Vec<(f64, f64)>]) -> Option<[f64; 4]> {
let mut points = series.iter().flatten().peekable();
points.peek()?;
Some(points.fold(
[
f64::INFINITY,
f64::NEG_INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
],
|[x0, x1, y0, y1], &(x, y)| [x0.min(x), x1.max(x), y0.min(y), y1.max(y)],
))
}
fn log_y(y: f64) -> f64 {
y.max(0.0).ln_1p()
}
fn log_series(series: &[Vec<(f64, f64)>]) -> Vec<Vec<(f64, f64)>> {
series
.iter()
.map(|points| points.iter().map(|&(x, y)| (x, log_y(y))).collect())
.collect()
}
impl PlotData {
pub fn is_empty(&self) -> bool {
match self {
Self::Lines(lines) => lines.drawn(false).next().is_none(),
Self::XRange(_) => true,
Self::Bars(data) => data.bars.is_empty(),
Self::Histogram(data) => data.bins.is_empty(),
Self::Kde(data) => data.series.is_empty(),
Self::Box(data) => data.stats.is_empty(),
Self::Heatmap(data) => data.counts.is_empty() || data.max_count <= 0.0,
}
}
pub fn notes(&self, middot: &str) -> Vec<String> {
let rows_of = |rows: usize| crate::home::discover::format_rows(rows);
match self {
Self::Bars(d) => {
let mut notes = chart_data::chart_notes(&d.rows, None, middot);
if let Some(note) = &d.rows_note {
notes.push(note.clone());
} else if let Some(rows) = d.counted {
notes.push(format!("counts of {} rows", rows_of(rows)));
} else if d.rows.sample_size.is_none() && !d.value_column.is_empty() {
notes.push(format!(
"all {} rows",
numfmt::group_chrome(d.rows.total_rows)
));
}
if d.no_value > 0 {
let noun = if d.no_value == 1 {
"category"
} else {
"categories"
};
notes.push(format!(
"{} {noun} without a value",
numfmt::group_chrome(d.no_value)
));
}
notes
}
Self::Lines(c) if c.rows_note.is_some() => c.rows_note.iter().cloned().collect(),
Self::Lines(c) => chart_data::chart_notes(&c.rows, None, middot),
Self::XRange(c) => chart_data::chart_notes(&c.rows, None, middot),
Self::Histogram(d) => chart_data::chart_notes(&d.rows, d.clipped.as_ref(), middot),
Self::Box(d) => {
let mut notes = chart_data::chart_notes(&d.rows, d.clipped.as_ref(), middot);
if d.of > 0 {
notes.push(format!(
"the {} largest of {} categories",
d.stats.len(),
numfmt::group_chrome(d.of)
));
}
notes
}
Self::Kde(d) => chart_data::chart_notes(&d.rows, d.clipped.as_ref(), middot),
Self::Heatmap(d) => chart_data::chart_notes(&d.rows, None, middot),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Axis {
pub title: String,
pub numbers: AxisNumbers,
pub kind: XAxisTemporalKind,
pub log: bool,
}
#[derive(Debug, Clone)]
pub struct Plot<'a> {
pub data: Cow<'a, PlotData>,
pub x: Axis,
pub y: Axis,
pub scatter: bool,
pub y_from_zero: bool,
}
impl Plot<'_> {
pub fn into_owned(self) -> Plot<'static> {
Plot {
data: Cow::Owned(self.data.into_owned()),
..self
}
}
pub fn drawn(&self) -> impl Iterator<Item = Drawn<'_>> {
let lines = match &*self.data {
PlotData::Lines(lines) => Some(lines),
_ => None,
};
lines.into_iter().flat_map(|lines| lines.drawn(self.y.log))
}
pub fn curves(&self) -> Vec<Curve<'_>> {
let curves: Vec<Curve<'_>> = match &*self.data {
PlotData::Lines(_) => self
.drawn()
.enumerate()
.map(|(slot, s)| Curve {
index: s.index,
slot,
points: Cow::Borrowed(s.points),
breaks: s.breaks,
other: s.other,
})
.collect(),
PlotData::Kde(data) => data
.series
.iter()
.enumerate()
.map(|(i, s)| Curve {
index: i,
slot: i,
points: Cow::Borrowed(&s.points),
breaks: &[],
other: data.other && i + 1 == data.series.len(),
})
.collect(),
PlotData::Histogram(data) if !data.groups.is_empty() => data
.step_outlines()
.into_iter()
.enumerate()
.map(|(i, points)| Curve {
index: i,
slot: i,
points: Cow::Owned(points),
breaks: &[],
other: data.other && i + 1 == data.groups.len(),
})
.collect(),
_ => Vec::new(),
};
let other = curves.iter().position(|c| c.other);
let mut curves: Vec<Option<Curve<'_>>> = curves.into_iter().map(Some).collect();
chart_data::drawing_order(curves.len(), other)
.filter_map(|i| curves[i].take())
.collect()
}
pub fn legend(&self) -> Vec<&str> {
match &*self.data {
PlotData::Lines(_) => self.drawn().map(|s| s.name).collect(),
PlotData::Bars(data) => data.groups.iter().map(String::as_str).collect(),
PlotData::Histogram(data) => data.groups.iter().map(|g| g.name.as_str()).collect(),
PlotData::Kde(data) => data.series.iter().map(|s| s.name.as_str()).collect(),
PlotData::Box(_) | PlotData::Heatmap(_) | PlotData::XRange(_) => Vec::new(),
}
}
pub fn other_at(&self) -> Option<usize> {
match &*self.data {
PlotData::Lines(_) => self.drawn().position(|s| s.other),
PlotData::Bars(data) => chart_data::other_at(data.other, data.groups.len()),
_ => self.curves().iter().find(|c| c.other).map(|c| c.slot),
}
}
}
#[derive(Debug, Clone)]
pub struct Curve<'a> {
pub index: usize,
pub slot: usize,
pub points: Cow<'a, [(f64, f64)]>,
pub breaks: &'a [usize],
pub other: bool,
}
pub struct PlotContext<'a> {
pub modal: &'a ChartModal,
pub spec: &'a ChartSpec,
pub numbers: &'a numfmt::NumberFormatSettings,
pub schema: Option<&'a Schema>,
}
pub fn plot<'a>(data: Option<&'a PlotData>, context: &PlotContext<'_>) -> Option<Plot<'a>> {
let PlotContext {
modal,
spec,
numbers,
schema,
} = *context;
let column = |name: &str| AxisNumbers::column(numbers, schema, name);
let title = |name: &str| modal.axis_title(name);
let axis = |title: String, numbers: AxisNumbers| Axis {
title,
numbers,
..Default::default()
};
let encoding = &spec.encoding;
let x_name = encoding.x.field.as_deref();
let ys = &encoding.y.field;
let (x, y) = match (spec.mark, data) {
(Mark::Line | Mark::Scatter, data) => return Some(lines(data, context)),
(Mark::Histogram, Some(PlotData::Histogram(data))) => (
axis(title(&data.column), column(&data.column)),
if data.share {
axis("Share".to_string(), AxisNumbers::measure(numbers, "Share"))
} else {
axis("Count".to_string(), AxisNumbers::count(numbers))
},
),
(Mark::Kde, Some(PlotData::Kde(_))) => (
axis(
x_name.map(title).unwrap_or_default(),
x_name.map(column).unwrap_or_default().fractional(),
),
axis(
"Density".to_string(),
AxisNumbers::measure(numbers, "Density"),
),
),
(Mark::Box, Some(PlotData::Box(_))) => (
axis(
x_name.unwrap_or_default().to_string(),
AxisNumbers::default(),
),
axis(
ys.first().map(|y| title(y)).unwrap_or_default(),
AxisNumbers::columns(numbers, schema, ys),
),
),
(Mark::Heatmap, Some(PlotData::Heatmap(data))) => (
axis(title(&data.x_column), column(&data.x_column)),
axis(title(&data.y_column), column(&data.y_column)),
),
(Mark::Bar, Some(PlotData::Bars(data))) => (
axis(
data.value_column.clone(),
AxisNumbers {
format: data.value_format(numbers),
whole: data.value_dtype.is_integer(),
},
),
Axis::default(),
),
_ => return None,
};
Some(Plot {
data: Cow::Borrowed(data?),
x,
y,
scatter: false,
y_from_zero: false,
})
}
fn lines<'a>(data: Option<&'a PlotData>, context: &PlotContext<'_>) -> Plot<'a> {
let PlotContext {
modal,
spec,
numbers,
schema,
} = *context;
let encoding = &spec.encoding;
let ys = &encoding.y.field;
let aggregate = encoding.y.aggregate;
let y_numbers = match aggregate {
Aggregate::Count | Aggregate::Distinct => AxisNumbers::count(numbers),
a if a.is_fractional() => AxisNumbers::columns(numbers, schema, ys).fractional(),
_ => AxisNumbers::columns(numbers, schema, ys),
};
let y_title = if aggregate == Aggregate::Count {
"count".to_string()
} else {
ys.iter()
.map(|y| modal.axis_title(y))
.collect::<Vec<_>>()
.join(", ")
};
let x_name = encoding.x.field.as_deref();
let kind = match data {
Some(PlotData::Lines(lines)) => lines.x_axis_kind,
Some(PlotData::XRange(range)) => range.x_axis_kind,
_ => match (x_name, schema) {
(Some(x), Some(schema)) => chart_data::x_axis_temporal_kind_for_column(schema, x),
_ => XAxisTemporalKind::Numeric,
},
};
let data = match data {
Some(PlotData::Lines(lines)) if modal.log_scale && lines.series_log.is_none() => {
let mut lines = lines.clone();
lines.keep_log(true);
Cow::Owned(PlotData::Lines(lines))
}
Some(data @ (PlotData::Lines(_) | PlotData::XRange(_))) => Cow::Borrowed(data),
_ => Cow::Owned(PlotData::Lines(LinesData::default())),
};
Plot {
data,
x: Axis {
title: x_name.map(|x| modal.axis_title(x)).unwrap_or_default(),
numbers: x_name
.map(|x| AxisNumbers::column(numbers, schema, x))
.unwrap_or_default(),
kind,
log: false,
},
y: Axis {
title: y_title,
numbers: y_numbers,
log: modal.log_scale,
..Default::default()
},
scatter: spec.mark == Mark::Scatter,
y_from_zero: modal.y_starts_at_zero,
}
}