Page was reverted to Anatoly's version, since he objected to the changes made by an anonymous editor. Anatoly should continue editing, then signal when he is ready for an organizer to review his article. After all parties are happy with the results, the page will be locked.

This page should now only be edited by Anatoly.


Graphics 101 – plotting a function

- by tsh73
- Jul 31, 2010 1:29 pm

Goal

(and history).
Once upon a time there was a person who wanted to plot some graphs. He supposedly knew his math well, but just never draw a thing in Basic. Of course he was directed to a help file; but he said "I have read through the help files on graphics and it just confuses me". Well, no wonder – help is a reference in a first place, so it's more or less OK to dump all graphic functions into one BIG page *shrugs*. There is tutorial in LB, but it surprisingly focuses on turtle graphics, and leaves plotting for the reader so to say.

So I thought I could as well write tutorial myself, with intent to cover "starting from open for graphics, via pixels, coordinates, points and lines. I guess that's all really needed, may be add printing text for labeling and flush."

So here I am. Forgive my Clatchian English.
I suppose you run LB (JB will do just fine) along as you read this tutorial.

Non-goals

I am not going to discuss GUI or animation or sprites – I just going to teach how to draw a static picture. Namely, to plot a function.

Pre-requisites

I suppose you already are "master of mainwin". You can calculate, check conditions, loop and print results. These are really basics, so if not, go to part one of LB / JB tutorial.

Where to draw

So, suppose prior to this you worked in mainwin. It is nice but it is not designed to draw stuff on – only to print text. So, we need something to draw upon.

There two types of things that allow drawing on itself – it's a window opened for graphics, and graphicbox added to ordinary window. You can have several graphicboxes on a window and draw in all of them. Pretty cool, but for now we take simplest course –
open "test" for graphics as #gr
#gr "trapclose [quit]"
wait
 
[quit]
    close #gr
    end
That's about as simple as it gets.
First line opens window titled "test", on which we can draw.
On my computer, it's about 300x300 pixels (more about pixels later)
(you can try to type for graphics_nsb_nf instead and get even simpler window)
Second install trapclose handler – names a label which will get called on closing window.
Third prevent our program from closing immediately :)
And two last lines are called then window is closed by Alt-F4 or by mouse – they explicitly close window and end program. (Without that, you can end up with no visible windows BUT Basic still running in memory).

Two more things.
If you do not need mainwin anymore, you type nomainwin on top of your program.
And if you need more space, you set WindowWidth and WindowHeight before opening window.

Pixels and coordinates

Ahhh. Pixels. That is it, exactly?

They are just dots on your monitor. Anything you see on a monitor composed from colored dots, named pixels. The pixel is smallest element of a picture.
Your monitor just now has current resolution, like 1280x1024. It's size of screen in pixels. ("just now" 'cause it could be changed). And it's maximal size of a window you can show (and draw upon).

Each pixel has coordinates. Basically that means they are numbered from 0, left to right and top to bottom. With whole numbers – there are no pixels between 0 and 1.
graph_tutorial_p1.gif
(On math they teach another coordinate plane, there axes go from left to right and from bottom to top. And where are plenty place between whole points.
graph_tutorial_p2.gif
We are going to respect that then plotting functions).

Make a dot

So. We know that screen consist of dots, and dots have coordinates.
So all we should actually do to draw a dot – make a command and give coordinates?
Almost:
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
#gr "set 100 100"
wait
 
[quit]
    close #gr
    end
As you see, we need to "put pen down" before drawing. (Unfortunate anachronism I might add).

We got a single black dot in the 100, 100 coordinates (100 pixels to the right and down from top-left corner of window. More exactly, from top-left corner of window client area – it's "working zone").

Make a bunch of dots

But single point is barely visible. Let's make a bunch, shall we?
But how we are going to do it? Why, with loops. FOR loops will do just fine.
Let X change from 100 to 200, and Y equal to X.
But how to put these numbers inside "set" command?
Let's see. Actually, line
  • #gr "set 100 100"
is a shortcut. Handy, but shortcut. Full version look so:
  • print #gr, "set 100 100"
Wait! It looks familiar?
It turns out that our graphic command (as ALL graphic commands) is an ordinary PRINT operator. We print to a window (#gr is a window handle) string, containing command.
That answers question how put numbers in command. Just like in PRINT:
  • #gr "set ";100; " ";100
Or
  • X=100: Y=100
  • #gr "set ";X; " ";Y
Just watch your spaces.

(There is handy consequence. If your drawing command does not work as expected, turn them in ordinary PRINT (by replacing print for #gr ) and examine it in mainwin.)

Clearing that, here's our program:
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
for X=100 to 200
    Y=X
    #gr, "set ";X;" ";Y
next
wait
 
[quit]
    close #gr
    end

Why, it's a line! – a line

Indeed. We just draw a line. I'm pretty sure there must be more straightforward way to draw a straight line?

Sure there are several. But most straightforward is
  • #gr "line 100 100 200 200"
The numbers are coordinates of first and second points, respectively.
(You can also place a first point in dedicated command and then issue command to draw from last point to another one. That comes handy then we need to draw point-by-point chain of lines:
  • #gr "place 100 100"
  • #gr "goto 200 200"
)
Same thing about putting variables in commands apply. (It applies EVERYWHERE).

Why, it's a circle! – circle

Damn, our first bunch of points behaved too simple. Let's try something fancy, with SINE and COSINE:
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
pi=acs(-1)
for t=0 to 2*pi step 0.01
    X=100*cos(t)+150
    Y=100*sin(t)+150
    #gr, "set ";X;" ";Y
next
wait
 
[quit]
    close #gr
    end
What? We got pretty round circle?

Let’s see why. It’s all math, you know.
We have that Pi number, equal 3.1415926… but much simpler to put pi=acs(-1) instead. If we change angle (t) from 0 to 2Pi (full circle, but measured in radians. Computers work in radians... that still converts to ordinary 0..360 degrees full circle, by multiplying by 180/Pi) then point (cos(t), sin(t)) will go along unit circle (circle with radius=1 and center (0,0) ) on coordinate plain.

All we added was scaling that to radius 100 (by multiplying) and shift it's center from (0,0) to (150, 150) (by adding).

And of course there is easier way to draw a circle. Just place center point, then command to draw circle with desired radius:
  • #gr "place 150 150"
  • #gr "circle 100"

But bunch of dots capable of more! (Archimedes spiral)

So far our examples looked too complex for task solved (things drawn). And there always was another, simpler way.

Do we need plotting point by point at all?
Why, sure we are. Look at this modification of last program. This beauty is called Archimedes' spiral (and actually this is example of polar plot, that is, plot of function in polar coordinates. Never mind.)
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
 
pi=acs(-1)
nLoops=10
for t=0 to 2*pi*nLoops step 0.01
    X=100*t/(2*pi*nLoops)*cos(t)+150
    Y=100*t/(2*pi*nLoops)*sin(t)+150
    #gr, "set ";X;" ";Y
next
wait
 
[quit]
    close #gr
    end

Turtle was here (and we need some stuff eventually)

Actually, besides “line, point” approach there are another one.
Turtle graphics.
The stuff like “pen down, go 30, pen up, go 20, turn 30, pen down, go 10”. LB supports it, too. It allows to easily draw stuff like this:
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
 
#gr "place 150 250"   'initial position found by trial and error
for i=1 to 30
    #gr "go 200"
    #gr "turn ";180+15  '180 degrees means turn backwards.
    'So, backwards and some
next
wait
 
[quit]
    close #gr
    end
I just think it's not well suited to plotting functions. (But it nicely covered in LB/JB tutorial, so look there in need). We'll need one thing, though.

Here's one trick I'll show you. Then we created window, I said it's approximately 300x300 pixels. But how do we measure it?

There is a command that places pen in the center of a drawing area. And another command that takes current coordinates into pair of variables. Then you double these variables, you'll get width and height of drawing area!
  • #gr "home"
  • #gr "posxy w h"
  • width=2*w: height=2*h
So "approximately 300x300 pixels" turned out to be 312x332.

Oh, and we was going to plot a function? A sine may be?

First, recall some math.
Let us say, we want to plot sine from –Pi to Pi, that is, full period. And we know that sine goes from -1 to 1, no more. So we have "logical" coordinates by X in range [-3.14, 3.14] and by Y in range [-1,1].

This should be mapped on "physical" coordinates – to actual pixels, starting from (0,0) and going approximately to (300,300). Some languages provide automatic translation; we have to do it ourselves. Do not worry, it's easy.

In our case, for X coordinate, we should move X range to 0: X+3.14, and then stretch that range (2*Pi roughly makes 6) to 300 pixels: (X+3.14)*50 (approximately).
Same apply to Y: Y+1, (Y+1)*150.
But Y axis on computer is inverted, so we should do this: 300-(Y+1)*150.
Let's try that.
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
pi=acs(-1)
 
for X=0-pi to pi step 0.01
    Y=sin(X)
    #gr "set ";(X+3.14)*50;" ";300-(Y+1)*150
next
wait
 
[quit]
    close #gr
    end

Wow. The sine wave all right, with proper orientation.
If you don't like to see gaps between dots, you can connect dots by using "goto" instead of "set":
  • #gr "goto ";(X+3.14)*50;" ";300-(Y+1)*150
Though, with that guesswork, we'll have hard time trying to place axes right…

No, I mean any function (scaling goes here)

Let's take "any" function.
Let us use f(x)= 1.5*x^2-2*sin(5*x), x in [-2,3].

So, to draw any function easily and be able to place axes, we need to eliminate the guesswork. Let’s build some formula, then use it to uniformly translate logical (math) coordinates to physical (screen) ones.

In general form:
To translate X from interval [0,1] to [c,d] we'll do X*(d-c)+c.
To translate X from interval [a,b] to [0,1] we'll do (X-a)/(b-a).
Combining, we'll get universal formula:
To translate X from interval [a,b] to [c,d] we'll do (X-a)/(b-a)*(d-c)+c.

We are going to use it to translate math coordinates to screen coordinates, so [a,b] is math range and [c,d] is screen one. But we can use it backwards, for example if we want to translate mouse clicks to math coordinates.

For ease of use, I suggest that we'll create two functions: sx(x) and sy(y), that'll convert math X to screen X and math Y to screen Y, respectively. But – our formula needs bunch of other numbers? No problem – all that numbers (a,b,c,d) are just range boundaries, and will not change. So we make them global just to save typing (lot's of it).
So, first global variables would be size of drawing area, let's name it winW, winH.
Other globals would be X range, name it xmin, xmax, and Y range, name it ymin, ymax.

Normally then plotting we know X range. But there to get Y range?
To get it, we just step through X range with same step we'll be plotting our graph and calculate ymin, ymax. Of course that would mean we calculate f(x) twice – first for getting Y range, second for actual plotting – but computers are pretty fast now.

One last thing before program. The coordinate axes are just two lines, that goes through point (0,0).
nomainwin
global winW, winH, xmin, xmax, ymin, ymax
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
 
#gr "home"
#gr "posxy w h"
winW=2*w: winH=2*h
'f(x)=1.5*x^2-2*sin(5*x), x in [-2,3]
xmin=-2: xmax=3
 
nPoints=winW    'we have only this much screen dots in X range
dx=(xmax-xmin)/nPoints   'so this will be step in math coordinates
 
'now, to get ymin, ymax we have to loop
ymin=f(xmin)
ymax=ymin
for x=xmin to xmax step dx
    y=f(x)
    if ymin > y then ymin = y
    if ymax < y then ymax = y
next
 
'now we just - plot function. Note same loop
y=f(xmin)
#gr "set ";sx(xmin);" ";sy(y)  'just set first dot
for x=xmin to xmax step dx
    y=f(x)
    #gr "goto ";sx(x);" ";sy(y)  'then connect dots
next
 
'and finally, add axis
#gr "line ";sx(xmin);" ";sy(0);" ";sx(xmax);" ";sy(0)
#gr "line ";sx(0);" ";sy(ymin);" ";sx(0);" ";sy(ymax)
wait
 
[quit]
    close #gr
    end
 
'"any" function. You can change it as you like
function f(x)
    f=1.5*x^2-2*sin(5*x)
end function
 
'To translate X from interval [a,b] to [c,d] we'll do (X-a)/(b-a)*(d-c)+c.
'create two functions: sx(x) and sy(y)
function sx(x)
    sx=(x-xmin)/(xmax-xmin)*winW
end function
function sy(y)
    sy=winH-(y-ymin)/(ymax-ymin)*winH   'Y is inverted, so winH-...
end function
Damn. This thing got a bit long, but I hope still understandable.

Adding text labeling

That's easy. You place the pen, then print "\" and the text.
Text placed top-left corner from the pen position.
If you print another time, output will be stacked under first one.
Just experiment a little.
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
#gr "place 100 100"
#gr "\Hello"
#gr "\from Liberty BASIC"
wait
 
[quit]
    close #gr
    end

For labeling a graph, you can use our translating functions sx(), sy().
So, for our graph, it could be like that:
'labeling
#gr "place ";sx(0)+5;" ";sy(0)-5
#gr "\0,0"
#gr "place ";sx(xmax)-20;" ";sy(0)-5
#gr "\X"
#gr "place ";sx(0)+5;" ";sy(ymax)+20
#gr "\Y"

And if you are going really fancy, you can change font, size and style. See in a help file.

Adding color and thickness

As easy as could get. You just command "color red" and "size 3",
  • #gr "color red"
  • #gr "size 3"
(for the list of colors, look in a help file. Or just experiment).
And all points and lines you'll draw after will be red and 3 pixels thick. But you can turn it back of course:
  • #gr "color black"
  • #gr "size 1"

Flush that thing down… err… I mean, stick it

For now you probably encountered that strange thing – then you run a program, it'll draw fine, but if you try to cover that window with another or do minimize/restore, it returns empty? Alas, that's not a bug – it's a feature. But the remedy is simple: just issue command "flush" after drawing. (You probably will want to do it in the end – each flush takes memory)
nomainwin
open "Try to cover me" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
#gr "place 50 100"
#gr "\I am flushed - I'll stay"
#gr "flush"
#gr "\I am NOT flushed - I'll disappear"
 
wait
 
[quit]
    close #gr
    end

Oh, and we could save that nice graph, do we?

Why, yes. There couple of commands just for that.
First one is getbmp, that takes picture from your graphics window. Later it could be drawn with drawbmp, or saved with bmpsave.
For our graph, it will be
  • #gr, "getbmp drawing 1 1 ";winW;" ";winH
  • bmpsave "drawing", "graph.bmp"
Now you have this picture saved as graph.bmp in the directory with your program.
Here what we'll got:
graph_tutorial_graph.gif

Where to get more

Why, if you have read this and want more, may be it's time to look back into a help file? I hope it will make more sense now ;)

Addendum

Whole program (just in case you missed something):
graph_tutorial.bas