Skip to content

Commit 1088d95

Browse files
Bromeonniladic
andauthored
Add bar charts and box plots (#863)
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>
1 parent 224d4d6 commit 1088d95

File tree

12 files changed

+1808
-415
lines changed

12 files changed

+1808
-415
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
88
## Unreleased
99

1010
### Added ⭐
11+
* Add bar charts and box plots ([#863](https://github.com/emilk/egui/pull/863)).
1112
* Add context menus: See `Ui::menu_button` and `Response::context_menu` ([#543](https://github.com/emilk/egui/pull/543)).
1213
* You can now read and write the cursor of a `TextEdit` ([#848](https://github.com/emilk/egui/pull/848)).
1314
* Most widgets containing text (`Label`, `Button` etc) now supports rich text ([#855](https://github.com/emilk/egui/pull/855)).

egui/src/util/float_ord.rs

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//! Total order on floating point types, assuming absence of NaN.
2+
//! Can be used for sorting, min/max computation, and other collection algorithms.
3+
4+
use std::cmp::Ordering;
5+
6+
/// Totally orderable floating-point value
7+
/// For not `f32` is supported; could be made generic if necessary.
8+
pub(crate) struct OrderedFloat(f32);
9+
10+
impl Eq for OrderedFloat {}
11+
12+
impl PartialEq<Self> for OrderedFloat {
13+
#[inline]
14+
fn eq(&self, other: &Self) -> bool {
15+
// NaNs are considered equal (equivalent when it comes to ordering
16+
if self.0.is_nan() {
17+
other.0.is_nan()
18+
} else {
19+
self.0 == other.0
20+
}
21+
}
22+
}
23+
24+
impl PartialOrd<Self> for OrderedFloat {
25+
#[inline]
26+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
27+
match self.0.partial_cmp(&other.0) {
28+
Some(ord) => Some(ord),
29+
None => Some(self.0.is_nan().cmp(&other.0.is_nan())),
30+
}
31+
}
32+
}
33+
34+
impl Ord for OrderedFloat {
35+
#[inline]
36+
fn cmp(&self, other: &Self) -> Ordering {
37+
match self.partial_cmp(other) {
38+
Some(ord) => ord,
39+
None => unreachable!(),
40+
}
41+
}
42+
}
43+
44+
/// Extension trait to provide `ord` method
45+
pub(crate) trait FloatOrd {
46+
/// Type to provide total order, useful as key in sorted contexts.
47+
fn ord(self) -> OrderedFloat;
48+
}
49+
50+
impl FloatOrd for f32 {
51+
#[inline]
52+
fn ord(self) -> OrderedFloat {
53+
OrderedFloat(self)
54+
}
55+
}
56+
57+
// TODO ordering may break down at least significant digits due to f64 -> f32 conversion
58+
// Possible solutions: generic OrderedFloat<T>, always OrderedFloat(f64)
59+
impl FloatOrd for f64 {
60+
#[inline]
61+
fn ord(self) -> OrderedFloat {
62+
OrderedFloat(self as f32)
63+
}
64+
}

egui/src/util/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
pub mod cache;
44
pub(crate) mod fixed_cache;
5+
pub(crate) mod float_ord;
56
mod history;
67
pub mod id_type_map;
78
pub mod undoer;

egui/src/widgets/plot/items/bar.rs

+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
use crate::emath::NumExt;
2+
use crate::epaint::{Color32, RectShape, Shape, Stroke};
3+
4+
use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement};
5+
use crate::plot::{BarChart, ScreenTransform, Value};
6+
7+
/// One bar in a [`BarChart`]. Potentially floating, allowing stacked bar charts.
8+
/// Width can be changed to allow variable-width histograms.
9+
#[derive(Clone, Debug, PartialEq)]
10+
pub struct Bar {
11+
/// Name of plot element in the diagram (annotated by default formatter)
12+
pub name: String,
13+
14+
/// Which direction the bar faces in the diagram
15+
pub orientation: Orientation,
16+
17+
/// Position on the argument (input) axis -- X if vertical, Y if horizontal
18+
pub argument: f64,
19+
20+
/// Position on the value (output) axis -- Y if vertical, X if horizontal
21+
pub value: f64,
22+
23+
/// For stacked bars, this denotes where the bar starts. None if base axis
24+
pub base_offset: Option<f64>,
25+
26+
/// Thickness of the bar
27+
pub bar_width: f64,
28+
29+
/// Line width and color
30+
pub stroke: Stroke,
31+
32+
/// Fill color
33+
pub fill: Color32,
34+
}
35+
36+
impl Bar {
37+
/// Create a bar. Its `orientation` is set by its [`BarChart`] parent.
38+
///
39+
/// - `argument`: Position on the argument axis (X if vertical, Y if horizontal).
40+
/// - `value`: Height of the bar (if vertical).
41+
///
42+
/// By default the bar is vertical and its base is at zero.
43+
pub fn new(argument: f64, height: f64) -> Bar {
44+
Bar {
45+
argument,
46+
value: height,
47+
orientation: Orientation::default(),
48+
name: Default::default(),
49+
base_offset: None,
50+
bar_width: 0.5,
51+
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
52+
fill: Color32::TRANSPARENT,
53+
}
54+
}
55+
56+
/// Name of this bar chart element.
57+
#[allow(clippy::needless_pass_by_value)]
58+
pub fn name(mut self, name: impl ToString) -> Self {
59+
self.name = name.to_string();
60+
self
61+
}
62+
63+
/// Add a custom stroke.
64+
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
65+
self.stroke = stroke.into();
66+
self
67+
}
68+
69+
/// Add a custom fill color.
70+
pub fn fill(mut self, color: impl Into<Color32>) -> Self {
71+
self.fill = color.into();
72+
self
73+
}
74+
75+
/// Offset the base of the bar.
76+
/// This offset is on the Y axis for a vertical bar
77+
/// and on the X axis for a horizontal bar.
78+
pub fn base_offset(mut self, offset: f64) -> Self {
79+
self.base_offset = Some(offset);
80+
self
81+
}
82+
83+
/// Set the bar width.
84+
pub fn width(mut self, width: f64) -> Self {
85+
self.bar_width = width;
86+
self
87+
}
88+
89+
/// Set orientation of the element as vertical. Argument axis is X.
90+
pub fn vertical(mut self) -> Self {
91+
self.orientation = Orientation::Vertical;
92+
self
93+
}
94+
95+
/// Set orientation of the element as horizontal. Argument axis is Y.
96+
pub fn horizontal(mut self) -> Self {
97+
self.orientation = Orientation::Horizontal;
98+
self
99+
}
100+
101+
pub(super) fn lower(&self) -> f64 {
102+
if self.value.is_sign_positive() {
103+
self.base_offset.unwrap_or(0.0)
104+
} else {
105+
self.base_offset.map_or(self.value, |o| o + self.value)
106+
}
107+
}
108+
109+
pub(super) fn upper(&self) -> f64 {
110+
if self.value.is_sign_positive() {
111+
self.base_offset.map_or(self.value, |o| o + self.value)
112+
} else {
113+
self.base_offset.unwrap_or(0.0)
114+
}
115+
}
116+
117+
pub(super) fn add_shapes(
118+
&self,
119+
transform: &ScreenTransform,
120+
highlighted: bool,
121+
shapes: &mut Vec<Shape>,
122+
) {
123+
let (stroke, fill) = if highlighted {
124+
highlighted_color(self.stroke, self.fill)
125+
} else {
126+
(self.stroke, self.fill)
127+
};
128+
129+
let rect = transform.rect_from_values(&self.bounds_min(), &self.bounds_max());
130+
let rect = Shape::Rect(RectShape {
131+
rect,
132+
corner_radius: 0.0,
133+
fill,
134+
stroke,
135+
});
136+
137+
shapes.push(rect);
138+
}
139+
140+
pub(super) fn add_rulers_and_text(
141+
&self,
142+
parent: &BarChart,
143+
plot: &PlotConfig<'_>,
144+
shapes: &mut Vec<Shape>,
145+
) {
146+
let text: Option<String> = parent
147+
.element_formatter
148+
.as_ref()
149+
.map(|fmt| fmt(self, parent));
150+
151+
add_rulers_and_text(self, plot, text, shapes);
152+
}
153+
}
154+
155+
impl RectElement for Bar {
156+
fn name(&self) -> &str {
157+
self.name.as_str()
158+
}
159+
160+
fn bounds_min(&self) -> Value {
161+
self.point_at(self.argument - self.bar_width / 2.0, self.lower())
162+
}
163+
164+
fn bounds_max(&self) -> Value {
165+
self.point_at(self.argument + self.bar_width / 2.0, self.upper())
166+
}
167+
168+
fn values_with_ruler(&self) -> Vec<Value> {
169+
let base = self.base_offset.unwrap_or(0.0);
170+
let value_center = self.point_at(self.argument, base + self.value);
171+
172+
let mut ruler_positions = vec![value_center];
173+
174+
if let Some(offset) = self.base_offset {
175+
ruler_positions.push(self.point_at(self.argument, offset));
176+
}
177+
178+
ruler_positions
179+
}
180+
181+
fn orientation(&self) -> Orientation {
182+
self.orientation
183+
}
184+
185+
fn default_values_format(&self, transform: &ScreenTransform) -> String {
186+
let scale = transform.dvalue_dpos();
187+
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
188+
format!("\n{:.*}", y_decimals, self.value)
189+
}
190+
}

0 commit comments

Comments
 (0)