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

Formatter for plot axis labels #1130

Merged
merged 7 commits into from
Jan 24, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
* 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 `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!
Expand All @@ -35,6 +36,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))
Bromeon marked this conversation as resolved.
Show resolved Hide resolved

### Contributors 🙏
* [danielkeller](https://github.com/danielkeller): [#1050](https://github.com/emilk/egui/pull/1050).
Expand Down
4 changes: 2 additions & 2 deletions egui/src/widgets/plot/items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions egui/src/widgets/plot/items/values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
64 changes: 54 additions & 10 deletions egui/src/widgets/plot/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -21,6 +21,9 @@ mod transform;
type CustomLabelFunc = dyn Fn(&str, &Value) -> String;
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.
Expand Down Expand Up @@ -80,6 +83,7 @@ pub struct Plot {
show_x: bool,
show_y: bool,
custom_label_func: CustomLabelFuncRef,
axis_formatters: [AxisFormatter; 2],
legend_config: Option<Legend>,
show_background: bool,
show_axes: [bool; 2],
Expand Down Expand Up @@ -107,6 +111,7 @@ impl Plot {
show_x: true,
show_y: true,
custom_label_func: None,
axis_formatters: [None, None], // [None; 2] requires Copy
legend_config: None,
show_background: true,
show_axes: [true; 2],
Expand Down Expand Up @@ -216,6 +221,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<F: 'static + Fn(f64) -> String>(mut self, func: F) -> Self {
Bromeon marked this conversation as resolved.
Show resolved Hide resolved
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<F: 'static + Fn(f64) -> String>(mut self, func: F) -> Self {
self.axis_formatters[1] = 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<f64>) -> Self {
Expand Down Expand Up @@ -270,6 +299,7 @@ impl Plot {
mut show_x,
mut show_y,
custom_label_func,
axis_formatters,
legend_config,
show_background,
show_axes,
Expand Down Expand Up @@ -442,6 +472,7 @@ impl Plot {
show_x,
show_y,
custom_label_func,
axis_formatters,
show_axes,
transform: transform.clone(),
};
Expand Down Expand Up @@ -650,6 +681,7 @@ struct PreparedPlot {
show_x: bool,
show_y: bool,
custom_label_func: CustomLabelFuncRef,
axis_formatters: [AxisFormatter; 2],
show_axes: [bool; 2],
transform: ScreenTransform,
}
Expand Down Expand Up @@ -680,7 +712,11 @@ impl PreparedPlot {
}

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();

Expand Down Expand Up @@ -740,18 +776,26 @@ 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, 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:
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));
}
}
}

Expand Down
35 changes: 34 additions & 1 deletion egui_demo_lib/src/apps/demo/plot_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ impl LineDemo {
ui.checkbox(animate, "animate");
ui.checkbox(square, "square view")
Bromeon marked this conversation as resolved.
Show resolved Hide resolved
.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")
Expand Down Expand Up @@ -523,15 +523,40 @@ impl ChartsDemo {
.name("Set 4")
.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 {
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)
.y_axis_formatter(y_fmt)
.data_aspect(1.0)
.show(ui, |plot_ui| {
plot_ui.bar_chart(chart1);
Expand Down Expand Up @@ -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
}