fortplot_matplotlib_plots.f90 Source File


Source Code

module fortplot_matplotlib_plots
    !! Matplotlib-compatible plot functions for imshow/pie/polar/step/stem/fill.
    !!
    !! Color kwargs accept either a named color string or an RGB triple via
    !! generic interfaces. `fill_between` makes `y1` required to match
    !! matplotlib's contract. `fill` accepts an optional `step` stage to
    !! mirror matplotlib's stair-fill mode.

    use iso_fortran_env, only: wp => real64
    use fortplot_figure_core, only: figure_t
    use fortplot_global, only: fig => global_figure
    use fortplot_matplotlib_color_utils, only: resolve_color_string_or_rgb
    use fortplot_matplotlib_session, only: ensure_fig_init
    use fortplot_logging, only: log_error, log_warning

    implicit none
    private

    public :: imshow, pie, polar, step, stem
    public :: fill, fill_between, twinx, twiny

    interface polar
        module procedure polar_string
        module procedure polar_rgb
    end interface polar

    interface step
        module procedure step_string
        module procedure step_rgb
    end interface step

    interface fill
        module procedure fill_string
        module procedure fill_rgb
        module procedure fill_default
    end interface fill

    interface fill_between
        module procedure fill_between_string
        module procedure fill_between_rgb
    end interface fill_between

contains

    subroutine imshow(z, cmap, alpha, vmin, vmax, origin, extent, interpolation, aspect)
        !! Display 2D array as an image (heatmap)
        real(wp), contiguous, intent(in) :: z(:,:)
        character(len=*), intent(in), optional :: cmap
        real(wp), intent(in), optional :: alpha, vmin, vmax
        character(len=*), intent(in), optional :: origin, interpolation, aspect
        real(wp), intent(in), optional :: extent(4)

        call ensure_fig_init()
        call fig%add_imshow(z, cmap=cmap, alpha=alpha, vmin=vmin, vmax=vmax, &
                            origin=origin, extent=extent, interpolation=interpolation, &
                            aspect=aspect)
    end subroutine imshow

    subroutine pie(values, labels, colors, explode, autopct, startangle)
        !! Create a pie chart
        real(wp), contiguous, intent(in) :: values(:)
        character(len=*), intent(in), optional :: labels(:)
        character(len=*), intent(in), optional :: colors(:)
        real(wp), intent(in), optional :: explode(:)
        character(len=*), intent(in), optional :: autopct
        real(wp), intent(in), optional :: startangle

        call ensure_fig_init()
        call fig%add_pie(values, labels=labels, autopct=autopct, startangle=startangle, &
                         colors=colors, explode=explode)
    end subroutine pie

    subroutine polar_string(theta, r, fmt, label, linestyle, marker, color)
        !! String-color variant of polar.
        real(wp), contiguous, intent(in) :: theta(:), r(:)
        character(len=*), intent(in), optional :: fmt, label
        character(len=*), intent(in), optional :: linestyle, marker
        character(len=*), intent(in), optional :: color

        call ensure_fig_init()
        call fig%add_polar(theta, r, label=label, fmt=fmt, linestyle=linestyle, &
                           marker=marker, color=color)
    end subroutine polar_string

    subroutine polar_rgb(theta, r, color, fmt, label, linestyle, marker)
        !! RGB-color variant of polar. Serialises the RGB triple as a hex
        !! string so the underlying implementation remains untouched.
        real(wp), contiguous, intent(in) :: theta(:), r(:)
        real(wp), intent(in) :: color(3)
        character(len=*), intent(in), optional :: fmt, label, linestyle, marker

        character(len=7) :: hex

        call ensure_fig_init()
        hex = rgb_to_hex(color)
        call fig%add_polar(theta, r, label=label, fmt=fmt, linestyle=linestyle, &
                           marker=marker, color=hex)
    end subroutine polar_rgb

    subroutine step_string(x, y, where, label, linestyle, color, linewidth)
        real(wp), contiguous, intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: where, label
        character(len=*), intent(in), optional :: linestyle
        character(len=*), intent(in), optional :: color
        real(wp), intent(in), optional :: linewidth

        call ensure_fig_init()
        call fig%add_step(x, y, label=label, where=where, linestyle=linestyle, &
                          color=color, linewidth=linewidth)
    end subroutine step_string

    subroutine step_rgb(x, y, color, where, label, linestyle, linewidth)
        real(wp), contiguous, intent(in) :: x(:), y(:)
        real(wp), intent(in) :: color(3)
        character(len=*), intent(in), optional :: where, label, linestyle
        real(wp), intent(in), optional :: linewidth

        character(len=7) :: hex

        call ensure_fig_init()
        hex = rgb_to_hex(color)
        call fig%add_step(x, y, label=label, where=where, linestyle=linestyle, &
                          color=hex, linewidth=linewidth)
    end subroutine step_rgb

    subroutine stem(x, y, linefmt, markerfmt, basefmt, label, bottom)
        !! Create a stem plot
        real(wp), contiguous, intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: linefmt, markerfmt, basefmt
        character(len=*), intent(in), optional :: label
        real(wp), intent(in), optional :: bottom

        call ensure_fig_init()
        call fig%add_stem(x, y, label=label, linefmt=linefmt, markerfmt=markerfmt, &
                          basefmt=basefmt, bottom=bottom)
    end subroutine stem

    subroutine fill_string(x, y, color, alpha, step)
        !! Fill the area between a curve and zero. `step` activates stair
        !! fill to match matplotlib's `step` argument on `fill_between`.
        real(wp), contiguous, intent(in) :: x(:), y(:)
        character(len=*), intent(in) :: color
        real(wp), intent(in), optional :: alpha
        character(len=*), intent(in), optional :: step

        real(wp), allocatable :: x_use(:), y_use(:)

        call ensure_fig_init()
        call apply_step_transform(x, y, step, x_use, y_use)
        call fig%add_fill(x_use, y_use, color=color, alpha=alpha)
    end subroutine fill_string

    subroutine fill_rgb(x, y, color, alpha, step)
        !! RGB-color variant of fill.
        real(wp), contiguous, intent(in) :: x(:), y(:)
        real(wp), intent(in) :: color(3)
        real(wp), intent(in), optional :: alpha
        character(len=*), intent(in), optional :: step

        character(len=7) :: hex
        real(wp), allocatable :: x_use(:), y_use(:)

        call ensure_fig_init()
        hex = rgb_to_hex(color)
        call apply_step_transform(x, y, step, x_use, y_use)
        call fig%add_fill(x_use, y_use, color=hex, alpha=alpha)
    end subroutine fill_rgb

    subroutine fill_default(x, y, alpha, step)
        !! `fill` called without an explicit color uses the figure palette.
        !! Kept as a dedicated overload so matplotlib-style no-color calls
        !! remain legal through the generic interface.
        real(wp), contiguous, intent(in) :: x(:), y(:)
        real(wp), intent(in), optional :: alpha
        character(len=*), intent(in), optional :: step

        real(wp), allocatable :: x_use(:), y_use(:)

        call ensure_fig_init()
        call apply_step_transform(x, y, step, x_use, y_use)
        call fig%add_fill(x_use, y_use, alpha=alpha)
    end subroutine fill_default

    subroutine fill_between_string(x, y1, y2, where, color, alpha, interpolate, step)
        !! Matplotlib-style fill_between with string color. `y1` is required
        !! (matching matplotlib); `y2` defaults to zero.
        real(wp), contiguous, intent(in) :: x(:)
        real(wp), contiguous, intent(in) :: y1(:)
        real(wp), intent(in), optional :: y2(:)
        logical, intent(in), optional :: where(:)
        character(len=*), intent(in), optional :: color
        real(wp), intent(in), optional :: alpha
        logical, intent(in), optional :: interpolate
        character(len=*), intent(in), optional :: step

        real(wp), allocatable :: x_use(:), y1_use(:), y2_use(:)
        logical, allocatable :: where_use(:)

        call ensure_fig_init()
        call apply_step_fill_between(x, y1, y2, where, step, x_use, y1_use, &
                                     y2_use, where_use)
        if (size(x_use) < 2) then
            call log_error('fill_between: need at least two points to form area')
            return
        end if

        if (allocated(where_use)) then
            call fig%add_fill_between(x_use, y1=y1_use, y2=y2_use, color=color, &
                                      alpha=alpha, where=where_use, &
                                      interpolate=interpolate)
        else
            call fig%add_fill_between(x_use, y1=y1_use, y2=y2_use, color=color, &
                                      alpha=alpha, interpolate=interpolate)
        end if
    end subroutine fill_between_string

    subroutine fill_between_rgb(x, y1, y2, where, color, alpha, interpolate, step)
        !! RGB-color variant of fill_between. Same positional layout as the
        !! string variant; `color` keyword type distinguishes the two.
        real(wp), contiguous, intent(in) :: x(:)
        real(wp), contiguous, intent(in) :: y1(:)
        real(wp), intent(in), optional :: y2(:)
        logical, intent(in), optional :: where(:)
        real(wp), intent(in) :: color(3)
        real(wp), intent(in), optional :: alpha
        logical, intent(in), optional :: interpolate
        character(len=*), intent(in), optional :: step

        character(len=7) :: hex

        call ensure_fig_init()
        hex = rgb_to_hex(color)
        call fill_between_string(x, y1=y1, color=hex, y2=y2, where=where, &
                                 alpha=alpha, interpolate=interpolate, step=step)
    end subroutine fill_between_rgb

    subroutine apply_step_transform(x, y, step, x_out, y_out)
        !! Generate a stair-cased (x, y) sequence for fill/fill_between.
        !!
        !! `pre`: each y value anchors the interval to its left.
        !! `post`: each y value anchors the interval to its right.
        !! `mid`: transitions happen at the midpoint between samples.
        real(wp), contiguous, intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: step
        real(wp), allocatable, intent(out) :: x_out(:), y_out(:)

        integer :: n, i, m

        if (.not. present(step)) then
            allocate (x_out(size(x)), y_out(size(y)))
            x_out = x
            y_out = y
            return
        end if

        n = size(x)
        select case (trim(step))
        case ('pre')
            m = 2*n - 1
            allocate (x_out(m), y_out(m))
            do i = 1, n - 1
                x_out(2*i - 1) = x(i)
                y_out(2*i - 1) = y(i)
                x_out(2*i) = x(i + 1)
                y_out(2*i) = y(i)
            end do
            x_out(m) = x(n)
            y_out(m) = y(n)
        case ('post')
            m = 2*n - 1
            allocate (x_out(m), y_out(m))
            x_out(1) = x(1)
            y_out(1) = y(1)
            do i = 2, n
                x_out(2*i - 2) = x(i)
                y_out(2*i - 2) = y(i - 1)
                x_out(2*i - 1) = x(i)
                y_out(2*i - 1) = y(i)
            end do
        case ('mid')
            m = 2*n
            allocate (x_out(m), y_out(m))
            do i = 1, n - 1
                x_out(2*i - 1) = x(i)
                y_out(2*i - 1) = y(i)
                x_out(2*i) = 0.5_wp*(x(i) + x(i + 1))
                y_out(2*i) = y(i)
            end do
            x_out(m - 1) = x(n)
            y_out(m - 1) = y(n - 1)
            x_out(m) = x(n)
            y_out(m) = y(n)
        case default
            call log_warning('fill: unsupported step value ' // trim(step))
            allocate (x_out(size(x)), y_out(size(y)))
            x_out = x
            y_out = y
        end select
    end subroutine apply_step_transform

    subroutine apply_step_fill_between(x, y1, y2, where, step, x_out, y1_out, &
                                       y2_out, where_out)
        !! Apply stair-casing to x, y1, y2, and optional mask for
        !! `fill_between(step=...)`. Mirrors `apply_step_transform` on the
        !! pair of curves and preserves boolean mask alignment.
        real(wp), contiguous, intent(in) :: x(:), y1(:)
        real(wp), intent(in), optional :: y2(:)
        logical, intent(in), optional :: where(:)
        character(len=*), intent(in), optional :: step
        real(wp), allocatable, intent(out) :: x_out(:), y1_out(:), y2_out(:)
        logical, allocatable, intent(out) :: where_out(:)

        real(wp), allocatable :: y2_work(:)
        logical, allocatable :: where_work(:)
        real(wp), allocatable :: x_stepped(:), y1_stepped(:), y2_stepped(:)
        logical, allocatable :: where_stepped(:)
        integer :: n

        n = size(x)
        allocate (y2_work(n))
        if (present(y2)) then
            if (size(y2) /= n) then
                call log_error('fill_between: y2 size mismatch')
                return
            end if
            y2_work = y2
        else
            y2_work = 0.0_wp
        end if

        if (present(where)) then
            if (size(where) /= n) then
                call log_error('fill_between: where mask size mismatch')
                return
            end if
            allocate (where_work(n), source=where)
        end if

        if (.not. present(step)) then
            allocate (x_out(n), y1_out(n), y2_out(n))
            x_out = x
            y1_out = y1
            y2_out = y2_work
            if (allocated(where_work)) then
                allocate (where_out(n), source=where_work)
            end if
            return
        end if

        call step_pair(x, y1, y2_work, where_work, trim(step), x_stepped, &
                       y1_stepped, y2_stepped, where_stepped)
        call move_alloc(x_stepped, x_out)
        call move_alloc(y1_stepped, y1_out)
        call move_alloc(y2_stepped, y2_out)
        if (allocated(where_stepped)) then
            call move_alloc(where_stepped, where_out)
        end if
    end subroutine apply_step_fill_between

    subroutine step_pair(x, y1, y2, where_in, mode, x_out, y1_out, y2_out, &
                         where_out)
        !! Stair-case both curves and the optional mask in lockstep.
        real(wp), contiguous, intent(in) :: x(:), y1(:), y2(:)
        logical, intent(in), allocatable :: where_in(:)
        character(len=*), intent(in) :: mode
        real(wp), allocatable, intent(out) :: x_out(:), y1_out(:), y2_out(:)
        logical, allocatable, intent(out) :: where_out(:)

        integer :: n, i, m
        logical :: has_mask

        n = size(x)
        has_mask = allocated(where_in)

        select case (mode)
        case ('pre')
            m = 2*n - 1
            call allocate_step_buffers(m, has_mask, x_out, y1_out, y2_out, where_out)
            do i = 1, n - 1
                x_out(2*i - 1) = x(i)
                y1_out(2*i - 1) = y1(i)
                y2_out(2*i - 1) = y2(i)
                x_out(2*i) = x(i + 1)
                y1_out(2*i) = y1(i)
                y2_out(2*i) = y2(i)
                if (has_mask) then
                    where_out(2*i - 1) = where_in(i)
                    where_out(2*i) = where_in(i)
                end if
            end do
            x_out(m) = x(n)
            y1_out(m) = y1(n)
            y2_out(m) = y2(n)
            if (has_mask) where_out(m) = where_in(n)

        case ('post')
            m = 2*n - 1
            call allocate_step_buffers(m, has_mask, x_out, y1_out, y2_out, where_out)
            x_out(1) = x(1)
            y1_out(1) = y1(1)
            y2_out(1) = y2(1)
            if (has_mask) where_out(1) = where_in(1)
            do i = 2, n
                x_out(2*i - 2) = x(i)
                y1_out(2*i - 2) = y1(i - 1)
                y2_out(2*i - 2) = y2(i - 1)
                x_out(2*i - 1) = x(i)
                y1_out(2*i - 1) = y1(i)
                y2_out(2*i - 1) = y2(i)
                if (has_mask) then
                    where_out(2*i - 2) = where_in(i - 1)
                    where_out(2*i - 1) = where_in(i)
                end if
            end do

        case ('mid')
            m = 2*n
            call allocate_step_buffers(m, has_mask, x_out, y1_out, y2_out, where_out)
            do i = 1, n - 1
                x_out(2*i - 1) = x(i)
                y1_out(2*i - 1) = y1(i)
                y2_out(2*i - 1) = y2(i)
                x_out(2*i) = 0.5_wp*(x(i) + x(i + 1))
                y1_out(2*i) = y1(i)
                y2_out(2*i) = y2(i)
                if (has_mask) then
                    where_out(2*i - 1) = where_in(i)
                    where_out(2*i) = where_in(i)
                end if
            end do
            x_out(m - 1) = x(n)
            y1_out(m - 1) = y1(n - 1)
            y2_out(m - 1) = y2(n - 1)
            x_out(m) = x(n)
            y1_out(m) = y1(n)
            y2_out(m) = y2(n)
            if (has_mask) then
                where_out(m - 1) = where_in(n)
                where_out(m) = where_in(n)
            end if

        case default
            call log_warning('fill_between: unsupported step value ' // mode)
            allocate (x_out(n), y1_out(n), y2_out(n))
            x_out = x
            y1_out = y1
            y2_out = y2
            if (has_mask) allocate (where_out(n), source=where_in)
        end select
    end subroutine step_pair

    subroutine allocate_step_buffers(m, has_mask, x_out, y1_out, y2_out, where_out)
        integer, intent(in) :: m
        logical, intent(in) :: has_mask
        real(wp), allocatable, intent(out) :: x_out(:), y1_out(:), y2_out(:)
        logical, allocatable, intent(out) :: where_out(:)

        allocate (x_out(m), y1_out(m), y2_out(m))
        if (has_mask) allocate (where_out(m))
    end subroutine allocate_step_buffers

    pure function rgb_to_hex(color) result(hex)
        !! Serialise an RGB triple as a matplotlib-style `#RRGGBB` hex
        !! string so the underlying string-only implementations can consume
        !! it without additional overloads. Components are clipped to [0,1].
        real(wp), intent(in) :: color(3)
        character(len=7) :: hex

        integer :: rgb(3), i
        real(wp) :: clipped

        do i = 1, 3
            clipped = max(0.0_wp, min(1.0_wp, color(i)))
            rgb(i) = nint(clipped*255.0_wp)
        end do
        write (hex, '("#",3(Z2.2))') rgb(1), rgb(2), rgb(3)
    end function rgb_to_hex

    subroutine twinx()
        !! Activate a secondary y-axis that shares the x-axis but renders on the right
        call ensure_fig_init()
        call fig%twinx()
    end subroutine twinx

    subroutine twiny()
        !! Activate a secondary x-axis that shares the y-axis but renders on the top
        call ensure_fig_init()
        call fig%twiny()
    end subroutine twiny

end module fortplot_matplotlib_plots