#plot #python #json-file #script-file #api-bindings #graphics

matplotlib

使用Python和Matplotlib在Rust中进行快速简略绘图

1个不稳定版本

0.1.0 2024年8月1日

#55 in 可视化

Download history 111/week @ 2024-07-31 4/week @ 2024-08-07

每月115次下载

AGPL-3.0-only

185KB
4.5K SLoC

Mpl

使用Python和Matplotlib在Rust中进行快速简略绘图,深受Haskell包matplotlib的启发。

目的

这个crate和matplotlib都通过生成一个临时Python源文件,并简单地调用系统的Python解释器,内部使用现有的Matplotlib安装。这种方法提供了一些优势。最重要的是,它使用更熟悉/方便的结构来分离绘图命令周围的逻辑和数据与最终绘制数据的画布,从而使得代码更加模块化。matplotlib提供了一种优雅的模型来单调地组合绘图命令,这个crate试图模仿它。

然而,这个crate和matplotlib都不是安全的库。特别是,两者都允许从裸字符串数据注入任意Python代码。这提供了很大的灵活性,但当然也使得大量操作对编译器不透明。因此,用户被警告不要在复杂程序中使用这个crate。相反,这个库针对的是只需要快速生成绘图的简单程序。

你应该使用这个库,如果你

  • 想要一个简单的方法将一些数据放入漂亮的图表中
  • 并且/或者熟悉Matplotlib,但不想直接使用Python

你不应该使用这个库,如果你

  • 想要保证输出Python代码的有效性
  • 想要处理由Python生成的错误更加稳健

你可能还对以下内容感兴趣

  • Plotpy,一个具有类似策略和更安全结构但更冗长构建模式且灵活性较低的策略库。
  • Plotters,一个纯Rust绘图库,可以完全控制图形上的所有内容。

它是如何工作的

库的主要两个组件是Mpl类型,代表绘图脚本,以及Matplotlib特性,代表脚本的一个元素。一个特定的Mpl对象可以与任何数量的实现了Matplotlib类型的对象组合,这为用户定义自己的绘图元素提供了很大的灵活性。当准备执行时,可以通过调用Mpl对象的run方法来将脚本的输出保存到文件,启动Matplotlib的交互式Qt界面,或者两者都做。上述操作也已通过Rust的&|运算符重载,以便仅仅为了乐趣来模拟matplotlib的API。

当执行Mpl::run时,与绘图命令关联的任何较大的数据结构(主要是数值数组)都会被序列化为JSON。这些数据以及绘图脚本本身,都会被写入操作系统的默认临时目录(例如Linux上的/tmp),然后使用std::process::Command调用系统的默认python3解释器来执行脚本,这将阻塞调用线程。显然,需要现有的Python 3和Matplotlib安装。脚本退出后,脚本和JSON文件都会被删除。

尽管许多常见的绘图命令都定义在mpl::commands中,但用户可以通过简单地实现Matplotlib来定义自己的命令。这需要声明命令是否应计为脚本的前置部分(在这种情况下,它会被自动排序到脚本顶部),应该包含哪些数据在JSON文件中,以及脚本最终应包含哪些Python代码。这个库不会验证任何Python代码。用户还可以实现MatplotlibOpts来添加可选的关键字参数。

use mpl::{
    Matplotlib,
    MatplotlibOpts,
    Opt,
    PyValue,
    AsPy,
    serde_json::Value,
};

// example impl for a basic call to `plot`

#[derive(Clone, Debug)]
struct Plot {
    x: Vec<f64>,
    y: Vec<f64>,
    opts: Vec<Opt>, // optional keyword arguments
}

impl Plot {
    /// Create a new `Plot` with no options.
    fn new<X, Y>(x: X, y: Y) -> Self
    where
        X: IntoIterator<Item = f64>,
        Y: IntoIterator<Item = f64>,
    {
        Self {
            x: x.into_iter().collect(),
            y: y.into_iter().collect(),
            opts: Vec::new(),
        }
    }
}

impl Matplotlib for Plot {
    // Commands with `is_prelude == true` are run first
    fn is_prelude(&self) -> bool { false }

    fn data(&self) -> Option<Value> {
        let x: Vec<Value> = self.x.iter().copied().map(Value::from).collect();
        let y: Vec<Value> = self.y.iter().copied().map(Value::from).collect();
        Some(Value::Array(vec![x.into(), y.into()]))
    }

    fn py_cmd(&self) -> String {
        // JSON data is guaranteed to be loaded in a variable called `data`
        format!("ax.plot(data[0], data[1], {})", self.opts.as_py())
    }
}

// allow for keyword arguments to be added
impl MatplotlibOpts for Plot {
    fn kwarg<T>(&mut self, key: &str, val: T) -> &mut Self
    where T: Into<PyValue>
    {
        self.opts.push((key, val).into());
        self
    }
}

示例

use std::f64::consts::TAU;
use mpl::{ Mpl, Run, MatplotlibOpts, commands as c };

let dx: f64 = TAU / 50.0;
let x: Vec<f64> = (0..50_u32).map(|k| f64::from(k) * dx).collect();
let y1: Vec<f64> = x.iter().copied().map(f64::sin).collect();
let y2: Vec<f64> = x.iter().copied().map(f64::cos).collect();

Mpl::new()
    & c::DefPrelude // a bunch of imports
    & c::rcparam("axes.grid", true) // global rc parameters
    & c::rcparam("axes.linewidth", 0.65)
    & c::rcparam("lines.linewidth", 0.8)
    & c::DefInit // fig, ax = plt.subplots()
    & c::plot(x.clone(), y1) // the basic plotting command
        .o("marker", "o") // pass optional keyword arguments
        .o("color", "b")  // via `MatplotlibOpts`
        .o("label", r"$\\sin(x)$")
    & c::plot(x,         y2) // `&` is overloaded to allow for Haskell-like
        .o("marker", "D")    // patterns, can also use `Mpl::then`
        .o("color", "r")
        .o("label", r"$\\cos(x)$")
    & c::legend()
    & c::xlabel("$x$")
    | Run::Show // `|` consumes the final `Mpl` value; this calls
                // `pyplot.show` to launch an interactive interface

依赖项

~1-2MB
~40K SLoC