fortplot_svg_axes.f90 Source File


Source Code

module fortplot_svg_axes
    !! Standalone SVG axes and label rendering

   use, intrinsic :: iso_fortran_env, only: wp => real64
   implicit none

   private
   public :: svg_render_ylabel_impl, svg_draw_axes_labels_impl

contains

   subroutine svg_render_ylabel_impl(pa_left, pa_bottom, pa_height, ylabel, svg_elem)
      real(wp), intent(in) :: pa_left, pa_bottom, pa_height
      character(len=*), intent(in) :: ylabel
      character(len=:), allocatable, intent(out) :: svg_elem
      real(wp) :: x, y
      character(len=1024) :: local_elem

      x = pa_left - 35.0_wp
      y = pa_bottom + pa_height*0.5_wp

      write (local_elem, '(A,F0.3,A,F0.3,A,A,A)') &
         '<text x="', x, '" y="', y, &
         '" font-family="sans-serif" font-size="12" '// &
         'text-anchor="middle" transform="rotate(-90 ', x, ' ', y, ')">', &
         trim(ylabel), '</text>'
      svg_elem = trim(local_elem)
   end subroutine svg_render_ylabel_impl

   subroutine svg_draw_axes_labels_impl(svg_content, pa_left, pa_right, pa_bottom, &
         pa_top, xscale, yscale, symlog_threshold, &
         x_min, x_max, y_min, y_max, &
         title, xlabel, ylabel, &
         x_date_format, y_date_format, &
         z_min, z_max, has_3d_plots, &
         custom_xticks, custom_xtick_labels, &
         custom_yticks, custom_ytick_labels)
      character(len=:), allocatable, intent(inout) :: svg_content
      real(wp), intent(in) :: pa_left, pa_right, pa_bottom, pa_top
      character(len=*), intent(in) :: xscale, yscale
      real(wp), intent(in) :: symlog_threshold
      real(wp), intent(in) :: x_min, x_max, y_min, y_max
      character(len=:), allocatable, intent(in), optional :: title, xlabel, ylabel
      character(len=*), intent(in), optional :: x_date_format, y_date_format
      real(wp), intent(in), optional :: z_min, z_max
      logical, intent(in) :: has_3d_plots
      real(wp), intent(in), optional :: custom_xticks(:), custom_yticks(:)
      character(len=*), intent(in), optional :: custom_xtick_labels(:)
      character(len=*), intent(in), optional :: custom_ytick_labels(:)
      character(len=1024) :: elem
      real(wp) :: mid_x, mid_y

      associate (xs => xscale, ys => yscale, st => symlog_threshold, &
            zmi => z_min, zma => z_max, h3d => has_3d_plots)
      end associate
      if (present(x_date_format)) then
         associate (unused_xfmt => len_trim(x_date_format))
         end associate
      end if
      if (present(y_date_format)) then
         associate (unused_yfmt => len_trim(y_date_format))
         end associate
      end if

      write (elem, '(A,F0.3,A,F0.3,A,F0.3,A,F0.3,A)') &
         '<rect x="', pa_left, '" y="', pa_top, '" width="', pa_right - pa_left, &
         '" height="', pa_bottom - pa_top, '" fill="none" stroke="black"/>'
      svg_content = svg_content//elem//new_line('a')

      call svg_draw_ticks(svg_content, pa_left, pa_right, pa_bottom, pa_top, &
         x_min, x_max, y_min, y_max, custom_xticks, &
         custom_xtick_labels, custom_yticks, custom_ytick_labels)

      mid_x = (pa_left + pa_right)/2.0_wp
      mid_y = (pa_top + pa_bottom)/2.0_wp

      if (present(title)) then
         if (len_trim(title) > 0) then
            write (elem, '(A,F0.3,A,F0.3,A,A,A)') &
               '<text x="', mid_x, '" y="', pa_top - 10.0_wp, &
               '" font-family="sans-serif" font-size="14" '// &
               'font-weight="bold" text-anchor="middle">', &
               trim(title), '</text>'
            svg_content = svg_content//elem//new_line('a')
         end if
      end if

      if (present(xlabel)) then
         if (len_trim(xlabel) > 0) then
            write (elem, '(A,F0.3,A,F0.3,A,A,A)') &
               '<text x="', mid_x, '" y="', pa_bottom + 35.0_wp, &
               '" font-family="sans-serif" font-size="12" text-anchor="middle">', &
               trim(xlabel), '</text>'
            svg_content = svg_content//elem//new_line('a')
         end if
      end if

      if (present(ylabel)) then
         if (len_trim(ylabel) > 0) then
            write (elem, '(A,F0.3,A,F0.3,A,F0.3,A,F0.3,A,A,A)') &
               '<text x="', pa_left - 45.0_wp, '" y="', mid_y, &
               '" font-family="sans-serif" font-size="12" '// &
               'text-anchor="middle" transform="rotate(-90 ', &
               pa_left - 45.0_wp, ' ', mid_y, ')">', trim(ylabel), '</text>'
            svg_content = svg_content//elem//new_line('a')
         end if
      end if
   end subroutine svg_draw_axes_labels_impl

   subroutine svg_draw_ticks(svg_content, pa_left, pa_right, pa_bottom, pa_top, &
         x_min, x_max, y_min, y_max, custom_xticks, &
         custom_xtick_labels, custom_yticks, custom_ytick_labels)
      character(len=:), allocatable, intent(inout) :: svg_content
      real(wp), intent(in) :: pa_left, pa_right, pa_bottom, pa_top
      real(wp), intent(in) :: x_min, x_max, y_min, y_max
      real(wp), intent(in), optional :: custom_xticks(:), custom_yticks(:)
      character(len=*), intent(in), optional :: custom_xtick_labels(:)
      character(len=*), intent(in), optional :: custom_ytick_labels(:)
      character(len=1024) :: elem, val_str
      integer :: i, nx, ny
      logical :: custom_x, custom_y
      real(wp) :: tick_x, tick_y, val

      custom_x = .false.
      custom_y = .false.
      if (present(custom_xticks) .and. present(custom_xtick_labels)) then
         custom_x = size(custom_xticks) == size(custom_xtick_labels)
      end if
      if (present(custom_yticks) .and. present(custom_ytick_labels)) then
         custom_y = size(custom_yticks) == size(custom_ytick_labels)
      end if
      nx = 5
      ny = 5
      if (custom_x) nx = size(custom_xticks)
      if (custom_y) ny = size(custom_yticks)
      do i = 1, nx
         val = x_min + real(i - 1, wp)/4.0_wp*(x_max - x_min)
         if (custom_x) val = custom_xticks(i)
         if (val < x_min .or. val > x_max) cycle
         tick_x = 0.5_wp*(pa_left + pa_right)
         if (x_max > x_min) &
            tick_x = pa_left + (val - x_min)/(x_max - x_min)*(pa_right - pa_left)
         write (val_str, '(G10.3)') val
         if (custom_x) val_str = custom_xtick_labels(i)
         write (elem, '(A,F0.3,A,F0.3,A,F0.3,A,F0.3,A)') &
            '<line x1="', tick_x, '" y1="', pa_bottom, &
            '" x2="', tick_x, '" y2="', pa_bottom + 5.0_wp, '" stroke="black"/>'
         svg_content = svg_content//elem//new_line('a')
         write (elem, '(A,F0.3,A,F0.3,A,A,A)') &
            '<text x="', tick_x, '" y="', pa_bottom + 18.0_wp, &
            '" font-family="sans-serif" font-size="10" text-anchor="middle">', &
            trim(adjustl(val_str)), '</text>'
         svg_content = svg_content//elem//new_line('a')
      end do

      do i = 1, ny
         val = y_max - real(i - 1, wp)/4.0_wp*(y_max - y_min)
         if (custom_y) val = custom_yticks(i)
         if (val < y_min .or. val > y_max) cycle
         tick_y = 0.5_wp*(pa_top + pa_bottom)
         if (y_max > y_min) &
            tick_y = pa_top + (y_max - val)/(y_max - y_min)*(pa_bottom - pa_top)
         write (val_str, '(G10.3)') val
         if (custom_y) val_str = custom_ytick_labels(i)
         write (elem, '(A,F0.3,A,F0.3,A,F0.3,A,F0.3,A)') &
            '<line x1="', pa_left - 5.0_wp, '" y1="', tick_y, &
            '" x2="', pa_left, '" y2="', tick_y, '" stroke="black"/>'
         svg_content = svg_content//elem//new_line('a')
         write (elem, '(A,F0.3,A,F0.3,A,A,A)') &
            '<text x="', pa_left - 8.0_wp, '" y="', tick_y + 4.0_wp, &
            '" font-family="sans-serif" font-size="10" text-anchor="end">', &
            trim(adjustl(val_str)), '</text>'
         svg_content = svg_content//elem//new_line('a')
      end do

   end subroutine svg_draw_ticks

end module fortplot_svg_axes