Skip to content

Commit 0bda9c8

Browse files
krystophnyclaude
andauthored
Fix #47: Support NaN values for disconnected line segments (#48)
* 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> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent cd503bc commit 0bda9c8

File tree

4 files changed

+342
-9
lines changed

4 files changed

+342
-9
lines changed

._.DS_Store

4 KB
Binary file not shown.

example/disconnected_lines.f90

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
program disconnected_lines
2+
use fortplot
3+
use fortplot_figure, only: figure_t
4+
use, intrinsic :: ieee_arithmetic, only: ieee_value, ieee_quiet_nan
5+
implicit none
6+
7+
type(figure_t) :: fig
8+
real(8) :: x(11), y(11), nan
9+
integer :: i
10+
11+
! Get NaN value
12+
nan = ieee_value(nan, ieee_quiet_nan)
13+
14+
call fig%initialize(800, 600)
15+
16+
! Create data with three disconnected segments using NaN as separator
17+
! First segment: sine wave from 0 to pi
18+
x(1:4) = [0.0_8, 1.047_8, 2.094_8, 3.142_8] ! 0, pi/3, 2pi/3, pi
19+
do i = 1, 4
20+
y(i) = sin(x(i))
21+
end do
22+
23+
! NaN separator
24+
x(5) = nan
25+
y(5) = nan
26+
27+
! Second segment: cosine wave from pi to 2pi
28+
x(6:9) = [3.142_8, 4.189_8, 5.236_8, 6.283_8] ! pi, 4pi/3, 5pi/3, 2pi
29+
do i = 6, 9
30+
y(i) = cos(x(i))
31+
end do
32+
33+
! NaN separator
34+
x(10) = nan
35+
y(10) = nan
36+
37+
! Third segment: horizontal line at y=0.5
38+
x(11) = 7.0_8
39+
y(11) = 0.5_8
40+
41+
! Plot disconnected segments with markers and lines
42+
call fig%add_plot(x, y, label='Disconnected segments', linestyle='o-')
43+
44+
! Add single point (will be disconnected from the line)
45+
call fig%add_plot([8.0_8], [-0.5_8], linestyle='rs', label='Single point')
46+
47+
! Configure plot
48+
call fig%set_title('Disconnected Line Segments Example')
49+
call fig%set_xlabel('x')
50+
call fig%set_ylabel('y')
51+
call fig%legend()
52+
53+
! Save in multiple formats
54+
call fig%savefig('build/example/disconnected_lines.png')
55+
call fig%savefig('build/example/disconnected_lines.pdf')
56+
call fig%savefig('build/example/disconnected_lines.txt')
57+
58+
print *, "Disconnected lines example saved to:"
59+
print *, " - build/example/disconnected_lines.png"
60+
print *, " - build/example/disconnected_lines.pdf"
61+
print *, " - build/example/disconnected_lines.txt"
62+
63+
end program disconnected_lines

src/fortplot_figure_core.f90

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,7 +1028,8 @@ subroutine render_line_plot(self, plot_idx)
10281028
end subroutine render_line_plot
10291029

10301030
subroutine render_markers(self, plot_idx)
1031-
!! Render markers at each data point
1031+
!! Render markers at each data point, skipping NaN values
1032+
use, intrinsic :: ieee_arithmetic, only: ieee_is_nan
10321033
class(figure_t), intent(inout) :: self
10331034
integer, intent(in) :: plot_idx
10341035
character(len=:), allocatable :: marker
@@ -1042,6 +1043,9 @@ subroutine render_markers(self, plot_idx)
10421043
if (marker == 'None') return
10431044

10441045
do i = 1, size(self%plots(plot_idx)%x)
1046+
! Skip points with NaN values
1047+
if (ieee_is_nan(self%plots(plot_idx)%x(i)) .or. ieee_is_nan(self%plots(plot_idx)%y(i))) cycle
1048+
10451049
x_trans = apply_scale_transform(self%plots(plot_idx)%x(i), self%xscale, self%symlog_threshold)
10461050
y_trans = apply_scale_transform(self%plots(plot_idx)%y(i), self%yscale, self%symlog_threshold)
10471051
call self%backend%draw_marker(x_trans, y_trans, marker)
@@ -1411,13 +1415,20 @@ subroutine draw_line_with_style(self, plot_idx, linestyle)
14111415
end subroutine draw_line_with_style
14121416

14131417
subroutine render_solid_line(self, plot_idx)
1414-
!! Render solid line by drawing all segments
1418+
!! Render solid line by drawing all segments, breaking on NaN values
1419+
use, intrinsic :: ieee_arithmetic, only: ieee_is_nan
14151420
class(figure_t), intent(inout) :: self
14161421
integer, intent(in) :: plot_idx
14171422
integer :: i
14181423
real(wp) :: x1_screen, y1_screen, x2_screen, y2_screen
14191424

14201425
do i = 1, size(self%plots(plot_idx)%x) - 1
1426+
! Skip segment if either point contains NaN
1427+
if (ieee_is_nan(self%plots(plot_idx)%x(i)) .or. ieee_is_nan(self%plots(plot_idx)%y(i)) .or. &
1428+
ieee_is_nan(self%plots(plot_idx)%x(i+1)) .or. ieee_is_nan(self%plots(plot_idx)%y(i+1))) then
1429+
cycle
1430+
end if
1431+
14211432
! Apply scale transformations
14221433
x1_screen = apply_scale_transform(self%plots(plot_idx)%x(i), self%xscale, self%symlog_threshold)
14231434
y1_screen = apply_scale_transform(self%plots(plot_idx)%y(i), self%yscale, self%symlog_threshold)
@@ -1430,6 +1441,7 @@ end subroutine render_solid_line
14301441

14311442
subroutine render_patterned_line(self, plot_idx, linestyle)
14321443
!! Render line with continuous pattern across segments (matplotlib-style)
1444+
use, intrinsic :: ieee_arithmetic, only: ieee_is_nan
14331445
class(figure_t), intent(inout) :: self
14341446
integer, intent(in) :: plot_idx
14351447
character(len=*), intent(in) :: linestyle
@@ -1439,25 +1451,41 @@ subroutine render_patterned_line(self, plot_idx, linestyle)
14391451
real(wp) :: pattern(20), pattern_length
14401452
integer :: pattern_size, pattern_index
14411453
logical :: drawing
1442-
integer :: i
1454+
integer :: i, valid_count
14431455
real(wp) :: x1_screen, y1_screen, x2_screen, y2_screen, dx, dy
14441456

14451457
! Get transformed data range for proper pattern scaling
14461458
real(wp) :: x_range, y_range, plot_scale
14471459
real(wp), allocatable :: x_trans(:), y_trans(:)
1460+
logical, allocatable :: valid_points(:)
14481461

14491462
! Transform all data points to get proper scaling
14501463
allocate(x_trans(size(self%plots(plot_idx)%x)))
14511464
allocate(y_trans(size(self%plots(plot_idx)%y)))
1465+
allocate(valid_points(size(self%plots(plot_idx)%x)))
14521466

1467+
valid_count = 0
14531468
do i = 1, size(self%plots(plot_idx)%x)
1454-
x_trans(i) = apply_scale_transform(self%plots(plot_idx)%x(i), self%xscale, self%symlog_threshold)
1455-
y_trans(i) = apply_scale_transform(self%plots(plot_idx)%y(i), self%yscale, self%symlog_threshold)
1469+
valid_points(i) = .not. (ieee_is_nan(self%plots(plot_idx)%x(i)) .or. ieee_is_nan(self%plots(plot_idx)%y(i)))
1470+
if (valid_points(i)) then
1471+
x_trans(i) = apply_scale_transform(self%plots(plot_idx)%x(i), self%xscale, self%symlog_threshold)
1472+
y_trans(i) = apply_scale_transform(self%plots(plot_idx)%y(i), self%yscale, self%symlog_threshold)
1473+
valid_count = valid_count + 1
1474+
else
1475+
x_trans(i) = 0.0_wp
1476+
y_trans(i) = 0.0_wp
1477+
end if
14561478
end do
14571479

1458-
x_range = maxval(x_trans) - minval(x_trans)
1459-
y_range = maxval(y_trans) - minval(y_trans)
1460-
plot_scale = max(x_range, y_range)
1480+
! Handle case where all points are NaN
1481+
if (valid_count > 0) then
1482+
x_range = maxval(x_trans, mask=valid_points) - minval(x_trans, mask=valid_points)
1483+
y_range = maxval(y_trans, mask=valid_points) - minval(y_trans, mask=valid_points)
1484+
plot_scale = max(x_range, y_range)
1485+
else
1486+
! All points are NaN, use default scale
1487+
plot_scale = 1.0_wp
1488+
end if
14611489

14621490
! Define pattern lengths (matplotlib-like)
14631491
dash_len = plot_scale * 0.03_wp ! 3% of range
@@ -1502,6 +1530,15 @@ subroutine render_patterned_line(self, plot_idx, linestyle)
15021530
drawing = .true. ! Start drawing
15031531

15041532
do i = 1, size(self%plots(plot_idx)%x) - 1
1533+
! Skip segment if either point is invalid (NaN)
1534+
if (.not. valid_points(i) .or. .not. valid_points(i+1)) then
1535+
! Reset pattern state when encountering NaN
1536+
current_distance = 0.0_wp
1537+
pattern_index = 1
1538+
drawing = .true.
1539+
cycle
1540+
end if
1541+
15051542
x1_screen = x_trans(i)
15061543
y1_screen = y_trans(i)
15071544
x2_screen = x_trans(i+1)
@@ -1519,7 +1556,7 @@ subroutine render_patterned_line(self, plot_idx, linestyle)
15191556
end do
15201557

15211558
! Clean up
1522-
deallocate(x_trans, y_trans)
1559+
deallocate(x_trans, y_trans, valid_points)
15231560
end subroutine render_patterned_line
15241561

15251562
subroutine render_segment_with_pattern(self, x1, y1, x2, y2, segment_length, &

0 commit comments

Comments
 (0)