docs: def cycle(arrays, descs=None, cadence=0.6, toworlds=None,
            drawoverlay=None, yflip=False, tostatuses=None, run_main=True,
            save_after_viewing=None):
      """Interactively display a series of 2D data arrays.
  
      arrays
        An iterable of 2D arrays (a 3D array works).
      descs
        An iterable of text descriptions, one for each array
      cadence
        The time delay before the next array is shown, in seconds.
      tostatuses
        An iterable of functions that convert cursor positions to a textual
        status output corresponding to that position. FIXME details needed.
      toworlds
        An iterable of functions that convert cursor positions to a
        latitude/longitude pair that is displayed in the status output.
        The `tostatuses` keyword is a more generic version of this.
        FIXME details needed.
      drawoverlay
        An optional function that draws an overlay on the display after
        the underlying data image is presented. FIXME details needed.
      yflip
        If true, have the numerical *y* coordinates have 0 refer to the
        bottom of the image. Note that the data array is still drawn such
        that its first row appears at the top!
      run_main
        If true, run the Gtk mainloop explicitly so that the function does
        not return until the window is closed. If false, no mainloop is run.
        If the application happens to already be running a mainloop in the
        background, the window will appear and the user will be able to
        interact with it while this thread continues executing.
      save_after_viewing
        If set to a string containing an integer percent-formatting specifier,
        the data will be written to a series of PNG files after the window is
        closed.
  
      """
      n = len(arrays)
      amin = amax = h = w = None
  
      if toworlds is not None and tostatuses is not None:
          raise ValueError('only one of "toworlds" and "tostatuses" may be given')
  
      if descs is None:
          descs = [''] * n
  
      for array in arrays:
          thish, thisw = array.shape
          thismin, thismax = array.min(), array.max()
  
          if not np.isfinite(thismin):
              thismin = array[np.ma.where(np.isfinite(array))].min()
          if not np.isfinite(thismax):
              thismax = array[np.ma.where(np.isfinite(array))].max()
  
          if amin is None:
              w, h, amin, amax = thisw, thish, thismin, thismax
          else:
              if thisw != w:
                  raise ValueError('array widths not all equal')
              if thish != h:
                  raise ValueError('array heights not all equal')
  
              amin = min(amin, thismin)
              amax = max(amax, thismax)
  
      stride = cairo.ImageSurface.format_stride_for_width(cairo.FORMAT_ARGB32, w)
      assert stride % 4 == 0 # stride is in bytes
      imgdata = np.empty((n, h, stride // 4), dtype=np.uint32)
      fixed = np.empty((n, h, w), dtype=np.int32)
      antimask = np.empty((n, h, w), dtype=np.bool_)
      surfaces = [None] * n
  
      imgdata.fill(0xFF000000)
  
      for i, array in enumerate(arrays):
          surfaces[i] = cairo.ImageSurface.create_for_data(imgdata[i], cairo.FORMAT_ARGB32,
                                                           w, h, stride)
  
          if np.ma.is_masked(array):
              filled = array.filled(amin)
              antimask[i] = ~array.mask
          else:
              filled = array
              antimask[i].fill(True)
  
          fixed[i] = (filled - amin) * (0x0FFFFFF0 / (amax - amin))
  
      def getn():
          return n
  
      def getshapei(i):
          return w, h
  
      def getdesci(i):
          return descs[i]
  
      clipped = np.zeros((h, w), dtype=np.int32) # scratch arrays -- two needed
      clipped2 = np.zeros((h, w), dtype=np.uint32) # to make numpy ufunc casting happy
  
      def settuningi(i, tunerx, tunery):
          np.bitwise_and(imgdata[i], 0xFF000000, imgdata[i])
  
          fmin = int(0x0FFFFFF0 * tunerx)
          fmax = int(0x0FFFFFF0 * tunery)
  
          if fmin == fmax:
              np.add(imgdata[i], 255 * (fixed[i] > fmin).astype(np.uint32), imgdata[i])
          else:
              np.clip(fixed[i], fmin, fmax, clipped)
              np.subtract(clipped, fmin, clipped)
              np.multiply(clipped, 255. / (fmax - fmin), clipped2, casting='unsafe')
              np.add(imgdata[i], clipped2, imgdata[i])
  
          np.multiply(imgdata[i], antimask[i], imgdata[i])
  
      def getsurfacei(i, xoffset, yoffset, width, height):
          return surfaces[i], xoffset, yoffset
  
      # see comment in view()
      nomasks = [not np.ma.is_masked(a) or a.mask is np.ma.nomask
                 for a in arrays]
  
      if tostatuses is None:
          if toworlds is None:
              tostatuses = [None] * n
          else:
              from .astutil import fmthours, fmtdeglat
  
              def make_status_func(toworld):
                  def status(y_and_x):
                      lat, lon = toworld(y_and_x)
                      return 'lat=%s lon=%s' % (fmtdeglat(lat),
                                                fmthours(lon))
  
              tostatuses = [make_status_func(toworlds[i]) for i in range(n)]
  
      def fmtstatusi(i, x, y):
          s = ''
          row = int(np.floor(y + 0.5))
          col = int(np.floor(x + 0.5))
          if row >= 0 and col >= 0 and row < h and col < w:
              if nomasks[i] or not arrays[i].mask[row,col]:
                  s += '%g ' % arrays[i][row,col]
          if yflip:
              y = h - 1 - y
              row = h - 1 - row
          s += '[%d,%d] x=%.1f y=%.1f' % (row, col, x, y)
          if tostatuses[i] is not None:
              s += ' ' + tostatuses[i](np.array([y, x]))
          return s
  
      cycler = Cycler()
      cycler.set_n_getter(getn)
      cycler.set_shape_getter(getshapei)
      cycler.set_desc_getter(getdesci)
      cycler.set_tuning_setter(settuningi)
      cycler.set_surface_getter(getsurfacei)
      cycler.set_status_formatter(fmtstatusi)
      cycler.set_overlay_drawer(drawoverlay)
      cycler.win.show_all()
  
      if run_main:
          cycler.win.connect('destroy', Gtk.main_quit)
          Gtk.main()
      else:
          cycler.win.connect('destroy', lambda e: cycler.win.destroy())
  
      if save_after_viewing is not None:
          for i in range(n):
              filename = save_after_viewing % (i,)
              settuningi(i, cycler.last_tunerx,  cycler.last_tunery)
              surface, xoffset, yoffset = getsurfacei(i, 0, 0, w, h)
              surface.write_to_png(filename)