module fortplot_subplot_rendering use, intrinsic :: iso_fortran_env, only: wp => real64 use fortplot_plot_data, only: subplot_data_t use fortplot_figure_initialization, only: figure_state_t use fortplot_figure_rendering_pipeline, only: calculate_figure_data_ranges, & setup_coordinate_system, & render_figure_axes, & render_all_plots, & render_figure_axes_labels_only use fortplot_margins, only: calculate_plot_area use fortplot_text_layout, only: TITLE_FONT_SIZE, TITLE_FONT_SIZE_PT, calculate_text_height_with_size use fortplot_pdf_coordinate, only: calculate_pdf_plot_area use fortplot_subplot_layout, only: compute_tight_subplot_margins use fortplot_png, only: png_context use fortplot_pdf, only: pdf_context use fortplot_pdf_core, only: PDF_TITLE_SIZE use fortplot_ascii, only: ascii_context use fortplot_raster_labels, only: render_title_centered_with_size use fortplot_pdf_text, only: estimate_pdf_text_width implicit none private public :: render_subplots contains subroutine render_subplots(state, subplots_array, subplot_rows, subplot_cols) type(figure_state_t), intent(inout) :: state type(subplot_data_t), intent(in) :: subplots_array(:, :) integer, intent(in) :: subplot_rows, subplot_cols integer :: nr, nc, i, j real(wp), allocatable :: left_f(:, :), right_f(:, :) real(wp), allocatable :: bottom_f(:, :), top_f(:, :) logical :: have_tight real(wp) :: base_left, base_right, base_bottom, base_top real(wp) :: wspace, hspace real(wp) :: total_w, total_h real(wp) :: ax_w, ax_h real(wp) :: gap_w, gap_h character(len=64) :: x_date_format, y_date_format real(wp) :: suptitle_height_frac real(wp) :: fig_w, fig_h nr = subplot_rows nc = subplot_cols x_date_format = '' y_date_format = '' if (allocated(state%xaxis_date_format)) x_date_format = state%xaxis_date_format if (allocated(state%yaxis_date_format)) y_date_format = state%yaxis_date_format ! Calculate suptitle height in fractional figure coordinates for layout adjustment suptitle_height_frac = compute_suptitle_height_frac(state, fig_w, fig_h) have_tight = .false. call compute_tight_subplot_margins(state%backend, subplots_array, nr, nc, & state%xscale, state%yscale, & state%symlog_threshold, & left_f, right_f, & bottom_f, top_f, have_tight) if (.not. have_tight) then base_left = 0.125_wp base_right = 0.90_wp base_bottom = 0.11_wp base_top = 0.88_wp wspace = 0.20_wp hspace = 0.20_wp ! Reserve space for suptitle above subplot area if (allocated(state%suptitle) .and. len_trim(state%suptitle) > 0) then base_top = base_top - suptitle_height_frac end if total_w = base_right - base_left total_h = base_top - base_bottom ax_w = total_w/ & (real(nc, wp) + wspace*real(max(0, nc - 1), wp)) gap_w = wspace*ax_w ax_h = total_h/ & (real(nr, wp) + hspace*real(max(0, nr - 1), wp)) gap_h = hspace*ax_h end if do i = 1, nr do j = 1, nc call render_subplot_cell( & state, subplots_array(i, j), i, j, have_tight, & left_f, right_f, bottom_f, top_f, & base_left, base_bottom, base_top, & ax_w, ax_h, gap_w, gap_h, & x_date_format, y_date_format) end do end do call render_suptitle(state, suptitle_height_frac) end subroutine render_subplots subroutine render_subplot_cell(state, sp, i, j, have_tight, & left_f, right_f, bottom_f, top_f, & base_left, base_bottom, base_top, & ax_w, ax_h, gap_w, gap_h, & x_date_format, y_date_format) !! Render a single subplot cell: margins, axes, plots, labels type(figure_state_t), intent(inout) :: state type(subplot_data_t), intent(in) :: sp integer, intent(in) :: i, j logical, intent(in) :: have_tight real(wp), intent(in), optional :: left_f(:,:), right_f(:,:) real(wp), intent(in), optional :: bottom_f(:,:), top_f(:,:) real(wp), intent(in) :: base_left, base_bottom, base_top real(wp), intent(in) :: ax_w, ax_h, gap_w, gap_h character(len=*), intent(in) :: x_date_format, y_date_format real(wp) :: subplot_left, subplot_right real(wp) :: subplot_bottom, subplot_top real(wp) :: lxmin, lxmax, lymin, lymax real(wp) :: lxmin_t, lxmax_t, lymin_t, lymax_t ! Set margins if (have_tight) then call set_subplot_margins(state%backend, left_f(i, j), & right_f(i, j), bottom_f(i, j), & top_f(i, j)) else subplot_left = base_left + real(j - 1, wp)*(ax_w + gap_w) subplot_right = subplot_left + ax_w subplot_top = base_top - real(i - 1, wp)*(ax_h + gap_h) subplot_bottom = subplot_top - ax_h call set_subplot_margins(state%backend, subplot_left, & subplot_right, subplot_bottom, & subplot_top) end if call calculate_figure_data_ranges(sp%plots, sp%plot_count, & sp%xlim_set, sp%ylim_set, & lxmin, lxmax, lymin, lymax, & lxmin_t, lxmax_t, lymin_t, lymax_t, & state%xscale, state%yscale, & state%symlog_threshold, & state%symlog_base, state%symlog_linscale) call setup_coordinate_system(state%backend, lxmin_t, lxmax_t, & lymin_t, lymax_t) call render_figure_axes(state%backend, state%xscale, state%yscale, & state%symlog_threshold, lxmin, lxmax, & lymin, lymax, sp%title, sp%xlabel, sp%ylabel, & sp%plots, sp%plot_count, & has_twinx=.false., has_twiny=.false., & state=state) if (sp%plot_count > 0) then call render_all_plots(state%backend, sp%plots, sp%plot_count, & lxmin_t, lxmax_t, lymin_t, lymax_t, & state%xscale, state%yscale, & state%symlog_threshold, state%width, & state%height, & state%margin_left, state%margin_right, & state%margin_bottom, state%margin_top) end if call render_figure_axes_labels_only(state%backend, state%xscale, & state%yscale, state%symlog_threshold, & lxmin, lxmax, lymin, lymax, & sp%title, sp%xlabel, sp%ylabel, & sp%plots, sp%plot_count, & has_twinx=.false., has_twiny=.false., & x_date_format=trim(x_date_format), & y_date_format=trim(y_date_format)) end subroutine render_subplot_cell subroutine render_suptitle(state, suptitle_height_frac) !! Render the figure-level suptitle above all subplots !! suptitle_height_frac is the fractional height reserved for suptitle !! in the figure layout (used to position the title above subplot area) type(figure_state_t), intent(inout) :: state real(wp), intent(in) :: suptitle_height_frac real(wp) :: suptitle_y_frac, center_x integer :: suptitle_y_px real(wp) :: font_scale real(wp) :: clearance_frac if (.not. allocated(state%suptitle)) return if (len_trim(state%suptitle) == 0) return font_scale = state%suptitle_fontsize/12.0_wp ! Clearance between suptitle baseline and subplot top area clearance_frac = 0.01_wp select type (bk => state%backend) class is (png_context) ! Position suptitle at top of figure minus its own height and clearance suptitle_y_frac = 1.0_wp - suptitle_height_frac - clearance_frac suptitle_y_px = int(real(bk%height, wp)*suptitle_y_frac) center_x = real(bk%width, wp)/2.0_wp call render_title_centered_with_size(bk%raster, bk%width, bk%height, & int(center_x), suptitle_y_px, & trim(state%suptitle), font_scale) class is (pdf_context) block real(wp) :: title_width, x_center, y_pos real(wp) :: scaled_font_size real(wp) :: black_color(3) scaled_font_size = PDF_TITLE_SIZE*font_scale title_width = estimate_pdf_text_width(trim(state%suptitle), & scaled_font_size) x_center = real(bk%width, wp)/2.0_wp ! Position suptitle at top of figure minus its own height and clearance y_pos = real(bk%height, wp)*(1.0_wp - suptitle_height_frac - clearance_frac) black_color = [0.0_wp, 0.0_wp, 0.0_wp] call bk%draw_text_styled(x_center, y_pos, trim(state%suptitle), & scaled_font_size, 0.0_wp, 'center', 'bottom', & .false., black_color) end block class is (ascii_context) call bk%set_title(trim(state%suptitle)) class default end select end subroutine render_suptitle function compute_suptitle_height_frac(state, fig_w, fig_h) result(h_frac) !! Compute the fractional height that the suptitle occupies in the figure. !! Returns 0.0 if no suptitle is set. type(figure_state_t), intent(in) :: state real(wp), intent(out) :: fig_w, fig_h real(wp) :: h_frac real(wp) :: suptitle_height_px real(wp) :: font_scale real(wp) :: scaled_font_size h_frac = 0.0_wp fig_w = 1.0_wp fig_h = 1.0_wp if (.not. allocated(state%suptitle)) return if (len_trim(state%suptitle) == 0) return font_scale = state%suptitle_fontsize/12.0_wp select type (bk => state%backend) class is (png_context) fig_w = real(bk%width, wp) fig_h = real(bk%height, wp) scaled_font_size = TITLE_FONT_SIZE_PT * bk%raster%dpi / 72.0_wp * font_scale suptitle_height_px = real(calculate_text_height_with_size( & scaled_font_size), wp) if (suptitle_height_px > 0 .and. fig_h > 0) then h_frac = suptitle_height_px/fig_h end if ! Minimum clearance: ensure at least 2% of figure height h_frac = max(h_frac, 0.02_wp) class is (pdf_context) fig_w = real(bk%width, wp) fig_h = real(bk%height, wp) scaled_font_size = PDF_TITLE_SIZE*font_scale ! Estimate suptitle height in PDF points suptitle_height_px = scaled_font_size*1.2_wp if (fig_h > 0) then ! Convert PDF points to figure-height fraction ! PDF uses points (1/72 inch), figure height is in inches at given DPI h_frac = suptitle_height_px/fig_h end if h_frac = max(h_frac, 0.02_wp) class default end select end function compute_suptitle_height_frac subroutine set_subplot_margins(backend, left_f, right_f, bottom_f, top_f) class(*), intent(inout) :: backend real(wp), intent(in) :: left_f, right_f, bottom_f, top_f select type (bk => backend) class is (png_context) bk%margins%left = left_f bk%margins%right = right_f bk%margins%bottom = bottom_f bk%margins%top = top_f call calculate_plot_area(bk%width, bk%height, bk%margins, bk%plot_area) class is (pdf_context) bk%margins%left = left_f bk%margins%right = right_f bk%margins%bottom = bottom_f bk%margins%top = top_f call calculate_pdf_plot_area(bk%width, bk%height, bk%margins, bk%plot_area) class is (ascii_context) class default end select end subroutine set_subplot_margins end module fortplot_subplot_rendering