fortplot_figure_plot_management.f90 Source File


Source Code

module fortplot_figure_plot_management
    !! Figure plot data management module
    !!
    !! Single Responsibility: Manage plot data storage and operations
    !! Extracted from fortplot_figure_core to improve modularity

    use, intrinsic :: iso_fortran_env, only: wp => real64
    use fortplot_plot_data, only: plot_data_t, PLOT_TYPE_LINE, PLOT_TYPE_CONTOUR, &
                                  PLOT_TYPE_PCOLORMESH, PLOT_TYPE_FILL, &
                                  PLOT_TYPE_SURFACE, PLOT_TYPE_PIE
    use fortplot_figure_initialization, only: figure_state_t
    use fortplot_logging, only: log_warning, log_info
    use fortplot_legend, only: legend_t
    use fortplot_errors, only: fortplot_error_t
    use fortplot_pcolormesh, only: coordinates_from_centers
    use fortplot_contour_level_calculation, only: compute_default_contour_levels
    implicit none

    private
    public :: add_line_plot_data, add_contour_plot_data, add_colored_contour_plot_data
    public :: add_surface_plot_data, add_pcolormesh_plot_data, &
              add_fill_between_plot_data
    public :: generate_default_contour_levels
    public :: setup_figure_legend, update_plot_ydata, validate_plot_data
    public :: next_plot_color

contains

    subroutine validate_plot_data(x, y, label)
        !! Validate plot data and provide informative warnings for edge cases
        !! Added for Issue #432: Better user feedback for problematic data
        !! Fixed Issue #833: Reduced warning verbosity for constant data
        real(wp), intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: label
        character(len=100) :: label_str
        logical :: has_label

        ! Prepare label for messages
        if (present(label)) then
            label_str = "'"//trim(label)//"'"
            has_label = len_trim(label) > 0
        else
            label_str = "(unlabeled plot)"
            has_label = .false.
        end if

        ! Check for zero-size arrays (always warn)
        if (size(x) == 0 .or. size(y) == 0) then
            call log_warning("Plot data "//trim(label_str)// &
                             " contains zero-size arrays. The plot will show axes "// &
                             "and labels but no data points.")
            return
        end if

        ! Check for mismatched array sizes (always warn)
        if (size(x) /= size(y)) then
            call log_warning("Plot data "//trim(label_str)// &
                             " has mismatched array sizes: "// &
                             trim(adjustl(transfer(size(x), '          ')))//" vs "// &
                             trim(adjustl(transfer(size(y), '          ')))// &
                             ". Only the common size will be plotted.")
        end if

        ! Check for single point case (informational only)
        if (size(x) == 1 .and. size(y) == 1) then
            call log_info("Plot data "//trim(label_str)// &
                          " contains a single point. Automatic scaling will add "// &
                          "margins for visibility.")
        end if

        ! Check for constant values - only warn for labeled plots.
        ! Unlabeled data is often test data.
        ! Unlabeled plots are often test data where constant values are expected
        if (has_label) then
            if (size(x) > 1 .and. abs(maxval(x) - minval(x)) < 1.0e-10_wp) then
                call log_warning("All x values in plot "//trim(label_str)// &
                                 " are identical. This may result in a vertical "// &
                                 "line or poor visualization.")
            end if

            if (size(y) > 1 .and. abs(maxval(y) - minval(y)) < 1.0e-10_wp) then
                call log_warning("All y values in plot "//trim(label_str)// &
                                 " are identical. This may result in a horizontal "// &
                                 "line or poor visualization.")
            end if
        end if
    end subroutine validate_plot_data

    pure function next_plot_color(state) result(color)
        !! Determine the next color from the figure palette using plot count
        type(figure_state_t), intent(in) :: state
        real(wp) :: color(3)
        integer :: palette_size

        palette_size = size(state%colors, 2)
        if (palette_size <= 0) then
            color = [0.0_wp, 0.0_wp, 0.0_wp]
        else
            color = state%colors(:, mod(state%plot_count, palette_size) + 1)
        end if
    end function next_plot_color

    subroutine add_line_plot_data(plots, plot_count, max_plots, &
                                  x, y, label, linestyle, color, marker)
        !! Add line plot data to internal storage with edge case validation
        !! Fixed Issue #432: Added data validation for better user feedback
        type(plot_data_t), intent(inout) :: plots(:)
        integer, intent(inout) :: plot_count
        integer, intent(in) :: max_plots
        real(wp), intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: label, linestyle, marker
        real(wp), intent(in) :: color(3)

        if (plot_count >= max_plots) then
            call log_warning("Maximum number of plots reached")
            return
        end if

        ! Validate input data and provide warnings
        call validate_plot_data(x, y, label)

        plot_count = plot_count + 1

        ! Store plot data
        plots(plot_count)%plot_type = PLOT_TYPE_LINE
        if (allocated(plots(plot_count)%x)) deallocate (plots(plot_count)%x)
        if (allocated(plots(plot_count)%y)) deallocate (plots(plot_count)%y)
        allocate (plots(plot_count)%x(size(x)))
        allocate (plots(plot_count)%y(size(y)))
        plots(plot_count)%x = x
        plots(plot_count)%y = y
        plots(plot_count)%color = color

        ! Process optional arguments
        if (present(label)) then
            plots(plot_count)%label = label
        end if

        if (present(linestyle)) then
            plots(plot_count)%linestyle = linestyle
        else
            plots(plot_count)%linestyle = '-'
        end if

        if (present(marker)) then
            if (len_trim(marker) > 0) then
                plots(plot_count)%marker = marker
            end if
        end if
    end subroutine add_line_plot_data

    subroutine add_fill_between_plot_data(plots, plot_count, max_plots, x, upper, &
                                          lower, mask, &
                                          color, alpha)
        !! Store a fill_between polygon for rendering
        type(plot_data_t), intent(inout) :: plots(:)
        integer, intent(inout) :: plot_count
        integer, intent(in) :: max_plots
        real(wp), intent(in) :: x(:)
        real(wp), intent(in) :: upper(:)
        real(wp), intent(in) :: lower(:)
        logical, intent(in), optional :: mask(:)
        real(wp), intent(in) :: color(3)
        real(wp), intent(in), optional :: alpha

        integer :: n
        logical :: has_mask

        if (plot_count >= max_plots) then
            call log_warning('fill_between: maximum number of plots reached')
            return
        end if

        n = size(x)
        if (n < 2) then
            call log_warning('fill_between: at least two points required for area fill')
            return
        end if

        if (size(upper) /= n .or. size(lower) /= n) then
            call log_warning('fill_between: array size mismatch')
            return
        end if

        if (present(mask)) then
            if (size(mask) /= n) then
                call log_warning('fill_between: mask size mismatch; '// &
                                 'ignoring fill segment')
                return
            end if
            if (.not. any(mask)) then
                call log_warning('fill_between: mask excludes all points')
                return
            end if
            has_mask = .true.
        else
            has_mask = .false.
        end if

        plot_count = plot_count + 1
        call reset_plot_storage(plots(plot_count))

        plots(plot_count)%plot_type = PLOT_TYPE_FILL
        plots(plot_count)%color = color
        if (present(alpha)) then
            plots(plot_count)%fill_alpha = max(0.0_wp, min(1.0_wp, alpha))
        else
            plots(plot_count)%fill_alpha = 1.0_wp
        end if

        call assign_vector(plots(plot_count)%fill_between_data%x, x)
        call assign_vector(plots(plot_count)%fill_between_data%upper, upper)
        call assign_vector(plots(plot_count)%fill_between_data%lower, lower)

        plots(plot_count)%fill_between_data%has_mask = has_mask
        if (has_mask) then
            call assign_logical_vector(plots(plot_count)%fill_between_data%mask, mask)
        else
            if (allocated(plots(plot_count)%fill_between_data%mask)) then
                deallocate (plots(plot_count)%fill_between_data%mask)
            end if
        end if
    end subroutine add_fill_between_plot_data

    subroutine assign_vector(target, source)
        real(wp), allocatable, intent(inout) :: target(:)
        real(wp), intent(in) :: source(:)
        real(wp), allocatable :: tmp(:)

        if (allocated(target)) deallocate (target)
        allocate (tmp(size(source)))
        tmp = source
        call move_alloc(tmp, target)
    end subroutine assign_vector

    subroutine assign_logical_vector(target, source)
        logical, allocatable, intent(inout) :: target(:)
        logical, intent(in) :: source(:)
        logical, allocatable :: tmp(:)

        if (allocated(target)) deallocate (target)
        allocate (tmp(size(source)))
        tmp = source
        call move_alloc(tmp, target)
    end subroutine assign_logical_vector

    subroutine reset_plot_storage(plot)
        type(plot_data_t), intent(inout) :: plot

        if (allocated(plot%fill_between_data%x)) deallocate (plot%fill_between_data%x)
        if (allocated(plot%fill_between_data%upper)) deallocate &
            (plot%fill_between_data%upper)
        if (allocated(plot%fill_between_data%lower)) deallocate &
            (plot%fill_between_data%lower)
        if (allocated(plot%fill_between_data%mask)) deallocate &
            (plot%fill_between_data%mask)
        plot%fill_between_data%has_mask = .false.
        plot%fill_alpha = 1.0_wp
        plot%surface_show_colorbar = .false.
        plot%surface_alpha = 1.0_wp
        plot%surface_linewidth = 1.0_wp
        plot%surface_use_colormap = .false.
        plot%surface_edgecolor = [0.0_wp, 0.447_wp, 0.698_wp]
        if (allocated(plot%surface_colormap)) deallocate (plot%surface_colormap)
        plot%pie_slice_count = 0
        if (allocated(plot%pie_start)) deallocate (plot%pie_start)
        if (allocated(plot%pie_end)) deallocate (plot%pie_end)
        if (allocated(plot%pie_offsets)) deallocate (plot%pie_offsets)
        if (allocated(plot%pie_colors)) deallocate (plot%pie_colors)
        if (allocated(plot%pie_label_pos)) deallocate (plot%pie_label_pos)
        if (allocated(plot%pie_values)) deallocate (plot%pie_values)
        if (allocated(plot%pie_source_index)) deallocate (plot%pie_source_index)
        if (allocated(plot%pie_labels)) deallocate (plot%pie_labels)
        if (allocated(plot%pie_autopct)) deallocate (plot%pie_autopct)
        plot%pie_radius = 1.0_wp
        plot%pie_center = [0.0_wp, 0.0_wp]
    end subroutine reset_plot_storage

    subroutine add_contour_plot_data(plots, plot_count, max_plots, colors, &
                                     x_grid, y_grid, z_grid, levels, label)
        !! Add contour plot data to internal storage
        type(plot_data_t), intent(inout) :: plots(:)
        integer, intent(inout) :: plot_count
        integer, intent(in) :: max_plots
        real(wp), intent(in) :: colors(:, :)
        real(wp), intent(in) :: x_grid(:), y_grid(:), z_grid(:, :)
        real(wp), intent(in), optional :: levels(:)
        character(len=*), intent(in), optional :: label

        if (plot_count >= max_plots) then
            call log_warning("Maximum number of plots reached")
            return
        end if

        plot_count = plot_count + 1

        ! Store plot data
        plots(plot_count)%plot_type = PLOT_TYPE_CONTOUR
        allocate (plots(plot_count)%x_grid(size(x_grid)))
        allocate (plots(plot_count)%y_grid(size(y_grid)))
        allocate (plots(plot_count)%z_grid(size(z_grid, 1), size(z_grid, 2)))
        plots(plot_count)%x_grid = x_grid
        plots(plot_count)%y_grid = y_grid
        plots(plot_count)%z_grid = z_grid

        if (present(levels)) then
            if (size(levels) > 0) then
                allocate (plots(plot_count)%contour_levels(size(levels)))
                plots(plot_count)%contour_levels = levels
            else
                call generate_default_contour_levels(plots(plot_count))
            end if
        else
            call generate_default_contour_levels(plots(plot_count))
        end if

        if (present(label)) then
            plots(plot_count)%label = label
        end if

        ! Set default color
        plots(plot_count)%color = colors(:, mod(plot_count - 1, 6) + 1)
        plots(plot_count)%use_color_levels = .false.
    end subroutine add_contour_plot_data

    subroutine add_colored_contour_plot_data(plots, plot_count, max_plots, &
                                             x_grid, y_grid, z_grid, levels, &
                                             colormap, show_colorbar, label)
        !! Add colored contour plot data
        type(plot_data_t), intent(inout) :: plots(:)
        integer, intent(inout) :: plot_count
        integer, intent(in) :: max_plots
        real(wp), intent(in) :: x_grid(:), y_grid(:), z_grid(:, :)
        real(wp), intent(in), optional :: levels(:)
        character(len=*), intent(in), optional :: colormap, label
        logical, intent(in), optional :: show_colorbar

        if (plot_count >= max_plots) then
            call log_warning("Maximum number of plots reached")
            return
        end if

        plot_count = plot_count + 1

        ! Store plot data
        plots(plot_count)%plot_type = PLOT_TYPE_CONTOUR
        allocate (plots(plot_count)%x_grid(size(x_grid)))
        allocate (plots(plot_count)%y_grid(size(y_grid)))
        allocate (plots(plot_count)%z_grid(size(z_grid, 1), size(z_grid, 2)))
        plots(plot_count)%x_grid = x_grid
        plots(plot_count)%y_grid = y_grid
        plots(plot_count)%z_grid = z_grid

        if (present(levels)) then
            if (size(levels) > 0) then
                allocate (plots(plot_count)%contour_levels(size(levels)))
                plots(plot_count)%contour_levels = levels
            else
                call generate_default_contour_levels(plots(plot_count))
            end if
        else
            call generate_default_contour_levels(plots(plot_count))
        end if

        if (present(colormap)) then
            plots(plot_count)%colormap = colormap
        else
            plots(plot_count)%colormap = 'crest'
        end if

        if (present(show_colorbar)) then
            plots(plot_count)%show_colorbar = show_colorbar
        else
            plots(plot_count)%show_colorbar = .true.
        end if

        if (present(label)) then
            plots(plot_count)%label = label
        end if

        plots(plot_count)%use_color_levels = .true.
        plots(plot_count)%fill_contours = .true.
    end subroutine add_colored_contour_plot_data

    subroutine add_surface_plot_data(plots, plot_count, max_plots, colors, &
                                     x_grid, y_grid, z_grid, label, colormap, &
                                     show_colorbar, alpha, edgecolor, linewidth, &
                                     filled)
        !! Add 3D surface plot data using structured grid storage
        type(plot_data_t), intent(inout) :: plots(:)
        integer, intent(inout) :: plot_count
        integer, intent(in) :: max_plots
        real(wp), intent(in) :: colors(:, :)
        real(wp), intent(in) :: x_grid(:), y_grid(:), z_grid(:, :)
        character(len=*), intent(in), optional :: label, colormap
        logical, intent(in), optional :: show_colorbar, filled
        real(wp), intent(in), optional :: alpha, linewidth
        real(wp), intent(in), optional :: edgecolor(3)

        real(wp) :: default_color(3)

        if (plot_count >= max_plots) then
            call log_warning("Maximum number of plots reached")
            return
        end if

        plot_count = plot_count + 1

        plots(plot_count)%plot_type = PLOT_TYPE_SURFACE

        if (allocated(plots(plot_count)%x_grid)) deallocate (plots(plot_count)%x_grid)
        if (allocated(plots(plot_count)%y_grid)) deallocate (plots(plot_count)%y_grid)
        if (allocated(plots(plot_count)%z_grid)) deallocate (plots(plot_count)%z_grid)
        allocate (plots(plot_count)%x_grid(size(x_grid)))
        allocate (plots(plot_count)%y_grid(size(y_grid)))
        allocate (plots(plot_count)%z_grid(size(z_grid, 1), size(z_grid, 2)))
        plots(plot_count)%x_grid = x_grid
        plots(plot_count)%y_grid = y_grid
        plots(plot_count)%z_grid = z_grid

        if (present(label)) then
            plots(plot_count)%label = label
        end if

        default_color = colors(:, mod(plot_count - 1, size(colors, 2)) + 1)
        plots(plot_count)%color = default_color

        if (present(edgecolor)) then
            plots(plot_count)%color = edgecolor
            plots(plot_count)%surface_edgecolor = edgecolor
        else
            plots(plot_count)%surface_edgecolor = default_color
        end if

        plots(plot_count)%surface_alpha = 1.0_wp
        if (present(alpha)) plots(plot_count)%surface_alpha = max(0.0_wp, &
                                                                  min(1.0_wp, alpha))

        plots(plot_count)%surface_linewidth = 1.0_wp
        if (present(linewidth)) plots(plot_count)%surface_linewidth = &
            max(1.0e-6_wp, linewidth)

        plots(plot_count)%surface_use_colormap = .false.
        if (allocated(plots(plot_count)%surface_colormap)) deallocate &
            (plots(plot_count)%surface_colormap)
        if (present(colormap)) then
            if (len_trim(colormap) > 0) then
                allocate (character(len=len_trim(colormap)) :: &
                          plots(plot_count)%surface_colormap)
                plots(plot_count)%surface_colormap = trim(colormap)
                plots(plot_count)%surface_use_colormap = .true.
            end if
        end if

        plots(plot_count)%surface_show_colorbar = .false.
        if (present(show_colorbar)) plots(plot_count)%surface_show_colorbar = &
            show_colorbar

        plots(plot_count)%surface_filled = .false.
        if (present(filled)) plots(plot_count)%surface_filled = filled
    end subroutine add_surface_plot_data

    subroutine add_pcolormesh_plot_data(plots, plot_count, max_plots, &
                                        x, y, c, colormap, vmin, vmax, &
                                        edgecolors, linewidths)
        !! Add pcolormesh plot data
        type(plot_data_t), intent(inout) :: plots(:)
        integer, intent(inout) :: plot_count
        integer, intent(in) :: max_plots
        real(wp), intent(in) :: x(:), y(:), c(:, :)
        character(len=*), intent(in), optional :: colormap
        real(wp), intent(in), optional :: vmin, vmax
        real(wp), intent(in), optional :: edgecolors(3)
        real(wp), intent(in), optional :: linewidths

        if (plot_count >= max_plots) then
            call log_warning("Maximum number of plots reached")
            return
        end if

        plot_count = plot_count + 1

        ! Store plot data
        plots(plot_count)%plot_type = PLOT_TYPE_PCOLORMESH

        ! Initialize pcolormesh data using proper method with error handling
        block
            type(fortplot_error_t) :: init_error
            integer :: data_nx, data_ny
            real(wp), allocatable :: x_edges(:), y_edges(:)

            data_ny = size(c, 1)
            data_nx = size(c, 2)

            if (size(x) == data_nx .and. size(y) == data_ny) then
                allocate (x_edges(data_nx + 1))
                allocate (y_edges(data_ny + 1))
                call coordinates_from_centers(x, x_edges)
                call coordinates_from_centers(y, y_edges)
                call plots(plot_count)%pcolormesh_data%initialize_regular_grid( &
                    x_edges, y_edges, c, colormap, init_error)
                deallocate (x_edges)
                deallocate (y_edges)
            elseif (size(x) == data_ny .and. size(y) == data_nx) then
                allocate (x_edges(data_ny + 1))
                allocate (y_edges(data_nx + 1))
                call coordinates_from_centers(x, x_edges)
                call coordinates_from_centers(y, y_edges)
                call plots(plot_count)%pcolormesh_data%initialize_regular_grid( &
                    x_edges, y_edges, c, colormap, init_error)
                deallocate (x_edges)
                deallocate (y_edges)
            else
                call plots(plot_count)%pcolormesh_data%initialize_regular_grid( &
                    x, y, c, colormap, init_error)
            end if
        end block

        ! Keep the user's requested colormap or the backend default; do not auto-switch.

        if (present(vmin)) then
            plots(plot_count)%pcolormesh_data%vmin = vmin
            plots(plot_count)%pcolormesh_data%vmin_set = .true.
        end if

        if (present(vmax)) then
            plots(plot_count)%pcolormesh_data%vmax = vmax
            plots(plot_count)%pcolormesh_data%vmax_set = .true.
        end if

        ! Do not apply any implicit symmetric normalization.
        ! Match matplotlib: use full data range.

        if (present(edgecolors)) then
            plots(plot_count)%pcolormesh_data%show_edges = .true.
            plots(plot_count)%pcolormesh_data%edge_color = edgecolors
        end if

        if (present(linewidths)) then
            plots(plot_count)%pcolormesh_data%edge_width = linewidths
        end if
    end subroutine add_pcolormesh_plot_data

    subroutine generate_default_contour_levels(plot_data)
        !! Generate default contour levels
        type(plot_data_t), intent(inout) :: plot_data

        real(wp) :: z_min, z_max

        z_min = minval(plot_data%z_grid)
        z_max = maxval(plot_data%z_grid)
        call compute_default_contour_levels(z_min, z_max, plot_data%contour_levels)
    end subroutine generate_default_contour_levels

    subroutine setup_figure_legend(legend_data, show_legend, plots, plot_count, &
                                   location, backend_name)
        !! Setup figure legend
        type(legend_t), intent(inout) :: legend_data
        logical, intent(inout) :: show_legend
        type(plot_data_t), intent(in) :: plots(:)
        integer, intent(in) :: plot_count
        character(len=*), intent(in), optional :: location
        character(len=*), intent(in), optional :: backend_name

        character(len=:), allocatable :: loc
        integer :: i

        loc = 'upper right'
        if (present(location)) loc = location

        show_legend = .true.

        ! Clear existing legend entries to prevent duplication
        call legend_data%clear()

        ! Set legend position based on location string
        call legend_data%set_position(loc)

        ! Add legend entries from plots
        do i = 1, plot_count
            if (plots(i)%plot_type == PLOT_TYPE_PIE) then
                call add_pie_legend_entries(legend_data, plots(i), backend_name)
                cycle
            end if

            if (allocated(plots(i)%label)) then
                if (len_trim(plots(i)%label) > 0) then
                    if (allocated(plots(i)%linestyle)) then
                        if (allocated(plots(i)%marker)) then
                            call legend_data%add_entry(plots(i)%label, &
                                                       plots(i)%color, &
                                                       plots(i)%linestyle, &
                                                       plots(i)%marker)
                        else
                            call legend_data%add_entry(plots(i)%label, &
                                                       plots(i)%color, &
                                                       plots(i)%linestyle)
                        end if
                    else
                        ! For PNG/PDF backends, add square markers for legend visibility
                        ! ASCII backend does not need markers.
                        ! It uses text-based legends.
                        if (present(backend_name) .and. trim(backend_name) == &
                            'ascii') then
                            call legend_data%add_entry(plots(i)%label, &
                                                       plots(i)%color)
                        else
                            call legend_data%add_entry(plots(i)%label, &
                                                       plots(i)%color, &
                                                       marker='s')
                        end if
                    end if
                end if
            end if
        end do
    end subroutine setup_figure_legend

    subroutine add_pie_legend_entries(legend_data, plot, backend_name)
        !! Add one legend entry per pie slice using wedge colors
        type(legend_t), intent(inout) :: legend_data
        type(plot_data_t), intent(in) :: plot
        character(len=*), intent(in), optional :: backend_name

        integer :: slice_count, i
        real(wp) :: color(3)
        real(wp) :: total, percent
        logical :: has_labels, has_values
        character(len=64) :: label_buf

        slice_count = plot%pie_slice_count
        if (slice_count <= 0) return

        has_labels = allocated(plot%pie_labels)
        if (has_labels) then
            if (size(plot%pie_labels) < slice_count) has_labels = .false.
        end if

        has_values = allocated(plot%pie_values)
        if (has_values) then
            if (size(plot%pie_values) < slice_count) has_values = .false.
        end if

        total = 0.0_wp
        if (has_values) total = sum(plot%pie_values(1:slice_count))

        do i = 1, slice_count
            if (allocated(plot%pie_colors)) then
                if (size(plot%pie_colors, 2) >= i) then
                    color = plot%pie_colors(:, i)
                else
                    color = plot%color
                end if
            else
                color = plot%color
            end if

            label_buf = ''
            if (has_labels) label_buf = trim(plot%pie_labels(i))

            if (len_trim(label_buf) == 0) then
                if (has_values .and. total > 0.0_wp) then
                    percent = 100.0_wp*plot%pie_values(i)/ &
                              max(total, tiny(1.0_wp))
                    write (label_buf, '("Slice ",I0," (",F6.1,"%)")') i, percent
                else
                    write (label_buf, '("Slice ",I0)') i
                end if
            end if

            ! Use ASCII characters for ASCII backend, square markers for PNG/PDF
            if (present(backend_name)) then
                if (trim(backend_name) == 'ascii') then
                    call legend_data%add_entry(trim(label_buf), color, &
                                               linestyle='None', &
                                               marker=get_pie_slice_marker_for_index(i))
                else
                    call legend_data%add_entry(trim(label_buf), color, &
                                               linestyle='None', marker='s')
                end if
            else
                ! Default to ASCII characters if backend_name not provided
                call legend_data%add_entry(trim(label_buf), color, &
                                           linestyle='None', &
                                           marker=get_pie_slice_marker_for_index(i))
            end if
        end do
    end subroutine add_pie_legend_entries

    pure function get_pie_slice_marker_for_index(slice_index) result(marker)
    !! Map pie slice index to distinct ASCII characters to ensure differentiation
        integer, intent(in) :: slice_index
        character(len=1) :: marker

        ! Hardcode distinct markers for each slice to test
        select case (slice_index)
        case (1)
            marker = '-'
        case (2)
            marker = '='
        case (3)
            marker = '%'
        case (4)
            marker = '#'
        case (5)
            marker = '@'
        case default
            marker = '+'
        end select
    end function get_pie_slice_marker_for_index
    subroutine update_plot_ydata(plots, plot_count, plot_index, y_new)
        !! Update y data for an existing plot
        type(plot_data_t), intent(inout) :: plots(:)
        integer, intent(in) :: plot_count
        integer, intent(in) :: plot_index
        real(wp), intent(in) :: y_new(:)
        character(len=32) :: idx_str, new_sz, old_sz

        if (plot_index < 1 .or. plot_index > plot_count) then
            write (idx_str, '(I0)') plot_index
            call log_warning("Invalid plot index: "//trim(adjustl(idx_str)))
            return
        end if

        if (.not. allocated(plots(plot_index)%y)) then
            write (idx_str, '(I0)') plot_index
            call log_warning("Plot "//trim(adjustl(idx_str))// &
                             " has no y data to update")
            return
        end if

        if (size(y_new) /= size(plots(plot_index)%y)) then
            write (new_sz, '(I0)') size(y_new)
            write (old_sz, '(I0)') size(plots(plot_index)%y)
            call log_warning("New y data size "//trim(adjustl(new_sz))// &
                             " does not match existing size "//trim(adjustl(old_sz)))
            return
        end if

        plots(plot_index)%y = y_new
    end subroutine update_plot_ydata

end module fortplot_figure_plot_management