module fortplot_figure_render_steps !! Rendering step procedures extracted from fortplot_figure_render_engine !! for size compliance (refs #1694) use, intrinsic :: iso_fortran_env, only: wp => real64 use fortplot_plot_data, only: plot_data_t, PLOT_TYPE_PIE, & PLOT_TYPE_CONTOUR, PLOT_TYPE_SURFACE, & PLOT_TYPE_SCATTER, PLOT_TYPE_PCOLORMESH, & colorbar_config_t use fortplot_context, only: plot_context use fortplot_figure_initialization, only: figure_state_t use fortplot_figure_rendering_pipeline, only: setup_coordinate_system, & render_figure_background, & render_figure_axes, & render_all_plots, & render_streamplot_arrows, & render_figure_axes_labels_only, & render_title_only, & render_polar_axes, & render_3d_front_frame, & render_ascii_grid use fortplot_figure_grid, only: render_grid_lines use fortplot_annotation_rendering, only: render_figure_annotations use fortplot_figure_aspect, only: enforce_aspect_ratio use fortplot_margins, only: plot_area_t use fortplot_figure_colorbar, only: render_colorbar, resolve_colorbar_mappable use fortplot_figure_contour_colorbar, only: get_contour_colorbar_levels use fortplot_png, only: png_context use fortplot_pdf, only: pdf_context use fortplot_ascii, only: ascii_context, ASCII_CHAR_ASPECT use fortplot_legend, only: legend_render use fortplot_legend_best, only: resolve_best_legend_position use fortplot_legend_drawing, only: legend_plot_pixel_dimensions implicit none private public :: render_background_and_grid public :: axes_box_pixels public :: render_axes_and_plots public :: render_labels_overlay public :: render_decorations public :: regenerate_pie_legend_for_backend public :: apply_aspect_ratio_if_needed public :: render_colorbar_with_config public :: resolve_plot_colorbar_request contains subroutine render_background_and_grid(state, ascii_bk) type(figure_state_t), intent(inout) :: state logical, intent(in) :: ascii_bk ! Propagate stored 3D view angles so render-time projection (axes, ! surfaces) matches the figure's current view. state%backend%view_azim = state%view_azim state%backend%view_elev = state%view_elev state%backend%view_dist = state%view_dist call setup_coordinate_system(state%backend, & state%x_min_transformed, state%x_max_transformed, & state%y_min_transformed, state%y_max_transformed, & sticky_x_min=state%sticky_x_min, & sticky_x_max=state%sticky_x_max, & sticky_y_min=state%sticky_y_min, & sticky_y_max=state%sticky_y_max) call render_figure_background(state%backend) if (state%grid_enabled .and. .not. state%polar_projection .and. .not. ascii_bk) then call render_grid_lines(state%backend, state%grid_enabled, & state%grid_which, state%grid_axis, & state%grid_alpha, state%width, state%height, & state%margin_left, state%margin_right, & state%margin_bottom, state%margin_top, & state%xscale, state%yscale, & state%symlog_threshold, 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%grid_linestyle, state%grid_color, & sticky_x_min=state%sticky_x_min, & sticky_x_max=state%sticky_x_max, & sticky_y_min=state%sticky_y_min, & sticky_y_max=state%sticky_y_max) end if if (state%polar_projection) then call render_polar_axes(state%backend, state%x_min_transformed, & state%x_max_transformed, & state%y_min_transformed, & state%y_max_transformed, state) end if end subroutine render_background_and_grid subroutine render_axes_and_plots(state, plots, plot_count, pie_only) type(figure_state_t), intent(inout) :: state type(plot_data_t), intent(inout) :: plots(:) integer, intent(in) :: plot_count logical, intent(in) :: pie_only integer :: ip logical :: any_3d, axes_after_data any_3d = .false. do ip = 1, plot_count if (plots(ip)%is_3d()) any_3d = .true. end do axes_after_data = .not. any_3d .and. .not. pie_only .and. & .not. state%polar_projection select type (backend => state%backend) class is (ascii_context) axes_after_data = .false. end select if (.not. pie_only .and. .not. state%polar_projection) then if (.not. axes_after_data) call render_primary_axes(state, plots, plot_count) else call render_title_only(state%backend, state%title, state%x_min, & state%x_max, state%y_min, state%y_max, & state%title_font_size) end if if (plot_count > 0) then call render_all_plots(state%backend, plots, plot_count, & state%x_min_transformed, state%x_max_transformed, & state%y_min_transformed, state%y_max_transformed, & state%xscale, state%yscale, state%symlog_threshold, & state%width, state%height, & state%margin_left, state%margin_right, & state%margin_bottom, state%margin_top, state=state) ! Front box spines paint over the data so near frame edges occlude ! curves and surfaces (global painter ordering, refs #1956). if (.not. pie_only .and. .not. state%polar_projection) then call render_3d_front_frame(state%backend, plots, plot_count, & state%x_min, state%x_max, & state%y_min, state%y_max) end if end if if (state%grid_enabled .and. .not. pie_only & .and. .not. state%polar_projection .and. .not. any_3d) then call render_ascii_grid(state%backend, state%xscale, state%yscale, & state%symlog_threshold, state%x_min, & state%x_max, state%y_min, state%y_max, & state%grid_axis, state%grid_which) end if if (allocated(state%stream_arrows)) then if (size(state%stream_arrows) > 0) then call render_streamplot_arrows(state%backend, state%stream_arrows) end if end if ! Matplotlib spines have zorder 2.5, above 2D data and stream arrows. ! Draw this pass once so clipped fills cannot cover the axis border. if (axes_after_data) call render_primary_axes(state, plots, plot_count) end subroutine render_axes_and_plots subroutine render_primary_axes(state, plots, plot_count) type(figure_state_t), intent(inout) :: state type(plot_data_t), intent(in) :: plots(:) integer, intent(in) :: plot_count call render_figure_axes(state%backend, state%xscale, state%yscale, & state%symlog_threshold, state%x_min, state%x_max, & state%y_min, state%y_max, state%title, & state%xlabel, state%ylabel, plots, plot_count, & has_twinx=state%has_twinx, & twinx_y_min=state%twinx_y_min, & twinx_y_max=state%twinx_y_max, & twinx_ylabel=state%twinx_ylabel, & twinx_yscale=state%twinx_yscale, & has_twiny=state%has_twiny, & twiny_x_min=state%twiny_x_min, & twiny_x_max=state%twiny_x_max, & twiny_xlabel=state%twiny_xlabel, & twiny_xscale=state%twiny_xscale, & state=state) end subroutine render_primary_axes subroutine render_labels_overlay(state, plots, plot_count, pie_only, & x_fmt, y_fmt, twinx_fmt, twiny_fmt) type(figure_state_t), intent(inout) :: state type(plot_data_t), intent(in) :: plots(:) integer, intent(in) :: plot_count logical, intent(in) :: pie_only character(len=64), intent(in) :: x_fmt, y_fmt, twinx_fmt, twiny_fmt if (pie_only .or. state%polar_projection) return call render_figure_axes_labels_only( & state%backend, state%xscale, state%yscale, & state%symlog_threshold, & state%x_min, state%x_max, state%y_min, state%y_max, & state%title, state%xlabel, state%ylabel, plots, plot_count, & has_twinx=state%has_twinx, & twinx_y_min=state%twinx_y_min, twinx_y_max=state%twinx_y_max, & twinx_ylabel=state%twinx_ylabel, twinx_yscale=state%twinx_yscale, & has_twiny=state%has_twiny, & twiny_x_min=state%twiny_x_min, twiny_x_max=state%twiny_x_max, & twiny_xlabel=state%twiny_xlabel, twiny_xscale=state%twiny_xscale, & custom_xticks=state%custom_xtick_positions, & custom_xtick_labels=state%custom_xtick_labels, & custom_yticks=state%custom_ytick_positions, & custom_ytick_labels=state%custom_ytick_labels, & x_date_format=x_fmt, y_date_format=y_fmt, & twinx_y_date_format=twinx_fmt, twiny_x_date_format=twiny_fmt) end subroutine render_labels_overlay subroutine render_decorations(state, plots, plot_count, have_cbar, & cbar_pa, vmin, vmax, cmap, saved_pa, & pa_supported, annotations, ann_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 logical, intent(in) :: have_cbar, pa_supported type(plot_area_t), intent(in) :: cbar_pa, saved_pa real(wp), intent(in) :: vmin, vmax character(len=20), intent(in) :: cmap type(text_annotation_t), intent(in), optional :: annotations(:) integer, intent(in), optional :: ann_count integer :: mappable_index real(wp) :: mapped_min, mapped_max, contour_width real(wp), allocatable :: line_levels(:) character(len=20) :: mapped_cmap logical :: mapped if (have_cbar) then call resolve_colorbar_mappable(plots, plot_count, & state%colorbar%plot_index, mappable_index, mapped_min, mapped_max, & mapped_cmap, mapped) if (mapped) then if (plots(mappable_index)%plot_type == PLOT_TYPE_CONTOUR .and. & .not. plots(mappable_index)%fill_contours) then contour_width = state%current_line_width if (plots(mappable_index)%line_width > 0.0_wp) & contour_width = plots(mappable_index)%line_width call get_contour_colorbar_levels(plots(mappable_index), line_levels) call render_colorbar_with_config(state%backend, state%colorbar, & cbar_pa, vmin, vmax, cmap, line_levels, contour_width) else call render_colorbar_with_config(state%backend, state%colorbar, & cbar_pa, vmin, vmax, cmap) end if end if end if if (state%show_legend .and. state%legend_data%num_entries > 0) then call regenerate_pie_legend_for_backend(state, plots, plot_count) call resolve_best_legend_for_state(state, plots, plot_count) call legend_render(state%legend_data, state%backend) end if if (present(annotations) .and. present(ann_count)) then if (ann_count > 0) then call render_figure_annotations(state%backend, annotations, & ann_count, & state%x_min, state%x_max, & state%y_min, state%y_max, & state%width, state%height, state%dpi, & state%margin_left, state%margin_right, & state%margin_bottom, state%margin_top) end if end if if (have_cbar .and. pa_supported) then select type (bk => state%backend) class is (png_context) bk%plot_area = saved_pa class is (pdf_context) bk%plot_area = saved_pa class default end select end if end subroutine render_decorations subroutine regenerate_pie_legend_for_backend(state, plots, plot_count) use fortplot_figure_plot_management, only: setup_figure_legend type(figure_state_t), intent(inout) :: state type(plot_data_t), intent(in) :: plots(:) integer, intent(in) :: plot_count integer :: i logical :: has_pie_charts has_pie_charts = .false. do i = 1, plot_count if (plots(i)%plot_type == PLOT_TYPE_PIE) then has_pie_charts = .true. exit end if end do if (has_pie_charts) then ! Place the pie legend inside the axes (matplotlib default). The ! 'east' location anchored the box at x_max + margin, outside the ! frame, so it was clipped. 'best' resolves to an inside corner ! using real free pixels, independent of the pie axis-equal range. call state%legend_data%clear() call setup_figure_legend(state%legend_data, state%show_legend, & plots, plot_count, 'best', state%backend_name) end if end subroutine regenerate_pie_legend_for_backend subroutine resolve_best_legend_for_state(state, plots, plot_count) !! Resolve a 'best' legend to a concrete anchor using the current !! backend data window and plot area. No-op for explicit placements. type(figure_state_t), intent(inout) :: state type(plot_data_t), intent(in) :: plots(:) integer, intent(in) :: plot_count integer :: px_w, px_h call legend_plot_pixel_dimensions(state%backend, px_w, px_h) call resolve_best_legend_position(state%legend_data, plots, plot_count, & state%backend%x_min, state%backend%x_max, & state%backend%y_min, state%backend%y_max, px_w, px_h) end subroutine resolve_best_legend_for_state subroutine apply_aspect_ratio_if_needed(state, has_pie_plots) type(figure_state_t), intent(inout) :: state logical, intent(in) :: has_pie_plots real(wp) :: plot_width_px, plot_height_px if (has_pie_plots) return if (state%aspect_mode == 'auto') return if (.not. axes_box_pixels(state%backend, plot_width_px, plot_height_px)) & return call enforce_aspect_ratio(state, plot_width_px, plot_height_px) end subroutine apply_aspect_ratio_if_needed logical function axes_box_pixels(backend, plot_width_px, plot_height_px) & result(ok) !! Physical width and height of the current axes box, in units that !! are equal in both directions (ASCII rows are scaled by the cell !! aspect). False for backends without a plot area. class(plot_context), intent(in) :: backend real(wp), intent(out) :: plot_width_px, plot_height_px ok = .true. select type (bk => backend) class is (png_context) plot_width_px = real(max(1, bk%plot_area%width), wp) plot_height_px = real(max(1, bk%plot_area%height), wp) class is (pdf_context) plot_width_px = real(max(1, bk%plot_area%width), wp) plot_height_px = real(max(1, bk%plot_area%height), wp) class is (ascii_context) plot_width_px = real(max(1, bk%plot_width - 3), wp) plot_height_px = real(max(1, bk%plot_height - 3), wp)*ASCII_CHAR_ASPECT class default plot_width_px = 0.0_wp plot_height_px = 0.0_wp ok = .false. end select end function axes_box_pixels subroutine render_colorbar_with_config(backend, cfg, plot_area, vmin, vmax, colormap, & line_levels, line_width) class(plot_context), intent(inout) :: backend type(colorbar_config_t), intent(in) :: cfg type(plot_area_t), intent(in) :: plot_area real(wp), intent(in) :: vmin, vmax character(len=*), intent(in) :: colormap real(wp), intent(in), optional :: line_levels(:), line_width if (cfg%ticks_set .and. cfg%ticklabels_set) then if (cfg%label_set) then call render_colorbar(backend, plot_area, vmin, vmax, & colormap, cfg%location, & cfg%label, cfg%ticks, & cfg%ticklabels, & cfg%label_fontsize, & line_levels=line_levels, line_width=line_width) else call render_colorbar(backend, plot_area, vmin, vmax, & colormap, cfg%location, & custom_ticks=cfg%ticks, & custom_ticklabels=cfg%ticklabels, & line_levels=line_levels, line_width=line_width) end if else if (cfg%ticks_set) then if (cfg%label_set) then call render_colorbar(backend, plot_area, vmin, vmax, & colormap, cfg%location, & cfg%label, cfg%ticks, & label_fontsize=cfg%label_fontsize, & line_levels=line_levels, line_width=line_width) else call render_colorbar(backend, plot_area, vmin, vmax, & colormap, cfg%location, & custom_ticks=cfg%ticks, & line_levels=line_levels, line_width=line_width) end if else if (cfg%label_set) then call render_colorbar(backend, plot_area, vmin, vmax, & colormap, cfg%location, & cfg%label, & label_fontsize=cfg%label_fontsize, & line_levels=line_levels, line_width=line_width) else call render_colorbar(backend, plot_area, vmin, vmax, & colormap, cfg%location, & line_levels=line_levels, line_width=line_width) end if end subroutine render_colorbar_with_config subroutine resolve_plot_colorbar_request(plots, plot_count, enabled, plot_idx) type(plot_data_t), intent(in) :: plots(:) integer, intent(in) :: plot_count logical, intent(out) :: enabled integer, intent(out) :: plot_idx integer :: i enabled = .false. plot_idx = 0 do i = 1, plot_count if (plots(i)%plot_type == PLOT_TYPE_CONTOUR) then if (plots(i)%fill_contours .and. plots(i)%show_colorbar) then if (allocated(plots(i)%z_grid)) then if (size(plots(i)%z_grid) > 0) then enabled = .true. plot_idx = i return end if end if end if end if if (plots(i)%plot_type == PLOT_TYPE_SURFACE) then if (plots(i)%surface_show_colorbar) then if (allocated(plots(i)%z_grid)) then if (size(plots(i)%z_grid) > 0) then enabled = .true. plot_idx = i return end if end if end if end if if (plots(i)%plot_type == PLOT_TYPE_SCATTER) then if (plots(i)%scatter_colorbar) then if (allocated(plots(i)%scatter_colors)) then if (size(plots(i)%scatter_colors) > 0) then enabled = .true. plot_idx = i return end if end if end if end if if (plots(i)%plot_type == PLOT_TYPE_PCOLORMESH) then if (plots(i)%show_colorbar) then if (allocated(plots(i)%pcolormesh_data%c_values)) then if (size(plots(i)%pcolormesh_data%c_values) > 0) then enabled = .true. plot_idx = i return end if end if end if end if end do end subroutine resolve_plot_colorbar_request end module fortplot_figure_render_steps