diff --git a/._.DS_Store b/._.DS_Store new file mode 100755 index 00000000..28c42fb2 Binary files /dev/null and b/._.DS_Store differ diff --git a/example/disconnected_lines.f90 b/example/disconnected_lines.f90 new file mode 100644 index 00000000..9cfef3ca --- /dev/null +++ b/example/disconnected_lines.f90 @@ -0,0 +1,63 @@ +program disconnected_lines + use fortplot + use fortplot_figure, only: figure_t + use, intrinsic :: ieee_arithmetic, only: ieee_value, ieee_quiet_nan + implicit none + + type(figure_t) :: fig + real(8) :: x(11), y(11), nan + integer :: i + + ! Get NaN value + nan = ieee_value(nan, ieee_quiet_nan) + + call fig%initialize(800, 600) + + ! Create data with three disconnected segments using NaN as separator + ! First segment: sine wave from 0 to pi + x(1:4) = [0.0_8, 1.047_8, 2.094_8, 3.142_8] ! 0, pi/3, 2pi/3, pi + do i = 1, 4 + y(i) = sin(x(i)) + end do + + ! NaN separator + x(5) = nan + y(5) = nan + + ! Second segment: cosine wave from pi to 2pi + x(6:9) = [3.142_8, 4.189_8, 5.236_8, 6.283_8] ! pi, 4pi/3, 5pi/3, 2pi + do i = 6, 9 + y(i) = cos(x(i)) + end do + + ! NaN separator + x(10) = nan + y(10) = nan + + ! Third segment: horizontal line at y=0.5 + x(11) = 7.0_8 + y(11) = 0.5_8 + + ! Plot disconnected segments with markers and lines + call fig%add_plot(x, y, label='Disconnected segments', linestyle='o-') + + ! Add single point (will be disconnected from the line) + call fig%add_plot([8.0_8], [-0.5_8], linestyle='rs', label='Single point') + + ! Configure plot + call fig%set_title('Disconnected Line Segments Example') + call fig%set_xlabel('x') + call fig%set_ylabel('y') + call fig%legend() + + ! Save in multiple formats + call fig%savefig('build/example/disconnected_lines.png') + call fig%savefig('build/example/disconnected_lines.pdf') + call fig%savefig('build/example/disconnected_lines.txt') + + print *, "Disconnected lines example saved to:" + print *, " - build/example/disconnected_lines.png" + print *, " - build/example/disconnected_lines.pdf" + print *, " - build/example/disconnected_lines.txt" + +end program disconnected_lines \ No newline at end of file diff --git a/src/fortplot_figure_core.f90 b/src/fortplot_figure_core.f90 index f9a1f043..0bcf6e82 100644 --- a/src/fortplot_figure_core.f90 +++ b/src/fortplot_figure_core.f90 @@ -1028,7 +1028,8 @@ subroutine render_line_plot(self, plot_idx) end subroutine render_line_plot subroutine render_markers(self, plot_idx) - !! Render markers at each data point + !! Render markers at each data point, skipping NaN values + use, intrinsic :: ieee_arithmetic, only: ieee_is_nan class(figure_t), intent(inout) :: self integer, intent(in) :: plot_idx character(len=:), allocatable :: marker @@ -1042,6 +1043,9 @@ subroutine render_markers(self, plot_idx) if (marker == 'None') return do i = 1, size(self%plots(plot_idx)%x) + ! Skip points with NaN values + if (ieee_is_nan(self%plots(plot_idx)%x(i)) .or. ieee_is_nan(self%plots(plot_idx)%y(i))) cycle + x_trans = apply_scale_transform(self%plots(plot_idx)%x(i), self%xscale, self%symlog_threshold) y_trans = apply_scale_transform(self%plots(plot_idx)%y(i), self%yscale, self%symlog_threshold) call self%backend%draw_marker(x_trans, y_trans, marker) @@ -1411,13 +1415,20 @@ subroutine draw_line_with_style(self, plot_idx, linestyle) end subroutine draw_line_with_style subroutine render_solid_line(self, plot_idx) - !! Render solid line by drawing all segments + !! Render solid line by drawing all segments, breaking on NaN values + use, intrinsic :: ieee_arithmetic, only: ieee_is_nan class(figure_t), intent(inout) :: self integer, intent(in) :: plot_idx integer :: i real(wp) :: x1_screen, y1_screen, x2_screen, y2_screen do i = 1, size(self%plots(plot_idx)%x) - 1 + ! Skip segment if either point contains NaN + if (ieee_is_nan(self%plots(plot_idx)%x(i)) .or. ieee_is_nan(self%plots(plot_idx)%y(i)) .or. & + ieee_is_nan(self%plots(plot_idx)%x(i+1)) .or. ieee_is_nan(self%plots(plot_idx)%y(i+1))) then + cycle + end if + ! Apply scale transformations x1_screen = apply_scale_transform(self%plots(plot_idx)%x(i), self%xscale, self%symlog_threshold) y1_screen = apply_scale_transform(self%plots(plot_idx)%y(i), self%yscale, self%symlog_threshold) @@ -1430,6 +1441,7 @@ end subroutine render_solid_line subroutine render_patterned_line(self, plot_idx, linestyle) !! Render line with continuous pattern across segments (matplotlib-style) + use, intrinsic :: ieee_arithmetic, only: ieee_is_nan class(figure_t), intent(inout) :: self integer, intent(in) :: plot_idx character(len=*), intent(in) :: linestyle @@ -1439,25 +1451,41 @@ subroutine render_patterned_line(self, plot_idx, linestyle) real(wp) :: pattern(20), pattern_length integer :: pattern_size, pattern_index logical :: drawing - integer :: i + integer :: i, valid_count real(wp) :: x1_screen, y1_screen, x2_screen, y2_screen, dx, dy ! Get transformed data range for proper pattern scaling real(wp) :: x_range, y_range, plot_scale real(wp), allocatable :: x_trans(:), y_trans(:) + logical, allocatable :: valid_points(:) ! Transform all data points to get proper scaling allocate(x_trans(size(self%plots(plot_idx)%x))) allocate(y_trans(size(self%plots(plot_idx)%y))) + allocate(valid_points(size(self%plots(plot_idx)%x))) + valid_count = 0 do i = 1, size(self%plots(plot_idx)%x) - x_trans(i) = apply_scale_transform(self%plots(plot_idx)%x(i), self%xscale, self%symlog_threshold) - y_trans(i) = apply_scale_transform(self%plots(plot_idx)%y(i), self%yscale, self%symlog_threshold) + valid_points(i) = .not. (ieee_is_nan(self%plots(plot_idx)%x(i)) .or. ieee_is_nan(self%plots(plot_idx)%y(i))) + if (valid_points(i)) then + x_trans(i) = apply_scale_transform(self%plots(plot_idx)%x(i), self%xscale, self%symlog_threshold) + y_trans(i) = apply_scale_transform(self%plots(plot_idx)%y(i), self%yscale, self%symlog_threshold) + valid_count = valid_count + 1 + else + x_trans(i) = 0.0_wp + y_trans(i) = 0.0_wp + end if end do - x_range = maxval(x_trans) - minval(x_trans) - y_range = maxval(y_trans) - minval(y_trans) - plot_scale = max(x_range, y_range) + ! Handle case where all points are NaN + if (valid_count > 0) then + x_range = maxval(x_trans, mask=valid_points) - minval(x_trans, mask=valid_points) + y_range = maxval(y_trans, mask=valid_points) - minval(y_trans, mask=valid_points) + plot_scale = max(x_range, y_range) + 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 @@ -1502,6 +1530,15 @@ subroutine render_patterned_line(self, plot_idx, linestyle) drawing = .true. ! Start drawing do i = 1, size(self%plots(plot_idx)%x) - 1 + ! Skip segment if either point is invalid (NaN) + if (.not. valid_points(i) .or. .not. valid_points(i+1)) then + ! Reset pattern state when encountering NaN + current_distance = 0.0_wp + pattern_index = 1 + drawing = .true. + cycle + end if + x1_screen = x_trans(i) y1_screen = y_trans(i) x2_screen = x_trans(i+1) @@ -1519,7 +1556,7 @@ subroutine render_patterned_line(self, plot_idx, linestyle) end do ! Clean up - deallocate(x_trans, y_trans) + deallocate(x_trans, y_trans, valid_points) end subroutine render_patterned_line subroutine render_segment_with_pattern(self, x1, y1, x2, y2, segment_length, & diff --git a/test/test_disconnected_lines.f90 b/test/test_disconnected_lines.f90 new file mode 100644 index 00000000..08409d9b --- /dev/null +++ b/test/test_disconnected_lines.f90 @@ -0,0 +1,233 @@ +program test_disconnected_lines + use fortplot + use fortplot_figure, only: figure_t + use, intrinsic :: ieee_arithmetic, only: ieee_value, ieee_quiet_nan + implicit none + + call test_multiple_plots_should_not_connect() + call test_nan_values_should_break_lines() + call test_nan_with_patterned_lines() + call test_nan_with_markers_only() + call test_all_linestyle_patterns() + call test_edge_cases() + call test_empty_and_small_arrays() + call test_all_nan_array_edge_case() + + print *, "All disconnected lines tests passed!" + +contains + + subroutine test_multiple_plots_should_not_connect() + type(figure_t) :: fig + real(8) :: x1(2), y1(2), x2(2), y2(2) + + call fig%initialize(400, 300) + + ! First line segment: (0,0) to (1,0) + x1 = [0.0_8, 1.0_8] + y1 = [0.0_8, 0.0_8] + call fig%add_plot(x1, y1) + + ! Second line segment: (2,1) to (3,1) + x2 = [2.0_8, 3.0_8] + y2 = [1.0_8, 1.0_8] + call fig%add_plot(x2, y2) + + ! This test will fail initially, showing the issue + call fig%savefig("test_disconnected_multiple_plots.png") + + print *, "Test multiple plots: Generated test_disconnected_multiple_plots.png" + end subroutine test_multiple_plots_should_not_connect + + subroutine test_nan_values_should_break_lines() + type(figure_t) :: fig + real(8) :: x(5), y(5), nan + + ! Get NaN value + nan = ieee_value(nan, ieee_quiet_nan) + + call fig%initialize(400, 300) + + ! Two segments separated by NaN: (0,0)-(1,0) and (2,1)-(3,1) + x = [0.0_8, 1.0_8, nan, 2.0_8, 3.0_8] + y = [0.0_8, 0.0_8, nan, 1.0_8, 1.0_8] + + call fig%add_plot(x, y) + + call fig%savefig("test_disconnected_nan_break.png") + + print *, "Test NaN line breaks: Generated test_disconnected_nan_break.png" + end subroutine test_nan_values_should_break_lines + + subroutine test_nan_with_patterned_lines() + type(figure_t) :: fig + real(8) :: x(7), y(7), nan + + ! Get NaN value + nan = ieee_value(nan, ieee_quiet_nan) + + call fig%initialize(400, 300) + + ! Test dashed line with NaN breaks + x = [0.0_8, 1.0_8, 2.0_8, nan, 3.0_8, 4.0_8, 5.0_8] + y = [0.0_8, 1.0_8, 0.0_8, nan, 1.0_8, 0.0_8, 1.0_8] + + call fig%add_plot(x, y, linestyle='--') + + call fig%savefig("test_disconnected_dashed.png") + + print *, "Test dashed NaN breaks: Generated test_disconnected_dashed.png" + end subroutine test_nan_with_patterned_lines + + subroutine test_nan_with_markers_only() + type(figure_t) :: fig + real(8) :: x(5), y(5), nan + + ! Get NaN value + nan = ieee_value(nan, ieee_quiet_nan) + + call fig%initialize(400, 300) + + ! Test markers only with NaN values + x = [0.0_8, 1.0_8, nan, 3.0_8, 4.0_8] + y = [0.0_8, 1.0_8, nan, 1.0_8, 0.0_8] + + call fig%add_plot(x, y, linestyle='o') + + call fig%savefig("test_disconnected_markers.png") + + print *, "Test markers with NaN: Generated test_disconnected_markers.png" + end subroutine test_nan_with_markers_only + + subroutine test_all_linestyle_patterns() + type(figure_t) :: fig + real(8) :: x(7), y(7), nan + + ! Get NaN value + nan = ieee_value(nan, ieee_quiet_nan) + + call fig%initialize(600, 400) + + ! Test all pattern types with NaN breaks + x = [0.0_8, 1.0_8, 2.0_8, nan, 3.0_8, 4.0_8, 5.0_8] + y = [0.0_8, 1.0_8, 0.0_8, nan, 1.0_8, 0.0_8, 1.0_8] + + ! Test dotted line + call fig%add_plot(x, y + 0.0_8, linestyle=':') + + ! Test dash-dot line + call fig%add_plot(x, y + 1.5_8, linestyle='-.') + + ! Test dashed line + call fig%add_plot(x, y + 3.0_8, linestyle='--') + + ! Test unknown pattern (should fall back to solid) + call fig%add_plot(x, y + 4.5_8, linestyle='unknown') + + call fig%savefig("test_all_patterns_nan.png") + + print *, "Test all patterns: Generated test_all_patterns_nan.png" + end subroutine test_all_linestyle_patterns + + subroutine test_edge_cases() + type(figure_t) :: fig + real(8) :: x(10), y(10), nan + integer :: i + + ! Get NaN value + nan = ieee_value(nan, ieee_quiet_nan) + + call fig%initialize(400, 300) + + ! Test all NaN values + x = [(nan, i=1,10)] + y = [(nan, i=1,10)] + call fig%add_plot(x, y, linestyle='-') + + ! Test starting with NaN + x(1:5) = [nan, nan, 1.0_8, 2.0_8, 3.0_8] + y(1:5) = [nan, nan, 0.0_8, 1.0_8, 0.0_8] + call fig%add_plot(x(1:5), y(1:5), linestyle='--') + + ! Test ending with NaN + x(1:5) = [1.0_8, 2.0_8, 3.0_8, nan, nan] + y(1:5) = [0.0_8, 1.0_8, 0.0_8, nan, nan] + call fig%add_plot(x(1:5), y(1:5) + 1.0_8, linestyle=':') + + ! Test consecutive NaN values in middle + x(1:7) = [0.0_8, 1.0_8, nan, nan, nan, 4.0_8, 5.0_8] + y(1:7) = [0.0_8, 1.0_8, nan, nan, nan, 1.0_8, 0.0_8] + call fig%add_plot(x(1:7), y(1:7) + 2.0_8, linestyle='-.') + + call fig%savefig("test_edge_cases_nan.png") + + print *, "Test edge cases: Generated test_edge_cases_nan.png" + end subroutine test_edge_cases + + subroutine test_empty_and_small_arrays() + type(figure_t) :: fig + real(8) :: x(1), y(1), x2(2), y2(2), nan + + ! Get NaN value + nan = ieee_value(nan, ieee_quiet_nan) + + call fig%initialize(400, 300) + + ! Test single point (should not draw lines) + x(1) = 1.0_8 + y(1) = 1.0_8 + call fig%add_plot(x, y, linestyle='-') + + ! Test single NaN point + x(1) = nan + y(1) = nan + call fig%add_plot(x, y, linestyle='o') + + ! Test two points with one NaN + x2 = [1.0_8, nan] + y2 = [0.0_8, nan] + call fig%add_plot(x2, y2, linestyle='--') + + ! Test two NaN points + x2 = [nan, nan] + y2 = [nan, nan] + call fig%add_plot(x2, y2, linestyle=':') + + ! Test very short segment + x2 = [1.0_8, 1.000001_8] + y2 = [1.0_8, 1.000001_8] + call fig%add_plot(x2, y2 + 1.0_8, linestyle='-.') + + call fig%savefig("test_small_arrays_nan.png") + + print *, "Test small arrays: Generated test_small_arrays_nan.png" + end subroutine test_empty_and_small_arrays + + subroutine test_all_nan_array_edge_case() + type(figure_t) :: fig + real(8) :: x(5), y(5), nan + integer :: i + + ! Get NaN value + nan = ieee_value(nan, ieee_quiet_nan) + + call fig%initialize(400, 300) + + ! Create array with all NaN values to test edge case + ! where valid_count = 0 and maxval/minval might fail + x = [(nan, i=1,5)] + y = [(nan, i=1,5)] + + ! Test with different line styles to ensure no crashes + call fig%add_plot(x, y, linestyle='-') ! solid + call fig%add_plot(x, y, linestyle='--') ! dashed + call fig%add_plot(x, y, linestyle=':') ! dotted + call fig%add_plot(x, y, linestyle='-.') ! dash-dot + call fig%add_plot(x, y, linestyle='o') ! markers only + + call fig%savefig("test_all_nan_edge.png") + + print *, "Test all NaN edge case: Generated test_all_nan_edge.png" + end subroutine test_all_nan_array_edge_case + +end program test_disconnected_lines \ No newline at end of file