fortplot_matplotlib_io.f90 Source File


Source Code

module fortplot_matplotlib_io
    !! File I/O and display operations for matplotlib-compatible API
    !! Contains figure management, saving, and showing functions
    
    use iso_fortran_env, only: wp => real64
    use fortplot_figure_core, only: figure_t
    use fortplot_global, only: fig => global_figure
    use fortplot_logging, only: log_error, log_warning, log_info
    use fortplot_security, only: safe_launch_viewer, safe_remove_file
    
    implicit none
    private
    
    ! Export I/O and display functions
    public :: figure, subplot
    public :: savefig, savefig_with_status
    public :: show, show_viewer
    public :: ensure_global_figure_initialized
    public :: get_global_figure
    
    ! Overloaded show interface
    interface show
        module procedure show_data, show_figure
    end interface show
    
contains

    subroutine ensure_global_figure_initialized()
        !! Ensure global figure is initialized before use (matplotlib compatibility)
        !! Auto-initializes with default dimensions if not already initialized
        if (.not. allocated(fig)) then
            allocate(figure_t :: fig)
        end if
        if (.not. fig%backend_associated()) then
            call fig%initialize()
        end if
    end subroutine ensure_global_figure_initialized
    
    function get_global_figure() result(global_fig)
        !! Get reference to the global figure for testing access to arrow data
        !! This allows tests to access fig%arrow_data without making fig public
        class(figure_t), pointer :: global_fig
        call ensure_global_figure_initialized()
        global_fig => fig
    end function get_global_figure

    subroutine figure(num, figsize, dpi)
        !! Create or activate a figure with optional size and DPI settings
        integer, intent(in), optional :: num
        real(8), dimension(2), intent(in), optional :: figsize
        integer, intent(in), optional :: dpi
        
        integer :: fig_num, fig_dpi
        real(8), dimension(2) :: size
        character(len=256) :: msg
        
        ! Set defaults
        fig_num = 1
        if (present(num)) fig_num = num
        
        size = [8.0d0, 6.0d0]  ! Default figure size in inches
        if (present(figsize)) size = figsize
        
        fig_dpi = 100
        if (present(dpi)) fig_dpi = dpi
        
        ! Validate inputs
        if (size(1) <= 0.0d0 .or. size(2) <= 0.0d0) then
            call log_error("figure: Invalid figure size")
            return
        end if
        
        if (fig_dpi <= 0) then
            call log_error("figure: Invalid DPI value")
            return
        end if
        
        ! Log figure creation
        write(msg, '(A,I0,A,F6.2,A,F6.2,A,I0,A)') &
            "Creating figure ", fig_num, " with size ", size(1), "x", size(2), &
            " inches at ", fig_dpi, " DPI"
        call log_info(trim(msg))
        
        ! Re-initialize global figure with new settings
        if (allocated(fig)) then
            deallocate(fig)
        end if
        
        allocate(figure_t :: fig)
        call fig%initialize(width=nint(size(1)*fig_dpi), &
                          height=nint(size(2)*fig_dpi))
    end subroutine figure

    subroutine subplot(nrows, ncols, index)
        !! Create subplot arrangement (simplified for single axis)
        !! Currently implements single plot behavior
        integer, intent(in) :: nrows, ncols, index
        
        character(len=256) :: msg
        
        call ensure_global_figure_initialized()
        
        ! Validate inputs
        if (nrows <= 0 .or. ncols <= 0) then
            call log_error("subplot: Invalid grid dimensions")
            return
        end if
        
        if (index <= 0 .or. index > nrows*ncols) then
            call log_error("subplot: Invalid subplot index")
            return
        end if
        
        ! Log subplot creation
        write(msg, '(A,I0,A,I0,A,I0,A)') &
            "Creating subplot ", index, " in ", nrows, "x", ncols, " grid"
        call log_info(trim(msg))
        
        ! Note: Current implementation doesn't support multiple subplots
        ! This is a stub for API compatibility
        if (index > 1) then
            call log_warning("subplot: Multiple subplots not yet implemented")
        end if
    end subroutine subplot

    subroutine savefig(filename, dpi, transparent, bbox_inches)
        !! Save the global figure to a file (pyplot-style)
        character(len=*), intent(in) :: filename
        integer, intent(in), optional :: dpi
        logical, intent(in), optional :: transparent
        character(len=*), intent(in), optional :: bbox_inches
        
        call ensure_global_figure_initialized()
        call fig%savefig(filename)
    end subroutine savefig

    subroutine savefig_with_status(filename, status, dpi, transparent, bbox_inches)
        !! Save figure with status return (error handling version)
        character(len=*), intent(in) :: filename
        integer, intent(out) :: status
        integer, intent(in), optional :: dpi
        logical, intent(in), optional :: transparent
        character(len=*), intent(in), optional :: bbox_inches
        
        call ensure_global_figure_initialized()
        call fig%savefig_with_status(filename, status)
    end subroutine savefig_with_status

    subroutine show_data(x, y, label, title_text, xlabel_text, ylabel_text, blocking)
        !! Show data as a simple plot with optional labels
        real(8), dimension(:), intent(in) :: x, y
        character(len=*), intent(in), optional :: label, title_text
        character(len=*), intent(in), optional :: xlabel_text, ylabel_text
        logical, intent(in), optional :: blocking
        
        call ensure_global_figure_initialized()
        
        ! Add the plot
        call fig%add_plot(x, y, label=label)
        
        ! Set labels if provided
        if (present(title_text)) call fig%set_title(title_text)
        if (present(xlabel_text)) call fig%set_xlabel(xlabel_text)
        if (present(ylabel_text)) call fig%set_ylabel(ylabel_text)
        
        ! Show the figure
        call fig%show(blocking=blocking)
    end subroutine show_data

    subroutine show_figure(blocking)
        !! Display the global figure (pyplot-style)
        logical, intent(in), optional :: blocking
        
        call ensure_global_figure_initialized()
        call fig%show(blocking=blocking)
    end subroutine show_figure

    subroutine show_viewer(blocking)
        !! Display figure using system viewer (cross-platform)
        logical, intent(in), optional :: blocking
        
        call ensure_global_figure_initialized()
        call show_viewer_implementation(blocking)
    end subroutine show_viewer

    function is_gui_available() result(gui_available)
        !! Check if GUI display is available
        logical :: gui_available
        character(len=256) :: display_var
        integer :: status
        
        gui_available = .false.
        
        ! Check for display environment
        call get_environment_variable("DISPLAY", display_var, status=status)
        if (status == 0 .and. len_trim(display_var) > 0) then
            gui_available = .true.
        end if
        
        ! Check for SSH without X forwarding
        call get_environment_variable("SSH_CLIENT", display_var, status=status)
        if (status == 0 .and. .not. gui_available) then
            call log_warning("SSH session detected without X forwarding")
        end if
    end function is_gui_available

    subroutine show_viewer_implementation(blocking)
        !! Internal implementation of viewer display
        logical, intent(in), optional :: blocking
        
        character(len=512) :: temp_file
        logical :: is_blocking, success
        integer :: status, unit
        real :: start_time, current_time
        
        is_blocking = .false.
        if (present(blocking)) is_blocking = blocking
        
        ! Check GUI availability
        if (.not. is_gui_available()) then
            call log_warning("No GUI available, saving to show_output.png instead")
            call fig%savefig("show_output.png")
            return
        end if
        
        ! Create temporary file
        call get_environment_variable("TMPDIR", temp_file, status=status)
        if (status /= 0) temp_file = "/tmp"
        
        write(temp_file, '(A,A,I0,A)') trim(temp_file), "/fortplot_", &
                                       int(rand(0)*1000000), ".png"
        
        ! Save figure to temporary file
        call fig%savefig_with_status(trim(temp_file), status)
        if (status /= 0) then
            call log_error("Failed to save figure for viewing")
            return
        end if
        
        ! Launch viewer
        call safe_launch_viewer(trim(temp_file), success)
        
        if (.not. success) then
            call log_error("Failed to launch image viewer")
            call safe_remove_file(trim(temp_file), success)
            return
        end if
        
        ! Handle blocking mode
        if (is_blocking) then
            call log_info("Viewer launched in blocking mode. Close viewer to continue.")
            ! Wait for viewer to close
            call cpu_time(start_time)
            do
                call cpu_time(current_time)
                if (current_time - start_time > 30.0) exit  ! 30 second timeout
                call sleep_fortran(100)  ! Sleep 100ms
            end do
        else
            ! Give viewer time to load file before deletion
            call sleep_fortran(1000)  ! Sleep 1000ms (1 second)
        end if
        
        ! Clean up temporary file
        call safe_remove_file(trim(temp_file), success)
    end subroutine show_viewer_implementation

    subroutine sleep_fortran(milliseconds)
        !! Simple sleep implementation using Fortran intrinsic
        integer, intent(in) :: milliseconds
        real :: seconds
        integer :: start_count, end_count, count_rate, target_count
        
        ! Convert milliseconds to seconds for system_clock
        seconds = real(milliseconds) / 1000.0
        
        ! Use system_clock for precise timing
        call system_clock(start_count, count_rate)
        target_count = int(seconds * real(count_rate))
        
        do
            call system_clock(end_count)
            if (end_count - start_count >= target_count) exit
        end do
    end subroutine sleep_fortran

end module fortplot_matplotlib_io