/** 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 } }