Repository for import to local machine

Dependencies:   DMBasicGUI DMSupport

GuiLibGraph.cpp

Committer:
jmitc91516
Date:
2017-07-31
Revision:
8:26e49e6955bd
Parent:
1:a5258871b33d

File content as of revision 8:26e49e6955bd:

#include "GuiLibGraph.h"

#include <cstring>

#include <float.h>


#define USE_LED_FOR_DEBUGGING

#ifdef USE_LED_FOR_DEBUGGING

#include "gpio_api.h"
#include "wait_api.h"
#include "toolchain.h"
#include "mbed_interface.h"

/*
    Draw a part of a profile - which will be a quadrilateral, vertical at left and right, horizontal at the bottom, but a sloping straight line at the top -
    by calling the EasyGUI GuiLib_VLine function multiple times.
    
    Args: colour of the profile to the left of the boundary
          colour of the profile to the right of the boundary
          X coords of the left and right edges of the section
          X coord of the colour boundary
          Y coord of the bottom
          Y coords of the top left and top right
          
    Returns true if OK, false if it failed (e.g. the coords were invalid).
*/
extern bool DrawProfileSectionUsingGuiLibVLine(GuiConst_INTCOLOR colour1, GuiConst_INTCOLOR colour2, GuiConst_INT16S xLeft, GuiConst_INT16S xRight, GuiConst_INT16S xColourBoundary, 
                                        GuiConst_INT16S yBottom, GuiConst_INT16S yTopLeft, GuiConst_INT16S yTopRight);


// Turn on LED 4 when we get an error (mismatched time/X coord) in the graph data (see below)
static void SetLed4(bool turnLedOn)
{
    gpio_t led_4; gpio_init_out(&led_4, LED4);

    if(turnLedOn) {
        gpio_write(&led_4, 1); // one appears to mean "turn LED 4 on"
    } else {
        gpio_write(&led_4, 0); // zero appears to turn it off
    }
}

#endif // USE_LED_FOR_DEBUGGING


/*
    The GuiLibGraphDataSet class - encapsulates a GuiLib_GraphDataPoint array,
    making access and use of it more straightforward than if the caller 
    had to do this directly.
    
    We assume that X coordinates are time values, and Y coordinates are temperature, pressure, etc, values.
    ******************************************************************************************************
    
    We leave it to the caller to decide what the Y coordinate units are, but we let the caller specify
    the time (i.e. X axis) units. If the caller changes the units, we recalculate the X coordinates to match.
    The options are minutes and seconds - we default to minutes.
    
    Note that the coord values are integers (as in the easyGUI Graph item).
*/

/*
    Static member function, to provide a standard way of converting the floating point coordinates we store internally (for accuracy),
    to the integer values required by easyGUI graphs. We always store the time value (X coordinate) as minutes, in units of 0.1 minute,
    as does the GC itself. We convert to integer minutes or seconds as required by the caller. 
    
    We do not concern ourselves here with the units of the Y coordinate - we assume that the caller knows what they are.
    
    Args: pointer to the GuiLib_GraphDataPoint object to receive the integer coordinates
          the floating point data to be converted
          the time unit (minutes or seconds) for the generated X coordinate
          
    No return value (we assume the conversion will always succeed).
*/
void GuiLibGraphDataSet::ConvertFloatingDataPointToGuiLibGraphDataPoint(GuiLib_GraphDataPoint* graphDataPoint, FloatingDataPoint floatingDataPoint, TimeUnit timeUnit)
{
    // The 'floor' mathematical function expects a double as its argument
    
    double doubleX = (double) floatingDataPoint.X;
    if(timeUnit == SECONDS) {
        doubleX *= 60.0;
    }
    
    double doubleY = (double) floatingDataPoint.Y;
    
    // Round, don't truncate
    graphDataPoint->X = (GuiConst_INT32S) floor(doubleX + 0.5);
    graphDataPoint->Y = (GuiConst_INT32S) floor(doubleY + 0.5);
}   

GuiLibGraphDataSet::GuiLibGraphDataSet()
{
    // No data yet...

    theGraphDataSet = NULL;
    
    graphDataSetActualLength = 0;
    graphDataSetSizeInIncrements = 0;
    
    nonRoundedTotalMethodTime = 0.0f;
}

GuiLibGraphDataSet::~GuiLibGraphDataSet()
{
    if(theGraphDataSet != NULL) {
        delete [] theGraphDataSet;
    }
}


/*
    Clears (i.e. deletes, removes) all the existing data (if any) for this dataset
*/
void GuiLibGraphDataSet::ClearData(void)
{
    if(theGraphDataSet != NULL) {
        delete [] theGraphDataSet;
        theGraphDataSet = NULL;    

        graphDataSetActualLength = 0;
        graphDataSetSizeInIncrements = 0;
    }
}
    

/*
    We extend the dataset array in steps of 'SIZE_INCREMENT'. This function does that,
    and copies the existing data to the extended array.
    
    It returns true if it succeeded in extending the dataset array, false otherwise.
*/
bool GuiLibGraphDataSet::ExtendDataSetArray(void)
{
    bool retVal = false;
    
    if(theGraphDataSet != NULL) {
        FloatingDataPoint* newGraphDataSet = new FloatingDataPoint[(graphDataSetSizeInIncrements + 1) * SIZE_INCREMENT];
        
        if(newGraphDataSet != NULL) {
            std::memcpy(newGraphDataSet, theGraphDataSet, (graphDataSetSizeInIncrements * SIZE_INCREMENT * sizeof(FloatingDataPoint)));
            
            delete [] theGraphDataSet;
            
            theGraphDataSet = newGraphDataSet;
            
            ++graphDataSetSizeInIncrements;
            
            // Leave 'graphDataSetActualLength' with whatever value it currently has.
            // We have only extended the array, not added any data to it.
            
            retVal = true; // Success
        }

    } else {
        // This is the first 'extension'
        theGraphDataSet = new FloatingDataPoint[SIZE_INCREMENT];
        
        if(theGraphDataSet != NULL) {
            graphDataSetSizeInIncrements = 1;            

            // We assume 'graphDataSetActualLength' is set to zero - leave it with this value.
            // We have only allocated memory for the array, not put any data in it.
            
            retVal = true; // Success
        }
    }
    
    return retVal;
}


/*
    Adds a datapoint with the specified X and Y values to the dataset array,
    making sure to keep the array elements in ascending order of X coordinate.
    Other member functions of this class rely on this.
    *************************************************
    
    Returns true for success, false for failure.
    
    Args: the X and Y values for the new datapoint
    
    Note that this function takes no account of the units specified for the X (i.e. time) axis - 
    we assume that the caller has already ensured that the X value is in those units.
*/
bool GuiLibGraphDataSet::AddDataPoint(float X, float Y)
{
    // Check that we do not already have a point at the same X coordinate - 
    // if so, return false
    for (int i = 0; i < graphDataSetActualLength; ++i) {
        if(theGraphDataSet[i].X == X) {
            return false;
        }

        if(theGraphDataSet[i].X > X) {
            // We have now reached a point with a greater X coord,
            // without finding a point with the same X coord - safe to continue
            break;
        }
    }
    
    if((graphDataSetActualLength + 1) > (graphDataSetSizeInIncrements * SIZE_INCREMENT)) {

        // We need to extend the array before adding the new datapoint

        if(!ExtendDataSetArray()) {
            return false;
        }
    }
            
    // We now have room for the new datapoint
    // - make sure we keep the datapoints in ascending order of X coord
    int positionToAdd = graphDataSetActualLength;

    if(graphDataSetActualLength > 0) {
        
        while(theGraphDataSet[positionToAdd - 1].X > X) {
            --positionToAdd;
            
            if(positionToAdd == 0) break;
        }
    
    
        // Move 'up' all the array elements 'above' positionToAdd
        
        for (int j = graphDataSetActualLength; j > positionToAdd; --j) {
            theGraphDataSet[j] = theGraphDataSet[j - 1];
        }
    }
        
    // We are now ready to add the new datapoint

    theGraphDataSet[positionToAdd].X = X;
    theGraphDataSet[positionToAdd].Y = Y;
    
    ++graphDataSetActualLength;
            
    return true;
}

/*
    Returns the X and Y coordinates of the datapoint at the specified index in the dataset.
    
    Returns true if we actually have a point at the index, false if not (does not set
    the destination variables in this case).
    
    Args: the index of the point required
          pointers to variables to contain the X and Y values of the specified datapoint.
          (It is up to the caller to make sure these are valid.)
*/
bool GuiLibGraphDataSet::GetDataPoint(int dataPointIndex, float *X, float *Y)
{
    if((dataPointIndex >= 0) && (dataPointIndex < graphDataSetActualLength)) {
        
        *X = theGraphDataSet[dataPointIndex].X;
        *Y = theGraphDataSet[dataPointIndex].Y;
    
        return true;
    }
    
    // 'else' index not valid
    return false;
}


/*
    Returns the Y coordinate of a point on the graph at the specified X coordinate.
    This will in general mean interpolating between two adjacent points - we assume
    a straight line in this case.
    
    Note that this function relies on the array elements being in ascending order
    of X coordinate.
*/
float GuiLibGraphDataSet::GetYCoordAtXCoord(float X)
{
    if(graphDataSetActualLength > 0) {
        
        if(graphDataSetActualLength > 1) {
        
            // Deal with the extreme cases first
            if(theGraphDataSet[0].X > X) {

                // Required point is before the start of our array
                
                double X0 = (double) theGraphDataSet[0].X;
                double Y0 = (double) theGraphDataSet[0].Y;
                double X1 = (double) theGraphDataSet[1].X;
                double Y1 = (double) theGraphDataSet[1].Y;
                
                double m = (Y1 - Y0) / (X1 - X0);
                double c = Y0 - (m * X0);
                
                return (float)(m * (double) X) + c;
            }
            
            // 'else' ...
            if(theGraphDataSet[graphDataSetActualLength - 1].X < X) {

                // Required point is after the end of our array
                
                double X0 = (double) theGraphDataSet[graphDataSetActualLength - 2].X;
                double Y0 = (double) theGraphDataSet[graphDataSetActualLength - 2].Y;
                double X1 = (double) theGraphDataSet[graphDataSetActualLength - 1].X;
                double Y1 = (double) theGraphDataSet[graphDataSetActualLength - 1].Y;
                
                double m = (Y1 - Y0) / (X1 - X0);
                double c = Y0 - (m * X0);
                
                return (float)(m * (double) X) + c;
            }
            
            // 'else' - required data point must either be coincident with one of our data points,
            //          or between two of them
            for (int i = 0; i < (graphDataSetActualLength - 1); ++i) {
                
                if(theGraphDataSet[i].X == X) {
                    // We already have a data point at the specified X coordinate
                    
                    return theGraphDataSet[i].Y;
                }

                // 'else'
                if(theGraphDataSet[i + 1].X == X) {
                    // We already have a data point at the specified X coordinate
                    
                    return theGraphDataSet[i + 1].Y;
                }
                
                // 'else'
                if(theGraphDataSet[i + 1].X > X) {
                
                    // We must interpolate between two data points
                    
                    double xValue = (double) X;
                    
                    double X0 = (double) theGraphDataSet[i].X;
                    double Y0 = (double) theGraphDataSet[i].Y;
                    double X1 = (double) theGraphDataSet[i + 1].X;
                    double Y1 = (double) theGraphDataSet[i + 1].Y;
                    
                    double m = (Y1 - Y0) / (X1 - X0);
                    double c = Y0 - (m * X0);
                    
                    return (float)((m * xValue) + c);
                }
            }
        }
        
        // 'else' we only have one datapoint - return its Y coord
        // (i.e. treat the 'graph' as a straight line)
        return theGraphDataSet[0].Y;
    }
    
    // 'else' we have no existing data points to interpolate
    return 0.0f;
}

/*
    Copy our current 'actual' floating point data array,
    to a GuiLib_GraphDataPoint array, as required by the easyGUI Graph.
    
    The actual floating point data is always in minutes, but may well have a fractional part,
    since the GC measures time, in effect, in units of 0.1 minute. The GuiLib_GraphDataPoint 
    structure, however, uses integers, so we copy the actual data to integer numbers
    either in minutes or seconds, as specified by the caller. We convert the original 
    floating point values to integers by rounding them, using our static function
    'ConvertFloatingDataPointToGuiLibGraphDataPoint'.
    
    If the caller wants minutes to be the time unit, we may well end up with multiple points
    at the same X (i.e. time) coordinate - guard against this by combining them into one point 
    whose Y coordinate is the average of the duplicate points. If the caller wants seconds,
    we assume that we cannot possibly have multiple points at the same X coordinate.
    
    Note that we pass a pointer to the GuiLib_GraphDataPoint array back to the caller.
    It is up to the caller to delete this array when it is no longer needed.
    ************************************************************************
    
    It is also up to the caller to check that the pointer we return is not NULL before using it.
    ********************************************************************************************
    
    
    Args: the time unit (minutes or seconds) to be used in the copy 
          a pointer to an unsigned integer to contain the number of points in the copied array
    
    Return value: pointer to the array of GuiLib_GraphDataPoints containing the copied data
    
*/
GuiLib_GraphDataPoint* GuiLibGraphDataSet::GetGraphDataPointCopy(TimeUnit timeUnit, GuiConst_INT16U *countOfCopiedPoints)
{
    // Remember that our 'native' data points are in time (i.e. X coordinate) units of 0.1 minute
    
    // First, a straight copy, rounding the X coordinate to the unit specified
    GuiLib_GraphDataPoint* graphDataSetCopy = new GuiLib_GraphDataPoint[graphDataSetActualLength];
    
    if(graphDataSetCopy == NULL) {
        
        // Failed to allocate the memory we need
        
        *countOfCopiedPoints = 0;
        return NULL;
    }

    // 'else' memory allocated - continue
    
    for(int index = 0; index < graphDataSetActualLength; ++index) {
        ConvertFloatingDataPointToGuiLibGraphDataPoint(&graphDataSetCopy[index], theGraphDataSet[index], timeUnit);
    }

    if(timeUnit == SECONDS) {
        
        // The simple case - since our 'native' data points are in 'units' of 0.1 minute, 
        // we cannot have multiple points at the same number of seconds, i.e. with the same X coordinate - 
        // so just return the duplicate array, with rounded X and Y coordinate values, that we created above
        
        *countOfCopiedPoints = graphDataSetActualLength; // Tell caller how many points there are
        return graphDataSetCopy;
    }
    
    
    // 'else' must be minutes - now, we must guard against multiple points at the same X coordinate
    //                          *******************************************************************
    
    // Count how many distinct points - i.e. with different X coordinate values - there are
    int pointIndex;
    GuiConst_INT32S lastXCoord = graphDataSetCopy[0].X;
    int countOfDistinctPoints = 1; // We have already looked at the first point
    for(pointIndex = 1; pointIndex < graphDataSetActualLength; ++pointIndex) {
        if(graphDataSetCopy[pointIndex].X > lastXCoord) {
            ++countOfDistinctPoints;
            lastXCoord = graphDataSetCopy[pointIndex].X;
        } 
        // 'else' this point has the same X coordinate as the previous one
    }
    
    // Now allocate an array to contain that number of points
    GuiLib_GraphDataPoint* graphDataSetMinutesCopy = new GuiLib_GraphDataPoint[countOfDistinctPoints];
    
    if(graphDataSetMinutesCopy == NULL) {
        // Failed to allocate the memory we need
        delete [] graphDataSetCopy;
        
        *countOfCopiedPoints = 0;
        return NULL;
    }
    
    // 'else' memory allocated - continue

    // When we find two or more points with the same X coordinate in the original dataset copy array,
    // we create one point in the 'minutes' array, whose Y coordinate is the average of the Y coordinates
    // of the original points
    
    graphDataSetMinutesCopy[0] = graphDataSetCopy[0];
    int minutesPointIndex = 0;
    lastXCoord = graphDataSetCopy[0].X;
    int multiPointCount = 1; // Count of the number of points at the current X coordinate
    for(pointIndex = 1; pointIndex < graphDataSetActualLength; ++pointIndex) {
        if(graphDataSetCopy[pointIndex].X > lastXCoord) {
            
            if(multiPointCount > 1) {
                // Set the minutes copy to the average of the Y values we have accumulated
                graphDataSetMinutesCopy[minutesPointIndex].Y /= multiPointCount;
                multiPointCount = 1;
            }
            
            ++minutesPointIndex;
            graphDataSetMinutesCopy[minutesPointIndex] = graphDataSetCopy[pointIndex];

            lastXCoord = graphDataSetCopy[pointIndex].X;
            
        } else {
            
            // This point must have the same X coordinate as the previous one - 
            // start accumulating the Y values, ready to take the average
            graphDataSetMinutesCopy[minutesPointIndex].Y += graphDataSetCopy[pointIndex].Y;
            ++multiPointCount;
        }
    }

    if(multiPointCount > 1) {
        graphDataSetMinutesCopy[minutesPointIndex].Y /= multiPointCount;        
    }

    delete [] graphDataSetCopy;  // No longer needed
      
    *countOfCopiedPoints = countOfDistinctPoints; // Tell caller how many points there are in the copied array
    return graphDataSetMinutesCopy; // Caller wants this array, not the original copy
}


/*
    Copy our current 'actual' floating point data array,
    to a GuiLib_GraphDataPoint array, as required by the easyGUI Graph.
    
    The actual floating point data is always in minutes, but may well have a fractional part,
    since the GC measures time, in effect, in units of 0.1 minute - and we have time as the X axis. 
    The GuiLib_GraphDataPoint structure, however, uses integers, so before copying the actual data 
    to integer numbers, we multiply the X values by 10.0. 
    
    Note that we pass a pointer to the GuiLib_GraphDataPoint array back to the caller.
    It is up to the caller to delete this array when it is no longer needed.
    ************************************************************************
    
    It is also up to the caller to check that the pointer we return is not NULL before using it.
    ********************************************************************************************
    
    
    Args: a pointer to an unsigned integer to contain the number of points in the copied array
    
    Return value: pointer to the array of GuiLib_GraphDataPoints containing the copied data
    
*/
GuiLib_GraphDataPoint* GuiLibGraphDataSet::GetGraphDataPointCopyInTenthsOfMinutes(float yAxisScaleFactor, GuiConst_INT16U *countOfCopiedPoints)
{
    GuiLib_GraphDataPoint* graphDataSetCopy = new GuiLib_GraphDataPoint[graphDataSetActualLength];
    
    if(graphDataSetCopy == NULL) {
        
        // Failed to allocate the memory we need
        
        *countOfCopiedPoints = 0;
        return NULL;
    }

    // 'else' memory allocated - continue...
    
    // The 'floor' mathematical function expects a double as its argument
    double doubleX;
    double doubleY;
        
    for(int index = 0; index < graphDataSetActualLength; ++index) {

        // 'floor' math(s) function requires a double argument, not float
        doubleX = ((double)theGraphDataSet[index].X) * 10.0;
        doubleY = (double)(theGraphDataSet[index].Y * yAxisScaleFactor);
    
        // Round, don't truncate
        graphDataSetCopy[index].X = (GuiConst_INT32S) floor(doubleX + 0.5);
        graphDataSetCopy[index].Y = (GuiConst_INT32S) floor(doubleY + 0.5);
    }

    *countOfCopiedPoints = graphDataSetActualLength; // Tell caller how many points there are
    return graphDataSetCopy;
}


/*
    Sets another GuiLibGraphDataSet object to a 'partial copy' of this one.
    A 'partial copy' is a copy that begins at the specified X start coordinate, and ends at the specified X end coordinate.
    We copy all of our coordinate pairs that lie between those two X values to the destination.
    If we do not have a coordinate pair exactly at the specified X start coordinate, we generate one 
    using our 'GetYCoordAtXCoord' member function. The same applies to the end X coordinate.
    
    Args: the X coordinate at which to start the copy
          the X coordinate at which to end the copy
          a pointer to the GuiLibGraphDataSet object which is to be the copy destination
*/
void GuiLibGraphDataSet::MakePartialCopy(float startXCoord, float endXCoord, GuiLibGraphDataSet* copyDestination)
{
    // First, clear the copy destination of any existing data
    copyDestination->ClearData();
     

    // If the end coord is less than the start coord, do nothing
    // - leave data clear
    if(endXCoord < startXCoord) {
        return;
    }

    if(graphDataSetActualLength > 0) {
        
        int pointIndex;
        for (pointIndex = 0; pointIndex < graphDataSetActualLength; ++pointIndex) {
            
            if(theGraphDataSet[pointIndex].X >= startXCoord) {
                break;
            }
        }
        
        if(pointIndex < graphDataSetActualLength) {
            if (theGraphDataSet[pointIndex].X > startXCoord) {
            
                // Need to add a start point at the specified start X coord
                copyDestination->AddDataPoint(startXCoord, GetYCoordAtXCoord(startXCoord));
                    
            } // 'else' the start point already has the correct X coordinate - no need to add another
            
            // pointIndex already has the correct start value
            for (; pointIndex < graphDataSetActualLength; ++pointIndex) {
                
                if(theGraphDataSet[pointIndex].X <= endXCoord) {
                    copyDestination->AddDataPoint(theGraphDataSet[pointIndex].X, theGraphDataSet[pointIndex].Y);
                } else {
                    break;
                }
            }
        
            if (theGraphDataSet[pointIndex - 1].X < endXCoord) {
            
                // Need to add a final point at the end X coord 
                copyDestination->AddDataPoint(endXCoord, GetYCoordAtXCoord(endXCoord));
                    
            } // 'else' the final point already has the correct X coordinate - no need to add another
            
        } // 'else' we have no points above the start X coordinate - nothing to copy  
              
    } // 'else' we have no points to copy
}

/*
    Sets another GuiLibGraphDataSet object to an 'interpolated partial copy' of this one.
    This is a copy that starts at a specified X coordinate and finishes at another specified X coordinate,
    and has a specified interval between X coordinates. This will, in general, require interpolating 
    between our existing data points.
        
    Args: the X coordinate at which to start
          the X coordinate at which to finish
          the X interval - i.e. the interval between X coords
          a pointer to the GuiLibGraphDataSet object which is to be the copy destination
*/
void GuiLibGraphDataSet::MakeInterpolatedPartialCopy(float startXCoord, float endXCoord, float xInterval, GuiLibGraphDataSet* copyDestination)
{
    // First, clear the copy destination of any existing data
    copyDestination->ClearData();
         
    // Let the for loop limits take care of whether the start and end X coordinates are in the correct order

    // Now copy/generate the actual points 
    float xCoord;
    float endLimit = endXCoord + (xInterval / 2.0f); // Should just be 'endXCoord' - but allow for floating point rounding,
                                                    // otherwise we may omit the last point 
    for (xCoord = startXCoord; xCoord <= endLimit; xCoord += xInterval) {
        copyDestination->AddDataPoint(xCoord, GetYCoordAtXCoord(xCoord));
    }
}

/*
    Sets another GuiLibGraphDataSet object to an 'interpolated partial copy' of this one.
    This is a copy that starts at a specified X coordinate and finishes at another specified X coordinate,
    and has a specified interval between X coordinates. This will, in general, require interpolating 
    between our existing data points.
    
    Unlike the original 'MakeInterpolatedPartialCopy' above, this guarantees to add a final point at the end 
    of the copy range, whether this makes an exact interval or not. This means that, in a profile where 
    the 'interpolated partial copy' is displayed as a bar chart underneath a line (created using the 
    'MakePartialCopy' function), we are guaranteed to have a bar at the exact end of the line - 
    it will not overhang empty space.
        
    Args: the X coordinate at which to start
          the X coordinate at which to finish
          the X interval - i.e. the interval between X coords
          a pointer to the GuiLibGraphDataSet object which is to be the copy destination
*/
void GuiLibGraphDataSet::MakeInterpolatedPartialCopyWithFinalPoint(float startXCoord, float endXCoord, float xInterval, GuiLibGraphDataSet* copyDestination)
{
    // First, clear the copy destination of any existing data
    copyDestination->ClearData();
         
    // Let the for loop limits take care of whether the start and end X coordinates are in the correct order

    // Now copy/generate the actual points 
    float xCoord;
    bool needFinalPoint = true;
    for (xCoord = startXCoord; xCoord <= endXCoord; xCoord += xInterval) {
        copyDestination->AddDataPoint(xCoord, GetYCoordAtXCoord(xCoord));
        
        if(xCoord == endXCoord) {
            needFinalPoint = false;
        }
    }
    
    if(needFinalPoint) {
        // We need to add a final point at the exact end of the copy range
        copyDestination->AddDataPoint(endXCoord, GetYCoordAtXCoord(endXCoord));
    }
}

/*
    Gets the current temperature for a particular component.
    
    Args: the GC command to get the temperature for that component
          the usbDevice and usbHostGC instances corresponding to the GC
    
    Returns the current temperature of the component as a floating-point value, in degrees C.
*/
float GuiLibGraphDataSet::GetComponentTemperature(char *cmd, USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    // Guard against simultaneous calls to usbHostGC->SetDeviceReport - 
    // it is not re-entrant (and nor is the GC)
    while(usbHostGC->ExecutingSetDeviceReport()) {}

    char response[50];
    usbHostGC->SetDeviceReport(usbDevice, cmd, response);
    // We expect a response like this: "Dxxx1234" - temp in units of 1 deg c
    
    float retVal;
    
    // But check for "EPKT" first
    if(response[0] == 'E') {
        retVal = -1.0f; // ** Caller must check for this **
    } else {
        sscanf(&response[4], "%f", &retVal);
    }
    
    return retVal;
}

/*
    Gets, and returns, the current column temperature.
    
    Returns the current column temperature as a floating-point value, in degrees C.
*/
float GuiLibGraphDataSet::GetColumnTemperature(USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
//#define GCOL_VALUE_IN_TENTHS_OF_A_DEGREE
#ifdef GCOL_VALUE_IN_TENTHS_OF_A_DEGREE
    // This value is actually in tenths of a degree, not whole degrees as stated above
    return ((GetComponentTemperature("GCOL", usbDevice, usbHostGC)) * 0.1f);
#undef GCOL_VALUE_IN_TENTHS_OF_A_DEGREE
#else
    return GetComponentTemperature("GCOL", usbDevice, usbHostGC);
#endif
}

/*
    Gets, and returns, the current injector temperature.
    
    Returns the current injector temperature as a floating-point value, in degrees C.
*/
float GuiLibGraphDataSet::GetInjectorTemperature(USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
//#define GINJ_VALUE_IN_TENTHS_OF_A_DEGREE
#ifdef GINJ_VALUE_IN_TENTHS_OF_A_DEGREE
    // This value is actually in tenths of a degree, not whole degrees as stated above
    return ((GetComponentTemperature("GINJ", usbDevice, usbHostGC)) * 0.1f);
#undef GINJ_VALUE_IN_TENTHS_OF_A_DEGREE
#else
    return GetComponentTemperature("GINJ", usbDevice, usbHostGC);
#endif
}

/*
    Gets, and returns, a time value, obtained using the command passed to it.
    We expect that this value will be returned by the GC in units of 0.1 minute.
    
    Args: a pointer to a null-terminated string containing the command to use
          pointers to the USBDevice and USBHost instances corresponding to the GC
    
    Note that this code is intended to be as efficient as possible.
    
    Returns the required time as a floating-point value, in minutes - i.e. scaled from 
    the value returned by the GC.
*/
float GuiLibGraphDataSet::GetTimeValue(char *cmd, USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    // Guard against simultaneous calls to usbHostGC->SetDeviceReport - 
    // it is not re-entrant (and nor is the GC)
    while(usbHostGC->ExecutingSetDeviceReport()) {}

    char buff[10];
    char response[50];
    usbHostGC->SetDeviceReport(usbDevice, cmd, response);
    // We expect a response like this: "Dxxx1234" - time in units of 0.1 minute
    
    float retVal;
    
    // But check for "EPKT" first
    if(response[0] == 'E') {
        retVal = -1.0f; // ** Caller must check for this **
    } else {
        buff[0] = response[4];
        buff[1] = response[5];
        buff[2] = response[6];
        buff[3] = '.';
        buff[4] = response[7];

        sscanf(buff, "%f", &retVal);
    }
    
    return retVal;
}

/*
    Gets, and returns, the initial hold time.
    
    Returns the initial hold time as a floating-point value, in minutes.
*/
float GuiLibGraphDataSet::GetInitialHoldTime(USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    return GetTimeValue("GTIM", usbDevice, usbHostGC);
}

/*
    Gets, and returns, the PTV/injector initial time.
    
    Returns the initial time as a floating-point value, in minutes.
*/
float GuiLibGraphDataSet::GetPTVInitialTime(USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    return GetTimeValue("GIPT", usbDevice, usbHostGC);
}

/*
    Gets, and returns, the initial pressure.
    
    Note that this code is intended to be as efficient as possible.
    
    Returns the initial pressure as a floating-point value, in minutes.
*/
float GuiLibGraphDataSet::GetInitialPressure(USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    // Guard against simultaneous calls to usbHostGC->SetDeviceReport - 
    // it is not re-entrant (and nor is the GC)
    while(usbHostGC->ExecutingSetDeviceReport()) {}

    char buff[10];
    char response[50];
    usbHostGC->SetDeviceReport(usbDevice, "GPRS", response);
    // We expect a response like this: "DPRS1234" - pressure in units of 0.1 psi
    
    float retVal;
    
    // But check for "EPKT" first
    if(response[0] == 'E') {
        retVal = -1.0f; // ** Caller must check for this **
    } else {
        buff[0] = response[4];
        buff[1] = response[5];
        buff[2] = response[6];
        buff[3] = '.';
        buff[4] = response[7];

        sscanf(buff, "%f", &retVal);
    }
    
    return retVal;
}

/*
    Gets a value from the GC for a particular ramp. 
    
    All commands that get ramp values from the GC have the form "Gxxr", where "G" means "get", 
    the characters "xx" are specific to the command, and "r" is the ramp index, 0 through 9. 
    The GC's response will have the form "Dxxrnnnn", where "nnnn" is a four digit value - 
    this is the value we want. This function converts that value to a float, and returns it in that form. 
    It is up to the caller to convert this into the correct units for the command, including scaling it if necessary.
    
    Args: the command to get the required ramp value (the three "Gxx" characters)
          the ramp index (0 to 9 inclusive)
          pointers to the USBDeviceConnected and USBHostGC instances that match the GC
          
    Returns the ramp value obtained from the GC, as an (unscaled) floating point value
*/
float GuiLibGraphDataSet::GetRampValue(char *cmd, int rampIndex, USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    // Check command length is valid
    if(strlen(cmd) != 3) {
        return 0.0f;
    }
    
    // Now the ramp index
    if((rampIndex < 0) || (rampIndex > 9)) {
        return 0.0f;
    }
    
    
    char response[GC_MESSAGE_LENGTH+2];

    // Guard against simultaneous calls to usbHostGC->SetDeviceReport - 
    // it is not re-entrant (and nor is the GC)
    while(usbHostGC->ExecutingSetDeviceReport()) {}

    char buff[100];
    sprintf(buff, "%s%d", cmd, rampIndex);
    usbHostGC->SetDeviceReport(usbDevice, buff, response);
    
    float rampValue;

    // We expect a response of the form "DXXnrrrr", where "XX" is the second and third character of the command,
    // 'n' is the ramp index, and "rrrr" is the required ramp value

    // But check for "EPKT" first...
    if(response[0] == 'E') {
        rampValue = 0.0f;
    } else {
        sscanf(&response[4], "%f", &rampValue);
    }
    
    return rampValue;
}

/*
    Gets the temperature ramp rate for a particular ramp.
    
    Args: the ramp index (0 to 9 inclusive)
          pointers to the USBDeviceConnected and USBHostGC instances that match the GC
          
    Returns the temperature ramp rate obtained from the GC, in degrees C per minute.
*/
float GuiLibGraphDataSet::GetTemperatureRampRate(int rampIndex, USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    return GetRampValue("GRP", rampIndex, usbDevice, usbHostGC);
}

/*
    Gets the PTV/injector temperature ramp rate for a particular ramp.
    
    Args: the ramp index (0 to 9 inclusive)
          pointers to the USBDeviceConnected and USBHostGC instances that match the GC
          
    Returns the temperature ramp rate obtained from the GC, in degrees C per minute.
*/
float GuiLibGraphDataSet::GetPTVTemperatureRampRate(int rampIndex, USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    return GetRampValue("GIP", rampIndex, usbDevice, usbHostGC);
}

/*
    Gets the upper temperature for a particular ramp.
    
    Args: the ramp index (0 to 9 inclusive)
          pointers to the USBDeviceConnected and USBHostGC instances that match the GC
          
    Returns the upper temperature obtained from the GC, in degrees C.
*/
float GuiLibGraphDataSet::GetRampUpperTemperature(int rampIndex, USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
//#define GRC_VALUE_IN_TENTHS_OF_A_DEGREE
#ifdef GRC_VALUE_IN_TENTHS_OF_A_DEGREE
    // This value is actually in tenths of a degree, not whole degrees as stated above
    return ((GetRampValue("GRC", rampIndex, usbDevice, usbHostGC)) * 0.1f);
#undef GRC_VALUE_IN_TENTHS_OF_A_DEGREE
#else
    return GetRampValue("GRC", rampIndex, usbDevice, usbHostGC);
#endif
}

/*
    Gets the upper temperature for a particular PTV/injector ramp.
    
    Args: the ramp index (0 to 9 inclusive)
          pointers to the USBDeviceConnected and USBHostGC instances that match the GC
          
    Returns the upper temperature obtained from the GC, in degrees C.
*/
float GuiLibGraphDataSet::GetPTVRampUpperTemperature(int rampIndex, USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    return GetRampValue("GIC", rampIndex, usbDevice, usbHostGC);
}

/*
    Gets the ramp time for a particular ramp.
    
    Args: the ramp index (0 to 9 inclusive)
          pointers to the USBDeviceConnected and USBHostGC instances that match the GC
          
    Returns the ramp time obtained from the GC, in minutes (actually the GC returns a value in units of 0.1 minute,
    but this function applies that scaling to the value before returning it).
*/
float GuiLibGraphDataSet::GetRampUpperTime(int rampIndex, USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    return ((GetRampValue("GRS", rampIndex, usbDevice, usbHostGC)) * 0.1f);
}

/*
    Gets the ramp time for a particular PTV/injector ramp.
    
    Args: the ramp index (0 to 9 inclusive)
          pointers to the USBDeviceConnected and USBHostGC instances that match the GC
          
    Returns the ramp time obtained from the GC, in minutes (actually the GC returns a value in units of 0.1 minute,
    but this function applies that scaling to the value before returning it).
*/
float GuiLibGraphDataSet::GetPTVRampUpperTime(int rampIndex, USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    return ((GetRampValue("GIS", rampIndex, usbDevice, usbHostGC)) * 0.1f);
}

/*
    Gets the pressure ramp rate for a particular ramp.
    
    Args: the ramp index (0 to 9 inclusive)
          pointers to the USBDeviceConnected and USBHostGC instances that match the GC
          
    Returns the pressure ramp rate obtained from the GC, in psi per minute (actually the GC returns a value 
    in units of 0.01 psi/min, but this function applies that scaling to the value before returning it).
*/
float GuiLibGraphDataSet::GetPressureRampRate(int rampIndex, USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    return ((GetRampValue("GPR", rampIndex, usbDevice, usbHostGC)) * 0.01f);
}

/*
    Gets the upper pressure for a particular ramp.
    
    Args: the ramp index (0 to 9 inclusive)
          pointers to the USBDeviceConnected and USBHostGC instances that match the GC
          
    Returns the upper pressure obtained from the GC, in psi (actually the GC returns a value 
    in units of 0.1 psi, but this function applies that scaling to the value before returning it).
*/
float GuiLibGraphDataSet::GetRampUpperPressure(int rampIndex, USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    return ((GetRampValue("GPU", rampIndex, usbDevice, usbHostGC)) * 0.1f);
}

/*
    Setup this dataset to match the column ramp temperature vs time values 
    currently set up in the GC.
    
    Args: - pointers to the USBDeviceConnected and USBHostGC instances that match the GC
    
    Returns the number of points in the updated dataset.
*/
int GuiLibGraphDataSet::SetupFromColumnTemperatureRampValues(USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    // First, delete all existing data from this dataset
    ClearData();
    

    // Now let the AddDataPoint function take care of extending the dataset array, etc
    
    // First point will be at the current temperature, time zero.
    // Second point will be at the same temperature, after the initial time.

    float initialTemperature = GetColumnTemperature(usbDevice, usbHostGC);
    float initialHoldTime = GetInitialHoldTime(usbDevice, usbHostGC);

    AddDataPoint(0, initialTemperature);
    AddDataPoint(initialHoldTime, initialTemperature);
    int pointCount = 2;
    
    // These values are specific to each ramp (index 0 through 9)
    float temperatureRampRate; // This is the rate at which the GC will increase the column temperature
                               // (if this is zero, this ramp is not used, and nor are any after it - 
                               // i.e. the previous ramp was the final one)
    float rampUpperTemperature;    // This is the upper (or "target") temperature
    float rampUpperTime; // Once the column reaches the upper/target temperature, the GC 
                         // will keep it at that temperature for this length of time - 
                         // at the end of this, the next ramp will start (unless this is the last one)
    float rampingTime;
    
    float totalMethodTime = initialHoldTime;
    
    float previousRampUpperTemperature = initialTemperature;
    
    // These are the coordinates we add to the graph dataset
    float X1, X2;
    float Y1, Y2;
    
#ifdef USE_VERSION_102 // This starts at ramp index 0 - older versions start at 1
    for (int rampIndex = 0; rampIndex < 10; ++rampIndex) {
#else
    for (int rampIndex = 1; rampIndex < 10; ++rampIndex) {
#endif        
        temperatureRampRate = GetTemperatureRampRate(rampIndex, usbDevice, usbHostGC);

        if(temperatureRampRate <= FLT_MIN) { // Avoid rounding errors - do not test for equality with zero
            // No more ramps
            break;
        }


        rampUpperTemperature = GetRampUpperTemperature(rampIndex, usbDevice, usbHostGC);
        
        // Sanity check 
        if(rampUpperTemperature < previousRampUpperTemperature) {
            // This should never happen. We must have got our temperature units confused (or something) - give up
            break;
        }

        rampingTime = (rampUpperTemperature - previousRampUpperTemperature) / temperatureRampRate;
        
        totalMethodTime += rampingTime;

        X1 = totalMethodTime;
        Y1 = rampUpperTemperature;
        
        if(AddDataPoint(X1, Y1)) { 
            ++pointCount; 
        }


        rampUpperTime = GetRampUpperTime(rampIndex, usbDevice, usbHostGC);

        if(rampUpperTime > FLT_MIN) { // i.e. > 0 (avoid floating point rounding errors)
        
            totalMethodTime += rampUpperTime;
    
            X2 = totalMethodTime;
            Y2 = rampUpperTemperature;
    
            if(AddDataPoint(X2, Y2)) { 
                ++pointCount;
            }
        }
        // 'Else' the ramp upper time is zero - we are not holding here - 
        // so do not add a second point (AddDataPoint does not allow 
        // two points at the same X coordinate anyway)
        // *** See comment [HOLDING INTERVAL] in SetupGasPressureProfileWithTimingsFromColumnMethod ***
                
        previousRampUpperTemperature = rampUpperTemperature;
    }
    
    nonRoundedTotalMethodTime = totalMethodTime;

    return pointCount;
}

/*
    Setup this dataset to match the ramp pressure vs time values currently set up in the GC. 
    Compares the time values with those for a specified column method, and (assuming the time values 
    should match those for the column method) turns on LED 3 if they do not.
    
    Args: - pointers to the USBDeviceConnected and USBHostGC instances that match the GC
          - pointer to a GuiLibGraphDataSet object containing the matching column method
    
    Returns the number of points in the updated dataset.
*/
int GuiLibGraphDataSet::SetupFromPressureRampValues(USBDeviceConnected* usbDevice, USBHostGC* usbHostGC, GuiLibGraphDataSet* columnMethodDataSet)
{
    // First, delete all existing data from this dataset
    ClearData();
    
    
    // Now let the AddDataPoint function take care of extending the dataset array, etc
    
    // First point will be at the initial pressure, time zero.
    // Second point will be at the same pressure, after the initial time.
    float initialPressure = (double) GetInitialPressure(usbDevice, usbHostGC);
    float initialHoldTime = (double) GetInitialHoldTime(usbDevice, usbHostGC);
    
    AddDataPoint(0, initialPressure);
    AddDataPoint(initialHoldTime, initialPressure);
    int pointCount = 2;    
    
    // These values are specific to each ramp (index 0 through 9)
    float pressureRampRate; // This is the rate at which the GC will increase the gas pressure
                            // (if this is zero, this ramp is not used, and nor are any after it - 
                            // i.e. the previous ramp was the final one)
    float rampUpperPressure;    // This is the upper (or "target") pressure
    float rampUpperTime; // Once the column reaches the upper/target pressure, the GC 
                         // will keep it at that pressure for this length of time - 
                         // at the end of this, the next ramp will start (unless this is the last one)
    float rampingTime;
    
    float totalMethodTime = initialHoldTime;
    
    float previousRampUpperPressure = initialPressure;
    
    // These are the coordinates we add to the graph dataset
    float X1, X2;
    float Y1, Y2;
    
    // These are the corresponding coordinates for the column method
    float columnX1, columnX2;
    float columnY1, columnY2;
    
#ifdef USE_VERSION_102 // This starts at ramp index 0, older versions start at 1
    for (int rampIndex = 0; rampIndex < 10; ++rampIndex) {
#else
    for (int rampIndex = 1; rampIndex < 10; ++rampIndex) {
#endif   
        pressureRampRate = GetPressureRampRate(rampIndex, usbDevice, usbHostGC);

        if(pressureRampRate <= FLT_MIN) { // Avoid rounding errors - do not test for equality with zero
            // No more ramps
            break;
        }


        rampUpperPressure = GetRampUpperPressure(rampIndex, usbDevice, usbHostGC);

        // Sanity check 
        if(rampUpperPressure < previousRampUpperPressure) {
            // This should never happen. We must have got our pressure units confused (or something) - give up
            break;
        }


        rampingTime = (rampUpperPressure - previousRampUpperPressure) / pressureRampRate;
        
        totalMethodTime += rampingTime;

        // Round the floating point values to the nearest integer
        X1 = totalMethodTime;
        Y1 = rampUpperPressure;
        
        if(columnMethodDataSet->GetDataPoint(pointCount, &columnX1, &columnY1)) {
            if(X1 != columnX1) {
                // Our time and the corresponding column method time do not match - 
                // indicate error by turning on LED 3, and use the column method value
                // (the two methods must match)
#ifdef USE_LED_FOR_DEBUGGING
                SetLed4(true);
#endif
                X1 = columnX1;
            }
        }

        if(AddDataPoint(X1, Y1)) { 
            ++pointCount; 
        }


        rampUpperTime = GetRampUpperTime(rampIndex, usbDevice, usbHostGC);
        
        totalMethodTime += rampUpperTime;

        X2 = totalMethodTime;
        Y2 = rampUpperPressure;

        if(columnMethodDataSet->GetDataPoint(pointCount, &columnX2, &columnY2)) {
            if(X2 != columnX2) {
                // Our time and the corresponding column method time do not match - 
                // indicate error by turning on LED 3, and use the column method value
                // (the two methods must match)
#ifdef USE_LED_FOR_DEBUGGING
                SetLed4(true);
#endif
                X2 = columnX2;
            }
        }

        if(AddDataPoint(X2, Y2)) { 
            ++pointCount;
        }
        
        previousRampUpperPressure = rampUpperPressure;
    }
    
    nonRoundedTotalMethodTime = totalMethodTime;
    
#ifdef USE_LED_FOR_DEBUGGING
    // Done with LED 4
    SetLed4(false);
#endif
    return pointCount;
}

/*
    Setup this dataset to give a injector temperature profile that matches a column method.
    This will show a constant injector temperature for the same length of time 
    as the specified column method.
    
    Args: - pointers to the USBDeviceConnected and USBHostGC instances that match the GC
          - pointer to a GuiLibGraphDataSet object containing the matching column method
    
    Returns the number of points in the updated dataset (this will always be 2).
*/
int GuiLibGraphDataSet::SetupInjectorTemperatureProfileToMatchColumnMethod(USBDeviceConnected* usbDevice, USBHostGC* usbHostGC, GuiLibGraphDataSet* columnMethodDataSet)
{
    // First, delete all existing data from this dataset
    ClearData();
    

    nonRoundedTotalMethodTime = columnMethodDataSet->nonRoundedTotalMethodTime;
    
    // Now let the AddDataPoint function take care of extending the dataset array, etc
    
    // First point will be at the current injector temperature, time zero.
    // Second point will be at the same temperature, at the end of the column method time.

    float injectorTemperature = GetInjectorTemperature(usbDevice, usbHostGC);
    float methodDuration = columnMethodDataSet->GetTotalMethodTime();
    
    AddDataPoint(0, injectorTemperature);
    AddDataPoint(methodDuration, injectorTemperature);
    int pointCount = 2;

    return pointCount;
}


/*
    Setup this dataset to an injector temperature profile derived purely from the 
    injector/PTV settings in the GC, without reference to the current column method (if any).
    
    Args: pointers to the USBDeviceConnected and USBHostGC instances that match the GC
    
    Returns the number of points in the updated dataset.
*/
int GuiLibGraphDataSet::SetupFromPTVTemperatureRampValues(USBDeviceConnected* usbDevice, USBHostGC* usbHostGC)
{
    // First, delete all existing data from this dataset
    ClearData();
    

    // Now let the AddDataPoint function take care of extending the dataset array, etc
    
    // First point will be at the current PTV temperature, time zero.
    // Second point will be at the same temperature, after the initial time.
    // Note that the words "injector" and "PTV" are effectively synonymous

    float initialTemperature = GetInjectorTemperature(usbDevice, usbHostGC);
    float initialTime = GetPTVInitialTime(usbDevice, usbHostGC);

    AddDataPoint(0, initialTemperature);
    AddDataPoint(initialTime, initialTemperature);
    int pointCount = 2;
    
    // These values are specific to each ramp (index 0 through 9)
    float temperatureRampRate; // This is the rate at which the GC will increase the PTV temperature
                               // (if this is zero, this ramp is not used, and nor are any after it - 
                               // i.e. the previous ramp was the final one)
    float rampUpperTemperature;    // This is the upper (or "target") temperature
    float rampUpperTime; // Once the PTV reaches the upper/target temperature, the GC 
                         // will keep it at that temperature for this length of time - 
                         // at the end of this, the next ramp will start (unless this is the last one)
    float rampingTime;
    
    float totalMethodTime = initialTime;
    
    float previousRampUpperTemperature = initialTemperature;
    
    // These are the coordinates we add to the graph dataset
    float X1, X2;
    float Y1, Y2;
    
#ifdef USE_VERSION_102 // This starts at ramp index 0 - older versions start at 1
    for (int rampIndex = 0; rampIndex < 10; ++rampIndex) {
#else
    for (int rampIndex = 1; rampIndex < 10; ++rampIndex) {
#endif        
        temperatureRampRate = GetPTVTemperatureRampRate(rampIndex, usbDevice, usbHostGC);

        if(temperatureRampRate <= FLT_MIN) { // Avoid rounding errors - do not test for equality with zero
            // No more ramps
            break;
        }


        rampUpperTemperature = GetPTVRampUpperTemperature(rampIndex, usbDevice, usbHostGC);
        
        // Sanity check 
        if(rampUpperTemperature < previousRampUpperTemperature) {
            // This should never happen. We must have got our temperature units confused (or something) - give up
            break;
        }

        rampingTime = (rampUpperTemperature - previousRampUpperTemperature) / temperatureRampRate;
        
        totalMethodTime += rampingTime;

        X1 = totalMethodTime;
        Y1 = rampUpperTemperature;
        
        if(AddDataPoint(X1, Y1)) { 
            ++pointCount; 
        }


        rampUpperTime = GetPTVRampUpperTime(rampIndex, usbDevice, usbHostGC);

        if(rampUpperTime > FLT_MIN) { // i.e. > 0 (avoid floating point rounding errors)
        
            totalMethodTime += rampUpperTime;
    
            X2 = totalMethodTime;
            Y2 = rampUpperTemperature;
    
            if(AddDataPoint(X2, Y2)) { 
                ++pointCount;
            }
        }
        // 'Else' the ramp upper time is zero - we are not holding here - 
        // so do not add a second point (AddDataPoint does not allow 
        // two points at the same X coordinate anyway)
        
        previousRampUpperTemperature = rampUpperTemperature;
    }
    
    nonRoundedTotalMethodTime = totalMethodTime;

    return pointCount;
}

/*
    Setup this dataset with a gas pressure profile that matches the current column method.
    The timings will be taken from the matching column method - which *must* already be set up.
    In Constant Pressure mode (first gas ramp rate == 0), the same pressure
    will be maintained from start to end of the method, while in Programmed Pressure mode
    (first gas ramp rate > 0), the pressure ramps will each start and end at the same times 
    as the corresponding temperature ramps in the column method.
    
    In both cases, the total duration will be the same as the column method.
    
    Args: - pointers to the USBDeviceConnected and USBHostGC instances that match the GC
          - pointer to a GuiLibGraphDataSet object containing the matching column method
    
    Returns the number of points in the updated dataset.
*/
int GuiLibGraphDataSet::SetupGasPressureProfileWithTimingsFromColumnMethod(USBDeviceConnected* usbDevice, USBHostGC* usbHostGC, GuiLibGraphDataSet* columnMethodDataSet)
{
    // First, delete all existing data from this dataset
    ClearData();

    // Now let the AddDataPoint function take care of extending the dataset array, etc

    int pointCount; 
    
    if(GetPressureRampRate(0, usbDevice, usbHostGC) > FLT_MIN) { // Avoid rounding errors

        // Programmed Pressure mode
            
        // First point will be at the initial pressure, time zero.
        // Second point will be at the same pressure, after the initial time.
        float initialPressure = (double) GetInitialPressure(usbDevice, usbHostGC);
        float initialHoldTime = (double) GetInitialHoldTime(usbDevice, usbHostGC);
        
        AddDataPoint(0, initialPressure);
        AddDataPoint(initialHoldTime, initialPressure);
        pointCount = 2;    
        
        // This value is specific to each ramp (index 0 through 9)
        float rampUpperPressure;    // This is the upper (or "target") pressure
        
        float totalMethodTime = initialHoldTime;
        
        // These are the coordinates we add to the graph dataset.
        // X coordinates are time values, Y coordinates are temperatures
        // (in the column dataset) or pressures (in this dataset).
        // Note that we get our time values from the column dataset - 
        // we do not calculate them here - we want to make sure they match
        float X1, Y1;
        float X2, Y2;
        
    #ifdef USE_VERSION_102 // This starts at ramp index 0, older versions start at 1
        for (int rampIndex = 0; rampIndex < 10; ++rampIndex) {
    #else
        for (int rampIndex = 1; rampIndex < 10; ++rampIndex) {
    #endif   
            rampUpperPressure = GetRampUpperPressure(rampIndex, usbDevice, usbHostGC);
    
            if(!columnMethodDataSet->GetDataPoint(pointCount, &X1, &Y1)) {
                // We have run out of points in the column method
                break;
            }
            
            AddDataPoint(X1, rampUpperPressure);
            totalMethodTime = X1;
            ++pointCount;
                
            if(!columnMethodDataSet->GetDataPoint(pointCount, &X2, &Y2)) {
                // We have run out of points in the column method
                break;
            }
            
            if(Y2 == Y1) {
                // Temperatures are the same - this is a non-zero holding interval
                AddDataPoint(X2, rampUpperPressure);
                totalMethodTime = X2;
                ++pointCount;
            }
            // 'else' we are not holding here - do not add a second point - 
            // the second point here is actually the end of the next ramp,
            // so re-use it for the next ramp (don't increment pointCount, 
            // so we get the same data point again 'next time round') 
            // *** See comment [HOLDING INTERVAL] in SetupFromColumnTemperatureRampValues ***
        }
        
        nonRoundedTotalMethodTime = totalMethodTime;
    
    } else { 
    
        // Constant pressure mode
    
        nonRoundedTotalMethodTime = columnMethodDataSet->nonRoundedTotalMethodTime;
        
        // First point will be at the initial gas pressure, time zero.
        // Second point will be at the same pressure, at the end of the column method time.
    
        float initialPressure = (double) GetInitialPressure(usbDevice, usbHostGC);
        float methodDuration = columnMethodDataSet->GetTotalMethodTime();
        
        AddDataPoint(0, initialPressure);
        AddDataPoint(methodDuration, initialPressure);
        pointCount = 2;
    }

    return pointCount;
}
    
/*
    Returns the total time expected to be taken by the current method.
    (This is effectively the same as the X coordinate of the final datapoint.)
    
    Takes no arguments.
*/
float GuiLibGraphDataSet::GetTotalMethodTime(void)
{
    if(graphDataSetActualLength <= 0) {
        return 0.0f;
    }
    
    // 'else' we have some data
    float finalX;
    float finalY;
    if(GetDataPoint((graphDataSetActualLength - 1), &finalX, &finalY)) {
        return (finalX);
    }
    
    // 'else' GetDataPoint failed for some reason
    return 0.0f;
}
    
/*
    Returns the range of X coordinate values.
    
    Args: pointers to the variables to contain the minimum and maximum X values
          (these are integers, as required for an easyGUI graph)
          the time unit to be used (minutes or seconds)
    
    No return code.
*/
void GuiLibGraphDataSet::GetXAxisRange(GuiConst_INT32S *xMin, GuiConst_INT32S *xMax, TimeUnit timeUnit)
{
    if(graphDataSetActualLength <= 0) {
        // We have no data
        *xMin = 0;
        *xMax = 0;
    
        return;
    }
    
    // 'else' we have some data
    float X1;
    float Y1;
    float X2;
    float Y2;
    GetDataPoint(0, &X1, &Y1);
    GetDataPoint((graphDataSetActualLength - 1), &X2, &Y2);
    
    if(timeUnit == SECONDS) {
        X1 *= 60.0f;
        X2 *= 60.0f;
    }
    
    *xMin = (GuiConst_INT32S) floor((double)X1 + 0.5);
    *xMax = (GuiConst_INT32S) floor((double)X2 + 0.5);
}

/*
    Returns the range of X coordinate values, in units of 0.1 minute.
    
    Args: pointers to the variables to contain the minimum and maximum X values
          (these are integers, as required for an easyGUI graph)
    
    No return code.
*/
void GuiLibGraphDataSet::GetXAxisRangeInTenthsOfMinutes(GuiConst_INT32S *xMin, GuiConst_INT32S *xMax)
{
    if(graphDataSetActualLength <= 0) {
        // We have no data
        *xMin = 0;
        *xMax = 0;
    
        return;
    }
    
    // 'else' we have some data
    float X1;
    float Y1;
    float X2;
    float Y2;
    GetDataPoint(0, &X1, &Y1);
    GetDataPoint((graphDataSetActualLength - 1), &X2, &Y2);
    
    *xMin = (GuiConst_INT32S) (X1 * 10.0f);
    *xMax = (GuiConst_INT32S) (X2 * 10.0f);
}

/*
    Returns the range of X coordinate values - but rounded to be a whole number of 'ticks' 
    (i.e. the interval between marks on the X axis). 
    
    Args: pointers to the variables to contain the minimum and maximum X values
          the tick size
          the time unit
    
    No return code.
*/
void GuiLibGraphDataSet::GetXAxisRange(GuiConst_INT32S *xMin, GuiConst_INT32S *xMax, GuiConst_INT32S xTickSize, TimeUnit timeUnit)
{
    GuiConst_INT32S rawXMin;
    GuiConst_INT32S rawXMax;
    
    GetXAxisRange(&rawXMin, &rawXMax, timeUnit);
    
    RoundAxisRangeByTickSize(xMin, xMax, rawXMin, rawXMax, xTickSize);
}

/*
    Returns the range of X coordinate values, in units of 0.1 minute - but rounded to be a whole number of 'ticks' 
    (i.e. the interval between marks on the X axis). 
    
    Args: pointers to the variables to contain the minimum and maximum X values
          the tick size
    
    No return code.
*/
void GuiLibGraphDataSet::GetXAxisRangeInTenthsOfMinutes(GuiConst_INT32S *xMin, GuiConst_INT32S *xMax, GuiConst_INT32S xTickSize)
{
    GuiConst_INT32S rawXMin;
    GuiConst_INT32S rawXMax;
    
    GetXAxisRangeInTenthsOfMinutes(&rawXMin, &rawXMax);
    
    RoundAxisRangeByTickSize(xMin, xMax, rawXMin, rawXMax, xTickSize);
}

/*
    Returns the range of Y coordinate values.
    
    Args: pointers to the variables to contain the minimum and maximum Y values
          (these are integers, as required for an easyGUI graph)
    
    No return code.
*/
void GuiLibGraphDataSet::GetYAxisRange(GuiConst_INT32S *yMin, GuiConst_INT32S *yMax)
{
    if(graphDataSetActualLength <= 0) {
        // We have no data
        *yMin = 0;
        *yMax = 0;
    
        return;
    }
    
    // 'else' we have some data
    float X;
    float Y;
    float minYSoFar;
    float maxYSoFar;
    
    GetDataPoint(0, &X, &Y);
    minYSoFar = Y;
    maxYSoFar = Y;
    
    for (int pointIndex = 1; pointIndex < graphDataSetActualLength; ++pointIndex) {
        GetDataPoint(pointIndex, &X, &Y);
        
        if(Y < minYSoFar) { minYSoFar = Y; }
        if(Y > maxYSoFar) { maxYSoFar = Y; }
    }
    
    *yMin = (GuiConst_INT32S) floor((double)minYSoFar + 0.5);
    *yMax = (GuiConst_INT32S) floor((double)maxYSoFar + 0.5);
}

/*
    Returns the range of Y coordinate values - but rounded to be a whole number of 'ticks' 
    (i.e. the interval between marks on the Y axis)
    
    Args: pointers to the variables to contain the minimum and maximum X values
          the tick size
    
    No return code.
*/
void GuiLibGraphDataSet::GetYAxisRange(GuiConst_INT32S *yMin, GuiConst_INT32S *yMax, GuiConst_INT32S yTickSize)
{
    GuiConst_INT32S rawYMin;
    GuiConst_INT32S rawYMax;
    
    GetYAxisRange(&rawYMin, &rawYMax);
    
    RoundAxisRangeByTickSize(yMin, yMax, rawYMin, rawYMax, yTickSize);
}

/*
    Rounds an axis range to be a whole number of 'ticks' - i.e. the range starts at the last tick below the minimum value,
    and ends at the first tick above the maximum value.
    
    Args: pointers to the variables to contain the rounded minimum and maximum
          the 'raw' minimum and maximum values
          the tick size
*/
void GuiLibGraphDataSet::RoundAxisRangeByTickSize(GuiConst_INT32S *axisMin, GuiConst_INT32S *axisMax, GuiConst_INT32S rawAxisMin, GuiConst_INT32S rawAxisMax, GuiConst_INT32S axisTickSize)
{
    if(axisTickSize > 0) {
        float fAxisTickSize = (float) axisTickSize;
        
        float fRawAxisMin = (float) rawAxisMin;
        float axisTickCountMin = fRawAxisMin / fAxisTickSize;
        *axisMin = (GuiConst_INT32S) (fAxisTickSize * (floor (axisTickCountMin)));
        
        float fRawAxisMax = (float) rawAxisMax;
        float axisTickCountMax = fRawAxisMax / fAxisTickSize;
        *axisMax = (GuiConst_INT32S) (fAxisTickSize * (ceil (axisTickCountMax)));
    } else {
        *axisMin = rawAxisMin;
        *axisMax = rawAxisMax;
    }
}


/*
    Draws the profile directly to the display, using the GuiLib_VLine function, via the DrawProfileSectionUsingGuiLibVLine function defined in main.cpp.
    In general, this will be two colours, with a boundary part way across, to represent how much of the method we have executed, and how much remains.
          
    This is the private version of this function, which takes a boolean value stating whether or not 
    to record the parameter values. One public version takes explicit values, and tells this function to record them, 
    while the other takes no parameters, simply passing this function the previously recorded values
    (this is so that the caller does not have to keep re-calculating the same values, possibly in more than one place).
    
    Args: the display X coordinate at which to start
          the display Y coordinate at which to start
          the X and Y scale factors - i.e. the values you have to multiply our X and Y coordinates by, to get display coordinates
          the colour of the first section
          the colour of the second section
          the X coordinate (internal, not display) at which the second section starts. If this is < 0, we draw the entire profile in the first colour
          flag saying whether or not to record the parameters
          
    No return code
*/
void GuiLibGraphDataSet::DrawUsingGuiLibVLine(GuiConst_INT16S xLeft, GuiConst_INT16S yBottom, double xScaleFactor, double yScaleFactor, 
                                              GuiConst_INTCOLOR firstColour, GuiConst_INTCOLOR secondColour, double xColourBoundary, bool recordParameters)
{
    if(theGraphDataSet != NULL) {
        
        GuiConst_INT16S profileSectionLeft;
        GuiConst_INT16S profileSectionRight;

        GuiConst_INT16S profileSectionTopLeft;
        GuiConst_INT16S profileSectionTopRight;
        
        GuiConst_INT16S profileColourBoundary = xLeft + (GuiConst_INT16S)floor(xColourBoundary * xScaleFactor);
        
        for(int graphDataSetIndex = 0; graphDataSetIndex < (graphDataSetActualLength - 1); ++graphDataSetIndex) {
            
            profileSectionLeft  = xLeft + (GuiConst_INT16S)floor(theGraphDataSet[graphDataSetIndex].X * xScaleFactor);
            profileSectionRight = xLeft + (GuiConst_INT16S)floor(theGraphDataSet[graphDataSetIndex + 1].X * xScaleFactor);
                            
            profileSectionTopLeft  = yBottom + (GuiConst_INT16S)floor(theGraphDataSet[graphDataSetIndex].Y * yScaleFactor);
            profileSectionTopRight = yBottom + (GuiConst_INT16S)floor(theGraphDataSet[graphDataSetIndex + 1].Y * yScaleFactor);

            DrawProfileSectionUsingGuiLibVLine(firstColour, secondColour, profileSectionLeft, profileSectionRight, profileColourBoundary, yBottom, profileSectionTopLeft, profileSectionTopRight);
        }
    }
    
    if(recordParameters) {
        DrawUsingGuiLibVLineParameters.isSet           = true;
        DrawUsingGuiLibVLineParameters.xLeft           = xLeft; 
        DrawUsingGuiLibVLineParameters.yBottom         = yBottom; 
        DrawUsingGuiLibVLineParameters.xScaleFactor    = xScaleFactor;
        DrawUsingGuiLibVLineParameters.yScaleFactor    = yScaleFactor;
        DrawUsingGuiLibVLineParameters.firstColour     = firstColour;
        DrawUsingGuiLibVLineParameters.secondColour    = secondColour;
        DrawUsingGuiLibVLineParameters.xColourBoundary = xColourBoundary;
    }
    //&&&&&
}

/*
    Call the DrawUsingGuiLibVLine function (see above) with newly-calculated parameter values.
    
    Args: the display X coordinate at which to start
          the display Y coordinate at which to start
          the X and Y scale factors - i.e. the values you have to multiply our X and Y coordinates by, to get display coordinates
          the colour of the first section
          the colour of the second section
          the X coordinate (internal, not display) at which the second section starts. If this is < 0, we draw the entire profile in the first colour

    No return code.
*/
void GuiLibGraphDataSet::DrawUsingGuiLibVLine(GuiConst_INT16S xLeft, GuiConst_INT16S yBottom, double xScaleFactor, double yScaleFactor, GuiConst_INTCOLOR firstColour, GuiConst_INTCOLOR secondColour, double xColourBoundary)
{
    DrawUsingGuiLibVLine(xLeft,
                    yBottom,
                    xScaleFactor,
                    yScaleFactor,
                    firstColour,
                    secondColour,
                    xColourBoundary,
                    true );
}

/*
    Repeats the last call to the version of DrawUsingGuiLibVLine with parameters,
    using the same parameter values (if available). This is so that the caller 
    does not have to repeatedly calculate the same values in more than one place.
*/
void GuiLibGraphDataSet::DrawUsingGuiLibVLine(void)
{
    if(DrawUsingGuiLibVLineParameters.isSet) {
        DrawUsingGuiLibVLine(DrawUsingGuiLibVLineParameters.xLeft,
                        DrawUsingGuiLibVLineParameters.yBottom,
                        DrawUsingGuiLibVLineParameters.xScaleFactor,
                        DrawUsingGuiLibVLineParameters.yScaleFactor,
                        DrawUsingGuiLibVLineParameters.firstColour,
                        DrawUsingGuiLibVLineParameters.secondColour,
                        DrawUsingGuiLibVLineParameters.xColourBoundary,
                        false );
    }
    // else no values available - do nothing
}



/*
    GuiLibGraph constructor - the graph index must be specified by the caller,
    and must match the index number specified for the graph in easyGUI
    ******************************************************************
*/
GuiLibGraph::GuiLibGraph(GuiConst_INT8U graphIndex)
{
    GuiLib_GraphIndex = graphIndex;
    
    xAxisUnits = MINUTES;
    
    for(int i = 0; i < DATASET_INDEX_COUNT; ++i) {
        dataSetDataPtr[i] = NULL;
    }
    
    xAxisLabelData.isSet = false;
}

GuiLibGraph::~GuiLibGraph()
{
    for(int i = 0; i < DATASET_INDEX_COUNT; ++i) {
        if(dataSetDataPtr[i] != NULL) {
            delete [] dataSetDataPtr[i];
        }
    }
}


void GuiLibGraph::SetXAxisUnits(TimeUnit newXAxisUnits)
{
    xAxisUnits = newXAxisUnits;
}

TimeUnit GuiLibGraph::GetXAxisUnits(void)
{
    return xAxisUnits;
}


/*
    Draws the axes for this graph.
    
    Encapsulates the easyGUI 'GuiLib_Graph_DrawAxes' function,
    passing it the index number specified for this graph
    
    Args: None (we already know the index number for this graph)
    
*/
GuiConst_INT8U GuiLibGraph::DrawAxes(void)
{
    if(xAxisUnits == SECONDS) {
        GuiLib_Graph_HideXAxis(GuiLib_GraphIndex, MINUTES_XAXIS_INDEX);
        GuiLib_Graph_ShowXAxis(GuiLib_GraphIndex, SECONDS_XAXIS_INDEX);
    } else {
        GuiLib_Graph_ShowXAxis(GuiLib_GraphIndex, MINUTES_XAXIS_INDEX);
        GuiLib_Graph_HideXAxis(GuiLib_GraphIndex, SECONDS_XAXIS_INDEX);
    }
    
    return GuiLib_Graph_DrawAxes(GuiLib_GraphIndex);
}

/*
    Since the units we are using for the time values (0.1 minute) cannot be displayed by the easyGUI graph object,
    which works only in integers, we are multiplying our x coordinates by 10 before passing them to the graph.
    This means that we cannot let the easyGUI graph label its own X axis, since the values will look incorrect
    to the user. We must therefore draw the values ourselves - which is what this function does.
          
    This is the private version of this function, which takes a boolean value stating whether or not 
    to record the parameter values. One public version takes explicit values, and tells this function to record them, 
    while the other takes no parameters, simply passing this function the previously recorded values
    (this is so that the caller does not have to keep re-calculating the same values, possibly in more than one place).
    
    Args: the start and end X axis values
          the tick size (i.e. the interval between successive values
          the X and Y coordinates of the graph
          the width of the graph
          boolean stating whether or not to record the parameter values
*/
void GuiLibGraph::DrawXAxisLabels(GuiConst_INT32S minValue, GuiConst_INT32S maxValue, GuiConst_INT32S tickSize, 
                     GuiConst_INT16S graphX, GuiConst_INT16S graphY, GuiConst_INT16S graphW, bool recordParameters)
{
    int intervalCount = (maxValue - minValue) / tickSize;
    
    GuiConst_INT16S xCoord = graphX + 10; // 10 is empirical - relative to the graph X coordinate, it puts the labels in the correct place
    GuiConst_INT16S xCoordInterval = graphW / intervalCount;

    GuiConst_INT16S yCoord = graphY + 15; // 15 is also empirical - relative to the graph Y coordinate, it puts the labels in the correct place
    
    const GuiConst_INT16U fontNo = GuiFont_Helv20Bold;
    char text[100];
        
    for(GuiConst_INT32S xValue = minValue; xValue <= maxValue; xValue += tickSize) {
        sprintf(text, "%d", xValue);
        
        GuiLib_DrawStr(
            xCoord,                    //GuiConst_INT16S X,
            yCoord,                    //GuiConst_INT16S Y,
            fontNo,                    //GuiConst_INT16U FontNo,
            text,                      //GuiConst_TEXT PrefixLocate *String,
            GuiLib_ALIGN_CENTER,       //GuiConst_INT8U Alignment, 
            GuiLib_PS_ON,              //GuiConst_INT8U PsWriting,
            GuiLib_TRANSPARENT_ON,     //GuiConst_INT8U Transparent,
            GuiLib_UNDERLINE_OFF,      //GuiConst_INT8U Underlining,
            0,                         //GuiConst_INT16S BackBoxSizeX,
            0,                         //GuiConst_INT16S BackBoxSizeY1,
            0,                         //GuiConst_INT16S BackBoxSizeY2,
            GuiLib_BBP_NONE,           //GuiConst_INT8U BackBorderPixels,
            0,                         //GuiConst_INTCOLOR ForeColor,
            0xFFFF                     //GuiConst_INTCOLOR BackColor
        ); 
        
        xCoord += xCoordInterval;
    }
    
    if(recordParameters) {
        xAxisLabelData.isSet = true;
        xAxisLabelData.minValue = minValue; 
        xAxisLabelData.maxValue = maxValue; 
        xAxisLabelData.tickSize = tickSize;
        xAxisLabelData.graphX   = graphX;
        xAxisLabelData.graphY   = graphY;
        xAxisLabelData.graphW   = graphW;
    }
}
                     
/*
    Call the DrawXAxisLabels function (see above) with newly-calculated parameter values.
    
    Args: the start and end X axis values
          the tick size (i.e. the interval between successive values
          the X and Y coordinates of the graph
          the width of the graph
*/
void GuiLibGraph::DrawXAxisLabels(GuiConst_INT32S minValue, GuiConst_INT32S maxValue, GuiConst_INT32S tickSize, 
                     GuiConst_INT16S graphX, GuiConst_INT16S graphY, GuiConst_INT16S graphW)
{
    DrawXAxisLabels(minValue,
                    maxValue,
                    tickSize,
                    graphX,
                    graphY,
                    graphW,
                    true );
}

/*
    Repeats the last call to the version of DrawXAxisLabels with parameters,
    using the same parameter values (if available). This is so that the caller 
    does not have to repeatedly calculate the same values in more than one place.
*/
void GuiLibGraph::DrawXAxisLabels(void)
{
    if(xAxisLabelData.isSet) {
        DrawXAxisLabels(xAxisLabelData.minValue,
                        xAxisLabelData.maxValue,
                        xAxisLabelData.tickSize,
                        xAxisLabelData.graphX,
                        xAxisLabelData.graphY,
                        xAxisLabelData.graphW,
                        false );
    }
    // else no values available - do nothing
}


/*
    Similarly to the X axis, we deliberately pass values that are larger than the real values to the Y axis.
    This is to avoid the 'stepped' appearance that the profile can have for methods with a small range of values in Y.
    This means that we cannot let the easyGUI graph label its own Y axis, since the values will look incorrect
    to the user. We must therefore draw the values ourselves - which is what this function does.
          
    This is the private version of this function, which takes a boolean value stating whether or not 
    to record the parameter values. One public version takes explicit values, and tells this function to record them, 
    while the other takes no parameters, simply passing this function the previously recorded values
    (this is so that the caller does not have to keep re-calculating the same values, possibly in more than one place).
    
    Args: the start and end Y axis values
          the tick size (i.e. the interval between successive values
          the X and Y coordinates of the graph
          the height of the graph
          boolean stating whether or not to record the parameter values
*/
void GuiLibGraph::DrawYAxisLabels(GuiConst_INT32S minValue, GuiConst_INT32S maxValue, GuiConst_INT32S tickSize, 
                     GuiConst_INT16S graphX, GuiConst_INT16S graphY, GuiConst_INT16S graphH, bool recordParameters)
{
    int intervalCount = (maxValue - minValue) / tickSize;
    
    GuiConst_INT16S xCoord = graphX; // GuiLib_ALIGN_RIGHT means that the text ends at the X coordinate

    GuiConst_INT16S yCoord = graphY - 2;  // 2 is also empirical - relative to the graph Y coordinate, it puts the labels in the correct place
    GuiConst_INT16S yCoordInterval = (graphH / intervalCount) - 2; // 2 is also empirical - otherwise, the labels get out of step with the Y axis ticks as they go up the graph
    
    const GuiConst_INT16U fontNo = GuiFont_Helv20Bold;
    char text[100];
        
    for(GuiConst_INT32S xValue = minValue; xValue <= maxValue; xValue += tickSize) {
        sprintf(text, "%d", xValue);
        
        GuiLib_DrawStr(
            xCoord,                    //GuiConst_INT16S X,
            yCoord,                    //GuiConst_INT16S Y,
            fontNo,                    //GuiConst_INT16U FontNo,
            text,                      //GuiConst_TEXT PrefixLocate *String,
            GuiLib_ALIGN_RIGHT,        //GuiConst_INT8U Alignment, 
            GuiLib_PS_ON,              //GuiConst_INT8U PsWriting,
            GuiLib_TRANSPARENT_ON,     //GuiConst_INT8U Transparent,
            GuiLib_UNDERLINE_OFF,      //GuiConst_INT8U Underlining,
            0,                         //GuiConst_INT16S BackBoxSizeX,
            0,                         //GuiConst_INT16S BackBoxSizeY1,
            0,                         //GuiConst_INT16S BackBoxSizeY2,
            GuiLib_BBP_NONE,           //GuiConst_INT8U BackBorderPixels,
            0,                         //GuiConst_INTCOLOR ForeColor,
            0xFFFF                     //GuiConst_INTCOLOR BackColor
        ); 
        
        yCoord -= yCoordInterval;
    }
    
    if(recordParameters) {
        yAxisLabelData.isSet = true;
        yAxisLabelData.minValue = minValue; 
        yAxisLabelData.maxValue = maxValue; 
        yAxisLabelData.tickSize = tickSize;
        yAxisLabelData.graphX   = graphX;
        yAxisLabelData.graphY   = graphY;
        yAxisLabelData.graphH   = graphH;
    }
}
                     
/*
    Call the DrawYAxisLabels function (see above) with newly-calculated parameter values.
    
    Args: the start and end Y axis values
          the tick size (i.e. the interval between successive values
          the X and Y coordinates of the graph
          the height of the graph
*/
void GuiLibGraph::DrawYAxisLabels(GuiConst_INT32S minValue, GuiConst_INT32S maxValue, GuiConst_INT32S tickSize, 
                     GuiConst_INT16S graphX, GuiConst_INT16S graphY, GuiConst_INT16S graphH)
{
    DrawYAxisLabels(minValue,
                    maxValue,
                    tickSize,
                    graphX,
                    graphY,
                    graphH,
                    true );
}

/*
    Repeats the last call to the version of DrawYAxisLabels with parameters,
    using the same parameter values (if available). This is so that the caller 
    does not have to repeatedly calculate the same values in more than one place.
*/
void GuiLibGraph::DrawYAxisLabels(void)
{
    if(yAxisLabelData.isSet) {
        DrawYAxisLabels(yAxisLabelData.minValue,
                        yAxisLabelData.maxValue,
                        yAxisLabelData.tickSize,
                        yAxisLabelData.graphX,
                        yAxisLabelData.graphY,
                        yAxisLabelData.graphH,
                        false );
    }
    // else no values available - do nothing
}


/*
    Draws the specified data set for this graph.
    
    Encapsulates the easyGUI 'GuiLib_Graph_DrawDataSet' function,
    passing it the index number specified for this graph,
    and the data set index number given by the caller.
    
    Note that a data set with that index must already have been created 
    in easyGUI for this graph.
    
    Args: index number of the data set
    
*/
GuiConst_INT8U GuiLibGraph::DrawDataSet(GuiConst_INT8U dataSetIndex)
{
    return GuiLib_Graph_DrawDataSet(GuiLib_GraphIndex, dataSetIndex);
}

/*
    Draws the specified data point in the specified data set for this graph.
    
    Encapsulates the easyGUI 'GuiLib_Graph_DrawDataPoint' function,
    passing it the index number specified for this graph,
    and the data set and data point index numbers given by the caller.
    
    Note that a data set with that index must already have been created 
    in easyGUI for this graph.
    
    Args: index number of the data set
          index number of the data point in that set
    
*/
GuiConst_INT8U GuiLibGraph::DrawDataPoint(GuiConst_INT8U dataSetIndex, GuiConst_INT16U dataPointIndex)
{
    return GuiLib_Graph_DrawDataPoint(GuiLib_GraphIndex, dataSetIndex, dataPointIndex);
}

/*
    Shows the specified data set for this graph.
    
    Encapsulates the easyGUI 'GuiLib_Graph_ShowDataSet' function,
    passing it the index number specified for this graph,
    and the data set index number given by the caller.
    
    Note that a data set with that index must already have been created 
    in easyGUI for this graph.

    Args: index number of the data set
    
*/
GuiConst_INT8U GuiLibGraph::ShowDataSet(GuiConst_INT8U dataSetIndex)
{
    return GuiLib_Graph_ShowDataSet(GuiLib_GraphIndex, dataSetIndex);
}

/*
    Hides the specified data set for this graph.
    
    Encapsulates the easyGUI 'GuiLib_Graph_HideDataSet' function,
    passing it the index number specified for this graph,
    and the data set index number given by the caller.
    
    Note that a data set with that index must already have been created 
    in easyGUI for this graph.

    Args: index number of the data set
    
*/
GuiConst_INT8U GuiLibGraph::HideDataSet(GuiConst_INT8U dataSetIndex)
{
    return GuiLib_Graph_HideDataSet(GuiLib_GraphIndex, dataSetIndex);
}

/*
    Sets the range of the X axis for this graph.
    
    Encapsulates the easyGUI 'GuiLib_Graph_SetXAxisRange' function,
    passing it the index number specified for this graph, 
    the X axis index number (this depends on the selected units), 
    and the minimum and maximum values given by the caller.
    
    Args: minimum value for the range
          maximum value for the range
        
*/
GuiConst_INT8U GuiLibGraph::SetXAxisRange(GuiConst_INT32S minValue, GuiConst_INT32S maxValue)
{
#define BOTH_AT_ONCE
#ifdef BOTH_AT_ONCE
    // The graph coordinates seem to relate to X axis zero, regardless of which one we are actually displaying -
    // so set both at once
    if(GuiLib_Graph_SetXAxisRange(GuiLib_GraphIndex, MINUTES_XAXIS_INDEX, minValue, maxValue) == 0) {
        // Failed
        return 0;
    }
    
    // 'else' OK
    return GuiLib_Graph_SetXAxisRange(GuiLib_GraphIndex, SECONDS_XAXIS_INDEX, minValue, maxValue);
#else
    GuiConst_INT8U xAxisIndex = (xAxisUnits == SECONDS) ? SECONDS_XAXIS_INDEX : MINUTES_XAXIS_INDEX;
    
    return GuiLib_Graph_SetXAxisRange(GuiLib_GraphIndex, xAxisIndex, minValue, maxValue);
#endif
}

/*
    Sets the range of the Y axis for this graph.
    
    Encapsulates the easyGUI 'GuiLib_Graph_SetYAxisRange' function,
    passing it the index number specified for this graph, 
    the Y axis index number (currently always zero), 
    and the minimum and maximum values for its range given by the caller.
    
    Args: minimum value for the range
          maximum value for the range
        
*/
GuiConst_INT8U GuiLibGraph::SetYAxisRange(GuiConst_INT32S minValue, GuiConst_INT32S maxValue)
{
    GuiConst_INT8U yAxisIndex = YAXIS_INDEX;
    
    return GuiLib_Graph_SetYAxisRange(GuiLib_GraphIndex, yAxisIndex, minValue, maxValue);
}

/*
    Sets the data for the specified data set.
    
    Args: the dataset index (this must already have been created in easyGUI)
          (a pointer to) a GuiLibGraphDataSet object containing the new data
          the time unit (minutes or seconds) to be used
          
    Note that we reject dataset index values outside the range 0-9
    (since we need to keep a pointer to the memory used, and currently 
     we only allow a fixed number of them).
          
*/
GuiConst_INT8U GuiLibGraph::SetDataForGraphDataSet(GuiConst_INT8U dataSetIndex, GuiLibGraphDataSet* dataSet, TimeUnit timeUnit)
{
    /*
        This is a copy of the declaration of 'GuiLib_Graph_AddDataSet' in GuiLib.h:
        
        extern GuiConst_INT8U GuiLib_Graph_AddDataSet(
                                           GuiConst_INT8U GraphIndex,
                                           GuiConst_INT8U DataSetIndex,
                                           GuiConst_INT8U XAxisIndex,
                                           GuiConst_INT8U YAxisIndex,
                                           GuiLib_GraphDataPoint *DataPtr,
                                           GuiConst_INT16U DataSize,
                                           GuiConst_INT16U DataCount,
                                           GuiConst_INT16U DataFirst);
                                           
        We supply either default values for these parameters,
        or get them from the GuiLibGraphDataSet object.
    */    
    
    if(dataSetIndex < DATASET_INDEX_COUNT) {
        // Make sure this data set, on this graph, has only the data we are about to 'add' to it -
        // get rid of any existing data
        GuiLib_Graph_RemoveDataSet(GuiLib_GraphIndex, dataSetIndex);
    
        if(dataSetDataPtr[dataSetIndex] != NULL) {
            delete [] dataSetDataPtr[dataSetIndex];
        }
    
        GuiConst_INT16U dataSize;
        GuiLib_GraphDataPoint* dataCopy = dataSet->GetGraphDataPointCopy(timeUnit, &dataSize); 
        
        if(dataCopy != NULL) {
            // Although the data is passed to us in an array allocated by the GuiLibGraphDataSet object,
            // we keep a pointer to it in our own array, so that we can delete it when it is no longer required.
            dataSetDataPtr[dataSetIndex] = dataCopy;
            
            // I think this easyGUI function has a misleading name - you cannot 'add' a dataset with this function - 
            // you can only specify the data for a dataset that already exists
            return GuiLib_Graph_AddDataSet(GuiLib_GraphIndex, dataSetIndex, 0, 0, dataSetDataPtr[dataSetIndex], dataSize, dataSize, 0);
        }
        // 'else' copy failed - drop through to error return
    }
    
    // 'else' dataset index out of range, or copy failed (see above)
    return 0;
}


/*
    Sets the data for the specified data set, using tenths of minutes as the X axis unit
    
    Args: the dataset index (this must already have been created in easyGUI)
          (a pointer to) a GuiLibGraphDataSet object containing the new data
          
    Note that we reject dataset index values outside the range 0-9
    (since we need to keep a pointer to the memory used, and currently 
     we only allow a fixed number of them).
          
*/
GuiConst_INT8U GuiLibGraph::SetDataForGraphDataSetInTenthsOfMinutes(GuiConst_INT8U dataSetIndex, float yAxisScaleFactor, GuiLibGraphDataSet* dataSet)
{
    /*
        This is a copy of the declaration of 'GuiLib_Graph_AddDataSet' in GuiLib.h:
        
        extern GuiConst_INT8U GuiLib_Graph_AddDataSet(
                                           GuiConst_INT8U GraphIndex,
                                           GuiConst_INT8U DataSetIndex,
                                           GuiConst_INT8U XAxisIndex,
                                           GuiConst_INT8U YAxisIndex,
                                           GuiLib_GraphDataPoint *DataPtr,
                                           GuiConst_INT16U DataSize,
                                           GuiConst_INT16U DataCount,
                                           GuiConst_INT16U DataFirst);
                                           
        We supply either default values for these parameters,
        or get them from the GuiLibGraphDataSet object.
    */    
    
    if(dataSetIndex < DATASET_INDEX_COUNT) {
        // Make sure this data set, on this graph, has only the data we are about to 'add' to it -
        // get rid of any existing data
        GuiLib_Graph_RemoveDataSet(GuiLib_GraphIndex, dataSetIndex);
    
        if(dataSetDataPtr[dataSetIndex] != NULL) {
            delete [] dataSetDataPtr[dataSetIndex];
        }
    
        GuiConst_INT16U dataSize;
        GuiLib_GraphDataPoint* dataCopy = dataSet->GetGraphDataPointCopyInTenthsOfMinutes(yAxisScaleFactor, &dataSize); 
        
        if(dataCopy != NULL) {
            // Although the data is passed to us in an array allocated by the GuiLibGraphDataSet object,
            // we keep a pointer to it in our own array, so that we can delete it when it is no longer required.
            dataSetDataPtr[dataSetIndex] = dataCopy;
            
            // I think this easyGUI function has a misleading name - you cannot 'add' a dataset with this function - 
            // you can only specify the data for a dataset that already exists
            return GuiLib_Graph_AddDataSet(GuiLib_GraphIndex, dataSetIndex, 0, 0, dataSetDataPtr[dataSetIndex], dataSize, dataSize, 0);
        }
        // 'else' copy failed - drop through to error return
    }
    
    // 'else' dataset index out of range, or copy failed (see above)
    return 0;
}


/*
    Sets the specified data set to consist of a single point.
    
    Args: the dataset index (this must already have been created in easyGUI)
          the X coordinate of the point
          the Y coordinate of the point
*/
GuiConst_INT8U GuiLibGraph::SetSinglePointForGraphDataSet(GuiConst_INT8U dataSetIndex, GuiConst_INT32S X, GuiConst_INT32S Y)
{
//    return GuiLib_Graph_AddDataPoint(GuiLib_GraphIndex, dataSetIndex, X, Y);

/*
        This is a copy of the declaration of 'GuiLib_Graph_AddDataSet' in GuiLib.h:
        
        extern GuiConst_INT8U GuiLib_Graph_AddDataSet(
                                           GuiConst_INT8U GraphIndex,
                                           GuiConst_INT8U DataSetIndex,
                                           GuiConst_INT8U XAxisIndex,
                                           GuiConst_INT8U YAxisIndex,
                                           GuiLib_GraphDataPoint *DataPtr,
                                           GuiConst_INT16U DataSize,
                                           GuiConst_INT16U DataCount,
                                           GuiConst_INT16U DataFirst);
                                           
        We supply either default values for these parameters,
        or get them from the values passed to us.
*/
    GuiLib_GraphDataPoint dataPoint;
    dataPoint.X = X;
    dataPoint.Y = Y;

    // I think this easyGUI function has a misleading name - you cannot 'add' a dataset with this function - 
    // you can only specify the data for a dataset that already exists
    return GuiLib_Graph_AddDataSet(GuiLib_GraphIndex, dataSetIndex, 0, 0, &dataPoint, 10, 1, 0);
}

/*
    Redraws this graph (by calling the easyGUI GuiLib_Graph_Redraw function).
    This redraws the entire graph, including background, axes and data sets.
*/
GuiConst_INT8U GuiLibGraph::Redraw(void)
{
    return GuiLib_Graph_Redraw(GuiLib_GraphIndex);
}