Piecharts and Director
November 3, 1998
by Michael Geary
One of our project managers recently asked me if it was possible to create dynamic pie charts with Director. "Of course." I immediately replied. He left, gratified that he had added another bomb to the arsenal he can drop on me at will, and I, ears still ringing from shellshock from the _last_ bomb that hit me, decided I'd better put my Lingo where my mouth is.
First a quick look at Director: We cannot yet create simple Quickdraw polygons with or without Lingo, much less pie wedges. This left two possibilites: sweeping a line with trails on, or dynamically assembling several premade 1-bit wedge castmembers, whose forecolor would be determined at Pie generation.
The second option is one that some have decided upon. The big advantage is that your graphics can be as crisp and anti-aliased as you want to make 'em. Another advantage is that your pie-charts will be printable, whereas anything created with trails is screenbound. For an example of this solution, check out Michael Maffie's Chance Encounters.
I opted for the line-sweeping solution. Its advantages include skinniness (my shockwave version is 2.7k), and dynamic re-sizeability. Technically another advantage is accuracy, because with the wedge-solution above, your accuracy is limited by the number of wedges you make.
So, here is the process:
First let's start with a fake master handler--one that gives us an idea of what isolated functions we'll need to draw upon to create a pie chart. We'll call it MakePie:
(fake lingo)
on MakePie identify what slices we need to make identify a circle sprite on stage find the circle's center point repeat with each segment sweep a line for each segment end repeat end
To keep things reusable, let's let the first two points of information change every time, that is, we will pass them as parameters to the function. That way we could theoretically have 120 different circles on stage and have each of them create different pie charts.
To begin we need a quickdraw circle, any size, on stage. We also need a line sprite, which will be dynamically controlled by these handlers.
Jumping into to Real Lingo now, we will pass this master handler two parameters:
SpriteNumber: The channel of the quickdraw circle you've placed onstage
ListOfPercentages: a list of percentages which will be represented as slices of the pie
on MakePie SpriteNumber, ListOfPercentages -- accepts data in the form: -- MakePie (7,[10,20,30,20,20]) -- identify the wand (the line which -- draws the segments set WandSprite = 2 -- identify the center point of the circle sprite set PieLeft = the left of sprite SpriteNumber set PieRight = the right of sprite SpriteNumber set PieTop = the top of sprite SpriteNumber set PieBottom = the bottom of sprite SpriteNumber set PieDiameter = PieRight - PieLeft set PieRadius =(.5 * PieDiameter) --this is the H & V of the actual center -- point of the circle set PieCenterH = (PieLeft + PieRadius) set PieCenterV = (PieTop + PieRadius) --establish a starting point for the wand set CurrentDegree = 0 --establish a variable which we will use to -- generate a random color for each segment set SegmentCount = 1 --now we loop through the list of percentages --and draw a slice for each item repeat with thisSlice in ListOfPercentages --convert percent to degrees set theDegrees = thisSlice/100.0*360 + CurrentDegree --move the amount of degrees clockwise --this handler is described below DrawSegment (WandSprite ,CurrentDegree, theDegrees ¬ ,PieRadius,point(PieCenterH,PieCenterV)) --prepare for next segment set CurrentDegree = theDegrees --change color. Note that this does not guarantee -- a unique color for each segment --but it does make it pretty darn likely. --A better solution might be to pass the color -- along with the number of slices. --this will prevent the wand from changing color -- when it's done if SegmentCount <> count(listOfSegments) then set the forecolor of sprite WandSprite = ¬ (SegmentCount * 10) + random(100) end if set SegmentCount = SegmentCount + 1 end repeat end
Okay. So our master handler is done, but it relies upon an external handler: DrawSegment. This DrawSegment handler accepts a number of parameters. I find it is often useful to isolate specific procedures and modularize them. You never know when you might need to draw a pie segment without necessarily having to draw an entire pie... :)
So let's take a look at DrawSegment. It accepts the parameters:
WandSprite (which sprite is acting as the sweeping line?)
StartDegree (where on the circle are we presently pointing?)
EndDegree (to what degree should we sweep?)
Radius (how big is the circle?)
StartPoint (what are the coordinates of the center of the circle?)
--here is where we actually draw a slice of pie. --We give it degree starting and ending points --as well as the circle radius and the center point -- of the circle on DrawSegment WandSprite, StartDegree, EndDegree,¬ Radius,StartPoint --turn on the trails of the wand set the trails of sprite WandSprite = true --now simply rotate the wand, which is composed -- of two quickdraw lines --from the starting degree to the ending degree repeat with thisDegree = StartDegree to EndDegree --Aha! another sub-handler! --this one (described below) simply points the -- line at a particular --degree point on the circle pointToDegree(WandSprite,thisDegree,Radius ¬ ,StartPoint) updatestage end repeat end
By now you may be wondering why we have all these sub-handlers. Yes, it certainly would have been possible to keep the code portable and put it all in one handler. However, having compartmentalized handlers is really a great thing. It keeps your code clean and readable, it allows you to quickly pinpoint what sections of your code are having problems, and it allows you to take small 'utility' handlers very easily from project to project.
Having said that, here is the PointToDegree handler. It accepts the following parameters:
WhichLineSprite: again, this is the wand sprite
WhichDegree: to which degree should the line point?
theRadius: how long should the line be?
CenterP: what is the center point of the circle?
By converting the radius and the degrees, both of which we already know, to a handler which converts from polar coordinates to cartesian (X,Y) coordinates, we can get an X and Y coordinate location for the point on the circle to which our line should point.
This handler actually simply produces the locH and locV of the outer point of the line. We will rely upon another handler to actually slap the line onstage.
on PointToDegree whichLineSprite,whichDegree¬ ,theRadius, CenterP --feel free to change this for a thicker -- /thinner line set LineThickness = 2 --we will consider the center of the circle -- point zero set ZeroX = the locH of CenterP set ZeroY = the locV of CenterP --get the degree in radians. This is necessary -- for the cartesian to --polar conversion which happens next --Here again we have a small 'utility' hander -- called degToRad (see below) set theDegree = degToRad(whichDegree) --We know where the starting point of the -- line is (CenterP) now we will find out -- the ending point --this is the polar to cartesian conversion set X2 = ZeroX + (theRadius * Cos(theDegree)) set Y2 = ZeroY + (theRadius * Sin(theDegree)) set X1 = ZeroX set Y1 = ZeroY --now we have our four points which define -- the starting and ending point of the line -- we wish to draw --place the line --Yup, another sub-handler drawLine (whichLineSprite,point(X1,Y1),point(X2, ¬ Y2), LineThickness) end
We're just about finished now. As you can see, this has really simply been a process of identifying encapsulated functions, and creating lingo tools to do these.
Our final 'real' handler comes to us by way of Jim Collins (thanks for all your contributions to Direct-L, Jim!). Because a Quickdraw line in Director is not a line at all, but a rectangle whose registration point changes depending on where the line is pointing, we need two lines to do the trick. One line point up to the right '/' and the other line points up the the left '\'. This DrawLine handler will determine, based on the two points we give it, which member to use, and will place that member in the sprite channel onstage.
The parameters it accepts are:
LineSprite: Which sprite channel is the wand using?
PointA: the origin (center of the circle) of the line
PointB: the point on the circle where the line terminates
LineWidth: How fat do you want your line?
on drawLine lineSprite, pointA, pointB, lineWidth --break points down into X's and Y'x set x1 = the locH of pointA set y1 = the locV of pointA set x2 = the locH of pointB set y2 = the locV of pointB --determine which line to use, this is the key if (x1 - x2) * (y1 - y2) > 0 then set the member of sprite lineSprite to ¬ member "lineLeft" else set the member of sprite lineSprite to ¬ member "lineRight" end if --adjust the thickness of the line. Fortunately -- Director does this for us set the lineSize of sprite lineSprite to lineWidth --now we find the actual points of the rectangle --of our quickdraw line set leftAdjust = lineWIdth / 2 set rightAdjust = lineWIdth - leftAdjust set left = min (x1, x2) - leftAdjust set right = max (x1, x2) + rightAdjust set top = min (y1, y2) - leftAdjust set bottom = max (y1, y2) + rightAdjust --implement the calculations set the rect of sprite lineSprite to rect (left, ¬ top, right, bottom) --Boom. Finally we get to see something on stage updateStage end drawLine
So, we're pretty much done. We cannot, however, leave out three handly utility functions which assist with polar coordinates. Their names are pretty much all the commentary they need. We only actually use the degToRad function, but the others will definitely be handy for the future:
on degToRad degAngle return degAngle*pi()/180.0 end on radToDeg radAngle return radAngle*180.0/pi() end on PolarToCartesian r,theta --get theta in radians set NewAngle = degToRad(theta) set theX = r * cos(newangle) set theY = r * sin(newangle) return [theX,theY] end
To sum it all up, we now have one master handler which is very easy to use, and which automatically draws upon sub-handlers as needed. We keep our hands clean, and our code is easy to expand and modify should we ever need to.
Here are the handlers we've made and used:
MakePie
DrawSegment
PointToDegree
drawLine
degToRad
and we've got two bonus handlers:
radToDeg
PolarToCartesian
This is obviously just a beginning. This pie chart generator could certainly be improved upon. This scheme is somewhat size-dependent in that when our line gets too thick (if we were doing a circle larger than about 225 pixels in diameter), our center point gets a bit distorted. There is also no guarantee that the pie slices will be a different color--only a very strong likelihood.
I'd be interested in hearing about your improvements or other experiences with this code. I can be reached at mgeary / at / antwerpes.de or mgeary / at / active-design.com.
A sample movie is available for download in Mac or PC format.
Copyright 1997-2024, Director Online. Article content copyright by respective authors.