More plot items (#471)
* Added plot items: * Arrows, also called "Quiver plots" in matplotlib etc. * Convex polygons * Text * Images Other changes: * Make HLine/VLine into PlotItems as well. * Add a "fill" property to Line so that we can fill/shade the area between a line and a horizontal reference line. * Add stems to Points, which are lines between the points and a horizontal reference line. * Allow using .. when specifying ranges for values generated by explicit callback functions, as an alias for f64::NEG_INFINITY..f64::INFINITY * Allow using ranges with exclusive end bounds for values generated by parametric callback functions to generate values where the first and last value are not the same. * update changelog * add legend background
This commit is contained in:
parent
e22c242d17
commit
147e7a47aa
5 changed files with 971 additions and 117 deletions
|
@ -8,6 +8,7 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [
|
|||
## Unreleased
|
||||
|
||||
### Added ⭐
|
||||
* [More plot items: Arrows, Polygons, Text, Images](https://github.com/emilk/egui/pull/471).
|
||||
* [Plot legend improvements](https://github.com/emilk/egui/pull/410).
|
||||
* [Line markers for plots](https://github.com/emilk/egui/pull/363).
|
||||
* Add right and bottom panels (`SidePanel::right` and `Panel::bottom`).
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -33,6 +33,7 @@ impl Corner {
|
|||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub struct Legend {
|
||||
pub text_style: TextStyle,
|
||||
pub background_alpha: f32,
|
||||
pub position: Corner,
|
||||
}
|
||||
|
||||
|
@ -40,17 +41,26 @@ impl Default for Legend {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
text_style: TextStyle::Body,
|
||||
background_alpha: 0.75,
|
||||
position: Corner::RightTop,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Legend {
|
||||
/// Which text style to use for the legend. Default: `TextStyle::Body`.
|
||||
pub fn text_style(mut self, style: TextStyle) -> Self {
|
||||
self.text_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// The alpha of the legend background. Default: `0.75`.
|
||||
pub fn background_alpha(mut self, alpha: f32) -> Self {
|
||||
self.background_alpha = alpha;
|
||||
self
|
||||
}
|
||||
|
||||
/// In which corner to place the legend. Default: `Corner::RightTop`.
|
||||
pub fn position(mut self, corner: Corner) -> Self {
|
||||
self.position = corner;
|
||||
self
|
||||
|
@ -220,17 +230,29 @@ impl Widget for &mut LegendWidget {
|
|||
Corner::RightTop | Corner::RightBottom => Align::RIGHT,
|
||||
};
|
||||
let layout = Layout::from_main_dir_and_cross_align(main_dir, cross_align);
|
||||
let legend_pad = 2.0;
|
||||
let legend_pad = 4.0;
|
||||
let legend_rect = rect.shrink(legend_pad);
|
||||
let mut legend_ui = ui.child_ui(legend_rect, layout);
|
||||
legend_ui
|
||||
.scope(|ui| {
|
||||
ui.style_mut().body_text_style = config.text_style;
|
||||
entries
|
||||
.iter_mut()
|
||||
.map(|(name, entry)| entry.ui(ui, name.clone()))
|
||||
.reduce(|r1, r2| r1.union(r2))
|
||||
.unwrap()
|
||||
let background_frame = Frame {
|
||||
margin: vec2(8.0, 4.0),
|
||||
corner_radius: ui.style().visuals.window_corner_radius,
|
||||
shadow: epaint::Shadow::default(),
|
||||
fill: ui.style().visuals.extreme_bg_color,
|
||||
stroke: ui.style().visuals.window_stroke(),
|
||||
}
|
||||
.multiply_with_opacity(config.background_alpha);
|
||||
background_frame
|
||||
.show(ui, |ui| {
|
||||
entries
|
||||
.iter_mut()
|
||||
.map(|(name, entry)| entry.ui(ui, name.clone()))
|
||||
.reduce(|r1, r2| r1.union(r2))
|
||||
.unwrap()
|
||||
})
|
||||
.inner
|
||||
})
|
||||
.inner
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ mod transform;
|
|||
use std::collections::HashSet;
|
||||
|
||||
use items::PlotItem;
|
||||
pub use items::{Arrows, Line, MarkerShape, PlotImage, Points, Polygon, Text, Value, Values};
|
||||
pub use items::{HLine, VLine};
|
||||
pub use items::{Line, MarkerShape, Points, Value, Values};
|
||||
use legend::LegendWidget;
|
||||
pub use legend::{Corner, Legend};
|
||||
use transform::{Bounds, ScreenTransform};
|
||||
|
@ -51,8 +51,6 @@ pub struct Plot {
|
|||
next_auto_color_idx: usize,
|
||||
|
||||
items: Vec<Box<dyn PlotItem>>,
|
||||
hlines: Vec<HLine>,
|
||||
vlines: Vec<VLine>,
|
||||
|
||||
center_x_axis: bool,
|
||||
center_y_axis: bool,
|
||||
|
@ -80,8 +78,6 @@ impl Plot {
|
|||
next_auto_color_idx: 0,
|
||||
|
||||
items: Default::default(),
|
||||
hlines: Default::default(),
|
||||
vlines: Default::default(),
|
||||
|
||||
center_x_axis: false,
|
||||
center_y_axis: false,
|
||||
|
@ -111,7 +107,6 @@ impl Plot {
|
|||
}
|
||||
|
||||
/// Add a data lines.
|
||||
/// You can add multiple lines.
|
||||
pub fn line(mut self, mut line: Line) -> Self {
|
||||
if line.series.is_empty() {
|
||||
return self;
|
||||
|
@ -122,12 +117,34 @@ impl Plot {
|
|||
line.stroke.color = self.auto_color();
|
||||
}
|
||||
self.items.push(Box::new(line));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a polygon. The polygon has to be convex.
|
||||
pub fn polygon(mut self, mut polygon: Polygon) -> Self {
|
||||
if polygon.series.is_empty() {
|
||||
return self;
|
||||
};
|
||||
|
||||
// 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));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a text.
|
||||
pub fn text(mut self, text: Text) -> Self {
|
||||
if text.text.is_empty() {
|
||||
return self;
|
||||
};
|
||||
|
||||
self.items.push(Box::new(text));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add data points.
|
||||
/// You can add multiple sets of points.
|
||||
pub fn points(mut self, mut points: Points) -> Self {
|
||||
if points.series.is_empty() {
|
||||
return self;
|
||||
|
@ -138,7 +155,26 @@ impl Plot {
|
|||
points.color = self.auto_color();
|
||||
}
|
||||
self.items.push(Box::new(points));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add arrows.
|
||||
pub fn arrows(mut self, mut arrows: Arrows) -> Self {
|
||||
if arrows.origins.is_empty() || arrows.tips.is_empty() {
|
||||
return self;
|
||||
};
|
||||
|
||||
// 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));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an image.
|
||||
pub fn image(mut self, image: PlotImage) -> Self {
|
||||
self.items.push(Box::new(image));
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -149,7 +185,7 @@ impl Plot {
|
|||
if hline.stroke.color == Color32::TRANSPARENT {
|
||||
hline.stroke.color = self.auto_color();
|
||||
}
|
||||
self.hlines.push(hline);
|
||||
self.items.push(Box::new(hline));
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -160,7 +196,7 @@ impl Plot {
|
|||
if vline.stroke.color == Color32::TRANSPARENT {
|
||||
vline.stroke.color = self.auto_color();
|
||||
}
|
||||
self.vlines.push(vline);
|
||||
self.items.push(Box::new(vline));
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -284,8 +320,6 @@ impl Widget for Plot {
|
|||
name,
|
||||
next_auto_color_idx: _,
|
||||
mut items,
|
||||
hlines,
|
||||
vlines,
|
||||
center_x_axis,
|
||||
center_y_axis,
|
||||
allow_zoom,
|
||||
|
@ -381,11 +415,9 @@ impl Widget for Plot {
|
|||
// Set bounds automatically based on content.
|
||||
if auto_bounds || !bounds.is_valid() {
|
||||
bounds = min_auto_bounds;
|
||||
hlines.iter().for_each(|line| bounds.extend_with_y(line.y));
|
||||
vlines.iter().for_each(|line| bounds.extend_with_x(line.x));
|
||||
items
|
||||
.iter()
|
||||
.for_each(|item| bounds.merge(&item.series().get_bounds()));
|
||||
.for_each(|item| bounds.merge(&item.get_bounds()));
|
||||
bounds.add_relative_margin(margin_fraction);
|
||||
}
|
||||
// Make sure they are not empty.
|
||||
|
@ -436,17 +468,14 @@ impl Widget for Plot {
|
|||
}
|
||||
|
||||
// Initialize values from functions.
|
||||
items.iter_mut().for_each(|item| {
|
||||
item.series_mut()
|
||||
.generate_points(transform.bounds().range_x())
|
||||
});
|
||||
items
|
||||
.iter_mut()
|
||||
.for_each(|item| item.initialize(transform.bounds().range_x()));
|
||||
|
||||
let bounds = *transform.bounds();
|
||||
|
||||
let prepared = Prepared {
|
||||
items,
|
||||
hlines,
|
||||
vlines,
|
||||
show_x,
|
||||
show_y,
|
||||
transform,
|
||||
|
@ -479,8 +508,6 @@ impl Widget for Plot {
|
|||
|
||||
struct Prepared {
|
||||
items: Vec<Box<dyn PlotItem>>,
|
||||
hlines: Vec<HLine>,
|
||||
vlines: Vec<VLine>,
|
||||
show_x: bool,
|
||||
show_y: bool,
|
||||
transform: ScreenTransform,
|
||||
|
@ -496,26 +523,10 @@ impl Prepared {
|
|||
|
||||
let transform = &self.transform;
|
||||
|
||||
for &hline in &self.hlines {
|
||||
let HLine { y, stroke } = hline;
|
||||
let points = [
|
||||
transform.position_from_value(&Value::new(transform.bounds().min[0], y)),
|
||||
transform.position_from_value(&Value::new(transform.bounds().max[0], y)),
|
||||
];
|
||||
shapes.push(Shape::line_segment(points, stroke));
|
||||
}
|
||||
|
||||
for &vline in &self.vlines {
|
||||
let VLine { x, stroke } = vline;
|
||||
let points = [
|
||||
transform.position_from_value(&Value::new(x, transform.bounds().min[1])),
|
||||
transform.position_from_value(&Value::new(x, transform.bounds().max[1])),
|
||||
];
|
||||
shapes.push(Shape::line_segment(points, stroke));
|
||||
}
|
||||
|
||||
let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default());
|
||||
plot_ui.set_clip_rect(*transform.frame());
|
||||
for item in &self.items {
|
||||
item.get_shapes(transform, &mut shapes);
|
||||
item.get_shapes(&mut plot_ui, transform, &mut shapes);
|
||||
}
|
||||
|
||||
if let Some(pointer) = response.hover_pos() {
|
||||
|
@ -632,13 +643,15 @@ impl Prepared {
|
|||
let mut closest_item = None;
|
||||
let mut closest_dist_sq = interact_radius.powi(2);
|
||||
for item in items {
|
||||
for value in &item.series().values {
|
||||
let pos = transform.position_from_value(value);
|
||||
let dist_sq = pointer.distance_sq(pos);
|
||||
if dist_sq < closest_dist_sq {
|
||||
closest_dist_sq = dist_sq;
|
||||
closest_value = Some(value);
|
||||
closest_item = Some(item.name());
|
||||
if let Some(values) = item.values() {
|
||||
for value in &values.values {
|
||||
let pos = transform.position_from_value(value);
|
||||
let dist_sq = pointer.distance_sq(pos);
|
||||
if dist_sq < closest_dist_sq {
|
||||
closest_dist_sq = dist_sq;
|
||||
closest_value = Some(value);
|
||||
closest_item = Some(item.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
use egui::*;
|
||||
use plot::{Corner, Legend, Line, MarkerShape, Plot, Points, Value, Values};
|
||||
use plot::{
|
||||
Arrows, Corner, HLine, Legend, Line, MarkerShape, Plot, PlotImage, Points, Polygon, Text,
|
||||
VLine, Value, Values,
|
||||
};
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
@ -44,7 +47,7 @@ impl LineDemo {
|
|||
ui.add(
|
||||
egui::DragValue::new(circle_radius)
|
||||
.speed(0.1)
|
||||
.clamp_range(0.0..=f32::INFINITY)
|
||||
.clamp_range(0.0..=f64::INFINITY)
|
||||
.prefix("r: "),
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
|
@ -90,7 +93,7 @@ impl LineDemo {
|
|||
let time = self.time;
|
||||
Line::new(Values::from_explicit_callback(
|
||||
move |x| 0.5 * (2.0 * x).sin() * time.sin(),
|
||||
f64::NEG_INFINITY..=f64::INFINITY,
|
||||
..,
|
||||
512,
|
||||
))
|
||||
.color(Color32::from_rgb(200, 100, 100))
|
||||
|
@ -187,7 +190,7 @@ impl Widget for &mut MarkerDemo {
|
|||
ui.add(
|
||||
egui::DragValue::new(&mut self.marker_radius)
|
||||
.speed(0.1)
|
||||
.clamp_range(0.0..=f32::INFINITY)
|
||||
.clamp_range(0.0..=f64::INFINITY)
|
||||
.prefix("marker radius: "),
|
||||
);
|
||||
ui.checkbox(&mut self.custom_marker_color, "custom marker color");
|
||||
|
@ -221,25 +224,13 @@ impl Default for LegendDemo {
|
|||
|
||||
impl LegendDemo {
|
||||
fn line_with_slope(slope: f64) -> Line {
|
||||
Line::new(Values::from_explicit_callback(
|
||||
move |x| slope * x,
|
||||
f64::NEG_INFINITY..=f64::INFINITY,
|
||||
100,
|
||||
))
|
||||
Line::new(Values::from_explicit_callback(move |x| slope * x, .., 100))
|
||||
}
|
||||
fn sin() -> Line {
|
||||
Line::new(Values::from_explicit_callback(
|
||||
move |x| x.sin(),
|
||||
f64::NEG_INFINITY..=f64::INFINITY,
|
||||
100,
|
||||
))
|
||||
Line::new(Values::from_explicit_callback(move |x| x.sin(), .., 100))
|
||||
}
|
||||
fn cos() -> Line {
|
||||
Line::new(Values::from_explicit_callback(
|
||||
move |x| x.cos(),
|
||||
f64::NEG_INFINITY..=f64::INFINITY,
|
||||
100,
|
||||
))
|
||||
Line::new(Values::from_explicit_callback(move |x| x.cos(), .., 100))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -259,6 +250,12 @@ impl Widget for &mut LegendDemo {
|
|||
ui.selectable_value(&mut config.position, position, format!("{:?}", position));
|
||||
});
|
||||
});
|
||||
ui.label("Background alpha:");
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut config.background_alpha)
|
||||
.speed(0.02)
|
||||
.clamp_range(0.0..=1.0),
|
||||
);
|
||||
let legend_plot = Plot::new("Legend Demo")
|
||||
.line(LegendDemo::line_with_slope(0.5).name("lines"))
|
||||
.line(LegendDemo::line_with_slope(1.0).name("lines"))
|
||||
|
@ -270,11 +267,82 @@ impl Widget for &mut LegendDemo {
|
|||
ui.add(legend_plot)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Default)]
|
||||
struct ItemsDemo {}
|
||||
|
||||
impl ItemsDemo {}
|
||||
|
||||
impl Widget for &mut ItemsDemo {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let n = 100;
|
||||
let mut sin_values: Vec<_> = (0..=n)
|
||||
.map(|i| remap(i as f64, 0.0..=n as f64, -TAU..=TAU))
|
||||
.map(|i| Value::new(i, i.sin()))
|
||||
.collect();
|
||||
|
||||
let line = Line::new(Values::from_values(sin_values.split_off(n / 2))).fill(-1.5);
|
||||
let polygon = Polygon::new(Values::from_parametric_callback(
|
||||
|t| (4.0 * t.sin() + 2.0 * t.cos(), 4.0 * t.cos() + 2.0 * t.sin()),
|
||||
0.0..TAU,
|
||||
100,
|
||||
));
|
||||
let points = Points::new(Values::from_values(sin_values))
|
||||
.stems(-1.5)
|
||||
.radius(1.0);
|
||||
|
||||
let arrows = {
|
||||
let pos_radius = 8.0;
|
||||
let tip_radius = 7.0;
|
||||
let arrow_origins = Values::from_parametric_callback(
|
||||
|t| (pos_radius * t.sin(), pos_radius * t.cos()),
|
||||
0.0..TAU,
|
||||
36,
|
||||
);
|
||||
let arrow_tips = Values::from_parametric_callback(
|
||||
|t| (tip_radius * t.sin(), tip_radius * t.cos()),
|
||||
0.0..TAU,
|
||||
36,
|
||||
);
|
||||
Arrows::new(arrow_origins, arrow_tips)
|
||||
};
|
||||
let image = PlotImage::new(
|
||||
TextureId::Egui,
|
||||
Value::new(0.0, 10.0),
|
||||
[
|
||||
ui.fonts().texture().width as f32 / 100.0,
|
||||
ui.fonts().texture().height as f32 / 100.0,
|
||||
],
|
||||
);
|
||||
|
||||
let plot = Plot::new("Items Demo")
|
||||
.hline(HLine::new(9.0).name("Lines horizontal"))
|
||||
.hline(HLine::new(-9.0).name("Lines horizontal"))
|
||||
.vline(VLine::new(9.0).name("Lines vertical"))
|
||||
.vline(VLine::new(-9.0).name("Lines vertical"))
|
||||
.line(line.name("Line with fill"))
|
||||
.polygon(polygon.name("Convex polygon"))
|
||||
.points(points.name("Points with stems"))
|
||||
.text(Text::new(Value::new(-3.0, -3.0), "wow").name("Text"))
|
||||
.text(Text::new(Value::new(-2.0, 2.5), "so graph").name("Text"))
|
||||
.text(Text::new(Value::new(3.0, 3.0), "much color").name("Text"))
|
||||
.text(Text::new(Value::new(2.5, -2.0), "such plot").name("Text"))
|
||||
.image(image.name("Image"))
|
||||
.arrows(arrows.name("Arrows"))
|
||||
.legend(Legend::default().position(Corner::RightBottom))
|
||||
.show_x(false)
|
||||
.show_y(false)
|
||||
.data_aspect(1.0);
|
||||
ui.add(plot)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum Panel {
|
||||
Lines,
|
||||
Markers,
|
||||
Legend,
|
||||
Items,
|
||||
}
|
||||
|
||||
impl Default for Panel {
|
||||
|
@ -288,6 +356,7 @@ pub struct PlotDemo {
|
|||
line_demo: LineDemo,
|
||||
marker_demo: MarkerDemo,
|
||||
legend_demo: LegendDemo,
|
||||
items_demo: ItemsDemo,
|
||||
open_panel: Panel,
|
||||
}
|
||||
|
||||
|
@ -326,6 +395,7 @@ impl super::View for PlotDemo {
|
|||
ui.selectable_value(&mut self.open_panel, Panel::Lines, "Lines");
|
||||
ui.selectable_value(&mut self.open_panel, Panel::Markers, "Markers");
|
||||
ui.selectable_value(&mut self.open_panel, Panel::Legend, "Legend");
|
||||
ui.selectable_value(&mut self.open_panel, Panel::Items, "Items");
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
|
@ -339,6 +409,9 @@ impl super::View for PlotDemo {
|
|||
Panel::Legend => {
|
||||
ui.add(&mut self.legend_demo);
|
||||
}
|
||||
Panel::Items => {
|
||||
ui.add(&mut self.items_demo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue