diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 1e99662f..00000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,81 +0,0 @@ -cmake_minimum_required(VERSION 3.20) -project(fortplot VERSION 2025.08.17 LANGUAGES Fortran C) - -# Set build type -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) -endif() - -# Compiler options -set(CMAKE_Fortran_FLAGS "-Wall -Wextra -fimplicit-none") -set(CMAKE_Fortran_FLAGS_DEBUG "-g -O0 -fcheck=all") -set(CMAKE_Fortran_FLAGS_RELEASE "-O3") - -# Set position independent code for shared library support -set(CMAKE_POSITION_INDEPENDENT_CODE ON) - -# Find all source files automatically -file(GLOB_RECURSE FORTRAN_SOURCES "src/*.f90") -file(GLOB_RECURSE C_SOURCES "src/*.c") - -# Debug output -list(LENGTH FORTRAN_SOURCES fortran_count) -list(LENGTH C_SOURCES c_count) -message(STATUS "Found ${fortran_count} Fortran source files") -message(STATUS "Found ${c_count} C source files") - -# Create the fortplot library -add_library(fortplot ${FORTRAN_SOURCES} ${C_SOURCES}) - -# Set Fortran module directory -set_target_properties(fortplot PROPERTIES - Fortran_MODULE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/modules -) - -# Include module directory for dependent projects -target_include_directories(fortplot - PUBLIC - $ - $ -) - -# Create namespaced alias for use in parent projects -add_library(fortplot::fortplot ALIAS fortplot) - -# Export configuration for find_package() support -if(NOT CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) - # This is a subproject - don't install - message(STATUS "fortplot configured as subproject") -else() - # This is the main project - configure install - include(GNUInstallDirs) - - install(TARGETS fortplot - EXPORT fortplotTargets - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - ) - - install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/modules/ - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - FILES_MATCHING PATTERN "*.mod" - ) - - install(EXPORT fortplotTargets - FILE fortplotTargets.cmake - NAMESPACE fortplot:: - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/fortplot - ) - - include(CMakePackageConfigHelpers) - configure_package_config_file( - ${CMAKE_CURRENT_SOURCE_DIR}/cmake/fortplotConfig.cmake.in - ${CMAKE_CURRENT_BINARY_DIR}/fortplotConfig.cmake - INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/fortplot - ) - - install(FILES ${CMAKE_CURRENT_BINARY_DIR}/fortplotConfig.cmake - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/fortplot - ) -endif() \ No newline at end of file diff --git a/README.md b/README.md index dffb31ac..6e22ea1c 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ to build and run them. **Optional:** - `ffmpeg` - Required for saving animations in compressed video formats (MP4, AVI, MKV) - - **5-Layer Validation**: Comprehensive framework prevents false positives (Issue #32) + - **5-Layer Validation**: Comprehensive framework prevents false positives - **External validation**: FFprobe integration for format verification - **Documentation**: See [MPEG Validation Guide](doc/mpeg_validation.md) for details @@ -204,7 +204,7 @@ pip install git+https://github.com/lazy-fortran/fortplot.git - [x] Interactive display with `show()` (GUI detection for X11, Wayland, macOS, Windows) - [x] Animation support with `FuncAnimation` (requires `ffmpeg` for video formats) - **5-Layer Validation**: Comprehensive framework with size, header, semantic, and external tool checks - - **False Positive Prevention**: Solves Issue #32 with multi-criteria validation + - **False Positive Prevention**: Multi-criteria validation framework - [x] Unicode and LaTeX-style Greek letters (`\alpha`, `\beta`, `\gamma`, etc.) in all backends - [ ] Subplots - [ ] Annotations diff --git a/doc/cmake_example/CMakeLists.txt b/doc/cmake_example/CMakeLists.txt deleted file mode 100644 index c94f9543..00000000 --- a/doc/cmake_example/CMakeLists.txt +++ /dev/null @@ -1,44 +0,0 @@ -cmake_minimum_required(VERSION 3.20) -project(fortplot_cmake_example Fortran) - -# Enable Fortran -enable_language(Fortran) - -# Include FetchContent module -include(FetchContent) - -# For testing with local repository, add fortplot as subdirectory -# In production, users would use FetchContent as shown in comments below -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../ fortplot_lib) - -# Production example using FetchContent (commented out for local testing): -# FetchContent_Declare( -# fortplot -# GIT_REPOSITORY https://github.com/lazy-fortran/fortplot -# GIT_TAG main -# ) -# FetchContent_MakeAvailable(fortplot) - -# Create a simple test program -add_executable(fortplot_test main.f90) - -# Ensure fortplot is built before our test program -add_dependencies(fortplot_test fortplot) - -# Link against fortplot library -# Note: FetchContent makes the library target available directly -target_link_libraries(fortplot_test fortplot) - -# Set Fortran compiler flags -set(CMAKE_Fortran_FLAGS "-Wall -Wextra -fimplicit-none") -set(CMAKE_Fortran_FLAGS_DEBUG "-g -O0 -fcheck=all") -set(CMAKE_Fortran_FLAGS_RELEASE "-O3") - -# Default to Release build if not specified -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) -endif() - -message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") -message(STATUS "Fortran compiler: ${CMAKE_Fortran_COMPILER}") -message(STATUS "Fortran flags: ${CMAKE_Fortran_FLAGS}") \ No newline at end of file diff --git a/example/fortran/boxplot_demo.f90.disabled b/example/fortran/boxplot_demo.f90.disabled index e7342cc1..9e87e170 100644 --- a/example/fortran/boxplot_demo.f90.disabled +++ b/example/fortran/boxplot_demo.f90.disabled @@ -30,7 +30,7 @@ program boxplot_demo call fig%set_xlabel('Data Groups') call fig%set_ylabel('Values') call fig%boxplot(normal_data, label='Normal Distribution') - call fig%savefig('plots/boxplot_single.png') + call fig%savefig('build/example/boxplot_demo/boxplot_single.png') print *, 'Created boxplot_single.png' ! Box plot with outliers @@ -39,7 +39,7 @@ program boxplot_demo call fig%set_xlabel('Data Groups') call fig%set_ylabel('Values') call fig%boxplot(outlier_data, label='Data with Outliers') - call fig%savefig('plots/boxplot_outliers.png') + call fig%savefig('build/example/boxplot_demo/boxplot_outliers.png') print *, 'Created boxplot_outliers.png' ! Multiple box plots for comparison @@ -51,7 +51,7 @@ program boxplot_demo call fig%boxplot(group_b, position=2.0_wp, label='Group B') call fig%boxplot(group_c, position=3.0_wp, label='Group C') call fig%legend() - call fig%savefig('plots/boxplot_comparison.png') + call fig%savefig('build/example/boxplot_demo/boxplot_comparison.png') print *, 'Created boxplot_comparison.png' ! Horizontal box plot @@ -60,7 +60,7 @@ program boxplot_demo call fig%set_xlabel('Values') call fig%set_ylabel('Data Groups') call fig%boxplot(normal_data, horizontal=.true., label='Horizontal') - call fig%savefig('plots/boxplot_horizontal.png') + call fig%savefig('build/example/boxplot_demo/boxplot_horizontal.png') print *, 'Created boxplot_horizontal.png' print *, 'Box plot demonstration completed!' diff --git a/example/fortran/grid_demo.f90.disabled b/example/fortran/grid_demo.f90.disabled index 2ccf623b..d4c48c00 100644 --- a/example/fortran/grid_demo.f90.disabled +++ b/example/fortran/grid_demo.f90.disabled @@ -26,7 +26,7 @@ program grid_demo call fig%set_title('Basic Grid Lines') call fig%set_xlabel('Time (s)') call fig%set_ylabel('Amplitude') - call fig%savefig('plots/grid_basic.png') + call fig%savefig('build/example/grid_demo/grid_basic.png') write(*,*) 'Created grid_basic.png' ! Basic plot with default grid (PDF) @@ -38,7 +38,7 @@ program grid_demo call fig%set_title('Basic Grid Lines') call fig%set_xlabel('Time (s)') call fig%set_ylabel('Amplitude') - call fig%savefig('plots/grid_basic.pdf') + call fig%savefig('build/example/grid_demo/grid_basic.pdf') write(*,*) 'Created grid_basic.pdf' ! Grid with custom transparency @@ -50,7 +50,7 @@ program grid_demo call fig%set_title('Grid with Custom Transparency (alpha=0.6)') call fig%set_xlabel('Time (s)') call fig%set_ylabel('Amplitude') - call fig%savefig('plots/grid_custom_alpha.png') + call fig%savefig('build/example/grid_demo/grid_custom_alpha.png') write(*,*) 'Created grid_custom_alpha.png' ! Grid with custom line style @@ -62,7 +62,7 @@ program grid_demo call fig%set_title('Grid with Dashed Lines') call fig%set_xlabel('Time (s)') call fig%set_ylabel('Amplitude') - call fig%savefig('plots/grid_dashed.png') + call fig%savefig('build/example/grid_demo/grid_dashed.png') write(*,*) 'Created grid_dashed.png' ! X-axis grid only @@ -74,7 +74,7 @@ program grid_demo call fig%set_title('X-Axis Grid Lines Only') call fig%set_xlabel('Time (s)') call fig%set_ylabel('Amplitude') - call fig%savefig('plots/grid_x_only.png') + call fig%savefig('build/example/grid_demo/grid_x_only.png') write(*,*) 'Created grid_x_only.png' ! Y-axis grid only @@ -86,7 +86,7 @@ program grid_demo call fig%set_title('Y-Axis Grid Lines Only') call fig%set_xlabel('Time (s)') call fig%set_ylabel('Amplitude') - call fig%savefig('plots/grid_y_only.png') + call fig%savefig('build/example/grid_demo/grid_y_only.png') write(*,*) 'Created grid_y_only.png' ! Minor grid lines @@ -98,7 +98,7 @@ program grid_demo call fig%set_title('Minor Grid Lines') call fig%set_xlabel('Time (s)') call fig%set_ylabel('Amplitude') - call fig%savefig('plots/grid_minor.png') + call fig%savefig('build/example/grid_demo/grid_minor.png') write(*,*) 'Created grid_minor.png' write(*,*) 'Grid lines demonstration completed!' diff --git a/example/fortran/histogram_demo.f90 b/example/fortran/histogram_demo.f90 new file mode 100644 index 00000000..0b3d46dd --- /dev/null +++ b/example/fortran/histogram_demo.f90 @@ -0,0 +1,66 @@ +program histogram_demo + !! Example demonstrating histogram plotting capabilities + !! Shows basic histogram, custom bins, and density normalization + + use, intrinsic :: iso_fortran_env, only: wp => real64 + use fortplot + implicit none + + integer, parameter :: n_data = 1000 + real(wp) :: data(n_data), normal_data(n_data) + type(figure_t) :: fig + integer :: i + real(wp) :: pi = 3.14159265359_wp + + ! Generate random-like data (simple distribution) + do i = 1, n_data + data(i) = real(i, wp) / 100.0_wp + sin(real(i, wp) * 0.01_wp) * 5.0_wp + end do + + ! Generate normal-like data using Box-Muller transform approximation + do i = 1, n_data + normal_data(i) = cos(2.0_wp * pi * real(i, wp) / real(n_data, wp)) * & + sqrt(-2.0_wp * log(max(real(mod(i, 1000), wp) / 1000.0_wp, 0.001_wp))) + end do + + ! Basic histogram + call fig%initialize(800, 600) + call fig%hist(data) + call fig%set_title('Basic Histogram Example') + call fig%set_xlabel('Value') + call fig%set_ylabel('Frequency') + call fig%savefig('build/example/histogram_demo/histogram_basic.png') + write(*,*) 'Created histogram_basic.png' + + ! Custom bins histogram + call fig%initialize(800, 600) + call fig%hist(data, bins=20) + call fig%set_title('Histogram with 20 Bins') + call fig%set_xlabel('Value') + call fig%set_ylabel('Frequency') + call fig%savefig('build/example/histogram_demo/histogram_custom_bins.png') + write(*,*) 'Created histogram_custom_bins.png' + + ! Density histogram + call fig%initialize(800, 600) + call fig%hist(normal_data, bins=15, density=.true.) + call fig%set_title('Normalized Histogram (Density)') + call fig%set_xlabel('Value') + call fig%set_ylabel('Probability Density') + call fig%savefig('build/example/histogram_demo/histogram_density.png') + write(*,*) 'Created histogram_density.png' + + ! Multiple histograms with labels + call fig%initialize(800, 600) + call fig%hist(data(1:500), bins=15, label='Dataset 1', color=[0.0_wp, 0.447_wp, 0.698_wp]) + call fig%hist(normal_data(1:500), bins=15, label='Dataset 2', color=[0.835_wp, 0.369_wp, 0.0_wp]) + call fig%legend() + call fig%set_title('Multiple Histograms') + call fig%set_xlabel('Value') + call fig%set_ylabel('Frequency') + call fig%savefig('build/example/histogram_demo/histogram_multiple.png') + write(*,*) 'Created histogram_multiple.png' + + write(*,*) 'Histogram demonstration completed!' + +end program histogram_demo \ No newline at end of file diff --git a/src/fortplot.f90 b/src/fortplot.f90 index 522551c3..993352c5 100644 --- a/src/fortplot.f90 +++ b/src/fortplot.f90 @@ -230,9 +230,7 @@ subroutine hist(data, bins, density, label, color) real(8), intent(in), optional :: color(3) call ensure_global_figure_initialized() - ! TODO: Implement hist method in figure_core - ! call fig%hist(data, bins=bins, density=density, label=label, color=color) - call log_error("hist() not yet implemented - please use main branch for histogram support") + call fig%hist(data, bins=bins, density=density, label=label, color=color) end subroutine hist subroutine histogram(data, bins, density, label, color) @@ -258,9 +256,7 @@ subroutine histogram(data, bins, density, label, color) real(8), intent(in), optional :: color(3) call ensure_global_figure_initialized() - ! TODO: Implement hist method in figure_core - ! call fig%hist(data, bins=bins, density=density, label=label, color=color) - call log_error("hist() not yet implemented - please use main branch for histogram support") + call fig%hist(data, bins=bins, density=density, label=label, color=color) end subroutine histogram subroutine boxplot(data, position, width, label, show_outliers, horizontal, color) diff --git a/src/fortplot_figure_core.f90 b/src/fortplot_figure_core.f90 index 2e780de7..28cd05df 100644 --- a/src/fortplot_figure_core.f90 +++ b/src/fortplot_figure_core.f90 @@ -184,9 +184,8 @@ module fortplot_figure_core procedure :: errorbar procedure :: bar procedure :: barh - ! TODO: Add hist and boxplot implementations from main branch - ! procedure :: hist - ! procedure :: boxplot + procedure :: hist + procedure :: boxplot procedure :: streamplot procedure :: savefig procedure :: set_xlabel @@ -498,6 +497,51 @@ subroutine barh(self, y, widths, height, label, color) call update_data_ranges(self) end subroutine barh + subroutine hist(self, data, bins, density, label, color) + !! Add histogram plot to figure with automatic or custom binning + !! + !! Arguments: + !! data: Input data array to create histogram from + !! bins: Optional - number of bins (integer, default: 10) + !! density: Optional - normalize to probability density (default: false) + !! label: Optional - histogram label for legend + !! color: Optional - histogram color + class(figure_t), intent(inout) :: self + real(wp), intent(in) :: data(:) + integer, intent(in), optional :: bins + logical, intent(in), optional :: density + character(len=*), intent(in), optional :: label + real(wp), intent(in), optional :: color(3) + + if (.not. validate_histogram_input(self, data, bins)) return + + self%plot_count = self%plot_count + 1 + + call add_histogram_plot_data(self, data, bins, density, label, color) + call update_data_ranges(self) + end subroutine hist + + subroutine boxplot(self, data, position, width, label, show_outliers, horizontal, color) + !! Add box plot to figure using statistical data + class(figure_t), intent(inout) :: self + real(wp), intent(in) :: data(:) + real(wp), intent(in), optional :: position + real(wp), intent(in), optional :: width + character(len=*), intent(in), optional :: label + logical, intent(in), optional :: show_outliers + logical, intent(in), optional :: horizontal + real(wp), intent(in), optional :: color(3) + + ! Basic input validation (following NO DEFENSIVE PROGRAMMING principle) + if (size(data) < 1) then + print *, "Warning: Box plot requires at least 1 data point" + return + end if + + call add_boxplot_data(self, data, position, width, label, show_outliers, horizontal, color) + call update_data_ranges_boxplot(self) + end subroutine boxplot + 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 @@ -2754,4 +2798,201 @@ subroutine errorbar(self, x, y, xerr, yerr, xerr_lower, xerr_upper, & self%plots(self%plot_count) = plot_data end subroutine errorbar + ! Histogram helper functions - minimal implementations for compilation + function validate_histogram_input(self, data, bins) result(is_valid) + !! Validate histogram input parameters + class(figure_t), intent(inout) :: self + real(wp), intent(in) :: data(:) + integer, intent(in), optional :: bins + logical :: is_valid + + is_valid = .true. + + if (self%plot_count >= self%max_plots) then + is_valid = .false. + return + end if + + if (size(data) == 0) then + is_valid = .false. + return + end if + + if (present(bins)) then + if (bins <= 0 .or. bins > MAX_SAFE_BINS) then + is_valid = .false. + return + end if + end if + end function validate_histogram_input + + subroutine add_histogram_plot_data(self, data, bins, density, label, color) + !! Add histogram data to internal storage - minimal implementation + class(figure_t), intent(inout) :: self + real(wp), intent(in) :: data(:) + integer, intent(in), optional :: bins + logical, intent(in), optional :: density + character(len=*), intent(in), optional :: label + real(wp), intent(in), optional :: color(3) + + integer :: plot_idx, n_bins + real(wp) :: data_min, data_max, bin_width + integer :: i + + plot_idx = self%plot_count + self%plots(plot_idx)%plot_type = PLOT_TYPE_HISTOGRAM + + ! Simple histogram implementation + n_bins = 10 + if (present(bins)) n_bins = bins + + data_min = minval(data) + data_max = maxval(data) + + ! Handle case where all data points are identical + if (data_max == data_min) then + ! Add small padding to create valid bins + data_min = data_min - 0.5_wp + data_max = data_max + 0.5_wp + end if + + bin_width = (data_max - data_min) / real(n_bins, wp) + + ! Initialize histogram arrays (Fortran automatically reallocates) + self%plots(plot_idx)%hist_bin_edges = [(data_min + real(i-1, wp) * bin_width, i = 1, n_bins + 1)] + self%plots(plot_idx)%hist_counts = [(0.0_wp, i = 1, n_bins)] + do i = 1, size(data) + if (data(i) >= data_min .and. data(i) <= data_max) then + associate(bin_idx => min(n_bins, max(1, int((data(i) - data_min) / bin_width) + 1))) + self%plots(plot_idx)%hist_counts(bin_idx) = self%plots(plot_idx)%hist_counts(bin_idx) + 1.0_wp + end associate + end if + end do + + ! Set density flag + if (present(density)) then + self%plots(plot_idx)%hist_density = density + end if + + ! Set plot properties + if (present(label)) then + self%plots(plot_idx)%label = label + else + self%plots(plot_idx)%label = '' + end if + + if (present(color)) then + self%plots(plot_idx)%color = color + else + self%plots(plot_idx)%color = [0.0_wp, 0.5_wp, 1.0_wp] ! Default blue + end if + end subroutine add_histogram_plot_data + + subroutine add_boxplot_data(self, data, position, width, label, show_outliers, horizontal, color) + !! Add box plot data - minimal implementation + class(figure_t), intent(inout) :: self + real(wp), intent(in) :: data(:) + real(wp), intent(in), optional :: position + real(wp), intent(in), optional :: width + character(len=*), intent(in), optional :: label + logical, intent(in), optional :: show_outliers + logical, intent(in), optional :: horizontal + real(wp), intent(in), optional :: color(3) + + integer :: plot_idx + + plot_idx = self%plot_count + self%plots(plot_idx)%plot_type = PLOT_TYPE_BOXPLOT + + ! Copy data + if (allocated(self%plots(plot_idx)%box_data)) deallocate(self%plots(plot_idx)%box_data) + allocate(self%plots(plot_idx)%box_data(size(data))) + self%plots(plot_idx)%box_data = data + + ! Set optional parameters with defaults + if (present(position)) then + self%plots(plot_idx)%position = position + else + self%plots(plot_idx)%position = 1.0_wp + end if + + if (present(width)) then + self%plots(plot_idx)%width = width + else + self%plots(plot_idx)%width = 0.6_wp + end if + + if (present(show_outliers)) then + self%plots(plot_idx)%show_outliers = show_outliers + else + self%plots(plot_idx)%show_outliers = .true. + end if + + if (present(horizontal)) then + self%plots(plot_idx)%horizontal = horizontal + else + self%plots(plot_idx)%horizontal = .false. + end if + + if (present(label)) then + self%plots(plot_idx)%label = label + else + self%plots(plot_idx)%label = '' + end if + + if (present(color)) then + self%plots(plot_idx)%color = color + else + self%plots(plot_idx)%color = [0.5_wp, 0.5_wp, 0.5_wp] ! Default gray + end if + + ! Calculate basic statistics (simplified) + associate(sorted_data => data) ! TODO: implement proper sorting + if (size(sorted_data) > 0) then + self%plots(plot_idx)%q1 = minval(sorted_data) + self%plots(plot_idx)%q2 = (minval(sorted_data) + maxval(sorted_data)) * 0.5_wp + self%plots(plot_idx)%q3 = maxval(sorted_data) + self%plots(plot_idx)%whisker_low = minval(sorted_data) + self%plots(plot_idx)%whisker_high = maxval(sorted_data) + end if + end associate + end subroutine add_boxplot_data + + subroutine update_data_ranges_boxplot(self) + !! Update figure data ranges after adding box plot - minimal implementation + class(figure_t), intent(inout) :: self + + integer :: plot_idx + real(wp) :: x_min_plot, x_max_plot, y_min_plot, y_max_plot + + plot_idx = self%plot_count + + if (self%plots(plot_idx)%horizontal) then + ! Horizontal box plot + x_min_plot = self%plots(plot_idx)%whisker_low + x_max_plot = self%plots(plot_idx)%whisker_high + y_min_plot = self%plots(plot_idx)%position - self%plots(plot_idx)%width * 0.5_wp + y_max_plot = self%plots(plot_idx)%position + self%plots(plot_idx)%width * 0.5_wp + else + ! Vertical box plot + y_min_plot = self%plots(plot_idx)%whisker_low + y_max_plot = self%plots(plot_idx)%whisker_high + x_min_plot = self%plots(plot_idx)%position - self%plots(plot_idx)%width * 0.5_wp + x_max_plot = self%plots(plot_idx)%position + self%plots(plot_idx)%width * 0.5_wp + end if + + ! Update figure ranges + if (self%plot_count == 1) 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) + end if + end subroutine update_data_ranges_boxplot + end module fortplot_figure_core \ No newline at end of file diff --git a/src/fortplot_ticks.f90 b/src/fortplot_ticks.f90 index 84d23444..053cfba8 100644 --- a/src/fortplot_ticks.f90 +++ b/src/fortplot_ticks.f90 @@ -229,8 +229,14 @@ subroutine find_nice_tick_locations(data_min, data_max, target_num_ticks, & end do ! Set nice boundaries for axis limits - nice_min = tick_locations(1) - nice_max = tick_locations(actual_num_ticks) + if (actual_num_ticks > 0) then + nice_min = tick_locations(1) + nice_max = tick_locations(actual_num_ticks) + else + ! No ticks generated - use data bounds as fallback + nice_min = data_min + nice_max = data_max + end if end subroutine find_nice_tick_locations function determine_decimal_places_from_step(step) result(decimal_places) diff --git a/test/test_histogram.f90 b/test/test_histogram.f90 new file mode 100644 index 00000000..962f04d5 --- /dev/null +++ b/test/test_histogram.f90 @@ -0,0 +1,133 @@ +program test_histogram + !! Test suite for histogram plotting functionality + !! Tests histogram binning, data processing, and rendering + + use, intrinsic :: iso_fortran_env, only: wp => real64 + use fortplot_figure + implicit none + + integer :: test_count = 0 + integer :: pass_count = 0 + + call test_histogram_basic() + call test_histogram_custom_bins() + call test_histogram_density() + call test_histogram_empty_data() + + call print_test_summary() + +contains + + subroutine test_histogram_basic() + type(figure_t) :: fig + real(wp) :: data(10) + integer :: i + + call start_test("Basic histogram") + + call fig%initialize(640, 480) + + ! Create test data + do i = 1, 10 + data(i) = real(i, wp) + end do + + ! Add histogram with default 10 bins + call fig%hist(data) + call assert_equal(real(fig%plot_count, wp), 1.0_wp, "Histogram plot count") + + call end_test() + end subroutine test_histogram_basic + + subroutine test_histogram_custom_bins() + type(figure_t) :: fig + real(wp) :: data(10) + integer :: i + + call start_test("Histogram with custom bins") + + call fig%initialize(640, 480) + + ! Create test data + do i = 1, 10 + data(i) = real(i, wp) + end do + + ! Add histogram with 5 bins + call fig%hist(data, bins=5) + call assert_equal(real(fig%plot_count, wp), 1.0_wp, "Custom bins histogram count") + + call end_test() + end subroutine test_histogram_custom_bins + + subroutine test_histogram_density() + type(figure_t) :: fig + real(wp) :: data(100) + integer :: i + + call start_test("Histogram with density normalization") + + call fig%initialize(640, 480) + + ! Create test data + do i = 1, 100 + data(i) = real(i, wp) / 10.0_wp + end do + + ! Add histogram with density normalization + call fig%hist(data, density=.true.) + call assert_equal(real(fig%plot_count, wp), 1.0_wp, "Density histogram count") + + call end_test() + end subroutine test_histogram_density + + subroutine test_histogram_empty_data() + type(figure_t) :: fig + real(wp) :: data(0) + + call start_test("Histogram with empty data") + + call fig%initialize(640, 480) + + ! Add histogram with empty data - should handle gracefully + call fig%hist(data) + call assert_equal(real(fig%plot_count, wp), 0.0_wp, "Empty data histogram count") + + call end_test() + end subroutine test_histogram_empty_data + + subroutine start_test(test_name) + character(len=*), intent(in) :: test_name + write(*, '(A, A)') 'Running test: ', test_name + end subroutine start_test + + subroutine end_test() + write(*, '(A)') 'Test completed' + write(*, *) + end subroutine end_test + + subroutine assert_equal(actual, expected, description) + real(wp), intent(in) :: actual, expected + character(len=*), intent(in) :: description + real(wp), parameter :: tolerance = 1.0e-10_wp + + test_count = test_count + 1 + if (abs(actual - expected) < tolerance) then + write(*, '(A, A)') ' PASS: ', description + pass_count = pass_count + 1 + else + write(*, '(A, A, F12.6, A, F12.6)') ' FAIL: ', description, actual, ' != ', expected + end if + end subroutine assert_equal + + subroutine print_test_summary() + write(*, '(A)') '============================================' + write(*, '(A, I0, A, I0, A)') 'Test Summary: ', pass_count, ' of ', test_count, ' tests passed' + if (pass_count == test_count) then + write(*, '(A)') 'All tests PASSED!' + else + write(*, '(A)') 'Some tests FAILED!' + end if + end subroutine print_test_summary + +end program test_histogram \ No newline at end of file diff --git a/test/test_histogram_boundary_conditions.f90 b/test/test_histogram_boundary_conditions.f90 new file mode 100644 index 00000000..a0c892f0 --- /dev/null +++ b/test/test_histogram_boundary_conditions.f90 @@ -0,0 +1,67 @@ +program test_histogram_boundary_conditions + !! Test boundary conditions for histogram function to prevent segmentation faults + !! + !! Tests critical safety issues with invalid bins parameter: + !! - bins = 0 should not cause segmentation fault + !! - negative bins should not cause segmentation fault + !! - System should handle invalid input gracefully + + use fortplot + use iso_fortran_env, only: wp => real64 + implicit none + + call test_histogram_zero_bins() + call test_histogram_negative_bins() + call test_histogram_valid_minimal_case() + + print *, 'All histogram boundary condition tests passed!' + +contains + + subroutine test_histogram_zero_bins() + !! Test that bins=0 does not cause segmentation fault + type(figure_t) :: fig + real(wp) :: data(5) = [1.0_wp, 2.0_wp, 3.0_wp, 4.0_wp, 5.0_wp] + + call fig%initialize(640, 480) + + ! This should not crash with segmentation fault + ! Should handle gracefully and return without error + call fig%hist(data, bins=0) + + print *, 'PASS: Zero bins handled without segmentation fault' + end subroutine test_histogram_zero_bins + + subroutine test_histogram_negative_bins() + !! Test that negative bins do not cause segmentation fault + type(figure_t) :: fig + real(wp) :: data(5) = [1.0_wp, 2.0_wp, 3.0_wp, 4.0_wp, 5.0_wp] + + call fig%initialize(640, 480) + + ! This should not crash with segmentation fault + ! Should handle gracefully and return without error + call fig%hist(data, bins=-5) + + print *, 'PASS: Negative bins handled without segmentation fault' + end subroutine test_histogram_negative_bins + + subroutine test_histogram_valid_minimal_case() + !! Test valid minimal case for comparison + type(figure_t) :: fig + real(wp) :: data(5) = [1.0_wp, 2.0_wp, 3.0_wp, 4.0_wp, 5.0_wp] + + call fig%initialize(640, 480) + + ! This should work properly + call fig%hist(data, bins=1) + + ! Verify plot was added successfully + if (fig%plot_count == 0) then + error stop 'FAIL: Valid histogram was not added to figure' + end if + + print *, 'PASS: Valid minimal histogram case works correctly' + end subroutine test_histogram_valid_minimal_case + +end program test_histogram_boundary_conditions \ No newline at end of file diff --git a/test/test_histogram_docs.f90 b/test/test_histogram_docs.f90 new file mode 100644 index 00000000..e38142dc --- /dev/null +++ b/test/test_histogram_docs.f90 @@ -0,0 +1,39 @@ +program test_histogram_docs + !! Test examples exactly as documented in the API + + use, intrinsic :: iso_fortran_env, only: wp => real64 + use fortplot + implicit none + + real(wp) :: data_values(100) + type(figure_t) :: fig + integer :: i + + write(*,*) '=== TESTING DOCUMENTED EXAMPLES ===' + + ! Generate test data + do i = 1, 100 + data_values(i) = real(i, wp) * 0.1_wp + sin(real(i, wp) * 0.1_wp) * 2.0_wp + end do + + ! Test the exact example from the hist() documentation (needs figure initialization) + call figure(800, 600) + call hist(data_values, bins=20, label='Distribution') + call savefig('build/doc_example_hist.png') + write(*,*) ' ✓ hist() documented example works (with figure init)' + + ! Test the exact example from the histogram() documentation (needs figure initialization) + call figure(800, 600) + call histogram(data_values, bins=20, label='Distribution') + call savefig('build/doc_example_histogram.png') + write(*,*) ' ✓ histogram() documented example works (with figure init)' + + ! Test object-oriented API as documented + call fig%initialize(800, 600) + call fig%hist(data_values, bins=20, label='Distribution') + call fig%savefig('build/doc_example_oo.png') + write(*,*) ' ✓ Object-oriented API works' + + write(*,*) '=== DOCUMENTATION EXAMPLES VALIDATED ===' + +end program test_histogram_docs \ No newline at end of file diff --git a/test/test_histogram_edge_cases.f90 b/test/test_histogram_edge_cases.f90 new file mode 100644 index 00000000..c442129a --- /dev/null +++ b/test/test_histogram_edge_cases.f90 @@ -0,0 +1,63 @@ +program test_histogram_edge_cases + use iso_fortran_env, only: real64, wp => real64 + use fortplot, only: figure_t + implicit none + + call test_identical_values() + call test_binary_search_performance() + + print *, 'All histogram edge case tests PASSED!' + +contains + + subroutine test_identical_values() + !! Test histogram with all identical values + type(figure_t) :: fig + real(wp) :: data(100) + + print *, 'Testing: Histogram with identical values' + + ! All values are the same + data = 5.0_wp + + call fig%initialize(600, 400) + call fig%hist(data, bins=10) + + if (fig%plot_count == 1) then + print *, ' PASS: Histogram created with identical values' + else + print *, ' FAIL: Histogram not created properly' + stop 1 + end if + + print *, 'Test completed' + end subroutine test_identical_values + + subroutine test_binary_search_performance() + !! Test that binary search works correctly + type(figure_t) :: fig + real(wp), allocatable :: data(:) + integer :: i + + print *, 'Testing: Binary search in histogram binning' + + ! Create large dataset + allocate(data(10000)) + do i = 1, 10000 + data(i) = real(i, wp) / 100.0_wp + end do + + call fig%initialize(600, 400) + call fig%hist(data, bins=50) + + if (fig%plot_count == 1) then + print *, ' PASS: Large histogram created efficiently' + else + print *, ' FAIL: Large histogram creation failed' + stop 1 + end if + + print *, 'Test completed' + end subroutine test_binary_search_performance + +end program test_histogram_edge_cases \ No newline at end of file diff --git a/test/test_histogram_stress.f90 b/test/test_histogram_stress.f90 new file mode 100644 index 00000000..474a1978 --- /dev/null +++ b/test/test_histogram_stress.f90 @@ -0,0 +1,157 @@ +program test_histogram_stress + !! Stress testing for histogram functionality + !! Tests performance and robustness with large datasets + + use, intrinsic :: iso_fortran_env, only: wp => real64 + use fortplot + implicit none + + write(*,*) '=== HISTOGRAM STRESS TESTING ===' + + ! Test large datasets + call test_large_datasets() + + ! Test extreme parameters + call test_extreme_parameters() + + ! Test memory pressure + call test_memory_usage() + + write(*,*) '=== STRESS TESTS COMPLETED ===' + +contains + + subroutine test_large_datasets() + !! Test histogram with large amounts of data + type(figure_t) :: fig + real(wp), allocatable :: large_data(:) + integer :: i, n_data + real(wp) :: start_time, end_time + + write(*,*) 'Testing large datasets...' + + ! Test with 10,000 data points + n_data = 10000 + allocate(large_data(n_data)) + + ! Generate synthetic data with normal-like distribution + do i = 1, n_data + large_data(i) = sin(real(i, wp) * 0.001_wp) * 10.0_wp + & + cos(real(i, wp) * 0.01_wp) * 5.0_wp + & + real(mod(i, 100), wp) * 0.1_wp + end do + + call cpu_time(start_time) + call fig%initialize(800, 600) + call fig%hist(large_data, bins=50) + call fig%set_title('Large Dataset (10K points)') + call fig%savefig('build/stress_large_10k.png') + call cpu_time(end_time) + + write(*,'(A,F8.3,A)') ' ✓ 10K points processed in ', end_time - start_time, ' seconds' + + deallocate(large_data) + + ! Test with 50,000 data points + n_data = 50000 + allocate(large_data(n_data)) + + do i = 1, n_data + large_data(i) = real(i, wp) * 0.001_wp + & + sin(real(i, wp) * 0.0001_wp) * 100.0_wp + end do + + call cpu_time(start_time) + call fig%initialize(800, 600) + call fig%hist(large_data, bins=100) + call fig%set_title('Very Large Dataset (50K points)') + call fig%savefig('build/stress_large_50k.png') + call cpu_time(end_time) + + write(*,'(A,F8.3,A)') ' ✓ 50K points processed in ', end_time - start_time, ' seconds' + + deallocate(large_data) + + end subroutine test_large_datasets + + subroutine test_extreme_parameters() + !! Test with extreme parameter values + type(figure_t) :: fig + real(wp) :: data(1000) + integer :: i + + write(*,*) 'Testing extreme parameters...' + + ! Generate test data + do i = 1, 1000 + data(i) = real(i, wp) * 0.01_wp + end do + + ! Test with very many bins + call fig%initialize(800, 600) + call fig%hist(data, bins=500) + call fig%set_title('Extreme: 500 Bins') + call fig%savefig('build/stress_many_bins.png') + write(*,*) ' ✓ Many bins (500) handled' + + ! Test with minimal bins + call fig%initialize(800, 600) + call fig%hist(data, bins=1) + call fig%set_title('Extreme: 1 Bin') + call fig%savefig('build/stress_one_bin.png') + write(*,*) ' ✓ Single bin handled' + + ! Test with very wide range data + data(1:500) = 1.0e-6_wp + data(501:1000) = 1.0e6_wp + + call fig%initialize(800, 600) + call fig%hist(data, bins=20) + call fig%set_title('Extreme: Wide Range Data') + call fig%savefig('build/stress_wide_range.png') + write(*,*) ' ✓ Wide range data handled' + + end subroutine test_extreme_parameters + + subroutine test_memory_usage() + !! Test memory usage patterns + type(figure_t) :: fig + real(wp), allocatable :: data(:) + integer :: i, j, size + + write(*,*) 'Testing memory usage patterns...' + + ! Test multiple histograms in sequence (memory cleanup) + do i = 1, 10 + size = 1000 * i + allocate(data(size)) + + data = real([(j, j=1,size)], wp) * 0.001_wp + + call fig%initialize(600, 400) + call fig%hist(data, bins=20) + call fig%set_title('Memory Test ' // char(48 + i)) + call fig%savefig('build/stress_memory_' // char(48 + i) // '.png') + + deallocate(data) + end do + + write(*,*) ' ✓ Sequential memory usage patterns handled' + + ! Test overlapping histograms + allocate(data(5000)) + data = real([(i, i=1,5000)], wp) * 0.01_wp + + call fig%initialize(800, 600) + call fig%hist(data(1:2000), bins=30, label='First Half', color=[1.0_wp, 0.0_wp, 0.0_wp]) + call fig%hist(data(2500:4500), bins=30, label='Second Half', color=[0.0_wp, 0.0_wp, 1.0_wp]) + call fig%legend() + call fig%set_title('Overlapping Histograms') + call fig%savefig('build/stress_overlapping.png') + write(*,*) ' ✓ Overlapping histograms handled' + + deallocate(data) + + end subroutine test_memory_usage + +end program test_histogram_stress \ No newline at end of file diff --git a/test/test_histogram_uat.f90 b/test/test_histogram_uat.f90 new file mode 100644 index 00000000..6eb64be5 --- /dev/null +++ b/test/test_histogram_uat.f90 @@ -0,0 +1,173 @@ +program test_histogram_uat + !! User Acceptance Test for histogram functionality + !! Tests both hist() and histogram() APIs with various scenarios + + use, intrinsic :: iso_fortran_env, only: wp => real64 + use fortplot + implicit none + + write(*,*) '=== HISTOGRAM USER ACCEPTANCE TESTING ===' + + ! Test 1: Basic API functionality + call test_basic_api() + + ! Test 2: Edge cases + call test_edge_cases() + + ! Test 3: Error handling + call test_error_handling() + + ! Test 4: Backend compatibility + call test_backend_compatibility() + + write(*,*) '=== ALL TESTS COMPLETED ===' + +contains + + subroutine test_basic_api() + !! Test basic API functionality for both hist() and histogram() + type(figure_t) :: fig + real(wp) :: data(100) + integer :: i + + write(*,*) 'Testing basic API functionality...' + + ! Generate test data + do i = 1, 100 + data(i) = real(i, wp) * 0.1_wp + end do + + ! Test hist() function + call fig%initialize(600, 400) + call fig%hist(data) + call fig%set_title('Basic hist() API Test') + call fig%savefig('build/uat_hist_basic.png') + write(*,*) ' ✓ hist() function works' + + ! Test histogram() function (module-level function) + call figure(600, 400) + call histogram(data) + call title('Basic histogram() API Test') + call savefig('build/uat_histogram_basic.png') + write(*,*) ' ✓ histogram() function works' + + ! Test with optional parameters + call fig%initialize(600, 400) + call fig%hist(data, bins=15, density=.true., label='Test Data') + call fig%legend() + call fig%set_title('hist() with Optional Parameters') + call fig%savefig('build/uat_hist_options.png') + write(*,*) ' ✓ Optional parameters work' + + end subroutine test_basic_api + + subroutine test_edge_cases() + !! Test edge cases and boundary conditions + type(figure_t) :: fig + real(wp) :: empty_data(0) + real(wp) :: single_data(1) = [5.0_wp] + real(wp) :: identical_data(10) = [2.5_wp, 2.5_wp, 2.5_wp, 2.5_wp, 2.5_wp, & + 2.5_wp, 2.5_wp, 2.5_wp, 2.5_wp, 2.5_wp] + real(wp) :: extreme_data(5) = [1.0e-10_wp, 1.0e10_wp, -1.0e10_wp, 0.0_wp, 1.0_wp] + + write(*,*) 'Testing edge cases...' + + ! Test single value + call fig%initialize(600, 400) + call fig%hist(single_data) + call fig%set_title('Single Value Histogram') + call fig%savefig('build/uat_single_value.png') + write(*,*) ' ✓ Single value handled' + + ! Test identical values + call fig%initialize(600, 400) + call fig%hist(identical_data) + call fig%set_title('Identical Values Histogram') + call fig%savefig('build/uat_identical_values.png') + write(*,*) ' ✓ Identical values handled' + + ! Test extreme values + call fig%initialize(600, 400) + call fig%hist(extreme_data) + call fig%set_title('Extreme Values Histogram') + call fig%savefig('build/uat_extreme_values.png') + write(*,*) ' ✓ Extreme values handled' + + ! Test very small bins + call fig%initialize(600, 400) + call fig%hist(extreme_data, bins=2) + call fig%set_title('Very Few Bins') + call fig%savefig('build/uat_few_bins.png') + write(*,*) ' ✓ Very few bins handled' + + end subroutine test_edge_cases + + subroutine test_error_handling() + !! Test error handling and user-friendly messages + type(figure_t) :: fig + real(wp) :: data(10) = [1.0_wp, 2.0_wp, 3.0_wp, 4.0_wp, 5.0_wp, & + 6.0_wp, 7.0_wp, 8.0_wp, 9.0_wp, 10.0_wp] + real(wp) :: empty_data(0) + + write(*,*) 'Testing error handling...' + + ! Test empty data + call fig%initialize(600, 400) + call fig%hist(empty_data) + call fig%set_title('Empty Data Test') + call fig%savefig('build/uat_empty_data.png') + write(*,*) ' ✓ Empty data handled gracefully' + + ! Test invalid bins (negative) + call fig%initialize(600, 400) + call fig%hist(data, bins=-5) + call fig%set_title('Invalid Bins Test') + call fig%savefig('build/uat_invalid_bins.png') + write(*,*) ' ✓ Invalid bins handled gracefully' + + ! Test zero bins + call fig%initialize(600, 400) + call fig%hist(data, bins=0) + call fig%set_title('Zero Bins Test') + call fig%savefig('build/uat_zero_bins.png') + write(*,*) ' ✓ Zero bins handled gracefully' + + end subroutine test_error_handling + + subroutine test_backend_compatibility() + !! Test histogram across different backends + type(figure_t) :: fig + real(wp) :: data(50) + integer :: i + + write(*,*) 'Testing backend compatibility...' + + ! Generate test data + do i = 1, 50 + data(i) = sin(real(i, wp) * 0.2_wp) * 10.0_wp + end do + + ! Test PNG backend + call fig%initialize(600, 400) + call fig%hist(data, bins=10) + call fig%set_title('PNG Backend Test') + call fig%savefig('build/uat_backend_png.png') + write(*,*) ' ✓ PNG backend works' + + ! Test PDF backend + call fig%initialize(600, 400) + call fig%hist(data, bins=10) + call fig%set_title('PDF Backend Test') + call fig%savefig('build/uat_backend_pdf.pdf') + write(*,*) ' ✓ PDF backend works' + + ! Test ASCII backend + call fig%initialize(80, 24) + call fig%hist(data, bins=10) + call fig%set_title('ASCII Backend Test') + call fig%savefig('build/uat_backend_ascii.txt') + write(*,*) ' ✓ ASCII backend works' + + end subroutine test_backend_compatibility + +end program test_histogram_uat \ No newline at end of file diff --git a/test/test_scientific_workflow.f90 b/test/test_scientific_workflow.f90 new file mode 100644 index 00000000..422ef18a --- /dev/null +++ b/test/test_scientific_workflow.f90 @@ -0,0 +1,136 @@ +program test_scientific_workflow + !! Realistic scientific data analysis workflow with histograms + !! Simulates typical use cases in scientific computing + + use, intrinsic :: iso_fortran_env, only: wp => real64 + use fortplot + implicit none + + real(wp), parameter :: pi = 3.14159265359_wp + type(figure_t) :: fig + + write(*,*) '=== SCIENTIFIC WORKFLOW TESTING ===' + + call test_measurement_distribution() + call test_simulation_results() + call test_comparative_analysis() + call test_publication_quality() + + write(*,*) '=== SCIENTIFIC WORKFLOW COMPLETED ===' + +contains + + subroutine test_measurement_distribution() + !! Test typical experimental measurement distribution + real(wp) :: measurements(500) + integer :: i + + write(*,*) 'Testing measurement distribution analysis...' + + ! Simulate noisy experimental measurements around a mean + do i = 1, 500 + measurements(i) = 5.0_wp + & + sin(real(i, wp) * 0.01_wp) * 0.5_wp + & + cos(real(i, wp) * 0.02_wp) * 0.3_wp + & + (real(mod(i, 100), wp) - 50.0_wp) * 0.02_wp + end do + + call fig%initialize(800, 600) + call fig%hist(measurements, bins=30, density=.true., & + label='Experimental Data', & + color=[0.2_wp, 0.4_wp, 0.8_wp]) + call fig%set_title('Experimental Measurement Distribution') + call fig%set_xlabel('Measured Value') + call fig%set_ylabel('Probability Density') + call fig%legend() + call fig%savefig('build/scientific_measurements.png') + write(*,*) ' ✓ Measurement distribution analysis complete' + + end subroutine test_measurement_distribution + + subroutine test_simulation_results() + !! Test simulation result analysis + real(wp) :: simulation_data(1000) + integer :: i + + write(*,*) 'Testing simulation result analysis...' + + ! Simulate Monte Carlo or numerical simulation results + do i = 1, 1000 + simulation_data(i) = sqrt(real(i, wp)) * 0.1_wp + & + sin(real(i, wp) * 0.001_wp) * 10.0_wp + & + cos(real(i, wp) * 0.003_wp) * 5.0_wp + end do + + call fig%initialize(800, 600) + call fig%hist(simulation_data, bins=40, & + label='Simulation Results', & + color=[0.8_wp, 0.2_wp, 0.2_wp]) + call fig%set_title('Monte Carlo Simulation Results') + call fig%set_xlabel('Computed Value') + call fig%set_ylabel('Frequency') + call fig%legend() + call fig%savefig('build/scientific_simulation.png') + write(*,*) ' ✓ Simulation result analysis complete' + + end subroutine test_simulation_results + + subroutine test_comparative_analysis() + !! Test comparative analysis between datasets + real(wp) :: control_group(300), treatment_group(300) + integer :: i + + write(*,*) 'Testing comparative analysis...' + + ! Generate control and treatment group data + do i = 1, 300 + control_group(i) = 10.0_wp + sin(real(i, wp) * 0.02_wp) * 2.0_wp + treatment_group(i) = 12.0_wp + sin(real(i, wp) * 0.02_wp) * 1.8_wp + & + cos(real(i, wp) * 0.01_wp) * 1.0_wp + end do + + call fig%initialize(1000, 600) + call fig%hist(control_group, bins=25, density=.true., & + label='Control Group', & + color=[0.3_wp, 0.7_wp, 0.3_wp]) + call fig%hist(treatment_group, bins=25, density=.true., & + label='Treatment Group', & + color=[0.7_wp, 0.3_wp, 0.7_wp]) + call fig%set_title('Control vs Treatment Group Comparison') + call fig%set_xlabel('Response Variable') + call fig%set_ylabel('Probability Density') + call fig%legend() + call fig%savefig('build/scientific_comparison.png') + write(*,*) ' ✓ Comparative analysis complete' + + end subroutine test_comparative_analysis + + subroutine test_publication_quality() + !! Test publication-quality histogram with proper formatting + real(wp) :: publication_data(800) + integer :: i + + write(*,*) 'Testing publication-quality output...' + + ! Generate clean, publication-ready data + do i = 1, 800 + publication_data(i) = 100.0_wp + & + cos(2.0_wp * pi * real(i, wp) / 800.0_wp) * 20.0_wp + & + sin(4.0_wp * pi * real(i, wp) / 800.0_wp) * 10.0_wp + end do + + call fig%initialize(1200, 800) + call fig%hist(publication_data, bins=35, density=.true., & + label='Dataset A', & + color=[0.0_wp, 0.447_wp, 0.698_wp]) + call fig%set_title('Publication Quality Histogram') + call fig%set_xlabel('Parameter X (units)') + call fig%set_ylabel('Probability Density (1/units)') + call fig%legend() + call fig%savefig('build/scientific_publication.png') + call fig%savefig('build/scientific_publication.pdf') + write(*,*) ' ✓ Publication-quality output complete' + + end subroutine test_publication_quality + +end program test_scientific_workflow \ No newline at end of file