mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
Move egui_plot to its own repo (#4828)
* Part of https://github.com/emilk/egui/issues/4705 `egui_plot` can now be found at https://github.com/emilk/egui_plot
This commit is contained in:
@@ -1,92 +0,0 @@
|
||||
# Changelog for egui_plot
|
||||
All notable changes to the `egui_plot` integration will be noted in this file.
|
||||
|
||||
This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.28.1 - 2024-07-05
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.28.0 - 2024-07-03
|
||||
### ⭐ Added
|
||||
* Hide all other series when alt-clicking in the legend [#4549](https://github.com/emilk/egui/pull/4549) by [@abey79](https://github.com/abey79)
|
||||
|
||||
### 🔧 Changed
|
||||
* `Plot::Items:allow_hover` give possibility to masked the interaction on hovered item [#2558](https://github.com/emilk/egui/pull/2558) by [@haricot](https://github.com/haricot)
|
||||
* Expose `ClosestElem` and `PlotConfig` [#4380](https://github.com/emilk/egui/pull/4380) by [@Narcha](https://github.com/Narcha)
|
||||
* Introduce lifetime to `egui_plot::Plot` to replace `'static` fields [#4435](https://github.com/emilk/egui/pull/4435) by [@Fabus1184](https://github.com/Fabus1184)
|
||||
* Plot now respects the `interact_radius` set in the UI's style [#4520](https://github.com/emilk/egui/pull/4520) by [@YgorSouza](https://github.com/YgorSouza)
|
||||
* Improve behavior of plot auto-bounds with reduced data [#4632](https://github.com/emilk/egui/pull/4632) by [@abey79](https://github.com/abey79)
|
||||
* Improve default formatter of tick-marks [#4738](https://github.com/emilk/egui/pull/4738) by [@emilk](https://github.com/emilk)
|
||||
|
||||
### 🐛 Fixed
|
||||
* Disable interaction for `ScrollArea` and `Plot` when UI is disabled [#4457](https://github.com/emilk/egui/pull/4457) by [@varphone](https://github.com/varphone)
|
||||
* Make sure plot size is positive [#4429](https://github.com/emilk/egui/pull/4429) by [@rustbasic](https://github.com/rustbasic)
|
||||
* Use `f64` for translate [#4637](https://github.com/emilk/egui/pull/4637) by [@Its-Just-Nans](https://github.com/Its-Just-Nans)
|
||||
* Clamp plot zoom values to valid range [#4695](https://github.com/emilk/egui/pull/4695) by [@Its-Just-Nans](https://github.com/Its-Just-Nans)
|
||||
* Fix plot bounds of empty plots [#4741](https://github.com/emilk/egui/pull/4741) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
## 0.27.2 - 2024-04-02
|
||||
* Allow zoom/pan a plot as long as it contains the mouse cursor [#4292](https://github.com/emilk/egui/pull/4292)
|
||||
* Prevent plot from resetting one axis while zooming/dragging the other [#4252](https://github.com/emilk/egui/pull/4252) (thanks [@YgorSouza](https://github.com/YgorSouza)!)
|
||||
* egui_plot: Fix the same plot tick label being painted multiple times [#4307](https://github.com/emilk/egui/pull/4307)
|
||||
|
||||
|
||||
## 0.27.1 - 2024-03-29
|
||||
* Nothing new
|
||||
|
||||
|
||||
## 0.27.0 - 2024-03-26
|
||||
* Add `sense` option to `Plot` [#4052](https://github.com/emilk/egui/pull/4052) (thanks [@AmesingFlank](https://github.com/AmesingFlank)!)
|
||||
* Plot widget - allow disabling scroll for x and y separately [#4051](https://github.com/emilk/egui/pull/4051) (thanks [@YgorSouza](https://github.com/YgorSouza)!)
|
||||
* Fix panic when the base step size is set to 0 [#4078](https://github.com/emilk/egui/pull/4078) (thanks [@abey79](https://github.com/abey79)!)
|
||||
* Expose `PlotGeometry` in public API [#4193](https://github.com/emilk/egui/pull/4193) (thanks [@dwuertz](https://github.com/dwuertz)!)
|
||||
|
||||
|
||||
## 0.26.2 - 2024-02-14
|
||||
* Nothing new
|
||||
|
||||
|
||||
## 0.26.1 - 2024-02-11
|
||||
* Nothing new
|
||||
|
||||
|
||||
## 0.26.0 - 2024-02-05
|
||||
* Make `egui_plot::PlotMemory` public [#3871](https://github.com/emilk/egui/pull/3871)
|
||||
* Customizable spacing of grid and axis label spacing [#3896](https://github.com/emilk/egui/pull/3896)
|
||||
* Change default plot line thickness from 1.0 to 1.5 [#3918](https://github.com/emilk/egui/pull/3918)
|
||||
* Automatically expand plot axes thickness to fit their labels [#3921](https://github.com/emilk/egui/pull/3921)
|
||||
* Plot items now have optional id which is returned in the plot's response when hovered [#3920](https://github.com/emilk/egui/pull/3920) (thanks [@Wumpf](https://github.com/Wumpf)!)
|
||||
* Parallel tessellation with opt-in `rayon` feature [#3934](https://github.com/emilk/egui/pull/3934)
|
||||
* Make `egui_plot::PlotItem` a public trait [#3943](https://github.com/emilk/egui/pull/3943)
|
||||
* Fix clip rect for plot items [#3955](https://github.com/emilk/egui/pull/3955) (thanks [@YgorSouza](https://github.com/YgorSouza)!)
|
||||
|
||||
|
||||
## 0.25.0 - 2024-01-08
|
||||
* Fix plot auto-bounds unset by default [#3722](https://github.com/emilk/egui/pull/3722) (thanks [@abey79](https://github.com/abey79)!)
|
||||
* Add methods to zoom a `Plot` programmatically [#2714](https://github.com/emilk/egui/pull/2714) (thanks [@YgorSouza](https://github.com/YgorSouza)!)
|
||||
* Add a public API for overriding plot legend traces' visibilities [#3534](https://github.com/emilk/egui/pull/3534) (thanks [@jayzhudev](https://github.com/jayzhudev)!)
|
||||
|
||||
|
||||
## 0.24.1 - 2024-12-03
|
||||
* Fix plot auto-bounds default [#3722](https://github.com/emilk/egui/pull/3722) (thanks [@abey79](https://github.com/abey79)!)
|
||||
|
||||
|
||||
## 0.24.0 - 2023-11-23
|
||||
* Add `emath::Vec2b`, replacing `egui_plot::AxisBools` [#3543](https://github.com/emilk/egui/pull/3543)
|
||||
* Add `auto_bounds/set_auto_bounds` to `PlotUi` [#3587](https://github.com/emilk/egui/pull/3587) [#3586](https://github.com/emilk/egui/pull/3586) (thanks [@abey79](https://github.com/abey79)!)
|
||||
* Update MSRV to Rust 1.72 [#3595](https://github.com/emilk/egui/pull/3595)
|
||||
|
||||
|
||||
## 0.23.0 - 2023-09-27 - Initial release, after being forked out from `egui`
|
||||
* Draw axis labels and ticks outside of plotting window [#2284](https://github.com/emilk/egui/pull/2284) (thanks [@JohannesProgrammiert](https://github.com/JohannesProgrammiert)!)
|
||||
* Add `PlotUi::response()` to replace `plot_clicked()` etc [#3223](https://github.com/emilk/egui/pull/3223)
|
||||
* Add rotation feature to plot images [#3121](https://github.com/emilk/egui/pull/3121) (thanks [@ThundR67](https://github.com/ThundR67)!)
|
||||
* Plot items: Image rotation and size in plot coordinates, polygon fill color [#3182](https://github.com/emilk/egui/pull/3182) (thanks [@s-nie](https://github.com/s-nie)!)
|
||||
* Add method to specify `tip_size` of plot arrows [#3138](https://github.com/emilk/egui/pull/3138) (thanks [@nagua](https://github.com/nagua)!)
|
||||
* Better handle additive colors in plots [#3387](https://github.com/emilk/egui/pull/3387)
|
||||
* Fix auto_bounds when only one axis has restricted navigation [#3171](https://github.com/emilk/egui/pull/3171) (thanks [@KoffeinFlummi](https://github.com/KoffeinFlummi)!)
|
||||
* Fix plot formatter not taking closures [#3260](https://github.com/emilk/egui/pull/3260) (thanks [@Wumpf](https://github.com/Wumpf)!)
|
||||
@@ -1,47 +0,0 @@
|
||||
[package]
|
||||
name = "egui_plot"
|
||||
version.workspace = true
|
||||
authors = [
|
||||
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>", # https://github.com/emilk
|
||||
"Jan Haller <bromeon@gmail.com>", # https://github.com/Bromeon
|
||||
"Sven Niederberger <s-niederberger@outlook.com>", # https://github.com/EmbersArc
|
||||
]
|
||||
description = "Immediate mode plotting for the egui GUI library"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage = "https://github.com/emilk/egui"
|
||||
license.workspace = true
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui"
|
||||
categories = ["visualization", "gui"]
|
||||
keywords = ["egui", "plot", "plotting"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[lib]
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
|
||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||
serde = ["dep:serde", "egui/serde"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
egui = { workspace = true, default-features = false }
|
||||
emath = { workspace = true, default-features = false }
|
||||
|
||||
ahash.workspace = true
|
||||
|
||||
#! ### Optional dependencies
|
||||
## Enable this when generating docs.
|
||||
document-features = { workspace = true, optional = true }
|
||||
|
||||
serde = { workspace = true, optional = true }
|
||||
@@ -1,11 +1 @@
|
||||
# egui_plot
|
||||
|
||||
[](https://crates.io/crates/egui_plot)
|
||||
[](https://docs.rs/egui_plot)
|
||||
[](https://github.com/rust-secure-code/safety-dance/)
|
||||

|
||||

|
||||
|
||||
Immediate mode plotting for [`egui`](https://github.com/emilk/egui).
|
||||
|
||||
[**Looking for a maintainer!**](https://github.com/emilk/egui/issues/4705)
|
||||
`egui_plot` has been moved to <https://github.com/emilk/egui_plot>
|
||||
|
||||
@@ -1,398 +0,0 @@
|
||||
use std::{fmt::Debug, ops::RangeInclusive, sync::Arc};
|
||||
|
||||
use egui::{
|
||||
emath::{remap_clamp, Rot2},
|
||||
epaint::TextShape,
|
||||
Pos2, Rangef, Rect, Response, Sense, TextStyle, TextWrapMode, Ui, Vec2, WidgetText,
|
||||
};
|
||||
|
||||
use super::{transform::PlotTransform, GridMark};
|
||||
|
||||
pub(super) type AxisFormatterFn<'a> = dyn Fn(GridMark, &RangeInclusive<f64>) -> String + 'a;
|
||||
|
||||
/// X or Y axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Axis {
|
||||
/// Horizontal X-Axis
|
||||
X = 0,
|
||||
|
||||
/// Vertical Y-axis
|
||||
Y = 1,
|
||||
}
|
||||
|
||||
impl From<Axis> for usize {
|
||||
#[inline]
|
||||
fn from(value: Axis) -> Self {
|
||||
match value {
|
||||
Axis::X => 0,
|
||||
Axis::Y => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Placement of the horizontal X-Axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VPlacement {
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
/// Placement of the vertical Y-Axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HPlacement {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Placement of an axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Placement {
|
||||
/// Bottom for X-axis, or left for Y-axis.
|
||||
LeftBottom,
|
||||
|
||||
/// Top for x-axis and right for y-axis.
|
||||
RightTop,
|
||||
}
|
||||
|
||||
impl From<HPlacement> for Placement {
|
||||
#[inline]
|
||||
fn from(placement: HPlacement) -> Self {
|
||||
match placement {
|
||||
HPlacement::Left => Self::LeftBottom,
|
||||
HPlacement::Right => Self::RightTop,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Placement> for HPlacement {
|
||||
#[inline]
|
||||
fn from(placement: Placement) -> Self {
|
||||
match placement {
|
||||
Placement::LeftBottom => Self::Left,
|
||||
Placement::RightTop => Self::Right,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VPlacement> for Placement {
|
||||
#[inline]
|
||||
fn from(placement: VPlacement) -> Self {
|
||||
match placement {
|
||||
VPlacement::Top => Self::RightTop,
|
||||
VPlacement::Bottom => Self::LeftBottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Placement> for VPlacement {
|
||||
#[inline]
|
||||
fn from(placement: Placement) -> Self {
|
||||
match placement {
|
||||
Placement::LeftBottom => Self::Bottom,
|
||||
Placement::RightTop => Self::Top,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Axis configuration.
|
||||
///
|
||||
/// Used to configure axis label and ticks.
|
||||
#[derive(Clone)]
|
||||
pub struct AxisHints<'a> {
|
||||
pub(super) label: WidgetText,
|
||||
pub(super) formatter: Arc<AxisFormatterFn<'a>>,
|
||||
pub(super) min_thickness: f32,
|
||||
pub(super) placement: Placement,
|
||||
pub(super) label_spacing: Rangef,
|
||||
}
|
||||
|
||||
// TODO(JohannesProgrammiert): this just a guess. It might cease to work if a user changes font size.
|
||||
const LINE_HEIGHT: f32 = 12.0;
|
||||
|
||||
impl<'a> AxisHints<'a> {
|
||||
/// Initializes a default axis configuration for the X axis.
|
||||
pub fn new_x() -> Self {
|
||||
Self::new(Axis::X)
|
||||
}
|
||||
|
||||
/// Initializes a default axis configuration for the Y axis.
|
||||
pub fn new_y() -> Self {
|
||||
Self::new(Axis::Y)
|
||||
}
|
||||
|
||||
/// Initializes a default axis configuration for the specified axis.
|
||||
///
|
||||
/// `label` is empty.
|
||||
/// `formatter` is default float to string formatter.
|
||||
pub fn new(axis: Axis) -> Self {
|
||||
Self {
|
||||
label: Default::default(),
|
||||
formatter: Arc::new(Self::default_formatter),
|
||||
min_thickness: 14.0,
|
||||
placement: Placement::LeftBottom,
|
||||
label_spacing: match axis {
|
||||
Axis::X => Rangef::new(60.0, 80.0), // labels can get pretty wide
|
||||
Axis::Y => Rangef::new(20.0, 30.0), // text isn't very high
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify custom formatter for ticks.
|
||||
///
|
||||
/// The first parameter of `formatter` is the raw tick value as `f64`.
|
||||
/// The second parameter of `formatter` is the currently shown range on this axis.
|
||||
pub fn formatter(
|
||||
mut self,
|
||||
fmt: impl Fn(GridMark, &RangeInclusive<f64>) -> String + 'a,
|
||||
) -> Self {
|
||||
self.formatter = Arc::new(fmt);
|
||||
self
|
||||
}
|
||||
|
||||
fn default_formatter(mark: GridMark, _range: &RangeInclusive<f64>) -> String {
|
||||
// Example: If the step to the next tick is `0.01`, we should use 2 decimals of precision:
|
||||
let num_decimals = -mark.step_size.log10().round() as usize;
|
||||
|
||||
emath::format_with_decimals_in_range(mark.value, num_decimals..=num_decimals)
|
||||
}
|
||||
|
||||
/// Specify axis label.
|
||||
///
|
||||
/// The default is 'x' for x-axes and 'y' for y-axes.
|
||||
#[inline]
|
||||
pub fn label(mut self, label: impl Into<WidgetText>) -> Self {
|
||||
self.label = label.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify minimum thickness of the axis
|
||||
#[inline]
|
||||
pub fn min_thickness(mut self, min_thickness: f32) -> Self {
|
||||
self.min_thickness = min_thickness;
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify maximum number of digits for ticks.
|
||||
#[inline]
|
||||
#[deprecated = "Use `min_thickness` instead"]
|
||||
pub fn max_digits(self, digits: usize) -> Self {
|
||||
self.min_thickness(12.0 * digits as f32)
|
||||
}
|
||||
|
||||
/// Specify the placement of the axis.
|
||||
///
|
||||
/// For X-axis, use [`VPlacement`].
|
||||
/// For Y-axis, use [`HPlacement`].
|
||||
#[inline]
|
||||
pub fn placement(mut self, placement: impl Into<Placement>) -> Self {
|
||||
self.placement = placement.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the minimum spacing between labels
|
||||
///
|
||||
/// When labels get closer together than the given minimum, then they become invisible.
|
||||
/// When they get further apart than the max, they are at full opacity.
|
||||
///
|
||||
/// Labels can never be closer together than the [`crate::Plot::grid_spacing`] setting.
|
||||
#[inline]
|
||||
pub fn label_spacing(mut self, range: impl Into<Rangef>) -> Self {
|
||||
self.label_spacing = range.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn thickness(&self, axis: Axis) -> f32 {
|
||||
match axis {
|
||||
Axis::X => self.min_thickness.max(if self.label.is_empty() {
|
||||
1.0 * LINE_HEIGHT
|
||||
} else {
|
||||
3.0 * LINE_HEIGHT
|
||||
}),
|
||||
Axis::Y => {
|
||||
self.min_thickness
|
||||
+ if self.label.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
LINE_HEIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct AxisWidget<'a> {
|
||||
pub range: RangeInclusive<f64>,
|
||||
pub hints: AxisHints<'a>,
|
||||
|
||||
/// The region where we draw the axis labels.
|
||||
pub rect: Rect,
|
||||
pub transform: Option<PlotTransform>,
|
||||
pub steps: Arc<Vec<GridMark>>,
|
||||
}
|
||||
|
||||
impl<'a> AxisWidget<'a> {
|
||||
/// if `rect` has width or height == 0, it will be automatically calculated from ticks and text.
|
||||
pub fn new(hints: AxisHints<'a>, rect: Rect) -> Self {
|
||||
Self {
|
||||
range: (0.0..=0.0),
|
||||
hints,
|
||||
rect,
|
||||
transform: None,
|
||||
steps: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the actual thickness of the axis.
|
||||
pub fn ui(self, ui: &mut Ui, axis: Axis) -> (Response, f32) {
|
||||
let response = ui.allocate_rect(self.rect, Sense::hover());
|
||||
|
||||
if !ui.is_rect_visible(response.rect) {
|
||||
return (response, 0.0);
|
||||
}
|
||||
|
||||
let visuals = ui.style().visuals.clone();
|
||||
|
||||
{
|
||||
let text = self.hints.label;
|
||||
let galley = text.into_galley(
|
||||
ui,
|
||||
Some(TextWrapMode::Extend),
|
||||
f32::INFINITY,
|
||||
TextStyle::Body,
|
||||
);
|
||||
let text_color = visuals
|
||||
.override_text_color
|
||||
.unwrap_or_else(|| ui.visuals().text_color());
|
||||
let angle: f32 = match axis {
|
||||
Axis::X => 0.0,
|
||||
Axis::Y => -std::f32::consts::TAU * 0.25,
|
||||
};
|
||||
// select text_pos and angle depending on placement and orientation of widget
|
||||
let text_pos = match self.hints.placement {
|
||||
Placement::LeftBottom => match axis {
|
||||
Axis::X => {
|
||||
let pos = response.rect.center_bottom();
|
||||
Pos2 {
|
||||
x: pos.x - galley.size().x / 2.0,
|
||||
y: pos.y - galley.size().y * 1.25,
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
let pos = response.rect.left_center();
|
||||
Pos2 {
|
||||
x: pos.x,
|
||||
y: pos.y + galley.size().x / 2.0,
|
||||
}
|
||||
}
|
||||
},
|
||||
Placement::RightTop => match axis {
|
||||
Axis::X => {
|
||||
let pos = response.rect.center_top();
|
||||
Pos2 {
|
||||
x: pos.x - galley.size().x / 2.0,
|
||||
y: pos.y + galley.size().y * 0.25,
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
let pos = response.rect.right_center();
|
||||
Pos2 {
|
||||
x: pos.x - galley.size().y * 1.5,
|
||||
y: pos.y + galley.size().x / 2.0,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
ui.painter()
|
||||
.add(TextShape::new(text_pos, galley, text_color).with_angle(angle));
|
||||
}
|
||||
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let Some(transform) = self.transform else {
|
||||
return (response, 0.0);
|
||||
};
|
||||
|
||||
let label_spacing = self.hints.label_spacing;
|
||||
|
||||
let mut thickness: f32 = 0.0;
|
||||
|
||||
// Add tick labels:
|
||||
for step in self.steps.iter() {
|
||||
let text = (self.hints.formatter)(*step, &self.range);
|
||||
if !text.is_empty() {
|
||||
let spacing_in_points =
|
||||
(transform.dpos_dvalue()[usize::from(axis)] * step.step_size).abs() as f32;
|
||||
|
||||
if spacing_in_points <= label_spacing.min {
|
||||
// Labels are too close together - don't paint them.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fade in labels as they get further apart:
|
||||
let strength = remap_clamp(spacing_in_points, label_spacing, 0.0..=1.0);
|
||||
|
||||
let text_color = super::color_from_strength(ui, strength);
|
||||
let galley = ui
|
||||
.painter()
|
||||
.layout_no_wrap(text, font_id.clone(), text_color);
|
||||
|
||||
if spacing_in_points < galley.size()[axis as usize] {
|
||||
continue; // the galley won't fit (likely too wide on the X axis).
|
||||
}
|
||||
|
||||
match axis {
|
||||
Axis::X => {
|
||||
thickness = thickness.max(galley.size().y);
|
||||
|
||||
let projected_point = super::PlotPoint::new(step.value, 0.0);
|
||||
let center_x = transform.position_from_point(&projected_point).x;
|
||||
let y = match VPlacement::from(self.hints.placement) {
|
||||
VPlacement::Bottom => self.rect.min.y,
|
||||
VPlacement::Top => self.rect.max.y - galley.size().y,
|
||||
};
|
||||
let pos = Pos2::new(center_x - galley.size().x / 2.0, y);
|
||||
ui.painter().add(TextShape::new(pos, galley, text_color));
|
||||
}
|
||||
Axis::Y => {
|
||||
thickness = thickness.max(galley.size().x);
|
||||
|
||||
let projected_point = super::PlotPoint::new(0.0, step.value);
|
||||
let center_y = transform.position_from_point(&projected_point).y;
|
||||
|
||||
match HPlacement::from(self.hints.placement) {
|
||||
HPlacement::Left => {
|
||||
let angle = 0.0; // TODO(emilk): allow users to rotate text
|
||||
|
||||
if angle == 0.0 {
|
||||
let x = self.rect.max.x - galley.size().x;
|
||||
let pos = Pos2::new(x, center_y - galley.size().y / 2.0);
|
||||
ui.painter().add(TextShape::new(pos, galley, text_color));
|
||||
} else {
|
||||
let right = Pos2::new(
|
||||
self.rect.max.x,
|
||||
center_y - galley.size().y / 2.0,
|
||||
);
|
||||
let width = galley.size().x;
|
||||
let left =
|
||||
right - Rot2::from_angle(angle) * Vec2::new(width, 0.0);
|
||||
|
||||
ui.painter().add(
|
||||
TextShape::new(left, galley, text_color).with_angle(angle),
|
||||
);
|
||||
}
|
||||
}
|
||||
HPlacement::Right => {
|
||||
let x = self.rect.min.x;
|
||||
let pos = Pos2::new(x, center_y - galley.size().y / 2.0);
|
||||
ui.painter().add(TextShape::new(pos, galley, text_color));
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
(response, thickness)
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
use egui::emath::NumExt;
|
||||
use egui::epaint::{Color32, RectShape, Rounding, Shape, Stroke};
|
||||
|
||||
use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement};
|
||||
use crate::{BarChart, Cursor, PlotPoint, PlotTransform};
|
||||
|
||||
/// One bar in a [`BarChart`]. Potentially floating, allowing stacked bar charts.
|
||||
/// Width can be changed to allow variable-width histograms.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Bar {
|
||||
/// Name of plot element in the diagram (annotated by default formatter)
|
||||
pub name: String,
|
||||
|
||||
/// Which direction the bar faces in the diagram
|
||||
pub orientation: Orientation,
|
||||
|
||||
/// Position on the argument (input) axis -- X if vertical, Y if horizontal
|
||||
pub argument: f64,
|
||||
|
||||
/// Position on the value (output) axis -- Y if vertical, X if horizontal
|
||||
pub value: f64,
|
||||
|
||||
/// For stacked bars, this denotes where the bar starts. None if base axis
|
||||
pub base_offset: Option<f64>,
|
||||
|
||||
/// Thickness of the bar
|
||||
pub bar_width: f64,
|
||||
|
||||
/// Line width and color
|
||||
pub stroke: Stroke,
|
||||
|
||||
/// Fill color
|
||||
pub fill: Color32,
|
||||
}
|
||||
|
||||
impl Bar {
|
||||
/// Create a bar. Its `orientation` is set by its [`BarChart`] parent.
|
||||
///
|
||||
/// - `argument`: Position on the argument axis (X if vertical, Y if horizontal).
|
||||
/// - `value`: Height of the bar (if vertical).
|
||||
///
|
||||
/// By default the bar is vertical and its base is at zero.
|
||||
pub fn new(argument: f64, height: f64) -> Self {
|
||||
Self {
|
||||
argument,
|
||||
value: height,
|
||||
orientation: Orientation::default(),
|
||||
name: Default::default(),
|
||||
base_offset: None,
|
||||
bar_width: 0.5,
|
||||
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
|
||||
fill: Color32::TRANSPARENT,
|
||||
}
|
||||
}
|
||||
|
||||
/// Name of this bar chart element.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[inline]
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a custom stroke.
|
||||
#[inline]
|
||||
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||
self.stroke = stroke.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a custom fill color.
|
||||
#[inline]
|
||||
pub fn fill(mut self, color: impl Into<Color32>) -> Self {
|
||||
self.fill = color.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Offset the base of the bar.
|
||||
/// This offset is on the Y axis for a vertical bar
|
||||
/// and on the X axis for a horizontal bar.
|
||||
#[inline]
|
||||
pub fn base_offset(mut self, offset: f64) -> Self {
|
||||
self.base_offset = Some(offset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bar width.
|
||||
#[inline]
|
||||
pub fn width(mut self, width: f64) -> Self {
|
||||
self.bar_width = width;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set orientation of the element as vertical. Argument axis is X.
|
||||
#[inline]
|
||||
pub fn vertical(mut self) -> Self {
|
||||
self.orientation = Orientation::Vertical;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set orientation of the element as horizontal. Argument axis is Y.
|
||||
#[inline]
|
||||
pub fn horizontal(mut self) -> Self {
|
||||
self.orientation = Orientation::Horizontal;
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn lower(&self) -> f64 {
|
||||
if self.value.is_sign_positive() {
|
||||
self.base_offset.unwrap_or(0.0)
|
||||
} else {
|
||||
self.base_offset.map_or(self.value, |o| o + self.value)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn upper(&self) -> f64 {
|
||||
if self.value.is_sign_positive() {
|
||||
self.base_offset.map_or(self.value, |o| o + self.value)
|
||||
} else {
|
||||
self.base_offset.unwrap_or(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn add_shapes(
|
||||
&self,
|
||||
transform: &PlotTransform,
|
||||
highlighted: bool,
|
||||
shapes: &mut Vec<Shape>,
|
||||
) {
|
||||
let (stroke, fill) = if highlighted {
|
||||
highlighted_color(self.stroke, self.fill)
|
||||
} else {
|
||||
(self.stroke, self.fill)
|
||||
};
|
||||
|
||||
let rect = transform.rect_from_values(&self.bounds_min(), &self.bounds_max());
|
||||
let rect = Shape::Rect(RectShape::new(rect, Rounding::ZERO, fill, stroke));
|
||||
|
||||
shapes.push(rect);
|
||||
}
|
||||
|
||||
pub(super) fn add_rulers_and_text(
|
||||
&self,
|
||||
parent: &BarChart,
|
||||
plot: &PlotConfig<'_>,
|
||||
shapes: &mut Vec<Shape>,
|
||||
cursors: &mut Vec<Cursor>,
|
||||
) {
|
||||
let text: Option<String> = parent
|
||||
.element_formatter
|
||||
.as_ref()
|
||||
.map(|fmt| fmt(self, parent));
|
||||
|
||||
add_rulers_and_text(self, plot, text, shapes, cursors);
|
||||
}
|
||||
}
|
||||
|
||||
impl RectElement for Bar {
|
||||
fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
fn bounds_min(&self) -> PlotPoint {
|
||||
self.point_at(self.argument - self.bar_width / 2.0, self.lower())
|
||||
}
|
||||
|
||||
fn bounds_max(&self) -> PlotPoint {
|
||||
self.point_at(self.argument + self.bar_width / 2.0, self.upper())
|
||||
}
|
||||
|
||||
fn values_with_ruler(&self) -> Vec<PlotPoint> {
|
||||
let base = self.base_offset.unwrap_or(0.0);
|
||||
let value_center = self.point_at(self.argument, base + self.value);
|
||||
|
||||
let mut ruler_positions = vec![value_center];
|
||||
|
||||
if let Some(offset) = self.base_offset {
|
||||
ruler_positions.push(self.point_at(self.argument, offset));
|
||||
}
|
||||
|
||||
ruler_positions
|
||||
}
|
||||
|
||||
fn orientation(&self) -> Orientation {
|
||||
self.orientation
|
||||
}
|
||||
|
||||
fn default_values_format(&self, transform: &PlotTransform) -> String {
|
||||
let scale = transform.dvalue_dpos();
|
||||
let scale = match self.orientation {
|
||||
Orientation::Horizontal => scale[0],
|
||||
Orientation::Vertical => scale[1],
|
||||
};
|
||||
let decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||
crate::format_number(self.value, decimals)
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
use egui::emath::NumExt as _;
|
||||
use egui::epaint::{Color32, RectShape, Rounding, Shape, Stroke};
|
||||
|
||||
use crate::{BoxPlot, Cursor, PlotPoint, PlotTransform};
|
||||
|
||||
use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement};
|
||||
|
||||
/// Contains the values of a single box in a box plot.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BoxSpread {
|
||||
/// Value of lower whisker (typically minimum).
|
||||
///
|
||||
/// The whisker is not drawn if `lower_whisker >= quartile1`.
|
||||
pub lower_whisker: f64,
|
||||
|
||||
/// Value of lower box threshold (typically 25% quartile)
|
||||
pub quartile1: f64,
|
||||
|
||||
/// Value of middle line in box (typically median)
|
||||
pub median: f64,
|
||||
|
||||
/// Value of upper box threshold (typically 75% quartile)
|
||||
pub quartile3: f64,
|
||||
|
||||
/// Value of upper whisker (typically maximum)
|
||||
///
|
||||
/// The whisker is not drawn if `upper_whisker <= quartile3`.
|
||||
pub upper_whisker: f64,
|
||||
}
|
||||
|
||||
impl BoxSpread {
|
||||
pub fn new(
|
||||
lower_whisker: f64,
|
||||
quartile1: f64,
|
||||
median: f64,
|
||||
quartile3: f64,
|
||||
upper_whisker: f64,
|
||||
) -> Self {
|
||||
Self {
|
||||
lower_whisker,
|
||||
quartile1,
|
||||
median,
|
||||
quartile3,
|
||||
upper_whisker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A box in a [`BoxPlot`] diagram. This is a low level graphical element; it will not compute quartiles and whiskers,
|
||||
/// letting one use their preferred formula. Use [`Points`][`super::Points`] to draw the outliers.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BoxElem {
|
||||
/// Name of plot element in the diagram (annotated by default formatter).
|
||||
pub name: String,
|
||||
|
||||
/// Which direction the box faces in the diagram.
|
||||
pub orientation: Orientation,
|
||||
|
||||
/// Position on the argument (input) axis -- X if vertical, Y if horizontal.
|
||||
pub argument: f64,
|
||||
|
||||
/// Values of the box
|
||||
pub spread: BoxSpread,
|
||||
|
||||
/// Thickness of the box
|
||||
pub box_width: f64,
|
||||
|
||||
/// Width of the whisker at minimum/maximum
|
||||
pub whisker_width: f64,
|
||||
|
||||
/// Line width and color
|
||||
pub stroke: Stroke,
|
||||
|
||||
/// Fill color
|
||||
pub fill: Color32,
|
||||
}
|
||||
|
||||
impl BoxElem {
|
||||
/// Create a box element. Its `orientation` is set by its [`BoxPlot`] parent.
|
||||
///
|
||||
/// Check [`BoxElem`] fields for detailed description.
|
||||
pub fn new(argument: f64, spread: BoxSpread) -> Self {
|
||||
Self {
|
||||
argument,
|
||||
orientation: Orientation::default(),
|
||||
name: String::default(),
|
||||
spread,
|
||||
box_width: 0.25,
|
||||
whisker_width: 0.15,
|
||||
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
|
||||
fill: Color32::TRANSPARENT,
|
||||
}
|
||||
}
|
||||
|
||||
/// Name of this box element.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[inline]
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a custom stroke.
|
||||
#[inline]
|
||||
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||
self.stroke = stroke.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a custom fill color.
|
||||
#[inline]
|
||||
pub fn fill(mut self, color: impl Into<Color32>) -> Self {
|
||||
self.fill = color.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the box width.
|
||||
#[inline]
|
||||
pub fn box_width(mut self, width: f64) -> Self {
|
||||
self.box_width = width;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the whisker width.
|
||||
#[inline]
|
||||
pub fn whisker_width(mut self, width: f64) -> Self {
|
||||
self.whisker_width = width;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set orientation of the element as vertical. Argument axis is X.
|
||||
#[inline]
|
||||
pub fn vertical(mut self) -> Self {
|
||||
self.orientation = Orientation::Vertical;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set orientation of the element as horizontal. Argument axis is Y.
|
||||
#[inline]
|
||||
pub fn horizontal(mut self) -> Self {
|
||||
self.orientation = Orientation::Horizontal;
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn add_shapes(
|
||||
&self,
|
||||
transform: &PlotTransform,
|
||||
highlighted: bool,
|
||||
shapes: &mut Vec<Shape>,
|
||||
) {
|
||||
let (stroke, fill) = if highlighted {
|
||||
highlighted_color(self.stroke, self.fill)
|
||||
} else {
|
||||
(self.stroke, self.fill)
|
||||
};
|
||||
|
||||
let rect = transform.rect_from_values(
|
||||
&self.point_at(self.argument - self.box_width / 2.0, self.spread.quartile1),
|
||||
&self.point_at(self.argument + self.box_width / 2.0, self.spread.quartile3),
|
||||
);
|
||||
let rect = Shape::Rect(RectShape::new(rect, Rounding::ZERO, fill, stroke));
|
||||
shapes.push(rect);
|
||||
|
||||
let line_between = |v1, v2| {
|
||||
Shape::line_segment(
|
||||
[
|
||||
transform.position_from_point(&v1),
|
||||
transform.position_from_point(&v2),
|
||||
],
|
||||
stroke,
|
||||
)
|
||||
};
|
||||
let median = line_between(
|
||||
self.point_at(self.argument - self.box_width / 2.0, self.spread.median),
|
||||
self.point_at(self.argument + self.box_width / 2.0, self.spread.median),
|
||||
);
|
||||
shapes.push(median);
|
||||
|
||||
if self.spread.upper_whisker > self.spread.quartile3 {
|
||||
let high_whisker = line_between(
|
||||
self.point_at(self.argument, self.spread.quartile3),
|
||||
self.point_at(self.argument, self.spread.upper_whisker),
|
||||
);
|
||||
shapes.push(high_whisker);
|
||||
if self.box_width > 0.0 {
|
||||
let high_whisker_end = line_between(
|
||||
self.point_at(
|
||||
self.argument - self.whisker_width / 2.0,
|
||||
self.spread.upper_whisker,
|
||||
),
|
||||
self.point_at(
|
||||
self.argument + self.whisker_width / 2.0,
|
||||
self.spread.upper_whisker,
|
||||
),
|
||||
);
|
||||
shapes.push(high_whisker_end);
|
||||
}
|
||||
}
|
||||
|
||||
if self.spread.lower_whisker < self.spread.quartile1 {
|
||||
let low_whisker = line_between(
|
||||
self.point_at(self.argument, self.spread.quartile1),
|
||||
self.point_at(self.argument, self.spread.lower_whisker),
|
||||
);
|
||||
shapes.push(low_whisker);
|
||||
if self.box_width > 0.0 {
|
||||
let low_whisker_end = line_between(
|
||||
self.point_at(
|
||||
self.argument - self.whisker_width / 2.0,
|
||||
self.spread.lower_whisker,
|
||||
),
|
||||
self.point_at(
|
||||
self.argument + self.whisker_width / 2.0,
|
||||
self.spread.lower_whisker,
|
||||
),
|
||||
);
|
||||
shapes.push(low_whisker_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn add_rulers_and_text(
|
||||
&self,
|
||||
parent: &BoxPlot,
|
||||
plot: &PlotConfig<'_>,
|
||||
shapes: &mut Vec<Shape>,
|
||||
cursors: &mut Vec<Cursor>,
|
||||
) {
|
||||
let text: Option<String> = parent
|
||||
.element_formatter
|
||||
.as_ref()
|
||||
.map(|fmt| fmt(self, parent));
|
||||
|
||||
add_rulers_and_text(self, plot, text, shapes, cursors);
|
||||
}
|
||||
}
|
||||
|
||||
impl RectElement for BoxElem {
|
||||
fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
fn bounds_min(&self) -> PlotPoint {
|
||||
let argument = self.argument - self.box_width.max(self.whisker_width) / 2.0;
|
||||
let value = self.spread.lower_whisker;
|
||||
self.point_at(argument, value)
|
||||
}
|
||||
|
||||
fn bounds_max(&self) -> PlotPoint {
|
||||
let argument = self.argument + self.box_width.max(self.whisker_width) / 2.0;
|
||||
let value = self.spread.upper_whisker;
|
||||
self.point_at(argument, value)
|
||||
}
|
||||
|
||||
fn values_with_ruler(&self) -> Vec<PlotPoint> {
|
||||
let median = self.point_at(self.argument, self.spread.median);
|
||||
let q1 = self.point_at(self.argument, self.spread.quartile1);
|
||||
let q3 = self.point_at(self.argument, self.spread.quartile3);
|
||||
let upper = self.point_at(self.argument, self.spread.upper_whisker);
|
||||
let lower = self.point_at(self.argument, self.spread.lower_whisker);
|
||||
|
||||
vec![median, q1, q3, upper, lower]
|
||||
}
|
||||
|
||||
fn orientation(&self) -> Orientation {
|
||||
self.orientation
|
||||
}
|
||||
|
||||
fn corner_value(&self) -> PlotPoint {
|
||||
self.point_at(self.argument, self.spread.upper_whisker)
|
||||
}
|
||||
|
||||
fn default_values_format(&self, transform: &PlotTransform) -> String {
|
||||
let scale = transform.dvalue_dpos();
|
||||
let scale = match self.orientation {
|
||||
Orientation::Horizontal => scale[0],
|
||||
Orientation::Vertical => scale[1],
|
||||
};
|
||||
let y_decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize)
|
||||
.at_most(6)
|
||||
.at_least(1);
|
||||
format!(
|
||||
"Max = {max:.decimals$}\
|
||||
\nQuartile 3 = {q3:.decimals$}\
|
||||
\nMedian = {med:.decimals$}\
|
||||
\nQuartile 1 = {q1:.decimals$}\
|
||||
\nMin = {min:.decimals$}",
|
||||
max = self.spread.upper_whisker,
|
||||
q3 = self.spread.quartile3,
|
||||
med = self.spread.median,
|
||||
q1 = self.spread.quartile1,
|
||||
min = self.spread.lower_whisker,
|
||||
decimals = y_decimals
|
||||
)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,73 +0,0 @@
|
||||
use egui::emath::NumExt as _;
|
||||
use egui::epaint::{Color32, Rgba, Stroke};
|
||||
|
||||
use crate::transform::{PlotBounds, PlotTransform};
|
||||
|
||||
use super::{Orientation, PlotPoint};
|
||||
|
||||
/// Trait that abstracts from rectangular 'Value'-like elements, such as bars or boxes
|
||||
pub(super) trait RectElement {
|
||||
fn name(&self) -> &str;
|
||||
|
||||
fn bounds_min(&self) -> PlotPoint;
|
||||
|
||||
fn bounds_max(&self) -> PlotPoint;
|
||||
|
||||
fn bounds(&self) -> PlotBounds {
|
||||
let mut bounds = PlotBounds::NOTHING;
|
||||
bounds.extend_with(&self.bounds_min());
|
||||
bounds.extend_with(&self.bounds_max());
|
||||
bounds
|
||||
}
|
||||
|
||||
/// At which argument (input; usually X) there is a ruler (usually vertical)
|
||||
fn arguments_with_ruler(&self) -> Vec<PlotPoint> {
|
||||
// Default: one at center
|
||||
vec![self.bounds().center()]
|
||||
}
|
||||
|
||||
/// At which value (output; usually Y) there is a ruler (usually horizontal)
|
||||
fn values_with_ruler(&self) -> Vec<PlotPoint>;
|
||||
|
||||
/// The diagram's orientation (vertical/horizontal)
|
||||
fn orientation(&self) -> Orientation;
|
||||
|
||||
/// Get X/Y-value for (argument, value) pair, taking into account orientation
|
||||
fn point_at(&self, argument: f64, value: f64) -> PlotPoint {
|
||||
match self.orientation() {
|
||||
Orientation::Horizontal => PlotPoint::new(value, argument),
|
||||
Orientation::Vertical => PlotPoint::new(argument, value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Right top of the rectangle (position of text)
|
||||
fn corner_value(&self) -> PlotPoint {
|
||||
//self.point_at(self.position + self.width / 2.0, value)
|
||||
PlotPoint {
|
||||
x: self.bounds_max().x,
|
||||
y: self.bounds_max().y,
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug formatting for hovered-over value, if none is specified by the user
|
||||
fn default_values_format(&self, transform: &PlotTransform) -> String;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helper functions
|
||||
|
||||
pub(super) fn highlighted_color(mut stroke: Stroke, fill: Color32) -> (Stroke, Color32) {
|
||||
stroke.width *= 2.0;
|
||||
|
||||
let mut fill = Rgba::from(fill);
|
||||
if fill.is_additive() {
|
||||
// Make slightly brighter
|
||||
fill = 1.3 * fill;
|
||||
} else {
|
||||
// Make more opaque:
|
||||
let fill_alpha = (2.0 * fill.a()).at_most(1.0);
|
||||
fill = fill.to_opaque().multiply(fill_alpha);
|
||||
}
|
||||
|
||||
(stroke, fill.into())
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
use std::ops::{Bound, RangeBounds, RangeInclusive};
|
||||
|
||||
use egui::{Pos2, Shape, Stroke, Vec2};
|
||||
|
||||
use crate::transform::PlotBounds;
|
||||
|
||||
/// A point coordinate in the plot.
|
||||
///
|
||||
/// Uses f64 for improved accuracy to enable plotting
|
||||
/// large values (e.g. unix time on x axis).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PlotPoint {
|
||||
/// This is often something monotonically increasing, such as time, but doesn't have to be.
|
||||
/// Goes from left to right.
|
||||
pub x: f64,
|
||||
|
||||
/// Goes from bottom to top (inverse of everything else in egui!).
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
impl From<[f64; 2]> for PlotPoint {
|
||||
#[inline]
|
||||
fn from([x, y]: [f64; 2]) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl PlotPoint {
|
||||
#[inline(always)]
|
||||
pub fn new(x: impl Into<f64>, y: impl Into<f64>) -> Self {
|
||||
Self {
|
||||
x: x.into(),
|
||||
y: y.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn to_pos2(self) -> Pos2 {
|
||||
Pos2::new(self.x as f32, self.y as f32)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn to_vec2(self) -> Vec2 {
|
||||
Vec2::new(self.x as f32, self.y as f32)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Solid, dotted, dashed, etc.
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum LineStyle {
|
||||
Solid,
|
||||
Dotted { spacing: f32 },
|
||||
Dashed { length: f32 },
|
||||
}
|
||||
|
||||
impl LineStyle {
|
||||
pub fn dashed_loose() -> Self {
|
||||
Self::Dashed { length: 10.0 }
|
||||
}
|
||||
|
||||
pub fn dashed_dense() -> Self {
|
||||
Self::Dashed { length: 5.0 }
|
||||
}
|
||||
|
||||
pub fn dotted_loose() -> Self {
|
||||
Self::Dotted { spacing: 10.0 }
|
||||
}
|
||||
|
||||
pub fn dotted_dense() -> Self {
|
||||
Self::Dotted { spacing: 5.0 }
|
||||
}
|
||||
|
||||
pub(super) fn style_line(
|
||||
&self,
|
||||
line: Vec<Pos2>,
|
||||
mut stroke: Stroke,
|
||||
highlight: bool,
|
||||
shapes: &mut Vec<Shape>,
|
||||
) {
|
||||
match line.len() {
|
||||
0 => {}
|
||||
1 => {
|
||||
let mut radius = stroke.width / 2.0;
|
||||
if highlight {
|
||||
radius *= 2f32.sqrt();
|
||||
}
|
||||
shapes.push(Shape::circle_filled(line[0], radius, stroke.color));
|
||||
}
|
||||
_ => {
|
||||
match self {
|
||||
Self::Solid => {
|
||||
if highlight {
|
||||
stroke.width *= 2.0;
|
||||
}
|
||||
shapes.push(Shape::line(line, stroke));
|
||||
}
|
||||
Self::Dotted { spacing } => {
|
||||
// Take the stroke width for the radius even though it's not "correct", otherwise
|
||||
// the dots would become too small.
|
||||
let mut radius = stroke.width;
|
||||
if highlight {
|
||||
radius *= 2f32.sqrt();
|
||||
}
|
||||
shapes.extend(Shape::dotted_line(&line, stroke.color, *spacing, radius));
|
||||
}
|
||||
Self::Dashed { length } => {
|
||||
if highlight {
|
||||
stroke.width *= 2.0;
|
||||
}
|
||||
let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
|
||||
shapes.extend(Shape::dashed_line(
|
||||
&line,
|
||||
stroke,
|
||||
*length,
|
||||
length * golden_ratio,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LineStyle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Solid => write!(f, "Solid"),
|
||||
Self::Dotted { spacing } => write!(f, "Dotted({spacing} px)"),
|
||||
Self::Dashed { length } => write!(f, "Dashed({length} px)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Determines whether a plot element is vertically or horizontally oriented.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Orientation {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl Default for Orientation {
|
||||
fn default() -> Self {
|
||||
Self::Vertical
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Represents many [`PlotPoint`]s.
|
||||
///
|
||||
/// These can be an owned `Vec` or generated with a function.
|
||||
pub enum PlotPoints {
|
||||
Owned(Vec<PlotPoint>),
|
||||
Generator(ExplicitGenerator),
|
||||
// Borrowed(&[PlotPoint]), // TODO(EmbersArc): Lifetimes are tricky in this case.
|
||||
}
|
||||
|
||||
impl Default for PlotPoints {
|
||||
fn default() -> Self {
|
||||
Self::Owned(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[f64; 2]> for PlotPoints {
|
||||
fn from(coordinate: [f64; 2]) -> Self {
|
||||
Self::new(vec![coordinate])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<[f64; 2]>> for PlotPoints {
|
||||
fn from(coordinates: Vec<[f64; 2]>) -> Self {
|
||||
Self::new(coordinates)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<[f64; 2]> for PlotPoints {
|
||||
fn from_iter<T: IntoIterator<Item = [f64; 2]>>(iter: T) -> Self {
|
||||
Self::Owned(iter.into_iter().map(|point| point.into()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl PlotPoints {
|
||||
pub fn new(points: Vec<[f64; 2]>) -> Self {
|
||||
Self::from_iter(points)
|
||||
}
|
||||
|
||||
pub fn points(&self) -> &[PlotPoint] {
|
||||
match self {
|
||||
Self::Owned(points) => points.as_slice(),
|
||||
Self::Generator(_) => &[],
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a line based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points.
|
||||
pub fn from_explicit_callback(
|
||||
function: impl Fn(f64) -> f64 + 'static,
|
||||
x_range: impl RangeBounds<f64>,
|
||||
points: usize,
|
||||
) -> Self {
|
||||
let start = match x_range.start_bound() {
|
||||
Bound::Included(x) | Bound::Excluded(x) => *x,
|
||||
Bound::Unbounded => f64::NEG_INFINITY,
|
||||
};
|
||||
let end = match x_range.end_bound() {
|
||||
Bound::Included(x) | Bound::Excluded(x) => *x,
|
||||
Bound::Unbounded => f64::INFINITY,
|
||||
};
|
||||
let x_range = start..=end;
|
||||
|
||||
let generator = ExplicitGenerator {
|
||||
function: Box::new(function),
|
||||
x_range,
|
||||
points,
|
||||
};
|
||||
|
||||
Self::Generator(generator)
|
||||
}
|
||||
|
||||
/// Draw a line based on a function `(x,y)=f(t)`, a range for t and the number of points.
|
||||
/// The range may be specified as start..end or as start..=end.
|
||||
pub fn from_parametric_callback(
|
||||
function: impl Fn(f64) -> (f64, f64),
|
||||
t_range: impl RangeBounds<f64>,
|
||||
points: usize,
|
||||
) -> Self {
|
||||
let start = match t_range.start_bound() {
|
||||
Bound::Included(x) => x,
|
||||
Bound::Excluded(_) => unreachable!(),
|
||||
Bound::Unbounded => panic!("The range for parametric functions must be bounded!"),
|
||||
};
|
||||
let end = match t_range.end_bound() {
|
||||
Bound::Included(x) | Bound::Excluded(x) => x,
|
||||
Bound::Unbounded => panic!("The range for parametric functions must be bounded!"),
|
||||
};
|
||||
let last_point_included = matches!(t_range.end_bound(), Bound::Included(_));
|
||||
let increment = if last_point_included {
|
||||
(end - start) / (points - 1) as f64
|
||||
} else {
|
||||
(end - start) / points as f64
|
||||
};
|
||||
(0..points)
|
||||
.map(|i| {
|
||||
let t = start + i as f64 * increment;
|
||||
function(t).into()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// From a series of y-values.
|
||||
/// The x-values will be the indices of these values
|
||||
pub fn from_ys_f32(ys: &[f32]) -> Self {
|
||||
ys.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &y)| [i as f64, y as f64])
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// From a series of y-values.
|
||||
/// The x-values will be the indices of these values
|
||||
pub fn from_ys_f64(ys: &[f64]) -> Self {
|
||||
ys.iter().enumerate().map(|(i, &y)| [i as f64, y]).collect()
|
||||
}
|
||||
|
||||
/// Returns true if there are no data points available and there is no function to generate any.
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Self::Owned(points) => points.is_empty(),
|
||||
Self::Generator(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// If initialized with a generator function, this will generate `n` evenly spaced points in the
|
||||
/// given range.
|
||||
pub(super) fn generate_points(&mut self, x_range: RangeInclusive<f64>) {
|
||||
if let Self::Generator(generator) = self {
|
||||
*self = Self::range_intersection(&x_range, &generator.x_range)
|
||||
.map(|intersection| {
|
||||
let increment =
|
||||
(intersection.end() - intersection.start()) / (generator.points - 1) as f64;
|
||||
(0..generator.points)
|
||||
.map(|i| {
|
||||
let x = intersection.start() + i as f64 * increment;
|
||||
let y = (generator.function)(x);
|
||||
[x, y]
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the intersection of two ranges if they intersect.
|
||||
fn range_intersection(
|
||||
range1: &RangeInclusive<f64>,
|
||||
range2: &RangeInclusive<f64>,
|
||||
) -> Option<RangeInclusive<f64>> {
|
||||
let start = range1.start().max(*range2.start());
|
||||
let end = range1.end().min(*range2.end());
|
||||
(start < end).then_some(start..=end)
|
||||
}
|
||||
|
||||
pub(super) fn bounds(&self) -> PlotBounds {
|
||||
match self {
|
||||
Self::Owned(points) => {
|
||||
let mut bounds = PlotBounds::NOTHING;
|
||||
for point in points {
|
||||
bounds.extend_with(point);
|
||||
}
|
||||
bounds
|
||||
}
|
||||
Self::Generator(generator) => generator.estimate_bounds(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Circle, Diamond, Square, Cross, …
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum MarkerShape {
|
||||
Circle,
|
||||
Diamond,
|
||||
Square,
|
||||
Cross,
|
||||
Plus,
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Asterisk,
|
||||
}
|
||||
|
||||
impl MarkerShape {
|
||||
/// Get a vector containing all marker shapes.
|
||||
pub fn all() -> impl ExactSizeIterator<Item = Self> {
|
||||
[
|
||||
Self::Circle,
|
||||
Self::Diamond,
|
||||
Self::Square,
|
||||
Self::Cross,
|
||||
Self::Plus,
|
||||
Self::Up,
|
||||
Self::Down,
|
||||
Self::Left,
|
||||
Self::Right,
|
||||
Self::Asterisk,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Query the points of the plot, for geometric relations like closest checks
|
||||
pub enum PlotGeometry<'a> {
|
||||
/// No geometry based on single elements (examples: text, image, horizontal/vertical line)
|
||||
None,
|
||||
|
||||
/// Point values (X-Y graphs)
|
||||
Points(&'a [PlotPoint]),
|
||||
|
||||
/// Rectangles (examples: boxes or bars)
|
||||
// Has currently no data, as it would require copying rects or iterating a list of pointers.
|
||||
// Instead, geometry-based functions are directly implemented in the respective PlotItem impl.
|
||||
Rects,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Describes a function y = f(x) with an optional range for x and a number of points.
|
||||
pub struct ExplicitGenerator {
|
||||
function: Box<dyn Fn(f64) -> f64>,
|
||||
x_range: RangeInclusive<f64>,
|
||||
points: usize,
|
||||
}
|
||||
|
||||
impl ExplicitGenerator {
|
||||
fn estimate_bounds(&self) -> PlotBounds {
|
||||
let mut bounds = PlotBounds::NOTHING;
|
||||
|
||||
let mut add_x = |x: f64| {
|
||||
// avoid infinities, as we cannot auto-bound on them!
|
||||
if x.is_finite() {
|
||||
bounds.extend_with_x(x);
|
||||
}
|
||||
let y = (self.function)(x);
|
||||
if y.is_finite() {
|
||||
bounds.extend_with_y(y);
|
||||
}
|
||||
};
|
||||
|
||||
let min_x = *self.x_range.start();
|
||||
let max_x = *self.x_range.end();
|
||||
|
||||
add_x(min_x);
|
||||
add_x(max_x);
|
||||
|
||||
if min_x.is_finite() && max_x.is_finite() {
|
||||
// Sample some points in the interval:
|
||||
const N: u32 = 8;
|
||||
for i in 1..N {
|
||||
let t = i as f64 / (N - 1) as f64;
|
||||
let x = crate::lerp(min_x..=max_x, t);
|
||||
add_x(x);
|
||||
}
|
||||
} else {
|
||||
// Try adding some points anyway:
|
||||
for x in [-1, 0, 1] {
|
||||
let x = x as f64;
|
||||
if min_x <= x && x <= max_x {
|
||||
add_x(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bounds
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Result of [`super::PlotItem::find_closest()`] search, identifies an element inside the item for immediate use
|
||||
pub struct ClosestElem {
|
||||
/// Position of hovered-over value (or bar/box-plot/…) in `PlotItem`
|
||||
pub index: usize,
|
||||
|
||||
/// Squared distance from the mouse cursor (needed to compare against other `PlotItems`, which might be nearer)
|
||||
pub dist_sq: f32,
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
use std::{collections::BTreeMap, string::String};
|
||||
|
||||
use crate::*;
|
||||
|
||||
use super::items::PlotItem;
|
||||
|
||||
/// Where to place the plot legend.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Corner {
|
||||
LeftTop,
|
||||
RightTop,
|
||||
LeftBottom,
|
||||
RightBottom,
|
||||
}
|
||||
|
||||
impl Corner {
|
||||
pub fn all() -> impl Iterator<Item = Self> {
|
||||
[
|
||||
Self::LeftTop,
|
||||
Self::RightTop,
|
||||
Self::LeftBottom,
|
||||
Self::RightBottom,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
|
||||
/// The configuration for a plot legend.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Legend {
|
||||
pub text_style: TextStyle,
|
||||
pub background_alpha: f32,
|
||||
pub position: Corner,
|
||||
|
||||
/// Used for overriding the `hidden_items` set in [`LegendWidget`].
|
||||
hidden_items: Option<ahash::HashSet<String>>,
|
||||
}
|
||||
|
||||
impl Default for Legend {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text_style: TextStyle::Body,
|
||||
background_alpha: 0.75,
|
||||
position: Corner::RightTop,
|
||||
|
||||
hidden_items: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Legend {
|
||||
/// Which text style to use for the legend. Default: `TextStyle::Body`.
|
||||
#[inline]
|
||||
pub fn text_style(mut self, style: TextStyle) -> Self {
|
||||
self.text_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// The alpha of the legend background. Default: `0.75`.
|
||||
#[inline]
|
||||
pub fn background_alpha(mut self, alpha: f32) -> Self {
|
||||
self.background_alpha = alpha;
|
||||
self
|
||||
}
|
||||
|
||||
/// In which corner to place the legend. Default: `Corner::RightTop`.
|
||||
#[inline]
|
||||
pub fn position(mut self, corner: Corner) -> Self {
|
||||
self.position = corner;
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies hidden items in the legend configuration to override the existing ones. This
|
||||
/// allows the legend traces' visibility to be controlled from the application code.
|
||||
#[inline]
|
||||
pub fn hidden_items<I>(mut self, hidden_items: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
self.hidden_items = Some(hidden_items.into_iter().collect());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LegendEntry {
|
||||
color: Color32,
|
||||
checked: bool,
|
||||
hovered: bool,
|
||||
}
|
||||
|
||||
impl LegendEntry {
|
||||
fn new(color: Color32, checked: bool) -> Self {
|
||||
Self {
|
||||
color,
|
||||
checked,
|
||||
hovered: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(&self, ui: &mut Ui, text: String, text_style: &TextStyle) -> Response {
|
||||
let Self {
|
||||
color,
|
||||
checked,
|
||||
hovered: _,
|
||||
} = self;
|
||||
|
||||
let font_id = text_style.resolve(ui.style());
|
||||
|
||||
let galley = ui.fonts(|f| f.layout_delayed_color(text, font_id, f32::INFINITY));
|
||||
|
||||
let icon_size = galley.size().y;
|
||||
let icon_spacing = icon_size / 5.0;
|
||||
let total_extra = vec2(icon_size + icon_spacing, 0.0);
|
||||
|
||||
let desired_size = total_extra + galley.size();
|
||||
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
|
||||
|
||||
response.widget_info(|| {
|
||||
WidgetInfo::selected(
|
||||
WidgetType::Checkbox,
|
||||
ui.is_enabled(),
|
||||
*checked,
|
||||
galley.text(),
|
||||
)
|
||||
});
|
||||
|
||||
let visuals = ui.style().interact(&response);
|
||||
let label_on_the_left = ui.layout().horizontal_placement() == Align::RIGHT;
|
||||
|
||||
let icon_position_x = if label_on_the_left {
|
||||
rect.right() - icon_size / 2.0
|
||||
} else {
|
||||
rect.left() + icon_size / 2.0
|
||||
};
|
||||
let icon_position = pos2(icon_position_x, rect.center().y);
|
||||
let icon_rect = Rect::from_center_size(icon_position, vec2(icon_size, icon_size));
|
||||
|
||||
let painter = ui.painter();
|
||||
|
||||
painter.add(epaint::CircleShape {
|
||||
center: icon_rect.center(),
|
||||
radius: icon_size * 0.5,
|
||||
fill: visuals.bg_fill,
|
||||
stroke: visuals.bg_stroke,
|
||||
});
|
||||
|
||||
if *checked {
|
||||
let fill = if *color == Color32::TRANSPARENT {
|
||||
ui.visuals().noninteractive().fg_stroke.color
|
||||
} else {
|
||||
*color
|
||||
};
|
||||
painter.add(epaint::Shape::circle_filled(
|
||||
icon_rect.center(),
|
||||
icon_size * 0.4,
|
||||
fill,
|
||||
));
|
||||
}
|
||||
|
||||
let text_position_x = if label_on_the_left {
|
||||
rect.right() - icon_size - icon_spacing - galley.size().x
|
||||
} else {
|
||||
rect.left() + icon_size + icon_spacing
|
||||
};
|
||||
|
||||
let text_position = pos2(text_position_x, rect.center().y - 0.5 * galley.size().y);
|
||||
painter.galley(text_position, galley, visuals.text_color());
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct LegendWidget {
|
||||
rect: Rect,
|
||||
entries: BTreeMap<String, LegendEntry>,
|
||||
config: Legend,
|
||||
}
|
||||
|
||||
impl LegendWidget {
|
||||
/// Create a new legend from items, the names of items that are hidden and the style of the
|
||||
/// text. Returns `None` if the legend has no entries.
|
||||
pub(super) fn try_new(
|
||||
rect: Rect,
|
||||
config: Legend,
|
||||
items: &[Box<dyn PlotItem>],
|
||||
hidden_items: &ahash::HashSet<String>, // Existing hidden items in the plot memory.
|
||||
) -> Option<Self> {
|
||||
// If `config.hidden_items` is not `None`, it is used.
|
||||
let hidden_items = config.hidden_items.as_ref().unwrap_or(hidden_items);
|
||||
|
||||
// Collect the legend entries. If multiple items have the same name, they share a
|
||||
// checkbox. If their colors don't match, we pick a neutral color for the checkbox.
|
||||
let mut entries: BTreeMap<String, LegendEntry> = BTreeMap::new();
|
||||
items
|
||||
.iter()
|
||||
.filter(|item| !item.name().is_empty())
|
||||
.for_each(|item| {
|
||||
entries
|
||||
.entry(item.name().to_owned())
|
||||
.and_modify(|entry| {
|
||||
if entry.color != item.color() {
|
||||
// Multiple items with different colors
|
||||
entry.color = Color32::TRANSPARENT;
|
||||
}
|
||||
})
|
||||
.or_insert_with(|| {
|
||||
let color = item.color();
|
||||
let checked = !hidden_items.contains(item.name());
|
||||
LegendEntry::new(color, checked)
|
||||
});
|
||||
});
|
||||
(!entries.is_empty()).then_some(Self {
|
||||
rect,
|
||||
entries,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
// Get the names of the hidden items.
|
||||
pub fn hidden_items(&self) -> ahash::HashSet<String> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter(|(_, entry)| !entry.checked)
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Get the name of the hovered items.
|
||||
pub fn hovered_item_name(&self) -> Option<String> {
|
||||
self.entries
|
||||
.iter()
|
||||
.find(|(_, entry)| entry.hovered)
|
||||
.map(|(name, _)| name.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut LegendWidget {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let LegendWidget {
|
||||
rect,
|
||||
entries,
|
||||
config,
|
||||
} = self;
|
||||
|
||||
let main_dir = match config.position {
|
||||
Corner::LeftTop | Corner::RightTop => Direction::TopDown,
|
||||
Corner::LeftBottom | Corner::RightBottom => Direction::BottomUp,
|
||||
};
|
||||
let cross_align = match config.position {
|
||||
Corner::LeftTop | Corner::LeftBottom => Align::LEFT,
|
||||
Corner::RightTop | Corner::RightBottom => Align::RIGHT,
|
||||
};
|
||||
let layout = Layout::from_main_dir_and_cross_align(main_dir, cross_align);
|
||||
let legend_pad = 4.0;
|
||||
let legend_rect = rect.shrink(legend_pad);
|
||||
let mut legend_ui = ui.child_ui(legend_rect, layout, None);
|
||||
legend_ui
|
||||
.scope(|ui| {
|
||||
let background_frame = Frame {
|
||||
inner_margin: vec2(8.0, 4.0).into(),
|
||||
rounding: ui.style().visuals.window_rounding,
|
||||
shadow: epaint::Shadow::NONE,
|
||||
fill: ui.style().visuals.extreme_bg_color,
|
||||
stroke: ui.style().visuals.window_stroke(),
|
||||
..Default::default()
|
||||
}
|
||||
.multiply_with_opacity(config.background_alpha);
|
||||
background_frame
|
||||
.show(ui, |ui| {
|
||||
let mut focus_on_item = None;
|
||||
|
||||
let response_union = entries
|
||||
.iter_mut()
|
||||
.map(|(name, entry)| {
|
||||
let response = entry.ui(ui, name.clone(), &config.text_style);
|
||||
|
||||
// Handle interactions. Alt-clicking must be deferred to end of loop
|
||||
// since it may affect all entries.
|
||||
handle_interaction_on_legend_item(&response, entry);
|
||||
if response.clicked() && ui.input(|r| r.modifiers.alt) {
|
||||
focus_on_item = Some(name.clone());
|
||||
}
|
||||
|
||||
response
|
||||
})
|
||||
.reduce(|r1, r2| r1.union(r2))
|
||||
.unwrap();
|
||||
|
||||
if let Some(focus_on_item) = focus_on_item {
|
||||
handle_focus_on_legend_item(&focus_on_item, entries);
|
||||
}
|
||||
|
||||
response_union
|
||||
})
|
||||
.inner
|
||||
})
|
||||
.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle per-entry interactions.
|
||||
fn handle_interaction_on_legend_item(response: &Response, entry: &mut LegendEntry) {
|
||||
entry.checked ^= response.clicked_by(PointerButton::Primary);
|
||||
entry.hovered = response.hovered();
|
||||
}
|
||||
|
||||
/// Handle alt-click interaction (which may affect all entries).
|
||||
fn handle_focus_on_legend_item(
|
||||
clicked_entry_name: &str,
|
||||
entries: &mut BTreeMap<String, LegendEntry>,
|
||||
) {
|
||||
// if all other items are already hidden, we show everything
|
||||
let is_focus_item_only_visible = entries
|
||||
.iter()
|
||||
.all(|(name, entry)| !entry.checked || (clicked_entry_name == name));
|
||||
|
||||
// either show everything or show only the focus item
|
||||
for (name, entry) in entries.iter_mut() {
|
||||
entry.checked = is_focus_item_only_visible || clicked_entry_name == name;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,81 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use egui::{Context, Id, Pos2, Vec2b};
|
||||
|
||||
use crate::{PlotBounds, PlotTransform};
|
||||
|
||||
/// Information about the plot that has to persist between frames.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Clone)]
|
||||
pub struct PlotMemory {
|
||||
/// Indicates if the plot uses automatic bounds.
|
||||
///
|
||||
/// This is set to `false` whenever the user modifies
|
||||
/// the bounds, for example by moving or zooming.
|
||||
pub auto_bounds: Vec2b,
|
||||
|
||||
/// Display string of the hovered legend item if any.
|
||||
pub hovered_legend_item: Option<String>,
|
||||
|
||||
/// Which items _not_ to show?
|
||||
pub hidden_items: ahash::HashSet<String>,
|
||||
|
||||
/// The transform from last frame.
|
||||
pub(crate) transform: PlotTransform,
|
||||
|
||||
/// Allows to remember the first click position when performing a boxed zoom
|
||||
pub(crate) last_click_pos_for_zoom: Option<Pos2>,
|
||||
|
||||
/// The thickness of each of the axes the previous frame.
|
||||
///
|
||||
/// This is used in the next frame to make the axes thicker
|
||||
/// in order to fit the labels, if necessary.
|
||||
pub(crate) x_axis_thickness: BTreeMap<usize, f32>,
|
||||
pub(crate) y_axis_thickness: BTreeMap<usize, f32>,
|
||||
}
|
||||
|
||||
impl PlotMemory {
|
||||
#[inline]
|
||||
pub fn transform(&self) -> PlotTransform {
|
||||
self.transform
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_transform(&mut self, t: PlotTransform) {
|
||||
self.transform = t;
|
||||
}
|
||||
|
||||
/// Plot-space bounds.
|
||||
#[inline]
|
||||
pub fn bounds(&self) -> &PlotBounds {
|
||||
self.transform.bounds()
|
||||
}
|
||||
|
||||
/// Plot-space bounds.
|
||||
#[inline]
|
||||
pub fn set_bounds(&mut self, bounds: PlotBounds) {
|
||||
self.transform.set_bounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl PlotMemory {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
ctx.data_mut(|d| d.get_persisted(id))
|
||||
}
|
||||
|
||||
pub fn store(self, ctx: &Context, id: Id) {
|
||||
ctx.data_mut(|d| d.insert_persisted(id, self));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "serde"))]
|
||||
impl PlotMemory {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
ctx.data_mut(|d| d.get_temp(id))
|
||||
}
|
||||
|
||||
pub fn store(self, ctx: &Context, id: Id) {
|
||||
ctx.data_mut(|d| d.insert_temp(id, self));
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
use crate::*;
|
||||
|
||||
/// Provides methods to interact with a plot while building it. It is the single argument of the closure
|
||||
/// provided to [`Plot::show`]. See [`Plot`] for an example of how to use it.
|
||||
pub struct PlotUi {
|
||||
pub(crate) ctx: Context,
|
||||
pub(crate) items: Vec<Box<dyn PlotItem>>,
|
||||
pub(crate) next_auto_color_idx: usize,
|
||||
pub(crate) last_plot_transform: PlotTransform,
|
||||
pub(crate) last_auto_bounds: Vec2b,
|
||||
pub(crate) response: Response,
|
||||
pub(crate) bounds_modifications: Vec<BoundsModification>,
|
||||
}
|
||||
|
||||
impl PlotUi {
|
||||
fn auto_color(&mut self) -> Color32 {
|
||||
let i = self.next_auto_color_idx;
|
||||
self.next_auto_color_idx += 1;
|
||||
let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
|
||||
let h = i as f32 * golden_ratio;
|
||||
Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO(emilk): OkLab or some other perspective color space
|
||||
}
|
||||
|
||||
pub fn ctx(&self) -> &Context {
|
||||
&self.ctx
|
||||
}
|
||||
|
||||
/// The plot bounds as they were in the last frame. If called on the first frame and the bounds were not
|
||||
/// further specified in the plot builder, this will return bounds centered on the origin. The bounds do
|
||||
/// not change until the plot is drawn.
|
||||
pub fn plot_bounds(&self) -> PlotBounds {
|
||||
*self.last_plot_transform.bounds()
|
||||
}
|
||||
|
||||
/// Set the plot bounds. Can be useful for implementing alternative plot navigation methods.
|
||||
pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) {
|
||||
self.bounds_modifications
|
||||
.push(BoundsModification::Set(plot_bounds));
|
||||
}
|
||||
|
||||
/// Move the plot bounds. Can be useful for implementing alternative plot navigation methods.
|
||||
pub fn translate_bounds(&mut self, delta_pos: Vec2) {
|
||||
self.bounds_modifications
|
||||
.push(BoundsModification::Translate(delta_pos));
|
||||
}
|
||||
|
||||
/// Whether the plot axes were in auto-bounds mode in the last frame. If called on the first
|
||||
/// frame, this is the [`Plot`]'s default auto-bounds mode.
|
||||
pub fn auto_bounds(&self) -> Vec2b {
|
||||
self.last_auto_bounds
|
||||
}
|
||||
|
||||
/// Set the auto-bounds mode for the plot axes.
|
||||
pub fn set_auto_bounds(&mut self, auto_bounds: Vec2b) {
|
||||
self.bounds_modifications
|
||||
.push(BoundsModification::AutoBounds(auto_bounds));
|
||||
}
|
||||
|
||||
/// Can be used to check if the plot was hovered or clicked.
|
||||
pub fn response(&self) -> &Response {
|
||||
&self.response
|
||||
}
|
||||
|
||||
/// Scale the plot bounds around a position in plot coordinates.
|
||||
///
|
||||
/// Can be useful for implementing alternative plot navigation methods.
|
||||
///
|
||||
/// The plot bounds are divided by `zoom_factor`, therefore:
|
||||
/// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data.
|
||||
/// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail.
|
||||
pub fn zoom_bounds(&mut self, zoom_factor: Vec2, center: PlotPoint) {
|
||||
self.bounds_modifications
|
||||
.push(BoundsModification::Zoom(zoom_factor, center));
|
||||
}
|
||||
|
||||
/// Scale the plot bounds around the hovered position, if any.
|
||||
///
|
||||
/// Can be useful for implementing alternative plot navigation methods.
|
||||
///
|
||||
/// The plot bounds are divided by `zoom_factor`, therefore:
|
||||
/// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data.
|
||||
/// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail.
|
||||
pub fn zoom_bounds_around_hovered(&mut self, zoom_factor: Vec2) {
|
||||
if let Some(hover_pos) = self.pointer_coordinate() {
|
||||
self.zoom_bounds(zoom_factor, hover_pos);
|
||||
}
|
||||
}
|
||||
|
||||
/// The pointer position in plot coordinates. Independent of whether the pointer is in the plot area.
|
||||
pub fn pointer_coordinate(&self) -> Option<PlotPoint> {
|
||||
// We need to subtract the drag delta to keep in sync with the frame-delayed screen transform:
|
||||
let last_pos = self.ctx().input(|i| i.pointer.latest_pos())? - self.response.drag_delta();
|
||||
let value = self.plot_from_screen(last_pos);
|
||||
Some(value)
|
||||
}
|
||||
|
||||
/// The pointer drag delta in plot coordinates.
|
||||
pub fn pointer_coordinate_drag_delta(&self) -> Vec2 {
|
||||
let delta = self.response.drag_delta();
|
||||
let dp_dv = self.last_plot_transform.dpos_dvalue();
|
||||
Vec2::new(delta.x / dp_dv[0] as f32, delta.y / dp_dv[1] as f32)
|
||||
}
|
||||
|
||||
/// Read the transform between plot coordinates and screen coordinates.
|
||||
pub fn transform(&self) -> &PlotTransform {
|
||||
&self.last_plot_transform
|
||||
}
|
||||
|
||||
/// Transform the plot coordinates to screen coordinates.
|
||||
pub fn screen_from_plot(&self, position: PlotPoint) -> Pos2 {
|
||||
self.last_plot_transform.position_from_point(&position)
|
||||
}
|
||||
|
||||
/// Transform the screen coordinates to plot coordinates.
|
||||
pub fn plot_from_screen(&self, position: Pos2) -> PlotPoint {
|
||||
self.last_plot_transform.value_from_position(position)
|
||||
}
|
||||
|
||||
/// Add an arbitrary item.
|
||||
pub fn add(&mut self, item: impl PlotItem + 'static) {
|
||||
self.items.push(Box::new(item));
|
||||
}
|
||||
|
||||
/// Add a data line.
|
||||
pub fn line(&mut self, mut line: Line) {
|
||||
if line.series.is_empty() {
|
||||
return;
|
||||
};
|
||||
|
||||
// Give the stroke an automatic color if no color has been assigned.
|
||||
if line.stroke.color == Color32::TRANSPARENT {
|
||||
line.stroke.color = self.auto_color();
|
||||
}
|
||||
self.items.push(Box::new(line));
|
||||
}
|
||||
|
||||
/// Add a polygon. The polygon has to be convex.
|
||||
pub fn polygon(&mut self, mut polygon: Polygon) {
|
||||
if polygon.series.is_empty() {
|
||||
return;
|
||||
};
|
||||
|
||||
// Give the stroke an automatic color if no color has been assigned.
|
||||
if polygon.stroke.color == Color32::TRANSPARENT {
|
||||
polygon.stroke.color = self.auto_color();
|
||||
}
|
||||
self.items.push(Box::new(polygon));
|
||||
}
|
||||
|
||||
/// Add a text.
|
||||
pub fn text(&mut self, text: Text) {
|
||||
if text.text.is_empty() {
|
||||
return;
|
||||
};
|
||||
|
||||
self.items.push(Box::new(text));
|
||||
}
|
||||
|
||||
/// Add data points.
|
||||
pub fn points(&mut self, mut points: Points) {
|
||||
if points.series.is_empty() {
|
||||
return;
|
||||
};
|
||||
|
||||
// Give the points an automatic color if no color has been assigned.
|
||||
if points.color == Color32::TRANSPARENT {
|
||||
points.color = self.auto_color();
|
||||
}
|
||||
self.items.push(Box::new(points));
|
||||
}
|
||||
|
||||
/// Add arrows.
|
||||
pub fn arrows(&mut self, mut arrows: Arrows) {
|
||||
if arrows.origins.is_empty() || arrows.tips.is_empty() {
|
||||
return;
|
||||
};
|
||||
|
||||
// Give the arrows an automatic color if no color has been assigned.
|
||||
if arrows.color == Color32::TRANSPARENT {
|
||||
arrows.color = self.auto_color();
|
||||
}
|
||||
self.items.push(Box::new(arrows));
|
||||
}
|
||||
|
||||
/// Add an image.
|
||||
pub fn image(&mut self, image: PlotImage) {
|
||||
self.items.push(Box::new(image));
|
||||
}
|
||||
|
||||
/// Add a horizontal line.
|
||||
/// Can be useful e.g. to show min/max bounds or similar.
|
||||
/// Always fills the full width of the plot.
|
||||
pub fn hline(&mut self, mut hline: HLine) {
|
||||
if hline.stroke.color == Color32::TRANSPARENT {
|
||||
hline.stroke.color = self.auto_color();
|
||||
}
|
||||
self.items.push(Box::new(hline));
|
||||
}
|
||||
|
||||
/// Add a vertical line.
|
||||
/// Can be useful e.g. to show min/max bounds or similar.
|
||||
/// Always fills the full height of the plot.
|
||||
pub fn vline(&mut self, mut vline: VLine) {
|
||||
if vline.stroke.color == Color32::TRANSPARENT {
|
||||
vline.stroke.color = self.auto_color();
|
||||
}
|
||||
self.items.push(Box::new(vline));
|
||||
}
|
||||
|
||||
/// Add a box plot diagram.
|
||||
pub fn box_plot(&mut self, mut box_plot: BoxPlot) {
|
||||
if box_plot.boxes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Give the elements an automatic color if no color has been assigned.
|
||||
if box_plot.default_color == Color32::TRANSPARENT {
|
||||
box_plot = box_plot.color(self.auto_color());
|
||||
}
|
||||
self.items.push(Box::new(box_plot));
|
||||
}
|
||||
|
||||
/// Add a bar chart.
|
||||
pub fn bar_chart(&mut self, mut chart: BarChart) {
|
||||
if chart.bars.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Give the elements an automatic color if no color has been assigned.
|
||||
if chart.default_color == Color32::TRANSPARENT {
|
||||
chart = chart.color(self.auto_color());
|
||||
}
|
||||
self.items.push(Box::new(chart));
|
||||
}
|
||||
}
|
||||
@@ -1,509 +0,0 @@
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use super::PlotPoint;
|
||||
use crate::*;
|
||||
|
||||
/// 2D bounding box of f64 precision.
|
||||
///
|
||||
/// The range of data values we show.
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct PlotBounds {
|
||||
pub(crate) min: [f64; 2],
|
||||
pub(crate) max: [f64; 2],
|
||||
}
|
||||
|
||||
impl PlotBounds {
|
||||
pub const NOTHING: Self = Self {
|
||||
min: [f64::INFINITY; 2],
|
||||
max: [-f64::INFINITY; 2],
|
||||
};
|
||||
|
||||
#[inline]
|
||||
pub fn from_min_max(min: [f64; 2], max: [f64; 2]) -> Self {
|
||||
Self { min, max }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn min(&self) -> [f64; 2] {
|
||||
self.min
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn max(&self) -> [f64; 2] {
|
||||
self.max
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_symmetrical(half_extent: f64) -> Self {
|
||||
Self {
|
||||
min: [-half_extent; 2],
|
||||
max: [half_extent; 2],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_finite(&self) -> bool {
|
||||
self.min[0].is_finite()
|
||||
&& self.min[1].is_finite()
|
||||
&& self.max[0].is_finite()
|
||||
&& self.max[1].is_finite()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_finite_x(&self) -> bool {
|
||||
self.min[0].is_finite() && self.max[0].is_finite()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_finite_y(&self) -> bool {
|
||||
self.min[1].is_finite() && self.max[1].is_finite()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.is_finite() && self.width() > 0.0 && self.height() > 0.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_valid_x(&self) -> bool {
|
||||
self.is_finite_x() && self.width() > 0.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_valid_y(&self) -> bool {
|
||||
self.is_finite_y() && self.height() > 0.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn width(&self) -> f64 {
|
||||
self.max[0] - self.min[0]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn height(&self) -> f64 {
|
||||
self.max[1] - self.min[1]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn center(&self) -> PlotPoint {
|
||||
[
|
||||
(self.min[0] + self.max[0]) / 2.0,
|
||||
(self.min[1] + self.max[1]) / 2.0,
|
||||
]
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Expand to include the given (x,y) value
|
||||
#[inline]
|
||||
pub fn extend_with(&mut self, value: &PlotPoint) {
|
||||
self.extend_with_x(value.x);
|
||||
self.extend_with_y(value.y);
|
||||
}
|
||||
|
||||
/// Expand to include the given x coordinate
|
||||
#[inline]
|
||||
pub fn extend_with_x(&mut self, x: f64) {
|
||||
self.min[0] = self.min[0].min(x);
|
||||
self.max[0] = self.max[0].max(x);
|
||||
}
|
||||
|
||||
/// Expand to include the given y coordinate
|
||||
#[inline]
|
||||
pub fn extend_with_y(&mut self, y: f64) {
|
||||
self.min[1] = self.min[1].min(y);
|
||||
self.max[1] = self.max[1].max(y);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clamp_to_finite(&mut self) {
|
||||
for d in 0..2 {
|
||||
self.min[d] = self.min[d].clamp(f64::MIN, f64::MAX);
|
||||
if self.min[d].is_nan() {
|
||||
self.min[d] = 0.0;
|
||||
}
|
||||
|
||||
self.max[d] = self.max[d].clamp(f64::MIN, f64::MAX);
|
||||
if self.max[d].is_nan() {
|
||||
self.max[d] = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn expand_x(&mut self, pad: f64) {
|
||||
if pad.is_finite() {
|
||||
self.min[0] -= pad;
|
||||
self.max[0] += pad;
|
||||
self.clamp_to_finite();
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn expand_y(&mut self, pad: f64) {
|
||||
if pad.is_finite() {
|
||||
self.min[1] -= pad;
|
||||
self.max[1] += pad;
|
||||
self.clamp_to_finite();
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn merge_x(&mut self, other: &Self) {
|
||||
self.min[0] = self.min[0].min(other.min[0]);
|
||||
self.max[0] = self.max[0].max(other.max[0]);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn merge_y(&mut self, other: &Self) {
|
||||
self.min[1] = self.min[1].min(other.min[1]);
|
||||
self.max[1] = self.max[1].max(other.max[1]);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_x(&mut self, other: &Self) {
|
||||
self.min[0] = other.min[0];
|
||||
self.max[0] = other.max[0];
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_x_center_width(&mut self, x: f64, width: f64) {
|
||||
self.min[0] = x - width / 2.0;
|
||||
self.max[0] = x + width / 2.0;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_y(&mut self, other: &Self) {
|
||||
self.min[1] = other.min[1];
|
||||
self.max[1] = other.max[1];
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_y_center_height(&mut self, y: f64, height: f64) {
|
||||
self.min[1] = y - height / 2.0;
|
||||
self.max[1] = y + height / 2.0;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn merge(&mut self, other: &Self) {
|
||||
self.min[0] = self.min[0].min(other.min[0]);
|
||||
self.min[1] = self.min[1].min(other.min[1]);
|
||||
self.max[0] = self.max[0].max(other.max[0]);
|
||||
self.max[1] = self.max[1].max(other.max[1]);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn translate_x(&mut self, delta: f64) {
|
||||
if delta.is_finite() {
|
||||
self.min[0] += delta;
|
||||
self.max[0] += delta;
|
||||
self.clamp_to_finite();
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn translate_y(&mut self, delta: f64) {
|
||||
if delta.is_finite() {
|
||||
self.min[1] += delta;
|
||||
self.max[1] += delta;
|
||||
self.clamp_to_finite();
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn translate(&mut self, delta: (f64, f64)) {
|
||||
self.translate_x(delta.0);
|
||||
self.translate_y(delta.1);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn zoom(&mut self, zoom_factor: Vec2, center: PlotPoint) {
|
||||
self.min[0] = center.x + (self.min[0] - center.x) / (zoom_factor.x as f64);
|
||||
self.max[0] = center.x + (self.max[0] - center.x) / (zoom_factor.x as f64);
|
||||
self.min[1] = center.y + (self.min[1] - center.y) / (zoom_factor.y as f64);
|
||||
self.max[1] = center.y + (self.max[1] - center.y) / (zoom_factor.y as f64);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn add_relative_margin_x(&mut self, margin_fraction: Vec2) {
|
||||
let width = self.width().max(0.0);
|
||||
self.expand_x(margin_fraction.x as f64 * width);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn add_relative_margin_y(&mut self, margin_fraction: Vec2) {
|
||||
let height = self.height().max(0.0);
|
||||
self.expand_y(margin_fraction.y as f64 * height);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn range_x(&self) -> RangeInclusive<f64> {
|
||||
self.min[0]..=self.max[0]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn range_y(&self) -> RangeInclusive<f64> {
|
||||
self.min[1]..=self.max[1]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn make_x_symmetrical(&mut self) {
|
||||
let x_abs = self.min[0].abs().max(self.max[0].abs());
|
||||
self.min[0] = -x_abs;
|
||||
self.max[0] = x_abs;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn make_y_symmetrical(&mut self) {
|
||||
let y_abs = self.min[1].abs().max(self.max[1].abs());
|
||||
self.min[1] = -y_abs;
|
||||
self.max[1] = y_abs;
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the screen rectangle and the plot bounds and provides methods to transform between them.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PlotTransform {
|
||||
/// The screen rectangle.
|
||||
frame: Rect,
|
||||
|
||||
/// The plot bounds.
|
||||
bounds: PlotBounds,
|
||||
|
||||
/// Whether to always center the x-range of the bounds.
|
||||
x_centered: bool,
|
||||
|
||||
/// Whether to always center the y-range of the bounds.
|
||||
y_centered: bool,
|
||||
}
|
||||
|
||||
impl PlotTransform {
|
||||
pub fn new(frame: Rect, bounds: PlotBounds, x_centered: bool, y_centered: bool) -> Self {
|
||||
debug_assert!(
|
||||
0.0 <= frame.width() && 0.0 <= frame.height(),
|
||||
"Bad plot frame: {frame:?}"
|
||||
);
|
||||
|
||||
// Since the current Y bounds an affect the final X bounds and vice versa, we need to keep
|
||||
// the original version of the `bounds` before we start modifying it.
|
||||
let mut new_bounds = bounds;
|
||||
|
||||
// Sanitize bounds.
|
||||
//
|
||||
// When a given bound axis is "thin" (e.g. width or height is 0) but finite, we center the
|
||||
// bounds around that value. If the other axis is "fat", we reuse its extent for the thin
|
||||
// axis, and default to +/- 1.0 otherwise.
|
||||
if !bounds.is_finite_x() {
|
||||
new_bounds.set_x(&PlotBounds::new_symmetrical(1.0));
|
||||
} else if bounds.width() <= 0.0 {
|
||||
new_bounds.set_x_center_width(
|
||||
bounds.center().x,
|
||||
if bounds.is_valid_y() {
|
||||
bounds.height()
|
||||
} else {
|
||||
1.0
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
if !bounds.is_finite_y() {
|
||||
new_bounds.set_y(&PlotBounds::new_symmetrical(1.0));
|
||||
} else if bounds.height() <= 0.0 {
|
||||
new_bounds.set_y_center_height(
|
||||
bounds.center().y,
|
||||
if bounds.is_valid_x() {
|
||||
bounds.width()
|
||||
} else {
|
||||
1.0
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// Scale axes so that the origin is in the center.
|
||||
if x_centered {
|
||||
new_bounds.make_x_symmetrical();
|
||||
};
|
||||
if y_centered {
|
||||
new_bounds.make_y_symmetrical();
|
||||
};
|
||||
|
||||
debug_assert!(
|
||||
new_bounds.is_valid(),
|
||||
"Bad final plot bounds: {new_bounds:?}"
|
||||
);
|
||||
|
||||
Self {
|
||||
frame,
|
||||
bounds: new_bounds,
|
||||
x_centered,
|
||||
y_centered,
|
||||
}
|
||||
}
|
||||
|
||||
/// ui-space rectangle.
|
||||
#[inline]
|
||||
pub fn frame(&self) -> &Rect {
|
||||
&self.frame
|
||||
}
|
||||
|
||||
/// Plot-space bounds.
|
||||
#[inline]
|
||||
pub fn bounds(&self) -> &PlotBounds {
|
||||
&self.bounds
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_bounds(&mut self, bounds: PlotBounds) {
|
||||
self.bounds = bounds;
|
||||
}
|
||||
|
||||
pub fn translate_bounds(&mut self, mut delta_pos: (f64, f64)) {
|
||||
if self.x_centered {
|
||||
delta_pos.0 = 0.;
|
||||
}
|
||||
if self.y_centered {
|
||||
delta_pos.1 = 0.;
|
||||
}
|
||||
delta_pos.0 *= self.dvalue_dpos()[0];
|
||||
delta_pos.1 *= self.dvalue_dpos()[1];
|
||||
self.bounds.translate((delta_pos.0, delta_pos.1));
|
||||
}
|
||||
|
||||
/// Zoom by a relative factor with the given screen position as center.
|
||||
pub fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) {
|
||||
let center = self.value_from_position(center);
|
||||
|
||||
let mut new_bounds = self.bounds;
|
||||
new_bounds.zoom(zoom_factor, center);
|
||||
|
||||
if new_bounds.is_valid() {
|
||||
self.bounds = new_bounds;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn position_from_point_x(&self, value: f64) -> f32 {
|
||||
remap(
|
||||
value,
|
||||
self.bounds.min[0]..=self.bounds.max[0],
|
||||
(self.frame.left() as f64)..=(self.frame.right() as f64),
|
||||
) as f32
|
||||
}
|
||||
|
||||
pub fn position_from_point_y(&self, value: f64) -> f32 {
|
||||
remap(
|
||||
value,
|
||||
self.bounds.min[1]..=self.bounds.max[1],
|
||||
(self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
|
||||
) as f32
|
||||
}
|
||||
|
||||
/// Screen/ui position from point on plot.
|
||||
pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 {
|
||||
pos2(
|
||||
self.position_from_point_x(value.x),
|
||||
self.position_from_point_y(value.y),
|
||||
)
|
||||
}
|
||||
|
||||
/// Plot point from screen/ui position.
|
||||
pub fn value_from_position(&self, pos: Pos2) -> PlotPoint {
|
||||
let x = remap(
|
||||
pos.x as f64,
|
||||
(self.frame.left() as f64)..=(self.frame.right() as f64),
|
||||
self.bounds.range_x(),
|
||||
);
|
||||
let y = remap(
|
||||
pos.y as f64,
|
||||
(self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
|
||||
self.bounds.range_y(),
|
||||
);
|
||||
PlotPoint::new(x, y)
|
||||
}
|
||||
|
||||
/// Transform a rectangle of plot values to a screen-coordinate rectangle.
|
||||
///
|
||||
/// This typically means that the rect is mirrored vertically (top becomes bottom and vice versa),
|
||||
/// since the plot's coordinate system has +Y up, while egui has +Y down.
|
||||
pub fn rect_from_values(&self, value1: &PlotPoint, value2: &PlotPoint) -> Rect {
|
||||
let pos1 = self.position_from_point(value1);
|
||||
let pos2 = self.position_from_point(value2);
|
||||
|
||||
let mut rect = Rect::NOTHING;
|
||||
rect.extend_with(pos1);
|
||||
rect.extend_with(pos2);
|
||||
rect
|
||||
}
|
||||
|
||||
/// delta position / delta value = how many ui points per step in the X axis in "plot space"
|
||||
pub fn dpos_dvalue_x(&self) -> f64 {
|
||||
self.frame.width() as f64 / self.bounds.width()
|
||||
}
|
||||
|
||||
/// delta position / delta value = how many ui points per step in the Y axis in "plot space"
|
||||
pub fn dpos_dvalue_y(&self) -> f64 {
|
||||
-self.frame.height() as f64 / self.bounds.height() // negated y axis!
|
||||
}
|
||||
|
||||
/// delta position / delta value = how many ui points per step in "plot space"
|
||||
pub fn dpos_dvalue(&self) -> [f64; 2] {
|
||||
[self.dpos_dvalue_x(), self.dpos_dvalue_y()]
|
||||
}
|
||||
|
||||
/// delta value / delta position = how much ground do we cover in "plot space" per ui point?
|
||||
pub fn dvalue_dpos(&self) -> [f64; 2] {
|
||||
[1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()]
|
||||
}
|
||||
|
||||
/// scale.x/scale.y ratio.
|
||||
///
|
||||
/// If 1.0, it means the scale factor is the same in both axes.
|
||||
fn aspect(&self) -> f64 {
|
||||
let rw = self.frame.width() as f64;
|
||||
let rh = self.frame.height() as f64;
|
||||
(self.bounds.width() / rw) / (self.bounds.height() / rh)
|
||||
}
|
||||
|
||||
/// Sets the aspect ratio by expanding the x- or y-axis.
|
||||
///
|
||||
/// This never contracts, so we don't miss out on any data.
|
||||
pub(crate) fn set_aspect_by_expanding(&mut self, aspect: f64) {
|
||||
let current_aspect = self.aspect();
|
||||
|
||||
let epsilon = 1e-5;
|
||||
if (current_aspect - aspect).abs() < epsilon {
|
||||
// Don't make any changes when the aspect is already almost correct.
|
||||
return;
|
||||
}
|
||||
|
||||
if current_aspect < aspect {
|
||||
self.bounds
|
||||
.expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
|
||||
} else {
|
||||
self.bounds
|
||||
.expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the aspect ratio by changing either the X or Y axis (callers choice).
|
||||
pub(crate) fn set_aspect_by_changing_axis(&mut self, aspect: f64, axis: Axis) {
|
||||
let current_aspect = self.aspect();
|
||||
|
||||
let epsilon = 1e-5;
|
||||
if (current_aspect - aspect).abs() < epsilon {
|
||||
// Don't make any changes when the aspect is already almost correct.
|
||||
return;
|
||||
}
|
||||
|
||||
match axis {
|
||||
Axis::X => {
|
||||
self.bounds
|
||||
.expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
|
||||
}
|
||||
Axis::Y => {
|
||||
self.bounds
|
||||
.expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user