From 06cc3061c18de808305030357b88363210e3b297 Mon Sep 17 00:00:00 2001 From: 4bb4 <67376761+4bb4@users.noreply.github.com> Date: Sun, 30 May 2021 11:45:16 +0200 Subject: [PATCH] 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>` 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. --- implot-examples/examples-shared/src/lib.rs | 80 +- .../examples-shared/src/line_plots.rs | 714 ++++++++++-------- implot-examples/implot-glium-demo/src/main.rs | 3 +- implot-examples/implot-wgpu-demo/src/main.rs | 4 +- src/plot.rs | 160 +++- 5 files changed, 570 insertions(+), 391 deletions(-) diff --git a/implot-examples/examples-shared/src/lib.rs b/implot-examples/examples-shared/src/lib.rs index 690ccbf..9d61368 100644 --- a/implot-examples/examples-shared/src/lib.rs +++ b/implot-examples/examples-shared/src/lib.rs @@ -9,44 +9,64 @@ pub mod text_plots; use imgui::{im_str, Condition, Ui, Window}; use implot::PlotUi; -pub fn show_demos(ui: &Ui, plot_ui: &PlotUi) { - Window::new(im_str!("implot-rs demo")) - .size([430.0, 450.0], Condition::FirstUseEver) - .build(ui, || { - ui.text(im_str!("Hello from implot-rs!")); - ui.text_wrapped(im_str!( - "The headers here demo the plotting features of the library.\ +/// 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")) + .size([430.0, 450.0], Condition::FirstUseEver) + .build(ui, || { + ui.text(im_str!("Hello from implot-rs!")); + ui.text_wrapped(im_str!( + "The headers here demo the plotting features of the library.\ Have a look at the example source code to see how they are implemented.\n\ Check out the demo from ImPlot itself first for instructions on how to\ interact with ImPlot plots." - )); + )); - ui.separator(); - ui.text(im_str!("Bar plots:")); - bar_plots::show_demo_headers(ui, plot_ui); + ui.separator(); + ui.text(im_str!("Bar plots:")); + bar_plots::show_demo_headers(ui, plot_ui); - ui.separator(); - ui.text(im_str!("Line plots:")); - line_plots::show_demo_headers(ui, plot_ui); + ui.separator(); + ui.text(im_str!("Line plots:")); + // The line plots demo is stateful + self.line_plots.show_demo_headers(ui, plot_ui); - ui.separator(); - ui.text(im_str!("Scatter plots:")); - scatter_plots::show_demo_headers(ui, plot_ui); + ui.separator(); + ui.text(im_str!("Scatter plots:")); + scatter_plots::show_demo_headers(ui, plot_ui); - ui.separator(); - ui.text(im_str!("Text plots:")); - text_plots::show_demo_headers(ui, plot_ui); + ui.separator(); + ui.text(im_str!("Text plots:")); + text_plots::show_demo_headers(ui, plot_ui); - ui.separator(); - ui.text(im_str!("Stairs plots:")); - stairs_plots::show_demo_headers(ui, plot_ui); + ui.separator(); + ui.text(im_str!("Stairs plots:")); + stairs_plots::show_demo_headers(ui, plot_ui); - ui.separator(); - ui.text(im_str!("Heatmaps:")); - heatmaps::show_demo_headers(ui, plot_ui); + ui.separator(); + ui.text(im_str!("Heatmaps:")); + heatmaps::show_demo_headers(ui, plot_ui); - ui.separator(); - ui.text(im_str!("Stem plots:")); - stem_plots::show_demo_headers(ui, plot_ui); - }); + ui.separator(); + ui.text(im_str!("Stem plots:")); + stem_plots::show_demo_headers(ui, plot_ui); + }); + } } diff --git a/implot-examples/examples-shared/src/line_plots.rs b/implot-examples/examples-shared/src/line_plots.rs index 782e6d6..978c2c3 100644 --- a/implot-examples/examples-shared/src/line_plots.rs +++ b/implot-examples/examples-shared/src/line_plots.rs @@ -11,367 +11,413 @@ use implot::{ StyleVar, YAxisChoice, }; -pub fn show_basic_plot(ui: &Ui, plot_ui: &PlotUi) { - ui.text(im_str!( - "This header just plots a line with as little code as possible." - )); - let content_width = ui.window_content_region_width(); - Plot::new("Simple line plot") - // The size call could also be omitted, though the defaults don't consider window - // width, which is why we're not doing so here. - .size([content_width, 300.0]) - .build(plot_ui, || { - // If this is called outside a plot build callback, the program will panic. - 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); - }); +use std::{cell::RefCell, rc::Rc}; + +/// State of the line plots demo. +pub struct LinePlotDemoState { + linked_limits: Rc>, } -pub fn show_two_yaxis_plot(ui: &Ui, plot_ui: &PlotUi) { - ui.text(im_str!( - "This header shows how to create a plot with multiple Y axes." - )); - let content_width = ui.window_content_region_width(); - Plot::new("Multiple Y axis plots") - // The size call could also be omitted, though the defaults don't consider window - // width, which is why we're not doing so here. - .size([content_width, 300.0]) - .with_plot_flags(&(PlotFlags::NONE | PlotFlags::Y_AXIS_2)) - .y_limits( - ImPlotRange { Min: 0.0, Max: 1.0 }, - YAxisChoice::First, - Condition::Always, - ) - .y_limits( - // One can also use [f32; 2], (f32, f32) and ImVec2 for limit setting - [1.0, 3.5], - YAxisChoice::Second, - Condition::Always, - ) - .build(plot_ui, || { - let x_positions = vec![0.1, 0.9]; +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 })), + } + } - // The first Y axis is the default - let y_positions = vec![0.1, 0.9]; - PlotLine::new("legend label").plot(&x_positions, &y_positions); + pub fn show_basic_plot(ui: &Ui, plot_ui: &PlotUi) { + ui.text(im_str!( + "This header just plots a line with as little code as possible." + )); + let content_width = ui.window_content_region_width(); + Plot::new("Simple line plot") + // The size call could also be omitted, though the defaults don't consider window + // width, which is why we're not doing so here. + .size([content_width, 300.0]) + .build(plot_ui, || { + // If this is called outside a plot build callback, the program will panic. + 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); + }); + } - // Now we switch to the second axis for the next call - set_plot_y_axis(YAxisChoice::Second); - let y_positions = vec![3.3, 1.2]; - PlotLine::new("legend label two").plot(&x_positions, &y_positions); - }); -} + pub fn show_two_yaxis_plot(ui: &Ui, plot_ui: &PlotUi) { + ui.text(im_str!( + "This header shows how to create a plot with multiple Y axes." + )); + let content_width = ui.window_content_region_width(); + Plot::new("Multiple Y axis plots") + // The size call could also be omitted, though the defaults don't consider window + // width, which is why we're not doing so here. + .size([content_width, 300.0]) + .with_plot_flags(&(PlotFlags::NONE | PlotFlags::Y_AXIS_2)) + .y_limits( + ImPlotRange { Min: 0.0, Max: 1.0 }, + YAxisChoice::First, + Condition::Always, + ) + .y_limits( + // One can also use [f32; 2], (f32, f32) and ImVec2 for limit setting + [1.0, 3.5], + YAxisChoice::Second, + Condition::Always, + ) + .build(plot_ui, || { + let x_positions = vec![0.1, 0.9]; -pub fn show_axis_equal_plot(ui: &Ui, plot_ui: &PlotUi) { - ui.text(im_str!("This plot has axis equal set (1:1 aspect ratio).")); - let content_width = ui.window_content_region_width(); - Plot::new("Axis equal line plot") - // The size call could also be omitted, though the defaults don't consider window - // width, which is why we're not doing so here. - .size([content_width, 300.0]) - .with_plot_flags(&(PlotFlags::NONE | PlotFlags::AXIS_EQUAL)) - .build(plot_ui, || { - // If this is called outside a plot build callback, the program will panic. - 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); - }); -} + // The first Y axis is the default + let y_positions = vec![0.1, 0.9]; + PlotLine::new("legend label").plot(&x_positions, &y_positions); -pub fn show_configurable_plot(ui: &Ui, plot_ui: &PlotUi) { - ui.text(im_str!( - "This header demos what we can configure about plots." - )); + // Now we switch to the second axis for the next call + set_plot_y_axis(YAxisChoice::Second); + let y_positions = vec![3.3, 1.2]; + PlotLine::new("legend label two").plot(&x_positions, &y_positions); + }); + } - // Settings for the plot - // - X and Y size in pixels - let x_size = 300.0; - let y_size = 200.0; - // - Strings for the axis labels - let x_label = "X label!"; - let y_label = "Y label!"; - // - Plot limits - let x_min = 2.0; - let x_max = 3.0; - let y_min = 1.0; - let y_max = 2.0; - // - Plot flags, see the PlotFlags docs for more info - let plot_flags = PlotFlags::NONE; - // - Axis flags, see the AxisFlags docs for more info. All flags are bitflags-created, - // so they support a bunch of convenient operations, see https://docs.rs/bitflags - let x_axis_flags = AxisFlags::NONE; - let y_axis_flags = AxisFlags::NONE; + pub fn show_axis_equal_plot(ui: &Ui, plot_ui: &PlotUi) { + ui.text(im_str!("This plot has axis equal set (1:1 aspect ratio).")); + let content_width = ui.window_content_region_width(); + Plot::new("Axis equal line plot") + // The size call could also be omitted, though the defaults don't consider window + // width, which is why we're not doing so here. + .size([content_width, 300.0]) + .with_plot_flags(&(PlotFlags::NONE | PlotFlags::AXIS_EQUAL)) + .build(plot_ui, || { + // If this is called outside a plot build callback, the program will panic. + 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); + }); + } - // - Unlabelled X axis ticks - let x_ticks = vec![2.2, 2.5, 2.8]; + pub fn show_configurable_plot(ui: &Ui, plot_ui: &PlotUi) { + ui.text(im_str!( + "This header demos what we can configure about plots." + )); - // - Labelled Y axis ticks - let y_ticks = vec![(1.1, "A".to_owned()), (1.4, "B".to_owned())]; + // Settings for the plot + // - X and Y size in pixels + let x_size = 300.0; + let y_size = 200.0; + // - Strings for the axis labels + let x_label = "X label!"; + let y_label = "Y label!"; + // - Plot limits + let x_min = 2.0; + let x_max = 3.0; + let y_min = 1.0; + let y_max = 2.0; + // - Plot flags, see the PlotFlags docs for more info + let plot_flags = PlotFlags::NONE; + // - Axis flags, see the AxisFlags docs for more info. All flags are bitflags-created, + // so they support a bunch of convenient operations, see https://docs.rs/bitflags + let x_axis_flags = AxisFlags::NONE; + let y_axis_flags = AxisFlags::NONE; - // Axis labels - Plot::new("Configured line plot") - .size([x_size, y_size]) - .x_label(&x_label) - .y_label(&y_label) - .x_limits( - ImPlotRange { - Min: x_min, - Max: x_max, - }, - // Always means that the limits stay what we force them to here, even if the user - // scrolls or drags in the plot with the mouse. FirstUseEver sets the limits the - // first time the plot is drawn, but the user can then modify them and the change - // will stick. - Condition::Always, - ) - .y_limits( - ImPlotRange { - Min: y_min, - Max: y_max, - }, - YAxisChoice::First, - Condition::Always, - ) - .x_ticks(&x_ticks, false) - .y_ticks_with_labels(YAxisChoice::First, &y_ticks, false) - // If any of these flag setting calls are omitted, the defaults are used. - .with_plot_flags(&plot_flags) - .with_x_axis_flags(&x_axis_flags) - .with_y_axis_flags(YAxisChoice::First, &y_axis_flags) - .with_legend_location(&PlotLocation::West, &PlotOrientation::Horizontal, true) - .build(plot_ui, || { - PlotLine::new("A line 2").plot(&vec![2.4, 2.9], &vec![1.1, 1.9]); - }); -} + // - Unlabelled X axis ticks + let x_ticks = vec![2.2, 2.5, 2.8]; -pub fn show_query_features_plot(ui: &Ui, plot_ui: &PlotUi) { - ui.text(im_str!( - "This header demos how to use the querying features." - )); - let content_width = ui.window_content_region_width(); + // - Labelled Y axis ticks + let y_ticks = vec![(1.1, "A".to_owned()), (1.4, "B".to_owned())]; - // Create some containers for exfiltrating data from the closure below - let mut hover_pos_plot: Option = None; - let mut hover_pos_pixels: Option = None; - let mut hover_pos_from_pixels: Option = None; - let mut plot_limits: Option = None; - let mut query_limits: Option = None; - let mut legend1_hovered = false; - let mut legend2_hovered = false; - - // Draw a plot - Plot::new("Plot querying") - .size([content_width, 300.0]) - .x_limits(ImPlotRange { Min: 0.0, Max: 5.0 }, Condition::FirstUseEver) - .y_limits( - ImPlotRange { Min: 0.0, Max: 5.0 }, - YAxisChoice::First, - Condition::FirstUseEver, - ) - .with_plot_flags(&(PlotFlags::NONE | PlotFlags::QUERY)) - .build(plot_ui, || { - if is_plot_hovered() { - hover_pos_plot = Some(get_plot_mouse_position(None)); - hover_pos_pixels = Some(plot_to_pixels_vec2(&(hover_pos_plot.unwrap()), None)); - } - - // Getting the plot position from pixels also works when the plot is not hovered, - // the coordinates are then simply outside the visible range. - hover_pos_from_pixels = Some(pixels_to_plot_vec2( - &ImVec2 { - x: ui.io().mouse_pos[0], - y: ui.io().mouse_pos[1], + // Axis labels + Plot::new("Configured line plot") + .size([x_size, y_size]) + .x_label(&x_label) + .y_label(&y_label) + .x_limits( + ImPlotRange { + Min: x_min, + Max: x_max, }, - None, + // Always means that the limits stay what we force them to here, even if the user + // scrolls or drags in the plot with the mouse. FirstUseEver sets the limits the + // first time the plot is drawn, but the user can then modify them and the change + // will stick. + Condition::Always, + ) + .y_limits( + ImPlotRange { + Min: y_min, + Max: y_max, + }, + YAxisChoice::First, + Condition::Always, + ) + .x_ticks(&x_ticks, false) + .y_ticks_with_labels(YAxisChoice::First, &y_ticks, false) + // If any of these flag setting calls are omitted, the defaults are used. + .with_plot_flags(&plot_flags) + .with_x_axis_flags(&x_axis_flags) + .with_y_axis_flags(YAxisChoice::First, &y_axis_flags) + .with_legend_location(&PlotLocation::West, &PlotOrientation::Horizontal, true) + .build(plot_ui, || { + PlotLine::new("A line 2").plot(&vec![2.4, 2.9], &vec![1.1, 1.9]); + }); + } + + pub fn show_query_features_plot(ui: &Ui, plot_ui: &PlotUi) { + ui.text(im_str!( + "This header demos how to use the querying features." + )); + let content_width = ui.window_content_region_width(); + + // Create some containers for exfiltrating data from the closure below + let mut hover_pos_plot: Option = None; + let mut hover_pos_pixels: Option = None; + let mut hover_pos_from_pixels: Option = None; + let mut plot_limits: Option = None; + let mut query_limits: Option = None; + let mut legend1_hovered = false; + let mut legend2_hovered = false; + + // Draw a plot + Plot::new("Plot querying") + .size([content_width, 300.0]) + .x_limits(ImPlotRange { Min: 0.0, Max: 5.0 }, Condition::FirstUseEver) + .y_limits( + ImPlotRange { Min: 0.0, Max: 5.0 }, + YAxisChoice::First, + Condition::FirstUseEver, + ) + .with_plot_flags(&(PlotFlags::NONE | PlotFlags::QUERY)) + .build(plot_ui, || { + if is_plot_hovered() { + hover_pos_plot = Some(get_plot_mouse_position(None)); + hover_pos_pixels = Some(plot_to_pixels_vec2(&(hover_pos_plot.unwrap()), None)); + } + + // Getting the plot position from pixels also works when the plot is not hovered, + // the coordinates are then simply outside the visible range. + hover_pos_from_pixels = Some(pixels_to_plot_vec2( + &ImVec2 { + x: ui.io().mouse_pos[0], + y: ui.io().mouse_pos[1], + }, + None, + )); + + // Plot a line so we have a legend entry + PlotLine::new("Legend1").plot(&vec![2.0, 2.0], &vec![2.0, 1.0]); + PlotLine::new("Legend2").plot(&vec![0.0, 0.0], &vec![1.0, 1.0]); + legend1_hovered = is_legend_entry_hovered("Legend1"); + legend2_hovered = is_legend_entry_hovered("Legend2"); + + if is_plot_queried() { + query_limits = Some(get_plot_query(None)); + } + plot_limits = Some(get_plot_limits(None)); + }); + + // Print some previously-exfiltrated info. This is because calling + // things like is_plot_hovered or get_plot_mouse_position() outside + // of an actual Plot is not allowed. + if let Some(pos) = hover_pos_plot { + ui.text(im_str!("hovered at {}, {}", pos.x, pos.y)); + } + if let Some(pixel_position) = hover_pos_pixels { + // Try out converting plot mouse position to pixel position + ui.text(im_str!( + "pixel pos from plot: {}, {}", + pixel_position.x, + pixel_position.y )); - - // Plot a line so we have a legend entry - PlotLine::new("Legend1").plot(&vec![2.0, 2.0], &vec![2.0, 1.0]); - PlotLine::new("Legend2").plot(&vec![0.0, 0.0], &vec![1.0, 1.0]); - legend1_hovered = is_legend_entry_hovered("Legend1"); - legend2_hovered = is_legend_entry_hovered("Legend2"); - - if is_plot_queried() { - query_limits = Some(get_plot_query(None)); - } - plot_limits = Some(get_plot_limits(None)); - }); - - // Print some previously-exfiltrated info. This is because calling - // things like is_plot_hovered or get_plot_mouse_position() outside - // of an actual Plot is not allowed. - if let Some(pos) = hover_pos_plot { - ui.text(im_str!("hovered at {}, {}", pos.x, pos.y)); - } - if let Some(pixel_position) = hover_pos_pixels { - // Try out converting plot mouse position to pixel position + ui.text(im_str!( + "pixel pos from imgui: {}, {}", + ui.io().mouse_pos[0], + ui.io().mouse_pos[1] + )); + } + if let Some(limits) = plot_limits { + ui.text(im_str!("Plot limits are {:#?}", limits)); + } + if let Some(query) = query_limits { + ui.text(im_str!("Query limits are {:#?}", query)); + } ui.text(im_str!( - "pixel pos from plot: {}, {}", - pixel_position.x, - pixel_position.y + "Legend hovering - 1: {}, 2: {}", + legend1_hovered, + legend2_hovered )); + + // Try out converting pixel position to plot position + if let Some(pos) = hover_pos_from_pixels { + ui.text(im_str!("plot pos from imgui: {}, {}", pos.x, pos.y,)); + } + } + + pub fn show_style_plot(ui: &Ui, plot_ui: &PlotUi) { ui.text(im_str!( - "pixel pos from imgui: {}, {}", - ui.io().mouse_pos[0], - ui.io().mouse_pos[1] + "This header demos how to use the styling features." )); - } - if let Some(limits) = plot_limits { - ui.text(im_str!("Plot limits are {:#?}", limits)); - } - if let Some(query) = query_limits { - ui.text(im_str!("Query limits are {:#?}", query)); - } - ui.text(im_str!( - "Legend hovering - 1: {}, 2: {}", - legend1_hovered, - legend2_hovered - )); + let content_width = ui.window_content_region_width(); - // Try out converting pixel position to plot position - if let Some(pos) = hover_pos_from_pixels { - ui.text(im_str!("plot pos from imgui: {}, {}", pos.x, pos.y,)); + // The style stack works the same as for other imgui things - we can push + // things to have them apply, then pop again to undo the change. In implot-rs, + // pushing returns a value on which we have to call .pop() later. Pushing + // variables can be done outside of plot calls as well. + let style = push_style_color(&PlotColorElement::PlotBg, 1.0, 1.0, 1.0, 0.2); + Plot::new("Style demo plot") + .size([content_width, 300.0]) + .x_limits(ImPlotRange { Min: 0.0, Max: 6.0 }, Condition::Always) + .y_limits( + ImPlotRange { + Min: -1.0, + Max: 3.0, + }, + YAxisChoice::First, + Condition::Always, + ) + .with_plot_flags(&(PlotFlags::NONE)) + .with_y_axis_flags(YAxisChoice::First, &(AxisFlags::NONE)) + .build(plot_ui, || { + // Markers can be selected as shown here. The markers are internally represented + // as an u32, hence this calling style. + let markerchoice = push_style_var_i32(&StyleVar::Marker, Marker::Cross as i32); + PlotLine::new("Left eye").plot(&vec![2.0, 2.0], &vec![2.0, 1.0]); + // Calling pop() on the return value of the push above will undo the marker choice. + markerchoice.pop(); + + // Line weights can be set the same way, along with some other things - see + // the docs of StyleVar for more info. + let lineweight = push_style_var_f32(&StyleVar::LineWeight, 5.0); + PlotLine::new("Right eye").plot(&vec![4.0, 4.0], &vec![2.0, 1.0]); + lineweight.pop(); + + let x_values = vec![1.0, 2.0, 4.0, 5.0]; + let y_values = vec![1.0, 0.0, 0.0, 1.0]; + PlotLine::new("Mouth").plot(&x_values, &y_values); + }); + + style.pop(); } -} -pub fn show_style_plot(ui: &Ui, plot_ui: &PlotUi) { - ui.text(im_str!( - "This header demos how to use the styling features." - )); - let content_width = ui.window_content_region_width(); + pub fn show_colormaps_plot(ui: &Ui, plot_ui: &PlotUi) { + ui.text(im_str!("This header demos how to select colormaps.")); + let content_width = ui.window_content_region_width(); - // The style stack works the same as for other imgui things - we can push - // things to have them apply, then pop again to undo the change. In implot-rs, - // pushing returns a value on which we have to call .pop() later. Pushing - // variables can be done outside of plot calls as well. - let style = push_style_color(&PlotColorElement::PlotBg, 1.0, 1.0, 1.0, 0.2); - Plot::new("Style demo plot") - .size([content_width, 300.0]) - .x_limits(ImPlotRange { Min: 0.0, Max: 6.0 }, Condition::Always) - .y_limits( - ImPlotRange { - Min: -1.0, - Max: 3.0, + // Select a colormap from the presets. The presets are listed in the Colormap enum + // and usually have something from 9 to 11 colors in them, with the second number + // being the option to resample the colormap to a custom number of colors if picked + // higher than 1. + set_colormap_from_preset(Colormap::Plasma, 1); + + Plot::new("Colormap demo plot") + .size([content_width, 300.0]) + .build(plot_ui, || { + (1..10) + .map(|x| x as f64 * 0.1) + .map(|x| { + PlotLine::new(&format!("{:3.3}", x)).plot(&vec![0.1, 0.9], &vec![x, x]) + }) + .count(); + }); + + // One can also specify a colormap as a vector of RGBA colors. ImPlot uses ImVec4 for this, + // so we follow suit. Make sure to set the last number (w in ImVec4) to 1.0 to see anything - + // it's the alpha channel. + set_colormap_from_vec(vec![ + ImVec4 { + x: 0.9, + y: 0.9, + z: 0.0, + w: 1.0, }, - YAxisChoice::First, - Condition::Always, - ) - .with_plot_flags(&(PlotFlags::NONE)) - .with_y_axis_flags(YAxisChoice::First, &(AxisFlags::NONE)) - .build(plot_ui, || { - // Markers can be selected as shown here. The markers are internally represented - // as an u32, hence this calling style. - let markerchoice = push_style_var_i32(&StyleVar::Marker, Marker::Cross as i32); - PlotLine::new("Left eye").plot(&vec![2.0, 2.0], &vec![2.0, 1.0]); - // Calling pop() on the return value of the push above will undo the marker choice. - markerchoice.pop(); + ImVec4 { + x: 0.0, + y: 0.9, + z: 0.9, + w: 1.0, + }, + ]); - // Line weights can be set the same way, along with some other things - see - // the docs of StyleVar for more info. - let lineweight = push_style_var_f32(&StyleVar::LineWeight, 5.0); - PlotLine::new("Right eye").plot(&vec![4.0, 4.0], &vec![2.0, 1.0]); - lineweight.pop(); + Plot::new("Colormap demo plot #2") + .size([content_width, 300.0]) + .build(plot_ui, || { + (1..10) + .map(|x| x as f64 * 0.1) + .map(|x| { + PlotLine::new(&format!("{:3.3}", x)).plot(&vec![0.1, 0.9], &vec![x, x]) + }) + .count(); + }); - let x_values = vec![1.0, 2.0, 4.0, 5.0]; - let y_values = vec![1.0, 0.0, 0.0, 1.0]; - PlotLine::new("Mouth").plot(&x_values, &y_values); - }); - - style.pop(); -} - -pub fn show_colormaps_plot(ui: &Ui, plot_ui: &PlotUi) { - ui.text(im_str!("This header demos how to select colormaps.")); - let content_width = ui.window_content_region_width(); - - // Select a colormap from the presets. The presets are listed in the Colormap enum - // and usually have something from 9 to 11 colors in them, with the second number - // being the option to resample the colormap to a custom number of colors if picked - // higher than 1. - set_colormap_from_preset(Colormap::Plasma, 1); - - Plot::new("Colormap demo plot") - .size([content_width, 300.0]) - .build(plot_ui, || { - (1..10) - .map(|x| x as f64 * 0.1) - .map(|x| PlotLine::new(&format!("{:3.3}", x)).plot(&vec![0.1, 0.9], &vec![x, x])) - .count(); - }); - - // One can also specify a colormap as a vector of RGBA colors. ImPlot uses ImVec4 for this, - // so we follow suit. Make sure to set the last number (w in ImVec4) to 1.0 to see anything - - // it's the alpha channel. - set_colormap_from_vec(vec![ - ImVec4 { - x: 0.9, - y: 0.9, - z: 0.0, - w: 1.0, - }, - ImVec4 { - x: 0.0, - y: 0.9, - z: 0.9, - w: 1.0, - }, - ]); - - Plot::new("Colormap demo plot #2") - .size([content_width, 300.0]) - .build(plot_ui, || { - (1..10) - .map(|x| x as f64 * 0.1) - .map(|x| PlotLine::new(&format!("{:3.3}", x)).plot(&vec![0.1, 0.9], &vec![x, x])) - .count(); - }); - - // Colormaps are not pushed, they are simply set, because they don't stack or anything. - // We can reset to the default by just setting the "Standard" preset. - set_colormap_from_preset(Colormap::Standard, 0); -} - -pub fn show_conversions_plot(ui: &Ui, plot_ui: &PlotUi) { - ui.text(im_str!( - "This header demonstrates (in code) how to convert various ranges into ImRange" - )); - let content_width = ui.window_content_region_width(); - Plot::new("Simple line plot, conversion 1") - .size([content_width, 300.0]) - .x_limits(ImVec2 { x: 0.0, y: 1.0 }, Condition::Always) - .y_limits([0.0, 1.0], YAxisChoice::First, Condition::Always) - .build(plot_ui, || { - // If this is called outside a plot build callback, the program will panic. - 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(ui: &Ui, plot_ui: &PlotUi) { - if CollapsingHeader::new(im_str!("Line plot: Basic")).build(&ui) { - show_basic_plot(&ui, &plot_ui); + // Colormaps are not pushed, they are simply set, because they don't stack or anything. + // We can reset to the default by just setting the "Standard" preset. + set_colormap_from_preset(Colormap::Standard, 0); } - if CollapsingHeader::new(im_str!("Line plot: Configured")).build(&ui) { - show_configurable_plot(&ui, &plot_ui); + + pub fn show_conversions_plot(ui: &Ui, plot_ui: &PlotUi) { + ui.text(im_str!( + "This header demonstrates (in code) how to convert various ranges into ImRange" + )); + let content_width = ui.window_content_region_width(); + Plot::new("Simple line plot, conversion 1") + .size([content_width, 300.0]) + .x_limits(ImVec2 { x: 0.0, y: 1.0 }, Condition::Always) + .y_limits([0.0, 1.0], YAxisChoice::First, Condition::Always) + .build(plot_ui, || { + // If this is called outside a plot build callback, the program will panic. + 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); + }); } - if CollapsingHeader::new(im_str!("Line Plot: Plot queries")).build(&ui) { - show_query_features_plot(&ui, &plot_ui); + + 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); + }); } - if CollapsingHeader::new(im_str!("Line plot: Plot styling")).build(&ui) { - show_style_plot(&ui, &plot_ui); - } - if CollapsingHeader::new(im_str!("Line plot: Colormaps")).build(&ui) { - show_colormaps_plot(&ui, &plot_ui); - } - if CollapsingHeader::new(im_str!("Line plot: Multiple Y Axes")).build(&ui) { - show_two_yaxis_plot(&ui, &plot_ui); - } - if CollapsingHeader::new(im_str!("Line plot: \"Axis equal\"")).build(&ui) { - show_axis_equal_plot(&ui, &plot_ui); - } - if CollapsingHeader::new(im_str!("Line plot: Range conversions")).build(&ui) { - show_conversions_plot(&ui, &plot_ui); + + pub fn show_demo_headers(&mut self, ui: &Ui, plot_ui: &PlotUi) { + if CollapsingHeader::new(im_str!("Line plot: Basic")).build(&ui) { + Self::show_basic_plot(&ui, &plot_ui); + } + if CollapsingHeader::new(im_str!("Line plot: Configured")).build(&ui) { + Self::show_configurable_plot(&ui, &plot_ui); + } + if CollapsingHeader::new(im_str!("Line Plot: Plot queries")).build(&ui) { + Self::show_query_features_plot(&ui, &plot_ui); + } + if CollapsingHeader::new(im_str!("Line plot: Plot styling")).build(&ui) { + Self::show_style_plot(&ui, &plot_ui); + } + if CollapsingHeader::new(im_str!("Line plot: Colormaps")).build(&ui) { + Self::show_colormaps_plot(&ui, &plot_ui); + } + if CollapsingHeader::new(im_str!("Line plot: Multiple Y Axes")).build(&ui) { + Self::show_two_yaxis_plot(&ui, &plot_ui); + } + if CollapsingHeader::new(im_str!("Line plot: \"Axis equal\"")).build(&ui) { + Self::show_axis_equal_plot(&ui, &plot_ui); + } + if CollapsingHeader::new(im_str!("Line plot: Range conversions")).build(&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); + } } } diff --git a/implot-examples/implot-glium-demo/src/main.rs b/implot-examples/implot-glium-demo/src/main.rs index ed67d71..954624f 100644 --- a/implot-examples/implot-glium-demo/src/main.rs +++ b/implot-examples/implot-glium-demo/src/main.rs @@ -8,6 +8,7 @@ fn main() { let system = support::init(file!()); let mut showing_demo = false; let mut showing_rust_demo = true; + let mut demo_state = examples_shared::DemoState::new(); let plotcontext = Context::create(); system.main_loop(move |_, ui| { // The context is moved into the closure after creation so plot_ui is valid. @@ -18,7 +19,7 @@ fn main() { } 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!")) diff --git a/implot-examples/implot-wgpu-demo/src/main.rs b/implot-examples/implot-wgpu-demo/src/main.rs index d6ce5db..954624f 100644 --- a/implot-examples/implot-wgpu-demo/src/main.rs +++ b/implot-examples/implot-wgpu-demo/src/main.rs @@ -1,4 +1,3 @@ - use imgui::{im_str, Condition, Window}; use implot::Context; @@ -9,6 +8,7 @@ fn main() { let system = support::init(file!()); let mut showing_demo = false; let mut showing_rust_demo = true; + let mut demo_state = examples_shared::DemoState::new(); let plotcontext = Context::create(); system.main_loop(move |_, ui| { // The context is moved into the closure after creation so plot_ui is valid. @@ -19,7 +19,7 @@ fn main() { } 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!")) diff --git a/src/plot.rs b/src/plot.rs index 129caad..4456da8 100644 --- a/src/plot.rs +++ b/src/plot.rs @@ -7,6 +7,7 @@ use bitflags::bitflags; pub use imgui::Condition; use imgui::{im_str, ImString}; use implot_sys as sys; +use std::{cell::RefCell, rc::Rc}; pub use sys::{ImPlotLimits, ImPlotPoint, ImPlotRange, ImVec2, ImVec4}; 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>), +} + /// 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: @@ -106,13 +116,9 @@ pub struct Plot { /// afterwards, and this ensures the ImString itself will stay alive long enough for the plot. y_label: ImString, /// X axis limits, if present - x_limits: Option, + x_limits: Option, /// Y axis limits, if present - y_limits: [Option; NUMBER_OF_Y_AXES], - /// Condition on which the x limits are set - x_limit_condition: Option, - /// Condition on which the y limits are set for each of the axes - y_limit_condition: [Option; NUMBER_OF_Y_AXES], + y_limits: [Option; NUMBER_OF_Y_AXES], /// Positions for custom X axis ticks, if any x_tick_positions: Option>, /// 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(), y_label: im_str!("").into(), x_limits: None, - y_limits: [None; NUMBER_OF_Y_AXES], - x_limit_condition: None, - y_limit_condition: [None; NUMBER_OF_Y_AXES], + y_limits: Default::default(), x_tick_positions: None, x_tick_labels: None, show_x_default_ticks: false, @@ -202,17 +206,33 @@ impl Plot { 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] pub fn x_limits>(mut self, limits: L, condition: Condition) -> Self { - self.x_limits = Some(limits.into()); - self.x_limit_condition = Some(condition); + self.x_limits = Some(AxisLimitSpecification::Single(limits.into(), 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>) -> Self { + self.x_limits = Some(AxisLimitSpecification::Linked(limits)); self } /// 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 /// [`Plot::y1_limits`]. + /// + /// Note: This conflicts with `linked_y_limits`, whichever is called last on plot construction + /// takes effect for a given axis. #[inline] pub fn y_limits>( mut self, @@ -221,32 +241,73 @@ impl Plot { condition: Condition, ) -> Self { let axis_index = y_axis_choice as usize; - self.y_limits[axis_index] = Some(limits.into()); - self.y_limit_condition[axis_index] = Some(condition); + self.y_limits[axis_index] = Some(AxisLimitSpecification::Single(limits.into(), condition)); self } /// 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] pub fn y1_limits>(self, limits: L, condition: Condition) -> Self { self.y_limits(limits, YAxisChoice::First, condition) } /// 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] pub fn y2_limits>(self, limits: L, condition: Condition) -> Self { self.y_limits(limits, YAxisChoice::Second, condition) } /// 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] pub fn y3_limits>(self, limits: L, condition: Condition) -> Self { 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>, + 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>) -> 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>) -> 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>) -> Self { + self.linked_y_limits(limits, YAxisChoice::Third) + } + /// 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 /// 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. fn maybe_set_axis_limits(&self) { - // Set X limits if specified - if let (Some(limits), Some(condition)) = (self.x_limits, self.x_limit_condition) { + // Limit-setting can either happen via direct limits or through linked limits. The version + // 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 { - 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 .iter() - .zip(self.y_limit_condition.iter()) .enumerate() - .for_each(|(k, (limits, condition))| { - if let (Some(limits), Some(condition)) = (limits, condition) { + .for_each(|(k, limit_spec)| { + if let Some(AxisLimitSpecification::Single(limits, condition)) = limit_spec { unsafe { sys::ImPlot_SetNextPlotLimitsY( 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