module fortplot_matplotlib_session !! Global figure session helpers shared across matplotlib-compatible wrappers !! !! Provides lifecycle management for the singleton figure used by the !! matplotlib facade. Responsibilities include creating and reusing the !! global figure, grid/subplot helpers, and viewer integration utilities. use, intrinsic :: iso_fortran_env, only: wp => real64 use fortplot_constants, only: REFERENCE_DPI use fortplot_figure_core, only: figure_t use fortplot_figure_initialization, only: figure_state_t use fortplot_figure_configuration, only: configure_figure_dimensions, setup_figure_backend use fortplot_legend, only: legend_t use fortplot_global, only: fig => global_figure use fortplot_logging, only: log_error, log_warning, log_info use fortplot_system_runtime, only: delete_file_runtime use fortplot_system_viewer, only: launch_system_viewer use fortplot_png, only: png_context use fortplot_pdf, only: pdf_context use fortplot_ascii, only: ascii_context implicit none private public :: ensure_fig_init public :: ensure_global_figure_initialized public :: get_global_figure public :: figure public :: subplot public :: subplots public :: subplots_grid public :: savefig public :: savefig_with_status public :: show_data public :: show_figure public :: show_viewer public :: ion, ioff, draw, pause logical, save :: interactive_mode = .false. contains subroutine ion() !! Enable interactive mode for live terminal visualization !! !! When interactive mode is enabled, plots are displayed in the terminal !! using ASCII art and can be updated in real-time using draw() or pause(). interactive_mode = .true. end subroutine ion subroutine ioff() !! Disable interactive mode interactive_mode = .false. end subroutine ioff subroutine draw() !! Redraw the current figure to terminal (ASCII output) !! !! Clears the terminal and displays the current plot state. !! Useful for live visualization in loops. call ensure_global_figure_initialized() call execute_command_line("clear", wait=.true.) call fig%savefig("terminal") end subroutine draw subroutine pause(seconds) !! Draw the current figure and pause for specified duration !! !! This is the primary function for live visualization. It: !! 1. Clears the terminal !! 2. Displays the current plot as ASCII art !! 3. Waits for the specified number of seconds !! !! Parameters !! seconds : real(wp), intent(in) !! Time to pause in seconds. real(wp), intent(in) :: seconds call draw() if (seconds > 0.0_wp) then call sleep_fortran(nint(seconds * 1000.0_wp)) end if end subroutine pause subroutine ensure_fig_init() !! Ensure the global figure exists and is 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_fig_init subroutine ensure_global_figure_initialized() !! Public wrapper to guarantee the singleton is ready for use call ensure_fig_init() end subroutine ensure_global_figure_initialized function get_global_figure() result(global_fig) !! Return the global figure pointer, initializing on demand class(figure_t), pointer :: global_fig call ensure_fig_init() global_fig => fig end function get_global_figure subroutine figure(num, figsize, dpi) !! Create or reset the shared figure. !! !! Parameters !! num : integer, optional !! Figure number used for compatibility with matplotlib. !! figsize : real(wp)(2), optional !! Figure size in inches as [width, height]. !! dpi : integer, optional !! Output resolution in dots per inch. integer, intent(in), optional :: num real(wp), dimension(2), intent(in), optional :: figsize integer, intent(in), optional :: dpi integer :: fig_num, fig_dpi real(wp), dimension(2) :: requested_size, safe_size integer :: width_px, height_px character(len=256) :: msg fig_num = 1 if (present(num)) fig_num = num requested_size = [6.4_wp, 4.8_wp] if (present(figsize)) requested_size = figsize fig_dpi = nint(REFERENCE_DPI) if (present(dpi)) fig_dpi = dpi if (requested_size(1) <= 0.0_wp .or. requested_size(2) <= 0.0_wp) 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 width_px = nint(requested_size(1) * real(fig_dpi, wp)) height_px = nint(requested_size(2) * real(fig_dpi, wp)) safe_size = requested_size if (width_px > 10000 .or. height_px > 10000) then call log_warning("figure: Large figure size may cause memory issues") end if write(msg, '(A,I0)') "figure: Creating figure ", fig_num call log_info(trim(msg)) fig = figure_t(state=figure_state_t(legend_data=legend_t())) call fig%initialize(dpi=real(fig_dpi, wp)) call configure_figure_dimensions(fig%state, width=width_px, height=height_px) if (allocated(fig%state%backend)) then select type (bk => fig%state%backend) type is (png_context) call setup_figure_backend(fig%state, 'png') type is (pdf_context) call setup_figure_backend(fig%state, 'pdf') type is (ascii_context) call setup_figure_backend(fig%state, 'ascii') class default call setup_figure_backend(fig%state, 'png') end select end if end subroutine figure subroutine subplot(nrows, ncols, index) !! Select a subplot in an `nrows` by `ncols` grid. !! !! Parameters !! nrows : integer !! Number of subplot rows. !! ncols : integer !! Number of subplot columns. !! index : integer !! 1-based subplot index in row-major order. integer, intent(in) :: nrows, ncols, index character(len=256) :: msg integer :: rows, cols call ensure_global_figure_initialized() 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 rows = fig%subplot_rows cols = fig%subplot_cols ! (Re)create grid if missing or shape changed if (rows /= nrows .or. cols /= ncols) then call fig%subplots(nrows, ncols) end if fig%current_subplot = index write(msg, '(A,I0,A,I0,A,I0,A)') & "Selected subplot ", index, " in ", nrows, "x", ncols, " grid" call log_info(trim(msg)) end subroutine subplot subroutine subplots(nrows, ncols, axes, sharex, sharey) !! Initialize a subplot grid. !! !! Parameters !! nrows : integer !! Number of subplot rows. !! ncols : integer !! Number of subplot columns. !! axes : integer, allocatable, optional !! Output 2D array of subplot indices in row-major order. !! sharex : logical, optional !! Accepted for matplotlib parity. !! sharey : logical, optional !! Accepted for matplotlib parity. integer, intent(in) :: nrows, ncols integer, allocatable, intent(out), optional :: axes(:,:) logical, intent(in), optional :: sharex, sharey integer :: i, j call ensure_global_figure_initialized() if (nrows <= 0 .or. ncols <= 0) then call log_error("subplots: Invalid grid dimensions") if (present(axes)) allocate(axes(0, 0)) return end if call fig%subplots(nrows, ncols) fig%current_subplot = 1 if (present(axes)) then allocate(axes(nrows, ncols)) do i = 1, nrows do j = 1, ncols axes(i, j) = (i - 1) * ncols + j end do end do end if ! sharex/sharey accepted for matplotlib parity; wiring tracked separately call ignore_unused_subplots_kwargs(sharex, sharey) end subroutine subplots subroutine ignore_unused_subplots_kwargs(sharex, sharey) !! Explicit no-op to document silent acceptance of matplotlib kwargs logical, intent(in), optional :: sharex, sharey if (present(sharex) .or. present(sharey)) return end subroutine ignore_unused_subplots_kwargs function subplots_grid(nrows, ncols) result(axes) !! Create a subplot grid and return axis indices in row-major order. !! !! Parameters !! nrows : integer !! Number of subplot rows. !! ncols : integer !! Number of subplot columns. !! !! Returns !! axes : integer, allocatable(:,:) !! Row-major subplot index matrix. integer, intent(in) :: nrows, ncols integer, allocatable :: axes(:,:) integer :: i, j call ensure_global_figure_initialized() if (nrows <= 0 .or. ncols <= 0) then call log_error("subplots_grid: Invalid grid dimensions") allocate(axes(0, 0)) return end if call fig%subplots(nrows, ncols) allocate(axes(nrows, ncols)) do i = 1, nrows do j = 1, ncols axes(i, j) = (i - 1) * ncols + j end do end do end function subplots_grid subroutine savefig(filename, dpi, transparent, bbox_inches) !! Save the current figure. !! !! Parameters !! filename : character(len=*), intent(in) !! Output path. The extension selects the backend. !! dpi : integer, optional !! Raster resolution applied before rendering. !! transparent : logical, optional !! Accepted for matplotlib parity. !! bbox_inches : character(len=*), optional !! Accepted for matplotlib parity. 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 apply_savefig_dpi(dpi) call consume_savefig_stubs(transparent, bbox_inches) call fig%savefig(filename) end subroutine savefig subroutine savefig_with_status(filename, status, dpi, transparent, bbox_inches) !! Save the figure and return a status code. !! !! Parameters !! filename : character(len=*), intent(in) !! Output path. The extension selects the backend. !! status : integer, intent(out) !! Return status from the save operation. !! dpi : integer, optional !! Raster resolution applied before rendering. !! transparent : logical, optional !! Accepted for matplotlib parity. !! bbox_inches : character(len=*), optional !! Accepted for matplotlib parity. 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 apply_savefig_dpi(dpi) call consume_savefig_stubs(transparent, bbox_inches) call fig%savefig_with_status(filename, status) end subroutine savefig_with_status subroutine apply_savefig_dpi(dpi) !! Propagate an explicit DPI value into figure state so rendering !! honours it. A non-positive value is rejected via log_error so the !! caller sees the problem without aborting the save. integer, intent(in), optional :: dpi if (.not. present(dpi)) return if (dpi <= 0) then call log_error('savefig: dpi must be positive; ignoring request') return end if call fig%set_dpi(real(dpi, wp)) end subroutine apply_savefig_dpi subroutine consume_savefig_stubs(transparent, bbox_inches) !! Accept transparent/bbox_inches without side effects. Placeholder !! until raster/vector backends gain explicit support. logical, intent(in), optional :: transparent character(len=*), intent(in), optional :: bbox_inches if (present(transparent)) continue if (present(bbox_inches)) continue end subroutine consume_savefig_stubs subroutine show_data(x, y, label, title_text, xlabel_text, ylabel_text, blocking) !! Show a line plot and optionally apply labels before displaying it. !! !! Parameters !! x : real(wp), contiguous, intent(in) !! X coordinates. !! y : real(wp), contiguous, intent(in) !! Y coordinates. !! label : character(len=*), optional !! Legend label. !! title_text : character(len=*), optional !! Figure title. !! xlabel_text : character(len=*), optional !! X-axis label. !! ylabel_text : character(len=*), optional !! Y-axis label. !! blocking : logical, optional !! Keep the viewer open when .true. real(wp), contiguous, 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() call fig%add_plot(x, y, label=label) 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) call fig%show(blocking=blocking) end subroutine show_data subroutine show_figure(blocking) !! Display the current figure. !! !! Parameters !! blocking : logical, optional !! Keep the viewer open when .true. logical, intent(in), optional :: blocking call ensure_global_figure_initialized() call fig%show(blocking=blocking) end subroutine show_figure subroutine show_viewer(blocking) !! Launch the system image viewer for the current figure. !! !! Parameters !! blocking : logical, optional !! Keep the viewer open when .true. logical, intent(in), optional :: blocking call ensure_global_figure_initialized() call show_viewer_implementation(blocking) end subroutine show_viewer subroutine show_viewer_implementation(blocking) logical, intent(in), optional :: blocking character(len=512) :: temp_file logical :: is_blocking, success integer :: status real :: start_time, current_time, random_val logical, save :: no_gui_warning_shown = .false. is_blocking = .false. if (present(blocking)) is_blocking = blocking if (.not. is_gui_available()) then if (.not. no_gui_warning_shown) then call log_warning("No GUI available, saving to show_output.png instead") no_gui_warning_shown = .true. end if call fig%savefig("show_output.png") return end if call random_number(random_val) 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(random_val * 1000000), ".png" 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 call launch_system_viewer(trim(temp_file), success) if (.not. success) then call log_error("Failed to launch image viewer") call delete_file_runtime(trim(temp_file), success) return end if if (is_blocking) then call log_info("Viewer launched in blocking mode. Close viewer to continue.") call cpu_time(start_time) do call cpu_time(current_time) if (current_time - start_time > 30.0) exit call sleep_fortran(100) end do else call sleep_fortran(1000) end if call delete_file_runtime(trim(temp_file), success) end subroutine show_viewer_implementation function is_gui_available() result(gui_available) logical :: gui_available character(len=256) :: display_var integer :: status logical, save :: ssh_warning_shown = .false. gui_available = .false. call get_environment_variable("DISPLAY", display_var, status=status) if (status == 0 .and. len_trim(display_var) > 0) then gui_available = .true. end if call get_environment_variable("SSH_CLIENT", display_var, status=status) if (status == 0 .and. .not. gui_available .and. .not. ssh_warning_shown) then call log_warning("SSH session detected without X forwarding") ssh_warning_shown = .true. end if end function is_gui_available subroutine sleep_fortran(milliseconds) integer, intent(in) :: milliseconds real :: seconds integer :: start_count, end_count, count_rate, target_count seconds = real(milliseconds) / 1000.0 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_session