module fortplot_figure_data_ranges use, intrinsic :: iso_fortran_env, only: wp => real64 use fortplot_scales, only: apply_scale_transform, apply_inverse_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_HISTOGRAM, & 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 public :: determine_sticky_edges contains subroutine determine_sticky_edges(plots, plot_count, axis_filter, & sticky_x_min, sticky_x_max, & sticky_y_min, sticky_y_max) !! Flag axis sides where the generic 5% data-range margin must not be !! applied, matching matplotlib's sticky edges. Bars carry their own !! margin (with a sticky baseline) in process_bar_plot_ranges; a !! pcolormesh and contour grids fill their data extent exactly. In each !! case the data range is authoritative and adding margin would float !! the artist off the axis (bars) or leave a gap around it (mesh/contour). type(plot_data_t), intent(in) :: plots(:) integer, intent(in) :: plot_count integer, intent(in), optional :: axis_filter logical, intent(out) :: sticky_x_min, sticky_x_max logical, intent(out) :: sticky_y_min, sticky_y_max integer :: i sticky_x_min = .false. sticky_x_max = .false. sticky_y_min = .false. sticky_y_max = .false. do i = 1, min(plot_count, size(plots)) if (present(axis_filter)) then if (plots(i)%axis /= axis_filter) cycle end if if (plots(i)%plot_type == PLOT_TYPE_BAR .or. & plots(i)%plot_type == PLOT_TYPE_HISTOGRAM .or. & plots(i)%plot_type == PLOT_TYPE_PCOLORMESH .or. & plots(i)%plot_type == PLOT_TYPE_CONTOUR) then sticky_x_min = .true. sticky_x_max = .true. sticky_y_min = .true. sticky_y_max = .true. return end if end do end subroutine determine_sticky_edges 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 logical :: sticky_x_min, sticky_x_max logical :: sticky_y_min, sticky_y_max 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, PLOT_TYPE_HISTOGRAM) 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) if (use_filter) then call determine_sticky_edges(plots, plot_count, filtered_axis, & sticky_x_min, sticky_x_max, & sticky_y_min, sticky_y_max) else call determine_sticky_edges(plots, plot_count, & sticky_x_min=sticky_x_min, & sticky_x_max=sticky_x_max, & sticky_y_min=sticky_y_min, & sticky_y_max=sticky_y_max) end if 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, & sticky_x_min, sticky_x_max, & sticky_y_min, sticky_y_max) 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 apply_default_axis_margin(coord_min, coord_max, scale_type, & symlog_threshold, base, linscale, & sticky_min, sticky_max) real(wp), intent(inout) :: coord_min, coord_max character(len=*), intent(in) :: scale_type real(wp), intent(in) :: symlog_threshold real(wp), intent(in), optional :: base, linscale logical, intent(in), optional :: sticky_min, sticky_max real(wp) :: transformed_min, transformed_max real(wp) :: expanded_min, expanded_max real(wp) :: clamped_min, clamped_max logical :: pin_min, pin_max pin_min = .false. pin_max = .false. if (present(sticky_min)) pin_min = sticky_min if (present(sticky_max)) pin_max = sticky_max if (coord_max <= coord_min) return transformed_min = apply_scale_transform(coord_min, scale_type, symlog_threshold, & base=base, linscale=linscale) transformed_max = apply_scale_transform(coord_max, scale_type, symlog_threshold, & base=base, linscale=linscale) call expand_transformed_range(transformed_min, transformed_max, & expanded_min, expanded_max, pin_min, pin_max) coord_min = apply_inverse_scale_transform(expanded_min, scale_type, symlog_threshold, & base=base, linscale=linscale) coord_max = apply_inverse_scale_transform(expanded_max, scale_type, symlog_threshold, & base=base, linscale=linscale) if (trim(scale_type) == 'log') then call clamp_extreme_log_range(coord_min, coord_max, clamped_min, clamped_max) coord_min = clamped_min coord_max = clamped_max end if end subroutine apply_default_axis_margin subroutine expand_transformed_range(data_min, data_max, expanded_min, expanded_max, & sticky_min, sticky_max) real(wp), intent(in) :: data_min, data_max real(wp), intent(out) :: expanded_min, expanded_max logical, intent(in) :: sticky_min, sticky_max real(wp), parameter :: DATA_RANGE_MARGIN = 0.05_wp real(wp) :: span if (data_max <= data_min) then expanded_min = data_min expanded_max = data_max return end if span = data_max - data_min if (sticky_min) then expanded_min = data_min else expanded_min = data_min - DATA_RANGE_MARGIN * span end if if (sticky_max) then expanded_max = data_max else expanded_max = data_max + DATA_RANGE_MARGIN * span end if end subroutine expand_transformed_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, & sticky_x_min, sticky_x_max, & sticky_y_min, sticky_y_max) 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 logical, intent(in), optional :: sticky_x_min, sticky_x_max logical, intent(in), optional :: sticky_y_min, sticky_y_max 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 if (.not. xlim_set) then call apply_default_axis_margin(x_min, x_max, xscale, symlog_threshold, & symlog_base, symlog_linscale, & sticky_min=sticky_x_min, & sticky_max=sticky_x_max) end if if (.not. ylim_set) then call apply_default_axis_margin(y_min, y_max, yscale, symlog_threshold, & symlog_base, symlog_linscale, & sticky_min=sticky_y_min, & sticky_max=sticky_y_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