fortplot_axes.f90 Source File


Source Code

module fortplot_axes
    !! Axes and tick generation module
    !! 
    !! This module handles axis drawing, tick computation, and label formatting
    !! for all scale types. Follows Single Responsibility Principle by focusing
    !! solely on axis-related functionality.
    
    use, intrinsic :: iso_fortran_env, only: wp => real64
    use fortplot_context
    use fortplot_scales
    use fortplot_constants, only: SCIENTIFIC_THRESHOLD_HIGH
    implicit none
    
    private
    public :: compute_scale_ticks, format_tick_label, MAX_TICKS
    
    integer, parameter :: MAX_TICKS = 20
    
    ! Format threshold constants for tick label formatting
    ! SCIENTIFIC_THRESHOLD_HIGH is imported from fortplot_constants
    real(wp), parameter :: SCIENTIFIC_THRESHOLD_LOW = 0.01_wp      ! Use scientific for abs(value) < this
    real(wp), parameter :: NO_DECIMAL_THRESHOLD = 100.0_wp         ! No decimal places for abs(value) >= this
    real(wp), parameter :: ONE_DECIMAL_THRESHOLD = 10.0_wp         ! One decimal place for abs(value) >= this
    real(wp), parameter :: TWO_DECIMAL_THRESHOLD = 1.0_wp          ! Two decimal places for abs(value) >= this
    
contains

    subroutine compute_scale_ticks(scale_type, data_min, data_max, threshold, tick_positions, num_ticks)
        !! Compute tick positions for different scale types
        !! 
        !! @param scale_type: Type of scale ('linear', 'log', 'symlog')
        !! @param data_min: Minimum data value
        !! @param data_max: Maximum data value
        !! @param threshold: Threshold for symlog (ignored for others)
        !! @param tick_positions: Output array of tick positions
        !! @param num_ticks: Number of ticks generated
        
        character(len=*), intent(in) :: scale_type
        real(wp), intent(in) :: data_min, data_max, threshold
        real(wp), intent(out) :: tick_positions(MAX_TICKS)
        integer, intent(out) :: num_ticks
        
        select case (trim(scale_type))
        case ('linear')
            call compute_linear_ticks(data_min, data_max, tick_positions, num_ticks)
        case ('log')
            call compute_log_ticks(data_min, data_max, tick_positions, num_ticks)
        case ('symlog')
            call compute_symlog_ticks(data_min, data_max, threshold, tick_positions, num_ticks)
        case default
            call compute_linear_ticks(data_min, data_max, tick_positions, num_ticks)
        end select
    end subroutine compute_scale_ticks

    subroutine compute_linear_ticks(data_min, data_max, tick_positions, num_ticks)
        !! Compute tick positions for linear scale
        real(wp), intent(in) :: data_min, data_max
        real(wp), intent(out) :: tick_positions(MAX_TICKS)
        integer, intent(out) :: num_ticks
        
        real(wp) :: range, step, nice_step, tick_value
        integer :: max_ticks_desired
        
        max_ticks_desired = 8
        range = data_max - data_min
        
        if (range <= 0.0_wp) then
            num_ticks = 0
            return
        end if
        
        step = range / real(max_ticks_desired, wp)
        nice_step = calculate_nice_step(step)
        
        ! Find first tick >= data_min
        tick_value = ceiling(data_min / nice_step) * nice_step
        num_ticks = 0
        
        do while (tick_value <= data_max .and. num_ticks < MAX_TICKS)
            num_ticks = num_ticks + 1
            tick_positions(num_ticks) = tick_value
            tick_value = tick_value + nice_step
        end do
    end subroutine compute_linear_ticks

    subroutine compute_log_ticks(data_min, data_max, tick_positions, num_ticks)
        !! Compute tick positions for logarithmic scale
        real(wp), intent(in) :: data_min, data_max
        real(wp), intent(out) :: tick_positions(MAX_TICKS)
        integer, intent(out) :: num_ticks
        
        real(wp) :: log_min, log_max, current_power
        integer :: start_power, end_power, power
        
        if (data_min <= 0.0_wp .or. data_max <= 0.0_wp) then
            num_ticks = 0
            return
        end if
        
        log_min = log10(data_min)
        log_max = log10(data_max)
        
        start_power = floor(log_min)
        end_power = ceiling(log_max)
        
        num_ticks = 0
        do power = start_power, end_power
            if (num_ticks >= MAX_TICKS) exit
            current_power = 10.0_wp**power
            if (current_power >= data_min .and. current_power <= data_max) then
                num_ticks = num_ticks + 1
                tick_positions(num_ticks) = current_power
            end if
        end do
    end subroutine compute_log_ticks

    subroutine compute_symlog_ticks(data_min, data_max, threshold, tick_positions, num_ticks)
        !! Compute tick positions for symmetric logarithmic scale
        real(wp), intent(in) :: data_min, data_max, threshold
        real(wp), intent(out) :: tick_positions(MAX_TICKS)
        integer, intent(out) :: num_ticks
        
        num_ticks = 0
        
        ! Add negative log region ticks
        if (data_min < -threshold) then
            call add_negative_symlog_ticks(data_min, -threshold, tick_positions, num_ticks)
        end if
        
        ! Add linear region ticks (only for the region within threshold bounds)
        if (max(data_min, -threshold) <= min(data_max, threshold)) then
            call add_linear_symlog_ticks(max(data_min, -threshold), min(data_max, threshold), &
                                       tick_positions, num_ticks)
        end if
        
        ! Add positive log region ticks
        if (data_max > threshold) then
            call add_positive_symlog_ticks(threshold, data_max, tick_positions, num_ticks)
        end if
    end subroutine compute_symlog_ticks

    function format_tick_label(value, scale_type) result(label)
        !! Format a tick value as a string label
        !! 
        !! @param value: Tick value to format
        !! @param scale_type: Scale type for context
        !! @return label: Formatted label string
        
        real(wp), intent(in) :: value
        character(len=*), intent(in) :: scale_type
        character(len=20) :: label
        real(wp) :: abs_value
        
        abs_value = abs(value)
        
        if (abs_value < 1.0e-10_wp) then
            label = '0'
        else if (trim(scale_type) == 'log' .and. is_power_of_ten(value)) then
            label = format_power_of_ten(value)
        else if (abs_value >= SCIENTIFIC_THRESHOLD_HIGH .or. abs_value < SCIENTIFIC_THRESHOLD_LOW) then
            ! Use scientific notation for very large or very small values
            write(label, '(ES10.2)') value
            label = adjustl(label)
        else if (abs_value >= NO_DECIMAL_THRESHOLD) then
            ! No decimal places for values >= 100
            write(label, '(F0.0)') value
        else if (abs_value >= ONE_DECIMAL_THRESHOLD) then
            ! One decimal place for values >= 10
            write(label, '(F0.1)') value
        else if (abs_value >= TWO_DECIMAL_THRESHOLD) then
            ! Two decimal places for values >= 1
            write(label, '(F0.2)') value
        else
            ! Three decimal places for small values
            write(label, '(F0.3)') value
        end if
        
        label = adjustl(label)
    end function format_tick_label



    ! Helper subroutines (implementation details)
    
    function calculate_nice_step(raw_step) result(nice_step)
        real(wp), intent(in) :: raw_step
        real(wp) :: nice_step, magnitude, normalized
        
        magnitude = 10.0_wp**floor(log10(raw_step))
        normalized = raw_step / magnitude
        
        if (normalized <= 1.0_wp) then
            nice_step = magnitude
        else if (normalized <= 2.0_wp) then
            nice_step = 2.0_wp * magnitude
        else if (normalized <= 5.0_wp) then
            nice_step = 5.0_wp * magnitude
        else
            nice_step = 10.0_wp * magnitude
        end if
    end function calculate_nice_step
    
    subroutine add_negative_symlog_ticks(data_min, upper_bound, tick_positions, num_ticks)
        !! Add ticks for negative logarithmic region of symlog scale
        real(wp), intent(in) :: data_min, upper_bound
        real(wp), intent(inout) :: tick_positions(MAX_TICKS)
        integer, intent(inout) :: num_ticks
        
        real(wp) :: log_min, log_max, current_power
        integer :: start_power, end_power, power
        
        if (data_min >= 0.0_wp .or. upper_bound >= 0.0_wp .or. upper_bound <= data_min) return
        
        ! Work with positive values for log calculations
        ! For negative range [-500, -1], we want powers that give us ticks in that range
        log_min = log10(-upper_bound)  ! log10(1) = 0 (closer to zero)
        log_max = log10(-data_min)     ! log10(500) = ~2.7 (larger magnitude)
        
        start_power = floor(log_min)
        end_power = ceiling(log_max)
        
        do power = start_power, end_power
            if (num_ticks >= MAX_TICKS) exit
            current_power = -(10.0_wp**power)
            
            ! Check if tick is within bounds, excluding threshold boundary
            if (current_power >= data_min - 1.0e-10_wp .and. &
                current_power < upper_bound - 1.0e-10_wp) then
                num_ticks = num_ticks + 1
                tick_positions(num_ticks) = current_power
            end if
        end do
    end subroutine add_negative_symlog_ticks
    
    subroutine add_linear_symlog_ticks(lower_bound, upper_bound, tick_positions, num_ticks)
        !! Add ticks for linear region of symlog scale
        real(wp), intent(in) :: lower_bound, upper_bound
        real(wp), intent(inout) :: tick_positions(MAX_TICKS)
        integer, intent(inout) :: num_ticks
        
        real(wp) :: range, step, tick_value
        integer :: max_linear_ticks
        
        if (upper_bound <= lower_bound) return
        
        range = upper_bound - lower_bound
        max_linear_ticks = 5  ! Reasonable number for linear region
        
        ! Always include zero if it's in the range
        if (lower_bound <= 0.0_wp .and. upper_bound >= 0.0_wp .and. num_ticks < MAX_TICKS) then
            num_ticks = num_ticks + 1
            tick_positions(num_ticks) = 0.0_wp
        end if
        
        ! Add additional linear ticks
        step = range / real(max_linear_ticks + 1, wp)
        step = calculate_nice_step(step)
        
        ! Find first tick >= lower_bound
        tick_value = ceiling(lower_bound / step) * step
        
        do while (tick_value <= upper_bound .and. num_ticks < MAX_TICKS)
            ! Skip zero if already added, avoid duplicates
            if (abs(tick_value) > 1.0e-10_wp) then
                num_ticks = num_ticks + 1
                tick_positions(num_ticks) = tick_value
            end if
            tick_value = tick_value + step
        end do
    end subroutine add_linear_symlog_ticks
    
    subroutine add_positive_symlog_ticks(lower_bound, data_max, tick_positions, num_ticks)
        !! Add ticks for positive logarithmic region of symlog scale
        real(wp), intent(in) :: lower_bound, data_max
        real(wp), intent(inout) :: tick_positions(MAX_TICKS)
        integer, intent(inout) :: num_ticks
        
        real(wp) :: log_min, log_max, current_power
        integer :: start_power, end_power, power
        
        if (lower_bound <= 0.0_wp .or. data_max <= 0.0_wp) return
        
        log_min = log10(lower_bound)
        log_max = log10(data_max)
        
        start_power = floor(log_min)
        end_power = ceiling(log_max)
        
        do power = start_power, end_power
            if (num_ticks >= MAX_TICKS) exit
            current_power = 10.0_wp**power
            
            ! Check if tick is within bounds, excluding threshold boundary
            if (current_power > lower_bound + 1.0e-10_wp .and. &
                current_power <= data_max + 1.0e-10_wp) then
                num_ticks = num_ticks + 1
                tick_positions(num_ticks) = current_power
            end if
        end do
    end subroutine add_positive_symlog_ticks
    
    function is_power_of_ten(value) result(is_power)
        real(wp), intent(in) :: value
        logical :: is_power
        real(wp) :: log_val
        log_val = log10(abs(value))
        is_power = abs(log_val - nint(log_val)) < 1.0e-10_wp
    end function is_power_of_ten
    
    function format_power_of_ten(value) result(formatted)
        real(wp), intent(in) :: value
        character(len=20) :: formatted
        integer :: exponent
        exponent = nint(log10(abs(value)))
        if (value < 0.0_wp) then
            write(formatted, '(A, I0)') '-10^', exponent
        else
            write(formatted, '(A, I0)') '10^', exponent
        end if
    end function format_power_of_ten
    

end module fortplot_axes