Making a plotting library in C


I recently found myself building some simple numerical models in C for a graduate class I'm taking. The assignment was simple code-wise and I figured that it would be a nice excuse to write some C code, something which I've not done in many months. Building the model went well, but when it came time to make the plots I needed for the assignment, I found myself in a small predicament.

If I'd built this with python I could just add import matplotlib.pyplot as plt at the top of the file and then use plt.subplots() and so on to create a figure whenever I needed one. I briefly considered trying to invoke matplotlib from C, but as I was on a deadline, I opted to just export the output to a CSV to plot with matplotlib "offline."

But the idea of making a re-useable codebase with which I could create matplotlib-style plots from C stuck with me.

In theory, a plotting library has two main components: the part which accepts data and other user input and builds it into paths, and the part which converts those paths (and other stuff, like axes) into graphics objects. I rarely use anything other than SVG files when doing homework or research, since I like having artifact-free resizing, so I decided that my library would only export SVGs. This significantly simplifies the latter part of the graphics library.

Exporting SVG files is pretty straightforward. They begin with some versioning/metadata, which is, practically speaking, always the same. Then there is the <svg> tag to begin the SVG. From there its a matter of adding <path>, <line>, and <rect> tags, and finishing the file with a </svg> tag. While fancy libraries like matplotlib encode glpyhs as paths and then place the paths using some elegant internal-linking features, I just used <text> objects. They're less easy to place since I don't know how large the text they contain will be, but they make for a simple and practical solution. At some point, I'll add a dependency on Freetype2 so that I can add convert glyphs to paths.


<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
    "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="640.00px" 
    height="480.00px" xmlns="http://www.w3.org/2000/svg" version="1.1">
    <!-- ... >
</svg>

There is some nuance to placing the text since I don't know the exact size when generating the file, so I use some SVG attribute tricks when writing them:


static void cg_write_svg_text(FILE* f, cg_text_t* text) {
    if (text->rotation != 0) {
        fprintf(f, "%s\n",
            text->x, text->y, text->fsize, text->x_anchor, -text->rotation,
            text->x, text->y, text->text);
    } else {
        fprintf(f, "%s\n", text->x, text->y, text->fsize,
            text->x_anchor, text->color, text->text);
    }
}

This code is pretty ugly, but it works well enough.

The interface part of the plotting library has turned out to be the more involved part. I've spent a lot more time on the axis formatting then I'd like to admit, but more on that later. For now, the data structure I'm using is pretty simple. The figure object (a struct called cg_figure_t contains information about axis bounds and scaling (log or linear), as well as axis titles.


typedef struct cg_fig_t {
    // figure information
    char *title, *xlabel, *ylabel;
    bool is_log_x, is_log_y;
    float xmin, xmax, ymin, ymax;
    char legend;

    // data information
    size_t n_series;
    series_t* head;
} cg_fig_t;

Additionally, it contains a linked list (I know, I know -- they're pretty bad -- I just wanted an excuse to implement one) which contains information about all the series that need to be added to that figure.


typedef struct series_t {
    // structural
    struct series_t* prev;
    struct series_t* next;

    // data
    size_t n_points;
    float *xs, *ys;
    char *label;
    bool is_owned;
} series_t;

The is_owned member is important for memory mangement; series can be added by passing a pointer to the data (which will get modified) or can be passed by pointer and copied (which prevents mutation). In the latter case, we need to remember to free the copied arrays to prevent a memory leak. Axis bounds are updated when series are added, but no other work is done until the user tries to save the figure to disk. While I do want to have some sort of interactive mode (a-la plt.show() matplotlib functionality), thats far too involved for me at the moment.

When the user calls cg_vector_file (saves to SVG) or cg_raster_file (saves to PNG/BMP, not implemented yet), the library converts the figure object into a collection of graphics primitives (lines, text objects, paths, and rectangles).


void cg_vector_file(cgraph_t* cgraph, cg_fig_t* fig, const char* path) {
    // create the composite object
    cg_composite_t* com;
    cg_init_composite(&com);
    
    // build composite from figure
    cg_composite(fig, com);

    // write composite to figure
    cg_write_svg(com, fig, path);

    cg_free_composite(&com);
}

This is where the real complexity, and real fun, begins. Creating and placing the axis labels and title are straight-forward, but making the axes and their ticks and tick labels is where the challenge begins. Generally, people want the ticks to be spaced in "easy" intervals (ones, halves, fives, etc), and want a "reasonable" quantity of ticks. The rules for this change slightly for log-scaled axes, which also changes how large the tick labels are when placed into the figure. I've placed an example of this below -- making the labels appear in an elegant way has been a challenge which I'm still struggling with.

From the preview its clear to see that there are some problems with the utility, mainly related to the axis placement. The legend also has some problems too: its not opaque and so it can be hard to read under some conditions. This is something I plan on addressing, along with support for bar charts, scatter plots, and combinations thereof. The code for making the plot is (I think) quite elegant:


#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <math.h>
#include "cgraph.h"

#ifndef M_PI
#define M_PI 3.1415926535f
#endif

int main_basic() {
    // setup cgraph instance
    cgraph_t* cgraph;
    cg_init(&cgraph);

    // create a figure
    cg_fig_t* fig;
    cg_make_figure(cgraph, &fig, 640.0, 480.0, false, false);
    
    // make a sinusoid
    size_t n = 1000;
    float* xs = (float*) malloc(n * sizeof(float));
    float* ys = (float*) malloc(n * sizeof(float));
    for (size_t i = 0; i < n; i++) {
        xs[i] = M_PI * i / 250.0f;
        ys[i] = sin(xs[i]);// * 0.01;
    }

    // plot sinusoid
    cg_plot_series(cgraph, fig, n, xs, ys, "sin(x)");
    
    size_t m = 9;
    float us[] = {0, 1.3, 4.2, 5.1, 7.0, 8.0, 11.2, 11.3, 12.4};
    float vs[] = {-.3, -.2, -.9, 0.4, 0.1, 0.5, 0.9, -0.2, 0.3};

    // plot the series
    cg_plot_series_cp(cgraph, fig, m, us, vs, "f(x)");
    
    // labels and title
    cg_set_xlabel(cgraph, fig, "X Axis");
    cg_set_ylabel(cgraph, fig, "Y Axis");
    cg_set_title(cgraph, fig, "Axis Title Goes Here");

    // add legend
    cg_set_legend(cgraph, fig, "top right");

    // save to svg
    cg_vector_file(cgraph, fig, "test_basic.svg");

    // clean up figure
    cg_free_figure(cgraph, &fig);

    // clean up cgraph instance
    cg_free(&cgraph);

    return 0;
}

Overall, the library has been somewhat useful -- while I don't want to use it for final versions of figures, I've been enjoying working with it for debugging purposes, and its been remarkably stable so far. At some point, I might make it publically available; I've been developing it on a self-hosted git setup instead of GitHub but I'm open to moving it there eventually if people want to use it.

Stay tuned for updates once I get the axis tick placement and labelling code working reliably.

/*
Create and initialize a cgraph instance.
*/
void cg_init(cgraph_t** cgraph);

/*
Destroy and free a cgraph instance.
*/
void cg_free(cgraph_t** cgraph);

/*
Create a new figure with a given size (in pixels) and properties.
*/
void cg_make_figure(cgraph_t* cgraph, cg_fig_t** fig,
    unsigned int w, unsigned int h, bool is_log_x, bool is_log_y);
void cg_make_figure_ext(cgraph_t* cgraph, cg_fig_t** fig, bool is_log_x,
    bool is_log_y, const char* xlabel, const char* ylabel,
    const char* title, const char* legend);

/*
Destroy a figure.
*/
void cg_free_figure(cgraph_t* cgraph, cg_fig_t** fig);

/*
Add a series to a figure. `_cp` variant copies data.
*/
void cg_plot_series(cgraph_t* cgraph, cg_fig_t* fig, size_t n,
    float* xs, float* ys, const char* label);
void cg_plot_series_cp(cgraph_t* cgraph, cg_fig_t* fig, size_t n,
    float* xs, float* ys, const char* label);

/*
Set axis limits.
*/
void cg_set_xlim(cgraph_t* cgraph, cg_fig_t* fig, float min, float max);
void cg_set_ylim(cgraph_t* cgraph, cg_fig_t* fig, float min, float max);

/*
Flip axis limits
*/
void cg_flip_x(cgraph_t* cgraph, cg_fig_t* fig);
void cg_flip_y(cgraph_t* cgraph, cg_fig_t* fig);

/*
Setting labels
*/
void cg_set_xlabel(cgraph_t* cgraph, cg_fig_t* fig, const char* label);
void cg_set_ylabel(cgraph_t* cgraph, cg_fig_t* fig, const char* label);
void cg_set_title(cgraph_t* cgraph, cg_fig_t* fig, const char* title);

/*
For configuring the legend. Align permits 5 values:
 - NULL, "top right", "top left", "bottom right", "bottom left"
*/
void cg_set_legend(cgraph_t* cgraph, cg_fig_t* fig, const char* align);

/*
Finalizing plots
*/
void cg_raster_file(cgraph_t* cgraph, cg_fig_t* fig, const char* path);
void cg_vector_file(cgraph_t* cgraph, cg_fig_t* fig, const char* path);
void cg_show(cgraph_t* cgraph, cg_fig_t* fig);