Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Design of a plotting library #10

Open
certik opened this issue May 10, 2024 · 8 comments
Open

Design of a plotting library #10

certik opened this issue May 10, 2024 · 8 comments

Comments

@certik
Copy link

certik commented May 10, 2024

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.

@everythingfunctional
Copy link

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
Copy link

perazz commented May 10, 2024

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

  • Your type(canvas) implements pixels and could be a good starting point for PNG or BMP, but it's better to keep the primitives separated from the implementation (pixel-by-pixel write). So for example:
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.

  • you'll need a module for the library parameters. for example: what integer size do you plan to use for the pixels? I see it something like:
use iso_fortran_env, only: int16, 

integer, parameter :: pixel = int16
integer, parameter :: rgb_level = int8
integer, parameter :: coord = real32
  • you already started base classes (great!). However, vec2 and vec3 are typically reserved for geometric entities. I suggest you create new modules for each of them, but separate the pixel representation from the geometric representation:
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
...
  • Great source of inspiration: 1 2

@AnonMiraj
Copy link
Owner

AnonMiraj commented May 10, 2024

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.

  • For GLFW, I have already considered an approach that will render the graphics in a simple way, making GLFW an optional dependency for the library, while leaving most of the code base in pure Fortran. It will also use just a small subset of GLFW methods and functions, so I will not need to make bindings for the entire GLFW library just for the project. There are also alternatives to GLFW like SDL (someone already made bindings for SDL2) or something like RGFW (https://github.com/ColleagueRiley/RGFW), which is a relatively new project but is just one header file and provides almost all GLFW functionality.
  • Regarding SVG, I have decided to try supporting it. It will not be easy to maintain both raster and vector graphics, but it is possible and i will do my best.
  • For PNG, I have already discussed how to support raster file drivers on this issue support for image file drivers #8. I have decided to make bindings for stb_image and stb_image_write and use them to write to PNG and BMP, but I am a bit confused by stdlib. Is there a way to write PNG or a deflating library made in Fortran?
  • As for PPM, it already exists in the current implementation.

@AnonMiraj
Copy link
Owner

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
Copy link
Owner

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

@johandweber
Copy link
Contributor

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
Copy link
Contributor

chart

@johandweber
Copy link
Contributor

johandweber commented May 11, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants