-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Changes: * New `BarChart` and `BoxPlot` diagrams * New `FloatOrd` trait for total ordering of float types * Refactoring of existing plot items Co-authored-by: niladic <[email protected]>
- Loading branch information
Showing
12 changed files
with
1,808 additions
and
415 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
Oops, something went wrong.