6.3: Making Graphs

The ADAPT WeBWorK Handbook · LibreTexts

0%

Contents

    Text

    Size
    100%
    Theme
    Typeface
    Line length
    Spacing

    From LibreTexts, under CC BY 4.0, as the source states it; its equations are written in TeX, which the PDF shows typeset. This page at LibreTexts.

    6.3: Making Graphs

    This page covers how to create graphs in WeBWorK PG/PGML problems. It focuses on PGgraphmacros.pl, which produces static GD bitmap images suitable for display-only graphs such as titration curves, enzyme kinetics plots, and dose-response data.

    Use this page when: you need to show a graph in a problem but students answer with RadioButtons, PopUp menus, or numeric blanks rather than by clicking on the graph itself. Load PGgraphmacros.pl via the macro block (see Common PG Macros), set up graph variables in the Setup block (see Setup), and always test the rendered output (see Testing and Debugging).

    Which graph macro to use

    Which graph macro to use: need, macro, and notes.
    Need Macro Notes
    Display-only graph (titration curve, data plot) PGgraphmacros.pl GD bitmap image, no answer entry
    Student clicks or draws on graph parserGraphTool.pl JS canvas, registers an answer entry

    Rule of thumb: if students answer via RadioButtons or other PGML widgets (not by clicking the graph), use PGgraphmacros.pl. GraphTool creates a phantom answer entry that can interfere with subsequent PGML answer evaluators.

    Minimal example with PGgraphmacros

    Copy this block into a new problem file, save, and preview. It draws a single sine curve on a labeled grid.

    loadMacros(
      'PGstandard.pl',
      'MathObjects.pl',
      'PGML.pl',
      'PGgraphmacros.pl',
      'PGcourse.pl',
    );
    
    # Create graph: init_graph(xmin, ymin, xmax, ymax, ...)
    $gr = init_graph(-1, 0, 10, 14,
      axes  => [0, 0],
      grid  => [11, 14],
      size  => [480, 400],
    );
    
    # Plot a function (string formula, not a code reference)
    add_functions($gr,
      "2 + 3*sin(x) for x in <0,10> using color:blue and weight:2"
    );
    
    # Render as an image
    $graph_img = image(insertGraph($gr), width => 480, height => 400, tex_size => 700);
    
    BEGIN_PGML
    Here is a graph:
    
    [$graph_img]*
    END_PGML
    

    Key points:

    • init_graph returns a GD graph object.
    • add_functions takes a string formula with for x in <min,max> using color:COLOR and weight:N.
    • image(insertGraph($gr), ...) produces an HTML img tag.
    • Use [$graph_img]* in PGML. The asterisk prevents HTML escaping.

    init_graph parameters

    $gr = init_graph($xmin, $ymin, $xmax, $ymax,
      axes  => [$x_origin, $y_origin],  # where axes cross
      grid  => [$x_divisions, $y_divisions],  # grid line count
      size  => [$width_px, $height_px],  # image dimensions
    );
    
    • axes => [0, 0] places the origin at (0, 0).
    • grid controls the number of light grid lines (not tick labels).
    • size sets the GD image dimensions in pixels.

    Adding labels

    Labels are positioned in graph coordinates (not pixel coordinates). The constructor is:

    new Label($x, $y, $text, $color, $h_align, $v_align)
    

    Horizontal alignment: 'left', 'center', 'right'. Vertical alignment: 'top', 'middle', 'bottom'.

    Axis labels at edges of the plot area

    $gr->lb(new Label($xmax, $ymin - 0.7, 'x-axis label', 'black', 'right', 'top'));
    $gr->lb(new Label($xmin - 0.1, $ymax, 'y-axis', 'black', 'right', 'top'));
    

    Tick labels

    # Tick labels on y-axis
    for $yv (0, 2, 4, 6, 8, 10, 12) {
      $gr->lb(new Label($xmin - 0.08, $yv, $yv, 'black', 'right', 'middle'));
    }
    
    # Tick labels on x-axis
    for $xv (0, 1, 2, 3) {
      $gr->lb(new Label($xv, $ymin - 0.3, $xv, 'black', 'center', 'top'));
    }
    

    add_functions syntax and computed coefficients

    The function string format is:

    "FORMULA for x in <XMIN,XMAX> using color:COLOR and weight:WEIGHT"
    
    • FORMULA: a Perl math expression using x as the variable (e.g., 2*x**3 + x - 1).
    • <XMIN,XMAX>: domain interval (angle brackets).
    • color:COLOR: blue, red, green, black, etc.
    • weight:WEIGHT: line thickness in pixels (1, 2, 3, ...).

    For computed coefficients, interpolate Perl variables into the string:

    $a = 0.5;
    $b = -1.2;
    $c = 3.0;
    $d = 1.5;
    $func_str = "$a*x**3 + $b*x**2 + $c*x + $d";
    add_functions($gr, "$func_str for x in <0,3> using color:blue and weight:2");
    

    Multiple curves

    Pass multiple formula strings to add_functions. Each string specifies its own color and weight.

    add_functions($gr,
      "$curve1 for x in <0,10> using color:blue and weight:2",
      "$curve2 for x in <0,10> using color:red and weight:2",
    );
    

    Point-by-point curves with moveTo and lineTo

    When a formula string cannot express the curve shape (parametric curves, physical models where x is not the independent variable), draw point-by-point with moveTo and lineTo. Use 300 to 400 points for smooth results.

    $first_pt = 1;
    for $i (0..400) {
      $t = $i / 400.0;
      # compute ($x_val, $y_val) from $t
      if ($first_pt) {
        $gr->moveTo($x_val, $y_val);
        $first_pt = 0;
      } else {
        $gr->lineTo($x_val, $y_val, 'blue', 2);
      }
    }
    

    This approach is essential when the x-axis value is a computed function of the sweep variable rather than the independent variable itself.

    Dashed lines

    PGgraphmacros has no built-in dash style. Draw dashes manually as short line segments with gaps:

    # Horizontal dashed line at y=$pka from x=0 to x=$xeq
    for $d (0..29) {
      $x1d = $d * 0.10;
      $x2d = $x1d + 0.05;
      if ($x1d < $xeq) {
        if ($x2d > $xeq) { $x2d = $xeq; }
        $gr->moveTo($x1d, $pka);
        $gr->lineTo($x2d, $pka, 'gray', 1);
      }
    }
    

    Tune dash length (0.05) and gap (0.05) relative to graph coordinates. For vertical dashes, swap x and y in the loop.

    Dots and stamps

    Use stamps with closed_circle or open_circle to mark individual points:

    # Filled circle at a point
    $gr->stamps(closed_circle($x, $y, 'black'));
    
    # Open circle (hollow)
    $gr->stamps(open_circle($x, $y, 'black'));
    

    Label sizing: why labels look small

    Labels in PGgraphmacros are GD bitmap text. This has important consequences:

    • tex_size on image() only affects TeX/PDF output, not HTML/GD output.
    • TeX size commands (\Large, \huge) in label strings render as literal text in GD, not as formatting.
    • GD::Font->Giant is the largest GD bitmap font (9 by 15 pixels) and is still small.
    • Rendering at 2x and displaying at 1x makes labels half-size. GD text is fixed-pixel; there is no display-DPI concept.

    GD font sizes

    Available via $label->font(GD::Font->NAME):

    GD font sizes: font, pixel size, and notes.
    Font Pixel size Notes
    Tiny 5 x 8 Too small for most uses
    Small 6 x 12 Default in most PG builds
    MediumBold 7 x 13 Slightly larger
    Large 8 x 16 Good default choice
    Giant 9 x 15 Largest available

    All are fixed-size bitmap fonts. The size difference between Small and Giant is modest.

    Pragmatic fixes for readable labels

    Keep size at the final display dimensions (no 2x trick). Improve readability through layout:

    # Slightly larger canvas with extra margin for labels
    $gr = init_graph(-0.6, -1.2, 3.4, 12.5,
      axes => [0, 0],
      grid => [8, 5],
      size => [520, 420],
    );
    
    # Axis labels at edges (right-aligned at plot boundary)
    $gr->lb(new Label(3.4, -0.7, 'OH- (equivalents)', 'black', 'right', 'top'));
    $gr->lb(new Label(-0.1, 12.5, 'pH', 'black', 'right', 'top'));
    
    # Fewer tick labels to reduce crowding
    for $yv (0, 2, 4, 6, 8, 10, 12) {
      $gr->lb(new Label(-0.08, $yv, $yv, 'black', 'right', 'middle'));
    }
    
    $graph_img = image(insertGraph($gr), width => 520, height => 420, tex_size => 700);
    

    Key techniques:

    • Widen graph bounds beyond data range (-0.6 to 3.4 instead of 0 to 3) to create margin space for labels.
    • Place axis labels at edges using $xmax/$ymax coordinates with 'right', 'top' alignment.
    • Reduce tick label count to avoid crowding (every 2 or 4 units instead of every 1).
    • Use high contrast (black text on white background).

    Worked example: triprotic titration curve

    A single cubic polynomial cannot produce the correct titration curve shape for a molecule with three pKa values. A cubic has at most one inflection point, but a 3-pKa titration needs three sigmoid steps. The correct approach uses a physical speciation model.

    Why not a cubic?

    A cubic ax^3 + bx^2 + cx + d through four points:

    • Has only one inflection point -- a triprotic titration needs three sigmoid transitions.
    • Anchors pKa values at integer equivalents (1, 2) instead of the correct half-equivalents (0.5, 1.5, 2.5).
    • Produces a smooth bend, not the characteristic staircase-with-shoulders shape.

    Speciation model (correct shape)

    Sweep pH and compute the equivalents of base (n_bar) from the charge balance:

    n_bar = (Ka1*H^2 + 2*Ka1*Ka2*H + 3*Ka1*Ka2*Ka3)
          / (H^3 + Ka1*H^2 + Ka1*Ka2*H + Ka1*Ka2*Ka3)
    

    This gives three sigmoid steps with buffer flats at half-equivalence points (0.5, 1.5, 2.5 equivalents) and steep transitions at equivalence points (1.0, 2.0 equivalents).

    Copy and edit (triprotic titration)

    $Ka1_val = 10**(-$pKa1_num);
    $Ka2_val = 10**(-$pKaR_num);
    $Ka3_val = 10**(-$pKa2_num);
    
    $num_pts = 400;
    $ph_lo = 0.5;
    $ph_hi = 12.5;
    $first_pt = 1;
    for $i (0..$num_pts) {
      $ph_sweep = $ph_lo + ($ph_hi - $ph_lo) * $i / $num_pts;
      $H_conc = 10**(-$ph_sweep);
    
      $denom = $H_conc**3
             + $Ka1_val * $H_conc**2
             + $Ka1_val * $Ka2_val * $H_conc
             + $Ka1_val * $Ka2_val * $Ka3_val;
      $nbar = ($Ka1_val * $H_conc**2
             + 2 * $Ka1_val * $Ka2_val * $H_conc
             + 3 * $Ka1_val * $Ka2_val * $Ka3_val) / $denom;
    
      if ($nbar >= 0 && $nbar <= 3.0) {
        if ($first_pt) {
          $gr->moveTo($nbar, $ph_sweep);
          $first_pt = 0;
        } else {
          $gr->lineTo($nbar, $ph_sweep, 'blue', 2);
        }
      }
    }
    

    This sweeps pH (y-axis) uniformly and computes equivalents (x-axis) as the dependent variable. The moveTo/lineTo approach handles this naturally since x is not the sweep variable.

    Alternative: stacked logistic sigmoids

    If you do not need physical accuracy, three logistic functions centered at half-equivalents approximate the shape. Note that this version does not pass exactly through pKa at each half-equivalence point; the speciation model is preferred for accuracy.

    $k = 10;  # steepness
    $y0 = 1.0;
    $y_top = 11.5;
    
    for $i (0..300) {
      $xx = 3.0 * $i / 300.0;
      $s1 = 1 / (1 + exp(-$k * ($xx - 0.5)));
      $s2 = 1 / (1 + exp(-$k * ($xx - 1.5)));
      $s3 = 1 / (1 + exp(-$k * ($xx - 2.5)));
    
      $ph = $y0
          + ($pKa1_num - $y0) * $s1
          + ($pKaR_num - $pKa1_num) * $s2
          + ($pKa2_num - $pKaR_num) * $s3
          + ($y_top - $pKa2_num) * $s3 * 0.20;
    
      # draw with moveTo/lineTo as shown above
    }
    

    GraphTool warning: answer evaluator interference

    parserGraphTool.pl creates a JS-based interactive canvas where students can draw or place points. Its ans_rule() registers an answer entry (typically AnSwEr0001). This shifts the numbering of subsequent PGML answer blanks and can cause "Unrecognized evaluator type ||" errors on later RadioButtons or other PGML widgets.

    Workaround (if you must use GraphTool alongside PGML answers):

    • Place GraphTool in a BEGIN_TEXT block with \{$gt->ans_rule()\}.
    • Use LABELED_ANS($gt_ans_id, $gt_checker) before the PGML block.
    • Keep RadioButtons and other widgets in a separate BEGIN_PGML block.

    Better approach: if the graph is display-only, use PGgraphmacros.pl instead.

    Sizing and margins

    The size parameter in init_graph sets the GD image pixel dimensions. The width and height in image() control the HTML display size. Always keep these matched at 1x -- rendering at 2x and displaying at 1x shrinks bitmap labels.

    # Good: matched sizes, extra margin from wider bounds
    $gr = init_graph(-0.6, -1.2, 3.4, 12.5,
      axes => [0, 0],
      grid => [8, 5],
      size => [520, 420],
    );
    $graph_img = image(insertGraph($gr), width => 520, height => 420, tex_size => 700);
    

    To create margin space for labels without shrinking the data region, widen the graph bounds beyond the data range (e.g., -0.6 to 3.4 for data spanning 0 to 3).

    tex_size controls the image width in TeX/PDF output only (700 = 70% of text width). It has no effect on HTML/GD rendering.

    Variable scoping reminder

    Do not use my on any variable that PGML needs to interpolate. PG runs in a safe compartment where my variables are invisible to PGML.

    # WRONG - invisible to PGML
    my $graph_img = image(insertGraph($gr), ...);
    
    # RIGHT - package-level, visible to PGML
    $graph_img = image(insertGraph($gr), ...);
    

    Unavailable macros

    The following graph-related macros exist in the OPL but are not available in the local PG 2.17 renderer:

    • PGlateximage.pl (LaTeX-to-image pipeline, requires LaTeX installation)
    • PGtikz.pl (TikZ/pgfplots, requires LaTeX installation)

    Stick to PGgraphmacros.pl for display graphs and parserGraphTool.pl for interactive graphs.

    Complete working example

    Simple function graph with numeric answer -- A full problem that plots a linear function and asks students to read a value from the graph.

    ## TITLE('Read a value from a graph')
    ## DESCRIPTION
    ## A linear function is plotted on a graph. Students read
    ## the y-value at a given x-coordinate.
    ## ENDDESCRIPTION
    ## KEYWORDS('graph','PGgraphmacros','numeric','function plot')
    ## DBsubject('Biostatistics')
    ## DBchapter('Data Visualization')
    ## DBsection('Reading Graphs')
    ## Date('2026-02-15')
    ## Author('Author name')
    ## Institution('Institution name')
    
    DOCUMENT();
    loadMacros(
    	"PGstandard.pl",
    	"MathObjects.pl",
    	"PGML.pl",
    	"PGgraphmacros.pl",
    	"PGcourse.pl",
    );
    
    Context("Numeric");
    $m = random(1, 3, 1);
    $b = random(1, 4, 1);
    $x_ask = random(2, 5, 1);
    $y_ans = Real($m * $x_ask + $b);
    
    $gr = init_graph(-1, -1, 8, 20,
    	axes  => [0, 0],
    	grid  => [9, 21],
    	size  => [400, 400],
    );
    add_functions($gr,
    	"$m*x + $b for x in <0,8> using color:blue and weight:2"
    );
    
    $graph_img = image(insertGraph($gr),
    	width => 400, height => 400, tex_size => 700);
    
    BEGIN_PGML
    The graph below shows a linear function.
    
    [$graph_img]*
    
    What is the y-value when x = [$x_ask]? [________]{$y_ans}
    END_PGML
    
    ENDDOCUMENT();
    

    Apply it today

    • Use PGgraphmacros.pl for any display-only graph. Reserve parserGraphTool.pl for problems where students answer by drawing.
    • Match init_graph size to image() width/height at 1x so bitmap labels stay readable.
    • Widen graph bounds to create margin space for axis labels and tick marks.
    • Use moveTo/lineTo for curves where x is not the independent variable (titration curves, parametric shapes).
    • Avoid my on any variable referenced inside PGML blocks.

    This page titled 6.3: Making Graphs was last modified on Fri, 27 Feb 2026 15:37:15 GMT and is shared under a CC BY 4.0 license and was authored, remixed, and/or curated by Neil R. Voss.