Skip to content

Commit

Permalink
Add bar charts and box plots (#863)
Browse files Browse the repository at this point in the history
Changes:
* New `BarChart` and `BoxPlot` diagrams
* New `FloatOrd` trait for total ordering of float types
* Refactoring of existing plot items

Co-authored-by: niladic <git@nil.choron.cc>
  • Loading branch information
Bromeon and niladic committed Nov 29, 2021
1 parent 224d4d6 commit 1088d95
Show file tree
Hide file tree
Showing 12 changed files with 1,808 additions and 415 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
## Unreleased

### Added ⭐
* Add bar charts and box plots ([#863](https://github.com/emilk/egui/pull/863)).
* Add context menus: See `Ui::menu_button` and `Response::context_menu` ([#543](https://github.com/emilk/egui/pull/543)).
* You can now read and write the cursor of a `TextEdit` ([#848](https://github.com/emilk/egui/pull/848)).
* Most widgets containing text (`Label`, `Button` etc) now supports rich text ([#855](https://github.com/emilk/egui/pull/855)).
Expand Down
64 changes: 64 additions & 0 deletions egui/src/util/float_ord.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! Total order on floating point types, assuming absence of NaN.
//! Can be used for sorting, min/max computation, and other collection algorithms.

use std::cmp::Ordering;

/// Totally orderable floating-point value
/// For not `f32` is supported; could be made generic if necessary.
pub(crate) struct OrderedFloat(f32);

impl Eq for OrderedFloat {}

impl PartialEq<Self> for OrderedFloat {
#[inline]
fn eq(&self, other: &Self) -> bool {
// NaNs are considered equal (equivalent when it comes to ordering
if self.0.is_nan() {
other.0.is_nan()
} else {
self.0 == other.0
}
}
}

impl PartialOrd<Self> for OrderedFloat {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match self.0.partial_cmp(&other.0) {
Some(ord) => Some(ord),
None => Some(self.0.is_nan().cmp(&other.0.is_nan())),
}
}
}

impl Ord for OrderedFloat {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
match self.partial_cmp(other) {
Some(ord) => ord,
None => unreachable!(),
}
}
}

/// Extension trait to provide `ord` method
pub(crate) trait FloatOrd {
/// Type to provide total order, useful as key in sorted contexts.
fn ord(self) -> OrderedFloat;
}

impl FloatOrd for f32 {
#[inline]
fn ord(self) -> OrderedFloat {
OrderedFloat(self)
}
}

// TODO ordering may break down at least significant digits due to f64 -> f32 conversion
// Possible solutions: generic OrderedFloat<T>, always OrderedFloat(f64)
impl FloatOrd for f64 {
#[inline]
fn ord(self) -> OrderedFloat {
OrderedFloat(self as f32)
}
}
1 change: 1 addition & 0 deletions egui/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

pub mod cache;
pub(crate) mod fixed_cache;
pub(crate) mod float_ord;
mod history;
pub mod id_type_map;
pub mod undoer;
Expand Down
190 changes: 190 additions & 0 deletions egui/src/widgets/plot/items/bar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
use crate::emath::NumExt;
use crate::epaint::{Color32, RectShape, Shape, Stroke};

use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement};
use crate::plot::{BarChart, ScreenTransform, Value};

/// One bar in a [`BarChart`]. Potentially floating, allowing stacked bar charts.
/// Width can be changed to allow variable-width histograms.
#[derive(Clone, Debug, PartialEq)]
pub struct Bar {
/// Name of plot element in the diagram (annotated by default formatter)
pub name: String,

/// Which direction the bar faces in the diagram
pub orientation: Orientation,

/// Position on the argument (input) axis -- X if vertical, Y if horizontal
pub argument: f64,

/// Position on the value (output) axis -- Y if vertical, X if horizontal
pub value: f64,

/// For stacked bars, this denotes where the bar starts. None if base axis
pub base_offset: Option<f64>,

/// Thickness of the bar
pub bar_width: f64,

/// Line width and color
pub stroke: Stroke,

/// Fill color
pub fill: Color32,
}

impl Bar {
/// Create a bar. Its `orientation` is set by its [`BarChart`] parent.
///
/// - `argument`: Position on the argument axis (X if vertical, Y if horizontal).
/// - `value`: Height of the bar (if vertical).
///
/// By default the bar is vertical and its base is at zero.
pub fn new(argument: f64, height: f64) -> Bar {
Bar {
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,
}
}

/// Name of this bar chart element.
#[allow(clippy::needless_pass_by_value)]
pub fn name(mut self, name: impl ToString) -> Self {
self.name = name.to_string();
self
}

/// Add a custom stroke.
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
self.stroke = stroke.into();
self
}

/// Add a custom fill color.
pub fn fill(mut self, color: impl Into<Color32>) -> Self {
self.fill = color.into();
self
}

/// Offset the base of the bar.
/// This offset is on the Y axis for a vertical bar
/// and on the X axis for a horizontal bar.
pub fn base_offset(mut self, offset: f64) -> Self {
self.base_offset = Some(offset);
self
}

/// Set the bar width.
pub fn width(mut self, width: f64) -> Self {
self.bar_width = width;
self
}

/// Set orientation of the element as vertical. Argument axis is X.
pub fn vertical(mut self) -> Self {
self.orientation = Orientation::Vertical;
self
}

/// Set orientation of the element as horizontal. Argument axis is Y.
pub fn horizontal(mut self) -> Self {
self.orientation = Orientation::Horizontal;
self
}

pub(super) 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(super) 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(super) fn add_shapes(
&self,
transform: &ScreenTransform,
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 {
rect,
corner_radius: 0.0,
fill,
stroke,
});

shapes.push(rect);
}

pub(super) fn add_rulers_and_text(
&self,
parent: &BarChart,
plot: &PlotConfig<'_>,
shapes: &mut Vec<Shape>,
) {
let text: Option<String> = parent
.element_formatter
.as_ref()
.map(|fmt| fmt(self, parent));

add_rulers_and_text(self, plot, text, shapes);
}
}

impl RectElement for Bar {
fn name(&self) -> &str {
self.name.as_str()
}

fn bounds_min(&self) -> Value {
self.point_at(self.argument - self.bar_width / 2.0, self.lower())
}

fn bounds_max(&self) -> Value {
self.point_at(self.argument + self.bar_width / 2.0, self.upper())
}

fn values_with_ruler(&self) -> Vec<Value> {
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: &ScreenTransform) -> String {
let scale = transform.dvalue_dpos();
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
format!("\n{:.*}", y_decimals, self.value)
}
}
Loading

0 comments on commit 1088d95

Please sign in to comment.