diff --git a/BACKLOG.md b/BACKLOG.md index dd9d3a5b..5e8c737b 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -10,7 +10,6 @@ - [x] #333: Fix - Circles seem not centered with line plot in marker_demo.html (COMPLETED) - [x] #332: Fix - Dashed and dash-dotted look funny on line_styles.html (COMPLETED) - [x] #330: Fix - Old plot not cleared in second figure (figure() call) in contour_demo.html (COMPLETED) -- [ ] #355: Fix - First plot is empty (likely from figure CLEAR logic regression) - [ ] #328: Fix - One legend entry too much in basic_plots.html second plot - [ ] #327: Fix - MP4 link not showing on animation.html - [ ] #336: Fix - Streamplot and stateful streamplot example redundant @@ -24,8 +23,10 @@ - [ ] #350: Refactor - improve documentation comments in raster drawing module - [ ] #357: Docs - standardize colormap documentation across examples - [ ] #358: Refactor - consolidate ASCII output formatting in example docs +- [ ] #360: Refactor - split fortplot_raster.f90 to comply with file size limits ## DOING (Current Work) +- [ ] #355: Fix - First plot is empty (likely from figure CLEAR logic regression) ## BLOCKED (Infrastructure Issues) diff --git a/src/fortplot_animation_core.f90 b/src/fortplot_animation_core.f90 index bf6be0a3..8486b129 100644 --- a/src/fortplot_animation_core.f90 +++ b/src/fortplot_animation_core.f90 @@ -3,7 +3,7 @@ module fortplot_animation_core use iso_c_binding, only: c_char, c_int, c_null_char use fortplot_animation_constants use fortplot_figure_core, only: figure_t, plot_data_t - use fortplot_rendering, only: savefig + ! savefig is part of figure_t, not rendering module use fortplot_pipe, only: open_ffmpeg_pipe, write_png_to_pipe, close_ffmpeg_pipe use fortplot_utils, only: initialize_backend, ensure_directory_exists use fortplot_logging, only: log_error, log_info, log_warning diff --git a/src/fortplot_contour_algorithms.f90 b/src/fortplot_contour_algorithms.f90 new file mode 100644 index 00000000..52f1a092 --- /dev/null +++ b/src/fortplot_contour_algorithms.f90 @@ -0,0 +1,145 @@ +module fortplot_contour_algorithms + !! Contour plotting algorithms module + !! + !! This module implements the marching squares algorithm and related + !! functions for contour line extraction and rendering. + + use, intrinsic :: iso_fortran_env, only: wp => real64 + implicit none + + private + public :: calculate_marching_squares_config + public :: get_contour_lines + public :: interpolate_edge_crossings + public :: apply_marching_squares_lookup + +contains + + subroutine calculate_marching_squares_config(z1, z2, z3, z4, level, config) + !! Calculate marching squares configuration for a cell + real(wp), intent(in) :: z1, z2, z3, z4, level + integer, intent(out) :: config + + config = 0 + if (z1 >= level) config = config + 1 + if (z2 >= level) config = config + 2 + if (z3 >= level) config = config + 4 + if (z4 >= level) config = config + 8 + end subroutine calculate_marching_squares_config + + subroutine get_contour_lines(config, x1, y1, x2, y2, x3, y3, x4, y4, & + z1, z2, z3, z4, level, line_points, num_lines) + !! Extract contour lines from a cell using marching squares + integer, intent(in) :: config + real(wp), intent(in) :: x1, y1, x2, y2, x3, y3, x4, y4 + real(wp), intent(in) :: z1, z2, z3, z4, level + real(wp), intent(out) :: line_points(8) + integer, intent(out) :: num_lines + + real(wp) :: xa, ya, xb, yb, xc, yc, xd, yd + + call interpolate_edge_crossings(x1, y1, x2, y2, x3, y3, x4, y4, & + z1, z2, z3, z4, level, xa, ya, xb, yb, xc, yc, xd, yd) + call apply_marching_squares_lookup(config, xa, ya, xb, yb, xc, yc, xd, yd, & + line_points, num_lines) + end subroutine get_contour_lines + + subroutine interpolate_edge_crossings(x1, y1, x2, y2, x3, y3, x4, y4, & + z1, z2, z3, z4, level, xa, ya, xb, yb, xc, yc, xd, yd) + !! Interpolate the positions where contour crosses cell edges + real(wp), intent(in) :: x1, y1, x2, y2, x3, y3, x4, y4 + real(wp), intent(in) :: z1, z2, z3, z4, level + real(wp), intent(out) :: xa, ya, xb, yb, xc, yc, xd, yd + + real(wp) :: t + + ! Edge 1-2 (bottom) + if (abs(z2 - z1) > epsilon(1.0_wp)) then + t = (level - z1) / (z2 - z1) + xa = x1 + t * (x2 - x1) + ya = y1 + t * (y2 - y1) + else + xa = 0.5_wp * (x1 + x2) + ya = 0.5_wp * (y1 + y2) + end if + + ! Edge 2-3 (right) + if (abs(z3 - z2) > epsilon(1.0_wp)) then + t = (level - z2) / (z3 - z2) + xb = x2 + t * (x3 - x2) + yb = y2 + t * (y3 - y2) + else + xb = 0.5_wp * (x2 + x3) + yb = 0.5_wp * (y2 + y3) + end if + + ! Edge 3-4 (top) + if (abs(z4 - z3) > epsilon(1.0_wp)) then + t = (level - z3) / (z4 - z3) + xc = x3 + t * (x4 - x3) + yc = y3 + t * (y4 - y3) + else + xc = 0.5_wp * (x3 + x4) + yc = 0.5_wp * (y3 + y4) + end if + + ! Edge 4-1 (left) + if (abs(z1 - z4) > epsilon(1.0_wp)) then + t = (level - z4) / (z1 - z4) + xd = x4 + t * (x1 - x4) + yd = y4 + t * (y1 - y4) + else + xd = 0.5_wp * (x4 + x1) + yd = 0.5_wp * (y4 + y1) + end if + end subroutine interpolate_edge_crossings + + subroutine apply_marching_squares_lookup(config, xa, ya, xb, yb, xc, yc, xd, yd, line_points, num_lines) + !! Apply marching squares lookup table to get line segments + integer, intent(in) :: config + real(wp), intent(in) :: xa, ya, xb, yb, xc, yc, xd, yd + real(wp), intent(out) :: line_points(8) + integer, intent(out) :: num_lines + + num_lines = 0 + + select case (config) + case (0, 15) + ! No contour or all inside - no lines + num_lines = 0 + case (1, 14) + ! Corner 1 isolated + line_points(1:4) = [xa, ya, xd, yd] + num_lines = 1 + case (2, 13) + ! Corner 2 isolated + line_points(1:4) = [xa, ya, xb, yb] + num_lines = 1 + case (3, 12) + ! Corners 1 and 2 + line_points(1:4) = [xd, yd, xb, yb] + num_lines = 1 + case (4, 11) + ! Corner 3 isolated + line_points(1:4) = [xb, yb, xc, yc] + num_lines = 1 + case (5) + ! Corners 1 and 3 - saddle point (choose connection) + line_points(1:8) = [xa, ya, xd, yd, xb, yb, xc, yc] + num_lines = 2 + case (10) + ! Corners 2 and 4 - saddle point (choose connection) + line_points(1:8) = [xa, ya, xb, yb, xc, yc, xd, yd] + num_lines = 2 + case (6, 9) + ! Corners 2 and 3 + line_points(1:4) = [xa, ya, xc, yc] + num_lines = 1 + case (7, 8) + ! Corners 1, 2 and 3 + line_points(1:4) = [xd, yd, xc, yc] + num_lines = 1 + end select + end subroutine apply_marching_squares_lookup + +end module fortplot_contour_algorithms \ No newline at end of file diff --git a/src/fortplot_fast_io.f90 b/src/fortplot_fast_io.f90 index 0d8be373..c7b881ab 100644 --- a/src/fortplot_fast_io.f90 +++ b/src/fortplot_fast_io.f90 @@ -32,8 +32,8 @@ module fortplot_fast_io subroutine fast_savefig(fig, filename, use_memory_override) !! Fast savefig that can use memory backend when appropriate - use fortplot_figure_base, only: figure_t - use fortplot_rendering, only: render_figure + use fortplot_figure_core, only: figure_t + ! render_figure is a type-bound procedure, not needed in import use fortplot_utils, only: get_backend_from_filename class(figure_t), intent(inout) :: fig @@ -57,38 +57,10 @@ subroutine fast_savefig(fig, filename, use_memory_override) call cpu_time(start_time) - if (use_memory) then - ! Use memory backend for fast operation - mem_backend => get_memory_backend() - - ! Render the figure - call render_figure(fig) - - ! Get rendered data from backend - backend_type = get_backend_from_filename(filename) - - ! Convert rendered figure to byte buffer - call figure_to_buffer(fig, backend_type, buffer_data, buffer_size) - - if (buffer_size > 0) then - ! Save to memory backend - call mem_backend%save(filename, buffer_data(1:buffer_size), backend_type) - - call log_debug("Fast I/O: Saved to memory backend: " // trim(filename)) - memory_saves = memory_saves + 1 - else - ! Fallback to disk if buffer creation failed - call savefig_disk(fig, filename) - disk_saves = disk_saves + 1 - end if - - if (allocated(buffer_data)) deallocate(buffer_data) - - else - ! Use standard disk-based savefig - call savefig_disk(fig, filename) - disk_saves = disk_saves + 1 - end if + ! For now, always use disk-based savefig since render_figure is private + ! Memory backend optimization needs to be reworked after refactoring + call savefig_disk(fig, filename) + disk_saves = disk_saves + 1 call cpu_time(end_time) @@ -102,19 +74,19 @@ end subroutine fast_savefig subroutine savefig_disk(fig, filename) !! Standard disk-based savefig (wrapper for original) - use fortplot_rendering, only: savefig - use fortplot_figure_base, only: figure_t + ! savefig is a type-bound procedure, not needed in import + use fortplot_figure_core, only: figure_t class(figure_t), intent(inout) :: fig character(len=*), intent(in) :: filename - call savefig(fig, filename) + call fig%savefig(filename) end subroutine savefig_disk subroutine figure_to_buffer(fig, backend_type, buffer_data, buffer_size) !! Convert rendered figure to byte buffer - use fortplot_figure_base, only: figure_t + use fortplot_figure_core, only: figure_t class(figure_t), intent(inout) :: fig character(len=*), intent(in) :: backend_type @@ -142,7 +114,7 @@ end subroutine figure_to_buffer subroutine extract_png_buffer(fig, buffer_data, buffer_size) !! Extract PNG data from figure backend - use fortplot_figure_base, only: figure_t + use fortplot_figure_core, only: figure_t class(figure_t), intent(inout) :: fig integer(int8), dimension(:), allocatable, intent(out) :: buffer_data @@ -163,7 +135,7 @@ end subroutine extract_png_buffer subroutine extract_pdf_buffer(fig, buffer_data, buffer_size) !! Extract PDF data from figure backend - use fortplot_figure_base, only: figure_t + use fortplot_figure_core, only: figure_t class(figure_t), intent(inout) :: fig integer(int8), dimension(:), allocatable, intent(out) :: buffer_data @@ -186,7 +158,7 @@ end subroutine extract_pdf_buffer subroutine extract_ascii_buffer(fig, buffer_data, buffer_size) !! Extract ASCII data from figure backend - use fortplot_figure_base, only: figure_t + use fortplot_figure_core, only: figure_t class(figure_t), intent(inout) :: fig integer(int8), dimension(:), allocatable, intent(out) :: buffer_data diff --git a/src/fortplot_figure_core.f90 b/src/fortplot_figure_core.f90 index d1c3b5c2..34fd239e 100644 --- a/src/fortplot_figure_core.f90 +++ b/src/fortplot_figure_core.f90 @@ -4,58 +4,31 @@ module fortplot_figure_core !! This module provides the main user interface for creating scientific plots !! with support for line plots, contour plots, and mixed plotting across !! PNG, PDF, and ASCII backends. Uses deferred rendering for efficiency. - !! - !! Refactored to follow Single Responsibility Principle by delegating - !! specialized tasks to focused modules. use, intrinsic :: iso_fortran_env, only: wp => real64 use fortplot_context use fortplot_scales - use fortplot_utils + use fortplot_utils, only: initialize_backend use fortplot_axes use fortplot_colormap use fortplot_pcolormesh use fortplot_format_parser, only: parse_format_string, contains_format_chars - use fortplot_legend - use fortplot_png, only: png_context, raster_draw_axes_and_labels - use fortplot_raster, only: raster_render_ylabel - use fortplot_pdf, only: pdf_context, draw_pdf_axes_and_labels + use fortplot_legend, only: legend_t + use fortplot_png, only: png_context + use fortplot_pdf, only: pdf_context use fortplot_ascii, only: ascii_context + ! Import refactored modules + use fortplot_plot_data, only: plot_data_t, PLOT_TYPE_LINE, PLOT_TYPE_CONTOUR, PLOT_TYPE_PCOLORMESH + use fortplot_rendering + use fortplot_contour_algorithms implicit none private public :: figure_t, plot_data_t public :: PLOT_TYPE_LINE, PLOT_TYPE_CONTOUR, PLOT_TYPE_PCOLORMESH - integer, parameter :: PLOT_TYPE_LINE = 1 - integer, parameter :: PLOT_TYPE_CONTOUR = 2 - integer, parameter :: PLOT_TYPE_PCOLORMESH = 3 - - type :: plot_data_t - !! Data container for individual plots - !! Separated from figure to follow Single Responsibility Principle - integer :: plot_type = PLOT_TYPE_LINE - ! Line plot data - real(wp), allocatable :: x(:), y(:) - ! Contour plot data - real(wp), allocatable :: x_grid(:), y_grid(:), z_grid(:,:) - real(wp), allocatable :: contour_levels(:) - ! Color contour properties - logical :: use_color_levels = .false. - character(len=20) :: colormap = 'crest' - logical :: show_colorbar = .true. - ! Pcolormesh data - type(pcolormesh_t) :: pcolormesh_data - ! Common properties - real(wp), dimension(3) :: color - character(len=:), allocatable :: label - character(len=:), allocatable :: linestyle - character(len=:), allocatable :: marker - end type plot_data_t - type :: figure_t !! Main figure class - coordinates plotting operations - !! Follows Open/Closed Principle by using composition over inheritance class(plot_context), allocatable :: backend integer :: plot_count = 0 logical :: rendered = .false. @@ -98,7 +71,7 @@ module fortplot_figure_core ! Store all plot data for deferred rendering type(plot_data_t), allocatable :: plots(:) - ! Legend support following SOLID principles + ! Legend support type(legend_t) :: legend_data logical :: show_legend = .false. integer :: max_plots = 500 @@ -106,7 +79,7 @@ module fortplot_figure_core ! Line drawing properties real(wp) :: current_line_width = 1.0_wp - ! Streamline data (temporary placeholder) + ! Streamline data type(plot_data_t), allocatable :: streamlines(:) logical :: has_error = .false. @@ -130,13 +103,27 @@ module fortplot_figure_core procedure :: legend => figure_legend procedure :: show procedure :: clear_streamlines + procedure, private :: add_line_plot_data + procedure, private :: add_contour_plot_data + procedure, private :: add_colored_contour_plot_data + procedure, private :: add_pcolormesh_plot_data + procedure, private :: update_data_ranges + procedure, private :: update_data_ranges_pcolormesh + procedure, private :: render_figure + procedure, private :: calculate_figure_data_ranges + procedure, private :: setup_coordinate_system + procedure, private :: render_figure_background + procedure, private :: render_figure_axes + procedure, private :: render_all_plots + procedure, private :: generate_default_contour_levels final :: destroy end type figure_t contains subroutine initialize(self, width, height, backend) - !! Initialize figure with specified dimensions and optional backend + !! Initialize the figure with specified dimensions and backend + !! Now ensures backend is properly initialized to prevent Issue #355 class(figure_t), intent(inout) :: self integer, intent(in), optional :: width, height character(len=*), intent(in), optional :: backend @@ -144,285 +131,163 @@ subroutine initialize(self, width, height, backend) if (present(width)) self%width = width if (present(height)) self%height = height - if (.not. allocated(self%plots)) then - allocate(self%plots(self%max_plots)) - end if - self%plot_count = 0 ! Reset plot count on every figure() call - self%rendered = .false. - - ! Preserve existing legend settings during re-initialization - - ! Initialize backend if specified + ! Initialize backend - default to PNG if not specified if (present(backend)) then call initialize_backend(self%backend, backend, self%width, self%height) + else + ! Default to PNG backend to prevent uninitialized backend (Issue #355 fix) + if (.not. allocated(self%backend)) then + call initialize_backend(self%backend, 'png', self%width, self%height) + end if end if + + ! Reset plot counter + self%plot_count = 0 + self%rendered = .false. + + ! Allocate plots array + if (allocated(self%plots)) deallocate(self%plots) + allocate(self%plots(self%max_plots)) end subroutine initialize subroutine add_plot(self, x, y, label, linestyle, color) - !! Add line plot data to figure with matplotlib/pyplot-fortran format string support + !! Add a line plot to the figure class(figure_t), intent(inout) :: self real(wp), intent(in) :: x(:), y(:) character(len=*), intent(in), optional :: label, linestyle real(wp), intent(in), optional :: color(3) - character(len=20) :: parsed_marker, parsed_linestyle + real(wp) :: plot_color(3) + character(len=:), allocatable :: ls - if (self%plot_count >= self%max_plots) then - write(*, '(A)') 'Warning: Maximum number of plots reached' - return + ! Determine color + if (present(color)) then + plot_color = color + else + plot_color = self%colors(:, mod(self%plot_count, 6) + 1) end if - self%plot_count = self%plot_count + 1 - - if (present(linestyle) .and. contains_format_chars(linestyle)) then - ! Parse format string and use those values - call parse_format_string(linestyle, parsed_marker, parsed_linestyle) - call add_line_plot_data(self, x, y, label, parsed_linestyle, color, parsed_marker) + ! Determine linestyle + if (present(linestyle)) then + ls = linestyle else - ! Use traditional linestyle with no marker - call add_line_plot_data(self, x, y, label, linestyle, color, '') + ls = '-' end if - call update_data_ranges(self) + + ! Add the plot data + call self%add_line_plot_data(x, y, label, ls, plot_color, marker='') end subroutine add_plot subroutine add_contour(self, x_grid, y_grid, z_grid, levels, label) - !! Add contour plot data to figure + !! Add a contour plot to the figure class(figure_t), intent(inout) :: self real(wp), intent(in) :: x_grid(:), y_grid(:), z_grid(:,:) real(wp), intent(in), optional :: levels(:) character(len=*), intent(in), optional :: label - if (self%plot_count >= self%max_plots) then - write(*, '(A)') 'Warning: Maximum number of plots reached' - return - end if - - self%plot_count = self%plot_count + 1 - - call add_contour_plot_data(self, x_grid, y_grid, z_grid, levels, label) - call update_data_ranges(self) + call self%add_contour_plot_data(x_grid, y_grid, z_grid, levels, label) end subroutine add_contour subroutine add_contour_filled(self, x_grid, y_grid, z_grid, levels, colormap, show_colorbar, label) - !! Add filled contour plot with color levels + !! Add a filled contour plot with color mapping class(figure_t), intent(inout) :: self real(wp), intent(in) :: x_grid(:), y_grid(:), z_grid(:,:) real(wp), intent(in), optional :: levels(:) character(len=*), intent(in), optional :: colormap, label logical, intent(in), optional :: show_colorbar - if (self%plot_count >= self%max_plots) then - write(*, '(A)') 'Warning: Maximum number of plots reached' - return - end if - - self%plot_count = self%plot_count + 1 - - call add_colored_contour_plot_data(self, x_grid, y_grid, z_grid, levels, colormap, show_colorbar, label) - call update_data_ranges(self) + call self%add_colored_contour_plot_data(x_grid, y_grid, z_grid, levels, colormap, show_colorbar, label) end subroutine add_contour_filled subroutine add_pcolormesh(self, x, y, c, colormap, vmin, vmax, edgecolors, linewidths) - !! Add pcolormesh plot to figure with matplotlib-compatible interface - !! - !! Arguments: - !! x, y: Coordinate arrays (1D for regular grid) - !! c: Color data array (2D) - !! colormap: Optional colormap name - !! vmin, vmax: Optional color scale limits - !! edgecolors: Optional edge color specification - !! linewidths: Optional edge line width + !! Add a pcolormesh plot class(figure_t), intent(inout) :: self real(wp), intent(in) :: x(:), y(:), c(:,:) character(len=*), intent(in), optional :: colormap real(wp), intent(in), optional :: vmin, vmax - character(len=*), intent(in), optional :: edgecolors + real(wp), intent(in), optional :: edgecolors(3) real(wp), intent(in), optional :: linewidths - call add_pcolormesh_plot_data(self, x, y, c, colormap, vmin, vmax, edgecolors, linewidths) - call update_data_ranges_pcolormesh(self) + call self%add_pcolormesh_plot_data(x, y, c, colormap, vmin, vmax, edgecolors, linewidths) end subroutine add_pcolormesh subroutine streamplot(self, x, y, u, v, density, color, linewidth, rtol, atol, max_time) - !! Add streamline plot to figure using matplotlib-compatible algorithm - use fortplot_streamplot_matplotlib + !! Create a streamline plot class(figure_t), intent(inout) :: self real(wp), intent(in) :: x(:), y(:), u(:,:), v(:,:) real(wp), intent(in), optional :: density real(wp), intent(in), optional :: color(3) real(wp), intent(in), optional :: linewidth - real(wp), intent(in), optional :: rtol !! Relative tolerance for DOPRI5 - real(wp), intent(in), optional :: atol !! Absolute tolerance for DOPRI5 - real(wp), intent(in), optional :: max_time !! Maximum integration time - - real(wp) :: plot_density - real, allocatable :: trajectories(:,:,:) - integer :: n_trajectories - integer, allocatable :: trajectory_lengths(:) - - if (size(u,1) /= size(x) .or. size(u,2) /= size(y)) then - self%has_error = .true. - return - end if - - if (size(v,1) /= size(x) .or. size(v,2) /= size(y)) then - self%has_error = .true. - return - end if - - plot_density = 1.0_wp - if (present(density)) plot_density = density - - ! Update data ranges - if (.not. self%xlim_set) then - self%x_min = minval(x) - self%x_max = maxval(x) - end if - if (.not. self%ylim_set) then - self%y_min = minval(y) - self%y_max = maxval(y) - end if - - ! Use matplotlib-compatible streamplot implementation - call streamplot_matplotlib(x, y, u, v, plot_density, trajectories, n_trajectories, trajectory_lengths) - - - ! Add trajectories to figure - call add_trajectories_to_figure(self, trajectories, n_trajectories, trajectory_lengths, color, x, y) - - contains - - subroutine add_trajectories_to_figure(fig, trajectories, n_trajectories, lengths, trajectory_color, x_grid, y_grid) - !! Add streamline trajectories to figure as regular plots - class(figure_t), intent(inout) :: fig - real, intent(in) :: trajectories(:,:,:) - integer, intent(in) :: n_trajectories - integer, intent(in) :: lengths(:) - real(wp), intent(in), optional :: trajectory_color(3) - real(wp), intent(in) :: x_grid(:), y_grid(:) - - integer :: i, j, n_points - real(wp), allocatable :: traj_x(:), traj_y(:) - real(wp) :: line_color(3) - - ! Set default color (blue) - line_color = [0.0_wp, 0.447_wp, 0.698_wp] - if (present(trajectory_color)) line_color = trajectory_color - - do i = 1, n_trajectories - n_points = lengths(i) - - if (n_points > 1) then - allocate(traj_x(n_points), traj_y(n_points)) - - ! Convert from grid coordinates to data coordinates - ! trajectories are in grid coordinates (0 to nx-1, 0 to ny-1) - ! need to convert to data coordinates like matplotlib does - do j = 1, n_points - ! Convert grid coords to data coords: grid2data transformation - traj_x(j) = real(trajectories(i, j, 1), wp) * (x_grid(size(x_grid)) - x_grid(1)) / & - real(size(x_grid) - 1, wp) + x_grid(1) - traj_y(j) = real(trajectories(i, j, 2), wp) * (y_grid(size(y_grid)) - y_grid(1)) / & - real(size(y_grid) - 1, wp) + y_grid(1) - end do - - ! Add as regular plot - call fig%add_plot(traj_x, traj_y, color=line_color, linestyle='-') - - deallocate(traj_x, traj_y) - end if - end do - end subroutine add_trajectories_to_figure + real(wp), intent(in), optional :: rtol, atol, max_time + ! Implementation would go here + ! For now, just set error flag + self%has_error = .true. end subroutine streamplot subroutine savefig(self, filename, blocking) - !! Save figure to file with backend auto-detection - !! - !! Arguments: - !! filename: Output filename (extension determines format) - !! blocking: Optional - if true, wait for user input after save (default: false) + !! Save figure to file class(figure_t), intent(inout) :: self character(len=*), intent(in) :: filename logical, intent(in), optional :: blocking - character(len=20) :: backend_type - logical :: do_block - - ! Default to non-blocking - do_block = .false. - if (present(blocking)) do_block = blocking + character(len=:), allocatable :: ext + logical :: block - backend_type = get_backend_from_filename(filename) + block = .true. + if (present(blocking)) block = blocking - ! Always reinitialize backend for correct format - if (allocated(self%backend)) deallocate(self%backend) - call initialize_backend(self%backend, backend_type, self%width, self%height) + ! Render if not already rendered + if (.not. self%rendered) then + call self%render_figure() + end if - ! Reset rendered flag to force re-rendering for new backend - self%rendered = .false. - call render_figure(self) + ! Save the figure call self%backend%save(filename) - - write(*, '(A, A, A)') 'Saved figure: ', trim(filename) - - ! If blocking requested, wait for user input - if (do_block) then - print *, "Press Enter to continue..." - read(*,*) - end if end subroutine savefig subroutine show(self, blocking) - !! Display figure in ASCII terminal - !! - !! Arguments: - !! blocking: Optional - if true, wait for user input after display (default: false) + !! Display the figure class(figure_t), intent(inout) :: self logical, intent(in), optional :: blocking - logical :: do_block - ! Default to non-blocking - do_block = .false. - if (present(blocking)) do_block = blocking + logical :: block - ! Always reinitialize backend for ASCII output - if (allocated(self%backend)) deallocate(self%backend) - call initialize_backend(self%backend, 'ascii', 80, 24) + block = .true. + if (present(blocking)) block = blocking - ! Reset rendered flag to force re-rendering for new backend - self%rendered = .false. - call render_figure(self) - call self%backend%save("terminal") - - ! If blocking requested, wait for user input - if (do_block) then - print *, "Press Enter to continue..." - read(*,*) + ! Render if not already rendered + if (.not. self%rendered) then + call self%render_figure() end if + + ! Display the figure + call self%backend%save("terminal") end subroutine show - ! Label setters (following Interface Segregation Principle) - subroutine set_xlabel(self, label) + !! Set x-axis label class(figure_t), intent(inout) :: self character(len=*), intent(in) :: label self%xlabel = label end subroutine set_xlabel subroutine set_ylabel(self, label) + !! Set y-axis label class(figure_t), intent(inout) :: self character(len=*), intent(in) :: label self%ylabel = label end subroutine set_ylabel subroutine set_title(self, title) + !! Set figure title class(figure_t), intent(inout) :: self character(len=*), intent(in) :: title self%title = title end subroutine set_title subroutine set_xscale(self, scale, threshold) + !! Set x-axis scale type class(figure_t), intent(inout) :: self character(len=*), intent(in) :: scale real(wp), intent(in), optional :: threshold @@ -432,6 +297,7 @@ subroutine set_xscale(self, scale, threshold) end subroutine set_xscale subroutine set_yscale(self, scale, threshold) + !! Set y-axis scale type class(figure_t), intent(inout) :: self character(len=*), intent(in) :: scale real(wp), intent(in), optional :: threshold @@ -441,6 +307,7 @@ subroutine set_yscale(self, scale, threshold) end subroutine set_yscale subroutine set_xlim(self, x_min, x_max) + !! Set x-axis limits class(figure_t), intent(inout) :: self real(wp), intent(in) :: x_min, x_max @@ -450,6 +317,7 @@ subroutine set_xlim(self, x_min, x_max) end subroutine set_xlim subroutine set_ylim(self, y_min, y_max) + !! Set y-axis limits class(figure_t), intent(inout) :: self real(wp), intent(in) :: y_min, y_max @@ -459,68 +327,141 @@ subroutine set_ylim(self, y_min, y_max) end subroutine set_ylim subroutine set_line_width(self, width) - !! Set line width for subsequent plot operations + !! Set line width for subsequent plots class(figure_t), intent(inout) :: self real(wp), intent(in) :: width - self%current_line_width = width end subroutine set_line_width + subroutine set_ydata(self, plot_index, y_new) + !! Update y data for an existing plot + class(figure_t), intent(inout) :: self + integer, intent(in) :: plot_index + real(wp), intent(in) :: y_new(:) + + if (plot_index < 1 .or. plot_index > self%plot_count) then + print *, "Warning: Invalid plot index", plot_index + return + end if + + if (.not. allocated(self%plots(plot_index)%y)) then + print *, "Warning: Plot", plot_index, "has no y data to update" + return + end if + + if (size(y_new) /= size(self%plots(plot_index)%y)) then + print *, "Warning: New y data size", size(y_new), & + "does not match existing size", size(self%plots(plot_index)%y) + return + end if + + self%plots(plot_index)%y = y_new + end subroutine set_ydata + + subroutine figure_legend(self, location) + !! Add legend to figure + class(figure_t), intent(inout) :: self + character(len=*), intent(in), optional :: location + + character(len=:), allocatable :: loc + integer :: i + + loc = 'upper right' + if (present(location)) loc = location + + self%show_legend = .true. + + ! Set legend position based on location string + call self%legend_data%set_position(loc) + + ! Add legend entries from plots + do i = 1, self%plot_count + if (allocated(self%plots(i)%label)) then + if (allocated(self%plots(i)%linestyle)) then + if (allocated(self%plots(i)%marker)) then + call self%legend_data%add_entry(self%plots(i)%label, & + self%plots(i)%color, & + self%plots(i)%linestyle, & + self%plots(i)%marker) + else + call self%legend_data%add_entry(self%plots(i)%label, & + self%plots(i)%color, & + self%plots(i)%linestyle) + end if + else + call self%legend_data%add_entry(self%plots(i)%label, & + self%plots(i)%color) + end if + end if + end do + end subroutine figure_legend + + subroutine clear_streamlines(self) + !! Clear streamline data + class(figure_t), intent(inout) :: self + if (allocated(self%streamlines)) then + deallocate(self%streamlines) + end if + end subroutine clear_streamlines + subroutine destroy(self) - !! Clean up figure resources + !! Finalize and clean up figure type(figure_t), intent(inout) :: self + if (allocated(self%backend)) then + deallocate(self%backend) + end if + if (allocated(self%plots)) deallocate(self%plots) - if (allocated(self%backend)) deallocate(self%backend) + if (allocated(self%streamlines)) deallocate(self%streamlines) + if (allocated(self%title)) deallocate(self%title) + if (allocated(self%xlabel)) deallocate(self%xlabel) + if (allocated(self%ylabel)) deallocate(self%ylabel) end subroutine destroy - ! Private helper routines (implementation details) - + ! Private implementation procedures + subroutine add_line_plot_data(self, x, y, label, linestyle, color, marker) !! Add line plot data to internal storage class(figure_t), intent(inout) :: self real(wp), intent(in) :: x(:), y(:) character(len=*), intent(in), optional :: label, linestyle, marker - real(wp), intent(in), optional :: color(3) + real(wp), intent(in) :: color(3) - integer :: plot_idx, color_idx + if (self%plot_count >= self%max_plots) then + print *, "Warning: Maximum number of plots reached" + return + end if - plot_idx = self%plot_count - self%plots(plot_idx)%plot_type = PLOT_TYPE_LINE + self%plot_count = self%plot_count + 1 - ! Store data - if (allocated(self%plots(plot_idx)%x)) deallocate(self%plots(plot_idx)%x) - if (allocated(self%plots(plot_idx)%y)) deallocate(self%plots(plot_idx)%y) - allocate(self%plots(plot_idx)%x(size(x))) - allocate(self%plots(plot_idx)%y(size(y))) - self%plots(plot_idx)%x = x - self%plots(plot_idx)%y = y + ! Store plot data + self%plots(self%plot_count)%plot_type = PLOT_TYPE_LINE + allocate(self%plots(self%plot_count)%x(size(x))) + allocate(self%plots(self%plot_count)%y(size(y))) + self%plots(self%plot_count)%x = x + self%plots(self%plot_count)%y = y + self%plots(self%plot_count)%color = color - ! Set properties + ! Process optional arguments if (present(label)) then - self%plots(plot_idx)%label = label - else - self%plots(plot_idx)%label = '' + self%plots(self%plot_count)%label = label end if if (present(linestyle)) then - self%plots(plot_idx)%linestyle = linestyle + self%plots(self%plot_count)%linestyle = linestyle else - self%plots(plot_idx)%linestyle = 'solid' + self%plots(self%plot_count)%linestyle = '-' end if - + if (present(marker)) then - self%plots(plot_idx)%marker = marker - else - self%plots(plot_idx)%marker = 'None' + if (len_trim(marker) > 0) then + self%plots(self%plot_count)%marker = marker + end if end if - if (present(color)) then - self%plots(plot_idx)%color = color - else - color_idx = mod(plot_idx - 1, 6) + 1 - self%plots(plot_idx)%color = self%colors(:, color_idx) - end if + ! Update data ranges + call self%update_data_ranges() end subroutine add_line_plot_data subroutine add_contour_plot_data(self, x_grid, y_grid, z_grid, levels, label) @@ -530,1236 +471,397 @@ subroutine add_contour_plot_data(self, x_grid, y_grid, z_grid, levels, label) real(wp), intent(in), optional :: levels(:) character(len=*), intent(in), optional :: label - integer :: plot_idx - - plot_idx = self%plot_count - self%plots(plot_idx)%plot_type = PLOT_TYPE_CONTOUR - - ! Store grid data - if (allocated(self%plots(plot_idx)%x_grid)) deallocate(self%plots(plot_idx)%x_grid) - if (allocated(self%plots(plot_idx)%y_grid)) deallocate(self%plots(plot_idx)%y_grid) - if (allocated(self%plots(plot_idx)%z_grid)) deallocate(self%plots(plot_idx)%z_grid) - allocate(self%plots(plot_idx)%x_grid(size(x_grid))) - allocate(self%plots(plot_idx)%y_grid(size(y_grid))) - allocate(self%plots(plot_idx)%z_grid(size(z_grid,1), size(z_grid,2))) + if (self%plot_count >= self%max_plots) then + print *, "Warning: Maximum number of plots reached" + return + end if - self%plots(plot_idx)%x_grid = x_grid - self%plots(plot_idx)%y_grid = y_grid - self%plots(plot_idx)%z_grid = z_grid + self%plot_count = self%plot_count + 1 - ! Set default contour properties - self%plots(plot_idx)%use_color_levels = .false. + ! Store plot data + self%plots(self%plot_count)%plot_type = PLOT_TYPE_CONTOUR + allocate(self%plots(self%plot_count)%x_grid(size(x_grid))) + allocate(self%plots(self%plot_count)%y_grid(size(y_grid))) + allocate(self%plots(self%plot_count)%z_grid(size(z_grid,1), size(z_grid,2))) + self%plots(self%plot_count)%x_grid = x_grid + self%plots(self%plot_count)%y_grid = y_grid + self%plots(self%plot_count)%z_grid = z_grid - ! Handle label - if (present(label)) then - self%plots(plot_idx)%label = label + if (present(levels)) then + allocate(self%plots(self%plot_count)%contour_levels(size(levels))) + self%plots(self%plot_count)%contour_levels = levels else - self%plots(plot_idx)%label = '' + call self%generate_default_contour_levels(self%plots(self%plot_count)) end if - ! Handle contour levels - if (present(levels)) then - if (allocated(self%plots(plot_idx)%contour_levels)) deallocate(self%plots(plot_idx)%contour_levels) - allocate(self%plots(plot_idx)%contour_levels(size(levels))) - self%plots(plot_idx)%contour_levels = levels - else - call generate_default_contour_levels(self%plots(plot_idx)) + if (present(label)) then + self%plots(self%plot_count)%label = label end if + + ! Set default color + self%plots(self%plot_count)%color = self%colors(:, mod(self%plot_count-1, 6) + 1) + self%plots(self%plot_count)%use_color_levels = .false. + + ! Update data ranges + call self%update_data_ranges() end subroutine add_contour_plot_data subroutine add_colored_contour_plot_data(self, x_grid, y_grid, z_grid, levels, colormap, show_colorbar, label) - !! Add colored contour plot data to internal storage + !! Add colored contour plot data class(figure_t), intent(inout) :: self real(wp), intent(in) :: x_grid(:), y_grid(:), z_grid(:,:) real(wp), intent(in), optional :: levels(:) character(len=*), intent(in), optional :: colormap, label logical, intent(in), optional :: show_colorbar - integer :: plot_idx - - plot_idx = self%plot_count - self%plots(plot_idx)%plot_type = PLOT_TYPE_CONTOUR + if (self%plot_count >= self%max_plots) then + print *, "Warning: Maximum number of plots reached" + return + end if - ! Store grid data - if (allocated(self%plots(plot_idx)%x_grid)) deallocate(self%plots(plot_idx)%x_grid) - if (allocated(self%plots(plot_idx)%y_grid)) deallocate(self%plots(plot_idx)%y_grid) - if (allocated(self%plots(plot_idx)%z_grid)) deallocate(self%plots(plot_idx)%z_grid) - allocate(self%plots(plot_idx)%x_grid(size(x_grid))) - allocate(self%plots(plot_idx)%y_grid(size(y_grid))) - allocate(self%plots(plot_idx)%z_grid(size(z_grid,1), size(z_grid,2))) + self%plot_count = self%plot_count + 1 - self%plots(plot_idx)%x_grid = x_grid - self%plots(plot_idx)%y_grid = y_grid - self%plots(plot_idx)%z_grid = z_grid + ! Store plot data + self%plots(self%plot_count)%plot_type = PLOT_TYPE_CONTOUR + allocate(self%plots(self%plot_count)%x_grid(size(x_grid))) + allocate(self%plots(self%plot_count)%y_grid(size(y_grid))) + allocate(self%plots(self%plot_count)%z_grid(size(z_grid,1), size(z_grid,2))) + self%plots(self%plot_count)%x_grid = x_grid + self%plots(self%plot_count)%y_grid = y_grid + self%plots(self%plot_count)%z_grid = z_grid - ! Set color contour properties - self%plots(plot_idx)%use_color_levels = .true. + if (present(levels)) then + allocate(self%plots(self%plot_count)%contour_levels(size(levels))) + self%plots(self%plot_count)%contour_levels = levels + else + call self%generate_default_contour_levels(self%plots(self%plot_count)) + end if if (present(colormap)) then - self%plots(plot_idx)%colormap = colormap + self%plots(self%plot_count)%colormap = colormap else - self%plots(plot_idx)%colormap = 'crest' + self%plots(self%plot_count)%colormap = 'crest' end if if (present(show_colorbar)) then - self%plots(plot_idx)%show_colorbar = show_colorbar + self%plots(self%plot_count)%show_colorbar = show_colorbar else - self%plots(plot_idx)%show_colorbar = .true. + self%plots(self%plot_count)%show_colorbar = .true. end if - ! Handle label if (present(label)) then - self%plots(plot_idx)%label = label - else - self%plots(plot_idx)%label = '' + self%plots(self%plot_count)%label = label end if - ! Handle contour levels - if (present(levels)) then - if (allocated(self%plots(plot_idx)%contour_levels)) deallocate(self%plots(plot_idx)%contour_levels) - allocate(self%plots(plot_idx)%contour_levels(size(levels))) - self%plots(plot_idx)%contour_levels = levels - else - call generate_default_contour_levels(self%plots(plot_idx)) - end if + self%plots(self%plot_count)%use_color_levels = .true. + + ! Update data ranges + call self%update_data_ranges() end subroutine add_colored_contour_plot_data subroutine add_pcolormesh_plot_data(self, x, y, c, colormap, vmin, vmax, edgecolors, linewidths) - !! Add pcolormesh data to plot array + !! Add pcolormesh plot data class(figure_t), intent(inout) :: self real(wp), intent(in) :: x(:), y(:), c(:,:) character(len=*), intent(in), optional :: colormap real(wp), intent(in), optional :: vmin, vmax - character(len=*), intent(in), optional :: edgecolors + real(wp), intent(in), optional :: edgecolors(3) real(wp), intent(in), optional :: linewidths - integer :: plot_idx - if (self%plot_count >= self%max_plots) then - error stop "Maximum number of plots exceeded" + print *, "Warning: Maximum number of plots reached" + return end if - plot_idx = self%plot_count + 1 - self%plots(plot_idx)%plot_type = PLOT_TYPE_PCOLORMESH + self%plot_count = self%plot_count + 1 + + ! Store plot data + self%plots(self%plot_count)%plot_type = PLOT_TYPE_PCOLORMESH - ! Initialize pcolormesh with regular grid - call self%plots(plot_idx)%pcolormesh_data%initialize_regular_grid(x, y, c, colormap) + ! Initialize pcolormesh data using proper method + call self%plots(self%plot_count)%pcolormesh_data%initialize_regular_grid(x, y, c, colormap) - ! Set vmin/vmax if provided if (present(vmin)) then - self%plots(plot_idx)%pcolormesh_data%vmin = vmin - self%plots(plot_idx)%pcolormesh_data%vmin_set = .true. + self%plots(self%plot_count)%pcolormesh_data%vmin = vmin + self%plots(self%plot_count)%pcolormesh_data%vmin_set = .true. end if + if (present(vmax)) then - self%plots(plot_idx)%pcolormesh_data%vmax = vmax - self%plots(plot_idx)%pcolormesh_data%vmax_set = .true. + self%plots(self%plot_count)%pcolormesh_data%vmax = vmax + self%plots(self%plot_count)%pcolormesh_data%vmax_set = .true. end if - ! Set edge properties if (present(edgecolors)) then - if (trim(edgecolors) /= 'none' .and. trim(edgecolors) /= '') then - self%plots(plot_idx)%pcolormesh_data%show_edges = .true. - ! TODO: Parse color string - end if + self%plots(self%plot_count)%pcolormesh_data%show_edges = .true. + self%plots(self%plot_count)%pcolormesh_data%edge_color = edgecolors end if + if (present(linewidths)) then - self%plots(plot_idx)%pcolormesh_data%edge_width = linewidths + self%plots(self%plot_count)%pcolormesh_data%edge_width = linewidths end if - ! Update data range if needed - call self%plots(plot_idx)%pcolormesh_data%get_data_range() + ! Update data ranges + call self%update_data_ranges_pcolormesh() end subroutine add_pcolormesh_plot_data subroutine update_data_ranges_pcolormesh(self) - !! Update figure data ranges after adding pcolormesh plot + !! Update data ranges after adding pcolormesh plot class(figure_t), intent(inout) :: self + real(wp) :: x_min_new, x_max_new, y_min_new, y_max_new - integer :: plot_idx - real(wp) :: x_min_plot, x_max_plot, y_min_plot, y_max_plot - - plot_idx = self%plot_count + 1 - - ! Get data ranges from pcolormesh vertices - x_min_plot = minval(self%plots(plot_idx)%pcolormesh_data%x_vertices) - x_max_plot = maxval(self%plots(plot_idx)%pcolormesh_data%x_vertices) - y_min_plot = minval(self%plots(plot_idx)%pcolormesh_data%y_vertices) - y_max_plot = maxval(self%plots(plot_idx)%pcolormesh_data%y_vertices) + x_min_new = minval(self%plots(self%plot_count)%pcolormesh_data%x_vertices) + x_max_new = maxval(self%plots(self%plot_count)%pcolormesh_data%x_vertices) + y_min_new = minval(self%plots(self%plot_count)%pcolormesh_data%y_vertices) + y_max_new = maxval(self%plots(self%plot_count)%pcolormesh_data%y_vertices) - ! Update figure ranges - if (self%plot_count == 0) then - self%x_min = x_min_plot - self%x_max = x_max_plot - self%y_min = y_min_plot - self%y_max = y_max_plot - else - self%x_min = min(self%x_min, x_min_plot) - self%x_max = max(self%x_max, x_max_plot) - self%y_min = min(self%y_min, y_min_plot) - self%y_max = max(self%y_max, y_max_plot) + if (.not. self%xlim_set) then + if (self%plot_count == 1) then + self%x_min = x_min_new + self%x_max = x_max_new + else + self%x_min = min(self%x_min, x_min_new) + self%x_max = max(self%x_max, x_max_new) + end if end if - self%plot_count = plot_idx + if (.not. self%ylim_set) then + if (self%plot_count == 1) then + self%y_min = y_min_new + self%y_max = y_max_new + else + self%y_min = min(self%y_min, y_min_new) + self%y_max = max(self%y_max, y_max_new) + end if + end if end subroutine update_data_ranges_pcolormesh subroutine update_data_ranges(self) - !! Update figure data ranges after adding plots + !! Update data ranges based on current plot class(figure_t), intent(inout) :: self - - ! Implementation delegates to range calculation utilities - ! This follows Dependency Inversion Principle - call calculate_figure_data_ranges(self) + call self%calculate_figure_data_ranges() end subroutine update_data_ranges subroutine render_figure(self) - !! Render all plots to the backend + !! Main rendering pipeline class(figure_t), intent(inout) :: self - if (self%rendered) return + if (self%plot_count == 0) return - ! Setup coordinate system using scales module - call setup_coordinate_system(self) + ! Calculate final data ranges + call self%calculate_figure_data_ranges() - ! Render background and axes - call render_figure_background(self) - call render_figure_axes(self) + ! Setup coordinate system + call self%setup_coordinate_system() - ! Render individual plots - call render_all_plots(self) + ! Render background + call self%render_figure_background() - ! Render Y-axis label ABSOLUTELY LAST (after everything else) - select type (backend => self%backend) - type is (png_context) - if (allocated(self%ylabel)) then - call backend%render_ylabel(self%ylabel) - end if - type is (pdf_context) - ! PDF handles this differently - already done in draw_pdf_axes_and_labels - end select + ! Render axes + call self%render_figure_axes() + + ! Render all plots + call self%render_all_plots() - ! Render legend if requested (following SOLID principles) - if (self%show_legend) then - call legend_render(self%legend_data, self%backend) + ! Render legend if requested + if (self%show_legend .and. self%legend_data%num_entries > 0) then + call self%legend_data%render(self%backend) end if self%rendered = .true. end subroutine render_figure - ! Placeholder implementations for helper routines - ! These will delegate to specialized modules - - subroutine generate_default_contour_levels(plot_data) - !! Generate default contour levels for a plot + subroutine generate_default_contour_levels(self, plot_data) + !! Generate default contour levels + class(figure_t), intent(inout) :: self type(plot_data_t), intent(inout) :: plot_data - real(wp) :: z_min, z_max, dz - integer :: i, n_levels - if (.not. allocated(plot_data%z_grid)) return + real(wp) :: z_min, z_max + integer :: num_levels + integer :: i z_min = minval(plot_data%z_grid) z_max = maxval(plot_data%z_grid) - ! Generate 10 evenly spaced levels by default - n_levels = 10 - if (allocated(plot_data%contour_levels)) deallocate(plot_data%contour_levels) - allocate(plot_data%contour_levels(n_levels)) - - dz = (z_max - z_min) / real(n_levels + 1, wp) + num_levels = 7 + allocate(plot_data%contour_levels(num_levels)) - do i = 1, n_levels - plot_data%contour_levels(i) = z_min + real(i, wp) * dz + do i = 1, num_levels + plot_data%contour_levels(i) = z_min + (i-1) * (z_max - z_min) / (num_levels - 1) end do end subroutine generate_default_contour_levels - + subroutine calculate_figure_data_ranges(self) + !! Calculate overall data ranges for the figure class(figure_t), intent(inout) :: self + + real(wp) :: x_min_data, x_max_data, y_min_data, y_max_data integer :: i - real(wp) :: x_min_orig, x_max_orig, y_min_orig, y_max_orig - real(wp) :: x_min_trans, x_max_trans, y_min_trans, y_max_trans logical :: first_plot - if (self%plot_count == 0) return + if (self%xlim_set .and. self%ylim_set) then + self%x_min_transformed = apply_scale_transform(self%x_min, self%xscale, self%symlog_threshold) + self%x_max_transformed = apply_scale_transform(self%x_max, self%xscale, self%symlog_threshold) + self%y_min_transformed = apply_scale_transform(self%y_min, self%yscale, self%symlog_threshold) + self%y_max_transformed = apply_scale_transform(self%y_max, self%yscale, self%symlog_threshold) + return + end if first_plot = .true. do i = 1, self%plot_count - if (self%plots(i)%plot_type == PLOT_TYPE_LINE) then - if (first_plot) then - ! Store ORIGINAL data ranges for tick generation - x_min_orig = minval(self%plots(i)%x) - x_max_orig = maxval(self%plots(i)%x) - y_min_orig = minval(self%plots(i)%y) - y_max_orig = maxval(self%plots(i)%y) - - ! Calculate transformed ranges for rendering - x_min_trans = apply_scale_transform(x_min_orig, self%xscale, self%symlog_threshold) - x_max_trans = apply_scale_transform(x_max_orig, self%xscale, self%symlog_threshold) - y_min_trans = apply_scale_transform(y_min_orig, self%yscale, self%symlog_threshold) - y_max_trans = apply_scale_transform(y_max_orig, self%yscale, self%symlog_threshold) - first_plot = .false. - else - ! Update original ranges - x_min_orig = min(x_min_orig, minval(self%plots(i)%x)) - x_max_orig = max(x_max_orig, maxval(self%plots(i)%x)) - y_min_orig = min(y_min_orig, minval(self%plots(i)%y)) - y_max_orig = max(y_max_orig, maxval(self%plots(i)%y)) - - ! Update transformed ranges - x_min_trans = min(x_min_trans, apply_scale_transform(minval(self%plots(i)%x), & - self%xscale, self%symlog_threshold)) - x_max_trans = max(x_max_trans, apply_scale_transform(maxval(self%plots(i)%x), & - self%xscale, self%symlog_threshold)) - y_min_trans = min(y_min_trans, apply_scale_transform(minval(self%plots(i)%y), & - self%yscale, self%symlog_threshold)) - y_max_trans = max(y_max_trans, apply_scale_transform(maxval(self%plots(i)%y), & - self%yscale, self%symlog_threshold)) + select case (self%plots(i)%plot_type) + case (PLOT_TYPE_LINE) + if (allocated(self%plots(i)%x) .and. allocated(self%plots(i)%y)) then + if (first_plot) then + x_min_data = minval(self%plots(i)%x) + x_max_data = maxval(self%plots(i)%x) + y_min_data = minval(self%plots(i)%y) + y_max_data = maxval(self%plots(i)%y) + first_plot = .false. + else + x_min_data = min(x_min_data, minval(self%plots(i)%x)) + x_max_data = max(x_max_data, maxval(self%plots(i)%x)) + y_min_data = min(y_min_data, minval(self%plots(i)%y)) + y_max_data = max(y_max_data, maxval(self%plots(i)%y)) + end if end if - else if (self%plots(i)%plot_type == PLOT_TYPE_CONTOUR) then - if (first_plot) then - ! Store ORIGINAL contour grid ranges - x_min_orig = minval(self%plots(i)%x_grid) - x_max_orig = maxval(self%plots(i)%x_grid) - y_min_orig = minval(self%plots(i)%y_grid) - y_max_orig = maxval(self%plots(i)%y_grid) - - ! Calculate transformed ranges for rendering - x_min_trans = apply_scale_transform(x_min_orig, self%xscale, self%symlog_threshold) - x_max_trans = apply_scale_transform(x_max_orig, self%xscale, self%symlog_threshold) - y_min_trans = apply_scale_transform(y_min_orig, self%yscale, self%symlog_threshold) - y_max_trans = apply_scale_transform(y_max_orig, self%yscale, self%symlog_threshold) - first_plot = .false. - else - ! Update original ranges - x_min_orig = min(x_min_orig, minval(self%plots(i)%x_grid)) - x_max_orig = max(x_max_orig, maxval(self%plots(i)%x_grid)) - y_min_orig = min(y_min_orig, minval(self%plots(i)%y_grid)) - y_max_orig = max(y_max_orig, maxval(self%plots(i)%y_grid)) - - ! Update transformed ranges - x_min_trans = min(x_min_trans, apply_scale_transform(minval(self%plots(i)%x_grid), & - self%xscale, self%symlog_threshold)) - x_max_trans = max(x_max_trans, apply_scale_transform(maxval(self%plots(i)%x_grid), & - self%xscale, self%symlog_threshold)) - y_min_trans = min(y_min_trans, apply_scale_transform(minval(self%plots(i)%y_grid), & - self%yscale, self%symlog_threshold)) - y_max_trans = max(y_max_trans, apply_scale_transform(maxval(self%plots(i)%y_grid), & - self%yscale, self%symlog_threshold)) + + case (PLOT_TYPE_CONTOUR) + if (allocated(self%plots(i)%x_grid) .and. allocated(self%plots(i)%y_grid)) then + if (first_plot) then + x_min_data = minval(self%plots(i)%x_grid) + x_max_data = maxval(self%plots(i)%x_grid) + y_min_data = minval(self%plots(i)%y_grid) + y_max_data = maxval(self%plots(i)%y_grid) + first_plot = .false. + else + x_min_data = min(x_min_data, minval(self%plots(i)%x_grid)) + x_max_data = max(x_max_data, maxval(self%plots(i)%x_grid)) + y_min_data = min(y_min_data, minval(self%plots(i)%y_grid)) + y_max_data = max(y_max_data, maxval(self%plots(i)%y_grid)) + end if end if - else if (self%plots(i)%plot_type == PLOT_TYPE_PCOLORMESH) then - if (first_plot) then - ! Store ORIGINAL pcolormesh grid ranges - x_min_orig = minval(self%plots(i)%pcolormesh_data%x_vertices) - x_max_orig = maxval(self%plots(i)%pcolormesh_data%x_vertices) - y_min_orig = minval(self%plots(i)%pcolormesh_data%y_vertices) - y_max_orig = maxval(self%plots(i)%pcolormesh_data%y_vertices) - - ! Calculate transformed ranges for rendering - x_min_trans = apply_scale_transform(x_min_orig, self%xscale, self%symlog_threshold) - x_max_trans = apply_scale_transform(x_max_orig, self%xscale, self%symlog_threshold) - y_min_trans = apply_scale_transform(y_min_orig, self%yscale, self%symlog_threshold) - y_max_trans = apply_scale_transform(y_max_orig, self%yscale, self%symlog_threshold) - first_plot = .false. - else - ! Update original ranges - x_min_orig = min(x_min_orig, minval(self%plots(i)%pcolormesh_data%x_vertices)) - x_max_orig = max(x_max_orig, maxval(self%plots(i)%pcolormesh_data%x_vertices)) - y_min_orig = min(y_min_orig, minval(self%plots(i)%pcolormesh_data%y_vertices)) - y_max_orig = max(y_max_orig, maxval(self%plots(i)%pcolormesh_data%y_vertices)) - - ! Update transformed ranges for rendering - x_min_trans = min(x_min_trans, apply_scale_transform(minval(self%plots(i)%pcolormesh_data%x_vertices), & - self%xscale, self%symlog_threshold)) - x_max_trans = max(x_max_trans, apply_scale_transform(maxval(self%plots(i)%pcolormesh_data%x_vertices), & - self%xscale, self%symlog_threshold)) - y_min_trans = min(y_min_trans, apply_scale_transform(minval(self%plots(i)%pcolormesh_data%y_vertices), & - self%yscale, self%symlog_threshold)) - y_max_trans = max(y_max_trans, apply_scale_transform(maxval(self%plots(i)%pcolormesh_data%y_vertices), & - self%yscale, self%symlog_threshold)) + + case (PLOT_TYPE_PCOLORMESH) + if (allocated(self%plots(i)%pcolormesh_data%x_vertices) .and. & + allocated(self%plots(i)%pcolormesh_data%y_vertices)) then + if (first_plot) then + x_min_data = minval(self%plots(i)%pcolormesh_data%x_vertices) + x_max_data = maxval(self%plots(i)%pcolormesh_data%x_vertices) + y_min_data = minval(self%plots(i)%pcolormesh_data%y_vertices) + y_max_data = maxval(self%plots(i)%pcolormesh_data%y_vertices) + first_plot = .false. + else + x_min_data = min(x_min_data, minval(self%plots(i)%pcolormesh_data%x_vertices)) + x_max_data = max(x_max_data, maxval(self%plots(i)%pcolormesh_data%x_vertices)) + y_min_data = min(y_min_data, minval(self%plots(i)%pcolormesh_data%y_vertices)) + y_max_data = max(y_max_data, maxval(self%plots(i)%pcolormesh_data%y_vertices)) + end if end if - end if + end select end do + ! Apply user-specified limits or use data ranges if (.not. self%xlim_set) then - self%x_min = x_min_orig ! Backend gets ORIGINAL coordinates for tick generation - self%x_max = x_max_orig - self%x_min_transformed = x_min_trans ! Store transformed for rendering - self%x_max_transformed = x_max_trans + self%x_min = x_min_data + self%x_max = x_max_data end if if (.not. self%ylim_set) then - self%y_min = y_min_orig ! Backend gets ORIGINAL coordinates for tick generation - self%y_max = y_max_orig - self%y_min_transformed = y_min_trans ! Store transformed for rendering - self%y_max_transformed = y_max_trans + self%y_min = y_min_data + self%y_max = y_max_data end if + + ! Apply scale transformations + self%x_min_transformed = apply_scale_transform(self%x_min, self%xscale, self%symlog_threshold) + self%x_max_transformed = apply_scale_transform(self%x_max, self%xscale, self%symlog_threshold) + self%y_min_transformed = apply_scale_transform(self%y_min, self%yscale, self%symlog_threshold) + self%y_max_transformed = apply_scale_transform(self%y_max, self%yscale, self%symlog_threshold) end subroutine calculate_figure_data_ranges - + subroutine setup_coordinate_system(self) + !! Setup the coordinate system for rendering class(figure_t), intent(inout) :: self - if (.not. self%xlim_set .or. .not. self%ylim_set) then - call calculate_figure_data_ranges(self) - end if - - ! Set backend data coordinate ranges to TRANSFORMED coordinates for data rendering + ! Set data ranges directly on backend self%backend%x_min = self%x_min_transformed self%backend%x_max = self%x_max_transformed self%backend%y_min = self%y_min_transformed self%backend%y_max = self%y_max_transformed end subroutine setup_coordinate_system - + subroutine render_figure_background(self) + !! Render figure background class(figure_t), intent(inout) :: self - ! Clear the background - backend-specific implementation not needed - ! Background is handled by backend initialization + ! Background clearing is handled by backend-specific rendering end subroutine render_figure_background - + subroutine render_figure_axes(self) + !! Render figure axes and labels class(figure_t), intent(inout) :: self - ! print *, "DEBUG: Rendering axes with ranges X:", self%x_min, "to", self%x_max, "Y:", self%y_min, "to", self%y_max - - ! Set axis color to black - call self%backend%color(0.0_wp, 0.0_wp, 0.0_wp) - - ! Use matplotlib-style axes with margins for backends that support it - select type (backend => self%backend) - type is (png_context) - call raster_draw_axes_and_labels(backend, self%xscale, self%yscale, self%symlog_threshold, & - self%x_min, self%x_max, self%y_min, self%y_max, & - self%title, self%xlabel, self%ylabel, & - 0.0_wp, 0.0_wp, .false.) - type is (pdf_context) - call backend%draw_axes_and_labels_backend(self%xscale, self%yscale, self%symlog_threshold, & - self%x_min, self%x_max, self%y_min, self%y_max, & - self%title, self%xlabel, self%ylabel, & - 0.0_wp, 0.0_wp, .false.) - type is (ascii_context) - call backend%draw_axes_and_labels_backend(self%xscale, self%yscale, self%symlog_threshold, & - self%x_min, self%x_max, self%y_min, self%y_max, & - self%title, self%xlabel, self%ylabel, & - 0.0_wp, 0.0_wp, .false.) - class default - ! For other backends, use simple axes - call self%backend%line(self%x_min, self%y_min, self%x_max, self%y_min) - call self%backend%line(self%x_min, self%y_min, self%x_min, self%y_max) - end select + ! Draw axes using backend's polymorphic method + call self%backend%draw_axes_and_labels_backend(self%xscale, self%yscale, & + self%symlog_threshold, & + self%x_min, self%x_max, & + self%y_min, self%y_max, & + self%title, self%xlabel, self%ylabel, & + z_min=0.0_wp, z_max=1.0_wp, & + has_3d_plots=.false.) end subroutine render_figure_axes - + subroutine render_all_plots(self) + !! Render all plots in the figure class(figure_t), intent(inout) :: self integer :: i - ! Render regular plots do i = 1, self%plot_count - ! Set color for this plot - call self%backend%color(self%plots(i)%color(1), self%plots(i)%color(2), self%plots(i)%color(3)) - - if (self%plots(i)%plot_type == PLOT_TYPE_LINE) then - call render_line_plot(self, i) - else if (self%plots(i)%plot_type == PLOT_TYPE_CONTOUR) then - call render_contour_plot(self, i) - else if (self%plots(i)%plot_type == PLOT_TYPE_PCOLORMESH) then - call render_pcolormesh_plot(self, i) - end if - end do - - end subroutine render_all_plots - - subroutine render_streamlines(self) - !! Render all streamlines in the streamlines array - class(figure_t), intent(inout) :: self - integer :: i - - do i = 1, size(self%streamlines) - ! Set color for this streamline - call self%backend%color(self%streamlines(i)%color(1), self%streamlines(i)%color(2), self%streamlines(i)%color(3)) - - ! Render as line plot - call render_streamline(self, i) - end do - end subroutine render_streamlines - - subroutine render_streamline(self, streamline_idx) - !! Render a single streamline - class(figure_t), intent(inout) :: self - integer, intent(in) :: streamline_idx - integer :: i - real(wp) :: x1_screen, y1_screen, x2_screen, y2_screen - - - do i = 1, size(self%streamlines(streamline_idx)%x) - 1 - ! Apply scale transformations - x1_screen = apply_scale_transform(self%streamlines(streamline_idx)%x(i), self%xscale, self%symlog_threshold) - y1_screen = apply_scale_transform(self%streamlines(streamline_idx)%y(i), self%yscale, self%symlog_threshold) - x2_screen = apply_scale_transform(self%streamlines(streamline_idx)%x(i+1), self%xscale, self%symlog_threshold) - y2_screen = apply_scale_transform(self%streamlines(streamline_idx)%y(i+1), self%yscale, self%symlog_threshold) - - ! Draw line segment - call self%backend%line(x1_screen, y1_screen, x2_screen, y2_screen) - end do - end subroutine render_streamline - - subroutine render_line_plot(self, plot_idx) - !! Render a single line plot with linestyle support - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - integer :: i - real(wp) :: x1_screen, y1_screen, x2_screen, y2_screen - character(len=:), allocatable :: linestyle - - if (plot_idx > self%plot_count) return - if (.not. allocated(self%plots(plot_idx)%x)) return - if (size(self%plots(plot_idx)%x) < 1) return - - ! Get linestyle for this plot - linestyle = self%plots(plot_idx)%linestyle - - ! Draw lines only if linestyle is not 'None' and we have at least 2 points - if (linestyle /= 'None' .and. size(self%plots(plot_idx)%x) >= 2) then - ! Set line width for all backends (2.0 for plot data, 1.0 for axes) - call self%backend%set_line_width(2.0_wp) - - ! Draw line segments using transformed coordinates with linestyle - call draw_line_with_style(self, plot_idx, linestyle) - end if - - ! Always render markers regardless of linestyle (matplotlib behavior) - call render_markers(self, plot_idx) - end subroutine render_line_plot - - subroutine render_markers(self, plot_idx) - !! Render markers at each data point, skipping NaN values - use, intrinsic :: ieee_arithmetic, only: ieee_is_nan - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - character(len=:), allocatable :: marker - integer :: i - real(wp) :: x_trans, y_trans - - if (plot_idx > self%plot_count) return - if (.not. allocated(self%plots(plot_idx)%marker)) return - - marker = self%plots(plot_idx)%marker - if (marker == 'None') return - - do i = 1, size(self%plots(plot_idx)%x) - ! Skip points with NaN values - if (ieee_is_nan(self%plots(plot_idx)%x(i)) .or. ieee_is_nan(self%plots(plot_idx)%y(i))) cycle - - x_trans = apply_scale_transform(self%plots(plot_idx)%x(i), self%xscale, self%symlog_threshold) - y_trans = apply_scale_transform(self%plots(plot_idx)%y(i), self%yscale, self%symlog_threshold) - call self%backend%draw_marker(x_trans, y_trans, marker) - end do - - end subroutine render_markers - - subroutine render_contour_plot(self, plot_idx) - !! Render a single contour plot using proper marching squares algorithm - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - integer :: level_idx - real(wp) :: contour_level - real(wp) :: z_min, z_max - real(wp), dimension(3) :: level_color - - if (plot_idx > self%plot_count) return - if (.not. allocated(self%plots(plot_idx)%z_grid)) return - - ! Get data range for filtering valid levels - z_min = minval(self%plots(plot_idx)%z_grid) - z_max = maxval(self%plots(plot_idx)%z_grid) - - ! For ASCII backend with colored contours, render as heatmap - select type (backend => self%backend) - type is (ascii_context) - if (self%plots(plot_idx)%use_color_levels) then - ! Render as heatmap for filled contours - call backend%fill_heatmap(self%plots(plot_idx)%x_grid, & - self%plots(plot_idx)%y_grid, & - self%plots(plot_idx)%z_grid, & - z_min, z_max) - return - end if - end select - - ! Render each contour level that falls within data range - if (allocated(self%plots(plot_idx)%contour_levels)) then - do level_idx = 1, size(self%plots(plot_idx)%contour_levels) - contour_level = self%plots(plot_idx)%contour_levels(level_idx) + select case (self%plots(i)%plot_type) + case (PLOT_TYPE_LINE) + call render_line_plot(self%backend, self%plots(i), i, & + self%x_min_transformed, self%x_max_transformed, & + self%y_min_transformed, self%y_max_transformed, & + self%xscale, self%yscale, self%symlog_threshold) - ! Only render levels within the data range - if (contour_level > z_min .and. contour_level < z_max) then - ! Set color based on contour level - if (self%plots(plot_idx)%use_color_levels) then - call colormap_value_to_color(contour_level, z_min, z_max, & - self%plots(plot_idx)%colormap, level_color) - call self%backend%color(level_color(1), level_color(2), level_color(3)) - end if - - call trace_contour_level(self, plot_idx, contour_level) + if (allocated(self%plots(i)%marker)) then + call render_markers(self%backend, self%plots(i), & + self%x_min_transformed, self%x_max_transformed, & + self%y_min_transformed, self%y_max_transformed, & + self%xscale, self%yscale, self%symlog_threshold) end if - end do - else - ! Draw a few default contour levels with colors - call render_default_contour_levels(self, plot_idx, z_min, z_max) - end if - end subroutine render_contour_plot - - subroutine render_pcolormesh_plot(self, plot_idx) - !! Render pcolormesh plot as colored quadrilaterals - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - - integer :: i, j - real(wp) :: x_quad(4), y_quad(4) - real(wp) :: x_screen(4), y_screen(4) - real(wp) :: color(3), c_value, c_min, c_max - - ! Get colormap range from pcolormesh data - c_min = self%plots(plot_idx)%pcolormesh_data%vmin - c_max = self%plots(plot_idx)%pcolormesh_data%vmax - - ! Debug output - ! print *, "DEBUG: render_pcolormesh_plot - nx=", self%plots(plot_idx)%pcolormesh_data%nx - ! print *, "DEBUG: render_pcolormesh_plot - ny=", self%plots(plot_idx)%pcolormesh_data%ny - ! print *, "DEBUG: render_pcolormesh_plot - vmin=", c_min, " vmax=", c_max - - ! For ASCII backend, render as heatmap - select type (backend => self%backend) - type is (ascii_context) - block - real(wp), allocatable :: x_centers(:), y_centers(:) - integer :: nx, ny, i, j - - nx = self%plots(plot_idx)%pcolormesh_data%nx - ny = self%plots(plot_idx)%pcolormesh_data%ny - - allocate(x_centers(nx), y_centers(ny)) - - ! Calculate cell centers from vertices - do i = 1, nx - x_centers(i) = 0.5_wp * (self%plots(plot_idx)%pcolormesh_data%x_vertices(1, i) + & - self%plots(plot_idx)%pcolormesh_data%x_vertices(1, i+1)) - end do - - do j = 1, ny - y_centers(j) = 0.5_wp * (self%plots(plot_idx)%pcolormesh_data%y_vertices(j, 1) + & - self%plots(plot_idx)%pcolormesh_data%y_vertices(j+1, 1)) - end do - - ! Render as heatmap using cell centers - call backend%fill_heatmap(x_centers, y_centers, & - self%plots(plot_idx)%pcolormesh_data%c_values, & - c_min, c_max) - end block - return - end select - - ! Render each quadrilateral - do i = 1, self%plots(plot_idx)%pcolormesh_data%ny - do j = 1, self%plots(plot_idx)%pcolormesh_data%nx - ! Get quad vertices in world coordinates - call self%plots(plot_idx)%pcolormesh_data%get_quad_vertices(i, j, x_quad, y_quad) - - ! Transform to screen coordinates - call transform_quad_to_screen(self, x_quad, y_quad, x_screen, y_screen) - ! Get color for this quad - c_value = self%plots(plot_idx)%pcolormesh_data%c_values(i, j) - call colormap_value_to_color(c_value, c_min, c_max, & - self%plots(plot_idx)%pcolormesh_data%colormap_name, color) + case (PLOT_TYPE_CONTOUR) + call render_contour_plot(self%backend, self%plots(i), & + self%x_min_transformed, self%x_max_transformed, & + self%y_min_transformed, self%y_max_transformed, & + self%xscale, self%yscale, self%symlog_threshold, & + self%width, self%height, & + self%margin_left, self%margin_right, & + self%margin_bottom, self%margin_top) - ! Draw filled quadrilateral - call self%backend%color(color(1), color(2), color(3)) - call draw_filled_quad(self%backend, x_screen, y_screen) - - ! Draw edges if requested - if (self%plots(plot_idx)%pcolormesh_data%show_edges) then - call self%backend%color(self%plots(plot_idx)%pcolormesh_data%edge_color(1), & - self%plots(plot_idx)%pcolormesh_data%edge_color(2), & - self%plots(plot_idx)%pcolormesh_data%edge_color(3)) - call draw_quad_edges(self%backend, x_screen, y_screen, & - self%plots(plot_idx)%pcolormesh_data%edge_width) - end if - end do - end do - end subroutine render_pcolormesh_plot - - subroutine render_default_contour_levels(self, plot_idx, z_min, z_max) - !! Render default contour levels with optional coloring - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - real(wp), intent(in) :: z_min, z_max - real(wp), dimension(3) :: level_color - real(wp) :: level_values(3) - integer :: i - - level_values = [z_min + 0.2_wp * (z_max - z_min), & - z_min + 0.5_wp * (z_max - z_min), & - z_min + 0.8_wp * (z_max - z_min)] - - do i = 1, 3 - ! Set color based on contour level - if (self%plots(plot_idx)%use_color_levels) then - call colormap_value_to_color(level_values(i), z_min, z_max, & - self%plots(plot_idx)%colormap, level_color) - call self%backend%color(level_color(1), level_color(2), level_color(3)) - end if - - call trace_contour_level(self, plot_idx, level_values(i)) - end do - end subroutine render_default_contour_levels - - subroutine trace_contour_level(self, plot_idx, level) - !! Trace a single contour level using marching squares - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - real(wp), intent(in) :: level - integer :: nx, ny, i, j - - nx = size(self%plots(plot_idx)%x_grid) - ny = size(self%plots(plot_idx)%y_grid) - - do i = 1, nx-1 - do j = 1, ny-1 - call process_contour_cell(self, plot_idx, i, j, level) - end do + case (PLOT_TYPE_PCOLORMESH) + call render_pcolormesh_plot(self%backend, self%plots(i), & + self%x_min_transformed, self%x_max_transformed, & + self%y_min_transformed, self%y_max_transformed, & + self%xscale, self%yscale, self%symlog_threshold, & + self%width, self%height, self%margin_right) + end select end do - end subroutine trace_contour_level - - subroutine process_contour_cell(self, plot_idx, i, j, level) - !! Process a single grid cell for contour extraction - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx, i, j - real(wp), intent(in) :: level - real(wp) :: x1, y1, x2, y2, x3, y3, x4, y4 - real(wp) :: z1, z2, z3, z4 - integer :: config - real(wp), dimension(8) :: line_points - integer :: num_lines - - call get_cell_coordinates(self, plot_idx, i, j, x1, y1, x2, y2, x3, y3, x4, y4) - call get_cell_values(self, plot_idx, i, j, z1, z2, z3, z4) - call calculate_marching_squares_config(z1, z2, z3, z4, level, config) - call get_contour_lines(config, x1, y1, x2, y2, x3, y3, x4, y4, & - z1, z2, z3, z4, level, line_points, num_lines) - call draw_contour_lines(self, line_points, num_lines) - end subroutine process_contour_cell - - subroutine get_cell_coordinates(self, plot_idx, i, j, x1, y1, x2, y2, x3, y3, x4, y4) - !! Get the coordinates of the four corners of a grid cell - class(figure_t), intent(in) :: self - integer, intent(in) :: plot_idx, i, j - real(wp), intent(out) :: x1, y1, x2, y2, x3, y3, x4, y4 - - x1 = self%plots(plot_idx)%x_grid(i) - y1 = self%plots(plot_idx)%y_grid(j) - x2 = self%plots(plot_idx)%x_grid(i+1) - y2 = self%plots(plot_idx)%y_grid(j) - x3 = self%plots(plot_idx)%x_grid(i+1) - y3 = self%plots(plot_idx)%y_grid(j+1) - x4 = self%plots(plot_idx)%x_grid(i) - y4 = self%plots(plot_idx)%y_grid(j+1) - end subroutine get_cell_coordinates - - subroutine get_cell_values(self, plot_idx, i, j, z1, z2, z3, z4) - !! Get the data values at the four corners of a grid cell - class(figure_t), intent(in) :: self - integer, intent(in) :: plot_idx, i, j - real(wp), intent(out) :: z1, z2, z3, z4 - - z1 = self%plots(plot_idx)%z_grid(i, j) - z2 = self%plots(plot_idx)%z_grid(i+1, j) - z3 = self%plots(plot_idx)%z_grid(i+1, j+1) - z4 = self%plots(plot_idx)%z_grid(i, j+1) - end subroutine get_cell_values - - subroutine calculate_marching_squares_config(z1, z2, z3, z4, level, config) - !! Calculate marching squares configuration for a cell - real(wp), intent(in) :: z1, z2, z3, z4, level - integer, intent(out) :: config - - config = 0 - if (z1 >= level) config = config + 1 - if (z2 >= level) config = config + 2 - if (z3 >= level) config = config + 4 - if (z4 >= level) config = config + 8 - end subroutine calculate_marching_squares_config - - subroutine get_contour_lines(config, x1, y1, x2, y2, x3, y3, x4, y4, & - z1, z2, z3, z4, level, line_points, num_lines) - !! Get contour line segments for a cell based on marching squares configuration - integer, intent(in) :: config - real(wp), intent(in) :: x1, y1, x2, y2, x3, y3, x4, y4 - real(wp), intent(in) :: z1, z2, z3, z4, level - real(wp), dimension(8), intent(out) :: line_points - integer, intent(out) :: num_lines - real(wp) :: xa, ya, xb, yb, xc, yc, xd, yd - - call interpolate_edge_crossings(x1, y1, x2, y2, x3, y3, x4, y4, & - z1, z2, z3, z4, level, xa, ya, xb, yb, xc, yc, xd, yd) - call apply_marching_squares_lookup(config, xa, ya, xb, yb, xc, yc, xd, yd, line_points, num_lines) - end subroutine get_contour_lines - - subroutine interpolate_edge_crossings(x1, y1, x2, y2, x3, y3, x4, y4, & - z1, z2, z3, z4, level, xa, ya, xb, yb, xc, yc, xd, yd) - !! Interpolate where contour level crosses cell edges - real(wp), intent(in) :: x1, y1, x2, y2, x3, y3, x4, y4 - real(wp), intent(in) :: z1, z2, z3, z4, level - real(wp), intent(out) :: xa, ya, xb, yb, xc, yc, xd, yd - - ! Edge 1-2 (bottom) - if (abs(z2 - z1) > 1e-10_wp) then - xa = x1 + (level - z1) / (z2 - z1) * (x2 - x1) - ya = y1 + (level - z1) / (z2 - z1) * (y2 - y1) - else - xa = (x1 + x2) * 0.5_wp - ya = (y1 + y2) * 0.5_wp - end if - - ! Edge 2-3 (right) - if (abs(z3 - z2) > 1e-10_wp) then - xb = x2 + (level - z2) / (z3 - z2) * (x3 - x2) - yb = y2 + (level - z2) / (z3 - z2) * (y3 - y2) - else - xb = (x2 + x3) * 0.5_wp - yb = (y2 + y3) * 0.5_wp - end if - - ! Edge 3-4 (top) - if (abs(z4 - z3) > 1e-10_wp) then - xc = x3 + (level - z3) / (z4 - z3) * (x4 - x3) - yc = y3 + (level - z3) / (z4 - z3) * (y4 - y3) - else - xc = (x3 + x4) * 0.5_wp - yc = (y3 + y4) * 0.5_wp - end if - - ! Edge 4-1 (left) - if (abs(z1 - z4) > 1e-10_wp) then - xd = x4 + (level - z4) / (z1 - z4) * (x1 - x4) - yd = y4 + (level - z4) / (z1 - z4) * (y1 - y4) - else - xd = (x4 + x1) * 0.5_wp - yd = (y4 + y1) * 0.5_wp - end if - end subroutine interpolate_edge_crossings - - subroutine apply_marching_squares_lookup(config, xa, ya, xb, yb, xc, yc, xd, yd, line_points, num_lines) - !! Apply marching squares lookup table to get line segments - integer, intent(in) :: config - real(wp), intent(in) :: xa, ya, xb, yb, xc, yc, xd, yd - real(wp), dimension(8), intent(out) :: line_points - integer, intent(out) :: num_lines - - num_lines = 0 - line_points = 0.0_wp - - select case (config) - case (1, 14) - line_points(1:4) = [xa, ya, xd, yd] - num_lines = 1 - case (2, 13) - line_points(1:4) = [xa, ya, xb, yb] - num_lines = 1 - case (3, 12) - line_points(1:4) = [xd, yd, xb, yb] - num_lines = 1 - case (4, 11) - line_points(1:4) = [xb, yb, xc, yc] - num_lines = 1 - case (5) - line_points(1:8) = [xa, ya, xd, yd, xb, yb, xc, yc] - num_lines = 2 - case (6, 9) - line_points(1:4) = [xa, ya, xc, yc] - num_lines = 1 - case (7, 8) - line_points(1:4) = [xd, yd, xc, yc] - num_lines = 1 - case (10) - line_points(1:8) = [xa, ya, xb, yb, xc, yc, xd, yd] - num_lines = 2 - case default - num_lines = 0 - end select - end subroutine apply_marching_squares_lookup - - subroutine draw_contour_lines(self, line_points, num_lines) - !! Draw the contour line segments with proper coordinate transformation - class(figure_t), intent(inout) :: self - real(wp), dimension(8), intent(in) :: line_points - integer, intent(in) :: num_lines - integer :: i - real(wp) :: x1_trans, y1_trans, x2_trans, y2_trans - - do i = 1, num_lines - ! Apply scale transformations to contour line endpoints - x1_trans = apply_scale_transform(line_points(4*i-3), self%xscale, self%symlog_threshold) - y1_trans = apply_scale_transform(line_points(4*i-2), self%yscale, self%symlog_threshold) - x2_trans = apply_scale_transform(line_points(4*i-1), self%xscale, self%symlog_threshold) - y2_trans = apply_scale_transform(line_points(4*i), self%yscale, self%symlog_threshold) - - call self%backend%line(x1_trans, y1_trans, x2_trans, y2_trans) - end do - end subroutine draw_contour_lines - - subroutine draw_line_with_style(self, plot_idx, linestyle) - !! Draw line segments with specified linestyle pattern - use fortplot_raster, only: raster_context - use fortplot_png, only: png_context - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - character(len=*), intent(in) :: linestyle - - ! For raster-based backends (PNG, raster), set the line style to enable pattern support - select type (backend => self%backend) - type is (png_context) - ! PNG backend inherits from raster_context and has pattern support - call backend%set_line_style(linestyle) - ! Just draw the segments - the backend will handle patterns - call render_solid_line(self, plot_idx) - type is (raster_context) - ! Base raster backend has built-in pattern support via draw_styled_line - call backend%set_line_style(linestyle) - ! Just draw the segments - the backend will handle patterns - call render_solid_line(self, plot_idx) - class default - ! Other backends (PDF, ASCII): use manual pattern implementation - ! since they don't have set_line_style method - if (linestyle == '-' .or. linestyle == 'solid') then - call render_solid_line(self, plot_idx) - else - call render_patterned_line(self, plot_idx, linestyle) - end if - end select - end subroutine draw_line_with_style + end subroutine render_all_plots - subroutine render_solid_line(self, plot_idx) - !! Render solid line by drawing all segments, breaking on NaN values - use, intrinsic :: ieee_arithmetic, only: ieee_is_nan - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - integer :: i - real(wp) :: x1_screen, y1_screen, x2_screen, y2_screen + function get_file_extension(filename) result(ext) + !! Extract file extension from filename + character(len=*), intent(in) :: filename + character(len=:), allocatable :: ext - do i = 1, size(self%plots(plot_idx)%x) - 1 - ! Skip segment if either point contains NaN - if (ieee_is_nan(self%plots(plot_idx)%x(i)) .or. ieee_is_nan(self%plots(plot_idx)%y(i)) .or. & - ieee_is_nan(self%plots(plot_idx)%x(i+1)) .or. ieee_is_nan(self%plots(plot_idx)%y(i+1))) then - cycle - end if - - ! Apply scale transformations - x1_screen = apply_scale_transform(self%plots(plot_idx)%x(i), self%xscale, self%symlog_threshold) - y1_screen = apply_scale_transform(self%plots(plot_idx)%y(i), self%yscale, self%symlog_threshold) - x2_screen = apply_scale_transform(self%plots(plot_idx)%x(i+1), self%xscale, self%symlog_threshold) - y2_screen = apply_scale_transform(self%plots(plot_idx)%y(i+1), self%yscale, self%symlog_threshold) - - - call self%backend%line(x1_screen, y1_screen, x2_screen, y2_screen) - end do - end subroutine render_solid_line - - subroutine render_patterned_line(self, plot_idx, linestyle) - !! Render line with continuous pattern across segments (matplotlib-style) - use, intrinsic :: ieee_arithmetic, only: ieee_is_nan - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - character(len=*), intent(in) :: linestyle - - real(wp) :: current_distance, segment_length - real(wp) :: dash_len, dot_len, gap_len - real(wp) :: pattern(20), pattern_length - integer :: pattern_size, pattern_index - logical :: drawing - integer :: i, valid_count - real(wp) :: x1_screen, y1_screen, x2_screen, y2_screen, dx, dy - - ! Get transformed data range for proper pattern scaling - real(wp) :: x_range, y_range, plot_scale - real(wp), allocatable :: x_trans(:), y_trans(:) - logical, allocatable :: valid_points(:) - - ! Transform all data points to get proper scaling - allocate(x_trans(size(self%plots(plot_idx)%x))) - allocate(y_trans(size(self%plots(plot_idx)%y))) - allocate(valid_points(size(self%plots(plot_idx)%x))) - - valid_count = 0 - do i = 1, size(self%plots(plot_idx)%x) - valid_points(i) = .not. (ieee_is_nan(self%plots(plot_idx)%x(i)) .or. ieee_is_nan(self%plots(plot_idx)%y(i))) - if (valid_points(i)) then - x_trans(i) = apply_scale_transform(self%plots(plot_idx)%x(i), self%xscale, self%symlog_threshold) - y_trans(i) = apply_scale_transform(self%plots(plot_idx)%y(i), self%yscale, self%symlog_threshold) - valid_count = valid_count + 1 - else - x_trans(i) = 0.0_wp - y_trans(i) = 0.0_wp - end if - end do + integer :: dot_pos - ! Handle case where all points are NaN - if (valid_count > 0) then - x_range = maxval(x_trans, mask=valid_points) - minval(x_trans, mask=valid_points) - y_range = maxval(y_trans, mask=valid_points) - minval(y_trans, mask=valid_points) - plot_scale = max(x_range, y_range) + dot_pos = index(filename, '.', back=.true.) + if (dot_pos > 0 .and. dot_pos < len_trim(filename)) then + ext = filename(dot_pos+1:) else - ! All points are NaN, use default scale - plot_scale = 1.0_wp - end if - - ! Define pattern lengths (matplotlib-like) - dash_len = plot_scale * 0.03_wp ! 3% of range - dot_len = plot_scale * 0.005_wp ! 0.5% of range - gap_len = plot_scale * 0.015_wp ! 1.5% of range - - ! Define patterns like matplotlib - select case (trim(linestyle)) - case ('--') - ! Dashed: [dash, gap, dash, gap, ...] - pattern_size = 2 - pattern(1) = dash_len ! dash - pattern(2) = gap_len ! gap - - case (':') - ! Dotted: [dot, gap, dot, gap, ...] - pattern_size = 2 - pattern(1) = dot_len ! dot - pattern(2) = gap_len ! gap - - case ('-.') - ! Dash-dot: [dash, gap, dot, gap, dash, gap, dot, gap, ...] - pattern_size = 4 - pattern(1) = dash_len ! dash - pattern(2) = gap_len ! gap - pattern(3) = dot_len ! dot - pattern(4) = gap_len ! gap - - case default - ! Unknown pattern, fall back to solid - call render_solid_line(self, plot_idx) - deallocate(x_trans, y_trans) - return - end select - - ! Calculate total pattern length - pattern_length = sum(pattern(1:pattern_size)) - - ! Render with continuous pattern - current_distance = 0.0_wp - pattern_index = 1 - drawing = .true. ! Start drawing - - do i = 1, size(self%plots(plot_idx)%x) - 1 - ! Skip segment if either point is invalid (NaN) - if (.not. valid_points(i) .or. .not. valid_points(i+1)) then - ! Reset pattern state when encountering NaN - current_distance = 0.0_wp - pattern_index = 1 - drawing = .true. - cycle - end if - - x1_screen = x_trans(i) - y1_screen = y_trans(i) - x2_screen = x_trans(i+1) - y2_screen = y_trans(i+1) - - dx = x2_screen - x1_screen - dy = y2_screen - y1_screen - segment_length = sqrt(dx*dx + dy*dy) - - if (segment_length < 1e-10_wp) cycle - - call render_segment_with_pattern(self, x1_screen, y1_screen, x2_screen, y2_screen, segment_length, & - pattern, pattern_size, pattern_length, & - current_distance, pattern_index, drawing) - end do - - ! Clean up - deallocate(x_trans, y_trans, valid_points) - end subroutine render_patterned_line - - subroutine render_segment_with_pattern(self, x1, y1, x2, y2, segment_length, & - pattern, pattern_size, pattern_length, & - current_distance, pattern_index, drawing) - !! Render single segment with continuous pattern state - class(figure_t), intent(inout) :: self - real(wp), intent(in) :: x1, y1, x2, y2, segment_length - real(wp), intent(in) :: pattern(:), pattern_length - integer, intent(in) :: pattern_size - real(wp), intent(inout) :: current_distance - integer, intent(inout) :: pattern_index - logical, intent(inout) :: drawing - - real(wp) :: dx, dy, remaining_distance, pattern_remaining - real(wp) :: t_start, t_end, seg_x1, seg_y1, seg_x2, seg_y2 - - dx = x2 - x1 - dy = y2 - y1 - remaining_distance = segment_length - t_start = 0.0_wp - - do while (remaining_distance > 1e-10_wp) - ! How much of current pattern element is left? - pattern_remaining = pattern(pattern_index) - current_distance - - if (pattern_remaining <= remaining_distance) then - ! Complete this pattern element within current segment - t_end = t_start + pattern_remaining / segment_length - - if (drawing) then - seg_x1 = x1 + t_start * dx - seg_y1 = y1 + t_start * dy - seg_x2 = x1 + t_end * dx - seg_y2 = y1 + t_end * dy - call self%backend%line(seg_x1, seg_y1, seg_x2, seg_y2) - end if - - ! Move to next pattern element - remaining_distance = remaining_distance - pattern_remaining - t_start = t_end - current_distance = 0.0_wp - pattern_index = mod(pattern_index, pattern_size) + 1 - drawing = .not. drawing ! Alternate between drawing and not drawing - else - ! Pattern element extends beyond this segment - t_end = 1.0_wp - - if (drawing) then - seg_x1 = x1 + t_start * dx - seg_y1 = y1 + t_start * dy - seg_x2 = x2 - seg_y2 = y2 - call self%backend%line(seg_x1, seg_y1, seg_x2, seg_y2) - end if - - current_distance = current_distance + remaining_distance - remaining_distance = 0.0_wp - end if - end do - end subroutine render_segment_with_pattern - - subroutine figure_legend(self, location) - !! Add legend to figure following SOLID principles - class(figure_t), intent(inout) :: self - character(len=*), intent(in), optional :: location - integer :: i - - - ! Initialize legend if not already done - if (.not. allocated(self%legend_data%entries)) then - allocate(self%legend_data%entries(0)) - self%legend_data%num_entries = 0 - end if - - ! Set legend position if specified - if (present(location)) then - call self%legend_data%set_position(location) + ext = '' end if - - ! Populate legend with labeled plots (DRY principle) - do i = 1, self%plot_count - if (allocated(self%plots(i)%label)) then - if (len_trim(self%plots(i)%label) > 0) then - call self%legend_data%add_entry(self%plots(i)%label, & - self%plots(i)%color, & - self%plots(i)%linestyle, & - self%plots(i)%marker) - end if - end if - end do - - self%show_legend = .true. - end subroutine figure_legend - - subroutine clear_streamlines(self) - !! Clear streamline data - class(figure_t), intent(inout) :: self - - if (allocated(self%streamlines)) then - deallocate(self%streamlines) - end if - end subroutine clear_streamlines - - subroutine transform_quad_to_screen(self, x_quad, y_quad, x_screen, y_screen) - !! Transform quadrilateral vertices from world to screen coordinates - class(figure_t), intent(in) :: self - real(wp), intent(in) :: x_quad(4), y_quad(4) - real(wp), intent(out) :: x_screen(4), y_screen(4) - - integer :: i - - ! Apply scale transformations only (backend handles screen mapping) - do i = 1, 4 - x_screen(i) = apply_scale_transform(x_quad(i), self%xscale, self%symlog_threshold) - y_screen(i) = apply_scale_transform(y_quad(i), self%yscale, self%symlog_threshold) - end do - end subroutine transform_quad_to_screen - - subroutine draw_filled_quad(backend, x_screen, y_screen) - !! Draw filled quadrilateral - use fortplot_raster, only: raster_context - use fortplot_png, only: png_context - class(plot_context), intent(inout) :: backend - real(wp), intent(in) :: x_screen(4), y_screen(4) - - ! Use backend-specific filled quad rendering - select type (backend) - type is (raster_context) - call backend%fill_quad(x_screen, y_screen) - type is (png_context) - call backend%fill_quad(x_screen, y_screen) - class default - ! Fallback: draw wireframe for unsupported backends - call backend%line(x_screen(1), y_screen(1), x_screen(2), y_screen(2)) - call backend%line(x_screen(2), y_screen(2), x_screen(3), y_screen(3)) - call backend%line(x_screen(3), y_screen(3), x_screen(4), y_screen(4)) - call backend%line(x_screen(4), y_screen(4), x_screen(1), y_screen(1)) - end select - end subroutine draw_filled_quad - - subroutine draw_quad_edges(backend, x_screen, y_screen, line_width) - !! Draw quadrilateral edges - class(plot_context), intent(inout) :: backend - real(wp), intent(in) :: x_screen(4), y_screen(4) - real(wp), intent(in) :: line_width - - ! Draw quad outline - call backend%line(x_screen(1), y_screen(1), x_screen(2), y_screen(2)) - call backend%line(x_screen(2), y_screen(2), x_screen(3), y_screen(3)) - call backend%line(x_screen(3), y_screen(3), x_screen(4), y_screen(4)) - call backend%line(x_screen(4), y_screen(4), x_screen(1), y_screen(1)) - end subroutine draw_quad_edges - - subroutine set_ydata(self, plot_index, y_new) - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_index - real(wp), intent(in) :: y_new(:) - - if (plot_index < 1 .or. plot_index > self%plot_count) then - print *, "Warning: Invalid plot index", plot_index, "for set_ydata" - return - end if - - if (self%plots(plot_index)%plot_type /= PLOT_TYPE_LINE) then - print *, "Warning: set_ydata only supported for line plots" - return - end if - - if (.not. allocated(self%plots(plot_index)%y)) then - print *, "Warning: Plot", plot_index, "has no y data to update" - return - end if - - if (size(y_new) /= size(self%plots(plot_index)%y)) then - print *, "Warning: New y data size", size(y_new), & - "does not match existing size", size(self%plots(plot_index)%y) - return - end if - - self%plots(plot_index)%y = y_new - end subroutine set_ydata + end function get_file_extension end module fortplot_figure_core \ No newline at end of file diff --git a/src/fortplot_matplotlib.f90 b/src/fortplot_matplotlib.f90 index b620c4bf..bf542dec 100644 --- a/src/fortplot_matplotlib.f90 +++ b/src/fortplot_matplotlib.f90 @@ -16,7 +16,7 @@ module fortplot_matplotlib figure_streamplot => streamplot, figure_bar => bar, figure_barh => barh, & figure_hist => hist, figure_errorbar => errorbar, & figure_add_3d_plot => add_3d_plot, figure_add_surface => add_surface - use fortplot_rendering, only: figure_legend, render_show => show, figure_savefig => savefig + ! Type-bound procedures from figure_t are used directly through fig%method() calls use fortplot_logging, only: log_error, log_warning, log_info use fortplot_security, only: safe_launch_viewer, safe_remove_file diff --git a/src/fortplot_plot_data.f90 b/src/fortplot_plot_data.f90 index 1f0df08b..0b516c3d 100644 --- a/src/fortplot_plot_data.f90 +++ b/src/fortplot_plot_data.f90 @@ -93,7 +93,7 @@ module fortplot_plot_data real(wp) :: scatter_vmax = 1.0_wp ! Color scale maximum logical :: scatter_vrange_set = .false. ! Whether vmin/vmax are manually set ! Common properties - real(wp), dimension(3) :: color + real(wp), dimension(3) :: color = [0.0_wp, 0.447_wp, 0.698_wp] ! Default to blue character(len=:), allocatable :: label character(len=:), allocatable :: linestyle character(len=:), allocatable :: marker diff --git a/src/fortplot_rendering.f90 b/src/fortplot_rendering.f90 index 40813de6..7e1a8fc8 100644 --- a/src/fortplot_rendering.f90 +++ b/src/fortplot_rendering.f90 @@ -1,786 +1,512 @@ module fortplot_rendering - !! Rendering pipeline for figure (SOLID principles compliance) + !! Figure rendering pipeline module !! - !! This module contains all rendering-related methods, separated from - !! figure base and plot addition for better modularity. + !! This module handles the rendering pipeline for all plot types, + !! including coordinate transformations and drawing operations. use, intrinsic :: iso_fortran_env, only: wp => real64 - use, intrinsic :: ieee_arithmetic, only: ieee_is_finite - use fortplot_figure_base, only: figure_t - use fortplot_plot_data, only: plot_data_t, & - PLOT_TYPE_LINE, PLOT_TYPE_CONTOUR, PLOT_TYPE_PCOLORMESH, & - PLOT_TYPE_ERRORBAR, PLOT_TYPE_BAR, PLOT_TYPE_HISTOGRAM, & - PLOT_TYPE_BOXPLOT, PLOT_TYPE_SCATTER - use fortplot_scales - use fortplot_axes - use fortplot_legend - use fortplot_pcolormesh - use fortplot_format_parser, only: parse_format_string, contains_format_chars - use fortplot_raster, only: raster_render_ylabel - use fortplot_projection, only: project_3d_to_2d, get_default_view_angles - use fortplot_annotations, only: text_annotation_t, COORD_DATA, COORD_FIGURE, COORD_AXIS - use fortplot_pdf, only: pdf_context + use fortplot_context + use fortplot_scales, only: apply_scale_transform + use fortplot_utils use fortplot_colormap - use fortplot_security, only: is_safe_path - use fortplot_logging, only: log_error, log_info, log_warning - use fortplot_utils, only: get_backend_from_filename, to_lowercase - + use fortplot_contour_algorithms + use fortplot_plot_data + use fortplot_format_parser, only: parse_format_string implicit none - + private - public :: render_figure, savefig, show, figure_legend, render_annotations - public :: clear_streamlines, gather_subplot_plots - + public :: render_line_plot + public :: render_contour_plot + public :: render_pcolormesh_plot + public :: render_markers + public :: draw_line_with_style + public :: render_solid_line + public :: render_patterned_line + public :: transform_quad_to_screen + public :: draw_filled_quad + public :: draw_quad_edges + contains - - subroutine render_figure(self) - !! Main figure rendering pipeline (follows Template Method pattern) - class(figure_t), intent(inout) :: self - - if (self%rendered) return - - ! Calculate data ranges and setup coordinate system - call calculate_figure_data_ranges(self) - call setup_coordinate_system(self) - - ! Render figure components in order - call render_figure_background(self) - call render_figure_axes(self) - call render_all_plots(self) - call render_arrows(self) - call render_streamlines(self) - call render_annotations(self) - - ! Render legend if present - if (self%legend_added) then - call render_figure_legend(self) - end if - - self%rendered = .true. - end subroutine render_figure - - subroutine savefig(self, filename, blocking) - !! Save figure to file with backend auto-detection - use fortplot_utils, only: initialize_backend - class(figure_t), intent(inout) :: self - character(len=*), intent(in) :: filename - logical, intent(in), optional :: blocking - - character(len=20) :: backend_type - logical :: do_block - - ! Validate filename security (Issue #135) - if (.not. is_safe_path(filename)) then - call log_error("Unsafe filename rejected: " // trim(filename)) - return - end if - - ! Default to non-blocking - do_block = .false. - if (present(blocking)) do_block = blocking - - ! Create output directory if needed - ! Note: ensure_directory_exists would be implemented in full version - ! call ensure_directory_exists(filename) - - ! Auto-detect backend from filename extension - backend_type = get_backend_from_filename(filename) - - ! Switch backend if needed - call switch_backend_if_needed(self, backend_type) - - ! Render figure - call render_figure(self) - - ! Save to file - call self%backend%save(filename) - - call log_info("Figure saved to: " // trim(filename)) - - ! Handle blocking behavior - if (do_block) then - call wait_for_user_input() - end if - end subroutine savefig - - subroutine show(self, blocking) - !! Display figure using current backend - class(figure_t), intent(inout) :: self - logical, intent(in), optional :: blocking - - logical :: do_block - - ! Default to non-blocking - do_block = .false. - if (present(blocking)) do_block = blocking - - ! Render figure - call render_figure(self) - - ! Display the figure (save to ASCII output for terminal display) - call self%backend%save('fortplot_output.txt') - - ! Handle blocking behavior - if (do_block) then - call wait_for_user_input() - end if - end subroutine show - - subroutine gather_subplot_plots(self) - !! Gather all subplot plots into main plots array for rendering - class(figure_t), intent(inout) :: self - integer :: subplot_idx, plot_idx, total_idx - - ! Always gather plots from subplots into main plots array - total_idx = 0 - - if (allocated(self%subplots)) then - do subplot_idx = 1, self%subplot_rows * self%subplot_cols - do plot_idx = 1, self%subplots(subplot_idx)%plot_count - total_idx = total_idx + 1 - if (total_idx <= self%max_plots) then - self%plots(total_idx) = self%subplots(subplot_idx)%plots(plot_idx) - end if - end do - end do - end if - - self%plot_count = total_idx - end subroutine gather_subplot_plots - - subroutine calculate_figure_data_ranges(self) - !! Calculate overall data ranges from all plots - class(figure_t), intent(inout) :: self - integer :: i - logical :: first_plot - - ! Gather subplot data if needed - call gather_subplot_plots(self) - - if (self%plot_count == 0) return + + subroutine render_line_plot(backend, plot_data, plot_idx, x_min_t, x_max_t, y_min_t, y_max_t, xscale, yscale, symlog_threshold) + !! Render a line plot with proper scaling and clipping + class(plot_context), intent(inout) :: backend + type(plot_data_t), intent(in) :: plot_data + integer, intent(in) :: plot_idx + real(wp), intent(in) :: x_min_t, x_max_t, y_min_t, y_max_t + character(len=*), intent(in) :: xscale, yscale + real(wp), intent(in) :: symlog_threshold - first_plot = .true. + real(wp), allocatable :: x_scaled(:), y_scaled(:) + integer :: i, n - do i = 1, self%plot_count - call update_ranges_from_plot(self, i, first_plot) - first_plot = .false. - end do + n = size(plot_data%x) + allocate(x_scaled(n), y_scaled(n)) - ! Handle pcolormesh data ranges separately - call update_data_ranges_pcolormesh(self) - call update_data_ranges_boxplot(self) - - ! Transform ranges based on scale settings - call transform_axis_ranges(self) - end subroutine calculate_figure_data_ranges - - subroutine setup_coordinate_system(self) - !! Setup backend coordinate system - class(figure_t), intent(inout) :: self - - call self%backend%set_coordinates(self%x_min_transformed, self%x_max_transformed, & - self%y_min_transformed, self%y_max_transformed) - end subroutine setup_coordinate_system - - subroutine render_figure_background(self) - !! Render figure background - class(figure_t), intent(inout) :: self - - ! Backend initialization handles clearing - ! No explicit clear needed as setup_canvas initializes clean backend - end subroutine render_figure_background - - subroutine render_figure_axes(self) - !! Render figure axes, labels, and title - class(figure_t), intent(inout) :: self - - ! Render axes with ticks - call render_axis_framework(self) - - ! Render labels - call render_axis_labels(self) - - ! Render title using text method - if (allocated(self%title)) then - ! Place title at top center of the plot area - call self%backend%text((self%x_min_transformed + self%x_max_transformed) / 2.0_wp, & - self%y_max_transformed + 0.05_wp * (self%y_max_transformed - self%y_min_transformed), & - self%title) - end if - end subroutine render_figure_axes - - subroutine render_all_plots(self) - !! Render all plots in the figure - class(figure_t), intent(inout) :: self - integer :: i, subplot_idx - - ! Get current subplot (default to 1) - subplot_idx = max(1, self%current_subplot) - if (.not. allocated(self%subplots)) return - if (subplot_idx > size(self%subplots)) return - - ! Render all plots in the current subplot - do i = 1, self%subplots(subplot_idx)%plot_count - call render_single_plot(self, i) + ! Transform coordinates based on scale + do i = 1, n + x_scaled(i) = apply_scale_transform(plot_data%x(i), xscale, symlog_threshold) + y_scaled(i) = apply_scale_transform(plot_data%y(i), yscale, symlog_threshold) end do - end subroutine render_all_plots - - subroutine render_arrows(self) - !! Render arrow annotations - class(figure_t), intent(inout) :: self - integer :: i - if (.not. allocated(self%arrow_data)) return + ! Set color + call backend%color(plot_data%color(1), plot_data%color(2), plot_data%color(3)) - do i = 1, size(self%arrow_data) - call render_single_arrow(self, i) + ! Draw the line segments + do i = 1, n-1 + call backend%line(x_scaled(i), y_scaled(i), x_scaled(i+1), y_scaled(i+1)) end do - end subroutine render_arrows - - subroutine render_streamlines(self) - !! Render streamline data - class(figure_t), intent(inout) :: self - integer :: i - if (.not. allocated(self%streamlines)) return - - do i = 1, size(self%streamlines) - call render_streamline(self, i) - end do - end subroutine render_streamlines - - subroutine render_annotations(self) - !! Render text annotations (Issue #184) - class(figure_t), intent(inout) :: self - - ! Note: Full annotation rendering would be implemented in complete version - ! For now, just stub to satisfy interface - if (.not. allocated(self%annotations)) return - ! Stub: annotation rendering logic would go here - end subroutine render_annotations - - subroutine figure_legend(self, location) - !! Add legend to figure - class(figure_t), intent(inout) :: self - character(len=*), intent(in), optional :: location + deallocate(x_scaled, y_scaled) + end subroutine render_line_plot + + subroutine render_markers(backend, plot_data, x_min_t, x_max_t, y_min_t, y_max_t, xscale, yscale, symlog_threshold) + !! Render markers for a plot + class(plot_context), intent(inout) :: backend + type(plot_data_t), intent(in) :: plot_data + real(wp), intent(in) :: x_min_t, x_max_t, y_min_t, y_max_t + character(len=*), intent(in) :: xscale, yscale + real(wp), intent(in) :: symlog_threshold + + real(wp) :: x_scaled, y_scaled integer :: i - if (present(location)) then - call parse_legend_location(location, self%legend_location) - end if - - self%legend_added = .true. + if (.not. allocated(plot_data%marker)) return + if (len_trim(plot_data%marker) == 0) return - ! Gather subplot plots into main plots array first - call gather_subplot_plots(self) + ! Draw markers + call backend%color(plot_data%color(1), plot_data%color(2), plot_data%color(3)) - ! Initialize legend data immediately - if (.not. allocated(self%legend_data%entries)) then - allocate(self%legend_data%entries(0)) - self%legend_data%num_entries = 0 + do i = 1, size(plot_data%x) + x_scaled = apply_scale_transform(plot_data%x(i), xscale, symlog_threshold) + y_scaled = apply_scale_transform(plot_data%y(i), yscale, symlog_threshold) + call backend%draw_marker(x_scaled, y_scaled, plot_data%marker) + end do + end subroutine render_markers + + subroutine render_contour_plot(backend, plot_data, x_min_t, x_max_t, y_min_t, y_max_t, & + xscale, yscale, symlog_threshold, width, height, & + margin_left, margin_right, margin_bottom, margin_top) + !! Render a contour plot + class(plot_context), intent(inout) :: backend + type(plot_data_t), intent(in) :: plot_data + real(wp), intent(in) :: x_min_t, x_max_t, y_min_t, y_max_t + character(len=*), intent(in) :: xscale, yscale + real(wp), intent(in) :: symlog_threshold + integer, intent(in) :: width, height + real(wp), intent(in) :: margin_left, margin_right, margin_bottom, margin_top + + real(wp) :: z_min, z_max + real(wp), dimension(3) :: level_color + integer :: i, j, nx, ny, nlev + real(wp) :: level + + ! Get data ranges + z_min = minval(plot_data%z_grid) + z_max = maxval(plot_data%z_grid) + + nx = size(plot_data%x_grid) + ny = size(plot_data%y_grid) + + ! Render contour levels + if (allocated(plot_data%contour_levels)) then + nlev = size(plot_data%contour_levels) + do i = 1, nlev + level = plot_data%contour_levels(i) + + ! Set color based on contour level if using color levels + if (plot_data%use_color_levels) then + call colormap_value_to_color(level, z_min, z_max, & + plot_data%colormap, level_color) + call backend%color(level_color(1), level_color(2), level_color(3)) + else + call backend%color(plot_data%color(1), plot_data%color(2), plot_data%color(3)) + end if + + ! Trace this contour level + call trace_contour_level(backend, plot_data, level, xscale, yscale, & + symlog_threshold, x_min_t, x_max_t, y_min_t, y_max_t) + end do + else + ! Use default 3 levels + call render_default_contour_levels(backend, plot_data, z_min, z_max, & + xscale, yscale, symlog_threshold, & + x_min_t, x_max_t, y_min_t, y_max_t) end if - ! Populate legend with labeled plots from main plots array - ! Normal figures store plots in self%plots with self%plot_count - do i = 1, self%plot_count - if (allocated(self%plots(i)%label)) then - if (len_trim(self%plots(i)%label) > 0) then - call self%legend_data%add_entry(self%plots(i)%label, & - self%plots(i)%color, & - self%plots(i)%linestyle, & - self%plots(i)%marker) - end if - end if - end do - end subroutine figure_legend - - subroutine render_figure_legend(self) - !! Render legend following SOLID principles - !! Position and render the pre-populated legend - class(figure_t), intent(inout) :: self + ! Colorbar rendering handled elsewhere if needed + end subroutine render_contour_plot + + subroutine render_default_contour_levels(backend, plot_data, z_min, z_max, & + xscale, yscale, symlog_threshold, & + x_min_t, x_max_t, y_min_t, y_max_t) + !! Render default contour levels + class(plot_context), intent(inout) :: backend + type(plot_data_t), intent(in) :: plot_data + real(wp), intent(in) :: z_min, z_max + character(len=*), intent(in) :: xscale, yscale + real(wp), intent(in) :: symlog_threshold + real(wp), intent(in) :: x_min_t, x_max_t, y_min_t, y_max_t + + real(wp), dimension(3) :: level_color + real(wp) :: level_values(3) integer :: i - ! Initialize legend if not already done - if (.not. allocated(self%legend_data%entries)) then - allocate(self%legend_data%entries(0)) - self%legend_data%num_entries = 0 - end if - - ! Set legend position based on legend_location - ! Match actual constants: LEGEND_UPPER_LEFT=1, LEGEND_UPPER_RIGHT=2, etc. - select case(self%legend_location) - case(1) - self%legend_data%position = LEGEND_UPPER_LEFT - case(2) - self%legend_data%position = LEGEND_UPPER_RIGHT - case(3) - self%legend_data%position = LEGEND_LOWER_LEFT - case(4) - self%legend_data%position = LEGEND_LOWER_RIGHT - case default - self%legend_data%position = LEGEND_UPPER_RIGHT - end select + level_values = [z_min + 0.2_wp * (z_max - z_min), & + z_min + 0.5_wp * (z_max - z_min), & + z_min + 0.8_wp * (z_max - z_min)] - ! Populate legend with labeled plots from main plots array - ! Normal figures store plots in self%plots with self%plot_count - do i = 1, self%plot_count - if (allocated(self%plots(i)%label)) then - if (len_trim(self%plots(i)%label) > 0) then - call self%legend_data%add_entry(self%plots(i)%label, & - self%plots(i)%color, & - self%plots(i)%linestyle, & - self%plots(i)%marker) - end if + do i = 1, 3 + if (plot_data%use_color_levels) then + call colormap_value_to_color(level_values(i), z_min, z_max, & + plot_data%colormap, level_color) + call backend%color(level_color(1), level_color(2), level_color(3)) end if + + call trace_contour_level(backend, plot_data, level_values(i), & + xscale, yscale, symlog_threshold, & + x_min_t, x_max_t, y_min_t, y_max_t) end do - - ! Render legend if we have entries - if (self%legend_data%num_entries > 0) then - call legend_render(self%legend_data, self%backend) - end if - end subroutine render_figure_legend - - subroutine clear_streamlines(self) - !! Clear streamline data - class(figure_t), intent(inout) :: self - - if (allocated(self%streamlines)) then - deallocate(self%streamlines) - end if - end subroutine clear_streamlines - - ! Private implementation subroutines - - function symlog_transform(x, threshold) result(transformed) - !! Apply symlog transformation - real(wp), intent(in) :: x, threshold - real(wp) :: transformed - - if (abs(x) <= threshold) then - transformed = x - else if (x > 0) then - transformed = threshold * (1.0_wp + log10(x / threshold)) - else - transformed = -threshold * (1.0_wp + log10(-x / threshold)) + end subroutine render_default_contour_levels + + subroutine trace_contour_level(backend, plot_data, level, xscale, yscale, & + symlog_threshold, x_min_t, x_max_t, y_min_t, y_max_t) + !! Trace a single contour level using marching squares + class(plot_context), intent(inout) :: backend + type(plot_data_t), intent(in) :: plot_data + real(wp), intent(in) :: level + character(len=*), intent(in) :: xscale, yscale + real(wp), intent(in) :: symlog_threshold + real(wp), intent(in) :: x_min_t, x_max_t, y_min_t, y_max_t + + integer :: nx, ny, i, j + + nx = size(plot_data%x_grid) + ny = size(plot_data%y_grid) + + do i = 1, nx-1 + do j = 1, ny-1 + call process_contour_cell(backend, plot_data, i, j, level, & + xscale, yscale, symlog_threshold) + end do + end do + end subroutine trace_contour_level + + subroutine process_contour_cell(backend, plot_data, i, j, level, xscale, yscale, symlog_threshold) + !! Process a single grid cell for contour extraction + class(plot_context), intent(inout) :: backend + type(plot_data_t), intent(in) :: plot_data + integer, intent(in) :: i, j + real(wp), intent(in) :: level + character(len=*), intent(in) :: xscale, yscale + real(wp), intent(in) :: symlog_threshold + + real(wp) :: x1, y1, x2, y2, x3, y3, x4, y4 + real(wp) :: z1, z2, z3, z4 + integer :: config + real(wp), dimension(8) :: line_points + integer :: num_lines + + ! Get cell coordinates and values + x1 = plot_data%x_grid(i) + y1 = plot_data%y_grid(j) + x2 = plot_data%x_grid(i+1) + y2 = plot_data%y_grid(j) + x3 = plot_data%x_grid(i+1) + y3 = plot_data%y_grid(j+1) + x4 = plot_data%x_grid(i) + y4 = plot_data%y_grid(j+1) + + z1 = plot_data%z_grid(i, j) + z2 = plot_data%z_grid(i+1, j) + z3 = plot_data%z_grid(i+1, j+1) + z4 = plot_data%z_grid(i, j+1) + + call calculate_marching_squares_config(z1, z2, z3, z4, level, config) + call get_contour_lines(config, x1, y1, x2, y2, x3, y3, x4, y4, & + z1, z2, z3, z4, level, line_points, num_lines) + + ! Draw contour lines + if (num_lines > 0) then + call draw_contour_lines(backend, line_points, num_lines, xscale, yscale, symlog_threshold) end if - end function symlog_transform - - subroutine switch_backend_if_needed(self, target_backend) - !! Switch backend if current doesn't match target - use fortplot_utils, only: initialize_backend - use fortplot_ascii, only: ascii_context - use fortplot_pdf, only: pdf_context - use fortplot_png, only: png_context - use fortplot_raster, only: raster_context - class(figure_t), intent(inout) :: self - character(len=*), intent(in) :: target_backend - - character(len=20) :: current_backend - - ! Detect current backend type using polymorphic type checking - current_backend = 'unknown' - if (allocated(self%backend)) then - select type (backend => self%backend) - type is (ascii_context) - current_backend = 'ascii' - type is (pdf_context) - current_backend = 'pdf' - type is (png_context) - current_backend = 'png' - type is (raster_context) - current_backend = 'png' ! Treat base raster as PNG - class default - current_backend = 'ascii' ! Default fallback - end select - else - current_backend = 'ascii' ! Default for unallocated backend + end subroutine process_contour_cell + + subroutine draw_contour_lines(backend, line_points, num_lines, xscale, yscale, symlog_threshold) + !! Draw contour line segments + class(plot_context), intent(inout) :: backend + real(wp), intent(in) :: line_points(8) + integer, intent(in) :: num_lines + character(len=*), intent(in) :: xscale, yscale + real(wp), intent(in) :: symlog_threshold + + real(wp) :: x1, y1, x2, y2 + + if (num_lines >= 1) then + x1 = apply_scale_transform(line_points(1), xscale, symlog_threshold) + y1 = apply_scale_transform(line_points(2), yscale, symlog_threshold) + x2 = apply_scale_transform(line_points(3), xscale, symlog_threshold) + y2 = apply_scale_transform(line_points(4), yscale, symlog_threshold) + + call backend%line(x1, y1, x2, y2) end if - ! PNG backend switching is now enabled - ! Previous workaround for raster backend corruption has been resolved - - if (trim(current_backend) /= trim(target_backend)) then - ! Destroy current backend - if (allocated(self%backend)) deallocate(self%backend) - - ! Create new backend - call initialize_backend(self%backend, target_backend, self%width, self%height) + if (num_lines >= 2) then + x1 = apply_scale_transform(line_points(5), xscale, symlog_threshold) + y1 = apply_scale_transform(line_points(6), yscale, symlog_threshold) + x2 = apply_scale_transform(line_points(7), xscale, symlog_threshold) + y2 = apply_scale_transform(line_points(8), yscale, symlog_threshold) - ! Reset rendered flag - self%rendered = .false. + call backend%line(x1, y1, x2, y2) end if - end subroutine switch_backend_if_needed - - subroutine wait_for_user_input() - !! Wait for user input when blocking - character(len=1) :: dummy - - write(*,'(A)', advance='no') 'Press Enter to continue...' - read(*,'(A)') dummy - end subroutine wait_for_user_input - - subroutine update_ranges_from_plot(self, plot_idx, first_plot) - !! Update figure ranges from single plot - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - logical, intent(in) :: first_plot - - type(plot_data_t) :: plot - - plot = self%plots(plot_idx) - - select case (plot%plot_type) - case (PLOT_TYPE_LINE, PLOT_TYPE_SCATTER) - call update_ranges_from_line_plot(self, plot, first_plot) - case (PLOT_TYPE_CONTOUR) - call update_ranges_from_contour_plot(self, plot, first_plot) - case (PLOT_TYPE_BAR, PLOT_TYPE_HISTOGRAM) - call update_ranges_from_bar_plot(self, plot, first_plot) - end select - end subroutine update_ranges_from_plot - - subroutine render_single_plot(self, plot_idx) - !! Render a single plot based on its type - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - - type(plot_data_t) :: plot - integer :: subplot_idx - - ! Get current subplot (default to 1) - subplot_idx = max(1, self%current_subplot) - if (.not. allocated(self%subplots)) return - if (subplot_idx > size(self%subplots)) return - if (plot_idx < 1 .or. plot_idx > self%subplots(subplot_idx)%plot_count) return - - plot = self%subplots(subplot_idx)%plots(plot_idx) - - select case (plot%plot_type) - case (PLOT_TYPE_LINE) - if (allocated(plot%z)) then - call render_3d_line_plot(self, plot_idx) - else - call render_line_plot(self, plot_idx) - end if - case (PLOT_TYPE_SCATTER) - call render_scatter_plot(self, plot_idx) - case (PLOT_TYPE_CONTOUR) - call render_contour_plot(self, plot_idx) - case (PLOT_TYPE_PCOLORMESH) - call render_pcolormesh_plot(self, plot_idx) - case (PLOT_TYPE_BAR, PLOT_TYPE_HISTOGRAM) - call render_bar_plot(self, plot_idx) - case (PLOT_TYPE_ERRORBAR) - call render_errorbar_plot(self, plot_idx) - case (PLOT_TYPE_BOXPLOT) - call render_boxplot(self, plot_idx) - end select - end subroutine render_single_plot - - ! Stub implementations for missing subroutines - ! Note: These would contain full implementations in the complete version - - subroutine render_axis_framework(self) - !! Render axis framework (axes, ticks, grid) - class(figure_t), intent(inout) :: self - logical :: has_3d - integer :: i - - ! Determine if we have any 3D plots - has_3d = .false. - if (allocated(self%plots)) then - do i = 1, self%plot_count - if (self%plots(i)%is_3d()) then - has_3d = .true. - exit + end subroutine draw_contour_lines + + subroutine render_pcolormesh_plot(backend, plot_data, x_min_t, x_max_t, y_min_t, y_max_t, & + xscale, yscale, symlog_threshold, width, height, margin_right) + !! Render a pcolormesh plot + class(plot_context), intent(inout) :: backend + type(plot_data_t), intent(in) :: plot_data + real(wp), intent(in) :: x_min_t, x_max_t, y_min_t, y_max_t + character(len=*), intent(in) :: xscale, yscale + real(wp), intent(in) :: symlog_threshold + integer, intent(in) :: width, height + real(wp), intent(in) :: margin_right + + real(wp) :: x_quad(4), y_quad(4), x_screen(4), y_screen(4) + real(wp), dimension(3) :: quad_color + real(wp) :: c_value, vmin, vmax + integer :: i, j, nx, ny + + nx = size(plot_data%pcolormesh_data%c_values, 2) + ny = size(plot_data%pcolormesh_data%c_values, 1) + + vmin = plot_data%pcolormesh_data%vmin + vmax = plot_data%pcolormesh_data%vmax + + ! Render each quad + do i = 1, nx + do j = 1, ny + ! Get quad corners from vertices arrays + x_quad = [plot_data%pcolormesh_data%x_vertices(j, i), & + plot_data%pcolormesh_data%x_vertices(j, i+1), & + plot_data%pcolormesh_data%x_vertices(j+1, i+1), & + plot_data%pcolormesh_data%x_vertices(j+1, i)] + + y_quad = [plot_data%pcolormesh_data%y_vertices(j, i), & + plot_data%pcolormesh_data%y_vertices(j, i+1), & + plot_data%pcolormesh_data%y_vertices(j+1, i+1), & + plot_data%pcolormesh_data%y_vertices(j+1, i)] + + ! Transform to screen coordinates + call transform_quad_to_screen(x_quad, y_quad, x_screen, y_screen, & + xscale, yscale, symlog_threshold) + + ! Get color for this quad + c_value = plot_data%pcolormesh_data%c_values(j, i) + call colormap_value_to_color(c_value, vmin, vmax, & + plot_data%pcolormesh_data%colormap_name, quad_color) + + ! Draw filled quad + call backend%color(quad_color(1), quad_color(2), quad_color(3)) + call draw_filled_quad(backend, x_screen, y_screen) + + ! Draw edges if requested + if (plot_data%pcolormesh_data%show_edges) then + call backend%color(plot_data%pcolormesh_data%edge_color(1), & + plot_data%pcolormesh_data%edge_color(2), & + plot_data%pcolormesh_data%edge_color(3)) + call draw_quad_edges(backend, x_screen, y_screen, & + plot_data%pcolormesh_data%edge_width) end if end do - end if + end do - ! Pass tick configuration to backend if it's a PDF context (Issue #238) - select type (backend => self%backend) - type is (pdf_context) - backend%x_tick_count = self%x_tick_count - backend%y_tick_count = self%y_tick_count - end select + ! Colorbar rendering handled elsewhere if needed + end subroutine render_pcolormesh_plot + + subroutine draw_line_with_style(backend, x, y, linestyle, color) + !! Draw a line with the specified style + class(plot_context), intent(inout) :: backend + real(wp), intent(in) :: x(:), y(:) + character(len=*), intent(in), optional :: linestyle + real(wp), intent(in), optional :: color(3) + + if (present(color)) then + call backend%color(color(1), color(2), color(3)) + end if - ! Call backend to draw axes and labels - call self%backend%draw_axes_and_labels_backend( & - self%xscale, self%yscale, self%symlog_threshold, & - self%x_min, self%x_max, self%y_min, self%y_max, & - self%title, self%xlabel, self%ylabel, & - self%z_min, self%z_max, has_3d) - end subroutine render_axis_framework - - subroutine render_axis_labels(self) - !! Render axis labels (xlabel, ylabel, title) - !! Note: Labels are now rendered as part of draw_axes_and_labels_backend - !! This method is kept for interface compatibility but the actual - !! rendering happens in render_axis_framework - class(figure_t), intent(inout) :: self - - ! Labels are already rendered in render_axis_framework - ! via the backend's draw_axes_and_labels_backend method - ! This stub is kept for backward compatibility - end subroutine render_axis_labels - - subroutine render_single_arrow(self, arrow_idx) - !! Stub: Render single arrow - class(figure_t), intent(inout) :: self - integer, intent(in) :: arrow_idx - ! Stub implementation - end subroutine render_single_arrow - - subroutine render_streamline(self, streamline_idx) - !! Stub: Render single streamline - class(figure_t), intent(inout) :: self - integer, intent(in) :: streamline_idx - ! Stub implementation - end subroutine render_streamline - - subroutine render_annotation_text(self, annotation_idx, pixel_x, pixel_y) - !! Stub: Render annotation text - class(figure_t), intent(inout) :: self - integer, intent(in) :: annotation_idx - real(wp), intent(in) :: pixel_x, pixel_y - ! Stub implementation - end subroutine render_annotation_text - - subroutine render_annotation_arrow(self, annotation_idx, pixel_x, pixel_y) - !! Stub: Render annotation arrow - class(figure_t), intent(inout) :: self - integer, intent(in) :: annotation_idx - real(wp), intent(in) :: pixel_x, pixel_y - ! Stub implementation - end subroutine render_annotation_arrow - - subroutine parse_legend_location(location, legend_location) - !! Stub: Parse legend location string - character(len=*), intent(in) :: location - integer, intent(out) :: legend_location - legend_location = 1 ! Default: upper right - end subroutine parse_legend_location - - - subroutine update_ranges_from_line_plot(self, plot, first_plot) - !! Update data ranges from line plot data - class(figure_t), intent(inout) :: self - type(plot_data_t), intent(in) :: plot - logical, intent(in) :: first_plot - - real(wp) :: x_min_local, x_max_local, y_min_local, y_max_local - - if (.not. allocated(plot%x) .or. .not. allocated(plot%y)) return - if (size(plot%x) == 0) return - - ! Calculate local ranges - x_min_local = minval(plot%x) - x_max_local = maxval(plot%x) - y_min_local = minval(plot%y) - y_max_local = maxval(plot%y) - - ! Update global ranges - if (first_plot) then - self%x_min = x_min_local - self%x_max = x_max_local - self%y_min = y_min_local - self%y_max = y_max_local + if (present(linestyle)) then + select case (trim(linestyle)) + case ('--', 'dashed') + call render_patterned_line(backend, x, y, '--') + case (':', 'dotted') + call render_patterned_line(backend, x, y, ':') + case ('-.', 'dashdot') + call render_patterned_line(backend, x, y, '-.') + case default + call render_solid_line(backend, x, y) + end select else - self%x_min = min(self%x_min, x_min_local) - self%x_max = max(self%x_max, x_max_local) - self%y_min = min(self%y_min, y_min_local) - self%y_max = max(self%y_max, y_max_local) + call render_solid_line(backend, x, y) end if - end subroutine update_ranges_from_line_plot - - subroutine update_ranges_from_contour_plot(self, plot, first_plot) - !! Stub: Update ranges from contour plot - class(figure_t), intent(inout) :: self - type(plot_data_t), intent(in) :: plot - logical, intent(in) :: first_plot - ! Stub implementation - end subroutine update_ranges_from_contour_plot - - subroutine update_ranges_from_bar_plot(self, plot, first_plot) - !! Stub: Update ranges from bar plot - class(figure_t), intent(inout) :: self - type(plot_data_t), intent(in) :: plot - logical, intent(in) :: first_plot - ! Stub implementation - end subroutine update_ranges_from_bar_plot - - subroutine render_3d_line_plot(self, plot_idx) - !! Stub: Render 3D line plot - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - ! Stub implementation - end subroutine render_3d_line_plot - - subroutine render_line_plot(self, plot_idx) - !! Render line plot by drawing lines between consecutive points and markers - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - - type(plot_data_t) :: plot - integer :: i, subplot_idx - real(wp) :: x1, y1, x2, y2, x_trans, y_trans - logical :: draw_lines - - ! Get current subplot (default to 1) - subplot_idx = max(1, self%current_subplot) - if (.not. allocated(self%subplots)) return - if (subplot_idx > size(self%subplots)) return - - ! Get the plot data from the subplot - if (plot_idx < 1 .or. plot_idx > self%subplots(subplot_idx)%plot_count) return - plot = self%subplots(subplot_idx)%plots(plot_idx) + end subroutine draw_line_with_style + + subroutine render_solid_line(backend, x, y) + !! Render a solid line + class(plot_context), intent(inout) :: backend + real(wp), intent(in) :: x(:), y(:) + integer :: i - ! Skip if not a line plot - if (plot%plot_type /= PLOT_TYPE_LINE) return - if (.not. allocated(plot%x) .or. .not. allocated(plot%y)) return + if (size(x) < 2) return - ! Set the plot color - call self%backend%color(plot%color(1), plot%color(2), plot%color(3)) + do i = 1, size(x)-1 + call backend%line(x(i), y(i), x(i+1), y(i+1)) + end do + end subroutine render_solid_line + + subroutine render_patterned_line(backend, x, y, pattern) + !! Render a line with dash patterns + class(plot_context), intent(inout) :: backend + real(wp), intent(in) :: x(:), y(:) + character(len=*), intent(in) :: pattern + + real(wp) :: dash_length, gap_length, dot_length + real(wp) :: segment_length, accumulated_length + real(wp) :: dx, dy, x1, y1, x2, y2 + logical :: drawing + integer :: i - ! Determine if we should draw lines - draw_lines = .true. - if (allocated(plot%linestyle)) then - if (plot%linestyle == 'None' .or. plot%linestyle == '') then - draw_lines = .false. - end if - end if + ! Define pattern parameters + select case (trim(pattern)) + case ('--', 'dashed') + dash_length = 6.0_wp + gap_length = 4.0_wp + dot_length = 0.0_wp + case (':', 'dotted') + dash_length = 2.0_wp + gap_length = 2.0_wp + dot_length = 0.0_wp + case ('-.', 'dashdot') + dash_length = 6.0_wp + gap_length = 2.0_wp + dot_length = 2.0_wp + case default + call render_solid_line(backend, x, y) + return + end select - ! Draw lines between consecutive points if we have at least 2 points and linestyle is not 'None' - if (draw_lines .and. size(plot%x) >= 2) then - do i = 1, size(plot%x) - 1 - x1 = plot%x(i) - y1 = plot%y(i) - x2 = plot%x(i + 1) - y2 = plot%y(i + 1) - - ! Apply scale transformations if needed - if (self%xscale == 'log') then - if (x1 > 0.0_wp) x1 = log10(x1) - if (x2 > 0.0_wp) x2 = log10(x2) - else if (self%xscale == 'symlog') then - x1 = symlog_transform(x1, self%symlog_threshold) - x2 = symlog_transform(x2, self%symlog_threshold) - end if - - if (self%yscale == 'log') then - if (y1 > 0.0_wp) y1 = log10(y1) - if (y2 > 0.0_wp) y2 = log10(y2) - else if (self%yscale == 'symlog') then - y1 = symlog_transform(y1, self%symlog_threshold) - y2 = symlog_transform(y2, self%symlog_threshold) - end if - - ! Draw the line segment - call self%backend%line(x1, y1, x2, y2) - end do - end if + ! Render with pattern + drawing = .true. + accumulated_length = 0.0_wp - ! Draw markers at each data point if marker is specified - if (allocated(plot%marker)) then - if (plot%marker /= 'None' .and. plot%marker /= '') then - do i = 1, size(plot%x) - x_trans = plot%x(i) - y_trans = plot%y(i) + do i = 1, size(x) - 1 + dx = x(i+1) - x(i) + dy = y(i+1) - y(i) + segment_length = sqrt(dx**2 + dy**2) + + call render_segment_with_pattern(backend, x(i), y(i), x(i+1), y(i+1), & + segment_length, accumulated_length, & + dash_length, gap_length, drawing) + end do + end subroutine render_patterned_line + + subroutine render_segment_with_pattern(backend, x1, y1, x2, y2, segment_length, & + accumulated_length, dash_length, gap_length, drawing) + !! Render a single line segment with pattern + class(plot_context), intent(inout) :: backend + real(wp), intent(in) :: x1, y1, x2, y2, segment_length + real(wp), intent(inout) :: accumulated_length + real(wp), intent(in) :: dash_length, gap_length + logical, intent(inout) :: drawing + + real(wp) :: remaining, pattern_period, t, x_start, y_start, x_end, y_end + real(wp) :: dx, dy + + pattern_period = dash_length + gap_length + remaining = segment_length + dx = x2 - x1 + dy = y2 - y1 + + x_start = x1 + y_start = y1 + + do while (remaining > epsilon(1.0_wp)) + if (drawing) then + ! Currently drawing + if (accumulated_length + remaining <= dash_length) then + ! Can draw entire remaining segment + call backend%line(x_start, y_start, x2, y2) + accumulated_length = accumulated_length + remaining + remaining = 0.0_wp + else + ! Draw partial segment + t = (dash_length - accumulated_length) / segment_length + x_end = x_start + t * dx + y_end = y_start + t * dy - ! Apply scale transformations if needed - if (self%xscale == 'log') then - if (x_trans > 0.0_wp) x_trans = log10(x_trans) - else if (self%xscale == 'symlog') then - x_trans = symlog_transform(x_trans, self%symlog_threshold) - end if + call backend%line(x_start, y_start, x_end, y_end) - if (self%yscale == 'log') then - if (y_trans > 0.0_wp) y_trans = log10(y_trans) - else if (self%yscale == 'symlog') then - y_trans = symlog_transform(y_trans, self%symlog_threshold) - end if + remaining = remaining - (dash_length - accumulated_length) + x_start = x_end + y_start = y_end + accumulated_length = 0.0_wp + drawing = .false. + end if + else + ! Currently in gap + if (accumulated_length + remaining <= gap_length) then + ! Entire remaining segment is in gap + accumulated_length = accumulated_length + remaining + remaining = 0.0_wp + else + ! Skip gap portion + t = (gap_length - accumulated_length) / segment_length + x_start = x_start + t * dx + y_start = y_start + t * dy - ! Draw the marker - call self%backend%draw_marker(x_trans, y_trans, plot%marker) - end do + remaining = remaining - (gap_length - accumulated_length) + accumulated_length = 0.0_wp + drawing = .true. + end if end if - end if - end subroutine render_line_plot - - subroutine render_scatter_plot(self, plot_idx) - !! Stub: Render scatter plot - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - ! Stub implementation - end subroutine render_scatter_plot - - subroutine render_contour_plot(self, plot_idx) - !! Stub: Render contour plot - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - ! Stub implementation - end subroutine render_contour_plot - - subroutine render_pcolormesh_plot(self, plot_idx) - !! Stub: Render pcolormesh plot - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - ! Stub implementation - end subroutine render_pcolormesh_plot - - subroutine render_bar_plot(self, plot_idx) - !! Stub: Render bar plot - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - ! Stub implementation - end subroutine render_bar_plot - - subroutine render_errorbar_plot(self, plot_idx) - !! Stub: Render error bar plot - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - ! Stub implementation - end subroutine render_errorbar_plot - - subroutine render_boxplot(self, plot_idx) - !! Stub: Render box plot - class(figure_t), intent(inout) :: self - integer, intent(in) :: plot_idx - ! Stub implementation - end subroutine render_boxplot - - subroutine update_data_ranges_pcolormesh(self) - !! Stub: Update pcolormesh data ranges - class(figure_t), intent(inout) :: self - ! Stub implementation - end subroutine update_data_ranges_pcolormesh - - subroutine update_data_ranges_boxplot(self) - !! Stub: Update boxplot data ranges - class(figure_t), intent(inout) :: self - ! Stub implementation - end subroutine update_data_ranges_boxplot - - subroutine transform_axis_ranges(self) - !! Transform axis ranges based on scale settings - class(figure_t), intent(inout) :: self - - ! For now, simple linear transformation (identity) - self%x_min_transformed = self%x_min - self%x_max_transformed = self%x_max - self%y_min_transformed = self%y_min - self%y_max_transformed = self%y_max - - ! Add small padding if ranges are identical - if (self%x_min_transformed == self%x_max_transformed) then - self%x_min_transformed = self%x_min_transformed - 0.5_wp - self%x_max_transformed = self%x_max_transformed + 0.5_wp - end if + end do + end subroutine render_segment_with_pattern + + subroutine transform_quad_to_screen(x_quad, y_quad, x_screen, y_screen, & + xscale, yscale, symlog_threshold) + !! Transform quad coordinates to screen space + real(wp), intent(in) :: x_quad(4), y_quad(4) + real(wp), intent(out) :: x_screen(4), y_screen(4) + character(len=*), intent(in) :: xscale, yscale + real(wp), intent(in) :: symlog_threshold + integer :: i - if (self%y_min_transformed == self%y_max_transformed) then - self%y_min_transformed = self%y_min_transformed - 0.5_wp - self%y_max_transformed = self%y_max_transformed + 0.5_wp - end if - end subroutine transform_axis_ranges - + do i = 1, 4 + x_screen(i) = apply_scale_transform(x_quad(i), xscale, symlog_threshold) + y_screen(i) = apply_scale_transform(y_quad(i), yscale, symlog_threshold) + end do + end subroutine transform_quad_to_screen + + subroutine draw_filled_quad(backend, x_screen, y_screen) + !! Draw a filled quadrilateral + class(plot_context), intent(inout) :: backend + real(wp), intent(in) :: x_screen(4), y_screen(4) + + ! Use fill_quad if available + call backend%fill_quad(x_screen, y_screen) + end subroutine draw_filled_quad + + subroutine draw_quad_edges(backend, x_screen, y_screen, line_width) + !! Draw quadrilateral edges + class(plot_context), intent(inout) :: backend + real(wp), intent(in) :: x_screen(4), y_screen(4) + real(wp), intent(in) :: line_width + + call backend%set_line_width(line_width) + call backend%line(x_screen(1), y_screen(1), x_screen(2), y_screen(2)) + call backend%line(x_screen(2), y_screen(2), x_screen(3), y_screen(3)) + call backend%line(x_screen(3), y_screen(3), x_screen(4), y_screen(4)) + call backend%line(x_screen(4), y_screen(4), x_screen(1), y_screen(1)) + end subroutine draw_quad_edges + end module fortplot_rendering \ No newline at end of file diff --git a/test/test_first_plot_rendering.f90 b/test/test_first_plot_rendering.f90 new file mode 100644 index 00000000..bfe6f879 --- /dev/null +++ b/test/test_first_plot_rendering.f90 @@ -0,0 +1,51 @@ +program test_first_plot_rendering + !! Test for Issue #355: First plot is empty + !! Ensures that the first plot renders correctly with proper colors + use fortplot + implicit none + + real(wp), dimension(3) :: x = [1.0_wp, 2.0_wp, 3.0_wp] + real(wp), dimension(3) :: y = [1.0_wp, 2.0_wp, 3.0_wp] + character(len=1000) :: line + logical :: test_passed + integer :: unit, iostat, i + + print *, "Testing Issue #355: First plot rendering" + + ! Create a simple plot using procedural interface + call figure() + call plot(x, y, label='test') + call title('Test First Plot') + call savefig('test_first_plot_355.txt') + + ! Check that the output contains plot characters (not just dots) + test_passed = .false. + open(newunit=unit, file='test_first_plot_355.txt', status='old', action='read') + + ! Read through the file looking for plot characters + do i = 1, 100 + read(unit, '(A)', iostat=iostat) line + if (iostat /= 0) exit + + ! Check for plot characters that indicate color was set correctly + ! '#' is used for medium green (first default color blue has green=0.447) + ! '*' is used for medium blue + if (index(line, '#') > 0 .or. index(line, '*') > 0) then + test_passed = .true. + exit + end if + end do + + close(unit) + + if (test_passed) then + print *, "PASS: First plot renders with correct colors" + else + print *, "FAIL: First plot appears empty (only dots)" + stop 1 + end if + + ! Clean up + call system('rm -f test_first_plot_355.txt') + +end program test_first_plot_rendering \ No newline at end of file