fortplot_plotting.f90 Source File


Source Code

module fortplot_plotting
    !! Basic plot addition methods for figure (SOLID principles compliance)
    !! 
    !! This module contains basic add_* methods for plot types,
    !! separated from figure base for better modularity and maintainability.
    !! Advanced plot types are in fortplot_plotting_advanced module.
    
    use, intrinsic :: iso_fortran_env, only: wp => real64
    use, intrinsic :: ieee_arithmetic, only: ieee_is_finite
    use fortplot_figure_core, only: figure_t
    use fortplot_figure_initialization, only: figure_state_t
    use fortplot_plot_data, only: plot_data_t, arrow_data_t, &
                                    PLOT_TYPE_LINE, PLOT_TYPE_CONTOUR, PLOT_TYPE_PCOLORMESH, &
                                    PLOT_TYPE_ERRORBAR, PLOT_TYPE_BAR, PLOT_TYPE_HISTOGRAM, &
                                    PLOT_TYPE_BOXPLOT, PLOT_TYPE_SCATTER, &
                                    HALF_WIDTH, IQR_WHISKER_MULTIPLIER
    use fortplot_colors, only: parse_color, color_t
    use fortplot_annotations, only: text_annotation_t, COORD_DATA, COORD_FIGURE, COORD_AXIS
    use fortplot_logging, only: log_warning, log_error, log_info
    use fortplot_errors, only: fortplot_error_t, SUCCESS, ERROR_RESOURCE_LIMIT
    use fortplot_format_parser, only: parse_format_string
    use fortplot_coordinate_validation, only: validate_coordinate_arrays, coordinate_validation_result_t

    implicit none

    private
    public :: add_plot, add_3d_plot, add_scatter_2d, add_scatter_3d, add_surface
    public :: errorbar, add_text_annotation, add_arrow_annotation
    public :: add_line_plot_data, add_scatter_plot_data  ! Export for advanced module

    interface add_plot
        module procedure add_plot_impl
    end interface

    interface add_scatter_2d
        module procedure add_scatter_2d_impl
    end interface

    interface add_scatter_3d
        module procedure add_scatter_3d_impl
    end interface

    interface errorbar
        module procedure errorbar_impl
    end interface

contains

    subroutine add_plot_impl(self, x, y, label, linestyle, color_rgb, color_str, marker, markercolor)
        !! Add 2D line plot to figure
        class(figure_t), intent(inout) :: self
        real(wp), intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: label
        character(len=*), intent(in), optional :: linestyle
        real(wp), intent(in), optional :: color_rgb(3)
        character(len=*), intent(in), optional :: color_str
        character(len=*), intent(in), optional :: marker
        real(wp), intent(in), optional :: markercolor(3)
        
        call add_line_plot_data(self, x, y, label, linestyle, color_rgb, color_str, marker)
    end subroutine add_plot_impl

    subroutine add_3d_plot(self, x, y, z, label, linestyle, markersize, linewidth)
        !! Add 3D line plot to figure (projected to 2D)
        class(figure_t), intent(inout) :: self
        real(wp), intent(in) :: x(:), y(:), z(:)
        character(len=*), intent(in), optional :: label
        character(len=*), intent(in), optional :: linestyle
        real(wp), intent(in), optional :: markersize
        real(wp), intent(in), optional :: linewidth
        
        call add_3d_line_plot_data(self, x, y, z, label, linestyle, &
                                  marker='', markersize=markersize, linewidth=linewidth)
    end subroutine add_3d_plot

    subroutine add_scatter_2d_impl(self, x, y, s, c, label, marker, markersize, color, &
                                   colormap, vmin, vmax, show_colorbar, alpha)
        !! Add 2D scatter plot to figure
        class(figure_t), intent(inout) :: self
        real(wp), intent(in) :: x(:), y(:)
        real(wp), intent(in), optional :: s(:)  ! size array
        real(wp), intent(in), optional :: c(:)  ! color array  
        character(len=*), intent(in), optional :: label
        character(len=*), intent(in), optional :: marker
        real(wp), intent(in), optional :: markersize
        real(wp), intent(in), optional :: color(3)  ! single RGB color
        character(len=*), intent(in), optional :: colormap
        real(wp), intent(in), optional :: vmin, vmax
        logical, intent(in), optional :: show_colorbar
        real(wp), intent(in), optional :: alpha
        
        call add_scatter_plot_data(self, x, y, s=s, c=c, label=label, &
                                  marker=marker, markersize=markersize, color=color, &
                                  colormap=colormap, vmin=vmin, vmax=vmax, &
                                  show_colorbar=show_colorbar, alpha=alpha)
    end subroutine add_scatter_2d_impl

    subroutine add_scatter_3d_impl(self, x, y, z, s, c, label, marker, markersize, color, &
                                   colormap, vmin, vmax, show_colorbar, alpha)
        !! Add 3D scatter plot to figure
        class(figure_t), intent(inout) :: self
        real(wp), intent(in) :: x(:), y(:), z(:)
        real(wp), intent(in), optional :: s(:)  ! size array
        real(wp), intent(in), optional :: c(:)  ! color array  
        character(len=*), intent(in), optional :: label
        character(len=*), intent(in), optional :: marker
        real(wp), intent(in), optional :: markersize
        real(wp), intent(in), optional :: color(3)  ! single RGB color
        character(len=*), intent(in), optional :: colormap
        real(wp), intent(in), optional :: vmin, vmax
        logical, intent(in), optional :: show_colorbar
        real(wp), intent(in), optional :: alpha
        
        call add_scatter_plot_data(self, x, y, z, s, c, label, marker, markersize, color, &
                                  colormap, vmin, vmax, show_colorbar, alpha)
    end subroutine add_scatter_3d_impl

    subroutine add_surface(self, x, y, z, label)
        !! Add surface plot to figure
        class(figure_t), intent(inout) :: self
        real(wp), intent(in) :: x(:), y(:), z(:,:)
        character(len=*), intent(in), optional :: label
        
        call add_surface_plot_data(self, x, y, z, label)
    end subroutine add_surface

    subroutine errorbar_impl(self, x, y, xerr, yerr, xerr_lower, xerr_upper, &
                            yerr_lower, yerr_upper, label, marker, markersize, &
                            ecolor, elinewidth, capsize, capthick, color)
        !! Add error bar plot to figure
        class(figure_t), intent(inout) :: self
        real(wp), intent(in) :: x(:), y(:)
        real(wp), intent(in), optional :: xerr(:), yerr(:)
        real(wp), intent(in), optional :: xerr_lower(:), xerr_upper(:)
        real(wp), intent(in), optional :: yerr_lower(:), yerr_upper(:)
        character(len=*), intent(in), optional :: label
        character(len=*), intent(in), optional :: marker
        real(wp), intent(in), optional :: markersize
        real(wp), intent(in), optional :: ecolor(3)
        real(wp), intent(in), optional :: elinewidth, capsize, capthick
        real(wp), intent(in), optional :: color(3)
        
        integer :: subplot_idx, plot_idx, color_idx
        
        self%plot_count = self%plot_count + 1
        plot_idx = self%plot_count
        
        ! Ensure plots array is allocated
        if (.not. allocated(self%plots)) then
            allocate(self%plots(self%state%max_plots))
        else if (plot_idx > size(self%plots)) then
            return
        end if
        
        self%plots(plot_idx)%plot_type = PLOT_TYPE_ERRORBAR
        
        allocate(self%plots(plot_idx)%x(size(x)))
        allocate(self%plots(plot_idx)%y(size(y)))
        self%plots(plot_idx)%x = x
        self%plots(plot_idx)%y = y
        
        ! Handle symmetric error bars
        if (present(xerr)) then
            allocate(self%plots(plot_idx)%xerr(size(xerr)))
            self%plots(plot_idx)%xerr = xerr
            self%plots(plot_idx)%has_xerr = .true.
        end if
        
        if (present(yerr)) then
            allocate(self%plots(plot_idx)%yerr(size(yerr)))
            self%plots(plot_idx)%yerr = yerr
            self%plots(plot_idx)%has_yerr = .true.
        end if
        
        ! Handle asymmetric error bars
        if (present(xerr_lower) .and. present(xerr_upper)) then
            allocate(self%plots(plot_idx)%xerr_lower(size(xerr_lower)))
            allocate(self%plots(plot_idx)%xerr_upper(size(xerr_upper)))
            self%plots(plot_idx)%xerr_lower = xerr_lower
            self%plots(plot_idx)%xerr_upper = xerr_upper
            self%plots(plot_idx)%has_xerr = .true.
            self%plots(plot_idx)%asymmetric_xerr = .true.
        end if
        
        if (present(yerr_lower) .and. present(yerr_upper)) then
            allocate(self%plots(plot_idx)%yerr_lower(size(yerr_lower)))
            allocate(self%plots(plot_idx)%yerr_upper(size(yerr_upper)))
            self%plots(plot_idx)%yerr_lower = yerr_lower
            self%plots(plot_idx)%yerr_upper = yerr_upper
            self%plots(plot_idx)%has_yerr = .true.
            self%plots(plot_idx)%asymmetric_yerr = .true.
        end if
        
        if (present(capsize)) then
            self%plots(plot_idx)%capsize = capsize
        end if
        
        if (present(elinewidth)) then
            self%plots(plot_idx)%elinewidth = elinewidth
        end if
        
        if (present(marker)) then
            self%plots(plot_idx)%marker = marker
        else
            self%plots(plot_idx)%marker = 'o'  ! Default to circle marker
        end if
        
        if (present(color)) then
            self%plots(plot_idx)%color = color
        else if (present(ecolor)) then
            self%plots(plot_idx)%color = ecolor
        else
            color_idx = mod(plot_idx - 1, 6) + 1
            self%plots(plot_idx)%color = self%state%colors(:, color_idx)
        end if
        
        if (present(label) .and. len_trim(label) > 0) then
            self%plots(plot_idx)%label = label
        end if
    end subroutine errorbar_impl

    subroutine add_text_annotation(self, x, y, text, coord_type, font_size, rotation, &
                                  ha, va, bbox, color, alpha, weight, style)
        !! Add text annotation to figure
        class(figure_t), intent(inout) :: self
        real(wp), intent(in) :: x, y
        character(len=*), intent(in) :: text
        character(len=*), intent(in), optional :: coord_type
        real(wp), intent(in), optional :: font_size
        real(wp), intent(in), optional :: rotation
        character(len=*), intent(in), optional :: ha, va
        logical, intent(in), optional :: bbox
        real(wp), intent(in), optional :: color(3)
        real(wp), intent(in), optional :: alpha
        character(len=*), intent(in), optional :: weight, style
        
        type(text_annotation_t) :: annotation
        
        ! Set annotation properties
        annotation%x = x
        annotation%y = y
        annotation%text = text
        
        if (present(coord_type)) then
            select case (coord_type)
            case ('data')
                annotation%coord_type = COORD_DATA
            case ('figure')
                annotation%coord_type = COORD_FIGURE
            case ('axes')
                annotation%coord_type = COORD_AXIS
            case default
                annotation%coord_type = COORD_DATA
            end select
        else
            annotation%coord_type = COORD_DATA
        end if
        
        if (present(font_size)) annotation%font_size = font_size
        if (present(rotation)) annotation%rotation = rotation
        if (present(ha)) annotation%ha = ha
        if (present(va)) annotation%va = va
        if (present(bbox)) then
            annotation%bbox = bbox
            annotation%has_bbox = bbox
        end if
        if (present(color)) annotation%color = color
        if (present(alpha)) annotation%alpha = alpha
        if (present(weight)) annotation%weight = weight
        if (present(style)) annotation%style = style
        
        ! Add annotation to figure
        if (.not. allocated(self%annotations)) then
            allocate(self%annotations(self%max_annotations))
        end if
        
        self%annotation_count = self%annotation_count + 1
        self%annotations(self%annotation_count) = annotation
    end subroutine add_text_annotation

    subroutine add_arrow_annotation(self, text, xy, xytext, xy_coord_type, xytext_coord_type, &
                                   arrowprops, font_size, color, alpha)
        !! Add arrow annotation to figure
        class(figure_t), intent(inout) :: self
        character(len=*), intent(in) :: text
        real(wp), intent(in) :: xy(2)  ! Arrow point
        real(wp), intent(in) :: xytext(2)  ! Text location
        character(len=*), intent(in), optional :: xy_coord_type
        character(len=*), intent(in), optional :: xytext_coord_type
        character(len=*), intent(in), optional :: arrowprops  ! JSON-like string
        real(wp), intent(in), optional :: font_size
        real(wp), intent(in), optional :: color(3)
        real(wp), intent(in), optional :: alpha
        
        type(text_annotation_t) :: annotation
        
        ! Set annotation properties
        annotation%x = xytext(1)
        annotation%y = xytext(2)
        annotation%text = text
        annotation%arrow_x = xy(1)
        annotation%arrow_y = xy(2)
        annotation%has_arrow = .true.
        
        if (present(xytext_coord_type)) then
            select case (xytext_coord_type)
            case ('data')
                annotation%coord_type = COORD_DATA
            case ('figure')
                annotation%coord_type = COORD_FIGURE
            case ('axes')
                annotation%coord_type = COORD_AXIS
            case default
                annotation%coord_type = COORD_DATA
            end select
        else
            annotation%coord_type = COORD_DATA
        end if
        
        if (present(xy_coord_type)) then
            select case (xy_coord_type)
            case ('data')
                annotation%arrow_coord_type = COORD_DATA
            case ('figure')
                annotation%arrow_coord_type = COORD_FIGURE
            case ('axes')
                annotation%arrow_coord_type = COORD_AXIS
            case default
                annotation%arrow_coord_type = COORD_DATA
            end select
        else
            annotation%arrow_coord_type = COORD_DATA
        end if
        
        if (present(arrowprops)) annotation%arrowstyle = arrowprops
        if (present(font_size)) annotation%font_size = font_size
        if (present(color)) annotation%color = color
        if (present(alpha)) annotation%alpha = alpha
        
        ! Add annotation to figure
        if (.not. allocated(self%annotations)) then
            allocate(self%annotations(self%max_annotations))
        end if
        
        self%annotation_count = self%annotation_count + 1
        self%annotations(self%annotation_count) = annotation
    end subroutine add_arrow_annotation

    ! Private implementation subroutines

    subroutine add_line_plot_data(self, x, y, label, linestyle, color_rgb, color_str, marker)
        !! Add line plot data with comprehensive validation
        !! Refactored to be under 100 lines (QADS compliance)
        class(figure_t), intent(inout) :: self
        real(wp), intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: label, linestyle, color_str, marker
        real(wp), intent(in), optional :: color_rgb(3)
        
        integer :: plot_idx
        type(coordinate_validation_result_t) :: validation
        
        ! Validate coordinate arrays (Issue #436 fix)
        validation = validate_coordinate_arrays(x, y, "add_line_plot_data")
        if (.not. validation%is_valid) then
            call log_error(trim(validation%message))
            return  ! Skip adding invalid data
        end if
        
        ! For single points, suggest using markers for better visibility
        if (validation%is_single_point .and. .not. present(marker)) then
            call log_warning("Single point detected - consider adding markers for better visibility")
        end if
        
        ! Get current plot index
        self%plot_count = self%plot_count + 1
        plot_idx = self%plot_count
        
        ! Ensure plots array is allocated
        if (.not. allocated(self%plots)) then
            allocate(self%plots(self%state%max_plots))
        else if (plot_idx > size(self%plots)) then
            return
        end if
        
        ! Initialize plot data
        call init_line_plot_data(self%plots(plot_idx), x, y)
        
        ! Set optional properties
        call set_line_plot_properties(self%plots(plot_idx), plot_idx, &
                                     label, linestyle, marker, color_rgb, color_str, self%state)
    end subroutine add_line_plot_data
    
    subroutine init_line_plot_data(plot, x, y)
        !! Initialize basic line plot data
        type(plot_data_t), intent(inout) :: plot
        real(wp), intent(in) :: x(:), y(:)
        
        plot%plot_type = PLOT_TYPE_LINE
        
        allocate(plot%x(size(x)))
        allocate(plot%y(size(y)))
        plot%x = x
        plot%y = y
    end subroutine init_line_plot_data
    
    subroutine set_line_plot_properties(plot, plot_idx, label, linestyle, marker, &
                                       color_rgb, color_str, state)
        !! Set line plot properties (style, color, etc.)
        !! Extracted from add_line_plot_data for QADS compliance
        type(plot_data_t), intent(inout) :: plot
        integer, intent(in) :: plot_idx
        character(len=*), intent(in), optional :: label, linestyle, marker, color_str
        real(wp), intent(in), optional :: color_rgb(3)
        type(figure_state_t), intent(in) :: state
        
        character(len=20) :: parsed_marker, parsed_linestyle
        real(wp) :: rgb(3)
        logical :: success
        integer :: color_idx
        
        ! Set label
        if (present(label) .and. len_trim(label) > 0) then
            plot%label = label
        end if
        
        ! Parse linestyle to extract marker and line style components
        if (present(linestyle)) then
            call parse_format_string(linestyle, parsed_marker, parsed_linestyle)
            
            if (len_trim(parsed_marker) > 0) then
                plot%marker = trim(parsed_marker)
                if (len_trim(parsed_linestyle) > 0) then
                    plot%linestyle = trim(parsed_linestyle)
                else
                    plot%linestyle = 'None'  ! No line, marker only
                end if
            else
                plot%linestyle = linestyle
                if (present(marker)) then
                    plot%marker = marker
                else
                    plot%marker = 'None'
                end if
            end if
        else
            plot%linestyle = 'solid'
            if (present(marker)) then
                plot%marker = marker
            else
                plot%marker = 'None'
            end if
        end if
        
        ! Handle color specification
        if (present(color_str)) then
            call parse_color(color_str, rgb, success)
            if (success) then
                plot%color = rgb
            else
                color_idx = mod(plot_idx - 1, 6) + 1
                plot%color = state%colors(:, color_idx)
            end if
        else if (present(color_rgb)) then
            plot%color = color_rgb
        else
            color_idx = mod(plot_idx - 1, 6) + 1
            plot%color = state%colors(:, color_idx)
        end if
    end subroutine set_line_plot_properties

    ! Additional plot implementation subroutines

    subroutine add_3d_line_plot_data(self, x, y, z, label, linestyle, marker, markersize, linewidth)
        !! Add 3D line plot data by projecting to 2D
        class(figure_t), intent(inout) :: self
        real(wp), intent(in) :: x(:), y(:), z(:)
        character(len=*), intent(in), optional :: label, linestyle, marker
        real(wp), intent(in), optional :: markersize, linewidth
        
        ! Project 3D to 2D for basic plotting (z-axis ignored for now)
        call add_line_plot_data(self, x, y, label, linestyle)
    end subroutine add_3d_line_plot_data

    subroutine add_surface_plot_data(self, x, y, z, label)
        !! Add surface plot data (simplified as contour representation)
        class(figure_t), intent(inout) :: self
        real(wp), intent(in) :: x(:), y(:), z(:,:)
        character(len=*), intent(in), optional :: label
        
        integer :: subplot_idx, plot_idx
        
        ! Get current plot index
        self%plot_count = self%plot_count + 1
        plot_idx = self%plot_count
        
        ! Ensure plots array is allocated
        if (.not. allocated(self%plots)) then
            allocate(self%plots(self%state%max_plots))
        else if (plot_idx > size(self%plots)) then
            return
        end if
        
        self%plots(plot_idx)%plot_type = PLOT_TYPE_CONTOUR
        
        ! Store grid data
        allocate(self%plots(plot_idx)%x_grid(size(x)))
        allocate(self%plots(plot_idx)%y_grid(size(y)))
        allocate(self%plots(plot_idx)%z_grid(size(z, 1), size(z, 2)))
        
        self%plots(plot_idx)%x_grid = x
        self%plots(plot_idx)%y_grid = y
        self%plots(plot_idx)%z_grid = z
        
        if (present(label) .and. len_trim(label) > 0) then
            self%plots(plot_idx)%label = label
        end if
    end subroutine add_surface_plot_data

    subroutine add_scatter_plot_data(self, x, y, z, s, c, label, marker, markersize, color, &
                                    colormap, vmin, vmax, show_colorbar, alpha)
        !! Add scatter plot data with optional properties
        class(figure_t), intent(inout) :: self
        real(wp), intent(in) :: x(:), y(:)
        real(wp), intent(in), optional :: z(:), s(:), c(:), markersize, color(3), vmin, vmax, alpha
        character(len=*), intent(in), optional :: label, marker, colormap
        logical, intent(in), optional :: show_colorbar
        
        integer :: subplot_idx, plot_idx
        
        self%plot_count = self%plot_count + 1
        plot_idx = self%plot_count
        
        ! Ensure plots array is allocated
        if (.not. allocated(self%plots)) then
            allocate(self%plots(self%state%max_plots))
        else if (plot_idx > size(self%plots)) then
            return
        end if
        
        self%plots(plot_idx)%plot_type = PLOT_TYPE_SCATTER
        
        allocate(self%plots(plot_idx)%x(size(x)))
        allocate(self%plots(plot_idx)%y(size(y)))
        
        self%plots(plot_idx)%x = x
        self%plots(plot_idx)%y = y
        
        if (present(z)) then
            allocate(self%plots(plot_idx)%z(size(z)))
            self%plots(plot_idx)%z = z
        end if
        
        if (present(s)) then
            allocate(self%plots(plot_idx)%scatter_sizes(size(s)))
            self%plots(plot_idx)%scatter_sizes = s
        end if
        
        if (present(c)) then
            allocate(self%plots(plot_idx)%scatter_colors(size(c)))
            self%plots(plot_idx)%scatter_colors = c
        end if
        
        if (present(color)) then
            self%plots(plot_idx)%color = color
        end if
        
        if (present(marker)) then
            self%plots(plot_idx)%marker = marker
        end if
        
        if (present(markersize)) then
            self%plots(plot_idx)%scatter_size_default = markersize
        end if
        
        if (present(colormap)) then
            self%plots(plot_idx)%scatter_colormap = colormap
        end if
        
        if (present(vmin)) then
            self%plots(plot_idx)%scatter_vmin = vmin
            self%plots(plot_idx)%scatter_vrange_set = .true.
        end if
        
        if (present(vmax)) then
            self%plots(plot_idx)%scatter_vmax = vmax
            self%plots(plot_idx)%scatter_vrange_set = .true.
        end if
        
        if (present(show_colorbar)) then
            self%plots(plot_idx)%scatter_colorbar = show_colorbar
        end if
        
        ! Note: alpha not directly supported in plot_data_t structure
        
        if (present(label) .and. len_trim(label) > 0) then
            self%plots(plot_idx)%label = label
        end if
    end subroutine add_scatter_plot_data

end module fortplot_plotting