Graphing With Imaging Lingo
June 20, 2003
by Robert J. Cameron
Using imaging Lingo to create a graph on-the-fly seems like an obvious thing to do. It's probably one of the first things that occur to most developers when they hear of the technology. This article describes a process I went through to graph some information over time with imaging Lingo, and hopefully will help your thought processes in your graphing endeavors.
The graphing system I built is capable of graphing multiple types of information at a time (several lines on the line graph). To enable easy changing of the z-order of the graph lines at runtime, I put each graph line in a separate bitmap cast member. Ignoring Macromedia's admonitions, I duplicate a blank cast member on-the-fly for each of my graph lines, plus my background grid. Extensive testing showed no problems. If this concerns you, simply create as many blank castmembers as you'll need.
Click here to see an example of this technique in action.
First, we need to get the start date and end date of the data we want to show. Next, decide how you want to show the range of values. For my system, I decided to have zero at the bottom of the chart and the highest value that appears in the set at the top. This may not apply well to your data, but it is easy enough to set up whatever range you would like.
Once we have the start date, end date, and a decision of how to display the range of values (the y position of the values), we need to determine how many pixels in the x direction will correspond to a single date, and how many pixels in the y direction will correspond to a unit of measurement of our value. This depends on how large we wish to display our chart. I've stored the width and height of my chart in pChartWidth and pChartHeight, respectively.
Let's start with the dates. Director's date object makes it very easy to determine how many days separate two dates. (Remember, some months have different lengths, leap years, etc. You could roll your own, but I found the date object suited my purposes.)
startDateObj = date (startDate)
endDateObj = date (endDate)
dateRange = endDateObj - startDateObj
pxPerDate = pChartWidth / float (dateRange)
So pxPerDate will be the distance in the x direction in pixels between data points, assuming we have a data point for every day. Next, we need to know what the highest-value data point is for our set of dates. This can be obtained with a sort or a simple for loop, putting the highest value into a valueRange variable. Since the low end of my value range will always be zero, simple division will give me the pixel distance in the y direction between any two consecutive integer values on the chart:
pxPerValue = pChartHeight / float (valueRange)
Of course, if valueRange is greater than pChartHeight, this is going to result in less than a one pixel difference between consecutive integer values.
Before I start charting values, I want some graph lines in the background of my chart to make it easier for the user to compare values. You may wish for your chart lines to be a static image, but I scale mine to the dates and values I'm showing. Arbitrarily, I chose to have a tickmark on the chart every 5 values if the range is less than 100, and show tickmarks every 10 values if it is greater than that.
member("emptyChart").duplicate(member 1 of castlib "charts")
workingImage = member 1 of castlib "charts"
workingImage.name = "graphlines"
--first, round to the next lowest 10
graphValueMax = valueRange /10 * 10
--if less than 100, mark every 5, else mark every 10
if graphValueMax < 100 then
valuesPerTick = 5
else
valuesPerTick = 10
end if
numLines = graphValueMax / valuesPerTick
tempImage = workingImage.image.duplicate()
repeat with i = 1 to numLines
adjustedValue = pChartHeight - (i * valuesPerTick * pxPerValue)
fromPoint = point(0,adjustedValue)
toPoint = point(pChartWidth,adjustedValue)
myResult = tempImage.draw(fromPoint,toPoint,[#shapeType:#line, #lineSize:1, #color: rgb(200,200,200)])
end repeat
That gives me y axis tickmarks. Next, I want the tickmarks for the dates. I decided to tick every day if less that 30 days are showing, every 5 days if the range is between 30 and 100, and every 10 days if it is greater than that. Again, the date object shows its usefulness.
As part of the process of creating the date tickmarks, I want to label them, so the user can see which day a tickmark corresponds to. Within my repeat loop, the actual date that the current tickmark corresponds to is startDateObj + (i * datesPerTick). I feed that date into my dateSlasher function, which returns a string in MM/DD/YYYY format, then set a text cast member's text property equal to that string. In order to have the text written vertically along the tickmark, I turn the text using a function I wrote called rotateImage, which turns any image -90 degrees. Then I copy the pixels from the rotated text image into the overall image for the graphlines.
if dateRange < 30 then
datesPerTick = 1
else if dateRange < 100 then
datesPerTick = 5
else
datesPerTick = 10
end if
numLines = dateRange / datesPerTick
repeat with i = 1 to numLines
adjustedDate = i * datesPerTick * pxPerDate
fromPoint = point(adjustedDate,0)
toPoint = point(adjustedDate,pChartHeight)
myResult = tempImage.draw(fromPoint,toPoint,[#shapeType:#line, #lineSize:1, #color: rgb(200,200,200)])
actualDate = startDateObj + (i * datesPerTick)
dateStr = dateSlasher(actualDate)
member("dateLabel").text = dateStr
rotatedText = rotateImage(member("dateLabel").image)
rangeLabelRect = rotatedText.rect
targetRect = rect(adjustedDate,pChartHeight rangeLabelRect.bottom,adjustedDate+rangeLabelRect.right,pChartHeight)
tempImage.copyPixels(rotatedText,targetRect,rangeLabelRect)
end repeat
Now, on to graphing the actual data points! Cycling through your data will depend on how it is stored. Mine consists of property lists within a property list. I loop through the types of information I want to show, and loop through the dates for which I have data within that. With the data I'm getting, it's possible that the type of data I am charting may not exist for a given date, so I need to check for a valid value before graphing.
How do we get the dot on the graph that will correspond to each piece of data? I subtract the date associated with the data point from the startDate, then multiply by pxPerDate to get the x position. To get the y position, I have to work down from the top of my chart, since my chart's origin is at the lower left and the origin for imaging Lingo pixel positions is in the upper left. I multiply the data value by pxPerValue, then subtract that result from the pixel height of my chart. Now I've got my x and y positions, which I'll store in a point variable newPoint.
I store the last point I drew for this data type in lastPoint. If there is no lastPoint, this must be the first point on this graph line, and I just color the pixel at that position. If lastPoint does have a value, I draw a line from lastPoint to newPoint, with the color I've selected for this type of information.
repeat with i in chartList
member("emptyChart").duplicate(member chartCtr of castlib "charts")
workingImage = member chartCtr of castlib "charts"
workingImage.name = "chart"&i&currUser.pName
tempImage = workingImage.image.duplicate()
repeat with j in currUser.trackingData
if j.dt >= startDate AND j.dt <= endDate then -- this date is in the range
adjustedDate = (date(j.dt) - startDateObj) * pxPerDate
--does this data exist for this date?
if integerP(j[i]) or floatP(j[i]) then
adjustedValue = pChartHeight - ((j[i]) * pxPerValue)
newPoint = point(adjustedDate,adjustedValue)
if voidP(lastPoint) then
myResult = tempImage.setPixel(newPoint,currColor)
else
myResult = tempImage.draw(lastPoint,newPoint,[#shapeType:#line, #lineSize:1, #color: currColor])
end if
lastPoint = newPoint
end if
end if
end repeat
--assign this image to a sprite
--adjust the locH and locV of the sprite as needed
--set sprite ink to 36, so lines on other graph sprites will be visible
--update counters
-- void out lastPoint
end repeat
When it finishes, there will be a sprite for my tickmarks, plus a sprite for each graph line I am displaying. Following through this same basic process, you can graph any information you wish. (If you're graphing the value of your dot-com stock options, be sure you've compensated for negative values.)
Archive files of this Director 8.5-compatible file are avalable in ZIP and SIT formats.
All colorized Lingo code samples have been processed by Dave Mennenoh's brilliant HTMLingo Xtra, available from his site at http://www.crackconspiracy.com/~davem/
Copyright 1997-2024, Director Online. Article content copyright by respective authors.