diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 511a4be1..57556876 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,4 +78,4 @@ jobs: cd build cmake .. make - ./fortplotlib_test \ No newline at end of file + ./fortplot_test \ No newline at end of file diff --git a/Makefile b/Makefile index ba3ecd91..67363d0e 100644 --- a/Makefile +++ b/Makefile @@ -104,6 +104,7 @@ create_build_dirs: @mkdir -p build/example/smart_show_demo @mkdir -p build/example/animation @mkdir -p build/example/stateful_streamplot + @mkdir -p build/example/histogram_demo @mkdir -p build/example/subplot_demo # Help target diff --git a/README.md b/README.md index 35818db7..508b6105 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ![fortplot logo](media/logo.jpg) -[![codecov](https://codecov.io/gh/krystophny/fortplot/branch/main/graph/badge.svg)](https://codecov.io/gh/krystophny/fortplot) -[![Documentation](https://img.shields.io/badge/docs-FORD-blue.svg)](https://krystophny.github.io/fortplot/) +[![codecov](https://codecov.io/gh/lazy-fortran/fortplot/branch/main/graph/badge.svg)](https://codecov.io/gh/lazy-fortran/fortplot) +[![Documentation](https://img.shields.io/badge/docs-FORD-blue.svg)](https://lazy-fortran.github.io/fortplot/) Fortran-native plotting inspired by Python's `matplotlib.pyplot` and https://github.com/jacobwilliams/pyplot-fortran . This library is under active development and API still subject to change. There are no external dependencies. Ironically, it has also Python interface installable via `pip` (see below) `fortplot.fortplot` that can be used as a drop-in replacement for `matplotlib.pyplot` for a limited set of features. @@ -111,7 +111,7 @@ to build and run them. Add to your `fpm.toml`: ```toml [[dependencies]] -fortplot = { git = "https://github.com/krystophny/fortplot" } +fortplot = { git = "https://github.com/lazy-fortran/fortplot" } ``` ### For CMake projects @@ -122,7 +122,7 @@ include(FetchContent) FetchContent_Declare( fortplot - GIT_REPOSITORY https://github.com/krystophny/fortplot + GIT_REPOSITORY https://github.com/lazy-fortran/fortplot GIT_TAG main ) FetchContent_MakeAvailable(fortplot) @@ -134,7 +134,7 @@ target_link_libraries(your_target fortplot::fortplot) Install the Python package with pip: ```bash -pip install git+https://github.com/krystophny/fortplot.git +pip install git+https://github.com/lazy-fortran/fortplot.git ``` ## Features @@ -146,7 +146,7 @@ pip install git+https://github.com/krystophny/fortplot.git - [x] Streamplots (`streamplot`) for vector field visualization - [ ] Scatter plots (`scatter`) - [ ] Bar charts (`bar`) -- [ ] Histograms (`hist`) +- [x] Histograms (`hist`) - [ ] Images (`imshow`) ### Backends diff --git a/doc/cmake_example/CMakeLists.txt b/doc/cmake_example/CMakeLists.txt index 92a6c3fe..94383b5f 100644 --- a/doc/cmake_example/CMakeLists.txt +++ b/doc/cmake_example/CMakeLists.txt @@ -7,19 +7,19 @@ enable_language(Fortran) # Include FetchContent module include(FetchContent) -# Fetch and build fortplotlib using CMake +# Fetch and build fortplot using CMake FetchContent_Declare( - fortplotlib - GIT_REPOSITORY https://github.com/krystophny/fortplot + fortplot + GIT_REPOSITORY https://github.com/lazy-fortran/fortplot GIT_TAG main ) -FetchContent_MakeAvailable(fortplotlib) +FetchContent_MakeAvailable(fortplot) # Create a simple test program add_executable(fortplot_test main.f90) -# Link against fortplotlib (using old name until rename is merged to main) -target_link_libraries(fortplot_test fortplotlib::fortplotlib) +# Link against fortplot +target_link_libraries(fortplot_test fortplot::fortplot) # Set Fortran compiler flags set(CMAKE_Fortran_FLAGS "-Wall -Wextra -fimplicit-none") diff --git a/doc/fpm_example/fpm.toml b/doc/fpm_example/fpm.toml index ef4f544a..78cc2886 100644 --- a/doc/fpm_example/fpm.toml +++ b/doc/fpm_example/fpm.toml @@ -3,5 +3,5 @@ version = "0.1.0" license = "MIT" [dependencies] -fortplot = { path = "../.." } +fortplot = { git = "https://github.com/lazy-fortran/fortplot" } diff --git a/doc/python_example/pyproject.toml b/doc/python_example/pyproject.toml index 5597e5ed..2c6b4c87 100644 --- a/doc/python_example/pyproject.toml +++ b/doc/python_example/pyproject.toml @@ -6,6 +6,6 @@ build-backend = "setuptools.build_meta" name = "fortplot-example" version = "0.1.0" dependencies = [ - "fortplot @ git+https://github.com/krystophny/fortplot.git", + "fortplot @ git+https://github.com/lazy-fortran/fortplot.git", "numpy" ] \ No newline at end of file diff --git a/example/fortran/basic_plots/README.md b/example/fortran/basic_plots/README.md index b92003b8..a1f7e49d 100644 --- a/example/fortran/basic_plots/README.md +++ b/example/fortran/basic_plots/README.md @@ -31,4 +31,4 @@ The example generates the following output files: - `simple_plot.png` - Simple sine wave visualization - `multi_line.png` - Multiple functions on the same plot -See the [documentation gallery](https://krystophny.github.io/fortplot/) for visual examples. \ No newline at end of file +See the [documentation gallery](https://lazy-fortran.github.io/fortplot/) for visual examples. diff --git a/example/fortran/colored_contours/README.md b/example/fortran/colored_contours/README.md index 89718937..282c2eb1 100644 --- a/example/fortran/colored_contours/README.md +++ b/example/fortran/colored_contours/README.md @@ -54,4 +54,4 @@ The example generates the following output files: - `saddle_plasma.pdf` - Vector format of the saddle point - `saddle_plasma.txt` - ASCII art of the saddle point pattern -See the [documentation gallery](https://krystophny.github.io/fortplot/) for visual examples. \ No newline at end of file +See the [documentation gallery](https://lazy-fortran.github.io/fortplot/) for visual examples. diff --git a/example/fortran/contour_demo/README.md b/example/fortran/contour_demo/README.md index 9ddff94d..48318a22 100644 --- a/example/fortran/contour_demo/README.md +++ b/example/fortran/contour_demo/README.md @@ -34,4 +34,4 @@ The example generates the following output files: - `mixed_plot.pdf` - Vector format of the mixed plot - `mixed_plot.txt` - ASCII art representation of the mixed plot -See the [documentation gallery](https://krystophny.github.io/fortplot/) for visual examples. \ No newline at end of file +See the [documentation gallery](https://lazy-fortran.github.io/fortplot/) for visual examples. 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/example/fortran/line_styles/README.md b/example/fortran/line_styles/README.md index d8c1d494..60327495 100644 --- a/example/fortran/line_styles/README.md +++ b/example/fortran/line_styles/README.md @@ -37,4 +37,4 @@ The example generates the following output files: - `line_styles.pdf` - Vector format of the same visualization - `line_styles.txt` - ASCII art representation of the line styles -See the [documentation gallery](https://krystophny.github.io/fortplot/) for visual examples. \ No newline at end of file +See the [documentation gallery](https://lazy-fortran.github.io/fortplot/) for visual examples. diff --git a/example/fortran/marker_demo/README.md b/example/fortran/marker_demo/README.md index 0f5fa827..45fc647c 100644 --- a/example/fortran/marker_demo/README.md +++ b/example/fortran/marker_demo/README.md @@ -51,4 +51,4 @@ The example generates the following output files: - `marker_colors.pdf` - Vector format of colored markers - `marker_colors.txt` - ASCII art with different marker representations -See the [documentation gallery](https://krystophny.github.io/fortplot/) for visual examples. \ No newline at end of file +See the [documentation gallery](https://lazy-fortran.github.io/fortplot/) for visual examples. diff --git a/example/fortran/pcolormesh_demo/README.md b/example/fortran/pcolormesh_demo/README.md index 9a9922c4..fe8bf1a3 100644 --- a/example/fortran/pcolormesh_demo/README.md +++ b/example/fortran/pcolormesh_demo/README.md @@ -44,4 +44,4 @@ The example generates the following output files: - `pcolormesh_sinusoidal.pdf` - Vector format of the sinusoidal pattern - `pcolormesh_sinusoidal.txt` - ASCII representation of the sine wave pattern -See the [documentation gallery](https://krystophny.github.io/fortplot/) for visual examples. \ No newline at end of file +See the [documentation gallery](https://lazy-fortran.github.io/fortplot/) for visual examples. diff --git a/example/fortran/scale_examples/README.md b/example/fortran/scale_examples/README.md index bc872212..de96d060 100644 --- a/example/fortran/scale_examples/README.md +++ b/example/fortran/scale_examples/README.md @@ -34,4 +34,4 @@ The example generates the following output files: - `symlog_scale.pdf` - Vector format of the symlog scale plot - `symlog_scale.txt` - ASCII art representation with symmetric log axes -See the [documentation gallery](https://krystophny.github.io/fortplot/) for visual examples. \ No newline at end of file +See the [documentation gallery](https://lazy-fortran.github.io/fortplot/) for visual examples. diff --git a/example/generate_example_docs.f90 b/example/generate_example_docs.f90 index 6ff1065d..c7fef197 100644 --- a/example/generate_example_docs.f90 +++ b/example/generate_example_docs.f90 @@ -199,17 +199,17 @@ subroutine write_source_links(unit_out, example_name) select case(example_name) case('animation') fortran_file = 'save_animation_demo.f90' - fortran_path = 'https://github.com/krystophny/fortplot/blob/main/example/fortran/' // & + fortran_path = 'https://github.com/lazy-fortran/fortplot/blob/main/example/fortran/' // & trim(example_name) // '/' // trim(fortran_file) local_fortran_path = 'example/fortran/' // trim(example_name) // '/' // trim(fortran_file) case('ascii_heatmap') fortran_file = 'ascii_heatmap_demo.f90' - fortran_path = 'https://github.com/krystophny/fortplot/blob/main/example/fortran/' // & + fortran_path = 'https://github.com/lazy-fortran/fortplot/blob/main/example/fortran/' // & trim(example_name) // '/' // trim(fortran_file) local_fortran_path = 'example/fortran/' // trim(example_name) // '/' // trim(fortran_file) case default fortran_file = trim(example_name) // '.f90' - fortran_path = 'https://github.com/krystophny/fortplot/blob/main/example/fortran/' // & + fortran_path = 'https://github.com/lazy-fortran/fortplot/blob/main/example/fortran/' // & trim(example_name) // '/' // trim(fortran_file) local_fortran_path = 'example/fortran/' // trim(example_name) // '/' // trim(fortran_file) end select @@ -229,7 +229,7 @@ subroutine write_source_links(unit_out, example_name) if (python_exists) then write(unit_out, '(A)') '' write(unit_out, '(A)') '🐍 **Python:** [' // trim(example_name) // & - '.py](https://github.com/krystophny/fortplot/blob/main/' // & + '.py](https://github.com/lazy-fortran/fortplot/blob/main/' // & trim(python_path) // ')' end if write(unit_out, '(A)') '' diff --git a/scripts/generate_example_docs.py b/scripts/generate_example_docs.py index e81c7109..cde23cf1 100644 --- a/scripts/generate_example_docs.py +++ b/scripts/generate_example_docs.py @@ -91,7 +91,7 @@ def generate_example_page(example_info, output_dir): content += "### PDF Output\n\n" for pdf in pdf_files: pdf_name = Path(pdf).name - content += f"- [{pdf_name}](https://github.com/krystophny/fortplot/blob/main/{pdf})\n" + content += f"- [{pdf_name}](https://github.com/lazy-fortran/fortplot/blob/main/{pdf})\n" content += "\n" # Add link back to source @@ -99,7 +99,7 @@ def generate_example_page(example_info, output_dir): content += f""" --- -Source: [{rel_source}](https://github.com/krystophny/fortplot/blob/main/{rel_source}) +Source: [{rel_source}](https://github.com/lazy-fortran/fortplot/blob/main/{rel_source}) """ # Write the markdown file diff --git a/src/fortplot.f90 b/src/fortplot.f90 index d67725f0..d7345d0f 100644 --- a/src/fortplot.f90 +++ b/src/fortplot.f90 @@ -26,6 +26,7 @@ module fortplot ! Re-export public interface public :: figure_t, wp public :: plot, contour, contour_filled, pcolormesh, streamplot, boxplot, show, show_viewer + public :: hist, histogram public :: xlabel, ylabel, title, legend public :: savefig, figure public :: add_plot, add_contour, add_contour_filled, add_pcolormesh @@ -164,6 +165,57 @@ subroutine streamplot(x, y, u, v, density) call fig%streamplot(x, y, u, v, density=density) end subroutine streamplot + subroutine hist(data, bins, density, label, color) + !! Add histogram plot to the global figure (pyplot-style) + !! + !! Creates a histogram from input data, compatible with matplotlib.pyplot.hist + !! + !! 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 as RGB values [0-1] + !! + !! Example: + !! ! Simple histogram + !! call hist(data_values, bins=20, label='Distribution') + real(8), intent(in) :: data(:) + integer, intent(in), optional :: bins + logical, intent(in), optional :: density + character(len=*), intent(in), optional :: label + real(8), intent(in), optional :: color(3) + + call ensure_global_figure_initialized() + call fig%hist(data, bins=bins, density=density, label=label, color=color) + end subroutine hist + + subroutine histogram(data, bins, density, label, color) + !! Add histogram plot to the global figure (pyplot-style) + !! + !! Alias for hist() subroutine - creates a histogram from input data + !! Compatible with matplotlib.pyplot.histogram + !! + !! 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 as RGB values [0-1] + !! + !! Example: + !! ! Simple histogram + !! call histogram(data_values, bins=20, label='Distribution') + real(8), intent(in) :: data(:) + integer, intent(in), optional :: bins + logical, intent(in), optional :: density + character(len=*), intent(in), optional :: label + real(8), intent(in), optional :: color(3) + + call ensure_global_figure_initialized() + 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) !! Add a box plot to the global figure (matplotlib-style) !! @@ -544,4 +596,12 @@ subroutine show_viewer(blocking) call show_viewer_implementation(blocking=blocking) end subroutine show_viewer + subroutine ensure_global_figure_initialized() + !! Ensure global figure is initialized before use (matplotlib compatibility) + !! Auto-initializes with default dimensions if not already initialized + if (.not. allocated(fig%backend)) then + call fig%initialize() + end if + end subroutine ensure_global_figure_initialized + end module fortplot diff --git a/src/fortplot_figure.f90 b/src/fortplot_figure.f90 index d2653627..c74b1755 100644 --- a/src/fortplot_figure.f90 +++ b/src/fortplot_figure.f90 @@ -9,7 +9,8 @@ module fortplot_figure !! Re-exports: utility functions from fortplot_utils use fortplot_figure_core, only: figure_t, plot_data_t, PLOT_TYPE_LINE, & - PLOT_TYPE_CONTOUR, PLOT_TYPE_PCOLORMESH, PLOT_TYPE_BOXPLOT + PLOT_TYPE_CONTOUR, PLOT_TYPE_PCOLORMESH, & + PLOT_TYPE_HISTOGRAM, PLOT_TYPE_BOXPLOT use fortplot_scales, only: apply_scale_transform, apply_inverse_scale_transform, & transform_x_coordinate, transform_y_coordinate use fortplot_utils, only: get_backend_from_filename, initialize_backend @@ -19,7 +20,8 @@ module fortplot_figure ! Re-export all public entities for backward compatibility public :: figure_t, plot_data_t - public :: PLOT_TYPE_LINE, PLOT_TYPE_CONTOUR, PLOT_TYPE_PCOLORMESH, PLOT_TYPE_BOXPLOT + public :: PLOT_TYPE_LINE, PLOT_TYPE_CONTOUR, PLOT_TYPE_PCOLORMESH, & + PLOT_TYPE_HISTOGRAM, PLOT_TYPE_BOXPLOT public :: apply_scale_transform, apply_inverse_scale_transform public :: transform_x_coordinate, transform_y_coordinate public :: get_backend_from_filename, initialize_backend diff --git a/src/fortplot_figure_core.f90 b/src/fortplot_figure_core.f90 index cbbe297c..938fad02 100644 --- a/src/fortplot_figure_core.f90 +++ b/src/fortplot_figure_core.f90 @@ -25,17 +25,38 @@ module fortplot_figure_core private public :: figure_t, plot_data_t, subplot_t - public :: PLOT_TYPE_LINE, PLOT_TYPE_CONTOUR, PLOT_TYPE_PCOLORMESH, PLOT_TYPE_BOXPLOT + public :: PLOT_TYPE_LINE, PLOT_TYPE_CONTOUR, PLOT_TYPE_PCOLORMESH, PLOT_TYPE_HISTOGRAM, PLOT_TYPE_BOXPLOT integer, parameter :: PLOT_TYPE_LINE = 1 integer, parameter :: PLOT_TYPE_CONTOUR = 2 integer, parameter :: PLOT_TYPE_PCOLORMESH = 3 + integer, parameter :: PLOT_TYPE_HISTOGRAM = 4 integer, parameter :: PLOT_TYPE_BOXPLOT = 5 + ! Histogram constants + integer, parameter :: DEFAULT_HISTOGRAM_BINS = 10 + integer, parameter :: MAX_SAFE_BINS = 10000 + real(wp), parameter :: IDENTICAL_VALUE_PADDING = 0.5_wp + real(wp), parameter :: BIN_EDGE_PADDING_FACTOR = 0.001_wp + ! Box plot constants real(wp), parameter :: BOX_PLOT_LINE_WIDTH = 2.0_wp real(wp), parameter :: HALF_WIDTH = 0.5_wp real(wp), parameter :: IQR_WHISKER_MULTIPLIER = 1.5_wp + + ! Line rendering constants + real(wp), parameter :: PLOT_LINE_WIDTH = 2.0_wp + real(wp), parameter :: AXIS_LINE_WIDTH = 1.0_wp + + ! Contour level constants + real(wp), parameter :: CONTOUR_LEVEL_LOW = 0.2_wp + real(wp), parameter :: CONTOUR_LEVEL_MID = 0.5_wp + real(wp), parameter :: CONTOUR_LEVEL_HIGH = 0.8_wp + + ! Line style pattern constants (as percentage of plot scale) + real(wp), parameter :: DASH_LENGTH_FACTOR = 0.03_wp + real(wp), parameter :: DOT_LENGTH_FACTOR = 0.005_wp + real(wp), parameter :: GAP_LENGTH_FACTOR = 0.015_wp type :: plot_data_t !! Data container for individual plots @@ -52,6 +73,10 @@ module fortplot_figure_core logical :: show_colorbar = .true. ! Pcolormesh data type(pcolormesh_t) :: pcolormesh_data + ! Histogram data + real(wp), allocatable :: hist_bin_edges(:) + real(wp), allocatable :: hist_counts(:) + logical :: hist_density = .false. ! Box plot data real(wp), allocatable :: box_data(:) real(wp) :: position = 1.0_wp @@ -164,6 +189,7 @@ module fortplot_figure_core procedure :: add_contour procedure :: add_contour_filled procedure :: add_pcolormesh + procedure :: hist procedure :: boxplot procedure :: streamplot procedure :: savefig @@ -227,7 +253,6 @@ subroutine add_plot(self, x, y, label, linestyle, color) character(len=20) :: parsed_marker, parsed_linestyle if (self%plot_count >= self%max_plots) then - write(*, '(A)') 'Warning: Maximum number of plots reached' return end if @@ -252,7 +277,6 @@ subroutine add_contour(self, x_grid, y_grid, z_grid, levels, label) 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 @@ -271,7 +295,6 @@ subroutine add_contour_filled(self, x_grid, y_grid, z_grid, levels, colormap, sh 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 @@ -302,6 +325,30 @@ subroutine add_pcolormesh(self, x, y, c, colormap, vmin, vmax, edgecolors, linew call update_data_ranges_pcolormesh(self) end subroutine add_pcolormesh + 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 @@ -443,7 +490,6 @@ subroutine savefig(self, filename, blocking) call render_figure(self) call self%backend%save(filename) - write(*, '(A, A, A)') 'Saved figure: ', trim(filename) ! If blocking requested, wait for user input if (do_block) then @@ -975,6 +1021,91 @@ subroutine add_pcolormesh_plot_data(self, x, y, c, colormap, vmin, vmax, edgecol call self%plots(plot_idx)%pcolormesh_data%get_data_range() end subroutine add_pcolormesh_plot_data + subroutine add_histogram_plot_data(self, data, bins, density, label, color) + !! Add histogram data to internal storage with binning + 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 + + plot_idx = self%plot_count + self%plots(plot_idx)%plot_type = PLOT_TYPE_HISTOGRAM + + ! Setup bins and calculate histogram data + call setup_histogram_bins(self, plot_idx, data, bins, density) + + ! Create x,y data for bar rendering + call create_histogram_xy_data(self%plots(plot_idx)%hist_bin_edges, & + self%plots(plot_idx)%hist_counts, & + self%plots(plot_idx)%x, & + self%plots(plot_idx)%y) + + ! Configure plot properties + call setup_histogram_plot_properties(self, plot_idx, label, color) + end subroutine add_histogram_plot_data + + subroutine setup_histogram_bins(self, plot_idx, data, bins, density) + !! Setup histogram binning and calculate counts + class(figure_t), intent(inout) :: self + integer, intent(in) :: plot_idx + real(wp), intent(in) :: data(:) + integer, intent(in), optional :: bins + logical, intent(in), optional :: density + + integer :: n_bins + + if (present(bins)) then + n_bins = bins + else + n_bins = DEFAULT_HISTOGRAM_BINS + end if + + call create_bin_edges_from_count(data, n_bins, self%plots(plot_idx)%hist_bin_edges) + call calculate_histogram_counts(data, self%plots(plot_idx)%hist_bin_edges, & + self%plots(plot_idx)%hist_counts) + + if (present(density)) then + self%plots(plot_idx)%hist_density = density + if (density) then + call normalize_histogram_density(self%plots(plot_idx)%hist_counts, & + self%plots(plot_idx)%hist_bin_edges) + end if + end if + end subroutine setup_histogram_bins + + subroutine setup_histogram_plot_properties(self, plot_idx, label, color) + !! Configure histogram plot label, color, and style + class(figure_t), intent(inout) :: self + integer, intent(in) :: plot_idx + character(len=*), intent(in), optional :: label + real(wp), intent(in), optional :: color(3) + + integer :: color_idx + + ! Set label + if (present(label)) then + self%plots(plot_idx)%label = label + else + self%plots(plot_idx)%label = '' + end if + + ! Set color + 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 + + ! Set default style + self%plots(plot_idx)%linestyle = 'solid' + self%plots(plot_idx)%marker = 'None' + end subroutine setup_histogram_plot_properties + subroutine update_data_ranges_pcolormesh(self) !! Update figure data ranges after adding pcolormesh plot class(figure_t), intent(inout) :: self @@ -1019,8 +1150,14 @@ subroutine add_boxplot_data(self, data, position, width, label, show_outliers, h integer :: plot_idx, color_idx + if (self%plot_count >= self%max_plots) then + return + end if + + self%plot_count = self%plot_count + 1 + plot_idx = self%plot_count + ! Expand plots array - plot_idx = self%plot_count + 1 call expand_plots_array(self, plot_idx) ! Set plot type and copy data @@ -1081,7 +1218,7 @@ subroutine update_data_ranges_boxplot(self) integer :: plot_idx real(wp) :: x_min_plot, x_max_plot, y_min_plot, y_max_plot - plot_idx = self%plot_count + 1 + plot_idx = self%plot_count if (self%plots(plot_idx)%horizontal) then ! Horizontal box plot - data range is in X direction @@ -1526,6 +1663,21 @@ subroutine calculate_figure_data_ranges(self) y_max_trans = max(y_max_trans, apply_scale_transform(maxval(self%plots(i)%pcolormesh_data%y_vertices), & self%yscale, self%symlog_threshold)) end if + else if (self%plots(i)%plot_type == PLOT_TYPE_HISTOGRAM) then + if (first_plot) then + ! Store ORIGINAL histogram ranges + 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. + end if else if (self%plots(i)%plot_type == PLOT_TYPE_BOXPLOT) then if (first_plot) then ! Store ORIGINAL box plot ranges @@ -1556,7 +1708,7 @@ subroutine calculate_figure_data_ranges(self) y_max_trans = apply_scale_transform(y_max_orig, self%yscale, self%symlog_threshold) first_plot = .false. else - ! Update original ranges for box plot + ! Update original ranges for subsequent box plot if (self%plots(i)%horizontal) then x_min_orig = min(x_min_orig, self%plots(i)%whisker_low) x_max_orig = max(x_max_orig, self%plots(i)%whisker_high) @@ -1672,6 +1824,8 @@ subroutine render_all_plots(self) call render_contour_plot(self, i) else if (self%plots(i)%plot_type == PLOT_TYPE_PCOLORMESH) then call render_pcolormesh_plot(self, i) + else if (self%plots(i)%plot_type == PLOT_TYPE_HISTOGRAM) then + call render_histogram_plot(self, i) else if (self%plots(i)%plot_type == PLOT_TYPE_BOXPLOT) then call render_boxplot_plot(self, i) end if @@ -1731,7 +1885,7 @@ subroutine render_line_plot(self, plot_idx) ! 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) + call self%backend%set_line_width(PLOT_LINE_WIDTH) ! Draw line segments using transformed coordinates with linestyle call draw_line_with_style(self, plot_idx, linestyle) @@ -1894,6 +2048,73 @@ subroutine render_pcolormesh_plot(self, plot_idx) end do end subroutine render_pcolormesh_plot + subroutine render_histogram_plot(self, plot_idx) + !! Render histogram plot as filled bars + class(figure_t), intent(inout) :: self + integer, intent(in) :: plot_idx + + integer :: i, n_bins + + if (plot_idx > self%plot_count) return + if (.not. allocated(self%plots(plot_idx)%hist_bin_edges)) return + if (.not. allocated(self%plots(plot_idx)%hist_counts)) return + + n_bins = size(self%plots(plot_idx)%hist_counts) + + ! Render each histogram bar as a filled rectangle + do i = 1, n_bins + if (self%plots(plot_idx)%hist_counts(i) > 0.0_wp) then + call render_histogram_bar(self, plot_idx, i) + end if + end do + end subroutine render_histogram_plot + + subroutine render_histogram_bar(self, plot_idx, bin_idx) + !! Render individual histogram bar with coordinates and drawing + class(figure_t), intent(inout) :: self + integer, intent(in) :: plot_idx, bin_idx + + real(wp) :: x_screen(4), y_screen(4) + + call transform_histogram_bar_coordinates(self, plot_idx, bin_idx, x_screen, y_screen) + call draw_histogram_bar_shape(self, x_screen, y_screen) + end subroutine render_histogram_bar + + subroutine transform_histogram_bar_coordinates(self, plot_idx, bin_idx, x_screen, y_screen) + !! Transform histogram bar coordinates from data to screen space + class(figure_t), intent(inout) :: self + integer, intent(in) :: plot_idx, bin_idx + real(wp), intent(out) :: x_screen(4), y_screen(4) + + real(wp) :: x1, y1, x2, y2 + + x1 = self%plots(plot_idx)%hist_bin_edges(bin_idx) + x2 = self%plots(plot_idx)%hist_bin_edges(bin_idx+1) + y1 = 0.0_wp + y2 = self%plots(plot_idx)%hist_counts(bin_idx) + + x_screen(1) = apply_scale_transform(x1, self%xscale, self%symlog_threshold) + y_screen(1) = apply_scale_transform(y1, self%yscale, self%symlog_threshold) + x_screen(2) = apply_scale_transform(x2, self%xscale, self%symlog_threshold) + y_screen(2) = apply_scale_transform(y1, self%yscale, self%symlog_threshold) + x_screen(3) = apply_scale_transform(x2, self%xscale, self%symlog_threshold) + y_screen(3) = apply_scale_transform(y2, self%yscale, self%symlog_threshold) + x_screen(4) = apply_scale_transform(x1, self%xscale, self%symlog_threshold) + y_screen(4) = apply_scale_transform(y2, self%yscale, self%symlog_threshold) + end subroutine transform_histogram_bar_coordinates + + subroutine draw_histogram_bar_shape(self, x_screen, y_screen) + !! Draw filled rectangle and outline for histogram bar + class(figure_t), intent(inout) :: self + real(wp), intent(in) :: x_screen(4), y_screen(4) + + call draw_filled_quad(self%backend, x_screen, y_screen) + call self%backend%line(x_screen(1), y_screen(1), x_screen(2), y_screen(2)) + call self%backend%line(x_screen(2), y_screen(2), x_screen(3), y_screen(3)) + call self%backend%line(x_screen(3), y_screen(3), x_screen(4), y_screen(4)) + call self%backend%line(x_screen(4), y_screen(4), x_screen(1), y_screen(1)) + end subroutine draw_histogram_bar_shape + subroutine render_boxplot_plot(self, plot_idx) !! Render box plot with quartiles, whiskers, and outliers class(figure_t), intent(inout) :: self @@ -2067,9 +2288,9 @@ subroutine render_default_contour_levels(self, plot_idx, z_min, z_max) 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)] + level_values = [z_min + CONTOUR_LEVEL_LOW * (z_max - z_min), & + z_min + CONTOUR_LEVEL_MID * (z_max - z_min), & + z_min + CONTOUR_LEVEL_HIGH * (z_max - z_min)] do i = 1, 3 ! Set color based on contour level @@ -2360,15 +2581,16 @@ subroutine render_patterned_line(self, plot_idx, linestyle) 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) + if (plot_scale <= 0.0_wp) plot_scale = 1.0_wp 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 + dash_len = plot_scale * DASH_LENGTH_FACTOR + dot_len = plot_scale * DOT_LENGTH_FACTOR + gap_len = plot_scale * GAP_LENGTH_FACTOR ! Define patterns like matplotlib select case (trim(linestyle)) @@ -2617,6 +2839,215 @@ subroutine set_ydata(self, plot_index, y_new) self%plots(plot_index)%y = y_new end subroutine set_ydata + subroutine create_bin_edges_from_count(data, n_bins, bin_edges) + !! Create evenly spaced bin edges from data range + real(wp), intent(in) :: data(:) + integer, intent(in) :: n_bins + real(wp), allocatable, intent(out) :: bin_edges(:) + + real(wp) :: data_min, data_max, bin_width + integer :: i + + data_min = minval(data) + data_max = maxval(data) + + ! Handle case where all data points are identical + if (data_min == data_max) then + ! Create bins centered around the single value + data_min = data_min - IDENTICAL_VALUE_PADDING + data_max = data_max + IDENTICAL_VALUE_PADDING + end if + + ! Add small padding to avoid edge cases + bin_width = (data_max - data_min) / real(n_bins, wp) + data_min = data_min - bin_width * BIN_EDGE_PADDING_FACTOR + data_max = data_max + bin_width * BIN_EDGE_PADDING_FACTOR + bin_width = (data_max - data_min) / real(n_bins, wp) + + allocate(bin_edges(n_bins + 1)) + do i = 1, n_bins + 1 + bin_edges(i) = data_min + real(i - 1, wp) * bin_width + end do + end subroutine create_bin_edges_from_count + + subroutine calculate_histogram_counts(data, bin_edges, counts) + !! Calculate histogram bin counts + real(wp), intent(in) :: data(:) + real(wp), intent(in) :: bin_edges(:) + real(wp), allocatable, intent(out) :: counts(:) + + integer :: n_bins, i, bin_idx + + n_bins = size(bin_edges) - 1 + allocate(counts(n_bins)) + counts = 0.0_wp + + do i = 1, size(data) + bin_idx = find_bin_index(data(i), bin_edges) + if (bin_idx > 0 .and. bin_idx <= n_bins) then + counts(bin_idx) = counts(bin_idx) + 1.0_wp + end if + end do + end subroutine calculate_histogram_counts + + integer function find_bin_index(value, bin_edges) result(bin_idx) + !! Find which bin a value belongs to using binary search + real(wp), intent(in) :: value + real(wp), intent(in) :: bin_edges(:) + + integer :: n_bins + + n_bins = size(bin_edges) - 1 + bin_idx = 0 + + ! Check if value is outside bin range + if (.not. is_value_in_range(value, bin_edges, n_bins)) return + + ! Handle exact match with upper bound + if (value == bin_edges(n_bins + 1)) then + bin_idx = n_bins + return + end if + + ! Perform binary search + bin_idx = binary_search_bins(value, bin_edges, n_bins) + end function find_bin_index + + logical function is_value_in_range(value, bin_edges, n_bins) result(in_range) + !! Check if value falls within bin range + real(wp), intent(in) :: value + real(wp), intent(in) :: bin_edges(:) + integer, intent(in) :: n_bins + + in_range = value >= bin_edges(1) .and. value <= bin_edges(n_bins + 1) + end function is_value_in_range + + integer function binary_search_bins(value, bin_edges, n_bins) result(bin_idx) + !! Binary search to find bin containing value + real(wp), intent(in) :: value + real(wp), intent(in) :: bin_edges(:) + integer, intent(in) :: n_bins + + integer :: left, right, mid + + left = 1 + right = n_bins + bin_idx = 0 + + do while (left <= right) + mid = (left + right) / 2 + if (value >= bin_edges(mid) .and. value < bin_edges(mid + 1)) then + bin_idx = mid + return + else if (value < bin_edges(mid)) then + right = mid - 1 + else + left = mid + 1 + end if + end do + end function binary_search_bins + + subroutine normalize_histogram_density(counts, bin_edges) + !! Normalize histogram to probability density + real(wp), intent(inout) :: counts(:) + real(wp), intent(in) :: bin_edges(:) + + real(wp) :: total_area, bin_width + integer :: i + + if (size(bin_edges) /= size(counts) + 1) then + print *, 'Warning: bin_edges size mismatch in density normalization' + return + end if + + total_area = 0.0_wp + do i = 1, size(counts) + bin_width = bin_edges(i+1) - bin_edges(i) + total_area = total_area + counts(i) * bin_width + end do + + if (total_area > 0.0_wp) then + counts = counts / total_area + end if + end subroutine normalize_histogram_density + + subroutine create_histogram_xy_data(bin_edges, counts, x, y) + !! Convert histogram data to x,y coordinates for rendering as bars + real(wp), intent(in) :: bin_edges(:), counts(:) + real(wp), allocatable, intent(out) :: x(:), y(:) + + integer :: n_bins, i, point_idx + + n_bins = size(counts) + + ! Create bar outline: 4 points per bin (bottom-left, top-left, top-right, bottom-right) + allocate(x(4 * n_bins + 1), y(4 * n_bins + 1)) + + point_idx = 1 + do i = 1, n_bins + call add_bar_outline_points(bin_edges(i), bin_edges(i+1), counts(i), & + x, y, point_idx) + end do + + ! Close the shape + x(point_idx) = bin_edges(1) + y(point_idx) = 0.0_wp + end subroutine create_histogram_xy_data + + subroutine add_bar_outline_points(x_left, x_right, count, x, y, point_idx) + !! Add the 4 corner points for a single bin outline + real(wp), intent(in) :: x_left, x_right, count + real(wp), intent(inout) :: x(:), y(:) + integer, intent(inout) :: point_idx + + ! Bottom-left + x(point_idx) = x_left + y(point_idx) = 0.0_wp + point_idx = point_idx + 1 + + ! Top-left + x(point_idx) = x_left + y(point_idx) = count + point_idx = point_idx + 1 + + ! Top-right + x(point_idx) = x_right + y(point_idx) = count + point_idx = point_idx + 1 + + ! Bottom-right + x(point_idx) = x_right + y(point_idx) = 0.0_wp + point_idx = point_idx + 1 + end subroutine add_bar_outline_points + + 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_axis_padding(x_min, x_max, y_min, y_max) !! Add 5% padding to axis ranges real(wp), intent(inout) :: x_min, x_max, y_min, y_max @@ -2907,12 +3338,16 @@ subroutine render_subplot_axes(self, subplot) ! Draw axes with ticks and axis labels, but NOT title (we'll draw that separately) call draw_axes_and_labels(backend, self%xscale, self%yscale, self%symlog_threshold, & subplot%x_min, subplot%x_max, subplot%y_min, subplot%y_max, & - title="", xlabel=subplot%xlabel, ylabel=subplot%ylabel) + title="", xlabel=subplot%xlabel, ylabel=subplot%ylabel, & + grid_enabled=self%grid_enabled, grid_axis=self%grid_axis, grid_which=self%grid_which, & + grid_alpha=self%grid_alpha, grid_linestyle=self%grid_linestyle, grid_color=self%grid_color) type is (pdf_context) ! Draw axes with ticks and axis labels, but NOT title (we'll draw that separately) call draw_pdf_axes_and_labels(backend, self%xscale, self%yscale, self%symlog_threshold, & subplot%x_min, subplot%x_max, subplot%y_min, subplot%y_max, & - title="", xlabel=subplot%xlabel, ylabel=subplot%ylabel) + title="", xlabel=subplot%xlabel, ylabel=subplot%ylabel, & + grid_enabled=self%grid_enabled, grid_axis=self%grid_axis, grid_which=self%grid_which, & + grid_alpha=self%grid_alpha, grid_linestyle=self%grid_linestyle, grid_color=self%grid_color) type is (ascii_context) ! ASCII backend doesn't support subplots yet ! Could draw a simple frame here if needed @@ -3064,5 +3499,4 @@ subroutine transform_subplot_coordinates(self, subplot, data_x, data_y, screen_x screen_x = real(subplot%x1, wp) + x_norm * subplot_width screen_y = real(subplot%y2, wp) - y_norm * subplot_height end subroutine transform_subplot_coordinates - end module fortplot_figure_core \ No newline at end of file diff --git a/test/test_global_hist_api.f90 b/test/test_global_hist_api.f90 new file mode 100644 index 00000000..876e7207 --- /dev/null +++ b/test/test_global_hist_api.f90 @@ -0,0 +1,64 @@ +program test_global_hist_api + !! Test global histogram API functions with boundary conditions + + use fortplot + use iso_fortran_env, only: wp => real64 + implicit none + + call test_global_hist_without_init() + call test_global_hist_zero_bins() + call test_global_histogram_negative_bins() + call test_global_hist_valid() + + print *, 'All global histogram API tests passed!' + +contains + + subroutine test_global_hist_without_init() + !! Test global hist() function without explicit figure initialization + !! This should work like matplotlib.pyplot.hist() - auto-initialize + real(wp) :: data(5) = [1.0_wp, 2.0_wp, 3.0_wp, 4.0_wp, 5.0_wp] + + ! Should auto-initialize and not crash (matplotlib compatibility) + call hist(data, bins=5, label='Auto Init Test') + + print *, 'PASS: Global hist() auto-initializes figure like matplotlib' + end subroutine test_global_hist_without_init + + subroutine test_global_hist_zero_bins() + !! Test global hist() function with zero bins + real(wp) :: data(5) = [1.0_wp, 2.0_wp, 3.0_wp, 4.0_wp, 5.0_wp] + + call figure(640, 480) + + ! Should not crash with zero bins + call hist(data, bins=0) + + print *, 'PASS: Global hist() with zero bins handled safely' + end subroutine test_global_hist_zero_bins + + subroutine test_global_histogram_negative_bins() + !! Test global histogram() function with negative bins + real(wp) :: data(5) = [1.0_wp, 2.0_wp, 3.0_wp, 4.0_wp, 5.0_wp] + + call figure(640, 480) + + ! Should not crash with negative bins + call histogram(data, bins=-10) + + print *, 'PASS: Global histogram() with negative bins handled safely' + end subroutine test_global_histogram_negative_bins + + subroutine test_global_hist_valid() + !! Test global hist() function with valid parameters + real(wp) :: data(5) = [1.0_wp, 2.0_wp, 3.0_wp, 4.0_wp, 5.0_wp] + + call figure(640, 480) + + ! Should work correctly with valid bins + call hist(data, bins=5) + + print *, 'PASS: Global hist() with valid bins works correctly' + end subroutine test_global_hist_valid + +end program test_global_hist_api \ No newline at end of file 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