mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 23:00:04 -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.
|
//! Simple plotting library.
|
||||||
|
|
||||||
use ahash::HashMap;
|
use ahash::HashMap;
|
||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use epaint::util::FloatOrd;
|
use epaint::util::FloatOrd;
|
||||||
use epaint::Hsva;
|
use epaint::Hsva;
|
||||||
|
|
||||||
|
use axis::AxisWidget;
|
||||||
use items::PlotItem;
|
use items::PlotItem;
|
||||||
use legend::LegendWidget;
|
use legend::LegendWidget;
|
||||||
|
|
||||||
@@ -17,16 +17,19 @@ pub use items::{
|
|||||||
pub use legend::{Corner, Legend};
|
pub use legend::{Corner, Legend};
|
||||||
pub use transform::{PlotBounds, PlotTransform};
|
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 items;
|
||||||
mod legend;
|
mod legend;
|
||||||
mod transform;
|
mod transform;
|
||||||
|
|
||||||
type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String;
|
type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String;
|
||||||
type LabelFormatter = Option<Box<LabelFormatterFn>>;
|
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 GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>;
|
||||||
type GridSpacer = Box<GridSpacerFn>;
|
type GridSpacer = Box<GridSpacerFn>;
|
||||||
@@ -206,7 +209,7 @@ pub struct Plot {
|
|||||||
show_y: bool,
|
show_y: bool,
|
||||||
label_formatter: LabelFormatter,
|
label_formatter: LabelFormatter,
|
||||||
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
||||||
axis_formatters: [AxisFormatter; 2],
|
axis_config: Vec<AxisConfig>,
|
||||||
legend_config: Option<Legend>,
|
legend_config: Option<Legend>,
|
||||||
show_background: bool,
|
show_background: bool,
|
||||||
show_axes: [bool; 2],
|
show_axes: [bool; 2],
|
||||||
@@ -248,7 +251,7 @@ impl Plot {
|
|||||||
show_y: true,
|
show_y: true,
|
||||||
label_formatter: None,
|
label_formatter: None,
|
||||||
coordinates_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,
|
legend_config: None,
|
||||||
show_background: true,
|
show_background: true,
|
||||||
show_axes: [true; 2],
|
show_axes: [true; 2],
|
||||||
@@ -415,36 +418,6 @@ impl Plot {
|
|||||||
self
|
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.
|
/// 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.
|
/// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units.
|
||||||
@@ -583,6 +556,15 @@ impl Plot {
|
|||||||
self
|
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.
|
/// 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> {
|
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))
|
self.show_dyn(ui, Box::new(build_fn))
|
||||||
@@ -615,7 +597,7 @@ impl Plot {
|
|||||||
mut show_y,
|
mut show_y,
|
||||||
label_formatter,
|
label_formatter,
|
||||||
coordinates_formatter,
|
coordinates_formatter,
|
||||||
axis_formatters,
|
axis_config,
|
||||||
legend_config,
|
legend_config,
|
||||||
reset,
|
reset,
|
||||||
show_background,
|
show_background,
|
||||||
@@ -628,7 +610,9 @@ impl Plot {
|
|||||||
sharp_grid_lines,
|
sharp_grid_lines,
|
||||||
} = self;
|
} = 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 size = {
|
||||||
let width = width
|
let width = width
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
@@ -651,10 +635,143 @@ impl Plot {
|
|||||||
.at_least(min_size.y);
|
.at_least(min_size.y);
|
||||||
vec2(width, height)
|
vec2(width, height)
|
||||||
};
|
};
|
||||||
|
// Determine complete rect of widget.
|
||||||
|
let complete_rect = Rect {
|
||||||
|
min: pos,
|
||||||
|
max: pos + size,
|
||||||
|
};
|
||||||
|
|
||||||
// Allocate the space.
|
// Next we want to create this layout.
|
||||||
let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
|
// 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.
|
// Load or initialize the memory.
|
||||||
let plot_id = ui.make_persistent_id(id_source);
|
let plot_id = ui.make_persistent_id(id_source);
|
||||||
ui.ctx().check_for_id_clash(plot_id, rect, "Plot");
|
ui.ctx().check_for_id_clash(plot_id, rect, "Plot");
|
||||||
@@ -958,7 +1075,7 @@ impl Plot {
|
|||||||
show_y,
|
show_y,
|
||||||
label_formatter,
|
label_formatter,
|
||||||
coordinates_formatter,
|
coordinates_formatter,
|
||||||
axis_formatters,
|
// axis_config,
|
||||||
show_axes,
|
show_axes,
|
||||||
transform,
|
transform,
|
||||||
draw_cursor_x: linked_cursors.as_ref().map_or(false, |(_, group)| group.x),
|
draw_cursor_x: linked_cursors.as_ref().map_or(false, |(_, group)| group.x),
|
||||||
@@ -1319,7 +1436,7 @@ struct PreparedPlot {
|
|||||||
show_y: bool,
|
show_y: bool,
|
||||||
label_formatter: LabelFormatter,
|
label_formatter: LabelFormatter,
|
||||||
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
||||||
axis_formatters: [AxisFormatter; 2],
|
// axis_formatters: [AxisFormatter; 2],
|
||||||
show_axes: [bool; 2],
|
show_axes: [bool; 2],
|
||||||
transform: PlotTransform,
|
transform: PlotTransform,
|
||||||
draw_cursor_x: bool,
|
draw_cursor_x: bool,
|
||||||
@@ -1337,13 +1454,7 @@ impl PreparedPlot {
|
|||||||
|
|
||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
if self.show_axes[d] {
|
if self.show_axes[d] {
|
||||||
self.paint_axis(
|
self.paint_axis(ui, d, &mut axes_shapes, self.sharp_grid_lines);
|
||||||
ui,
|
|
||||||
d,
|
|
||||||
self.show_axes[1 - d],
|
|
||||||
&mut axes_shapes,
|
|
||||||
self.sharp_grid_lines,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1426,27 +1537,26 @@ impl PreparedPlot {
|
|||||||
&self,
|
&self,
|
||||||
ui: &Ui,
|
ui: &Ui,
|
||||||
axis: usize,
|
axis: usize,
|
||||||
other_axis_shown: bool,
|
|
||||||
shapes: &mut Vec<(Shape, f32)>,
|
shapes: &mut Vec<(Shape, f32)>,
|
||||||
sharp_grid_lines: bool,
|
sharp_grid_lines: bool,
|
||||||
) {
|
) {
|
||||||
#![allow(clippy::collapsible_else_if)]
|
#![allow(clippy::collapsible_else_if)]
|
||||||
let Self {
|
let Self {
|
||||||
transform,
|
transform,
|
||||||
axis_formatters,
|
// axis_formatters,
|
||||||
grid_spacers,
|
grid_spacers,
|
||||||
clamp_grid,
|
clamp_grid,
|
||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let bounds = transform.bounds();
|
let bounds = transform.bounds();
|
||||||
let axis_range = match axis {
|
let _axis_range = match axis {
|
||||||
0 => bounds.range_x(),
|
0 => bounds.range_x(),
|
||||||
1 => bounds.range_y(),
|
1 => bounds.range_y(),
|
||||||
_ => panic!("Axis {} does not exist.", axis),
|
_ => 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
|
// 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]);
|
let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]);
|
||||||
@@ -1528,34 +1638,34 @@ impl PreparedPlot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- axis labels
|
// --- axis labels
|
||||||
const MIN_TEXT_SPACING: f32 = 40.0;
|
// const MIN_TEXT_SPACING: f32 = 40.0;
|
||||||
if spacing_in_points > MIN_TEXT_SPACING {
|
// if spacing_in_points > MIN_TEXT_SPACING {
|
||||||
let text_strength =
|
// let text_strength =
|
||||||
remap_clamp(spacing_in_points, MIN_TEXT_SPACING..=150.0, 0.0..=1.0);
|
// remap_clamp(spacing_in_points, MIN_TEXT_SPACING..=150.0, 0.0..=1.0);
|
||||||
let color = color_from_contrast(ui, text_strength);
|
// let color = color_from_contrast(ui, text_strength);
|
||||||
let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
|
// let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
|
||||||
formatter(value_main, &axis_range)
|
// formatter(value_main, &axis_range)
|
||||||
} else {
|
// } else {
|
||||||
emath::round_to_decimals(value_main, 5).to_string() // hack
|
// 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)
|
// // 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;
|
// 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"
|
// // Custom formatters can return empty string to signal "no label at this resolution"
|
||||||
if !text.is_empty() && !skip_origin_y {
|
// if !text.is_empty() && !skip_origin_y {
|
||||||
let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color);
|
// 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:
|
// // Make sure we see the labels, even if the axis is off-screen:
|
||||||
text_pos[1 - axis] = text_pos[1 - axis]
|
// text_pos[1 - axis] = text_pos[1 - axis]
|
||||||
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
|
// .at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
|
||||||
.at_least(transform.frame().min[1 - axis] + 1.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 {
|
fn color_from_contrast(ui: &Ui, contrast: f32) -> Color32 {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::f64::consts::TAU;
|
use std::f64::consts::TAU;
|
||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
use egui::plot::{AxisBools, GridInput, GridMark, PlotResponse};
|
use egui::plot::{AxisBools, GridInput, GridMark, PlotResponse};
|
||||||
use egui::*;
|
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)]
|
#[derive(PartialEq)]
|
||||||
@@ -523,28 +514,6 @@ impl CustomAxisDemo {
|
|||||||
100.0 * y
|
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| {
|
let label_fmt = |_s: &str, val: &PlotPoint| {
|
||||||
format!(
|
format!(
|
||||||
"Day {d}, {h}:{m:02}\n{p:.2}%",
|
"Day {d}, {h}:{m:02}\n{p:.2}%",
|
||||||
@@ -559,8 +528,6 @@ impl CustomAxisDemo {
|
|||||||
|
|
||||||
Plot::new("custom_axes")
|
Plot::new("custom_axes")
|
||||||
.data_aspect(2.0 * MINS_PER_DAY as f32)
|
.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)
|
.x_grid_spacer(CustomAxisDemo::x_grid)
|
||||||
.label_formatter(label_fmt)
|
.label_formatter(label_fmt)
|
||||||
.show(ui, |plot_ui| {
|
.show(ui, |plot_ui| {
|
||||||
|
|||||||
Reference in New Issue
Block a user