module fortplot_figure_rendering_pipeline !! Figure rendering pipeline module !! !! Single Responsibility: Coordinate the complete rendering pipeline !! Extracted from fortplot_figure_core to improve modularity use, intrinsic :: iso_fortran_env, only: wp => real64 use fortplot_context use fortplot_figure_data_ranges, only: calculate_figure_data_ranges use fortplot_plot_data, only: plot_data_t, & PLOT_TYPE_CONTOUR, & PLOT_TYPE_PCOLORMESH, & PLOT_TYPE_SCATTER, & PLOT_TYPE_FILL, & PLOT_TYPE_BOXPLOT, & PLOT_TYPE_ERRORBAR, & PLOT_TYPE_SURFACE, & PLOT_TYPE_PIE, & PLOT_TYPE_BAR, & PLOT_TYPE_REFLINE, & PLOT_TYPE_QUIVER, & PLOT_TYPE_POLAR, & AXIS_PRIMARY, & AXIS_TWINX, & AXIS_TWINY use fortplot_figure_initialization, only: figure_state_t use fortplot_raster, only: raster_context use fortplot_raster_axes, only: raster_draw_x_minor_ticks, & raster_draw_y_minor_ticks use fortplot_tick_calculation, only: calculate_minor_tick_positions, & calculate_log_minor_tick_positions use fortplot_axes, only: compute_scale_ticks, MAX_TICKS use fortplot_twin_axes_rendering, only: setup_twin_axes_state, & render_twin_labels ! Plot dispatch and renderers extracted to submodules use fortplot_figure_plot_dispatch, only: render_all_plots use fortplot_figure_plot_renderers, only: render_streamplot_arrows, & render_polar_axes implicit none private public :: calculate_figure_data_ranges, setup_coordinate_system public :: render_figure_background, render_figure_axes, render_all_plots public :: render_streamplot_arrows public :: render_figure_axes_labels_only, render_title_only public :: render_polar_axes public :: render_3d_front_frame public :: render_ascii_grid public :: expand_data_range real(wp), parameter :: DATA_RANGE_MARGIN = 0.05_wp contains subroutine setup_coordinate_system(backend, x_min_transformed, x_max_transformed, & y_min_transformed, y_max_transformed, & sticky_x_min, sticky_x_max, & sticky_y_min, sticky_y_max) !! Setup the coordinate system for rendering !! The transformed bounds passed here already include any autoscale !! margin from calculate_figure_data_ranges, so this routine just hands !! them to the backend. use fortplot_pdf, only: pdf_context use fortplot_raster, only: raster_context class(plot_context), intent(inout) :: backend real(wp), intent(in) :: x_min_transformed, x_max_transformed real(wp), intent(in) :: y_min_transformed, y_max_transformed logical, intent(in), optional :: sticky_x_min, sticky_x_max logical, intent(in), optional :: sticky_y_min, sticky_y_max select type (bk => backend) class is (pdf_context) call bk%set_coordinates(x_min_transformed, x_max_transformed, & y_min_transformed, y_max_transformed) class is (raster_context) call bk%set_coordinates(x_min_transformed, x_max_transformed, & y_min_transformed, y_max_transformed) class default call backend%set_coordinates(x_min_transformed, x_max_transformed, & y_min_transformed, y_max_transformed) end select end subroutine setup_coordinate_system subroutine expand_data_range(data_min, data_max, expanded_min, expanded_max, & sticky_min, sticky_max) !! Expand a data range by DATA_RANGE_MARGIN (5%) of the span on each !! side, matching matplotlib's default axes margin (rcParams !! axes.{x,y}margin = 0.05). Also keeps markers at exact boundaries !! clear of the plot frame stroke. !! !! A side flagged sticky is left unexpanded, matching matplotlib's !! sticky edges: bar baselines pin to 0 with no margin beyond them. real(wp), intent(in) :: data_min, data_max real(wp), intent(out) :: expanded_min, expanded_max logical, intent(in), optional :: sticky_min, sticky_max real(wp) :: span logical :: pin_min, pin_max pin_min = .false. pin_max = .false. if (present(sticky_min)) pin_min = sticky_min if (present(sticky_max)) pin_max = sticky_max if (data_max <= data_min) then expanded_min = data_min expanded_max = data_max return end if span = data_max - data_min if (pin_min) then expanded_min = data_min else expanded_min = data_min - DATA_RANGE_MARGIN*span end if if (pin_max) then expanded_max = data_max else expanded_max = data_max + DATA_RANGE_MARGIN*span end if end subroutine expand_data_range subroutine render_figure_background(backend) !! Render figure background class(plot_context), intent(inout) :: backend ! Background clearing is handled by backend-specific rendering end subroutine render_figure_background subroutine render_figure_axes(backend, xscale, yscale, symlog_threshold, & x_min, x_max, y_min, y_max, title, xlabel, ylabel, & plots, plot_count, has_twinx, twinx_y_min, & twinx_y_max, & twinx_ylabel, twinx_yscale, has_twiny, twiny_x_min, & twiny_x_max, & twiny_xlabel, twiny_xscale, state) !! Render figure axes and labels !! For raster backends, split rendering to prevent label overlap issues class(plot_context), intent(inout) :: backend character(len=*), intent(in) :: xscale, yscale real(wp), intent(in) :: symlog_threshold real(wp), intent(in) :: x_min, x_max, y_min, y_max character(len=:), allocatable, intent(in) :: title, xlabel, ylabel type(plot_data_t), intent(in) :: plots(:) integer, intent(in) :: plot_count logical, intent(in), optional :: has_twinx, has_twiny real(wp), intent(in), optional :: twinx_y_min, twinx_y_max real(wp), intent(in), optional :: twiny_x_min, twiny_x_max character(len=:), allocatable, intent(in), optional :: twinx_ylabel, & twiny_xlabel character(len=*), intent(in), optional :: twinx_yscale, twiny_xscale type(figure_state_t), intent(in), optional :: state logical :: has_3d real(wp) :: zmin, zmax call detect_3d_extent(plots, plot_count, has_3d, zmin, zmax) call resolve_twin_axes_params(has_twinx, twinx_y_min, twinx_y_max, twinx_yscale, & has_twiny, twiny_x_min, twiny_x_max, twiny_xscale, & state, xscale, yscale) call dispatch_backend_axes_rendering(backend, xscale, yscale, symlog_threshold, & x_min, x_max, y_min, y_max, title, xlabel, ylabel, & zmin, zmax, has_3d, state) ! For raster backends without 3D, draw minor ticks after axes lines select type (backend) class is (raster_context) if (.not. has_3d) then if (present(state)) then call render_minor_ticks_raster(backend, xscale, yscale, & symlog_threshold, & x_min, x_max, y_min, y_max, & state) end if end if end select end subroutine render_figure_axes subroutine render_ascii_grid(backend, xscale, yscale, symlog_threshold, & x_min, x_max, y_min, y_max, grid_axis, grid_which) !! Paint text-backend grid glyphs aligned to major ticks after the data !! series so the layer policy keeps data on top of the grid (issue !! #2074). No-op for non-text backends, whose grid is drawn as lines. use fortplot_ascii, only: ascii_context use fortplot_ascii_drawing, only: draw_text_grid_lines class(plot_context), intent(inout) :: backend character(len=*), intent(in) :: xscale, yscale real(wp), intent(in) :: symlog_threshold real(wp), intent(in) :: x_min, x_max, y_min, y_max character(len=1), intent(in) :: grid_axis character(len=*), intent(in) :: grid_which integer :: x_cols(MAX_TICKS), y_rows(MAX_TICKS) integer :: num_x, num_y integer :: axis_col, right_col, top_row, bottom_row logical :: use_plot_area if (trim(grid_which) /= 'major' .and. trim(grid_which) /= 'both') return select type (bk => backend) type is (ascii_context) use_plot_area = bk%plot_area%width > 0 .and. bk%plot_area%height > 0 if (use_plot_area) then axis_col = bk%plot_area%left + 1 right_col = axis_col + max(1, bk%plot_area%width - 2) bottom_row = bk%plot_area%bottom + bk%plot_area%height - 1 top_row = bottom_row - max(1, bk%plot_area%height - 2) else axis_col = 2 right_col = bk%plot_width - 1 top_row = 2 bottom_row = bk%plot_height - 1 end if num_x = 0 if (grid_axis == 'x' .or. grid_axis == 'b') then call ascii_grid_x_cols(bk, xscale, symlog_threshold, x_min, x_max, & axis_col, right_col, use_plot_area, x_cols, & num_x) end if num_y = 0 if (grid_axis == 'y' .or. grid_axis == 'b') then call ascii_grid_y_rows(bk, yscale, symlog_threshold, y_min, y_max, & top_row, bottom_row, use_plot_area, y_rows, & num_y) end if call draw_text_grid_lines(bk%canvas, x_cols, num_x, y_rows, num_y, & top_row, bottom_row, axis_col, right_col) class default return end select end subroutine render_ascii_grid subroutine ascii_grid_x_cols(bk, xscale, symlog_threshold, x_min, x_max, & axis_col, right_col, use_plot_area, x_cols, num_x) !! Screen columns of major x ticks, mirroring render_ascii_x_ticks so the !! grid lines land exactly under the drawn tick marks (issue #2074). use fortplot_ascii, only: ascii_context type(ascii_context), intent(in) :: bk character(len=*), intent(in) :: xscale real(wp), intent(in) :: symlog_threshold, x_min, x_max integer, intent(in) :: axis_col, right_col logical, intent(in) :: use_plot_area integer, intent(out) :: x_cols(:) integer, intent(out) :: num_x real(wp) :: xt(MAX_TICKS), frac integer :: nxt, i, col logical :: use_custom num_x = 0 use_custom = .false. if (allocated(bk%custom_xtick_positions) .and. & allocated(bk%custom_xtick_labels)) then if (size(bk%custom_xtick_positions) > 0 .and. & size(bk%custom_xtick_positions) == size(bk%custom_xtick_labels)) then use_custom = .true. end if end if if (use_custom) then nxt = min(size(bk%custom_xtick_positions), MAX_TICKS) xt(1:nxt) = bk%custom_xtick_positions(1:nxt) else call compute_scale_ticks(xscale, x_min, x_max, symlog_threshold, xt, nxt) end if if (x_max <= x_min) return do i = 1, nxt if (xt(i) < x_min) cycle if (xt(i) > x_max) cycle frac = (xt(i) - x_min)/(x_max - x_min) if (use_plot_area) then col = axis_col + nint(frac*real(max(1, bk%plot_area%width - 2), wp)) else col = 1 + nint(frac*real(bk%plot_width - 2, wp)) end if col = max(axis_col, min(col, right_col)) num_x = num_x + 1 x_cols(num_x) = col end do end subroutine ascii_grid_x_cols subroutine ascii_grid_y_rows(bk, yscale, symlog_threshold, y_min, y_max, & top_row, bottom_row, use_plot_area, y_rows, num_y) !! Screen rows of major y ticks, mirroring render_ascii_y_ticks so the !! grid lines land exactly beside the drawn tick labels (issue #2074). use fortplot_ascii, only: ascii_context type(ascii_context), intent(in) :: bk character(len=*), intent(in) :: yscale real(wp), intent(in) :: symlog_threshold, y_min, y_max integer, intent(in) :: top_row, bottom_row logical, intent(in) :: use_plot_area integer, intent(out) :: y_rows(:) integer, intent(out) :: num_y real(wp) :: yt(MAX_TICKS), frac integer :: nyt, i, row num_y = 0 call compute_scale_ticks(yscale, y_min, y_max, symlog_threshold, yt, nyt) if (y_max <= y_min) return do i = 1, nyt if (yt(i) < y_min) cycle if (yt(i) > y_max) cycle frac = (y_max - yt(i))/(y_max - y_min) if (use_plot_area) then row = bottom_row - nint(frac*real(max(1, bk%plot_area%height - 2), wp)) else row = nint(frac*real(bk%plot_height, wp)) end if row = max(top_row, min(row, bottom_row)) num_y = num_y + 1 y_rows(num_y) = row end do end subroutine ascii_grid_y_rows subroutine render_3d_front_frame(backend, plots, plot_count, x_min, x_max, & y_min, y_max) !! Draw the front box spines after the data for the 3D case so they !! occlude curves and surfaces (global painter ordering, refs #1956). !! Raster, PDF, and ASCII backends all carry the projected 3D box so the !! near spines occlude the data on every backend (refs #2054). use fortplot_3d_axes, only: draw_3d_front_frame use fortplot_pdf, only: pdf_context use fortplot_ascii, only: ascii_context class(plot_context), intent(inout) :: backend type(plot_data_t), intent(in) :: plots(:) integer, intent(in) :: plot_count real(wp), intent(in) :: x_min, x_max, y_min, y_max logical :: has_3d real(wp) :: zmin, zmax call detect_3d_extent(plots, plot_count, has_3d, zmin, zmax) if (.not. has_3d) return select type (backend) class is (raster_context) call draw_3d_front_frame(backend, x_min, x_max, y_min, y_max, zmin, zmax) class is (pdf_context) call draw_3d_front_frame(backend, x_min, x_max, y_min, y_max, zmin, zmax) class is (ascii_context) call draw_3d_front_frame(backend, x_min, x_max, y_min, y_max, zmin, zmax) class default return end select end subroutine render_3d_front_frame subroutine resolve_twin_axes_params(has_twinx, twinx_y_min, twinx_y_max, twinx_yscale, & has_twiny, twiny_x_min, twiny_x_max, twiny_xscale, & state, default_xscale, default_yscale) !! Resolve optional twin axes parameters (no-op stub for future use) logical, intent(in), optional :: has_twinx, has_twiny real(wp), intent(in), optional :: twinx_y_min, twinx_y_max real(wp), intent(in), optional :: twiny_x_min, twiny_x_max character(len=*), intent(in), optional :: twinx_yscale, twiny_xscale type(figure_state_t), intent(in), optional :: state character(len=*), intent(in) :: default_xscale, default_yscale end subroutine resolve_twin_axes_params subroutine dispatch_backend_axes_rendering(backend, xscale, yscale, symlog_threshold, & x_min, x_max, y_min, y_max, title, xlabel, ylabel, & zmin, zmax, has_3d, state) !! Dispatch axes rendering to backend-specific implementation class(plot_context), intent(inout) :: backend character(len=*), intent(in) :: xscale, yscale real(wp), intent(in) :: symlog_threshold real(wp), intent(in) :: x_min, x_max, y_min, y_max character(len=:), allocatable, intent(in) :: title, xlabel, ylabel real(wp), intent(in) :: zmin, zmax logical, intent(in) :: has_3d type(figure_state_t), intent(in), optional :: state character(len=64) :: xfmt, yfmt character(len=:), allocatable :: t_title, t_xlabel, t_ylabel xfmt = '' yfmt = '' if (present(state)) then if (allocated(state%xaxis_date_format)) xfmt = state%xaxis_date_format if (allocated(state%yaxis_date_format)) yfmt = state%yaxis_date_format end if ! Workaround for gfortran bug: unallocated allocatable characters passed ! to optional arguments cause segfaults. Allocate temporaries with empty ! strings when the originals are unallocated. t_title = '' t_xlabel = '' t_ylabel = '' if (allocated(title)) t_title = title if (allocated(xlabel)) t_xlabel = xlabel if (allocated(ylabel)) t_ylabel = ylabel select type (backend) class is (raster_context) if (has_3d) then call backend%draw_axes_and_labels_backend( & xscale, yscale, symlog_threshold, x_min, x_max, y_min, y_max, & t_title, t_xlabel, t_ylabel, x_date_format=trim(xfmt), & y_date_format=trim(yfmt), z_min=zmin, z_max=zmax, & has_3d_plots=.true.) else call backend%draw_axes_lines_and_ticks(xscale, yscale, & symlog_threshold, & x_min, x_max, & y_min, y_max) end if class default call backend%draw_axes_and_labels_backend( & xscale, yscale, symlog_threshold, x_min, x_max, y_min, y_max, & t_title, t_xlabel, t_ylabel, x_date_format=xfmt, y_date_format=yfmt, & z_min=zmin, z_max=zmax, has_3d_plots=has_3d) end select end subroutine dispatch_backend_axes_rendering subroutine render_minor_ticks_raster(backend, xscale, yscale, symlog_threshold, & x_min, x_max, y_min, y_max, state) !! Render minor ticks for raster backends when enabled use fortplot_raster, only: raster_context type(raster_context), intent(inout) :: backend character(len=*), intent(in) :: xscale, yscale real(wp), intent(in) :: symlog_threshold real(wp), intent(in) :: x_min, x_max, y_min, y_max type(figure_state_t), intent(in) :: state real(wp) :: major_x(MAX_TICKS), major_y(MAX_TICKS) real(wp) :: minor_x(MAX_TICKS*10), minor_y(MAX_TICKS*10) integer :: num_major_x, num_major_y, num_minor_x, num_minor_y real(wp) :: vx_min, vx_max, vy_min, vy_max logical :: draw_minor_x, draw_minor_y ! matplotlib draws sub-decade (2..9) minor ticks on log axes by default; ! linear axes only get minor ticks when the user enables them explicitly. draw_minor_x = state%minor_ticks_x .or. trim(xscale) == 'log' draw_minor_y = state%minor_ticks_y .or. trim(yscale) == 'log' if (.not. draw_minor_x .and. .not. draw_minor_y) return vx_min = x_min; vx_max = x_max vy_min = y_min; vy_max = y_max ! Get major tick positions if (draw_minor_x) then if (trim(xscale) == 'linear') then call compute_scale_ticks(xscale, vx_min, vx_max, symlog_threshold, & major_x, num_major_x, & step_min=x_min, step_max=x_max) else call compute_scale_ticks(xscale, x_min, x_max, symlog_threshold, & major_x, num_major_x) end if if (num_major_x >= 2) then if (trim(xscale) == 'log') then call calculate_log_minor_tick_positions(major_x, num_major_x, & x_min, x_max, & minor_x, num_minor_x) else call calculate_minor_tick_positions(major_x, num_major_x, & state%minor_tick_count, & vx_min, vx_max, & minor_x, num_minor_x) end if if (num_minor_x > 0) then call raster_draw_x_minor_ticks(backend%raster, backend%width, & backend%height, backend%plot_area, & xscale, symlog_threshold, & minor_x(1:num_minor_x), & vx_min, vx_max) end if end if end if if (draw_minor_y) then if (trim(yscale) == 'linear') then call compute_scale_ticks(yscale, vy_min, vy_max, symlog_threshold, & major_y, num_major_y, & step_min=y_min, step_max=y_max) else call compute_scale_ticks(yscale, y_min, y_max, symlog_threshold, & major_y, num_major_y) end if if (num_major_y >= 2) then if (trim(yscale) == 'log') then call calculate_log_minor_tick_positions(major_y, num_major_y, & y_min, y_max, & minor_y, num_minor_y) else call calculate_minor_tick_positions(major_y, num_major_y, & state%minor_tick_count, & vy_min, vy_max, & minor_y, num_minor_y) end if if (num_minor_y > 0) then call raster_draw_y_minor_ticks(backend%raster, backend%width, & backend%height, backend%plot_area, & yscale, symlog_threshold, & minor_y(1:num_minor_y), & vy_min, vy_max) end if end if end if end subroutine render_minor_ticks_raster subroutine render_figure_axes_labels_only(backend, xscale, yscale, & symlog_threshold, & x_min, x_max, y_min, y_max, & title, xlabel, ylabel, & plots, plot_count, has_twinx, & twinx_y_min, twinx_y_max, & twinx_ylabel, twinx_yscale, has_twiny, & twiny_x_min, twiny_x_max, & twiny_xlabel, twiny_xscale, & custom_xticks, custom_xtick_labels, & custom_yticks, custom_ytick_labels, & x_date_format, y_date_format, & twinx_y_date_format, twiny_x_date_format) !! Render ONLY axis labels (for raster and PDF backends after plots are drawn) class(plot_context), intent(inout) :: backend character(len=*), intent(in) :: xscale, yscale real(wp), intent(in) :: symlog_threshold real(wp), intent(in) :: x_min, x_max, y_min, y_max character(len=:), allocatable, intent(in) :: title, xlabel, ylabel type(plot_data_t), intent(in) :: plots(:) integer, intent(in) :: plot_count logical, intent(in), optional :: has_twinx, has_twiny real(wp), intent(in), optional :: twinx_y_min, twinx_y_max real(wp), intent(in), optional :: twiny_x_min, twiny_x_max character(len=:), allocatable, intent(in), optional :: twinx_ylabel, & twiny_xlabel character(len=*), intent(in), optional :: twinx_yscale, twiny_xscale real(wp), intent(in), optional :: custom_xticks(:), custom_yticks(:) character(len=*), intent(in), optional :: custom_xtick_labels(:) character(len=*), intent(in), optional :: custom_ytick_labels(:) character(len=*), intent(in), optional :: x_date_format, y_date_format character(len=*), intent(in), optional :: twinx_y_date_format, & twiny_x_date_format logical :: has_3d real(wp) :: zmin_dummy, zmax_dummy logical :: has_twinx_local, has_twiny_local real(wp) :: twinx_y_min_local, twinx_y_max_local real(wp) :: twiny_x_min_local, twiny_x_max_local character(len=16) :: twinx_scale_local, twiny_scale_local call detect_3d_extent(plots, plot_count, has_3d, zmin_dummy, zmax_dummy) if (has_3d) return call setup_twin_axes_state(has_twinx, has_twiny, twinx_y_min, twinx_y_max, & twiny_x_min, twiny_x_max, twinx_yscale, & twiny_xscale, twinx_ylabel, twiny_xlabel, & xscale, yscale, has_twinx_local, has_twiny_local, & twinx_y_min_local, twinx_y_max_local, & twiny_x_min_local, twiny_x_max_local, & twinx_scale_local, twiny_scale_local) select type (backend) class is (raster_context) ! The title is drawn here, before the secondary top axis ticks set ! raster%last_x_tick_max_height_top. Seed an estimate so the title's ! twiny branch lifts it above the top-axis block; the exact value is ! filled in later for the top xlabel placement. if (has_twiny_local) call seed_top_tick_height(backend) call backend%draw_axis_labels_only(xscale, yscale, symlog_threshold, & x_min, x_max, y_min, y_max, title, & xlabel, ylabel, custom_xticks, & custom_xtick_labels, custom_yticks, & custom_ytick_labels, & x_date_format=x_date_format, & y_date_format=y_date_format) class default end select call render_twin_labels(backend, xscale, yscale, symlog_threshold, & x_min, x_max, y_min, y_max, title, xlabel, & ylabel, custom_xticks, custom_xtick_labels, & custom_yticks, custom_ytick_labels, & has_twinx_local, has_twiny_local, & twinx_scale_local, twiny_scale_local, & twinx_y_min_local, twinx_y_max_local, & twiny_x_min_local, twiny_x_max_local, & x_date_format, y_date_format, & twinx_y_date_format, twiny_x_date_format, & twinx_ylabel, twiny_xlabel, draw_primary_labels=.false.) end subroutine render_figure_axes_labels_only subroutine seed_top_tick_height(backend) !! Pre-seed the top tick-label height used to position the title above a !! twiny block. Matches raster_draw_x_axis_ticks_top, which measures tick !! labels with calculate_text_height; '0' is a representative digit. use fortplot_text, only: calculate_text_height type(raster_context), intent(inout) :: backend integer :: h h = calculate_text_height('0') if (h <= 0) h = 1 backend%raster%last_x_tick_max_height_top = & max(backend%raster%last_x_tick_max_height_top, h) end subroutine seed_top_tick_height subroutine render_title_only(backend, title, x_min, x_max, y_min, y_max, & custom_title_font_size) !! Render only the figure title without drawing axes use fortplot_raster, only: raster_context use fortplot_raster_labels, only: render_title_centered use fortplot_pdf, only: pdf_context use fortplot_pdf_core, only: PDF_TITLE_SIZE use fortplot_pdf_text, only: estimate_pdf_text_width use fortplot_ascii, only: ascii_context class(plot_context), intent(inout) :: backend character(len=:), allocatable, intent(in) :: title real(wp), intent(in) :: x_min, x_max, y_min, y_max real(wp), intent(in), optional :: custom_title_font_size real(wp) :: y_span, y_pos, x_pos if (.not. allocated(title)) return if (len_trim(title) == 0) return select type (backend) class is (raster_context) call render_title_centered(backend%raster, backend%width, & backend%height, backend%plot_area, trim(title), & custom_title_font_size) return class is (pdf_context) block real(wp) :: area_width real(wp) :: area_height real(wp) :: title_width_pdf real(wp) :: x_range real(wp) :: y_range real(wp) :: x_fraction real(wp) :: x_start real(wp) :: y_offset area_width = max(1.0e-9_wp, real(backend%plot_area%width, wp)) area_height = max(1.0e-9_wp, real(backend%plot_area%height, wp)) x_range = max(1.0e-9_wp, x_max - x_min) y_range = max(1.0e-9_wp, y_max - y_min) title_width_pdf = estimate_pdf_text_width(trim(title), PDF_TITLE_SIZE) x_fraction = min(1.0_wp, title_width_pdf/area_width) x_start = x_min + 0.5_wp*x_range*(1.0_wp - x_fraction) y_offset = (20.0_wp/area_height)*y_range call backend%color(0.0_wp, 0.0_wp, 0.0_wp) call backend%text(x_start, y_max + y_offset, trim(title)) end block return class is (ascii_context) call backend%set_title(trim(title)) return class default call backend%color(0.0_wp, 0.0_wp, 0.0_wp) end select y_span = max(1.0e-6_wp, y_max - y_min) x_pos = 0.5_wp*(x_min + x_max) y_pos = y_max + 0.08_wp*y_span call backend%text(x_pos, y_pos, trim(title)) end subroutine render_title_only subroutine detect_3d_extent(plots, plot_count, has_3d, zmin, zmax) !! Detect if any plot is 3D and compute z-range type(plot_data_t), intent(in) :: plots(:) integer, intent(in) :: plot_count logical, intent(out) :: has_3d real(wp), intent(out) :: zmin, zmax integer :: i logical :: first has_3d = .false. first = .true. zmin = 0.0_wp zmax = 1.0_wp do i = 1, plot_count if (plots(i)%is_3d()) then has_3d = .true. if (allocated(plots(i)%z)) then if (size(plots(i)%z) > 0) then if (first) then zmin = minval(plots(i)%z) zmax = maxval(plots(i)%z) first = .false. else zmin = min(zmin, minval(plots(i)%z)) zmax = max(zmax, maxval(plots(i)%z)) end if end if end if if (allocated(plots(i)%z_grid)) then if (size(plots(i)%z_grid) > 0) then if (first) then zmin = minval(plots(i)%z_grid) zmax = maxval(plots(i)%z_grid) first = .false. else zmin = min(zmin, minval(plots(i)%z_grid)) zmax = max(zmax, maxval(plots(i)%z_grid)) end if end if end if end if end do end subroutine detect_3d_extent end module fortplot_figure_rendering_pipeline