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:
Emil Ernerfeldt 2024-07-15 18:45:19 +02:00 committed by GitHub
parent 1384410cb4
commit cb9f30482f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 11 additions and 8196 deletions

View File

@ -29,4 +29,4 @@ jobs:
with:
mode: minimum
count: 1
labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui_plot, egui-wgpu, egui-winit, egui, epaint, exclude from changelog, typo"
labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui-wgpu, egui-winit, egui, epaint, exclude from changelog, typo"

View File

@ -5,7 +5,7 @@ Also see [`CONTRIBUTING.md`](CONTRIBUTING.md) for what to do before opening a PR
## Crate overview
The crates in this repository are: `egui, emath, epaint, egui_extras, egui_plot, egui-winit, egui_glow, egui_demo_lib, egui_demo_app`.
The crates in this repository are: `egui, emath, epaint, egui_extras, egui-winit, egui_glow, egui_demo_lib, egui_demo_app`.
### `egui`: The main GUI library.
Example code: `if ui.button("Click me").clicked() { … }`
@ -24,9 +24,6 @@ Depends on `emath`.
### `egui_extras`
This adds additional features on top of `egui`.
### `egui_plot`
Plotting for `egui`.
### `egui-winit`
This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [winit](https://crates.io/crates/winit).

View File

@ -1,7 +1,7 @@
# egui changelog
All notable changes to the `egui` crate will be documented in this file.
NOTE: this is just the changelog for the core `egui` crate. [`eframe`](crates/eframe/CHANGELOG.md), [`egui_plot`](crates/egui_plot/CHANGELOG.md), [`ecolor`](crates/ecolor/CHANGELOG.md), [`epaint`](crates/epaint/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs!
NOTE: this is just the changelog for the core `egui` crate. [`eframe`](crates/eframe/CHANGELOG.md), [`ecolor`](crates/ecolor/CHANGELOG.md), [`epaint`](crates/epaint/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs!
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.

View File

@ -1,2 +1 @@
/crates/egui_plot @Bromeon @EmbersArc
/crates/egui-wgpu @Wumpf

View File

@ -1059,15 +1059,6 @@ dependencies = [
"env_logger",
]
[[package]]
name = "custom_plot_manipulation"
version = "0.1.0"
dependencies = [
"eframe",
"egui_plot",
"env_logger",
]
[[package]]
name = "custom_window_frame"
version = "0.1.0"
@ -1302,7 +1293,6 @@ dependencies = [
"document-features",
"egui",
"egui_extras",
"egui_plot",
"log",
"serde",
"unicode_names2",
@ -1348,17 +1338,6 @@ dependencies = [
"winit",
]
[[package]]
name = "egui_plot"
version = "0.28.1"
dependencies = [
"ahash",
"document-features",
"egui",
"emath",
"serde",
]
[[package]]
name = "ehttp"
version = "0.5.0"
@ -3288,17 +3267,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "save_plot"
version = "0.1.0"
dependencies = [
"eframe",
"egui_plot",
"env_logger",
"image",
"rfd",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"

View File

@ -6,7 +6,6 @@ members = [
"crates/egui_demo_lib",
"crates/egui_extras",
"crates/egui_glow",
"crates/egui_plot",
"crates/egui-wgpu",
"crates/egui-winit",
"crates/egui",
@ -58,7 +57,6 @@ emath = { version = "0.28.1", path = "crates/emath", default-features = false }
ecolor = { version = "0.28.1", path = "crates/ecolor", default-features = false }
epaint = { version = "0.28.1", path = "crates/epaint", default-features = false }
egui = { version = "0.28.1", path = "crates/egui", default-features = false }
egui_plot = { version = "0.28.1", path = "crates/egui_plot", default-features = false }
egui-winit = { version = "0.28.1", path = "crates/egui-winit", default-features = false }
egui_extras = { version = "0.28.1", path = "crates/egui_extras", default-features = false }
egui-wgpu = { version = "0.28.1", path = "crates/egui-wgpu", default-features = false }

View File

@ -44,6 +44,7 @@ We don't update the MSRV in a patch release, unless we really, really need to.
- check the in-browser profiler
* [ ] check the color test
* [ ] update `eframe_template` and test
* [ ] update `egui_plot` and test
* [ ] update `egui_tiles` and test
* [ ] test with Rerun
* [ ] `./scripts/check.sh`
@ -76,7 +77,6 @@ I usually do this all on the `master` branch, but doing it in a release branch i
(cd crates/ecolor && cargo publish --quiet) && echo "✅ ecolor"
(cd crates/epaint && cargo publish --quiet) && echo "✅ epaint"
(cd crates/egui && cargo publish --quiet) && echo "✅ egui"
(cd crates/egui_plot && cargo publish --quiet) && echo "✅ egui_plot"
(cd crates/egui-winit && cargo publish --quiet) && echo "✅ egui-winit"
(cd crates/egui_extras && cargo publish --quiet) && echo "✅ egui_extras"
(cd crates/egui-wgpu && cargo publish --quiet) && echo "✅ egui-wgpu"
@ -94,4 +94,5 @@ I usually do this all on the `master` branch, but doing it in a release branch i
## After release
* [ ] publish new `eframe_template`
* [ ] publish new `egui_plot`
* [ ] publish new `egui_tiles`

View File

@ -34,7 +34,7 @@ default = []
chrono = ["egui_extras/datepicker", "dep:chrono"]
## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["egui/serde", "egui_plot/serde", "dep:serde", "egui_extras/serde"]
serde = ["egui/serde", "dep:serde", "egui_extras/serde"]
## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect).
syntect = ["egui_extras/syntect"]
@ -43,7 +43,6 @@ syntect = ["egui_extras/syntect"]
[dependencies]
egui = { workspace = true, default-features = false, features = ["color-hex"] }
egui_extras = { workspace = true, features = ["default"] }
egui_plot = { workspace = true, features = ["default"] }
log.workspace = true
unicode_names2 = { version = "0.6.0", default-features = false } # this old version has fewer dependencies

View File

@ -1,51 +1,6 @@
use egui::Vec2b;
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
enum Plot {
Sin,
Bell,
Sigmoid,
}
fn gaussian(x: f64) -> f64 {
let var: f64 = 2.0;
f64::exp(-(x / var).powi(2)) / (var * f64::sqrt(std::f64::consts::TAU))
}
fn sigmoid(x: f64) -> f64 {
-1.0 + 2.0 / (1.0 + f64::exp(-x))
}
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ContextMenus {
plot: Plot,
show_axes: Vec2b,
allow_drag: bool,
allow_zoom: bool,
allow_scroll: bool,
center_x_axis: bool,
center_y_axis: bool,
width: f32,
height: f32,
}
impl Default for ContextMenus {
fn default() -> Self {
Self {
plot: Plot::Sin,
show_axes: Vec2b::TRUE,
allow_drag: true,
allow_zoom: true,
allow_scroll: true,
center_x_axis: false,
center_y_axis: false,
width: 400.0,
height: 200.0,
}
}
}
pub struct ContextMenus {}
impl crate::Demo for ContextMenus {
fn name(&self) -> &'static str {
@ -66,8 +21,10 @@ impl crate::View for ContextMenus {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.menu_button("Click for menu", Self::nested_menus);
ui.button("Right-click for menu")
.context_menu(Self::nested_menus);
if ui.ctx().is_context_menu_open() {
ui.label("Context menu is open");
} else {
@ -75,49 +32,6 @@ impl crate::View for ContextMenus {
}
});
ui.separator();
ui.label("Right-click plot to edit it!");
ui.horizontal(|ui| {
self.example_plot(ui).context_menu(|ui| {
ui.menu_button("Plot", |ui| {
if ui.radio_value(&mut self.plot, Plot::Sin, "Sin").clicked()
|| ui
.radio_value(&mut self.plot, Plot::Bell, "Gaussian")
.clicked()
|| ui
.radio_value(&mut self.plot, Plot::Sigmoid, "Sigmoid")
.clicked()
{
ui.close_menu();
}
});
egui::Grid::new("button_grid").show(ui, |ui| {
ui.add(
egui::DragValue::new(&mut self.width)
.range(0.0..=f32::INFINITY)
.speed(1.0)
.prefix("Width: "),
);
ui.add(
egui::DragValue::new(&mut self.height)
.range(0.0..=f32::INFINITY)
.speed(1.0)
.prefix("Height: "),
);
ui.end_row();
ui.checkbox(&mut self.show_axes[0], "x-Axis");
ui.checkbox(&mut self.show_axes[1], "y-Axis");
ui.end_row();
if ui.checkbox(&mut self.allow_drag, "Drag").changed()
|| ui.checkbox(&mut self.allow_zoom, "Zoom").changed()
|| ui.checkbox(&mut self.allow_scroll, "Scroll").changed()
{
ui.close_menu();
}
});
});
});
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});
@ -125,36 +39,6 @@ impl crate::View for ContextMenus {
}
impl ContextMenus {
fn example_plot(&self, ui: &mut egui::Ui) -> egui::Response {
use egui_plot::{Line, PlotPoints};
let n = 128;
let line = Line::new(
(0..=n)
.map(|i| {
use std::f64::consts::TAU;
let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU);
match self.plot {
Plot::Sin => [x, x.sin()],
Plot::Bell => [x, 10.0 * gaussian(x)],
Plot::Sigmoid => [x, sigmoid(x)],
}
})
.collect::<PlotPoints>(),
);
egui_plot::Plot::new("example_plot")
.show_axes(self.show_axes)
.allow_drag(self.allow_drag)
.allow_zoom(self.allow_zoom)
.allow_scroll(self.allow_scroll)
.center_x_axis(self.center_x_axis)
.center_x_axis(self.center_y_axis)
.width(self.width)
.height(self.height)
.data_aspect(1.0)
.show(ui, |plot_ui| plot_ui.line(line))
.response
}
fn nested_menus(ui: &mut egui::Ui) {
ui.set_max_width(200.0); // To make sure we wrap long text

View File

@ -36,7 +36,6 @@ impl Default for Demos {
Box::<super::painting::Painting>::default(),
Box::<super::pan_zoom::PanZoom>::default(),
Box::<super::panels::Panels>::default(),
Box::<super::plot_demo::PlotDemo>::default(),
Box::<super::scrolling::Scrolling>::default(),
Box::<super::sliders::Sliders>::default(),
Box::<super::strip_demo::StripDemo>::default(),

View File

@ -22,7 +22,6 @@ pub mod painting;
pub mod pan_zoom;
pub mod panels;
pub mod password;
pub mod plot_demo;
pub mod scrolling;
pub mod sliders;
pub mod strip_demo;

File diff suppressed because it is too large Load Diff

View File

@ -248,10 +248,6 @@ impl WidgetGallery {
});
ui.end_row();
ui.add(doc_link_label_with_crate("egui_plot", "Plot", "plot"));
example_plot(ui);
ui.end_row();
ui.hyperlink_to(
"Custom widget:",
super::toggle_switch::url_to_file_source_code(),
@ -264,25 +260,6 @@ impl WidgetGallery {
}
}
fn example_plot(ui: &mut egui::Ui) -> egui::Response {
use egui_plot::{Line, PlotPoints};
let n = 128;
let line_points: PlotPoints = (0..=n)
.map(|i| {
use std::f64::consts::TAU;
let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU);
[x, x.sin()]
})
.collect();
let line = Line::new(line_points);
egui_plot::Plot::new("example_plot")
.height(32.0)
.show_axes(false)
.data_aspect(1.0)
.show(ui, |plot_ui| plot_ui.line(line))
.response
}
fn doc_link_label<'a>(title: &'a str, search_term: &'a str) -> impl egui::Widget + 'a {
doc_link_label_with_crate("egui", title, search_term)
}

View File

@ -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)!)

View File

@ -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 }

View File

@ -1,11 +1 @@
# egui_plot
[![Latest version](https://img.shields.io/crates/v/egui_plot.svg)](https://crates.io/crates/egui_plot)
[![Documentation](https://docs.rs/egui_plot/badge.svg)](https://docs.rs/egui_plot)
[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)
![MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
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>

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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())
}

View File

@ -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,
}

View File

@ -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

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}
}
}

View File

@ -1,23 +0,0 @@
[package]
name = "custom_plot_manipulation"
version = "0.1.0"
authors = ["Ygor Souza <ygor.souza@protonmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.76"
publish = false
[lints]
workspace = true
[dependencies]
eframe = { workspace = true, features = [
"default",
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] }
egui_plot.workspace = true
env_logger = { version = "0.10", default-features = false, features = [
"auto-color",
"humantime",
] }

View File

@ -1,7 +0,0 @@
Example how to use raw input events to implement alternative controls to pan and zoom the plot
```sh
cargo run -p custom_plot_manipulation
```
![](screenshot.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,129 +0,0 @@
//! This example shows how to implement custom gestures to pan and zoom in the plot
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
#![allow(rustdoc::missing_crate_level_docs)] // it's an example
use eframe::egui::{self, DragValue, Event, Vec2};
use egui_plot::{Legend, Line, PlotPoints};
fn main() -> eframe::Result {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions::default();
eframe::run_native(
"Plot",
options,
Box::new(|_cc| Ok(Box::<PlotExample>::default())),
)
}
struct PlotExample {
lock_x: bool,
lock_y: bool,
ctrl_to_zoom: bool,
shift_to_horizontal: bool,
zoom_speed: f32,
scroll_speed: f32,
}
impl Default for PlotExample {
fn default() -> Self {
Self {
lock_x: false,
lock_y: false,
ctrl_to_zoom: false,
shift_to_horizontal: false,
zoom_speed: 1.0,
scroll_speed: 1.0,
}
}
}
impl eframe::App for PlotExample {
fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
egui::SidePanel::left("options").show(ctx, |ui| {
ui.checkbox(&mut self.lock_x, "Lock x axis").on_hover_text("Check to keep the X axis fixed, i.e., pan and zoom will only affect the Y axis");
ui.checkbox(&mut self.lock_y, "Lock y axis").on_hover_text("Check to keep the Y axis fixed, i.e., pan and zoom will only affect the X axis");
ui.checkbox(&mut self.ctrl_to_zoom, "Ctrl to zoom").on_hover_text("If unchecked, the behavior of the Ctrl key is inverted compared to the default controls\ni.e., scrolling the mouse without pressing any keys zooms the plot");
ui.checkbox(&mut self.shift_to_horizontal, "Shift for horizontal scroll").on_hover_text("If unchecked, the behavior of the shift key is inverted compared to the default controls\ni.e., hold to scroll vertically, release to scroll horizontally");
ui.horizontal(|ui| {
ui.add(
DragValue::new(&mut self.zoom_speed)
.range(0.1..=2.0)
.speed(0.1),
);
ui.label("Zoom speed").on_hover_text("How fast to zoom in and out with the mouse wheel");
});
ui.horizontal(|ui| {
ui.add(
DragValue::new(&mut self.scroll_speed)
.range(0.1..=100.0)
.speed(0.1),
);
ui.label("Scroll speed").on_hover_text("How fast to pan with the mouse wheel");
});
});
egui::CentralPanel::default().show(ctx, |ui| {
let (scroll, pointer_down, modifiers) = ui.input(|i| {
let scroll = i.events.iter().find_map(|e| match e {
Event::MouseWheel {
unit: _,
delta,
modifiers: _,
} => Some(*delta),
_ => None,
});
(scroll, i.pointer.primary_down(), i.modifiers)
});
ui.label("This example shows how to use raw input events to implement different plot controls than the ones egui provides by default, e.g., default to zooming instead of panning when the Ctrl key is not pressed, or controlling much it zooms with each mouse wheel step.");
egui_plot::Plot::new("plot")
.allow_zoom(false)
.allow_drag(false)
.allow_scroll(false)
.legend(Legend::default())
.show(ui, |plot_ui| {
if let Some(mut scroll) = scroll {
if modifiers.ctrl == self.ctrl_to_zoom {
scroll = Vec2::splat(scroll.x + scroll.y);
let mut zoom_factor = Vec2::from([
(scroll.x * self.zoom_speed / 10.0).exp(),
(scroll.y * self.zoom_speed / 10.0).exp(),
]);
if self.lock_x {
zoom_factor.x = 1.0;
}
if self.lock_y {
zoom_factor.y = 1.0;
}
plot_ui.zoom_bounds_around_hovered(zoom_factor);
} else {
if modifiers.shift == self.shift_to_horizontal {
scroll = Vec2::new(scroll.y, scroll.x);
}
if self.lock_x {
scroll.x = 0.0;
}
if self.lock_y {
scroll.y = 0.0;
}
let delta_pos = self.scroll_speed * scroll;
plot_ui.translate_bounds(delta_pos);
}
}
if plot_ui.response().hovered() && pointer_down {
let mut pointer_translate = -plot_ui.pointer_coordinate_drag_delta();
if self.lock_x {
pointer_translate.x = 0.0;
}
if self.lock_y {
pointer_translate.y = 0.0;
}
plot_ui.translate_bounds(pointer_translate);
}
let sine_points = PlotPoints::from_explicit_callback(|x| x.sin(), .., 5000);
plot_ui.line(Line::new(sine_points).name("Sine"));
});
});
}
}

View File

@ -1,24 +0,0 @@
[package]
name = "save_plot"
version = "0.1.0"
authors = ["hacknus <l_stoeckli@bluewin.ch>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.76"
publish = false
[lints]
workspace = true
[dependencies]
eframe = { workspace = true, features = [
"default",
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] }
egui_plot.workspace = true
image = { workspace = true, features = ["png"] }
rfd = "0.13.0"
env_logger = { version = "0.10", default-features = false, features = [
"auto-color",
"humantime",
] }

View File

@ -1,7 +0,0 @@
This example shows that you can save a plot in egui as a png.
```sh
cargo run -p save_plot
```
![](screenshot.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -1,75 +0,0 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
#![allow(rustdoc::missing_crate_level_docs)] // it's an example
use eframe::egui;
use egui_plot::{Legend, Line, Plot, PlotPoints};
fn main() -> eframe::Result {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([350.0, 200.0]),
..Default::default()
};
eframe::run_native(
"My egui App with a plot",
options,
Box::new(|_cc| Ok(Box::<MyApp>::default())),
)
}
#[derive(Default)]
struct MyApp {}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let mut plot_rect = None;
egui::CentralPanel::default().show(ctx, |ui| {
if ui.button("Save Plot").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot);
}
let my_plot = Plot::new("My Plot").legend(Legend::default());
// let's create a dummy line in the plot
let graph: Vec<[f64; 2]> = vec![[0.0, 1.0], [2.0, 3.0], [3.0, 2.0]];
let inner = my_plot.show(ui, |plot_ui| {
plot_ui.line(Line::new(PlotPoints::from(graph)).name("curve"));
});
// Remember the position of the plot
plot_rect = Some(inner.response.rect);
});
// Check for returned screenshot:
let screenshot = ctx.input(|i| {
for event in &i.raw.events {
if let egui::Event::Screenshot { image, .. } = event {
return Some(image.clone());
}
}
None
});
if let (Some(screenshot), Some(plot_location)) = (screenshot, plot_rect) {
if let Some(mut path) = rfd::FileDialog::new().save_file() {
path.set_extension("png");
// for a full size application, we should put this in a different thread,
// so that the GUI doesn't lag during saving
let pixels_per_point = ctx.pixels_per_point();
let plot = screenshot.region(&plot_location, Some(pixels_per_point));
// save the plot to png
image::save_buffer(
&path,
plot.as_raw(),
plot.width() as u32,
plot.height() as u32,
image::ColorType::Rgba8,
)
.unwrap();
eprintln!("Image saved to {path:?}.");
}
}
}
}

View File

@ -223,7 +223,6 @@ def main() -> None:
"ecolor",
"eframe",
"egui_extras",
"egui_plot",
"egui_glow",
"egui-wgpu",
"egui-winit",