GraphVisual: 2D graphs
#include <morph/GraphVisual.h>
Table of contents
- Overview
Overview
morph::GraphVisual
is a class that draws 2D graphs.
A short example of a program that draws a graph with GraphVisual
is (omitting the #include
lines for <morph/Visual.h>
, <morph/GraphVisual.h>
and <morph/vvec.h>
):
int main() {
morph::vvec<double> x = { -7,-6,-5,-4,-3,-2,-1,-0.5, 0, 0.5, 1, 2, 3, 4, 5, 6, 7 }; // data
morph::Visual v(1024, 768, "Made with morph::GraphVisual"); // Visual scene/window
// 5 lines of C++ code to create a 2D Graph
auto gv = std::make_unique<morph::GraphVisual<double>> (morph::vec<float>({0,0,0}));
v.bindmodel (gv);
gv->setdata (x, x.pow(3));
gv->finalize();
v.addVisualModel (gv);
v.keepOpen();
}
As usual, we create the VisualModel
with a call to std::make_unique<>
, and then call the boilerplate bindmodel
function to wire up some callbacks.
In this example, there’s just one line setting up the GraphVisual
: gv->setdata (x, x.pow(3));
. This passes data into the GraphVisual instance. The first argument to setdata copies the values in x
to the abscissa (x axis) of the graph and x.pow(3)
is copied to the ordinate (y axis).
The GraphVisual is then finalized (creating OpenGL vertices and indices) and added to the Visual
scene giving this result:
The screenshot shows a graph of the function, which has the default dimensions of 1x1 units in model space. GraphVisual has auto-scaled the data into model units so that it fits inside the axes of the graph and it has automatically determined suitable tick values to show. It has used the default font (VisualFont::DVSans) and font size (0.05) for the axis labels and tick labels. The data markers have the default colour of morph::colour::royalblue
and lines of a default thickness (0.03) are drawn between the markers in black.
The rest of this page describes how you can choose different defaults do draw a variety of differently styled graphs.
Setting data
The first example showed only a single dataset in the graph. The call to setdata
omitted to specify a dataset index, and because this was the first dataset, its index was set automatically to 0. You can add multiple datasets to a GraphVisual. Each call to setdata will increment the dataset index. We could add a second dataset to the previous example:
// ...
gv->setdata (x, x.pow(3), "x cubed"); // dataset index 0
gv->setdata (x, 5.0 * x.pow(2), "5 times x squared"); // dataset index 1
gv->finalize();
// etc...
The result is shown in the left of these two graphs:
Notice that we gave a third argument to setdata
which defines the dataset’s datalabel. This is automatically displayed in a legend above the graph axes. With the two setdata calls, the GraphVisual now displays two sets of data points, giving the second dataset a new marker shape (‘uptriangle’) and colour. The scaling from the first set of data points is applied to second and subsequent datasets. To illustrate, look at the right hand graph. This also has two datasets, with the second dataset now showing 10x2. Some of the elements of this dataset extend beyond the boundary of the axes and are not drawn. If you are allowing GraphVisual to autoscale the data, remember that it is the first dataset for which the scaling is computed. To work around this, you can manually define the limits of your axes (see Controlling the data limits).
Setting data with a DatasetStyle
Each dataset in a GraphVisual has an associated morph::DatasetStyle
. This defines how the dataset should be drawn on the graph. It allows you to choose whether data is represented by markers, lines or both. It lets you set the line width and colour and the marker shape, size and colour.
When you make a call to setdata(x, y)
or setdata(x, y, "datalabel")
, a default DatasetStyle is created and then the setdata (x, y, DatasetStyle)
overload is called. For manual control over the style of your data, simply create a DatasetStyle before you call setdata
as in this example:
morph::DatasetStyle ds; // Equivalent: morph::DatasetStyle ds (morph::stylepolicy::both);
ds.datalabel = "x cubed";
ds.markercolour = morph::colour::springgreen3;
ds.markersize = ds.markersize * 1.5;
ds.markerstyle = morph::markerstyle::pentagon;
gv->setdata (x, x.pow(3), ds);
Create a DatasetStyle
instance, ds
, then change some of its members. Here, we set the datalabel, changed the colour for the markers and increased the marker size. Here’s the resulting graph:
You can modify the DatasetStyle
object and use it for later setdata
calls. It need not stay in scope after setdata
returns.
You can find DatasetStyle in the header morph/DatasetStyle.h.
The public member attributes of morph::DatasetStyle
that you can modify are:
morph::stylepolicy stylepolicy
Should we plot markers, lines or both? Options aremarkers
,lines
,both
(coloured markers, black lines)allcolour
(coloured markers and lines) orbar
(bar graph). The default isboth
; if you want an alternative, pass it to the DatasetStyle constructor.float linewidth
Thickness of lines in model units. Default is 0.007.std::array<float, 3> linecolour
The colour of the dataset line. RGB values in range [0,1].float markersize
Diameter of the corners of the marker shape. Default is 0.03.float markergap
A space between the markers and the lines. Default is 0.03.morph::markerstyle markerstyle
The shape of the data markers.square
,triangle
,hexagon
, etc. Options in graphstyles.h.std::array<float, 3> markercolour
The colour of the datum markers. RGB values in range [0,1].
Line graph examples
To make a line graph, create a DatasetStyle with stylepolicy::lines
passed to the constructor. Set the linewidth
and linecolour
to your liking:
// Left hand graph
morph::DatasetStyle ds(morph::stylepolicy::lines); // initialize for line graphs
ds.datalabel = "x cubed";
ds.linewidth = 0.014f;
gv->setdata (x, x.pow(3), ds); // line will be the default colour of black
// Right hand graph. Note we re-use ds, simply changing some of its parameters
ds.datalabel = "5 times x squared";
ds.linewidth = 0.0035f;
ds.linecolour = morph::colour::crimson;
gv2->setdata (x, 5.0 * x.pow(2), ds);
The lines are drawn made up from straight line segments between the data points (any curve fitting is left for the client code to carry out).
Marker-only graph examples
Create a DatasetStyle
with morph::stylepolicy::markers
and then change the defaults for markersize
, markerstyle
and markercolour
.
// Left hand graph
morph::DatasetStyle ds(morph::stylepolicy::markers); // initialize for markers only
ds.datalabel = "x cubed";
ds.markersize = 0.06f;
ds.markerstyle = morph::markerstyle::diamond;
ds.markercolour = morph::colour::purple2;
gv->setdata (x, x.pow(3), ds); // line will be the default colour of black
// Right hand graph (re-using ds)
ds.datalabel = "5 times x squared";
ds.markersize = 0.014f;
ds.markerstyle = morph::markerstyle::uphexagon;
ds.markercolour = morph::colour::magenta;
gv2->setdata (x, 5.0 * x.pow(2), ds);
When you choose morph::stylepolicy::markers
, your datasets are plotted with a shaped marker at each datum. The left hand example has purple squares, the right had has magenta hexagons.
Marker plus line graph examples
There are six options for a marker-and-line graph:
// Left hand graph
morph::DatasetStyle ds(morph::stylepolicy::both); // equivalent to: morph::DatasetStyle ds;
ds.datalabel = "x cubed";
ds.linewidth = 0.0035f;
ds.linecolour = morph::colour::purple2;
ds.markersize = 0.06f;
ds.markerstyle = morph::markerstyle::diamond;
ds.markercolour = morph::colour::magenta;
ds.markergap = 0.04;
gv->setdata (x, x.pow(3), ds);
// Right hand graph (re-using ds)
ds.datalabel = "5 times x squared";
ds.linewidth = 0.012f;
ds.linecolour = morph::colour::navy;
ds.markersize = 0.014f;
ds.markerstyle = morph::markerstyle::uphexagon;
ds.markercolour = morph::colour::dodgerblue;
ds.markergap = 0.02;
gv2->setdata (x, 5.0 * x.pow(2), ds);
Unicode for special characters
You can incorporate unicode characters into your DatasetStyle
datalabels and axis labels. Use morph::unicode::toUtf8()
to generate a UTF-8 string, as documented in Unicode text handling. Any of the string
attributes that you can set can be passed a string containing UTF-8 character codes. For example:
using uc = morph::unicode; // I usually shorten morph::unicode for readable code
ds.datalabel = "x cubed is usually written as x" + uc::toUtf8 (uc::ss3); // ss3: superscript 3
gv->setdata (x, x.pow(3), ds);
Most of the codes are intuitively named;
unicode::alpha
, unicode::beta
, unicode::gamma
and so on. Consult the unicode header file unicode.h for the full list.
Appending or updating data
If you need to change all the data points in a graph, you’ll want to use the update
function.
Prepping a dataset
Your program may need to start with a graph that has an empty dataset and add to it with the append
method. In this case, you must first prepare the graphs with as many datasets as you will use.
The axes
You can choose from a few different axis styles. The default style is morph::axisstyle::box
, as shown in the previous examples. This draws a box around the graph, with ticks drawn on the x and y axes only. Other options include axisstyle::boxfullticks
, axisstyle::L
and axisstyle::cross
. Examples of these look like this:
The code to setup the top left graph starts like this:
auto gv = std::make_unique<morph::GraphVisual<float>>(morph::vec<float>({0,0,0}));
v.bindmodel (gv);
gv->axisstyle = morph::axisstyle::L; // That's all you have to do
// ...rest of the setup
If you want to change the axis ticks then it’s this:
//...
gv->axisstyle = morph::axisstyle::L;
gv->tickstyle = morph::tickstyle::ticksin; // tickstyle::ticksout is the default
// ...rest of the setup
You can alter the colour of the axes by changing axiscolour…
//...
gv->axisstyle = morph::axisstyle::L;
gv->tickstyle = morph::tickstyle::ticksin;
gv->axiscolour = morph::colour::cobalt;
// ...rest of the setup
… and the axes line thickness with gv->axislinewidth
:
//...
gv->axisstyle = morph::axisstyle::L;
gv->tickstyle = morph::tickstyle::ticksin;
gv->axiscolour = morph::colour::cobalt;
gv->axislinewidth = 0.013f;
// ...rest of the setup
Here are the four graphs again with thicker, coloured axes: Note that the axis colour is applied to the axis box/cross/L, the axis ticks, axis tick labels and axis labels.