AnonMiraj / fig

FIG (Fortran Intuitive Graphics)
MIT License
20 stars 2 forks source link

Design of a plotting library #10

Open certik opened 1 month ago

certik commented 1 month ago

It would be interesting to create a plotting library frontend. For that we should learn from Matplotlib, as well as the C++ version of it: https://github.com/alandefreitas/matplotplusplus. We need to figure out some good intermediate representation for vector objects that represent the plot.

For the backends some ideas are:

The last two backends can use the algorithms from this library.

CC @everythingfunctional, @perazz.

everythingfunctional commented 1 month ago

Here's my initial thoughts on the current design and where I think it ought to go.

The current "API" does not really assume bitmap as the underlying representation, which is good. The current implementation really does kind of assume bitmap as the underlying representation, which will mean some significant refactoring, which I think to a degree will motivate some adjustments to the API.

Also, the current API is very procedural. Create a canvas, draw a thing on it, draw another thing on it, have it output to somewhere. I'm not suggesting that's necessarily the wrong approach, but I wonder if there is a more declarative style that could be explored for the API.

I certainly agree that we should have some sort of intermediate representation that at least resembles vector graphics. Now here comes the question in terms of design. How do the API, intermediate representation, canvas, and backends interact with eachother. Some options.

A canvas is an abstract type, that supports the user API, and backends are implementations of that type. The intermediate representation is then a shared implementation detail of different backends. This has the benefit that different implementations can be optimized quite easily, but at the cost of maintainability, because adding to the API requires that all backends be immediately updated to account for the new feature.

A canvas has a backend, that it interacts with immediately when calls to the API are made. This somewhat decouples the backends from the API, but likely couples them more directly to the intermediate representation.

A canvas just translates API calls into intermediate representation, which is later processed by a backend. This has the benefit of fully decoupling the backends from the API, and potentially decoupling (to a degree) from the intermediate representation. The question then becomes does the canvas make calls to the backend, or vice versa. This has the effect that now the user has full control over when things are actually sent to some output.

I don't have strong preferences for which option to choose, but wanted to give some food for thought.

perazz commented 1 month ago

It's a great start @AnonMiraj. I would like to add some practical comments to complement the design suggestions.

type, abstract :: canvas
   character(:), allocatable :: title
   integer(coordinate) :: width, height
   type(background) :: bg
   type(line), allocatable :: lines(:)
   type(circle), allocatable :: circles(:)
   ...
end type canvas

type, extends(canvas) :: bmp_canvas
   type(point), allocatable :: pixels(:,:)
   contains
      procedure :: to_file  
end type bmp_canvas

This way you keep the primitives separate from the implementation.

use iso_fortran_env, only: int16, 

integer, parameter :: pixel = int16
integer, parameter :: rgb_level = int8
integer, parameter :: coord = real32

module fig_vec2
use fig_constants

type, public :: vec2
   real(coord) :: x,y
end type vec2
...
end module fig_vec2

module fig_points
use fig_constants

type, public :: point
   integer(pixel) :: x,y
end type point
...
AnonMiraj commented 1 month ago

It would be interesting to create a plotting library frontend. For that we should learn from Matplotlib, as well as the C++ version of it: https://github.com/alandefreitas/matplotplusplus. We need to figure out some good intermediate representation for vector objects that represent the plot.

For the backends some ideas are:

* the glfw library could be used (it seems it is in conda).

* SVG (pure Fortran, just emit the xml as a string)

* PNG (use a C library to actually save it, or stdlib)

* PPM (use stdlib, pure Fortran)

The last two backends can use the algorithms from this library.

CC @everythingfunctional, @perazz.

Yeah, I agree, a plotting library is certainly going to be really cool. My current plan is to leave the first 6 weeks of my GSoC period mostly for creating the API for the library and the back-end, and then spend the last 6 weeks just working on the plotting side.

AnonMiraj commented 1 month ago

Here's my initial thoughts on the current design and where I think it ought to go.

The current "API" does not really assume bitmap as the underlying representation, which is good. The current implementation really does kind of assume bitmap as the underlying representation, which will mean some significant refactoring, which I think to a degree will motivate some adjustments to the API.

Initially, I wasn't planning to support vector graphics. That's why, for the current new design, there will need to be a lot of refactoring to support it.

Also, the current API is very procedural. Create a canvas, draw a thing on it, draw another thing on it, have it output to somewhere. I'm not suggesting that's necessarily the wrong approach, but I wonder if there is a more declarative style that could be explored for the API.

While I agree that a declarative approach is the right choice if the library were just for SVG or a plotting library, I'm not very keen on making this the case for FIG. It would be quite limiting for a general-use graphics library to be declarative. A procedural approach is going to give users more control and will be more fitting for my current vision of the library. It is possible in the future to use FIG to create a declarative API for graphics, but I don't think a declarative approach is the best option for the library right now—at least, that's my opinion. We may discuss this further in the next meeting.

However, I do plan to make the plotting library declarative. It is going to be more usable, flexible, and intuitive overall like this.

I certainly agree that we should have some sort of intermediate representation that at least resembles vector graphics. Now here comes the question in terms of design. How do the API, intermediate representation, canvas, and backends interact with eachother. Some options.

A canvas is an abstract type, that supports the user API, and backends are implementations of that type. The intermediate representation is then a shared implementation detail of different backends. This has the benefit that different implementations can be optimized quite easily, but at the cost of maintainability, because adding to the API requires that all backends be immediately updated to account for the new feature.

A canvas has a backend, that it interacts with immediately when calls to the API are made. This somewhat decouples the backends from the API, but likely couples them more directly to the intermediate representation.

A canvas just translates API calls into intermediate representation, which is later processed by a backend. This has the benefit of fully decoupling the backends from the API, and potentially decoupling (to a degree) from the intermediate representation. The question then becomes does the canvas make calls to the backend, or vice versa. This has the effect that now the user has full control over when things are actually sent to some output.

I don't have strong preferences for which option to choose, but wanted to give some food for thought.

I am more lenient towards an abstract canvas with different backends, but other options have a lot of merit too.

AnonMiraj commented 1 month ago

@perazz thanks for all the suggestions i will keep them in mind when i start to refactor the backend.

johandweber commented 1 month ago

Also, the current API is very procedural. Create a canvas, draw a thing on it, draw another thing on it, have it output to somewhere. I'm not suggesting that's necessarily the wrong approach, but I wonder if there is a more declarative style that could be explored for the API.

In my (not yet very well thought) opinion, having the same canvas as an argument may indicate which operations belong to the same "object" (which may or may not translate to an derived type in Fortran).

johandweber commented 1 month ago

chart

johandweber commented 1 month ago

As a (hopefully useful) basis for discussion, I have drawn a schematic for a possible architecture.

I do not claim that this is necessarily the best option.

The color blue means that I am really unsure about it.

Of course not everything can be done at once and it might be important to prioritize parts of the scheme.