Added support for linked axes. See comments.

This commit adds support for linked axes between plots. One can specify
such linked limits using the new `linked_x_axis()` function (and the
variants for the Y axes) by specifying an `Rc<RefCell<ImPlotRange>>`
value, and passing clones of the same `Rc` to other plots.

The values within those `Rc` need to be kept persistent between frames,
hence the way to use this functionality is to keep a clone of the `Rc`
outside the frame-drawing function as part of the application state.

The regular limit setting API is unaffected.
This commit is contained in:
4bb4 2021-05-30 11:45:16 +02:00
parent ad80781f4d
commit 06cc3061c1
5 changed files with 570 additions and 391 deletions

View file

@ -9,7 +9,25 @@ pub mod text_plots;
use imgui::{im_str, Condition, Ui, Window}; use imgui::{im_str, Condition, Ui, Window};
use implot::PlotUi; use implot::PlotUi;
pub fn show_demos(ui: &Ui, plot_ui: &PlotUi) { /// State of the demo code
pub struct DemoState {
/// State of the line plots demo
line_plots: line_plots::LinePlotDemoState,
}
impl DemoState {
/// Create a new demo code state object with default values in it.
pub fn new() -> Self {
Self {
line_plots: line_plots::LinePlotDemoState::new(),
}
}
/// Show all the demos
pub fn show_demos(&mut self, ui: &Ui, plot_ui: &PlotUi) {
// Most of the demos are currently still stateless, so the code here mostly just calls into
// the modules. The line plots demo is stateful though. Things will be refactored soon to
// make all the individual demos stateful to unify things more.
Window::new(im_str!("implot-rs demo")) Window::new(im_str!("implot-rs demo"))
.size([430.0, 450.0], Condition::FirstUseEver) .size([430.0, 450.0], Condition::FirstUseEver)
.build(ui, || { .build(ui, || {
@ -27,7 +45,8 @@ pub fn show_demos(ui: &Ui, plot_ui: &PlotUi) {
ui.separator(); ui.separator();
ui.text(im_str!("Line plots:")); ui.text(im_str!("Line plots:"));
line_plots::show_demo_headers(ui, plot_ui); // The line plots demo is stateful
self.line_plots.show_demo_headers(ui, plot_ui);
ui.separator(); ui.separator();
ui.text(im_str!("Scatter plots:")); ui.text(im_str!("Scatter plots:"));
@ -50,3 +69,4 @@ pub fn show_demos(ui: &Ui, plot_ui: &PlotUi) {
stem_plots::show_demo_headers(ui, plot_ui); stem_plots::show_demo_headers(ui, plot_ui);
}); });
} }
}

View file

@ -11,6 +11,21 @@ use implot::{
StyleVar, YAxisChoice, StyleVar, YAxisChoice,
}; };
use std::{cell::RefCell, rc::Rc};
/// State of the line plots demo.
pub struct LinePlotDemoState {
linked_limits: Rc<RefCell<ImPlotRange>>,
}
impl LinePlotDemoState {
/// Create a new line plots demo state object with default values in it.
pub fn new() -> Self {
Self {
linked_limits: Rc::new(RefCell::new(ImPlotRange { Min: 0.0, Max: 1.0 })),
}
}
pub fn show_basic_plot(ui: &Ui, plot_ui: &PlotUi) { pub fn show_basic_plot(ui: &Ui, plot_ui: &PlotUi) {
ui.text(im_str!( ui.text(im_str!(
"This header just plots a line with as little code as possible." "This header just plots a line with as little code as possible."
@ -296,7 +311,9 @@ pub fn show_colormaps_plot(ui: &Ui, plot_ui: &PlotUi) {
.build(plot_ui, || { .build(plot_ui, || {
(1..10) (1..10)
.map(|x| x as f64 * 0.1) .map(|x| x as f64 * 0.1)
.map(|x| PlotLine::new(&format!("{:3.3}", x)).plot(&vec![0.1, 0.9], &vec![x, x])) .map(|x| {
PlotLine::new(&format!("{:3.3}", x)).plot(&vec![0.1, 0.9], &vec![x, x])
})
.count(); .count();
}); });
@ -323,7 +340,9 @@ pub fn show_colormaps_plot(ui: &Ui, plot_ui: &PlotUi) {
.build(plot_ui, || { .build(plot_ui, || {
(1..10) (1..10)
.map(|x| x as f64 * 0.1) .map(|x| x as f64 * 0.1)
.map(|x| PlotLine::new(&format!("{:3.3}", x)).plot(&vec![0.1, 0.9], &vec![x, x])) .map(|x| {
PlotLine::new(&format!("{:3.3}", x)).plot(&vec![0.1, 0.9], &vec![x, x])
})
.count(); .count();
}); });
@ -349,29 +368,56 @@ pub fn show_conversions_plot(ui: &Ui, plot_ui: &PlotUi) {
}); });
} }
pub fn show_demo_headers(ui: &Ui, plot_ui: &PlotUi) { pub fn show_linked_x_axis_plots(&mut self, ui: &Ui, plot_ui: &PlotUi) {
ui.text(im_str!(
"These plots have their X axes linked, but not the Y axes"
));
let content_width = ui.window_content_region_width();
Plot::new("Linked plot 1")
.size([content_width, 300.0])
.linked_x_limits(self.linked_limits.clone())
.build(plot_ui, || {
let x_positions = vec![0.1, 0.9];
let y_positions = vec![0.1, 0.9];
PlotLine::new("legend label").plot(&x_positions, &y_positions);
});
Plot::new("Linked plot 2")
.size([content_width, 300.0])
.linked_x_limits(self.linked_limits.clone())
.build(plot_ui, || {
let x_positions = vec![0.1, 0.9];
let y_positions = vec![0.1, 0.9];
PlotLine::new("legend label").plot(&x_positions, &y_positions);
});
}
pub fn show_demo_headers(&mut self, ui: &Ui, plot_ui: &PlotUi) {
if CollapsingHeader::new(im_str!("Line plot: Basic")).build(&ui) { if CollapsingHeader::new(im_str!("Line plot: Basic")).build(&ui) {
show_basic_plot(&ui, &plot_ui); Self::show_basic_plot(&ui, &plot_ui);
} }
if CollapsingHeader::new(im_str!("Line plot: Configured")).build(&ui) { if CollapsingHeader::new(im_str!("Line plot: Configured")).build(&ui) {
show_configurable_plot(&ui, &plot_ui); Self::show_configurable_plot(&ui, &plot_ui);
} }
if CollapsingHeader::new(im_str!("Line Plot: Plot queries")).build(&ui) { if CollapsingHeader::new(im_str!("Line Plot: Plot queries")).build(&ui) {
show_query_features_plot(&ui, &plot_ui); Self::show_query_features_plot(&ui, &plot_ui);
} }
if CollapsingHeader::new(im_str!("Line plot: Plot styling")).build(&ui) { if CollapsingHeader::new(im_str!("Line plot: Plot styling")).build(&ui) {
show_style_plot(&ui, &plot_ui); Self::show_style_plot(&ui, &plot_ui);
} }
if CollapsingHeader::new(im_str!("Line plot: Colormaps")).build(&ui) { if CollapsingHeader::new(im_str!("Line plot: Colormaps")).build(&ui) {
show_colormaps_plot(&ui, &plot_ui); Self::show_colormaps_plot(&ui, &plot_ui);
} }
if CollapsingHeader::new(im_str!("Line plot: Multiple Y Axes")).build(&ui) { if CollapsingHeader::new(im_str!("Line plot: Multiple Y Axes")).build(&ui) {
show_two_yaxis_plot(&ui, &plot_ui); Self::show_two_yaxis_plot(&ui, &plot_ui);
} }
if CollapsingHeader::new(im_str!("Line plot: \"Axis equal\"")).build(&ui) { if CollapsingHeader::new(im_str!("Line plot: \"Axis equal\"")).build(&ui) {
show_axis_equal_plot(&ui, &plot_ui); Self::show_axis_equal_plot(&ui, &plot_ui);
} }
if CollapsingHeader::new(im_str!("Line plot: Range conversions")).build(&ui) { if CollapsingHeader::new(im_str!("Line plot: Range conversions")).build(&ui) {
show_conversions_plot(&ui, &plot_ui); Self::show_conversions_plot(&ui, &plot_ui);
}
if CollapsingHeader::new(im_str!("Line plot: Linked plots")).build(&ui) {
self.show_linked_x_axis_plots(&ui, &plot_ui);
}
} }
} }

View file

@ -8,6 +8,7 @@ fn main() {
let system = support::init(file!()); let system = support::init(file!());
let mut showing_demo = false; let mut showing_demo = false;
let mut showing_rust_demo = true; let mut showing_rust_demo = true;
let mut demo_state = examples_shared::DemoState::new();
let plotcontext = Context::create(); let plotcontext = Context::create();
system.main_loop(move |_, ui| { system.main_loop(move |_, ui| {
// The context is moved into the closure after creation so plot_ui is valid. // The context is moved into the closure after creation so plot_ui is valid.
@ -18,7 +19,7 @@ fn main() {
} }
if showing_rust_demo { if showing_rust_demo {
examples_shared::show_demos(ui, &plot_ui); demo_state.show_demos(ui, &plot_ui);
} }
Window::new(im_str!("Welcome to the ImPlot-rs demo!")) Window::new(im_str!("Welcome to the ImPlot-rs demo!"))

View file

@ -1,4 +1,3 @@
use imgui::{im_str, Condition, Window}; use imgui::{im_str, Condition, Window};
use implot::Context; use implot::Context;
@ -9,6 +8,7 @@ fn main() {
let system = support::init(file!()); let system = support::init(file!());
let mut showing_demo = false; let mut showing_demo = false;
let mut showing_rust_demo = true; let mut showing_rust_demo = true;
let mut demo_state = examples_shared::DemoState::new();
let plotcontext = Context::create(); let plotcontext = Context::create();
system.main_loop(move |_, ui| { system.main_loop(move |_, ui| {
// The context is moved into the closure after creation so plot_ui is valid. // The context is moved into the closure after creation so plot_ui is valid.
@ -19,7 +19,7 @@ fn main() {
} }
if showing_rust_demo { if showing_rust_demo {
examples_shared::show_demos(ui, &plot_ui); demo_state.show_demos(ui, &plot_ui);
} }
Window::new(im_str!("Welcome to the ImPlot-rs demo!")) Window::new(im_str!("Welcome to the ImPlot-rs demo!"))

View file

@ -7,6 +7,7 @@ use bitflags::bitflags;
pub use imgui::Condition; pub use imgui::Condition;
use imgui::{im_str, ImString}; use imgui::{im_str, ImString};
use implot_sys as sys; use implot_sys as sys;
use std::{cell::RefCell, rc::Rc};
pub use sys::{ImPlotLimits, ImPlotPoint, ImPlotRange, ImVec2, ImVec4}; pub use sys::{ImPlotLimits, ImPlotPoint, ImPlotRange, ImVec2, ImVec4};
const DEFAULT_PLOT_SIZE_X: f32 = 400.0; const DEFAULT_PLOT_SIZE_X: f32 = 400.0;
@ -77,6 +78,15 @@ bitflags! {
} }
} }
/// Internally-used struct for storing axis limits
#[derive(Clone)]
enum AxisLimitSpecification {
/// Direct limits, specified as values
Single(ImPlotRange, Condition),
/// Limits that are linked to limits of other plots (via clones of the same Rc)
Linked(Rc<RefCell<ImPlotRange>>),
}
/// Struct to represent an ImPlot. This is the main construct used to contain all kinds of plots in ImPlot. /// Struct to represent an ImPlot. This is the main construct used to contain all kinds of plots in ImPlot.
/// ///
/// `Plot` is to be used (within an imgui window) with the following pattern: /// `Plot` is to be used (within an imgui window) with the following pattern:
@ -106,13 +116,9 @@ pub struct Plot {
/// afterwards, and this ensures the ImString itself will stay alive long enough for the plot. /// afterwards, and this ensures the ImString itself will stay alive long enough for the plot.
y_label: ImString, y_label: ImString,
/// X axis limits, if present /// X axis limits, if present
x_limits: Option<ImPlotRange>, x_limits: Option<AxisLimitSpecification>,
/// Y axis limits, if present /// Y axis limits, if present
y_limits: [Option<ImPlotRange>; NUMBER_OF_Y_AXES], y_limits: [Option<AxisLimitSpecification>; NUMBER_OF_Y_AXES],
/// Condition on which the x limits are set
x_limit_condition: Option<Condition>,
/// Condition on which the y limits are set for each of the axes
y_limit_condition: [Option<Condition>; NUMBER_OF_Y_AXES],
/// Positions for custom X axis ticks, if any /// Positions for custom X axis ticks, if any
x_tick_positions: Option<Vec<f64>>, x_tick_positions: Option<Vec<f64>>,
/// Labels for custom X axis ticks, if any. I'd prefer to store these together /// Labels for custom X axis ticks, if any. I'd prefer to store these together
@ -164,9 +170,7 @@ impl Plot {
x_label: im_str!("").into(), x_label: im_str!("").into(),
y_label: im_str!("").into(), y_label: im_str!("").into(),
x_limits: None, x_limits: None,
y_limits: [None; NUMBER_OF_Y_AXES], y_limits: Default::default(),
x_limit_condition: None,
y_limit_condition: [None; NUMBER_OF_Y_AXES],
x_tick_positions: None, x_tick_positions: None,
x_tick_labels: None, x_tick_labels: None,
show_x_default_ticks: false, show_x_default_ticks: false,
@ -202,17 +206,33 @@ impl Plot {
self self
} }
/// Set the x limits of the plot /// Set the x limits of the plot.
///
/// Note: This conflicts with `linked_x_limits`, whichever is called last on plot construction
/// takes effect.
#[inline] #[inline]
pub fn x_limits<L: Into<ImPlotRange>>(mut self, limits: L, condition: Condition) -> Self { pub fn x_limits<L: Into<ImPlotRange>>(mut self, limits: L, condition: Condition) -> Self {
self.x_limits = Some(limits.into()); self.x_limits = Some(AxisLimitSpecification::Single(limits.into(), condition));
self.x_limit_condition = Some(condition); self
}
/// Set linked x limits for this plot. Pass clones of the same `Rc` into other plots
/// to link their limits with the same values.
///
/// Note: This conflicts with `x_limits`, whichever is called last on plot construction takes
/// effect.
#[inline]
pub fn linked_x_limits(mut self, limits: Rc<RefCell<ImPlotRange>>) -> Self {
self.x_limits = Some(AxisLimitSpecification::Linked(limits));
self self
} }
/// Set the Y limits of the plot for the given Y axis. Call multiple times with different /// Set the Y limits of the plot for the given Y axis. Call multiple times with different
/// `y_axis_choice` values to set for multiple axes, or use the convenience methods such as /// `y_axis_choice` values to set for multiple axes, or use the convenience methods such as
/// [`Plot::y1_limits`]. /// [`Plot::y1_limits`].
///
/// Note: This conflicts with `linked_y_limits`, whichever is called last on plot construction
/// takes effect for a given axis.
#[inline] #[inline]
pub fn y_limits<L: Into<ImPlotRange>>( pub fn y_limits<L: Into<ImPlotRange>>(
mut self, mut self,
@ -221,32 +241,73 @@ impl Plot {
condition: Condition, condition: Condition,
) -> Self { ) -> Self {
let axis_index = y_axis_choice as usize; let axis_index = y_axis_choice as usize;
self.y_limits[axis_index] = Some(limits.into()); self.y_limits[axis_index] = Some(AxisLimitSpecification::Single(limits.into(), condition));
self.y_limit_condition[axis_index] = Some(condition);
self self
} }
/// Convenience function to directly set the Y limits for the first Y axis. To programmatically /// Convenience function to directly set the Y limits for the first Y axis. To programmatically
/// (or on demand) decide which axie to set limits for, use [`Plot::y_limits`] /// (or on demand) decide which axis to set limits for, use [`Plot::y_limits`]
#[inline] #[inline]
pub fn y1_limits<L: Into<ImPlotRange>>(self, limits: L, condition: Condition) -> Self { pub fn y1_limits<L: Into<ImPlotRange>>(self, limits: L, condition: Condition) -> Self {
self.y_limits(limits, YAxisChoice::First, condition) self.y_limits(limits, YAxisChoice::First, condition)
} }
/// Convenience function to directly set the Y limits for the second Y axis. To /// Convenience function to directly set the Y limits for the second Y axis. To
/// programmatically (or on demand) decide which axie to set limits for, use [`Plot::y_limits`] /// programmatically (or on demand) decide which axis to set limits for, use [`Plot::y_limits`]
#[inline] #[inline]
pub fn y2_limits<L: Into<ImPlotRange>>(self, limits: L, condition: Condition) -> Self { pub fn y2_limits<L: Into<ImPlotRange>>(self, limits: L, condition: Condition) -> Self {
self.y_limits(limits, YAxisChoice::Second, condition) self.y_limits(limits, YAxisChoice::Second, condition)
} }
/// Convenience function to directly set the Y limits for the third Y axis. To programmatically /// Convenience function to directly set the Y limits for the third Y axis. To programmatically
/// (or on demand) decide which axie to set limits for, use [`Plot::y_limits`] /// (or on demand) decide which axis to set limits for, use [`Plot::y_limits`]
#[inline] #[inline]
pub fn y3_limits<L: Into<ImPlotRange>>(self, limits: L, condition: Condition) -> Self { pub fn y3_limits<L: Into<ImPlotRange>>(self, limits: L, condition: Condition) -> Self {
self.y_limits(limits, YAxisChoice::Third, condition) self.y_limits(limits, YAxisChoice::Third, condition)
} }
/// Set linked Y limits of the plot for the given Y axis. Pass clones of the same `Rc` into
/// other plots to link their limits with the same values. Call multiple times with different
/// `y_axis_choice` values to set for multiple axes, or use the convenience methods such as
/// [`Plot::y1_limits`].
///
/// Note: This conflicts with `y_limits`, whichever is called last on plot construction takes
/// effect for a given axis.
#[inline]
pub fn linked_y_limits(
mut self,
limits: Rc<RefCell<ImPlotRange>>,
y_axis_choice: YAxisChoice,
) -> Self {
let axis_index = y_axis_choice as usize;
self.y_limits[axis_index] = Some(AxisLimitSpecification::Linked(limits));
self
}
/// Convenience function to directly set linked Y limits for the first Y axis. To
/// programmatically (or on demand) decide which axis to set limits for, use
/// [`Plot::linked_y_limits`].
#[inline]
pub fn linked_y1_limits(self, limits: Rc<RefCell<ImPlotRange>>) -> Self {
self.linked_y_limits(limits, YAxisChoice::First)
}
/// Convenience function to directly set linked Y limits for the second Y axis. To
/// programmatically (or on demand) decide which axis to set limits for, use
/// [`Plot::linked_y_limits`].
#[inline]
pub fn linked_y2_limits(self, limits: Rc<RefCell<ImPlotRange>>) -> Self {
self.linked_y_limits(limits, YAxisChoice::Second)
}
/// Convenience function to directly set linked Y limits for the third Y axis. To
/// programmatically (or on demand) decide which axis to set limits for, use
/// [`Plot::linked_y_limits`].
#[inline]
pub fn linked_y3_limits(self, limits: Rc<RefCell<ImPlotRange>>) -> Self {
self.linked_y_limits(limits, YAxisChoice::Third)
}
/// Set X ticks without labels for the plot. The vector contains one label each in /// Set X ticks without labels for the plot. The vector contains one label each in
/// the form of a tuple `(label_position, label_string)`. The `show_default` setting /// the form of a tuple `(label_position, label_string)`. The `show_default` setting
/// determines whether the default ticks are also shown. /// determines whether the default ticks are also shown.
@ -343,20 +404,27 @@ impl Plot {
/// Internal helper function to set axis limits in case they are specified. /// Internal helper function to set axis limits in case they are specified.
fn maybe_set_axis_limits(&self) { fn maybe_set_axis_limits(&self) {
// Set X limits if specified // Limit-setting can either happen via direct limits or through linked limits. The version
if let (Some(limits), Some(condition)) = (self.x_limits, self.x_limit_condition) { // of implot we link to here has different APIs for the two (separate per-axis calls for
// direct, and one call for everything together for linked), hence the code here is a bit
// clunky and takes the two approaches separately instead of a unified "match".
// --- Direct limit-setting ---
if let Some(AxisLimitSpecification::Single(limits, condition)) = &self.x_limits {
unsafe { unsafe {
sys::ImPlot_SetNextPlotLimitsX(limits.Min, limits.Max, condition as sys::ImGuiCond); sys::ImPlot_SetNextPlotLimitsX(
limits.Min,
limits.Max,
*condition as sys::ImGuiCond,
);
} }
} }
// Set Y limits if specified
self.y_limits self.y_limits
.iter() .iter()
.zip(self.y_limit_condition.iter())
.enumerate() .enumerate()
.for_each(|(k, (limits, condition))| { .for_each(|(k, limit_spec)| {
if let (Some(limits), Some(condition)) = (limits, condition) { if let Some(AxisLimitSpecification::Single(limits, condition)) = limit_spec {
unsafe { unsafe {
sys::ImPlot_SetNextPlotLimitsY( sys::ImPlot_SetNextPlotLimitsY(
limits.Min, limits.Min,
@ -367,6 +435,50 @@ impl Plot {
} }
} }
}); });
// --- Linked limit-setting ---
let (xmin_pointer, xmax_pointer) =
if let Some(AxisLimitSpecification::Linked(value)) = &self.x_limits {
let mut borrowed = value.borrow_mut();
(
&mut (*borrowed).Min as *mut _,
&mut (*borrowed).Max as *mut _,
)
} else {
(std::ptr::null_mut(), std::ptr::null_mut())
};
let y_limit_pointers: Vec<(*mut f64, *mut f64)> = self
.y_limits
.iter()
.map(|limit_spec| {
if let Some(AxisLimitSpecification::Linked(value)) = limit_spec {
let mut borrowed = value.borrow_mut();
(
&mut (*borrowed).Min as *mut _,
&mut (*borrowed).Max as *mut _,
)
} else {
(std::ptr::null_mut(), std::ptr::null_mut())
}
})
.collect();
unsafe {
// Calling this unconditionally here as calling it with all NULL pointers should not
// affect anything. In terms of unsafety, the pointers should be OK as long as any plot
// struct that has an Rc to the same data is alive.
sys::ImPlot_LinkNextPlotLimits(
xmin_pointer,
xmax_pointer,
y_limit_pointers[0].0,
y_limit_pointers[0].1,
y_limit_pointers[1].0,
y_limit_pointers[1].1,
y_limit_pointers[2].0,
y_limit_pointers[2].1,
)
}
} }
/// Internal helper function to set tick labels in case they are specified. This does the /// Internal helper function to set tick labels in case they are specified. This does the