module fortplot_figure_render_engine !! Shared rendering engine for figure outputs. !! !! Centralises single-axis and subplot rendering so the functional and !! object-oriented APIs share identical drawing behaviour. use, intrinsic :: iso_fortran_env, only: wp => real64 use fortplot_plot_data, only: plot_data_t, subplot_data_t, AXIS_PRIMARY, & AXIS_TWINX, AXIS_TWINY, PLOT_TYPE_PIE use fortplot_figure_initialization, only: figure_state_t use fortplot_figure_rendering_pipeline, only: calculate_figure_data_ranges use fortplot_figure_aspect, only: contains_pie_plot, enforce_pie_axis_equal, & only_pie_plots use fortplot_margins, only: calculate_plot_area, plot_area_t use fortplot_figure_colorbar, only: prepare_colorbar_layout, & resolve_colorbar_mappable use fortplot_subplot_rendering, only: render_subplots use fortplot_png, only: png_context use fortplot_pdf, only: pdf_context use fortplot_ascii, only: ascii_context, ASCII_CHAR_ASPECT use fortplot_figure_render_steps, only: & render_background_and_grid, render_axes_and_plots, & render_labels_overlay, render_decorations, & apply_aspect_ratio_if_needed, resolve_plot_colorbar_request use fortplot_raster, only: raster_context implicit none private public :: figure_render contains subroutine figure_render(state, plots, plot_count, annotations, annotation_count, & subplots_array, subplot_rows, subplot_cols) use fortplot_annotations, only: text_annotation_t type(figure_state_t), intent(inout) :: state type(plot_data_t), intent(inout) :: plots(:) integer, intent(in) :: plot_count type(text_annotation_t), intent(in), optional :: annotations(:) integer, intent(in), optional :: annotation_count type(subplot_data_t), intent(in), optional :: subplots_array(:, :) integer, intent(in), optional :: subplot_rows, subplot_cols logical :: have_subplots have_subplots = .false. if (present(subplots_array) .and. present(subplot_rows) .and. & present(subplot_cols)) then if (size(subplots_array, 1) == subplot_rows .and. & size(subplots_array, 2) == subplot_cols) then have_subplots = subplot_rows > 0 .and. subplot_cols > 0 end if end if if (have_subplots) then call render_subplots(state, subplots_array, subplot_rows, subplot_cols) else call render_single_axis(state, plots, plot_count, annotations, & annotation_count) end if state%rendered = .true. end subroutine figure_render subroutine render_single_axis(state, plots, plot_count, annotations, & annotation_count) use fortplot_annotations, only: text_annotation_t type(figure_state_t), intent(inout) :: state type(plot_data_t), intent(inout) :: plots(:) integer, intent(in) :: plot_count type(text_annotation_t), intent(in), optional :: annotations(:) integer, intent(in), optional :: annotation_count logical :: pie_only, ascii_backend, have_colorbar, plot_area_supported type(plot_area_t) :: saved_plot_area, colorbar_plot_area real(wp) :: cbar_vmin, cbar_vmax character(len=20) :: cbar_colormap character(len=64) :: x_date_format, y_date_format character(len=64) :: twinx_y_date_format, twiny_x_date_format call apply_raster_config(state) call compute_all_data_ranges(state, plots, plot_count) call resolve_date_formats(state, x_date_format, y_date_format, & twinx_y_date_format, twiny_x_date_format) call resolve_pie_and_aspect(state, plots, plot_count, & pie_only, ascii_backend) call resolve_colorbar_layout(state, plots, plot_count, have_colorbar, & saved_plot_area, colorbar_plot_area, & cbar_vmin, cbar_vmax, cbar_colormap, & plot_area_supported) call render_background_and_grid(state, ascii_backend) call render_axes_and_plots(state, plots, plot_count, pie_only) call render_labels_overlay(state, plots, plot_count, pie_only, & x_date_format, y_date_format, & twinx_y_date_format, twiny_x_date_format) call render_decorations(state, plots, plot_count, have_colorbar, & colorbar_plot_area, cbar_vmin, cbar_vmax, & cbar_colormap, saved_plot_area, & plot_area_supported, annotations, & annotation_count) end subroutine render_single_axis subroutine apply_raster_config(state) type(figure_state_t), intent(inout) :: state integer :: i, n select type (bk => state%backend) class is (raster_context) bk%raster%config_title_font_size = state%title_font_size bk%raster%config_label_font_size = state%label_font_size bk%raster%config_tick_font_size = state%tick_font_size if (state%custom_xticks_set .and. & allocated(state%custom_xtick_positions)) then bk%raster%config_xtick_values = state%custom_xtick_positions end if if (state%custom_yticks_set .and. & allocated(state%custom_ytick_positions)) then bk%raster%config_ytick_values = state%custom_ytick_positions end if class is (ascii_context) if (allocated(bk%custom_xtick_positions)) & deallocate (bk%custom_xtick_positions) if (allocated(bk%custom_xtick_labels)) & deallocate (bk%custom_xtick_labels) if (state%custom_xticks_set .and. & allocated(state%custom_xtick_positions) .and. & allocated(state%custom_xtick_labels)) then n = min(size(state%custom_xtick_positions), & size(state%custom_xtick_labels)) allocate (bk%custom_xtick_positions(n)) allocate (bk%custom_xtick_labels(n)) do i = 1, n bk%custom_xtick_positions(i) = state%custom_xtick_positions(i) bk%custom_xtick_labels(i) = state%custom_xtick_labels(i) end do end if class is (pdf_context) if (allocated(bk%custom_xtick_positions)) & deallocate (bk%custom_xtick_positions) if (allocated(bk%custom_ytick_positions)) & deallocate (bk%custom_ytick_positions) if (allocated(bk%custom_xtick_labels)) & deallocate (bk%custom_xtick_labels) if (allocated(bk%custom_ytick_labels)) & deallocate (bk%custom_ytick_labels) if (state%custom_xticks_set .and. & allocated(state%custom_xtick_positions) .and. & allocated(state%custom_xtick_labels)) then n = min(size(state%custom_xtick_positions), & size(state%custom_xtick_labels)) allocate (bk%custom_xtick_positions(n)) allocate (bk%custom_xtick_labels(n)) do i = 1, n bk%custom_xtick_positions(i) = state%custom_xtick_positions(i) bk%custom_xtick_labels(i) = state%custom_xtick_labels(i) end do end if if (state%custom_yticks_set .and. & allocated(state%custom_ytick_positions) .and. & allocated(state%custom_ytick_labels)) then n = min(size(state%custom_ytick_positions), & size(state%custom_ytick_labels)) allocate (bk%custom_ytick_positions(n)) allocate (bk%custom_ytick_labels(n)) do i = 1, n bk%custom_ytick_positions(i) = state%custom_ytick_positions(i) bk%custom_ytick_labels(i) = state%custom_ytick_labels(i) end do end if class default end select end subroutine apply_raster_config subroutine compute_all_data_ranges(state, plots, plot_count) type(figure_state_t), intent(inout) :: state type(plot_data_t), intent(inout) :: plots(:) integer, intent(in) :: plot_count real(wp) :: x_min_dummy, x_max_dummy, y_min_dummy, y_max_dummy real(wp) :: twinx_y_min, twinx_y_max real(wp) :: twinx_y_min_trans, twinx_y_max_trans real(wp) :: twiny_x_min, twiny_x_max real(wp) :: twiny_x_min_trans, twiny_x_max_trans call calculate_figure_data_ranges(plots, plot_count, & state%xlim_set, state%ylim_set, & state%x_min, state%x_max, & state%y_min, state%y_max, & state%x_min_transformed, & state%x_max_transformed, & state%y_min_transformed, & state%y_max_transformed, & state%xscale, state%yscale, & state%symlog_threshold, & state%symlog_base, state%symlog_linscale, & axis_filter=AXIS_PRIMARY) if (state%has_twinx) then x_min_dummy = state%x_min x_max_dummy = state%x_max twinx_y_min = state%twinx_y_min twinx_y_max = state%twinx_y_max call calculate_figure_data_ranges(plots, plot_count, & xlim_set=.true., & ylim_set=state%twinx_ylim_set, & x_min=x_min_dummy, x_max=x_max_dummy, & y_min=twinx_y_min, y_max=twinx_y_max, & x_min_transformed=x_min_dummy, & x_max_transformed=x_max_dummy, & y_min_transformed=twinx_y_min_trans, & y_max_transformed=twinx_y_max_trans, & xscale=state%xscale, & yscale=state%twinx_yscale, & symlog_threshold=state%symlog_threshold, & symlog_base=state%symlog_base, & symlog_linscale=state%symlog_linscale, & axis_filter=AXIS_TWINX) state%twinx_y_min = twinx_y_min state%twinx_y_max = twinx_y_max state%twinx_y_min_transformed = twinx_y_min_trans state%twinx_y_max_transformed = twinx_y_max_trans end if if (state%has_twiny) then twiny_x_min = state%twiny_x_min twiny_x_max = state%twiny_x_max y_min_dummy = state%y_min y_max_dummy = state%y_max call calculate_figure_data_ranges(plots, plot_count, & xlim_set=state%twiny_xlim_set, & ylim_set=.true., & x_min=twiny_x_min, x_max=twiny_x_max, & y_min=y_min_dummy, y_max=y_max_dummy, & x_min_transformed=twiny_x_min_trans, & x_max_transformed=twiny_x_max_trans, & y_min_transformed=y_min_dummy, & y_max_transformed=y_max_dummy, & xscale=state%twiny_xscale, & yscale=state%yscale, & symlog_threshold=state%symlog_threshold, & symlog_base=state%symlog_base, & symlog_linscale=state%symlog_linscale, & axis_filter=AXIS_TWINY) state%twiny_x_min = twiny_x_min state%twiny_x_max = twiny_x_max state%twiny_x_min_transformed = twiny_x_min_trans state%twiny_x_max_transformed = twiny_x_max_trans end if end subroutine compute_all_data_ranges subroutine resolve_date_formats(state, x_fmt, y_fmt, twinx_fmt, twiny_fmt) type(figure_state_t), intent(in) :: state character(len=64), intent(out) :: x_fmt, y_fmt, twinx_fmt, twiny_fmt x_fmt = ''; y_fmt = ''; twinx_fmt = ''; twiny_fmt = '' if (allocated(state%xaxis_date_format)) x_fmt = state%xaxis_date_format if (allocated(state%yaxis_date_format)) y_fmt = state%yaxis_date_format if (allocated(state%twinx_yaxis_date_format)) twinx_fmt = state%twinx_yaxis_date_format if (allocated(state%twiny_xaxis_date_format)) twiny_fmt = state%twiny_xaxis_date_format end subroutine resolve_date_formats subroutine resolve_pie_and_aspect(state, plots, plot_count, pie_only, ascii_bk) type(figure_state_t), intent(inout) :: state type(plot_data_t), intent(in) :: plots(:) integer, intent(in) :: plot_count logical, intent(out) :: pie_only, ascii_bk logical :: has_pie, have_pie_px real(wp) :: pw, ph pie_only = .false.; ascii_bk = .false. has_pie = contains_pie_plot(plots, plot_count) if (has_pie) then have_pie_px = .false. select type (bk => state%backend) class is (png_context) pw = real(max(1, bk%plot_area%width), wp) ph = real(max(1, bk%plot_area%height), wp) have_pie_px = .true. class is (pdf_context) pw = real(max(1, bk%plot_area%width), wp) ph = real(max(1, bk%plot_area%height), wp) have_pie_px = .true. class is (ascii_context) pw = real(max(1, bk%plot_width - 3), wp) ph = real(max(1, bk%plot_height - 3), wp) * ASCII_CHAR_ASPECT have_pie_px = .true.; ascii_bk = .true. class default end select if (have_pie_px) then call enforce_pie_axis_equal(state, pw, ph) else call enforce_pie_axis_equal(state) end if pie_only = only_pie_plots(plots, plot_count) else call apply_aspect_ratio_if_needed(state, has_pie) end if end subroutine resolve_pie_and_aspect subroutine resolve_colorbar_layout(state, plots, plot_count, have_cbar, & saved_pa, cbar_pa, vmin, vmax, cmap, & pa_supported) type(figure_state_t), intent(inout) :: state type(plot_data_t), intent(in) :: plots(:) integer, intent(in) :: plot_count logical, intent(out) :: have_cbar, pa_supported type(plot_area_t), intent(out) :: saved_pa, cbar_pa real(wp), intent(out) :: vmin, vmax character(len=20), intent(out) :: cmap logical :: have_mappable integer :: mappable_index type(plot_area_t) :: main_pa have_cbar = state%colorbar_enabled have_mappable = .false.; pa_supported = .false. if (.not. have_cbar) then call resolve_plot_colorbar_request(plots, plot_count, & state%colorbar_enabled, & state%colorbar_plot_index) have_cbar = state%colorbar_enabled end if if (have_cbar) then call resolve_colorbar_mappable(plots, plot_count, & state%colorbar_plot_index, & mappable_index, vmin, vmax, cmap, & have_mappable) have_cbar = have_mappable end if if (have_cbar) then call prepare_colorbar_layout(state%backend, state%colorbar_location, & state%colorbar_fraction, state%colorbar_pad, & state%colorbar_shrink, saved_pa, main_pa, & cbar_pa, pa_supported) if (.not. pa_supported) have_cbar = .false. end if end subroutine resolve_colorbar_layout end module fortplot_figure_render_engine