fortplot_pdf_axes.f90 Source File


Source Code

module fortplot_pdf_axes
    !! PDF axes, grid, tick drawing, and plot frame operations
    !!
    !! Thin coordinator module that delegates to specialized sub-modules:
    !! - fortplot_pdf_axes_tick_data: tick position/label computation
    !! - fortplot_pdf_axes_drawing: frame, tick marks, minor ticks
    !! - fortplot_pdf_axes_text: title/label text rendering with mathtext

    use iso_fortran_env, only: wp => real64
    use fortplot_pdf_core, only: pdf_context_core, PDF_MARGIN, &
                                 PDF_TICK_SIZE, PDF_LABEL_SIZE, &
                                 PDF_TICK_LABEL_SIZE, PDF_TITLE_SIZE
    use fortplot_constants, only: XLABEL_VERTICAL_OFFSET
    use fortplot_pdf_drawing, only: pdf_stream_writer
    use fortplot_pdf_text, only: draw_pdf_text, draw_pdf_text_bold, &
                                 draw_mixed_font_text, draw_rotated_mixed_font_text, &
                                 draw_pdf_mathtext, estimate_pdf_text_width
    use fortplot_text_helpers, only: prepare_mathtext_if_needed
    use fortplot_text_layout, only: has_mathtext, preprocess_math_text
    use fortplot_latex_parser, only: process_latex_in_text
    use fortplot_mathtext, only: mathtext_element_t, parse_mathtext
    use fortplot_pdf_mathtext_render, only: render_mathtext_element_pdf
    use fortplot_unicode, only: utf8_char_length, utf8_to_codepoint
    use fortplot_axes, only: compute_scale_ticks, format_tick_label, MAX_TICKS
    use fortplot_tick_calculation, only: determine_decimals_from_ticks, &
                                         format_tick_value_consistent
    use fortplot_scales, only: apply_scale_transform
    ! Sub-modules
    use fortplot_pdf_axes_tick_data, only: initialize_tick_arrays, &
                                           generate_x_axis_ticks, &
                                           generate_y_axis_ticks, &
                                           apply_custom_axis_ticks, &
                                           generate_axis_ticks_internal, &
                                           subsample_ticks, &
                                           fill_tick_positions_and_labels, &
                                           handle_zero_range_ticks
    use fortplot_pdf_axes_drawing, only: draw_pdf_frame_with_area, &
                                         draw_pdf_tick_marks_with_area, &
                                         draw_pdf_tick_labels_with_area, &
                                         draw_pdf_minor_tick_marks
    use fortplot_pdf_axes_text, only: draw_pdf_title_and_labels, &
                                      render_mixed_text, &
                                      render_rotated_mixed_text
    implicit none
    private

    ! Public procedures (re-exported from sub-modules)
    public :: draw_pdf_axes_and_labels
    public :: draw_pdf_frame_with_area
    public :: draw_pdf_tick_marks_with_area
    public :: draw_pdf_tick_labels_with_area
    public :: draw_pdf_title_and_labels
    public :: setup_axes_data_ranges
    public :: generate_tick_data
    public :: render_mixed_text
    public :: draw_pdf_minor_tick_marks

contains

    subroutine setup_axes_data_ranges(ctx, x_min_orig, x_max_orig, y_min_orig, &
                                      y_max_orig, &
                                      x_min_adj, x_max_adj, y_min_adj, y_max_adj, &
                                      xscale, yscale)
        !! Set up data ranges for axes with optional log scaling
        type(pdf_context_core), intent(inout) :: ctx
        real(wp), intent(in) :: x_min_orig, x_max_orig, y_min_orig, y_max_orig
        real(wp), intent(out) :: x_min_adj, x_max_adj, y_min_adj, y_max_adj
        character(len=*), intent(in), optional :: xscale, yscale

        real(wp) :: x_range, y_range

        ! Initialize adjusted values
        x_min_adj = x_min_orig
        x_max_adj = x_max_orig
        y_min_adj = y_min_orig
        y_max_adj = y_max_orig

        ! Apply log scaling if requested
        if (present(xscale)) then
            if (xscale == 'log' .and. x_min_adj > 0.0_wp) then
                x_min_adj = log10(x_min_adj)
                x_max_adj = log10(x_max_adj)
            end if
        end if

        if (present(yscale)) then
            if (yscale == 'log' .and. y_min_adj > 0.0_wp) then
                y_min_adj = log10(y_min_adj)
                y_max_adj = log10(y_max_adj)
            end if
        end if

        ! Ensure valid ranges
        x_range = x_max_adj - x_min_adj
        y_range = y_max_adj - y_min_adj

        if (abs(x_range) < 1.0e-10_wp) then
            x_min_adj = x_min_adj - 0.5_wp
            x_max_adj = x_max_adj + 0.5_wp
        end if

        if (abs(y_range) < 1.0e-10_wp) then
            y_min_adj = y_min_adj - 0.5_wp
            y_max_adj = y_max_adj + 0.5_wp
        end if
    end subroutine setup_axes_data_ranges

  subroutine generate_tick_data(ctx, data_x_min, data_x_max, data_y_min, data_y_max, &
                                   x_positions, y_positions, x_labels, y_labels, &
                                   num_x_ticks, num_y_ticks, xscale, yscale, &
                                   x_date_format, y_date_format, &
                                   plot_area_left, plot_area_bottom, plot_area_width, &
                                   plot_area_height, &
                                   symlog_threshold, &
                                   custom_xticks, custom_xtick_labels, &
                                   custom_yticks, custom_ytick_labels)
        !! Generate tick positions and labels for axes
        type(pdf_context_core), intent(in) :: ctx
        real(wp), intent(in) :: data_x_min, data_x_max, data_y_min, data_y_max
        real(wp), allocatable, intent(out) :: x_positions(:), y_positions(:)
        character(len=50), allocatable, intent(out) :: x_labels(:), y_labels(:)
        integer, intent(out) :: num_x_ticks, num_y_ticks
        character(len=*), intent(in), optional :: xscale, yscale
        character(len=*), intent(in), optional :: x_date_format, y_date_format
        real(wp), intent(in) :: plot_area_left, plot_area_bottom, plot_area_width, &
                                plot_area_height
        real(wp), intent(in), optional :: symlog_threshold
        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(:)

        ! Calculate number of ticks and allocate arrays
        call initialize_tick_arrays(plot_area_width, plot_area_height, num_x_ticks, &
                                    num_y_ticks, &
                                    x_positions, y_positions, x_labels, y_labels)

        ! Generate X axis ticks
        call generate_x_axis_ticks(data_x_min, data_x_max, num_x_ticks, &
                                   plot_area_left, &
                                   plot_area_width, x_positions, x_labels, xscale, &
                                   date_format=x_date_format, &
                                   symlog_threshold=symlog_threshold, &
                                   custom_xticks=custom_xticks, &
                                   custom_xtick_labels=custom_xtick_labels)

        ! Generate Y axis ticks
        call generate_y_axis_ticks(data_y_min, data_y_max, num_y_ticks, &
                                   plot_area_bottom, &
                                   plot_area_height, y_positions, y_labels, yscale, &
                                   date_format=y_date_format, &
                                   symlog_threshold=symlog_threshold, &
                                   custom_yticks=custom_yticks, &
                                   custom_ytick_labels=custom_ytick_labels)

    end subroutine generate_tick_data

    subroutine draw_pdf_axes_and_labels(ctx, xscale, yscale, symlog_threshold, &
                                        data_x_min, data_x_max, data_y_min, &
                                        data_y_max, title, xlabel, ylabel, &
                                        x_date_format, y_date_format, &
                                        plot_area_left, plot_area_bottom, &
                                        plot_area_width, plot_area_height, &
                                        custom_xticks, custom_xtick_labels, &
                                        custom_yticks, custom_ytick_labels)
        !! Draw complete axes system with labels using actual plot area coordinates
        type(pdf_context_core), intent(inout) :: ctx
        character(len=*), intent(in), optional :: xscale, yscale
        real(wp), intent(in), optional :: symlog_threshold
        real(wp), intent(in) :: data_x_min, data_x_max, data_y_min, data_y_max
        character(len=*), intent(in), optional :: title, xlabel, ylabel
        character(len=*), intent(in), optional :: x_date_format, y_date_format
        real(wp), intent(in) :: plot_area_left, plot_area_bottom, plot_area_width, &
                                plot_area_height
        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(:)

        real(wp), allocatable :: x_positions(:), y_positions(:)
        character(len=50), allocatable :: x_labels(:), y_labels(:)
        integer :: num_x_ticks, num_y_ticks
        real(wp) :: x_min_adj, x_max_adj, y_min_adj, y_max_adj

        ! Setup data ranges and generate ticks
        call prepare_axes_data(ctx, data_x_min, data_x_max, data_y_min, data_y_max, &
                               x_min_adj, x_max_adj, y_min_adj, y_max_adj, &
                               x_positions, y_positions, x_labels, y_labels, &
                               num_x_ticks, num_y_ticks, xscale, yscale, &
                               x_date_format, y_date_format, &
                               plot_area_left, plot_area_bottom, plot_area_width, &
                               plot_area_height, &
                               symlog_threshold, &
                               custom_xticks=custom_xticks, &
                               custom_xtick_labels=custom_xtick_labels, &
                               custom_yticks=custom_yticks, &
                               custom_ytick_labels=custom_ytick_labels)

        ! Draw axes elements
        call draw_axes_elements(ctx, x_positions, y_positions, x_labels, y_labels, &
                                num_x_ticks, num_y_ticks, title, xlabel, ylabel, &
                                plot_area_left, plot_area_bottom, plot_area_width, &
                                plot_area_height)
    end subroutine draw_pdf_axes_and_labels

    subroutine prepare_axes_data(ctx, data_x_min, data_x_max, data_y_min, data_y_max, &
                                 x_min_adj, x_max_adj, y_min_adj, y_max_adj, &
                                 x_positions, y_positions, x_labels, y_labels, &
                                 num_x_ticks, num_y_ticks, xscale, yscale, &
                                 x_date_format, y_date_format, &
                                 plot_area_left, plot_area_bottom, plot_area_width, &
                                 plot_area_height, &
                                 symlog_threshold, &
                                 custom_xticks, custom_xtick_labels, &
                                 custom_yticks, custom_ytick_labels)
        !! Prepare axes data ranges and generate tick positions
        type(pdf_context_core), intent(inout) :: ctx
        real(wp), intent(in) :: data_x_min, data_x_max, data_y_min, data_y_max
        real(wp), intent(out) :: x_min_adj, x_max_adj, y_min_adj, y_max_adj
        real(wp), allocatable, intent(out) :: x_positions(:), y_positions(:)
        character(len=50), allocatable, intent(out) :: x_labels(:), y_labels(:)
        integer, intent(out) :: num_x_ticks, num_y_ticks
        character(len=*), intent(in), optional :: xscale, yscale
        character(len=*), intent(in), optional :: x_date_format, y_date_format
        real(wp), intent(in) :: plot_area_left, plot_area_bottom, plot_area_width, &
                                plot_area_height
        real(wp), intent(in), optional :: symlog_threshold
        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(:)

        call setup_axes_data_ranges(ctx, data_x_min, data_x_max, data_y_min, &
                                    data_y_max, &
                                    x_min_adj, x_max_adj, y_min_adj, y_max_adj, &
                                    xscale, yscale)

        call generate_tick_data(ctx, data_x_min, data_x_max, data_y_min, data_y_max, &
                                x_positions, y_positions, x_labels, y_labels, &
                                num_x_ticks, num_y_ticks, xscale, yscale, &
                                x_date_format, y_date_format, &
                                plot_area_left, plot_area_bottom, plot_area_width, &
                                plot_area_height, &
                                symlog_threshold, &
                                custom_xticks=custom_xticks, &
                                custom_xtick_labels=custom_xtick_labels, &
                                custom_yticks=custom_yticks, &
                                custom_ytick_labels=custom_ytick_labels)
    end subroutine prepare_axes_data

    subroutine draw_axes_elements(ctx, x_positions, y_positions, x_labels, y_labels, &
                                  num_x_ticks, num_y_ticks, title, xlabel, ylabel, &
                                  plot_area_left, plot_area_bottom, plot_area_width, &
                                  plot_area_height)
        !! Draw axes frame, ticks, and labels
        type(pdf_context_core), intent(inout) :: ctx
        real(wp), contiguous, intent(in) :: x_positions(:), y_positions(:)
        character(len=50), intent(in) :: x_labels(:), y_labels(:)
        integer, intent(in) :: num_x_ticks, num_y_ticks
        character(len=*), intent(in), optional :: title, xlabel, ylabel
        real(wp), intent(in) :: plot_area_left, plot_area_bottom, plot_area_width, &
                                plot_area_height
        real(wp) :: max_y_tick_label_width

        ! Ensure axes are drawn in black independent of prior plot color state
        call ctx%set_color(0.0_wp, 0.0_wp, 0.0_wp)
        call ctx%set_line_width(1.0_wp)

        call draw_pdf_frame_with_area(ctx, plot_area_left, plot_area_bottom, &
                                      plot_area_width, plot_area_height)

        call draw_pdf_tick_marks_with_area(ctx, x_positions, y_positions, num_x_ticks, &
                                           num_y_ticks, &
                                           plot_area_left, plot_area_bottom)

        max_y_tick_label_width = 0.0_wp
        call draw_pdf_tick_labels_with_area(ctx, x_positions, y_positions, x_labels, &
                                            y_labels, &
                                            num_x_ticks, num_y_ticks, plot_area_left, &
                                            plot_area_bottom, &
                                            plot_area_height, &
                                            max_y_tick_label_width)

        if (present(title) .or. present(xlabel) .or. present(ylabel)) then
            call draw_pdf_title_and_labels(ctx, title, xlabel, ylabel, &
                                           plot_area_left, plot_area_bottom, &
                                           plot_area_width, plot_area_height, &
                                           max_y_tick_label_width)
        end if
    end subroutine draw_axes_elements

end module fortplot_pdf_axes