Graphics [GM2] draw_plot() function for basic graphing of data

M

Morne

Guest
GM Version: GM v2.1.3.273 Runtime v2.1.3.189
Target Platform: ALL for general tutorial
Download: N/A, until feature complete or if there is a big need
Links: N/A
Difficulty: Medium, GML knowledge required to modify.

Summary:
Plot basic single array of values on a Y-X-plot to a surface object. Place it anywhere and with custom Height and Width for plot surface. Plot (graph) will autoscale the Y & X axis based on axis scale to stretch it to surface size. Very basic implementation to get you started. Will demo a sigmoid() function {also useful when building neural nets}. Example function call in GM DRAW event (if inputs defined):
Code:
draw_plot(surfX,surfY,surfW,surfH, Xvals,Yvals, minX,maxX,minY,maxY,axisTic);
Tutorial:
Here is example result (on black background):


First you will need an array of X values and calculate some equation Y values, I've provided you with the sigmoid() function, see script below:
Code:
/// @function sigmoid(n)
/// @description Returns 'x' conversion to 0-1 value clipping, formula '1/(1-exp(-n))' for an 'n' range of (-5.30 to +5.30) useful for Neural Nets input scaling or normalisation
/// @arg {real} n where n is any typical equation X value
var n = argument0;
return 1/(1+exp(-n))
Extra:
If you rather want to plot the derivative of sigmoid, you can have this one below:
Code:
/// @function sigmoid_derivative(n)
/// @description Returns the Derivative or phi of equation sigmoid(n)
/// @arg {real} n where n is any typical equation X value
    var n = argument0;
    return sigmoid(n)*(1-sigmoid(n))
I will use sigmoid(n) for to replicate the plot graph above. But you can pass any data in an X & Y array to the draw_plot(...) function, as long as they are of equal length. The purpose of this was to plot equation results, which will always yield the same number of Y-values for X-values as defined in the plot space x-axis and y-axis. Please be careful not to get confused with the game maker x,y pixel coordinates. This function will translate automatically the Y, X values of the data to the x,y pixel coordinates on your surface.

The method I used was to use a custom object 'objPlot' so I can create my graph plot anywhere. This function draw_plot() function will thus be called in the 'objPlot' draw event, with some other code in its create event to initialise the plot surface variables. This also will enable you to easily add plot draw controls ingame or should I say in-app. I will not cover the ingame controls in this tutorial and leave it up to you for an excercise.

1. So if you have not created the sigmoid() function yet, please create a script and copy the sigmoid() script above into it and save it.

2. Now create a new object called 'objPlot' or whatever you want. This will when created or placed in a room draw the plot surface with this object x,y as its top left corner. (see variables surfX, surfY in objPlot create event below)

3. Add a CREATE EVENT and add the code below: {code blocks are commented and will not be explained in detail}
Code:
/// @description Insert description here
// You can write your code in this editor
surfID = -1;
canUpdateSurface = true;
canClearSurface = false;
//Init (for function in draw event below)
//draw_plot(x,y,480,320,Xvals,Yvals,-5,6,-5,10,1); 
//draw_plot(surfX,surfY,surfW,surfH, Xvals,Yvals, minX,maxX,minY,maxY,axisTic); 
surfX = x;
surfY = y;
surfW = 480;
surfH = 320;
minX = -5;
minY = -2;
maxX = 5;
maxY = 2;
axisTic = 1;
//this enable to add keyboard/button dynamic control to plot
//axisTicIncrement = 0.1;
/* TEST DATA Array[]*/
Xvals = [-6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6];
//Evaluate Yvalues and create array for sigmoid(Xvals)
lenA = array_length_1d(Xvals);
for (var i=lenA-1; i>-1; i--;) {
    Yvals[i] = sigmoid(Xvals[i]);    //custom equation
}
4. Add an Alarm[0] EVENT, add code below:
Code:
/// @description canUpdateSurface = true
canUpdateSurface = true;
5. Add a DRAW EVENT, add code below:
Code:
/// @description Insert description here
// You can write your code in this editor
//draw_self();
//Debug: Draw values in list left of Plot (not on surface)
draw_set_color(c_white); 
for (var i=0; i<array_length_1d(Yvals); i++;) {
    draw_text(x-128,y+i*16,"(Xval,Yval)=("+string(Xvals[i])+","+string(Yvals[i])+")");   
}
//Draw actual plot
//draw_plot(x,y,480,320,Xvals,Yvals,-5,6,-5,10,1); 
draw_plot(surfX,surfY,surfW,surfH, Xvals,Yvals, minX,maxX,minY,maxY,axisTic);
6. Place 'objPlot' in room (preferably just left of center and top area of room), SAVE game.

7. Run game (press F5)!

8. Extra:
If you want to test plot axis scaling ingame then add a Key Press LEFT EVENT with
Code:
/// @description decrease minX
minX -= axisTic; //axisTicIncrement;
canClearSurface = true;
and also add EVENT: Key Press - RIGHT with
Code:
/// @description increase maxX
maxX += axisTic; //axisTicIncrement;
canClearSurface = true;
Then SAVE, and RUN (F5) again.

Feel free to comment, ask questions or suggest improvements. I just wanted to share this feature since it is not part of out of the box GMS functionality and shows how to use surfaces. It is very basic and currently can only draw one graph, but you can easily draw additional ones while the surface is in target in the draw_plot() function.
 
M

Morne

Guest
Oh dear, my apologies looks like I forgot to add the actual main script for the function draw_plot(),
here it is below:
Code:
/// @function draw_plot(surfX,surfY,surfW,surfH, Xvals,Yvals, minX,maxX,minY,maxY,axisTic); 
/// @description Draw a basic plot of Y values over X values, with min and max value limits on X-axis. 
/// @arg surfX
/// @arg surfY
/// @arg surfW
/// @arg surfH
/// @arg Xvals
/// @arg Yvals
/// @arg minX
/// @arg maxX
/// @arg minY
/// @arg maxY
/// @arg axisTic Draws tics and values along both axis at same increments
//this script goes in draw event
//requires surfPlot declaration in CreateEvent: surfID = -1;
//Declare arguments:
//surfID    = argument0;
surfX    = argument0;
surfY    = argument1;
surfW    = argument2;
surfH    = argument3;
Xvals    = argument4;
Yvals   = argument5;
minX    = argument6;
maxX    = argument7;
minY    = argument8;
maxY    = argument9;
axisTic = argument10;
surfCol = c_white; //no blending
surfAlpha = 1;
draw_set_font(fontCalibri10);
//local vars
var borderW = min(16,0.1*surfW); //Border on surfaces edges where no drawing happens
var borderH = min(16,0.1*surfH); //already scaled and represents pixels on total surface
//Create Axis-Arrays
var axisLenX = (maxX-minX); //[values] not pixels, must still be streched to fit surface when drawn
var axisLenY = (maxY-minY);
//Compute the scale of the Positive side versus the Negative side of each axis
var axisScaleX = abs(maxX)/abs(minX);
var axisScaleY = abs(maxY)/abs(minY);
//Compute length of each side of each axis
/* OR just use scale min and max values haha
var axisLenXPos = axisLenX*axisScaleX;
var axisLenXNeg = axisLenX - axisLenXPos;
var axisLenYPos = axisLenY*axisScaleY;
var axisLenYNeg = axisLenY - axisLenYPos;
*/
//Calculate Stretching scaling vars to fit surface size, and box within the no-draw borders (based on AXIS defined ranges, not values in arrays)
var scaleW = (surfW-2*borderW)/axisLenX;
var scaleH = (surfH-2*borderH)/axisLenY;
//Create Axis-Arrays with values scaled already to pixels coords for surface scale 
var axisLenXtic = round(axisLenX/axisTic)+1; //number of tics on x-axis for full width range to draw (sections + 1)
var axisLenYtic = round(axisLenY/axisTic)+1; //...same for tics on y-axis
for (var axi=axisLenXtic-1; axi>-1; axi--;) {
    //Since we draw from left to right, or from negative to positive values on x-axis array[neg to pos] values
    XaxisTics[axi] = (maxX - axisTic*axi)*scaleW; //x-coords of x-axis tic marks translated to surface pixels
    XaxisTicVals[axi] = (maxX - axisTic*axi);
}
for (var axj=axisLenYtic-1; axj>-1; axj--;) {
    //Since we draw from top to bottom, or from positive values on y-axis array[pos to neg] values
    //YaxisTics[axj] = (maxY - axisTic*axj)*scaleH; 
    YaxisTics[axj] = (minY + axisTic*axj)*scaleH; //y-coords of y-axis tic marks translated to surface pixels
    YaxisTicVals[axj] = (minY + axisTic*axj);
}
//Translate to Plot Origin (remember surfaces draw from top left)
XA_0 = borderW + abs(minX)*scaleW;
YA_0 = borderH + abs(maxY)*scaleH;
if (surfID == -1) {
   
    //Create plot surface if it doesn't exist
    surfID = surface_create(surfW, surfH);
    surface_set_target(surfID);   
    draw_clear(c_white); //clear surface to black 
    //draw_clear_alpha(c_black, 0); //OR: clear to black and make transparent
    surface_reset_target();
    canClearSurface = false;
    canUpdateSurface = true;
    show_debug_message("Note: Plot surface created")
   
} else { 
   
    draw_set_color(c_white);   
    if canUpdateSurface { /*Optimisation here to reduce draw steps*/
    surface_set_target(surfID);       
             
        //Test draw origin:
        draw_set_color(c_red);
        draw_circle(XA_0,YA_0,5,true);
        draw_circle(XA_0,YA_0,1,false); //dot, inside circle center
        draw_set_color(c_black);
       
      //Draw X-axis
        //Draw left to right
        XA_x1 = borderW;
        XA_y1 = YA_0; 
        XA_x2 = XA_x1 + axisLenX*scaleW;
        XA_y2 = YA_0;       
        draw_line(XA_x1,XA_y1,XA_x2,XA_y2); //draw X-axis
        //draw tic marks on x-axis & value labels
        draw_set_halign(fa_middle);
        draw_set_valign(fa_center);
        var ticPx, ticPy;
        for (var axi=axisLenXtic-1; axi>-1; axi--;) {
            ticPx = XaxisTics[axi];
            ticPy = YA_0;       
            var ticLineH = 4;
            draw_line(XA_0+ticPx,ticPy-ticLineH/2,XA_0+ticPx,ticPy+ticLineH/2);
            var xLabelOffset = 12; //[pixels]
            draw_text(XA_0+ticPx,ticPy+xLabelOffset,string(XaxisTicVals[axi]));
        }
       
      //Draw Y-axis
        draw_set_color(c_black);
        //draw from top to down on screen plot surface
        YA_x1 = XA_0;
        YA_y1 = borderH; 
        YA_x2 = XA_0;
        YA_y2 = YA_y1 + axisLenY*scaleH; 
        draw_line(YA_x1,YA_y1,YA_x2,YA_y2); //draw Y-axis
        //draw tic marks on y-axis & value labels
        draw_set_halign(fa_middle);
        draw_set_valign(fa_center);
        var ticPx, ticPy;
        for (var axj=axisLenYtic-1; axj>-1; axj--;) {
        //for (var axj=0; axj<array_length_1d(YaxisTicVals); axj++;) {
            ticPx = XA_0;
            ticPy = -borderH + axisLenY - YaxisTics[axj];           
            var ticLineW = 4;
            draw_line(ticPx-ticLineW/2,YA_0+ticPy,ticPx+ticLineW/2,YA_0+ticPy);
            var yLabelOffset = 6; //[pixels]
            draw_text(ticPx-ticLineW/2-yLabelOffset,YA_0+ticPy,string(YaxisTicVals[axj]));
        }
       
        //Draw Y-values of Equation (Actual Graph, data on plot)
        draw_set_color(c_blue);
        var valPx, valPy;
        for (var i=0; i<array_length_1d(Yvals); i++;) {
            if Yvals[i] >= minY && Yvals[i] <= maxY { //only draw values in range of y-axis
              if Xvals[i] >= minX && Xvals[i] <= maxX { //only draw values in range of x-axis
                valPx = XA_0 + Xvals[i]*scaleW;
                valPy = YA_0 - Yvals[i]*scaleH; 
                draw_circle(valPx, valPy, 5, true); //circle
                draw_circle(valPx, valPy, 1, false); //dot, inside circle center
              }
            }
           
        }
               
    surface_reset_target();
    //Update surface alarm, draw steps allowed (optimisation)
    canUpdateSurface = false;
    alarm[0] = 10; //*(room_speed/30);
    } //don't update surface draw.
}
if surface_exists(surfID) {
     
      if canClearSurface {
        surface_set_target(surfID);   
        draw_clear(c_white); //clear surface to black 
        //draw_clear_alpha(c_black, 0); //OR: clear to black and make transparent
        surface_reset_target();
        canClearSurface = false;
        show_debug_message("Note: Plot surface cleared")
      }
   
      //Draw final plot or trend or graph, on clean surface
      draw_surface_stretched_ext(surfID,surfX,surfY,surfW,surfH,surfCol,surfAlpha);
} else {
    surfID = -1; //If you minimize window or surface is lost, re-create it all.   
}
 
Top