This essay will provide you with some crude, and in some ways flawed, subroutines... but I hope the corners I've cut make them more accessible to Lazarus beginners.
This essay, by the way, was created after my problems with "fencepost errors" in respect of drawing with Lazarus and Delphi. (They were explained in PixelProblem.htm.) I may have RE-made those old mistakes, in the (relatively trivial) graphing elements of this... but THINK (and hope!) not.
Many of the subroutines is meant to be of general use. Each of the routines you should be able to "patch into" something have "DGTool", for "DrawGraphTool" near the start of their names.
If, by the way, you are not an "old hand" when it comes to using subroutines, you may want to read a tutorial on using subroutines which I wrote for Lazarus's inspiration, Delphi. (For our wants, there are hardly any differences.) Using subroutines- i.e. procedures and functions- can make programming a lot easier!
There are a few global variables... something we try to avoid. They too have DGTool in their names.
This essay carries on from the first "Draw Graph Tools", lt2n-drawtools.htm.. but should be useable on its own. If you want to be taken gently by the hand through the early parts of what's assumed knowledge here, try the first essay first.
Uses, needs what that needed... and adds...
// procedure DGToolClearDrawingArea // procedure DGToolScrollLeft // function iDGToolYScale // procedure DGToolDoDotWWr // procedure Delay // function extRemap //
DGToolClearDrawingArea; ...ummm... clears the drawing area. I.e. erases everything on it. Changes everything to whatever color you have stored in the new global variable clDGToolGraphBackGround (type TColor). You can use the Lazarus-provided color constants, e.g. clRed, or a raw number. See http://wiki.freepascal.org/Colors. That variable also now determines the "background color" when the graphing area is first displayed, by the way. (In lt2n_drawtools_a, it always had a white background.)
DGToolScrollLeft scrolls everything on the graph to the left on pixel's worth. The right hand column is filled with whatever color clDGToolGraphBackGround specifies at the moment. The column of pixels that was at the left edge of the graph "falls off", and is lost.
Puts a dot on the drawing area, in the color of your choice. If you give "bad" coordinates, it wraps the placement "around" (vertically).
By using DGToolYScale (see below) "on" the values passed to DGToolDoDotWWr, you can say "put the dot between 10 and 20 percent "up" the graph. And if the value being plotted is, say 280, from a sensor expected to return values between 200 and 300, put the dot 15% of the way from the 10% level to the 20% level.
Re-read that. DGToolDoDotWWr with DGToolYScale give you very simple plotting of multiple graphs on one drawing surface. See also the source-code, which contains demos. Full sourcecode supplied.
Generally useful. A simple little thing, but to save you going off to the internet land to fetch your own. Gives a non-blocking delay of at least... but probably not exactly... x milliseconds.
Generally useful. Needs 5 numbers... If you passed 280,200,300,10,20, it would return 18: Start with the 280. That is the number being re-mapped. the 200,300 tells you the range that it came FROM. (Note it is 80% "along" the number line from 200 to 300.) The last two parameters declare the range you want the number mapped TO... 10-20, in this case. 80% along THAT line is 18. Simpler to explain than it was to write the subroutine.
Just before we start with DGToolYScale, I have to tell you that this TUTORIAL unravelled a bit towards the end. but, you can fetch the full sourcecode of DrawTools_B, for free. THAT has been completed... carefully... and is full of demos and rems. If I were an informed reader of this tutorial, I would glance the stuff about DGToolYScale that comes next, and when you've had enough, go to the sourcecode!
DGToolYScale is a "helper" function, for use with DGToolDoDotWWr, which was available as part of lt2n_drawtools_a.
DGToolDoDotWWr puts a dot on the graph in the color of your choice, and takes three parameters.... iX, iY and iColor. iX and iY say WHERE the dot should go, and iColor determines the color it will be.
iX determines the left-and-right-ness of the dot. Use 0 to put it in the left-most column, and iDGToolImgWidth-1 to put it in the right hand column. (Note the "-1")
So far, so simple, so useable.
iY determines the up-down-ness of the dot. 0 puts it at the top of the graph, iDGToolImgHeight-1 (don't miss the "-1") puts it at the bottom of the graph. Not particularly user-friendly.
So I created DGToolYScale
Hang on to you hat.
It has 5 parameters... we'll come to them in a moment.
Suppose you have a sensor that, in the conditions which interest you, returns values from 28 to 232. And suppose that your graphing area is 150 pixels high. We'll assume that the sensor reading is in iSensor.
A VERY crude answer would be...
procedure TLT2N_DrawTools_bF1.DemoCrudeScaling; //demo of a crude plotting of a faked sensor // reading //The HEART of this is the call to DGToolDoDotWWr. Most of // the rest is there to make it happen 60 times, and to // provide faked sensor readings var bCount:byte;//(0-255) shiDrift:shortint;//(-128 to 127) boDirectionUp:boolean;//Which way should readings drift? iSensor:integer;//(positive AND negative numbers in the range allowed.) function iFakedSensor:integer; //uses several shared variables. Bad practice! begin iSensor:=iSensor+shiDrift; result:=iSensor; end;//iFakedSensor begin //From here... iSensor:=100;//Initial value from sensor. shiDrift:=-2; boDirectionUp:=true; //... to here: Just setting up the sensor reading faker. //Now read sensor, plot at right edge, scroll... 60 times. for bCount:=0 to 60 do begin DGToolDoDotWWr(iDGToolImgWidth-1,iFakedSensor,clGreen); // max sensible second term: iDGToolImgHeight-1 DGToolScrollLeft; Delay(20); end;//for... end;//DemoCrudeScaling
That calls DGToolDoDotWWr repeatedly, with the X coordinate set so that the new dots appear at the right hand side of the 150 pixel high graph. (You can't tell it is 150 pixels from the code fragment... but it is, in the context I want!) After each is plotted, the graph scrolls one pixel to the left. What's REALLY crude is that the sensor readings are 98, 96, 94, 92....
.. and they eventually become negative. Which would be BAD, if we were just using Lazarus's pixel plotting routine, but because we are using our DGToolDoDotWWr (even though it has Lazarus's routine at it's heart), at least our line of dots "wraps".
But! And for me it is a biggish "but"... the line goes UP when our numbers go DOWN.
We'll deal with that in a moment. First let's create a "good" "fake" sensor. We wanted one to give values from 28 to 232, remember.
Don't be overwhelmed by the following... it took a lot of code to create a sensible fake sensor... if you skim down to the bottom, where it says "begin //main block of DemoCrudeScaling", you'll see that as long as we have a source of sensor readings, doing the graph is still quite simple... mostly what we had before!
procedure TLT2N_DrawTools_bF1.DemoCrudeScaling; //demo of a crude plotting of a faked sensor // reading //The HEART of this is the call to DGToolDoDotWWr. Most of // the rest is there to make it happen 60 times, and to // provide faked sensor readings var bCount, bDrift:byte;//(0-255) boDirectionUp:boolean;//Which way should readings drift? iSensor:integer;//(positive AND negative numbers in the range allowed.) function iFakedSensor:integer; //Uses several shared variables. Bad practice! begin //From time to time, change direction of // drift if (random(10)=0) then boDirectionUp:= not boDirectionUp; //From time to time, change size of drift if (random(8)=0) then begin bDrift:=random(3); end;//change size of drift. //Apply drift to previous sensor reading if boDirectionUp then iSensor:=iSensor+bDrift//no ; here else iSensor:=iSensor-bDrift; //Clip, if value has gone beyond // the (arbitrary) bonds required of // this routine's design parameters if (iSensor>232) then begin iSensor:=232; boDirectionUp:=false; end; if (iSensor<28) then begin iSensor:=28; boDirectionUp:=true; end; result:=iSensor; end;//iFakedSensor begin //main block of DemoCrudeScaling //From here... iSensor:=100;//Initial value from sensor. bDrift:=2; boDirectionUp:=true; //... to here: Just setting up the sensor reading faker. //Now read sensor, plot at right edge, scroll... many times. for bCount:=0 to 160 do begin DGToolDoDotWWr(iDGToolImgWidth-1,iFakedSensor,clGreen); // max sensible second term: iDGToolImgHeight-1 DGToolScrollLeft; Delay(20); end;//for... end;//DemoCrudeScaling
And that "works"... fairly well. But remember that our graph is 150 pixels high, and that our (arbitrarily chosen, for discussion) sensor returns nothing less than 28, but sometimes readings as high as 232. If the sensor returns values above 150, the line will wrap. Untidy. And the part of the graph from y=0 to y=26 isn't used at all (except for wrap-arounds.)
And it is still all upside down.
A simple, crude "fix" to the sometimes the numbers are too big" problem would be to use the following for the "plot pixel". Note the "div 2". That divides the number in iFakedSensor, and sends the result to DGToolDoDotWWr. Now the number passed for plotting... previously 28 to 232 is 14 to 116. (inclusive).
DGToolDoDotWWr(iDGToolImgWidth-1,iFakedSensor div 2,clGreen);
That would "work". But is terribly "ad hoc". But we'll use it as a starting point for the goal we are working towards: A function to "fix" ANY range of numbers to fit where we want them on our graph. It will be called DGToolYScale... as in "this scales a Y value to make it "right" for the graphing area available."
It will also fix the problem of the numbers being "upside down".
Note that I said that DGToolYScale will let us put the dots from the readings where we want them. Sometimes they will be using the whole height of the graphing area. Sometimes we will have two graphs on the graphing area... one on the top half, the other on the bottom half.
First we'll take what we had a moment ago, and rearrange things somewhat, so that we have started our DGToolYScale. For the moment, it will just do the "div 2" that was our crude fix. (I'll explain the "ext" and the "round" in a moment.
Here's the main part of our "demo" code, and all of the code, so far....
for bCount:=0 to 160 do begin DGToolDoDotWWr(iDGToolImgWidth-1,iDGToolYScale(iFakedSensor),clGreen); // max sensible second term: iDGToolImgHeight-1 DGToolScrollLeft; Delay(5); end;//for... end;//DemoCrudeScaling function TLT2N_DrawTools_bF1.iDGToolYScale(extRaw:extended):integer; begin result:=round(extRaw/2); end;//iDGToolYScale(extRaw:extended):integer;
The "extended" data type allows fractions. In our discussion, our sensor only returns integers. But we're building iDGToolYScale to be able to handle many possibilities. (Some will say "but that will be slow.". Yes. It will. The whole APPROACH is flawed, if you want, say, to display audio waveforms in real time. But the principles you'll learn here will still be relevant when you re-work the code to make it do the same thing, but faster.)
When I pass an integer-type value into an extended-type variable, there is no problem. The fact that extRaw can hold 4.5 doesn't stop it holding "plain" 4. (Though it might call it "4.0"!)
So the function can take 4 or 4.5 as the value that is passed TO it.
When you are dealing with "real" type numbers, of which "extended" is one class, you don't use "div 2". (That's for when you are using integer-type data, and it truncates, by the way. 7 div 2 is 3.) With real-type variable, you use "/" to divide. And the answer can include a fractional part.
So far, so good? We're going through iDGToolYScale, and have now looked at "extRaw/2".
The "round(extRaw/2)" says "round off the result of extRaw divided by two". THAT will be an integer, and thus it can be the result for iDGToolYScale
So, good, we have a structure. Now lets make iDGToolYScale a whole lot fancier.
Besides the number to be scaled, returned in integer form, we will add 4 numbers to what we send to the function.
The first two will specify the lowest and highest values we expect the sensor to return. (If values outside that range arise, they will only lead to dots higher or lower than we wanted, with wrapping happening when necessary.)
The second two should be in the range 0-99, inclusive.
If they are 0 and 9, the dots resulting should appear in the bottom 10% of the graphing area.
To make the dots all be in the upper half, you would use 50 and 99.
And, oh by the way, the enhanced function also turns the graph "the right way up". That happens by the magic of it's last line.
Umm. Things got a bit messy. Sorry. "The Story" will not be fully present here. I got too engrossed in Making It Work, and failed to record, document all that went into getting from "Hello World" to "The Finished Product".
For fellow obsessives out there, I saved what I could. I will try to zip and upload that, later. You can dig in it to your heart's content.
But most of us are mostly interested in the finished product. So... without all the details of how I got there, we have, ta da!...
Thank you for trying to read the above, imperfect text. I hope your efforts will be rewarded when you download (free) the full sourcecode for this application. It is full of rems and demos to help you get to grips with using this code... which is, I sincerely believe, MUCH better than this documentation!
In a sense, it is the 2018 manifestation of something I've been writing new versions of since 1983. It should be "coming on" by now!
Your thoughts, as ever, very welcome.
Search across all my sites with the Google search...
Page tested for compliance with INDUSTRY (not MS-only) standards, using the free, publicly accessible validator at validator.w3.org. Mostly passes. There were two "unknown attributes" in Google+ button code. Sigh.
....... P a g e . . . E n d s .....