Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Test
name: Arch
on: [pull_request]
jobs:
test_on_arch_linux:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Test
name: Ubuntu
on: [pull_request]
jobs:
test_and_coverage:
Expand Down
13 changes: 13 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
},
"cSpell.words": [
"Adelie",
"archlinux",
"arrowstyle",
"axhline",
"axisbelow",
Expand All @@ -32,6 +33,8 @@
"clabel",
"CLOSEPOLY",
"cmap",
"colorbar",
"colormap",
"COLORMAPS",
"columnspacing",
"contourf",
Expand All @@ -54,6 +57,7 @@
"horizontalalignment",
"hspace",
"imshow",
"integ",
"joinstyle",
"kwargs",
"labelcolor",
Expand All @@ -66,19 +70,26 @@
"linestyles",
"LINETO",
"linewidths",
"linspace",
"markeredgecolor",
"markeredgewidth",
"markerfacecolor",
"markersize",
"markevery",
"Mathematica",
"Matplotlib",
"meshgrid",
"MOVETO",
"mplot",
"nocapture",
"noconfirm",
"nord",
"numpoints",
"numpy",
"pacman",
"patheffects",
"plotpy",
"polycurve",
"polyline",
"pyplot",
"rarrow",
Expand All @@ -88,6 +99,8 @@
"savefig",
"showfliers",
"stepfilled",
"superquadric",
"superquadrics",
"suptitle",
"textcolor",
"TICKDOWN",
Expand Down
93 changes: 79 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
[![documentation](https://img.shields.io/badge/plotpy-documentation-blue)](https://docs.rs/plotpy)
[![Track Awesome List](https://www.trackawesomelist.com/badge.svg)](https://www.trackawesomelist.com/rust-unofficial/awesome-rust/)

[![Test & Coverage](https://github.com/cpmech/plotpy/actions/workflows/test_and_coverage.yml/badge.svg)](https://github.com/cpmech/plotpy/actions/workflows/test_and_coverage.yml)
[![Arch](https://github.com/cpmech/plotpy/actions/workflows/arch.yml/badge.svg)](https://github.com/cpmech/plotpy/actions/workflows/arch.yml)
[![Ubuntu](https://github.com/cpmech/plotpy/actions/workflows/ubuntu.yml/badge.svg)](https://github.com/cpmech/plotpy/actions/workflows/ubuntu.yml)


## Contents <!-- omit from toc -->

- [Introduction](#introduction)
- [Installation](#installation)
- [Setting Cargo.toml](#setting-cargotoml)
- [Use of Jupyter via evcxr](#use-of-jupyter-via-evcxr)
- [Arch Linux](#arch-linux)
- [Debian/Ubuntu Linux](#debianubuntu-linux)
- [Other systems](#other-systems)
- [Setting Cargo.toml](#setting-cargotoml)
- [Use of Jupyter via evcxr](#use-of-jupyter-via-evcxr)
- [Examples](#examples)
- [Barplot](#barplot)
- [Boxplot](#boxplot)
Expand All @@ -23,6 +27,9 @@
- [InsetAxes](#insetaxes)
- [Surface](#surface)
- [Text](#text)
- [Architecture](#architecture)
- [Chaining pattern (builder style)](#chaining-pattern-builder-style)
- [Consistent conventions across all files](#consistent-conventions-across-all-files)


## Introduction
Expand All @@ -39,17 +46,29 @@ See also the [examples directory](https://github.com/cpmech/plotpy/tree/main/exa

## Installation

*This code is mainly tested on Debian/Ubuntu/Linux.*
*This code is mainly tested on Arch Linux and Debian/Ubuntu Linux.*

This crate needs Python3 and Matplotlib, of course.
This crate needs Python3 and Matplotlib.

On Debian/Ubuntu/Linux, run:
### Arch Linux

Install the dependencies:

```bash
pacman -Syu --noconfirm python-matplotlib
```

### Debian/Ubuntu Linux

Install the dependencies:

```bash
sudo apt install python3-matplotlib
```

**Important:** The Rust code will call `python3` via `std::process::Command`. However, there is an option to call a different python executable; for instance (the code below is no tested):
### Other systems

It is possible to run `plotpy` in other systems where Python and Matplotlib are already installed. The Rust code calls `python3` via `std::process::Command`. However, there is an option to call a different python executable; for instance (the code below is untested):

```text
let mut plot = Plot::new();
Expand All @@ -58,9 +77,7 @@ plot.set_python_exe("C:\Windows11\WhereIs\python.exe")
.save(...)?;
```



## Setting Cargo.toml
### Setting Cargo.toml

[![Crates.io](https://img.shields.io/crates/v/plotpy.svg)](https://crates.io/crates/plotpy)

Expand All @@ -71,13 +88,11 @@ plot.set_python_exe("C:\Windows11\WhereIs\python.exe")
plotpy = "*"
```



## Use of Jupyter via evcxr
### Use of Jupyter via evcxr

Plotpy can be used with Jupyter via [evcxr](https://github.com/evcxr/evcxr). Thus, it can interactively display the plots in a Jupyter Notebook. This feature requires the installation of `evcxr`. See the [Jupyter/evcxr article](https://depth-first.com/articles/2020/09/21/interactive-rust-in-a-repl-and-jupyter-notebook-with-evcxr/).

The following code shows a minimal example (not tested)
The following code shows a minimal example (the code below is untested)

```text
// set the python path
Expand All @@ -98,6 +113,8 @@ plot.set_python_exe(python)

## Examples

Note, below `StrError` is defined as `pub type StrError = &'static str`; — a type alias for a static string slice. It's used throughout the library as the error type returned from functions. It's essentially a lightweight, allocation-free error type that avoids pulling in a full error-handling crate.



### Barplot
Expand Down Expand Up @@ -506,3 +523,51 @@ fn main() -> Result<(), StrError> {
```

![text](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_text.svg)

---

## Architecture

(Generated by [DeepSeek](https://www.deepseek.com/))

**Core idea**: Generates Python 3 scripts as strings from Rust, then executes them via `python3`. Not a direct API wrapper — it's a code generator.

- **25 source files** in `src/`, each a standalone module
- Each "graph entity" struct (`Curve`, `Barplot`, `Boxplot`, `Contour`, `Surface`, `Canvas`, `Histogram`, `Text`, etc.) implements `GraphMaker` trait (`get_buffer()` + `clear_buffer()`)
- `Plot` is the central coordinator — collects buffers via `add(&entity)`, prepends a Python header, appends `plt.savefig()`, writes `.py` file, executes it
- Only one dependency: `num-traits = "0.2"` (for generic `Num` bound)
- Two data abstraction traits: `AsVector` (for 1D data) and `AsMatrix` (for 2D data)

### Chaining pattern (builder style)

The entire library follows `something.method1().method2().method3()` pervasively.

**Graph entities** — setters return `&mut Self`:
```text
curve.set_label("logistic")
.set_line_color("#5f9cd8")
.set_line_style("-")
.set_line_width(5.0);
curve.draw(&x, &y);
```
Note: `draw()` methods don't return `&mut Self` (they finalize by writing Python code). But `points_begin()`/`points_add()`/`points_end()` do chain.

**Plot** — everything returns `&mut Self`:
```text
plot.set_subplot(2, 2, 1)
.set_title("first")
.add(&curve1)
.grid_labels_legend("x", "y")
.set_equal_axes(true);
```

### Consistent conventions across all files

- `new()` → defaults (empty strings, `0.0` sentinels)
- `set_*()` → returns `&mut Self`
- `options()` → private method builds CSV-style parameter string
- `draw()` → writes Python to `buffer` using `write!` macro (all `.unwrap()` since `String` writes are infallible)
- `GraphMaker` impl → exposes the buffer
- Inline `#[cfg(test)] mod tests` in every file + integration tests under `tests/`
- `max_width = 120` in `rustfmt.toml`
- Error type: `pub type StrError = &'static str;`
2 changes: 1 addition & 1 deletion src/as_vector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ mod tests {
let y: &[f64] = &[10.0, 20.0, 30.0];
assert_eq!(vector_str(&y), "10,20,30,\n");

// stack-allocated (fixed-size) 2D array
// stack-allocated (fixed-size) 1D array
let z = [100.0, 200.0, 300.0];
assert_eq!(vector_str(&z), "100,200,300,\n");
}
Expand Down
89 changes: 77 additions & 12 deletions src/auxiliary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,59 @@ pub fn sign(x: f64) -> f64 {
}
}

/// Implements the superquadric function involving sin(x)
/// Implements the superquadric auxiliary function involving sin(x)
///
/// ```text
/// suq_sin(x;k) = sign(sin(x)) · |sin(x)|ᵏ
/// ```
///
/// `suq_sin(x;k)` is the `f(ω;m)` function from <https://en.wikipedia.org/wiki/Superquadrics>
/// This is the angular shaping function for [superquadrics](https://en.wikipedia.org/wiki/Superquadrics).
/// When `k = 2` the result is the standard parametric form of a sphere/ellipsoid.
/// Values `k < 1` produce a "pinched" shape; `k > 2` produces a "squared" shape.
///
/// `suq_sin(x;k)` corresponds to `f(ω;m)` in the superquadric literature.
///
/// See also: [`suq_cos`]
pub fn suq_sin(x: f64, k: f64) -> f64 {
sign(f64::sin(x)) * f64::powf(f64::abs(f64::sin(x)), k)
}

/// Implements the superquadric auxiliary involving cos(x)
/// Implements the superquadric auxiliary function involving cos(x)
///
/// ```text
/// suq_cos(x;k) = sign(cos(x)) · |cos(x)|ᵏ
/// ```
///
/// `suq_cos(x;k)` is the `g(ω;m)` function from <https://en.wikipedia.org/wiki/Superquadrics>
/// This is the angular shaping function for [superquadrics](https://en.wikipedia.org/wiki/Superquadrics).
/// When `k = 2` the result is the standard parametric form of a sphere/ellipsoid.
/// Values `k < 1` produce a "pinched" shape; `k > 2` produces a "squared" shape.
///
/// `suq_cos(x;k)` corresponds to `g(ω;m)` in the superquadric literature.
///
/// See also: [`suq_sin`]
pub fn suq_cos(x: f64, k: f64) -> f64 {
sign(f64::cos(x)) * f64::powf(f64::abs(f64::cos(x)), k)
}

/// Returns evenly spaced numbers over a specified closed interval
///
/// Analogous to [numpy.linspace](https://numpy.org/doc/stable/reference/generated/numpy.linspace.html).
/// Both `start` and `stop` are included in the output (closed interval).
///
/// # Examples
///
/// ```
/// use plotpy::linspace;
///
/// assert_eq!(linspace(0.0, 1.0, 3), vec![0.0, 0.5, 1.0]);
/// assert_eq!(linspace(0.0, 1.0, 5), vec![0.0, 0.25, 0.5, 0.75, 1.0]);
/// ```
///
/// # Edge cases
///
/// - `count == 0` returns an empty vector
/// - `count == 1` returns `[start]`
/// - `count == 2` returns `[start, stop]`
pub fn linspace(start: f64, stop: f64, count: usize) -> Vec<f64> {
if count == 0 {
return Vec::new();
Expand All @@ -68,18 +98,34 @@ pub fn linspace(start: f64, stop: f64, count: usize) -> Vec<f64> {
res
}

/// Generates 2d points (meshgrid)
/// Generates 2d meshgrid points
///
/// This is analogous to [numpy.meshgrid](https://numpy.org/doc/stable/reference/generated/numpy.meshgrid.html)
/// with `indexing='ij'`. Produces two (`ny` × `nx`) matrices where each row has the same `y`
/// value and each column has the same `x` value.
///
/// # Input
///
/// * `xmin`, `xmax` -- range along x
/// * `ymin`, `ymax` -- range along y
/// * `nx` -- is the number of points along x (must be `>= 2`)
/// * `ny` -- is the number of points along y (must be `>= 2`)
/// * `nx` -- number of points along x (must be `>= 2`)
/// * `ny` -- number of points along y (must be `>= 2`)
///
/// # Output
///
/// * `x`, `y` -- (`ny` by `nx`) 2D arrays
/// * `x`, `y` -- (`ny` by `nx`) 2D arrays such that `x[i][j] = xmin + j·dx` and `y[i][j] = ymin + i·dy`
///
/// # Example
///
/// ```
/// use plotpy::generate2d;
///
/// let (x, y) = generate2d(-1.0, 1.0, -3.0, 3.0, 2, 3);
/// assert_eq!(x, vec![vec![-1.0, 1.0], vec![-1.0, 1.0], vec![-1.0, 1.0]]);
/// assert_eq!(y, vec![vec![-3.0, -3.0], vec![0.0, 0.0], vec![3.0, 3.0]]);
/// ```
///
/// See also: [`generate3d`]
pub fn generate2d(xmin: f64, xmax: f64, ymin: f64, ymax: f64, nx: usize, ny: usize) -> (Vec<Vec<f64>>, Vec<Vec<f64>>) {
let mut x = vec![vec![0.0; nx]; ny];
let mut y = vec![vec![0.0; nx]; ny];
Expand Down Expand Up @@ -107,19 +153,38 @@ pub fn generate2d(xmin: f64, xmax: f64, ymin: f64, ymax: f64, nx: usize, ny: usi
(x, y)
}

/// Generates 3d points (function over meshgrid)
/// Generates 3d points by evaluating a function over a 2d meshgrid
///
/// Creates the same `(x, y)` grid as [`generate2d`], then evaluates `calc_z(x, y)` at each
/// grid point to produce the `z` matrix. This is the typical input for [`Surface::draw`](crate::Surface::draw)
/// and [`Contour::draw`](crate::Contour::draw).
///
/// # Input
///
/// * `xmin`, `xmax` -- range along x
/// * `ymin`, `ymax` -- range along y
/// * `nx` -- is the number of points along x (must be `>= 2`)
/// * `ny` -- is the number of points along y (must be `>= 2`)
/// * `calc_z` -- is a function of (xij, yij) that calculates zij
/// * `nx` -- number of points along x (must be `>= 2`)
/// * `ny` -- number of points along y (must be `>= 2`)
/// * `calc_z` -- function `f(x, y)` that returns `z` at each grid point
///
/// # Output
///
/// * `x`, `y`, `z` -- (`ny` by `nx`) 2D arrays
///
/// # Example
///
/// ```
/// use plotpy::generate3d;
///
/// let (x, y, z) = generate3d(-1.0, 1.0, -1.0, 1.0, 3, 3, |x, y| x * x + y * y);
/// assert_eq!(z, vec![
/// vec![2.0, 1.0, 2.0],
/// vec![1.0, 0.0, 1.0],
/// vec![2.0, 1.0, 2.0],
/// ]);
/// ```
///
/// See also: [`generate2d`]
pub fn generate3d<F>(
xmin: f64,
xmax: f64,
Expand Down
Loading
Loading