feat: wip egui plot

This commit is contained in:
Ilya Zlobintsev
2025-02-09 15:33:30 +02:00
parent ff55dc7634
commit d0f29d1349
5 changed files with 293 additions and 64 deletions

222
Cargo.lock generated
View File

@@ -2,6 +2,22 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "ab_glyph"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0"
dependencies = [
"ab_glyph_rasterizer",
"owned_ttf_parser",
]
[[package]]
name = "ab_glyph_rasterizer"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
[[package]]
name = "addr2line"
version = "0.24.2"
@@ -597,12 +613,73 @@ dependencies = [
"syn",
]
[[package]]
name = "ecolor"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "878e9005799dd739e5d5d89ff7480491c12d0af571d44399bcaefa1ee172dd76"
dependencies = [
"bytemuck",
"emath",
]
[[package]]
name = "egui"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d2768eaa6d5c80a6e2a008da1f0e062dff3c83eb2b28605ea2d0732d46e74d6"
dependencies = [
"ahash",
"bitflags 2.8.0",
"emath",
"epaint",
"nohash-hasher",
"profiling",
]
[[package]]
name = "egui_glow"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a53e2374a964c3c793cb0b8ead81bca631f24974bc0b747d1a5622f4e39fdd0"
dependencies = [
"ahash",
"bytemuck",
"egui",
"glow",
"log",
"memoffset",
"profiling",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "egui_plot"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1794c66fb727dac28dffed2e4b548e5118d1cccc331d368a35411d68725dde71"
dependencies = [
"ahash",
"egui",
"emath",
]
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "emath"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55b7b6be5ad1d247f11738b0e4699d9c20005ed366f2c29f5ec1f8e1de180bc2"
dependencies = [
"bytemuck",
]
[[package]]
name = "encode_unicode"
version = "1.0.0"
@@ -648,6 +725,41 @@ dependencies = [
"syn",
]
[[package]]
name = "epaint"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "275b665a7b9611d8317485187e5458750850f9e64604d3c58434bb3fc1d22915"
dependencies = [
"ab_glyph",
"ahash",
"bytemuck",
"ecolor",
"emath",
"epaint_default_fonts",
"nohash-hasher",
"parking_lot",
"profiling",
]
[[package]]
name = "epaint_default_fonts"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9343d356d7cac894dacafc161b4654e0881301097bdf32a122ed503d97cb94b6"
[[package]]
name = "epoxy"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b96028ce3ff03972312fd8243281858e80fc0f9838b1f035676b6c199214d9e"
dependencies = [
"gl_generator",
"libc",
"pkg-config",
"shared_library",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@@ -932,6 +1044,27 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "gl_generator"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a795170cbd85b5a7baa58d6d7525cae6a03e486859860c220f7ebbbdd379d0a"
dependencies = [
"khronos_api",
"log",
"xml-rs 0.7.0",
]
[[package]]
name = "gl_loader"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e32d96dd5f881490e537041d5532320812ba096097f07fccb4626578da0b99d3"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "glib"
version = "0.20.7"
@@ -982,6 +1115,18 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "glow"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
dependencies = [
"js-sys",
"slotmap",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gobject-sys"
version = "0.20.7"
@@ -1047,6 +1192,18 @@ dependencies = [
"system-deps",
]
[[package]]
name = "gtk-egui-area"
version = "0.1.1"
dependencies = [
"egui",
"egui_glow",
"epoxy",
"gl_loader",
"gtk4",
"libloading 0.8.6",
]
[[package]]
name = "gtk4"
version = "0.9.5"
@@ -1257,6 +1414,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "khronos_api"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037ab472c33f67b5fbd3e9163a2645319e5356fcd355efa6d4eb7fff4bbcb554"
[[package]]
name = "kqueue"
version = "1.0.8"
@@ -1358,6 +1521,8 @@ dependencies = [
"anyhow",
"cairo-rs",
"chrono",
"egui_plot",
"gtk-egui-area",
"gtk4",
"itertools",
"lact-client",
@@ -1613,6 +1778,12 @@ dependencies = [
"memoffset",
]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "nom"
version = "7.1.3"
@@ -1751,6 +1922,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owned_ttf_parser"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4"
dependencies = [
"ttf-parser",
]
[[package]]
name = "pango"
version = "0.20.7"
@@ -1909,6 +2089,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "profiling"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
[[package]]
name = "quote"
version = "1.0.38"
@@ -2208,6 +2394,16 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shared_library"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
dependencies = [
"lazy_static",
"libc",
]
[[package]]
name = "shlex"
version = "1.3.0"
@@ -2248,6 +2444,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "slotmap"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
dependencies = [
"version_check",
]
[[package]]
name = "smallvec"
version = "1.13.2"
@@ -2580,6 +2785,12 @@ dependencies = [
"syn",
]
[[package]]
name = "ttf-parser"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]]
name = "uds_windows"
version = "1.1.0"
@@ -2645,7 +2856,7 @@ version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81086c28be67a8759cd80cbb3c8f7b520e0874605fc5eb74d5a1c9c2d1878e79"
dependencies = [
"xml-rs",
"xml-rs 0.8.25",
]
[[package]]
@@ -3003,6 +3214,15 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "xml-rs"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c1cb601d29fe2c2ac60a2b2e5e293994d87a1f6fa9687a31a15270f909be9c2"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "xml-rs"
version = "0.8.25"

View File

@@ -34,8 +34,10 @@ plotters = { version = "0.3.5", default-features = false, features = [
plotters-cairo = "0.7.0"
cairo-rs = { version = "0.20", default-features = false }
itertools = "0.13.0"
gtk-egui-area = { path = "../../gtk-egui-area" }
thread-priority = "1.1.0"
egui_plot = "0.31.0"
[dev-dependencies]
pretty_assertions = "1.4.0"

View File

@@ -38,6 +38,7 @@ impl relm4::Component for GraphsWindow {
set_margin_all: 10,
set_row_spacing: 20,
set_column_spacing: 20,
set_column_homogeneous: true,
attach[0, 0, 1, 1]: temperature_plot = &Plot {
set_title: "Temperature",

View File

@@ -1,14 +1,18 @@
use chrono::NaiveDateTime;
use egui_plot::AxisHints;
use egui_plot::PlotBounds;
use egui_plot::PlotPoint;
use egui_plot::PlotPoints;
use glib::Properties;
use gtk::{glib, prelude::*, subclass::prelude::*};
use gtk_egui_area::egui;
use gtk_egui_area::EguiArea;
use std::cell::Cell;
use std::cell::RefCell;
use std::collections::BTreeMap;
use super::render_thread::{RenderRequest, RenderThread};
#[derive(Properties, Default)]
#[properties(wrapper_type = super::Plot)]
pub struct Plot {
@@ -24,7 +28,6 @@ pub struct Plot {
secondary_y_label_area_relative_size: Cell<f64>,
pub(super) data: RefCell<PlotData>,
pub(super) dirty: Cell<bool>,
render_thread: RenderThread,
#[property(get, set)]
time_period_seconds: Cell<i64>,
}
@@ -33,7 +36,7 @@ pub struct Plot {
impl ObjectSubclass for Plot {
const NAME: &'static str = "Plot";
type Type = super::Plot;
type ParentType = gtk::Widget;
type ParentType = gtk::Box;
}
#[glib::derived_properties]
@@ -41,58 +44,62 @@ impl ObjectImpl for Plot {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
let obj = self.obj().clone();
obj.set_height_request(250);
obj.set_hexpand(true);
obj.set_vexpand(true);
let area = EguiArea::new(move |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
let data = obj.imp().data.borrow();
egui_plot::Plot::new(obj.title())
.legend(egui_plot::Legend::default().position(egui_plot::Corner::RightBottom))
.custom_x_axes(vec![AxisHints::new_x().label("Time").formatter(
|point, _| {
let time = chrono::DateTime::from_timestamp_millis(point.value as i64)
.unwrap()
.naive_local()
.time();
time.to_string()
},
)])
.show(ui, |plot_ui| {
let max_x = data
.line_series_iter()
.next()
.and_then(|(_, points)| points.last())
.map(|point| point.x)
.unwrap_or_default();
let min_x = max_x - (obj.time_period_seconds() * 1000) as f64;
let bounds = PlotBounds::from_min_max(
[min_x, -f64::INFINITY],
[max_x, f64::INFINITY],
);
plot_ui.set_plot_bounds(bounds);
plot_ui.set_auto_bounds([false, true]);
for (name, points) in data.line_series_iter() {
plot_ui.line(
egui_plot::Line::new(PlotPoints::Borrowed(points)).name(name),
);
}
}
impl WidgetImpl for Plot {
fn snapshot(&self, snapshot: &gtk::Snapshot) {
let width = self.obj().width() as u32;
let height = self.obj().height() as u32;
if width == 0 || height == 0 {
return;
}
let last_texture = self.render_thread.get_last_texture();
let size_changed = last_texture
.as_ref()
.map(|texture| (texture.width() as u32, texture.height() as u32) != (width, height))
.unwrap_or(true);
if self.dirty.replace(false) || size_changed {
self.render_thread.replace_render_request(RenderRequest {
data: self.data.borrow().clone(),
width,
height,
title: self.title.borrow().clone(),
value_suffix: self.value_suffix.borrow().clone(),
secondary_value_suffix: self.secondary_value_suffix.borrow().clone(),
y_label_area_relative_size: self.y_label_area_relative_size.get(),
secondary_y_label_relative_area_size: self
.secondary_y_label_area_relative_size
.get(),
supersample_factor: 4,
time_period_seconds: self.time_period_seconds.get(),
})
});
}
// Rendering is always behind by at least one frame, but it's not an issue
if let Some(texture) = last_texture {
let bounds = gtk::graphene::Rect::new(0.0, 0.0, width as f32, height as f32);
// Uses by default Trillinear texture filtering, which is quite good at 4x supersampling
snapshot.append_texture(&texture, &bounds);
}
});
self.obj().append(&area);
}
}
impl WidgetImpl for Plot {}
impl BoxImpl for Plot {}
#[derive(Default, Clone)]
pub struct PlotData {
pub(super) line_series: BTreeMap<String, Vec<(i64, f64)>>,
pub(super) secondary_line_series: BTreeMap<String, Vec<(i64, f64)>>,
pub(super) line_series: BTreeMap<String, Vec<PlotPoint>>,
pub(super) secondary_line_series: BTreeMap<String, Vec<PlotPoint>>,
pub(super) throttling: Vec<(i64, (String, bool))>,
}
@@ -109,7 +116,10 @@ impl PlotData {
self.line_series
.entry(name.to_owned())
.or_default()
.push((time.and_utc().timestamp_millis(), point));
.push(PlotPoint::new(
time.and_utc().timestamp_millis() as f64,
point,
));
}
pub fn push_secondary_line_series_with_time(
@@ -121,7 +131,10 @@ impl PlotData {
self.secondary_line_series
.entry(name.to_owned())
.or_default()
.push((time.and_utc().timestamp_millis(), point));
.push(PlotPoint::new(
time.and_utc().timestamp_millis() as f64,
point,
));
}
pub fn push_throttling(&mut self, name: &str, point: bool) {
@@ -134,11 +147,11 @@ impl PlotData {
));
}
pub fn line_series_iter(&self) -> impl Iterator<Item = (&String, &Vec<(i64, f64)>)> {
pub fn line_series_iter(&self) -> impl Iterator<Item = (&String, &Vec<PlotPoint>)> {
self.line_series.iter()
}
pub fn secondary_line_series_iter(&self) -> impl Iterator<Item = (&String, &Vec<(i64, f64)>)> {
pub fn secondary_line_series_iter(&self) -> impl Iterator<Item = (&String, &Vec<PlotPoint>)> {
self.secondary_line_series.iter()
}
@@ -151,23 +164,17 @@ impl PlotData {
pub fn trim_data(&mut self, last_seconds: i64) {
// Limit data to N seconds
for data in self.line_series.values_mut() {
let maximum_point = data
.last()
.map(|(date_time, _)| *date_time)
.unwrap_or_default();
let maximum_point = data.last().map(|point| point.x as i64).unwrap_or_default();
data.retain(|(time_point, _)| ((maximum_point - *time_point) / 1000) < last_seconds);
data.retain(|point| ((maximum_point - point.x as i64) / 1000) < last_seconds);
}
self.line_series.retain(|_, data| !data.is_empty());
for data in self.secondary_line_series.values_mut() {
let maximum_point = data
.last()
.map(|(date_time, _)| *date_time)
.unwrap_or_default();
let maximum_point = data.last().map(|point| point.x as i64).unwrap_or_default();
data.retain(|(time_point, _)| ((maximum_point - *time_point) / 1000) < last_seconds);
data.retain(|point| ((maximum_point - point.x as i64) / 1000) < last_seconds);
}
self.secondary_line_series

View File

@@ -1,17 +1,16 @@
mod cubic_spline;
// mod cubic_spline;
mod imp;
mod render_thread;
mod to_texture_ext;
use std::cell::RefMut;
// mod render_thread;
// mod to_texture_ext;
pub use imp::PlotData;
use std::cell::RefMut;
use gtk::glib::{self, subclass::types::ObjectSubclassIsExt, Object};
glib::wrapper! {
pub struct Plot(ObjectSubclass<imp::Plot>)
@extends gtk::Widget;
@extends gtk::Widget, gtk::Box;
}
impl Default for Plot {