Customize grid spacing in plots (#1180)
This commit is contained in:
parent
676ff047e9
commit
e22f6d9a7e
3 changed files with 305 additions and 50 deletions
|
@ -16,6 +16,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
|
||||||
* Added `Frame::outer_margin`.
|
* Added `Frame::outer_margin`.
|
||||||
* Added `Painter::hline` and `Painter::vline`.
|
* Added `Painter::hline` and `Painter::vline`.
|
||||||
* Added `Link` and `ui.link` ([#1506](https://github.com/emilk/egui/pull/1506)).
|
* Added `Link` and `ui.link` ([#1506](https://github.com/emilk/egui/pull/1506)).
|
||||||
|
* Added `Plot::x_grid_spacer` and `Plot::y_grid_spacer` for custom grid spacing ([#1180](https://github.com/emilk/egui/pull/1180)).
|
||||||
|
|
||||||
### Changed 🔧
|
### Changed 🔧
|
||||||
* `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)).
|
* `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)).
|
||||||
|
@ -30,7 +31,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
|
||||||
* Renamed `Painter::sub_region` to `Painter::with_clip_rect`.
|
* Renamed `Painter::sub_region` to `Painter::with_clip_rect`.
|
||||||
|
|
||||||
### Fixed 🐛
|
### Fixed 🐛
|
||||||
* Fixed `ComboBox`:es always being rendered left-aligned ([#1304](https://github.com/emilk/egui/pull/1304)).
|
* Fixed `ComboBox`es always being rendered left-aligned ([#1304](https://github.com/emilk/egui/pull/1304)).
|
||||||
* Fixed ui code that could lead to a deadlock ([#1380](https://github.com/emilk/egui/pull/1380)).
|
* Fixed ui code that could lead to a deadlock ([#1380](https://github.com/emilk/egui/pull/1380)).
|
||||||
* Text is darker and more readable in bright mode ([#1412](https://github.com/emilk/egui/pull/1412)).
|
* Text is darker and more readable in bright mode ([#1412](https://github.com/emilk/egui/pull/1412)).
|
||||||
* Fixed `Ui::add_visible` sometimes leaving the `Ui` in a disabled state. ([#1436](https://github.com/emilk/egui/issues/1436)).
|
* Fixed `Ui::add_visible` sometimes leaving the `Ui` in a disabled state. ([#1436](https://github.com/emilk/egui/issues/1436)).
|
||||||
|
|
|
@ -6,6 +6,7 @@ use crate::*;
|
||||||
use epaint::ahash::AHashSet;
|
use epaint::ahash::AHashSet;
|
||||||
use epaint::color::Hsva;
|
use epaint::color::Hsva;
|
||||||
use epaint::util::FloatOrd;
|
use epaint::util::FloatOrd;
|
||||||
|
|
||||||
use items::PlotItem;
|
use items::PlotItem;
|
||||||
use legend::LegendWidget;
|
use legend::LegendWidget;
|
||||||
use transform::ScreenTransform;
|
use transform::ScreenTransform;
|
||||||
|
@ -26,6 +27,9 @@ type LabelFormatter = Option<Box<LabelFormatterFn>>;
|
||||||
type AxisFormatterFn = dyn Fn(f64, &RangeInclusive<f64>) -> String;
|
type AxisFormatterFn = dyn Fn(f64, &RangeInclusive<f64>) -> String;
|
||||||
type AxisFormatter = Option<Box<AxisFormatterFn>>;
|
type AxisFormatter = Option<Box<AxisFormatterFn>>;
|
||||||
|
|
||||||
|
type GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>;
|
||||||
|
type GridSpacer = Box<GridSpacerFn>;
|
||||||
|
|
||||||
/// Specifies the coordinates formatting when passed to [`Plot::coordinates_formatter`].
|
/// Specifies the coordinates formatting when passed to [`Plot::coordinates_formatter`].
|
||||||
pub struct CoordinatesFormatter {
|
pub struct CoordinatesFormatter {
|
||||||
function: Box<dyn Fn(&Value, &PlotBounds) -> String>,
|
function: Box<dyn Fn(&Value, &PlotBounds) -> String>,
|
||||||
|
@ -61,6 +65,8 @@ impl Default for CoordinatesFormatter {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO: large enough for a wide label
|
||||||
|
|
||||||
/// Information about the plot that has to persist between frames.
|
/// Information about the plot that has to persist between frames.
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -186,6 +192,7 @@ pub struct Plot {
|
||||||
legend_config: Option<Legend>,
|
legend_config: Option<Legend>,
|
||||||
show_background: bool,
|
show_background: bool,
|
||||||
show_axes: [bool; 2],
|
show_axes: [bool; 2],
|
||||||
|
grid_spacers: [GridSpacer; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Plot {
|
impl Plot {
|
||||||
|
@ -219,6 +226,7 @@ impl Plot {
|
||||||
legend_config: None,
|
legend_config: None,
|
||||||
show_background: true,
|
show_background: true,
|
||||||
show_axes: [true; 2],
|
show_axes: [true; 2],
|
||||||
|
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -393,6 +401,49 @@ impl Plot {
|
||||||
self
|
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.
|
||||||
|
///
|
||||||
|
/// The function has this signature:
|
||||||
|
/// ```ignore
|
||||||
|
/// fn get_step_sizes(input: GridInput) -> Vec<GridMark>;
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This function should return all marks along the visible range of the X axis.
|
||||||
|
/// `step_size` also determines how thick/faint each line is drawn.
|
||||||
|
/// For example, if x = 80..=230 is visible and you want big marks at steps of
|
||||||
|
/// 100 and small ones at 25, you can return:
|
||||||
|
/// ```no_run
|
||||||
|
/// # use egui::plot::GridMark;
|
||||||
|
/// vec![
|
||||||
|
/// // 100s
|
||||||
|
/// GridMark { value: 100.0, step_size: 100.0 },
|
||||||
|
/// GridMark { value: 200.0, step_size: 100.0 },
|
||||||
|
///
|
||||||
|
/// // 25s
|
||||||
|
/// GridMark { value: 125.0, step_size: 25.0 },
|
||||||
|
/// GridMark { value: 150.0, step_size: 25.0 },
|
||||||
|
/// GridMark { value: 175.0, step_size: 25.0 },
|
||||||
|
/// GridMark { value: 225.0, step_size: 25.0 },
|
||||||
|
/// ];
|
||||||
|
/// # ()
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// There are helpers for common cases, see [`log_grid_spacer`] and [`uniform_grid_spacer`].
|
||||||
|
pub fn x_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec<GridMark> + 'static) -> Self {
|
||||||
|
self.grid_spacers[0] = Box::new(spacer);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units.
|
||||||
|
///
|
||||||
|
/// See [`Self::x_grid_spacer`] for explanation.
|
||||||
|
pub fn y_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec<GridMark> + 'static) -> Self {
|
||||||
|
self.grid_spacers[1] = Box::new(spacer);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Expand bounds to include the given x value.
|
/// Expand bounds to include the given x value.
|
||||||
/// For instance, to always show the y axis, call `plot.include_x(0.0)`.
|
/// 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 {
|
pub fn include_x(mut self, x: impl Into<f64>) -> Self {
|
||||||
|
@ -463,6 +514,7 @@ impl Plot {
|
||||||
show_background,
|
show_background,
|
||||||
show_axes,
|
show_axes,
|
||||||
linked_axes,
|
linked_axes,
|
||||||
|
grid_spacers,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
// Determine the size of the plot in the UI
|
// Determine the size of the plot in the UI
|
||||||
|
@ -706,6 +758,7 @@ impl Plot {
|
||||||
axis_formatters,
|
axis_formatters,
|
||||||
show_axes,
|
show_axes,
|
||||||
transform: transform.clone(),
|
transform: transform.clone(),
|
||||||
|
grid_spacers,
|
||||||
};
|
};
|
||||||
prepared.ui(ui, &response);
|
prepared.ui(ui, &response);
|
||||||
|
|
||||||
|
@ -922,6 +975,80 @@ impl PlotUi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Grid
|
||||||
|
|
||||||
|
/// Input for "grid spacer" functions.
|
||||||
|
///
|
||||||
|
/// See [`Plot::x_grid_spacer()`] and [`Plot::y_grid_spacer()`].
|
||||||
|
pub struct GridInput {
|
||||||
|
/// Min/max of the visible data range (the values at the two edges of the plot,
|
||||||
|
/// for the current axis).
|
||||||
|
pub bounds: (f64, f64),
|
||||||
|
|
||||||
|
/// Recommended (but not required) lower-bound on the step size returned by custom grid spacers.
|
||||||
|
///
|
||||||
|
/// Computed as the ratio between the diagram's bounds (in plot coordinates) and the viewport
|
||||||
|
/// (in frame/window coordinates), scaled up to represent the minimal possible step.
|
||||||
|
pub base_step_size: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One mark (horizontal or vertical line) in the background grid of a plot.
|
||||||
|
pub struct GridMark {
|
||||||
|
/// X or Y value in the plot.
|
||||||
|
pub value: f64,
|
||||||
|
|
||||||
|
/// The (approximate) distance to the next value of same thickness.
|
||||||
|
///
|
||||||
|
/// Determines how thick the grid line is painted. It's not important that `step_size`
|
||||||
|
/// matches the difference between two `value`s precisely, but rather that grid marks of
|
||||||
|
/// same thickness have same `step_size`. For example, months can have a different number
|
||||||
|
/// of days, but consistently using a `step_size` of 30 days is a valid approximation.
|
||||||
|
pub step_size: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively splits the grid into `base` subdivisions (e.g. 100, 10, 1).
|
||||||
|
///
|
||||||
|
/// The logarithmic base, expressing how many times each grid unit is subdivided.
|
||||||
|
/// 10 is a typical value, others are possible though.
|
||||||
|
pub fn log_grid_spacer(log_base: i64) -> GridSpacer {
|
||||||
|
let log_base = log_base as f64;
|
||||||
|
let get_step_sizes = move |input: GridInput| -> Vec<GridMark> {
|
||||||
|
// The distance between two of the thinnest grid lines is "rounded" up
|
||||||
|
// to the next-bigger power of base
|
||||||
|
let smallest_visible_unit = next_power(input.base_step_size, log_base);
|
||||||
|
|
||||||
|
let step_sizes = [
|
||||||
|
smallest_visible_unit,
|
||||||
|
smallest_visible_unit * log_base,
|
||||||
|
smallest_visible_unit * log_base * log_base,
|
||||||
|
];
|
||||||
|
|
||||||
|
generate_marks(step_sizes, input.bounds)
|
||||||
|
};
|
||||||
|
|
||||||
|
Box::new(get_step_sizes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits the grid into uniform-sized spacings (e.g. 100, 25, 1).
|
||||||
|
///
|
||||||
|
/// This function should return 3 positive step sizes, designating where the lines in the grid are drawn.
|
||||||
|
/// Lines are thicker for larger step sizes. Ordering of returned value is irrelevant.
|
||||||
|
///
|
||||||
|
/// Why only 3 step sizes? Three is the number of different line thicknesses that egui typically uses in the grid.
|
||||||
|
/// Ideally, those 3 are not hardcoded values, but depend on the visible range (accessible through `GridInput`).
|
||||||
|
pub fn uniform_grid_spacer(spacer: impl Fn(GridInput) -> [f64; 3] + 'static) -> GridSpacer {
|
||||||
|
let get_marks = move |input: GridInput| -> Vec<GridMark> {
|
||||||
|
let bounds = input.bounds;
|
||||||
|
let step_sizes = spacer(input);
|
||||||
|
generate_marks(step_sizes, bounds)
|
||||||
|
};
|
||||||
|
|
||||||
|
Box::new(get_marks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
struct PreparedPlot {
|
struct PreparedPlot {
|
||||||
items: Vec<Box<dyn PlotItem>>,
|
items: Vec<Box<dyn PlotItem>>,
|
||||||
show_x: bool,
|
show_x: bool,
|
||||||
|
@ -931,6 +1058,7 @@ struct PreparedPlot {
|
||||||
axis_formatters: [AxisFormatter; 2],
|
axis_formatters: [AxisFormatter; 2],
|
||||||
show_axes: [bool; 2],
|
show_axes: [bool; 2],
|
||||||
transform: ScreenTransform,
|
transform: ScreenTransform,
|
||||||
|
grid_spacers: [GridSpacer; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreparedPlot {
|
impl PreparedPlot {
|
||||||
|
@ -979,6 +1107,7 @@ impl PreparedPlot {
|
||||||
let Self {
|
let Self {
|
||||||
transform,
|
transform,
|
||||||
axis_formatters,
|
axis_formatters,
|
||||||
|
grid_spacers,
|
||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
@ -991,43 +1120,31 @@ impl PreparedPlot {
|
||||||
|
|
||||||
let font_id = TextStyle::Body.resolve(ui.style());
|
let font_id = TextStyle::Body.resolve(ui.style());
|
||||||
|
|
||||||
let base: i64 = 10;
|
|
||||||
let basef = base as f64;
|
|
||||||
|
|
||||||
let min_line_spacing_in_points = 6.0; // TODO: large enough for a wide label
|
|
||||||
let step_size = transform.dvalue_dpos()[axis] * min_line_spacing_in_points;
|
|
||||||
let step_size = basef.powi(step_size.abs().log(basef).ceil() as i32);
|
|
||||||
|
|
||||||
let step_size_in_points = (transform.dpos_dvalue()[axis] * step_size).abs() as f32;
|
|
||||||
|
|
||||||
// Where on the cross-dimension to show the label values
|
// Where on the cross-dimension to show the label values
|
||||||
|
let bounds = transform.bounds();
|
||||||
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]);
|
||||||
|
|
||||||
for i in 0.. {
|
let input = GridInput {
|
||||||
let value_main = step_size * (bounds.min[axis] / step_size + i as f64).floor();
|
bounds: (bounds.min[axis], bounds.max[axis]),
|
||||||
if value_main > bounds.max[axis] {
|
base_step_size: transform.dvalue_dpos()[axis] * MIN_LINE_SPACING_IN_POINTS,
|
||||||
break;
|
};
|
||||||
}
|
let steps = (grid_spacers[axis])(input);
|
||||||
|
|
||||||
|
for step in steps {
|
||||||
|
let value_main = step.value;
|
||||||
|
|
||||||
let value = if axis == 0 {
|
let value = if axis == 0 {
|
||||||
Value::new(value_main, value_cross)
|
Value::new(value_main, value_cross)
|
||||||
} else {
|
} else {
|
||||||
Value::new(value_cross, value_main)
|
Value::new(value_cross, value_main)
|
||||||
};
|
};
|
||||||
let pos_in_gui = transform.position_from_value(&value);
|
|
||||||
|
|
||||||
let n = (value_main / step_size).round() as i64;
|
let pos_in_gui = transform.position_from_value(&value);
|
||||||
let spacing_in_points = if n % (base * base) == 0 {
|
let spacing_in_points = (transform.dpos_dvalue()[axis] * step.step_size).abs() as f32;
|
||||||
step_size_in_points * (basef * basef) as f32 // think line (multiple of 100)
|
|
||||||
} else if n % base == 0 {
|
|
||||||
step_size_in_points * basef as f32 // medium line (multiple of 10)
|
|
||||||
} else {
|
|
||||||
step_size_in_points // thin line
|
|
||||||
};
|
|
||||||
|
|
||||||
let line_alpha = remap_clamp(
|
let line_alpha = remap_clamp(
|
||||||
spacing_in_points,
|
spacing_in_points,
|
||||||
(min_line_spacing_in_points as f32)..=300.0,
|
(MIN_LINE_SPACING_IN_POINTS as f32)..=300.0,
|
||||||
0.0..=0.15,
|
0.0..=0.15,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1119,3 +1236,38 @@ impl PreparedPlot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns next bigger power in given base
|
||||||
|
/// e.g.
|
||||||
|
/// ```ignore
|
||||||
|
/// use egui::plot::next_power;
|
||||||
|
/// assert_eq!(next_power(0.01, 10.0), 0.01);
|
||||||
|
/// assert_eq!(next_power(0.02, 10.0), 0.1);
|
||||||
|
/// assert_eq!(next_power(0.2, 10.0), 1);
|
||||||
|
/// ```
|
||||||
|
fn next_power(value: f64, base: f64) -> f64 {
|
||||||
|
assert_ne!(value, 0.0); // can be negative (typical for Y axis)
|
||||||
|
base.powi(value.abs().log(base).ceil() as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill in all values between [min, max] which are a multiple of `step_size`
|
||||||
|
fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec<GridMark> {
|
||||||
|
let mut steps = vec![];
|
||||||
|
fill_marks_between(&mut steps, step_sizes[0], bounds);
|
||||||
|
fill_marks_between(&mut steps, step_sizes[1], bounds);
|
||||||
|
fill_marks_between(&mut steps, step_sizes[2], bounds);
|
||||||
|
steps
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill in all values between [min, max] which are a multiple of `step_size`
|
||||||
|
fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64, f64)) {
|
||||||
|
assert!(max > min);
|
||||||
|
let first = (min / step_size).ceil() as i64;
|
||||||
|
let last = (max / step_size).ceil() as i64;
|
||||||
|
|
||||||
|
let marks_iter = (first..last).map(|i| {
|
||||||
|
let value = (i as f64) * step_size;
|
||||||
|
GridMark { value, step_size }
|
||||||
|
});
|
||||||
|
out.extend(marks_iter);
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use std::f64::consts::TAU;
|
use std::f64::consts::TAU;
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
use egui::plot::{GridInput, GridMark};
|
||||||
use egui::*;
|
use egui::*;
|
||||||
use plot::{
|
use plot::{
|
||||||
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner, HLine,
|
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner, HLine,
|
||||||
|
@ -309,6 +311,125 @@ impl Widget for &mut LegendDemo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Default)]
|
||||||
|
struct CustomAxisDemo {}
|
||||||
|
|
||||||
|
impl CustomAxisDemo {
|
||||||
|
const MINS_PER_DAY: f64 = 24.0 * 60.0;
|
||||||
|
const MINS_PER_H: f64 = 60.0;
|
||||||
|
|
||||||
|
fn logistic_fn() -> Line {
|
||||||
|
fn days(min: f64) -> f64 {
|
||||||
|
CustomAxisDemo::MINS_PER_DAY * min
|
||||||
|
}
|
||||||
|
|
||||||
|
let values = Values::from_explicit_callback(
|
||||||
|
move |x| 1.0 / (1.0 + (-2.5 * (x / CustomAxisDemo::MINS_PER_DAY - 2.0)).exp()),
|
||||||
|
days(0.0)..days(5.0),
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
Line::new(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
fn x_grid(input: GridInput) -> Vec<GridMark> {
|
||||||
|
// Note: this always fills all possible marks. For optimization, `input.bounds`
|
||||||
|
// could be used to decide when the low-interval grids (minutes) should be added.
|
||||||
|
|
||||||
|
let mut marks = vec![];
|
||||||
|
|
||||||
|
let (min, max) = input.bounds;
|
||||||
|
let min = min.floor() as i32;
|
||||||
|
let max = max.ceil() as i32;
|
||||||
|
|
||||||
|
for i in min..=max {
|
||||||
|
let step_size = if i % Self::MINS_PER_DAY as i32 == 0 {
|
||||||
|
// 1 day
|
||||||
|
Self::MINS_PER_DAY
|
||||||
|
} else if i % Self::MINS_PER_H as i32 == 0 {
|
||||||
|
// 1 hour
|
||||||
|
Self::MINS_PER_H
|
||||||
|
} else if i % 5 == 0 {
|
||||||
|
// 5min
|
||||||
|
5.0
|
||||||
|
} else {
|
||||||
|
// skip grids below 5min
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
marks.push(GridMark {
|
||||||
|
value: i as f64,
|
||||||
|
step_size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
marks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for &mut CustomAxisDemo {
|
||||||
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
|
const MINS_PER_DAY: f64 = CustomAxisDemo::MINS_PER_DAY;
|
||||||
|
const MINS_PER_H: f64 = CustomAxisDemo::MINS_PER_H;
|
||||||
|
|
||||||
|
fn get_day(x: f64) -> f64 {
|
||||||
|
(x / MINS_PER_DAY).floor()
|
||||||
|
}
|
||||||
|
fn get_hour(x: f64) -> f64 {
|
||||||
|
(x.rem_euclid(MINS_PER_DAY) / MINS_PER_H).floor()
|
||||||
|
}
|
||||||
|
fn get_minute(x: f64) -> f64 {
|
||||||
|
x.rem_euclid(MINS_PER_H).floor()
|
||||||
|
}
|
||||||
|
fn get_percent(y: f64) -> f64 {
|
||||||
|
(100.0 * y).round()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {}", get_day(x))
|
||||||
|
} else {
|
||||||
|
// Hours and minutes
|
||||||
|
format!("{h}:{m:02}", h = get_hour(x), m = get_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!("{}%", get_percent(y))
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let label_fmt = |_s: &str, val: &Value| {
|
||||||
|
format!(
|
||||||
|
"Day {d}, {h}:{m:02}\n{p}%",
|
||||||
|
d = get_day(val.x),
|
||||||
|
h = get_hour(val.x),
|
||||||
|
m = get_minute(val.x),
|
||||||
|
p = get_percent(val.y)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
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| {
|
||||||
|
plot_ui.line(CustomAxisDemo::logistic_fn());
|
||||||
|
})
|
||||||
|
.response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
struct LinkedAxisDemo {
|
struct LinkedAxisDemo {
|
||||||
link_x: bool,
|
link_x: bool,
|
||||||
|
@ -604,40 +725,15 @@ impl ChartsDemo {
|
||||||
.name("Set 4")
|
.name("Set 4")
|
||||||
.stack_on(&[&chart1, &chart2, &chart3]);
|
.stack_on(&[&chart1, &chart2, &chart3]);
|
||||||
|
|
||||||
let mut x_fmt: fn(f64, &std::ops::RangeInclusive<f64>) -> String = |val, _range| {
|
|
||||||
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, &std::ops::RangeInclusive<f64>) -> String = |val, _range| {
|
|
||||||
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);
|
||||||
|
@ -720,6 +816,7 @@ enum Panel {
|
||||||
Charts,
|
Charts,
|
||||||
Items,
|
Items,
|
||||||
Interaction,
|
Interaction,
|
||||||
|
CustomAxes,
|
||||||
LinkedAxes,
|
LinkedAxes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -737,6 +834,7 @@ pub struct PlotDemo {
|
||||||
charts_demo: ChartsDemo,
|
charts_demo: ChartsDemo,
|
||||||
items_demo: ItemsDemo,
|
items_demo: ItemsDemo,
|
||||||
interaction_demo: InteractionDemo,
|
interaction_demo: InteractionDemo,
|
||||||
|
custom_axes_demo: CustomAxisDemo,
|
||||||
linked_axes_demo: LinkedAxisDemo,
|
linked_axes_demo: LinkedAxisDemo,
|
||||||
open_panel: Panel,
|
open_panel: Panel,
|
||||||
}
|
}
|
||||||
|
@ -782,6 +880,7 @@ impl super::View for PlotDemo {
|
||||||
ui.selectable_value(&mut self.open_panel, Panel::Charts, "Charts");
|
ui.selectable_value(&mut self.open_panel, Panel::Charts, "Charts");
|
||||||
ui.selectable_value(&mut self.open_panel, Panel::Items, "Items");
|
ui.selectable_value(&mut self.open_panel, Panel::Items, "Items");
|
||||||
ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction");
|
ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction");
|
||||||
|
ui.selectable_value(&mut self.open_panel, Panel::CustomAxes, "Custom Axes");
|
||||||
ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes");
|
ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes");
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
@ -805,6 +904,9 @@ impl super::View for PlotDemo {
|
||||||
Panel::Interaction => {
|
Panel::Interaction => {
|
||||||
ui.add(&mut self.interaction_demo);
|
ui.add(&mut self.interaction_demo);
|
||||||
}
|
}
|
||||||
|
Panel::CustomAxes => {
|
||||||
|
ui.add(&mut self.custom_axes_demo);
|
||||||
|
}
|
||||||
Panel::LinkedAxes => {
|
Panel::LinkedAxes => {
|
||||||
ui.add(&mut self.linked_axes_demo);
|
ui.add(&mut self.linked_axes_demo);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue