Graph class in wxWidgets

Nick Evan 0 Tallied Votes 893 Views Share

Background

I was looking for a simple control in wxWidgets which I could use to plot out some value against time, but I found that apparently I was the only one that needed that kind of graph. (so I don't know why I'm posting it... but anyway).

I decided to create my own class. In the snippet below I choose the Y-axes in a range from 0-100 (% for example) and the x-axes in 3 different scales: 1 hour, 1 day and 2 weeks. This may or may not be useful in your case, so change it if you need something else :)

How to use it

De neGraph class inherits from "wxScrolledWindow", so you'll need two things:
- a 'MyApp' (is always required with wxWidgets)
- a 'MyFrame'. You can pass the pointer from this class to the neGraph class, to let it know who is it's owner.

So to draw the neGraph, you could add something like the following in the constructor from MyFrame:

/* Create a new graph with dimensions 750x500 */
m_plot = new neGraph( this,wxSize(750,500)); 
/* Set offset from wxCoord(0,0), so basically the position of the graph */
m_plot->SetOffSet(wxSize(0,0));
/* Set x-scale to '2' which is 1 hour*/
m_plot->SetScale(2);
/* Scroll up */
m_plot->Scroll(0,0);
/* Draw a grid */
m_plot->DrawGrid(true);


Adding data

The graph reads all it's data from this: std::vector< neLine > grid_data; . You can add up to a maximum of ten lines in one graph. For each neLine you add to the graph, a new line is drawn in a new color. The legend will also be automatically updated with the right color and line-name.

The neLine is constructed with a vector of coordinates and a name. These coordinates consist out of a time in minutes and a corresponding y-value. You will have to calculate the time in minutes yourself. For example: If you have a coordinate with minutes=1 and y = 2, the graph will draw the point one minute from the start of x scale.

Trivia:

The x-axes will automatically change the scale of the caption if the graph is resized.
I've added some small helper-function to the file that might come in usefull. You'll find them at the bottom of the file.

Header file (graph.h)

#include <vector>
#include "wx/wx.h"
#include "main.h" // for myFrame*

#define BUTTON_START        7000
#define COMBO_BOX_TIMESPAN  7001

#define C_HOUR       "1 hour"
#define C_DAY        "1 day"
#define C_WEEKS      "2 weeks"
#define STD_OFFSET   50

struct neCoord
{
    long minutes;
    double y;
};

class neLine
{
public:
    std::vector<neCoord> coords;
    neLine() {line_name = "<empty>";}
    neLine(std::vector<neCoord>,wxString name = "<empty>");
    ~neLine(){};
    void ClearItems(void);
    wxString line_name;
    void Add(neCoord in_coord){coords.push_back(in_coord);}
};

// define a scrollable canvas for drawing onto
class neGraph: public wxScrolledWindow
{
public:
    neGraph( MyFrame*);
    neGraph( MyFrame*, wxSize );
    ~neGraph();
    void SetOffSet(wxSize);
    void SetScale(int);
    int  GetScale(void) { return scale; }
    void SetStartDate(const wxChar*);
    void AddData(neLine in_data) {grid_data.push_back(in_data);}
    void ClearGraph(void);
    void SetGridSize(wxSize);
    void DrawGrid(bool in ) { grid = in; }
private:
    void InitGraph(wxInitDialogEvent &event);
    void OnPaint(wxPaintEvent &event);
    void OnStartClicked(wxCommandEvent &event);
    void OnTimeSelection(wxCommandEvent &event);
    void DrawAxes(wxDC& dc);
    void DrawControls(void);
    void FillAxes(wxDC& dc);
    void DrawData(wxDC& dc);
    wxDateTime start;
    int scale;
    bool grid;
    MyFrame *m_owner;
    wxSize g_size;
    wxSize req_g_size;
    wxSize offset;
    std::vector< neLine > grid_data;
    wxTextCtrl *t_inputd;
    wxTextCtrl *t_inputt;
    wxButton *b_go;
    wxComboBox *c_timespan;
    DECLARE_EVENT_TABLE();
};

void neBox(const wxString);
std::string IntToString(int Input, int add_a_zero = 0);

#endif

NOTE:

This code is only meant as a starting point to create your own graph. It's neither great, done, or fail-proof so don't come complaining if you don't like it ;). I just posted the code here because it took me a few hours to create and I want to save someone else the trouble.
Of course I'll be more then happy to give further information if it's required, so post comments!

#include <sstream>
#include <string>
#include "graph.h"
#include "wx/wx.h"

using std::vector;

neLine::neLine(std::vector<neCoord> in_vec,wxString name)
{
    coords = in_vec;
    line_name = name;
}

void neLine::ClearItems(void)
{
    for (unsigned int i = 0; i < coords.size(); i++)
        coords.pop_back();
}

BEGIN_EVENT_TABLE(neGraph, wxScrolledWindow)
    EVT_PAINT       (neGraph::OnPaint)
    EVT_INIT_DIALOG (neGraph::InitGraph)
    EVT_BUTTON      (BUTTON_START, neGraph::OnStartClicked)
    EVT_COMBOBOX    (COMBO_BOX_TIMESPAN, neGraph::OnTimeSelection)
END_EVENT_TABLE()

neGraph::neGraph(MyFrame *parent)
: wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
                   wxHSCROLL | wxVSCROLL | wxNO_FULL_REPAINT_ON_RESIZE)
{
    start = wxDateTime::Now();
    scale = 0;
    grid = false;
    g_size.x = 170;
    g_size.y = 220;
    req_g_size = wxSize(170,220);
    m_owner = parent;
    offset = wxSize(STD_OFFSET,STD_OFFSET);
    DrawControls();
}

neGraph::neGraph( MyFrame *parent, wxSize gridsize)
: wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
                   wxHSCROLL | wxVSCROLL | wxNO_FULL_REPAINT_ON_RESIZE)
{
    start = wxDateTime::Now();
    scale = 0;
    grid = false;
    m_owner = parent;
    offset = wxSize(STD_OFFSET,STD_OFFSET);
    req_g_size = gridsize;
    SetGridSize(gridsize);
    DrawControls();
}

neGraph::~neGraph()
{
    delete t_inputd;
    delete t_inputt;
    delete b_go;
    delete c_timespan;
}

void neGraph::SetGridSize(wxSize gridsize) 
{
    req_g_size = gridsize;
    /* First find a number of pixels that can be cut in equal parts */
    while (gridsize.y % 10) gridsize.y--;
    if (scale == 0) /* 2 weeks */
        while (gridsize.x % 14) gridsize.x--; 
    else  /* one day or 1 hour*/
        while (gridsize.x % 12) gridsize.x--; 

    g_size.x = gridsize.x;
    g_size.y = gridsize.y;
}

void neGraph::ClearGraph()
{
    for (unsigned int i = 0; i < grid_data.size(); i++)
        grid_data.clear();
}
void neGraph::SetStartDate(const wxChar* indate)
{
    start.ParseDate(indate);
}
void neGraph::SetOffSet(wxSize off)
{
    offset.x = off.x + STD_OFFSET;
    offset.y = off.y + STD_OFFSET;
}

/* - Set X-axes scale. 0=2 weeks, 1=1 day, 2=1 hour*/
void neGraph::SetScale(int sc)
{
    scale = sc;
    SetGridSize(req_g_size);
} 

void neGraph::DrawAxes(wxDC& dc)
{
    if (grid)
    {
        wxPen pen(wxColour(180,180,180), 1); 
        dc.SetPen(pen);
        for (int i = 0; i < g_size.x; i += 10)
            dc.DrawLine(offset.x+i, offset.y, offset.x+i, offset.y+g_size.y);
        for (int j = 0; j < g_size.y; j += 10)
            dc.DrawLine(offset.x, offset.y+j, offset.x+g_size.x, offset.y+j);
    }
    wxPen pen2(*wxBLACK, 2); 
    dc.SetPen(pen2);
    dc.DrawLine(offset.x, offset.y, offset.x, offset.y+g_size.y+5);
    dc.DrawLine(offset.x-5, offset.y+g_size.y, offset.x+g_size.x, offset.y+g_size.y);
    dc.DrawLine(offset.x-5, offset.y, offset.x+5, offset.y);
    dc.DrawLine(offset.x+g_size.x, offset.y+g_size.y-5, offset.x+g_size.x, offset.y+g_size.y+5);
    dc.SetPen(wxNullPen);
}

void neGraph::FillAxes(wxDC& dc)
{
    double step=0;
    wxDateTime datetime = start;
    // X-axes can have only 3 scales (2 weeks, 1 day or 1 hour)
    switch (scale)
    {
    case 0: // 2 weeks
        step = g_size.x / 14;
        for (int i = 0; i <= 14; i++)
        {
            std::string temp = IntToString(datetime.GetDay(),1) + "-" + IntToString(datetime.GetMonth()+1,1);
            dc.DrawLine(offset.x+(i*step), offset.y+g_size.y, offset.x+(i*step), offset.y+g_size.y+5); 
            if ((g_size.x > 500) || (g_size.x > 250 && g_size.x <=500 && !(i%2)) ||(g_size.x <= 250 && !(i%4)))
                dc.DrawText(temp , offset.x+(step*i)-20, offset.y+g_size.y+10);
            datetime.Add(wxTimeSpan::Day());
        }
        break;
    case 1: // 1 day
        step = g_size.x / 12;
        for (int i = 0; i <= 12; i++)
        {
            std::string temp = IntToString(datetime.GetHour(),1) + ":" + IntToString(datetime.GetMinute(),1);
            dc.DrawLine(offset.x+(i*step), offset.y+g_size.y, offset.x+(i*step), offset.y+g_size.y+5); 
            if ((g_size.x > 500) || (g_size.x > 250 && g_size.x <=500 && !(i%2)) ||(g_size.x <= 250 && !(i%4)))
                dc.DrawText(temp, offset.x+(step*i)-20, offset.y+g_size.y+10);
            datetime.Add(wxTimeSpan::Hours(2));
        }
        break;
    case 2: // 1 hour
        step = g_size.x / 12;
        for (int i = 0; i <= 12; i++)
        {
            std::string temp = IntToString(datetime.GetHour(),1) + ":" + IntToString(datetime.GetMinute(),1);
            dc.DrawLine(offset.x+(i*step), offset.y+g_size.y, offset.x+(i*step), offset.y+g_size.y+5); 
            if ((g_size.x > 500) || (g_size.x > 250 && g_size.x <=500 && !(i%2)) ||(g_size.x <= 250 && !(i%4)))
                dc.DrawText(temp, offset.x+(step*i)-20, offset.y+g_size.y+10);
            datetime.Add(wxTimeSpan::Minutes(5));
        }
        break;
    }

    step = g_size.y / 10;
    for (int i = 0; i <= 10; i ++)
    {
        dc.DrawLine(offset.x-5, offset.y+g_size.y-(i*step),offset.x,offset.y+g_size.y-(i*step));
        dc.DrawText(IntToString(i*10), offset.x-50, offset.y+g_size.y-(i*step)-10);
    }   
}

void neGraph::DrawData(wxDC& dc)
{
    wxColour colour_arr[10] = {*wxBLUE,*wxRED,*wxCYAN,*wxGREEN,*wxWHITE,*wxLIGHT_GREY,*wxBLUE,*wxRED,*wxCYAN,*wxGREEN};
    // calc how many minutes 
    double minute_step=0.0;
    switch(scale)
    {
    case 0:
        minute_step = (double)g_size.x / (14.0*24.0*60.0);
        break;
    case 1:
        minute_step = (double)g_size.x / (24.0*60.0);
        break;
    case 2:
        minute_step = (double)g_size.x / 60.0;
        break;
    }
    // max of 10 line per graph 
    for (unsigned int line = 0; line < grid_data.size() && line < 10; line++)
    {
        wxPen pen(colour_arr[line], 1); 
        dc.SetBrush(colour_arr[line]);
        dc.SetPen(pen);
        dc.DrawRoundedRectangle(offset.x+g_size.x, offset.y+(25*line), 30, 20,5.0);
        dc.FloodFill(offset.x+g_size.x+10, offset.y+(25*line)+10, *wxWHITE, wxFLOOD_SURFACE);
        dc.DrawText(grid_data[line].line_name,offset.x+g_size.x+40, offset.y+(25*line)); 

        for (unsigned int point = 0; point < grid_data[line].coords.size() -1; point++)
        {
            dc.DrawLine(
                offset.x+(grid_data[line].coords[point].minutes*minute_step),
                offset.y+g_size.y-((g_size.y/100)*grid_data[line].coords[point].y),
                offset.x+(grid_data[line].coords[point + 1].minutes*minute_step),
                offset.y+g_size.y-((g_size.y/100)*grid_data[line].coords[point + 1].y)
                );
        }
    }   
}

void neGraph::DrawControls()
{
    t_inputd = new wxTextCtrl(this, -1, "dd-mm-yyyy", wxPoint(20,0));
    t_inputt = new wxTextCtrl(this, -1, "hh:mm:ss", wxPoint(t_inputd->GetPosition().x+t_inputd->GetSize().x + 10,0));
    c_timespan = new wxComboBox(this, COMBO_BOX_TIMESPAN,C_HOUR, wxPoint(t_inputt->GetPosition().x+t_inputt->GetSize().x + 10, 0));
    c_timespan->AppendString(C_HOUR);
    c_timespan->AppendString(C_DAY);
    c_timespan->AppendString(C_WEEKS);
    b_go = new wxButton(this, BUTTON_START , "Start",wxPoint(c_timespan->GetPosition().x+c_timespan->GetSize().x + 10, 0));     
}


void neGraph::OnPaint(wxPaintEvent &WXUNUSED(event) )
{      
    wxPaintDC dc(this);
    PrepareDC(dc);
    dc.Clear();
    DrawAxes(dc);
    FillAxes(dc);
    DrawData(dc);
}

void neGraph::InitGraph(wxInitDialogEvent &WXUNUSED(event))
{
}

void neGraph::OnStartClicked(wxCommandEvent &WXUNUSED(event))
{
    wxDateTime in_time;
    wxString dt = t_inputd->GetValue() + " " + t_inputt->GetValue();
    if (!in_time.ParseDateTime(dt))
    {
        neBox("Error in input!");
        t_inputt->SetValue("hh:mm:ss");
        t_inputd->SetValue("dd-mm-yyyy");
        return;
    }
    start = in_time;
    ClearGraph();
    /*****************************/
    /* TODO: Reload data here !! */
    /*****************************/
    this->Refresh();
}

void neGraph::OnTimeSelection(wxCommandEvent &WXUNUSED(event))
{
    if (c_timespan->GetValue() == C_DAY)
        SetScale(1);
    else if (c_timespan->GetValue() == C_WEEKS)
        SetScale(0);
    else SetScale(2);
    ClearGraph();
    /*****************************/
    /* TODO: Reload data here !! */
    /*****************************/
    this->Refresh();
}

/* some common functions */
void neBox(const wxString input)
{
    wxMessageDialog* temp =  new wxMessageDialog(NULL,input);
    temp->ShowModal();
    delete temp;
    return; 
}

std::string IntToString(int Input, int add_a_zero)
{
    std::stringstream temp;
    std::string str;
	temp << Input;
	temp >> str;
    if (add_a_zero && Input < 10) str = "0" + str;
	return str;
}
callmegood 0 Newbie Poster

Woots. Haven't gone through the codes, but seems like you've got something I can use =D

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.