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_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: extract_ascii_rgb_data, get_ascii_png_data, prepare_ascii_3d_data use fortplot_ascii_elements, only: render_ascii_ylabel, draw_ascii_axes_and_labels, render_ascii_axes use, intrinsic :: iso_fortran_env, only: wp => real64 implicit none private public :: ascii_context, create_ascii_canvas 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 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_finalize 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 !! 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 end type ascii_context character(len=*), parameter :: DENSITY_CHARS = ' ░▒▓█' character(len=*), parameter :: BOX_CHARS = '-|+++++++' ! Color filtering thresholds real(wp), parameter :: LIGHT_COLOR_THRESHOLD = 0.8_wp real(wp), parameter :: MEDIUM_COLOR_THRESHOLD = 0.7_wp 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 chars are ~1.5x taller than wide, so for 4:3 visual ratio: ! 80 chars wide × (3/4) × (1/1.5) = 80 × 0.75 × 0.67 ≈ 40 → 30 chars high w = 80 h = 30 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. 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 real(wp) :: dx, dy, length, step_x, step_y, x, y integer :: steps, i, px, py character(len=1) :: line_char real(wp) :: luminance ! Calculate luminance for better character selection ! Using standard luminance formula luminance = 0.299_wp * this%current_r + 0.587_wp * this%current_g + 0.114_wp * this%current_b ! Select character based on color dominance and luminance ! Don't skip any colors - render everything if (luminance > 0.9_wp) then ! Very bright colors still get rendered with lighter characters line_char = ':' else if (this%current_g > 0.7_wp) then line_char = '@' else if (this%current_g > 0.3_wp) then line_char = '#' else if (this%current_b > 0.7_wp) then line_char = '*' else if (this%current_b > 0.3_wp) then line_char = 'o' else if (this%current_r > 0.7_wp) then line_char = '%' else if (this%current_r > 0.3_wp) then line_char = '+' else line_char = '.' end if 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 - this%x_min) / (this%x_max - this%x_min) * real(this%plot_width - 3, wp)) + 2 py = (this%plot_height - 1) - int((y - this%y_min) / (this%y_max - this%y_min) * real(this%plot_height - 3, wp)) if (px >= 2 .and. px <= this%plot_width - 1 .and. py >= 2 .and. py <= this%plot_height - 1) then if (this%canvas(py, px) == ' ') then this%canvas(py, px) = line_char else if (this%canvas(py, px) /= line_char) then this%canvas(py, px) = get_blend_char(this%canvas(py, px), line_char) end if end if x = x + step_x y = y + step_y end do 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 integer :: text_x, text_y character(len=500) :: processed_text integer :: processed_len ! Process LaTeX commands to Unicode call process_latex_in_text(text, processed_text, processed_len) ! Store text element for later rendering if (this%num_text_elements < size(this%text_elements)) then this%num_text_elements = this%num_text_elements + 1 ! Convert coordinates - check if already in screen coordinates if (x >= 1.0_wp .and. x <= real(this%plot_width, wp) .and. & y >= 1.0_wp .and. y <= real(this%plot_height, wp)) then ! Already in screen coordinates (e.g., from legend) text_x = nint(x) text_y = nint(y) else ! Convert from data coordinates to canvas coordinates text_x = nint((x - this%x_min) / (this%x_max - this%x_min) * real(this%plot_width, wp)) text_y = nint((this%y_max - y) / (this%y_max - this%y_min) * real(this%plot_height, wp)) end if ! Clamp to canvas bounds ! For legend text (already in screen coordinates), don't truncate based on length if (x >= 1.0_wp .and. x <= real(this%plot_width, wp) .and. & y >= 1.0_wp .and. y <= real(this%plot_height, wp)) then ! For legend text, only clamp starting position, let text extend as needed text_x = max(1, min(text_x, this%plot_width)) else ! For other text, prevent overflow text_x = max(1, min(text_x, this%plot_width - processed_len)) end if text_y = max(1, min(text_y, this%plot_height)) this%text_elements(this%num_text_elements)%text = processed_text(1:processed_len) this%text_elements(this%num_text_elements)%x = text_x this%text_elements(this%num_text_elements)%y = text_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_finalize(this, filename) class(ascii_context), intent(inout) :: this character(len=*), intent(in) :: filename integer :: unit, ios character(len=512) :: error_msg if (len_trim(filename) == 0 .or. trim(filename) == "terminal") then call output_to_terminal(this) else open(newunit=unit, file=filename, status='replace', iostat=ios, iomsg=error_msg) if (ios /= 0) then call log_error("Cannot save ASCII file '" // trim(filename) // "': " // trim(error_msg)) ! Fall back to terminal output call log_info("Falling back to terminal output due to file error") call output_to_terminal(this) return end if call output_to_file(this, unit) close(unit) call log_info("Unicode plot saved to '" // trim(filename) // "'") end if end subroutine ascii_finalize subroutine output_to_terminal(this) class(ascii_context), intent(inout) :: this integer :: i, j ! Render text elements to canvas before output call render_text_elements_to_canvas(this%canvas, this%text_elements, & this%num_text_elements, & this%plot_width, this%plot_height) if (allocated(this%title_text)) then print '(A)', '' ! Empty line before title call print_centered_title(this%title_text, this%plot_width) end if print '(A)', '+' // repeat('-', this%plot_width) // '+' do i = 1, this%plot_height write(*, '(A)', advance='no') '|' do j = 1, this%plot_width write(*, '(A)', advance='no') this%canvas(i, j) end do print '(A)', '|' end do print '(A)', '+' // repeat('-', this%plot_width) // '+' ! Print xlabel below the plot if present if (allocated(this%xlabel_text)) then call print_centered_title(this%xlabel_text, this%plot_width) end if ! Print ylabel (simple horizontal placement for now) if (allocated(this%ylabel_text)) then print '(A)', this%ylabel_text end if end subroutine output_to_terminal subroutine output_to_file(this, unit) class(ascii_context), intent(inout) :: this integer, intent(in) :: unit integer :: i, j ! Render text elements to canvas before output call render_text_elements_to_canvas(this%canvas, this%text_elements, & this%num_text_elements, & this%plot_width, this%plot_height) if (allocated(this%title_text)) then write(unit, '(A)') '' ! Empty line before title call write_centered_title(unit, this%title_text, this%plot_width) end if write(unit, '(A)') '+' // repeat('-', this%plot_width) // '+' do i = 1, this%plot_height write(unit, '(A)', advance='no') '|' do j = 1, this%plot_width write(unit, '(A)', advance='no') this%canvas(i, j) end do write(unit, '(A)') '|' end do write(unit, '(A)') '+' // repeat('-', this%plot_width) // '+' ! Write xlabel below the plot if present if (allocated(this%xlabel_text)) then call write_centered_title(unit, this%xlabel_text, this%plot_width) end if ! Write ylabel to the left side of the plot if present if (allocated(this%ylabel_text)) then write(unit, '(A)') this%ylabel_text end if end subroutine output_to_file 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(this) result(output) !! Get the complete ASCII canvas as a string class(ascii_context), intent(in) :: this character(len=:), allocatable :: output character(len=1000) :: line_buffer integer :: i, j, total_len, line_len ! Calculate total length needed total_len = this%height * (this%width + 1) ! +1 for newline per row allocate(character(len=total_len) :: output) output = "" do i = 1, this%height line_buffer = "" do j = 1, this%width line_buffer(j:j) = this%canvas(i, j) end do line_len = len_trim(line_buffer(:this%width)) if (line_len == 0) line_len = 1 ! Ensure at least one character output = output // line_buffer(1:line_len) // new_line('a') end do end function ascii_get_output 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) integer :: px(4), py(4), i, j, min_x, max_x, min_y, max_y character(len=1) :: fill_char real(wp) :: color_intensity integer :: char_index ! Convert coordinates to ASCII canvas coordinates (matching line drawing algorithm) do i = 1, 4 ! Map to usable plot area (excluding 1-char border on each side) px(i) = int((x_quad(i) - this%x_min) / & (this%x_max - this%x_min) * real(this%plot_width - 3, wp)) + 2 py(i) = (this%plot_height - 1) - int((y_quad(i) - this%y_min) / & (this%y_max - this%y_min) * real(this%plot_height - 3, wp)) end do ! Calculate color intensity from RGB values (luminance formula) color_intensity = 0.299_wp * this%current_r + 0.587_wp * this%current_g + & 0.114_wp * this%current_b ! Map color intensity to ASCII character index with proper low-intensity handling if (color_intensity <= 0.001_wp) then char_index = 1 ! Space for zero intensity else ! Map 0.0-1.0 intensity to full character range 1-len(ASCII_CHARS) char_index = min(len(ASCII_CHARS), max(1, int(color_intensity * len(ASCII_CHARS)) + 1)) end if fill_char = ASCII_CHARS(char_index:char_index) ! Fill bounding rectangle with bounds checking min_x = max(2, min(minval(px), this%plot_width - 1)) max_x = max(2, min(maxval(px), this%plot_width - 1)) min_y = max(2, min(minval(py), this%plot_height - 1)) max_y = max(2, min(maxval(py), this%plot_height - 1)) do j = min_y, max_y do i = min_x, max_x ! Use density-aware character selection if (this%canvas(j, i) == ' ' .or. & get_char_density(fill_char) > get_char_density(this%canvas(j, i))) then this%canvas(j, i) = fill_char end if end do end do 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 call render_ascii_legend_specialized(legend, this, legend_x, legend_y) 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) use, intrinsic :: iso_fortran_env, only: real64 class(ascii_context), intent(in) :: this integer, intent(in) :: width, height real(real64), intent(out) :: rgb_data(width, height, 3) ! Suppress unused parameter warning if (this%width < 0) then ! This condition is never true, but suppresses unused parameter warning end if ! ASCII backend doesn't have RGB data for animation - fill with dummy data rgb_data = 0.0_real64 ! 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 ! Suppress unused parameter warnings if (this%width < 0 .or. width < 0 .or. height < 0) then ! This condition is never true, but suppresses unused parameter warnings end if ! 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(:) ! Suppress unused parameter warnings if (this%width < 0 .or. size(plots) < 0) then ! This condition is never true, but suppresses unused parameter warnings end if ! 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 ! Suppress unused parameter warnings if (this%width < 0 .or. len_trim(ylabel) < 0) then ! This condition is never true, but suppresses unused parameter warnings end if ! 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 use fortplot_axes, only: compute_scale_ticks, format_tick_label, MAX_TICKS 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 real(wp) :: x_tick_positions(MAX_TICKS), y_tick_positions(MAX_TICKS) integer :: num_x_ticks, num_y_ticks, i character(len=50) :: tick_label real(wp) :: tick_x, tick_y ! Suppress unused parameter warnings if (present(z_min) .and. present(z_max)) then if (z_min < -huge(z_min) .or. z_max > huge(z_max)) then ! This condition is never true, but suppresses unused parameter warnings end if end if if (.not. has_3d_plots) then ! Reference has_3d_plots to suppress warning end if ! ASCII backend: explicitly set title and draw simple axes if (present(title)) then if (allocated(title)) then call this%set_title(title) end if end if ! Draw axes call this%line(x_min, y_min, x_max, y_min) call this%line(x_min, y_min, x_min, y_max) ! Generate tick marks and labels for ASCII ! X-axis ticks (drawn as characters along bottom axis) call compute_scale_ticks(xscale, x_min, x_max, symlog_threshold, x_tick_positions, num_x_ticks) do i = 1, num_x_ticks tick_x = x_tick_positions(i) ! For ASCII, draw tick marks as characters in the text output tick_label = format_tick_label(tick_x, xscale) call this%text(tick_x, y_min - 0.05_wp * (y_max - y_min), trim(tick_label)) end do ! Y-axis ticks (drawn as characters along left axis) call compute_scale_ticks(yscale, y_min, y_max, symlog_threshold, y_tick_positions, num_y_ticks) do i = 1, num_y_ticks tick_y = y_tick_positions(i) tick_label = format_tick_label(tick_y, yscale) call this%text(x_min - 0.1_wp * (x_max - x_min), tick_y, trim(tick_label)) end do ! Store xlabel and ylabel for rendering outside the plot frame if (present(xlabel)) then if (allocated(xlabel)) then this%xlabel_text = xlabel end if end if if (present(ylabel)) then if (allocated(ylabel)) then this%ylabel_text = ylabel end if end if 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 y_min = this%y_min y_max = this%y_max 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%y_min = y_min this%y_max = y_max 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 ! Suppress unused parameter warnings if (this%width < 0) then ! This condition is never true, but suppresses unused parameter warning end if if (present(title_text) .and. present(xlabel_text) .and. present(ylabel_text)) then if (len_trim(title_text) < 0 .or. len_trim(xlabel_text) < 0 .or. len_trim(ylabel_text) < 0) then ! This condition is never true, but suppresses unused parameter warnings end if 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 end module fortplot_ascii