module fortplot_legend_drawing !! Legend drawing and rendering procedures !! !! Single Responsibility: Legend box drawing, entry rendering, and positioning use fortplot_ascii_mathtext, only: sanitize_ascii_text use fortplot_context, only: plot_context use fortplot_legend_layout, only: legend_box_t, calculate_legend_box use fortplot_legend_state, only: legend_t, legend_entry_t use fortplot_layout, only: plot_margins_t, plot_area_t, calculate_plot_area use fortplot_text, only: calculate_text_height, get_font_ascent_ratio use, intrinsic :: iso_fortran_env, only: wp => real64 implicit none private public :: render_ascii_legend, render_standard_legend, & calculate_legend_position, backend_is_ascii contains subroutine render_ascii_legend(legend, backend, legend_x, legend_y) !! Render compact ASCII legend with proper formatting type(legend_t), intent(in) :: legend class(plot_context), intent(inout) :: backend real(wp), intent(in) :: legend_x, legend_y integer :: i real(wp) :: text_x, text_y character(len=:), allocatable :: legend_line integer :: available_width character(len=256) :: sanitized_label integer :: sanitized_len do i = 1, legend%num_entries text_x = legend_x text_y = legend_y + real(i-1, wp) text_y = max(1.0_wp, min(text_y, real(max(2, backend%height - 2), wp))) call backend%color(legend%entries(i)%color(1), & legend%entries(i)%color(2), & legend%entries(i)%color(3)) call sanitize_ascii_text(legend%entries(i)%label, sanitized_label, sanitized_len) if (allocated(legend%entries(i)%marker) .and. & trim(legend%entries(i)%marker) /= '' .and. & trim(legend%entries(i)%marker) /= 'None') then legend_line = get_ascii_marker_char(legend%entries(i)%marker) // ' ' // & trim(sanitized_label(1:sanitized_len)) else legend_line = '-- ' // trim(sanitized_label(1:sanitized_len)) end if available_width = max(1, backend%width - int(text_x) + 1) if (len_trim(legend_line) > available_width) then legend_line = legend_line(1:available_width) end if call backend%text(text_x, text_y, legend_line) end do end subroutine render_ascii_legend subroutine render_standard_legend(legend, backend, legend_x, legend_y) !! Render standard legend for PNG/PDF backends with improved sizing type(legend_t), intent(in) :: legend class(plot_context), intent(inout) :: backend real(wp), intent(in) :: legend_x, legend_y type(legend_box_t) :: box character(len=:), allocatable :: labels(:) real(wp) :: data_width, data_height call initialize_legend_rendering(legend, backend, box, labels, data_width, data_height) call draw_legend_frame(backend, legend_x, legend_y, box) call render_legend_entries(legend, backend, legend_x, legend_y, box) end subroutine render_standard_legend subroutine initialize_legend_rendering(legend, backend, box, labels, data_width, data_height) !! Initialize legend rendering components type(legend_t), intent(in) :: legend class(plot_context), intent(in) :: backend type(legend_box_t), intent(out) :: box character(len=:), allocatable, intent(out) :: labels(:) real(wp), intent(out) :: data_width, data_height integer :: i type(plot_margins_t) :: margins type(plot_area_t) :: plot_area integer :: px_w, px_h allocate(character(len=256) :: labels(legend%num_entries)) do i = 1, legend%num_entries labels(i) = legend%entries(i)%label end do data_width = backend%x_max - backend%x_min data_height = backend%y_max - backend%y_min call calculate_plot_area(backend%width, backend%height, margins, plot_area) px_w = max(1, plot_area%width) px_h = max(1, plot_area%height) box = calculate_legend_box(labels, data_width, data_height, & legend%num_entries, legend%position, px_w, px_h) end subroutine initialize_legend_rendering subroutine draw_legend_frame(backend, legend_x, legend_y, box) !! Draw legend background box and border class(plot_context), intent(inout) :: backend real(wp), intent(in) :: legend_x, legend_y type(legend_box_t), intent(in) :: box real(wp) :: box_x1, box_y1, box_x2, box_y2 box_x1 = legend_x box_y1 = legend_y box_x2 = box_x1 + box%width box_y2 = box_y1 - box%height call backend%color(1.0_wp, 1.0_wp, 1.0_wp) call draw_legend_box(backend, box_x1, box_y1, box_x2, box_y2) call backend%color(0.0_wp, 0.0_wp, 0.0_wp) call draw_legend_border(backend, box_x1, box_y1, box_x2, box_y2) end subroutine draw_legend_frame subroutine render_legend_entries(legend, backend, legend_x, legend_y, box) !! Render all legend entries (lines, markers, text) type(legend_t), intent(in) :: legend class(plot_context), intent(inout) :: backend real(wp), intent(in) :: legend_x, legend_y type(legend_box_t), intent(in) :: box real(wp) :: ascent_ratio, line_x1, line_x2, line_center_y, text_x, text_baseline integer :: i ascent_ratio = get_font_ascent_ratio() do i = 1, legend%num_entries call calculate_entry_positions(legend_x, legend_y, box, ascent_ratio, i, & line_x1, line_x2, line_center_y, text_x, & text_baseline) call render_legend_line(legend%entries(i), backend, line_x1, line_x2, & line_center_y) call render_legend_marker(legend%entries(i), backend, line_x1, line_x2, & line_center_y) call render_legend_text(legend%entries(i), backend, text_x, text_baseline) end do end subroutine render_legend_entries subroutine calculate_entry_positions(legend_x, legend_y, box, ascent_ratio, entry_idx, & line_x1, line_x2, line_center_y, text_x, & text_baseline) !! Calculate positions for legend entry components real(wp), intent(in) :: legend_x, legend_y, ascent_ratio type(legend_box_t), intent(in) :: box integer, intent(in) :: entry_idx real(wp), intent(out) :: line_x1, line_x2, line_center_y, text_x, text_baseline real(wp) :: entry_stride, entry_top_y, entry_baseline, entry_offset line_x1 = legend_x + box%padding_x line_x2 = line_x1 + box%line_length entry_stride = box%entry_height + box%entry_spacing entry_top_y = legend_y - box%padding - real(entry_idx - 1, wp) * entry_stride entry_baseline = entry_top_y - ascent_ratio * box%entry_height entry_offset = (ascent_ratio - 0.5_wp) * box%entry_height line_center_y = entry_baseline + entry_offset text_x = line_x2 + box%text_spacing text_baseline = entry_baseline end subroutine calculate_entry_positions subroutine render_legend_line(entry, backend, line_x1, line_x2, line_center_y) !! Render legend line for entry type(legend_entry_t), intent(in) :: entry class(plot_context), intent(inout) :: backend real(wp), intent(in) :: line_x1, line_x2, line_center_y call backend%color(entry%color(1), entry%color(2), entry%color(3)) if (allocated(entry%linestyle)) then if (trim(entry%linestyle) /= 'None' .and. trim(entry%linestyle) /= 'none') then call backend%set_line_style(entry%linestyle) call backend%line(line_x1, line_center_y, line_x2, line_center_y) end if else call backend%set_line_style('-') call backend%line(line_x1, line_center_y, line_x2, line_center_y) end if end subroutine render_legend_line subroutine render_legend_marker(entry, backend, line_x1, line_x2, line_center_y) !! Render legend marker for entry type(legend_entry_t), intent(in) :: entry class(plot_context), intent(inout) :: backend real(wp), intent(in) :: line_x1, line_x2, line_center_y if (allocated(entry%marker)) then if (trim(entry%marker) /= 'None' .and. trim(entry%marker) /= 'none' .and. & len_trim(entry%marker) > 0) then call backend%set_marker_colors(entry%color(1), entry%color(2), entry%color(3), & entry%color(1), entry%color(2), entry%color(3)) call backend%draw_marker((line_x1 + line_x2) / 2.0_wp, & line_center_y, entry%marker) end if end if end subroutine render_legend_marker subroutine render_legend_text(entry, backend, text_x, text_y) !! Render legend text for entry type(legend_entry_t), intent(in) :: entry class(plot_context), intent(inout) :: backend real(wp), intent(in) :: text_x, text_y call backend%color(0.0_wp, 0.0_wp, 0.0_wp) call backend%text(text_x, text_y, entry%label) end subroutine render_legend_text subroutine calculate_legend_position(legend, backend, x, y) !! Calculate legend position based on backend and position setting type(legend_t), intent(in) :: legend class(plot_context), intent(in) :: backend real(wp), intent(out) :: x, y real(wp) :: data_width, data_height type(legend_box_t) :: box character(len=:), allocatable :: labels(:) integer :: i type(plot_margins_t) :: margins type(plot_area_t) :: plot_area integer :: px_w, px_h logical :: ascii_mode integer :: screen_width, screen_height integer :: margin_x, margin_y integer :: longest_entry, entry_len, prefix_len integer :: ascii_x, ascii_y, total_lines data_width = backend%x_max - backend%x_min data_height = backend%y_max - backend%y_min ascii_mode = backend_is_ascii(backend) if (ascii_mode) then screen_width = max(1, backend%width) screen_height = max(1, backend%height) margin_x = 3 margin_y = 0 longest_entry = 0 do i = 1, legend%num_entries entry_len = len_trim(legend%entries(i)%label) prefix_len = 3 if (allocated(legend%entries(i)%marker)) then if (len_trim(legend%entries(i)%marker) > 0 .and. & trim(legend%entries(i)%marker) /= 'None') then prefix_len = 2 end if end if longest_entry = max(longest_entry, prefix_len + entry_len) end do if (longest_entry == 0) longest_entry = 1 longest_entry = min(longest_entry, max(1, screen_width - margin_x)) total_lines = max(legend%num_entries, 1) select case (legend%position) case (1) ascii_x = margin_x ascii_y = margin_y + 1 case (2) ascii_x = max(margin_x, screen_width - longest_entry - margin_x + 1) ascii_y = margin_y + 1 case (3) ascii_x = margin_x ascii_y = max(margin_y + 1, screen_height - total_lines - margin_y + 2) case (4) ascii_x = max(margin_x, screen_width - longest_entry - margin_x + 1) ascii_y = max(margin_y + 1, screen_height - total_lines - margin_y + 2) case (5) ascii_x = max(margin_x, screen_width - longest_entry - margin_x + 1) ascii_y = (screen_height - total_lines)*0.5_wp + 1 case default ascii_x = max(margin_x, screen_width - longest_entry - margin_x + 1) ascii_y = margin_y + 1 end select if (legend%position == 1 .or. legend%position == 2) then ascii_y = max(ascii_y, margin_y + 4) end if if (legend%num_entries > 0) then if (ascii_y + legend%num_entries - 1 > screen_height - margin_y) then ascii_y = max(2, screen_height - legend%num_entries - margin_y) end if end if ascii_x = max(2, min(ascii_x, max(2, screen_width - 1))) ascii_y = max(1, min(ascii_y, max(2, screen_height - 1))) x = real(ascii_x, wp) y = real(ascii_y, wp) else allocate(character(len=256) :: labels(legend%num_entries)) do i = 1, legend%num_entries labels(i) = legend%entries(i)%label end do call calculate_plot_area(backend%width, backend%height, margins, plot_area) px_w = max(1, plot_area%width) px_h = max(1, plot_area%height) box = calculate_legend_box(labels, data_width, data_height, & legend%num_entries, legend%position, px_w, px_h) x = backend%x_min + box%x y = backend%y_min + box%y end if end subroutine calculate_legend_position subroutine draw_legend_box(backend, x1, y1, x2, y2) !! Draw filled white rectangle for legend background class(plot_context), intent(inout) :: backend real(wp), intent(in) :: x1, y1, x2, y2 real(wp) :: x_quad(4), y_quad(4) call backend%set_line_style('-') call backend%color(1.0_wp, 1.0_wp, 1.0_wp) x_quad = [x1, x2, x2, x1] y_quad = [y1, y1, y2, y2] call backend%fill_quad(x_quad, y_quad) end subroutine draw_legend_box subroutine draw_legend_border(backend, x1, y1, x2, y2) !! Draw thin border around legend box matching axes frame style class(plot_context), intent(inout) :: backend real(wp), intent(in) :: x1, y1, x2, y2 if (backend%width > 80 .or. backend%height > 24) then call backend%set_line_width(0.5_wp) end if call backend%set_line_style('-') call backend%color(0.0_wp, 0.0_wp, 0.0_wp) call backend%line(x1, y1, x2, y1) call backend%line(x2, y1, x2, y2) call backend%line(x2, y2, x1, y2) call backend%line(x1, y2, x1, y1) end subroutine draw_legend_border pure function get_ascii_marker_char(marker_style) result(marker_char) !! Convert marker style to ASCII character character(len=*), intent(in) :: marker_style character(len=1) :: marker_char select case (trim(marker_style)) case ('o') marker_char = 'o' case ('s') marker_char = '#' case ('D', 'd') marker_char = '%' case ('x') marker_char = 'x' case ('+') marker_char = '+' case ('*') marker_char = '*' case ('^') marker_char = '^' case ('v') marker_char = 'v' case ('<') marker_char = '<' case ('>') marker_char = '>' case ('p') marker_char = 'P' case ('h', 'H') marker_char = 'H' case ('-') marker_char = '-' case ('=') marker_char = '=' case ('%') marker_char = '%' case ('@') marker_char = '@' case ('#') marker_char = '#' case default marker_char = '*' end select end function get_ascii_marker_char pure logical function backend_is_ascii(backend) !! Detect whether backend is operating in ASCII mode based on canvas size class(plot_context), intent(in) :: backend backend_is_ascii = backend%width <= 120 .and. backend%height <= 60 end function backend_is_ascii end module fortplot_legend_drawing