Skip to content

Commit

Permalink
Expand plot axes thickness to fit their labels (#3921)
Browse files Browse the repository at this point in the history
Expand the plot axis thickness as the contained plot axis labels get
wider.

This fixes a problem where the plot labels would otherwise get clipped.


![plot-axis-expansion](https://github.com/emilk/egui/assets/1148717/4500a26e-4a11-401d-9e8e-2d98d02ef3b7)
  • Loading branch information
emilk authored Jan 30, 2024
1 parent 01597fe commit 527f4bf
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 264 deletions.
13 changes: 11 additions & 2 deletions crates/egui/src/painter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
Color32, Context, FontId,
};
use epaint::{
text::{Fonts, Galley},
text::{Fonts, Galley, LayoutJob},
CircleShape, ClippedShape, RectShape, Rounding, Shape, Stroke,
};

Expand Down Expand Up @@ -436,9 +436,18 @@ impl Painter {
self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
}

/// Lay out this text layut job in a galley.
///
/// Paint the results with [`Self::galley`].
#[inline]
#[must_use]
pub fn layout_job(&self, layout_job: LayoutJob) -> Arc<Galley> {
self.fonts(|f| f.layout_job(layout_job))
}

/// Paint text that has already been laid out in a [`Galley`].
///
/// You can create the [`Galley`] with [`Self::layout`].
/// You can create the [`Galley`] with [`Self::layout`] or [`Self::layout_job`].
///
/// Any uncolored parts of the [`Galley`] (using [`Color32::PLACEHOLDER`]) will be replaced with the given color.
///
Expand Down
204 changes: 126 additions & 78 deletions crates/egui_plot/src/axis.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::{fmt::Debug, ops::RangeInclusive, sync::Arc};

use egui::{
emath::{remap_clamp, round_to_decimals},
emath::{remap_clamp, round_to_decimals, Rot2},
epaint::TextShape,
Pos2, Rangef, Rect, Response, Sense, Shape, TextStyle, Ui, WidgetText,
Pos2, Rangef, Rect, Response, Sense, TextStyle, Ui, Vec2, WidgetText,
};

use super::{transform::PlotTransform, GridMark};
Expand Down Expand Up @@ -64,6 +64,16 @@ impl From<HPlacement> for Placement {
}
}

impl From<Placement> for HPlacement {
#[inline]
fn from(placement: Placement) -> Self {
match placement {
Placement::LeftBottom => Self::Left,
Placement::RightTop => Self::Right,
}
}
}

impl From<VPlacement> for Placement {
#[inline]
fn from(placement: VPlacement) -> Self {
Expand All @@ -74,6 +84,16 @@ impl From<VPlacement> for Placement {
}
}

impl From<Placement> for VPlacement {
#[inline]
fn from(placement: Placement) -> Self {
match placement {
Placement::LeftBottom => Self::Bottom,
Placement::RightTop => Self::Top,
}
}
}

/// Axis configuration.
///
/// Used to configure axis label and ticks.
Expand Down Expand Up @@ -211,16 +231,18 @@ impl AxisHints {

#[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>>,
pub range: RangeInclusive<f64>,
pub hints: AxisHints,

/// The region where we draw the axis labels.
pub rect: Rect,
pub transform: Option<PlotTransform>,
pub 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 {
pub fn new(hints: AxisHints, rect: Rect) -> Self {
Self {
range: (0.0..=0.0),
hints,
Expand All @@ -230,70 +252,76 @@ impl AxisWidget {
}
}

pub fn ui(self, ui: &mut Ui, axis: Axis) -> Response {
/// Returns the actual thickness of the axis.
pub fn ui(self, ui: &mut Ui, axis: Axis) -> (Response, f32) {
let response = ui.allocate_rect(self.rect, Sense::hover());

if !ui.is_rect_visible(response.rect) {
return response;
return (response, 0.0);
}

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,

{
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,
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,
},
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,
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,
}
}
}
},
};
},
};

ui.painter()
.add(TextShape::new(text_pos, galley, text_color).with_angle(angle));
ui.painter()
.add(TextShape::new(text_pos, galley, text_color).with_angle(angle));
}

// --- add ticks ---
let font_id = TextStyle::Body.resolve(ui.style());
let Some(transform) = self.transform else {
return response;
return (response, 0.0);
};

let label_spacing = self.hints.label_spacing;

let mut thickness: f32 = 0.0;

// Add tick labels:
for step in self.steps.iter() {
let text = (self.hints.formatter)(*step, self.hints.digits, &self.range);
if !text.is_empty() {
Expand All @@ -314,41 +342,61 @@ impl AxisWidget {
.layout_no_wrap(text, font_id.clone(), text_color);

if spacing_in_points < galley.size()[axis as usize] {
continue; // the galley won't fit
continue; // the galley won't fit (likely too wide on the X axis).
}

let text_pos = match axis {
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,
};
thickness = thickness.max(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,
}
let center_x = transform.position_from_point(&projected_point).x;
let y = match VPlacement::from(self.hints.placement) {
VPlacement::Bottom => self.rect.min.y,
VPlacement::Top => self.rect.max.y - galley.size().y,
};
let pos = Pos2::new(center_x - galley.size().x / 2.0, y);
ui.painter().add(TextShape::new(pos, galley, text_color));
}
Axis::Y => {
let x = match self.hints.placement {
Placement::LeftBottom => self.rect.max.x - galley.size().x,
Placement::RightTop => self.rect.min.x,
};
thickness = thickness.max(galley.size().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,
}
let center_y = transform.position_from_point(&projected_point).y;

match HPlacement::from(self.hints.placement) {
HPlacement::Left => {
let angle = 0.0; // TODO: allow users to rotate text

if angle == 0.0 {
let x = self.rect.max.x - galley.size().x;
let pos = Pos2::new(x, center_y - galley.size().y / 2.0);
ui.painter().add(TextShape::new(pos, galley, text_color));
} else {
let right = Pos2::new(
self.rect.max.x,
center_y - galley.size().y / 2.0,
);
let width = galley.size().x;
let left =
right - Rot2::from_angle(angle) * Vec2::new(width, 0.0);

ui.painter().add(
TextShape::new(left, galley, text_color).with_angle(angle),
);
}
}
HPlacement::Right => {
let x = self.rect.min.x;
let pos = Pos2::new(x, center_y - galley.size().y / 2.0);
ui.painter().add(TextShape::new(pos, galley, text_color));
}
};
}
};

ui.painter()
.add(Shape::galley(text_pos, galley, text_color));
}
}

response
(response, thickness)
}
}
Loading

0 comments on commit 527f4bf

Please sign in to comment.