fortplot_figure_data_ranges.f90 Source File


Source Code

module fortplot_figure_data_ranges
    use, intrinsic :: iso_fortran_env, only: wp => real64
    use fortplot_scales, only: apply_scale_transform, clamp_extreme_log_range
    use fortplot_logging, only: log_debug
    use fortplot_plot_data, only: plot_data_t, PLOT_TYPE_LINE, &
                                    PLOT_TYPE_CONTOUR, PLOT_TYPE_PCOLORMESH, &
                                    PLOT_TYPE_SCATTER, PLOT_TYPE_FILL, &
                                    PLOT_TYPE_BOXPLOT, PLOT_TYPE_ERRORBAR, &
                                    PLOT_TYPE_SURFACE, PLOT_TYPE_PIE, &
                                    PLOT_TYPE_BAR, PLOT_TYPE_QUIVER
    use fortplot_figure_data_ranges_specialized, only: &
        process_line_plot_ranges, process_fill_between_ranges, &
        process_pie_ranges, process_contour_plot_ranges, &
        process_pcolormesh_ranges, process_boxplot_ranges, &
        process_errorbar_ranges, process_bar_plot_ranges
    implicit none

    private
    public :: calculate_figure_data_ranges

contains

    subroutine calculate_figure_data_ranges(plots, plot_count, xlim_set, ylim_set, &
                                           x_min, x_max, y_min, y_max, &
                                           x_min_transformed, x_max_transformed, &
                                           y_min_transformed, y_max_transformed, &
                                           xscale, yscale, symlog_threshold, &
                                           symlog_base, symlog_linscale, axis_filter)
        !! Calculate overall data ranges for the figure with robust edge case handling
        type(plot_data_t), intent(in) :: plots(:)
        integer, intent(in) :: plot_count
        logical, intent(in) :: xlim_set, ylim_set
        real(wp), intent(inout) :: x_min, x_max, y_min, y_max
        real(wp), intent(out) :: x_min_transformed, x_max_transformed
        real(wp), intent(out) :: y_min_transformed, y_max_transformed
        character(len=*), intent(in) :: xscale, yscale
        real(wp), intent(in) :: symlog_threshold
        real(wp), intent(in), optional :: symlog_base, symlog_linscale
        integer, intent(in), optional :: axis_filter

        real(wp) :: x_min_data, x_max_data, y_min_data, y_max_data
        logical :: first_plot, has_valid_data
        integer :: i
        integer :: filtered_axis
        logical :: use_filter

        use_filter = present(axis_filter)
        if (use_filter) filtered_axis = axis_filter

        call initialize_data_ranges(xlim_set, ylim_set, x_min, x_max, y_min, y_max, &
                                    x_min_transformed, x_max_transformed, &
                                    y_min_transformed, y_max_transformed, &
                                    xscale, yscale, symlog_threshold, &
                                    symlog_base, symlog_linscale, &
                                    x_min_data, x_max_data, y_min_data, y_max_data, &
                                    first_plot, has_valid_data)
        if (xlim_set .and. ylim_set) return

        do i = 1, plot_count
            if (use_filter) then
                if (plots(i)%axis /= filtered_axis) cycle
            end if
            select case (plots(i)%plot_type)
            case (PLOT_TYPE_LINE)
                call process_line_plot_ranges(plots(i), first_plot, has_valid_data, &
                                              x_min_data, x_max_data, &
                                              y_min_data, y_max_data)

            case (PLOT_TYPE_SCATTER)
                call process_line_plot_ranges(plots(i), first_plot, has_valid_data, &
                                              x_min_data, x_max_data, &
                                              y_min_data, y_max_data)

            case (PLOT_TYPE_ERRORBAR)
                call process_errorbar_ranges(plots(i), first_plot, has_valid_data, &
                                              x_min_data, x_max_data, &
                                              y_min_data, y_max_data)

            case (PLOT_TYPE_BAR)
                call process_bar_plot_ranges(plots(i), first_plot, has_valid_data, &
                                              x_min_data, x_max_data, &
                                              y_min_data, y_max_data)

            case (PLOT_TYPE_FILL)
                call process_fill_between_ranges(plots(i), first_plot, has_valid_data, &
                                                 x_min_data, x_max_data, &
                                                 y_min_data, y_max_data)

            case (PLOT_TYPE_PIE)
                call process_pie_ranges(plots(i), first_plot, has_valid_data, &
                                        x_min_data, x_max_data, y_min_data, y_max_data)

            case (PLOT_TYPE_CONTOUR)
                call process_contour_plot_ranges(plots(i), first_plot, has_valid_data, &
                                                 x_min_data, x_max_data, &
                                                 y_min_data, y_max_data)

            case (PLOT_TYPE_SURFACE)
                call process_contour_plot_ranges(plots(i), first_plot, has_valid_data, &
                                                 x_min_data, x_max_data, &
                                                 y_min_data, y_max_data)

            case (PLOT_TYPE_PCOLORMESH)
                call process_pcolormesh_ranges(plots(i), first_plot, has_valid_data, &
                                               x_min_data, x_max_data, &
                                               y_min_data, y_max_data)

            case (PLOT_TYPE_BOXPLOT)
                call process_boxplot_ranges(plots(i), first_plot, has_valid_data, &
                                            x_min_data, x_max_data, &
                                            y_min_data, y_max_data)

            case (PLOT_TYPE_QUIVER)
                call process_line_plot_ranges(plots(i), first_plot, has_valid_data, &
                                              x_min_data, x_max_data, &
                                              y_min_data, y_max_data)

            end select
        end do

        call apply_single_point_margins(has_valid_data, x_min_data, x_max_data, &
                                        y_min_data, y_max_data)

      call finalize_data_ranges(xlim_set, ylim_set, x_min, x_max, y_min, y_max, &
                                   x_min_data, x_max_data, y_min_data, y_max_data, &
                                   x_min_transformed, x_max_transformed, &
                                   y_min_transformed, y_max_transformed, &
                                   xscale, yscale, symlog_threshold, &
                                   symlog_base, symlog_linscale)
    end subroutine calculate_figure_data_ranges

    subroutine initialize_data_ranges(xlim_set, ylim_set, x_min, x_max, y_min, y_max, &
                                      x_min_transformed, x_max_transformed, &
                                      y_min_transformed, y_max_transformed, &
                                      xscale, yscale, symlog_threshold, &
                                      symlog_base, symlog_linscale, &
                                      x_min_data, x_max_data, y_min_data, y_max_data, &
                                      first_plot, has_valid_data)
        logical, intent(in) :: xlim_set, ylim_set
        real(wp), intent(in) :: x_min, x_max, y_min, y_max
        real(wp), intent(out) :: x_min_transformed, x_max_transformed
        real(wp), intent(out) :: y_min_transformed, y_max_transformed
        character(len=*), intent(in) :: xscale, yscale
        real(wp), intent(in) :: symlog_threshold
        real(wp), intent(in), optional :: symlog_base, symlog_linscale
        real(wp), intent(out) :: x_min_data, x_max_data, y_min_data, y_max_data
        logical, intent(out) :: first_plot, has_valid_data

        if (xlim_set .and. ylim_set) then
            x_min_transformed = apply_scale_transform(x_min, xscale, symlog_threshold, &
                                                      base=symlog_base, linscale=symlog_linscale)
            x_max_transformed = apply_scale_transform(x_max, xscale, symlog_threshold, &
                                                      base=symlog_base, linscale=symlog_linscale)
            y_min_transformed = apply_scale_transform(y_min, yscale, symlog_threshold, &
                                                      base=symlog_base, linscale=symlog_linscale)
            y_max_transformed = apply_scale_transform(y_max, yscale, symlog_threshold, &
                                                      base=symlog_base, linscale=symlog_linscale)
            return
        end if

        first_plot = .true.
        has_valid_data = .false.
        x_min_data = 0.0_wp
        x_max_data = 1.0_wp
        y_min_data = 0.0_wp
        y_max_data = 1.0_wp
    end subroutine initialize_data_ranges

    subroutine apply_single_point_margins(has_valid_data, x_min_data, x_max_data, &
                                          y_min_data, y_max_data)
        logical, intent(in) :: has_valid_data
        real(wp), intent(inout) :: x_min_data, x_max_data, y_min_data, y_max_data

        real(wp) :: range_x, range_y, margin_factor
        real(wp) :: machine_precision_threshold

        margin_factor = 0.1_wp
        machine_precision_threshold = 100.0_wp * epsilon(1.0_wp)

        if (has_valid_data) then
            range_x = x_max_data - x_min_data
            range_y = y_max_data - y_min_data

            if (abs(range_x) < 1.0e-10_wp .or. &
                abs(range_x) < machine_precision_threshold) then

                call expand_precision_range(x_min_data, x_max_data, range_x, &
                                           margin_factor, machine_precision_threshold)
            end if

            if (abs(range_y) < 1.0e-10_wp .or. &
                abs(range_y) < machine_precision_threshold) then

                call expand_precision_range(y_min_data, y_max_data, range_y, &
                                           margin_factor, machine_precision_threshold)
            end if
        end if
    end subroutine apply_single_point_margins

    subroutine expand_precision_range(coord_min, coord_max, current_range, &
                                     margin_factor, precision_threshold)
        real(wp), intent(inout) :: coord_min, coord_max
        real(wp), intent(in) :: current_range, margin_factor, precision_threshold

        real(wp) :: range_center, expanded_range, absolute_scale
        real(wp) :: minimum_visible_range

        range_center = (coord_min + coord_max) * 0.5_wp
        absolute_scale = max(abs(coord_min), abs(coord_max))

        if (absolute_scale < precision_threshold) then
            minimum_visible_range = margin_factor
        else
            minimum_visible_range = absolute_scale * margin_factor
        end if

        if (abs(current_range) < minimum_visible_range) then
            expanded_range = minimum_visible_range
            coord_min = range_center - expanded_range * 0.5_wp
            coord_max = range_center + expanded_range * 0.5_wp
        end if
    end subroutine expand_precision_range

    subroutine finalize_data_ranges(xlim_set, ylim_set, x_min, x_max, y_min, y_max, &
                                     x_min_data, x_max_data, y_min_data, y_max_data, &
                                     x_min_transformed, x_max_transformed, &
                                     y_min_transformed, y_max_transformed, &
                                     xscale, yscale, symlog_threshold, &
                                     symlog_base, symlog_linscale)
        logical, intent(in) :: xlim_set, ylim_set
        real(wp), intent(inout) :: x_min, x_max, y_min, y_max
        real(wp), intent(in) :: x_min_data, x_max_data, y_min_data, y_max_data
        real(wp), intent(out) :: x_min_transformed, x_max_transformed
        real(wp), intent(out) :: y_min_transformed, y_max_transformed
        character(len=*), intent(in) :: xscale, yscale
        real(wp), intent(in) :: symlog_threshold
        real(wp), intent(in), optional :: symlog_base, symlog_linscale

        real(wp) :: x_clamped_min, x_clamped_max, y_clamped_min, y_clamped_max
        character(len=256) :: msg

        if (.not. xlim_set) then
            x_min = x_min_data
            x_max = x_max_data
        end if

        if (.not. ylim_set) then
            y_min = y_min_data
            y_max = y_max_data
        end if

        if (trim(xscale) == 'log') then
            call clamp_extreme_log_range(x_min, x_max, x_clamped_min, x_clamped_max)
            if (abs(x_clamped_min - x_min) > 1.0e-10_wp .or. &
                abs(x_clamped_max - x_max) > 1.0e-10_wp) then
                write(msg, '(A,A,F12.4,A,F12.4,A,F12.4,A,F12.4)') &
                    'X-axis range clamped for log scale visualization; ', &
                    'original=', x_min, ' to ', x_max, &
                    '; clamped=', x_clamped_min, ' to ', x_clamped_max
                call log_debug(trim(adjustl(msg)))
            end if
            x_min = x_clamped_min
            x_max = x_clamped_max
        end if

        if (trim(yscale) == 'log') then
            call clamp_extreme_log_range(y_min, y_max, y_clamped_min, y_clamped_max)
            if (abs(y_clamped_min - y_min) > 1.0e-10_wp .or. &
                abs(y_clamped_max - y_max) > 1.0e-10_wp) then
                write(msg, '(A,A,F12.4,A,F12.4,A,F12.4,A,F12.4)') &
                    'Y-axis range clamped for log scale visualization; ', &
                    'original=', y_min, ' to ', y_max, &
                    '; clamped=', y_clamped_min, ' to ', y_clamped_max
                call log_debug(trim(adjustl(msg)))
            end if
            y_min = y_clamped_min
            y_max = y_clamped_max
        end if

        x_min_transformed = apply_scale_transform(x_min, xscale, symlog_threshold, &
                                                  base=symlog_base, linscale=symlog_linscale)
        x_max_transformed = apply_scale_transform(x_max, xscale, symlog_threshold, &
                                                  base=symlog_base, linscale=symlog_linscale)
        y_min_transformed = apply_scale_transform(y_min, yscale, symlog_threshold, &
                                                  base=symlog_base, linscale=symlog_linscale)
        y_max_transformed = apply_scale_transform(y_max, yscale, symlog_threshold, &
                                                  base=symlog_base, linscale=symlog_linscale)
    end subroutine finalize_data_ranges
end module fortplot_figure_data_ranges