fortplot_matplotlib_hist_wrappers.f90 Source File


Source Code

module fortplot_matplotlib_hist_wrappers
    !! Matplotlib-style hist/histogram wrappers split out from
    !! fortplot_matplotlib_plot_wrappers to keep each source file under
    !! the 1000-line hard limit.

    use, intrinsic :: iso_fortran_env, only: wp => real64
    use fortplot_global, only: fig => global_figure
    use fortplot_logging, only: log_error
    use fortplot_matplotlib_color_utils, only: resolve_color_string_or_rgb
    use fortplot_matplotlib_session, only: ensure_fig_init
    use fortplot_string_utils, only: to_lowercase

    implicit none
    private

    public :: hist
    public :: histogram

    interface hist
        module procedure hist_rgb
        module procedure hist_string
    end interface hist

    interface histogram
        module procedure histogram_rgb
        module procedure histogram_string
    end interface histogram

contains

    subroutine hist_rgb(data, bins, range, density, weights, cumulative, &
                        histtype, orientation, stacked, log, label, color, alpha)
        !! Draw a histogram with an RGB color.
        !!
        !! Parameters
        !! data : real(wp), contiguous, intent(in)
        !!     Sample values.
        !! bins : integer, optional
        !!     Number of bins.
        !! range : real(wp), optional
        !!     Lower and upper bin range.
        !! density : logical, optional
        !!     Normalize the histogram when .true.
        !! weights : real(wp), optional
        !!     Per-sample weights.
        !! cumulative : logical, optional
        !!     Accumulate bin counts when .true.
        !! histtype : character(len=*), optional
        !!     Histogram style keyword.
        !! orientation : character(len=*), optional
        !!     'vertical' or 'horizontal'.
        !! stacked : logical, optional
        !!     Accepted for matplotlib parity.
        !! log : logical, optional
        !!     Accepted for matplotlib parity.
        !! label : character(len=*), optional
        !!     Legend label.
        !! color : real(wp)(3), optional
        !!     Solid RGB color.
        !! alpha : real(wp), optional
        !!     Bar transparency.
        real(wp), contiguous, intent(in) :: data(:)
        integer, intent(in), optional :: bins
        real(wp), intent(in), optional :: range(2)
        logical, intent(in), optional :: density
        real(wp), intent(in), optional :: weights(:)
        logical, intent(in), optional :: cumulative
        character(len=*), intent(in), optional :: histtype, orientation
        logical, intent(in), optional :: stacked, log
        character(len=*), intent(in), optional :: label
        real(wp), intent(in), optional :: color(3)
        real(wp), intent(in), optional :: alpha

        call dispatch_histogram(data, bins, range, density, weights, cumulative, &
                                histtype, orientation, stacked, log, label, &
                                color_rgb=color, alpha=alpha)
    end subroutine hist_rgb

    subroutine hist_string(data, color, bins, range, density, weights, &
                           cumulative, histtype, orientation, stacked, log, &
                           label, alpha)
        !! Draw a histogram with a named color.
        !!
        !! Parameters
        !! data : real(wp), contiguous, intent(in)
        !!     Sample values.
        !! color : character(len=*), intent(in)
        !!     Named or hex color string.
        !! bins : integer, optional
        !!     Number of bins.
        !! range : real(wp), optional
        !!     Lower and upper bin range.
        !! density : logical, optional
        !!     Normalize the histogram when .true.
        !! weights : real(wp), optional
        !!     Per-sample weights.
        !! cumulative : logical, optional
        !!     Accumulate bin counts when .true.
        !! histtype : character(len=*), optional
        !!     Histogram style keyword.
        !! orientation : character(len=*), optional
        !!     'vertical' or 'horizontal'.
        !! stacked : logical, optional
        !!     Accepted for matplotlib parity.
        !! log : logical, optional
        !!     Accepted for matplotlib parity.
        !! label : character(len=*), optional
        !!     Legend label.
        !! alpha : real(wp), optional
        !!     Bar transparency.
        real(wp), contiguous, intent(in) :: data(:)
        character(len=*), intent(in) :: color
        integer, intent(in), optional :: bins
        real(wp), intent(in), optional :: range(2)
        logical, intent(in), optional :: density
        real(wp), intent(in), optional :: weights(:)
        logical, intent(in), optional :: cumulative
        character(len=*), intent(in), optional :: histtype, orientation
        logical, intent(in), optional :: stacked, log
        character(len=*), intent(in), optional :: label
        real(wp), intent(in), optional :: alpha

        real(wp) :: color_rgb(3)
        logical :: has_color

        call resolve_color_string_or_rgb(color_str=color, context='hist', &
                                         rgb_out=color_rgb, has_color=has_color)
        if (has_color) then
            call dispatch_histogram(data, bins, range, density, weights, &
                                    cumulative, histtype, orientation, stacked, &
                                    log, label, color_rgb=color_rgb, alpha=alpha)
        else
            call dispatch_histogram(data, bins, range, density, weights, &
                                    cumulative, histtype, orientation, stacked, &
                                    log, label, alpha=alpha)
        end if
    end subroutine hist_string

    subroutine histogram_rgb(data, bins, range, density, weights, cumulative, &
                             histtype, orientation, stacked, log, label, color, &
                             alpha)
        !! Matplotlib-style alias for hist_rgb.
        !!
        !! Parameters
        !! data : real(wp), contiguous, intent(in)
        !!     Sample values.
        !! bins : integer, optional
        !!     Number of bins.
        !! range : real(wp), optional
        !!     Lower and upper bin range.
        !! density : logical, optional
        !!     Normalize the histogram when .true.
        !! weights : real(wp), optional
        !!     Per-sample weights.
        !! cumulative : logical, optional
        !!     Accumulate bin counts when .true.
        !! histtype : character(len=*), optional
        !!     Histogram style keyword.
        !! orientation : character(len=*), optional
        !!     'vertical' or 'horizontal'.
        !! stacked : logical, optional
        !!     Accepted for matplotlib parity.
        !! log : logical, optional
        !!     Accepted for matplotlib parity.
        !! label : character(len=*), optional
        !!     Legend label.
        !! color : real(wp)(3), optional
        !!     Solid RGB color.
        !! alpha : real(wp), optional
        !!     Bar transparency.
        real(wp), contiguous, intent(in) :: data(:)
        integer, intent(in), optional :: bins
        real(wp), intent(in), optional :: range(2)
        logical, intent(in), optional :: density
        real(wp), intent(in), optional :: weights(:)
        logical, intent(in), optional :: cumulative
        character(len=*), intent(in), optional :: histtype, orientation
        logical, intent(in), optional :: stacked, log
        character(len=*), intent(in), optional :: label
        real(wp), intent(in), optional :: color(3)
        real(wp), intent(in), optional :: alpha

        call hist_rgb(data, bins=bins, range=range, density=density, &
                      weights=weights, cumulative=cumulative, histtype=histtype, &
                      orientation=orientation, stacked=stacked, log=log, &
                      label=label, color=color, alpha=alpha)
    end subroutine histogram_rgb

    subroutine histogram_string(data, color, bins, range, density, weights, &
                                cumulative, histtype, orientation, stacked, log, &
                                label, alpha)
        !! Matplotlib-style alias for hist_string.
        !!
        !! Parameters
        !! data : real(wp), contiguous, intent(in)
        !!     Sample values.
        !! color : character(len=*), intent(in)
        !!     Named or hex color string.
        !! bins : integer, optional
        !!     Number of bins.
        !! range : real(wp), optional
        !!     Lower and upper bin range.
        !! density : logical, optional
        !!     Normalize the histogram when .true.
        !! weights : real(wp), optional
        !!     Per-sample weights.
        !! cumulative : logical, optional
        !!     Accumulate bin counts when .true.
        !! histtype : character(len=*), optional
        !!     Histogram style keyword.
        !! orientation : character(len=*), optional
        !!     'vertical' or 'horizontal'.
        !! stacked : logical, optional
        !!     Accepted for matplotlib parity.
        !! log : logical, optional
        !!     Accepted for matplotlib parity.
        !! label : character(len=*), optional
        !!     Legend label.
        !! alpha : real(wp), optional
        !!     Bar transparency.
        real(wp), contiguous, intent(in) :: data(:)
        character(len=*), intent(in) :: color
        integer, intent(in), optional :: bins
        real(wp), intent(in), optional :: range(2)
        logical, intent(in), optional :: density
        real(wp), intent(in), optional :: weights(:)
        logical, intent(in), optional :: cumulative
        character(len=*), intent(in), optional :: histtype, orientation
        logical, intent(in), optional :: stacked, log
        character(len=*), intent(in), optional :: label
        real(wp), intent(in), optional :: alpha

        call hist_string(data, color=color, bins=bins, range=range, &
                         density=density, weights=weights, cumulative=cumulative, &
                         histtype=histtype, orientation=orientation, &
                         stacked=stacked, log=log, label=label, alpha=alpha)
    end subroutine histogram_string

    subroutine dispatch_histogram(data, bins, range, density, weights, cumulative, &
                                  histtype, orientation, stacked, log, label, &
                                  color_rgb, alpha)
        !! Central histogram entry point shared by hist/histogram overloads.
        use fortplot_figure_core_advanced, only: core_hist
        real(wp), contiguous, intent(in) :: data(:)
        integer, intent(in), optional :: bins
        real(wp), intent(in), optional :: range(2)
        logical, intent(in), optional :: density
        real(wp), intent(in), optional :: weights(:)
        logical, intent(in), optional :: cumulative
        character(len=*), intent(in), optional :: histtype, orientation
        logical, intent(in), optional :: stacked, log
        character(len=*), intent(in), optional :: label
        real(wp), intent(in), optional :: color_rgb(3)
        real(wp), intent(in), optional :: alpha

        if (present(histtype)) then
            if (.not. histtype_is_supported(histtype)) then
                call log_error('hist: unsupported histtype')
                return
            end if
        end if

        call ensure_fig_init()

        if (size(data) == 0) return
        call core_hist(fig%plots, fig%state, fig%plot_count, data, bins, density, label, &
                       color_rgb, range, weights, cumulative, orientation, alpha)
        call finalise_histogram(alpha, histtype, stacked, log)
        call fig%relocate_last_plot_to_subplot()
    end subroutine dispatch_histogram

    subroutine compute_weighted_histogram(data, n_bins, range, weights, density, &
                                          cumulative, bin_edges, bin_counts)
        !! Extend the core binning with range-clipping, per-sample weights,
        !! density normalisation, and cumulative accumulation.
        real(wp), contiguous, intent(in) :: data(:)
        integer, intent(in) :: n_bins
        real(wp), intent(in), optional :: range(2)
        real(wp), intent(in), optional :: weights(:)
        logical, intent(in), optional :: density, cumulative
        real(wp), allocatable, intent(out) :: bin_edges(:), bin_counts(:)

        integer :: i, bin_index, n_data
        real(wp) :: data_min, data_max, bin_width, total

        n_data = size(data)
        if (present(weights)) then
            if (size(weights) /= n_data) then
                call log_error('hist: weights length must match data')
                return
            end if
        end if

        if (present(range)) then
            data_min = range(1)
            data_max = range(2)
        else
            data_min = minval(data)
            data_max = maxval(data)
            if (abs(data_max - data_min) < epsilon(1.0_wp)) then
                data_min = data_min - 0.5_wp
                data_max = data_max + 0.5_wp
            end if
        end if

        if (data_max <= data_min) then
            call log_error('hist: range upper bound must exceed lower bound')
            return
        end if

        allocate (bin_edges(n_bins + 1), bin_counts(n_bins))
        bin_width = (data_max - data_min)/real(n_bins, wp)
        do i = 1, n_bins + 1
            bin_edges(i) = data_min + real(i - 1, wp)*bin_width
        end do
        bin_counts = 0.0_wp

        do i = 1, n_data
            if (data(i) < data_min .or. data(i) > data_max) cycle
            bin_index = min(n_bins, max(1, int((data(i) - data_min)/bin_width) + 1))
            if (present(weights)) then
                bin_counts(bin_index) = bin_counts(bin_index) + weights(i)
            else
                bin_counts(bin_index) = bin_counts(bin_index) + 1.0_wp
            end if
        end do

        if (present(density)) then
            if (density) then
                total = sum(bin_counts)*bin_width
                if (total > 0.0_wp) bin_counts = bin_counts/total
            end if
        end if

        if (present(cumulative)) then
            if (cumulative) then
                do i = 2, n_bins
                    bin_counts(i) = bin_counts(i) + bin_counts(i - 1)
                end do
            end if
        end if
    end subroutine compute_weighted_histogram

    pure function orientation_is_horizontal(orientation) result(horizontal)
        character(len=*), intent(in) :: orientation
        logical :: horizontal
        horizontal = trim(orientation) == 'horizontal' .or. &
                     trim(orientation) == 'Horizontal' .or. &
                     trim(orientation) == 'HORIZONTAL'
    end function orientation_is_horizontal

    function histtype_is_supported(histtype) result(supported)
        character(len=*), intent(in) :: histtype
        logical :: supported
        character(len=:), allocatable :: normalized

        normalized = to_lowercase(trim(histtype))
        select case (normalized)
        case ('bar', 'barstacked', 'step', 'stepfilled')
            supported = .true.
        case default
            supported = .false.
        end select
    end function histtype_is_supported

    subroutine finalise_histogram(alpha, histtype, stacked, log)
        !! Attach optional metadata to the last histogram plot. These kwargs
        !! have no current rendering effect but are recorded so callers can
        !! inspect them through the plot_data surface and so the facade does
        !! not emit per-call warnings in correct matplotlib code.
        real(wp), intent(in), optional :: alpha
        character(len=*), intent(in), optional :: histtype
        logical, intent(in), optional :: stacked, log

        integer :: idx

        idx = fig%plot_count
        if (idx < 1) return
        if (.not. allocated(fig%plots)) return
        if (idx > size(fig%plots)) return

        if (present(alpha)) then
            fig%plots(idx)%fill_alpha = max(0.0_wp, min(1.0_wp, alpha))
            fig%plots(idx)%marker_face_alpha = fig%plots(idx)%fill_alpha
        end if
        if (present(histtype) .or. present(stacked) .or. present(log)) then
            continue
        end if
    end subroutine finalise_histogram

end module fortplot_matplotlib_hist_wrappers