fortplot_ascii.f90 Source File


Source Code

module fortplot_ascii
    !! ASCII terminal plotting backend
    !!
    !! This module implements text-based plotting for terminal output using
    !! ASCII characters and Unicode box drawing characters. Provides basic
    !! line plotting with character density mapping for visualization.
    !!
    !! Author: fortplot contributors
    
    use fortplot_context, only: plot_context, setup_canvas
    use fortplot_logging, only: log_info, log_error
    use fortplot_latex_parser, only: process_latex_in_text
    use fortplot_constants, only: EPSILON_COMPARE
    use fortplot_ascii_utils, only: text_element_t, get_char_density, get_blend_char, ASCII_CHARS
    use fortplot_ascii_utils, only: render_text_elements_to_canvas, print_centered_title, write_centered_title
    use fortplot_ascii_utils, only: ascii_marker_char
    use fortplot_ascii_elements, only: draw_ascii_marker, fill_ascii_heatmap, draw_ascii_arrow
    use fortplot_ascii_elements, only: render_ascii_legend_specialized, calculate_ascii_legend_dimensions
    use fortplot_ascii_elements, only: set_ascii_legend_border_width, calculate_ascii_legend_position
    use fortplot_ascii_elements, only: draw_ascii_axes_and_labels
    use fortplot_ascii_elements, only: reset_ascii_legend_lines_helper, append_ascii_legend_line_helper
    use fortplot_ascii_elements, only: ascii_draw_text_helper
    use fortplot_ascii_backend_support, only: extract_ascii_rgb_data, get_ascii_png_data, prepare_ascii_3d_data
    use fortplot_ascii_backend_support, only: render_ascii_ylabel, render_ascii_axes
    use fortplot_ascii_rendering, only: ascii_finalize => ascii_finalize, ascii_get_output
    use fortplot_ascii_primitives, only: ascii_draw_line_primitive, ascii_fill_quad_primitive
    use fortplot_ascii_primitives, only: ascii_draw_text_primitive
    use, intrinsic :: iso_fortran_env, only: wp => real64
    implicit none
    
    private
    public :: ascii_context, create_ascii_canvas, ASCII_CHAR_ASPECT

    real(wp), parameter :: ASCII_CHAR_ASPECT = 2.0_wp

    type, extends(plot_context) :: ascii_context
        character(len=1), allocatable :: canvas(:,:)
        character(len=:), allocatable :: title_text
        character(len=:), allocatable :: xlabel_text
        character(len=:), allocatable :: ylabel_text
        logical :: title_set = .false.  ! Track if title was explicitly set
        type(text_element_t), allocatable :: text_elements(:)
        integer :: num_text_elements = 0
        real(wp) :: current_r, current_g, current_b
        integer :: plot_width = 80
        integer :: plot_height = 24
        character(len=96), allocatable :: legend_lines(:)
        integer :: num_legend_lines = 0
        logical :: capturing_legend = .false.
        real(wp) :: stored_y_min = 0.0_wp
        real(wp) :: stored_y_max = 0.0_wp
        logical :: has_stored_y_range = .false.
    contains
        procedure :: line => ascii_draw_line
        procedure :: color => ascii_set_color
        procedure :: text => ascii_draw_text
        procedure :: set_line_width => ascii_set_line_width
        procedure :: set_line_style => ascii_set_line_style
        procedure :: save => ascii_save
        procedure :: set_title => ascii_set_title
        procedure :: draw_marker => ascii_draw_marker
        procedure :: set_marker_colors => ascii_set_marker_colors
        procedure :: set_marker_colors_with_alpha => ascii_set_marker_colors_with_alpha
        procedure :: fill_heatmap => ascii_fill_heatmap
        procedure :: draw_arrow => ascii_draw_arrow
        procedure :: get_ascii_output => ascii_get_output_method
        
        !! New polymorphic methods to eliminate SELECT TYPE
        procedure :: get_width_scale => ascii_get_width_scale
        procedure :: get_height_scale => ascii_get_height_scale
        procedure :: fill_quad => ascii_fill_quad
        procedure :: render_legend_specialized => ascii_render_legend_specialized
        procedure :: calculate_legend_dimensions => ascii_calculate_legend_dimensions
        procedure :: set_legend_border_width => ascii_set_legend_border_width
        procedure :: calculate_legend_position_backend => ascii_calculate_legend_position
        procedure :: extract_rgb_data => ascii_extract_rgb_data
        procedure :: get_png_data_backend => ascii_get_png_data
        procedure :: prepare_3d_data => ascii_prepare_3d_data
        procedure :: render_ylabel => ascii_render_ylabel
        procedure :: draw_axes_and_labels_backend => ascii_draw_axes_and_labels
        procedure :: save_coordinates => ascii_save_coordinates
        procedure :: set_coordinates => ascii_set_coordinates
        procedure :: render_axes => ascii_render_axes
        procedure :: clear_ascii_legend => ascii_clear_legend_lines
        procedure :: add_ascii_legend_entry => ascii_add_legend_entry
        procedure :: clear_pie_legend_entries => ascii_clear_pie_legend_entries
        procedure :: register_pie_legend_entry => ascii_register_pie_legend_entry
    end type ascii_context
    
    character(len=*), parameter :: DENSITY_CHARS = ' ░▒▓█'
    character(len=*), parameter :: BOX_CHARS = '-|+++++++'
    
contains

    function create_ascii_canvas(width, height) result(ctx)
        integer, intent(in), optional :: width, height
        type(ascii_context) :: ctx
        integer :: w, h
        
        ! Suppress unused parameter warnings
        associate(unused_w => width, unused_h => height); end associate
        
        ! ASCII backend uses 4:3 aspect ratio accounting for terminal character dimensions
        ! Terminal characters are taller than they are wide; a 24-row canvas keeps the
        ! legend heuristics inside ASCII mode while preserving enough vertical space.
        w = 80
        h = 24
        
        call setup_canvas(ctx, w, h)
        
        ctx%plot_width = w
        ctx%plot_height = h
        
        allocate(ctx%canvas(h, w))
        ctx%canvas = ' '
        
        ! Initialize text elements storage (start with capacity for 20 text elements)
        allocate(ctx%text_elements(20))
        ctx%num_text_elements = 0
        ctx%title_set = .false.
        allocate(ctx%legend_lines(0))
        ctx%num_legend_lines = 0
        ctx%capturing_legend = .false.

        ctx%current_r = 0.0_wp
        ctx%current_g = 0.0_wp 
        ctx%current_b = 1.0_wp
    end function create_ascii_canvas
    
    subroutine ascii_draw_line(this, x1, y1, x2, y2)
        class(ascii_context), intent(inout) :: this
        real(wp), intent(in) :: x1, y1, x2, y2
        
        call ascii_draw_line_primitive(this%canvas, x1, y1, x2, y2, &
                                      this%x_min, this%x_max, this%y_min, this%y_max, &
                                      this%plot_width, this%plot_height, &
                                      this%current_r, this%current_g, this%current_b)
    end subroutine ascii_draw_line
    
    subroutine ascii_set_color(this, r, g, b)
        class(ascii_context), intent(inout) :: this
        real(wp), intent(in) :: r, g, b
        
        this%current_r = r
        this%current_g = g
        this%current_b = b
    end subroutine ascii_set_color
    
    subroutine ascii_set_line_width(this, width)
        !! Set line width for ASCII context (no-op as ASCII uses fixed character width)
        class(ascii_context), intent(inout) :: this
        real(wp), intent(in) :: width
        
        ! Suppress unused parameter warnings
        associate(unused_int => this%width, unused_real => width); end associate
        
        ! ASCII context doesn't support variable line widths
        ! This is a no-op to satisfy the interface
    end subroutine ascii_set_line_width
    
    subroutine ascii_set_line_style(this, style)
        !! Set line style for ASCII context (no-op as ASCII uses fixed characters)
        class(ascii_context), intent(inout) :: this
        character(len=*), intent(in) :: style
        
        ! Suppress unused parameter warnings
        associate(unused_int => this%width, unused_style => style); end associate
        
        ! ASCII context doesn't support different line styles
        ! All lines are rendered as continuous ASCII characters
        ! This is a no-op to satisfy the interface
    end subroutine ascii_set_line_style
    
    subroutine ascii_draw_text(this, x, y, text)
        class(ascii_context), intent(inout) :: this
        real(wp), intent(in) :: x, y
        character(len=*), intent(in) :: text


        if (this%num_text_elements < size(this%text_elements)) then
            this%num_text_elements = this%num_text_elements + 1
            this%text_elements(this%num_text_elements)%text = trim(text)
            this%text_elements(this%num_text_elements)%x = nint(x)
            this%text_elements(this%num_text_elements)%y = nint(y)
            this%text_elements(this%num_text_elements)%color_r = this%current_r
            this%text_elements(this%num_text_elements)%color_g = this%current_g
            this%text_elements(this%num_text_elements)%color_b = this%current_b
        end if
    end subroutine ascii_draw_text
    
    subroutine ascii_set_title(this, title)
        !! Explicitly set title for ASCII backend
        class(ascii_context), intent(inout) :: this
        character(len=*), intent(in) :: title
        character(len=500) :: processed_title
        integer :: processed_len
        
        ! Process LaTeX commands in title
        call process_latex_in_text(title, processed_title, processed_len)
        this%title_text = processed_title(1:processed_len)
        this%title_set = .true.
    end subroutine ascii_set_title
    
    subroutine ascii_save(this, filename)
        class(ascii_context), intent(inout) :: this
        character(len=*), intent(in) :: filename
        
        call ascii_finalize(this%canvas, this%text_elements, this%num_text_elements, &
                           this%plot_width, this%plot_height, &
                           this%title_text, this%xlabel_text, this%ylabel_text, &
                           this%legend_lines, this%num_legend_lines, filename)
    end subroutine ascii_save

    subroutine ascii_draw_marker(this, x, y, style)
        class(ascii_context), intent(inout) :: this
        real(wp), intent(in) :: x, y
        character(len=*), intent(in) :: style
        
        call draw_ascii_marker(this%canvas, x, y, style, &
                              this%x_min, this%x_max, this%y_min, this%y_max, &
                              this%plot_width, this%plot_height)
    end subroutine ascii_draw_marker

    subroutine ascii_set_marker_colors(this, edge_r, edge_g, edge_b, face_r, face_g, face_b)
        class(ascii_context), intent(inout) :: this
        real(wp), intent(in) :: edge_r, edge_g, edge_b
        real(wp), intent(in) :: face_r, face_g, face_b
        
        ! Suppress unused parameter warnings
        associate(unused_int => this%width, &
                  unused_real => edge_r + edge_g + edge_b + face_r + face_g + face_b); end associate
        
        ! ASCII backend doesn't support separate marker colors
        ! This is a stub implementation for interface compliance
    end subroutine ascii_set_marker_colors

    subroutine ascii_set_marker_colors_with_alpha(this, edge_r, edge_g, edge_b, edge_alpha, &
                                                  face_r, face_g, face_b, face_alpha)
        class(ascii_context), intent(inout) :: this
        real(wp), intent(in) :: edge_r, edge_g, edge_b, edge_alpha
        real(wp), intent(in) :: face_r, face_g, face_b, face_alpha
        
        ! Suppress unused parameter warnings  
        associate(unused_int => this%width, &
                  unused_real => edge_r + edge_g + edge_b + edge_alpha + &
                                face_r + face_g + face_b + face_alpha); end associate
        
        ! ASCII backend doesn't support separate marker colors or transparency
        ! This is a stub implementation for interface compliance
    end subroutine ascii_set_marker_colors_with_alpha
    
    subroutine ascii_fill_heatmap(this, x_grid, y_grid, z_grid, z_min, z_max)
        class(ascii_context), intent(inout) :: this
        real(wp), intent(in) :: x_grid(:), y_grid(:), z_grid(:,:)
        real(wp), intent(in) :: z_min, z_max
        
        call fill_ascii_heatmap(this%canvas, x_grid, y_grid, z_grid, z_min, z_max, &
                               this%x_min, this%x_max, this%y_min, this%y_max, &
                               this%plot_width, this%plot_height)
    end subroutine ascii_fill_heatmap

    subroutine ascii_draw_arrow(this, x, y, dx, dy, size, style)
        class(ascii_context), intent(inout) :: this
        real(wp), intent(in) :: x, y, dx, dy, size
        character(len=*), intent(in) :: style
        
        call draw_ascii_arrow(this%canvas, x, y, dx, dy, size, style, &
                             this%x_min, this%x_max, this%y_min, this%y_max, &
                             this%width, this%height, &
                             this%has_rendered_arrows, this%uses_vector_arrows, this%has_triangular_arrows)
    end subroutine ascii_draw_arrow

    function ascii_get_output_method(this) result(output)
        !! Get the complete ASCII canvas as a string
        class(ascii_context), intent(in) :: this
        character(len=:), allocatable :: output
        
        output = ascii_get_output(this%canvas, this%width, this%height)
    end function ascii_get_output_method

    function ascii_get_width_scale(this) result(scale)
        !! Get width scaling factor for coordinate transformation
        class(ascii_context), intent(in) :: this
        real(wp) :: scale
        
        ! Calculate scaling from logical to ASCII coordinates
        if (this%plot_width > 0 .and. this%x_max > this%x_min) then
            scale = real(this%plot_width, wp) / (this%x_max - this%x_min)
        else
            scale = 1.0_wp
        end if
    end function ascii_get_width_scale

    function ascii_get_height_scale(this) result(scale)
        !! Get height scaling factor for coordinate transformation  
        class(ascii_context), intent(in) :: this
        real(wp) :: scale
        
        ! Calculate scaling from logical to ASCII coordinates
        if (this%plot_height > 0 .and. this%y_max > this%y_min) then
            scale = real(this%plot_height, wp) / (this%y_max - this%y_min)
        else
            scale = 1.0_wp
        end if
    end function ascii_get_height_scale

    subroutine ascii_fill_quad(this, x_quad, y_quad)
        !! Fill quadrilateral using character mapping based on current color
        class(ascii_context), intent(inout) :: this
        real(wp), intent(in) :: x_quad(4), y_quad(4)
        
        call ascii_fill_quad_primitive(this%canvas, x_quad, y_quad, &
                                      this%x_min, this%x_max, this%y_min, this%y_max, &
                                      this%plot_width, this%plot_height, &
                                      this%current_r, this%current_g, this%current_b)
    end subroutine ascii_fill_quad

    subroutine ascii_render_legend_specialized(this, legend, legend_x, legend_y)
        use fortplot_legend, only: legend_t
        class(ascii_context), intent(inout) :: this
        type(legend_t), intent(in) :: legend
        real(wp), intent(in) :: legend_x, legend_y

        integer :: i
        character(len=96) :: line_buffer
        character(len=:), allocatable :: label_text
        character(len=1) :: marker_char

        ! Suppress unused parameters (legend positions are handled outside canvas)
        associate(unused_x => legend_x, unused_y => legend_y); end associate

        call reset_ascii_legend_lines_helper(this%legend_lines, this%num_legend_lines)

        if (legend%num_entries <= 0) return

        call append_ascii_legend_line_helper(this%legend_lines, this%num_legend_lines, 'Legend:')

        do i = 1, legend%num_entries
            if (allocated(legend%entries(i)%label)) then
                label_text = trim(legend%entries(i)%label)
            else
                label_text = ''
            end if

            if (len_trim(label_text) == 0) then
                write(line_buffer, '("Series ",I0)') i
                label_text = trim(line_buffer)
            end if

            ! Always try to get marker for legend entries, with robust fallback
            marker_char = '*'  ! Default fallback
            if (allocated(legend%entries(i)%marker)) then
                if (trim(legend%entries(i)%marker) /= 'None' .and. &
                    len_trim(legend%entries(i)%marker) > 0) then
                    ! Use direct character for pie chart markers
                    marker_char = trim(legend%entries(i)%marker)
                end if
            end if

            line_buffer = '  ' // marker_char // ' ' // label_text
            call append_ascii_legend_line_helper(this%legend_lines, this%num_legend_lines, trim(line_buffer))
        end do
    end subroutine ascii_render_legend_specialized

    subroutine ascii_calculate_legend_dimensions(this, legend, legend_width, legend_height)
        use fortplot_legend, only: legend_t
        class(ascii_context), intent(in) :: this
        type(legend_t), intent(in) :: legend
        real(wp), intent(out) :: legend_width, legend_height
        
        call calculate_ascii_legend_dimensions(legend, this%width, legend_width, legend_height)
    end subroutine ascii_calculate_legend_dimensions

    subroutine ascii_set_legend_border_width(this)
        class(ascii_context), intent(inout) :: this
        
        ! Suppress unused parameter warning
        if (this%width < 0) then
        end if
        
        call set_ascii_legend_border_width()
    end subroutine ascii_set_legend_border_width

    subroutine ascii_calculate_legend_position(this, legend, x, y)
        !! Calculate ASCII-specific legend position using character coordinates
        use fortplot_legend, only: legend_t, LEGEND_UPPER_LEFT, LEGEND_UPPER_RIGHT, LEGEND_LOWER_LEFT, LEGEND_LOWER_RIGHT
        class(ascii_context), intent(in) :: this
        type(legend_t), intent(in) :: legend
        real(wp), intent(out) :: x, y
        real(wp) :: legend_width, legend_height, margin_x, margin_y
        
        ! Get ASCII-specific dimensions
        call this%calculate_legend_dimensions(legend, legend_width, legend_height)
        
        margin_x = 2.0_wp      ! 2 character margin
        margin_y = 1.0_wp      ! 1 line margin
        
        select case (legend%position)
        case (LEGEND_UPPER_LEFT)
            x = margin_x
            y = margin_y
        case (LEGEND_UPPER_RIGHT)
            ! Position legend so its text fits within the canvas
            ! For ASCII, be more conservative to avoid clipping
            x = real(this%width, wp) - legend_width - margin_x - 5.0_wp
            x = max(margin_x, x)  ! But not too far left
            y = margin_y + 2.0_wp  ! Start lower to leave room for multiple entries
        case (LEGEND_LOWER_LEFT)
            x = margin_x
            y = real(this%height, wp) - legend_height - margin_y
        case (LEGEND_LOWER_RIGHT)
            x = real(this%width, wp) - legend_width - margin_x
            y = real(this%height, wp) - legend_height - margin_y
        case default
            ! Default to upper right corner  
            x = real(this%width, wp) - legend_width - margin_x
            y = margin_y
        end select
    end subroutine ascii_calculate_legend_position

    subroutine ascii_extract_rgb_data(this, width, height, rgb_data)
        !! Extract RGB data from ASCII backend (not supported - dummy data)
        class(ascii_context), intent(in) :: this
        integer, intent(in) :: width, height
        real(wp), intent(out) :: rgb_data(width, height, 3)
        
        ! Reference otherwise-unused member without unreachable branch
        associate(unused_w => this%width); end associate
        
        ! ASCII backend doesn't have RGB data for animation - fill with dummy data
        rgb_data = 0.0_wp  ! Black background
    end subroutine ascii_extract_rgb_data

    subroutine ascii_get_png_data(this, width, height, png_data, status)
        !! Get PNG data from ASCII backend (not supported)
        class(ascii_context), intent(in) :: this
        integer, intent(in) :: width, height
        integer(1), allocatable, intent(out) :: png_data(:)
        integer, intent(out) :: status
        
        ! Reference otherwise-unused parameters without unreachable branches
        associate(unused_w => this%width, unused_pw => width, unused_ph => height); end associate
        
        ! ASCII backend doesn't provide PNG data
        allocate(png_data(0))
        status = -1
    end subroutine ascii_get_png_data

    subroutine ascii_prepare_3d_data(this, plots)
        !! Prepare 3D data for ASCII backend (no-op - ASCII doesn't use 3D data)
        use fortplot_plot_data, only: plot_data_t
        class(ascii_context), intent(inout) :: this
        type(plot_data_t), intent(in) :: plots(:)
        
        ! Reference otherwise-unused parameters without unreachable branches
        associate(unused_w => this%width, unused_n => size(plots)); end associate
        
        ! ASCII backend doesn't need 3D data preparation - no-op
    end subroutine ascii_prepare_3d_data

    subroutine ascii_render_ylabel(this, ylabel)
        !! Render Y-axis label for ASCII backend (no-op - handled elsewhere)
        class(ascii_context), intent(inout) :: this
        character(len=*), intent(in) :: ylabel
        
        ! Reference otherwise-unused parameters without unreachable branches
        associate(unused_w => this%width, unused_l => len_trim(ylabel)); end associate
        
        ! ASCII backend handles Y-axis labels differently - no-op
    end subroutine ascii_render_ylabel

    subroutine ascii_draw_axes_and_labels(this, xscale, yscale, symlog_threshold, &
                                         x_min, x_max, y_min, y_max, &
                                         title, xlabel, ylabel, &
                                         z_min, z_max, has_3d_plots)
        !! Draw axes and labels for ASCII backend
        class(ascii_context), intent(inout) :: this
        character(len=*), intent(in) :: xscale, yscale
        real(wp), intent(in) :: symlog_threshold
        real(wp), intent(in) :: x_min, x_max, y_min, y_max
        character(len=:), allocatable, intent(in), optional :: title, xlabel, ylabel
        real(wp), intent(in), optional :: z_min, z_max
        logical, intent(in) :: has_3d_plots
        
        ! Call the module version with all required parameters
        call draw_ascii_axes_and_labels(this%canvas, xscale, yscale, symlog_threshold, &
                                       x_min, x_max, y_min, y_max, &
                                       title, xlabel, ylabel, &
                                       z_min, z_max, has_3d_plots, &
                                       this%current_r, this%current_g, this%current_b, &
                                       this%plot_width, this%plot_height, &
                                       this%title_text, this%xlabel_text, this%ylabel_text, &
                                       this%text_elements, this%num_text_elements)
    end subroutine ascii_draw_axes_and_labels

    subroutine ascii_save_coordinates(this, x_min, x_max, y_min, y_max)
        !! Save current coordinate system
        class(ascii_context), intent(in) :: this
        real(wp), intent(out) :: x_min, x_max, y_min, y_max
        
        x_min = this%x_min
        x_max = this%x_max
        if (this%has_stored_y_range) then
            y_min = this%stored_y_min
            y_max = this%stored_y_max
        else
            y_min = this%y_min
            y_max = this%y_max
        end if
    end subroutine ascii_save_coordinates

    subroutine ascii_set_coordinates(this, x_min, x_max, y_min, y_max)
        !! Set coordinate system
        class(ascii_context), intent(inout) :: this
        real(wp), intent(in) :: x_min, x_max, y_min, y_max
        
        this%x_min = x_min
        this%x_max = x_max
        this%stored_y_min = y_min
        this%stored_y_max = y_max
        this%has_stored_y_range = .true.
        this%y_min = y_min * ASCII_CHAR_ASPECT
        this%y_max = y_max * ASCII_CHAR_ASPECT
    end subroutine ascii_set_coordinates

    subroutine ascii_render_axes(this, title_text, xlabel_text, ylabel_text)
        !! Render axes for ASCII context (stub implementation)
        class(ascii_context), intent(inout) :: this
        character(len=*), intent(in), optional :: title_text, xlabel_text, ylabel_text
        
        ! Reference otherwise-unused members/optionals without unreachable branches
        associate(unused_w => this%width); end associate
        if (present(title_text)) then; associate(unused_lt => len_trim(title_text)); end associate; end if
        if (present(xlabel_text)) then; associate(unused_lx => len_trim(xlabel_text)); end associate; end if
        if (present(ylabel_text)) then; associate(unused_ly => len_trim(ylabel_text)); end associate; end if
        
        ! ASCII axes are rendered as part of draw_axes_and_labels_backend
        ! This is a stub to satisfy the interface
    end subroutine ascii_render_axes



    subroutine ascii_clear_legend_lines(this, header)
        class(ascii_context), intent(inout) :: this
        character(len=*), intent(in), optional :: header

        call reset_ascii_legend_lines_helper(this%legend_lines, this%num_legend_lines)

        if (present(header)) then
            if (len_trim(header) > 0) then
                call append_ascii_legend_line_helper(this%legend_lines, this%num_legend_lines, trim(header))
            end if
        end if

        this%capturing_legend = .false.
    end subroutine ascii_clear_legend_lines

    subroutine decode_ascii_legend_line(raw_text, formatted_line, entry_label)
        character(len=*), intent(in) :: raw_text
        character(len=96), intent(out) :: formatted_line
        character(len=64), intent(out) :: entry_label

        character(len=:), allocatable :: trimmed_text
        integer :: first_space

        formatted_line = ''
        entry_label = ''

        trimmed_text = trim(adjustl(raw_text))
        if (len_trim(trimmed_text) == 0) return

        if (len(trimmed_text) >= 3 .and. trimmed_text(1:3) == '-- ') then
            if (len(trimmed_text) > 3) then
                entry_label = trim(adjustl(trimmed_text(4:)))
            else
                entry_label = ''
            end if
            formatted_line = '  - ' // trim(entry_label)
        else
            formatted_line = '  ' // trim(trimmed_text)
            first_space = index(trimmed_text, ' ')
            if (first_space > 0 .and. first_space < len(trimmed_text)) then
                entry_label = trim(adjustl(trimmed_text(first_space + 1:)))
            else
                entry_label = trim(trimmed_text)
            end if
        end if
    end subroutine decode_ascii_legend_line

    subroutine ascii_add_legend_entry(this, label, value_text)
        class(ascii_context), intent(inout) :: this
        character(len=*), intent(in) :: label
        character(len=*), intent(in), optional :: value_text

        character(len=96) :: line_buffer
        character(len=:), allocatable :: value_trimmed

        line_buffer = '  ' // trim(label)

        if (present(value_text)) then
            value_trimmed = trim(value_text)
            if (len_trim(value_trimmed) > 0) then
                line_buffer = trim(line_buffer) // ' (' // value_trimmed // ')'
            end if
        end if

        call append_ascii_legend_line_helper(this%legend_lines, this%num_legend_lines, trim(line_buffer))
    end subroutine ascii_add_legend_entry

    subroutine ascii_clear_pie_legend_entries(this)
        class(ascii_context), intent(inout) :: this

        ! For ASCII backend, pie legends are managed by the text module
        ! Clear the standard legend lines instead
        call this%clear_ascii_legend()
    end subroutine ascii_clear_pie_legend_entries

    subroutine ascii_register_pie_legend_entry(this, label, value_text)
        class(ascii_context), intent(inout) :: this
        character(len=*), intent(in) :: label
        character(len=*), intent(in) :: value_text

        ! For ASCII backend, pie legends are handled by the text module
        ! This is a placeholder that delegates to the standard legend system
        call this%add_ascii_legend_entry(label, value_text)
    end subroutine ascii_register_pie_legend_entry

end module fortplot_ascii