Skip to content

Instantly share code, notes, and snippets.

@rctlmk
Created May 10, 2023 17:00
Show Gist options
  • Save rctlmk/d386fe0a9d6c36daa042192c970ed6e0 to your computer and use it in GitHub Desktop.
Save rctlmk/d386fe0a9d6c36daa042192c970ed6e0 to your computer and use it in GitHub Desktop.
egui pie charts
[package]
name = "egui-pie-chart"
version = "0.1.0"
edition = "2021"
[dependencies]
egui = "0.21"
egui_extras = "0.21"
eframe = "0.21"
use eframe::{egui, NativeOptions};
use egui_extras::{Size, StripBuilder};
use pie_chart::PieChart;
mod pie_chart;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let options = NativeOptions {
initial_window_size: Some(egui::Vec2::new(600.0, 480.0)),
..NativeOptions::default()
};
eframe::run_native("Pie charts", options, Box::new(|ctx| Box::new(PieCharts::new(ctx))))?;
Ok(())
}
struct PieCharts {
pie_charts: Vec<PieChart>,
}
impl Default for PieCharts {
fn default() -> Self {
let data: Vec<_> = (0..8).map(|i| (0.125, format!("{}: 12.5%", i + 1))).collect();
let pie_charts = vec![
PieChart::new("First", &[(15.0, "25%"), (45.0, "75%")]),
PieChart::new("Second", &data),
PieChart::new(
"Third",
&[(600.0, "Big"), (200.0, "Small"), (600.0, "Also Big"), (400.0, "Medium")],
),
PieChart::new("Fourth", &[(14.0, "100%")]),
];
Self { pie_charts }
}
}
impl PieCharts {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
Self::default()
}
fn show(&mut self, ui: &mut egui::Ui) {
StripBuilder::new(ui).sizes(Size::remainder(), 2).vertical(|mut strip| {
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
strip.cell(|ui| {
self.pie_charts[0].show(ui);
});
strip.cell(|ui| {
self.pie_charts[1].show(ui);
});
});
});
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
strip.cell(|ui| {
self.pie_charts[2].show(ui);
});
strip.cell(|ui| {
self.pie_charts[3].show(ui);
});
});
});
});
}
}
impl eframe::App for PieCharts {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let central_panel = egui::CentralPanel::default();
central_panel.show(ctx, |ui| self.show(ui));
}
}
use std::f64::consts::TAU;
use egui::plot::{Legend, Plot, PlotPoint, PlotPoints, Polygon, Text};
use egui::{Align2, RichText};
const FULL_CIRCLE_VERTICES: f64 = 240.0;
const RADIUS: f64 = 1.0;
pub struct PieChart {
name: String,
sectors: Vec<Sector>,
}
impl PieChart {
pub fn new<S: AsRef<str>, L: AsRef<str>>(name: S, data: &[(f64, L)]) -> Self {
let sum: f64 = data.iter().map(|(f, _)| f).sum();
let slices: Vec<_> = data.iter().map(|(f, n)| (f / sum, n)).collect();
let step = TAU / FULL_CIRCLE_VERTICES;
let mut offset = 0.0_f64;
let sectors = slices
.iter()
.map(|(p, n)| {
let vertices = (FULL_CIRCLE_VERTICES * p).round() as usize;
let start = TAU * offset;
let end = TAU * (offset + p);
let sector = Sector::new(n, start, end, vertices, step);
offset += p;
sector
})
.collect();
Self {
name: name.as_ref().to_string(),
sectors,
}
}
pub fn show(&mut self, ui: &mut egui::Ui) {
let sectors = self.sectors.clone();
Plot::new(&self.name)
.label_formatter(|_: &str, _: &PlotPoint| String::default())
.show_background(false)
.legend(Legend::default())
.show_axes([false; 2])
.clamp_grid(true)
.allow_boxed_zoom(false)
.allow_drag(false)
.allow_zoom(false)
.allow_scroll(false)
.data_aspect(1.0)
// .set_margin_fraction([0.7; 2].into()) // this won't prevent the plot from moving
// `include_*` will lock it into place
.include_x(-2.0)
.include_x(2.0)
.include_y(-2.0)
.include_y(2.0)
.show(ui, |plot_ui| {
for sector in sectors.into_iter() {
let highlight = plot_ui.pointer_coordinate().map(|p| sector.contains(&p)).unwrap_or_default();
let Sector { name, points, .. } = sector;
plot_ui.polygon(Polygon::new(PlotPoints::new(points)).name(&name).highlight(highlight));
if highlight {
let p = plot_ui.pointer_coordinate().unwrap();
// TODO proper zoom
let text = RichText::new(&name).size(15.0).heading();
plot_ui.text(Text::new(p, text).name(&name).anchor(Align2::LEFT_BOTTOM));
}
}
});
}
}
#[derive(Clone)]
struct Sector {
name: String,
start: f64,
end: f64,
points: Vec<[f64; 2]>,
}
impl Sector {
pub fn new<S: AsRef<str>>(name: S, start: f64, end: f64, vertices: usize, step: f64) -> Self {
let mut points = vec![];
if end - TAU != start {
points.push([0.0, 0.0]);
}
points.push([RADIUS * start.sin(), RADIUS * start.cos()]);
for v in 1..vertices {
let t = start + step * v as f64;
points.push([RADIUS * t.sin(), RADIUS * t.cos()]);
}
points.push([RADIUS * end.sin(), RADIUS * end.cos()]);
Self {
name: name.as_ref().to_string(),
start,
end,
points,
}
}
pub fn contains(&self, &PlotPoint { x, y }: &PlotPoint) -> bool {
let r = y.hypot(x);
let mut theta = x.atan2(y);
if theta < 0.0 {
theta += TAU;
}
r < RADIUS && theta > self.start && theta < self.end
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment