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_ascii_utils, only: is_legend_entry_text, & is_registered_legend_label, is_autopct_text, & get_blend_char, 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, 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_width, plot_height, & title_text, xlabel_text, ylabel_text, & text_elements, num_text_elements, & custom_xticks, custom_xtick_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 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(:) real(wp) :: x_tick_positions(MAX_TICKS), y_tick_positions(MAX_TICKS) character(len=500) :: processed_title integer :: processed_len ! 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 call process_axis_labels(title, processed_title, processed_len, title_text) call draw_ascii_axis_lines(canvas, x_min, x_max, y_min, y_max, plot_width, plot_height) call render_ascii_x_ticks(xscale, x_min, x_max, y_min, y_max, symlog_threshold, & x_tick_positions, custom_xticks, custom_xtick_labels, & x_date_format, 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_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) call process_axis_labels(ylabel, processed_title, processed_len, ylabel_text) end subroutine draw_ascii_axes_and_labels subroutine process_axis_labels(label, processed_title, processed_len, output_text) !! Process a single axis label (title, xlabel, or ylabel) for ASCII output 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 output_text = '' if (present(label)) then if (allocated(label)) then call sanitize_ascii_text(label, processed_title, processed_len) output_text = processed_title(1:processed_len) end if end if end subroutine process_axis_labels subroutine draw_ascii_axis_lines(canvas, x_min, x_max, y_min, y_max, plot_width, plot_height) !! Draw horizontal and vertical axis lines on ASCII canvas character(len=1), intent(inout) :: canvas(:, :) real(wp), intent(in) :: x_min, x_max, y_min, y_max integer, intent(in) :: plot_width, plot_height call draw_line_on_canvas_local(canvas, x_min, y_min, x_max, y_min, & x_min, x_max, y_min, y_max, plot_width, & plot_height, '-') call draw_line_on_canvas_local(canvas, x_min, y_min, x_min, y_max, & x_min, x_max, y_min, y_max, plot_width, & plot_height, '|') end subroutine draw_ascii_axis_lines subroutine render_ascii_x_ticks(xscale, x_min, x_max, y_min, y_max, symlog_threshold, & x_tick_positions, custom_xticks, custom_xtick_labels, & x_date_format, 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=*), 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 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 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 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 associate (sx => nint((tick_x - x_min)/(x_max - x_min)* & real(plot_width - 2, wp)) + 1, & sy => plot_height) call add_text_element(text_elements, num_text_elements, & real(max(1, min(sx, 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_width, plot_height) end associate 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_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 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(:) 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 allocate (row_best_len(plot_height)) allocate (row_best_label(plot_height)) row_best_len = 0 row_best_label = '' 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 row = nint((y_max - tick_y)/(y_max - y_min)*real(plot_height, wp)) row = max(1, min(row, plot_height)) 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 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_width, plot_height) end if end do end subroutine render_ascii_y_ticks subroutine draw_line_on_canvas_local(canvas, x1, y1, x2, y2, x_min, x_max, y_min, & y_max, plot_width, plot_height, line_char) character(len=1), intent(inout) :: canvas(:, :) real(wp), intent(in) :: x1, y1, x2, y2 real(wp), intent(in) :: x_min, x_max, y_min, y_max integer, intent(in) :: plot_width, plot_height character(len=1), intent(in) :: line_char real(wp) :: dx, dy, length, step_x, step_y, x, y integer :: steps, i, px, py dx = x2 - x1 dy = y2 - y1 length = sqrt(dx*dx + dy*dy) if (length < 1e-6_wp) return steps = max(int(length*4), max(abs(int(dx)), abs(int(dy)))) + 1 step_x = dx/real(steps, wp) step_y = dy/real(steps, wp) x = x1 y = y1 do i = 0, steps ! Map to usable plot area (excluding 1-char border on each side) px = int((x - x_min)/(x_max - x_min)*real(plot_width - 3, wp)) + 2 py = (plot_height - 1) - int((y - y_min)/(y_max - & y_min)*real(plot_height - 3, wp)) if (px >= 2 .and. px <= plot_width - 1 .and. py >= 2 .and. py <= & plot_height - 1) then if (canvas(py, px) == ' ') then canvas(py, px) = line_char else if (canvas(py, px) /= line_char) then canvas(py, px) = get_blend_char(canvas(py, px), line_char) end if end if x = x + step_x y = y + step_y end do end subroutine draw_line_on_canvas_local 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_height, current_r, current_g, 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