Introduction to Efficient Creation of Detailed Plots

Introduction

A few weeks ago, we showed you how to create a detailed plot from a recent article in the American Economic Review. That article contained several plots that contain quite a bit of similar and stylized formatting. Today we will show you how to efficiently create two of these graphs.

Our main goals are to get you thinking about code reuse and how it can help you:

  • Get more results from your limited research time.
  • Avoid the frustration that comes from growing mountains of spaghetti code.

Our Graphs

This is what we will create today. As you can see they share many style attributes. This gives us a great opportunity to reuse code.

Line plots from an American Economic Review article created by GAUSS.

You can download the data here.

Our Initial Code

This is not a massive amount of code and many of you might be tempted to just copy and paste this code and make the minor modifications needed to get your desired result. I completely understand. Your biggest problem is probably a lack of time, so productivity is paramount.

While it might feel like this is a shortcut, it will saddle you with technical debt. Technical debt is just a fancy term that describes the stress, frustration, and time-wasting that inevitably occurs when you take shortcuts like this.

Not only will this save you pain, but it might save you some embarrassment as well. These sorts of mundane issues are real drivers of the replication crises in research today.

Your research is important and I know you want to do it right, so let's get started!

new;
cls;

/*
** Load and preview data
*/
int_rate = loadd("int_rate.csv");

tail(int_rate);

ks = { 0.517, 0.653, 0.781  };

/*
** Graph data
*/

// Graph size
plotCanvasSize("px", 500 | 400);

// Default settings
struct plotControl plt;
plt = plotGetDefaults("xy");

// Font
plotSetFonts(&plt, "all", "roboto", 14);

// Legend
plotSetLegend(&plt, "", "vcenter left inside", 1);
plotSetLegendBkd(&plt, 0);

// Main line settings
clrs = getColorPalette("set2");
plotSetLinePen(&plt, 4, clrs[3 2], 1|3);

// Axes outline (spine)
plotSetOutlineEnabled(&plt, 1);

// X-axis
plotSetTextInterpreter(&plt, "latex", "xaxis");
plotSetXAxisLabel(&plt, "\\text{country opacity }, \\omega");

// Y-axis
plotSetYLabel(&plt, "interest rate");

// Draw main plot
plotXY(plt, int_rate, "high + low ~ x");

// Style and add vertical lines
plotSetLinePen(&plt, 1, "#CCC", 2);
plotAddVLine(plt, ks);

// Style text boxes
struct plotAnnotation ant;
ant = annotationGetDefaults();
annotationSetTextInterpreter(&ant, "latex");
annotationSetLinePen(&ant, 0, "", -1);
annotationSetFont(&ant, "", 14, "#3333");
annotationSetBkd(&ant, "", 0);

// Add text boxes
plotAddTextbox(ant, "\\omega_1", ks[1], 0.15);
plotAddTextbox(ant, "\\omega_2", ks[2], 0.15);
plotAddTextbox(ant, "\\omega_3", ks[3], 0.15);

Initial Code Simplification

We will start by creating a procedure to hold some of the plot styling functions that we want to repeat and apply them to the first plot only. Then we will add the data for the second plot.

It looks like all of the styling applied before the call to plotXY will be the same in both plots, but the y-axis label text is different. So, let's create a procedure that will apply the main settings:

new;
cls;

/*
** Load and preview data
*/
int_rate = loadd("int_rate.csv");

tail(int_rate);

ks = { 0.517, 0.653, 0.781  };

/*
** Graph data
*/

// Graph size
plotCanvasSize("px", 500 | 400);

// Declare plotControl structure
struct plotControl plt;

// Fill with defaults for this project
plt = pltDefaults();

// Set y-axis label for first plot
plotSetYLabel(&plt, "interest rate");

// Draw first plot
plotXY(plt, int_rate, "high + low ~ x");

proc (1) = pltDefaults();
    local clrs;

    struct plotControl plt;
    plt = plotGetDefaults("xy");

    // Font
    plotSetFonts(&plt, "all", "roboto", 14);

    // Legend
    plotSetLegend(&plt, "", "vcenter left inside", 1);
    plotSetLegendBkd(&plt, 0);

    // Main line settings
    clrs = getColorPalette("set2");
    plotSetLinePen(&plt, 4, clrs[3 2], 1|3);

    // Axes outline (spine)
    plotSetOutlineEnabled(&plt, 1);

    // X-axis
    plotSetTextInterpreter(&plt, "latex", "xaxis");
    plotSetXAxisLabel(&plt, "\\text{country opacity }, \\omega");

    retp(plt);
endp;

While this code is slightly longer when drawing just one plot, it will save us when we add the next plot. Before we do that, we need to address the vertical lines and annotations.

Simplifying the Annotations

Looking over the plots at the top of this article shows us that the vertical lines and the omega text boxes all depend on the ks vector. Since they seem to be intertwined, it is probably safe to put them in one procedure.

The simplest thing to do would be to add all the annotation code to a single procedure like this:

proc (0) = pltAddOmegas(ks);

    struct plotControl plt;
    plt = plotGetDefaults("xy");

    // Style and add vertical lines
    plotSetLinePen(&plt, 1, "#CCC", 2);
    plotAddVLine(plt, ks);

    // Style text boxes
    struct plotAnnotation ant;
    ant = annotationGetDefaults();
    annotationSetTextInterpreter(&ant, "latex");
    annotationSetLinePen(&ant, 0, "", -1);
    annotationSetFont(&ant, "", 14, "#3333");
    annotationSetBkd(&ant, "", 0);

    // Add text boxes
    plotAddTextbox(ant, "\\omega_1", ks[1], 0.15);
    plotAddTextbox(ant, "\\omega_2", ks[2], 0.15);
    plotAddTextbox(ant, "\\omega_3", ks[3], 0.15);
endp;

and then call that procedure right after plotXY. In this case, it is not a bad place to start. However, since we are in learning mode, let's pretend that we were going to create more graphs in this file that would add text boxes with the same styling, but would use different greek letters and would be located in a different place in the graph.

In that case, we would probably want to separate the text box styling from the text box drawing, like this:

proc (1) = textBoxDefaults();
    struct plotAnnotation ant;
    ant = annotationGetDefaults();

    annotationSetTextInterpreter(&ant, "latex");
    annotationSetLinePen(&ant, 0, "", -1);
    annotationSetFont(&ant, "", 14, "#3333");
    annotationSetBkd(&ant, "", 0);

    retp(ant);
endp;

proc (0) = pltAddOmegas(ks);

    struct plotControl plt;
    plt = plotGetDefaults("xy");

    // Style and add vertical lines
    plotSetLinePen(&plt, 1, "#CCC", 2);
    plotAddVLine(plt, ks);

    struct plotAnnotation ant;
    ant = textBoxDefaults();

    // Add text boxes
    plotAddTextbox(ant, "\\omega_1", ks[1], 0.15);
    plotAddTextbox(ant, "\\omega_2", ks[2], 0.15);
    plotAddTextbox(ant, "\\omega_3", ks[3], 0.15);
endp;

Conclusion and final code

Below is the final code to create the graphs from the top of this blog. This isn't designed to show you the best way to write this code, but rather to get you started with the idea of code reuse.

Software engineers sometimes use the acronym DRY — Don't Repeat Yourself. While that is a great practice, even just repeating yourself less often will bring you great rewards.

new;
cls;

/*
** Load and preview data
*/
int_rate = loadd("int_rate.csv");
tail(int_rate);

rationing = loadd("rationing.csv");
tail(rationing);

ks = { 0.517, 0.653, 0.781  };

/*
** Graph data
*/

// Graph size
plotCanvasSize("px", 1000 | 400);

// Declare plotControl structure and
// fill with defaults for this project
struct plotControl plt;
plt = pltDefaults();

/*
** Interest rate plot
*/

// Create grid for multiple plots
plotLayout(1,2,1);

// Set y-axis label for first plot
plotSetYLabel(&plt, "interest rate");

// Draw first plot
plotXY(plt, int_rate, "high + low ~ x");
pltAddOmegas(ks);

/*
** Rationing plot
*/

// Create grid for multiple plots
plotLayout(1,2,2);

// Set y-axis label for first plot
plotSetYLabel(&plt, "rationing");

// Draw first plot
plotXY(plt, rationing, "high + low ~ x");
pltAddOmegas(ks);

proc (1) = pltDefaults();
    local clrs;

    struct plotControl plt;
    plt = plotGetDefaults("xy");

    // Font
    plotSetFonts(&plt, "all", "roboto", 14);

    // Legend
    plotSetLegend(&plt, "", "vcenter left inside", 1);
    plotSetLegendBkd(&plt, 0);

    // Main line settings
    clrs = getColorPalette("set2");
    plotSetLinePen(&plt, 4, clrs[3 2], 1|3);

    // Axes outline (spine)
    plotSetOutlineEnabled(&plt, 1);

    // X-axis
    plotSetTextInterpreter(&plt, "latex", "xaxis");
    plotSetXAxisLabel(&plt, "\\text{country opacity }, \\omega");

    retp(plt);
endp;

proc (1) = textBoxDefaults();
    struct plotAnnotation ant;
    ant = annotationGetDefaults();

    annotationSetTextInterpreter(&ant, "latex");
    annotationSetLinePen(&ant, 0, "", -1);
    annotationSetFont(&ant, "", 14, "#3333");
    annotationSetBkd(&ant, "", 0);

    retp(ant);
endp;

proc (0) = pltAddOmegas(ks);

    struct plotControl plt;
    plt = plotGetDefaults("xy");

    // Style and add vertical lines
    plotSetLinePen(&plt, 1, "#CCC", 2);
    plotAddVLine(plt, ks);

    struct plotAnnotation ant;
    ant = textBoxDefaults();

    // Add text boxes
    plotAddTextbox(ant, "\\omega_1", ks[1], 0.15);
    plotAddTextbox(ant, "\\omega_2", ks[2], 0.15);
    plotAddTextbox(ant, "\\omega_3", ks[3], 0.15);
endp;

Further Reading

  1. Advanced Formatting Techniques for Creating AER Quality Plots.
  2. Visualizing COVID-19 Panel Data With GAUSS 22.
  3. How to Mix, Match and Style Different Graph Types.
Leave a Reply