module fortplot_figure_initialization !! Figure initialization module !! !! Single Responsibility: Initialize figures, manage state lifecycle, !! and provide lazy storage allocation. !! Configuration procedures (backend setup, dimensions, labels, scales, limits) !! are in fortplot_figure_configuration. use, intrinsic :: iso_fortran_env, only: wp => real64 use fortplot_constants, only: REFERENCE_DPI, TAB10_RGB use fortplot_context use fortplot_utils, only: initialize_backend use fortplot_legend, only: legend_t, legend_entry_t use fortplot_plot_data, only: plot_data_t, arrow_data_t, AXIS_PRIMARY, & AXIS_TWINX, AXIS_TWINY implicit none private public :: figure_state_t, initialize_figure_state, reset_figure_state public :: ensure_figure_storage type :: figure_state_t !! Figure state and configuration data !! Encapsulates all configuration and state management class(plot_context), allocatable :: backend character(len=10) :: backend_name = 'png' integer :: plot_count = 0 logical :: rendered = .false. ! Figure dimensions integer :: width = 640 integer :: height = 480 real(wp) :: dpi = REFERENCE_DPI ! Plot area settings real(wp) :: margin_left = 0.15_wp real(wp) :: margin_right = 0.05_wp real(wp) :: margin_bottom = 0.15_wp real(wp) :: margin_top = 0.05_wp ! Scale settings character(len=10) :: xscale = 'linear' character(len=10) :: yscale = 'linear' real(wp) :: symlog_threshold = 1.0_wp real(wp) :: symlog_base = 10.0_wp real(wp) :: symlog_linscale = 1.0_wp character(len=:), allocatable :: xaxis_date_format character(len=:), allocatable :: yaxis_date_format ! Axis limits real(wp) :: x_min = 0.0_wp, x_max = 1.0_wp, y_min = 0.0_wp, y_max = 1.0_wp real(wp) :: x_min_transformed = 0.0_wp, x_max_transformed = 1.0_wp real(wp) :: y_min_transformed = 0.0_wp, y_max_transformed = 1.0_wp logical :: xlim_set = .false., ylim_set = .false. ! Secondary axis support integer :: active_axis = AXIS_PRIMARY logical :: has_twinx = .false. logical :: has_twiny = .false. character(len=:), allocatable :: twinx_ylabel character(len=:), allocatable :: twiny_xlabel character(len=10) :: twinx_yscale = 'linear' character(len=10) :: twiny_xscale = 'linear' character(len=:), allocatable :: twinx_yaxis_date_format character(len=:), allocatable :: twiny_xaxis_date_format logical :: twinx_ylim_set = .false. logical :: twiny_xlim_set = .false. real(wp) :: twinx_y_min = 0.0_wp real(wp) :: twinx_y_max = 1.0_wp real(wp) :: twiny_x_min = 0.0_wp real(wp) :: twiny_x_max = 1.0_wp real(wp) :: twinx_y_min_transformed = 0.0_wp real(wp) :: twinx_y_max_transformed = 1.0_wp real(wp) :: twiny_x_min_transformed = 0.0_wp real(wp) :: twiny_x_max_transformed = 1.0_wp ! Labels character(len=:), allocatable :: title character(len=:), allocatable :: xlabel character(len=:), allocatable :: ylabel character(len=:), allocatable :: suptitle real(wp) :: suptitle_fontsize = 14.0_wp real(wp), dimension(3, 10) :: colors = TAB10_RGB ! Legend support type(legend_t) :: legend_data logical :: show_legend = .false. integer :: max_plots = 500 ! Drawing properties (line width in points, 1pt = 1/72 inch) real(wp) :: current_line_width = 1.5_wp logical :: has_error = .false. ! Grid settings logical :: grid_enabled = .false. character(len=10) :: grid_which = 'both' character(len=1) :: grid_axis = 'b' real(wp) :: grid_alpha = 1.0_wp character(len=10) :: grid_linestyle = '-' character(len=7) :: grid_color = '#b0b0b0' ! Configurable font sizes (pixels, -1 = use default) real(wp) :: title_font_size = -1.0_wp real(wp) :: label_font_size = -1.0_wp real(wp) :: tick_font_size = -1.0_wp ! Stateful colorbar (matplotlib-style) logical :: colorbar_enabled = .false. integer :: colorbar_plot_index = 0 character(len=10) :: colorbar_location = 'right' real(wp) :: colorbar_fraction = 0.15_wp real(wp) :: colorbar_pad = 0.05_wp real(wp) :: colorbar_shrink = 1.0_wp logical :: colorbar_label_set = .false. character(len=:), allocatable :: colorbar_label ! Custom colorbar tick positions and labels logical :: colorbar_ticks_set = .false. logical :: colorbar_ticklabels_set = .false. real(wp), allocatable :: colorbar_ticks(:) character(len=50), allocatable :: colorbar_ticklabels(:) real(wp) :: colorbar_label_fontsize = 10.0_wp ! Streamplot arrow storage (rendered after plots) type(arrow_data_t), allocatable :: stream_arrows(:) ! Minor ticks configuration logical :: minor_ticks_x = .false. logical :: minor_ticks_y = .false. integer :: minor_tick_count = 5 ! Custom tick labels (categorical axis support) logical :: custom_xticks_set = .false. logical :: custom_yticks_set = .false. real(wp), allocatable :: custom_xtick_positions(:) real(wp), allocatable :: custom_ytick_positions(:) character(len=50), allocatable :: custom_xtick_labels(:) character(len=50), allocatable :: custom_ytick_labels(:) ! Aspect ratio control (auto, equal, or numeric ratio) character(len=10) :: aspect_mode = 'auto' real(wp) :: aspect_ratio = 1.0_wp ! Tight layout configuration logical :: tight_layout_enabled = .false. real(wp) :: tight_pad = 1.08_wp real(wp) :: tight_w_pad = 0.0_wp real(wp) :: tight_h_pad = 0.0_wp ! Polar projection configuration logical :: polar_projection = .false. real(wp) :: polar_theta_min = 0.0_wp real(wp) :: polar_theta_max = 6.283185307179586_wp ! 2*pi real(wp) :: polar_r_min = 0.0_wp real(wp) :: polar_r_max = 1.0_wp integer :: polar_theta_gridlines = 12 ! Default: 30-degree intervals integer :: polar_r_gridlines = 5 ! Default: 5 radial circles logical :: polar_theta_direction_cw = .false. ! Counter-clockwise default real(wp) :: polar_theta_offset = 0.0_wp ! 0 deg at east (matplotlib) end type figure_state_t contains subroutine initialize_figure_state(state, width, height, backend, dpi) !! Initialize figure state with specified parameters !! Added Issue #854: Parameter validation for user input safety !! Added DPI support for OO interface consistency with matplotlib interface type(figure_state_t), intent(inout) :: state integer, intent(in), optional :: width, height character(len=*), intent(in), optional :: backend real(wp), intent(in), optional :: dpi call set_state_dpi(state, dpi) call set_state_dimensions(state, width, height) call set_state_backend(state, backend) call reset_state_for_initialization(state) end subroutine initialize_figure_state subroutine set_state_dpi(state, dpi) use fortplot_parameter_validation, only: validation_warning type(figure_state_t), intent(inout) :: state real(wp), intent(in), optional :: dpi if (present(dpi)) then if (dpi <= 0.0_wp) then call validation_warning("Invalid DPI value, using default 100.0", & "figure_initialization") state%dpi = REFERENCE_DPI else state%dpi = dpi end if else state%dpi = REFERENCE_DPI end if end subroutine set_state_dpi subroutine set_state_dimensions(state, width, height) use fortplot_parameter_validation, only: validate_plot_dimensions, & parameter_validation_result_t type(figure_state_t), intent(inout) :: state integer, intent(in), optional :: width, height type(parameter_validation_result_t) :: validation real(wp) :: width_real, height_real if (present(width)) then width_real = real(width, wp) height_real = real(state%height, wp) if (present(height)) height_real = real(height, wp) validation = validate_plot_dimensions(width_real, height_real, & "figure_initialization") if (validation%is_valid) then state%width = width else state%width = 640 end if end if if (present(height)) then width_real = real(state%width, wp) height_real = real(height, wp) validation = validate_plot_dimensions(width_real, height_real, & "figure_initialization") if (validation%is_valid) then state%height = height else state%height = 480 end if end if end subroutine set_state_dimensions subroutine set_state_backend(state, backend) use fortplot_parameter_validation, only: validation_warning type(figure_state_t), intent(inout) :: state character(len=*), intent(in), optional :: backend if (present(backend)) then if (len_trim(backend) == 0) then call validation_warning( & "Empty backend name provided, using default 'png'", & "figure_initialization") state%backend_name = 'png' call initialize_backend(state%backend, 'png', state%width, & state%height, state%dpi) else if (backend /= 'png' .and. backend /= 'pdf' .and. backend /= & 'ascii') then call validation_warning( & "Unknown backend '"//trim(backend)//"', using default 'png'", & "figure_initialization") state%backend_name = 'png' call initialize_backend(state%backend, 'png', state%width, & state%height, state%dpi) else state%backend_name = backend call initialize_backend(state%backend, backend, state%width, & state%height, state%dpi) end if else if (.not. allocated(state%backend)) then state%backend_name = 'png' call initialize_backend(state%backend, 'png', state%width, & state%height, state%dpi) end if end if end subroutine set_state_backend subroutine reset_state_for_initialization(state) type(figure_state_t), intent(inout) :: state type(legend_entry_t), allocatable :: new_entries(:) character(len=:), allocatable :: scratch type(arrow_data_t), allocatable :: scratch_arrows(:) state%plot_count = 0 state%rendered = .false. state%show_legend = .false. state%xlim_set = .false. state%ylim_set = .false. state%has_error = .false. state%legend_data%num_entries = 0 if (allocated(state%legend_data%entries)) deallocate(state%legend_data%entries) allocate(state%legend_data%entries(0)) state%active_axis = AXIS_PRIMARY state%has_twinx = .false. state%has_twiny = .false. state%twinx_ylim_set = .false. state%twiny_xlim_set = .false. state%twinx_yscale = 'linear' state%twiny_xscale = 'linear' if (allocated(state%xaxis_date_format)) & call move_alloc(state%xaxis_date_format, scratch) if (allocated(state%yaxis_date_format)) & call move_alloc(state%yaxis_date_format, scratch) if (allocated(state%twinx_yaxis_date_format)) & call move_alloc(state%twinx_yaxis_date_format, scratch) if (allocated(state%twiny_xaxis_date_format)) & call move_alloc(state%twiny_xaxis_date_format, scratch) state%twinx_y_min = 0.0_wp state%twinx_y_max = 1.0_wp state%twiny_x_min = 0.0_wp state%twiny_x_max = 1.0_wp state%twinx_y_min_transformed = 0.0_wp state%twinx_y_max_transformed = 1.0_wp state%twiny_x_min_transformed = 0.0_wp state%twiny_x_max_transformed = 1.0_wp if (allocated(state%twinx_ylabel)) call move_alloc(state%twinx_ylabel, scratch) if (allocated(state%twiny_xlabel)) call move_alloc(state%twiny_xlabel, scratch) state%colorbar_enabled = .false. state%colorbar_plot_index = 0 state%colorbar_location = 'right' state%colorbar_fraction = 0.15_wp state%colorbar_pad = 0.05_wp state%colorbar_shrink = 1.0_wp state%colorbar_label_set = .false. if (allocated(state%colorbar_label)) & call move_alloc(state%colorbar_label, scratch) state%colorbar_ticks_set = .false. state%colorbar_ticklabels_set = .false. state%colorbar_label_fontsize = 10.0_wp if (allocated(state%suptitle)) call move_alloc(state%suptitle, scratch) state%suptitle_fontsize = 14.0_wp if (allocated(state%stream_arrows)) & call move_alloc(state%stream_arrows, scratch_arrows) state%minor_ticks_x = .false. state%minor_ticks_y = .false. state%minor_tick_count = 5 state%custom_xticks_set = .false. state%custom_yticks_set = .false. if (allocated(state%custom_xtick_positions)) & deallocate (state%custom_xtick_positions) if (allocated(state%custom_ytick_positions)) & deallocate (state%custom_ytick_positions) if (allocated(state%custom_xtick_labels)) & deallocate (state%custom_xtick_labels) if (allocated(state%custom_ytick_labels)) & deallocate (state%custom_ytick_labels) state%aspect_mode = 'auto' state%aspect_ratio = 1.0_wp state%tight_layout_enabled = .false. state%tight_pad = 1.08_wp state%tight_w_pad = 0.0_wp state%tight_h_pad = 0.0_wp state%polar_projection = .false. state%polar_theta_min = 0.0_wp state%polar_theta_max = 6.283185307179586_wp state%polar_r_min = 0.0_wp state%polar_r_max = 1.0_wp state%polar_theta_gridlines = 12 state%polar_r_gridlines = 5 state%polar_theta_direction_cw = .false. state%polar_theta_offset = 0.0_wp end subroutine reset_state_for_initialization subroutine reset_figure_state(state) !! Reset figure state to initial values type(figure_state_t), intent(inout) :: state type(legend_entry_t), allocatable :: new_entries(:) character(len=:), allocatable :: scratch type(arrow_data_t), allocatable :: scratch_arrows(:) state%plot_count = 0 state%rendered = .false. state%show_legend = .false. ! Initialize legend data (safe initialization without manual deallocate) state%legend_data%num_entries = 0 allocate (new_entries(0)) call move_alloc(new_entries, state%legend_data%entries) ! Reset axis limits and labels state%xlim_set = .false. state%ylim_set = .false. state%title = '' state%xlabel = '' state%ylabel = '' if (allocated(state%suptitle)) deallocate (state%suptitle) state%suptitle_fontsize = 14.0_wp state%active_axis = AXIS_PRIMARY state%has_twinx = .false. state%has_twiny = .false. state%twinx_ylim_set = .false. state%twiny_xlim_set = .false. state%twinx_yscale = 'linear' state%twiny_xscale = 'linear' state%twinx_y_min = 0.0_wp state%twinx_y_max = 1.0_wp state%twiny_x_min = 0.0_wp state%twiny_x_max = 1.0_wp state%twinx_y_min_transformed = 0.0_wp state%twinx_y_max_transformed = 1.0_wp state%twiny_x_min_transformed = 0.0_wp state%twiny_x_max_transformed = 1.0_wp if (allocated(state%twinx_ylabel)) call move_alloc(state%twinx_ylabel, scratch) if (allocated(state%twiny_xlabel)) call move_alloc(state%twiny_xlabel, scratch) state%colorbar_enabled = .false. state%colorbar_plot_index = 0 state%colorbar_location = 'right' state%colorbar_fraction = 0.15_wp state%colorbar_pad = 0.05_wp state%colorbar_shrink = 1.0_wp state%colorbar_label_set = .false. if (allocated(state%colorbar_label)) & call move_alloc(state%colorbar_label, scratch) state%colorbar_ticks_set = .false. state%colorbar_ticklabels_set = .false. state%colorbar_label_fontsize = 10.0_wp state%has_error = .false. if (allocated(state%stream_arrows)) & call move_alloc(state%stream_arrows, scratch_arrows) state%minor_ticks_x = .false. state%minor_ticks_y = .false. state%minor_tick_count = 5 state%custom_xticks_set = .false. state%custom_yticks_set = .false. state%aspect_mode = 'auto' state%aspect_ratio = 1.0_wp state%tight_layout_enabled = .false. state%tight_pad = 1.08_wp state%tight_w_pad = 0.0_wp state%tight_h_pad = 0.0_wp state%polar_projection = .false. state%polar_theta_min = 0.0_wp state%polar_theta_max = 6.283185307179586_wp state%polar_r_min = 0.0_wp state%polar_r_max = 1.0_wp state%polar_theta_gridlines = 12 state%polar_r_gridlines = 5 state%polar_theta_direction_cw = .false. state%polar_theta_offset = 0.0_wp end subroutine reset_figure_state subroutine ensure_figure_storage(plots, state) !! Lazily allocate plot storage and the active backend so that !! operations invoked on a default-initialized figure_t do not !! dereference unallocated arrays (#1638). type(plot_data_t), allocatable, intent(inout) :: plots(:) type(figure_state_t), intent(inout) :: state if (.not. allocated(plots)) then allocate (plots(state%max_plots)) end if if (.not. allocated(state%backend)) then call initialize_backend(state%backend, trim(state%backend_name), & state%width, state%height, state%dpi) end if end subroutine ensure_figure_storage end module fortplot_figure_initialization