September 25, 2008

Easily displaying two-dimensional data with GtkDatabox

Author: Ben Martin

Many applications need to graphically display the relation between two data axes. Common examples are how one resource such as CPU load or an exchange rate varies over time. GtkDatabox makes presenting such information in a GTK+ desktop application much simpler.

Version 0.8.2.2 is packaged for Fedora 9, while version 0.7.0.x is available for both Ubuntu Hardy and as a 1-Click install for openSUSE. I'll build from source using the latest version 0.9.0.1 on a 64-bit Fedora 9 machine.

GtkDatabox includes support for the Glade user interface designer. You can choose to enable support for version 2.x or version 3.x of Glade when building GtkDatabox. You must specifically enable Glade support using configure parameters if you want to build it. To get the version of Glade support that you want, you may wish to build GtkDatabox from source even if there is a package supplied for your distribution.

$ tar xzf /.../gtkdatabox-0.9.0.1.tar.gz
$ cd ./gtkdatabox-*/
$ ./configure --help|grep glade
--enable-libglade enable libglade support
--enable-glade enable glade-3 support
$ ./configure --enable-libglade --enable-glade

...
config.status: creating config.h
config.status: executing depfiles commands

Install libglade-2.0 module: yes
Install glade-3 module: yes

$ make
$ sudo make install

The GtkDatabox widget includes support for rectangular selection and zooming. The programs in examples/basics and examples/basics2 will give you an idea of how the selection and zooming works. Basically, you draw a box around something to select it. To cancel the selection, click outside of the selection box. You can click in the selection box to zoom in and show only the area in the selection. The right mouse button zooms out a little bit; when you click the right button with the Shift key pressed, the view is returned to the default zoom.

The examples directory provides good insight into how the GtkDatabox API is intended to be used. Shown below is the basics2 example from the GtkDatabox tarball. Other examples include usage with Glade, and monitoring signals so that the application can see where the mouse is using the coordinates of the data that is being viewed by GtkDatabox rather than the coordinates the X Window System reports for the mouse position.

When using the GtkDatabox widget, the first objects of interest are subclasses of GtkDataboxGraph. Unfortunately the API reference documentation is not shown on the GtkDatabox Web site; you have to open the documentation from docs/reference/html in the expanded GtkDatabox sources. You cannot use the GtkDataboxGraph class directly, but there are subclasses for displaying a background grid (GtkDataboxGrid) and the relation between two axes (GtkDataboxXYCGraph). For the latter, you can choose to show only your data points (GtkDataboxPoints), have your data points connected with lines (GtkDataboxLines), have your data points presented as a bar chart (GtkDataboxBars), or simply place markers at specific offsets in the graph (GtkDataboxMarkers).

An example of using GtkDatabox is shown below. It includes the header files, then declares two GtkLabel objects. You would probably hand a pointer to these labels to the show_motion_notify_cb in a real application to avoid having them as globals. The show_motion_notify_cb callback function is called by GtkDatabox whenever the mouse moves over the GtkDatabox widget. I use xlabel and ylabel to display the mouse location to the user in this callback.

The first five or so lines of main are boilerplate GTK+ code to create a window and set it up. The main GUI is created inside the vbox vertical box widget. Four labels are created at the top of the display inside a single GtkTable. This takes us down to the gtk_databox_create_box_with_scrollbars_and_rulers call, which not only creates the GtkDatabox object but also places it into the container databoxcontainer for you, allowing the scrollbars and other decorations to be created automatically.

I then define five points using the grlen, x, and y definitions and pass them to gtk_databox_lines_new to create a new graph in the GtkDatabox displaying these points connected by lines. The graph_add call on the next line actually adds the graph object to the GtkDatabox. The motion_notify_event signal on the GtkDatabox is then connected to the show_motion_notify_cb defined at the top of the example and some normal widget packing is performed.

Notice the two functions just before the gtk_widget_show_all function call. You can either nominate where you would like the bounds of the display to be explicitly, by using the gtk_databox_set_total_limits function, or let GtkDatabox figure out how to display all your graphs by calling gtk_databox_auto_rescale.

The last parameter to auto_rescale lets you define a border as a relative percentage of the actual data area. This way you can have a little bit of space around your graph data but still let GtkDatabox automatically decide how to scale the data for the display.

#include <stdio.h>

#include <gtk/gtk.h>
#include <gtkdatabox.h>
#include <gtkdatabox_points.h>
#include <gtkdatabox_ruler.h>
#include <math.h>

GtkWidget *xlabel = 0;
GtkWidget *ylabel = 0;

static gint
show_motion_notify_cb (GtkWidget ** entries,
GdkEventMotion * event,
GtkWidget *widget )
{
gfloat x, y;
gchar *text;
GtkDatabox *box = GTK_DATABOX (widget);

x = gtk_databox_pixel_to_value_x (box, event->x);
y = gtk_databox_pixel_to_value_y (box, event->y);

const int bufsz = 100;
char buf[bufsz+1];
snprintf( buf, bufsz, "%g", x );
gtk_label_set_text( GTK_LABEL(xlabel), buf );
snprintf( buf, bufsz, "%g", y );
gtk_label_set_text( GTK_LABEL(ylabel), buf );
}

gint
main (gint argc, char *argv[])
{
gtk_init (&argc, &argv);

GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_widget_set_size_request (window, 500, 500);
g_signal_connect (GTK_OBJECT (window), "destroy",
G_CALLBACK (gtk_main_quit), NULL);
gtk_window_set_title (GTK_WINDOW (window), "GtkDatabox on linux.com");

GtkWidget *vbox = gtk_vbox_new (FALSE, 0);
gtk_container_add (GTK_CONTAINER (window), vbox);

GtkWidget* table = gtk_table_new ( 1, 2, 0 );
GtkWidget *label = 0;
int c = 0, r = 0;

gtk_table_attach(GTK_TABLE(table), gtk_label_new("X:"),
c, c+1, r, r+1,
GTK_FILL, 0, 0, 0 );
xlabel = gtk_label_new("");
c++;
gtk_table_attach(GTK_TABLE(table), xlabel,
c, c+1, r, r+1,
GTK_FILL, 0, 0, 0 );

r++; c = 0;
gtk_table_attach(GTK_TABLE(table), gtk_label_new("Y:"),
c, c+1, r, r+1,
GTK_FILL, 0, 0, 0 );
ylabel = gtk_label_new("");
c++;
gtk_table_attach(GTK_TABLE(table), ylabel,
c, c+1, r, r+1,
GTK_FILL, 0, 0, 0 );

GtkWidget* databox = 0;
GtkWidget* databoxcontainer = 0;
gtk_databox_create_box_with_scrollbars_and_rulers (
&databox, &databoxcontainer,
TRUE, TRUE, TRUE, TRUE);

const int grlen = 5;
gfloat x[] = { 1 , 2, 3, 4, 5, 0 };
gfloat y[] = { 4 , 5, 7.5, 11, 4.3, 0 };
GdkColor markerColor = { 0, 65000, 0, 0 };
GtkDataboxGraph* gr = gtk_databox_lines_new( grlen, x, y, &markerColor, 1 );
gtk_databox_graph_add (GTK_DATABOX (databox), gr);

g_signal_connect_swapped (G_OBJECT (databox),
"motion_notify_event",
G_CALLBACK (show_motion_notify_cb), 0 );

gtk_box_pack_start(GTK_BOX(vbox), table, 0, 0, 0 );
gtk_box_pack_start(GTK_BOX(vbox), databoxcontainer, 1, 1, 0 );

// gtk_databox_set_total_limits (GTK_DATABOX (databox), 0., 5.5, 12, 4 );
gtk_databox_auto_rescale (GTK_DATABOX (databox), 0.05);

gtk_widget_show_all (window);

gtk_main ();
return 0;
}

To change the graph you have to remove the existing one and replace it with a new graph object, as shown below. The modify_data_cb will be called two seconds after the application is displayed. A call to auto_rescale is wise to make sure that the new data is fully visible. A word of warning here: the data points are not copied by GtkDatabox, so you should either allocate them dynamically and keep them around for the lifetime of the GtkDatabox yourself, or, as in this case, use static buffers to ensure a lifetime longer than the GtkDatabox. If you don't make the data points hang around, GtkDatabox will likely change the size of the display but most likely silently fail to actually paint your new lines.

gint
modify_data_cb(gpointer data)
{
const int grlen = 5;
static gfloat x[] = { 1 , 2, 3, 4, 5, 0 };
static gfloat y[] = { 4 , 25, 7.5, 11, 4.3, 0 };
static GdkColor markerColor = { 0, 0, 0, 65000 };

gtk_databox_graph_remove( databox, gr );

gr = gtk_databox_lines_new( grlen, x, y, &markerColor, 1 );
gtk_databox_graph_add (GTK_DATABOX (databox), gr);

gtk_databox_auto_rescale (GTK_DATABOX (databox), 0.05);

}
...
int interval = 2000;
gtk_timeout_add( interval, modify_data_cb, databox );

gtk_main ();
return 0;
}

If you have time series data or another relations between two variables that you wish to present in a GTK+ application, Gtkdatabox might be just what you are looking for -- unless you need to edit the nodes of the graph by dragging the nodes around, in which case you should consider using another widget instead. You can of course pragmatically modify your graph with GtkDatabox by copying an existing one, modifying it, deleting the original, and adding a new graph.

Categories:

  • Programming
  • Graphics & Multimedia
Click Here!