Add bar charts and box plots (#863)
Changes: * New `BarChart` and `BoxPlot` diagrams * New `FloatOrd` trait for total ordering of float types * Refactoring of existing plot items Co-authored-by: niladic <git@nil.choron.cc>
This commit is contained in:
parent
224d4d6d26
commit
1088d950e9
12 changed files with 1813 additions and 420 deletions
|
@ -8,6 +8,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added ⭐
|
### Added ⭐
|
||||||
|
* Add bar charts and box plots ([#863](https://github.com/emilk/egui/pull/863)).
|
||||||
* Add context menus: See `Ui::menu_button` and `Response::context_menu` ([#543](https://github.com/emilk/egui/pull/543)).
|
* Add context menus: See `Ui::menu_button` and `Response::context_menu` ([#543](https://github.com/emilk/egui/pull/543)).
|
||||||
* You can now read and write the cursor of a `TextEdit` ([#848](https://github.com/emilk/egui/pull/848)).
|
* You can now read and write the cursor of a `TextEdit` ([#848](https://github.com/emilk/egui/pull/848)).
|
||||||
* Most widgets containing text (`Label`, `Button` etc) now supports rich text ([#855](https://github.com/emilk/egui/pull/855)).
|
* Most widgets containing text (`Label`, `Button` etc) now supports rich text ([#855](https://github.com/emilk/egui/pull/855)).
|
||||||
|
|
64
egui/src/util/float_ord.rs
Normal file
64
egui/src/util/float_ord.rs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
//! Total order on floating point types, assuming absence of NaN.
|
||||||
|
//! Can be used for sorting, min/max computation, and other collection algorithms.
|
||||||
|
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
/// Totally orderable floating-point value
|
||||||
|
/// For not `f32` is supported; could be made generic if necessary.
|
||||||
|
pub(crate) struct OrderedFloat(f32);
|
||||||
|
|
||||||
|
impl Eq for OrderedFloat {}
|
||||||
|
|
||||||
|
impl PartialEq<Self> for OrderedFloat {
|
||||||
|
#[inline]
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
// NaNs are considered equal (equivalent when it comes to ordering
|
||||||
|
if self.0.is_nan() {
|
||||||
|
other.0.is_nan()
|
||||||
|
} else {
|
||||||
|
self.0 == other.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd<Self> for OrderedFloat {
|
||||||
|
#[inline]
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
match self.0.partial_cmp(&other.0) {
|
||||||
|
Some(ord) => Some(ord),
|
||||||
|
None => Some(self.0.is_nan().cmp(&other.0.is_nan())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for OrderedFloat {
|
||||||
|
#[inline]
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
match self.partial_cmp(other) {
|
||||||
|
Some(ord) => ord,
|
||||||
|
None => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension trait to provide `ord` method
|
||||||
|
pub(crate) trait FloatOrd {
|
||||||
|
/// Type to provide total order, useful as key in sorted contexts.
|
||||||
|
fn ord(self) -> OrderedFloat;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FloatOrd for f32 {
|
||||||
|
#[inline]
|
||||||
|
fn ord(self) -> OrderedFloat {
|
||||||
|
OrderedFloat(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO ordering may break down at least significant digits due to f64 -> f32 conversion
|
||||||
|
// Possible solutions: generic OrderedFloat<T>, always OrderedFloat(f64)
|
||||||
|
impl FloatOrd for f64 {
|
||||||
|
#[inline]
|
||||||
|
fn ord(self) -> OrderedFloat {
|
||||||
|
OrderedFloat(self as f32)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub(crate) mod fixed_cache;
|
pub(crate) mod fixed_cache;
|
||||||
|
pub(crate) mod float_ord;
|
||||||
mod history;
|
mod history;
|
||||||
pub mod id_type_map;
|
pub mod id_type_map;
|
||||||
pub mod undoer;
|
pub mod undoer;
|
||||||
|
|
190
egui/src/widgets/plot/items/bar.rs
Normal file
190
egui/src/widgets/plot/items/bar.rs
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
use crate::emath::NumExt;
|
||||||
|
use crate::epaint::{Color32, RectShape, Shape, Stroke};
|
||||||
|
|
||||||
|
use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement};
|
||||||
|
use crate::plot::{BarChart, ScreenTransform, Value};
|
||||||
|
|
||||||
|
/// One bar in a [`BarChart`]. Potentially floating, allowing stacked bar charts.
|
||||||
|
/// Width can be changed to allow variable-width histograms.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Bar {
|
||||||
|
/// Name of plot element in the diagram (annotated by default formatter)
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
/// Which direction the bar faces in the diagram
|
||||||
|
pub orientation: Orientation,
|
||||||
|
|
||||||
|
/// Position on the argument (input) axis -- X if vertical, Y if horizontal
|
||||||
|
pub argument: f64,
|
||||||
|
|
||||||
|
/// Position on the value (output) axis -- Y if vertical, X if horizontal
|
||||||
|
pub value: f64,
|
||||||
|
|
||||||
|
/// For stacked bars, this denotes where the bar starts. None if base axis
|
||||||
|
pub base_offset: Option<f64>,
|
||||||
|
|
||||||
|
/// Thickness of the bar
|
||||||
|
pub bar_width: f64,
|
||||||
|
|
||||||
|
/// Line width and color
|
||||||
|
pub stroke: Stroke,
|
||||||
|
|
||||||
|
/// Fill color
|
||||||
|
pub fill: Color32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bar {
|
||||||
|
/// Create a bar. Its `orientation` is set by its [`BarChart`] parent.
|
||||||
|
///
|
||||||
|
/// - `argument`: Position on the argument axis (X if vertical, Y if horizontal).
|
||||||
|
/// - `value`: Height of the bar (if vertical).
|
||||||
|
///
|
||||||
|
/// By default the bar is vertical and its base is at zero.
|
||||||
|
pub fn new(argument: f64, height: f64) -> Bar {
|
||||||
|
Bar {
|
||||||
|
argument,
|
||||||
|
value: height,
|
||||||
|
orientation: Orientation::default(),
|
||||||
|
name: Default::default(),
|
||||||
|
base_offset: None,
|
||||||
|
bar_width: 0.5,
|
||||||
|
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
|
||||||
|
fill: Color32::TRANSPARENT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Name of this bar chart element.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub fn name(mut self, name: impl ToString) -> Self {
|
||||||
|
self.name = name.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a custom stroke.
|
||||||
|
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||||
|
self.stroke = stroke.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a custom fill color.
|
||||||
|
pub fn fill(mut self, color: impl Into<Color32>) -> Self {
|
||||||
|
self.fill = color.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offset the base of the bar.
|
||||||
|
/// This offset is on the Y axis for a vertical bar
|
||||||
|
/// and on the X axis for a horizontal bar.
|
||||||
|
pub fn base_offset(mut self, offset: f64) -> Self {
|
||||||
|
self.base_offset = Some(offset);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the bar width.
|
||||||
|
pub fn width(mut self, width: f64) -> Self {
|
||||||
|
self.bar_width = width;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set orientation of the element as vertical. Argument axis is X.
|
||||||
|
pub fn vertical(mut self) -> Self {
|
||||||
|
self.orientation = Orientation::Vertical;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set orientation of the element as horizontal. Argument axis is Y.
|
||||||
|
pub fn horizontal(mut self) -> Self {
|
||||||
|
self.orientation = Orientation::Horizontal;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn lower(&self) -> f64 {
|
||||||
|
if self.value.is_sign_positive() {
|
||||||
|
self.base_offset.unwrap_or(0.0)
|
||||||
|
} else {
|
||||||
|
self.base_offset.map_or(self.value, |o| o + self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn upper(&self) -> f64 {
|
||||||
|
if self.value.is_sign_positive() {
|
||||||
|
self.base_offset.map_or(self.value, |o| o + self.value)
|
||||||
|
} else {
|
||||||
|
self.base_offset.unwrap_or(0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn add_shapes(
|
||||||
|
&self,
|
||||||
|
transform: &ScreenTransform,
|
||||||
|
highlighted: bool,
|
||||||
|
shapes: &mut Vec<Shape>,
|
||||||
|
) {
|
||||||
|
let (stroke, fill) = if highlighted {
|
||||||
|
highlighted_color(self.stroke, self.fill)
|
||||||
|
} else {
|
||||||
|
(self.stroke, self.fill)
|
||||||
|
};
|
||||||
|
|
||||||
|
let rect = transform.rect_from_values(&self.bounds_min(), &self.bounds_max());
|
||||||
|
let rect = Shape::Rect(RectShape {
|
||||||
|
rect,
|
||||||
|
corner_radius: 0.0,
|
||||||
|
fill,
|
||||||
|
stroke,
|
||||||
|
});
|
||||||
|
|
||||||
|
shapes.push(rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn add_rulers_and_text(
|
||||||
|
&self,
|
||||||
|
parent: &BarChart,
|
||||||
|
plot: &PlotConfig<'_>,
|
||||||
|
shapes: &mut Vec<Shape>,
|
||||||
|
) {
|
||||||
|
let text: Option<String> = parent
|
||||||
|
.element_formatter
|
||||||
|
.as_ref()
|
||||||
|
.map(|fmt| fmt(self, parent));
|
||||||
|
|
||||||
|
add_rulers_and_text(self, plot, text, shapes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RectElement for Bar {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
self.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds_min(&self) -> Value {
|
||||||
|
self.point_at(self.argument - self.bar_width / 2.0, self.lower())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds_max(&self) -> Value {
|
||||||
|
self.point_at(self.argument + self.bar_width / 2.0, self.upper())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn values_with_ruler(&self) -> Vec<Value> {
|
||||||
|
let base = self.base_offset.unwrap_or(0.0);
|
||||||
|
let value_center = self.point_at(self.argument, base + self.value);
|
||||||
|
|
||||||
|
let mut ruler_positions = vec![value_center];
|
||||||
|
|
||||||
|
if let Some(offset) = self.base_offset {
|
||||||
|
ruler_positions.push(self.point_at(self.argument, offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
ruler_positions
|
||||||
|
}
|
||||||
|
|
||||||
|
fn orientation(&self) -> Orientation {
|
||||||
|
self.orientation
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_values_format(&self, transform: &ScreenTransform) -> String {
|
||||||
|
let scale = transform.dvalue_dpos();
|
||||||
|
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||||
|
format!("\n{:.*}", y_decimals, self.value)
|
||||||
|
}
|
||||||
|
}
|
286
egui/src/widgets/plot/items/box_elem.rs
Normal file
286
egui/src/widgets/plot/items/box_elem.rs
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
use crate::emath::NumExt;
|
||||||
|
use crate::epaint::{Color32, RectShape, Shape, Stroke};
|
||||||
|
|
||||||
|
use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement};
|
||||||
|
use crate::plot::{BoxPlot, ScreenTransform, Value};
|
||||||
|
|
||||||
|
/// Contains the values of a single box in a box plot.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct BoxSpread {
|
||||||
|
/// Value of lower whisker (typically minimum).
|
||||||
|
///
|
||||||
|
/// The whisker is not drawn if `lower_whisker >= quartile1`.
|
||||||
|
pub lower_whisker: f64,
|
||||||
|
|
||||||
|
/// Value of lower box threshold (typically 25% quartile)
|
||||||
|
pub quartile1: f64,
|
||||||
|
|
||||||
|
/// Value of middle line in box (typically median)
|
||||||
|
pub median: f64,
|
||||||
|
|
||||||
|
/// Value of upper box threshold (typically 75% quartile)
|
||||||
|
pub quartile3: f64,
|
||||||
|
|
||||||
|
/// Value of upper whisker (typically maximum)
|
||||||
|
///
|
||||||
|
/// The whisker is not drawn if `upper_whisker <= quartile3`.
|
||||||
|
pub upper_whisker: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoxSpread {
|
||||||
|
pub fn new(
|
||||||
|
lower_whisker: f64,
|
||||||
|
quartile1: f64,
|
||||||
|
median: f64,
|
||||||
|
quartile3: f64,
|
||||||
|
upper_whisker: f64,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
lower_whisker,
|
||||||
|
quartile1,
|
||||||
|
median,
|
||||||
|
quartile3,
|
||||||
|
upper_whisker,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A box in a [`BoxPlot`] diagram. This is a low level graphical element; it will not compute quartiles and whiskers,
|
||||||
|
/// letting one use their preferred formula. Use [`Points`][`super::Points`] to draw the outliers.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct BoxElem {
|
||||||
|
/// Name of plot element in the diagram (annotated by default formatter).
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
/// Which direction the box faces in the diagram.
|
||||||
|
pub orientation: Orientation,
|
||||||
|
|
||||||
|
/// Position on the argument (input) axis -- X if vertical, Y if horizontal.
|
||||||
|
pub argument: f64,
|
||||||
|
|
||||||
|
/// Values of the box
|
||||||
|
pub spread: BoxSpread,
|
||||||
|
|
||||||
|
/// Thickness of the box
|
||||||
|
pub box_width: f64,
|
||||||
|
|
||||||
|
/// Width of the whisker at minimum/maximum
|
||||||
|
pub whisker_width: f64,
|
||||||
|
|
||||||
|
/// Line width and color
|
||||||
|
pub stroke: Stroke,
|
||||||
|
|
||||||
|
/// Fill color
|
||||||
|
pub fill: Color32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoxElem {
|
||||||
|
/// Create a box element. Its `orientation` is set by its [`BoxPlot`] parent.
|
||||||
|
///
|
||||||
|
/// Check [`BoxElem`] fields for detailed description.
|
||||||
|
pub fn new(argument: f64, spread: BoxSpread) -> Self {
|
||||||
|
Self {
|
||||||
|
argument,
|
||||||
|
orientation: Orientation::default(),
|
||||||
|
name: String::default(),
|
||||||
|
spread,
|
||||||
|
box_width: 0.25,
|
||||||
|
whisker_width: 0.15,
|
||||||
|
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
|
||||||
|
fill: Color32::TRANSPARENT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Name of this box element.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub fn name(mut self, name: impl ToString) -> Self {
|
||||||
|
self.name = name.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a custom stroke.
|
||||||
|
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||||
|
self.stroke = stroke.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a custom fill color.
|
||||||
|
pub fn fill(mut self, color: impl Into<Color32>) -> Self {
|
||||||
|
self.fill = color.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the box width.
|
||||||
|
pub fn box_width(mut self, width: f64) -> Self {
|
||||||
|
self.box_width = width;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the whisker width.
|
||||||
|
pub fn whisker_width(mut self, width: f64) -> Self {
|
||||||
|
self.whisker_width = width;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set orientation of the element as vertical. Argument axis is X.
|
||||||
|
pub fn vertical(mut self) -> Self {
|
||||||
|
self.orientation = Orientation::Vertical;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set orientation of the element as horizontal. Argument axis is Y.
|
||||||
|
pub fn horizontal(mut self) -> Self {
|
||||||
|
self.orientation = Orientation::Horizontal;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn add_shapes(
|
||||||
|
&self,
|
||||||
|
transform: &ScreenTransform,
|
||||||
|
highlighted: bool,
|
||||||
|
shapes: &mut Vec<Shape>,
|
||||||
|
) {
|
||||||
|
let (stroke, fill) = if highlighted {
|
||||||
|
highlighted_color(self.stroke, self.fill)
|
||||||
|
} else {
|
||||||
|
(self.stroke, self.fill)
|
||||||
|
};
|
||||||
|
|
||||||
|
let rect = transform.rect_from_values(
|
||||||
|
&self.point_at(self.argument - self.box_width / 2.0, self.spread.quartile1),
|
||||||
|
&self.point_at(self.argument + self.box_width / 2.0, self.spread.quartile3),
|
||||||
|
);
|
||||||
|
let rect = Shape::Rect(RectShape {
|
||||||
|
rect,
|
||||||
|
corner_radius: 0.0,
|
||||||
|
fill,
|
||||||
|
stroke,
|
||||||
|
});
|
||||||
|
shapes.push(rect);
|
||||||
|
|
||||||
|
let line_between = |v1, v2| {
|
||||||
|
Shape::line_segment(
|
||||||
|
[
|
||||||
|
transform.position_from_value(&v1),
|
||||||
|
transform.position_from_value(&v2),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let median = line_between(
|
||||||
|
self.point_at(self.argument - self.box_width / 2.0, self.spread.median),
|
||||||
|
self.point_at(self.argument + self.box_width / 2.0, self.spread.median),
|
||||||
|
);
|
||||||
|
shapes.push(median);
|
||||||
|
|
||||||
|
if self.spread.upper_whisker > self.spread.quartile3 {
|
||||||
|
let high_whisker = line_between(
|
||||||
|
self.point_at(self.argument, self.spread.quartile3),
|
||||||
|
self.point_at(self.argument, self.spread.upper_whisker),
|
||||||
|
);
|
||||||
|
shapes.push(high_whisker);
|
||||||
|
if self.box_width > 0.0 {
|
||||||
|
let high_whisker_end = line_between(
|
||||||
|
self.point_at(
|
||||||
|
self.argument - self.whisker_width / 2.0,
|
||||||
|
self.spread.upper_whisker,
|
||||||
|
),
|
||||||
|
self.point_at(
|
||||||
|
self.argument + self.whisker_width / 2.0,
|
||||||
|
self.spread.upper_whisker,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
shapes.push(high_whisker_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.spread.lower_whisker < self.spread.quartile1 {
|
||||||
|
let low_whisker = line_between(
|
||||||
|
self.point_at(self.argument, self.spread.quartile1),
|
||||||
|
self.point_at(self.argument, self.spread.lower_whisker),
|
||||||
|
);
|
||||||
|
shapes.push(low_whisker);
|
||||||
|
if self.box_width > 0.0 {
|
||||||
|
let low_whisker_end = line_between(
|
||||||
|
self.point_at(
|
||||||
|
self.argument - self.whisker_width / 2.0,
|
||||||
|
self.spread.lower_whisker,
|
||||||
|
),
|
||||||
|
self.point_at(
|
||||||
|
self.argument + self.whisker_width / 2.0,
|
||||||
|
self.spread.lower_whisker,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
shapes.push(low_whisker_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn add_rulers_and_text(
|
||||||
|
&self,
|
||||||
|
parent: &BoxPlot,
|
||||||
|
plot: &PlotConfig<'_>,
|
||||||
|
shapes: &mut Vec<Shape>,
|
||||||
|
) {
|
||||||
|
let text: Option<String> = parent
|
||||||
|
.element_formatter
|
||||||
|
.as_ref()
|
||||||
|
.map(|fmt| fmt(self, parent));
|
||||||
|
|
||||||
|
add_rulers_and_text(self, plot, text, shapes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RectElement for BoxElem {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
self.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds_min(&self) -> Value {
|
||||||
|
let argument = self.argument - self.box_width.max(self.whisker_width) / 2.0;
|
||||||
|
let value = self.spread.lower_whisker;
|
||||||
|
self.point_at(argument, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds_max(&self) -> Value {
|
||||||
|
let argument = self.argument + self.box_width.max(self.whisker_width) / 2.0;
|
||||||
|
let value = self.spread.upper_whisker;
|
||||||
|
self.point_at(argument, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn values_with_ruler(&self) -> Vec<Value> {
|
||||||
|
let median = self.point_at(self.argument, self.spread.median);
|
||||||
|
let q1 = self.point_at(self.argument, self.spread.quartile1);
|
||||||
|
let q3 = self.point_at(self.argument, self.spread.quartile3);
|
||||||
|
let upper = self.point_at(self.argument, self.spread.upper_whisker);
|
||||||
|
let lower = self.point_at(self.argument, self.spread.lower_whisker);
|
||||||
|
|
||||||
|
vec![median, q1, q3, upper, lower]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn orientation(&self) -> Orientation {
|
||||||
|
self.orientation
|
||||||
|
}
|
||||||
|
|
||||||
|
fn corner_value(&self) -> Value {
|
||||||
|
self.point_at(self.argument, self.spread.upper_whisker)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_values_format(&self, transform: &ScreenTransform) -> String {
|
||||||
|
let scale = transform.dvalue_dpos();
|
||||||
|
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||||
|
format!(
|
||||||
|
"\nMax = {max:.decimals$}\
|
||||||
|
\nQuartile 3 = {q3:.decimals$}\
|
||||||
|
\nMedian = {med:.decimals$}\
|
||||||
|
\nQuartile 1 = {q1:.decimals$}\
|
||||||
|
\nMin = {min:.decimals$}",
|
||||||
|
max = self.spread.upper_whisker,
|
||||||
|
q3 = self.spread.quartile3,
|
||||||
|
med = self.spread.median,
|
||||||
|
q1 = self.spread.quartile1,
|
||||||
|
min = self.spread.lower_whisker,
|
||||||
|
decimals = y_decimals
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,131 +1,89 @@
|
||||||
//! Contains items that can be added to a plot.
|
//! Contains items that can be added to a plot.
|
||||||
|
|
||||||
use std::ops::{Bound, RangeBounds, RangeInclusive};
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
use epaint::Mesh;
|
use epaint::Mesh;
|
||||||
|
|
||||||
use super::transform::{PlotBounds, ScreenTransform};
|
use crate::util::float_ord::FloatOrd;
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
use super::{PlotBounds, ScreenTransform};
|
||||||
|
use rect_elem::*;
|
||||||
|
use values::*;
|
||||||
|
|
||||||
|
pub use bar::Bar;
|
||||||
|
pub use box_elem::{BoxElem, BoxSpread};
|
||||||
|
pub use values::{LineStyle, MarkerShape, Value, Values};
|
||||||
|
|
||||||
|
mod bar;
|
||||||
|
mod box_elem;
|
||||||
|
mod rect_elem;
|
||||||
|
mod values;
|
||||||
|
|
||||||
const DEFAULT_FILL_ALPHA: f32 = 0.05;
|
const DEFAULT_FILL_ALPHA: f32 = 0.05;
|
||||||
|
|
||||||
/// A value in the value-space of the plot.
|
/// Container to pass-through several parameters related to plot visualization
|
||||||
///
|
pub(super) struct PlotConfig<'a> {
|
||||||
/// Uses f64 for improved accuracy to enable plotting
|
pub ui: &'a Ui,
|
||||||
/// large values (e.g. unix time on x axis).
|
pub transform: &'a ScreenTransform,
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
pub show_x: bool,
|
||||||
pub struct Value {
|
pub show_y: bool,
|
||||||
/// This is often something monotonically increasing, such as time, but doesn't have to be.
|
|
||||||
/// Goes from left to right.
|
|
||||||
pub x: f64,
|
|
||||||
/// Goes from bottom to top (inverse of everything else in egui!).
|
|
||||||
pub y: f64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Value {
|
/// Trait shared by things that can be drawn in the plot.
|
||||||
#[inline(always)]
|
pub(super) trait PlotItem {
|
||||||
pub fn new(x: impl Into<f64>, y: impl Into<f64>) -> Self {
|
fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>);
|
||||||
Self {
|
fn initialize(&mut self, x_range: RangeInclusive<f64>);
|
||||||
x: x.into(),
|
fn name(&self) -> &str;
|
||||||
y: y.into(),
|
fn color(&self) -> Color32;
|
||||||
}
|
fn highlight(&mut self);
|
||||||
}
|
fn highlighted(&self) -> bool;
|
||||||
|
fn geometry(&self) -> PlotGeometry<'_>;
|
||||||
|
fn get_bounds(&self) -> PlotBounds;
|
||||||
|
|
||||||
#[inline(always)]
|
fn find_closest(&self, point: Pos2, transform: &ScreenTransform) -> Option<ClosestElem> {
|
||||||
pub fn to_pos2(self) -> Pos2 {
|
match self.geometry() {
|
||||||
Pos2::new(self.x as f32, self.y as f32)
|
PlotGeometry::None => None,
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
PlotGeometry::Points(points) => points
|
||||||
pub fn to_vec2(self) -> Vec2 {
|
.iter()
|
||||||
Vec2::new(self.x as f32, self.y as f32)
|
.enumerate()
|
||||||
}
|
.map(|(index, value)| {
|
||||||
}
|
let pos = transform.position_from_value(value);
|
||||||
|
let dist_sq = point.distance_sq(pos);
|
||||||
|
ClosestElem { index, dist_sq }
|
||||||
|
})
|
||||||
|
.min_by_key(|e| e.dist_sq.ord()),
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
PlotGeometry::Rects => {
|
||||||
|
panic!("If the PlotItem is made of rects, it should implement find_closest()")
|
||||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
|
||||||
pub enum LineStyle {
|
|
||||||
Solid,
|
|
||||||
Dotted { spacing: f32 },
|
|
||||||
Dashed { length: f32 },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LineStyle {
|
|
||||||
pub fn dashed_loose() -> Self {
|
|
||||||
Self::Dashed { length: 10.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dashed_dense() -> Self {
|
|
||||||
Self::Dashed { length: 5.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dotted_loose() -> Self {
|
|
||||||
Self::Dotted { spacing: 10.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dotted_dense() -> Self {
|
|
||||||
Self::Dotted { spacing: 5.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn style_line(
|
|
||||||
&self,
|
|
||||||
line: Vec<Pos2>,
|
|
||||||
mut stroke: Stroke,
|
|
||||||
highlight: bool,
|
|
||||||
shapes: &mut Vec<Shape>,
|
|
||||||
) {
|
|
||||||
match line.len() {
|
|
||||||
0 => {}
|
|
||||||
1 => {
|
|
||||||
let mut radius = stroke.width / 2.0;
|
|
||||||
if highlight {
|
|
||||||
radius *= 2f32.sqrt();
|
|
||||||
}
|
|
||||||
shapes.push(Shape::circle_filled(line[0], radius, stroke.color));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
match self {
|
|
||||||
LineStyle::Solid => {
|
|
||||||
if highlight {
|
|
||||||
stroke.width *= 2.0;
|
|
||||||
}
|
|
||||||
shapes.push(Shape::line(line, stroke));
|
|
||||||
}
|
|
||||||
LineStyle::Dotted { spacing } => {
|
|
||||||
// Take the stroke width for the radius even though it's not "correct", otherwise
|
|
||||||
// the dots would become too small.
|
|
||||||
let mut radius = stroke.width;
|
|
||||||
if highlight {
|
|
||||||
radius *= 2f32.sqrt();
|
|
||||||
}
|
|
||||||
shapes.extend(Shape::dotted_line(&line, stroke.color, *spacing, radius));
|
|
||||||
}
|
|
||||||
LineStyle::Dashed { length } => {
|
|
||||||
if highlight {
|
|
||||||
stroke.width *= 2.0;
|
|
||||||
}
|
|
||||||
let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
|
|
||||||
shapes.extend(Shape::dashed_line(
|
|
||||||
&line,
|
|
||||||
stroke,
|
|
||||||
*length,
|
|
||||||
length * golden_ratio,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for LineStyle {
|
fn on_hover(&self, elem: ClosestElem, shapes: &mut Vec<Shape>, plot: &PlotConfig<'_>) {
|
||||||
fn to_string(&self) -> String {
|
let points = match self.geometry() {
|
||||||
match self {
|
PlotGeometry::Points(points) => points,
|
||||||
LineStyle::Solid => "Solid".into(),
|
PlotGeometry::None => {
|
||||||
LineStyle::Dotted { spacing } => format!("Dotted{}Px", spacing),
|
panic!("If the PlotItem has no geometry, on_hover() must not be called")
|
||||||
LineStyle::Dashed { length } => format!("Dashed{}Px", length),
|
|
||||||
}
|
}
|
||||||
|
PlotGeometry::Rects => {
|
||||||
|
panic!("If the PlotItem is made of rects, it should implement on_hover()")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let line_color = if plot.ui.visuals().dark_mode {
|
||||||
|
Color32::from_gray(100).additive()
|
||||||
|
} else {
|
||||||
|
Color32::from_black_alpha(180)
|
||||||
|
};
|
||||||
|
|
||||||
|
// this method is only called, if the value is in the result set of find_closest()
|
||||||
|
let value = points[elem.index];
|
||||||
|
let pointer = plot.transform.position_from_value(&value);
|
||||||
|
shapes.push(Shape::circle_filled(pointer, 3.0, line_color));
|
||||||
|
|
||||||
|
rulers_at_value(pointer, value, self.name(), plot, shapes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,8 +187,8 @@ impl PlotItem for HLine {
|
||||||
self.highlight
|
self.highlight
|
||||||
}
|
}
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
fn geometry(&self) -> PlotGeometry<'_> {
|
||||||
None
|
PlotGeometry::None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_bounds(&self) -> PlotBounds {
|
fn get_bounds(&self) -> PlotBounds {
|
||||||
|
@ -339,8 +297,8 @@ impl PlotItem for VLine {
|
||||||
self.highlight
|
self.highlight
|
||||||
}
|
}
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
fn geometry(&self) -> PlotGeometry<'_> {
|
||||||
None
|
PlotGeometry::None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_bounds(&self) -> PlotBounds {
|
fn get_bounds(&self) -> PlotBounds {
|
||||||
|
@ -351,203 +309,6 @@ impl PlotItem for VLine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait shared by things that can be drawn in the plot.
|
|
||||||
pub(super) trait PlotItem {
|
|
||||||
fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>);
|
|
||||||
fn initialize(&mut self, x_range: RangeInclusive<f64>);
|
|
||||||
fn name(&self) -> &str;
|
|
||||||
fn color(&self) -> Color32;
|
|
||||||
fn highlight(&mut self);
|
|
||||||
fn highlighted(&self) -> bool;
|
|
||||||
fn values(&self) -> Option<&Values>;
|
|
||||||
fn get_bounds(&self) -> PlotBounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Describes a function y = f(x) with an optional range for x and a number of points.
|
|
||||||
struct ExplicitGenerator {
|
|
||||||
function: Box<dyn Fn(f64) -> f64>,
|
|
||||||
x_range: RangeInclusive<f64>,
|
|
||||||
points: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Values {
|
|
||||||
pub(super) values: Vec<Value>,
|
|
||||||
generator: Option<ExplicitGenerator>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Values {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
values: Vec::new(),
|
|
||||||
generator: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Values {
|
|
||||||
pub fn from_values(values: Vec<Value>) -> Self {
|
|
||||||
Self {
|
|
||||||
values,
|
|
||||||
generator: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_values_iter(iter: impl Iterator<Item = Value>) -> Self {
|
|
||||||
Self::from_values(iter.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw a line based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points.
|
|
||||||
pub fn from_explicit_callback(
|
|
||||||
function: impl Fn(f64) -> f64 + 'static,
|
|
||||||
x_range: impl RangeBounds<f64>,
|
|
||||||
points: usize,
|
|
||||||
) -> Self {
|
|
||||||
let start = match x_range.start_bound() {
|
|
||||||
Bound::Included(x) | Bound::Excluded(x) => *x,
|
|
||||||
Bound::Unbounded => f64::NEG_INFINITY,
|
|
||||||
};
|
|
||||||
let end = match x_range.end_bound() {
|
|
||||||
Bound::Included(x) | Bound::Excluded(x) => *x,
|
|
||||||
Bound::Unbounded => f64::INFINITY,
|
|
||||||
};
|
|
||||||
let x_range = start..=end;
|
|
||||||
|
|
||||||
let generator = ExplicitGenerator {
|
|
||||||
function: Box::new(function),
|
|
||||||
x_range,
|
|
||||||
points,
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
values: Vec::new(),
|
|
||||||
generator: Some(generator),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw a line based on a function `(x,y)=f(t)`, a range for t and the number of points.
|
|
||||||
/// The range may be specified as start..end or as start..=end.
|
|
||||||
pub fn from_parametric_callback(
|
|
||||||
function: impl Fn(f64) -> (f64, f64),
|
|
||||||
t_range: impl RangeBounds<f64>,
|
|
||||||
points: usize,
|
|
||||||
) -> Self {
|
|
||||||
let start = match t_range.start_bound() {
|
|
||||||
Bound::Included(x) => x,
|
|
||||||
Bound::Excluded(_) => unreachable!(),
|
|
||||||
Bound::Unbounded => panic!("The range for parametric functions must be bounded!"),
|
|
||||||
};
|
|
||||||
let end = match t_range.end_bound() {
|
|
||||||
Bound::Included(x) | Bound::Excluded(x) => x,
|
|
||||||
Bound::Unbounded => panic!("The range for parametric functions must be bounded!"),
|
|
||||||
};
|
|
||||||
let last_point_included = matches!(t_range.end_bound(), Bound::Included(_));
|
|
||||||
let increment = if last_point_included {
|
|
||||||
(end - start) / (points - 1) as f64
|
|
||||||
} else {
|
|
||||||
(end - start) / points as f64
|
|
||||||
};
|
|
||||||
let values = (0..points).map(|i| {
|
|
||||||
let t = start + i as f64 * increment;
|
|
||||||
let (x, y) = function(t);
|
|
||||||
Value { x, y }
|
|
||||||
});
|
|
||||||
Self::from_values_iter(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// From a series of y-values.
|
|
||||||
/// The x-values will be the indices of these values
|
|
||||||
pub fn from_ys_f32(ys: &[f32]) -> Self {
|
|
||||||
let values: Vec<Value> = ys
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, &y)| Value {
|
|
||||||
x: i as f64,
|
|
||||||
y: y as f64,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
Self::from_values(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if there are no data points available and there is no function to generate any.
|
|
||||||
pub(super) fn is_empty(&self) -> bool {
|
|
||||||
self.generator.is_none() && self.values.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If initialized with a generator function, this will generate `n` evenly spaced points in the
|
|
||||||
/// given range.
|
|
||||||
pub(super) fn generate_points(&mut self, x_range: RangeInclusive<f64>) {
|
|
||||||
if let Some(generator) = self.generator.take() {
|
|
||||||
if let Some(intersection) = Self::range_intersection(&x_range, &generator.x_range) {
|
|
||||||
let increment =
|
|
||||||
(intersection.end() - intersection.start()) / (generator.points - 1) as f64;
|
|
||||||
self.values = (0..generator.points)
|
|
||||||
.map(|i| {
|
|
||||||
let x = intersection.start() + i as f64 * increment;
|
|
||||||
let y = (generator.function)(x);
|
|
||||||
Value { x, y }
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the intersection of two ranges if they intersect.
|
|
||||||
fn range_intersection(
|
|
||||||
range1: &RangeInclusive<f64>,
|
|
||||||
range2: &RangeInclusive<f64>,
|
|
||||||
) -> Option<RangeInclusive<f64>> {
|
|
||||||
let start = range1.start().max(*range2.start());
|
|
||||||
let end = range1.end().min(*range2.end());
|
|
||||||
(start < end).then(|| start..=end)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn get_bounds(&self) -> PlotBounds {
|
|
||||||
let mut bounds = PlotBounds::NOTHING;
|
|
||||||
self.values
|
|
||||||
.iter()
|
|
||||||
.for_each(|value| bounds.extend_with(value));
|
|
||||||
bounds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
||||||
pub enum MarkerShape {
|
|
||||||
Circle,
|
|
||||||
Diamond,
|
|
||||||
Square,
|
|
||||||
Cross,
|
|
||||||
Plus,
|
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
Asterisk,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MarkerShape {
|
|
||||||
/// Get a vector containing all marker shapes.
|
|
||||||
pub fn all() -> impl Iterator<Item = MarkerShape> {
|
|
||||||
[
|
|
||||||
Self::Circle,
|
|
||||||
Self::Diamond,
|
|
||||||
Self::Square,
|
|
||||||
Self::Cross,
|
|
||||||
Self::Plus,
|
|
||||||
Self::Up,
|
|
||||||
Self::Down,
|
|
||||||
Self::Left,
|
|
||||||
Self::Right,
|
|
||||||
Self::Asterisk,
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A series of values forming a path.
|
/// A series of values forming a path.
|
||||||
pub struct Line {
|
pub struct Line {
|
||||||
pub(super) series: Values,
|
pub(super) series: Values,
|
||||||
|
@ -706,8 +467,8 @@ impl PlotItem for Line {
|
||||||
self.highlight
|
self.highlight
|
||||||
}
|
}
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
fn geometry(&self) -> PlotGeometry<'_> {
|
||||||
Some(&self.series)
|
PlotGeometry::Points(&self.series.values)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_bounds(&self) -> PlotBounds {
|
fn get_bounds(&self) -> PlotBounds {
|
||||||
|
@ -836,8 +597,8 @@ impl PlotItem for Polygon {
|
||||||
self.highlight
|
self.highlight
|
||||||
}
|
}
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
fn geometry(&self) -> PlotGeometry<'_> {
|
||||||
Some(&self.series)
|
PlotGeometry::Points(&self.series.values)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_bounds(&self) -> PlotBounds {
|
fn get_bounds(&self) -> PlotBounds {
|
||||||
|
@ -949,8 +710,8 @@ impl PlotItem for Text {
|
||||||
self.highlight
|
self.highlight
|
||||||
}
|
}
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
fn geometry(&self) -> PlotGeometry<'_> {
|
||||||
None
|
PlotGeometry::None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_bounds(&self) -> PlotBounds {
|
fn get_bounds(&self) -> PlotBounds {
|
||||||
|
@ -1182,8 +943,8 @@ impl PlotItem for Points {
|
||||||
self.highlight
|
self.highlight
|
||||||
}
|
}
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
fn geometry(&self) -> PlotGeometry<'_> {
|
||||||
Some(&self.series)
|
PlotGeometry::Points(&self.series.values)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_bounds(&self) -> PlotBounds {
|
fn get_bounds(&self) -> PlotBounds {
|
||||||
|
@ -1297,8 +1058,8 @@ impl PlotItem for Arrows {
|
||||||
self.highlight
|
self.highlight
|
||||||
}
|
}
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
fn geometry(&self) -> PlotGeometry<'_> {
|
||||||
Some(&self.origins)
|
PlotGeometry::Points(&self.origins.values)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_bounds(&self) -> PlotBounds {
|
fn get_bounds(&self) -> PlotBounds {
|
||||||
|
@ -1427,8 +1188,8 @@ impl PlotItem for PlotImage {
|
||||||
self.highlight
|
self.highlight
|
||||||
}
|
}
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
fn geometry(&self) -> PlotGeometry<'_> {
|
||||||
None
|
PlotGeometry::None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_bounds(&self) -> PlotBounds {
|
fn get_bounds(&self) -> PlotBounds {
|
||||||
|
@ -1446,3 +1207,477 @@ impl PlotItem for PlotImage {
|
||||||
bounds
|
bounds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A bar chart.
|
||||||
|
pub struct BarChart {
|
||||||
|
pub(super) bars: Vec<Bar>,
|
||||||
|
pub(super) default_color: Color32,
|
||||||
|
pub(super) name: String,
|
||||||
|
/// A custom element formatter
|
||||||
|
pub(super) element_formatter: Option<Box<dyn Fn(&Bar, &BarChart) -> String>>,
|
||||||
|
highlight: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BarChart {
|
||||||
|
/// Create a bar chart. It defaults to vertically oriented elements.
|
||||||
|
pub fn new(bars: Vec<Bar>) -> BarChart {
|
||||||
|
BarChart {
|
||||||
|
bars,
|
||||||
|
default_color: Color32::TRANSPARENT,
|
||||||
|
name: String::new(),
|
||||||
|
element_formatter: None,
|
||||||
|
highlight: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the default color. It is set on all elements that do not already have a specific color.
|
||||||
|
/// This is the color that shows up in the legend.
|
||||||
|
/// It can be overridden at the bar level (see [[`Bar`]]).
|
||||||
|
/// Default is `Color32::TRANSPARENT` which means a color will be auto-assigned.
|
||||||
|
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
||||||
|
let plot_color = color.into();
|
||||||
|
self.default_color = plot_color;
|
||||||
|
self.bars.iter_mut().for_each(|b| {
|
||||||
|
if b.fill == Color32::TRANSPARENT && b.stroke.color == Color32::TRANSPARENT {
|
||||||
|
b.fill = plot_color.linear_multiply(0.2);
|
||||||
|
b.stroke.color = plot_color;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Name of this chart.
|
||||||
|
///
|
||||||
|
/// This name will show up in the plot legend, if legends are turned on. Multiple charts may
|
||||||
|
/// share the same name, in which case they will also share an entry in the legend.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub fn name(mut self, name: impl ToString) -> Self {
|
||||||
|
self.name = name.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set all elements to be in a vertical orientation.
|
||||||
|
/// Argument axis will be X and bar values will be on the Y axis.
|
||||||
|
pub fn vertical(mut self) -> Self {
|
||||||
|
self.bars.iter_mut().for_each(|b| {
|
||||||
|
b.orientation = Orientation::Vertical;
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set all elements to be in a horizontal orientation.
|
||||||
|
/// Argument axis will be Y and bar values will be on the X axis.
|
||||||
|
pub fn horizontal(mut self) -> Self {
|
||||||
|
self.bars.iter_mut().for_each(|b| {
|
||||||
|
b.orientation = Orientation::Horizontal;
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the width (thickness) of all its elements.
|
||||||
|
pub fn width(mut self, width: f64) -> Self {
|
||||||
|
self.bars.iter_mut().for_each(|b| {
|
||||||
|
b.bar_width = width;
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Highlight all plot elements.
|
||||||
|
pub fn highlight(mut self) -> Self {
|
||||||
|
self.highlight = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a custom way to format an element.
|
||||||
|
/// Can be used to display a set number of decimals or custom labels.
|
||||||
|
pub fn element_formatter(mut self, formatter: Box<dyn Fn(&Bar, &BarChart) -> String>) -> Self {
|
||||||
|
self.element_formatter = Some(formatter);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stacks the bars on top of another chart.
|
||||||
|
/// Positive values are stacked on top of other positive values.
|
||||||
|
/// Negative values are stacked below other negative values.
|
||||||
|
pub fn stack_on(mut self, others: &[&BarChart]) -> Self {
|
||||||
|
for (index, bar) in self.bars.iter_mut().enumerate() {
|
||||||
|
let new_base_offset = if bar.value.is_sign_positive() {
|
||||||
|
others
|
||||||
|
.iter()
|
||||||
|
.filter_map(|other_chart| other_chart.bars.get(index).map(|bar| bar.upper()))
|
||||||
|
.max_by_key(|value| value.ord())
|
||||||
|
} else {
|
||||||
|
others
|
||||||
|
.iter()
|
||||||
|
.filter_map(|other_chart| other_chart.bars.get(index).map(|bar| bar.lower()))
|
||||||
|
.min_by_key(|value| value.ord())
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(value) = new_base_offset {
|
||||||
|
bar.base_offset = Some(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlotItem for BarChart {
|
||||||
|
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||||
|
self.bars.iter().for_each(|b| {
|
||||||
|
b.add_shapes(transform, self.highlight, shapes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
self.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn color(&self) -> Color32 {
|
||||||
|
self.default_color
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight(&mut self) {
|
||||||
|
self.highlight = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlighted(&self) -> bool {
|
||||||
|
self.highlight
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geometry(&self) -> PlotGeometry<'_> {
|
||||||
|
PlotGeometry::Rects
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_bounds(&self) -> PlotBounds {
|
||||||
|
let mut bounds = PlotBounds::NOTHING;
|
||||||
|
self.bars.iter().for_each(|b| {
|
||||||
|
bounds.merge(&b.bounds());
|
||||||
|
});
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_closest(&self, point: Pos2, transform: &ScreenTransform) -> Option<ClosestElem> {
|
||||||
|
find_closest_rect(&self.bars, point, transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_hover(&self, elem: ClosestElem, shapes: &mut Vec<Shape>, plot: &PlotConfig<'_>) {
|
||||||
|
let bar = &self.bars[elem.index];
|
||||||
|
|
||||||
|
bar.add_shapes(plot.transform, true, shapes);
|
||||||
|
bar.add_rulers_and_text(self, plot, shapes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A diagram containing a series of [`BoxElem`] elements.
|
||||||
|
pub struct BoxPlot {
|
||||||
|
pub(super) boxes: Vec<BoxElem>,
|
||||||
|
pub(super) default_color: Color32,
|
||||||
|
pub(super) name: String,
|
||||||
|
/// A custom element formatter
|
||||||
|
pub(super) element_formatter: Option<Box<dyn Fn(&BoxElem, &BoxPlot) -> String>>,
|
||||||
|
highlight: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoxPlot {
|
||||||
|
/// Create a plot containing multiple `boxes`. It defaults to vertically oriented elements.
|
||||||
|
pub fn new(boxes: Vec<BoxElem>) -> Self {
|
||||||
|
Self {
|
||||||
|
boxes,
|
||||||
|
default_color: Color32::TRANSPARENT,
|
||||||
|
name: String::new(),
|
||||||
|
element_formatter: None,
|
||||||
|
highlight: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the default color. It is set on all elements that do not already have a specific color.
|
||||||
|
/// This is the color that shows up in the legend.
|
||||||
|
/// It can be overridden at the element level (see [`BoxElem`]).
|
||||||
|
/// Default is `Color32::TRANSPARENT` which means a color will be auto-assigned.
|
||||||
|
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
||||||
|
let plot_color = color.into();
|
||||||
|
self.default_color = plot_color;
|
||||||
|
self.boxes.iter_mut().for_each(|box_elem| {
|
||||||
|
if box_elem.fill == Color32::TRANSPARENT
|
||||||
|
&& box_elem.stroke.color == Color32::TRANSPARENT
|
||||||
|
{
|
||||||
|
box_elem.fill = plot_color.linear_multiply(0.2);
|
||||||
|
box_elem.stroke.color = plot_color;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Name of this box plot diagram.
|
||||||
|
///
|
||||||
|
/// This name will show up in the plot legend, if legends are turned on. Multiple series may
|
||||||
|
/// share the same name, in which case they will also share an entry in the legend.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub fn name(mut self, name: impl ToString) -> Self {
|
||||||
|
self.name = name.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set all elements to be in a vertical orientation.
|
||||||
|
/// Argument axis will be X and values will be on the Y axis.
|
||||||
|
pub fn vertical(mut self) -> Self {
|
||||||
|
self.boxes.iter_mut().for_each(|box_elem| {
|
||||||
|
box_elem.orientation = Orientation::Vertical;
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set all elements to be in a horizontal orientation.
|
||||||
|
/// Argument axis will be Y and values will be on the X axis.
|
||||||
|
pub fn horizontal(mut self) -> Self {
|
||||||
|
self.boxes.iter_mut().for_each(|box_elem| {
|
||||||
|
box_elem.orientation = Orientation::Horizontal;
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Highlight all plot elements.
|
||||||
|
pub fn highlight(mut self) -> Self {
|
||||||
|
self.highlight = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a custom way to format an element.
|
||||||
|
/// Can be used to display a set number of decimals or custom labels.
|
||||||
|
pub fn element_formatter(
|
||||||
|
mut self,
|
||||||
|
formatter: Box<dyn Fn(&BoxElem, &BoxPlot) -> String>,
|
||||||
|
) -> Self {
|
||||||
|
self.element_formatter = Some(formatter);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlotItem for BoxPlot {
|
||||||
|
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||||
|
self.boxes.iter().for_each(|b| {
|
||||||
|
b.add_shapes(transform, self.highlight, shapes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
self.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn color(&self) -> Color32 {
|
||||||
|
self.default_color
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight(&mut self) {
|
||||||
|
self.highlight = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlighted(&self) -> bool {
|
||||||
|
self.highlight
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geometry(&self) -> PlotGeometry<'_> {
|
||||||
|
PlotGeometry::Rects
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_bounds(&self) -> PlotBounds {
|
||||||
|
let mut bounds = PlotBounds::NOTHING;
|
||||||
|
self.boxes.iter().for_each(|b| {
|
||||||
|
bounds.merge(&b.bounds());
|
||||||
|
});
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_closest(&self, point: Pos2, transform: &ScreenTransform) -> Option<ClosestElem> {
|
||||||
|
find_closest_rect(&self.boxes, point, transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_hover(&self, elem: ClosestElem, shapes: &mut Vec<Shape>, plot: &PlotConfig<'_>) {
|
||||||
|
let box_plot = &self.boxes[elem.index];
|
||||||
|
|
||||||
|
box_plot.add_shapes(plot.transform, true, shapes);
|
||||||
|
box_plot.add_rulers_and_text(self, plot, shapes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Helper functions
|
||||||
|
|
||||||
|
fn rulers_color(ui: &Ui) -> Color32 {
|
||||||
|
if ui.visuals().dark_mode {
|
||||||
|
Color32::from_gray(100).additive()
|
||||||
|
} else {
|
||||||
|
Color32::from_black_alpha(180)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vertical_line(pointer: Pos2, transform: &ScreenTransform, line_color: Color32) -> Shape {
|
||||||
|
let frame = transform.frame();
|
||||||
|
Shape::line_segment(
|
||||||
|
[
|
||||||
|
pos2(pointer.x, frame.top()),
|
||||||
|
pos2(pointer.x, frame.bottom()),
|
||||||
|
],
|
||||||
|
(1.0, line_color),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn horizontal_line(pointer: Pos2, transform: &ScreenTransform, line_color: Color32) -> Shape {
|
||||||
|
let frame = transform.frame();
|
||||||
|
Shape::line_segment(
|
||||||
|
[
|
||||||
|
pos2(frame.left(), pointer.y),
|
||||||
|
pos2(frame.right(), pointer.y),
|
||||||
|
],
|
||||||
|
(1.0, line_color),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_rulers_and_text(
|
||||||
|
elem: &dyn RectElement,
|
||||||
|
plot: &PlotConfig<'_>,
|
||||||
|
text: Option<String>,
|
||||||
|
shapes: &mut Vec<Shape>,
|
||||||
|
) {
|
||||||
|
let orientation = elem.orientation();
|
||||||
|
let show_argument = plot.show_x && orientation == Orientation::Vertical
|
||||||
|
|| plot.show_y && orientation == Orientation::Horizontal;
|
||||||
|
let show_values = plot.show_y && orientation == Orientation::Vertical
|
||||||
|
|| plot.show_x && orientation == Orientation::Horizontal;
|
||||||
|
|
||||||
|
let line_color = rulers_color(plot.ui);
|
||||||
|
|
||||||
|
// Rulers for argument (usually vertical)
|
||||||
|
if show_argument {
|
||||||
|
let push_argument_ruler = |argument: Value, shapes: &mut Vec<Shape>| {
|
||||||
|
let position = plot.transform.position_from_value(&argument);
|
||||||
|
let line = match orientation {
|
||||||
|
Orientation::Horizontal => horizontal_line(position, plot.transform, line_color),
|
||||||
|
Orientation::Vertical => vertical_line(position, plot.transform, line_color),
|
||||||
|
};
|
||||||
|
shapes.push(line);
|
||||||
|
};
|
||||||
|
|
||||||
|
for pos in elem.arguments_with_ruler() {
|
||||||
|
push_argument_ruler(pos, shapes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rulers for values (usually horizontal)
|
||||||
|
if show_values {
|
||||||
|
let push_value_ruler = |value: Value, shapes: &mut Vec<Shape>| {
|
||||||
|
let position = plot.transform.position_from_value(&value);
|
||||||
|
let line = match orientation {
|
||||||
|
Orientation::Horizontal => vertical_line(position, plot.transform, line_color),
|
||||||
|
Orientation::Vertical => horizontal_line(position, plot.transform, line_color),
|
||||||
|
};
|
||||||
|
shapes.push(line);
|
||||||
|
};
|
||||||
|
|
||||||
|
for pos in elem.values_with_ruler() {
|
||||||
|
push_value_ruler(pos, shapes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text
|
||||||
|
let text = text.unwrap_or({
|
||||||
|
let mut text = elem.name().to_string(); // could be empty
|
||||||
|
|
||||||
|
if show_values {
|
||||||
|
text.push_str(&elem.default_values_format(plot.transform));
|
||||||
|
}
|
||||||
|
|
||||||
|
text
|
||||||
|
});
|
||||||
|
|
||||||
|
let corner_value = elem.corner_value();
|
||||||
|
shapes.push(Shape::text(
|
||||||
|
plot.ui.fonts(),
|
||||||
|
plot.transform.position_from_value(&corner_value) + vec2(3.0, -2.0),
|
||||||
|
Align2::LEFT_BOTTOM,
|
||||||
|
text,
|
||||||
|
TextStyle::Body,
|
||||||
|
plot.ui.visuals().text_color(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a cross of horizontal and vertical ruler at the `pointer` position.
|
||||||
|
/// `value` is used to for text displaying X/Y coordinates.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(super) fn rulers_at_value(
|
||||||
|
pointer: Pos2,
|
||||||
|
value: Value,
|
||||||
|
name: &str,
|
||||||
|
plot: &PlotConfig<'_>,
|
||||||
|
shapes: &mut Vec<Shape>,
|
||||||
|
) {
|
||||||
|
let line_color = rulers_color(plot.ui);
|
||||||
|
if plot.show_x {
|
||||||
|
shapes.push(vertical_line(pointer, plot.transform, line_color));
|
||||||
|
}
|
||||||
|
if plot.show_y {
|
||||||
|
shapes.push(horizontal_line(pointer, plot.transform, line_color));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut prefix = String::new();
|
||||||
|
|
||||||
|
if !name.is_empty() {
|
||||||
|
prefix = format!("{}\n", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = {
|
||||||
|
let scale = plot.transform.dvalue_dpos();
|
||||||
|
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||||
|
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||||
|
if plot.show_x && plot.show_y {
|
||||||
|
format!(
|
||||||
|
"{}x = {:.*}\ny = {:.*}",
|
||||||
|
prefix, x_decimals, value.x, y_decimals, value.y
|
||||||
|
)
|
||||||
|
} else if plot.show_x {
|
||||||
|
format!("{}x = {:.*}", prefix, x_decimals, value.x)
|
||||||
|
} else if plot.show_y {
|
||||||
|
format!("{}y = {:.*}", prefix, y_decimals, value.y)
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
shapes.push(Shape::text(
|
||||||
|
plot.ui.fonts(),
|
||||||
|
pointer + vec2(3.0, -2.0),
|
||||||
|
Align2::LEFT_BOTTOM,
|
||||||
|
text,
|
||||||
|
TextStyle::Body,
|
||||||
|
plot.ui.visuals().text_color(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_closest_rect<'a, T>(
|
||||||
|
rects: impl IntoIterator<Item = &'a T>,
|
||||||
|
point: Pos2,
|
||||||
|
transform: &ScreenTransform,
|
||||||
|
) -> Option<ClosestElem>
|
||||||
|
where
|
||||||
|
T: 'a + RectElement,
|
||||||
|
{
|
||||||
|
rects
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, bar)| {
|
||||||
|
let bar_rect: Rect = transform.rect_from_values(&bar.bounds_min(), &bar.bounds_max());
|
||||||
|
let dist_sq = bar_rect.distance_sq_to_pos(point);
|
||||||
|
|
||||||
|
ClosestElem { index, dist_sq }
|
||||||
|
})
|
||||||
|
.min_by_key(|e| e.dist_sq.ord())
|
||||||
|
}
|
61
egui/src/widgets/plot/items/rect_elem.rs
Normal file
61
egui/src/widgets/plot/items/rect_elem.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use super::{Orientation, Value};
|
||||||
|
use crate::plot::transform::{PlotBounds, ScreenTransform};
|
||||||
|
use epaint::emath::NumExt;
|
||||||
|
use epaint::{Color32, Rgba, Stroke};
|
||||||
|
|
||||||
|
/// Trait that abstracts from rectangular 'Value'-like elements, such as bars or boxes
|
||||||
|
pub(super) trait RectElement {
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
fn bounds_min(&self) -> Value;
|
||||||
|
fn bounds_max(&self) -> Value;
|
||||||
|
|
||||||
|
fn bounds(&self) -> PlotBounds {
|
||||||
|
let mut bounds = PlotBounds::NOTHING;
|
||||||
|
bounds.extend_with(&self.bounds_min());
|
||||||
|
bounds.extend_with(&self.bounds_max());
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
/// At which argument (input; usually X) there is a ruler (usually vertical)
|
||||||
|
fn arguments_with_ruler(&self) -> Vec<Value> {
|
||||||
|
// Default: one at center
|
||||||
|
vec![self.bounds().center()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// At which value (output; usually Y) there is a ruler (usually horizontal)
|
||||||
|
fn values_with_ruler(&self) -> Vec<Value>;
|
||||||
|
|
||||||
|
/// The diagram's orientation (vertical/horizontal)
|
||||||
|
fn orientation(&self) -> Orientation;
|
||||||
|
|
||||||
|
/// Get X/Y-value for (argument, value) pair, taking into account orientation
|
||||||
|
fn point_at(&self, argument: f64, value: f64) -> Value {
|
||||||
|
match self.orientation() {
|
||||||
|
Orientation::Horizontal => Value::new(value, argument),
|
||||||
|
Orientation::Vertical => Value::new(argument, value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Right top of the rectangle (position of text)
|
||||||
|
fn corner_value(&self) -> Value {
|
||||||
|
//self.point_at(self.position + self.width / 2.0, value)
|
||||||
|
Value {
|
||||||
|
x: self.bounds_max().x,
|
||||||
|
y: self.bounds_max().y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Debug formatting for hovered-over value, if none is specified by the user
|
||||||
|
fn default_values_format(&self, transform: &ScreenTransform) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Helper functions
|
||||||
|
|
||||||
|
pub(super) fn highlighted_color(mut stroke: Stroke, fill: Color32) -> (Stroke, Color32) {
|
||||||
|
stroke.width *= 2.0;
|
||||||
|
let fill = Rgba::from(fill);
|
||||||
|
let fill_alpha = (2.0 * fill.a()).at_most(1.0);
|
||||||
|
let fill = fill.to_opaque().multiply(fill_alpha);
|
||||||
|
(stroke, fill.into())
|
||||||
|
}
|
352
egui/src/widgets/plot/items/values.rs
Normal file
352
egui/src/widgets/plot/items/values.rs
Normal file
|
@ -0,0 +1,352 @@
|
||||||
|
use epaint::{Pos2, Shape, Stroke, Vec2};
|
||||||
|
use std::ops::{Bound, RangeBounds, RangeInclusive};
|
||||||
|
|
||||||
|
use crate::plot::transform::PlotBounds;
|
||||||
|
|
||||||
|
/// A value in the value-space of the plot.
|
||||||
|
///
|
||||||
|
/// Uses f64 for improved accuracy to enable plotting
|
||||||
|
/// large values (e.g. unix time on x axis).
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Value {
|
||||||
|
/// This is often something monotonically increasing, such as time, but doesn't have to be.
|
||||||
|
/// Goes from left to right.
|
||||||
|
pub x: f64,
|
||||||
|
/// Goes from bottom to top (inverse of everything else in egui!).
|
||||||
|
pub y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn new(x: impl Into<f64>, y: impl Into<f64>) -> Self {
|
||||||
|
Self {
|
||||||
|
x: x.into(),
|
||||||
|
y: y.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn to_pos2(self) -> Pos2 {
|
||||||
|
Pos2::new(self.x as f32, self.y as f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn to_vec2(self) -> Vec2 {
|
||||||
|
Vec2::new(self.x as f32, self.y as f32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
pub enum LineStyle {
|
||||||
|
Solid,
|
||||||
|
Dotted { spacing: f32 },
|
||||||
|
Dashed { length: f32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineStyle {
|
||||||
|
pub fn dashed_loose() -> Self {
|
||||||
|
Self::Dashed { length: 10.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dashed_dense() -> Self {
|
||||||
|
Self::Dashed { length: 5.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dotted_loose() -> Self {
|
||||||
|
Self::Dotted { spacing: 10.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dotted_dense() -> Self {
|
||||||
|
Self::Dotted { spacing: 5.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn style_line(
|
||||||
|
&self,
|
||||||
|
line: Vec<Pos2>,
|
||||||
|
mut stroke: Stroke,
|
||||||
|
highlight: bool,
|
||||||
|
shapes: &mut Vec<Shape>,
|
||||||
|
) {
|
||||||
|
match line.len() {
|
||||||
|
0 => {}
|
||||||
|
1 => {
|
||||||
|
let mut radius = stroke.width / 2.0;
|
||||||
|
if highlight {
|
||||||
|
radius *= 2f32.sqrt();
|
||||||
|
}
|
||||||
|
shapes.push(Shape::circle_filled(line[0], radius, stroke.color));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
match self {
|
||||||
|
LineStyle::Solid => {
|
||||||
|
if highlight {
|
||||||
|
stroke.width *= 2.0;
|
||||||
|
}
|
||||||
|
shapes.push(Shape::line(line, stroke));
|
||||||
|
}
|
||||||
|
LineStyle::Dotted { spacing } => {
|
||||||
|
// Take the stroke width for the radius even though it's not "correct", otherwise
|
||||||
|
// the dots would become too small.
|
||||||
|
let mut radius = stroke.width;
|
||||||
|
if highlight {
|
||||||
|
radius *= 2f32.sqrt();
|
||||||
|
}
|
||||||
|
shapes.extend(Shape::dotted_line(&line, stroke.color, *spacing, radius));
|
||||||
|
}
|
||||||
|
LineStyle::Dashed { length } => {
|
||||||
|
if highlight {
|
||||||
|
stroke.width *= 2.0;
|
||||||
|
}
|
||||||
|
let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
|
||||||
|
shapes.extend(Shape::dashed_line(
|
||||||
|
&line,
|
||||||
|
stroke,
|
||||||
|
*length,
|
||||||
|
length * golden_ratio,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for LineStyle {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
LineStyle::Solid => "Solid".into(),
|
||||||
|
LineStyle::Dotted { spacing } => format!("Dotted{}Px", spacing),
|
||||||
|
LineStyle::Dashed { length } => format!("Dashed{}Px", length),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Orientation {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Orientation {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Vertical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub struct Values {
|
||||||
|
pub(super) values: Vec<Value>,
|
||||||
|
generator: Option<ExplicitGenerator>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Values {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
values: Vec::new(),
|
||||||
|
generator: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Values {
|
||||||
|
pub fn from_values(values: Vec<Value>) -> Self {
|
||||||
|
Self {
|
||||||
|
values,
|
||||||
|
generator: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_values_iter(iter: impl Iterator<Item = Value>) -> Self {
|
||||||
|
Self::from_values(iter.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a line based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points.
|
||||||
|
pub fn from_explicit_callback(
|
||||||
|
function: impl Fn(f64) -> f64 + 'static,
|
||||||
|
x_range: impl RangeBounds<f64>,
|
||||||
|
points: usize,
|
||||||
|
) -> Self {
|
||||||
|
let start = match x_range.start_bound() {
|
||||||
|
Bound::Included(x) | Bound::Excluded(x) => *x,
|
||||||
|
Bound::Unbounded => f64::NEG_INFINITY,
|
||||||
|
};
|
||||||
|
let end = match x_range.end_bound() {
|
||||||
|
Bound::Included(x) | Bound::Excluded(x) => *x,
|
||||||
|
Bound::Unbounded => f64::INFINITY,
|
||||||
|
};
|
||||||
|
let x_range = start..=end;
|
||||||
|
|
||||||
|
let generator = ExplicitGenerator {
|
||||||
|
function: Box::new(function),
|
||||||
|
x_range,
|
||||||
|
points,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
values: Vec::new(),
|
||||||
|
generator: Some(generator),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a line based on a function `(x,y)=f(t)`, a range for t and the number of points.
|
||||||
|
/// The range may be specified as start..end or as start..=end.
|
||||||
|
pub fn from_parametric_callback(
|
||||||
|
function: impl Fn(f64) -> (f64, f64),
|
||||||
|
t_range: impl RangeBounds<f64>,
|
||||||
|
points: usize,
|
||||||
|
) -> Self {
|
||||||
|
let start = match t_range.start_bound() {
|
||||||
|
Bound::Included(x) => x,
|
||||||
|
Bound::Excluded(_) => unreachable!(),
|
||||||
|
Bound::Unbounded => panic!("The range for parametric functions must be bounded!"),
|
||||||
|
};
|
||||||
|
let end = match t_range.end_bound() {
|
||||||
|
Bound::Included(x) | Bound::Excluded(x) => x,
|
||||||
|
Bound::Unbounded => panic!("The range for parametric functions must be bounded!"),
|
||||||
|
};
|
||||||
|
let last_point_included = matches!(t_range.end_bound(), Bound::Included(_));
|
||||||
|
let increment = if last_point_included {
|
||||||
|
(end - start) / (points - 1) as f64
|
||||||
|
} else {
|
||||||
|
(end - start) / points as f64
|
||||||
|
};
|
||||||
|
let values = (0..points).map(|i| {
|
||||||
|
let t = start + i as f64 * increment;
|
||||||
|
let (x, y) = function(t);
|
||||||
|
Value { x, y }
|
||||||
|
});
|
||||||
|
Self::from_values_iter(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From a series of y-values.
|
||||||
|
/// The x-values will be the indices of these values
|
||||||
|
pub fn from_ys_f32(ys: &[f32]) -> Self {
|
||||||
|
let values: Vec<Value> = ys
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, &y)| Value {
|
||||||
|
x: i as f64,
|
||||||
|
y: y as f64,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Self::from_values(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if there are no data points available and there is no function to generate any.
|
||||||
|
pub(crate) fn is_empty(&self) -> bool {
|
||||||
|
self.generator.is_none() && self.values.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If initialized with a generator function, this will generate `n` evenly spaced points in the
|
||||||
|
/// given range.
|
||||||
|
pub(super) fn generate_points(&mut self, x_range: RangeInclusive<f64>) {
|
||||||
|
if let Some(generator) = self.generator.take() {
|
||||||
|
if let Some(intersection) = Self::range_intersection(&x_range, &generator.x_range) {
|
||||||
|
let increment =
|
||||||
|
(intersection.end() - intersection.start()) / (generator.points - 1) as f64;
|
||||||
|
self.values = (0..generator.points)
|
||||||
|
.map(|i| {
|
||||||
|
let x = intersection.start() + i as f64 * increment;
|
||||||
|
let y = (generator.function)(x);
|
||||||
|
Value { x, y }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the intersection of two ranges if they intersect.
|
||||||
|
fn range_intersection(
|
||||||
|
range1: &RangeInclusive<f64>,
|
||||||
|
range2: &RangeInclusive<f64>,
|
||||||
|
) -> Option<RangeInclusive<f64>> {
|
||||||
|
let start = range1.start().max(*range2.start());
|
||||||
|
let end = range1.end().min(*range2.end());
|
||||||
|
(start < end).then(|| start..=end)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_bounds(&self) -> PlotBounds {
|
||||||
|
let mut bounds = PlotBounds::NOTHING;
|
||||||
|
self.values
|
||||||
|
.iter()
|
||||||
|
.for_each(|value| bounds.extend_with(value));
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum MarkerShape {
|
||||||
|
Circle,
|
||||||
|
Diamond,
|
||||||
|
Square,
|
||||||
|
Cross,
|
||||||
|
Plus,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Asterisk,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MarkerShape {
|
||||||
|
/// Get a vector containing all marker shapes.
|
||||||
|
pub fn all() -> impl Iterator<Item = MarkerShape> {
|
||||||
|
[
|
||||||
|
Self::Circle,
|
||||||
|
Self::Diamond,
|
||||||
|
Self::Square,
|
||||||
|
Self::Cross,
|
||||||
|
Self::Plus,
|
||||||
|
Self::Up,
|
||||||
|
Self::Down,
|
||||||
|
Self::Left,
|
||||||
|
Self::Right,
|
||||||
|
Self::Asterisk,
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Query the values of the plot, for geometric relations like closest checks
|
||||||
|
pub(crate) enum PlotGeometry<'a> {
|
||||||
|
/// No geometry based on single elements (examples: text, image, horizontal/vertical line)
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// Point values (X-Y graphs)
|
||||||
|
Points(&'a [Value]),
|
||||||
|
|
||||||
|
/// Rectangles (examples: boxes or bars)
|
||||||
|
// Has currently no data, as it would require copying rects or iterating a list of pointers.
|
||||||
|
// Instead, geometry-based functions are directly implemented in the respective PlotItem impl.
|
||||||
|
Rects,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Describes a function y = f(x) with an optional range for x and a number of points.
|
||||||
|
struct ExplicitGenerator {
|
||||||
|
function: Box<dyn Fn(f64) -> f64>,
|
||||||
|
x_range: RangeInclusive<f64>,
|
||||||
|
points: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Result of [`PlotItem::find_closest()`] search, identifies an element inside the item for immediate use
|
||||||
|
pub(crate) struct ClosestElem {
|
||||||
|
/// Position of hovered-over value (or bar/box-plot/...) in PlotItem
|
||||||
|
pub index: usize,
|
||||||
|
|
||||||
|
/// Squared distance from the mouse cursor (needed to compare against other PlotItems, which might be nearer)
|
||||||
|
pub dist_sq: f32,
|
||||||
|
}
|
|
@ -1,23 +1,23 @@
|
||||||
//! Simple plotting library.
|
//! Simple plotting library.
|
||||||
|
|
||||||
|
use crate::util::float_ord::FloatOrd;
|
||||||
|
use crate::*;
|
||||||
|
use color::Hsva;
|
||||||
|
use epaint::ahash::AHashSet;
|
||||||
|
use items::PlotItem;
|
||||||
|
use legend::LegendWidget;
|
||||||
|
use transform::{PlotBounds, ScreenTransform};
|
||||||
|
|
||||||
|
pub use items::{
|
||||||
|
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape,
|
||||||
|
PlotImage, Points, Polygon, Text, VLine, Value, Values,
|
||||||
|
};
|
||||||
|
pub use legend::{Corner, Legend};
|
||||||
|
|
||||||
mod items;
|
mod items;
|
||||||
mod legend;
|
mod legend;
|
||||||
mod transform;
|
mod transform;
|
||||||
|
|
||||||
use items::PlotItem;
|
|
||||||
pub use items::{
|
|
||||||
Arrows, HLine, Line, LineStyle, MarkerShape, PlotImage, Points, Polygon, Text, VLine, Value,
|
|
||||||
Values,
|
|
||||||
};
|
|
||||||
use legend::LegendWidget;
|
|
||||||
pub use legend::{Corner, Legend};
|
|
||||||
pub use transform::PlotBounds;
|
|
||||||
use transform::ScreenTransform;
|
|
||||||
|
|
||||||
use crate::*;
|
|
||||||
use color::Hsva;
|
|
||||||
use epaint::ahash::AHashSet;
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Information about the plot that has to persist between frames.
|
/// Information about the plot that has to persist between frames.
|
||||||
|
@ -581,6 +581,32 @@ impl PlotUi {
|
||||||
}
|
}
|
||||||
self.items.push(Box::new(vline));
|
self.items.push(Box::new(vline));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a box plot diagram.
|
||||||
|
pub fn box_plot(&mut self, mut box_plot: BoxPlot) {
|
||||||
|
if box_plot.boxes.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the elements an automatic color if no color has been assigned.
|
||||||
|
if box_plot.default_color == Color32::TRANSPARENT {
|
||||||
|
box_plot = box_plot.color(self.auto_color());
|
||||||
|
}
|
||||||
|
self.items.push(Box::new(box_plot));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a bar chart.
|
||||||
|
pub fn bar_chart(&mut self, mut chart: BarChart) {
|
||||||
|
if chart.bars.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the elements an automatic color if no color has been assigned.
|
||||||
|
if chart.default_color == Color32::TRANSPARENT {
|
||||||
|
chart = chart.color(self.auto_color());
|
||||||
|
}
|
||||||
|
self.items.push(Box::new(chart));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PreparedPlot {
|
struct PreparedPlot {
|
||||||
|
@ -713,88 +739,31 @@ impl PreparedPlot {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let interact_radius: f32 = 16.0;
|
let interact_radius_sq: f32 = (16.0f32).powi(2);
|
||||||
let mut closest_value = None;
|
|
||||||
let mut closest_item = None;
|
|
||||||
let mut closest_dist_sq = interact_radius.powi(2);
|
|
||||||
for item in items {
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut prefix = String::new();
|
let candidates = items.iter().filter_map(|item| {
|
||||||
if let Some(name) = closest_item {
|
let item = &**item;
|
||||||
if !name.is_empty() {
|
let closest = item.find_closest(pointer, transform);
|
||||||
prefix = format!("{}\n", name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let line_color = if ui.visuals().dark_mode {
|
Some(item).zip(closest)
|
||||||
Color32::from_gray(100).additive()
|
});
|
||||||
} else {
|
|
||||||
Color32::from_black_alpha(180)
|
let closest = candidates
|
||||||
|
.min_by_key(|(_, elem)| elem.dist_sq.ord())
|
||||||
|
.filter(|(_, elem)| elem.dist_sq <= interact_radius_sq);
|
||||||
|
|
||||||
|
let plot = items::PlotConfig {
|
||||||
|
ui,
|
||||||
|
transform,
|
||||||
|
show_x: *show_x,
|
||||||
|
show_y: *show_y,
|
||||||
};
|
};
|
||||||
|
|
||||||
let value = if let Some(value) = closest_value {
|
if let Some((item, elem)) = closest {
|
||||||
let position = transform.position_from_value(value);
|
item.on_hover(elem, shapes, &plot);
|
||||||
shapes.push(Shape::circle_filled(position, 3.0, line_color));
|
|
||||||
*value
|
|
||||||
} else {
|
} else {
|
||||||
transform.value_from_position(pointer)
|
let value = transform.value_from_position(pointer);
|
||||||
};
|
items::rulers_at_value(pointer, value, "", &plot, shapes);
|
||||||
let pointer = transform.position_from_value(&value);
|
}
|
||||||
|
|
||||||
let rect = transform.frame();
|
|
||||||
|
|
||||||
if *show_x {
|
|
||||||
// vertical line
|
|
||||||
shapes.push(Shape::line_segment(
|
|
||||||
[pos2(pointer.x, rect.top()), pos2(pointer.x, rect.bottom())],
|
|
||||||
(1.0, line_color),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if *show_y {
|
|
||||||
// horizontal line
|
|
||||||
shapes.push(Shape::line_segment(
|
|
||||||
[pos2(rect.left(), pointer.y), pos2(rect.right(), pointer.y)],
|
|
||||||
(1.0, line_color),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = {
|
|
||||||
let scale = transform.dvalue_dpos();
|
|
||||||
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
|
||||||
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
|
||||||
if *show_x && *show_y {
|
|
||||||
format!(
|
|
||||||
"{}x = {:.*}\ny = {:.*}",
|
|
||||||
prefix, x_decimals, value.x, y_decimals, value.y
|
|
||||||
)
|
|
||||||
} else if *show_x {
|
|
||||||
format!("{}x = {:.*}", prefix, x_decimals, value.x)
|
|
||||||
} else if *show_y {
|
|
||||||
format!("{}y = {:.*}", prefix, y_decimals, value.y)
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
shapes.push(Shape::text(
|
|
||||||
ui.fonts(),
|
|
||||||
pointer + vec2(3.0, -2.0),
|
|
||||||
Align2::LEFT_BOTTOM,
|
|
||||||
text,
|
|
||||||
TextStyle::Body,
|
|
||||||
ui.visuals().text_color(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,14 @@ impl PlotBounds {
|
||||||
self.max[1] - self.min[1]
|
self.max[1] - self.min[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn center(&self) -> Value {
|
||||||
|
Value {
|
||||||
|
x: (self.min[0] + self.max[0]) / 2.0,
|
||||||
|
y: (self.min[1] + self.max[1]) / 2.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expand to include the given (x,y) value
|
||||||
pub(crate) fn extend_with(&mut self, value: &Value) {
|
pub(crate) fn extend_with(&mut self, value: &Value) {
|
||||||
self.extend_with_x(value.x);
|
self.extend_with_x(value.x);
|
||||||
self.extend_with_y(value.y);
|
self.extend_with_y(value.y);
|
||||||
|
@ -225,6 +233,20 @@ impl ScreenTransform {
|
||||||
Value::new(x, y)
|
Value::new(x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Transform a rectangle of plot values to a screen-coordinate rectangle.
|
||||||
|
///
|
||||||
|
/// This typically means that the rect is mirrored vertically (top becomes bottom and vice versa),
|
||||||
|
/// since the plot's coordinate system has +Y up, while egui has +Y down.
|
||||||
|
pub fn rect_from_values(&self, value1: &Value, value2: &Value) -> Rect {
|
||||||
|
let pos1 = self.position_from_value(value1);
|
||||||
|
let pos2 = self.position_from_value(value2);
|
||||||
|
|
||||||
|
let mut rect = Rect::NOTHING;
|
||||||
|
rect.extend_with(pos1);
|
||||||
|
rect.extend_with(pos2);
|
||||||
|
rect
|
||||||
|
}
|
||||||
|
|
||||||
/// delta position / delta value
|
/// delta position / delta value
|
||||||
pub fn dpos_dvalue_x(&self) -> f64 {
|
pub fn dpos_dvalue_x(&self) -> f64 {
|
||||||
self.frame.width() as f64 / self.bounds.width()
|
self.frame.width() as f64 / self.bounds.width()
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
use std::f64::consts::TAU;
|
||||||
|
|
||||||
use egui::*;
|
use egui::*;
|
||||||
use plot::{
|
use plot::{
|
||||||
Arrows, Corner, HLine, Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, Points, Polygon,
|
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, Corner, HLine, Legend, Line, LineStyle,
|
||||||
Text, VLine, Value, Values,
|
MarkerShape, Plot, PlotImage, Points, Polygon, Text, VLine, Value, Values,
|
||||||
};
|
};
|
||||||
use std::f64::consts::TAU;
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
struct LineDemo {
|
struct LineDemo {
|
||||||
|
@ -430,18 +431,203 @@ impl Widget for &mut InteractionDemo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
enum Chart {
|
||||||
|
GaussBars,
|
||||||
|
StackedBars,
|
||||||
|
BoxPlot,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Chart {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::GaussBars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct ChartsDemo {
|
||||||
|
chart: Chart,
|
||||||
|
vertical: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ChartsDemo {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
vertical: true,
|
||||||
|
chart: Chart::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChartsDemo {
|
||||||
|
fn bar_gauss(&self, ui: &mut Ui) -> Response {
|
||||||
|
let mut chart = BarChart::new(
|
||||||
|
(-395..=395)
|
||||||
|
.step_by(10)
|
||||||
|
.map(|x| x as f64 * 0.01)
|
||||||
|
.map(|x| {
|
||||||
|
(
|
||||||
|
x,
|
||||||
|
(-x * x / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
// The 10 factor here is purely for a nice 1:1 aspect ratio
|
||||||
|
.map(|(x, f)| Bar::new(x, f * 10.0).width(0.095))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
.color(Color32::LIGHT_BLUE)
|
||||||
|
.name("Normal Distribution");
|
||||||
|
if !self.vertical {
|
||||||
|
chart = chart.horizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
Plot::new("Normal Distribution Demo")
|
||||||
|
.legend(Legend::default())
|
||||||
|
.data_aspect(1.0)
|
||||||
|
.show(ui, |plot_ui| plot_ui.bar_chart(chart))
|
||||||
|
.response
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bar_stacked(&self, ui: &mut Ui) -> Response {
|
||||||
|
let mut chart1 = BarChart::new(vec![
|
||||||
|
Bar::new(0.5, 1.0).name("Day 1"),
|
||||||
|
Bar::new(1.5, 3.0).name("Day 2"),
|
||||||
|
Bar::new(2.5, 1.0).name("Day 3"),
|
||||||
|
Bar::new(3.5, 2.0).name("Day 4"),
|
||||||
|
Bar::new(4.5, 4.0).name("Day 5"),
|
||||||
|
])
|
||||||
|
.width(0.7)
|
||||||
|
.name("Set 1");
|
||||||
|
|
||||||
|
let mut chart2 = BarChart::new(vec![
|
||||||
|
Bar::new(0.5, 1.0),
|
||||||
|
Bar::new(1.5, 1.5),
|
||||||
|
Bar::new(2.5, 0.1),
|
||||||
|
Bar::new(3.5, 0.7),
|
||||||
|
Bar::new(4.5, 0.8),
|
||||||
|
])
|
||||||
|
.width(0.7)
|
||||||
|
.name("Set 2")
|
||||||
|
.stack_on(&[&chart1]);
|
||||||
|
|
||||||
|
let mut chart3 = BarChart::new(vec![
|
||||||
|
Bar::new(0.5, -0.5),
|
||||||
|
Bar::new(1.5, 1.0),
|
||||||
|
Bar::new(2.5, 0.5),
|
||||||
|
Bar::new(3.5, -1.0),
|
||||||
|
Bar::new(4.5, 0.3),
|
||||||
|
])
|
||||||
|
.width(0.7)
|
||||||
|
.name("Set 3")
|
||||||
|
.stack_on(&[&chart1, &chart2]);
|
||||||
|
|
||||||
|
let mut chart4 = BarChart::new(vec![
|
||||||
|
Bar::new(0.5, 0.5),
|
||||||
|
Bar::new(1.5, 1.0),
|
||||||
|
Bar::new(2.5, 0.5),
|
||||||
|
Bar::new(3.5, -0.5),
|
||||||
|
Bar::new(4.5, -0.5),
|
||||||
|
])
|
||||||
|
.width(0.7)
|
||||||
|
.name("Set 4")
|
||||||
|
.stack_on(&[&chart1, &chart2, &chart3]);
|
||||||
|
|
||||||
|
if !self.vertical {
|
||||||
|
chart1 = chart1.horizontal();
|
||||||
|
chart2 = chart2.horizontal();
|
||||||
|
chart3 = chart3.horizontal();
|
||||||
|
chart4 = chart4.horizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
Plot::new("Stacked Bar Chart Demo")
|
||||||
|
.legend(Legend::default())
|
||||||
|
.data_aspect(1.0)
|
||||||
|
.show(ui, |plot_ui| {
|
||||||
|
plot_ui.bar_chart(chart1);
|
||||||
|
plot_ui.bar_chart(chart2);
|
||||||
|
plot_ui.bar_chart(chart3);
|
||||||
|
plot_ui.bar_chart(chart4);
|
||||||
|
})
|
||||||
|
.response
|
||||||
|
}
|
||||||
|
|
||||||
|
fn box_plot(&self, ui: &mut Ui) -> Response {
|
||||||
|
let yellow = Color32::from_rgb(248, 252, 168);
|
||||||
|
let mut box1 = BoxPlot::new(vec![
|
||||||
|
BoxElem::new(0.5, BoxSpread::new(1.5, 2.2, 2.5, 2.6, 3.1)).name("Day 1"),
|
||||||
|
BoxElem::new(2.5, BoxSpread::new(0.4, 1.0, 1.1, 1.4, 2.1)).name("Day 2"),
|
||||||
|
BoxElem::new(4.5, BoxSpread::new(1.7, 2.0, 2.2, 2.5, 2.9)).name("Day 3"),
|
||||||
|
])
|
||||||
|
.name("Experiment A");
|
||||||
|
|
||||||
|
let mut box2 = BoxPlot::new(vec![
|
||||||
|
BoxElem::new(1.0, BoxSpread::new(0.2, 0.5, 1.0, 2.0, 2.7)).name("Day 1"),
|
||||||
|
BoxElem::new(3.0, BoxSpread::new(1.5, 1.7, 2.1, 2.9, 3.3))
|
||||||
|
.name("Day 2: interesting")
|
||||||
|
.stroke(Stroke::new(1.5, yellow))
|
||||||
|
.fill(yellow.linear_multiply(0.2)),
|
||||||
|
BoxElem::new(5.0, BoxSpread::new(1.3, 2.0, 2.3, 2.9, 4.0)).name("Day 3"),
|
||||||
|
])
|
||||||
|
.name("Experiment B");
|
||||||
|
|
||||||
|
let mut box3 = BoxPlot::new(vec![
|
||||||
|
BoxElem::new(1.5, BoxSpread::new(2.1, 2.2, 2.6, 2.8, 3.0)).name("Day 1"),
|
||||||
|
BoxElem::new(3.5, BoxSpread::new(1.3, 1.5, 1.9, 2.2, 2.4)).name("Day 2"),
|
||||||
|
BoxElem::new(5.5, BoxSpread::new(0.2, 0.4, 1.0, 1.3, 1.5)).name("Day 3"),
|
||||||
|
])
|
||||||
|
.name("Experiment C");
|
||||||
|
|
||||||
|
if !self.vertical {
|
||||||
|
box1 = box1.horizontal();
|
||||||
|
box2 = box2.horizontal();
|
||||||
|
box3 = box3.horizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
Plot::new("Box Plot Demo")
|
||||||
|
.legend(Legend::default())
|
||||||
|
.show(ui, |plot_ui| {
|
||||||
|
plot_ui.box_plot(box1);
|
||||||
|
plot_ui.box_plot(box2);
|
||||||
|
plot_ui.box_plot(box3);
|
||||||
|
})
|
||||||
|
.response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for &mut ChartsDemo {
|
||||||
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
|
ui.label("Type:");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.selectable_value(&mut self.chart, Chart::GaussBars, "Histogram");
|
||||||
|
ui.selectable_value(&mut self.chart, Chart::StackedBars, "Stacked Bar Chart");
|
||||||
|
ui.selectable_value(&mut self.chart, Chart::BoxPlot, "Box Plot");
|
||||||
|
});
|
||||||
|
ui.label("Orientation:");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.selectable_value(&mut self.vertical, true, "Vertical");
|
||||||
|
ui.selectable_value(&mut self.vertical, false, "Horizontal");
|
||||||
|
});
|
||||||
|
match self.chart {
|
||||||
|
Chart::GaussBars => self.bar_gauss(ui),
|
||||||
|
Chart::StackedBars => self.bar_stacked(ui),
|
||||||
|
Chart::BoxPlot => self.box_plot(ui),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
enum Panel {
|
enum Panel {
|
||||||
Lines,
|
Lines,
|
||||||
Markers,
|
Markers,
|
||||||
Legend,
|
Legend,
|
||||||
|
Charts,
|
||||||
Items,
|
Items,
|
||||||
Interaction,
|
Interaction,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Panel {
|
impl Default for Panel {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::Lines
|
Self::Charts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -450,6 +636,7 @@ pub struct PlotDemo {
|
||||||
line_demo: LineDemo,
|
line_demo: LineDemo,
|
||||||
marker_demo: MarkerDemo,
|
marker_demo: MarkerDemo,
|
||||||
legend_demo: LegendDemo,
|
legend_demo: LegendDemo,
|
||||||
|
charts_demo: ChartsDemo,
|
||||||
items_demo: ItemsDemo,
|
items_demo: ItemsDemo,
|
||||||
interaction_demo: InteractionDemo,
|
interaction_demo: InteractionDemo,
|
||||||
open_panel: Panel,
|
open_panel: Panel,
|
||||||
|
@ -492,6 +679,7 @@ impl super::View for PlotDemo {
|
||||||
ui.selectable_value(&mut self.open_panel, Panel::Lines, "Lines");
|
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::Markers, "Markers");
|
||||||
ui.selectable_value(&mut self.open_panel, Panel::Legend, "Legend");
|
ui.selectable_value(&mut self.open_panel, Panel::Legend, "Legend");
|
||||||
|
ui.selectable_value(&mut self.open_panel, Panel::Charts, "Charts");
|
||||||
ui.selectable_value(&mut self.open_panel, Panel::Items, "Items");
|
ui.selectable_value(&mut self.open_panel, Panel::Items, "Items");
|
||||||
ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction");
|
ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction");
|
||||||
});
|
});
|
||||||
|
@ -507,6 +695,9 @@ impl super::View for PlotDemo {
|
||||||
Panel::Legend => {
|
Panel::Legend => {
|
||||||
ui.add(&mut self.legend_demo);
|
ui.add(&mut self.legend_demo);
|
||||||
}
|
}
|
||||||
|
Panel::Charts => {
|
||||||
|
ui.add(&mut self.charts_demo);
|
||||||
|
}
|
||||||
Panel::Items => {
|
Panel::Items => {
|
||||||
ui.add(&mut self.items_demo);
|
ui.add(&mut self.items_demo);
|
||||||
}
|
}
|
||||||
|
|
|
@ -309,6 +309,27 @@ impl Rect {
|
||||||
self.width() * self.height()
|
self.width() * self.height()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn distance_sq_to_pos(&self, pos: Pos2) -> f32 {
|
||||||
|
let dx = if self.min.x > pos.x {
|
||||||
|
self.min.x - pos.x
|
||||||
|
} else if pos.x > self.max.x {
|
||||||
|
pos.x - self.max.x
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let dy = if self.min.y > pos.y {
|
||||||
|
self.min.y - pos.y
|
||||||
|
} else if pos.y > self.max.y {
|
||||||
|
pos.y - self.max.y
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
dx * dx + dy * dy
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn x_range(&self) -> RangeInclusive<f32> {
|
pub fn x_range(&self) -> RangeInclusive<f32> {
|
||||||
self.min.x..=self.max.x
|
self.min.x..=self.max.x
|
||||||
|
|
Loading…
Reference in a new issue