module fortplot_coordinate_validation !! Input validation for coordinate arrays and edge case handling !! !! This module provides comprehensive validation for coordinate data to prevent !! silent failures and ensure robust plotting across all backends. !! Addresses Issue #436 - single point plotting failures. use, intrinsic :: iso_fortran_env, only: wp => real64 use, intrinsic :: ieee_arithmetic, only: ieee_is_finite use fortplot_logging, only: log_error, log_warning, log_info implicit none private public :: validate_coordinate_arrays, coordinate_validation_result_t public :: is_valid_single_point, is_empty_array, has_machine_precision_issues public :: validate_coordinate_ranges, suggest_marker_for_single_point ! Validation result type type :: coordinate_validation_result_t logical :: is_valid = .false. logical :: is_single_point = .false. logical :: is_empty = .false. logical :: has_precision_issues = .false. logical :: has_large_values = .false. logical :: should_use_markers = .false. character(len=256) :: message = "" character(len=32) :: suggested_marker = "" end type coordinate_validation_result_t ! Constants for validation real(wp), parameter :: LARGE_COORDINATE_THRESHOLD = 1.0e10_wp real(wp), parameter :: PRECISION_THRESHOLD = 100.0_wp * epsilon(1.0_wp) contains function validate_coordinate_arrays(x, y, context) result(validation) !! Comprehensive validation of coordinate arrays for plotting !! Returns validation result with detailed information about the data real(wp), contiguous, intent(in) :: x(:), y(:) character(len=*), intent(in), optional :: context type(coordinate_validation_result_t) :: validation character(len=64) :: ctx integer :: n ! Set context for error messages if (present(context)) then ctx = context else ctx = "coordinate validation" end if n = size(x) ! Initialize result validation%is_valid = .false. validation%is_single_point = .false. validation%is_empty = .false. validation%has_precision_issues = .false. validation%has_large_values = .false. validation%should_use_markers = .false. validation%message = "" validation%suggested_marker = "" ! Check array size consistency if (size(x) /= size(y)) then validation%message = trim(ctx) // ": x and y arrays must have the same size" call log_error(trim(validation%message)) return end if ! Check for empty arrays if (n == 0) then validation%is_empty = .true. validation%is_valid = .true. ! Empty is valid, just a special case validation%message = trim(ctx) // ": empty arrays provided" call log_info(trim(validation%message)) return end if ! Check for single point if (n == 1) then validation%is_single_point = .true. validation%should_use_markers = .true. validation%suggested_marker = "o" ! Circle marker for single points end if ! Validate individual coordinate values if (.not. validate_coordinate_ranges(x, y, validation)) then validation%message = trim(ctx) // ": invalid coordinate values detected" call log_error(trim(validation%message)) return end if ! Check for machine precision issues (coordinates too close together) if (n > 1) then if (has_machine_precision_issues(x, y)) then validation%has_precision_issues = .true. validation%message = trim(ctx) // ": coordinates may be too close for reliable rendering" call log_warning(trim(validation%message)) end if end if ! All validations passed validation%is_valid = .true. ! Generate informative message if (validation%is_single_point) then validation%message = trim(ctx) // ": single point detected - consider using markers" else if (validation%has_precision_issues) then validation%message = trim(ctx) // ": coordinates validated with precision warnings" else validation%message = trim(ctx) // ": coordinates validated successfully" end if end function validate_coordinate_arrays function validate_coordinate_ranges(x, y, validation) result(is_valid) !! Validate that coordinate values are finite and reasonable real(wp), contiguous, intent(in) :: x(:), y(:) type(coordinate_validation_result_t), intent(inout) :: validation logical :: is_valid integer :: i real(wp) :: max_abs_x, max_abs_y is_valid = .true. max_abs_x = 0.0_wp max_abs_y = 0.0_wp ! Check each coordinate do i = 1, size(x) ! Check for NaN or infinite values if (.not. ieee_is_finite(x(i)) .or. .not. ieee_is_finite(y(i))) then call log_error("Invalid coordinate detected: NaN or infinite value") is_valid = .false. return end if ! Track maximum values for large coordinate detection max_abs_x = max(max_abs_x, abs(x(i))) max_abs_y = max(max_abs_y, abs(y(i))) end do ! Check for excessively large coordinates if (max_abs_x > LARGE_COORDINATE_THRESHOLD .or. max_abs_y > LARGE_COORDINATE_THRESHOLD) then validation%has_large_values = .true. call log_warning("Large coordinate values detected - may cause rendering issues") end if end function validate_coordinate_ranges function has_machine_precision_issues(x, y) result(has_issues) !! Check if coordinates span too small a range to be reliably rendered. !! A collapsed axis (whole extent at machine-precision scale relative to !! the coordinate magnitude) cannot be distinguished after normalization. !! Individual coincident adjacent points, common near extrema of smooth !! curves, are normal and must not be flagged. real(wp), contiguous, intent(in) :: x(:), y(:) logical :: has_issues has_issues = .false. if (size(x) < 2) return if (axis_range_below_precision(x)) has_issues = .true. if (axis_range_below_precision(y)) has_issues = .true. end function has_machine_precision_issues function axis_range_below_precision(v) result(below) !! True when the axis extent is negligible next to the value magnitude. real(wp), contiguous, intent(in) :: v(:) logical :: below real(wp) :: range_v, max_abs below = .false. range_v = maxval(v) - minval(v) if (range_v <= 0.0_wp) return max_abs = maxval(abs(v)) if (range_v < PRECISION_THRESHOLD * max_abs) below = .true. end function axis_range_below_precision function is_valid_single_point(x, y) result(is_single) !! Check if arrays represent a valid single point real(wp), contiguous, intent(in) :: x(:), y(:) logical :: is_single is_single = (size(x) == 1 .and. size(y) == 1 .and. & ieee_is_finite(x(1)) .and. ieee_is_finite(y(1))) end function is_valid_single_point function is_empty_array(x, y) result(is_empty) !! Check if arrays are empty real(wp), contiguous, intent(in) :: x(:), y(:) logical :: is_empty is_empty = (size(x) == 0 .or. size(y) == 0) end function is_empty_array function suggest_marker_for_single_point(backend_type) result(marker) !! Suggest appropriate marker type for single point based on backend character(len=*), intent(in) :: backend_type character(len=8) :: marker select case (trim(backend_type)) case ('ascii') marker = "*" ! Asterisk works well in ASCII case ('png', 'pdf') marker = "o" ! Circle for graphical backends case default marker = "o" ! Default to circle end select end function suggest_marker_for_single_point end module fortplot_coordinate_validation