use std::marker::PhantomData;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders};
use super::{Component, EventContext, RenderContext};
use crate::input::{Event, Key};
mod render;
mod state;
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct BoxPlotData {
label: String,
min: f64,
q1: f64,
median: f64,
q3: f64,
max: f64,
outliers: Vec<f64>,
color: Color,
}
impl BoxPlotData {
pub fn new(
label: impl Into<String>,
min: f64,
q1: f64,
median: f64,
q3: f64,
max: f64,
) -> Self {
Self {
label: label.into(),
min,
q1,
median,
q3,
max,
outliers: Vec::new(),
color: Color::Cyan,
}
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn with_outliers(mut self, outliers: Vec<f64>) -> Self {
self.outliers = outliers;
self
}
pub fn label(&self) -> &str {
&self.label
}
pub fn min(&self) -> f64 {
self.min
}
pub fn q1(&self) -> f64 {
self.q1
}
pub fn median(&self) -> f64 {
self.median
}
pub fn q3(&self) -> f64 {
self.q3
}
pub fn max(&self) -> f64 {
self.max
}
pub fn outliers(&self) -> &[f64] {
&self.outliers
}
pub fn color(&self) -> Color {
self.color
}
pub fn iqr(&self) -> f64 {
self.q3 - self.q1
}
pub fn range(&self) -> f64 {
self.max - self.min
}
pub fn set_label(&mut self, label: impl Into<String>) {
self.label = label.into();
}
pub fn set_color(&mut self, color: Color) {
self.color = color;
}
pub fn set_outliers(&mut self, outliers: Vec<f64>) {
self.outliers = outliers;
}
pub fn add_outlier(&mut self, value: f64) {
self.outliers.push(value);
}
pub fn overall_min(&self) -> f64 {
let outlier_min = self
.outliers
.iter()
.copied()
.reduce(f64::min)
.unwrap_or(self.min);
f64::min(self.min, outlier_min)
}
pub fn overall_max(&self) -> f64 {
let outlier_max = self
.outliers
.iter()
.copied()
.reduce(f64::max)
.unwrap_or(self.max);
f64::max(self.max, outlier_max)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum BoxPlotOrientation {
Vertical,
Horizontal,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum BoxPlotMessage {
NextDataset,
PrevDataset,
ToggleOutliers,
SetDatasets(Vec<BoxPlotData>),
AddDataset(BoxPlotData),
ClearDatasets,
SetOrientation(BoxPlotOrientation),
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct BoxPlotState {
datasets: Vec<BoxPlotData>,
title: Option<String>,
show_outliers: bool,
orientation: BoxPlotOrientation,
selected: usize,
}
impl Default for BoxPlotState {
fn default() -> Self {
Self {
datasets: Vec::new(),
title: None,
show_outliers: true,
orientation: BoxPlotOrientation::Vertical,
selected: 0,
}
}
}
pub struct BoxPlot(PhantomData<()>);
impl Component for BoxPlot {
type State = BoxPlotState;
type Message = BoxPlotMessage;
type Output = ();
fn init() -> Self::State {
BoxPlotState::default()
}
fn handle_event(
_state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
let key = event.as_key()?;
match key.code {
Key::Right | Key::Char('l') => Some(BoxPlotMessage::NextDataset),
Key::Left | Key::Char('h') => Some(BoxPlotMessage::PrevDataset),
Key::Char('o') => Some(BoxPlotMessage::ToggleOutliers),
_ => None,
}
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
BoxPlotMessage::NextDataset => {
if !state.datasets.is_empty() {
state.selected = (state.selected + 1) % state.datasets.len();
}
}
BoxPlotMessage::PrevDataset => {
if !state.datasets.is_empty() {
state.selected = if state.selected == 0 {
state.datasets.len() - 1
} else {
state.selected - 1
};
}
}
BoxPlotMessage::ToggleOutliers => {
state.show_outliers = !state.show_outliers;
}
BoxPlotMessage::SetDatasets(datasets) => {
state.datasets = datasets;
state.selected = 0;
}
BoxPlotMessage::AddDataset(dataset) => {
state.datasets.push(dataset);
}
BoxPlotMessage::ClearDatasets => {
state.datasets.clear();
state.selected = 0;
}
BoxPlotMessage::SetOrientation(orientation) => {
state.orientation = orientation;
}
}
None
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
if ctx.area.height < 3 || ctx.area.width < 3 {
return;
}
crate::annotation::with_registry(|reg| {
reg.register(
ctx.area,
crate::annotation::Annotation::container("box_plot")
.with_focus(ctx.focused)
.with_disabled(ctx.disabled),
);
});
let border_style = if ctx.disabled {
ctx.theme.disabled_style()
} else if ctx.focused {
ctx.theme.focused_border_style()
} else {
ctx.theme.border_style()
};
let mut block = Block::default()
.borders(Borders::ALL)
.border_style(border_style);
if let Some(ref title) = state.title {
block = block.title(title.as_str());
}
let inner = block.inner(ctx.area);
ctx.frame.render_widget(block, ctx.area);
if inner.height == 0 || inner.width == 0 || state.datasets.is_empty() {
return;
}
match state.orientation {
BoxPlotOrientation::Vertical => {
render::render_vertical(
state,
ctx.frame,
inner,
ctx.theme,
ctx.focused,
ctx.disabled,
);
}
BoxPlotOrientation::Horizontal => {
render::render_horizontal(
state,
ctx.frame,
inner,
ctx.theme,
ctx.focused,
ctx.disabled,
);
}
}
}
}
#[cfg(test)]
mod snapshot_tests;
#[cfg(test)]
mod tests;