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, arrow_data_t, PLOT_TYPE_LINE, & 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_projection, only: project_3d_to_2d, get_default_view_angles use fortplot_rendering, only: render_line_plot, render_contour_plot, & render_pcolormesh_plot, render_fill_between_plot, & render_markers, render_boxplot_plot, & render_errorbar_plot, & render_pie_plot, render_bar_plot use fortplot_legend, only: legend_t use fortplot_surface_rendering, only: render_surface_plot use fortplot_polar_rendering, only: render_polar_data, render_polar_boundary, & render_polar_radial_gridlines, & render_polar_angular_gridlines, & render_polar_angular_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 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) !! Setup the coordinate system for rendering !! Adds a small margin to data ranges to prevent boundary data from !! being clipped by the plot frame stroke. 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 real(wp) :: x_min_adj, x_max_adj, y_min_adj, y_max_adj ! Apply small margin to prevent boundary clipping select type (bk => backend) class is (pdf_context) call expand_data_range(x_min_transformed, x_max_transformed, & x_min_adj, x_max_adj) call expand_data_range(y_min_transformed, y_max_transformed, & y_min_adj, y_max_adj) call bk%set_coordinates(x_min_adj, x_max_adj, y_min_adj, y_max_adj) class is (raster_context) call expand_data_range(x_min_transformed, x_max_transformed, & x_min_adj, x_max_adj) call expand_data_range(y_min_transformed, y_max_transformed, & y_min_adj, y_max_adj) call bk%set_coordinates(x_min_adj, x_max_adj, y_min_adj, y_max_adj) 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) !! Expand a data range by DATA_RANGE_MARGIN (5%) on each side, !! keeping the range center fixed. Prevents markers at exact boundaries !! from being clipped by the plot frame stroke. real(wp), intent(in) :: data_min, data_max real(wp), intent(out) :: expanded_min, expanded_max real(wp) :: center, half_range if (data_max <= data_min) then expanded_min = data_min expanded_max = data_max return end if center = 0.5_wp*(data_min + data_max) half_range = 0.5_wp*(data_max - data_min) half_range = half_range*(1.0_wp + DATA_RANGE_MARGIN) expanded_min = center - half_range expanded_max = center + half_range 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 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 if (.not. state%minor_ticks_x .and. .not. state%minor_ticks_y) return ! Get major tick positions if (state%minor_ticks_x) then call compute_scale_ticks(xscale, x_min, x_max, symlog_threshold, & major_x, num_major_x) 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, & x_min, x_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), & x_min, x_max) end if end if end if if (state%minor_ticks_y) then call compute_scale_ticks(yscale, y_min, y_max, symlog_threshold, & major_y, num_major_y) 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, & y_min, y_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), & y_min, y_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) 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 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