How to make a simple line graph
This lesson shows how to make a simple line graph from a table of data.
Create a new MainStack
First, we'll create a stack that will contain our line graph and its script.
1. From the File menu, select New Mainstack. This creates a new stack named "Untitled" and opens its window.
2. From the Object menu, select Stack Inspector. This opens the window for editing the stack's properties.
3. Change the stack's name to "Line Graph."
4. Also enter "Line Graph" in the Title block. This displays the stack's name in the window title bar as shown.
Create the graph's basic objects
Next, we create the three basic objects that make up our line graph, and a button to contain the script that draws the graph.
1. Select the Rectangle tool from the tool palette. Drag and size a rectangle to create the space where the graph will be drawn.
2. Drag the Scrolling List Field object from the tool palette onto the stack window. Drag and size it as you like. The list will contain the data for the graph, and may be hidden or left visible. You may wish to make the stack window larger at this point, to contain both the rectangle and list.
3. Select the Line tool from the palette. Drag a short line segment as shown. It can be any length, any orientation. We will be changing its size and shape using a script. (The lesson How to build an interactive plotter with a polygon shows how to use a polygon graphic instead of a line graphic.)
4. Drag a Rectangle Button object onto the stack window. You can name the button later; for now, we'll leave it as "Button".
Add the data
1. Open the inspector for the scrolling list field. There are two ways to do this:
(a) Click on the field to select it, and choose Object Inspector from the Object menu.
(b) Right-click (or Control-click) on the list to pop up a menu. Choose Property Inspector from the menu.
2. Select Contents from the top pull-down menu in the inspector. We'll use this method to enter the data, because the list field is not editable by default.
3. Enter at least three points of data, listing the x value first, a comma, and the y value. Be sure NOT to include a space after the comma! The points need not be in any order; we'll deal with that later. Include a negative y value if you like.
4. Name the list "Data".
Plan the script
Okay, we've put together the basic ingredients for our line graph: the area it will be drawn in, the line graphic, the data table and data, and a button to hold the script. What does the script have to do? Use the diagram above to orient yourself.
1. Using the x,y points in the "Data" list, make a corresponding set of x,y points in window coordinates. In these coordinates, the point 0,0 is at the top left of the window. Window x increases to the right, and window y increases downward. To fit into the rectangle plotting area, this set of window points must meet these conditions:
(a) The smallest window x (smallest data x) is the left coordinate of the rectangle, so the graph starts at the left side of the rectangle.
(b) The largest window x (largest data x) is the right coordinate of the rectangle, so the graph reaches across the whole rectangle.
(c) The window y values must fit between the top and bottom y-coordinates of the rectangle. We need to remember that the "Data" points have graph y increasing upward, while the corresponding window points have window y increasing downward. So the largest window y (smallest data y) is at the bottom of the rectangle, and the smallest window y (largest data y) is at the top of the rectangle.
(d) The list of window x,y coordinates must be in increasing order of window x.
2. Once we have the list of window x,y coordinates, we set the points of the line graphic to the list. That's it.
Do some math
We need to convert data x to window x, and data y to window y. Let's start with x and do some algebra.
In words, window x must be the left of the rectangle plus something that depends on data x. The something has to be zero for the smallest data x. So a first guess is
windowX = rectLeft + (dataX - smallestDataX)
Okay so far. But when dataX is largest, windowX must be rectRight. That means that there must be a scaling factor to convert the range of data x to the range of window x. Let's call this tScale and put it into our equation:
windowX = rectLeft + tScale * (dataX - smallestDataX)
When windowX is rectRight, dataX must be largestDataX. Putting these values in:
rectRight = rectLeft + tScale * (largestDataX - smallestDataX)
rectRight - rectLeft = tScale * (largestDataX - smallestDataX)
tScale = (rectRight - rectLeft)/(largestDataX - smallestDataX)
Our final conversion formula is then:
windowX = rectLeft + ((rectRight - rectLeft)/(largestDataX - smallestDataX)) * (dataX - smallestDataX)
What about y? Well, the situation is exactly the same except that window y decreases as data y increases. With a little thought, we get this conversion formula:
windowY = rectBottom - ((rectBottom-rectTop)/(largestDataY - smallestDataY)) * (dataY - smallestDataY)
Checking, we see that when DataY is largestDataY, windowY is rectTop. Ahh.
Open the script of button "Button" for editing, by either (a) right-clicking (Control-clicking) on it and choosing Edit Script from the popup menu, or (b) selecting the button and choosing Object Script from the Object menu. We'll begin by defining the constants in our equation, smallestDataX, largestDataX, smallestDataY, largestDataY, rectBottom, rectTop, rectLeft, rectRight. The script opens with an automatic mouseUp handler.
NOTE: The complete script of button "Button" is printed at the end of this lesson in a form that can be copied and pasted into your own stack.
1. Define the constants as local variables. It's not necessary but is good form, and alerts readers to important values.
2. Use the properties of graphic "Rectangle" to set its constants. We could leave this out, but then every time we wanted rectLeft we'd have to say "the left of graphic "Rectangle", which makes the code slower and harder to read.
3. To distinguish the x and y values in the data, we're going to use item 1 and item 2. Just in case, we should make sure that the item delimiter, itemDel, is comma. (For example, someone might later insert some lines above this in which they change the itemDel to tab and forget to change it back.)
4. Sort the data by y value to find the smallest and largest values. This is faster and less complex that a repeat loop looking at each line in turn. (Try writing one and see!) The smallestDataY appears as item 2 on the first line of the sorted field, the largestDataY appears as item 2 on the last line of the sorted field.
5. Use the same method to find the smallestDataX and the largestDataX. We also leave the list of points in the field in increasing DataX order.
6. This step shows a simple way to debug the scripting by displaying the calculated quantities in an answer dialog. When the script is finished, this step can be commented out.
Calculate the window points
Use the formulas from before to create a new list of window points. Then set the points of the graphic "Line" to that new list, and voila!
1. Use a repeat loop to step through each line of the data, converting to window coordinates.
2. Get the values dataX and dataY from the current line.
3. Calculate the corresponding windowX and windowY from the formulas. (It's natural but wrong in revTalk to say windowX = ....)
4. Add the new window point to a list stored in the variable windowList. Since values of window x and y can only be integers, use the round() function to eliminate fractional parts.
5. Set the points of graphic "Line" to the new list of windowPoints and RunRev draws the graph. Ta da!
The hard work is done but...
Here's the result for the data shown. Looks correct, but there are a lot of improvements to be made. That's up to you.
Here's what I would consider to be the minimum needed:
1. Put a label at the top and bottom of the y axis, and a label on the left and right of the x axis. In the script, put largestDataY, smallestDataY, smallestDataX, largestDataX into these labels. Create the labels beforehand and fill them from the script.
2. If dataY has negative values (as it does here), draw a horizontal line at the calculated value of windowY from rectLeft to rectRight. Put a label on the y axis showing the line is y=0. Create a line graphic for this beforehand but keep it hidden; show it and locate it from the script as needed.
3. If dataX has negative values, do the same for a vertical line at x=0.
4. Change the foreColor of graphic "Line" so it contrasts with the rectangle.
5. Change the name of the button to "Draw Graph".
Before you start making improvements, however, enjoy the tool you've made. Put in a lot more data points. Write a script to fill the data with points from a mathematical function. You'll find that this little project is quite capable!
Complete script to copy and paste
local smallestDataX, largestDataX, smallestDataY, largestDataY
local rectBotton, rectTop, rectLeft, rectRight
put the bottom of graphic "Rectangle" into rectBottom
put the top of graphic "Rectangle" into rectTop
put the left of graphic "Rectangle" into rectLeft
put the right of graphic "Rectangle" into rectRight
## sort the data to get the largest and smallest values
set the itemDel to comma -- usually unnecessary, but just in case...
## sort the data by y value first
sort lines of field "Data" ascending numeric by item 2 of each
put item 2 of line 1 of fld "Data" into smallestDataY
put item 2 of the last line of fld "Data" into largestDataY
## use the same method for x value
## this leaves the data sorted by x, as we want
sort lines of field "Data" ascending numeric by item 1 of each
put item 1 of line 1 of fld "Data" into smallestDataX
put item 1 of the last line of fld "Data" into largestDataX
-- put "smallestDataY = " & smallestDataY & cr & "largestDataY = " & largestDataY & cr into tMessage
-- put "smallestDataX = " & smallestDataX & cr & "largestDataX = " & largestDataX after tMessage
-- answer tMessage
repeat with x = 1 to the number of lines in field "Data"
get line x of field "Data"
## get the data values
put item 1 of it into dataX
put item 2 of it into dataY
## calculate the window values
put rectLeft + ((rectRight - rectLeft)/(largestDataX - smallestDataX)) * (dataX - smallestDataX) into windowX
put rectBottom - ((rectBottom-rectTop)/(largestDataY - smallestDataY)) * (dataY - smallestDataY) into windowY
## add the new window point to the list, rounding to an integer
put round(windowX) & comma & round(windowY) & cr after windowPoints
## draw the graph
set the points of graphic "Line" to windowPoints