fortplot_ascii_text.f90 Source File


Source Code

module fortplot_ascii_text
     !! ASCII terminal plotting backend - Text and Layout Management
     !!
     !! This module handles ASCII text rendering, axes labels, and complex
     !! text processing. Legend management is delegated to fortplot_ascii_legend.
     !!
     !! Author: fortplot contributors

     use fortplot_axes, only: compute_scale_ticks, format_tick_label, MAX_TICKS
     use fortplot_tick_calculation, only: determine_decimals_from_ticks, &
                                          format_tick_value_consistent
     use fortplot_ascii_mathtext, only: sanitize_ascii_text
     use fortplot_margins, only: plot_area_t
     use fortplot_ascii_utils, only: is_legend_entry_text, &
                                     is_registered_legend_label, is_autopct_text, &
                                     text_element_t
     use fortplot_ascii_legend, only: reset_ascii_legend_lines_helper, &
                                      append_ascii_legend_line_helper, &
                                      register_legend_entry_helper, &
                                      assign_pending_autopct_helper, &
                                      enqueue_pie_autopct, dequeue_pie_autopct, &
                                      clear_pie_legend_entries, get_pie_autopct, &
                                      decode_ascii_legend_line
     use fortplot_ascii_text_elements, only: add_text_element, store_text_element
     use fortplot_ascii_drawing, only: draw_text_axis_frame, draw_text_axis_tick
     use, intrinsic :: iso_fortran_env, only: wp => real64
     implicit none

     private
     public :: draw_ascii_axes_and_labels, ascii_draw_text_helper

contains

    subroutine draw_ascii_axes_and_labels(canvas, xscale, yscale, symlog_threshold, &
                                           x_min, x_max, y_min, y_max, &
                                           title, xlabel, ylabel, &
                                           x_date_format, y_date_format, &
                                           z_min, z_max, has_3d_plots, &
                                           current_r, current_g, current_b, &
                                           plot_area, plot_width, plot_height, &
                                           title_text, xlabel_text, ylabel_text, &
                                           text_elements, num_text_elements, &
                                           custom_xticks, custom_xtick_labels, &
                                           unicode_labels)
        !! Draw axes and labels for ASCII backend
        character(len=1), intent(inout) :: canvas(:, :)
        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
        character(len=*), intent(in), optional :: x_date_format, y_date_format
        real(wp), intent(in), optional :: z_min, z_max
        logical, intent(in) :: has_3d_plots
        real(wp), intent(in) :: current_r, current_g, current_b
        type(plot_area_t), intent(in) :: plot_area
        integer, intent(in) :: plot_width, plot_height
        character(len=:), allocatable, intent(inout) :: title_text, xlabel_text, &
                                                        ylabel_text
        type(text_element_t), intent(inout) :: text_elements(:)
        integer, intent(inout) :: num_text_elements
        real(wp), intent(in), optional :: custom_xticks(:)
        character(len=*), intent(in), optional :: custom_xtick_labels(:)
        logical, intent(in), optional :: unicode_labels

        real(wp) :: x_tick_positions(MAX_TICKS), y_tick_positions(MAX_TICKS)
        character(len=500) :: processed_title
        integer :: processed_len
        logical :: raw_labels

        ! Reference optional parameters without unreachable branches
        if (present(z_min)) then; associate (unused_zmin => z_min); end associate; end if
        if (present(z_max)) then; associate (unused_zmax => z_max); end associate; end if
        associate (unused_h3d => has_3d_plots); end associate

        raw_labels = .false.
        if (present(unicode_labels)) raw_labels = unicode_labels

        call process_axis_labels(title, processed_title, processed_len, title_text, &
                                 raw_labels)
        if (has_3d_plots) then
            call process_axis_labels(xlabel, processed_title, processed_len, &
                                     xlabel_text, raw_labels)
            call process_axis_labels(ylabel, processed_title, processed_len, &
                                     ylabel_text, raw_labels)
            return
        end if
        call draw_ascii_axis_lines(canvas, x_min, x_max, y_min, y_max, plot_area, &
                                   plot_width, plot_height)
        call render_ascii_x_ticks(canvas, xscale, x_min, x_max, y_min, y_max, symlog_threshold, &
                                  x_tick_positions, custom_xticks, custom_xtick_labels, &
                                  x_date_format, plot_area, plot_width, plot_height, &
                                  text_elements, num_text_elements, current_r, current_g, current_b)
        call render_ascii_y_ticks(yscale, x_min, x_max, y_min, y_max, symlog_threshold, &
                                  y_tick_positions, y_date_format, plot_area, plot_width, plot_height, &
                                  text_elements, num_text_elements, current_r, current_g, current_b)
        call process_axis_labels(xlabel, processed_title, processed_len, xlabel_text, &
                                 raw_labels)
        call process_axis_labels(ylabel, processed_title, processed_len, ylabel_text, &
                                 raw_labels)
    end subroutine draw_ascii_axes_and_labels

    subroutine process_axis_labels(label, processed_title, processed_len, output_text, &
                                   raw_label)
        !! Process a single axis label (title, xlabel, or ylabel) for text output.
        !! In ASCII mode the label is sanitized to printable ASCII; when
        !! raw_label is set (Unicode charset) the original text is preserved so
        !! Unicode annotations survive Unicode output (issue #2060).
        character(len=:), allocatable, intent(in), optional :: label
        character(len=500), intent(inout) :: processed_title
        integer, intent(inout) :: processed_len
        character(len=:), allocatable, intent(out) :: output_text
        logical, intent(in), optional :: raw_label
        logical :: keep_raw

        keep_raw = .false.
        if (present(raw_label)) keep_raw = raw_label

        output_text = ''
        if (present(label)) then
            if (allocated(label)) then
                if (keep_raw) then
                    output_text = label
                else
                    call sanitize_ascii_text(label, processed_title, processed_len)
                    output_text = processed_title(1:processed_len)
                end if
            end if
        end if
    end subroutine process_axis_labels

    subroutine draw_ascii_axis_lines(canvas, x_min, x_max, y_min, y_max, plot_area, &
                                     plot_width, plot_height)
        !! Draw solid left and bottom axis spines on the ASCII canvas.
        !! Spines are filled cell-by-cell in screen space (issue #2069) so they
        !! never degrade into a dashed data-like line as data-space sampling did.
        character(len=1), intent(inout) :: canvas(:, :)
        real(wp), intent(in) :: x_min, x_max, y_min, y_max
        type(plot_area_t), intent(in) :: plot_area
        integer, intent(in) :: plot_width, plot_height

        integer :: axis_col, bottom_row, top_row, right_col

        call text_axis_frame_bounds(x_min, x_max, y_min, y_max, plot_area, &
                                    plot_width, plot_height, &
                                    axis_col, bottom_row, top_row, right_col)
        call draw_text_axis_frame(canvas, axis_col, bottom_row, top_row, right_col)
    end subroutine draw_ascii_axis_lines

    subroutine text_axis_frame_bounds(x_min, x_max, y_min, y_max, plot_area, &
                                      plot_width, plot_height, &
                                      axis_col, bottom_row, top_row, right_col)
        !! Screen-space bounds of the axis frame, matching the marker/line
        !! coordinate mapping so spines align with plotted data and tick labels.
        real(wp), intent(in) :: x_min, x_max, y_min, y_max
        type(plot_area_t), intent(in) :: plot_area
        integer, intent(in) :: plot_width, plot_height
        integer, intent(out) :: axis_col, bottom_row, top_row, right_col
        logical :: use_plot_area

        use_plot_area = plot_area%width > 0 .and. plot_area%height > 0
        if (use_plot_area) then
            call map_to_plot_area(x_min, y_min, x_min, x_max, y_min, y_max, &
                                  plot_area, axis_col, bottom_row)
            call map_to_plot_area(x_max, y_max, x_min, x_max, y_min, y_max, &
                                  plot_area, right_col, top_row)
        else
            axis_col = 2
            top_row = 2
            bottom_row = plot_height - 1
            right_col = plot_width - 1
        end if
    end subroutine text_axis_frame_bounds

subroutine render_ascii_x_ticks(canvas, xscale, x_min, x_max, y_min, y_max, &
                                 symlog_threshold, &
                                 x_tick_positions, custom_xticks, custom_xtick_labels, &
                                 x_date_format, plot_area, plot_width, plot_height, &
                                 text_elements, num_text_elements, current_r, current_g, current_b)
        !! Render X-axis tick marks and labels on ASCII canvas
        character(len=1), intent(inout) :: canvas(:, :)
        character(len=*), intent(in) :: xscale
        real(wp), intent(in) :: x_min, x_max, y_min, y_max, symlog_threshold
        real(wp), contiguous, intent(inout) :: x_tick_positions(:)
        real(wp), intent(in), optional :: custom_xticks(:)
        character(len=*), intent(in), optional :: custom_xtick_labels(:)
        character(len=*), intent(in), optional :: x_date_format
        type(plot_area_t), intent(in) :: plot_area
        integer, intent(in) :: plot_width, plot_height
        type(text_element_t), intent(inout) :: text_elements(:)
        integer, intent(inout) :: num_text_elements
        real(wp), intent(in) :: current_r, current_g, current_b

        integer :: num_x_ticks, i
        character(len=50) :: tick_label
        real(wp) :: tick_x
        integer :: decimals
        logical :: use_custom_xticks
        logical :: use_plot_area
        integer :: axis_col, bottom_row, top_row, right_col

        use_custom_xticks = .false.
        if (present(custom_xticks) .and. present(custom_xtick_labels)) then
            if (size(custom_xticks) > 0 .and. &
                size(custom_xticks) == size(custom_xtick_labels)) then
                use_custom_xticks = .true.
                num_x_ticks = min(size(custom_xticks), MAX_TICKS)
                x_tick_positions(1:num_x_ticks) = custom_xticks(1:num_x_ticks)
            end if
        end if

        if (.not. use_custom_xticks) then
            call compute_scale_ticks(xscale, x_min, x_max, symlog_threshold, &
                                     x_tick_positions, num_x_ticks)
        else
            num_x_ticks = min(size(custom_xticks), MAX_TICKS)
        end if

        decimals = 0
        if (trim(xscale) == 'linear' .and. num_x_ticks >= 2 .and. &
            .not. use_custom_xticks) then
            decimals = determine_decimals_from_ticks(x_tick_positions, num_x_ticks)
        end if
        use_plot_area = plot_area%width > 0 .and. plot_area%height > 0
        call text_axis_frame_bounds(x_min, x_max, y_min, y_max, plot_area, &
                                    plot_width, plot_height, &
                                    axis_col, bottom_row, top_row, right_col)
        associate (unused_top => top_row); end associate

        do i = 1, num_x_ticks
            tick_x = x_tick_positions(i)
            if (use_custom_xticks) then
                tick_label = custom_xtick_labels(i)
            else if (trim(xscale) == 'linear') then
                tick_label = format_tick_value_consistent(tick_x, decimals)
            else
                tick_label = format_tick_label(tick_x, xscale, &
                                               date_format=x_date_format, &
                                               data_min=x_min, data_max=x_max)
            end if
            if (use_plot_area) then
                associate (sx => nint((tick_x - x_min)/(x_max - x_min)* &
                               real(max(1, plot_area%width - 2), wp)) + plot_area%left + 1, &
                             sy => min(plot_height, plot_area%bottom + plot_area%height))
                    ! Center the label on the tick column rather than left-anchoring it,
                    ! so labels sit under the bar/tick they name (issue #1957).
                    call draw_text_axis_tick(canvas, bottom_row, &
                                             max(axis_col, min(sx, right_col)))
                    associate (start_col => sx - len_trim(tick_label)/2)
                        call add_text_element(text_elements, num_text_elements, &
                                              real(max(plot_area%left + 1, &
                                                       min(start_col, plot_area%left + plot_area%width - 1)), wp), &
                                              real(sy, wp), &
                                              trim(tick_label), &
                                              current_r, current_g, current_b, &
                                              x_min, x_max, y_min, y_max, plot_area, plot_width, plot_height)
                    end associate
                end associate
            else
                associate (sx => nint((tick_x - x_min)/(x_max - x_min)* &
                               real(plot_width - 2, wp)) + 1, &
                             sy => plot_height)
                    call draw_text_axis_tick(canvas, bottom_row, &
                                             max(axis_col, min(sx, right_col)))
                    associate (start_col => sx - len_trim(tick_label)/2)
                        call add_text_element(text_elements, num_text_elements, &
                                              real(max(1, min(start_col, plot_width - 1)), wp), &
                                              real(sy, wp), &
                                              trim(tick_label), &
                                              current_r, current_g, current_b, &
                                              x_min, x_max, y_min, y_max, plot_area, plot_width, plot_height)
                    end associate
                end associate
            end if
        end do
    end subroutine render_ascii_x_ticks

subroutine render_ascii_y_ticks(yscale, x_min, x_max, y_min, y_max, symlog_threshold, &
                                   y_tick_positions, y_date_format, plot_area, plot_width, plot_height, &
                                   text_elements, num_text_elements, current_r, current_g, current_b)
        !! Render Y-axis tick marks with row-based de-duplication on ASCII canvas
        character(len=*), intent(in) :: yscale
        real(wp), intent(in) :: x_min, x_max, y_min, y_max, symlog_threshold
        real(wp), contiguous, intent(inout) :: y_tick_positions(:)
        character(len=*), intent(in), optional :: y_date_format
        type(plot_area_t), intent(in) :: plot_area
        integer, intent(in) :: plot_width, plot_height
        type(text_element_t), intent(inout) :: text_elements(:)
        integer, intent(inout) :: num_text_elements
        real(wp), intent(in) :: current_r, current_g, current_b

        integer :: num_y_ticks, i, row
        character(len=50) :: tick_label
        real(wp) :: tick_y
        integer :: decimals
        integer, allocatable :: row_best_len(:)
        character(len=64), allocatable :: row_best_label(:)
        logical :: use_plot_area

        call compute_scale_ticks(yscale, y_min, y_max, symlog_threshold, &
                                 y_tick_positions, num_y_ticks)
        decimals = 0
        if (trim(yscale) == 'linear' .and. num_y_ticks >= 2) then
            decimals = determine_decimals_from_ticks(y_tick_positions, num_y_ticks)
        end if
        use_plot_area = plot_area%width > 0 .and. plot_area%height > 0

        if (use_plot_area) then
            allocate (row_best_len(max(1, plot_area%bottom + plot_area%height + 1)))
            allocate (row_best_label(max(1, plot_area%bottom + plot_area%height + 1)))
            row_best_len = 0
            row_best_label = ''
        else
            allocate (row_best_len(max(1, plot_height + 1)))
            allocate (row_best_label(max(1, plot_height + 1)))
            row_best_len = 0
            row_best_label = ''
        end if

        do i = 1, num_y_ticks
            tick_y = y_tick_positions(i)
            if (trim(yscale) == 'linear') then
                tick_label = format_tick_value_consistent(tick_y, decimals)
            else
                tick_label = format_tick_label(tick_y, yscale, &
                                               date_format=y_date_format, &
                                               data_min=y_min, data_max=y_max)
            end if
            if (use_plot_area) then
                row = plot_area%bottom + plot_area%height - 1 - &
                      nint((tick_y - y_min)/(y_max - y_min)*real(max(1, plot_area%height - 2), wp))
                row = max(plot_area%bottom + 1, min(row, plot_area%bottom + plot_area%height - 1))
            else
                row = nint((y_max - tick_y)/(y_max - y_min)*real(plot_height, wp))
                row = max(1, min(row, plot_height))
            end if
            if (len_trim(tick_label) > row_best_len(row)) then
                row_best_len(row) = len_trim(tick_label)
                row_best_label(row) = adjustl(tick_label)
            end if
        end do

        if (use_plot_area) then
            do row = plot_area%bottom + 1, plot_area%bottom + plot_area%height - 1
                if (row_best_len(row) > 0) then
                    call add_text_element(text_elements, num_text_elements, &
                                          real(plot_area%left + 1, wp), real(row, wp), &
                                          trim(row_best_label(row)), &
                                          current_r, current_g, current_b, &
                                          x_min, x_max, y_min, y_max, plot_area, plot_width, plot_height)
                end if
            end do
        else
            do row = 1, plot_height
                if (row_best_len(row) > 0 .and. row < plot_height) then
                    call add_text_element(text_elements, num_text_elements, &
                                          2.0_wp, real(row, wp), &
                                          trim(row_best_label(row)), &
                                          current_r, current_g, current_b, &
                                          x_min, x_max, y_min, y_max, plot_area, plot_width, plot_height)
                end if
            end do
        end if
    end subroutine render_ascii_y_ticks

    subroutine map_to_plot_area(x, y, x_min, x_max, y_min, y_max, plot_area, px, py)
        real(wp), intent(in) :: x, y, x_min, x_max, y_min, y_max
        type(plot_area_t), intent(in) :: plot_area
        integer, intent(out) :: px, py
        integer :: inner_width, inner_height

        if (plot_area%width > 0 .and. plot_area%height > 0) then
            inner_width = max(1, plot_area%width - 2)
            inner_height = max(1, plot_area%height - 2)
            px = plot_area%left + 1 + nint((x - x_min)/(x_max - x_min)*real(inner_width, wp))
            py = plot_area%bottom + plot_area%height - 1 - &
                 nint((y - y_min)/(y_max - y_min)*real(inner_height, wp))
        else
            px = 1
            py = 1
        end if
    end subroutine map_to_plot_area

    subroutine ascii_draw_text_helper(text_elements, num_text_elements, &
                                      legend_lines, num_legend_lines, &
                                      capturing_legend, &
                                      pie_legend_labels, pie_legend_values, &
                                      pie_legend_count, &
                                      pie_autopct_queue, pie_autopct_count, &
                                      legend_entry_indices, legend_entry_has_autopct, &
                                      legend_entry_labels, legend_entry_count, &
                                      legend_autopct_cursor, &
                                      x, y, text, x_min, x_max, y_min, y_max, &
                                      plot_width, plot_height, current_r, &
                                      current_g, current_b)
        !! ASCII text drawing with legend processing (moved from main module)
        type(text_element_t), intent(inout) :: text_elements(:)
        integer, intent(inout) :: num_text_elements
        character(len=96), allocatable, intent(inout) :: legend_lines(:)
        integer, intent(inout) :: num_legend_lines
        logical, intent(inout) :: capturing_legend
        character(len=64), allocatable, intent(inout) :: pie_legend_labels(:)
        character(len=32), allocatable, intent(inout) :: pie_legend_values(:)
        integer, intent(inout) :: pie_legend_count
        character(len=32), allocatable, intent(inout) :: pie_autopct_queue(:)
        integer, intent(inout) :: pie_autopct_count
        integer, allocatable, intent(inout) :: legend_entry_indices(:)
        logical, allocatable, intent(inout) :: legend_entry_has_autopct(:)
        character(len=64), allocatable, intent(inout) :: legend_entry_labels(:)
        integer, intent(inout) :: legend_entry_count, legend_autopct_cursor
        real(wp), intent(in) :: x, y
        character(len=*), intent(in) :: text
        real(wp), intent(in) :: x_min, x_max, y_min, y_max
        integer, intent(in) :: plot_width, plot_height
        real(wp), intent(in) :: current_r, current_g, current_b

        integer :: text_x, text_y
        character(len=:), allocatable :: processed_text, trimmed_text
        character(len=96) :: formatted_line
        character(len=64) :: entry_label
        character(len=32) :: autopct_value
        logical :: handled

        trimmed_text = trim(adjustl(text))
        handled = .false.

        if (trimmed_text == 'ASCII Legend') then
            call handle_ascii_legend_init(legend_lines, num_legend_lines, &
                                          capturing_legend, legend_entry_count, &
                                          legend_autopct_cursor)
            handled = .true.
        else if (is_autopct_text(trimmed_text)) then
            call handle_ascii_autopct(trimmed_text, pie_autopct_queue, pie_autopct_count, &
                                      legend_lines, num_legend_lines, &
                                      legend_entry_indices, legend_entry_has_autopct, &
                                      legend_autopct_cursor, legend_entry_count)
            handled = .true.
        else if (capturing_legend) then
            call handle_ascii_legend_capture(trimmed_text, formatted_line, entry_label, &
                                             autopct_value, pie_autopct_queue, pie_autopct_count, &
                                             pie_legend_labels, pie_legend_values, pie_legend_count, &
                                             legend_lines, num_legend_lines, capturing_legend, &
                                             legend_entry_indices, legend_entry_has_autopct, &
                                             legend_entry_labels, legend_entry_count, &
                                             legend_autopct_cursor, handled)
        end if

        if (.not. handled .and. legend_entry_count > 0) then
            if (is_registered_legend_label(legend_entry_labels, legend_entry_count, &
                                           trimmed_text)) return
        end if

        if (.not. handled) then
            call store_text_element(text_elements, num_text_elements, text_x, text_y, &
                                    processed_text, x, y, text, x_min, x_max, y_min, y_max, &
                                    plot_width=plot_width, plot_height=plot_height, &
                                    current_r=current_r, current_g=current_g, current_b=current_b)
        end if
    end subroutine ascii_draw_text_helper

    subroutine handle_ascii_legend_init(legend_lines, num_legend_lines, &
                                        capturing_legend, legend_entry_count, &
                                        legend_autopct_cursor)
        !! Initialize ASCII legend state
        character(len=96), allocatable, intent(inout) :: legend_lines(:)
        integer, intent(inout) :: num_legend_lines
        logical, intent(inout) :: capturing_legend
        integer, intent(inout) :: legend_entry_count
        integer, intent(inout) :: legend_autopct_cursor

        call reset_ascii_legend_lines_helper(legend_lines, num_legend_lines)
        call append_ascii_legend_line_helper(legend_lines, num_legend_lines, 'Legend:')
        capturing_legend = .true.
        legend_entry_count = 0
        legend_autopct_cursor = 1
    end subroutine handle_ascii_legend_init

    subroutine handle_ascii_autopct(trimmed_text, pie_autopct_queue, pie_autopct_count, &
                                     legend_lines, num_legend_lines, &
                                     legend_entry_indices, legend_entry_has_autopct, &
                                     legend_autopct_cursor, legend_entry_count)
        !! Handle autopct text for pie charts
        character(len=*), intent(in) :: trimmed_text
        character(len=32), allocatable, intent(inout) :: pie_autopct_queue(:)
        integer, intent(inout) :: pie_autopct_count
        character(len=96), allocatable, intent(inout) :: legend_lines(:)
        integer, intent(inout) :: num_legend_lines
        integer, allocatable, intent(inout) :: legend_entry_indices(:)
        logical, allocatable, intent(inout) :: legend_entry_has_autopct(:)
        integer, intent(inout) :: legend_autopct_cursor
        integer, intent(inout) :: legend_entry_count

        call enqueue_pie_autopct(pie_autopct_queue, pie_autopct_count, trimmed_text)
        call assign_pending_autopct_helper(legend_lines, num_legend_lines, &
                                           pie_autopct_queue, pie_autopct_count, &
                                           legend_entry_indices, legend_entry_has_autopct, &
                                           legend_autopct_cursor, legend_entry_count)
    end subroutine handle_ascii_autopct

    subroutine handle_ascii_legend_capture(trimmed_text, formatted_line, entry_label, &
                                            autopct_value, pie_autopct_queue, pie_autopct_count, &
                                            pie_legend_labels, pie_legend_values, pie_legend_count, &
                                            legend_lines, num_legend_lines, capturing_legend, &
                                            legend_entry_indices, legend_entry_has_autopct, &
                                            legend_entry_labels, legend_entry_count, &
                                            legend_autopct_cursor, handled)
        !! Handle legend content capture phase
        character(len=*), intent(in) :: trimmed_text
        character(len=96), intent(inout) :: formatted_line
        character(len=64), intent(inout) :: entry_label
        character(len=32), intent(inout) :: autopct_value
        character(len=32), allocatable, intent(inout) :: pie_autopct_queue(:)
        integer, intent(inout) :: pie_autopct_count
        character(len=64), allocatable, intent(inout) :: pie_legend_labels(:)
        character(len=32), allocatable, intent(inout) :: pie_legend_values(:)
        integer, intent(inout) :: pie_legend_count
        character(len=96), allocatable, intent(inout) :: legend_lines(:)
        integer, intent(inout) :: num_legend_lines
        logical, intent(inout) :: capturing_legend
        integer, allocatable, intent(inout) :: legend_entry_indices(:)
        logical, allocatable, intent(inout) :: legend_entry_has_autopct(:)
        character(len=64), allocatable, intent(inout) :: legend_entry_labels(:)
        integer, intent(inout) :: legend_entry_count
        integer, intent(inout) :: legend_autopct_cursor
        logical, intent(out) :: handled

        handled = .false.

        if (len_trim(trimmed_text) == 0) then
            capturing_legend = .false.
            call clear_pie_legend_entries(pie_legend_labels, pie_legend_values, &
                                              pie_legend_count, pie_autopct_queue, pie_autopct_count)
            handled = .true.
            return
        end if

        if (.not. is_legend_entry_text(trimmed_text)) then
            capturing_legend = .false.
            call clear_pie_legend_entries(pie_legend_labels, pie_legend_values, &
                                              pie_legend_count, pie_autopct_queue, pie_autopct_count)
            return
        end if

        call decode_ascii_legend_line(trimmed_text, formatted_line, entry_label)
        if (len_trim(entry_label) > 0 .and. len_trim(formatted_line) > 0) then
            autopct_value = ''
            if (pie_autopct_count > 0) then
                autopct_value = dequeue_pie_autopct(pie_autopct_queue, pie_autopct_count)
            else
                autopct_value = get_pie_autopct(pie_legend_labels, pie_legend_values, &
                                                pie_legend_count, entry_label)
            end if
            if (len_trim(autopct_value) > 0) then
                formatted_line = trim(formatted_line)//' ('//trim(autopct_value)//')'
            end if
            call append_ascii_legend_line_helper(legend_lines, num_legend_lines, &
                                                 trim(formatted_line))
            call register_legend_entry_helper(legend_entry_indices, &
                                             legend_entry_has_autopct, &
                                             legend_entry_labels, legend_entry_count, &
                                             legend_autopct_cursor, num_legend_lines, &
                                             entry_label, len_trim(autopct_value) > 0)
            call assign_pending_autopct_helper(legend_lines, num_legend_lines, &
                                               pie_autopct_queue, pie_autopct_count, &
                                               legend_entry_indices, legend_entry_has_autopct, &
                                               legend_autopct_cursor, legend_entry_count)
      handled = .true.
         end if
     end subroutine handle_ascii_legend_capture

end module fortplot_ascii_text