Skip to content

Commit 4b0db35

Browse files
krystophnyclaude
andauthored
Comprehensive Histogram Support (Issue #49) (#79)
* fix: Support NaN values for disconnected line segments Implements NaN support in add_plot to allow disconnected line segments, resolving issue #47. When NaN values are encountered in x or y arrays, the line drawing is interrupted, creating separate segments. Changes: - Modified render_solid_line to skip segments containing NaN values - Updated render_patterned_line to handle NaN with pattern state reset - Enhanced render_markers to skip NaN data points - All backends (PNG, PDF, ASCII) automatically support this feature This allows users to plot disconnected segments in a single add_plot call by inserting NaN values as separators, similar to matplotlib behavior. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * test: Improve coverage for NaN line breaking Add additional test cases to cover: - Patterned lines (dashed) with NaN breaks - Markers-only plots with NaN values This tests more code paths in render_patterned_line and render_markers to improve test coverage. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * test: Comprehensive NaN line breaking coverage Add extensive test coverage for NaN handling edge cases: - All line style patterns (solid, dashed, dotted, dash-dot, unknown) - Edge cases: all NaN arrays, leading/trailing NaN, consecutive NaN - Small arrays: single points, two points with NaN combinations - Robust handling when valid_count = 0 using masked maxval/minval Also fix potential crash when all points are NaN by adding safeguard for maxval/minval operations and providing default plot_scale. This should significantly improve code coverage metrics. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: Correct array indexing in disconnected_lines example Fix bug where cosine calculation used incorrect indexing x(i-5+1) instead of x(i), which would produce incorrect plot values. Addresses review feedback from PR #48. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: Add histogram plotting support with TDD * Add PLOT_TYPE_HISTOGRAM constant and hist method to figure_t * Implement histogram binning with automatic and custom bin counts * Support density normalization for probability density plots * Include comprehensive test suite with edge cases * Add example demonstrating various histogram features * Support all backends (PNG, PDF, ASCII) Follows matplotlib-compatible API: fig%hist(data, bins=10, density=.false.) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: Break long lines in fortplot_figure module The lines exceeded the 132 character limit causing CI failures. Split the use and public statements across multiple lines. * fix: Address code review feedback for histogram implementation - Add size validation to normalize_histogram_density to prevent array bounds violations - Handle edge case when all data points are identical to avoid zero bin width - Add protection against zero plot scale division in pattern calculations - Optimize find_bin_index to use binary search instead of linear search for O(log n) performance - Add comprehensive edge case tests to verify fixes These improvements address all the valid concerns raised in the PR review. * fix: Update histogram demo to use FORD-compatible output directory and fix branding * fix: refactor histogram routines for SOLID compliance and eliminate magic numbers - Split find_bin_index (34 lines) into three focused functions under 30 lines: * find_bin_index (22 lines): main entry point * is_value_in_range (8 lines): range validation * binary_search_bins (24 lines): binary search logic - Extract magic numbers to named constants: * DEFAULT_HISTOGRAM_BINS = 10 (was hardcoded 10) * IDENTICAL_VALUE_PADDING = 0.5_wp (was hardcoded 0.5_wp) * BIN_EDGE_PADDING_FACTOR = 0.001_wp (was hardcoded 0.001_wp) - Maintain Single Responsibility Principle compliance - All histogram tests pass, functionality preserved * fix: Correct dependency name in fpm example * fix: refactor remaining histogram routines for 30-line SOLID compliance Split remaining large histogram routines into focused sub-routines: - add_histogram_plot_data (62→26 lines): Separated bin setup and properties - render_histogram_plot (45→20 lines): Extracted individual bar rendering - create_histogram_xy_data (39→22 lines): Split outline point generation New helper routines (all ≤30 lines): - setup_histogram_bins (28 lines): Handle bin creation and normalization - setup_histogram_plot_properties (28 lines): Configure label, color, style - render_histogram_bar (10 lines): Coordinate single bar rendering - transform_histogram_bar_coordinates (22 lines): Data to screen transforms - draw_histogram_bar_shape (11 lines): Backend drawing calls - add_bar_outline_points (26 lines): Generate 4-corner bin outline All routines now comply with SOLID single responsibility principle. Functionality and performance preserved exactly. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: expose histogram in main fortplot API for matplotlib compatibility Adds hist() and histogram() subroutines to main fortplot module public interface, enabling matplotlib-style usage: - call hist(data, bins=20, density=.true., label='Distribution') - call histogram(data, bins=20, density=.true., label='Distribution') Both APIs forward all parameters (data, bins, density, label, color) to the existing figure%hist() method, ensuring identical functionality. Resolves CRITICAL API exposure gap where histogram was only available through figure method (fig%hist()) but not through global API like other plot types (plot, contour, etc.). 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: refactor hist subroutine for SOLID compliance Extract validation logic into separate helper function to reduce main histogram subroutine from 31 lines to 23 lines, satisfying the mandatory 30-line maximum requirement. This maintains single responsibility principle while preserving all existing functionality. - Add validate_histogram_input helper function - Maintain all error checking and warning functionality - All histogram tests continue to pass 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: address critical security and quality findings - Add input validation for bins parameter in histogram functions - Remove 40 binary artifact files from working directory - Replace warning output pollution with silent error handling - Extract magic numbers to named constants for line width, contour levels, and pattern factors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: mark histogram feature as implemented in README Update checkbox to reflect that histogram functionality is complete and functional in the library API. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: prevent segmentation faults in histogram with invalid bins parameter - Move bins validation from setup_histogram_bins to validate_histogram_input - Validate bins > 0 and bins <= MAX_SAFE_BINS before processing - Prevent plot_count increment when validation fails - Add comprehensive test coverage for boundary conditions - Test both figure.hist() and global hist()/histogram() APIs 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: update repository references from krystophny/fortplotlib to lazy-fortran/fortplot Updates CMake configuration, documentation URLs, and example references to use correct repository location for CI/CD pipeline functionality. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: remove binary files and build artifacts from workspace - Remove all test output files (*.png, *.pdf, test_*.txt) - Remove build directories and .mod files - Remove macOS artifact file (._.DS_Store) - Ensure clean workspace with no binary files or build artifacts - .gitignore properly configured to prevent future binary commits 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: update CMake example to use correct fortplot target names - Changed fortplotlib to fortplot in FetchContent_Declare - Updated target link from fortplotlib::fortplotlib to fortplot::fortplot - Aligns with actual CMake targets defined in main CMakeLists.txt - Resolves CMake CI build failure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: correct test executable name in CI and improve histogram plot indexing * fix: move histogram test files from app/ to test/ directory for proper organization * fix: resolve segmentation fault in global pyplot-style histogram functions Address critical issue where global hist() and histogram() functions crashed with segmentation fault due to uninitialized global figure backend. Root cause: Global figure variable declared but never auto-initialized when global functions called methods on it. Solution: - Add ensure_global_figure_initialized() helper function - Auto-initialize global figure with default dimensions if backend not allocated - Call helper before hist operations in both hist() and histogram() functions - Maintain matplotlib compatibility by auto-initializing like pyplot.hist() Changes: - src/fortplot.f90: Add auto-initialization to hist() and histogram() - test/test_global_hist_api.f90: Add test for auto-initialization behavior Verification: - Global hist(data) now works without explicit figure() call - Object-oriented fig%hist() continues working unchanged - All existing tests pass - New test verifies matplotlib-compatible auto-initialization 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: correct output paths in boxplot and grid examples - Fix boxplot_demo to use build/example/boxplot_demo/ paths - Fix grid_demo to use build/example/grid_demo/ paths - Resolves runtime failures due to missing plots/ directory - Ensures consistent example output directory structure * fix: resolve histogram allocation error in merged implementation Fixes runtime error when histogram arrays were already allocated: - Add proper deallocation checks before allocating hist_bin_edges - Add proper deallocation checks before allocating hist_counts - Prevents "Attempting to allocate already allocated variable" error This completes the histogram merge integration with main branch. All histogram functionality now works correctly with the new logging system and error bar plotting features. Tested with: - make test (all histogram tests pass) - make example histogram_demo (works correctly) - make example errorbar_demo (main branch features work) - make example bar_chart_demo (main branch features work) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: clean up issue references in README Remove specific issue number references for cleaner documentation. Maintains descriptive text about validation frameworks without coupling to specific issue numbers. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: remove obsolete CMake files The project has migrated to FPM build system, making CMake files unnecessary and confusing for new contributors. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: critical array bounds violation in histogram tick calculation - Fix array bounds error at fortplot_ticks.f90:233 when actual_num_ticks = 0 - Add proper bounds checking for edge cases in tick location calculation - Handle zero tick scenarios by falling back to data bounds - Resolves critical runtime bug that blocked all histogram savefig() operations - All histogram functionality now fully operational across PNG, PDF, ASCII backends - Maintains backward compatibility for normal tick calculation scenarios 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: eliminate critical allocation errors in histogram functionality Replace explicit allocate() statements with direct assignment to allocatable arrays. Fortran automatically handles reallocation when using direct assignment, preventing "attempting to allocate already allocated variable" runtime errors. Key changes: - Remove explicit allocate() calls for hist_bin_edges and hist_counts - Use array constructor syntax for cleaner initialization - Ensure zero allocation errors in all histogram operations Fixes all histogram test failures and runtime crashes. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e75f60e commit 4b0db35

16 files changed

+1101
-149
lines changed

CMakeLists.txt

Lines changed: 0 additions & 81 deletions
This file was deleted.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ to build and run them.
136136

137137
**Optional:**
138138
- `ffmpeg` - Required for saving animations in compressed video formats (MP4, AVI, MKV)
139-
- **5-Layer Validation**: Comprehensive framework prevents false positives (Issue #32)
139+
- **5-Layer Validation**: Comprehensive framework prevents false positives
140140
- **External validation**: FFprobe integration for format verification
141141
- **Documentation**: See [MPEG Validation Guide](doc/mpeg_validation.md) for details
142142

@@ -204,7 +204,7 @@ pip install git+https://github.com/lazy-fortran/fortplot.git
204204
- [x] Interactive display with `show()` (GUI detection for X11, Wayland, macOS, Windows)
205205
- [x] Animation support with `FuncAnimation` (requires `ffmpeg` for video formats)
206206
- **5-Layer Validation**: Comprehensive framework with size, header, semantic, and external tool checks
207-
- **False Positive Prevention**: Solves Issue #32 with multi-criteria validation
207+
- **False Positive Prevention**: Multi-criteria validation framework
208208
- [x] Unicode and LaTeX-style Greek letters (`\alpha`, `\beta`, `\gamma`, etc.) in all backends
209209
- [ ] Subplots
210210
- [ ] Annotations

doc/cmake_example/CMakeLists.txt

Lines changed: 0 additions & 44 deletions
This file was deleted.

example/fortran/boxplot_demo.f90.disabled

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ program boxplot_demo
3030
call fig%set_xlabel('Data Groups')
3131
call fig%set_ylabel('Values')
3232
call fig%boxplot(normal_data, label='Normal Distribution')
33-
call fig%savefig('plots/boxplot_single.png')
33+
call fig%savefig('build/example/boxplot_demo/boxplot_single.png')
3434
print *, 'Created boxplot_single.png'
3535

3636
! Box plot with outliers
@@ -39,7 +39,7 @@ program boxplot_demo
3939
call fig%set_xlabel('Data Groups')
4040
call fig%set_ylabel('Values')
4141
call fig%boxplot(outlier_data, label='Data with Outliers')
42-
call fig%savefig('plots/boxplot_outliers.png')
42+
call fig%savefig('build/example/boxplot_demo/boxplot_outliers.png')
4343
print *, 'Created boxplot_outliers.png'
4444

4545
! Multiple box plots for comparison
@@ -51,7 +51,7 @@ program boxplot_demo
5151
call fig%boxplot(group_b, position=2.0_wp, label='Group B')
5252
call fig%boxplot(group_c, position=3.0_wp, label='Group C')
5353
call fig%legend()
54-
call fig%savefig('plots/boxplot_comparison.png')
54+
call fig%savefig('build/example/boxplot_demo/boxplot_comparison.png')
5555
print *, 'Created boxplot_comparison.png'
5656

5757
! Horizontal box plot
@@ -60,7 +60,7 @@ program boxplot_demo
6060
call fig%set_xlabel('Values')
6161
call fig%set_ylabel('Data Groups')
6262
call fig%boxplot(normal_data, horizontal=.true., label='Horizontal')
63-
call fig%savefig('plots/boxplot_horizontal.png')
63+
call fig%savefig('build/example/boxplot_demo/boxplot_horizontal.png')
6464
print *, 'Created boxplot_horizontal.png'
6565

6666
print *, 'Box plot demonstration completed!'

example/fortran/grid_demo.f90.disabled

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ program grid_demo
2626
call fig%set_title('Basic Grid Lines')
2727
call fig%set_xlabel('Time (s)')
2828
call fig%set_ylabel('Amplitude')
29-
call fig%savefig('plots/grid_basic.png')
29+
call fig%savefig('build/example/grid_demo/grid_basic.png')
3030
write(*,*) 'Created grid_basic.png'
3131

3232
! Basic plot with default grid (PDF)
@@ -38,7 +38,7 @@ program grid_demo
3838
call fig%set_title('Basic Grid Lines')
3939
call fig%set_xlabel('Time (s)')
4040
call fig%set_ylabel('Amplitude')
41-
call fig%savefig('plots/grid_basic.pdf')
41+
call fig%savefig('build/example/grid_demo/grid_basic.pdf')
4242
write(*,*) 'Created grid_basic.pdf'
4343

4444
! Grid with custom transparency
@@ -50,7 +50,7 @@ program grid_demo
5050
call fig%set_title('Grid with Custom Transparency (alpha=0.6)')
5151
call fig%set_xlabel('Time (s)')
5252
call fig%set_ylabel('Amplitude')
53-
call fig%savefig('plots/grid_custom_alpha.png')
53+
call fig%savefig('build/example/grid_demo/grid_custom_alpha.png')
5454
write(*,*) 'Created grid_custom_alpha.png'
5555

5656
! Grid with custom line style
@@ -62,7 +62,7 @@ program grid_demo
6262
call fig%set_title('Grid with Dashed Lines')
6363
call fig%set_xlabel('Time (s)')
6464
call fig%set_ylabel('Amplitude')
65-
call fig%savefig('plots/grid_dashed.png')
65+
call fig%savefig('build/example/grid_demo/grid_dashed.png')
6666
write(*,*) 'Created grid_dashed.png'
6767

6868
! X-axis grid only
@@ -74,7 +74,7 @@ program grid_demo
7474
call fig%set_title('X-Axis Grid Lines Only')
7575
call fig%set_xlabel('Time (s)')
7676
call fig%set_ylabel('Amplitude')
77-
call fig%savefig('plots/grid_x_only.png')
77+
call fig%savefig('build/example/grid_demo/grid_x_only.png')
7878
write(*,*) 'Created grid_x_only.png'
7979

8080
! Y-axis grid only
@@ -86,7 +86,7 @@ program grid_demo
8686
call fig%set_title('Y-Axis Grid Lines Only')
8787
call fig%set_xlabel('Time (s)')
8888
call fig%set_ylabel('Amplitude')
89-
call fig%savefig('plots/grid_y_only.png')
89+
call fig%savefig('build/example/grid_demo/grid_y_only.png')
9090
write(*,*) 'Created grid_y_only.png'
9191

9292
! Minor grid lines
@@ -98,7 +98,7 @@ program grid_demo
9898
call fig%set_title('Minor Grid Lines')
9999
call fig%set_xlabel('Time (s)')
100100
call fig%set_ylabel('Amplitude')
101-
call fig%savefig('plots/grid_minor.png')
101+
call fig%savefig('build/example/grid_demo/grid_minor.png')
102102
write(*,*) 'Created grid_minor.png'
103103

104104
write(*,*) 'Grid lines demonstration completed!'

example/fortran/histogram_demo.f90

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
program histogram_demo
2+
!! Example demonstrating histogram plotting capabilities
3+
!! Shows basic histogram, custom bins, and density normalization
4+
5+
use, intrinsic :: iso_fortran_env, only: wp => real64
6+
use fortplot
7+
implicit none
8+
9+
integer, parameter :: n_data = 1000
10+
real(wp) :: data(n_data), normal_data(n_data)
11+
type(figure_t) :: fig
12+
integer :: i
13+
real(wp) :: pi = 3.14159265359_wp
14+
15+
! Generate random-like data (simple distribution)
16+
do i = 1, n_data
17+
data(i) = real(i, wp) / 100.0_wp + sin(real(i, wp) * 0.01_wp) * 5.0_wp
18+
end do
19+
20+
! Generate normal-like data using Box-Muller transform approximation
21+
do i = 1, n_data
22+
normal_data(i) = cos(2.0_wp * pi * real(i, wp) / real(n_data, wp)) * &
23+
sqrt(-2.0_wp * log(max(real(mod(i, 1000), wp) / 1000.0_wp, 0.001_wp)))
24+
end do
25+
26+
! Basic histogram
27+
call fig%initialize(800, 600)
28+
call fig%hist(data)
29+
call fig%set_title('Basic Histogram Example')
30+
call fig%set_xlabel('Value')
31+
call fig%set_ylabel('Frequency')
32+
call fig%savefig('build/example/histogram_demo/histogram_basic.png')
33+
write(*,*) 'Created histogram_basic.png'
34+
35+
! Custom bins histogram
36+
call fig%initialize(800, 600)
37+
call fig%hist(data, bins=20)
38+
call fig%set_title('Histogram with 20 Bins')
39+
call fig%set_xlabel('Value')
40+
call fig%set_ylabel('Frequency')
41+
call fig%savefig('build/example/histogram_demo/histogram_custom_bins.png')
42+
write(*,*) 'Created histogram_custom_bins.png'
43+
44+
! Density histogram
45+
call fig%initialize(800, 600)
46+
call fig%hist(normal_data, bins=15, density=.true.)
47+
call fig%set_title('Normalized Histogram (Density)')
48+
call fig%set_xlabel('Value')
49+
call fig%set_ylabel('Probability Density')
50+
call fig%savefig('build/example/histogram_demo/histogram_density.png')
51+
write(*,*) 'Created histogram_density.png'
52+
53+
! Multiple histograms with labels
54+
call fig%initialize(800, 600)
55+
call fig%hist(data(1:500), bins=15, label='Dataset 1', color=[0.0_wp, 0.447_wp, 0.698_wp])
56+
call fig%hist(normal_data(1:500), bins=15, label='Dataset 2', color=[0.835_wp, 0.369_wp, 0.0_wp])
57+
call fig%legend()
58+
call fig%set_title('Multiple Histograms')
59+
call fig%set_xlabel('Value')
60+
call fig%set_ylabel('Frequency')
61+
call fig%savefig('build/example/histogram_demo/histogram_multiple.png')
62+
write(*,*) 'Created histogram_multiple.png'
63+
64+
write(*,*) 'Histogram demonstration completed!'
65+
66+
end program histogram_demo

src/fortplot.f90

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,7 @@ subroutine hist(data, bins, density, label, color)
230230
real(8), intent(in), optional :: color(3)
231231

232232
call ensure_global_figure_initialized()
233-
! TODO: Implement hist method in figure_core
234-
! call fig%hist(data, bins=bins, density=density, label=label, color=color)
235-
call log_error("hist() not yet implemented - please use main branch for histogram support")
233+
call fig%hist(data, bins=bins, density=density, label=label, color=color)
236234
end subroutine hist
237235

238236
subroutine histogram(data, bins, density, label, color)
@@ -258,9 +256,7 @@ subroutine histogram(data, bins, density, label, color)
258256
real(8), intent(in), optional :: color(3)
259257

260258
call ensure_global_figure_initialized()
261-
! TODO: Implement hist method in figure_core
262-
! call fig%hist(data, bins=bins, density=density, label=label, color=color)
263-
call log_error("hist() not yet implemented - please use main branch for histogram support")
259+
call fig%hist(data, bins=bins, density=density, label=label, color=color)
264260
end subroutine histogram
265261

266262
subroutine boxplot(data, position, width, label, show_outliers, horizontal, color)

0 commit comments

Comments
 (0)