# Ezel

A good plotting library

![](/files/tPIiskUIQ2pMWjpuC1BM)

Features

* Fast GPU-accelerated rendering
* Automatic layout
* Built-in Themes
* Configurable finishes for publication: image quality, color space, anti alias

| Version | Issue                                               | Status |
| ------- | --------------------------------------------------- | ------ |
| v0.1.0  | Grid Layout                                         | ★      |
| ​       | Axis                                                | ★      |
| ​       | Cartesian 2D Plane                                  | ★      |
| ​       | Scatter and Line Plot                               | ★      |
| ​       | ​                                                   |        |
| v0.2.0  | Wgpu Renderer                                       | ★      |
| ​       | Render in a window                                  | ★      |
| ​       | ​                                                   |        |
| v0.3.0  | Interactive camera (focus, zoom, panning)           |        |
| ​       | Time Axis                                           |        |
| ​       | Configurable Axis Range                             |        |
| ​       | Legend                                              |        |
| ​       | User Scripting (Rhai, Mun, Lua) in interactive view |        |
| ​       | ​                                                   |        |
| v0.4.0  | Shared Axis                                         |        |
| ​       | Categorical Axis                                    |        |
| ​       | Twin Axis                                           |        |
| ​       | Interaction with Objects                            |        |
| ​       | ​                                                   |        |
| v0.5.0  | Grammar of Graphics Interface                       |        |


# 1. Getting Started

### Setup

```toml
[dependencies]
ezel = "*"
```

### Simple Plot

```rust
fn main() {
  let plot = ezel::Cartesian2::new();
  plot.x_axis.label = Some("X axis".to_string());
  plot.y_axis.label = Some("Y value".to_string());
  plot.scatter(Data2{});
  plot.save_to_file("example.png").unwrap();
}
```

### Multi Plot

```rust
use piet::Color;

fn main() {
  let node1 = plot_example1();
  let node2 = {
    let mut node = ezel::Grid::new();
    node.push(0, 0, plot_example2());
    node.push_side(0, 0, Side::TopLeft, ezel::PlainBox::new(Color::GREEN));
  };
  let node0 = ezel::Grid::new();
  node0.push(0, 0, node1);
  node0.push(0, 1, node2);
  node0.save_to_file("example.png").unwrap();
}
```

### GG (Grammar of Graphics) API

```rust
use ezel::gg;

fn main() {
    let df: polars::DataFrame = ..;
    let plot = gg::Plot::new()
        .data(df)
        .geometry(gg::Geometry::Line)
        .map_x("column1")
        .map_y("column2")
        .map_color("column1")
        .theme(gg::Theme::Matplotlib)
        .build();
    plot.save_to_file("example.png");
}
```


# 2. Concepts

<figure><img src="/files/ffMZS9HCclTeFnfXW9bv" alt=""><figcaption></figcaption></figure>

### Data Source

Ezel's APIs expect `polars::frame::DataFrame` as a data source.

If you have other data types, they should be converted to `DataFrame` first.

```rust
use ezel::prelude::*;
use polars::prelude::*;

let mut x = vec![..];
let mut y = vec![..];

let df = df!(
    "x" => &x,
    "y" => &y
)
.unwrap();

let mut plot = Cartesian2::default();
plot.scatter(df, Column("x".to_string()), Column("y".to_string()));
```

More ergonomic interfaces are planned to be added.

```
let x: Vec<f64>;
let y: Vec<f64>;

ezel::quick::scatter_xy(x, y); // Cartesian2 + x + y

let mut plot = Cartesian2::default();
plot.scatter_xy(x, y); // two Vec<f64>
```

### Data Types (i64, f64, ..)

`f64` are expected in most places. If you have `f32` data, simply convert it to `f64`.

### Composition

Ezel borrows the idea of protrusion from `Makie.jl`. The main areas of items in the grid are aligned by rows and columns.

### Composition

TODO

### Protrusion

TODO

### Attribute

Attributes are the properties of plots such as marker size, color, etc.

There are 3 types of attribute value: a const, categorical column, or scalar column.

```rust
marker_size: ConstOrScalar<f64>, // const or scalar
marker_shape: ConstOrCategorical<MarkerShape>, // const or categorical
marker_color: ConstOrColumn<Color>, // const or categorical or scalar
```

* `scatter.marker.size = Const(10.0)` means all markers have the same size 10.0.
* `scatter.marker.shape =` `Column("species".to_string())`assigns to each species a marker shape from the theme's shape cycle (e.g. [🟢](https://whatemoji.org/green-circle/)->[**🟩** ](https://whatemoji.org/green-square/)->[💚](https://whatemoji.org/green-heart/)-> ..).
* `scatter.marker.color = Column("column_name".to_string())`uses a color from the current color cycle ([🟢](https://whatemoji.org/green-circle/)->[🟡](https://whatemoji.org/yellow-circle/)->[🟤](https://whatemoji.org/brown-circle/)->[🔴](https://whatemoji.org/red-circle/)->..) if the column is categorical or string. Otherwise it uses a color from the current color map (![](/files/QKjRTvUsTfmbRl0le0u1))

If you want to use a f64 column in polars' DataFrame as categorical data, the easiest way is to cast it to the string dtype.

<br>


# Axis

### Types

#### Scalar Axis

Axis scales are available in scalar axis.

```
scalar_axis.scale = ...
```

* Identity Scale (default)
* Log Scale
* Exponential Scale
* Custom Scale

#### DateTime Axis

\-

#### Categorical Axis

A categorical axis provides a center point and width.

The location and size of histogram bars are determined by the axis property.

***

### Tick Locators

A tick locator determines major ticks, minor ticks, and tick labels, and "nice numbers".

#### Wilkinson

#### Talbot

An extension of Wilkinson algorithm. It is described in "An Extension of Wilkinson’s Algorithm for Positioning Tick Labels on Axes".

#### Cryptowatch

A 2-level datetime axis motivated by Cryptowatch. This design reduces the amount of label text, making a clean look without losing the required level of details.

The below is an example of typical datetime labeling. "2020-05-" is unnecessarily repeated and the label space is dense. If there were more ticks, we would have overlapping labels or would have to skip some labels.

![Matplotlib](/files/vTAbTykuSduHkm2w2JId)

On the other hand, 2-level layout has 2 rows of labels. The less important unit (2022-04-xx) is extracted out to the second level. The interval of the first level (5 hours) is determined from the current zoom state, label density, and datetime resolution.

![Cryptowatch Desktop](/files/hY2rYirgSKuUyLOENAo4)


# 3. Attributes

#### Size (Column\<f64>)

* f64 -> f64

#### Color (Column\<piet::Color>)

* \[-1.5, 3.2, ..] -> ColorMap (continuous)
* \[0, 5, 2, ..] -> user cast to categorical -> ColorCycle (categorical)
* \["a", "b", ..] -> user cast to categorical -> ColorCycle (categorical)
* \["red", "green", ..] -> arbitrary color
* None

When string, it supports css color strings with extensions.

Example of supported colors:

```
transparent
gold
rebeccapurple
lime
#0f0
#0f0f
#00ff00
#00ff00ff
rgb(0,255,0)
rgb(0% 100% 0%)
rgb(0 255 0 / 100%)
rgba(0,255,0,1)
hsl(120,100%,50%)
hsl(120deg 100% 50%)
hsl(-240 100% 50%)
hsl(-240deg 100% 50%)
hsl(0.3333turn 100% 50%)
hsl(133.333grad 100% 50%)
hsl(2.0944rad 100% 50%)
hsla(120,100%,50%,100%)
hwb(120 0% 0%)
hwb(480deg 0% 0% / 100%)
hsv(120,100%,100%)
hsv(120deg 100% 100% / 100%)
```

```
match dtype {
  Float64 => ColorMap,
  Categorical or Integer => ColorCycle,
  String => ColorName, 
}
```

#### Shape (Column\<Shape>)

* i64, string -> ShapeCycle (categorical)
* string -> shape name


# 4. Cloud

Ezel Cloud is a online place for managing and sharing your plots.

### Usage

1. Create an account and generate an API key
2. Configure the API key in the code.

```
ezel::cloud::init("xJf93j0fFJ");
```

3\. Upload your plot.

```
ezel::cloud::upload("My Plot", plot, ezel::cloud::ShareConfig { .. }).await?;
```

\[Image of browser]

4\. List and download your plots.

```d
for item in ezel::cloud::list().await? {
    let plot = item.download();
    plot.save(item.filename);
}
```

5\. See the uploads in the browser.

### Pricing

|                                 | Free    | Lite | Pro |
| ------------------------------- | ------- | ---- | --- |
|                                 | $0      | $0   | $0  |
| Storage                         |         |      |     |
| Sharing                         |         |      |     |
| Expiry                          | 30 days | -    | -   |
| Annotation, Comment, Discussion | -       | -    | O   |


# Style Customization

When plotting, ezel uses a lot of aesthetic parameters.

Even rendering a simple scatter plot requires

* marker size
* marker shape
* marker color
* background color
* x axis label size
* x axis label text
* x axis label color
* ..

For a minimal API, they are all optional and structured into `MarkerOpts`, `AxisOpts`, etc.

```rust
struct Cartesian2 {
    marker_opts: Option<MarkerOpts>,
}

let mut plot = ezel::Cartesian2::new();
plot.style.marker = MarkerOpts::default();
plot.style.marker.size = Some(10.0);
```

When unspecified, the value is provided by the theme.

### Theme

A theme is a collection of default style configurations.

You can create a new theme or use one of built-in themes.

```rust
let mut theme: Theme = ezel::theme::MATPLOTLIB.clone();
theme.marker.size = Some(12.0);  // customize the matplotlib theme
```

\[screenshot of themes]


# Data Lifetime

ezel retains user data in memory until the object is dropped.

```rust
for _ in 0..1000 {
    plot.scatter(random_million_points());
}
// plot has 1_000*1_000_000 points in memory

// layout and axis limits are determined from 1_000*1_000_000 points
plot.draw_to_file("large.png", (500, 500)).unwrap();
```

Sometimes it is desirable to save the memory by dumping the data onto the bitmap target and dropping the data immediately. ezel does not support this yet, meanwhile you can use [plotters](https://github.com/38/plotters) library at the cost of manual specification of layouts and limits.

```rust
// Plotters example
use plotters::prelude::*;

let mut cc = ChartBuilder::on(&upper)
    .margin(5)
    .set_all_label_area_size(50)  // you should manually calculate this
    .caption("Sine and Cosine", ("sans-serif", 40))
    .build_cartesian_2d(-3.4f32..3.4, -1.2f32..1.2f32)?;

cc.configure_mesh()
    .x_labels(20)  // you should manually calculate this
    .y_labels(10)  // you should manually calculate this
    .disable_mesh()
    .x_label_formatter(&|v| format!("{:.1}", v))
    .y_label_formatter(&|v| format!("{:.1}", v))
    .draw()?;
```


# What is GoG

### Other GoG Libraries

These are all well-made libraries. They slightly differ in the api naming and the usage of arithmetic operators (+, \*). For example, Altair uses + for concatenation of subplots, while Algebra of Graphics uses + for merging layers.

R

* ggplot2

Python

* Altair
* Plotnine

Julia

* [Algebra of Graphics](https://github.com/JuliaPlots/AlgebraOfGraphics.jl)


# Interface

### Layer

A primitive in gg interface is `Layer`.


# PlainBox

A rectangle useful for debugging layout


# Cartesian2

A 2D cartesian plane with xy axes - aka xy plot


# Basics

![](/files/7qhWSm01ACY01Z2g3eup)![](/files/tokVEs4GGBggFwpjq3hD)


# Geometry

A geometry that can be added to Cartesian2

###

###

### Scatter

### Line

### Histrogram

### Density

### Text

### Polygon


# Cartesian3

A 3D cartesian plane with xyz axes (xyz plot)


# Text

A multi-line text

```
Text {
    width: Some(),
    pivot: Center,
}
```

### Alignment

### Font

### Pivot


# NodeGraph

A graph with nodes and edges


# Grid

![](/files/yedaooOGWT6VMd1W85qn)![](/files/ec3LBarXjpsnCin8IUY0)

````
```rust
let mut grid = Grid::new();
grid.push(0, 0, plot1);
grid.push(0, 1, plot2);
grid.push(1, 0..2, plot3);
grid.draw_to_file("grid.png", (800, 600)).unwrap();
```
````


# Rendering

* Multi frame rendering: 1 Canvas, each frame creates Canvas
* trait **Renderable** implementors: Grid, Cartesian2D, Cartesian3D
  * fn encode(\&self, ctx: \&mut Canvas)
  * fn layout(\&self, solver: \&mut Solver, theme: \&Theme)
  * fn size(\&self) -> \&WidgetSizeVars
* struct **Canvas**<'a>
  * \&Easel
  * \&TextureView
  * area: Box2
  * layout: \&Solver
  * BindGroupLayout
  * renderers
    * MarkerRenderer
    * VanillaRenderer
    * TextRenderer
  * layout\_cache: HashMap<\*const dyn Renderable, Box2>
* struct **Easel**
  * wgpu::Instance
  * wgpu::Adapter
  * wgpu::Device
  * wgpu::Queue
  * RenderStateDetail
  * size
  * wgpu::CommandBuffer
  * wgpu::SurfaceTexture
  * layout\_cache
* Each Plot has its own PrimitiveRenderer, CommandEncoder, RenderPass instance to produce the CommandBuffer.
* CommandBuffer from each plot is collected and submitted to the queue at once.
* FontSystem is a global resource behind mutex. This is because the font loading is expensive.
* Easel consists of several sub renderers, where each has its own shader responsible for a specific type of primitives.

Grid::render\_to\_buffer (new Easel, new Canvas)\
->Grid::render\_root\
->Grid::layout()\
->Child1::layout()\
->Child2::layout()\
-> put Solver in ctx\
->Child1::render(ctx)\
->create Easel (takes one viewport, holds primitives + transform)\
->RenderPass::new()\
->pass.set\_scissor\_rect()\
->encoder.finish()\
->render\_context.push(CommandBuffer)\
->Child2::render()\
->RenderPass::new()\
->pass.set\_scissor\_rect()\
->encoder.finish()\
->return CommandBuffer\
->Queue::submit(&\[cmd\_buf1, cmd\_buf2])\
->Easel.present()

Grid::render\_svg\
->Grid::render\_root\
->Grid::layout()\
..\
->Child1::render(SVGRenderContext)\
->render\_context.push(a tree of SVG elements)\
->Child2::render()\
->return a tree of SVG elements\
->aggregate all SVG elements

Easel (1 per app)

* wgpu::Device, wgpu::Surface, ..

Canvas (1 per frame, create view on the root, states mutated down the tree)

* link to Easel
* \&view
* transform
* mouse cursor
* theme (theme = node.theme or theme)
* layout: \&Solver

PrimtiveRenderer (1 per node, 3D, scissor)

* new(view)
* LineRenderer - 1 encoder + 1 render pass -> CommandBuffer -> ctx.submit(cmd\_buf)
* TextRenderer - 1 encoder + 1 render pass -> CommandBuffer -> ctx.submit(cmd\_buf)
* finish() -> CommandBuffer
* theme

Renderable

* save() (provided)
  * create Easel, Canvas
* render\_root(ctx) (provided)
* layout
* render(ctx)
  * fn render(\&self, ctx: \&Canvas)
  * create PrimitiveRenderer
  * ctx.submit(CommandBuffer)

Cartesian2D::render()

* for geom in geometries:
  * geom.render(\&mut PrimitiveRenderer)

Cartesian2D::scatter() -> Cartesian2D::push(Geometry::Scatter{..}) -> Vec\
Cartesian2D::line() -> Cartesian2D::push(Geometry::Line{..}) -> Path

have 2 primtiive renderers:\
Primitives in data coordinates - uniform - zoom, pan, ..\
Primitives in pixel coordinates


# Coordinates

Data Coordinates

* The coordinates of user data

Axis Coordinates

* The coordiantes after AxisScale applied
* Data (1, 10, 100) -> Log10 Scale -> (0, 1, 2)

Plot Coordinates

* The local drawing location in the plot viewport
* The linear mapping between Axis Coordinates and Plot Coordinates dynamically change per zoom, pan, etc

Window Coordinates

* The global coordinates
* Translate Plot Coordinates


