Silverfrost Forums

Welcome to our forums

Exploring Complex Functions Visually: Interactive Phase–Magnitude Plots in ClearWin+

16 Jan 2026 14:12GMT (Edited: 18 Jan 2026 11:13GMT) #32717

The domain_plot_mod module provides an interactive ClearWin+–based visualization tool for complex-valued functions over a two-dimensional real domain. Given strictly monotonic real coordinate arrays x and y (not necessarily uniform) and a sampled complex field z(x,y), the subroutine DOMAIN_PLOT(x,y,z) renders a phase–magnitude domain plot in which amplitude is mapped to perceptual brightness, and phase is overlaid through hue (for HSV) or optional contour structures. The code performs preprocessing including safe logarithmic scaling of magnitudes, adaptive percentile-based clipping to control extreme poles, and bilinear interpolation of amplitude and phase using unit-circle projections to avoid angular discontinuities. Rendering is handled pixel-by-pixel via a plotting callback, supporting multiple colour maps (Rainbow/HSV, Viridis, and Magma), adjustable saturation and brightness floors, optional magnitude and phase contours blended toward white or black, and interactive mouse readout of complex values with selectable angle units and ranges. The design emphasizes perceptual clarity and interactive exploration of complex function behaviour while remaining efficient and compatible with native ClearWin+ graphics primitives.

module domain_plot_mod
use clrwin
implicit none
private
public domain_plot
real*8, allocatable :: xc(:), yc(:), amp(:,:), phase(:,:), &
                      zabslog1psorted(:), ccos(:,:), csin(:,:)
real*8, parameter :: pi = 4.d0*atan(1.d0), twopi = 2.d0*pi
real*8, parameter :: halfpi = 0.5d0*pi, rec2pi = 1.d0/(2.d0*pi)
real*8, parameter :: rad2deg = 180.d0/pi
real*8, parameter :: log2 = log(2.d0), reclog2 = 1.d0/log2
real*8 :: xmin, xmax, ymin, ymax, zabsmin, zabsmax
integer :: screen_width, screen_depth, graphics_width, graphics_depth
integer :: mleft, mtop, mright, mbottom, fleft, ftop, fright, fbottom
integer :: frame_center_x, frame_width
integer :: show_mag_contours, show_phase_contours,show_grid_lines
real*8 ::  perc, sat, v_min, width, strength
logical :: first_plot_adjust
integer :: menu_on = 1
character(len=5), parameter :: contour_colour(2) = ['White','Black']
integer :: contour_colour_selected = 1
character(len=5), parameter :: phase_angle(2) = ['Rad  ','Deg  ']
integer :: phase_angle_selected = 1
character(len=8), parameter ::phase_angle_range(2) = ['-pi, +pi','0, +2pi ']
integer :: phase_angle_range_selected = 1
character(len=11), parameter :: colour_map(4) = ['Rainbow HSV','Viridis    ',&
                   'Magma      ','Turbo      ']
integer :: colour_map_selected = 1

! --------------------------------------------------------------------
! Viridis colormap
! Source: Matplotlib (Stefan van der Walt, Nathaniel Smith, Eric Firing)
! Licence: CC0 1.0 Universal (public domain, no restrictions)
! --------------------------------------------------------------------
real*8, parameter :: viridis18(18,3) = reshape([ &
    ! R column (18)
    0.267004,0.281924,0.280255,0.263663,0.237441,0.208623,&
    0.182256,0.159194,0.137770,0.121148,0.128087,0.180653,&
    0.274149,0.395174,0.535621,0.688944,0.845561,0.993248,&
    ! G column (18)
    0.004874,0.089666,0.165693,0.237631,0.305202,0.367752,&
    0.426184,0.482237,0.537492,0.592739,0.647749,0.701402,&
    0.751988,0.797475,0.835785,0.865448,0.887322,0.906157,&
    ! B column (18)
    0.329415,0.412415,0.476498,0.518762,0.541921,0.552675,&
    0.557120,0.558073,0.554906,0.544641,0.523491,0.488189,&
    0.436601,0.367757,0.281908,0.182725,0.099702,0.143936],[18,3])

! --------------------------------------------------------------------
! Magma colormap
! Source: Matplotlib (Stefan van der Walt, Nathaniel Smith, Eric Firing)
! Licence: CC0 1.0 Universal (public domain, no restrictions)
! --------------------------------------------------------------------
real*8, parameter :: magma18(18,3) = reshape([ &
    ! R column (18)
    0.001462,0.035520,0.102815,0.191460,0.291366,0.384299,&
    0.475780,0.569172,0.664915,0.761077,0.852126,0.925937,&
    0.969680,0.989363,0.996580,0.996727,0.992440,0.987053,&
    ! G column (18)
    0.000466,0.028397,0.063010,0.064818,0.064553,0.097855,&
    0.134577,0.167454,0.198075,0.231214,0.276106,0.346844,&
    0.446936,0.557873,0.668256,0.776795,0.884330,0.991438,&
    ! B column (18)
    0.013866,0.125209,0.257854,0.396152,0.475462,0.501002,&
    0.507921,0.504105,0.488836,0.460162,0.418573,0.374959,&
    0.360311,0.391671,0.456192,0.541039,0.640099,0.749504],[18,3])

! --------------------------------------------------------------------
! Turbo colourmap (Google, 2019)
! Source: Anton Mikhailov, Google AI Blog
! Licence: Apache 2.0 (permissive, attribution recommended)
! 18-sample turbo, arranged as R-block, then G-block, then B-block
! --------------------------------------------------------------------
real*8, parameter :: turbo18(18,3) = reshape([ &
  ! R column (18)
  0.189950,0.248300,0.275090,0.268780,0.186250,0.100260, &
  0.135260,0.320060,0.546580,0.715770,0.858680,0.960490, &
  0.996540,0.975450,0.910240,0.807990,0.664490,0.479600, &
  ! G column (18)
  0.071760,0.241430,0.400720,0.549950,0.697750,0.829550, &
  0.921970,0.979740,0.999070,0.968750,0.886550,0.771810, &
  0.631930,0.457400,0.295990,0.177530,0.084360,0.015830, &
  ! B column (18)
  0.232170,0.612860,0.866920,0.993030,0.954980,0.813890, &
  0.665560,0.476540,0.295810,0.208150,0.212300,0.228110, &
  0.187380,0.113050,0.048140,0.016600,0.004240,0.010550 ], [18,3])

contains

  subroutine domain_plot(x,y,z)
  real*8, intent(in) :: x(:)
  real*8, intent(in) :: y(:)
  complex*16, intent(in) :: z(:,:)
  integer :: i, j, k, nx, ny, iw
  logical :: ismonotonicinc
    nx = size(x) ; ny = size(y)
    if (size(z,dim=1) .ne. nx .or. size(z,dim=2) .ne. ny) &
      STOP 'Non-conformant arrays in call to DOMAIN_PLOT'
    ! Check that x and y are strictly monotonic increasing
    ismonotonicinc = .true.
    do i = 1, nx -1
      if (x(i+1) <= x(i)) then
        ismonotonicinc = .false.
        exit
      end if
    end do
    if (ismonotonicinc) then
      do j = 1, ny-1
        if (y(j+1) <= y(j)) then
          ismonotonicinc = .false.
          exit
        end if
      end do
    end if
    if (.not. ismonotonicinc) then
      print*, 'DOMAIN_PLOT: x and/or y are not strictly monotonic increasing'
      return
    end if
    ! Allocate storage arrays
    if (allocated(xc))    deallocate(xc)
    if (allocated(yc))    deallocate(yc)
    if (allocated(amp))   deallocate(amp)
    if (allocated(phase)) deallocate(phase)
    if (allocated(zabslog1psorted)) deallocate(zabslog1psorted)
    if (allocated(ccos))  deallocate(ccos)
    if (allocated(csin))  deallocate(csin)
    
    allocate (xc(nx),yc(ny),amp(nx,ny),phase(nx,ny),zabslog1psorted(nx*ny),ccos(nx,ny),csin(nx,ny))
    xmin = x(1) ; xmax = x(nx) ; ymin = y(1) ; ymax = y(ny)
    ! Copy arguments x and y to global module arrays xc and yc to make
    ! them accessible later
    xc = x ; yc = y
    ! Do some preprocessing before the pl callback
    zabsmin = huge(1.d0)
    zabsmax = 0.d0
    k = 0
    do j = 1, ny
      do i = 1, nx
        amp(i,j)  = abs(z(i,j))
        zabsmin = min( zabsmin, amp(i,j) )
        zabsmax = max( zabsmax, amp(i,j) )
        if (amp(i,j) > 1.0d-300) then
          phase(i,j) = atan2(aimag(z(i,j)), real(z(i,j)))
        else
          ! Avoid "starbust" 
          phase(i,j) = phase(max(1,i-1),max(1,j-1))
        end if
        ! Projections onto the unit circle
        ccos(i,j) = cos(phase(i,j))
        csin(i,j) = sin(phase(i,j))
        ! Flatten log(1+|Z|) into vector - sorted later
        k = k + 1
        zabslog1psorted(k) = safe_log1p(amp(i,j))
      end do
    end do
    ! Sort zabslog1psorted once, so don't need to do again if
    ! percentile cut off changed
    call quicksort(zabslog1psorted)
    
    if (zabsmax > zabsmin) then
      ! Prevent first render being performed before the window appears
      first_plot_adjust = .true.  
      screen_width = clearwin_info@('SCREEN_WIDTH')
      screen_depth = clearwin_info@('SCREEN_DEPTH')
      graphics_width = int(0.6d0*dble(screen_width))
      graphics_depth = int(0.7d0*dble(screen_depth))
      call calc_pl_margins(graphics_width, graphics_depth, mleft, mtop, mright, mbottom)
      ! Default pl margins are relatively large for large regions, reduce by 2/3
      mleft   = 2*mleft/3
      mtop    = 2*mtop/3
      mright  = 2*mright/3
      mbottom = 2*mbottom/3
      ftop    = mtop
      fbottom = graphics_depth - mbottom
      fleft   = mleft
      fright  = graphics_width - mright
      frame_width    = graphics_width - mleft - mright
      frame_center_x = mleft + frame_width/2
      ! Set default plotting parameters
      show_mag_contours = 1
      show_phase_contours = 1
      show_grid_lines = 0
      sat  = 0.8d0
      v_min = 0.2d0
      width = 0.06d0
      strength = 0.35d0
      ! Choose good percentile value for pole clipping using adaptpercentile
      perc = adaptpercentile(zabslog1psorted)

      iw = winio@('%ww[no_sysmenu]&')
      iw = winio@('%mn[~Exit]&',menu_on,'exit')
      iw = winio@('%mn[~Help]&',menu_on,help_cb)
      iw = winio@('%fn[Arial]%ts%bf%bg&',1.2d0,rgb@(220,220,220))
      call winop@('%pl[native, n_graphs=1,x_array,frame,link=user,external_ticks]')
      call winop@('%pl[axes_pen=2,frame_pen=2,xaxis="Re(z)",yaxis="Im(z)"]')
      call winop@('%pl[full_mouse_input,gridlines]')
      call winop_flt@('%pl[x_min]',xmin)
      call winop_flt@('%pl[x_max]',xmax)
      call winop_flt@('%pl[y_min]',ymin)
      call winop_flt@('%pl[y_max]',ymax)
      call set_pl_margins(mleft, mtop, mright, mbottom)
      iw = winio@('%2.1ob[invisible]&')
      iw = winio@('%^pl&',graphics_width,graphics_depth,2,[0.d0,1.d0],[0.d0,1.d0],pl_cb)
      iw = winio@('%cb&')
      iw = winio@('%sf&')
      iw = winio@('%fl&',0.d0,1.d0)
      iw = winio@('Colour map:%ta%ta%`bg[white]%ls%ff&', &
            colour_map, 4, colour_map_selected)
      iw = winio@('Saturation:%ta%ta%`bg[white]%6rf%nl&',sat)
      iw = winio@('Clip poles at percentile:%ta%`bg[white]%6rf%nl&',perc)
      iw = winio@('Minimum brightness:%ta%ta%`bg[white]%6rf%nl&',v_min)
      iw = winio@('Contour width:%ta%ta%`bg[white]%6rf%nl&',width)
      iw = winio@('Contour strength%ta%ta%`bg[white]%6rf%nl&',strength)
      iw = winio@('Contour colour:%ta%ta%`bg[white]%ls%ff&',contour_colour,2,contour_colour_selected)
      iw = winio@('%`fl&')
      iw = winio@('%`rb[Show magnitude contours]%nl&',show_mag_contours)
      iw = winio@('%`rb[Show phase contours]%nl&',show_phase_contours)
      iw = winio@('%`rb[Show grid lines]%nl&',show_grid_lines)
      iw = winio@('%2nl%cn%~^bn[Update]%2nl&',menu_on,update_cb)
      iw = winio@('Following change how arg F(z) is displayed%nlas mouse moves across display%2nl&')
      iw = winio@('Phase angle unit:%ta%ta%`bg[white]%ls%ff&',phase_angle,2,phase_angle_selected)
      iw = winio@('Phase angle range:%ta%ta%`bg[white]%ls%ff&',phase_angle_range,2,phase_angle_range_selected)
      iw = winio@('%cb&')
      iw = winio@('%sc&',sc_cb)
      iw = winio@('')
    else
      print*, 'Domain_plot: Input is level plane.'
    end if
  end subroutine domain_plot

  !----------------------------------------------------------------
  integer function sc_cb()
  !----------------------------------------------------------------
    first_plot_adjust = .false.
    sc_cb = update_cb()
  end function sc_cb
  
  !----------------------------------------------------------------
  integer function update_cb()
  !----------------------------------------------------------------
    call set_cursor_waiting@(1)
    call simpleplot_redraw@()
    call set_cursor_waiting@(0)
    update_cb = 2
  end function update_cb

  !----------------------------------------------------------------
  recursive integer function pl_cb()
  !----------------------------------------------------------------
  integer :: i, j, k, icol
  integer :: mouse_x, mouse_y
  real*8 :: x, y, r, h
  real*8 :: red, green, blue, t, f, d, line
  real*8 :: target_phase, fphase
  real*8 :: v, scale_factor
  real*8 :: mag, angle, xperc, recw2, v_target
  integer :: text_x, text_y, tw, th, idx, npoints
  integer(2) :: i2, j2
  integer :: val
  real*8, parameter :: ref_mag = 1000.d0
  real*8, parameter :: width_min = 1.0d-6
  character(len=256) ::clearwin_string@, cb_reason
  character(len=512) ::data_string
  character(len=*), parameter :: fmt1 = '( &
            & "Z = (",1X,SP,ES14.5E3,",",1X,SP,ES14.5E3,")",3X, &
            & "|f(Z)| =",1X,SP,ES14.5E3,3X, &
            & "arg f(Z) =",1X,SP,F7.2,1X,"rad")'
  character(len=*), parameter :: fmt2 = '( &
            & "Z = (",1X,SP,ES14.5E3,",",1X,SP,ES14.5E3,")",3X, &
            & "|f(Z)| =",1X,SP,ES14.5E3,3X, &
            & "arg f(Z) =",1X,SP,F7.1,1X,"deg")'
  logical, save :: pl_cb_active = .false.
  logical, save :: first = .true.
  integer,save :: iwhite, iblack
  
    if (pl_cb_active) then
      pl_cb = 2
      return
    end if
    
    if (first) then
      iwhite = rgb@(255,255,255)
      iblack = rgb@(0,0,0)
      first = .false.
    end if
    
    pl_cb_active = .true.
    menu_on = 0
    cb_reason = clearwin_string@('CALLBACK_REASON')
    
    if (cb_reason .eq. 'RESIZE') then
      
      pl_cb_active = .false.
      menu_on = 1
      pl_cb=2
      return
      
    else if (cb_reason .eq. 'MOUSE_MOVE') then
      
      mouse_x = clearwin_info@('GRAPHICS_MOUSE_X')
      mouse_y = clearwin_info@('GRAPHICS_MOUSE_Y')
      
      call draw_filled_rectangle@(0, int(0.9d0*dble(mtop)), graphics_width,0, iwhite)
      
      if (mouse_x .ge. fleft .and. mouse_x .le. fright .and. &
          mouse_y .ge. ftop  .and. mouse_y .le. fbottom)    then
          
        k = get_plot_data@(mouse_x,mouse_y,x,y)
        
        if (k == 0 .and. x >= xmin .and. x <= xmax .and. y >= ymin .and. y <= ymax) then
          call z_rh_bilinear(x,y,mag,angle)
          if (phase_angle_range_selected .eq. 2) then
            if (angle < 0.d0) angle = angle + twopi
          end if
          if (phase_angle_selected .eq. 1) then
            write(data_string,fmt1) x, y, mag, angle
          else
            write(data_string,fmt2) x, y, mag, angle*rad2deg
          end if
          call get_text_size@(trim(adjustl(data_string))//char(0), tw, th)
          text_x = frame_center_x - tw/2
          text_y = ftop - th - 20    ! small gap above frame
          call draw_characters@(data_string, text_x, text_y, iblack)
        end if
      end if
      pl_cb_active = .false.
      menu_on = 1
      pl_cb=2
      return
      
    else if (cb_reason .eq. 'PLOT_ADJUST') then
      
      if (first_plot_adjust) then
        pl_cb_active = .false.
        pl_cb = 2
        return
      end if
      
      npoints = size(zabslog1psorted)
      idx = max(1, min(npoints, nint(perc * dble(npoints))))
      xperc = zabslog1psorted(idx)
      xperc = exp(xperc) - 1.d0
      ! Guard against zero percentile
      scale_factor = 1.d0
      if (xperc > 0.d0) scale_factor = ref_mag / xperc

      recw2 = 1.d0/(width*width)
      !set v_target for contour colours
      if (contour_colour_selected .eq. 1) then
        ! White
        v_target = 1.d0
      else
        ! Black
        v_target = 0.d0
      end if
      
      ! Render the surface
      
      do i = fleft, fright, 1
        do j = fbottom, ftop, -1
          k = get_plot_data@(i,j,x,y)
          ! Don't draw over grid lines if show_grid_lines = 1
          if (show_grid_lines .eq. 1) then
            i2 = i ; j2 = j
            call get_rgb_value@(i2,j2,val)
            if (val .ne. iwhite) cycle
          end if
          
          ! Interpolate amplitude and phase
          call z_rh_bilinear(x,y,r,h)
          h = (h + pi) * rec2pi
          h = h - floor(h)
          r = r*scale_factor
          
          ! Base brightness from amplitude
          v = 1.d0 - 1.d0/(1.d0 + safe_log1p(r))
          !!v = safe_log1p(r) / safe_log1p(r + 1.d0)
          v = max(v, v_min)
          
          ! Magnitude contours (white or black rings)
          if (show_mag_contours == 1) then
            if (r > 0.d0 .and. width > width_min) then
                t = log(r)*reclog2
                f = t - floor(t)
                d = min(f, 1.d0 - f)
                line = exp(-d*d*recw2)
                ! Blend towards selected contour colour
                  v = v + (v_target - v)*line*strength
            end if
          end if
          
          ! Phase contours (white or black lines)
          if (show_phase_contours == 1 .and. width .gt. width_min) then
              do k = 0, 3
                target_phase = halfpi*dble(k)
                fphase = abs(h*twopi - target_phase)
                if (fphase > pi) fphase = twopi - fphase
                line = exp(-fphase*fphase*recw2)
                ! Blend towards selected contour colour
                  v = v + (v_target - v)*line*strength
            end do
          end if
          
          ! Final clamp of brightness
          v = min(1.d0, max(v_min, v))
          
          ! Convert HSV or V to RGB depending on selected colour map
          select case(colour_map_selected)
            case(1) ; call hsv_to_rgb(h, sat, v, red, green, blue)
            case(2) ; call cmap_rgb(viridis18,v, red, green, blue)
            case(3) ; call cmap_rgb(magma18,v, red, green, blue)
            case(4) ; call cmap_rgb(turbo18,v, red, green, blue)
          end select
          
          icol = rgb@(int(255.d0*red), int(255.d0*green), int(255.d0*blue))
          ! Draw pixels using draw_filled_rectangle@
          call draw_filled_rectangle@(i,j,i,j,icol)
        end do
        if (mod(i,2) == 0) call temporary_yield@()
      end do
      menu_on = 1
      pl_cb_active = .false.
      pl_cb = 2
      return
    end if
    ! Defensive, should never get here
    menu_on = 1
    pl_cb_active = .false.
    pl_cb = 2
  end function pl_cb

  !----------------------------------------------------------------
  subroutine z_rh_bilinear(xt, yt, r, h)
  !----------------------------------------------------------------
  real*8, intent(in)  :: xt, yt
  real*8, intent(out) :: r, h
  integer, save:: i=1, j=1
  real*8 :: x1, x2, y1, y2, tx, ty
  real*8 :: c, s, rrr
  ! Uses global arrays yc, xc, amp, ccos, csin
    ! Locate cell
    i  = bin_search2(xc, xt, i)     ;  j  = bin_search2(yc, yt, j)
    x1 = xc(i)  ;  x2 = xc(i+1) ;  y1 = yc(j)  ;  y2 = yc(j+1)
    tx = (xt - x1) / (x2 - x1)  ;  ty = (yt - y1) / (y2 - y1)
    ! Bilinear interpolation of amplitude
    r = (1.d0-tx)*(1.d0-ty)*amp(i,  j  ) + tx*(1.d0-ty)*amp(i+1,j  ) + &
        (1.d0-tx)*(ty     )*amp(i,  j+1) + tx*(ty     )*amp(i+1,j+1)
    ! Bilinear interpolation on unit circle
    c = (1.d0-tx)*(1.d0-ty)*ccos(i,  j  ) + tx*(1.d0-ty)*ccos(i+1,j  ) + &
        (1.d0-tx)*(ty     )*ccos(i,  j+1) + tx*(ty     )*ccos(i+1,j+1)
    s = (1.d0-tx)*(1.d0-ty)*csin(i,  j  ) + tx*(1.d0-ty)*csin(i+1,j  ) + &
        (1.d0-tx)*(ty     )*csin(i,  j+1) + tx*(ty     )*csin(i+1,j+1)
    ! Normalise and recover phase
    rrr = hypot(c,s)
    h = 0.d0
    if (rrr > 0.d0) h = atan2(s/rrr, c/rrr)
  end subroutine z_rh_bilinear

  !----------------------------------------------------------------
  subroutine hsv_to_rgb(h, s, v, r, g, b)
  !----------------------------------------------------------------
  real*8, intent(in) :: h, s, v
  real*8, intent(out) :: r, g, b
  real*8 :: i, f, p, q, t
    i = floor(6.d0*h) ; f = 6.d0*h - i
    p = v*(1.d0 - s)  ; q = v*(1.d0 - f*s)
    t = v*(1.d0 - (1.d0-f)*s)
    select case (mod(int(i),6))
      case (0); r=v; g=t; b=p
      case (1); r=q; g=v; b=p
      case (2); r=p; g=v; b=t
      case (3); r=p; g=q; b=v
      case (4); r=t; g=p; b=v
      case (5); r=v; g=p; b=q
    end select
  end subroutine hsv_to_rgb

  !----------------------------------------------------------------
  subroutine calc_pl_margins(width, height, left, top, right, bottom)
  !----------------------------------------------------------------
  integer, intent(in) :: width, height
  integer, intent(out) :: left, top, right, bottom
  integer :: m, hoffset, voffset
    m = max( 28, int(0.1d0 * max(width, height)) )
    bottom = m ;         right  = m
    hoffset = m / 2 ;    voffset = 0
    left = m + hoffset ; top  = m + voffset
  end subroutine calc_pl_margins

  !----------------------------------------------------------------
  subroutine quicksort(a)
  !----------------------------------------------------------------
  ! Textbook recursive quicksort
  !----------------------------------------------------------------
  real*8, intent(inout) :: a(:)
    call qs(a, 1, size(a))
    contains
    recursive subroutine qs(a, left, right)
    real*8, intent(inout) :: a(:)
    integer, intent(in) :: left, right
    integer :: i, j
    real*8 :: pivot, tmp
      if (left >= right) return
      pivot = a((left+right)/2)
      i = left ; j = right
      do
        do while (a(i) < pivot)
          i = i + 1
        end do
        do while (a(j) > pivot)
          j = j - 1
        end do
        if (i <= j) then
          tmp = a(i) ; a(i) = a(j) ; a(j) = tmp
          i = i + 1 ; j = j - 1
        end if
        if (i > j) exit
      end do
      call qs(a, left, j)
      call qs(a, i, right)
    end subroutine qs
  end subroutine quicksort

  !----------------------------------------------------------------
  real*8 function safe_log1p(x)
  !----------------------------------------------------------------
  ! Returns log(1+x) safely for x > -1
  !----------------------------------------------------------------
  real*8, intent(in) :: x
  real*8 :: ax
  real*8, parameter :: x_tiny = sqrt(epsilon(1.d0))
    ax = abs(x)
    if (x <= -1.d0) then
      safe_log1p = -huge(1.d0)
    else if (ax < x_tiny) then
      safe_log1p = x*(1.d0 - x*(0.5d0 - x*(1.d0/3.d0)))
    else if (x > huge(1.d0)/2.d0) then
      safe_log1p = log(x)
    else
      safe_log1p = log(1.d0 + x)
    end if
  end function safe_log1p

  real*8 function adaptpercentile(zlog)
  !-------------------------------------------------------------------
  ! Compute an adaptive percentile for amplitude clipping
  ! Input:  zlog(:) = log1p(abs(z)) sorted ascending
  ! Output: adaptive_percentile = suggested percentile [0,1]
  !-----------------------------------------------------------------------
  ! Tail Ratio           Interpretation                  Adaptive
  ! (q99-q50)/(q95-q50)                                  percentile
  !-----------------------------------------------------------------------
  ! < 0.5                Very light tail                 0.99             
  ! < 1.0                Light tail, mild outliers       0.97             
  ! < 2.0                Moderate tail                   0.95             
  ! < 4.0                Heavy tail                      0.92             
  ! >= 4.0               Extreme tail (very large poles) 0.90             
  !-----------------------------------------------------------------------
  real*8, intent(in) :: zlog(:)
  integer :: n
  real*8 :: q50, q95, q99, tail_ratio
  real*8, parameter :: default_perc = 0.95d0
    n = size(zlog)
    if (n < 10) then
      ! Too few points, fallback
      adaptpercentile = default_perc
      return
    end if
    ! Fetch percentile values
    q50 = zlog(max(1, min(n, nint(0.50d0 * dble(n)))))
    q95 = zlog(max(1, min(n, nint(0.95d0 * dble(n)))))
    q99 = zlog(max(1, min(n, nint(0.99d0 * dble(n)))))
    ! Guard against degenerate cases
    if (q95 <= q50) then
      adaptpercentile = 0.95d0
      return
    end if
    ! Tail ratio: how far the extreme tail is from the bulk
    tail_ratio = (q99 - q50) / (q95 - q50)
    ! Map tail_ratio to adaptive percentile
    if (tail_ratio < 0.5d0) then
      adaptpercentile = 0.99d0
    else if (tail_ratio < 1.0d0) then
      adaptpercentile = 0.97d0
    else if (tail_ratio < 2.0d0) then
      adaptpercentile = 0.95d0
    else if (tail_ratio < 4.0d0) then
      adaptpercentile = 0.92d0
    else
      adaptpercentile = 0.90d0
    end if
  end function adaptpercentile

  !----------------------------------------------------------------
  subroutine cmap_rgb(cmap,t, r, g, b)
  !----------------------------------------------------------------
  real*8, intent(in) :: cmap(:,:)
  real*8, intent(in)  :: t        ! 0 <= t <= 1
  real*8, intent(out) :: r, g, b
  integer :: n, i
  real*8 :: x, w
    n = size(cmap, dim=1)
    x = max(0.d0, min(1.d0, t)) * dble(n-1)
    i = int(x) + 1
    w = x - dble(i-1)
    if (i >= n) then
      r = cmap(n,1)
      g = cmap(n,2)
      b = cmap(n,3)
    else
      r = (1.d0-w)*cmap(i,1) + w*cmap(i+1,1)
      g = (1.d0-w)*cmap(i,2) + w*cmap(i+1,2)
      b = (1.d0-w)*cmap(i,3) + w*cmap(i+1,3)
    end if
  end subroutine cmap_rgb

  !----------------------------------------------------------------
  integer function bin_search2(xx, x, jlo0) result(jlo)
  !----------------------------------------------------------------
  ! An accelerated bracket search that exploits locality to 
  ! outperform binary search on correlated queries
  !----------------------------------------------------------------
  real*8, intent(in) :: xx(:), x
  integer, intent(in) :: jlo0
  integer :: n, inc, jhi, jm
  logical :: ascnd
  real*8  :: xlo, xhi
    n   = size(xx)
    xlo = xx(1)
    xhi = xx(n)
    ascnd = (xhi >= xlo)
    jlo = jlo0
    if (jlo >= 1 .and. jlo < n) then
       if (ascnd) then
          if (x >= xx(jlo) .and. x < xx(jlo+1)) return
       else
          if (x <= xx(jlo) .and. x > xx(jlo+1)) return
       end if
    end if
    if (jlo <= 0 .or. jlo > n) then
       jlo = 0
       jhi = n + 1
    else
       inc = 1
       if (ascnd) then
          if (x >= xx(jlo)) then
             do
                jhi = jlo + inc
                if (jhi > n) then
                   jhi = n + 1
                   exit
                end if
                if (x >= xx(jhi)) then
                   jlo = jhi
                   inc = inc + inc
                else
                   exit
                end if
             end do
          else
             jhi = jlo
             do
                jlo = jhi - inc
                if (jlo < 1) then
                   jlo = 0
                   exit
                end if
                if (x < xx(jlo)) then
                   jhi = jlo
                   inc = inc + inc
                else
                   exit
                end if
             end do
          end if
       else
          if (x <= xx(jlo)) then
             do
                jhi = jlo + inc
                if (jhi > n) then
                   jhi = n + 1
                   exit
               end if
                 if (x <= xx(jhi)) then
                   jlo = jhi
                   inc = inc + inc
                else
                   exit
                end if
             end do
          else
             jhi = jlo
             do
                jlo = jhi - inc
                if (jlo < 1) then
                   jlo = 0
                   exit
                end if
                if (x > xx(jlo)) then
                   jhi = jlo
                   inc = inc + inc
                else
                   exit
                end if
             end do
          end if
       end if
    end if
    do while (jhi - jlo > 1)
       jm = (jhi + jlo) / 2
       if (ascnd) then
          if (x >= xx(jm)) then
             jlo = jm
          else
            jhi = jm
           end if
       else
          if (x <= xx(jm)) then
             jlo = jm
          else
             jhi = jm
          end if
       end if
    end do
    ! If outside range, clamp to last valid index
    jlo = max(1, min(size(xx)-1, jlo))
  end function bin_search2

  !----------------------------------------------------------------
  subroutine set_pl_margins(L,T,R,B)
  !----------------------------------------------------------------
  integer, intent(in) :: L,T,R, B
  character(len=120) :: margintxt
  character(len=*), parameter :: fmt1 = '(A,I4,A,I4,A,I4,A,I4,A)'
    write(margintxt,fmt1) "%pl[margin=(",L,",",T,",",R,",",B,")]" 
    call winop@(margintxt)
  end subroutine set_pl_margins

  !----------------------------------------------------------------
  integer function help_cb()
  !----------------------------------------------------------------
  integer :: iw
    iw = winio@('%fn[Arial]%ts%bg&',1.2d0,rgb@(220,220,220))
    iw = winio@('%ob%~bg[white]&')
    iw = winio@('Colour map allows you to selection standard Rainbow/HSV colour map,%nl&')
    iw = winio@('or the perceptually uniform colour maps Viridis or Magma, and also Turbo.%2nl&')
    iw = winio@('Viridis, Magma and Turbo do not display phase angle in the pixel rendering.%2nl&')
    iw = winio@('Increasing saturation increases color intensity (for rainbow/HSV only).%2nl&')
    iw = winio@('Increasing minimum brightness raises the darkest pixels.%2nl&')
    iw = winio@('Changing percentile changes the clipping of extreme amplitudes.%2nl&')
    iw = winio@('Adjusting contour width changes the width of magnitude/phase rings.%2nl&')
    iw = winio@('Contours can be blended towards a base white or black in Rainbow/HSV.%2nl&')
    iw = winio@('Checking/unchecking contours toggles them on/off%2nl&')
    iw = winio@('Checking/unchecking grid lines draws/erases the grid%nl%cb&')
    iw = winio@('')
    help_cb = 2
  end function help_cb

end module domain_plot_mod

Below is a sample program which demonstrates the use of the code.

program test_domain_plot
use domain_plot_mod, only : domain_plot
implicit none
integer, parameter :: dp = kind(1.d0)
integer, parameter :: n = 300
integer :: i, j
real(dp) :: re_z(n), im_z(n)
complex(dp) :: f(n,n), z_temp, cj
  cj = cmplx(0.d0,1.d0,kind=dp)
  
  ! Generate a test function
  do i = 1, n
    re_z(i) = -2.05d0 + 5.0d0 * dble(i-1) / dble(n-1)
    do j = 1, n
      im_z(j)  = -2.0d0 + 5.0d0 * dble(j-1) / dble(n-1)
      z_temp = cmplx(re_z(i),im_z(j),kind=dp)
      f(i,j) = ((z_temp*z_temp - 1.d0)*(z_temp - 2.d0 - cj)**2 )/(z_temp*z_temp+2.d0+2.d0*cj)
    end do
  end do
  ! Plot function
  call domain_plot(re_z,im_z,f)
  
  ! Generate second test functon
  do i = 1, n
    re_z(i) = -2.d0 + 4.d0*dble(i-1)/dble(n-1)
    do j = 1, n
      im_z(j) = -2.d0 + 4.d0*dble(j-1)/dble(n-1)
      z_temp = cmplx(re_z(i),im_z(j),kind=dp)
      f(i,j) = (z_temp-1.d0)/((z_temp**2)+z_temp+1.d0)
    end do
  end do
  ! Plot the second test function
  call domain_plot(re_z,im_z,f)

end program test_domain_plot
Help topics: RGB@ WINIO@
17 Jan 2026 15:42GMT (Edited: 18 Jan 2026 15:53GMT) #32721

Hello Kenneth

An excellent and very informative piece of coding to highlight the graphics capabilities of Clearwin+ in ftn95. Seeing how the graphics functions are laid out and the output is a big help for those of us, myself included, who are still getting to grips with it.

Good to see how to apply/generate new colour schemes. Having used QGIS for a number of years I have generated several custom colour ramps, so it should be relatively easy to compile new colour ramps.

Lester

17 Jan 2026 18:20GMT #32722

Wow, that is very nice indeed:

image.png

18 Jan 2026 01:39GMT (Edited: 18 Jan 2026 02:02GMT) #32724

Robert's image caused me to say "That's not right!".

A micro-optimisation I added to the code at some point last week, flips the Y axis, and I was totally blind to this until this evening.

The module code in the first post has now been corrected. There is also an additional colour map "Turbo" and the rendering is slightly faster.

19 Jan 2026 13:28GMT #32734

Hi Kenneth,

Just a quick query. When plot is generated, the text and check boxes are right up against the white display region. How do you adjust it so it is further offset from the plot margin? I'm guessing this links in with the comment below too.

Also, can the check box radio buttons be left-aligned in the centre of the options section. I can see how to centre the blocks iw = winio@('%cn%`rb[Show magnitude contours]%nl&',show_mag_contours), but this centres the radio button and text as a whole. Is there a simple syntax to control the distance between the check box (rb) and the text associated with it?

Lester

Help topics: WINIO@
19 Jan 2026 14:14GMT #32735

Lester, If you replace the block of code starting at iw = winio@('%ww[no_sysmenu]&') and ending at iw = winio@('') you will get the desired layout. The extra space between the text/checkboxes and the graphics region is generated by an extra cell in the %ob. The checkboxes are enclosed by a %cn%ob ....... %cb to centre them. %ff ensures the next control is below the last. If you removed the [invisible] option from each %ob you will see how the %ob are nested to create the required alignment.

      iw = winio@('%ww[no_sysmenu]&')
      iw = winio@('%mn[~Exit]&',menu_on,'exit')
      iw = winio@('%mn[~Help]&',menu_on,help_cb)
      iw = winio@('%fn[Arial]%ts%bf%bg&',1.2d0,rgb@(220,220,220))
      call winop@('%pl[native, n_graphs=1,x_array,frame,link=user,external_ticks]')
      call winop@('%pl[axes_pen=2,frame_pen=2,xaxis="Re(z)",yaxis="Im(z)"]')
      call winop@('%pl[full_mouse_input,gridlines]')
      call winop_flt@('%pl[x_min]',xmin)
      call winop_flt@('%pl[x_max]',xmax)
      call winop_flt@('%pl[y_min]',ymin)
      call winop_flt@('%pl[y_max]',ymax)
      call set_pl_margins(mleft, mtop, mright, mbottom)
      iw = winio@('%3.1ob[invisible]&')
      iw = winio@('%^pl&',graphics_width,graphics_depth,2,[0.d0,1.d0],[0.d0,1.d0],pl_cb)
      iw = winio@('%cb    %cb&')
      iw = winio@('%sf&')
      iw = winio@('%fl&',0.d0,1.d0)
      iw = winio@('Colour map:%ta%ta%`bg[white]%ls%ff&', &
            colour_map, 4, colour_map_selected)
      iw = winio@('Saturation:%ta%ta%`bg[white]%6rf%nl&',sat)
      iw = winio@('Clip poles at percentile:%ta%`bg[white]%6rf%nl&',perc)
      iw = winio@('Minimum brightness:%ta%ta%`bg[white]%6rf%nl&',v_min)
      iw = winio@('Contour width:%ta%ta%`bg[white]%6rf%nl&',width)
      iw = winio@('Contour strength%ta%ta%`bg[white]%6rf%nl&',strength)
      iw = winio@('Contour colour:%ta%ta%`bg[white]%ls%ff&',contour_colour,2,contour_colour_selected)
      iw = winio@('%`fl&')
      iw = winio@('%ff%cn%ob[invisible]&')
      iw = winio@('%`rb[Show magnitude contours]%nl&',show_mag_contours)
      iw = winio@('%`rb[Show phase contours]%nl&',show_phase_contours)
      iw = winio@('%`rb[Show grid lines]%nl&',show_grid_lines)
      iw = winio@('%cb&')
      iw = winio@('%ff%2nl%cn%~^bn[Update]%2nl&',menu_on,update_cb)
      iw = winio@('Following change how arg F(z) is displayed%nlas mouse moves across display%2nl&')
      iw = winio@('Phase angle unit:%ta%ta%`bg[white]%ls%ff&',phase_angle,2,phase_angle_selected)
      iw = winio@('Phase angle range:%ta%ta%`bg[white]%ls%ff&',phase_angle_range,2,phase_angle_range_selected)
      iw = winio@('%cb&')
      iw = winio@('%sc&',sc_cb)
      iw = winio@('')
Help topics: %cb %ff %ob RGB@ WINIO@
19 Jan 2026 15:47GMT #32737

Thanks for the information Kenneth, that really helps with understanding the formatting.

Lester

Please login to reply.