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 20162 days, 22 hours, 26 minutes ago.