use std::ops::RangeInclusive;
use egui::Color32;
use egui::CornerRadius;
use egui::Id;
use egui::Shape;
use egui::Stroke;
use egui::Ui;
use egui::epaint::RectShape;
use emath::Float as _;
use emath::NumExt as _;
use emath::Pos2;
use crate::aesthetics::Orientation;
use crate::axis::PlotTransform;
use crate::bounds::PlotBounds;
use crate::bounds::PlotPoint;
use crate::colors::highlighted_color;
use crate::cursor::Cursor;
use crate::items::ClosestElem;
use crate::items::PlotConfig;
use crate::items::PlotGeometry;
use crate::items::PlotItem;
use crate::items::PlotItemBase;
use crate::items::add_rulers_and_text;
use crate::label::LabelFormatter;
use crate::math::find_closest_rect;
use crate::rect_elem::RectElement;
pub struct BarChart {
base: PlotItemBase,
pub(crate) bars: Vec<Bar>,
default_color: Color32,
pub(crate) element_formatter: Option<Box<dyn Fn(&Bar, &Self) -> String>>,
}
impl BarChart {
pub fn new(name: impl Into<String>, bars: Vec<Bar>) -> Self {
Self {
base: PlotItemBase::new(name.into()),
bars,
default_color: Color32::TRANSPARENT,
element_formatter: None,
}
}
#[inline]
pub fn color(mut self, color: impl Into<Color32>) -> Self {
let plot_color = color.into();
self.default_color = plot_color;
for b in &mut self.bars {
if b.fill == Color32::TRANSPARENT && b.stroke.color == Color32::TRANSPARENT {
b.fill = plot_color.linear_multiply(0.2);
b.stroke.color = plot_color;
}
}
self
}
#[inline]
pub fn vertical(mut self) -> Self {
for b in &mut self.bars {
b.orientation = Orientation::Vertical;
}
self
}
#[inline]
pub fn horizontal(mut self) -> Self {
for b in &mut self.bars {
b.orientation = Orientation::Horizontal;
}
self
}
#[inline]
pub fn width(mut self, width: f64) -> Self {
for b in &mut self.bars {
b.bar_width = width;
}
self
}
#[inline]
pub fn element_formatter(mut self, formatter: Box<dyn Fn(&Bar, &Self) -> String>) -> Self {
self.element_formatter = Some(formatter);
self
}
#[inline]
pub fn stack_on(mut self, others: &[&Self]) -> Self {
for (index, bar) in self.bars.iter_mut().enumerate() {
let new_base_offset = if bar.value.is_sign_positive() {
others
.iter()
.filter_map(|other_chart| other_chart.bars.get(index).map(|bar| bar.upper()))
.max_by_key(|value| value.ord())
} else {
others
.iter()
.filter_map(|other_chart| other_chart.bars.get(index).map(|bar| bar.lower()))
.min_by_key(|value| value.ord())
};
if let Some(value) = new_base_offset {
bar.base_offset = Some(value);
}
}
self
}
#[expect(clippy::needless_pass_by_value, reason = "to allow various string types")]
#[inline]
pub fn name(mut self, name: impl ToString) -> Self {
self.base_mut().name = name.to_string();
self
}
#[inline]
pub fn highlight(mut self, highlight: bool) -> Self {
self.base_mut().highlight = highlight;
self
}
#[inline]
pub fn allow_hover(mut self, hovering: bool) -> Self {
self.base_mut().allow_hover = hovering;
self
}
#[inline]
pub fn id(mut self, id: impl Into<Id>) -> Self {
self.base_mut().id = id.into();
self
}
}
impl PlotItem for BarChart {
fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
for b in &self.bars {
b.add_shapes(transform, self.base.highlight, shapes);
}
}
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
}
fn color(&self) -> Color32 {
self.default_color
}
fn geometry(&self) -> PlotGeometry<'_> {
PlotGeometry::Rects
}
fn bounds(&self) -> PlotBounds {
let mut bounds = PlotBounds::NOTHING;
for b in &self.bars {
bounds.merge(&b.bounds());
}
bounds
}
fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option<ClosestElem> {
find_closest_rect(&self.bars, point, transform)
}
fn on_hover(
&self,
_plot_area_response: &egui::Response,
elem: ClosestElem,
shapes: &mut Vec<Shape>,
cursors: &mut Vec<Cursor>,
plot: &PlotConfig<'_>,
_: &Option<LabelFormatter<'_>>,
) {
let bar = &self.bars[elem.index];
bar.add_shapes(plot.transform, true, shapes);
bar.add_rulers_and_text(self, plot, shapes, cursors);
}
fn base(&self) -> &PlotItemBase {
&self.base
}
fn base_mut(&mut self) -> &mut PlotItemBase {
&mut self.base
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Bar {
pub name: String,
pub orientation: Orientation,
pub argument: f64,
pub value: f64,
pub base_offset: Option<f64>,
pub bar_width: f64,
pub stroke: Stroke,
pub fill: Color32,
}
impl Bar {
pub fn new(argument: f64, height: f64) -> Self {
Self {
argument,
value: height,
orientation: Orientation::default(),
name: Default::default(),
base_offset: None,
bar_width: 0.5,
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
fill: Color32::TRANSPARENT,
}
}
#[expect(clippy::needless_pass_by_value, reason = "to allow various string types")]
#[inline]
pub fn name(mut self, name: impl ToString) -> Self {
self.name = name.to_string();
self
}
#[inline]
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
self.stroke = stroke.into();
self
}
#[inline]
pub fn fill(mut self, color: impl Into<Color32>) -> Self {
self.fill = color.into();
self
}
#[inline]
pub fn base_offset(mut self, offset: f64) -> Self {
self.base_offset = Some(offset);
self
}
#[inline]
pub fn width(mut self, width: f64) -> Self {
self.bar_width = width;
self
}
#[inline]
pub fn vertical(mut self) -> Self {
self.orientation = Orientation::Vertical;
self
}
#[inline]
pub fn horizontal(mut self) -> Self {
self.orientation = Orientation::Horizontal;
self
}
pub(in crate::items) fn lower(&self) -> f64 {
if self.value.is_sign_positive() {
self.base_offset.unwrap_or(0.0)
} else {
self.base_offset.map_or(self.value, |o| o + self.value)
}
}
pub(in crate::items) fn upper(&self) -> f64 {
if self.value.is_sign_positive() {
self.base_offset.map_or(self.value, |o| o + self.value)
} else {
self.base_offset.unwrap_or(0.0)
}
}
pub(in crate::items) fn add_shapes(&self, transform: &PlotTransform, highlighted: bool, shapes: &mut Vec<Shape>) {
let (stroke, fill) = if highlighted {
highlighted_color(self.stroke, self.fill)
} else {
(self.stroke, self.fill)
};
let rect = transform.rect_from_values(&self.bounds_min(), &self.bounds_max());
let rect = Shape::Rect(RectShape::new(
rect,
CornerRadius::ZERO,
fill,
stroke,
egui::StrokeKind::Inside,
));
shapes.push(rect);
}
pub(in crate::items) fn add_rulers_and_text(
&self,
parent: &BarChart,
plot: &PlotConfig<'_>,
shapes: &mut Vec<Shape>,
cursors: &mut Vec<Cursor>,
) {
let text: Option<String> = parent.element_formatter.as_ref().map(|fmt| fmt(self, parent));
add_rulers_and_text(self, plot, text, shapes, cursors);
}
}
impl RectElement for Bar {
fn name(&self) -> &str {
self.name.as_str()
}
fn bounds_min(&self) -> PlotPoint {
self.point_at(self.argument - self.bar_width / 2.0, self.lower())
}
fn bounds_max(&self) -> PlotPoint {
self.point_at(self.argument + self.bar_width / 2.0, self.upper())
}
fn values_with_ruler(&self) -> Vec<PlotPoint> {
let base = self.base_offset.unwrap_or(0.0);
let value_center = self.point_at(self.argument, base + self.value);
let mut ruler_positions = vec![value_center];
if let Some(offset) = self.base_offset {
ruler_positions.push(self.point_at(self.argument, offset));
}
ruler_positions
}
fn orientation(&self) -> Orientation {
self.orientation
}
fn default_values_format(&self, transform: &PlotTransform) -> String {
let scale = transform.dvalue_dpos();
let scale = match self.orientation {
Orientation::Horizontal => scale[0],
Orientation::Vertical => scale[1],
};
let decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
crate::label::format_number(self.value, decimals)
}
}