The app has crashed. See the developer console for details.
"); 57 | panic!("Failed to start eframe: {e:?}"); 58 | } 59 | } 60 | } 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /egui_plot/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Simple plotting library for [`egui`](https://github.com/emilk/egui). 2 | //! 3 | //! Check out [`Plot`] for how to get started. 4 | //! 5 | //! [**Looking for maintainer!**](https://github.com/emilk/egui/issues/4705) 6 | //! 7 | //! ## Feature flags 8 | #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] 9 | //! 10 | 11 | mod aesthetics; 12 | mod axis; 13 | mod bounds; 14 | mod colors; 15 | mod cursor; 16 | mod data; 17 | mod grid; 18 | mod items; 19 | mod label; 20 | mod math; 21 | mod memory; 22 | mod overlays; 23 | mod placement; 24 | mod plot; 25 | mod rect_elem; 26 | mod utils; 27 | 28 | pub use crate::aesthetics::LineStyle; 29 | pub use crate::aesthetics::MarkerShape; 30 | pub use crate::aesthetics::Orientation; 31 | pub use crate::axis::Axis; 32 | pub use crate::axis::AxisHints; 33 | pub use crate::axis::PlotTransform; 34 | pub use crate::bounds::PlotBounds; 35 | pub use crate::bounds::PlotPoint; 36 | pub use crate::colors::color_from_strength; 37 | pub use crate::cursor::Cursor; 38 | pub use crate::data::PlotPoints; 39 | pub use crate::grid::GridInput; 40 | pub use crate::grid::GridMark; 41 | pub use crate::grid::log_grid_spacer; 42 | pub use crate::grid::uniform_grid_spacer; 43 | pub use crate::items::Arrows; 44 | pub use crate::items::Bar; 45 | pub use crate::items::BarChart; 46 | pub use crate::items::BoxElem; 47 | pub use crate::items::BoxPlot; 48 | pub use crate::items::BoxSpread; 49 | pub use crate::items::ClosestElem; 50 | pub use crate::items::FilledArea; 51 | pub use crate::items::HLine; 52 | pub use crate::items::Heatmap; 53 | pub use crate::items::Line; 54 | pub use crate::items::PlotConfig; 55 | pub use crate::items::PlotGeometry; 56 | pub use crate::items::PlotImage; 57 | pub use crate::items::PlotItem; 58 | pub use crate::items::PlotItemBase; 59 | pub use crate::items::Points; 60 | pub use crate::items::Polygon; 61 | pub use crate::items::Span; 62 | pub use crate::items::Text; 63 | pub use crate::items::VLine; 64 | pub use crate::label::LabelFormatter; 65 | pub use crate::label::default_label_formatter; 66 | pub use crate::label::format_number; 67 | pub use crate::memory::PlotMemory; 68 | pub use crate::overlays::ColorConflictHandling; 69 | pub use crate::overlays::CoordinatesFormatter; 70 | pub use crate::overlays::Legend; 71 | pub use crate::placement::Corner; 72 | pub use crate::placement::HPlacement; 73 | pub use crate::placement::Placement; 74 | pub use crate::placement::VPlacement; 75 | pub use crate::plot::Plot; 76 | pub use crate::plot::PlotResponse; 77 | pub use crate::plot::PlotUi; 78 | -------------------------------------------------------------------------------- /egui_plot/src/memory.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use egui::Context; 4 | use egui::Id; 5 | use egui::Pos2; 6 | use egui::Vec2b; 7 | 8 | use crate::axis::PlotTransform; 9 | use crate::bounds::PlotBounds; 10 | 11 | /// Information about the plot that has to persist between frames. 12 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 13 | #[derive(Clone)] 14 | pub struct PlotMemory { 15 | /// Indicates if the plot uses automatic bounds. 16 | /// 17 | /// This is set to `false` whenever the user modifies 18 | /// the bounds, for example by moving or zooming. 19 | pub auto_bounds: Vec2b, 20 | 21 | /// Hovered legend item if any. 22 | pub hovered_legend_item: Option` and `` blocks as well as Markdown code blocks.
29 | include_verbatim = true
30 |
31 | # Proceed for server connections considered insecure (invalid TLS).
32 | insecure = true
33 |
34 | # Maximum number of allowed retries before a link is declared dead.
35 | max_retries = 4
36 |
37 | # Wait time between attempts in seconds.
38 | retry_wait_time = 2
39 |
40 | # Comma-separated list of accepted status codes for valid links.
41 | accept = [
42 | "100..=103", # Informational codes.
43 | "200..=299", # Success codes.
44 | "429", # Too many requests. This is practically never a sign of a broken link.
45 | ]
46 |
47 |
48 | # Exclude URLs and mail addresses from checking (supports regex).
49 | exclude = [
50 | # Strings with replacements.
51 | '/__VIEWER_VERSION__/', # Replacement variable __VIEWER_VERSION__.
52 | '/\$', # Replacement variable $.
53 | '/GIT_HASH/', # Replacement variable GIT_HASH.
54 | '\{\}', # Ignore links with string interpolation.
55 | '\$relpath\^', # Relative paths as used by rerun_cpp's doc header.
56 | '%7B.+%7D', # Ignore strings that look like ready to use links but contain a replacement strings. The URL escaping is for '{.+}' (this seems to be needed for html embedded urls since lychee assumes they use this encoding).
57 | '%7B%7D', # Ignore links with string interpolation, escaped variant.
58 |
59 | # Local links that require further setup.
60 | 'http://127.0.0.1',
61 | 'http://localhost',
62 | 'recording:/', # rrd recording link.
63 | 'ws:/',
64 | 're_viewer.js', # Build artifact that html is linking to.
65 |
66 | # Api endpoints.
67 | 'https://fonts.googleapis.com/', # Font API entrypoint, not a link.
68 | 'https://fonts.gstatic.com/', # Font API entrypoint, not a link.
69 | 'https://tel.rerun.io/', # Analytics endpoint.
70 |
71 | # Avoid rate limiting.
72 | 'https://crates.io/crates/.*', # Avoid crates.io rate-limiting
73 | 'https://github.com/Bromeon',
74 | 'https://github.com/EmbersArc',
75 | 'https://github.com/emilk',
76 | 'https://github.com/emilk/egui_plot/commit/\.*', # Ignore links to our own commits (typically in changelog).
77 | 'https://github.com/emilk/egui_plot/pull/\.*', # Ignore links to our own pull requests (typically in changelog).
78 | ]
79 |
--------------------------------------------------------------------------------
/deny.toml:
--------------------------------------------------------------------------------
1 | # Copied from https://github.com/rerun-io/rerun_template
2 | #
3 | # https://github.com/EmbarkStudios/cargo-deny
4 | #
5 | # cargo-deny checks our dependency tree for copy-left licenses,
6 | # duplicate dependencies, and rustsec advisories (https://rustsec.org/advisories).
7 | #
8 | # Install: `cargo install cargo-deny`
9 | # Check: `cargo deny check`.
10 |
11 |
12 | # Note: running just `cargo deny check` without a `--target` can result in
13 | # false positives due to https://github.com/EmbarkStudios/cargo-deny/issues/324
14 | [graph]
15 | targets = [
16 | { triple = "aarch64-apple-darwin" },
17 | { triple = "i686-pc-windows-gnu" },
18 | { triple = "i686-pc-windows-msvc" },
19 | { triple = "i686-unknown-linux-gnu" },
20 | { triple = "wasm32-unknown-unknown" },
21 | { triple = "x86_64-apple-darwin" },
22 | { triple = "x86_64-pc-windows-gnu" },
23 | { triple = "x86_64-pc-windows-msvc" },
24 | { triple = "x86_64-unknown-linux-gnu" },
25 | { triple = "x86_64-unknown-linux-musl" },
26 | { triple = "x86_64-unknown-redox" },
27 | ]
28 | all-features = true
29 |
30 |
31 | [advisories]
32 | version = 2
33 | ignore = [
34 | "RUSTSEC-2024-0436", # https://rustsec.org/advisories/RUSTSEC-2024-0436 - paste is unmaintained - https://github.com/dtolnay/paste
35 | ]
36 |
37 |
38 | [bans]
39 | multiple-versions = "deny"
40 | wildcards = "deny"
41 | deny = [
42 | { name = "cmake", reason = "It has hurt me too much" },
43 | { name = "openssl-sys", reason = "Use rustls" },
44 | { name = "openssl", reason = "Use rustls" },
45 | ]
46 |
47 | skip = []
48 | skip-tree = [{ name = "eframe", reason = "Dev-dependency" }]
49 |
50 |
51 | [licenses]
52 | version = 2
53 | private = { ignore = true }
54 | confidence-threshold = 0.93 # We want really high confidence when inferring licenses from text
55 | allow = [
56 | "Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html
57 | "Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)
58 | "BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd)
59 | "BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised)
60 | "BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained
61 | "CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/
62 | "ISC", # https://www.tldrlegal.com/license/isc-license
63 | # "MIT-0", # https://choosealicense.com/licenses/mit-0/
64 | "MIT", # https://tldrlegal.com/license/mit-license
65 | "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux.
66 | "OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html
67 | # "OpenSSL", # https://www.openssl.org/source/license.html - used on Linux
68 | "Ubuntu-font-1.0", # https://ubuntu.com/legal/font-licence
69 | # "Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html
70 | "Unicode-3.0", # https://spdx.org/licenses/Unicode-3.0.html
71 | "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib)
72 | ]
73 | exceptions = []
74 |
75 | [[licenses.clarify]]
76 | name = "webpki"
77 | expression = "ISC"
78 | license-files = [{ path = "LICENSE", hash = 0x001c7e6c }]
79 |
80 | [[licenses.clarify]]
81 | name = "ring"
82 | expression = "MIT AND ISC AND OpenSSL"
83 | license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }]
84 |
85 |
86 | [sources]
87 | unknown-registry = "deny"
88 | unknown-git = "deny"
89 |
90 | [sources.allow-org]
91 | github = ["emilk", "rerun-io"]
92 |
--------------------------------------------------------------------------------
/examples/interaction/src/app.rs:
--------------------------------------------------------------------------------
1 | use eframe::egui;
2 | use eframe::egui::Response;
3 | use egui_plot::Line;
4 | use egui_plot::Plot;
5 | use egui_plot::PlotPoint;
6 | use egui_plot::PlotPoints;
7 | use egui_plot::PlotResponse;
8 |
9 | #[derive(Default)]
10 | pub struct InteractionExample {
11 | last_bounds: Option,
12 | last_screen_pos: egui::Pos2,
13 | last_pointer_coordinate: Option,
14 | last_pointer_drag_delta: egui::Vec2,
15 | last_hovered: bool,
16 | last_hovered_item: Option,
17 | }
18 |
19 | impl InteractionExample {
20 | pub fn show_plot(&mut self, ui: &mut egui::Ui) -> Response {
21 | let id = ui.make_persistent_id("interaction_demo");
22 |
23 | let plot = Plot::new("interaction_demo").id(id).height(300.0);
24 |
25 | let PlotResponse {
26 | response,
27 | inner: (screen_pos, pointer_coordinate, pointer_coordinate_drag_delta, bounds, hovered),
28 | hovered_plot_item,
29 | ..
30 | } = plot.show(ui, |plot_ui| {
31 | plot_ui.line(
32 | Line::new("sin", PlotPoints::from_explicit_callback(move |x| x.sin(), .., 100))
33 | .color(egui::Color32::RED),
34 | );
35 | plot_ui.line(
36 | Line::new("cos", PlotPoints::from_explicit_callback(move |x| x.cos(), .., 100))
37 | .color(egui::Color32::BLUE),
38 | );
39 |
40 | (
41 | plot_ui.screen_from_plot(PlotPoint::new(0.0, 0.0)),
42 | plot_ui.pointer_coordinate(),
43 | plot_ui.pointer_coordinate_drag_delta(),
44 | plot_ui.plot_bounds(),
45 | plot_ui.response().hovered(),
46 | )
47 | });
48 |
49 | // Store for display in controls
50 | self.last_bounds = Some(bounds);
51 | self.last_screen_pos = screen_pos;
52 | self.last_pointer_coordinate = pointer_coordinate;
53 | self.last_pointer_drag_delta = pointer_coordinate_drag_delta;
54 | self.last_hovered = hovered;
55 | self.last_hovered_item = hovered_plot_item;
56 |
57 | response
58 | }
59 |
60 | pub fn show_controls(&self, ui: &mut egui::Ui) -> Response {
61 | if let Some(bounds) = &self.last_bounds {
62 | ui.label(format!(
63 | "plot bounds: min: {:.02?}, max: {:.02?}",
64 | bounds.min(),
65 | bounds.max()
66 | ));
67 | }
68 |
69 | ui.label(format!(
70 | "origin in screen coordinates: x: {:.02}, y: {:.02}",
71 | self.last_screen_pos.x, self.last_screen_pos.y
72 | ));
73 | ui.label(format!("plot hovered: {}", self.last_hovered));
74 | let coordinate_text = if let Some(coordinate) = self.last_pointer_coordinate {
75 | format!("x: {:.02}, y: {:.02}", coordinate.x, coordinate.y)
76 | } else {
77 | "None".to_owned()
78 | };
79 | ui.label(format!("pointer coordinate: {coordinate_text}"));
80 | let coordinate_text = format!(
81 | "x: {:.02}, y: {:.02}",
82 | self.last_pointer_drag_delta.x, self.last_pointer_drag_delta.y
83 | );
84 | ui.label(format!("pointer coordinate drag delta: {coordinate_text}"));
85 |
86 | let hovered_item = if self.last_hovered_item == Some(egui::Id::new("sin")) {
87 | "red sin"
88 | } else if self.last_hovered_item == Some(egui::Id::new("cos")) {
89 | "blue cos"
90 | } else {
91 | "none"
92 | };
93 | ui.label(format!("hovered plot item: {hovered_item}"))
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/egui_plot/src/utils.rs:
--------------------------------------------------------------------------------
1 | use egui::Color32;
2 | use egui::FontId;
3 | use egui::Painter;
4 |
5 | // Utility function to find a truncated candidate to fit a text label into a
6 | // given width. If the width is large enough for the text, a string with the
7 | // full text will be returned. If the width is too small to display the full
8 | // text, it finds the longest text with "..." appended at the end that we can
9 | // display within the given width. If the width is too small to display the
10 | // first character followed by "..." then we return an empty string.
11 | pub(crate) fn find_name_candidate(name: &str, width: f32, painter: &Painter, font_id: &FontId) -> String {
12 | let galley = painter.layout_no_wrap(name.to_owned(), font_id.clone(), Color32::BLACK);
13 |
14 | if galley.size().x <= width || name.is_empty() {
15 | return name.to_owned();
16 | }
17 |
18 | // If we don't have enough space for the name to be displayed in the span, we
19 | // search for the longest candidate that fits, where a candidate is a
20 | // truncated version of the name followed by "...".
21 | let chars: Vec = name.chars().collect();
22 |
23 | // First test the minimum candidate which is the first letter followed by "..."
24 | let mut min_candidate = chars[0].to_string();
25 | min_candidate.push_str("...");
26 | let galley = painter.layout_no_wrap(min_candidate.clone(), font_id.clone(), Color32::BLACK);
27 | if galley.size().x > width {
28 | return String::new();
29 | }
30 |
31 | // Then do a binary search to find the longest possible candidate
32 | let mut low = 1;
33 | let mut high = chars.len();
34 | let mut best = String::new();
35 |
36 | while low <= high {
37 | let mid = usize::midpoint(low, high);
38 | let mut candidate: String = chars[..mid].iter().collect();
39 | candidate.push_str("...");
40 |
41 | let candidate_width = painter
42 | .layout_no_wrap(candidate.clone(), font_id.clone(), Color32::BLACK)
43 | .size()
44 | .x;
45 |
46 | if candidate_width <= width {
47 | best = candidate;
48 | low = mid + 1;
49 | } else {
50 | high = mid.saturating_sub(1);
51 | if high == 0 {
52 | break;
53 | }
54 | }
55 | }
56 |
57 | best
58 | }
59 |
60 | /// Initialize logging so that the testing framework can capture log output.
61 | ///
62 | /// Call this at the top of a test function to see log output from that test.
63 | /// The logging output will only be shown when the following conditions are met:
64 | /// - The test fails
65 | /// - The `RUST_LOG` environment variable is set to a level at or below the message level
66 | ///
67 | /// When running a specific test:
68 | /// ```sh
69 | /// RUST_LOG=info cargo test auto_bounds_true
70 | /// ```
71 | ///
72 | /// If the something causes the test to panic so hard that it never shows logging output,
73 | /// you can use `--nocapture` to see log output as it happens:
74 | /// ```sh
75 | /// RUST_LOG=info cargo test auto_bounds_true -- --nocapture
76 | /// ```
77 | #[cfg(test)]
78 | pub(crate) fn init_test_logger() {
79 | use std::io::Write as _;
80 | let _result: Result<(), log::SetLoggerError> = env_logger::builder()
81 | .is_test(true)
82 | .format(|buf, record| {
83 | let level_style = buf.default_level_style(record.level());
84 | writeln!(
85 | buf,
86 | "[{level_style}{}{level_style:#} {}:{}] {}",
87 | record.level(),
88 | record.file().unwrap_or("unknown"),
89 | record.line().unwrap_or(0),
90 | record.args()
91 | )
92 | })
93 | .try_init();
94 | }
95 |
--------------------------------------------------------------------------------
/examples/items/src/app.rs:
--------------------------------------------------------------------------------
1 | use std::f64::consts::TAU;
2 |
3 | use eframe::egui;
4 | use eframe::egui::Response;
5 | use egui::vec2;
6 | use egui_plot::Arrows;
7 | use egui_plot::HLine;
8 | use egui_plot::Legend;
9 | use egui_plot::Line;
10 | use egui_plot::Plot;
11 | use egui_plot::PlotImage;
12 | use egui_plot::PlotPoint;
13 | use egui_plot::PlotPoints;
14 | use egui_plot::Points;
15 | use egui_plot::Polygon;
16 | use egui_plot::Text;
17 | use egui_plot::VLine;
18 |
19 | #[derive(Default)]
20 | pub struct ItemsExample {
21 | texture: Option,
22 | }
23 |
24 | impl ItemsExample {
25 | pub fn show_plot(&mut self, ui: &mut egui::Ui) -> Response {
26 | let n = 100;
27 | let mut sin_values: Vec<_> = (0..=n)
28 | .map(|i| egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU))
29 | .map(|i| [i, i.sin()])
30 | .collect();
31 |
32 | let line = Line::new("sin(x)", sin_values.split_off(n / 2)).fill(-1.5);
33 | let polygon = Polygon::new(
34 | "polygon",
35 | PlotPoints::from_parametric_callback(
36 | |t| (4.0 * t.sin() + 2.0 * t.cos(), 4.0 * t.cos() + 2.0 * t.sin()),
37 | 0.0..TAU,
38 | 100,
39 | ),
40 | );
41 | let points = Points::new("sin(x)", sin_values).stems(-1.5).radius(1.0);
42 |
43 | let arrows = {
44 | let pos_radius = 8.0;
45 | let tip_radius = 7.0;
46 | let arrow_origins =
47 | PlotPoints::from_parametric_callback(|t| (pos_radius * t.sin(), pos_radius * t.cos()), 0.0..TAU, 36);
48 | let arrow_tips =
49 | PlotPoints::from_parametric_callback(|t| (tip_radius * t.sin(), tip_radius * t.cos()), 0.0..TAU, 36);
50 | Arrows::new("arrows", arrow_origins, arrow_tips)
51 | };
52 |
53 | let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
54 | ui.ctx()
55 | .load_texture("plot_demo", egui::ColorImage::example(), Default::default())
56 | });
57 | let image = PlotImage::new(
58 | "image",
59 | texture,
60 | PlotPoint::new(0.0, 10.0),
61 | 5.0 * vec2(texture.aspect_ratio(), 1.0),
62 | );
63 |
64 | let plot = Plot::new("items_demo")
65 | .legend(
66 | Legend::default()
67 | .position(egui_plot::Corner::RightBottom)
68 | .title("Items"),
69 | )
70 | .show_x(false)
71 | .show_y(false)
72 | .data_aspect(1.0);
73 | plot.show(ui, |plot_ui| {
74 | plot_ui.hline(HLine::new("Lines horizontal", 9.0));
75 | plot_ui.hline(HLine::new("Lines horizontal", -9.0));
76 | plot_ui.vline(VLine::new("Lines vertical", 9.0));
77 | plot_ui.vline(VLine::new("Lines vertical", -9.0));
78 | plot_ui.line(line.name("Line with fill").id("line_with_fill"));
79 | plot_ui.polygon(polygon.name("Convex polygon").id("convex_polygon"));
80 | plot_ui.points(points.name("Points with stems").id("points_with_stems"));
81 | plot_ui.text(Text::new("Text", PlotPoint::new(-3.0, -3.0), "wow").id("text0"));
82 | plot_ui.text(Text::new("Text", PlotPoint::new(-2.0, 2.5), "so graph").id("text1"));
83 | plot_ui.text(Text::new("Text", PlotPoint::new(3.0, 3.0), "much color").id("text2"));
84 | plot_ui.text(Text::new("Text", PlotPoint::new(2.5, -2.0), "such plot").id("text3"));
85 | plot_ui.image(image.name("Image"));
86 | plot_ui.arrows(arrows.name("Arrows"));
87 | })
88 | .response
89 | }
90 |
91 | #[expect(clippy::unused_self, reason = "required by the example template")]
92 | pub fn show_controls(&self, ui: &mut egui::Ui) -> Response {
93 | // No controls for this example
94 | ui.scope(|_ui| {}).response
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/examples/heatmap/src/app.rs:
--------------------------------------------------------------------------------
1 | use eframe::egui;
2 | use eframe::egui::Color32;
3 | use eframe::egui::Response;
4 | use eframe::egui::vec2;
5 | use egui_plot::Legend;
6 | use egui_plot::Plot;
7 |
8 | pub const TURBO_COLORMAP: [Color32; 10] = [
9 | Color32::from_rgb(48, 18, 59),
10 | Color32::from_rgb(35, 106, 141),
11 | Color32::from_rgb(30, 160, 140),
12 | Color32::from_rgb(88, 200, 98),
13 | Color32::from_rgb(164, 223, 39),
14 | Color32::from_rgb(228, 223, 14),
15 | Color32::from_rgb(250, 187, 13),
16 | Color32::from_rgb(246, 135, 8),
17 | Color32::from_rgb(213, 68, 2),
18 | Color32::from_rgb(122, 4, 2),
19 | ];
20 |
21 | pub struct HeatmapDemo {
22 | tick: f64,
23 | animate: bool,
24 | show_labels: bool,
25 | palette: Vec,
26 | rows: usize,
27 | cols: usize,
28 | }
29 |
30 | impl Default for HeatmapDemo {
31 | fn default() -> Self {
32 | Self {
33 | tick: 0.0,
34 | animate: false,
35 | show_labels: true,
36 | palette: TURBO_COLORMAP.to_vec(),
37 | rows: 5,
38 | cols: 5,
39 | }
40 | }
41 | }
42 |
43 | impl HeatmapDemo {
44 | pub fn show_controls(&mut self, ui: &mut egui::Ui) -> Response {
45 | ui.horizontal(|ui| {
46 | ui.group(|ui| {
47 | ui.vertical(|ui| {
48 | ui.checkbox(&mut self.animate, "Animate");
49 | if self.animate {
50 | ui.ctx().request_repaint();
51 | self.tick += 1.0;
52 | }
53 | ui.checkbox(&mut self.show_labels, "Show labels");
54 | });
55 | });
56 | ui.group(|ui| {
57 | ui.vertical(|ui| {
58 | ui.add(egui::Slider::new(&mut self.rows, 0..=50).text("Rows"));
59 | ui.add(egui::Slider::new(&mut self.cols, 0..=50).text("Columns"));
60 | });
61 | });
62 | ui.group(|ui| {
63 | ui.horizontal(|ui| {
64 | ui.add_enabled_ui(self.palette.len() > 1, |ui| {
65 | if ui.button("Pop color").clicked() {
66 | self.palette.pop();
67 | }
68 | });
69 | if ui.button("Push color").clicked()
70 | && let Some(last) = self.palette.last()
71 | {
72 | self.palette.push(*last);
73 | }
74 | });
75 | ui.horizontal(|ui| {
76 | for color in &mut self.palette {
77 | ui.color_edit_button_srgba(color);
78 | }
79 | })
80 | })
81 | })
82 | .response
83 | }
84 |
85 | #[expect(clippy::needless_pass_by_ref_mut, reason = "to allow mutation of self")]
86 | pub fn show_plot(&mut self, ui: &mut egui::Ui) -> Response {
87 | let mut values = Vec::new();
88 | for y in 0..self.rows {
89 | for x in 0..self.cols {
90 | let y = y as f64;
91 | let x = x as f64;
92 | let cols = self.cols as f64;
93 | let rows = self.rows as f64;
94 | values.push(((x + self.tick) / rows).sin() + ((y + self.tick) / cols).cos());
95 | }
96 | }
97 |
98 | let heatmap = egui_plot::Heatmap::new(values, self.cols)
99 | .palette(&self.palette)
100 | .highlight(true)
101 | .show_labels(self.show_labels);
102 |
103 | Plot::new("Heatmap Demo")
104 | .legend(Legend::default())
105 | .allow_zoom(false)
106 | .allow_scroll(false)
107 | .allow_drag(false)
108 | .allow_axis_zoom_drag(false)
109 | .allow_boxed_zoom(false)
110 | .set_margin_fraction(vec2(0.0, 0.0))
111 | .show(ui, |plot_ui| {
112 | plot_ui.heatmap(heatmap);
113 | })
114 | .response
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/ECOSYSTEM.md:
--------------------------------------------------------------------------------
1 | # Plotting libraries in Rust
2 |
3 | I searched over published crates on crates.io in November 2025 and found some interesting libraries.
4 | I briefly describe their functionality/main limitations here.
5 |
6 | Note that as far as I've looked, `egui_plot` is the only plotting library that supports plot interactions straight through Rust, without going through Javascript or other binding layers.
7 | Also, `egui_plot` produces a list of painting commands that are sent to a backend renderer, which can be GPU accelerated and easily integrated into other GUIs.
8 | Therefore, `egui_plot` can efficiently render large number of (interactive) plot items.
9 | See [`egui`](https://github.com/emilk/egui) for more info about GPU integration.
10 |
11 | ## Rust rendering libraries
12 |
13 | | Name | Description |
14 | |-------------------------------------------------------------|---------------------------------------------------------------------------------|
15 | | [`plotters`](https://crates.io/crates/plotters) | Pure Rust, interesting! But interactivity seems to be done via Javascript only. |
16 | | [`graplot`](https://crates.io/crates/graplot) | Pure Rust, but inactive |
17 | | [`quill`](https://crates.io/crates/quill) | Pure Rust, SVG, basic plots |
18 | | [`plotlib`](https://crates.io/crates/plotlib) | Pure Rust, SVG/text, basic features, looks abandoned |
19 | | [`rustplotlib`](https://crates.io/crates/rustplotlib) | Pure Rust, inactive |
20 | | [`criterion-plot`](https://crates.io/crates/criterion-plot) | not maintained |
21 | | [`runmat-plot`](https://crates.io/crates/runmat-plot) | Uses matplotlib-like DSL |
22 | | [`cgrustplot`](https://crates.io/crates/cgrustplot) | terminal plotting |
23 | | [`termplot`](https://crates.io/crates/termplot) | terminal plotting |
24 | | [`lowcharts`](https://crates.io/crates/lowcharts) | terminal plotting |
25 |
26 | ## Wrappers around other plotting libraries
27 |
28 | Following crates wrap other plotting libraries:
29 |
30 | | Name | Description |
31 | |-----------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
32 | | [`dear-imgui-rs`](https://crates.io/crates/dear-imgui-rs) | C/C++ bindings to https://github.com/ocornut/imgui. IMGUI is probably the most interesting library out there, as it is also immediate-mode based like `egui` and `egui_plot`. |
33 | | [`gnuplot`](https://crates.io/crates/gnuplot) | C/C++ bindings to http://www.gnuplot.info/ |
34 | | [`plotly`](https://crates.io/crates/plotly) | JS wrapper |
35 | | [`charming`](https://crates.io/crates/charming) | JS wrapper |
36 | | [`charts-rs`](https://crates.io/crates/charts-rs) | JS wrapper |
37 | | [`plotpy`](https://crates.io/crates/plotpy) | Python wrapper |
38 | | [`poloto`](https://crates.io/crates/poloto) | SVG, no interaction |
39 |
--------------------------------------------------------------------------------
/egui_plot/src/aesthetics.rs:
--------------------------------------------------------------------------------
1 | use egui::Shape;
2 | use egui::Stroke;
3 | use egui::epaint::ColorMode;
4 | use egui::epaint::PathStroke;
5 | use emath::Pos2;
6 | use emath::Rect;
7 | use emath::pos2;
8 |
9 | /// Solid, dotted, dashed, etc.
10 | #[derive(Debug, PartialEq, Clone, Copy)]
11 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
12 | pub enum LineStyle {
13 | Solid,
14 | Dotted { spacing: f32 },
15 | Dashed { length: f32 },
16 | }
17 |
18 | impl LineStyle {
19 | pub fn dashed_loose() -> Self {
20 | Self::Dashed { length: 10.0 }
21 | }
22 |
23 | pub fn dashed_dense() -> Self {
24 | Self::Dashed { length: 5.0 }
25 | }
26 |
27 | pub fn dotted_loose() -> Self {
28 | Self::Dotted { spacing: 10.0 }
29 | }
30 |
31 | pub fn dotted_dense() -> Self {
32 | Self::Dotted { spacing: 5.0 }
33 | }
34 |
35 | pub(crate) fn style_line(&self, line: Vec, mut stroke: PathStroke, highlight: bool, shapes: &mut Vec) {
36 | let path_stroke_color = match &stroke.color {
37 | ColorMode::Solid(c) => *c,
38 | ColorMode::UV(callback) => callback(Rect::from_min_max(pos2(0., 0.), pos2(0., 0.)), pos2(0., 0.)),
39 | };
40 | match line.len() {
41 | 0 => {}
42 | 1 => {
43 | let mut radius = stroke.width / 2.0;
44 | if highlight {
45 | radius *= 2f32.sqrt();
46 | }
47 | shapes.push(Shape::circle_filled(line[0], radius, path_stroke_color));
48 | }
49 | _ => {
50 | match self {
51 | Self::Solid => {
52 | if highlight {
53 | stroke.width *= 2.0;
54 | }
55 | shapes.push(Shape::line(line, stroke));
56 | }
57 | Self::Dotted { spacing } => {
58 | // Take the stroke width for the radius even though it's not "correct",
59 | // otherwise the dots would become too small.
60 | let mut radius = stroke.width;
61 | if highlight {
62 | radius *= 2f32.sqrt();
63 | }
64 | shapes.extend(Shape::dotted_line(&line, path_stroke_color, *spacing, radius));
65 | }
66 | Self::Dashed { length } => {
67 | if highlight {
68 | stroke.width *= 2.0;
69 | }
70 | let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
71 | shapes.extend(Shape::dashed_line(
72 | &line,
73 | Stroke::new(stroke.width, path_stroke_color),
74 | *length,
75 | length * golden_ratio,
76 | ));
77 | }
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
84 | impl std::fmt::Display for LineStyle {
85 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 | match self {
87 | Self::Solid => write!(f, "Solid"),
88 | Self::Dotted { spacing } => write!(f, "Dotted({spacing} px)"),
89 | Self::Dashed { length } => write!(f, "Dashed({length} px)"),
90 | }
91 | }
92 | }
93 |
94 | /// Determines whether a plot element is vertically or horizontally oriented.
95 | #[derive(Copy, Clone, Debug, PartialEq, Eq)]
96 | pub enum Orientation {
97 | Horizontal,
98 | Vertical,
99 | }
100 |
101 | impl Default for Orientation {
102 | fn default() -> Self {
103 | Self::Vertical
104 | }
105 | }
106 |
107 | /// Circle, Diamond, Square, Cross, …
108 | #[derive(Debug, PartialEq, Eq, Clone, Copy)]
109 | pub enum MarkerShape {
110 | Circle,
111 | Diamond,
112 | Square,
113 | Cross,
114 | Plus,
115 | Up,
116 | Down,
117 | Left,
118 | Right,
119 | Asterisk,
120 | }
121 |
122 | impl MarkerShape {
123 | /// Get a vector containing all marker shapes.
124 | pub fn all() -> impl ExactSizeIterator- {
125 | [
126 | Self::Circle,
127 | Self::Diamond,
128 | Self::Square,
129 | Self::Cross,
130 | Self::Plus,
131 | Self::Up,
132 | Self::Down,
133 | Self::Left,
134 | Self::Right,
135 | Self::Asterisk,
136 | ]
137 | .iter()
138 | .copied()
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/.github/workflows/rust.yml:
--------------------------------------------------------------------------------
1 | # Copied from https://github.com/rerun-io/rerun_template
2 | on:
3 | push:
4 | branches:
5 | - "main"
6 | pull_request:
7 | types: [ opened, synchronize ]
8 |
9 | name: Rust
10 |
11 | env:
12 | RUSTFLAGS: -D warnings
13 | RUSTDOCFLAGS: -D warnings
14 |
15 | jobs:
16 | rust-check:
17 | name: Rust
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v4
21 |
22 | - uses: actions-rs/toolchain@v1
23 | with:
24 | profile: default
25 | toolchain: 1.88.0
26 | override: true
27 |
28 | - name: Install packages (Linux)
29 | if: runner.os == 'Linux'
30 | uses: awalsh128/cache-apt-pkgs-action@v1.4.3
31 | with:
32 | packages: libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgtk-3-dev # libgtk-3-dev is used by rfd
33 | version: 1.0
34 | execute_install_scripts: true
35 |
36 | - name: Set up cargo cache
37 | uses: Swatinem/rust-cache@v2
38 |
39 | - name: Rustfmt
40 | run: cargo fmt --all -- --check
41 |
42 | - name: Lint vertical spacing
43 | run: ./scripts/lint.py
44 |
45 | - name: check --all-features
46 | run: cargo check --all-features --all-targets
47 |
48 | - name: check default features
49 | run: cargo check --all-targets
50 |
51 | - name: check --no-default-features
52 | run: cargo check --no-default-features --lib --all-targets
53 |
54 | - name: cargo doc --lib
55 | run: cargo doc --lib --no-deps --all-features
56 |
57 | - name: cargo doc --document-private-items
58 | run: cargo doc --document-private-items --no-deps --all-features
59 |
60 | - name: Clippy
61 | run: cargo clippy --all-targets --all-features -- -D warnings
62 |
63 | # ---------------------------------------------------------------------------
64 |
65 | check_wasm:
66 | name: Check wasm32
67 | runs-on: ubuntu-latest
68 | steps:
69 | - uses: actions/checkout@v4
70 | - uses: actions-rs/toolchain@v1
71 | with:
72 | profile: minimal
73 | toolchain: 1.88.0
74 | target: wasm32-unknown-unknown
75 | override: true
76 | components: clippy
77 |
78 | - name: Set up cargo cache
79 | uses: Swatinem/rust-cache@v2
80 |
81 | - name: Check wasm32
82 | run: cargo check --target wasm32-unknown-unknown --lib
83 |
84 | - name: Clippy wasm32
85 | env:
86 | CLIPPY_CONF_DIR: "scripts/clippy_wasm" # Use scripts/clippy_wasm/clippy.toml
87 | run: cargo clippy --target wasm32-unknown-unknown --lib -- -D warnings
88 |
89 | # ---------------------------------------------------------------------------
90 |
91 | cargo-deny:
92 | name: Check Rust dependencies (cargo-deny)
93 | runs-on: ubuntu-latest
94 | steps:
95 | - uses: actions/checkout@v3
96 | - uses: EmbarkStudios/cargo-deny-action@v2
97 | with:
98 | rust-version: "1.88.0"
99 | log-level: warn
100 | command: check
101 |
102 | # ---------------------------------------------------------------------------
103 |
104 | trunk:
105 | name: trunk build
106 | runs-on: ubuntu-latest
107 | steps:
108 | - uses: actions/checkout@v4
109 | - uses: actions-rs/toolchain@v1
110 | with:
111 | profile: minimal
112 | toolchain: 1.88.0
113 | target: wasm32-unknown-unknown
114 | override: true
115 | - name: Download and install Trunk binary
116 | run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf-
117 | - name: Build
118 | run: cd demo && ../trunk build
119 |
120 | # ---------------------------------------------------------------------------
121 |
122 | tests:
123 | name: Run tests
124 | # We run the tests on macOS because it will run with an actual GPU,
125 | # which is needed by the egui_kittest snapshot tests.
126 | runs-on: macos-latest
127 |
128 | steps:
129 | - uses: actions/checkout@v4
130 | with:
131 | lfs: true
132 | - uses: dtolnay/rust-toolchain@master
133 | with:
134 | toolchain: 1.88.0
135 |
136 | - name: Set up cargo cache
137 | uses: Swatinem/rust-cache@v2
138 |
139 | - name: Run tests
140 | run: RUST_BACKTRACE=1 cargo test --all-features
141 |
142 | - name: Run doc-tests
143 | run: RUST_BACKTRACE=1 cargo test --all-features --doc
144 |
145 | - name: Upload artifacts
146 | uses: actions/upload-artifact@v4
147 | if: failure()
148 | with:
149 | name: test-results
150 | path: "**/*.png"
151 |
--------------------------------------------------------------------------------
/egui_plot/src/items/text.rs:
--------------------------------------------------------------------------------
1 | use std::ops::RangeInclusive;
2 |
3 | use egui::Color32;
4 | use egui::Id;
5 | use egui::Shape;
6 | use egui::Stroke;
7 | use egui::TextStyle;
8 | use egui::Ui;
9 | use egui::WidgetText;
10 | use egui::epaint::TextShape;
11 | use emath::Align2;
12 |
13 | use crate::axis::PlotTransform;
14 | use crate::bounds::PlotBounds;
15 | use crate::bounds::PlotPoint;
16 | use crate::items::PlotGeometry;
17 | use crate::items::PlotItem;
18 | use crate::items::PlotItemBase;
19 |
20 | impl Text {
21 | pub fn new(name: impl Into
, position: PlotPoint, text: impl Into) -> Self {
22 | Self {
23 | base: PlotItemBase::new(name.into()),
24 | text: text.into(),
25 | position,
26 | color: Color32::TRANSPARENT,
27 | anchor: Align2::CENTER_CENTER,
28 | }
29 | }
30 |
31 | /// Text color.
32 | #[inline]
33 | pub fn color(mut self, color: impl Into) -> Self {
34 | self.color = color.into();
35 | self
36 | }
37 |
38 | /// Anchor position of the text. Default is `Align2::CENTER_CENTER`.
39 | #[inline]
40 | pub fn anchor(mut self, anchor: Align2) -> Self {
41 | self.anchor = anchor;
42 | self
43 | }
44 |
45 | /// Name of this plot item.
46 | ///
47 | /// This name will show up in the plot legend, if legends are turned on.
48 | ///
49 | /// Setting the name via this method does not change the item's id, so you
50 | /// can use it to change the name dynamically between frames without
51 | /// losing the item's state. You should make sure the name passed to
52 | /// [`Self::new`] is unique and stable for each item, or set unique and
53 | /// stable ids explicitly via [`Self::id`].
54 | #[expect(clippy::needless_pass_by_value, reason = "to allow various string types")]
55 | #[inline]
56 | pub fn name(mut self, name: impl ToString) -> Self {
57 | self.base_mut().name = name.to_string();
58 | self
59 | }
60 |
61 | /// Highlight this plot item, typically by scaling it up.
62 | ///
63 | /// If false, the item may still be highlighted via user interaction.
64 | #[inline]
65 | pub fn highlight(mut self, highlight: bool) -> Self {
66 | self.base_mut().highlight = highlight;
67 | self
68 | }
69 |
70 | /// Allowed hovering this item in the plot. Default: `true`.
71 | #[inline]
72 | pub fn allow_hover(mut self, hovering: bool) -> Self {
73 | self.base_mut().allow_hover = hovering;
74 | self
75 | }
76 |
77 | /// Sets the id of this plot item.
78 | ///
79 | /// By default the id is determined from the name passed to [`Self::new`],
80 | /// but it can be explicitly set to a different value.
81 | #[inline]
82 | pub fn id(mut self, id: impl Into) -> Self {
83 | self.base_mut().id = id.into();
84 | self
85 | }
86 | }
87 |
88 | impl PlotItem for Text {
89 | fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) {
90 | let color = if self.color == Color32::TRANSPARENT {
91 | ui.style().visuals.text_color()
92 | } else {
93 | self.color
94 | };
95 |
96 | let galley =
97 | self.text
98 | .clone()
99 | .into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, TextStyle::Small);
100 |
101 | let pos = transform.position_from_point(&self.position);
102 | let rect = self.anchor.anchor_size(pos, galley.size());
103 |
104 | shapes.push(TextShape::new(rect.min, galley, color).into());
105 |
106 | if self.base.highlight {
107 | shapes.push(Shape::rect_stroke(
108 | rect.expand(1.0),
109 | 1.0,
110 | Stroke::new(0.5, color),
111 | egui::StrokeKind::Outside,
112 | ));
113 | }
114 | }
115 |
116 | fn initialize(&mut self, _x_range: RangeInclusive) {}
117 |
118 | fn color(&self) -> Color32 {
119 | self.color
120 | }
121 |
122 | fn geometry(&self) -> PlotGeometry<'_> {
123 | PlotGeometry::None
124 | }
125 |
126 | fn bounds(&self) -> PlotBounds {
127 | let mut bounds = PlotBounds::NOTHING;
128 | bounds.extend_with(&self.position);
129 | bounds
130 | }
131 |
132 | fn base(&self) -> &PlotItemBase {
133 | &self.base
134 | }
135 |
136 | fn base_mut(&mut self) -> &mut PlotItemBase {
137 | &mut self.base
138 | }
139 | }
140 |
141 | /// Text inside the plot.
142 | #[derive(Clone)]
143 | pub struct Text {
144 | base: PlotItemBase,
145 | pub(crate) text: WidgetText,
146 | pub(crate) position: PlotPoint,
147 | pub(crate) color: Color32,
148 | pub(crate) anchor: Align2,
149 | }
150 |
--------------------------------------------------------------------------------
/examples_utils/src/lib.rs:
--------------------------------------------------------------------------------
1 | use eframe::egui;
2 |
3 | /// Trait for examples that can be displayed in the demo gallery.
4 | pub trait PlotExample {
5 | /// The name of the example. Should match directory name.
6 | fn name(&self) -> &'static str;
7 |
8 | /// The title of the example.
9 | fn title(&self) -> &'static str;
10 |
11 | /// The description of the example.
12 | fn description(&self) -> &'static str;
13 |
14 | /// The tags of the example.
15 | fn tags(&self) -> &'static [&'static str];
16 |
17 | /// The thumbnail image of the example.
18 | /// Should be 192x192 pixels. It is automatically generated from the
19 | /// screenshot of the example.
20 | fn thumbnail_bytes(&self) -> &'static [u8];
21 |
22 | /// The code of the example.
23 | fn code_bytes(&self) -> &'static [u8];
24 |
25 | /// The UI of the example.
26 | fn show_ui(&mut self, ui: &mut egui::Ui) -> egui::Response;
27 |
28 | /// The controls for the example.
29 | fn show_controls(&mut self, ui: &mut egui::Ui) -> egui::Response;
30 | }
31 |
32 | #[doc(hidden)]
33 | #[cfg(not(target_arch = "wasm32"))]
34 | pub mod internal {
35 | use std::path::PathBuf;
36 |
37 | use egui_kittest::Harness;
38 | use egui_kittest::SnapshotOptions;
39 |
40 | pub fn run_screenshot_test(builder: impl Fn(&mut eframe::CreationContext<'_>) -> State, manifest_dir: &str)
41 | where
42 | State: eframe::App,
43 | {
44 | let output_path = PathBuf::from(manifest_dir);
45 | let options = SnapshotOptions::new()
46 | .threshold(2.0)
47 | .failed_pixel_count_threshold(5)
48 | .output_path(output_path);
49 |
50 | // Generate main screenshot
51 | let mut harness = Harness::builder()
52 | .with_size(egui::Vec2::new(800.0, 800.0))
53 | .build_eframe(&builder);
54 | harness.run();
55 | harness.snapshot_options("screenshot", &options);
56 |
57 | // Generate thumbnail
58 | let mut thumb_harness = Harness::builder()
59 | .with_size(egui::Vec2::new(192.0, 192.0))
60 | .build_eframe(&builder);
61 | thumb_harness.run();
62 | let _ = thumb_harness.try_snapshot_options("screenshot_thumb", &options);
63 | }
64 | }
65 |
66 | /// Macro to generate a simple native `main` function for an `eframe` example
67 | /// and a corresponding screenshot test. Intended to be used for [`PlotExample`]
68 | /// implementations.
69 | ///
70 | /// # Example
71 | ///
72 | /// ```no_run,ignore
73 | /// use examples_utils::make_main;
74 | /// use my_example::MyExample;
75 | ///
76 | /// make_main!(MyExample);
77 | /// ```
78 | #[macro_export]
79 | macro_rules! make_main {
80 | ($inner:ident) => {
81 | use eframe::egui;
82 |
83 | // Generate wrapper struct
84 | #[derive(Default)]
85 | pub struct AppWrapper {
86 | pub inner: $inner,
87 | pub plot_only: bool,
88 | }
89 |
90 | impl eframe::App for AppWrapper {
91 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
92 | egui::CentralPanel::default().show(ctx, |ui| {
93 | if self.plot_only {
94 | self.inner.show_plot(ui);
95 | } else {
96 | ui.vertical(|ui| {
97 | self.inner.show_controls(ui);
98 | ui.separator();
99 | self.inner.show_plot(ui);
100 | });
101 | }
102 | });
103 | }
104 | }
105 |
106 | /// Native entry-point for the example.
107 | fn main() -> eframe::Result {
108 | use $crate::PlotExample as _;
109 |
110 | env_logger::init();
111 |
112 | // Derive the application title from the `PlotExample` implementation.
113 | let app_name: &'static str = <$inner as $crate::PlotExample>::title(&<$inner as Default>::default());
114 |
115 | let options = eframe::NativeOptions::default();
116 | eframe::run_native(
117 | app_name,
118 | options,
119 | Box::new(|_cc| Ok(Box::new(AppWrapper::default()))),
120 | )
121 | }
122 |
123 | /// Screenshot tests for the example.
124 | ///
125 | /// This uses `egui_kittest` under the hood and is only compiled for
126 | /// non-WASM targets.
127 | #[cfg(all(test, not(target_arch = "wasm32")))]
128 | mod screenshot_tests {
129 | use super::AppWrapper;
130 |
131 | #[allow(non_snake_case)]
132 | #[test]
133 | fn $inner() {
134 | ::examples_utils::internal::run_screenshot_test(
135 | |_cc| AppWrapper {
136 | plot_only: true,
137 | ..Default::default()
138 | },
139 | env!("CARGO_MANIFEST_DIR"),
140 | );
141 | }
142 | }
143 | };
144 | }
145 |
--------------------------------------------------------------------------------
/examples/custom_axes/src/app.rs:
--------------------------------------------------------------------------------
1 | use std::ops::RangeInclusive;
2 |
3 | use eframe::egui;
4 | use eframe::egui::Response;
5 | use egui_plot::AxisHints;
6 | use egui_plot::GridInput;
7 | use egui_plot::GridMark;
8 | use egui_plot::Line;
9 | use egui_plot::Plot;
10 | use egui_plot::PlotPoint;
11 | use egui_plot::PlotPoints;
12 |
13 | #[derive(Default)]
14 | pub struct CustomAxesExample {}
15 |
16 | impl CustomAxesExample {
17 | const MINS_PER_DAY: f64 = 24.0 * 60.0;
18 | const MINS_PER_H: f64 = 60.0;
19 |
20 | fn logistic_fn<'a>() -> Line<'a> {
21 | fn days(min: f64) -> f64 {
22 | CustomAxesExample::MINS_PER_DAY * min
23 | }
24 |
25 | let values = PlotPoints::from_explicit_callback(
26 | move |x| 1.0 / (1.0 + (-2.5 * (x / Self::MINS_PER_DAY - 2.0)).exp()),
27 | days(0.0)..days(5.0),
28 | 100,
29 | );
30 | Line::new("logistic fn", values)
31 | }
32 |
33 | #[expect(clippy::needless_pass_by_value, reason = "to allow various range types")]
34 | fn x_grid(input: GridInput) -> Vec {
35 | let mut marks = vec![];
36 |
37 | let (min, max) = input.bounds;
38 | let min = min.floor() as i32;
39 | let max = max.ceil() as i32;
40 |
41 | for i in min..=max {
42 | let step_size = if i % Self::MINS_PER_DAY as i32 == 0 {
43 | Self::MINS_PER_DAY
44 | } else if i % Self::MINS_PER_H as i32 == 0 {
45 | Self::MINS_PER_H
46 | } else if i % 5 == 0 {
47 | 5.0
48 | } else {
49 | continue;
50 | };
51 |
52 | marks.push(GridMark {
53 | value: i as f64,
54 | step_size,
55 | });
56 | }
57 |
58 | marks
59 | }
60 |
61 | #[expect(clippy::unused_self, reason = "required by the example template")]
62 | pub fn show_plot(&self, ui: &mut egui::Ui) -> Response {
63 | const MINS_PER_DAY: f64 = CustomAxesExample::MINS_PER_DAY;
64 | const MINS_PER_H: f64 = CustomAxesExample::MINS_PER_H;
65 |
66 | fn day(x: f64) -> f64 {
67 | (x / MINS_PER_DAY).floor()
68 | }
69 |
70 | fn hour(x: f64) -> f64 {
71 | (x.rem_euclid(MINS_PER_DAY) / MINS_PER_H).floor()
72 | }
73 |
74 | fn minute(x: f64) -> f64 {
75 | x.rem_euclid(MINS_PER_H).floor()
76 | }
77 |
78 | fn percent(y: f64) -> f64 {
79 | 100.0 * y
80 | }
81 |
82 | let time_formatter = |mark: GridMark, _range: &RangeInclusive| {
83 | let minutes = mark.value;
84 | if !(0.0..5.0 * MINS_PER_DAY).contains(&minutes) {
85 | String::new()
86 | } else if is_approx_integer(minutes / MINS_PER_DAY) {
87 | format!("Day {}", day(minutes))
88 | } else {
89 | format!("{h}:{m:02}", h = hour(minutes), m = minute(minutes))
90 | }
91 | };
92 |
93 | let percentage_formatter = |mark: GridMark, _range: &RangeInclusive| {
94 | let percent = 100.0 * mark.value;
95 | if is_approx_zero(percent) {
96 | String::new()
97 | } else if is_approx_integer(percent) {
98 | format!("{percent:.0}%")
99 | } else {
100 | String::new()
101 | }
102 | };
103 |
104 | let label_fmt = |_s: &str, val: &PlotPoint| {
105 | format!(
106 | "Day {d}, {h}:{m:02}\n{p:.2}%",
107 | d = day(val.x),
108 | h = hour(val.x),
109 | m = minute(val.x),
110 | p = percent(val.y)
111 | )
112 | };
113 |
114 | let x_axes = vec![
115 | AxisHints::new_x()
116 | .label("Time")
117 | .formatter(time_formatter)
118 | .placement(egui_plot::VPlacement::Top),
119 | AxisHints::new_x().label("Time").formatter(time_formatter),
120 | AxisHints::new_x().label("Value"),
121 | ];
122 | let y_axes = vec![
123 | AxisHints::new_y().label("Percent").formatter(percentage_formatter),
124 | AxisHints::new_y()
125 | .label("Absolute")
126 | .placement(egui_plot::HPlacement::Right),
127 | ];
128 | Plot::new("custom_axes")
129 | .data_aspect(2.0 * MINS_PER_DAY as f32)
130 | .custom_x_axes(x_axes)
131 | .custom_y_axes(y_axes)
132 | .x_grid_spacer(Self::x_grid)
133 | .label_formatter(label_fmt)
134 | .show(ui, |plot_ui| {
135 | plot_ui.line(Self::logistic_fn());
136 | })
137 | .response
138 | }
139 |
140 | #[expect(clippy::unused_self, reason = "required by the example template")]
141 | pub fn show_controls(&self, ui: &mut egui::Ui) -> Response {
142 | ui.label("Zoom in on the X-axis to see hours and minutes")
143 | }
144 | }
145 |
146 | fn is_approx_zero(val: f64) -> bool {
147 | val.abs() < 1e-6
148 | }
149 |
150 | fn is_approx_integer(val: f64) -> bool {
151 | val.fract().abs() < 1e-6
152 | }
153 |
--------------------------------------------------------------------------------
/egui_plot/src/items/polygon.rs:
--------------------------------------------------------------------------------
1 | use std::ops::RangeInclusive;
2 |
3 | use egui::Color32;
4 | use egui::Id;
5 | use egui::Shape;
6 | use egui::Stroke;
7 | use egui::Ui;
8 | use egui::epaint::PathStroke;
9 |
10 | use crate::aesthetics::LineStyle;
11 | use crate::axis::PlotTransform;
12 | use crate::bounds::PlotBounds;
13 | use crate::colors::DEFAULT_FILL_ALPHA;
14 | use crate::data::PlotPoints;
15 | use crate::items::PlotGeometry;
16 | use crate::items::PlotItem;
17 | use crate::items::PlotItemBase;
18 |
19 | /// A convex polygon.
20 | pub struct Polygon<'a> {
21 | base: PlotItemBase,
22 | pub(crate) series: PlotPoints<'a>,
23 | pub(crate) stroke: Stroke,
24 | pub(crate) fill_color: Option,
25 | pub(crate) style: LineStyle,
26 | }
27 |
28 | impl<'a> Polygon<'a> {
29 | pub fn new(name: impl Into, series: impl Into>) -> Self {
30 | Self {
31 | base: PlotItemBase::new(name.into()),
32 | series: series.into(),
33 | stroke: Stroke::new(1.0, Color32::TRANSPARENT),
34 | fill_color: None,
35 | style: LineStyle::Solid,
36 | }
37 | }
38 |
39 | /// Add a custom stroke.
40 | #[inline]
41 | pub fn stroke(mut self, stroke: impl Into) -> Self {
42 | self.stroke = stroke.into();
43 | self
44 | }
45 |
46 | /// Set the stroke width.
47 | #[inline]
48 | pub fn width(mut self, width: impl Into) -> Self {
49 | self.stroke.width = width.into();
50 | self
51 | }
52 |
53 | /// Fill color. Defaults to the stroke color with added transparency.
54 | #[inline]
55 | pub fn fill_color(mut self, color: impl Into) -> Self {
56 | self.fill_color = Some(color.into());
57 | self
58 | }
59 |
60 | /// Set the outline's style. Default is `LineStyle::Solid`.
61 | #[inline]
62 | pub fn style(mut self, style: LineStyle) -> Self {
63 | self.style = style;
64 | self
65 | }
66 |
67 | /// Name of this plot item.
68 | ///
69 | /// This name will show up in the plot legend, if legends are turned on.
70 | ///
71 | /// Setting the name via this method does not change the item's id, so you
72 | /// can use it to change the name dynamically between frames without
73 | /// losing the item's state. You should make sure the name passed to
74 | /// [`Self::new`] is unique and stable for each item, or set unique and
75 | /// stable ids explicitly via [`Self::id`].
76 | #[expect(clippy::needless_pass_by_value, reason = "to allow various string types")]
77 | #[inline]
78 | pub fn name(mut self, name: impl ToString) -> Self {
79 | self.base_mut().name = name.to_string();
80 | self
81 | }
82 |
83 | /// Highlight this plot item, typically by scaling it up.
84 | ///
85 | /// If false, the item may still be highlighted via user interaction.
86 | #[inline]
87 | pub fn highlight(mut self, highlight: bool) -> Self {
88 | self.base_mut().highlight = highlight;
89 | self
90 | }
91 |
92 | /// Allowed hovering this item in the plot. Default: `true`.
93 | #[inline]
94 | pub fn allow_hover(mut self, hovering: bool) -> Self {
95 | self.base_mut().allow_hover = hovering;
96 | self
97 | }
98 |
99 | /// Sets the id of this plot item.
100 | ///
101 | /// By default the id is determined from the name passed to [`Self::new`],
102 | /// but it can be explicitly set to a different value.
103 | #[inline]
104 | pub fn id(mut self, id: impl Into) -> Self {
105 | self.base_mut().id = id.into();
106 | self
107 | }
108 | }
109 |
110 | impl PlotItem for Polygon<'_> {
111 | fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) {
112 | let Self {
113 | base,
114 | series,
115 | stroke,
116 | fill_color,
117 | style,
118 | ..
119 | } = self;
120 |
121 | let mut values_tf: Vec<_> = series
122 | .points()
123 | .iter()
124 | .map(|v| transform.position_from_point(v))
125 | .collect();
126 |
127 | let fill_color = fill_color.unwrap_or(stroke.color.linear_multiply(DEFAULT_FILL_ALPHA));
128 |
129 | let shape = Shape::convex_polygon(values_tf.clone(), fill_color, Stroke::NONE);
130 | shapes.push(shape);
131 |
132 | if let Some(first) = values_tf.first() {
133 | values_tf.push(*first); // close the polygon
134 | }
135 |
136 | style.style_line(
137 | values_tf,
138 | PathStroke::new(stroke.width, stroke.color),
139 | base.highlight,
140 | shapes,
141 | );
142 | }
143 |
144 | fn initialize(&mut self, x_range: RangeInclusive) {
145 | self.series.generate_points(x_range);
146 | }
147 |
148 | fn color(&self) -> Color32 {
149 | self.stroke.color
150 | }
151 |
152 | fn geometry(&self) -> PlotGeometry<'_> {
153 | PlotGeometry::Points(self.series.points())
154 | }
155 |
156 | fn bounds(&self) -> PlotBounds {
157 | self.series.bounds()
158 | }
159 |
160 | fn base(&self) -> &PlotItemBase {
161 | &self.base
162 | }
163 |
164 | fn base_mut(&mut self) -> &mut PlotItemBase {
165 | &mut self.base
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | egui_plot
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | Loading…
130 |
131 |
132 |
133 |
134 |
135 |
136 |
144 |
145 |
146 |
147 |
148 |
149 |
--------------------------------------------------------------------------------