diff --git a/src/pyplot_module.F90 b/src/pyplot_module.F90 index e0b6523..e229171 100644 --- a/src/pyplot_module.F90 +++ b/src/pyplot_module.F90 @@ -58,6 +58,7 @@ module pyplot_module private character(len=:), allocatable :: str !! string buffer + integer :: str_len = 0 !! current length of `str` character(len=1) :: raw_str_token = ' ' !! will be 'r' if using raw strings @@ -91,6 +92,7 @@ module pyplot_module procedure, public :: add_bar !! add a barplot to pyplot instance procedure, public :: add_imshow !! add an image plot (using `imshow`) procedure, public :: add_hist !! add a histogram plot to pyplot instance + procedure, public :: add_text !! add text annotation to pyplot instance procedure, public :: savefig !! save plots of pyplot instance procedure, public :: showfig !! show plots of pyplot instance procedure, public :: destroy !! destroy pyplot instance @@ -114,6 +116,7 @@ subroutine destroy(me) class(pyplot),intent(inout) :: me !! pyplot handler + me%str_len = 0 if (allocated(me%str)) deallocate(me%str) if (allocated(me%real_fmt)) deallocate(me%real_fmt) @@ -136,22 +139,36 @@ subroutine add_str(me,str) integer :: n_str !! length of input `str` character(len=:),allocatable :: tmp !! tmp string for building the result - ! original - !me%str = me%str//str//new_line(' ') - if (len(str)==0) return + ! original + !me%str = me%str//str//new_line(' ') ! the above can sometimes cause a stack overflow in the ! intel Fortran compiler, so we replace with this: if (allocated(me%str)) then - n_old = len(me%str) - n_str = len(str) - allocate(character(len=n_old+n_str+1) :: tmp) - tmp(1:n_old) = me%str - tmp(n_old+1:) = str//new_line(' ') - call move_alloc(tmp, me%str) + + ! if there is room in the current string, then + ! insert str into it. otherwise, allocate a new string + ! with enough room and move the old string into it before adding str. + + if (me%str_len + len(str) + 1 <= len(me%str)) then + me%str(me%str_len+1:me%str_len+len(str)) = str + me%str_len = me%str_len + len(str) + me%str(me%str_len+1:me%str_len+1) = new_line(' ') + me%str_len = me%str_len + 1 + else + n_old = me%str_len + n_str = len(str) + ! double size to avoid too many allocations + allocate(character(len=2*(n_old+n_str)) :: tmp) + tmp(1:n_old) = me%str + tmp(n_old+1:) = str//new_line(' ') + call move_alloc(tmp, me%str) + me%str_len = n_old + n_str + 1 + end if else allocate(me%str, source = str//new_line(' ')) + me%str_len = len(me%str) end if end subroutine add_str @@ -1099,6 +1116,107 @@ subroutine add_imshow(me, x, xlim, ylim, istat) end subroutine add_imshow !***************************************************************************************** +!***************************************************************************************** +!> author: Jacob Williams +! +! Add a text annotation to the plot. +! +!### Example +!```fortran +! call plt%add_text(5.0_wp, 10.0_wp, 'Peak', fontsize=12, color='red', & +! horizontalalignment='center', verticalalignment='bottom') +!``` + + subroutine add_text(me, x, y, txt, fontsize, color, horizontalalignment, & + verticalalignment, rotation, alpha, fontweight, & + fontstyle, istat) + + class(pyplot), intent(inout) :: me !! pyplot handler + real(wp), intent(in) :: x !! x position in data coordinates + real(wp), intent(in) :: y !! y position in data coordinates + character(len=*), intent(in) :: txt !! text string to display + integer, intent(in), optional :: fontsize !! font size + character(len=*), intent(in), optional :: color !! text color + character(len=*), intent(in), optional :: horizontalalignment !! horizontal alignment: 'left', 'center', 'right' + character(len=*), intent(in), optional :: verticalalignment !! vertical alignment: 'top', 'center', 'bottom', 'baseline' + real(wp), intent(in), optional :: rotation !! rotation angle in degrees + real(wp), intent(in), optional :: alpha !! transparency (0.0 to 1.0) + character(len=*), intent(in), optional :: fontweight !! 'normal', 'bold', 'heavy', 'light', 'ultrabold', 'ultralight' + character(len=*), intent(in), optional :: fontstyle !! 'normal', 'italic', 'oblique' + integer, intent(out),optional :: istat !! status output (0 means no problems) + + character(len=:), allocatable :: xstr !! x value stringified + character(len=:), allocatable :: ystr !! y value stringified + character(len=:), allocatable :: txt_str !! plot command string + character(len=:), allocatable :: rotation_str !! rotation value stringified + character(len=:), allocatable :: alpha_str !! alpha value stringified + character(len=max_int_len) :: fontsize_str !! fontsize value stringified + + if (allocated(me%str)) then + + if (present(istat)) istat = 0 + + ! convert coordinates to strings + call real_to_string(x, me%real_fmt, xstr) + call real_to_string(y, me%real_fmt, ystr) + + ! build the text command + txt_str = 'ax.text('//xstr//', '//ystr//', '//& + trim(me%raw_str_token)//'"'//trim(txt)//'"' + + ! add optional parameters + if (present(fontsize)) then + call optional_int_to_string(fontsize, fontsize_str, '10') + txt_str = txt_str//', fontsize='//trim(fontsize_str) + end if + + if (present(color)) then + txt_str = txt_str//', color='//trim(me%raw_str_token)//'"'//trim(color)//'"' + end if + + if (present(horizontalalignment)) then + txt_str = txt_str//', horizontalalignment='//trim(me%raw_str_token)//'"'//& + trim(horizontalalignment)//'"' + end if + + if (present(verticalalignment)) then + txt_str = txt_str//', verticalalignment='//trim(me%raw_str_token)//'"'//& + trim(verticalalignment)//'"' + end if + + if (present(rotation)) then + call real_to_string(rotation, me%real_fmt, rotation_str) + txt_str = txt_str//', rotation='//rotation_str + end if + + if (present(alpha)) then + call real_to_string(alpha, me%real_fmt, alpha_str) + txt_str = txt_str//', alpha='//alpha_str + end if + + if (present(fontweight)) then + txt_str = txt_str//', fontweight='//trim(me%raw_str_token)//'"'//& + trim(fontweight)//'"' + end if + + if (present(fontstyle)) then + txt_str = txt_str//', style='//trim(me%raw_str_token)//'"'//& + trim(fontstyle)//'"' + end if + + txt_str = txt_str//')' + + ! add the text command + call me%add_str(txt_str) + + else + if (present(istat)) istat = -1 + write(error_unit,'(A)') 'Error in add_text: pyplot class not properly initialized.' + end if + + end subroutine add_text +!***************************************************************************************** + !***************************************************************************************** !> author: Alexander Sandrock ! @@ -1451,7 +1569,7 @@ subroutine execute(me, pyfile, istat, python, wait) end if !write to the file: - write(iunit, '(A)') me%str + write(iunit, '(A)') me%str(1:me%str_len) !to ensure that the file is there for the next !command line call, we have to close it here. diff --git a/test/color_test.f90 b/test/color_test.f90 index 7cd83b1..c7b411d 100644 --- a/test/color_test.f90 +++ b/test/color_test.f90 @@ -9,7 +9,7 @@ program color_test implicit none type(pyplot) :: plt !! pytplot handler - integer :: istat + integer :: istat, i real(wp), parameter :: F(3) = [0.4510d0, 0.3098d0, 0.5882d0] ! Fortran-lang color real(wp), parameter :: Y(3) = [0.9608d0, 0.8157d0, 0.0118d0] ! Yellow @@ -22,8 +22,14 @@ program color_test call plt%initialize(figsize=[20,10],title='color test') - call plt%add_plot(Ax,Ay,label='',linestyle='o',markersize=5,color=F) - call plt%add_plot(Bx,By,label='',linestyle='o',markersize=5,color=Y) + call plt%add_plot(Ax,Ay,label='',linestyle='o',markersize=50,color=F) + call plt%add_plot(Bx,By,label='',linestyle='o',markersize=50,color=Y) + + do i = 1, 3 + call plt%add_text(Ax(i), Ay(i), "F", fontsize=40, color="white", & + horizontalalignment="center", verticalalignment="center", & + istat=istat) + end do call plt%savefig(testdir//'color_test.png',& pyfile=testdir//'color_test.py',istat=istat) diff --git a/test/text_test.f90 b/test/text_test.f90 new file mode 100644 index 0000000..0a59e01 --- /dev/null +++ b/test/text_test.f90 @@ -0,0 +1,86 @@ +!***************************************************************************************** +!> author: Jacob Williams +! license: BSD +! +! Test of the add_text routine. + + program text_test + + use pyplot_module + use iso_fortran_env, only: wp => real64 + + implicit none + + type(pyplot) :: plt + real(wp), dimension(100) :: x, y + real(wp) :: pi + integer :: i + integer :: istat + + pi = acos(-1.0_wp) + + ! Generate some data + do i = 1, 100 + x(i) = real(i-1, wp) * 2.0_wp * pi / 99.0_wp + y(i) = sin(x(i)) + end do + + ! Initialize the plot + call plt%initialize(grid=.true., xlabel='x', ylabel='sin(x)', & + title='Text Annotation Demo', & + legend=.false., figsize=[10,6]) + + ! Add the main plot + call plt%add_plot(x, y, label='sin(x)', linestyle='b-', linewidth=2) + + ! Add various text annotations with different styles + call plt%add_text(pi/2.0_wp, 1.0_wp, 'Peak at π/2', & + fontsize=12, & + color='red', & + horizontalalignment='center', & + verticalalignment='bottom', & + istat=istat) + + call plt%add_text(3.0_wp*pi/2.0_wp, -1.0_wp, 'Minimum at 3π/2', & + fontsize=12, & + color='blue', & + horizontalalignment='center', & + verticalalignment='top', & + istat=istat) + + call plt%add_text(pi, 0.0_wp, 'Zero crossing', & + fontsize=10, & + color='green', & + rotation=45.0_wp, & + horizontalalignment='left', & + verticalalignment='bottom', & + istat=istat) + + call plt%add_text(0.5_wp, -0.8_wp, 'Bold text', & + fontsize=14, & + color='purple', & + fontweight='bold', & + istat=istat) + + call plt%add_text(5.0_wp, 0.5_wp, 'Italic text', & + fontsize=11, & + color='orange', & + fontstyle='italic', & + istat=istat) + + call plt%add_text(4.0_wp, -0.5_wp, 'Semi-transparent', & + fontsize=10, & + color='black', & + alpha=0.5_wp, & + istat=istat) + + ! Save the figure + call plt%savefig('test/text_test.png', istat=istat) + + if (istat /= 0) then + write(*,*) 'Error saving figure' + else + write(*,*) 'Figure saved successfully: test/text_test.png' + end if + + end program text_test