fortplot_pdf_coordinate.f90 Source File


Source Code

module fortplot_pdf_coordinate
    !! PDF coordinate transformation and polymorphic method support
    !! Handles coordinate normalization and backend-specific method implementations

    use, intrinsic :: iso_fortran_env, only: wp => real64
    use fortplot_pdf_core, only: pdf_context_core
    use fortplot_pdf_text, only: draw_mixed_font_text, draw_rotated_mixed_font_text, &
                                 draw_pdf_mathtext
    use fortplot_latex_parser, only: process_latex_in_text
    use fortplot_pdf_drawing, only: draw_pdf_arrow, draw_pdf_circle_with_outline, &
                                    draw_pdf_square_with_outline, &
                                    draw_pdf_diamond_with_outline, &
                                    draw_pdf_x_marker
    use fortplot_plot_data, only: plot_data_t
    use fortplot_margins, only: plot_area_t, plot_margins_t
    implicit none

    private

    ! Forward declaration for PDF context type
    type :: pdf_context_handle
        real(wp) :: x_min, x_max, y_min, y_max
        integer :: width, height
        type(plot_area_t) :: plot_area
        type(pdf_context_core) :: core_ctx
    end type pdf_context_handle

    public :: pdf_context_handle
    public :: normalize_to_pdf_coords, safe_coordinate_transform
    public :: pdf_get_width_scale, pdf_get_height_scale
    public :: pdf_extract_rgb_data, pdf_get_png_data
    public :: pdf_prepare_3d_data, pdf_render_ylabel
    public :: calculate_pdf_plot_area

contains

    subroutine normalize_to_pdf_coords(ctx, x, y, pdf_x, pdf_y)
        type(pdf_context_handle), intent(in) :: ctx
        real(wp), intent(in) :: x, y
        real(wp), intent(out) :: pdf_x, pdf_y
        real(wp) :: x_range, y_range
        real(wp) :: x_scale, y_scale
        real(wp), parameter :: EPSILON = 1.0e-10_wp
        real(wp) :: left, right, bottom, top

        ! Calculate data ranges with epsilon protection
        x_range = ctx%x_max - ctx%x_min
        y_range = ctx%y_max - ctx%y_min

        ! Frame edges
        left = real(ctx%plot_area%left, wp)
        right = real(ctx%plot_area%left + ctx%plot_area%width, wp)
        bottom = real(ctx%plot_area%bottom, wp)
        top = real(ctx%plot_area%bottom + ctx%plot_area%height, wp)

        ! Map X using independent scale (center if degenerate)
        if (abs(x_range) < EPSILON) then
            pdf_x = left + (right - left)*0.5_wp
        else
            x_scale = (right - left)/x_range
            pdf_x = (x - ctx%x_min)*x_scale + left
        end if

        ! Map Y using independent scale (center if degenerate)
        if (abs(y_range) < EPSILON) then
            pdf_y = bottom + (top - bottom)*0.5_wp
        else
            y_scale = (top - bottom)/y_range
            pdf_y = (y - ctx%y_min)*y_scale + bottom
        end if

        ! Clamp only for in-range data points. Annotations can intentionally place
        ! elements outside the plot area (Matplotlib behavior), so do not clamp
        ! those coordinates.
        if (x >= ctx%x_min - EPSILON .and. x <= ctx%x_max + EPSILON) then
            if (pdf_x < left) pdf_x = left
            if (pdf_x > right) pdf_x = right
        end if
        if (y >= ctx%y_min - EPSILON .and. y <= ctx%y_max + EPSILON) then
            if (pdf_y < bottom) pdf_y = bottom
            if (pdf_y > top) pdf_y = top
        end if
    end subroutine normalize_to_pdf_coords

    real(wp) function pdf_get_width_scale(ctx) result(scale)
        !! Get width scale in drawing units per data unit.
        type(pdf_context_handle), intent(in) :: ctx
        real(wp) :: x_range

        x_range = ctx%x_max - ctx%x_min
        if (abs(x_range) < 1.0e-12_wp) x_range = 1.0_wp
        scale = real(ctx%plot_area%width, wp)/x_range
    end function pdf_get_width_scale

    real(wp) function pdf_get_height_scale(ctx) result(scale)
        !! Get height scale in drawing units per data unit.
        type(pdf_context_handle), intent(in) :: ctx
        real(wp) :: y_range

        y_range = ctx%y_max - ctx%y_min
        if (abs(y_range) < 1.0e-12_wp) y_range = 1.0_wp
        scale = real(ctx%plot_area%height, wp)/y_range
    end function pdf_get_height_scale

    ! Removed unused pdf_fill_quad/pdf_fill_heatmap helpers (deprecated, not referenced)

    subroutine pdf_extract_rgb_data(ctx, width, height, rgb_data)
        type(pdf_context_handle), intent(in) :: ctx
        integer, intent(in) :: width, height
        real(wp), intent(out) :: rgb_data(width, height, 3)
        ! PDF does not have RGB pixel data, return white.
        rgb_data = 1.0_wp
    end subroutine pdf_extract_rgb_data

    subroutine pdf_get_png_data(ctx, width, height, png_data, status)
        type(pdf_context_handle), intent(in) :: ctx
        integer, intent(in) :: width, height
        integer(1), allocatable, intent(out) :: png_data(:)
        integer, intent(out) :: status
        ! PDF does not generate PNG data.
        allocate (png_data(0))
        status = -1
    end subroutine pdf_get_png_data

    subroutine pdf_prepare_3d_data(ctx, plots)
        type(pdf_context_handle), intent(inout) :: ctx
        type(plot_data_t), intent(in) :: plots(:)
    end subroutine pdf_prepare_3d_data

    subroutine pdf_render_ylabel(ctx, ylabel)
        type(pdf_context_handle), intent(inout) :: ctx
        character(len=*), intent(in) :: ylabel

        real(wp) :: x, y

        x = real(ctx%plot_area%left - 30, wp)  ! Reduced spacing from 40 to 30
        y = real(ctx%plot_area%bottom + ctx%plot_area%height/2, wp)

        call draw_rotated_mixed_font_text(ctx%core_ctx, x, y, ylabel)
    end subroutine pdf_render_ylabel

    subroutine safe_coordinate_transform(x, y, x_min, x_max, y_min, y_max, &
                                         plot_left, plot_width, plot_bottom, &
                                         plot_height, &
                                         pdf_x, pdf_y)
        !! Safe coordinate transformation using independent x/y scales
        real(wp), intent(in) :: x, y
        real(wp), intent(in) :: x_min, x_max, y_min, y_max
        real(wp), intent(in) :: plot_left, plot_width, plot_bottom, plot_height
        real(wp), intent(out) :: pdf_x, pdf_y
        real(wp), parameter :: EPSILON = 1.0e-10_wp
        real(wp) :: x_range, y_range
        real(wp) :: x_scale, y_scale
        ! Calculate ranges with epsilon protection
        x_range = x_max - x_min
        y_range = y_max - y_min

        ! Map X using independent scale (center if degenerate)
        if (abs(x_range) < EPSILON) then
            pdf_x = plot_left + plot_width*0.5_wp
        else
            x_scale = plot_width/x_range
            pdf_x = (x - x_min)*x_scale + plot_left
        end if

        ! Map Y using independent scale (center if degenerate)
        if (abs(y_range) < EPSILON) then
            pdf_y = plot_bottom + plot_height*0.5_wp
        else
            y_scale = plot_height/y_range
            pdf_y = (y - y_min)*y_scale + plot_bottom
        end if
    end subroutine safe_coordinate_transform

    subroutine calculate_pdf_plot_area(canvas_width, canvas_height, margins, plot_area)
        !! Calculate plot area for PDF backend (mathematical coordinates: Y=0 at bottom)
        integer, intent(in) :: canvas_width, canvas_height
        type(plot_margins_t), intent(in) :: margins
        type(plot_area_t), intent(out) :: plot_area
        ! Calculate positions for mathematical coordinate system (Y=0 at bottom)
        plot_area%left = int(margins%left*real(canvas_width, wp))
        plot_area%width = int(margins%right*real(canvas_width, wp)) - plot_area%left
        ! For PDF mathematical coordinates (Y=0 at bottom), use direct calculation
        plot_area%bottom = int(margins%bottom*real(canvas_height, wp))
        plot_area%height = int(margins%top*real(canvas_height, wp)) - plot_area%bottom
    end subroutine calculate_pdf_plot_area

end module fortplot_pdf_coordinate