Grid.frink

Download or view Grid.frink in plain text format


/** This class makes a grid with coordinates around a graphics object.

    To use it, create a graphics object g with natural coordinates.  Completely
    draw into that graphics object.  Then, you can create another graphics
    object with grid lines using the following:

   grid = new Grid
   grid.auto[g]
   g.add[grid.getGrid[]]

   This creates automatic coordinate lines around the graphics object.  You
   can create a grid with more control using the functions provided below.  See
   the auto function to see some typical calls.

   You can also manually add grid lines corresponding to calendar dates.
   To make this work, the date axis of your graphic should use the JD[date]
   function to generate and add coordinates.

   See sample usage in manhattanhengemoon.frink and simplegraph5.frink . 
*/


class Grid
{
   /** The graphics object this will use to render to. */
   var g2 = new graphics

   /** A flag indicating we've initialized a font. */
   var fontInitialized = false

   /** Static fields for Calendar fields. */
   class var YEAR         = staticJava["java.util.Calendar", "YEAR"]
   class var MONTH        = staticJava["java.util.Calendar", "MONTH"]
   class var DAY_OF_MONTH = staticJava["java.util.Calendar", "DAY_OF_MONTH"]
   class var HOUR_OF_DAY  = staticJava["java.util.Calendar", "HOUR_OF_DAY"]
   class var MINUTE       = staticJava["java.util.Calendar", "MINUTE"]
   class var SECOND       = staticJava["java.util.Calendar", "SECOND"]
   class var MILLISECOND  = staticJava["java.util.Calendar", "MILLISECOND"]

   var horizUnit = 1
   var verticalUnit = -1
   
   /** Make an automatic grid. */
   auto[g is graphics, enclose=false, maxMajorTicks=10, maxMinorTicks=40] :=
   {
//      println["Doing horizontal lines"]
      autoHorizontalLines[g, enclose, verticalUnit, maxMajorTicks, maxMinorTicks]
//      println["Doing vertical lines"]
      autoVerticalLines[g, enclose, horizUnit, maxMajorTicks, maxMinorTicks]
      color[0,0,0]
//      println["Doing horizontal labels"]
      autoHorizontalLabels[g, enclose, verticalUnit, maxMajorTicks, maxMinorTicks]
//      println["Doing vertical labels"]
      autoVerticalLabels[g, enclose, horizUnit, maxMajorTicks, maxMinorTicks]
   }

   /** Set the default units for the horizontal and vertical axes.  If numbers
       increase upward, verticalUnit should be -1 */

   setUnits[horizUnit, verticalUnit] :=
   {
      this.horizUnit = horizUnit
      this.verticalUnit = verticalUnit
   }
   
   /** Make automatic horizontal lines */
   autoHorizontalLines[g is graphics, enclose=false, unit=verticalUnit, maxMajorTicks=10, maxMinorTicks=40] :=
   {
//      println["In autoHorizontal lines, unit=$unit"]
      [left, top, right, bottom] = getBoundingBox[g]
      if left == undef or top == undef
         return
      
      width = right-left
      height = bottom-top

      // TODO:  THIS DOES NOT ENSURE THAT THE NUMBER OF MAJOR AND MINOR TICKS
      // ARE MULTIPLES OF EACH OTHER.  This makes things look weird like you're
      // "missing" some ticks or have weird "extra" ticks.  Refactor this so
      // that minor ticks are an integer multiple of major ticks.
      if (maxMinorTicks > 0) and (height != 0 height)
      {
         color[.5,.5,.5,.2]
         minorTick = calcAutoTickSize[height, maxMinorTicks, unit]
//         println["minorTick is $minorTick"]
         makeHorizontalLines[g, minorTick, enclose]
      }

      if (maxMajorTicks > 0) and (height != 0 height)
      {
         color[.5,.5,.5,.3]
         majorTick = calcAutoTickSize[height, maxMajorTicks, unit]
         if (majorTick < 1 unit)
            majorTick = 1 unit
//         println["majorTick is $majorTick"]         
         makeHorizontalLines[g, majorTick, enclose]
      }
   }

   
   /** Make automatic horizontal labels */
   autoHorizontalLabels[g is graphics, enclose=false, unit=verticalUnit, maxMajorTicks=10, maxMinorTicks=40, formatFunc=undef] :=
   {
//      println["In autoHorizontalLabels, unit=$unit"]
      [left, top, right, bottom] = getBoundingBox[g]
      if left == undef or top == undef
         return

      width = right-left
      height = bottom-top

      // TODO:  Set label colors
      if (maxMinorTicks > 0) and (height != 0 height)
      {
         minorTick = calcAutoTickSize[height, maxMinorTicks, unit]
         makeHorizontalLabels[g, minorTick, unit, enclose, formatFunc]
      }
   }

   
   /** Make automatic vertical lines */
   autoVerticalLines[g is graphics, enclose=false, unit=horizUnit, maxMajorTicks=10, maxMinorTicks=40] :=
   {
      [left, top, right, bottom] = getBoundingBox[g]
      if left == undef or top == undef
         return

      width = right-left
      height = bottom-top

      // TODO:  Set line colors
      // 
      // TODO:  THIS DOES NOT ENSURE THAT THE NUMBER OF MAJOR AND MINOR TICKS
      // ARE MULTIPLES OF EACH OTHER.  This makes things look weird like you're
      // "missing" some ticks or have weird "extra" ticks.  Refactor this so
      // that minor ticks are an integer multiple of major ticks.
      if (maxMinorTicks > 0) and (width != 0 width)
      {
         minorTick = calcAutoTickSize[width, maxMinorTicks, unit]
         makeVerticalLines[g, minorTick, enclose]
      }

      if (maxMajorTicks > 0) and (width != 0 width)
      {
         majorTick = calcAutoTickSize[width, maxMajorTicks, unit]
         if (majorTick < 1 unit)
            majorTick = 1 unit
         makeVerticalLines[g, majorTick, enclose]
      }
   }

   /** Make automatic vertical labels */
   autoVerticalLabels[g is graphics, enclose=false, unit=horizUnit, maxMajorTicks=10, maxMinorTicks=40, formatFunc=undef] :=
   {
      [left, top, right, bottom] = getBoundingBox[g]
      if left == undef or top == undef
         return

      width = right-left
      height = bottom-top

      // TODO:  Set line colors
      if (maxMinorTicks > 0) and (width != 0 width)
      {
         minorTick = calcAutoTickSize[width, maxMinorTicks, unit]
         makeVerticalLabels[g, minorTick, unit, enclose, formatFunc]
      }
   }

   
   /** Makes horizontal lines in the current drawing color.

       args:
         g:        An already-generated graphics object that we're going to
                   generate lines for.
         stepSize: the interval between lines.  This should have the same
                   dimensions as the vertical axis of the graphics object
         enclose:  A boolean flag.  If true, this makes at least one line
                   above and below the object, enclosing it.
   */

   makeHorizontalLines[g is graphics, stepSize, enclose = false] :=
   {
      [west, north, east, south] = getBoundingBox[g]
      if left == undef or top == undef
         return

      if enclose
      {
         southest = ceil[south, stepSize]
         northest = floor[north, stepSize]
      } else
      {
         southest = floor[south, stepSize]
         northest = ceil[north, stepSize]
      }
      
      for lat = northest to southest + 1/2 stepSize step stepSize
         g2.line[west, lat, east, lat]
   }

   
   /** Makes labels on the sides of the grid.

       args:
         g:        An already-generated graphics object that we're going to
                   generate labels for.
         stepSize: the interval between lines.  This should have the same
                   dimensions as the vertical axis of the graphics object
         enclose:  A boolean flag.  If true, this makes at least one label
                   above and below the object, enclosing it.
   */

   makeHorizontalLabels[g is graphics, stepSize, unit, enclose = false, formatFunc=undef] :=
   {
      if ! fontInitialized
         initializeFont[g]
      
      [west, north, east, south] = getBoundingBox[g]
      if left == undef or top == undef
         return

      if enclose
      {
         southest = ceil[south, stepSize]
         northest = floor[north, stepSize]
      } else
      {
         southest = floor[south, stepSize]
         northest = ceil[north, stepSize]
      }

      unitNum = unit
      if isString[unitNum]
         unitNum = eval[unit]
      
      digits = 0
      if abs[stepSize] < abs[unitNum]
         digits = ceil[-log[abs[stepSize/unitNum]]]
      for lat = northest to southest + 1/2 stepSize step stepSize
      {
         if (formatFunc == undef)
            text = format[lat, unit, digits]
         else
            text = formatFunc[lat]
         
         g2.text[" $text", east, lat, "left", "center"]
         g2.text["$text ", west, lat, "right", "center"]
      }
   }

   
   /** Makes vertical lines in the current drawing color.

       args:
         g:        An already-generated graphics object that we're going to
                   generate labels for.
         stepSize: the interval between lines.  This should have the same
                   dimensions as the horizontal axis of the graphics object
         enclose:  A boolean flag.  If true, this makes at least one line
                   to the left and right the object, enclosing it.
   */

   makeVerticalLines[g is graphics, stepSize, enclose = false] :=
   {
//      println["In makeVerticalLines[$stepSize]"]
      
      [west, north, east, south] = getBoundingBox[g]
      if left == undef or top == undef
         return

//      println["West is $west,  east is $east"]

      if enclose
      {
         westest = floor[west, stepSize]
         eastest = ceil[east, stepSize]
      } else
      {
         westest = ceil[west, stepSize]
         eastest = floor[east, stepSize]
      }
      
      for long = westest to eastest + 1/2 stepSize step stepSize
         g2.line[long, north, long, south]
   }

   /** Makes vertical lines for calendar dates/times.  This assumes that
       the horizontal axis is a Julian Day.

       args:
         g:        An already-generated graphics object that we're going to
                   generate labels for.
   
         field:    A field corresponding to one of the fields: YEAR, MONTH,
                   DAY_OF_MONTH, HOUR_OF_DAY, MINUTE, SECOND, MILLISECOND
   
         enclose:  A boolean flag.  If true, this makes at least one line
                   to the left and right the object, enclosing it.
   
         tz:       A string indicating the (optional timezone)
   */

   makeVerticalCalendarLines[g is graphics, field, enclose=false, tz=undef] :=
   {
      [west, north, east, south] = getBoundingBox[g]
      if left == undef or top == undef
         return

      beginDate = JD[west]
      endDate = JD[east]

//      println["West is $west,  east is $east"]

      if enclose
      {
         earliest = beginPlusOffset[beginDate, field, 0]
         latest   = beginPlusOffset[endDate, field, 1]
      } else
      {
         earliest = beginPlusOffset[beginDate,field, 1]
         latest =   beginPlusOffset[endDate, field, 0]
      }

      date = earliest
      while date <= latest
      {
         g2.line[JD[date], north, JD[date], south]
         date = beginPlusOffset[date, field, 1]
      }
   }

   
   /** Makes vertical labels in the current drawing color.

       args:
         g:        An already-generated graphics object that we're going to
                   generate labels for.
         stepSize: the interval between lines.  This should have the same
                   dimensions as the horizontal axis of the graphics object
         enclose:  A boolean flag.  If true, this makes at least one line
                   to the left and right of the object, enclosing it.
   */

   makeVerticalLabels[g is graphics, stepSize, unit, enclose = false, formatFunc=undef] :=
   {
      if ! fontInitialized
         initializeFont[g]
      
      [west, north, east, south] = getBoundingBox[g]
      if west == undef or north == undef
         return

      if enclose
      {
         westest = floor[west, stepSize]
         eastest = ceil[east, stepSize]
      } else
      {
         westest = ceil[west, stepSize]
         eastest = floor[east, stepSize]
      }

      digits = 0
      unitNum = unit
      if isString[unitNum]
         unitNum = eval[unit]
      
      if abs[stepSize] < abs[unitNum]
         digits = ceil[-log[abs[stepSize/unitNum]]]

      // TODO:  Allow specification of rotation
      angle = -90 deg

      // If the 2 axes have different dimensions, there's no way to calculate
      // a useful rotated bounding box.
      if ! (west conforms north)
         angle = 0 deg
      
      quad = round[angle / (90 deg)] mod 4

      for long = westest to eastest + 1/2 stepSize step stepSize
      {
         if (formatFunc == undef)
            text = format[long, unit, digits]
         else
            text = formatFunc[long]

         // Bottom labels
         if quad == 0    // 0 degrees rotation
            g2.text[text, long, south, "center", "top"]
         if quad == 1    // 90 degrees ccw
            g2.text["$text ", long, south, "right", "center", angle]
         if quad == 2    // 180 degrees rotation
            g2.text[text, long, south, "center", "bottom", angle]
         if quad == 3    // 90 degrees cw
            g2.text[" $text", long, south, "left", "center", angle]
         
         // Top labels
         if quad == 0    // 0 degrees rotation
            g2.text[text, long, north, "center", "bottom"]
         if quad == 1    // 90 degrees ccw
            g2.text[" $text", long, north, "left", "center", angle]
         if quad == 2    // 180 degrees rotation
            g2.text[text, long, north, "center", "top", angle]
         if quad == 3    // 90 degrees cw
            g2.text["$text ", long, north, "right", "center", angle]
      }
   }

   
   /** Makes vertical labels for calendar dates/times.  This assumes that
       the horizontal axis is a Julian Day.

       args:
         g:        An already-generated graphics object that we're going to
                   generate labels for.
   
         field:    A field corresponding to one of the fields: YEAR, MONTH,
                   DAY_OF_MONTH, HOUR_OF_DAY, MINUTE, SECOND, MILLISECOND
   
         enclose:  A boolean flag.  If true, this makes at least one line
                   to the left and right the object, enclosing it.
   
         tz:       A string indicating the (optional timezone)

         func:     A two-argument function [date, tz] which is normally
                   undefined, but if you define it, it should return a text
                   label for the vertical axis at the specified time in the
                   specified timezone.
   */

   makeVerticalCalendarLabels[g is graphics, field, enclose=false, tz=undef, func=undef] :=
   {
      [west, north, east, south] = getBoundingBox[g]
      if left == undef or top == undef
         return

      beginDate = JD[west]
      endDate = JD[east]

      if enclose
      {
         earliest = beginPlusOffset[beginDate, field, 0]
         latest   = beginPlusOffset[endDate, field, 1]
      } else
      {
         earliest = beginPlusOffset[beginDate,field, 1]
         latest =   beginPlusOffset[endDate, field, 0]
      }

      if (func == undef)
      {
         if field == YEAR
            fmt = ###yyyy###
         if field == MONTH
            fmt = ###yyyy-MM###
         if field == DAY_OF_MONTH
            fmt = ###yyyy-MM-dd###
         if field == HOUR_OF_DAY or field == MINUTE
            fmt = ###yyyy-MM-dd HH:mm###
         if field == SECOND
            fmt = ###yyyy-MM-dd HH:mm:ss###
         if field == MILLISECOND
            fmt = ###yyyy-MM-dd HH:mm:ss.SSS ###
      }

      // TODO:  Allow specification of rotation
      angle = -90 deg

      // If the 2 axes have different dimensions, there's no way to calculate
      // a useful rotated bounding box.
      if ! (west conforms north)
         angle = 0 deg
      
      quad = round[angle / (90 deg)] mod 4
      
      date = earliest
      while date <= latest
      {
         jd = JD[date]

         if func != undef
            text = func[date, tz]
         else
         {
            if tz
               text = (date -> [fmt, tz])
            else
               text = (date -> fmt)
         }
         
         // Bottom labels
         if quad == 0    // 0 degrees rotation
            g2.text[text, jd, south, "center", "top"]
         if quad == 1    // 90 degrees ccw
            g2.text["$text ", jd, south, "right", "center", angle]
         if quad == 2    // 180 degrees rotation
            g2.text[text, jd, south, "center", "bottom", angle]
         if quad == 3    // 90 degrees cw
            g2.text[" $text", jd, south, "left", "center", angle]
         
         // Top labels
         if quad == 0    // 0 degrees rotation
            g2.text[text, jd, north, "center", "bottom"]
         if quad == 1    // 90 degrees ccw
            g2.text[" $text", jd, north, "left", "center", angle]
         if quad == 2    // 180 degrees rotation
            g2.text[text, jd, north, "center", "top", angle]
         if quad == 3    // 90 degrees cw
            g2.text["$text ", jd, north, "right", "center", angle]

         date = beginPlusOffset[date, field, 1]
      }
   }

   /** Calculates the size of a tick given the span
       (which is either width or height) and the maximum number of ticks.
       This tries to make the ticks a multiple of 10, but if enough ticks fit,
       it will return a multiple of 2 or 5. */

   calcAutoTickSize[span, maxTicks, unit] :=
   {
//      println["in calcAutoTickSize, span=$span, maxTicks=$maxTicks, unit=$unit"]
      dimensionless = span / abs[unit]
      if ! (dimensionless conforms 1)
      {
         println["in Grid.calcAutoTickSize[]:  When graphing points with units of measure, you must first call grid.setUnits[horizUnits, verticalUnits] before calling any auto... functions.  The arguments should be the base dimensions you want to see the results in, e.g. m/s^2, or 1 for dimensionless values.  The second argument verticalUnits should be negative (e.g. -m/s^2, -1) if the values increase upwards."]
         return undef
      }
      
      tick = 10^ceil[log[dimensionless / maxTicks]] abs[unit]
      if (span / tick) * 5 < maxTicks
         tick = tick / 5
      else
         if (span / tick) * 2 < maxTicks
            tick = tick / 2

      return tick        
   }

   
   /** Sets the current drawing color to the specified color. */
   color[c is color] :=
   {
      g2.color[c]
   }

   
   /** Sets the current drawing color to the specified color. */
   color[r, g, b, a=1] :=
   {
      g2.color[r, g, b, a]
   }

   
   /** Sets the font name and height of the font.  This is analogous to
       graphics.font[fontName, height] */

   font[fontName, height] :=
   {
      g2.font[fontName, height]
      fontInitialized = true
   }

   /** Sets the stroke width used in the graph fron this point on. */
   stroke[size] :=
   {
      g2.stroke[size]
   }
   
   /** If the font has not been initialized to something, its dimensions will
       be unknown and labels won't be sized correctly.  This allocates a
       graphics object and tries to guess a reasonable font height based on
       the size of the graphic. */

   initializeFont[g is graphics] :=
   {
      if ! fontInitialized
      {
         [west, north, east, south] = getBoundingBox[g]
         if west == undef or north == undef
            return

         height = (south - north) / 60
         font["Monospaced", height]
         fontInitialized = true
      }
   }

   /** Returns a graphics object for the grid that has been generated. */
   getGrid[] :=
   {
      return g2
   }

   /** Returns a graphics object for the grid that has been generated. */
   getGraphics[] :=
   {
      return g2
   }
}


Download or view Grid.frink in plain text format


This is a program written in the programming language Frink.
For more information, view the Frink Documentation or see More Sample Frink Programs.

Alan Eliasen was born 20217 days, 15 hours, 57 minutes ago.