Skip to content

PLplot integration

vmagnin edited this page Apr 20, 2022 · 5 revisions

Scientific plotting and gtk-fortran

Introduction

Since Fortran is primarily a scientific programming language, it is useful to have scientific data plotting tools available to draw into a GTK window created by gtk-fortran. The PLplot library can potentially provide that functionality as it has a Fortran iso_c_binding and also support for drawing to external cairo surfaces. For this reason the necessary tools for integrating PLplot with gtk-fortran are distributed as part of the gtk-fortran tree.

Requirements and installation

In the following it will be assumed that the CMake build system is being used. The source codes are from the GTK 4 branch.

For most Linux distributions, PLplot is distributed as a number of packages (the precise division depends on the distribution) however for most distributions if the -dev or -devel package and the cairo drivers are selected the other required packages will be installed as well.

The cmake scripts for gtk-fortran include a search for PLplot's libraries and Fortran module files. If these are found, then the interface module and the examples will be build (unless you explicitly prevent it by adding the -D EXCLUDE_PLPLOT=true option to cmake ..).

The interface module

Correctly configuring PLplot's external cairo driver requires the use of the low-level pl_cmd routine to connect the cairo context to the driver. The module plplot_extra provides an interface to the pl_cmd routine.

The examples

Several examples from the PLplot examples page have been adapted to work within gtk-fortran, and are included in the plplot subdirectory of gtk-fortran:

  • Example 1: Four basic x-y plots in a 2x2 layout. These are embedded in a scrolled window. (hl_plplot1e.f90)
  • Example 4: Two logarithmic x-y plots. Displayed in separate drawing areas in a notebook. (hl_plplot4e.f90)
  • Example 8: A 3-D surface plot. With controls to set the options. The window can be resized. (hl_plplot8e.f90)
  • Example 17: A constantly updating strip plot. (hl_plplot17e.f90)
  • Example 30: A demonstration of gradients and transparency. (hl_plplot30.f90)

Note that on some systems, it can be necessary to resize the window to see the plots when the surface is greater than the size of the window.

Example 8

Elements of a PLplot + gtk-fortran program

In the following section we will highlight the key features of the first PLplot example hl_plplot1e.f90.

Globals

These variables are shared by all of the other modules and the main program.

module common_ex1
  use, intrinsic :: iso_c_binding      ! Enable the c-binding routines & constants
  ! These are the gtk & glib routines used explicitly in the code:
  use gtk, only: gtk_widget_show, gtk_window_set_child, gtk_window_destroy
  ! These are the high-level drawing area modules:
  use gtk_draw_hl
  ! This makes the low-level pl_cmd routine accessible:
  use plplot_extra

  ! The size of the drawing area:
  integer(c_int) :: height, width
  ! The top-level window must be here as its destroy signal need not come from it:
  type(c_ptr) :: window
end module common_ex1

Main program

The main program creates the interface and does the initial drawing of the plots via the activate() callback function.

module handlers_ex1

...

  subroutine activate(app, gdata) bind(c)
    ! This gives the main program access to the plotting code
    use plplot_code_ex1
    use gtk, only: gtk_application_window_new, gtk_window_set_title
    implicit none
    type(c_ptr), value, intent(in)  :: app, gdata
    ! Pointers toward our GTK widgets:
    type(c_ptr) :: drawing, scroll_w, base, qbut

    ! Set the size of the drawing area (these are global variables)
    height = 1000
    width = 1200

    ! Create a top-level window and then pack a column box into it:
    window = gtk_application_window_new(app)
    call gtk_window_set_title(window, "PLplot x01 / gtk-fortran (extcairo)"//c_null_char)

    base = hl_gtk_box_new()
    call gtk_window_set_child(window, base)

    ! Create a drawing area, in a 600x500 scrolled window.
    ! The high-level drawing area creator automatically adds a cairo surface as
    ! backing store. Here we use the default expose/draw callback which
    ! just copies the backing store to the drawing surface.
    ! Pack it into the vertical box.
    drawing = hl_gtk_drawing_area_new(size=[width, height], &
         & has_alpha = FALSE, &
         & scroll = scroll_w, &
         & ssize=[ 600, 500 ])
    call hl_gtk_box_pack(base, scroll_w)

    ! Add a quit button, and pack that into the box as well:
    qbut = hl_gtk_button_new("Quit"//c_null_char, clicked=c_funloc(quit_cb))
    call hl_gtk_box_pack(base, qbut, expand=FALSE)

    ! Display the widgets:
    call gtk_widget_show(window)

    ! Call the plotting routine:
    call x01f95(drawing)
  end subroutine activate
end module handlers_ex1


program cairo_plplot_ex1
  use, intrinsic :: iso_c_binding, only: c_ptr, c_funloc, c_null_char
  ! Set the size of the drawing area (these are global variables)
  use handlers_ex1
  use gtk_hl_container, only: hl_gtk_application_new
  implicit none
  type(c_ptr) :: my_app

  ! Initalize GTK, create the GUI and launch the main loop:
  my_app = hl_gtk_application_new("gtk-fortran.plplot.hl_plplot1e"//c_null_char, &
                             & c_funloc(activate))
end program cairo_plplot_ex1

The plotting code

Here we will only discuss how to set up the plplot to access the gtk-fortran drawing surface. A discussion of how to make plots using PLplot is beyond the scope of this document. The actual plotting code is take straight from the Fortran 95 version of example 1 on the PLplot examples page.

module plplot_code_ex1
  use plplot, PI => PL_PI
  use common_ex1

  implicit none
  real(plflt) :: xscale, yscale, xoff, yoff

contains
  subroutine x01f95(area)

    type(c_ptr), intent(in) :: area
    type(c_ptr) :: cc
    character(len=80) :: version
    character(len=20) :: geometry
    integer :: digmax

    ! needed for use as functions instead of subroutines
    integer :: plparseopts_rc
    integer :: plsetopt_rc

    ! Define colour map 0 to match the "GRAFFER" colour table in
    ! place of the PLPLOT default.
    integer, parameter, dimension(16) :: &
      & rval = [255, 0, 255, 0, 0, 0, 255, 255, 255, 127, 0, 0, 127, 255, 85, 170],&
      & gval = [ 255, 0, 0, 255, 0, 255, 0, 255, 127, 255, 255, 127, 0, 0, 85, 170],&
      & bval = [ 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 127, 255, 255, 127, 85, 170]

    !  Process command-line arguments
    plparseopts_rc = plparseopts(PL_PARSE_FULL)
    if (plparseopts_rc /= 0) stop "plparseopts error"

    !  Print plplot version
    call plgver(version)
    write (*,'(a,a)') 'PLplot library version: ', trim(version)

    ! Get a cairo context from the drawing area.
    cc = hl_gtk_drawing_area_cairo_new(area)

    !  Initialize plplot
    call plscmap0(rval, gval, bval)
    call plsdev("extcairo")

    ! By default the "extcairo" driver does not reset the background
    ! This is equivalent to the command line option "-drvopt set_background=1"
    plsetopt_rc = plsetopt("drvopt", "set_background=1")
    if (plsetopt_rc /= 0) stop "plsetopt error"

    ! The "extcairo" device doesn't read the size from the context.
    write(geometry, "(I0,'x',I0)") width, height
    plsetopt_rc = plsetopt( 'geometry', geometry)
    if (plsetopt_rc /= 0) stop "plsetopt error"

    !  Divide page into 2x2 plots
    call plstar(2,2)

    ! Tell the "extcairo" driver where the context is located. This must be
    ! done AFTER the plstar or plinit call.
    call pl_cmd(PLESC_DEVINIT, cc)

    ! Actual plotting code
    .
    .
    .

    !  Don't forget to call PLEND to finish off, and then delete the
    !  cairo context.

    call plend()
    call hl_gtk_drawing_area_cairo_destroy(cc)

  end subroutine x01f95

The crucial points here are:

  • Getting a cairo context for the cairo surface used by the drawing area (hl_gtk_drawing_area_cairo_new).
  • The selection of the extcairo driver. This is the only driver that can be used to write to a gtk_drawing_area (actually there is a kludgy way to do it using the mem driver and pixbufs but it is messy and not compatible with the refresh management of the high-level drawing area).
  • Setting the driver options with plsetopt, most notably the geometry. Rather than using globals for the geometry, it would also be possible to use cairo_get_target to get the cairo context's drawing surface and then cairo_image_surface_get_width and cairo_image_surface_get_height, or gtk_widget_get_allocated_width and gtk_widget_get_allocated_height to find the geometry.
  • The call to pl_cmd: this must come after the call to plinit or plstar. This call connects the driver's output to the cairo context.
  • Destroying the cairo context after drawing (N.B. This just releases the reference obtained at the start).
  • GTK 3: if you draw after starting gtk_main you also need a call to gtk_widget_queue_draw after the plend call, see example hl_plplot8e.f90.

Callbacks

The callback routines (also sometimes called event handlers or signal handlers) specify actions to be taken when GTK signals or GDK events are emitted by widgets.

module handlers_ex1
  use common_Ex1
  use gtk_hl_container
  use gtk_hl_button
  use gtk_draw_hl
  use, intrinsic :: iso_c_binding

  implicit none
  integer(c_int) :: run_status = TRUE
  real(c_double), parameter :: pi = acos(-1.0_c_double)

contains

  subroutine quit_cb(widget, gdata) bind(c)
    type(c_ptr), value, intent(in) :: widget, gdata
    call gtk_window_destroy(window)
  end subroutine quit_cb

...

This is pretty much as simple as it gets. There is just a callback for the clicked signal from the Quit button, as in GTK 4 the destroy signal emitted when you close the window is automatically managed by the GtkApplication.

The key points to note here:

  • All callbacks must be declared with the bind(c) property so that they can be correctly called by GTK.
  • Most (all?) callback arguments are C-pointers passed by value.
  • Signal callbacks are subroutines with two arguments, the first is the widget emitting the signal and the second is user data. Some signals do have extra arguments--see the examples directory for some such cases.
  • Event callbacks are functions and have at least three arguments, the second being the event structure. The function should normally return FALSE.
  • GTK 3: if you do not provide at least a handler for the destroy signal or the delete-event event then killing the window via the window manager will cause the window to disappear but the program will not exit.
Clone this wiki locally