Draw axis labels and ticks outside of plotting window (#2284)
* Always draw axis labels at plot borders * Revert "Always draw axis labels at plot borders" This reverts commit 9235e6603366d3b8a8189e2a5fc28c9780b7f54f. * Add axis labels for plots * First Draft of axis labels outside of plotting window * plot: Tick placement of opposite axes and digit constraints * plot: Axis label API * plot: Update demo lib * plot: resolve clippy warning * Update changelog * Remove default axis * Fix clippy * plot: Remove unused comments * plot-axis: Rebase label opacity calculation on master * plot: Resolve check.sh warnings * plot-axis: Use 'into impl<WidgetText>' as axis label formatter * plot-axis: Expose more conveniece functions to public API. Add axis labels to demo app * plot-axes: Resolve ./scripts/check.sh warnings * typo in comment * Use `TAU` instead of the legacy `PI` * Simpler generic syntax * Use `Arc` to avoid some expensive clones * Use `Margin` instead of a,b,c,d * Add some vertical spacing * De-duplicate color_from_contrast * better naming * Fix typos * cnt -> num * Axis are present by default, with empty names * Add HPlacement and VPlacement * Don't catch clicks and drags on axes * Remove generics to minimize monomorphization code bloat * Create helper function * Remove changelog entry * Simplify more --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
parent
a3ae81cadb
commit
dbe55ba46a
|
|
@ -338,6 +338,13 @@ pub struct Margin {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Margin {
|
impl Margin {
|
||||||
|
pub const ZERO: Self = Self {
|
||||||
|
left: 0.0,
|
||||||
|
right: 0.0,
|
||||||
|
top: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn same(margin: f32) -> Self {
|
pub fn same(margin: f32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ pub use text_edit::{TextBuffer, TextEdit};
|
||||||
///
|
///
|
||||||
/// [`Button`], [`Label`], [`Slider`], etc all implement the [`Widget`] trait.
|
/// [`Button`], [`Label`], [`Slider`], etc all implement the [`Widget`] trait.
|
||||||
///
|
///
|
||||||
|
/// You only need to implement `Widget` if you care about being able to do `ui.add(your_widget);`.
|
||||||
|
///
|
||||||
/// Note that the widgets ([`Button`], [`TextEdit`] etc) are
|
/// Note that the widgets ([`Button`], [`TextEdit`] etc) are
|
||||||
/// [builders](https://doc.rust-lang.org/1.0.0/style/ownership/builders.html),
|
/// [builders](https://doc.rust-lang.org/1.0.0/style/ownership/builders.html),
|
||||||
/// and not objects that hold state.
|
/// and not objects that hold state.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,318 @@
|
||||||
|
use std::{fmt::Debug, ops::RangeInclusive, sync::Arc};
|
||||||
|
|
||||||
|
use epaint::{
|
||||||
|
emath::{remap_clamp, round_to_decimals},
|
||||||
|
Pos2, Rect, Shape, Stroke, TextShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{Response, Sense, TextStyle, Ui, WidgetText};
|
||||||
|
|
||||||
|
use super::{transform::PlotTransform, GridMark};
|
||||||
|
|
||||||
|
pub(super) type AxisFormatterFn = fn(f64, usize, &RangeInclusive<f64>) -> String;
|
||||||
|
|
||||||
|
/// X or Y axis.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Axis {
|
||||||
|
/// Horizontal X-Axis
|
||||||
|
X,
|
||||||
|
|
||||||
|
/// Vertical Y-axis
|
||||||
|
Y,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Axis> for usize {
|
||||||
|
#[inline]
|
||||||
|
fn from(value: Axis) -> Self {
|
||||||
|
match value {
|
||||||
|
Axis::X => 0,
|
||||||
|
Axis::Y => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Placement of the horizontal X-Axis.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum VPlacement {
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Placement of the vertical Y-Axis.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum HPlacement {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Placement of an axis.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Placement {
|
||||||
|
/// Bottom for X-axis, or left for Y-axis.
|
||||||
|
LeftBottom,
|
||||||
|
|
||||||
|
/// Top for x-axis and right for y-axis.
|
||||||
|
RightTop,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HPlacement> for Placement {
|
||||||
|
#[inline]
|
||||||
|
fn from(placement: HPlacement) -> Self {
|
||||||
|
match placement {
|
||||||
|
HPlacement::Left => Placement::LeftBottom,
|
||||||
|
HPlacement::Right => Placement::RightTop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<VPlacement> for Placement {
|
||||||
|
#[inline]
|
||||||
|
fn from(placement: VPlacement) -> Self {
|
||||||
|
match placement {
|
||||||
|
VPlacement::Top => Placement::RightTop,
|
||||||
|
VPlacement::Bottom => Placement::LeftBottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Axis configuration.
|
||||||
|
///
|
||||||
|
/// Used to configure axis label and ticks.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AxisHints {
|
||||||
|
pub(super) label: WidgetText,
|
||||||
|
pub(super) formatter: AxisFormatterFn,
|
||||||
|
pub(super) digits: usize,
|
||||||
|
pub(super) placement: Placement,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this just a guess. It might cease to work if a user changes font size.
|
||||||
|
const LINE_HEIGHT: f32 = 12.0;
|
||||||
|
|
||||||
|
impl Default for AxisHints {
|
||||||
|
/// Initializes a default axis configuration for the specified axis.
|
||||||
|
///
|
||||||
|
/// `label` is empty.
|
||||||
|
/// `formatter` is default float to string formatter.
|
||||||
|
/// maximum `digits` on tick label is 5.
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
label: Default::default(),
|
||||||
|
formatter: Self::default_formatter,
|
||||||
|
digits: 5,
|
||||||
|
placement: Placement::LeftBottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AxisHints {
|
||||||
|
/// Specify custom formatter for ticks.
|
||||||
|
///
|
||||||
|
/// The first parameter of `formatter` is the raw tick value as `f64`.
|
||||||
|
/// The second parameter is the maximum number of characters that fit into y-labels.
|
||||||
|
/// The second parameter of `formatter` is the currently shown range on this axis.
|
||||||
|
pub fn formatter(mut self, fmt: fn(f64, usize, &RangeInclusive<f64>) -> String) -> Self {
|
||||||
|
self.formatter = fmt;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_formatter(tick: f64, max_digits: usize, _range: &RangeInclusive<f64>) -> String {
|
||||||
|
if tick.abs() > 10.0_f64.powf(max_digits as f64) {
|
||||||
|
let tick_rounded = tick as isize;
|
||||||
|
return format!("{tick_rounded:+e}");
|
||||||
|
}
|
||||||
|
let tick_rounded = round_to_decimals(tick, max_digits);
|
||||||
|
if tick.abs() < 10.0_f64.powf(-(max_digits as f64)) && tick != 0.0 {
|
||||||
|
return format!("{tick_rounded:+e}");
|
||||||
|
}
|
||||||
|
tick_rounded.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specify axis label.
|
||||||
|
///
|
||||||
|
/// The default is 'x' for x-axes and 'y' for y-axes.
|
||||||
|
pub fn label(mut self, label: impl Into<WidgetText>) -> Self {
|
||||||
|
self.label = label.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specify maximum number of digits for ticks.
|
||||||
|
///
|
||||||
|
/// This is considered by the default tick formatter and affects the width of the y-axis
|
||||||
|
pub fn max_digits(mut self, digits: usize) -> Self {
|
||||||
|
self.digits = digits;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specify the placement of the axis.
|
||||||
|
///
|
||||||
|
/// For X-axis, use [`VPlacement`].
|
||||||
|
/// For Y-axis, use [`HPlacement`].
|
||||||
|
pub fn placement(mut self, placement: impl Into<Placement>) -> Self {
|
||||||
|
self.placement = placement.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn thickness(&self, axis: Axis) -> f32 {
|
||||||
|
match axis {
|
||||||
|
Axis::X => {
|
||||||
|
if self.label.is_empty() {
|
||||||
|
1.0 * LINE_HEIGHT
|
||||||
|
} else {
|
||||||
|
3.0 * LINE_HEIGHT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Axis::Y => {
|
||||||
|
if self.label.is_empty() {
|
||||||
|
(self.digits as f32) * LINE_HEIGHT
|
||||||
|
} else {
|
||||||
|
(self.digits as f32 + 1.0) * LINE_HEIGHT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(super) struct AxisWidget {
|
||||||
|
pub(super) range: RangeInclusive<f64>,
|
||||||
|
pub(super) hints: AxisHints,
|
||||||
|
pub(super) rect: Rect,
|
||||||
|
pub(super) transform: Option<PlotTransform>,
|
||||||
|
pub(super) steps: Arc<Vec<GridMark>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AxisWidget {
|
||||||
|
/// if `rect` as width or height == 0, is will be automatically calculated from ticks and text.
|
||||||
|
pub(super) fn new(hints: AxisHints, rect: Rect) -> Self {
|
||||||
|
Self {
|
||||||
|
range: (0.0..=0.0),
|
||||||
|
hints,
|
||||||
|
rect,
|
||||||
|
transform: None,
|
||||||
|
steps: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(self, ui: &mut Ui, axis: Axis) -> Response {
|
||||||
|
let response = ui.allocate_rect(self.rect, Sense::hover());
|
||||||
|
|
||||||
|
if ui.is_rect_visible(response.rect) {
|
||||||
|
let visuals = ui.style().visuals.clone();
|
||||||
|
let text = self.hints.label;
|
||||||
|
let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Body);
|
||||||
|
let text_color = visuals
|
||||||
|
.override_text_color
|
||||||
|
.unwrap_or_else(|| ui.visuals().text_color());
|
||||||
|
let angle: f32 = match axis {
|
||||||
|
Axis::X => 0.0,
|
||||||
|
Axis::Y => -std::f32::consts::TAU * 0.25,
|
||||||
|
};
|
||||||
|
// select text_pos and angle depending on placement and orientation of widget
|
||||||
|
let text_pos = match self.hints.placement {
|
||||||
|
Placement::LeftBottom => match axis {
|
||||||
|
Axis::X => {
|
||||||
|
let pos = response.rect.center_bottom();
|
||||||
|
Pos2 {
|
||||||
|
x: pos.x - galley.size().x / 2.0,
|
||||||
|
y: pos.y - galley.size().y * 1.25,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Axis::Y => {
|
||||||
|
let pos = response.rect.left_center();
|
||||||
|
Pos2 {
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y + galley.size().x / 2.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Placement::RightTop => match axis {
|
||||||
|
Axis::X => {
|
||||||
|
let pos = response.rect.center_top();
|
||||||
|
Pos2 {
|
||||||
|
x: pos.x - galley.size().x / 2.0,
|
||||||
|
y: pos.y + galley.size().y * 0.25,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Axis::Y => {
|
||||||
|
let pos = response.rect.right_center();
|
||||||
|
Pos2 {
|
||||||
|
x: pos.x - galley.size().y * 1.5,
|
||||||
|
y: pos.y + galley.size().x / 2.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let shape = TextShape {
|
||||||
|
pos: text_pos,
|
||||||
|
galley: galley.galley,
|
||||||
|
underline: Stroke::NONE,
|
||||||
|
override_text_color: Some(text_color),
|
||||||
|
angle,
|
||||||
|
};
|
||||||
|
ui.painter().add(shape);
|
||||||
|
|
||||||
|
// --- add ticks ---
|
||||||
|
let font_id = TextStyle::Body.resolve(ui.style());
|
||||||
|
let transform = match self.transform {
|
||||||
|
Some(t) => t,
|
||||||
|
None => return response,
|
||||||
|
};
|
||||||
|
|
||||||
|
for step in self.steps.iter() {
|
||||||
|
let text = (self.hints.formatter)(step.value, self.hints.digits, &self.range);
|
||||||
|
if !text.is_empty() {
|
||||||
|
const MIN_TEXT_SPACING: f32 = 20.0;
|
||||||
|
const FULL_CONTRAST_SPACING: f32 = 40.0;
|
||||||
|
let spacing_in_points =
|
||||||
|
(transform.dpos_dvalue()[usize::from(axis)] * step.step_size).abs() as f32;
|
||||||
|
|
||||||
|
if spacing_in_points <= MIN_TEXT_SPACING {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let line_strength = remap_clamp(
|
||||||
|
spacing_in_points,
|
||||||
|
MIN_TEXT_SPACING..=FULL_CONTRAST_SPACING,
|
||||||
|
0.0..=1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let line_color = super::color_from_strength(ui, line_strength);
|
||||||
|
let galley = ui
|
||||||
|
.painter()
|
||||||
|
.layout_no_wrap(text, font_id.clone(), line_color);
|
||||||
|
|
||||||
|
let text_pos = match axis {
|
||||||
|
Axis::X => {
|
||||||
|
let y = match self.hints.placement {
|
||||||
|
Placement::LeftBottom => self.rect.min.y,
|
||||||
|
Placement::RightTop => self.rect.max.y - galley.size().y,
|
||||||
|
};
|
||||||
|
let projected_point = super::PlotPoint::new(step.value, 0.0);
|
||||||
|
Pos2 {
|
||||||
|
x: transform.position_from_point(&projected_point).x
|
||||||
|
- galley.size().x / 2.0,
|
||||||
|
y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Axis::Y => {
|
||||||
|
let x = match self.hints.placement {
|
||||||
|
Placement::LeftBottom => self.rect.max.x - galley.size().x,
|
||||||
|
Placement::RightTop => self.rect.min.x,
|
||||||
|
};
|
||||||
|
let projected_point = super::PlotPoint::new(0.0, step.value);
|
||||||
|
Pos2 {
|
||||||
|
x,
|
||||||
|
y: transform.position_from_point(&projected_point).y
|
||||||
|
- galley.size().y / 2.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.painter().add(Shape::galley(text_pos, galley));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
use epaint::Pos2;
|
||||||
|
|
||||||
|
use crate::{Context, Id};
|
||||||
|
|
||||||
|
use super::{transform::ScreenTransform, AxisBools};
|
||||||
|
|
||||||
|
/// Information about the plot that has to persist between frames.
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(super) struct PlotMemory {
|
||||||
|
/// Indicates if the user has modified the bounds, for example by moving or zooming,
|
||||||
|
/// or if the bounds should be calculated based by included point or auto bounds.
|
||||||
|
pub(super) bounds_modified: AxisBools,
|
||||||
|
|
||||||
|
pub(super) hovered_entry: Option<String>,
|
||||||
|
|
||||||
|
pub(super) hidden_items: ahash::HashSet<String>,
|
||||||
|
|
||||||
|
pub(super) last_screen_transform: ScreenTransform,
|
||||||
|
|
||||||
|
/// Allows to remember the first click position when performing a boxed zoom
|
||||||
|
pub(super) last_click_pos_for_zoom: Option<Pos2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlotMemory {
|
||||||
|
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||||
|
ctx.data().get_persisted(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store(self, ctx: &Context, id: Id) {
|
||||||
|
ctx.data().insert_persisted(id, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
//! Simple plotting library.
|
//! Simple plotting library.
|
||||||
|
|
||||||
use ahash::HashMap;
|
use std::{ops::RangeInclusive, sync::Arc};
|
||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
use crate::*;
|
use ahash::HashMap;
|
||||||
use epaint::util::FloatOrd;
|
use epaint::util::FloatOrd;
|
||||||
use epaint::Hsva;
|
use epaint::Hsva;
|
||||||
|
|
||||||
|
use axis::AxisWidget;
|
||||||
use items::PlotItem;
|
use items::PlotItem;
|
||||||
use legend::LegendWidget;
|
use legend::LegendWidget;
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
pub use items::{
|
pub use items::{
|
||||||
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape,
|
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape,
|
||||||
Orientation, PlotImage, PlotPoint, PlotPoints, Points, Polygon, Text, VLine,
|
Orientation, PlotImage, PlotPoint, PlotPoints, Points, Polygon, Text, VLine,
|
||||||
|
|
@ -17,16 +19,17 @@ pub use items::{
|
||||||
pub use legend::{Corner, Legend};
|
pub use legend::{Corner, Legend};
|
||||||
pub use transform::{PlotBounds, PlotTransform};
|
pub use transform::{PlotBounds, PlotTransform};
|
||||||
|
|
||||||
use self::items::{horizontal_line, rulers_color, vertical_line};
|
use items::{horizontal_line, rulers_color, vertical_line};
|
||||||
|
|
||||||
|
pub use axis::{Axis, AxisHints, HPlacement, Placement, VPlacement};
|
||||||
|
|
||||||
|
mod axis;
|
||||||
mod items;
|
mod items;
|
||||||
mod legend;
|
mod legend;
|
||||||
mod transform;
|
mod transform;
|
||||||
|
|
||||||
type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String;
|
type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String;
|
||||||
type LabelFormatter = Option<Box<LabelFormatterFn>>;
|
type LabelFormatter = Option<Box<LabelFormatterFn>>;
|
||||||
type AxisFormatterFn = dyn Fn(f64, &RangeInclusive<f64>) -> String;
|
|
||||||
type AxisFormatter = Option<Box<AxisFormatterFn>>;
|
|
||||||
|
|
||||||
type GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>;
|
type GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>;
|
||||||
type GridSpacer = Box<GridSpacerFn>;
|
type GridSpacer = Box<GridSpacerFn>;
|
||||||
|
|
@ -78,6 +81,7 @@ pub struct AxisBools {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AxisBools {
|
impl AxisBools {
|
||||||
|
#[inline]
|
||||||
pub fn new(x: bool, y: bool) -> Self {
|
pub fn new(x: bool, y: bool) -> Self {
|
||||||
Self { x, y }
|
Self { x, y }
|
||||||
}
|
}
|
||||||
|
|
@ -89,11 +93,19 @@ impl AxisBools {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<bool> for AxisBools {
|
impl From<bool> for AxisBools {
|
||||||
|
#[inline]
|
||||||
fn from(val: bool) -> Self {
|
fn from(val: bool) -> Self {
|
||||||
AxisBools { x: val, y: val }
|
AxisBools { x: val, y: val }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<[bool; 2]> for AxisBools {
|
||||||
|
#[inline]
|
||||||
|
fn from([x, y]: [bool; 2]) -> Self {
|
||||||
|
AxisBools { x, y }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Information about the plot that has to persist between frames.
|
/// Information about the plot that has to persist between frames.
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -182,8 +194,7 @@ pub struct PlotResponse<R> {
|
||||||
pub struct Plot {
|
pub struct Plot {
|
||||||
id_source: Id,
|
id_source: Id,
|
||||||
|
|
||||||
center_x_axis: bool,
|
center_axis: AxisBools,
|
||||||
center_y_axis: bool,
|
|
||||||
allow_zoom: AxisBools,
|
allow_zoom: AxisBools,
|
||||||
allow_drag: AxisBools,
|
allow_drag: AxisBools,
|
||||||
allow_scroll: bool,
|
allow_scroll: bool,
|
||||||
|
|
@ -208,11 +219,12 @@ pub struct Plot {
|
||||||
show_y: bool,
|
show_y: bool,
|
||||||
label_formatter: LabelFormatter,
|
label_formatter: LabelFormatter,
|
||||||
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
||||||
axis_formatters: [AxisFormatter; 2],
|
x_axes: Vec<AxisHints>, // default x axes
|
||||||
|
y_axes: Vec<AxisHints>, // default y axes
|
||||||
legend_config: Option<Legend>,
|
legend_config: Option<Legend>,
|
||||||
show_background: bool,
|
show_background: bool,
|
||||||
show_axes: [bool; 2],
|
show_axes: AxisBools,
|
||||||
|
show_grid: AxisBools,
|
||||||
grid_spacers: [GridSpacer; 2],
|
grid_spacers: [GridSpacer; 2],
|
||||||
sharp_grid_lines: bool,
|
sharp_grid_lines: bool,
|
||||||
clamp_grid: bool,
|
clamp_grid: bool,
|
||||||
|
|
@ -224,8 +236,7 @@ impl Plot {
|
||||||
Self {
|
Self {
|
||||||
id_source: Id::new(id_source),
|
id_source: Id::new(id_source),
|
||||||
|
|
||||||
center_x_axis: false,
|
center_axis: false.into(),
|
||||||
center_y_axis: false,
|
|
||||||
allow_zoom: true.into(),
|
allow_zoom: true.into(),
|
||||||
allow_drag: true.into(),
|
allow_drag: true.into(),
|
||||||
allow_scroll: true,
|
allow_scroll: true,
|
||||||
|
|
@ -250,11 +261,12 @@ impl Plot {
|
||||||
show_y: true,
|
show_y: true,
|
||||||
label_formatter: None,
|
label_formatter: None,
|
||||||
coordinates_formatter: None,
|
coordinates_formatter: None,
|
||||||
axis_formatters: [None, None], // [None; 2] requires Copy
|
x_axes: vec![Default::default()],
|
||||||
|
y_axes: vec![Default::default()],
|
||||||
legend_config: None,
|
legend_config: None,
|
||||||
show_background: true,
|
show_background: true,
|
||||||
show_axes: [true; 2],
|
show_axes: true.into(),
|
||||||
|
show_grid: true.into(),
|
||||||
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
|
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
|
||||||
sharp_grid_lines: true,
|
sharp_grid_lines: true,
|
||||||
clamp_grid: false,
|
clamp_grid: false,
|
||||||
|
|
@ -311,15 +323,15 @@ impl Plot {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Always keep the x-axis centered. Default: `false`.
|
/// Always keep the X-axis centered. Default: `false`.
|
||||||
pub fn center_x_axis(mut self, on: bool) -> Self {
|
pub fn center_x_axis(mut self, on: bool) -> Self {
|
||||||
self.center_x_axis = on;
|
self.center_axis.x = on;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Always keep the y-axis centered. Default: `false`.
|
/// Always keep the Y-axis centered. Default: `false`.
|
||||||
pub fn center_y_axis(mut self, on: bool) -> Self {
|
pub fn center_y_axis(mut self, on: bool) -> Self {
|
||||||
self.center_y_axis = on;
|
self.center_axis.y = on;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -417,36 +429,6 @@ impl Plot {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provide a function to customize the labels for the X axis based on the current visible value range.
|
|
||||||
///
|
|
||||||
/// This is useful for custom input domains, e.g. date/time.
|
|
||||||
///
|
|
||||||
/// If axis labels should not appear for certain values or beyond a certain zoom/resolution,
|
|
||||||
/// the formatter function can return empty strings. This is also useful if your domain is
|
|
||||||
/// discrete (e.g. only full days in a calendar).
|
|
||||||
pub fn x_axis_formatter(
|
|
||||||
mut self,
|
|
||||||
func: impl Fn(f64, &RangeInclusive<f64>) -> String + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.axis_formatters[0] = Some(Box::new(func));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Provide a function to customize the labels for the Y axis based on the current value range.
|
|
||||||
///
|
|
||||||
/// This is useful for custom value representation, e.g. percentage or units.
|
|
||||||
///
|
|
||||||
/// If axis labels should not appear for certain values or beyond a certain zoom/resolution,
|
|
||||||
/// the formatter function can return empty strings. This is also useful if your Y values are
|
|
||||||
/// discrete (e.g. only integers).
|
|
||||||
pub fn y_axis_formatter(
|
|
||||||
mut self,
|
|
||||||
func: impl Fn(f64, &RangeInclusive<f64>) -> String + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.axis_formatters[1] = Some(Box::new(func));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configure how the grid in the background is spaced apart along the X axis.
|
/// Configure how the grid in the background is spaced apart along the X axis.
|
||||||
///
|
///
|
||||||
/// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units.
|
/// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units.
|
||||||
|
|
@ -538,11 +520,19 @@ impl Plot {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show the axes.
|
/// Show axis labels and grid tick values on the side of the plot.
|
||||||
/// Can be useful to disable if the plot is overlaid over an existing grid or content.
|
///
|
||||||
/// Default: `[true; 2]`.
|
/// Default: `[true; 2]`.
|
||||||
pub fn show_axes(mut self, show: [bool; 2]) -> Self {
|
pub fn show_axes(mut self, show: impl Into<AxisBools>) -> Self {
|
||||||
self.show_axes = show;
|
self.show_axes = show.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a grid overlay on the plot.
|
||||||
|
///
|
||||||
|
/// Default: `[true; 2]`.
|
||||||
|
pub fn show_grid(mut self, show: impl Into<AxisBools>) -> Self {
|
||||||
|
self.show_grid = show.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -585,6 +575,94 @@ impl Plot {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the x axis label of the main X-axis.
|
||||||
|
///
|
||||||
|
/// Default: no label.
|
||||||
|
pub fn x_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
|
||||||
|
if let Some(main) = self.x_axes.first_mut() {
|
||||||
|
main.label = label.into();
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the y axis label of the main Y-axis.
|
||||||
|
///
|
||||||
|
/// Default: no label.
|
||||||
|
pub fn y_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
|
||||||
|
if let Some(main) = self.y_axes.first_mut() {
|
||||||
|
main.label = label.into();
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the position of the main X-axis.
|
||||||
|
pub fn x_axis_position(mut self, placement: axis::VPlacement) -> Self {
|
||||||
|
if let Some(main) = self.x_axes.first_mut() {
|
||||||
|
main.placement = placement.into();
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the position of the main Y-axis.
|
||||||
|
pub fn y_axis_position(mut self, placement: axis::HPlacement) -> Self {
|
||||||
|
if let Some(main) = self.y_axes.first_mut() {
|
||||||
|
main.placement = placement.into();
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specify custom formatter for ticks on the main X-axis.
|
||||||
|
///
|
||||||
|
/// The first parameter of `fmt` is the raw tick value as `f64`.
|
||||||
|
/// The second parameter is the maximum requested number of characters per tick label.
|
||||||
|
/// The second parameter of `fmt` is the currently shown range on this axis.
|
||||||
|
pub fn x_axis_formatter(mut self, fmt: fn(f64, usize, &RangeInclusive<f64>) -> String) -> Self {
|
||||||
|
if let Some(main) = self.x_axes.first_mut() {
|
||||||
|
main.formatter = fmt;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specify custom formatter for ticks on the main Y-axis.
|
||||||
|
///
|
||||||
|
/// The first parameter of `formatter` is the raw tick value as `f64`.
|
||||||
|
/// The second parameter is the maximum requested number of characters per tick label.
|
||||||
|
/// The second parameter of `formatter` is the currently shown range on this axis.
|
||||||
|
pub fn y_axis_formatter(mut self, fmt: fn(f64, usize, &RangeInclusive<f64>) -> String) -> Self {
|
||||||
|
if let Some(main) = self.y_axes.first_mut() {
|
||||||
|
main.formatter = fmt;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the main Y-axis-width by number of digits
|
||||||
|
///
|
||||||
|
/// The default is 5 digits.
|
||||||
|
///
|
||||||
|
/// > Todo: This is experimental. Changing the font size might break this.
|
||||||
|
pub fn y_axis_width(mut self, digits: usize) -> Self {
|
||||||
|
if let Some(main) = self.y_axes.first_mut() {
|
||||||
|
main.digits = digits;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set custom configuration for X-axis
|
||||||
|
///
|
||||||
|
/// More than one axis may be specified. The first specified axis is considered the main axis.
|
||||||
|
pub fn custom_x_axes(mut self, hints: Vec<AxisHints>) -> Self {
|
||||||
|
self.x_axes = hints;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set custom configuration for left Y-axis
|
||||||
|
///
|
||||||
|
/// More than one axis may be specified. The first specified axis is considered the main axis.
|
||||||
|
pub fn custom_y_axes(mut self, hints: Vec<AxisHints>) -> Self {
|
||||||
|
self.y_axes = hints;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Interact with and add items to the plot and finally draw it.
|
/// Interact with and add items to the plot and finally draw it.
|
||||||
pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> PlotResponse<R> {
|
pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> PlotResponse<R> {
|
||||||
self.show_dyn(ui, Box::new(build_fn))
|
self.show_dyn(ui, Box::new(build_fn))
|
||||||
|
|
@ -597,8 +675,7 @@ impl Plot {
|
||||||
) -> PlotResponse<R> {
|
) -> PlotResponse<R> {
|
||||||
let Self {
|
let Self {
|
||||||
id_source,
|
id_source,
|
||||||
center_x_axis,
|
center_axis,
|
||||||
center_y_axis,
|
|
||||||
allow_zoom,
|
allow_zoom,
|
||||||
allow_drag,
|
allow_drag,
|
||||||
allow_scroll,
|
allow_scroll,
|
||||||
|
|
@ -617,11 +694,13 @@ impl Plot {
|
||||||
mut show_y,
|
mut show_y,
|
||||||
label_formatter,
|
label_formatter,
|
||||||
coordinates_formatter,
|
coordinates_formatter,
|
||||||
axis_formatters,
|
x_axes,
|
||||||
|
y_axes,
|
||||||
legend_config,
|
legend_config,
|
||||||
reset,
|
reset,
|
||||||
show_background,
|
show_background,
|
||||||
show_axes,
|
show_axes,
|
||||||
|
show_grid,
|
||||||
linked_axes,
|
linked_axes,
|
||||||
linked_cursors,
|
linked_cursors,
|
||||||
|
|
||||||
|
|
@ -630,7 +709,9 @@ impl Plot {
|
||||||
sharp_grid_lines,
|
sharp_grid_lines,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
// Determine the size of the plot in the UI
|
// Determine position of widget.
|
||||||
|
let pos = ui.available_rect_before_wrap().min;
|
||||||
|
// Determine size of widget.
|
||||||
let size = {
|
let size = {
|
||||||
let width = width
|
let width = width
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
|
|
@ -653,9 +734,79 @@ impl Plot {
|
||||||
.at_least(min_size.y);
|
.at_least(min_size.y);
|
||||||
vec2(width, height)
|
vec2(width, height)
|
||||||
};
|
};
|
||||||
|
// Determine complete rect of widget.
|
||||||
|
let complete_rect = Rect {
|
||||||
|
min: pos,
|
||||||
|
max: pos + size,
|
||||||
|
};
|
||||||
|
// Next we want to create this layout.
|
||||||
|
// Incides are only examples.
|
||||||
|
//
|
||||||
|
// left right
|
||||||
|
// +---+---------x----------+ +
|
||||||
|
// | | X-axis 3 |
|
||||||
|
// | +--------------------+ top
|
||||||
|
// | | X-axis 2 |
|
||||||
|
// +-+-+--------------------+-+-+
|
||||||
|
// |y|y| |y|y|
|
||||||
|
// |-|-| |-|-|
|
||||||
|
// |A|A| |A|A|
|
||||||
|
// y|x|x| Plot Window |x|x|
|
||||||
|
// |i|i| |i|i|
|
||||||
|
// |s|s| |s|s|
|
||||||
|
// |1|0| |2|3|
|
||||||
|
// +-+-+--------------------+-+-+
|
||||||
|
// | X-axis 0 | |
|
||||||
|
// +--------------------+ | bottom
|
||||||
|
// | X-axis 1 | |
|
||||||
|
// + +--------------------+---+
|
||||||
|
//
|
||||||
|
|
||||||
// Allocate the space.
|
let mut plot_rect: Rect = {
|
||||||
let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
|
// Calcuclate the space needed for each axis labels.
|
||||||
|
let mut margin = Margin::ZERO;
|
||||||
|
if show_axes.x {
|
||||||
|
for cfg in &x_axes {
|
||||||
|
match cfg.placement {
|
||||||
|
axis::Placement::LeftBottom => {
|
||||||
|
margin.bottom += cfg.thickness(Axis::X);
|
||||||
|
}
|
||||||
|
axis::Placement::RightTop => {
|
||||||
|
margin.top += cfg.thickness(Axis::X);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if show_axes.y {
|
||||||
|
for cfg in &y_axes {
|
||||||
|
match cfg.placement {
|
||||||
|
axis::Placement::LeftBottom => {
|
||||||
|
margin.left += cfg.thickness(Axis::Y);
|
||||||
|
}
|
||||||
|
axis::Placement::RightTop => {
|
||||||
|
margin.right += cfg.thickness(Axis::Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine plot rectangle
|
||||||
|
margin.shrink_rect(complete_rect)
|
||||||
|
};
|
||||||
|
|
||||||
|
let [mut x_axis_widgets, mut y_axis_widgets] =
|
||||||
|
axis_widgets(show_axes, plot_rect, [&x_axes, &y_axes]);
|
||||||
|
|
||||||
|
// If too little space, remove axis widgets
|
||||||
|
if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 {
|
||||||
|
y_axis_widgets.clear();
|
||||||
|
x_axis_widgets.clear();
|
||||||
|
plot_rect = complete_rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate the plot window.
|
||||||
|
let response = ui.allocate_rect(plot_rect, Sense::drag());
|
||||||
|
let rect = plot_rect;
|
||||||
|
|
||||||
// Load or initialize the memory.
|
// Load or initialize the memory.
|
||||||
let plot_id = ui.make_persistent_id(id_source);
|
let plot_id = ui.make_persistent_id(id_source);
|
||||||
|
|
@ -679,8 +830,8 @@ impl Plot {
|
||||||
last_plot_transform: PlotTransform::new(
|
last_plot_transform: PlotTransform::new(
|
||||||
rect,
|
rect,
|
||||||
min_auto_bounds,
|
min_auto_bounds,
|
||||||
center_x_axis,
|
center_axis.x,
|
||||||
center_y_axis,
|
center_axis.y,
|
||||||
),
|
),
|
||||||
last_click_pos_for_zoom: None,
|
last_click_pos_for_zoom: None,
|
||||||
});
|
});
|
||||||
|
|
@ -841,7 +992,7 @@ impl Plot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut transform = PlotTransform::new(rect, bounds, center_x_axis, center_y_axis);
|
let mut transform = PlotTransform::new(rect, bounds, center_axis.x, center_axis.y);
|
||||||
|
|
||||||
// Enforce aspect ratio
|
// Enforce aspect ratio
|
||||||
if let Some(data_aspect) = data_aspect {
|
if let Some(data_aspect) = data_aspect {
|
||||||
|
|
@ -949,6 +1100,39 @@ impl Plot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- transform initialized
|
||||||
|
|
||||||
|
// Add legend widgets to plot
|
||||||
|
let bounds = transform.bounds();
|
||||||
|
let x_axis_range = bounds.range_x();
|
||||||
|
let x_steps = Arc::new({
|
||||||
|
let input = GridInput {
|
||||||
|
bounds: (bounds.min[0], bounds.max[0]),
|
||||||
|
base_step_size: transform.dvalue_dpos()[0] * MIN_LINE_SPACING_IN_POINTS * 2.0,
|
||||||
|
};
|
||||||
|
(grid_spacers[0])(input)
|
||||||
|
});
|
||||||
|
let y_axis_range = bounds.range_y();
|
||||||
|
let y_steps = Arc::new({
|
||||||
|
let input = GridInput {
|
||||||
|
bounds: (bounds.min[1], bounds.max[1]),
|
||||||
|
base_step_size: transform.dvalue_dpos()[1] * MIN_LINE_SPACING_IN_POINTS * 2.0,
|
||||||
|
};
|
||||||
|
(grid_spacers[1])(input)
|
||||||
|
});
|
||||||
|
for mut widget in x_axis_widgets {
|
||||||
|
widget.range = x_axis_range.clone();
|
||||||
|
widget.transform = Some(transform);
|
||||||
|
widget.steps = x_steps.clone();
|
||||||
|
widget.ui(ui, Axis::X);
|
||||||
|
}
|
||||||
|
for mut widget in y_axis_widgets {
|
||||||
|
widget.range = y_axis_range.clone();
|
||||||
|
widget.transform = Some(transform);
|
||||||
|
widget.steps = y_steps.clone();
|
||||||
|
widget.ui(ui, Axis::Y);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize values from functions.
|
// Initialize values from functions.
|
||||||
for item in &mut items {
|
for item in &mut items {
|
||||||
item.initialize(transform.bounds().range_x());
|
item.initialize(transform.bounds().range_x());
|
||||||
|
|
@ -960,16 +1144,16 @@ impl Plot {
|
||||||
show_y,
|
show_y,
|
||||||
label_formatter,
|
label_formatter,
|
||||||
coordinates_formatter,
|
coordinates_formatter,
|
||||||
axis_formatters,
|
show_grid,
|
||||||
show_axes,
|
|
||||||
transform,
|
transform,
|
||||||
draw_cursor_x: linked_cursors.as_ref().map_or(false, |(_, group)| group.x),
|
draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.1.x),
|
||||||
draw_cursor_y: linked_cursors.as_ref().map_or(false, |(_, group)| group.y),
|
draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.1.y),
|
||||||
draw_cursors,
|
draw_cursors,
|
||||||
grid_spacers,
|
grid_spacers,
|
||||||
sharp_grid_lines,
|
sharp_grid_lines,
|
||||||
clamp_grid,
|
clamp_grid,
|
||||||
};
|
};
|
||||||
|
|
||||||
let plot_cursors = prepared.ui(ui, &response);
|
let plot_cursors = prepared.ui(ui, &response);
|
||||||
|
|
||||||
if let Some(boxed_zoom_rect) = boxed_zoom_rect {
|
if let Some(boxed_zoom_rect) = boxed_zoom_rect {
|
||||||
|
|
@ -1024,7 +1208,7 @@ impl Plot {
|
||||||
} else {
|
} else {
|
||||||
response
|
response
|
||||||
};
|
};
|
||||||
|
ui.advance_cursor_after_rect(complete_rect);
|
||||||
PlotResponse {
|
PlotResponse {
|
||||||
inner,
|
inner,
|
||||||
response,
|
response,
|
||||||
|
|
@ -1033,6 +1217,79 @@ impl Plot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn axis_widgets(
|
||||||
|
show_axes: AxisBools,
|
||||||
|
plot_rect: Rect,
|
||||||
|
[x_axes, y_axes]: [&[AxisHints]; 2],
|
||||||
|
) -> [Vec<AxisWidget>; 2] {
|
||||||
|
let mut x_axis_widgets = Vec::<AxisWidget>::new();
|
||||||
|
let mut y_axis_widgets = Vec::<AxisWidget>::new();
|
||||||
|
|
||||||
|
// Widget count per border of plot in order left, top, right, bottom
|
||||||
|
struct NumWidgets {
|
||||||
|
left: usize,
|
||||||
|
top: usize,
|
||||||
|
right: usize,
|
||||||
|
bottom: usize,
|
||||||
|
}
|
||||||
|
let mut num_widgets = NumWidgets {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
};
|
||||||
|
if show_axes.x {
|
||||||
|
for cfg in x_axes {
|
||||||
|
let size_y = Vec2::new(0.0, cfg.thickness(Axis::X));
|
||||||
|
let rect = match cfg.placement {
|
||||||
|
axis::Placement::LeftBottom => {
|
||||||
|
let off = num_widgets.bottom as f32;
|
||||||
|
num_widgets.bottom += 1;
|
||||||
|
Rect {
|
||||||
|
min: plot_rect.left_bottom() + size_y * off,
|
||||||
|
max: plot_rect.right_bottom() + size_y * (off + 1.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
axis::Placement::RightTop => {
|
||||||
|
let off = num_widgets.top as f32;
|
||||||
|
num_widgets.top += 1;
|
||||||
|
Rect {
|
||||||
|
min: plot_rect.left_top() - size_y * (off + 1.0),
|
||||||
|
max: plot_rect.right_top() - size_y * off,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if show_axes.y {
|
||||||
|
for cfg in y_axes {
|
||||||
|
let size_x = Vec2::new(cfg.thickness(Axis::Y), 0.0);
|
||||||
|
let rect = match cfg.placement {
|
||||||
|
axis::Placement::LeftBottom => {
|
||||||
|
let off = num_widgets.left as f32;
|
||||||
|
num_widgets.left += 1;
|
||||||
|
Rect {
|
||||||
|
min: plot_rect.left_top() - size_x * (off + 1.0),
|
||||||
|
max: plot_rect.left_bottom() - size_x * off,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
axis::Placement::RightTop => {
|
||||||
|
let off = num_widgets.right as f32;
|
||||||
|
num_widgets.right += 1;
|
||||||
|
Rect {
|
||||||
|
min: plot_rect.right_top() + size_x * off,
|
||||||
|
max: plot_rect.right_bottom() + size_x * (off + 1.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[x_axis_widgets, y_axis_widgets]
|
||||||
|
}
|
||||||
|
|
||||||
/// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply
|
/// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply
|
||||||
/// them at the right time, as other modifications need to happen first.
|
/// them at the right time, as other modifications need to happen first.
|
||||||
enum BoundsModification {
|
enum BoundsModification {
|
||||||
|
|
@ -1268,6 +1525,7 @@ pub struct GridInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One mark (horizontal or vertical line) in the background grid of a plot.
|
/// One mark (horizontal or vertical line) in the background grid of a plot.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct GridMark {
|
pub struct GridMark {
|
||||||
/// X or Y value in the plot.
|
/// X or Y value in the plot.
|
||||||
pub value: f64,
|
pub value: f64,
|
||||||
|
|
@ -1329,14 +1587,14 @@ struct PreparedPlot {
|
||||||
show_y: bool,
|
show_y: bool,
|
||||||
label_formatter: LabelFormatter,
|
label_formatter: LabelFormatter,
|
||||||
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
||||||
axis_formatters: [AxisFormatter; 2],
|
// axis_formatters: [AxisFormatter; 2],
|
||||||
show_axes: [bool; 2],
|
|
||||||
transform: PlotTransform,
|
transform: PlotTransform,
|
||||||
|
show_grid: AxisBools,
|
||||||
|
grid_spacers: [GridSpacer; 2],
|
||||||
draw_cursor_x: bool,
|
draw_cursor_x: bool,
|
||||||
draw_cursor_y: bool,
|
draw_cursor_y: bool,
|
||||||
draw_cursors: Vec<Cursor>,
|
draw_cursors: Vec<Cursor>,
|
||||||
|
|
||||||
grid_spacers: [GridSpacer; 2],
|
|
||||||
sharp_grid_lines: bool,
|
sharp_grid_lines: bool,
|
||||||
clamp_grid: bool,
|
clamp_grid: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -1345,16 +1603,11 @@ impl PreparedPlot {
|
||||||
fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
|
fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
|
||||||
let mut axes_shapes = Vec::new();
|
let mut axes_shapes = Vec::new();
|
||||||
|
|
||||||
for d in 0..2 {
|
if self.show_grid.x {
|
||||||
if self.show_axes[d] {
|
self.paint_grid(ui, &mut axes_shapes, Axis::X);
|
||||||
self.paint_axis(
|
}
|
||||||
ui,
|
if self.show_grid.y {
|
||||||
d,
|
self.paint_grid(ui, &mut axes_shapes, Axis::Y);
|
||||||
self.show_axes[1 - d],
|
|
||||||
&mut axes_shapes,
|
|
||||||
self.sharp_grid_lines,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the axes by strength so that those with higher strength are drawn in front.
|
// Sort the axes by strength so that those with higher strength are drawn in front.
|
||||||
|
|
@ -1431,41 +1684,27 @@ impl PreparedPlot {
|
||||||
cursors
|
cursors
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint_axis(
|
fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis) {
|
||||||
&self,
|
|
||||||
ui: &Ui,
|
|
||||||
axis: usize,
|
|
||||||
other_axis_shown: bool,
|
|
||||||
shapes: &mut Vec<(Shape, f32)>,
|
|
||||||
sharp_grid_lines: bool,
|
|
||||||
) {
|
|
||||||
#![allow(clippy::collapsible_else_if)]
|
#![allow(clippy::collapsible_else_if)]
|
||||||
|
|
||||||
let Self {
|
let Self {
|
||||||
transform,
|
transform,
|
||||||
axis_formatters,
|
// axis_formatters,
|
||||||
grid_spacers,
|
grid_spacers,
|
||||||
clamp_grid,
|
clamp_grid,
|
||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let bounds = transform.bounds();
|
let iaxis = usize::from(axis);
|
||||||
let axis_range = match axis {
|
|
||||||
0 => bounds.range_x(),
|
|
||||||
1 => bounds.range_y(),
|
|
||||||
_ => panic!("Axis {axis} does not exist."),
|
|
||||||
};
|
|
||||||
|
|
||||||
let font_id = TextStyle::Body.resolve(ui.style());
|
|
||||||
|
|
||||||
// Where on the cross-dimension to show the label values
|
// Where on the cross-dimension to show the label values
|
||||||
let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]);
|
let bounds = transform.bounds();
|
||||||
|
let value_cross = 0.0_f64.clamp(bounds.min[1 - iaxis], bounds.max[1 - iaxis]);
|
||||||
|
|
||||||
let input = GridInput {
|
let input = GridInput {
|
||||||
bounds: (bounds.min[axis], bounds.max[axis]),
|
bounds: (bounds.min[iaxis], bounds.max[iaxis]),
|
||||||
base_step_size: transform.dvalue_dpos()[axis] * MIN_LINE_SPACING_IN_POINTS,
|
base_step_size: transform.dvalue_dpos()[iaxis] * MIN_LINE_SPACING_IN_POINTS,
|
||||||
};
|
};
|
||||||
let steps = (grid_spacers[axis])(input);
|
let steps = (grid_spacers[iaxis])(input);
|
||||||
|
|
||||||
let clamp_range = clamp_grid.then(|| {
|
let clamp_range = clamp_grid.then(|| {
|
||||||
let mut tight_bounds = PlotBounds::NOTHING;
|
let mut tight_bounds = PlotBounds::NOTHING;
|
||||||
|
|
@ -1481,25 +1720,27 @@ impl PreparedPlot {
|
||||||
let value_main = step.value;
|
let value_main = step.value;
|
||||||
|
|
||||||
if let Some(clamp_range) = clamp_range {
|
if let Some(clamp_range) = clamp_range {
|
||||||
if axis == 0 {
|
match axis {
|
||||||
if !clamp_range.range_x().contains(&value_main) {
|
Axis::X => {
|
||||||
continue;
|
if !clamp_range.range_x().contains(&value_main) {
|
||||||
};
|
continue;
|
||||||
} else {
|
};
|
||||||
if !clamp_range.range_y().contains(&value_main) {
|
}
|
||||||
continue;
|
Axis::Y => {
|
||||||
};
|
if !clamp_range.range_y().contains(&value_main) {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = if axis == 0 {
|
let value = match axis {
|
||||||
PlotPoint::new(value_main, value_cross)
|
Axis::X => PlotPoint::new(value_main, value_cross),
|
||||||
} else {
|
Axis::Y => PlotPoint::new(value_cross, value_main),
|
||||||
PlotPoint::new(value_cross, value_main)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let pos_in_gui = transform.position_from_point(&value);
|
let pos_in_gui = transform.position_from_point(&value);
|
||||||
let spacing_in_points = (transform.dpos_dvalue()[axis] * step.step_size).abs() as f32;
|
let spacing_in_points = (transform.dpos_dvalue()[iaxis] * step.step_size).abs() as f32;
|
||||||
|
|
||||||
if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 {
|
if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 {
|
||||||
let line_strength = remap_clamp(
|
let line_strength = remap_clamp(
|
||||||
|
|
@ -1508,24 +1749,27 @@ impl PreparedPlot {
|
||||||
0.0..=1.0,
|
0.0..=1.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
let line_color = color_from_contrast(ui, line_strength);
|
let line_color = color_from_strength(ui, line_strength);
|
||||||
|
|
||||||
let mut p0 = pos_in_gui;
|
let mut p0 = pos_in_gui;
|
||||||
let mut p1 = pos_in_gui;
|
let mut p1 = pos_in_gui;
|
||||||
p0[1 - axis] = transform.frame().min[1 - axis];
|
p0[1 - iaxis] = transform.frame().min[1 - iaxis];
|
||||||
p1[1 - axis] = transform.frame().max[1 - axis];
|
p1[1 - iaxis] = transform.frame().max[1 - iaxis];
|
||||||
|
|
||||||
if let Some(clamp_range) = clamp_range {
|
if let Some(clamp_range) = clamp_range {
|
||||||
if axis == 0 {
|
match axis {
|
||||||
p0.y = transform.position_from_point_y(clamp_range.min[1]);
|
Axis::X => {
|
||||||
p1.y = transform.position_from_point_y(clamp_range.max[1]);
|
p0.y = transform.position_from_point_y(clamp_range.min[1]);
|
||||||
} else {
|
p1.y = transform.position_from_point_y(clamp_range.max[1]);
|
||||||
p0.x = transform.position_from_point_x(clamp_range.min[0]);
|
}
|
||||||
p1.x = transform.position_from_point_x(clamp_range.max[0]);
|
Axis::Y => {
|
||||||
|
p0.x = transform.position_from_point_x(clamp_range.min[0]);
|
||||||
|
p1.x = transform.position_from_point_x(clamp_range.max[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sharp_grid_lines {
|
if self.sharp_grid_lines {
|
||||||
// Round to avoid aliasing
|
// Round to avoid aliasing
|
||||||
p0 = ui.ctx().round_pos_to_pixels(p0);
|
p0 = ui.ctx().round_pos_to_pixels(p0);
|
||||||
p1 = ui.ctx().round_pos_to_pixels(p1);
|
p1 = ui.ctx().round_pos_to_pixels(p1);
|
||||||
|
|
@ -1536,47 +1780,6 @@ impl PreparedPlot {
|
||||||
line_strength,
|
line_strength,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_TEXT_SPACING: f32 = 40.0;
|
|
||||||
if spacing_in_points > MIN_TEXT_SPACING {
|
|
||||||
let text_strength =
|
|
||||||
remap_clamp(spacing_in_points, MIN_TEXT_SPACING..=150.0, 0.0..=1.0);
|
|
||||||
let color = color_from_contrast(ui, text_strength);
|
|
||||||
|
|
||||||
let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
|
|
||||||
formatter(value_main, &axis_range)
|
|
||||||
} else {
|
|
||||||
emath::round_to_decimals(value_main, 5).to_string() // hack
|
|
||||||
};
|
|
||||||
|
|
||||||
// Skip origin label for y-axis if x-axis is already showing it (otherwise displayed twice)
|
|
||||||
let skip_origin_y = axis == 1 && other_axis_shown && value_main == 0.0;
|
|
||||||
|
|
||||||
// Custom formatters can return empty string to signal "no label at this resolution"
|
|
||||||
if !text.is_empty() && !skip_origin_y {
|
|
||||||
let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color);
|
|
||||||
|
|
||||||
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
|
|
||||||
|
|
||||||
// Make sure we see the labels, even if the axis is off-screen:
|
|
||||||
text_pos[1 - axis] = text_pos[1 - axis]
|
|
||||||
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
|
|
||||||
.at_least(transform.frame().min[1 - axis] + 1.0);
|
|
||||||
|
|
||||||
shapes.push((Shape::galley(text_pos, galley), text_strength));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color_from_contrast(ui: &Ui, contrast: f32) -> Color32 {
|
|
||||||
let bg = ui.visuals().extreme_bg_color;
|
|
||||||
let fg = ui.visuals().widgets.open.fg_stroke.color;
|
|
||||||
let mix = 0.5 * contrast.sqrt();
|
|
||||||
Color32::from_rgb(
|
|
||||||
lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8,
|
|
||||||
lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8,
|
|
||||||
lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1682,3 +1885,15 @@ pub fn format_number(number: f64, num_decimals: usize) -> String {
|
||||||
format!("{:.*}", num_decimals.at_least(1), number)
|
format!("{:.*}", num_decimals.at_least(1), number)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine a color from a 0-1 strength value.
|
||||||
|
pub fn color_from_strength(ui: &Ui, strength: f32) -> Color32 {
|
||||||
|
let bg = ui.visuals().extreme_bg_color;
|
||||||
|
let fg = ui.visuals().widgets.open.fg_stroke.color;
|
||||||
|
let mix = 0.5 * strength.sqrt();
|
||||||
|
Color32::from_rgb(
|
||||||
|
lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8,
|
||||||
|
lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8,
|
||||||
|
lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
use std::f64::consts::TAU;
|
use std::f64::consts::TAU;
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
use egui::plot::{AxisBools, GridInput, GridMark, PlotResponse};
|
|
||||||
use egui::*;
|
use egui::*;
|
||||||
use plot::{
|
|
||||||
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner, HLine,
|
use egui::plot::{
|
||||||
Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, PlotPoint, PlotPoints, Points, Polygon,
|
Arrows, AxisBools, AxisHints, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter,
|
||||||
Text, VLine,
|
Corner, GridInput, GridMark, HLine, Legend, Line, LineStyle, MarkerShape, Plot, PlotImage,
|
||||||
|
PlotPoint, PlotPoints, PlotResponse, Points, Polygon, Text, VLine,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
@ -119,17 +119,9 @@ impl super::View for PlotDemo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_approx_zero(val: f64) -> bool {
|
|
||||||
val.abs() < 1e-6
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_approx_integer(val: f64) -> bool {
|
|
||||||
val.fract().abs() < 1e-6
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
struct LineDemo {
|
struct LineDemo {
|
||||||
animate: bool,
|
animate: bool,
|
||||||
time: f64,
|
time: f64,
|
||||||
|
|
@ -138,6 +130,8 @@ struct LineDemo {
|
||||||
square: bool,
|
square: bool,
|
||||||
proportional: bool,
|
proportional: bool,
|
||||||
coordinates: bool,
|
coordinates: bool,
|
||||||
|
show_axes: bool,
|
||||||
|
show_grid: bool,
|
||||||
line_style: LineStyle,
|
line_style: LineStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,6 +145,8 @@ impl Default for LineDemo {
|
||||||
square: false,
|
square: false,
|
||||||
proportional: true,
|
proportional: true,
|
||||||
coordinates: true,
|
coordinates: true,
|
||||||
|
show_axes: true,
|
||||||
|
show_grid: true,
|
||||||
line_style: LineStyle::Solid,
|
line_style: LineStyle::Solid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -165,9 +161,10 @@ impl LineDemo {
|
||||||
circle_center,
|
circle_center,
|
||||||
square,
|
square,
|
||||||
proportional,
|
proportional,
|
||||||
line_style,
|
|
||||||
coordinates,
|
coordinates,
|
||||||
..
|
show_axes,
|
||||||
|
show_grid,
|
||||||
|
line_style,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
|
@ -195,6 +192,13 @@ impl LineDemo {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.checkbox(show_axes, "Show axes");
|
||||||
|
ui.checkbox(show_grid, "Show grid");
|
||||||
|
ui.checkbox(coordinates, "Show coordinates on hover")
|
||||||
|
.on_hover_text("Can take a custom formatting function.");
|
||||||
|
});
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap = Some(false);
|
||||||
ui.checkbox(animate, "Animate");
|
ui.checkbox(animate, "Animate");
|
||||||
|
|
@ -202,8 +206,6 @@ impl LineDemo {
|
||||||
.on_hover_text("Always keep the viewport square.");
|
.on_hover_text("Always keep the viewport square.");
|
||||||
ui.checkbox(proportional, "Proportional data axes")
|
ui.checkbox(proportional, "Proportional data axes")
|
||||||
.on_hover_text("Tick are the same size on both axes.");
|
.on_hover_text("Tick are the same size on both axes.");
|
||||||
ui.checkbox(coordinates, "Show coordinates")
|
|
||||||
.on_hover_text("Can take a custom formatting function.");
|
|
||||||
|
|
||||||
ComboBox::from_label("Line style")
|
ComboBox::from_label("Line style")
|
||||||
.selected_text(line_style.to_string())
|
.selected_text(line_style.to_string())
|
||||||
|
|
@ -268,11 +270,16 @@ impl LineDemo {
|
||||||
impl LineDemo {
|
impl LineDemo {
|
||||||
fn ui(&mut self, ui: &mut Ui) -> Response {
|
fn ui(&mut self, ui: &mut Ui) -> Response {
|
||||||
self.options_ui(ui);
|
self.options_ui(ui);
|
||||||
|
|
||||||
if self.animate {
|
if self.animate {
|
||||||
ui.ctx().request_repaint();
|
ui.ctx().request_repaint();
|
||||||
self.time += ui.input(|i| i.unstable_dt).at_most(1.0 / 30.0) as f64;
|
self.time += ui.input(|i| i.unstable_dt).at_most(1.0 / 30.0) as f64;
|
||||||
};
|
};
|
||||||
let mut plot = Plot::new("lines_demo").legend(Legend::default());
|
let mut plot = Plot::new("lines_demo")
|
||||||
|
.legend(Legend::default())
|
||||||
|
.y_axis_width(4)
|
||||||
|
.show_axes(self.show_axes)
|
||||||
|
.show_grid(self.show_grid);
|
||||||
if self.square {
|
if self.square {
|
||||||
plot = plot.view_aspect(1.0);
|
plot = plot.view_aspect(1.0);
|
||||||
}
|
}
|
||||||
|
|
@ -429,8 +436,8 @@ impl LegendDemo {
|
||||||
);
|
);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
});
|
});
|
||||||
|
|
||||||
let legend_plot = Plot::new("legend_demo")
|
let legend_plot = Plot::new("legend_demo")
|
||||||
|
.y_axis_width(2)
|
||||||
.legend(config.clone())
|
.legend(config.clone())
|
||||||
.data_aspect(1.0);
|
.data_aspect(1.0);
|
||||||
legend_plot
|
legend_plot
|
||||||
|
|
@ -523,7 +530,7 @@ impl CustomAxesDemo {
|
||||||
100.0 * y
|
100.0 * y
|
||||||
}
|
}
|
||||||
|
|
||||||
let x_fmt = |x, _range: &RangeInclusive<f64>| {
|
let x_fmt = |x, _digits, _range: &RangeInclusive<f64>| {
|
||||||
if x < 0.0 * MINS_PER_DAY || x >= 5.0 * MINS_PER_DAY {
|
if x < 0.0 * MINS_PER_DAY || x >= 5.0 * MINS_PER_DAY {
|
||||||
// No labels outside value bounds
|
// No labels outside value bounds
|
||||||
String::new()
|
String::new()
|
||||||
|
|
@ -536,7 +543,7 @@ impl CustomAxesDemo {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let y_fmt = |y, _range: &RangeInclusive<f64>| {
|
let y_fmt = |y, _digits, _range: &RangeInclusive<f64>| {
|
||||||
// Display only integer percentages
|
// Display only integer percentages
|
||||||
if !is_approx_zero(y) && is_approx_integer(100.0 * y) {
|
if !is_approx_zero(y) && is_approx_integer(100.0 * y) {
|
||||||
format!("{:.0}%", percent(y))
|
format!("{:.0}%", percent(y))
|
||||||
|
|
@ -557,10 +564,23 @@ impl CustomAxesDemo {
|
||||||
|
|
||||||
ui.label("Zoom in on the X-axis to see hours and minutes");
|
ui.label("Zoom in on the X-axis to see hours and minutes");
|
||||||
|
|
||||||
|
let x_axes = vec![
|
||||||
|
AxisHints::default().label("Time").formatter(x_fmt),
|
||||||
|
AxisHints::default().label("Value"),
|
||||||
|
];
|
||||||
|
let y_axes = vec![
|
||||||
|
AxisHints::default()
|
||||||
|
.label("Percent")
|
||||||
|
.formatter(y_fmt)
|
||||||
|
.max_digits(4),
|
||||||
|
AxisHints::default()
|
||||||
|
.label("Absolute")
|
||||||
|
.placement(plot::HPlacement::Right),
|
||||||
|
];
|
||||||
Plot::new("custom_axes")
|
Plot::new("custom_axes")
|
||||||
.data_aspect(2.0 * MINS_PER_DAY as f32)
|
.data_aspect(2.0 * MINS_PER_DAY as f32)
|
||||||
.x_axis_formatter(x_fmt)
|
.custom_x_axes(x_axes)
|
||||||
.y_axis_formatter(y_fmt)
|
.custom_y_axes(y_axes)
|
||||||
.x_grid_spacer(CustomAxesDemo::x_grid)
|
.x_grid_spacer(CustomAxesDemo::x_grid)
|
||||||
.label_formatter(label_fmt)
|
.label_formatter(label_fmt)
|
||||||
.show(ui, |plot_ui| {
|
.show(ui, |plot_ui| {
|
||||||
|
|
@ -582,15 +602,11 @@ struct LinkedAxesDemo {
|
||||||
|
|
||||||
impl Default for LinkedAxesDemo {
|
impl Default for LinkedAxesDemo {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let link_x = true;
|
|
||||||
let link_y = false;
|
|
||||||
let link_cursor_x = true;
|
|
||||||
let link_cursor_y = false;
|
|
||||||
Self {
|
Self {
|
||||||
link_x,
|
link_x: true,
|
||||||
link_y,
|
link_y: true,
|
||||||
link_cursor_x,
|
link_cursor_x: true,
|
||||||
link_cursor_y,
|
link_cursor_y: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -642,25 +658,29 @@ impl LinkedAxesDemo {
|
||||||
|
|
||||||
let link_group_id = ui.id().with("linked_demo");
|
let link_group_id = ui.id().with("linked_demo");
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
Plot::new("linked_axis_1")
|
Plot::new("left-top")
|
||||||
.data_aspect(1.0)
|
.data_aspect(1.0)
|
||||||
.width(250.0)
|
.width(250.0)
|
||||||
.height(250.0)
|
.height(250.0)
|
||||||
.link_axis(link_group_id, self.link_x, self.link_y)
|
.link_axis(link_group_id, self.link_x, self.link_y)
|
||||||
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
|
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
|
||||||
.show(ui, LinkedAxesDemo::configure_plot);
|
.show(ui, LinkedAxesDemo::configure_plot);
|
||||||
Plot::new("linked_axis_2")
|
Plot::new("right-top")
|
||||||
.data_aspect(2.0)
|
.data_aspect(2.0)
|
||||||
.width(150.0)
|
.width(150.0)
|
||||||
.height(250.0)
|
.height(250.0)
|
||||||
|
.y_axis_width(3)
|
||||||
|
.y_axis_label("y")
|
||||||
|
.y_axis_position(plot::HPlacement::Right)
|
||||||
.link_axis(link_group_id, self.link_x, self.link_y)
|
.link_axis(link_group_id, self.link_x, self.link_y)
|
||||||
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
|
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
|
||||||
.show(ui, LinkedAxesDemo::configure_plot);
|
.show(ui, LinkedAxesDemo::configure_plot);
|
||||||
});
|
});
|
||||||
Plot::new("linked_axis_3")
|
Plot::new("left-bottom")
|
||||||
.data_aspect(0.5)
|
.data_aspect(0.5)
|
||||||
.width(250.0)
|
.width(250.0)
|
||||||
.height(150.0)
|
.height(150.0)
|
||||||
|
.x_axis_label("x")
|
||||||
.link_axis(link_group_id, self.link_x, self.link_y)
|
.link_axis(link_group_id, self.link_x, self.link_y)
|
||||||
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
|
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
|
||||||
.show(ui, LinkedAxesDemo::configure_plot)
|
.show(ui, LinkedAxesDemo::configure_plot)
|
||||||
|
|
@ -889,6 +909,7 @@ impl ChartsDemo {
|
||||||
Plot::new("Normal Distribution Demo")
|
Plot::new("Normal Distribution Demo")
|
||||||
.legend(Legend::default())
|
.legend(Legend::default())
|
||||||
.clamp_grid(true)
|
.clamp_grid(true)
|
||||||
|
.y_axis_width(3)
|
||||||
.allow_zoom(self.allow_zoom)
|
.allow_zoom(self.allow_zoom)
|
||||||
.allow_drag(self.allow_drag)
|
.allow_drag(self.allow_drag)
|
||||||
.show(ui, |plot_ui| plot_ui.bar_chart(chart))
|
.show(ui, |plot_ui| plot_ui.bar_chart(chart))
|
||||||
|
|
@ -1003,3 +1024,11 @@ impl ChartsDemo {
|
||||||
.response
|
.response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_approx_zero(val: f64) -> bool {
|
||||||
|
val.abs() < 1e-6
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_approx_integer(val: f64) -> bool {
|
||||||
|
val.fract().abs() < 1e-6
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,7 @@ fn example_plot(ui: &mut egui::Ui) -> egui::Response {
|
||||||
let line = Line::new(line_points);
|
let line = Line::new(line_points);
|
||||||
egui::plot::Plot::new("example_plot")
|
egui::plot::Plot::new("example_plot")
|
||||||
.height(32.0)
|
.height(32.0)
|
||||||
|
.show_axes(false)
|
||||||
.data_aspect(1.0)
|
.data_aspect(1.0)
|
||||||
.show(ui, |plot_ui| plot_ui.line(line))
|
.show(ui, |plot_ui| plot_ui.line(line))
|
||||||
.response
|
.response
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue