mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 05:10:03 -04:00
Break out plotting to own crate egui_plot (#3282)
This replaces `egui::plot` with the new crate `egui_plot`
This commit is contained in:
190
crates/egui_plot/src/items/bar.rs
Normal file
190
crates/egui_plot/src/items/bar.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
use egui::emath::NumExt;
|
||||
use egui::epaint::{Color32, RectShape, Rounding, Shape, Stroke};
|
||||
|
||||
use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement};
|
||||
use crate::{BarChart, Cursor, PlotPoint, PlotTransform};
|
||||
|
||||
/// 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: &PlotTransform,
|
||||
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::new(rect, Rounding::ZERO, fill, stroke));
|
||||
|
||||
shapes.push(rect);
|
||||
}
|
||||
|
||||
pub(super) fn add_rulers_and_text(
|
||||
&self,
|
||||
parent: &BarChart,
|
||||
plot: &PlotConfig<'_>,
|
||||
shapes: &mut Vec<Shape>,
|
||||
cursors: &mut Vec<Cursor>,
|
||||
) {
|
||||
let text: Option<String> = parent
|
||||
.element_formatter
|
||||
.as_ref()
|
||||
.map(|fmt| fmt(self, parent));
|
||||
|
||||
add_rulers_and_text(self, plot, text, shapes, cursors);
|
||||
}
|
||||
}
|
||||
|
||||
impl RectElement for Bar {
|
||||
fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
fn bounds_min(&self) -> PlotPoint {
|
||||
self.point_at(self.argument - self.bar_width / 2.0, self.lower())
|
||||
}
|
||||
|
||||
fn bounds_max(&self) -> PlotPoint {
|
||||
self.point_at(self.argument + self.bar_width / 2.0, self.upper())
|
||||
}
|
||||
|
||||
fn values_with_ruler(&self) -> Vec<PlotPoint> {
|
||||
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: &PlotTransform) -> String {
|
||||
let scale = transform.dvalue_dpos();
|
||||
let scale = match self.orientation {
|
||||
Orientation::Horizontal => scale[0],
|
||||
Orientation::Vertical => scale[1],
|
||||
};
|
||||
let decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||
crate::format_number(self.value, decimals)
|
||||
}
|
||||
}
|
||||
289
crates/egui_plot/src/items/box_elem.rs
Normal file
289
crates/egui_plot/src/items/box_elem.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
use egui::emath::NumExt as _;
|
||||
use egui::epaint::{Color32, RectShape, Rounding, Shape, Stroke};
|
||||
|
||||
use crate::{BoxPlot, Cursor, PlotPoint, PlotTransform};
|
||||
|
||||
use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement};
|
||||
|
||||
/// 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: &PlotTransform,
|
||||
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::new(rect, Rounding::ZERO, fill, stroke));
|
||||
shapes.push(rect);
|
||||
|
||||
let line_between = |v1, v2| {
|
||||
Shape::line_segment(
|
||||
[
|
||||
transform.position_from_point(&v1),
|
||||
transform.position_from_point(&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>,
|
||||
cursors: &mut Vec<Cursor>,
|
||||
) {
|
||||
let text: Option<String> = parent
|
||||
.element_formatter
|
||||
.as_ref()
|
||||
.map(|fmt| fmt(self, parent));
|
||||
|
||||
add_rulers_and_text(self, plot, text, shapes, cursors);
|
||||
}
|
||||
}
|
||||
|
||||
impl RectElement for BoxElem {
|
||||
fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
fn bounds_min(&self) -> PlotPoint {
|
||||
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) -> PlotPoint {
|
||||
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<PlotPoint> {
|
||||
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) -> PlotPoint {
|
||||
self.point_at(self.argument, self.spread.upper_whisker)
|
||||
}
|
||||
|
||||
fn default_values_format(&self, transform: &PlotTransform) -> String {
|
||||
let scale = transform.dvalue_dpos();
|
||||
let scale = match self.orientation {
|
||||
Orientation::Horizontal => scale[0],
|
||||
Orientation::Vertical => scale[1],
|
||||
};
|
||||
let y_decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize)
|
||||
.at_most(6)
|
||||
.at_least(1);
|
||||
format!(
|
||||
"Max = {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
|
||||
)
|
||||
}
|
||||
}
|
||||
1795
crates/egui_plot/src/items/mod.rs
Normal file
1795
crates/egui_plot/src/items/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
65
crates/egui_plot/src/items/rect_elem.rs
Normal file
65
crates/egui_plot/src/items/rect_elem.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use egui::emath::NumExt as _;
|
||||
use egui::epaint::{Color32, Rgba, Stroke};
|
||||
|
||||
use crate::transform::{PlotBounds, PlotTransform};
|
||||
|
||||
use super::{Orientation, PlotPoint};
|
||||
|
||||
/// 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) -> PlotPoint;
|
||||
|
||||
fn bounds_max(&self) -> PlotPoint;
|
||||
|
||||
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<PlotPoint> {
|
||||
// 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<PlotPoint>;
|
||||
|
||||
/// 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) -> PlotPoint {
|
||||
match self.orientation() {
|
||||
Orientation::Horizontal => PlotPoint::new(value, argument),
|
||||
Orientation::Vertical => PlotPoint::new(argument, value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Right top of the rectangle (position of text)
|
||||
fn corner_value(&self) -> PlotPoint {
|
||||
//self.point_at(self.position + self.width / 2.0, value)
|
||||
PlotPoint {
|
||||
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: &PlotTransform) -> 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())
|
||||
}
|
||||
433
crates/egui_plot/src/items/values.rs
Normal file
433
crates/egui_plot/src/items/values.rs
Normal file
@@ -0,0 +1,433 @@
|
||||
use std::ops::{Bound, RangeBounds, RangeInclusive};
|
||||
|
||||
use egui::{Pos2, Shape, Stroke, Vec2};
|
||||
|
||||
use crate::transform::PlotBounds;
|
||||
|
||||
/// A point coordinate in 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 PlotPoint {
|
||||
/// 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 From<[f64; 2]> for PlotPoint {
|
||||
#[inline]
|
||||
fn from([x, y]: [f64; 2]) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl PlotPoint {
|
||||
#[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{spacing}Px"),
|
||||
LineStyle::Dashed { length } => format!("Dashed{length}Px"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Determines whether a plot element is vertically or horizontally oriented.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Orientation {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl Default for Orientation {
|
||||
fn default() -> Self {
|
||||
Self::Vertical
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Represents many [`PlotPoint`]s.
|
||||
///
|
||||
/// These can be an owned `Vec` or generated with a function.
|
||||
pub enum PlotPoints {
|
||||
Owned(Vec<PlotPoint>),
|
||||
Generator(ExplicitGenerator),
|
||||
// Borrowed(&[PlotPoint]), // TODO: Lifetimes are tricky in this case.
|
||||
}
|
||||
|
||||
impl Default for PlotPoints {
|
||||
fn default() -> Self {
|
||||
Self::Owned(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[f64; 2]> for PlotPoints {
|
||||
fn from(coordinate: [f64; 2]) -> Self {
|
||||
Self::new(vec![coordinate])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<[f64; 2]>> for PlotPoints {
|
||||
fn from(coordinates: Vec<[f64; 2]>) -> Self {
|
||||
Self::new(coordinates)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<[f64; 2]> for PlotPoints {
|
||||
fn from_iter<T: IntoIterator<Item = [f64; 2]>>(iter: T) -> Self {
|
||||
Self::Owned(iter.into_iter().map(|point| point.into()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl PlotPoints {
|
||||
pub fn new(points: Vec<[f64; 2]>) -> Self {
|
||||
Self::from_iter(points)
|
||||
}
|
||||
|
||||
pub fn points(&self) -> &[PlotPoint] {
|
||||
match self {
|
||||
PlotPoints::Owned(points) => points.as_slice(),
|
||||
PlotPoints::Generator(_) => &[],
|
||||
}
|
||||
}
|
||||
|
||||
/// 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::Generator(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
|
||||
};
|
||||
(0..points)
|
||||
.map(|i| {
|
||||
let t = start + i as f64 * increment;
|
||||
let (x, y) = function(t);
|
||||
[x, y]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// From a series of y-values.
|
||||
/// The x-values will be the indices of these values
|
||||
pub fn from_ys_f32(ys: &[f32]) -> Self {
|
||||
ys.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &y)| [i as f64, y as f64])
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// From a series of y-values.
|
||||
/// The x-values will be the indices of these values
|
||||
pub fn from_ys_f64(ys: &[f64]) -> Self {
|
||||
ys.iter().enumerate().map(|(i, &y)| [i as f64, y]).collect()
|
||||
}
|
||||
|
||||
/// Returns true if there are no data points available and there is no function to generate any.
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
PlotPoints::Owned(points) => points.is_empty(),
|
||||
PlotPoints::Generator(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 Self::Generator(generator) = self {
|
||||
*self = Self::range_intersection(&x_range, &generator.x_range)
|
||||
.map(|intersection| {
|
||||
let increment =
|
||||
(intersection.end() - intersection.start()) / (generator.points - 1) as f64;
|
||||
(0..generator.points)
|
||||
.map(|i| {
|
||||
let x = intersection.start() + i as f64 * increment;
|
||||
let y = (generator.function)(x);
|
||||
[x, y]
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_some(start..=end)
|
||||
}
|
||||
|
||||
pub(super) fn bounds(&self) -> PlotBounds {
|
||||
match self {
|
||||
PlotPoints::Owned(points) => {
|
||||
let mut bounds = PlotBounds::NOTHING;
|
||||
for point in points {
|
||||
bounds.extend_with(point);
|
||||
}
|
||||
bounds
|
||||
}
|
||||
PlotPoints::Generator(generator) => generator.estimate_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 ExactSizeIterator<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 points 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 [PlotPoint]),
|
||||
|
||||
/// 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.
|
||||
pub struct ExplicitGenerator {
|
||||
function: Box<dyn Fn(f64) -> f64>,
|
||||
x_range: RangeInclusive<f64>,
|
||||
points: usize,
|
||||
}
|
||||
|
||||
impl ExplicitGenerator {
|
||||
fn estimate_bounds(&self) -> PlotBounds {
|
||||
let mut bounds = PlotBounds::NOTHING;
|
||||
|
||||
let mut add_x = |x: f64| {
|
||||
// avoid infinities, as we cannot auto-bound on them!
|
||||
if x.is_finite() {
|
||||
bounds.extend_with_x(x);
|
||||
}
|
||||
let y = (self.function)(x);
|
||||
if y.is_finite() {
|
||||
bounds.extend_with_y(y);
|
||||
}
|
||||
};
|
||||
|
||||
let min_x = *self.x_range.start();
|
||||
let max_x = *self.x_range.end();
|
||||
|
||||
add_x(min_x);
|
||||
add_x(max_x);
|
||||
|
||||
if min_x.is_finite() && max_x.is_finite() {
|
||||
// Sample some points in the interval:
|
||||
const N: u32 = 8;
|
||||
for i in 1..N {
|
||||
let t = i as f64 / (N - 1) as f64;
|
||||
let x = crate::lerp(min_x..=max_x, t);
|
||||
add_x(x);
|
||||
}
|
||||
} else {
|
||||
// Try adding some points anyway:
|
||||
for x in [-1, 0, 1] {
|
||||
let x = x as f64;
|
||||
if min_x <= x && x <= max_x {
|
||||
add_x(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bounds
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Result of [`super::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,
|
||||
}
|
||||
Reference in New Issue
Block a user