module fortplot_pdf_axes_tick_data !! PDF axis tick data generation module !! !! Handles tick position calculation, label formatting, and subsampling. !! Pure computation module - no PDF context dependency. use iso_fortran_env, only: wp => real64 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 implicit none private ! Public procedures public :: initialize_tick_arrays public :: generate_x_axis_ticks public :: generate_y_axis_ticks public :: apply_custom_axis_ticks public :: generate_axis_ticks_internal public :: subsample_ticks public :: fill_tick_positions_and_labels public :: handle_zero_range_ticks contains subroutine initialize_tick_arrays(plot_width, plot_height, num_x_ticks, & num_y_ticks, & x_positions, y_positions, x_labels, y_labels) !! Initialize tick count and allocate arrays real(wp), intent(in) :: plot_width, plot_height integer, intent(out) :: num_x_ticks, num_y_ticks real(wp), allocatable, intent(out) :: x_positions(:), y_positions(:) character(len=50), allocatable, intent(out) :: x_labels(:), y_labels(:) integer, parameter :: TARGET_TICKS = 8 ! Determine number of ticks using plot area dimensions num_x_ticks = min(TARGET_TICKS, max(2, int(plot_width/50.0_wp))) num_y_ticks = min(TARGET_TICKS, max(2, int(plot_height/40.0_wp))) ! Allocate arrays allocate (x_positions(num_x_ticks)) allocate (y_positions(num_y_ticks)) allocate (x_labels(num_x_ticks)) allocate (y_labels(num_y_ticks)) end subroutine initialize_tick_arrays subroutine generate_x_axis_ticks(data_min, data_max, num_ticks, plot_left, & plot_width, & positions, labels, scale_type, date_format, & symlog_threshold, custom_xticks, custom_xtick_labels) !! Generate X axis tick positions and labels real(wp), intent(in) :: data_min, data_max, plot_left, plot_width integer, intent(inout) :: num_ticks real(wp), intent(out) :: positions(:) character(len=50), intent(out) :: labels(:) character(len=*), intent(in), optional :: scale_type character(len=*), intent(in), optional :: date_format real(wp), intent(in), optional :: symlog_threshold real(wp), intent(in), optional :: custom_xticks(:) character(len=*), intent(in), optional :: custom_xtick_labels(:) call generate_axis_ticks_internal(data_min, data_max, num_ticks, plot_left, & plot_width, & positions, labels, scale_type, date_format, & symlog_threshold, 'x', & custom_xticks=custom_xticks, & custom_xtick_labels=custom_xtick_labels) end subroutine generate_x_axis_ticks subroutine generate_y_axis_ticks(data_min, data_max, num_ticks, plot_bottom, & plot_height, & positions, labels, scale_type, date_format, & symlog_threshold, custom_yticks, custom_ytick_labels) !! Generate Y axis tick positions and labels real(wp), intent(in) :: data_min, data_max, plot_bottom, plot_height integer, intent(inout) :: num_ticks real(wp), intent(out) :: positions(:) character(len=50), intent(out) :: labels(:) character(len=*), intent(in), optional :: scale_type character(len=*), intent(in), optional :: date_format real(wp), intent(in), optional :: symlog_threshold real(wp), intent(in), optional :: custom_yticks(:) character(len=*), intent(in), optional :: custom_ytick_labels(:) call generate_axis_ticks_internal(data_min, data_max, num_ticks, plot_bottom, & plot_height, & positions, labels, scale_type, date_format, & symlog_threshold, 'y', & custom_yticks=custom_yticks, & custom_ytick_labels=custom_ytick_labels) end subroutine generate_y_axis_ticks subroutine apply_custom_axis_ticks(axis, custom_xticks, custom_xtick_labels, & custom_yticks, custom_ytick_labels, & data_min, data_max, plot_start, & plot_size, num_ticks, positions, labels, & scale_type, symlog_threshold) !! Apply custom tick positions/labels, converting data coords to plot area coords character, intent(in) :: axis 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), intent(in) :: data_min, data_max, plot_start, plot_size integer, intent(inout) :: num_ticks real(wp), intent(out) :: positions(:) character(len=50), intent(out) :: labels(:) character(len=*), intent(in), optional :: scale_type real(wp), intent(in), optional :: symlog_threshold character(len=16) :: scale real(wp) :: thr integer :: used_ticks integer :: i logical :: use_custom real(wp) :: data_min_t, data_max_t use_custom = .false. if (axis == 'x') then if (present(custom_xticks) .and. present(custom_xtick_labels) .and. & size(custom_xticks) > 0 .and. size(custom_xticks) == & size(custom_xtick_labels)) use_custom = .true. else if (present(custom_yticks) .and. present(custom_ytick_labels) .and. & size(custom_yticks) > 0 .and. size(custom_yticks) == & size(custom_ytick_labels)) use_custom = .true. end if if (.not. use_custom) return scale = 'linear'; if (present(scale_type)) scale = scale_type thr = 1.0_wp; if (present(symlog_threshold)) thr = symlog_threshold if (axis == 'x') then used_ticks = min(num_ticks, size(custom_xticks)) labels(1:used_ticks) = custom_xtick_labels(1:used_ticks) else used_ticks = min(num_ticks, size(custom_yticks)) labels(1:used_ticks) = custom_ytick_labels(1:used_ticks) end if do i = 1, used_ticks if (axis == 'x') then positions(i) = custom_xticks(i) else positions(i) = custom_yticks(i) end if end do data_min_t = apply_scale_transform(data_min, scale, thr) data_max_t = apply_scale_transform(data_max, scale, thr) do i = 1, used_ticks positions(i) = apply_scale_transform(positions(i), scale, thr) end do if (data_max_t > data_min_t) then do i = 1, used_ticks positions(i) = plot_start + (positions(i) - data_min_t)/(data_max_t - & data_min_t)*plot_size end do else do i = 1, used_ticks positions(i) = plot_start + 0.5_wp*plot_size end do end if do i = used_ticks + 1, num_ticks labels(i) = '' end do num_ticks = used_ticks end subroutine apply_custom_axis_ticks subroutine generate_axis_ticks_internal(data_min, data_max, num_ticks, plot_start, & plot_size, & positions, labels, scale_type, & date_format, & symlog_threshold, axis, & custom_xticks, custom_xtick_labels, & custom_yticks, custom_ytick_labels) !! Internal helper to generate axis tick positions and labels real(wp), intent(in) :: data_min, data_max, plot_start, plot_size integer, intent(inout) :: num_ticks real(wp), intent(out) :: positions(:) character(len=50), intent(out) :: labels(:) character(len=*), intent(in), optional :: scale_type character(len=*), intent(in), optional :: date_format real(wp), intent(in), optional :: symlog_threshold character, intent(in) :: axis ! 'x' or 'y' 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) :: tvals(MAX_TICKS) integer :: nt character(len=16) :: scale real(wp) :: thr integer :: used_ticks call apply_custom_axis_ticks(axis, custom_xticks, custom_xtick_labels, & custom_yticks, custom_ytick_labels, & data_min, data_max, plot_start, plot_size, & num_ticks, positions, labels, scale_type, & symlog_threshold) if (num_ticks == 0) return scale = 'linear' if (present(scale_type)) scale = scale_type thr = 1.0_wp if (present(symlog_threshold)) thr = symlog_threshold call compute_scale_ticks(scale, data_min, data_max, thr, tvals, nt) if (nt <= 0) then num_ticks = min(num_ticks, size(positions)) if (num_ticks <= 0) then num_ticks = 0 return end if call handle_zero_range_ticks(data_min, num_ticks, plot_start + & plot_size*0.5_wp, & positions, labels, scale, date_format) return end if used_ticks = min(num_ticks, size(positions)) if (used_ticks <= 0) then num_ticks = 0 return end if ! Subsample ticks when more are generated than can be drawn, ! ensuring the first and last ticks always span the full data range. if (nt > used_ticks) then call subsample_ticks(tvals, nt, used_ticks, scale, thr) nt = used_ticks end if call fill_tick_positions_and_labels(tvals, nt, data_min, data_max, plot_start, & plot_size, & used_ticks, positions, labels, scale, thr, & date_format) num_ticks = used_ticks end subroutine generate_axis_ticks_internal subroutine subsample_ticks(tvals, nt, max_ticks, scale, threshold) !! Subsample ticks in transformed coordinate space for proportional visual spacing. real(wp), contiguous, intent(inout) :: tvals(:) integer, intent(in) :: nt, max_ticks character(len=*), intent(in) :: scale real(wp), intent(in) :: threshold integer :: k, idx, best_idx real(wp) :: frac, target_t, best_dist, dist real(wp), allocatable :: tvals_t(:) if (nt <= max_ticks) return ! Transform ticks to display space allocate (tvals_t(nt)) do k = 1, nt tvals_t(k) = apply_scale_transform(tvals(k), scale, threshold) end do ! For each subsampled slot, find the tick closest in transformed space do k = 2, max_ticks - 1 frac = real(k - 1, wp) / real(max_ticks - 1, wp) target_t = tvals_t(1) + frac * (tvals_t(nt) - tvals_t(1)) best_idx = 1 best_dist = abs(tvals_t(1) - target_t) do idx = 2, nt dist = abs(tvals_t(idx) - target_t) if (dist < best_dist) then best_dist = dist best_idx = idx end if end do tvals(k) = tvals(best_idx) end do tvals(max_ticks) = tvals(nt) end subroutine subsample_ticks subroutine fill_tick_positions_and_labels(tvals, nt, data_min, data_max, & plot_start, plot_size, & num_ticks, positions, labels, & scale, threshold, date_format) !! Fill tick positions and labels arrays real(wp), contiguous, intent(in) :: tvals(:) real(wp), intent(in) :: data_min, data_max, plot_start, & plot_size, threshold integer, intent(in) :: nt, num_ticks real(wp), intent(out) :: positions(:) character(len=50), intent(out) :: labels(:) character(len=*), intent(in) :: scale character(len=*), intent(in), optional :: date_format real(wp) :: min_t, max_t, tv_t integer :: i, limit, decimals min_t = apply_scale_transform(data_min, scale, threshold) max_t = apply_scale_transform(data_max, scale, threshold) decimals = 0 if (trim(scale) == 'linear' .and. nt >= 2) then decimals = determine_decimals_from_ticks(tvals, nt) end if limit = min(num_ticks, size(positions)) do i = 1, limit if (i > nt) exit tv_t = apply_scale_transform(tvals(i), scale, threshold) if (max_t > min_t) then positions(i) = plot_start + (tv_t - min_t)/(max_t - min_t)*plot_size else positions(i) = plot_start + 0.5_wp*plot_size end if if (trim(scale) == 'linear') then labels(i) = adjustl(format_tick_value_consistent(tvals(i), decimals)) else labels(i) = adjustl(format_tick_label(tvals(i), scale, & date_format=date_format, & data_min=data_min, & data_max=data_max)) end if end do do i = nt + 1, limit labels(i) = '' end do end subroutine fill_tick_positions_and_labels subroutine handle_zero_range_ticks(data_value, num_ticks, center_position, & positions, labels, scale_type, date_format) !! Handle ticks for zero or near-zero range data real(wp), intent(in) :: data_value, center_position integer, intent(in) :: num_ticks real(wp), intent(out) :: positions(:) character(len=50), intent(out) :: labels(:) character(len=*), intent(in), optional :: scale_type character(len=*), intent(in), optional :: date_format integer :: i do i = 1, num_ticks positions(i) = center_position if (present(scale_type)) then labels(i) = adjustl(format_tick_label(data_value, scale_type, & date_format=date_format, & data_min=data_value, & data_max=data_value)) else labels(i) = adjustl(format_tick_label(data_value, 'linear')) end if end do end subroutine handle_zero_range_ticks end module fortplot_pdf_axes_tick_data