mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 05:10:03 -04:00
Add axis labels for plots
This commit is contained in:
committed by
JohannesProgrammiert
parent
8be55f10e9
commit
26494597eb
205
crates/egui/src/widgets/plot/axis.rs
Normal file
205
crates/egui/src/widgets/plot/axis.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use std::{
|
||||
fmt::{Debug, Formatter},
|
||||
ops::RangeInclusive,
|
||||
};
|
||||
|
||||
use epaint::{Pos2, Rect, Stroke, TextShape};
|
||||
|
||||
use crate::{Response, Sense, TextStyle, Ui, Widget, WidgetText};
|
||||
|
||||
pub(super) type AxisFormatterFn = fn(f64, &RangeInclusive<f64>) -> String;
|
||||
|
||||
/// Axis specifier.
|
||||
///
|
||||
/// Used to specify which kind of axis an [`AxisConfig`] refers to.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Axis {
|
||||
X = 0,
|
||||
Y = 1,
|
||||
}
|
||||
|
||||
/// Placement configuration for an axis.
|
||||
///
|
||||
/// `Default` means bottom for x, left for y.
|
||||
/// `Opposite` means top for x, right for y.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Placement {
|
||||
Default,
|
||||
Opposite,
|
||||
}
|
||||
|
||||
/// Axis configuration.
|
||||
///
|
||||
/// Used to configure axis label and ticks.
|
||||
#[derive(Clone)]
|
||||
pub struct AxisConfig {
|
||||
pub(super) placement: Placement,
|
||||
label: String,
|
||||
pub(super) formatter: AxisFormatterFn,
|
||||
pub(super) axis: Axis,
|
||||
}
|
||||
|
||||
impl Debug for AxisConfig {
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(
|
||||
fmt,
|
||||
"AxisConfig ( placement: {:?}, label: {}, formatter: ???, axis: {:?} )",
|
||||
self.placement, self.label, self.axis
|
||||
)
|
||||
}
|
||||
}
|
||||
const LINE_HEIGHT: f32 = 12.0;
|
||||
|
||||
impl AxisConfig {
|
||||
/// Initializes a default axis configuration for the specified [`Axis`].
|
||||
///
|
||||
/// `placement` is bottom for x-axes and left for y-axes
|
||||
/// `label` is empty
|
||||
/// `formatter` is default float to string formatter
|
||||
pub const fn default(axis: Axis) -> Self {
|
||||
Self {
|
||||
placement: Placement::Default,
|
||||
label: String::new(),
|
||||
formatter: Self::default_formatter,
|
||||
axis,
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify axis label
|
||||
pub fn label(mut self, label: String) -> Self {
|
||||
self.label = label;
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify custom formatter for ticks.
|
||||
///
|
||||
/// The first parameter of `formatter` is the raw tick value as `f64`.
|
||||
/// The second paramter of `formatter` is the currently shown range on this axis.
|
||||
pub fn tick_formatter(mut self, formatter: fn(f64, &RangeInclusive<f64>) -> String) -> Self {
|
||||
self.formatter = formatter;
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the placement for this axis.
|
||||
pub fn placement(mut self, placement: Placement) -> Self {
|
||||
self.placement = placement;
|
||||
self
|
||||
}
|
||||
|
||||
fn default_formatter(tick: f64, _range: &RangeInclusive<f64>) -> String {
|
||||
tick.to_string()
|
||||
}
|
||||
|
||||
pub(super) fn thickness(&self) -> f32 {
|
||||
if self.label.is_empty() {
|
||||
LINE_HEIGHT
|
||||
} else {
|
||||
2.0 * LINE_HEIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct AxisWidget {
|
||||
config: AxisConfig,
|
||||
pub(super) rect: Rect,
|
||||
}
|
||||
|
||||
impl AxisWidget {
|
||||
/// if `rect` as width or height == 0, is will be automatically calculated from ticks and text.
|
||||
pub(super) fn new(config: AxisConfig, rect: Rect) -> Self {
|
||||
Self { config, rect }
|
||||
}
|
||||
|
||||
// fn calc_size(&mut self) {
|
||||
// if self.rect.height() == 0.0 {
|
||||
// if self.config.axis == Axis::X {
|
||||
// // calculate height of x-axis label: ticks + label
|
||||
// let y = self.rect.min.y;
|
||||
// if self.config.label.is_empty() {
|
||||
// self.rect.extend_with_y(y + LINE_HEIGHT);
|
||||
// }
|
||||
// else {
|
||||
// self.rect.extend_with_y(y + 2.0*LINE_HEIGHT)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if self.rect.width() == 0.0 {
|
||||
// if self.config.axis == Axis::Y {
|
||||
// // calculate width of y-axis label: ticks + label
|
||||
// if self.config.label.is_empty() {
|
||||
// self.rect.extend_with_x(50.0);
|
||||
// }
|
||||
// else {
|
||||
// self.rect.extend_with_x(100.0)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// pub(super) fn exact_size(mut self, rect: Rect) -> Self {
|
||||
// self.rect = rect;
|
||||
// self
|
||||
// }
|
||||
}
|
||||
|
||||
impl Widget for AxisWidget {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let response = ui.allocate_rect(self.rect, Sense::click_and_drag());
|
||||
if ui.is_rect_visible(response.rect) {
|
||||
let visuals = ui.style().visuals.clone();
|
||||
let text: WidgetText = self.config.label.into();
|
||||
let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Body);
|
||||
let text_color = visuals
|
||||
.override_text_color
|
||||
.unwrap_or(ui.visuals().text_color());
|
||||
let angle: f32 = match self.config.axis {
|
||||
Axis::X => 0.0,
|
||||
Axis::Y => -std::f32::consts::PI * 0.5,
|
||||
};
|
||||
// select text_pos and angle depending on placement and orientation of widget
|
||||
let text_pos = match self.config.placement {
|
||||
Placement::Default => match self.config.axis {
|
||||
Axis::X => {
|
||||
let pos = response.rect.center_bottom();
|
||||
Pos2 {
|
||||
x: pos.x - galley.size().x / 2.0,
|
||||
y: pos.y - galley.size().y,
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
let pos = response.rect.left_center();
|
||||
Pos2 {
|
||||
x: pos.x,
|
||||
y: pos.y + galley.size().x / 2.0,
|
||||
}
|
||||
}
|
||||
},
|
||||
Placement::Opposite => match self.config.axis {
|
||||
Axis::X => {
|
||||
let pos = response.rect.center_top();
|
||||
Pos2 {
|
||||
x: pos.x - galley.size().x / 2.0,
|
||||
y: pos.y + galley.size().y / 2.0,
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
let pos = response.rect.right_center();
|
||||
Pos2 {
|
||||
x: pos.x - galley.size().y,
|
||||
y: pos.y + galley.size().x / 2.0,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
let shape = TextShape {
|
||||
pos: text_pos,
|
||||
galley: galley.galley,
|
||||
underline: Stroke::NONE,
|
||||
override_text_color: Some(text_color),
|
||||
angle,
|
||||
};
|
||||
ui.painter().add(shape);
|
||||
}
|
||||
response
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
//! Simple plotting library.
|
||||
|
||||
use ahash::HashMap;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use crate::*;
|
||||
use epaint::util::FloatOrd;
|
||||
use epaint::Hsva;
|
||||
|
||||
use axis::AxisWidget;
|
||||
use items::PlotItem;
|
||||
use legend::LegendWidget;
|
||||
|
||||
@@ -17,16 +17,19 @@ pub use items::{
|
||||
pub use legend::{Corner, Legend};
|
||||
pub use transform::{PlotBounds, PlotTransform};
|
||||
|
||||
use self::items::{horizontal_line, rulers_color, vertical_line};
|
||||
use self::{
|
||||
axis::Axis,
|
||||
axis::AxisConfig,
|
||||
items::{horizontal_line, rulers_color, vertical_line},
|
||||
};
|
||||
|
||||
pub mod axis;
|
||||
mod items;
|
||||
mod legend;
|
||||
mod transform;
|
||||
|
||||
type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String;
|
||||
type LabelFormatter = Option<Box<LabelFormatterFn>>;
|
||||
type AxisFormatterFn = dyn Fn(f64, &RangeInclusive<f64>) -> String;
|
||||
type AxisFormatter = Option<Box<AxisFormatterFn>>;
|
||||
|
||||
type GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>;
|
||||
type GridSpacer = Box<GridSpacerFn>;
|
||||
@@ -206,7 +209,7 @@ pub struct Plot {
|
||||
show_y: bool,
|
||||
label_formatter: LabelFormatter,
|
||||
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
||||
axis_formatters: [AxisFormatter; 2],
|
||||
axis_config: Vec<AxisConfig>,
|
||||
legend_config: Option<Legend>,
|
||||
show_background: bool,
|
||||
show_axes: [bool; 2],
|
||||
@@ -248,7 +251,7 @@ impl Plot {
|
||||
show_y: true,
|
||||
label_formatter: None,
|
||||
coordinates_formatter: None,
|
||||
axis_formatters: [None, None], // [None; 2] requires Copy
|
||||
axis_config: vec![AxisConfig::default(Axis::X), AxisConfig::default(Axis::Y)],
|
||||
legend_config: None,
|
||||
show_background: true,
|
||||
show_axes: [true; 2],
|
||||
@@ -415,36 +418,6 @@ impl Plot {
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a function to customize the labels for the X axis based on the current visible value range.
|
||||
///
|
||||
/// 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, &RangeInclusive<f64>) -> String + 'static,
|
||||
) -> Self {
|
||||
self.axis_formatters[0] = Some(Box::new(func));
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a function to customize the labels for the Y axis based on the current value range.
|
||||
///
|
||||
/// 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, &RangeInclusive<f64>) -> String + 'static,
|
||||
) -> Self {
|
||||
self.axis_formatters[1] = Some(Box::new(func));
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure how the grid in the background is spaced apart along the X axis.
|
||||
///
|
||||
/// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units.
|
||||
@@ -583,6 +556,15 @@ impl Plot {
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure Axes.
|
||||
///
|
||||
/// Takes a vector of [`AxisConfig`] objects as argument to configure the plot axes.
|
||||
/// See [`AxisConfig`] for available options.
|
||||
pub fn axes(mut self, axis_config: Vec<AxisConfig>) -> Self {
|
||||
self.axis_config = axis_config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Interact with and add items to the plot and finally draw it.
|
||||
pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> PlotResponse<R> {
|
||||
self.show_dyn(ui, Box::new(build_fn))
|
||||
@@ -615,7 +597,7 @@ impl Plot {
|
||||
mut show_y,
|
||||
label_formatter,
|
||||
coordinates_formatter,
|
||||
axis_formatters,
|
||||
axis_config,
|
||||
legend_config,
|
||||
reset,
|
||||
show_background,
|
||||
@@ -628,7 +610,9 @@ impl Plot {
|
||||
sharp_grid_lines,
|
||||
} = self;
|
||||
|
||||
// Determine the size of the plot in the UI
|
||||
// Determine position of widget.
|
||||
let pos = ui.available_rect_before_wrap().min;
|
||||
// Determine size of widget.
|
||||
let size = {
|
||||
let width = width
|
||||
.unwrap_or_else(|| {
|
||||
@@ -651,10 +635,143 @@ impl Plot {
|
||||
.at_least(min_size.y);
|
||||
vec2(width, height)
|
||||
};
|
||||
// Determine complete rect of widget.
|
||||
let complete_rect = Rect {
|
||||
min: pos,
|
||||
max: pos + size,
|
||||
};
|
||||
|
||||
// Allocate the space.
|
||||
let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
|
||||
// Next we want to create this layout.
|
||||
// Incides are only examples.
|
||||
//
|
||||
// +-b-+---------x----------+ +
|
||||
// | | x-Axis 3 |
|
||||
// c +--------------------+
|
||||
// | | x-Axis 2 |
|
||||
// +-+-+--------------------+-+-+
|
||||
// |y|y| |y|y|
|
||||
// |-|-| |-|-|
|
||||
// |A|A| |A|A|
|
||||
// y|x|x| Plot Window |x|x|
|
||||
// |i|i| |i|i|
|
||||
// |s|s| |s|s|
|
||||
// |1|0| |2|3|
|
||||
// +-+-+--------------------+-+-+
|
||||
// | x-Axis 0 | |
|
||||
// +--------------------+ a
|
||||
// | x-Axis 1 | |
|
||||
// + +--------------------+-d-+
|
||||
//
|
||||
|
||||
let mut axis_widgets = Vec::<AxisWidget>::new();
|
||||
let plot_rect: Rect;
|
||||
{
|
||||
// find dimensions of axis labels
|
||||
// for a, b, c, d meanings see picture
|
||||
let mut a = 0.0;
|
||||
let mut b = 0.0;
|
||||
let mut c = 0.0;
|
||||
let mut d = 0.0;
|
||||
for cfg in &axis_config {
|
||||
match cfg.placement {
|
||||
axis::Placement::Default => match cfg.axis {
|
||||
Axis::X => {
|
||||
a += cfg.thickness();
|
||||
}
|
||||
Axis::Y => {
|
||||
b += cfg.thickness();
|
||||
}
|
||||
},
|
||||
axis::Placement::Opposite => match cfg.axis {
|
||||
Axis::X => {
|
||||
c += cfg.thickness();
|
||||
}
|
||||
Axis::Y => {
|
||||
d += cfg.thickness();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
// determine plot rectangle
|
||||
plot_rect = Rect {
|
||||
min: complete_rect.min + Vec2::new(b, c),
|
||||
max: complete_rect.max - Vec2::new(d, a),
|
||||
};
|
||||
|
||||
// determine absolute rectangle for each axis label widget
|
||||
// widget cnt per border of plot in order left, top, right, bottom
|
||||
struct WidgetCnt {
|
||||
left: usize,
|
||||
top: usize,
|
||||
right: usize,
|
||||
bottom: usize,
|
||||
}
|
||||
let mut widget_cnt = WidgetCnt {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
};
|
||||
for cfg in &axis_config {
|
||||
let size_x = Vec2 {
|
||||
x: cfg.thickness(),
|
||||
y: 0.0,
|
||||
};
|
||||
let size_y = Vec2 {
|
||||
x: 0.0,
|
||||
y: cfg.thickness(),
|
||||
};
|
||||
let rect: Rect = match cfg.placement {
|
||||
axis::Placement::Default => match cfg.axis {
|
||||
Axis::X => {
|
||||
let off = widget_cnt.bottom as f32;
|
||||
widget_cnt.bottom += 1;
|
||||
Rect {
|
||||
min: plot_rect.left_bottom() + size_y * off,
|
||||
max: plot_rect.right_bottom() + size_y * (off + 1.0),
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
let off = widget_cnt.left as f32;
|
||||
widget_cnt.left += 1;
|
||||
Rect {
|
||||
min: plot_rect.left_top() - size_x * (off + 1.0),
|
||||
max: plot_rect.left_bottom() - size_x * off,
|
||||
}
|
||||
}
|
||||
},
|
||||
axis::Placement::Opposite => match cfg.axis {
|
||||
Axis::X => {
|
||||
let off = widget_cnt.top as f32;
|
||||
widget_cnt.top += 1;
|
||||
Rect {
|
||||
min: plot_rect.left_top() - size_y * (off + 1.0),
|
||||
max: plot_rect.right_top() - size_y * off,
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
let off = widget_cnt.right as f32;
|
||||
widget_cnt.right += 1;
|
||||
Rect {
|
||||
min: plot_rect.right_top() + size_x * off,
|
||||
max: plot_rect.right_bottom() + size_x * (off + 1.0),
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate the plot window.
|
||||
// let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
|
||||
|
||||
for widget in axis_widgets {
|
||||
ui.add(widget);
|
||||
}
|
||||
let mut response = ui.allocate_rect(complete_rect, Sense::drag());
|
||||
response.rect = plot_rect;
|
||||
let rect = plot_rect;
|
||||
// Load or initialize the memory.
|
||||
let plot_id = ui.make_persistent_id(id_source);
|
||||
ui.ctx().check_for_id_clash(plot_id, rect, "Plot");
|
||||
@@ -958,7 +1075,7 @@ impl Plot {
|
||||
show_y,
|
||||
label_formatter,
|
||||
coordinates_formatter,
|
||||
axis_formatters,
|
||||
// axis_config,
|
||||
show_axes,
|
||||
transform,
|
||||
draw_cursor_x: linked_cursors.as_ref().map_or(false, |(_, group)| group.x),
|
||||
@@ -1319,7 +1436,7 @@ struct PreparedPlot {
|
||||
show_y: bool,
|
||||
label_formatter: LabelFormatter,
|
||||
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
||||
axis_formatters: [AxisFormatter; 2],
|
||||
// axis_formatters: [AxisFormatter; 2],
|
||||
show_axes: [bool; 2],
|
||||
transform: PlotTransform,
|
||||
draw_cursor_x: bool,
|
||||
@@ -1337,13 +1454,7 @@ impl PreparedPlot {
|
||||
|
||||
for d in 0..2 {
|
||||
if self.show_axes[d] {
|
||||
self.paint_axis(
|
||||
ui,
|
||||
d,
|
||||
self.show_axes[1 - d],
|
||||
&mut axes_shapes,
|
||||
self.sharp_grid_lines,
|
||||
);
|
||||
self.paint_axis(ui, d, &mut axes_shapes, self.sharp_grid_lines);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1426,27 +1537,26 @@ impl PreparedPlot {
|
||||
&self,
|
||||
ui: &Ui,
|
||||
axis: usize,
|
||||
other_axis_shown: bool,
|
||||
shapes: &mut Vec<(Shape, f32)>,
|
||||
sharp_grid_lines: bool,
|
||||
) {
|
||||
#![allow(clippy::collapsible_else_if)]
|
||||
let Self {
|
||||
transform,
|
||||
axis_formatters,
|
||||
// axis_formatters,
|
||||
grid_spacers,
|
||||
clamp_grid,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let bounds = transform.bounds();
|
||||
let axis_range = match axis {
|
||||
let _axis_range = match axis {
|
||||
0 => bounds.range_x(),
|
||||
1 => bounds.range_y(),
|
||||
_ => panic!("Axis {} does not exist.", axis),
|
||||
};
|
||||
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let _font_id = TextStyle::Body.resolve(ui.style());
|
||||
|
||||
// Where on the cross-dimension to show the label values
|
||||
let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]);
|
||||
@@ -1528,34 +1638,34 @@ impl PreparedPlot {
|
||||
}
|
||||
|
||||
// --- axis labels
|
||||
const MIN_TEXT_SPACING: f32 = 40.0;
|
||||
if spacing_in_points > MIN_TEXT_SPACING {
|
||||
let text_strength =
|
||||
remap_clamp(spacing_in_points, MIN_TEXT_SPACING..=150.0, 0.0..=1.0);
|
||||
let color = color_from_contrast(ui, text_strength);
|
||||
let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
|
||||
formatter(value_main, &axis_range)
|
||||
} else {
|
||||
emath::round_to_decimals(value_main, 5).to_string() // hack
|
||||
};
|
||||
// const MIN_TEXT_SPACING: f32 = 40.0;
|
||||
// if spacing_in_points > MIN_TEXT_SPACING {
|
||||
// let text_strength =
|
||||
// remap_clamp(spacing_in_points, MIN_TEXT_SPACING..=150.0, 0.0..=1.0);
|
||||
// let color = color_from_contrast(ui, text_strength);
|
||||
// let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
|
||||
// formatter(value_main, &axis_range)
|
||||
// } else {
|
||||
// emath::round_to_decimals(value_main, 5).to_string() // hack
|
||||
// };
|
||||
|
||||
// Skip origin label for y-axis if x-axis is already showing it (otherwise displayed twice)
|
||||
let skip_origin_y = axis == 1 && other_axis_shown && value_main == 0.0;
|
||||
// // Skip origin label for y-axis if x-axis is already showing it (otherwise displayed twice)
|
||||
// let skip_origin_y = axis == 1 && other_axis_shown && value_main == 0.0;
|
||||
|
||||
// Custom formatters can return empty string to signal "no label at this resolution"
|
||||
if !text.is_empty() && !skip_origin_y {
|
||||
let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color);
|
||||
// // Custom formatters can return empty string to signal "no label at this resolution"
|
||||
// if !text.is_empty() && !skip_origin_y {
|
||||
// let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color);
|
||||
|
||||
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
|
||||
// let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
|
||||
|
||||
// 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);
|
||||
// // 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), text_strength));
|
||||
}
|
||||
}
|
||||
// shapes.push((Shape::galley(text_pos, galley), text_strength));
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fn color_from_contrast(ui: &Ui, contrast: f32) -> Color32 {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::f64::consts::TAU;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use egui::plot::{AxisBools, GridInput, GridMark, PlotResponse};
|
||||
use egui::*;
|
||||
@@ -119,14 +118,6 @@ 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
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(PartialEq)]
|
||||
@@ -523,28 +514,6 @@ impl CustomAxisDemo {
|
||||
100.0 * y
|
||||
}
|
||||
|
||||
let x_fmt = |x, _range: &RangeInclusive<f64>| {
|
||||
if x < 0.0 * MINS_PER_DAY || x >= 5.0 * MINS_PER_DAY {
|
||||
// No labels outside value bounds
|
||||
String::new()
|
||||
} else if is_approx_integer(x / MINS_PER_DAY) {
|
||||
// Days
|
||||
format!("Day {}", day(x))
|
||||
} else {
|
||||
// Hours and minutes
|
||||
format!("{h}:{m:02}", h = hour(x), m = minute(x))
|
||||
}
|
||||
};
|
||||
|
||||
let y_fmt = |y, _range: &RangeInclusive<f64>| {
|
||||
// Display only integer percentages
|
||||
if !is_approx_zero(y) && is_approx_integer(100.0 * y) {
|
||||
format!("{:.0}%", percent(y))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
|
||||
let label_fmt = |_s: &str, val: &PlotPoint| {
|
||||
format!(
|
||||
"Day {d}, {h}:{m:02}\n{p:.2}%",
|
||||
@@ -559,8 +528,6 @@ impl CustomAxisDemo {
|
||||
|
||||
Plot::new("custom_axes")
|
||||
.data_aspect(2.0 * MINS_PER_DAY as f32)
|
||||
.x_axis_formatter(x_fmt)
|
||||
.y_axis_formatter(y_fmt)
|
||||
.x_grid_spacer(CustomAxisDemo::x_grid)
|
||||
.label_formatter(label_fmt)
|
||||
.show(ui, |plot_ui| {
|
||||
|
||||
Reference in New Issue
Block a user