PDL::Graphics::Prima::Simple - a very simple plotting interface for PDL::Graphics::Prima
use PDL::Graphics::Prima::Simple; use PDL; # --( Super simple line and symbol plots )-- # Generate some data - a sine curve my $x = sequence(100) / 20; my $y = sin($x); # Draw x/y pairs. Default x-value are sequential: line_plot($y); line_plot($x, $y); circle_plot($y); circle_plot($x, $y); triangle_plot($y); triangle_plot($x, $y); square_plot($y); square_plot($x, $y); diamond_plot($y); diamond_plot($x, $y); X_plot($y); X_plot($x, $y); cross_plot($y); cross_plot($x, $y); asterisk_plot($y); asterisk_plot($x, $y); # Sketch the sine function for x initially from 0 to 10: func_plot(0 => 10, \&PDL::sin); # --( Super simple histogram )-- # Plot a distribution hist_plot($distribution); # Tweak the binning hist_plot($distribution, bt::Linear(normalize => 0)); # --( Super simple matrix plots )-- # Generate some data - a wavy pattern my $image = sin(sequence(100)/10) + sin(sequence(100)/20)->transpose; # Generate a grayscale image: matrix_plot($image); # smallest is white imag_plot($image); # smallest is black # Set the x and y coordinates for the image boundaries # left, right, bottom, top matrix_plot([ 0, 1 ], [ 0, 2 ], $image); imag_plot( [ 0, 1 ], [ 0, 2 ], $image); # --( More complex plots )-- # Use the more general 'plot' function for # multiple DataSets and more plotting features: my $colors = pal::Rainbow()->apply($x); plot( -lines => ds::Pair($x, $y , plotType => ppair::Lines ), -color_squares => ds::Pair($x, $y + 1 , colors => $colors, , plotType => ppair::Squares(filled => 1) ), x => { label => 'Time' }, y => { label => 'Sine' }, ); # --( Managing window interaction )-- # In scripts and older Term::ReadLine, if you press # the letter 'q' when viewing a plot, you can # re-invoke interaction with: twiddle; # query auto-twiddling my $is_auto_twiddline = auto_twiddle; # Turn auto-twiddling on or off auto_twiddle(1); auto_twiddle(0);
One of Perl's mottos is to "make easy things easy, and hard things possible." The bulk of the modules provided by the PDL::Graphics::Prima distribution focus on the latter half, making hard but very powerful things possible. This module tackles the other half: making easy things easy. This module provides a number of simple functions for quick data plotting so that you can easily get a first look at your data. You can think of this as providing a function-based interface to plotting, in contrast to the GUI/OO interface of the rest of PDL::Graphics::Prima. Or you can look at this as providing a handy collection of plot (and window) constructors for the standard use cases. Either perspective is basically correct.
In addition to making easy plots easy, this module and its documentation are meant to serve as an introduction to PDL::Graphics::Prima. If you are new to the libarary, this is where you should start.
This module is built on the Perl Data Language and the Prima GUI toolkit. You do not need to know much about Prima to use this tutorial, but you should know the basics of PDL.
When you use this library, you get a handful of functions for quick data plotting that include
line_plot takes one or two piddles, one for y and optionally one for x, and makes a line plot with them; if you don't give an x piddle, it uses sequential integers starting from zero.
Various symbol plots, like cross_plot and diamond_plot, take one or two piddles, one for y and optionally one for x, and and plots the symbol at the given points; if you don't give an x piddle, it uses sequential integers starting from zero.
func_plot takes an initial plot min/max and a function reference and makes a line plot of the function; it can optionally take the number of points to evaluate on each drawing operation; see "ds::Func" in PDL::Graphics::Prima::DataSet
hist_plot takes a piddle of data to be plotted and an optional bin type and plots a histogram. If no bin type is supplied, linear binning is used.
Both matrix_plot and imag_plot take one or three arguments, the first two of which specify the x-bounds and the y-bounds, the third (or only) of which is the matrix to plot
plot is a much more powerful routine that takes the same arguments you would pass to the constructor of a plot widget, and packs the resulting plot widget in a stand-alone window.
And with that, let's look at how to use these to view data!
To begin, let's assume you have some x/y data that you want to plot with lines connecting them. My canonical example is a sine wave. Here's a complete script to get us started:
use PDL; use PDL::Graphics::Prima::Simple; my $x = sequence(100)/10; line_plot($x, $x->sin); print "All done!\n";
The use PDL
line pulls in all the machinery for the Perl Data Language and, more importantly, imports the sequence
function (among many others) into the current package. On the third line we use that function to generate a bunch of x-data with a spacing of 1/10. In the last line we plot the sine of that sequence of points.
When you run the above example script, it will block at the line_plot
command and display a Prima window with your plot. (On the other hand, if you are in the pdl shell, the plot will be displayed but it will not block your shell if you have a recent version of Term::ReadLine.) We will return to the blocking behavior, and how to call this in a non-blocking fashion, in a little bit.
For now, turn your attention to the plot. This is a highly interactive plot, as are all plots made with PDL::Graphics::Prima. In particular, your plot responds to the following user interactions:
Clicking and dragging your right mouse button will zoom into a specific region. You will see a zoom rectangle on the plot until you release the mouse, at which point the plot will be zoomed-in to the region that you selected.
You can zoom-in and zoom-out using your scroll wheel. The zooming is designed to keep the data under the mouse at the same location as you zoom in and out. (This is not a 100% guarantee, but for most uses it works about right.)
Once you have zoomed into a region, you can examine nearby data by clicking and dragging with your left mouse button, much like an interactive map.
Right-clicking on the plot will bring up a context menu with options including restoring auto-scaling, copying the current plot image to your clipboard* (to paste directly into, say, Microsoft's PowerPoint or LibreOffice's Impress), and saving the current plot image to a postscript or raster file. Postscript output is always supported, but the supported raster output file formats depend on the image libraries and codecs that Prima was able to build against, so are system- and machine-dependent. For additional information on raster images, see Prima::Image.
* For reasons not clear to me, copying the plot to the clipboard does not seem to work on Mac and appear to be due to limitations with the X-window bindings.
When packed into a resizable window (as is the case in this example), the plot can be resized and it will be updated and redrawn smoothly.
The library lets you specify the x- and y-bounds of the plot, but if you do not specify bounds, the axis bounds will be calculated to tightly fit the data. In fact, the library is designed to automatically choose axis boundaries that fit your data and symbols exactly. (And if you wanted a bit of padding included in that auto-fitting... well... it's on my todo list. :-)
Having played around with the plot widget, you probably want to know how to modify it programatically, by adding a title or axis labels, perhaps. "What sort of options," you ask, "does line_plot accept for me to specify these things?" Well, you can't specify those in your call to line_plot. You either add them to the object after line_plot builds something for you, or you use the more powerful but verbose plot function.
"But WHY?" you ask. "WHY can't I just specify a plot title in line_plot and be done with it?" The reason is simple. The underlying library is built on a very clean and well-thought-out object and I would rather not waste my time creating or your time learning some intermediate API. A means for specifying the plot title in line_plot is the first step down the road of confused mental models. So, it's really in your best interst. Honest. :-)
"But WHY?" you ask again. OK, OK, I'll tell you how to essentially get what you want.
First, you can use the line_plot
command to build a plot object and return them to you without blocking your script. This will allow you to modify the properties of the plot object before it gets displayed. For example, I can add a plot title and specifically choose when to view the plot like so:
use PDL; use PDL::Graphics::Prima::Simple; # Non-blocking auto_twiddle(0); # Build the plot my $x = sequence(100)/10; my $plot = line_plot($x, $x->sin); # Add a title $plot->title('The sine wave'); # Display the plot twiddle();
You next may ask how you modify the axis properties, such as setting the bounds or giving them labels. The axes are sub-objects of the plot, accessed with like-named accessors: $plot->x. The properties of the axes that you can modify include the min, max, scaling type, and axis label, and are discussed in greater detail under their own documentation. Let's see how to set the x- and y-axis labels:
use PDL; use PDL::Graphics::Prima::Simple; # Build the plot auto_twiddle(0); my $x = sequence(100)/10; my $plot = line_plot($x, $x->sin); # Add a title and axis labels $plot->title('The Harmonic Oscillator'); $plot->x->label('time (s)'); $plot->y->label('displacement (cm)'); # Display the plot twiddle();
NOTE: The current state of readline integration, and its work-around, are in a state of flux. These docs are not entirely accurate. But they're close enough. Expect this to get fixed soon.
NOTE: Term::ReadLine::Perl is a pure-perl implementation of Term::ReadLine::Gnu and it is very nice. However, it does not play nicely with the Prima readline integration for reasons I do not yet fully understand. In particular, the displayed text and cursor position are always displayed one step "behind" what you last indicated with the navigation keys (but they are always up-to-date when you type normal letters). If you have any ideas for how to remedy this, please let me know! Thanks!
You can do the same sorts of manipulations from the console and see the updates as soon as you press enter. The equivalent commands as the ones shown above are:
pdl> use PDL::Graphics::Prima::Simple pdl> auto_twiddle(0) pdl> $x = sequence(100)/10 # After the next commant, the line plot window will pop up pdl> $plot = line_plot($x, $x->sin) pdl> $plot->title('The Harmonic Oscillator') pdl> $plot->x->label('time (s)') pdl> $plot->y->label('displacement (cm)')
Each method call to the plot command will cause the plot to get updated with the new element or feature.
If you want to set the axis bounds, you can use the min, max and minmax functions. For example, continuing from the PDL shell, this sets the x-minimum to zero:
pdl> $plot->x->min(0)
If you drag the plot around with your mouse and want to see the current value of the min, you could say:
pdl> p $plot->x->min
This will print two numbers, actually, the second being a boolean flag indicating whether or not autoscaling is on. To just get the minimum value, call the min
method in scalar context:
pdl> p scalar($plot->x->min)
If you want to re-enable autoscaling, you pass in a special value for the min or max (or both) denoted by the constant lm::Auto
as in
# Set the x-axis to autoscaling pdl> $plot->x->minmax(lm::Auto, lm::Auto); # Set the y-minimum to autoscaling pdl> $plot->y->min(lm::Auto);
I've only spoken so far about connecting x/y data with lines, but there are other ways to visualize x/y data, and even other forms of data you may wish to represent. For example, if you want to visualize a distribution of data, you would want to use a histogram:
use PDL; use PDL::Graphics::Prima::Simple; # Generate 100 samples with mean 0 and standard deviation 1 my $distribution = grandom(100); hist_plot($distribution);
Or, you may have an image of data that you want to view.
use PDL; use PDL::Graphics::Prima::Simple; # Generate 100 samples with mean 0 and standard deviation 1 my $image = rvals(100, 100) matrix_plot($image);
And you won't be surprised to learn there are many other ways to display x/y data, as well. But let's keep moving.
What if you wanted to plot additional data along with the current data? You do this by creating a new DataSet. Let's start by adding a DataSet with the cosine of a slightly different x-range:
use PDL; use PDL::Graphics::Prima::Simple; # Build the plot my $x = sequence(100)/10; my ($window, $plot) = line_plot($x, $x->sin); # Add a new dataset my $x2 = sequence(100)/10 + 1; $plot->dataSets{'cosine'} = ds::Pair($x2, $x2->cos); # Display the plot $window->execute;
The important line in this example is the part that includes ds::Pair
. Like lm::Auto
, ds::Pair
is a function that uses a short prefix so that it's fast and easy to type. This is a common idiom in the Prima toolkit (though in the toolkit it is used exclusively for specifying useful constants). ds::Pair
is just a short-hand for a constructor with an enormous name:
# Short form my $ds = ds::Pair($x, $y); # Long form my $ds = PDL::Graphics::Prima::DataSet::Pair->new(x => $x, y => $y);
If you actually run that example, you will notice that the sine cureve is plotted as a line, just as before, but the cosine curve is plotted with a collection of unconnected diamonds. If you want to plot the cosine curve with a line, you need to specify the line plot type:
... # Add a new dataset $plot->dataSets{'cosine'} = ds::Pair($x, $x->cos, plotType => ppair::Lines ); ...
Perl is supposed to Do What I Mean, and you could argue that this is a case of the plotting library not doing what you mean: the cosine curve "should" be plotted with lines, because that's how the sine curve was plotted. However, I believe that such behavior violates the Principle of Least Surprise. The default plot type for pairwise data is diamonds, so if you didn't specify a plot type, you actually meant to plot diamonds.
That brings us to the important distinction between a DataSet and a PlotType. A DataSet contains the x/y data, or the distribution, or the image matrix that you want shown, along with one or more means of visualizing that data. The means for visualizing that data are what are called PlotTypes, and different DataSets use distinct PlotTypes.
Although interacting with the plot object after creation is fun, it is also nice to be able to specify all of these settings when the plot is initially created. I have already explained how limited the super-simple interface is, and why I chose to restrict it to be so limited. In light of that, I now show you how to specify all of these properties and more with a single plot
command.
Let's begin by examining the line_plot command. The documentation below states that the function call with args ($x, $y)
is equivalent to this line:
plot(-data => ds::Pair($x, $y, plotType => ppair::Lines));
Let's expand that into a full example and include the title, x-label, and y-label:
use PDL; use PDL::Graphics::Prima::Simple; # Build the plot my $x = sequence(100)/10; plot( # Create the DataSet with the Lines pairwise plotType -data => ds::Pair($x, $x->sin, plotType => ppair::Lines ), # Set the title and axis labels title => 'The Harmonic Oscillator', x => { label => 'time (s)' }, y => { label => 'displacement (cm)' }, );
Notice that all arguments to plot are key/value pairs. There is a clean, heierarchical structure to the function call, and it is clear simply by examining the punctuation which settings go with which piece. For example, the ppair::Lines argument clearly belongs to the ds::Pair. In this case, we use data structures that Perl provides to help convey the structure of the plot we are trying to create.
Precisely what happens when you call these functions depends on your environment. These functions always create a stand-alone window with the plot, but they may or may not pause your script or shell while you interact with the plot. In regular Perl scripts, the code will block at these function calls until you close the window or press 'q' in one of the plot windows. For some folks, when using the PDL shell the functions return immediately, letting them peform more calculations or create new plots while keeping other plot windows open. For other folks, it's possible to go back and forth between an active plot and an inactive PDL shell, or an active shell and an inactive plot. From the shell, the twiddle
function makes the plot active; from the plot, pressing 'q' returns focus to the shell.
The main drawback of using the Simple interface instead of the full-blown widget interface is that it differs from the normal Prima GUI application interface. I hope that this makes it easier for you to get started with this plotting library, but I hope that you also take the time to learn how to write Prima applications, with the Plot widget as just one component for user interaction. If you need any substantial amount of user interaction or real-time behavior, I suggest you work with the full Prima toolkit.
You may find yourself working on a script and wanting to create multiple plot windows at once, without blocking until all of them are ready. You do this by explicitly turning off autotwiddling. Specifically, you would write something like this:
my $was_auto_twiddling = auto_twiddle; auto_twiddle(0); # ... plotting operations ... # All done; restore previous autotwiddling # state and let the use interact: auto_twiddle($was_auto_twiddling); twiddle();
Although you can plot multiple DataSets in the same plot window, and you can simultaneously build multiple plot windows, a limitation of this Simple interface is that you cannot create multiple independent plots in the same window. This is achieved using the full GUI toolkit by creating two plot widgets packed into a larger container widget. A tutorial for this sort of thing is in the works but hasn't made it into the distribution yet. Stay tuned!
Having covered that introductory material, let's cover things a little more systematically.
These functions are bare-bones means for visualizing your data that are no more than simple wrappers around the more powerful plot function. If you just want to have a quick look at your data, you should start with these. These functions can return the plot widget itself, allowing you to modify it, but see the plot if want more control over your plot, such as plotting multiple data sets, using variable or advanced symbols, using multiple plot types, controlling the axis scaling, using colors, or setting the title or axis labels.
In all of these plots, bad values in x and y are simply omitted.
The line_plot
function takes either one or two arguments. In the one-argument form, the argument is a piddle with your y data. In the two argument form the arguments are a piddle with your x data and a piddle with your y data. The function plots them by drawing black lines on a white background from one point to the next. Usually $x
and $y
will have the same dimensions, but you can use any data that are PDL-thread compatible. For example, here's a way to compare three sets of data that have the exact same x-values:
my $x = sequence(100)/10; my $y = sequence(3)->transpose + sin($x); # Add mild linear trends to the first and second: use PDL::NiceSlice; $y(:, 0) += $x/5; $y(:, 1) -= $x/6; line_plot($x, $y);
The x-values do not need to be sorted. For example, this plots a sine wave sine wave oscillating horizontally:
my $y = sequence(100)/10; my $x = sin($y); line_plot($x, $y);
For the truly lazy, you can simply supply the y-values:
my $y = sin(sequence(100)/10); line_plot($y);
Bad values in your data, if they exist, will simply be skipped, inserting a gap into the line.
For the two-argument form, to generate the same plot using the plot command, you would type this:
plot(-data => ds::Pair($x, $y, plotType => ppair::Lines));
For the one-argument form, you would type this:
plot(-data => ds::Pair($y->xvals, $y, plotType => ppair::Lines));
Plots filled circles at (x, y). (See line_plot for a more detailed description.) Equivalent plot commands for the two-argument forms include:
plot(-data => ds::Pair($x, $y, plotType => ppair::Blobs)); plot(-data => ds::Pair( $x, $y, plotType => ppair::Symbol( filled => 'yes', N_points => 0, ), ));
Plots filled upright triangles at (x, y). (See line_plot for a more detailed description.) Equivalent plot commands for the two-argument form include:
plot(-data => ds::Pair( $x, $y, plotType => ppair::Triangles(filled => 1) )); plot(-data => ds::Pair( $x, $y, plotType => ppair::Symbol( filled => 'yes', N_points => 3, orientation => 'up', ), ));
Plots filled squares at (x, y). (See line_plot for a more detailed description.) Equivalent plot commands for the two-argument form include:
plot(-data => ds::Pair( $x, $y, plotType => ppair::Squares(filled => 1) )); plot(-data => ds::Pair( $x, $y, plotType => ppair::Symbol( filled => 'yes', N_points => 4, orientation => 45, ), ));
Plots filled diamonds at (x, y). (See line_plot for a more detailed description.) Equivalent plot commands for the two-argument form include:
plot(-data => ds::Pair($x, $y)); plot(-data => ds::Pair( $x, $y, plotType => ppair::Diamonds(filled => 1) )); plot(-data => ds::Pair( $x, $y, plotType => ppair::Symbol( filled => 'yes', N_points => 4, ), ));
Plots crosses (i.e. plus symbols) at (x, y). (See line_plot for a more detailed description.) Equivalent plot commands for the two-argument form include:
plot(-data => ds::Pair($x, $y, plotType => ppair::Crosses)); plot(-data => ds::Pair( $x, $y, plotType => ppair::Symbol( N_points => 4, skip => 0, ), ));
Plots X symbols at (x, y). (See line_plot for a more detailed description.) Equivalent plot commands for the two-argument form include:
plot(-data => ds::Pair($x, $y, plotType => ppair::Xs)); plot(-data => ds::Pair( $x, $y, plotType => ppair::Symbol( N_points => 4, skip => 0, orientation => 45, ), ));
Plots five-pointed asterisks at (x, y). (See line_plot for a more detailed description.) Equivalent plot commands for the two-argument form include:
plot(-data => ds::Pair( $x, $y, plotType => ppair::Asterisks(N_points => 5) )); plot(-data => ds::Pair( $x, $y, plotType => ppair::Symbol( N_points => 5, skip => 0, orientation => 'up', ), ));
The func_plot
function takes three or four arguments and plots a function. The first two arguments are the initial x-bounds (min and max); the third argument is the function that you want to plot; the optional fourth argument is the number of points that you want to use in generating the plot. The resulting figure will have a black line drawn against a white background.
The function itself will be called whenever the plot needs to be redrawn. It will be passed a single argument: a piddle with sequential x-points at which the function should be evaluated. The function should return a piddle of y-points to be plotted. Here are some examples:
# Plot PDL's exponential function: func_plot (1, 5, \&PDL::exp); # this time with higher resolution: func_plot (1, 5, \&PDL::exp, 1000); # Plot a rescaled tangent function: func_plot (1, 5, sub { my $xs = shift; return 5 * ($xs / 4)->tan }); # or equivalently, if you "use PDL": func_plot (1, 5, sub { my $xs = shift; return 5 * tan($xs / 4); });
Your function can return bad values, in which case they will not be drawn. For example, here is a function that plots a decaying exponential, but only for values of x greater than or equal to zero. It starts with an initial view of x running from 0 to 4:
func_plot (0, 4, sub { my $xs = shift; my $ys = exp(-$xs); return $ys->setbadif($xs < 0); });
The return value must have thread-compatible dimensions, but that means that they do not need to be identical to the input x. For example, a scalar is thread-compatible with a vector, so the following will create a line at a y-value of 1:
func_plot (0, 4, sub { 1 });
Or, you can return a piddle with more dimensions than the input:
func_plot (0, 4, sub { my $xs = shift; my $first_ys = sin($xs); my $second_ys = cos($xs) + 3; return $first_ys->cat($second_ys); });
If you do not specify the number of points to draw, the equivalent plot command is this:
plot( -data => ds::Func($func_ref), x => { min => $xmin, max => $xmax }, );
If you do specify the number of points to draw, the equivalent plot command is this:
plot( -data => ds::Func($func_ref, N_points => $N_points), x => { min => $xmin, max => $xmax }, );
The hist_plot
function takes a distribution of data that you want to visualize and an optional binning type. This is a fairly simple wrapper around the linear binning type for the Distribution DataSet using the histogram pairwise plot type. It plots the histogram as black-outlined rectangles against a white background.
The equivalent plot commands is:
plot(-data => ds::Dist($distribution));
A lot of defaults go into that, so it might be useful to see some of those expanded a bit:
plot(-data => ds::Dist($distribution, plotType => ppair::Histogram, binning => bt::Linear ) );
You could tweak the binning, for example by using a strict logarithmic binning, which throws an exception if your distribution includes negative data:
hist_plot($distribution, bt::StrictLog);
or you could specify that you want 15 bins:
hist_plot($distribution, bt::Linear(nbins => 15));
There is quite a bit more to explore here, and it will be covered below.
The matrix_plot
function plots a grayscale rendering of a matrix with the largest values closest to black, the smallest values closest to white. (For the opposite color sense, see imag_plot.) This function takes either one or three arguments. The first designates the x min and max of the plot; the second designates the y min and max of the plot, and the third (or only) specifies a matrix that you want to have plotted in grayscale. The x-edges and y-edges arguments should be two-element array references, such as:
matrix_plot ([0 => 5], [1 => 10], $matrix);
Bad values, if your matrix has any, are skipped. This means that you will have a white spot in its place (since the background is white), which is not great. Future versions may use a different color to specify bad values.
Not specifying edges looks like this:
matrix_plot ($matrix);
and its equivalent plot commands are:
plot(-image => ds::Grid( $matrix, x_bounds => [0, 1], y_bounds => [0, 1], )); plot(-image => ds::Grid( $matrix, x_bounds => [0, 1], y_bounds => [0, 1], plotType => pgrid::Matrix, ));
Specifying edges looks like this:
matrix_plot ([0 => 5], [1 => 10], $matrix);
and the quivalent is:
plot(-image => ds::Grid( $matrix, x_bounds => [0, 5], y_bounds => [1, 10], plotType => pgrid::Matrix, ));
The imag_plot
function is identical to matrix_plot except that the color scaling runs from black (lowest) to white (highest). This form is especially useful for plotting black and white images, in which case the brightest points should be white.
Omitting edges, the command looks like this:
imag_plot($matrix);
which has the equivalent plot command:
plot(-image => ds::Grid( $matrix, x_bounds => [0, 1], y_bounds => [0, 1], plotType => pgrid::Matrix( palette => pal::BlackToWhite ), ));
Specifying edges looks like this:
imag_plot ([0 => 5], [1 => 10], $matrix);
and the quivalent is:
plot(-image => ds::Grid( $matrix, x_bounds => [0, 5], y_bounds => [1, 10], plotType => pgrid::Matrix( palette => pal::BlackToWhite ), ));
The plot
function is the real workhorse of this module. Not only does it provide the functionality behind all of the above simple functions, but it also lets you plot multiple DataSets, specify axis labels and a plot title, direct the axis scaling (linear or logarithmic), and set many other properties of the plot.
Arguments that you pass to this function are almost identical to the arguments that you would use to create a Plot widget, so it serves as an excellent sandbox for playing with the widget's constructor. Also, once you understand how to use this function, using the actual widget in an interactive GUI script is simply a matter of understanding how to structure a GUI program.
The plot
function takes options and DataSet specifications as key/value pairs. The basic usage of plot
looks like this:
plot( -dataset1 => ds::Pair($x1, $y1, ...options...), x => { axis => options, }, -dataset2 => ds::Pair($x2, $y2, ...options...), y => { axis => options, }, -mydist => ds::Set($data, ...options...), title => 'Title!', titleSpace => 60, -the_image => ds::Grid($image, ...options...), ... Prima Drawable options ... );
Notice that some of the keys begin with a dash while others do not. Any key that begins with a dash should be followed by a DataSet object (created using the ds::xxx
constructors). You can use any name that you wish for your DataSets, the only requirement is that the name begins with a dash. (The dash is optional when it comes time to retrieve the DataSet later.) The keys that do not begin with a dash are Plot options. The Plot widget has a handful of Plot-specific properties, but you can also specify any property of a Prima::Widget object.
In the world of PDL::Graphics::Prima
, the fundamental object is the Plot. Each plot can hold one or more DataSets, and each DataSet is visualized using one or more "" in PDL::Graphics::Prima::PlotTypes. This makes the plotType the simplest element to discuss, so I'll start there.
Each DataSet can have one or more plotTypes. If you only want to specify a single plotType, you can do so by specifying it after the plotType key for your DataSet:
-data => ds::Pair( ... plotType => ppair::Squares, ... )
You can specify multiple plotTypes by passing them in an anonymous array:
-data => ds::Pair( ... plotTypes => [ppair::Triangles, ppair::Lines], ... )
(Note that the singular and plural keys plotType
and plotTypes
are interchangeable. Use whichever is appropriate.)
All the plotTypes take key/value paired arguments. You can specify various Prima::Drawable properties like lineWidth or color; you can pass plotType-specific options like symbol size (for ppair::Symbol and its derivatives) using the size
key or the baseline height for ppair::Histogram using the baseline
key; and some of the plotTypes have required arguments, such as at least one error bar specification with ppair::ErrorBars. To create red blobs, you would use something like this:
ppair::Blobs(color => cl::LightRed)
To create blobs of all different colors, you would use the plural colors
key and specify a piddle with Color values. (That's discussed below in an example.) To specify a 5-pixel line width for a Lines plotType, you would say
ppair::Lines(lineWidth => 5)
When a DataSet gets drawn, it draws the different plotTypes in the order specified. For example, suppose you specify cl::Black
filled triangles and cl::LightRed
lines. If the triangles are specified first, they will have red lines drawn through them, and if the triangles are second, the triangles will be drawn over the red lines.
Each DataSet has a default plot type. For Dists, it is ppair::histogram. For Pairs, it is ppair::Diamonds. For Grids, it is pgrid::Matrix. If you want to use a different plotType, you need to specify it as illustrated by the translations given in the super-simple examples above. The plotTypes are discussed thoroughly in PDL::Graphics::Prima::PlotType, and are summarized below:
Pair plotTypes ============== ppair::Lines - lines from point to point ppair::TrendLines - a linear fit to the data ppair::Blobs - blobs (filled ellipses) with specifiable x- and y- radii ppair::Symbols - open or filled regular geometric shapes with many options: size, orientation, number of points, skip pattern, and fill ppair::Triangles - open or filled triangles with specifiable orientations and sizes ppair::Squares - open or filled squares with specifiable sizes ppair::Diamonds - open or filled diamonds with specifiable sizes ppair::Stars - open or filled star shapes with specifiable sizes, orientations, and number of points ppair::Asterisks - asterisk shapes with specifiable size, orientation, and number of points ppair::Xs - four-point asterisks that look like xs, with specifiable sizes ppair::Crosses - four-point asterisks that look like + signs, with specifiable sizes ppair::Spikes - spikes to (x,y) from a specified vertical or horizontal baseline ppair::Histogram - histograms with specifiable baseline and top padding ppair::ErrorBars - error bars with specified x/y errors and cap sizes Grid plotTypes ============== pgrid::Matrix - colored rectangles, i.e. images
More plot types are planned, but the ones listed above are the currently implemented ones.
The plotTypes are the simplest unit in "" in PDL::Graphics::Prima. The next largest unit is the DataSet, which not only holds data of various kinds, but also holds the plotTypes that are to be applied to the given data.
You can plot one or more sets of data on a given Plot. You do this by specifying the DataSet's name with a dash, followed by a DataSet constructor. The DataSet constructor specifies the properties of your DataSet, including the data itself along with the plotType or plotTypes.
In addition to the two types that I have already alluded to, namely Pairs and Grids, there are a collection of derived Datasets. These include the Function-based DataSet and the Distribution Dataset.
The Pairs DataSet targets x/y paired data and lets you visualize trends and correlations between two collections of data. The constructor takes two arguments (the x-data and the y-data) and then key/value pairs that indicate your plotTypes and specify precisely how you want the data visualized:
-scatter => ds::Pair($student_weights, $student_heights, ...options...)
Typical x/y plots are plotted in such a way that the x-data is sorted in increasing order, but this is not required. This means that it is just as easy to draw a sine function as it is to draw a spiral or a scatter plot.
Dists take a single piddle of unordered data and visually represents it with aggregated plots like histograms, cumulative distributions, or curves to distribution fits. The constructor takes a single piddle argument that represents that data to plot, and then key/value pairs that let you tweak how the data is visualized. The most important key is the binning key, which dictates how the distribution should be mapped to pairwise data.
-distribution => ds::Dist($student_heights, binning => bt::Log(min => 2), ...options... )
Any pairwise plot type will work with the distribution plot types, but some make more (or less) sense than others. For example, CDFs are better visualized with lines whereas linearly binned data are sometimes better visualized with histograms.
Func DataSets let you specify a function to plot, rather than forcing you to evaluate a specific function at fixed values of x. They inherit from Pairs (in an OO sense) and differ in that the constructor expects a single function reference rather than two piddles of data:
-model => ds::Func(\&my_model, ...options...)
Because Func inherits from Pairs, any Pairs PlotType will also work with a Func DataSet.
The Grid DataSet is what you would use to visualize matrices or images. It takes a single piddle which represents a matrix of data, followed by key/value pairs the specify how you want the data plotted. In particular, there are many ways to specify the grid centers or boundaries.
-terrain => ds::Grid($landscape, ...options...)
The data that you specify for the Set, Pair, and Grid DataSets do not need to be piddles: anything that can be converted to a piddle, including scalar numbers and anonymous arrays of values, can be specified. That means that the following are valid DataSet specifications:
-data => ds::Pair(sequence(10), sequence(10)->sin) -data => ds::Pair([1, 2, 3], [1, 4, 9]) -data => ds::Pair(sequence(100), 5)
Once you have specified the data or function that you want to plot, you can specify other options with key/value pairs. I discussed the plotType key already, but you can also specify any property in Prima::Drawable. When you specify properties from Prima::Drawable, these become the default parameters for all the plotTypes that belong to this DataSet. For example, you can specify a default color as cl::LightRed
, and then the lines, blobs, and error bars will be drawn in red unless they override the colors themselves. Function-based DataSets also recognize the N_points
key, which indicates the number of points to use in evaluating the function.
To get an idea of how this works, suppose I have some data that I want to compare with a model. In this case, I would have two DataSets, the data (plotted using error bars) and the model (plotted using a line). I would plot all of this with code like so:
plot( # The experimental data -data => ds::Pair( $x, $y, # I want error bars along with squares: plotTypes => [ ppair::ErrorBars(y_err => $y_errors), ppair::Squares(filled => 1), ], ), # The model: -model => ds::Func( \&my_model, # Default plotType is diamonds, but I want lines: plotType => ppair::Lines, lineStyle => lp::ShortDash, ), );
The part -data => ds::Pair(...)
specifies the details for how you want to plot the experimental data and the part -model
specifies the details for how you want to plot the model.
The DataSets are plotted in ASCIIbetical order, which means that in the example above, the model will be drawn over the error bars and squares. If you want the data plotted over the model curve, you should choose different names so that they sort the way you want. For example, using -curve
instead of -model
might work. So would changing the names from -data
and -model
to -b_data
and -a_model
, respectively.
Finally we come to setting plot-wide properties. As already discussed, you can disperse DataSets among your other Plot properties. Plot-wide properties include the title and the amount of room you want for the title (called titleSpace), the axis specifications, and any default Prima::Drawable properties that you want applied to your plot.
The text for your plot's title should be a simple Perl string. UTF8 characters are allowed, which means you can insert Greek or other symbols as you need them. However, Prima, and therefore PDL::Graphics::Prima, does not support fancy typesetting like subscripts or superscripts. (If you want that in the Plot library, you should probably consider petitioning for and helping add that functionality to Prima. Open-source is great like that!) The amount of space allocated for the title is currently set at 80 pixels. You can specify a different size if you prefer. (It should probably be calculated based on the current font-size---Prima makes it relatively easy to do that---but that's not yet implemented.) Space is allocated for the title only when you specify one; if none is specified, you have more room for your plot.
Axis labels have similar restrictions and capabilities as the title string, but are properties of the axes themselves, which additionally have specifiable bounds (min and max) and scaling type. At the moment, the only two scaling types are sc::Linear
and sc::Log
. The bounds can be set to a specific numeric value, or to lm::Auto
if you want the bounds automatically computed based on your data and plotTypes.
Finally, this is essentially a widget constructor, and as such you can specify any Prima::Widget properties that you like. These include all the properties in Prima::Drawable. For example, the default background color is white, because I like a white background on my plots. If you disagree, you can change the widget's background and foreground colors by specifying them. The DataSets and their plotTypes will inherit these properties (most importantly, the foreground color) and use them unless you override those properties seperately.
This first example is a simple line plot with triangles at each point. There's only one DataSet, and it has only two plotTypes:
use strict; use warnings; use PDL::Graphics::Prima::Simple; use PDL; my $x = sequence(100)/10; my $y = sin($x); plot( -data => ds::Pair( $x, $y, plotTypes => [ ppair::Triangles, ppair::Lines, ], ), );
Now for something more fun. This figure uses bright colors and random circle radii. Notice that the lineWidth of 3 obscures many of the circles since their radii are between 1 and 5. This has only one DataSet and two plotTypes like the previous example. In contrast to the previous example, it specifies a number of properties for the plotTypes:
use strict; use warnings; use PDL::Graphics::Prima::Simple; use PDL; my $x = sequence(100)/10; my $y = sin($x); my $colors = pal::Rainbow->apply($y); plot( -data => ds::Pair( $x, $y, plotTypes => [ ppair::Blobs ( radius => 1 + $x->random*4, colors => $colors, ), ppair::Lines ( lineWidths => 3, ), ], ), );
Here I use a black background and white foreground, and plot the circles over the line instead of under it. I achieve this by changing the order of the plotTypes---Lines then Blobs.
use strict; use warnings; use PDL::Graphics::Prima::Simple; use PDL; my $x = sequence(100)/10; my $y = sin($x); my $colors = pal::Rainbow->apply($y); my $radius = 1 + $x->random*4; plot( -data => ds::Pair( $x, $y, plotTypes => [ ppair::Lines ( lineWidths => 3, ), ppair::Blobs ( radius => $radius, colors => $colors, ), ], ), backColor => cl::Black, color => cl::White, );
I find the smaller points very difficult to see, so here's a version in which I 'wrap' the points with a white radius. I also use the Symbols plotType instead of the Blobs plotType because it's a bit more flexible:
use strict; use warnings; use PDL::Graphics::Prima::Simple; use PDL; my $x = sequence(100)/10; my $y = sin($x); my $colors = pal::Rainbow->apply($y); my $radius = 1 + $x->random*4; plot( -data => ds::Pair( $x, $y, plotTypes => [ ppair::Lines ( lineWidths => 3, ), ppair::Symbols( size => 1 + $radius, filled => 'no', N_points => 0, ), ppair::Symbols ( size => $radius, colors => $colors, filled => 'yes', N_points => 0, ), ], ), backColor => cl::Black, color => cl::White, );
Here I use PDL threading to achieve the same ends as the previous example, but only using one Blobs plotType instead of two.
use strict; use warnings; use PDL::Graphics::Prima::Simple; use PDL; my $x = sequence(100)/10; my $y = sin($x); my $rainbow_colors = pal::Rainbow->apply($y); my $whites = $y->ones * cl::White; my $colors = cat($whites, $rainbow_colors); my $inner_radius = 1 + $x->random*4; my $radius = cat($inner_radius + 1, $inner_radius); plot( -data => ds::Pair( $x, $y, plotTypes => [ ppair::Lines ( lineWidths => 3, ), ppair::Blobs( radius => $radius, colors => $colors, ), ], ), backColor => cl::Black, color => cl::White, );
Here I generate some linear data with noise and perform a least-squares fit to it. In this case I perform the least-squares fit by hand, since not everybody will have Slatec installed. The important part is the use PDL::Graphics::Prima::Simple
line and beyond.
This example demonstrates the use of multiple data sets, the use of the ErrorBars plotType and the use of function-based data sets.
use strict; use warnings; use PDL; my $x = sequence(100)/10; my $y = $x/2 - 3 + $x->grandom*3; my $y_err = 2*$x->grandom->abs + 1; # Calculate the slope and intercept: my $S = sum(1/$y_err); my $S_x = sum($x/$y_err); my $S_y = sum($y/$y_err); my $S_xx = sum($x*$x/$y_err); my $S_xy = sum($x*$y/$y_err); my $slope = ($S_xy * $S - $S_x * $S_y) / ($S_xx * $S - $S_x * $S_x); my $y0 = ($S_xy - $slope * $S_xx) / $S_x; use PDL::Graphics::Prima::Simple; plot( -data => ds::Pair( $x, $y, plotTypes => [ ppair::Diamonds(filled => 'yes'), ppair::ErrorBars(y_err => $y_err), ], ), -func => ds::Func( sub { $y0 + $slope * $_[0] }, lineWidth => 2, color => cl::LightRed, ), );
That example used a function-based DataSet, but we could just as easily have used ppair::TrendLines
to compute the fit for us. The only difference between the last example and the one below is that the trendline for this next example does not extend out to infinity in the x-direction but terminates at the end of the data.
use strict; use warnings; use PDL; my $x = sequence(100)/10; my $y = $x/2 - 3 + $x->grandom*3; my $y_err = 2*$x->grandom->abs + 1; use PDL::Graphics::Prima::Simple; plot( -data => ds::Pair( $x, $y, plotTypes => [ ppair::Diamonds(filled => 'yes'), ppair::ErrorBars(y_err => $y_err), ppair::TrendLines( weights => $y_err, lineWidths => 2, colors => cl::LightRed, ), ], ), );
You can extend the Simple interface with GUI methods. The next example gives you a first glimps into GUI-flavored programming by overriding the onMouseMove method for this Prima widget. There are many ways of setting, overriding, or adding callbacks in relation to all sorts of GUI events, but when using the plot
command, your only option is to specify it as OnEventName.
In this example, I add (that's add, not override, becaus Prima rocks) a method that prints out the mouse position to the console. I also use logarithmic scaling in the x direction (and specify x- and y-axes labels) to make things interesting:
use strict; use warnings; use PDL::Graphics::Prima::Simple; use PDL; # Turn on autoflush: $|++; my $x = sequence(100)/10 + 1; my $y = sin($x); plot( -data => ds::Pair( $x, $y, plotTypes => [ ppair::Xs, ppair::Lines, ], ), onMouseMove => sub { # Get the widget and the coordinates: my ($self, $button_and_modifier, $x_pixel, $y_pixel) = @_; # Convert the pixel coordinates to real values: my $x = $self->x->pixels_to_reals($x_pixel); my $y = $self->y->pixels_to_reals($y_pixel); # Print the results: print "\r($x, $y) "; }, x => { label => 'time (s)', scaling => sc::Log, }, y => { label => 'displacement (m)', } );
This kind of immediate feedback can be very useful. It is even possible to capture keyboard events and respond to user interaction this way. But please, don't do that. If you need any substantial amount of user interaction, you would do much better to learn to create a Prima application with buttons, lists, and input lines, along with the Plot widget. For that, see the (soon to be written) PDL::Graphics::Prima::InteractiveTut.
There are a couple of ways to call this module. The first is just a simple use statement:
use PDL::Graphics::Prima::Simple;
This will import the above mentioned functions into your local package's symbol table, which is generally what you want. If you do not want any functions imported, you can call it with an empty string:
use PDL::Graphics::Prima::Simple '';
or you can name the functions that you want imported:
use PDL::Graphics::Prima::Simple qw(plot);
In addition, you can specify the default window plot size by passing a two element anonymous array:
# default to 800 x 800 window: use PDL::Graphics::Prima::Simple [800, 800]; # default to 800 wide by 600 tall, import nothing: use PDL::Graphics::Prima::Simple [800, 600], ''; # default to 300 wide by 450 tall, import 'plot' function: use PDL::Graphics::Prima::Simple [300, 450], 'plot';
As an experimental feature, you can provide a subroutine reference in the import method. This subroutine reference will get invoked by the plotting commands instead of the default plotting mechanism. This is of marginal utility, and might be better achieved with a simple glob assignment of *PDL::Graphics::Prima::Simple::plot
to your desired function. The glob assignment has the advantage that it can be local
ized.
Note: I am considering adding a '-hold' option, which would cause all void-context plot commands in scripts to not block, and cause the Prima event loop to be run at the end of the script. However, I haven't settled on either the behavior or the name. Input is welcome!
I am sure there are bugs in this software. If you find them, you can report them in one of two ways:
You can (and should!) join the PDL mailing list, and you should feel free to post questions about this or any other PDL module on that list. For details on how to sign-up, see http://pdl.perl.org/?page=mailing-lists.
The best place to report problems that you are sure are problems is at http://github.com/run4flat/PDL-Graphics-Prima/issues.
For an introduction to Prima see Prima::tutorial.
David Mertens (dcmertens.perl@gmail.com)
Here is the full list of modules in this distribution:
Defines the Plot widget for use in Prima applications
Specifies the behavior of axes (but not the scaling)
Specifies the behavior of DataSets
Defines the lm:: namespace
Specifies a collection of different color palettes
Defines the different ways to visualize your data
Encapsulates all interaction with the Term::ReadLine family of modules.
Specifies different kinds of scaling, including linear and logarithmic
Defines a number of useful functions for generating simple and not-so-simple plots
Unless otherwise stated, all contributions in code and documentation are copyright (c) their respective authors, all rights reserved.
Portions of this module's code are copyright (c) 2011 The Board of Trustees at the University of Illinois.
Portions of this module's code are copyright (c) 2011-2013 Northwestern University.
Portions of this module's code are copyright (c) 2013-2014 Dickinson College.
This module's documentation is copyright (c) 2011-2014 David Mertens.
This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself.