Formatter for plot axis labels (#1130)
* Fix Orientation not exposed, although there are public fields with its type * Implement formatters for X/Y axis labels * Use array instead of separate X/Y formatters * Swap axis formatters if charts are horizontal * Review suggestions
This commit is contained in:
parent
3418eb5d35
commit
366f544655
5 changed files with 97 additions and 17 deletions
|
@ -16,6 +16,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
|
||||||
* `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)).
|
* `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 `Ui::add_visible` and `Ui::add_visible_ui`.
|
||||||
* Added `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147))
|
* Added `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147))
|
||||||
|
* Added `Plot::x_axis_formatter` and `Plot::y_axis_formatter` for custom axis labels ([#1130](https://github.com/emilk/egui/pull/1130))
|
||||||
|
|
||||||
### Changed 🔧
|
### Changed 🔧
|
||||||
* ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding!
|
* ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding!
|
||||||
|
@ -36,6 +37,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
|
||||||
|
|
||||||
### Fixed 🐛
|
### Fixed 🐛
|
||||||
* Context menu now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043))
|
* 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 🙏
|
### Contributors 🙏
|
||||||
* [danielkeller](https://github.com/danielkeller): [#1050](https://github.com/emilk/egui/pull/1050).
|
* [danielkeller](https://github.com/danielkeller): [#1050](https://github.com/emilk/egui/pull/1050).
|
||||||
|
|
|
@ -9,11 +9,11 @@ use crate::*;
|
||||||
|
|
||||||
use super::{CustomLabelFuncRef, PlotBounds, ScreenTransform};
|
use super::{CustomLabelFuncRef, PlotBounds, ScreenTransform};
|
||||||
use rect_elem::*;
|
use rect_elem::*;
|
||||||
use values::*;
|
use values::{ClosestElem, PlotGeometry};
|
||||||
|
|
||||||
pub use bar::Bar;
|
pub use bar::Bar;
|
||||||
pub use box_elem::{BoxElem, BoxSpread};
|
pub use box_elem::{BoxElem, BoxSpread};
|
||||||
pub use values::{LineStyle, MarkerShape, Value, Values};
|
pub use values::{LineStyle, MarkerShape, Orientation, Value, Values};
|
||||||
|
|
||||||
mod bar;
|
mod bar;
|
||||||
mod box_elem;
|
mod box_elem;
|
||||||
|
|
|
@ -125,6 +125,7 @@ impl ToString for LineStyle {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Determines whether a plot element is vertically or horizontally oriented.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Orientation {
|
pub enum Orientation {
|
||||||
Horizontal,
|
Horizontal,
|
||||||
|
|
|
@ -10,7 +10,7 @@ use transform::{PlotBounds, ScreenTransform};
|
||||||
|
|
||||||
pub use items::{
|
pub use items::{
|
||||||
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape,
|
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};
|
pub use legend::{Corner, Legend};
|
||||||
|
|
||||||
|
@ -21,6 +21,9 @@ mod transform;
|
||||||
type CustomLabelFunc = dyn Fn(&str, &Value) -> String;
|
type CustomLabelFunc = dyn Fn(&str, &Value) -> String;
|
||||||
type CustomLabelFuncRef = Option<Box<CustomLabelFunc>>;
|
type CustomLabelFuncRef = Option<Box<CustomLabelFunc>>;
|
||||||
|
|
||||||
|
type AxisFormatterFn = dyn Fn(f64) -> String;
|
||||||
|
type AxisFormatter = Option<Box<AxisFormatterFn>>;
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Information about the plot that has to persist between frames.
|
/// Information about the plot that has to persist between frames.
|
||||||
|
@ -80,6 +83,7 @@ pub struct Plot {
|
||||||
show_x: bool,
|
show_x: bool,
|
||||||
show_y: bool,
|
show_y: bool,
|
||||||
custom_label_func: CustomLabelFuncRef,
|
custom_label_func: CustomLabelFuncRef,
|
||||||
|
axis_formatters: [AxisFormatter; 2],
|
||||||
legend_config: Option<Legend>,
|
legend_config: Option<Legend>,
|
||||||
show_background: bool,
|
show_background: bool,
|
||||||
show_axes: [bool; 2],
|
show_axes: [bool; 2],
|
||||||
|
@ -107,6 +111,7 @@ impl Plot {
|
||||||
show_x: true,
|
show_x: true,
|
||||||
show_y: true,
|
show_y: true,
|
||||||
custom_label_func: None,
|
custom_label_func: None,
|
||||||
|
axis_formatters: [None, None], // [None; 2] requires Copy
|
||||||
legend_config: None,
|
legend_config: None,
|
||||||
show_background: true,
|
show_background: true,
|
||||||
show_axes: [true; 2],
|
show_axes: [true; 2],
|
||||||
|
@ -208,11 +213,35 @@ impl Plot {
|
||||||
/// .show(ui, |plot_ui| plot_ui.line(line));
|
/// .show(ui, |plot_ui| plot_ui.line(line));
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn custom_label_func<F: 'static + Fn(&str, &Value) -> String>(
|
pub fn custom_label_func(
|
||||||
mut self,
|
mut self,
|
||||||
custom_lebel_func: F,
|
custom_label_func: impl Fn(&str, &Value) -> String + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.custom_label_func = Some(Box::new(custom_lebel_func));
|
self.custom_label_func = Some(Box::new(custom_label_func));
|
||||||
|
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(mut self, func: impl Fn(f64) -> String + 'static) -> Self {
|
||||||
|
self.axis_formatters[0] = 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(mut self, func: impl Fn(f64) -> String + 'static) -> Self {
|
||||||
|
self.axis_formatters[1] = Some(Box::new(func));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,6 +299,7 @@ impl Plot {
|
||||||
mut show_x,
|
mut show_x,
|
||||||
mut show_y,
|
mut show_y,
|
||||||
custom_label_func,
|
custom_label_func,
|
||||||
|
axis_formatters,
|
||||||
legend_config,
|
legend_config,
|
||||||
show_background,
|
show_background,
|
||||||
show_axes,
|
show_axes,
|
||||||
|
@ -442,6 +472,7 @@ impl Plot {
|
||||||
show_x,
|
show_x,
|
||||||
show_y,
|
show_y,
|
||||||
custom_label_func,
|
custom_label_func,
|
||||||
|
axis_formatters,
|
||||||
show_axes,
|
show_axes,
|
||||||
transform: transform.clone(),
|
transform: transform.clone(),
|
||||||
};
|
};
|
||||||
|
@ -650,6 +681,7 @@ struct PreparedPlot {
|
||||||
show_x: bool,
|
show_x: bool,
|
||||||
show_y: bool,
|
show_y: bool,
|
||||||
custom_label_func: CustomLabelFuncRef,
|
custom_label_func: CustomLabelFuncRef,
|
||||||
|
axis_formatters: [AxisFormatter; 2],
|
||||||
show_axes: [bool; 2],
|
show_axes: [bool; 2],
|
||||||
transform: ScreenTransform,
|
transform: ScreenTransform,
|
||||||
}
|
}
|
||||||
|
@ -680,7 +712,11 @@ impl PreparedPlot {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) {
|
fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) {
|
||||||
let Self { transform, .. } = self;
|
let Self {
|
||||||
|
transform,
|
||||||
|
axis_formatters,
|
||||||
|
..
|
||||||
|
} = self;
|
||||||
|
|
||||||
let bounds = transform.bounds();
|
let bounds = transform.bounds();
|
||||||
|
|
||||||
|
@ -740,18 +776,26 @@ impl PreparedPlot {
|
||||||
|
|
||||||
if text_alpha > 0.0 {
|
if text_alpha > 0.0 {
|
||||||
let color = color_from_alpha(ui, text_alpha);
|
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, font_id.clone(), color);
|
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
|
||||||
|
};
|
||||||
|
|
||||||
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
|
// 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, font_id.clone(), color);
|
||||||
|
|
||||||
// Make sure we see the labels, even if the axis is off-screen:
|
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
|
||||||
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));
|
// 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,8 +71,8 @@ impl LineDemo {
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap = Some(false);
|
||||||
ui.checkbox(animate, "animate");
|
ui.checkbox(animate, "Animate");
|
||||||
ui.checkbox(square, "square view")
|
ui.checkbox(square, "Square view")
|
||||||
.on_hover_text("Always keep the viewport square.");
|
.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.");
|
.on_hover_text("Tick are the same size on both axes.");
|
||||||
|
@ -523,15 +523,40 @@ impl ChartsDemo {
|
||||||
.name("Set 4")
|
.name("Set 4")
|
||||||
.stack_on(&[&chart1, &chart2, &chart3]);
|
.stack_on(&[&chart1, &chart2, &chart3]);
|
||||||
|
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
// Otherwise return empty string (i.e. no label)
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut y_fmt: fn(f64) -> String = |val| {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if !self.vertical {
|
if !self.vertical {
|
||||||
chart1 = chart1.horizontal();
|
chart1 = chart1.horizontal();
|
||||||
chart2 = chart2.horizontal();
|
chart2 = chart2.horizontal();
|
||||||
chart3 = chart3.horizontal();
|
chart3 = chart3.horizontal();
|
||||||
chart4 = chart4.horizontal();
|
chart4 = chart4.horizontal();
|
||||||
|
std::mem::swap(&mut x_fmt, &mut y_fmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
Plot::new("Stacked Bar Chart Demo")
|
Plot::new("Stacked Bar Chart Demo")
|
||||||
.legend(Legend::default())
|
.legend(Legend::default())
|
||||||
|
.x_axis_formatter(x_fmt)
|
||||||
|
.y_axis_formatter(y_fmt)
|
||||||
.data_aspect(1.0)
|
.data_aspect(1.0)
|
||||||
.show(ui, |plot_ui| {
|
.show(ui, |plot_ui| {
|
||||||
plot_ui.bar_chart(chart1);
|
plot_ui.bar_chart(chart1);
|
||||||
|
@ -698,3 +723,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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue