diff --git a/.gitignore b/.gitignore index 348c64b..acdea29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ rust_modules /target Cargo.lock -call_python3_works.py \ No newline at end of file +call_python3_works.py +.aider* diff --git a/.vscode/settings.json b/.vscode/settings.json index 9af7baa..d53e8ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,6 +27,7 @@ "CARETRIGHTBASE", "CARETUP", "CARETUPBASE", + "cbook", "clabel", "CLOSEPOLY", "cmap", @@ -51,6 +52,7 @@ "hspace", "imshow", "joinstyle", + "kwargs", "labelcolor", "labelpad", "labelsize", diff --git a/README.md b/README.md index 9a34a32..a62b660 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - [Curve](#curve) - [Histogram](#histogram) - [Image](#image) + - [InsetAxes](#insetaxes) - [Surface](#surface) - [Text](#text) @@ -126,8 +127,9 @@ fn main() -> Result<(), StrError> { plot.set_inv_y() .add(&bar) .set_title("Fruits") - .set_label_x("price") - .save("/tmp/plotpy/doc_tests/doc_barplot_3.svg")?; + .set_label_x("price"); + + // plot.save("/tmp/plotpy/doc_tests/doc_barplot_3.svg")?; Ok(()) } ``` @@ -166,8 +168,9 @@ fn main() -> Result<(), StrError> { let mut plot = Plot::new(); plot.add(&boxes) .set_title("boxplot documentation test") - .set_ticks_x_labels(&ticks, &labels) - .save("/tmp/plotpy/doc_tests/doc_boxplot_2.svg")?; + .set_ticks_x_labels(&ticks, &labels); + + // plot.save("/tmp/plotpy/doc_tests/doc_boxplot_2.svg")?; Ok(()) } ``` @@ -216,7 +219,8 @@ fn main() -> Result<(), StrError> { .set_hide_axes(true) .set_equal_axes(true) .set_show_errors(true); - plot.save("/tmp/plotpy/doc_tests/doc_canvas_polycurve.svg")?; + + // plot.save("/tmp/plotpy/doc_tests/doc_canvas_polycurve.svg")?; Ok(()) } ``` @@ -249,11 +253,10 @@ fn main() -> Result<(), StrError> { // add contour to plot let mut plot = Plot::new(); - plot.add(&contour); - plot.set_labels("x", "y"); + plot.add(&contour) + .set_labels("x", "y"); - // save figure - plot.save("/tmp/plotpy/readme_contour.svg")?; + // plot.save("/tmp/plotpy/readme_contour.svg")?; Ok(()) } ``` @@ -294,10 +297,11 @@ fn main() -> Result<(), StrError> { // add curve to plot let mut plot = Plot::new(); - plot.add(&curve).set_num_ticks_y(11).grid_labels_legend("x", "y"); + plot.add(&curve) + .set_num_ticks_y(11) + .grid_labels_legend("x", "y"); - // save figure - plot.save("/tmp/plotpy/doc_tests/doc_curve.svg")?; + // plot.save("/tmp/plotpy/doc_tests/doc_curve.svg")?; Ok(()) } ``` @@ -337,8 +341,7 @@ fn main() -> Result<(), StrError> { .set_frame_border(true, false, true, false) .grid_labels_legend("values", "count"); - // save figure - plot.save("/tmp/plotpy/doc_tests/doc_histogram.svg")?; + // plot.save("/tmp/plotpy/doc_tests/doc_histogram.svg")?; Ok(()) } ``` @@ -370,7 +373,8 @@ fn main() -> Result<(), StrError> { // save figure let mut plot = Plot::new(); plot.add(&img); - plot.save("/tmp/plotpy/doc_tests/doc_image_1.svg")?; + + // plot.save("/tmp/plotpy/doc_tests/doc_image_1.svg")?; Ok(()) } ``` @@ -378,6 +382,34 @@ fn main() -> Result<(), StrError> { ![image](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_image_1.svg) +### InsetAxes + +```rust +use plotpy::{Curve, InsetAxes, Plot, StrError}; + +fn main() -> Result<(), StrError> { + // draw curve + let mut curve = Curve::new(); + curve.draw(&[0.0, 1.0, 2.0], &[0.0, 1.0, 4.0]); + + // allocate inset and add curve to it + let mut inset = InsetAxes::new(); + inset + .add(&curve) // add curve to inset + .set_range(0.5, 1.5, 0.5, 1.5) // set the range of the inset + .draw(0.5, 0.5, 0.4, 0.3); + + // add curve and inset to plot + let mut plot = Plot::new(); + plot.add(&curve) + .set_range(0.0, 5.0, 0.0, 5.0) + .add(&inset); // IMPORTANT: add inset after setting the range + + // plot.save("/tmp/plotpy/doc_tests/doc_inset_axes_add.svg")?; + Ok(()) +} +``` + ### Surface @@ -432,8 +464,9 @@ fn main() -> Result<(), StrError> { // save figure plot.set_equal_axes(true) - .set_figure_size_points(600.0, 600.0) - .save("/tmp/plotpy/readme_superquadric.svg")?; + .set_figure_size_points(600.0, 600.0); + + // plot.save("/tmp/plotpy/readme_superquadric.svg")?; Ok(()) } ``` @@ -467,8 +500,7 @@ fn main() -> Result<(), StrError> { let mut plot = Plot::new(); plot.add(&text); - // save figure - plot.save("/tmp/plotpy/doc_tests/doc_text.svg")?; + // plot.save("/tmp/plotpy/doc_tests/doc_text.svg")?; Ok(()) } ``` diff --git a/examples/README.md b/examples/README.md index 667a100..5a9f872 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,9 +1,26 @@ -# Examples +# Examples Please check out [the documentation](https://docs.rs/plotpy) and the integration tests in the [tests directory](https://github.com/cpmech/plotpy/tree/main/tests) Some output of integration tests are shown below. +## Contents + +- [Barplot](#barplot) +- [Boxplot](#boxplot) +- [Canvas](#canvas) +- [Contour](#contour) +- [Curve](#curve) +- [Histogram](#histogram) +- [Image](#image) +- [InsetAxes](#insetaxes) +- [Legend](#legend) +- [Plot](#plot) +- [Subplot and GridSpec](#subplot-and-gridspec) +- [Slope icon](#slope-icon) +- [Surface and wireframe](#surface-and-wireframe) +- [Text](#text) + ## Barplot [test_barplot.rs](https://github.com/cpmech/plotpy/tree/main/tests/test_barplot.rs) @@ -81,6 +98,18 @@ Some output of integration tests are shown below. ![image](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_image_1.svg) +## InsetAxes + +[test_inset_axes.rs](https://github.com/cpmech/plotpy/tree/main/tests/test_inset_axes.rs) + +![inset_axes](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_inset_axes_1.svg) +![inset_axes](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_inset_axes_2.svg) +![inset_axes](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_inset_axes_3.svg) +![inset_axes](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_inset_axes_4.svg) +![inset_axes](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_inset_axes_5.svg) +![inset_axes](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_inset_axes_6.svg) + + ## Legend [test_legend.rs](https://github.com/cpmech/plotpy/tree/main/tests/test_legend.rs) diff --git a/figures/doc_inset_axes_add.svg b/figures/doc_inset_axes_add.svg new file mode 100644 index 0000000..1a56fd0 --- /dev/null +++ b/figures/doc_inset_axes_add.svg @@ -0,0 +1,450 @@ + + + + + + + + 2025-02-09T16:20:44.240614 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/figures/integ_inset_axes_1.svg b/figures/integ_inset_axes_1.svg new file mode 100644 index 0000000..060a89d --- /dev/null +++ b/figures/integ_inset_axes_1.svg @@ -0,0 +1,742 @@ + + + + + + + + 2025-02-09T12:25:33.542424 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/figures/integ_inset_axes_2.svg b/figures/integ_inset_axes_2.svg new file mode 100644 index 0000000..e550ef8 --- /dev/null +++ b/figures/integ_inset_axes_2.svg @@ -0,0 +1,819 @@ + + + + + + + + 2025-02-09T16:11:02.892451 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/figures/integ_inset_axes_3.svg b/figures/integ_inset_axes_3.svg new file mode 100644 index 0000000..b9becf4 --- /dev/null +++ b/figures/integ_inset_axes_3.svg @@ -0,0 +1,560 @@ + + + + + + + + 2025-02-09T16:11:02.874881 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/figures/integ_inset_axes_4.svg b/figures/integ_inset_axes_4.svg new file mode 100644 index 0000000..ed80227 --- /dev/null +++ b/figures/integ_inset_axes_4.svg @@ -0,0 +1,555 @@ + + + + + + + + 2025-02-09T17:23:02.772219 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/figures/integ_inset_axes_5.svg b/figures/integ_inset_axes_5.svg new file mode 100644 index 0000000..54a89d9 --- /dev/null +++ b/figures/integ_inset_axes_5.svg @@ -0,0 +1,2453 @@ + + + + + + + + 2025-02-09T16:58:04.371011 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/figures/integ_inset_axes_6.svg b/figures/integ_inset_axes_6.svg new file mode 100644 index 0000000..b07d5ca --- /dev/null +++ b/figures/integ_inset_axes_6.svg @@ -0,0 +1,978 @@ + + + + + + + + 2025-02-09T17:07:49.564328 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/canvas.rs b/src/canvas.rs index 307fb5d..37abd64 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -154,6 +154,7 @@ pub struct Canvas { } impl Canvas { + /// Creates a new Canvas object pub fn new() -> Self { Canvas { // features diff --git a/src/constants.rs b/src/constants.rs index 5c6795a..1a0a030 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -103,6 +103,10 @@ def set_equal_axes(): print('VERSION of MATPLOTLIB = {}'.format(matplotlib.__version__)) print('ERROR: set_box_aspect is missing in this version of Matplotlib') +# Function to ignore calls to plt such as the colorbar in an inset Axes +def ignore_this(*args, **kwargs): + pass + ################## plotting commands follow after this line ############################ "; @@ -146,6 +150,6 @@ mod tests { #[test] fn constants_are_correct() { - assert_eq!(PYTHON_HEADER.len(), 2770); + assert_eq!(PYTHON_HEADER.len(), 2886); } } diff --git a/src/inset_axes.rs b/src/inset_axes.rs new file mode 100644 index 0000000..4b72fda --- /dev/null +++ b/src/inset_axes.rs @@ -0,0 +1,398 @@ +use super::GraphMaker; +use std::fmt::Write; + +/// Implements the capability to add inset Axes to existing Axes. +/// +/// # Warning +/// +/// **WARNING:** If the range of axes has been modified in [crate::Plot], e.g. by `plot.set_range(...)`, +/// then the inset must be added after the range has been set. Otherwise, the inset will not be displayed correctly. +/// Specifically the connector lines will not be drawn if the inset is added before `set_range`. +/// +/// For example, below is the correct procedure: +/// +/// ``` +/// use plotpy::{Plot, InsetAxes}; +/// +/// let mut inset = InsetAxes::new(); +/// inset.draw(0.5, 0.5, 0.4, 0.3); +/// +/// let mut plot = Plot::new(); +/// plot.set_range(0.0, 10.0, 0.0, 10.0) +/// .add(&inset); // IMPORTANT: add inset after setting the range +/// ``` +pub struct InsetAxes { + xmin: f64, + xmax: f64, + ymin: f64, + ymax: f64, + extra_for_axes: String, + extra_for_indicator: String, + indicator_line_style: String, + indicator_line_color: String, + indicator_line_width: f64, + indicator_hatch: String, + indicator_alpha: Option, + axes_visible: bool, + title: String, + buffer: String, +} + +impl InsetAxes { + /// Creates a new `InsetAxes` object with an empty buffer. + /// + /// # Returns + /// + /// A new instance of `InsetAxes`. + /// + /// # Warning + /// + /// **WARNING:** If the range of axes has been modified in [crate::Plot], e.g. by `plot.set_range(...)`, + /// then the inset must be added after the range has been set. Otherwise, the inset will not be displayed correctly. + /// Specifically the connector lines will not be drawn if the inset is added before `set_range`. + /// + /// For example, below is the correct procedure: + /// + /// ``` + /// use plotpy::{InsetAxes, Plot}; + /// let mut inset = InsetAxes::new(); + /// let mut plot = Plot::new(); + /// plot.set_range(0.0, 10.0, 0.0, 10.0) + /// .add(&inset); // IMPORTANT: add inset after setting the range + /// ``` + pub fn new() -> Self { + Self { + xmin: 0.0, + xmax: 1.0, + ymin: 0.0, + ymax: 1.0, + extra_for_axes: String::new(), + extra_for_indicator: String::new(), + indicator_line_style: String::new(), + indicator_line_color: String::new(), + indicator_line_width: 0.0, + indicator_hatch: String::new(), + indicator_alpha: None, + axes_visible: false, + title: String::new(), + buffer: String::new(), + } + } + + /// Sets the line style for the indicator (e.g. "--", ":", "-.") + pub fn set_indicator_line_style(&mut self, style: &str) -> &mut Self { + self.indicator_line_style = style.to_string(); + self + } + + /// Sets the line color for the indicator (e.g. "red", "#FF0000") + pub fn set_indicator_line_color(&mut self, color: &str) -> &mut Self { + self.indicator_line_color = color.to_string(); + self + } + + /// Sets the line width for the indicator + pub fn set_indicator_line_width(&mut self, width: f64) -> &mut Self { + self.indicator_line_width = width; + self + } + + /// Sets the alpha (opacity) for the indicator + pub fn set_indicator_alpha(&mut self, alpha: f64) -> &mut Self { + self.indicator_alpha = Some(alpha); + self + } + + /// Sets the hatch pattern for the indicator (e.g. "/", "\\", "|", "-", "+", "x", "o", "O", ".", "*") + /// + /// Common hatch patterns include: + /// + /// * "/" - diagonal hatching + /// * "\" - back diagonal hatching + /// * "|" - vertical hatching + /// * "-" - horizontal hatching + /// * "+" - crossed hatching + /// * "x" - crossed diagonal hatching + /// * "o" - small circle hatching + /// * "O" - large circle hatching + /// * "." - dot hatching + /// * "*" - star hatching + /// + /// [See options in ](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.indicate_inset.html#matplotlib.axes.Axes.indicate_inset) + /// + /// [See Matplotlib's documentation for more hatch patterns](https://matplotlib.org/stable/gallery/shapes_and_collections/hatch_demo.html) + pub fn set_indicator_hatch(&mut self, hatch: &str) -> &mut Self { + self.indicator_hatch = hatch.to_string(); + self + } + + /// Adds new graph entity + /// + /// # Warning + /// + /// **WARNING:** If the range of axes has been modified in [crate::Plot], e.g. by `plot.set_range(...)`, + /// then the inset must be added after the range has been set. Otherwise, the inset will not be displayed correctly. + /// Specifically the connector lines will not be drawn if the inset is added before `set_range`. + /// + /// For example, below is the correct procedure: + /// + /// ``` + /// use plotpy::{InsetAxes, Plot}; + /// let mut inset = InsetAxes::new(); + /// let mut plot = Plot::new(); + /// plot.set_range(0.0, 10.0, 0.0, 10.0) + /// .add(&inset); // IMPORTANT: add inset after setting the range + /// ``` + pub fn add(&mut self, graph: &dyn GraphMaker) -> &mut Self { + // Note: the order of replacements is important + let buf = graph + .get_buffer() + .replace("plt.gca()", "zoom") + .replace("plt.barh", "zoom.barh") + .replace("plt.bar", "zoom.bar") + .replace("plt.contourf", "zoom.contourf") + .replace("plt.contour", "zoom.contour") + .replace("plt.clabel", "zoom.clabel") + .replace("plt.colorbar", "ignore_this") + .replace("cb.ax.set_ylabel", "ignore_this") + .replace("plt.imshow", "zoom.imshow") + .replace("plt.hist", "zoom.hist") + .replace("plt.plot", "zoom.plot") + .replace("plt.text", "zoom.text"); + self.buffer.push_str(&buf); + self + } + + /// Draws the inset Axes. + /// + /// Example of normalized coordinates: `(0.5, 0.5, 0.4, 0.3)`. + /// + /// # Arguments + /// + /// * `u0` -- The normalized (0 to 1) horizontal figure coordinate of the lower-left corner of the inset Axes. + /// * `v0` -- The normalized (0 to 1) vertical figure coordinate of the lower-left corner of the inset Axes. + /// * `width` -- The width of the inset Axes. + /// * `height` -- The height of the inset Axes. + /// + /// # Warning + /// + /// **WARNING:** If the range of axes has been modified in [crate::Plot], e.g. by `plot.set_range(...)`, + /// then the inset must be added after the range has been set. Otherwise, the inset will not be displayed correctly. + /// Specifically the connector lines will not be drawn if the inset is added before `set_range`. + /// + /// For example, below is the correct procedure: + /// + /// ``` + /// use plotpy::{Curve, InsetAxes, Plot, StrError}; + /// + /// fn main() -> Result<(), StrError> { + /// // draw curve + /// let mut curve = Curve::new(); + /// curve.draw(&[0.0, 1.0, 2.0], &[0.0, 1.0, 4.0]); + /// + /// // allocate inset and add curve to it + /// let mut inset = InsetAxes::new(); + /// inset + /// .add(&curve) // add curve to inset + /// .set_range(0.5, 1.5, 0.5, 1.5) // set the range of the inset + /// .draw(0.5, 0.5, 0.4, 0.3); + /// + /// // add curve and inset to plot + /// let mut plot = Plot::new(); + /// plot.add(&curve) + /// .set_range(0.0, 5.0, 0.0, 5.0) + /// .add(&inset) // IMPORTANT: add inset after setting the range + /// .save("/tmp/plotpy/doc_tests/doc_inset_axes_add.svg") + /// } + /// ``` + /// + /// ![doc_inset_axes_add.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_inset_axes_add.svg) + pub fn draw(&mut self, u0: f64, v0: f64, width: f64, height: f64) { + let opt1 = self.options_for_axes(); + let opt2 = self.options_for_indicator(); + self.buffer.insert_str( + 0, + &format!( + "zoom=plt.gca().inset_axes([{},{},{},{}],xlim=({},{}),ylim=({},{}){})\n", + u0, v0, width, height, self.xmin, self.xmax, self.ymin, self.ymax, opt1, + ), + ); + if !self.axes_visible { + write!(&mut self.buffer, "zoom.set_xticks([])\nzoom.set_yticks([])\n").unwrap(); + } + if !self.title.is_empty() { + write!(&mut self.buffer, "zoom.set_title(r'{}')\n", self.title).unwrap(); + } + write!(&mut self.buffer, "plt.gca().indicate_inset_zoom(zoom{})\n", opt2,).unwrap(); + } + + /// Sets the limits of axes in the inset. + pub fn set_range(&mut self, xmin: f64, xmax: f64, ymin: f64, ymax: f64) -> &mut Self { + self.xmin = xmin; + self.xmax = xmax; + self.ymin = ymin; + self.ymax = ymax; + self + } + + /// Sets extra Matplotlib commands for the inset Axes (comma separated). + /// + /// [See Matplotlib's documentation for extra parameters]() + pub fn set_extra_for_axes(&mut self, extra: &str) -> &mut Self { + self.extra_for_axes = extra.to_string(); + self + } + + /// Sets extra Matplotlib commands for the indicator (comma separated). + /// + /// [See Matplotlib's documentation for extra parameters](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.indicate_inset.html#matplotlib.axes.Axes.indicate_inset) + pub fn set_extra_for_indicator(&mut self, extra: &str) -> &mut Self { + self.extra_for_indicator = extra.to_string(); + self + } + + /// Sets the visibility of the axes ticks + /// + /// # Arguments + /// + /// * `visible` - If true, shows the axes ticks. If false, hides them. + pub fn set_visibility(&mut self, visible: bool) -> &mut Self { + self.axes_visible = visible; + self + } + + /// Sets the title of the inset axes + pub fn set_title(&mut self, title: &str) -> &mut Self { + self.title = title.to_string(); + self + } + + /// Returns options for the inset Axes + fn options_for_axes(&self) -> String { + let mut opt = String::new(); + if !self.extra_for_axes.is_empty() { + write!(&mut opt, ",{}", self.extra_for_axes).unwrap(); + } + opt + } + + /// Returns options for the indicator + fn options_for_indicator(&self) -> String { + let mut opt = String::new(); + if !self.indicator_line_style.is_empty() { + write!(&mut opt, ",linestyle='{}'", self.indicator_line_style).unwrap(); + } + if !self.indicator_line_color.is_empty() { + write!(&mut opt, ",edgecolor='{}'", self.indicator_line_color).unwrap(); + } + if self.indicator_line_width > 0.0 { + write!(&mut opt, ",linewidth={}", self.indicator_line_width).unwrap(); + } + if !self.indicator_hatch.is_empty() { + write!(&mut opt, ",hatch='{}'", self.indicator_hatch).unwrap(); + } + if let Some(alpha) = self.indicator_alpha { + write!(&mut opt, ",alpha={}", alpha).unwrap(); + } + if !self.extra_for_indicator.is_empty() { + write!(&mut opt, ",{}", self.extra_for_indicator).unwrap(); + } + opt + } +} + +impl GraphMaker for InsetAxes { + /// Returns a reference to the buffer containing the generated commands. + /// + /// # Returns + /// + /// A reference to the buffer as a `String`. + fn get_buffer<'a>(&'a self) -> &'a String { + &self.buffer + } + + /// Clears the buffer, removing all stored commands. + fn clear_buffer(&mut self) { + self.buffer.clear(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use super::InsetAxes; + use crate::GraphMaker; + + #[test] + fn test_new() { + let inset = InsetAxes::new(); + assert_eq!(inset.xmin, 0.0); + assert_eq!(inset.xmax, 1.0); + assert_eq!(inset.ymin, 0.0); + assert_eq!(inset.ymax, 1.0); + assert!(inset.buffer.is_empty()); + } + + #[test] + fn test_set_range() { + let mut inset = InsetAxes::new(); + inset.set_range(-1.0, 2.0, -3.0, 4.0); + assert_eq!(inset.xmin, -1.0); + assert_eq!(inset.xmax, 2.0); + assert_eq!(inset.ymin, -3.0); + assert_eq!(inset.ymax, 4.0); + } + + #[test] + fn test_set_title() { + let mut inset = InsetAxes::new(); + inset.set_title("Test Title"); + assert_eq!(inset.title, "Test Title"); + } + + #[test] + fn test_set_visibility() { + let mut inset = InsetAxes::new(); + inset.set_visibility(true); + assert!(inset.axes_visible); + inset.set_visibility(false); + assert!(!inset.axes_visible); + } + + #[test] + fn test_indicator_options() { + let mut inset = InsetAxes::new(); + inset + .set_indicator_line_style("--") + .set_indicator_line_color("red") + .set_indicator_line_width(2.0) + .set_indicator_hatch("/") + .set_indicator_alpha(0.5); + + let options = inset.options_for_indicator(); + assert!(options.contains("linestyle='--'")); + assert!(options.contains("edgecolor='red'")); + assert!(options.contains("linewidth=2")); + assert!(options.contains("hatch='/'")); + assert!(options.contains("alpha=0.5")); + } + + #[test] + fn test_draw_basic() { + let mut inset = InsetAxes::new(); + inset.draw(0.5, 0.5, 0.4, 0.3); + let buffer = inset.get_buffer(); + assert!(buffer.contains("zoom=plt.gca().inset_axes([0.5,0.5,0.4,0.3]")); + assert!(buffer.contains("plt.gca().indicate_inset_zoom(zoom")); + } + + #[test] + fn test_clear_buffer() { + let mut inset = InsetAxes::new(); + inset.draw(0.5, 0.5, 0.4, 0.3); + assert!(!inset.buffer.is_empty()); + inset.clear_buffer(); + assert!(inset.buffer.is_empty()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 95fa812..970814c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -86,6 +86,7 @@ mod curve; mod fileio; mod histogram; mod image; +mod inset_axes; mod legend; mod plot; mod slope_icon; @@ -108,6 +109,7 @@ pub use curve::*; use fileio::*; pub use histogram::*; pub use image::*; +pub use inset_axes::*; pub use legend::*; pub use plot::*; pub use slope_icon::*; diff --git a/tests/test_inset_axes.rs b/tests/test_inset_axes.rs new file mode 100644 index 0000000..b6cc590 --- /dev/null +++ b/tests/test_inset_axes.rs @@ -0,0 +1,254 @@ +use plotpy::{generate3d, Barplot, Canvas, Contour, Curve, Histogram, Image, InsetAxes, Plot, StrError, Text}; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::Path; + +const OUT_DIR: &str = "/tmp/plotpy/integ_tests"; + +#[test] +fn test_inset_axes_1() -> Result<(), StrError> { + // draw image + let data = [ + [0.8, 2.4, 2.5, 3.9, 0.0, 4.0, 0.0], + [2.4, 0.0, 4.0, 1.0, 2.7, 0.0, 0.0], + [1.1, 2.4, 0.8, 4.3, 1.9, 4.4, 0.0], + [0.6, 0.0, 0.3, 0.0, 3.1, 0.0, 0.0], + [0.7, 1.7, 0.6, 2.6, 2.2, 6.2, 0.0], + [1.3, 1.2, 0.0, 0.0, 0.0, 3.2, 5.1], + [0.1, 2.0, 0.0, 1.4, 0.0, 1.9, 6.3], + ]; + let mut img = Image::new(); + let mut plot = Plot::new(); + img.set_colormap_name("terrain").set_extra("alpha=0.8").draw(&data); + plot.add(&img); + + // inset axes + let mut inset = InsetAxes::new(); + inset + .set_title("ZOOM") + .set_visibility(true) + .set_indicator_line_color("red") + .set_indicator_line_style("--") + .set_indicator_line_width(2.0) + .set_indicator_alpha(1.0) + .set_indicator_hatch("x") + .set_extra_for_axes("xlabel='X',ylabel='Y'") + .set_extra_for_indicator("label='INDICATOR',visible=True") + .set_range(0.0, 1.0, 5.0, 6.0); + inset.add(&img).draw(0.5, 0.5, 0.4, 0.3); + + // add entities to plot + plot.add(&img).add(&inset); + + // save figure + let path = Path::new(OUT_DIR).join("integ_inset_axes_1.svg"); + plot.set_show_errors(true).save(&path)?; + + // check number of lines + let file = File::open(path).map_err(|_| "cannot open file")?; + let buffered = BufReader::new(file); + let lines_iter = buffered.lines(); + let n = lines_iter.count().clone(); + assert!(n > 680 && n < 800); + Ok(()) +} + +#[test] +fn test_inset_axes_2() -> Result<(), StrError> { + // data + let x = [0, 1, 2, 3, 4]; + let y = [5, 4, 3, 2, 1]; + + // define a function to draw with vertical and horizontal bars + let draw = |plot: &mut Plot, horizontal: bool| { + // allocate the Barplot and InsetAxes instances + let mut bar = Barplot::new(); + let mut inset = InsetAxes::new(); + + // configure the barplot + bar.set_horizontal(horizontal).draw(&x, &y); + + // configure the inset axes + inset.set_range(0.5, 2.5, 2.0, 4.5); + + // add barplot to inset + inset.add(&bar).draw(0.65, 0.65, 0.335, 0.33); + + // add barplot and inset to plot + plot.add(&bar).add(&inset); + }; + + // allocate plot and add each type of figure to a subplot + let mut plot = Plot::new(); + + // vertical bars + plot.set_subplot(1, 2, 1); + draw(&mut plot, false); + + // horizontal bars + plot.set_subplot(1, 2, 2); + draw(&mut plot, true); + + // save figure + let path = Path::new(OUT_DIR).join("integ_inset_axes_2.svg"); + plot.set_figure_size_points(650.0, 250.0) + .set_show_errors(true) + .save(&path)?; + + // check number of lines + let file = File::open(path).map_err(|_| "cannot open file")?; + let buffered = BufReader::new(file); + let lines_iter = buffered.lines(); + let n = lines_iter.count().clone(); + assert!(n > 790 && n < 850); + Ok(()) +} + +#[test] +fn test_inset_axes_3() -> Result<(), StrError> { + // canvas + let mut canvas = Canvas::new(); + canvas + .set_face_color("None") + .set_edge_color("red") + .draw_circle(0.5, 0.5, 0.45); + + // inset axes + let mut inset = InsetAxes::new(); + inset + .set_indicator_alpha(1.0) + .set_indicator_line_color("blue") + .set_range(0.5, 1.0, 0.5, 1.0) + .add(&canvas) + .draw(0.65, 0.65, 0.335, 0.33); + + // add to plot + let mut plot = Plot::new(); + plot.add(&canvas); + + // save figure + let path = Path::new(OUT_DIR).join("integ_inset_axes_3.svg"); + plot.set_range(0.0, 2.0, 0.0, 2.0) + .add(&inset) // <<<<<<<<<<<<< IMPORTANT: thus must be after set_range + .set_equal_axes(true) + .set_show_errors(true) + .save(&path)?; + + // check number of lines + let file = File::open(path).map_err(|_| "cannot open file")?; + let buffered = BufReader::new(file); + let lines_iter = buffered.lines(); + let n = lines_iter.count().clone(); + assert!(n > 520 && n < 600); + Ok(()) +} + +#[test] +fn test_inset_axes_4() -> Result<(), StrError> { + // curve + let mut curve = Curve::new(); + let x = &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; + let y = &[1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0]; + curve.draw(x, y); + + // text + let mut text = Text::new(); + text.set_align_horizontal("center").draw(8.0, 64.0, "LOOK!"); + + // inset axes + let mut inset = InsetAxes::new(); + inset + .add(&text) + .add(&curve) + .set_range(7.0, 9.0, 40.0, 70.0) + .draw(0.05, 0.25, 0.4, 0.7); + + // add to plot + let mut plot = Plot::new(); + plot.add(&curve).add(&text); + + // save figure + let path = Path::new(OUT_DIR).join("integ_inset_axes_4.svg"); + plot.set_range(0.0, 10.0, 0.0, 100.0) + .add(&inset) // <<<<<<<<<<<<< IMPORTANT: thus must be after set_range + .set_show_errors(true) + .save(&path)?; + + // check number of lines + let file = File::open(path).map_err(|_| "cannot open file")?; + let buffered = BufReader::new(file); + let lines_iter = buffered.lines(); + let n = lines_iter.count().clone(); + assert!(n > 500 && n < 600); + Ok(()) +} + +#[test] +fn test_inset_axes_5() -> Result<(), StrError> { + // contour + let mut contour = Contour::new(); + contour.set_colorbar_label("TEMPERATURE").set_number_format_cb("%.1f"); + let n = 9; + let (x, y, z) = generate3d(-2.0, 2.0, -2.0, 2.0, n, n, |x, y| x * x + y * y); + contour.draw(&x, &y, &z); + + // inset axes + let mut inset = InsetAxes::new(); + inset + .set_indicator_line_color("yellow") + .add(&contour) + .set_range(-1.0, 1.0, -1.0, 1.0) + .draw(0.78, 0.78, 0.2, 0.2); + + // add to plot + let mut plot = Plot::new(); + plot.add(&contour); + + // save figure + let path = Path::new(OUT_DIR).join("integ_inset_axes_5.svg"); + plot.add(&inset).set_show_errors(true).save(&path)?; + + // check number of lines + let file = File::open(path).map_err(|_| "cannot open file")?; + let buffered = BufReader::new(file); + let lines_iter = buffered.lines(); + let n = lines_iter.count().clone(); + assert!(n > 2400 && n < 2500); + Ok(()) +} + +#[test] +fn test_inset_axes_6() -> Result<(), StrError> { + // histogram + let mut histogram = Histogram::new(); + let values = vec![ + vec![1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 4, 5, 6], // first series + vec![-1, -1, 0, 1, 2, 3], // second series + vec![5, 6, 7, 8], // third series + ]; + let labels = ["first", "second", "third"]; + histogram.draw(&values, &labels); + + // inset axes + let mut inset = InsetAxes::new(); + inset + .add(&histogram) + .set_range(1.5, 2.5, 0.5, 1.2) + .draw(0.6, 0.55, 0.35, 0.4); + + // add to plot + let mut plot = Plot::new(); + plot.add(&histogram); + + // save figure + let path = Path::new(OUT_DIR).join("integ_inset_axes_6.svg"); + plot.add(&inset).set_show_errors(true).save(&path)?; + + // check number of lines + let file = File::open(path).map_err(|_| "cannot open file")?; + let buffered = BufReader::new(file); + let lines_iter = buffered.lines(); + let n = lines_iter.count().clone(); + assert!(n > 920 && n < 1010); + Ok(()) +}