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:
Sven Niederberger 2021-06-24 12:29:51 +02:00 committed by GitHub
parent e22c242d17
commit 147e7a47aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 971 additions and 117 deletions

View file

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

View file

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

View file

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

View file

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