From 453047b3d1e82b93a3035e1fdd821c6e111542f1 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Mon, 17 Jan 2022 13:20:44 +0100 Subject: [PATCH 1/5] Fix Orientation not exposed, although there are public fields with its type --- egui/src/widgets/plot/items/mod.rs | 4 ++-- egui/src/widgets/plot/items/values.rs | 1 + egui/src/widgets/plot/mod.rs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/egui/src/widgets/plot/items/mod.rs b/egui/src/widgets/plot/items/mod.rs index 99f7fb86d99..d4edb8b325e 100644 --- a/egui/src/widgets/plot/items/mod.rs +++ b/egui/src/widgets/plot/items/mod.rs @@ -9,11 +9,11 @@ use crate::*; use super::{CustomLabelFuncRef, PlotBounds, ScreenTransform}; use rect_elem::*; -use values::*; +use values::{ClosestElem, PlotGeometry}; pub use bar::Bar; pub use box_elem::{BoxElem, BoxSpread}; -pub use values::{LineStyle, MarkerShape, Value, Values}; +pub use values::{LineStyle, MarkerShape, Orientation, Value, Values}; mod bar; mod box_elem; diff --git a/egui/src/widgets/plot/items/values.rs b/egui/src/widgets/plot/items/values.rs index 01410c916c1..5b198752cf6 100644 --- a/egui/src/widgets/plot/items/values.rs +++ b/egui/src/widgets/plot/items/values.rs @@ -125,6 +125,7 @@ impl ToString for LineStyle { // ---------------------------------------------------------------------------- +/// Determines whether a plot element is vertically or horizontally oriented. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Orientation { Horizontal, diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 23f60bc67ff..7f72c6284d8 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -10,7 +10,7 @@ use transform::{PlotBounds, ScreenTransform}; pub use items::{ Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape, - PlotImage, Points, Polygon, Text, VLine, Value, Values, + Orientation, PlotImage, Points, Polygon, Text, VLine, Value, Values, }; pub use legend::{Corner, Legend}; From d3d5e59f9a884560ab30d9cf87ff83c705966cfd Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Mon, 17 Jan 2022 18:27:37 +0100 Subject: [PATCH 2/5] Implement formatters for X/Y axis labels --- CHANGELOG.md | 2 + egui/src/widgets/plot/mod.rs | 67 +++++++++++++++++++++--- egui_demo_lib/src/apps/demo/plot_demo.rs | 33 +++++++++++- 3 files changed, 93 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b24912a1495..687a55142e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w ### Added ⭐ * `Context::load_texture` to convert an image into a texture which can be displayed using e.g. `ui.image(texture, size)` ([#1110](https://github.com/emilk/egui/pull/1110)). * Added `Ui::add_visible` and `Ui::add_visible_ui`. +* Added `Plot::x_axis_formatter` and `Plot::y_axis_formatter` for custom axis labels ([#1130](https://github.com/emilk/egui/pull/1130)) ### Changed 🔧 * ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding! @@ -23,6 +24,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w ### Fixed 🐛 * Context menu now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043)) +* Plot `Orientation` was not public, although fields using this type were ([#1130](https://github.com/emilk/egui/pull/1130)) ### Contributors 🙏 * [danielkeller](https://github.com/danielkeller): [#1050](https://github.com/emilk/egui/pull/1050). diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 7f72c6284d8..dc1d1dc53af 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -21,6 +21,9 @@ mod transform; type CustomLabelFunc = dyn Fn(&str, &Value) -> String; type CustomLabelFuncRef = Option>; +type AxisFormatterFn = dyn Fn(f64) -> String; +type AxisFormatter = Option>; + // ---------------------------------------------------------------------------- /// Information about the plot that has to persist between frames. @@ -80,6 +83,8 @@ pub struct Plot { show_x: bool, show_y: bool, custom_label_func: CustomLabelFuncRef, + x_axis_formatter: AxisFormatter, + y_axis_formatter: AxisFormatter, legend_config: Option, show_background: bool, show_axes: [bool; 2], @@ -107,6 +112,8 @@ impl Plot { show_x: true, show_y: true, custom_label_func: None, + x_axis_formatter: None, + y_axis_formatter: None, legend_config: None, show_background: true, show_axes: [true; 2], @@ -216,6 +223,30 @@ impl Plot { self } + /// Provide a function to customize the labels for the X axis. + /// + /// This is useful for custom input domains, e.g. date/time. + /// + /// If axis labels should not appear for certain values or beyond a certain zoom/resolution, + /// the formatter function can return empty strings. This is also useful if your domain is + /// discrete (e.g. only full days in a calendar). + pub fn x_axis_formatter String>(mut self, func: F) -> Self { + self.x_axis_formatter = Some(Box::new(func)); + self + } + + /// Provide a function to customize the labels for the Y axis. + /// + /// This is useful for custom value representation, e.g. percentage or units. + /// + /// If axis labels should not appear for certain values or beyond a certain zoom/resolution, + /// the formatter function can return empty strings. This is also useful if your Y values are + /// discrete (e.g. only integers). + pub fn y_axis_formatter String>(mut self, func: F) -> Self { + self.y_axis_formatter = Some(Box::new(func)); + self + } + /// Expand bounds to include the given x value. /// For instance, to always show the y axis, call `plot.include_x(0.0)`. pub fn include_x(mut self, x: impl Into) -> Self { @@ -270,6 +301,8 @@ impl Plot { mut show_x, mut show_y, custom_label_func, + x_axis_formatter, + y_axis_formatter, legend_config, show_background, show_axes, @@ -442,6 +475,8 @@ impl Plot { show_x, show_y, custom_label_func, + x_axis_formatter, + y_axis_formatter, show_axes, transform: transform.clone(), }; @@ -650,6 +685,8 @@ struct PreparedPlot { show_x: bool, show_y: bool, custom_label_func: CustomLabelFuncRef, + x_axis_formatter: AxisFormatter, + y_axis_formatter: AxisFormatter, show_axes: [bool; 2], transform: ScreenTransform, } @@ -739,18 +776,32 @@ impl PreparedPlot { if text_alpha > 0.0 { let color = color_from_alpha(ui, text_alpha); - let text = emath::round_to_decimals(value_main, 5).to_string(); // hack - let galley = ui.painter().layout_no_wrap(text, text_style, color); + let relevant_formatter = if axis == 0 { + &self.x_axis_formatter + } else { + &self.y_axis_formatter + }; - let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y); + let text: String = if let Some(formatter) = relevant_formatter { + formatter(value_main) + } else { + emath::round_to_decimals(value_main, 5).to_string() // hack + }; + + // Custom formatters can return empty string to signal "no label at this resolution" + if !text.is_empty() { + let galley = ui.painter().layout_no_wrap(text, text_style, color); - // Make sure we see the labels, even if the axis is off-screen: - text_pos[1 - axis] = text_pos[1 - axis] - .at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0) - .at_least(transform.frame().min[1 - axis] + 1.0); + let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y); - shapes.push(Shape::galley(text_pos, galley)); + // Make sure we see the labels, even if the axis is off-screen: + text_pos[1 - axis] = text_pos[1 - axis] + .at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0) + .at_least(transform.frame().min[1 - axis] + 1.0); + + shapes.push(Shape::galley(text_pos, galley)); + } } } diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 4e55e4bd9aa..85e414cc495 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -74,7 +74,7 @@ impl LineDemo { ui.checkbox(animate, "animate"); ui.checkbox(square, "square view") .on_hover_text("Always keep the viewport square."); - ui.checkbox(proportional, "Proportional data axes") + ui.checkbox(proportional, "proportional data axes") .on_hover_text("Tick are the same size on both axes."); ComboBox::from_label("Line style") @@ -544,8 +544,39 @@ impl ChartsDemo { chart4 = chart4.horizontal(); } + fn is_approx_zero(val: f64) -> bool { + val.abs() < 1e-6 + } + fn is_approx_integer(val: f64) -> bool { + val.fract().abs() < 1e-6 + } + + let x_fmt = |val: f64| { + if val >= 0.0 && val <= 4.0 && is_approx_integer(val) { + // Only label full days from 0 to 4 + format!("Day {}", val) + } else { + // Otherwise return empty string (i.e. no label) + String::new() + } + }; + + let y_fmt = |val: f64| { + let percent = 100.0 * val; + + if is_approx_integer(percent) && !is_approx_zero(percent) { + // Only show integer percentages, + // and don't show at Y=0 (label overlaps with X axis label) + format!("{}%", percent) + } else { + String::new() + } + }; + Plot::new("Stacked Bar Chart Demo") .legend(Legend::default()) + .x_axis_formatter(x_fmt) + .y_axis_formatter(y_fmt) .data_aspect(1.0) .show(ui, |plot_ui| { plot_ui.bar_chart(chart1); From 0f693c731038a709f8e70903105e30c6422a2f29 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Mon, 17 Jan 2022 19:55:56 +0100 Subject: [PATCH 3/5] Use array instead of separate X/Y formatters --- egui/src/widgets/plot/mod.rs | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index dc1d1dc53af..93760b8e85b 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -83,8 +83,7 @@ pub struct Plot { show_x: bool, show_y: bool, custom_label_func: CustomLabelFuncRef, - x_axis_formatter: AxisFormatter, - y_axis_formatter: AxisFormatter, + axis_formatters: [AxisFormatter; 2], legend_config: Option, show_background: bool, show_axes: [bool; 2], @@ -112,8 +111,7 @@ impl Plot { show_x: true, show_y: true, custom_label_func: None, - x_axis_formatter: None, - y_axis_formatter: None, + axis_formatters: [None, None], // [None; 2] requires Copy legend_config: None, show_background: true, show_axes: [true; 2], @@ -231,7 +229,7 @@ impl Plot { /// the formatter function can return empty strings. This is also useful if your domain is /// discrete (e.g. only full days in a calendar). pub fn x_axis_formatter String>(mut self, func: F) -> Self { - self.x_axis_formatter = Some(Box::new(func)); + self.axis_formatters[0] = Some(Box::new(func)); self } @@ -243,7 +241,7 @@ impl Plot { /// the formatter function can return empty strings. This is also useful if your Y values are /// discrete (e.g. only integers). pub fn y_axis_formatter String>(mut self, func: F) -> Self { - self.y_axis_formatter = Some(Box::new(func)); + self.axis_formatters[1] = Some(Box::new(func)); self } @@ -301,8 +299,7 @@ impl Plot { mut show_x, mut show_y, custom_label_func, - x_axis_formatter, - y_axis_formatter, + axis_formatters, legend_config, show_background, show_axes, @@ -475,8 +472,7 @@ impl Plot { show_x, show_y, custom_label_func, - x_axis_formatter, - y_axis_formatter, + axis_formatters, show_axes, transform: transform.clone(), }; @@ -685,8 +681,7 @@ struct PreparedPlot { show_x: bool, show_y: bool, custom_label_func: CustomLabelFuncRef, - x_axis_formatter: AxisFormatter, - y_axis_formatter: AxisFormatter, + axis_formatters: [AxisFormatter; 2], show_axes: [bool; 2], transform: ScreenTransform, } @@ -717,7 +712,11 @@ impl PreparedPlot { } fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec) { - let Self { transform, .. } = self; + let Self { + transform, + axis_formatters, + .. + } = self; let bounds = transform.bounds(); let text_style = TextStyle::Body; @@ -777,13 +776,7 @@ impl PreparedPlot { if text_alpha > 0.0 { let color = color_from_alpha(ui, text_alpha); - let relevant_formatter = if axis == 0 { - &self.x_axis_formatter - } else { - &self.y_axis_formatter - }; - - let text: String = if let Some(formatter) = relevant_formatter { + let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() { formatter(value_main) } else { emath::round_to_decimals(value_main, 5).to_string() // hack From a3e8186609b4f1e8a203a56ca2ad44b356d0e61b Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Mon, 17 Jan 2022 20:05:24 +0100 Subject: [PATCH 4/5] Swap axis formatters if charts are horizontal --- egui_demo_lib/src/apps/demo/plot_demo.rs | 34 +++++++++++++----------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 85e414cc495..5db990e9ac8 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -537,21 +537,7 @@ impl ChartsDemo { .name("Set 4") .stack_on(&[&chart1, &chart2, &chart3]); - if !self.vertical { - chart1 = chart1.horizontal(); - chart2 = chart2.horizontal(); - chart3 = chart3.horizontal(); - chart4 = chart4.horizontal(); - } - - fn is_approx_zero(val: f64) -> bool { - val.abs() < 1e-6 - } - fn is_approx_integer(val: f64) -> bool { - val.fract().abs() < 1e-6 - } - - let x_fmt = |val: f64| { + let mut x_fmt: fn(f64) -> String = |val| { if val >= 0.0 && val <= 4.0 && is_approx_integer(val) { // Only label full days from 0 to 4 format!("Day {}", val) @@ -561,7 +547,7 @@ impl ChartsDemo { } }; - let y_fmt = |val: f64| { + let mut y_fmt: fn(f64) -> String = |val| { let percent = 100.0 * val; if is_approx_integer(percent) && !is_approx_zero(percent) { @@ -573,6 +559,14 @@ impl ChartsDemo { } }; + if !self.vertical { + chart1 = chart1.horizontal(); + chart2 = chart2.horizontal(); + chart3 = chart3.horizontal(); + chart4 = chart4.horizontal(); + std::mem::swap(&mut x_fmt, &mut y_fmt); + } + Plot::new("Stacked Bar Chart Demo") .legend(Legend::default()) .x_axis_formatter(x_fmt) @@ -743,3 +737,11 @@ impl super::View for PlotDemo { } } } + +fn is_approx_zero(val: f64) -> bool { + val.abs() < 1e-6 +} + +fn is_approx_integer(val: f64) -> bool { + val.fract().abs() < 1e-6 +} From 005b961bcb3f73f3ac859e3f11105ae26dbc051f Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Mon, 24 Jan 2022 20:51:22 +0100 Subject: [PATCH 5/5] Review suggestions --- egui/src/widgets/plot/mod.rs | 10 +++++----- egui_demo_lib/src/apps/demo/plot_demo.rs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index ca9656a1600..a2bf6d1e625 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -213,11 +213,11 @@ impl Plot { /// .show(ui, |plot_ui| plot_ui.line(line)); /// # }); /// ``` - pub fn custom_label_func String>( + pub fn custom_label_func( mut self, - custom_lebel_func: F, + custom_label_func: impl Fn(&str, &Value) -> String + 'static, ) -> Self { - self.custom_label_func = Some(Box::new(custom_lebel_func)); + self.custom_label_func = Some(Box::new(custom_label_func)); self } @@ -228,7 +228,7 @@ impl Plot { /// If axis labels should not appear for certain values or beyond a certain zoom/resolution, /// the formatter function can return empty strings. This is also useful if your domain is /// discrete (e.g. only full days in a calendar). - pub fn x_axis_formatter String>(mut self, func: F) -> Self { + pub fn x_axis_formatter(mut self, func: impl Fn(f64) -> String + 'static) -> Self { self.axis_formatters[0] = Some(Box::new(func)); self } @@ -240,7 +240,7 @@ impl Plot { /// If axis labels should not appear for certain values or beyond a certain zoom/resolution, /// the formatter function can return empty strings. This is also useful if your Y values are /// discrete (e.g. only integers). - pub fn y_axis_formatter String>(mut self, func: F) -> Self { + pub fn y_axis_formatter(mut self, func: impl Fn(f64) -> String + 'static) -> Self { self.axis_formatters[1] = Some(Box::new(func)); self } diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 5d1d5f40b2e..931bc40a779 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -71,10 +71,10 @@ impl LineDemo { ui.vertical(|ui| { ui.style_mut().wrap = Some(false); - ui.checkbox(animate, "animate"); - ui.checkbox(square, "square view") + ui.checkbox(animate, "Animate"); + ui.checkbox(square, "Square view") .on_hover_text("Always keep the viewport square."); - ui.checkbox(proportional, "proportional data axes") + ui.checkbox(proportional, "Proportional data axes") .on_hover_text("Tick are the same size on both axes."); ComboBox::from_label("Line style")