fortplot_figure_render_steps.f90 Source File


Source Code

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