Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bar charts + box plots continued #863

Merged
merged 9 commits into from
Nov 29, 2021
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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea of having our own OrderedFloat without having to pull in the much bigger ordered-float crate. There already is an epaint::f32_hash function in epaint/src/lib.rs. Perhaps we should implement Hash on OrderedFloat and move it to epaint/src/util/ordered_float.rs.

But let's make it support NaN instead of panicing on them!

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can also help with this refactor after merge

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I'll gladly open a new PR after this one, but probably makes sense to keep it out of this (already big) changeset.


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 {
Bromeon marked this conversation as resolved.
Show resolved Hide resolved
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