-
Notifications
You must be signed in to change notification settings - Fork 9
Plots
It is often the case that you will want to plot experimental measurements as
they are being taken. To allow this, JISA
provides the Plot
class as part of
its GUI
package.
This offers a straight-forward means of creating, managing and displaying plots.
- Creating a Plot
- Quickly Plotting a ResultTable
- Data Series
- Error Bars
- Plotting Data in a ResultTable
- Styling Series
- Chaining Series Methods
- Fitted Lines
- Point Ordering
- Working with Large Data Sets
- Controlling Plots
- User Interaction
- Toolbar Buttons
- Saving Plots
- Save Buttons
The first thing to do is to create a Plot
object. This is easily done like so:
Java
Plot myPlot = new Plot("Title", "X Label", "Y Label");
Kotlin
val myPlot = Plot("Title", "X Label", "Y Label")
Python
myPlot = Plot("Title", "X Label", "Y Label")
This will create a plot with the specified title and axis labels. If we now show it like so:
myPlot.show();
we will see the following window appear:
When creating a Plot
you can give it a ResultTable
object and will
automatically plot each column in it as a separate series against the first
column.
For example, let's say we have the following:
val TIME = Column.ofDecimals("Time", "s")
val VOLTAGE = Column.ofDecimals("Voltage", "V")
val CURRENT = Column.ofDecimals("Current", "A")
val POWER = Column.ofDecimals("Power", "W")
val results = ResultList(TIME, VOLTAGE, CURRENT, POWER)
Then if we were to create a Plot
like so:
val plot = Plot("Plot Title", results);
we will end up with a plot of all but the first column plotted against the first column. In our case that means:
- Voltage vs Time
- Current vs Time
- Power vs Time
When shown (plot.show()
) we will see
If you want a different column on the x-axis, then specify its Column<...>
object after the ResultTable
like so
val plot = Plot("Plot Title", results, VOLTAGE)
which would result in:
Note that the "Power [W]" series is just very small so it's completely hidden behind the "Current [A]" series. It's not actually missing. (JISA plots series in reverse order, hence earlier series get plotted over later ones).
Points on a plot are grouped into different series. Each series in a Plot
is
represented by a Series
object. To create a new series, call createSeries()
on the relevant Plot
object, like so:
Java
Series series = myPlot.createSeries()
Kotlin
var series = myPlot.createSeries()
Python
series = myPlot.createSeries()
You can specify extra options by chaining methods like so:
series.setName("Name")
.setColour(Colour.TEAL)
.setLineWidth(2.5)
.setMarkerVisible(true);
In the above example, we have set the name of the series to "My Data", set it to appear teal in colour, set its line width to 2.5 pixels and told it to show markers for each data point.
Since these are chainable, you can call them all in one go when creating the series like so:
Series series = myPlot.createSeries()
.setName("Name")
.setColour(Colour.TEAL)
.setLineWidth(2.5)
.setMarkerVisible(true);
Now, to add data to the series, use the addPoint(...)
method like so:
series.addPoint(0, 10);
series.addPoint(1, 42);
series.addPoint(2, 12);
series.addPoint(3, 33);
series.addPoint(4, 95);
series.addPoint(5, 25);
The result looks like:
Alternatively:
Java
for (double x : Range.linear(0, 2.0 * Math.PI, 100)) {
series.addPoint(x, Math.sin(x));
}
Kotlin
for (x in Range.linear(0, 2.0 * PI, 100)) {
series.addPoint(x, sin(x))
}
Python
for x in Range.linear(0, 2.0 * PI, 100):
series.addPoint(x, sin(x))
You can add y-axis error bars to your data points by specifying the +/- error
value as a third argument when adding a point to a Series
:
series.addPoint(xValue, yValue, yError);
For instance, if we take our previous example, then we can add a +/- 0.2
error
bar to each point like so:
for (x in values) {
series.addPoint(x, sin(x), 0.2)
}
resulting in error bars like so
If we wanted both x and y error bars, then just specify two error values like so:
series.addPoint(xValue, yValue, xError, yError)
If we thus modified our previous example to the following:
for (x in values) {
series.addPoint(x, sin(x), 0.2, 0.2)
}
then we would get:
If you only want x error bars, then do the same, but leave the yError
value as
0.0
:
for (x in values) {
series.addPoint(x, sin(x), 0.2, 0.0)
}
resulting in:
After creating a series:
val series = plot.createSeries()
you can make that series automatically track the contents of a ResultTable
in
a number of different ways. Specifically, you can:
- Plot one column against another
- Plot one column against another using a third (and fourth) for error bar values
- Split data into sub-series based on the value of a column
- Only plot certain points based on a filter
- Plot (and everything else listed above) functions of columns by specifying lambda functions
The most basic example of making a Series
watch a ResultTable
is where it is
set to plot one column against another. First, let's assume we have the
following Plot
and a ResultTable
that has been filled with data:
val plot = Plot("Plot Title")
val TIME = Column.ofDecimals("Time")
val VOLTAGE = Column.ofDecimals("Voltage")
val CURRENT = Column.ofDecimals("Current")
val POWER = Column.ofDecimals("Power")
val results = ResultList(TIME, VOLTAGE, CURRENT, POWER)
and that we have now created a new series:
val series = plot.createSeries()
You can then instruct this series to follow two of the columns in your
ResultTable
by use of the watch(...)
method:
series.watch(results, xCol, yCol)
where xCol
and yCol
are the Column<...>
objects of the x-axis and y-axis
columns respectively. For instance, if we wanted to plot TIME
on the x-axis
and POWER
on the y-axis:
series.watch(results, TIME, POWER)
This whole process can be condensed by chaining methods like so:
val series = plot.createSeries().watch(results, TIME, POWER)
Java Example
// Define our columns
Column<Double> TIME = Column.ofDecimals("Time", "s");
Column<Double> VOLTAGE = Column.ofDecimals("Voltage", "V");
Column<Double> CURRENT = Column.ofDecimals("Current", "A");
Column<Double> POWER = Column.ofDecimals("Power", "W");
// Create a table using said columns
ResultList results = new ResultList(TIME, VOLTAGE, CURRENT, POWER);
// Create a plot, create a series in that plot that watches
// the table and plots the TIME and POWER columns on x and y.
Plot plot = new Plot("Plot Title");
Series series = plot.createSeries().watch(results, TIME, POWER);
// Show the plot in its own window
plot.show();
Kotlin Example
// Define our columns
val TIME = Column.ofDecimals("Time", "s")
val VOLTAGE = Column.ofDecimals("Voltage", "V")
val CURRENT = Column.ofDecimals("Current", "A")
val POWER = Column.ofDecimals("Power", "W")
// Create a table using said columns
val results = ResultList(TIME, VOLTAGE, CURRENT, POWER)
// Create a plot, create a series in that plot that watches
// the table and plots the TIME and POWER columns on x and y.
val plot = Plot("Plot Title")
val series = plot.createSeries().watch(results, TIME, POWER)
// Show the plot in its own window
plot.show()
Python Example
# Define our columns
TIME = Column.ofDecimals("Time", "s")
VOLTAGE = Column.ofDecimals("Voltage", "V")
CURRENT = Column.ofDecimals("Current", "A")
POWER = Column.ofDecimals("Power", "W")
# Create a table using said columns
results = ResultList(TIME, VOLTAGE, CURRENT, POWER)
# Create a plot, create a series in that plot that watches
# the table and plots the TIME and POWER columns on x and y.
plot = Plot("Plot Title")
series = plot.createSeries().watch(results, TIME, POWER)
# Show the plot in its own window
plot.show()
All of these will result in:
Notice how the plot has automatically adopted the name and units of the plotted columns. JISA plots are programmed to recognise a certain set of standard units (including, in this case, Watts) that it can use SI prefixes in front of. Hence, why the y-axis has changed to mW despite the
POWER
column having units of W. That is, JISA plots can automatically scale columns with SI prefixes for the following units: {m, s, K, g, N, Pa, V, A, W, Ohm, Ω, Hz, V/K, F, H, J, eV}.
If we have a third column that we want to use for error values, then we can do so by specifying its index after those for the x-column and y-column like so:
// For y error bars
series.watch(results, xCol, yCol, eYCol);
// For x and y
series.watch(results, xCol, yCol, eXCol, eYCol);
For instance, let's say we have the following:
val TIME = Column.ofDecimals("Time", "s")
val VOLTAGE = Column.ofDecimals("Voltage", "V")
val CURRENT = Column.ofDecimals("Current", "A")
val POWER = Column.ofDecimals("Power", "W")
val ERROR = Column.ofDecimals("Power Error", "W")
val results = ResultList(TIME, VOLTAGE, CURRENT, ERROR)
val plot = Plot("Plot Title")
then we could plot "Power" vs "Time" with error bars defined by "Error" like so:
plot.createSeries().watch(results, TIME, POWER, ERROR)
which would look something like:
After creating a Series
object that you have told to watch(...)
two columns
in a ResultTable
, you can further instruct it to automatically split the data
it plots into separate sub-series depending on the value of a third column.
This us done by use of split(splitCol)
where splitCol
is the column object
of the column to split by. For example, let's say we have the following
ResultTable
and Plot
:
val VOLTAGE = Column.ofDecimals("Voltage", "V")
val CURRENT = Column.ofDecimals("Current", "A")
val GATE = Column.ofDecimals("Gate", "V")
val results = ResultList(VOLTAGE, CURRENT, GATE)
val plot = Plot("Plot Title")
and we want to plot Current
vs Voltage
but grouped into series based on the
value in Gate
. If we've gone ahead and created a series that tracks Current vs
Voltage like so:
val series = plot.createSeries().watch(results, VOLTAGE , CURRENT)
then we can make it split the data into sub-series based on the value in GATE
by calling split(...)
on it:
series.split(GATE)
Just as before, this can all be chained together like so:
val series = plot.createSeries()
.watch(results, VOLTAGE, CURRENT)
.split(GATE)
The result of this will look something like:
You can further format the names generated for each sub-series by specifying a formatting pattern:
series.split(GATE, "Gate: %s V")
which will result in the following:
If need be, you can specify a lambda for the series name generation instead:
Java
series.split(GATE, row -> String.format("Gate: %s V", row.get(GATE)));
Kotlin
series.split(GATE) { row -> "Gate: ${row[GATE]} V"}
// Or, using the Kotlin "it" shortcut for single-argument lambdas
series.split(GATE) { "Gate: ${it[GATE]} V"}
Python
series.split(GATE, lambda row: "Gate: %s V" % row.get(GATE))
You can specify which points to actually plot from a ResultTable
by sepcifying
a Predicate
lambda as a filter. For instance, if we have the following:
val TIME = Column.ofDecimals("Time", "s")
val VOLTAGE = Column.ofDecimals("Voltage", "V")
val CURRENT = Column.ofDecimals("Current", "A")
val POWER = Column.ofDecimals("Power", "W")
val ERROR = Column.ofDecimals("Voltage Error", "V")
val results = ResultList(TIME, VOLTAGE, CURRENT, ERROR)
val plot = Plot("Plot Title")
val series = plot.createSeries().watch(results, TIME, VOLTAGE)
but we wanted our series to only plot rows from results
where the Power column
has a value greater than 25e-3
then we can call filter(...)
on the series.
We need to give this method a lambda function that takes a single row and
returns either true
or false
depending on whether we want to plot it or not.
Recall that individual rows in a ResultTable
are represented by Row
objects
and that for a Row
, r
, the values inside it are accessed by use of
r.get(column)
(or r[column]
in Kotlin). Therefore, if we wanted to check if
column POWER
is greater than 25e-3
we would have something like
r.get(POWER) > 25e-3
.
Java
series.filter(r -> r.get(POWER) > 25e-3);
Kotlin
series.filter { r -> r[POWER] > 25e-3 }
// or
series.filter { it[POWER] > 25e-3 }
Python
series.filter(lambda r: r.get(POWER) > 25e-3)
As with all these methods, we can chain them:
plot.createSeries()
.watch(results, TIME, VOLTAGE)
.filter {r -> r[POWER] > 25e-3};
This will then only plot (TIME, VOLTAGE) points from the ResultTable
from rows
where POWER
is greater than 25mW.
Anywhere that you need to specify a column index, you can instead specify a lambda with a single argument (representing a row) that returns a value based on some function of the values in that row.
For instance, if we had:
val TIME = Column.ofDecimals("Time", "s")
val VOLTAGE = Column.ofDecimals("Voltage", "V")
val CURRENT = Column.ofDecimals("Current", "A")
val results = ResultList(TIME, VOLTAGE, CURRENT)
but we wanted to plot Power vs Time (current times voltage) then we could define
this using a lambda that returns VOLTAGE * CURRENT
.
Recall that individual rows in a ResultTable
are represented by Row
objects
and that for a Row
, r
, the values inside it are accessed by use of
r.get(column)
(or r[column]
in Kotlin). Therefore, if we wanted VOLTAGE * CURRENT
, we would have something like r.get(VOLTAGE) * r.get(CURRENT)
(or
r[VOLTAGE] * r[CURRENT]
in Kotlin).
The exact syntax used to define a lambda function depends on the language you are using. Here's our example written in Java, Kotlin and Python:
Java
r -> r.get(VOLTAGE) * r.get(CURRENT)
Kotlin
{ r -> r[VOLTAGE] * r[CURRENT] }
// OR
{ it[VOLTAGE] * it[CURRENT] }
Python (needs to be wrapped in toEvaluable(...)
for PyJISA)
toEvaluable(lambda r: r.get(VOLTAGE) * r.get(CURRENT))
When using a lambda in place of a column number for a method call, all other columns must be specified this way too. So for our example:
Java
series.watch(results, r-> r.get(TIME), r -> r.get(VOLTAGE) * r.get(CURRENT));
Kotlin
series.watch(results, { r -> r[TIME] }, { r -> r[VOLTAGE] * r[CURRENT] })
// OR
series.watch(results, { it[TIME] }, { it[VOLTAGE] * it[CURRENT] })
Python
series.watch(
results,
lambda r: r.get(TIME),
lambda r: r.get(VOLTAGE) * r.get(CURRENT)
)
Now that we have a data series, in the form of a Series
object, we can
customise how it looks somewhat. Specifically we can:
- Change its colour
- Show/hide its markers
- Change its marker shape
- Change the line width
- Hide the line entirely
If the series you are styling has sub-series, then they will also be styled the same way.
To set the colour of a series, use setColour(...)
like so:
series.setColour(Colour.PURPLE);
When generating sub-series within a series (for example if using split(...)
or
watchAll(...)
), they are coloured according to a list of colours. The first
sub-series will be the first colour in the list, the second the second in the
list etc. When the end of the list is reached, it begins again at the first
colour.
To change the contents of this list, use the setColourSequence(...)
method
like so:
series.setColourSequence(colour1, colour2, colour3, etc...);
For instance, if we wanted sub-series generated by the series to go blue, green, orange, teal (then back to blue, etc):
series.setColourSequence(Colour.BLUE, Colour.GREEN, Colour.ORANGE, Colour.TEAL);
This will immediately update the colours for any existing sub-series as well as making sure any new sub-series are coloured accordingly.
We can hide the data point markers like so:
series.setMarkerVisible(false);
You can also change which symbol is used for the markers like so:
series.setMarkerShape(shape);
series.setMarkerSize(size);
The available shapes are:
Series.Shape.CIRCLE
Series.Shape.DOT
Series.Shape.SQUARE
Series.Shape.TRIANGLE
Series.Shape.STAR
Series.Shape.DIAMOND
Series.Shape.DASH
Series.Shape.PLUS
Series.Shape.CROSS
For example:
series.setColour(Colour.PURPLE);
series.setMarkerShape(Series.Shape.STAR);
series.setMarkerSize(10.0);
// or, all in one go:
series.setColour(Colour.PURPLE)
.setMarkerShape(Series.Shape.STAR)
.setMarkerSize(10.0);
You can alter the line width using:
series.setLineWidth(15.0);
To hide the line altogether you can use:
series.setLineVisible(false);
To make it show again just supply it with true
instead:
series.setLineVisible(true);
All Series
methods that don't otherwise return any value instead return a
self-reference. In basic terms, this means you can chain further method calls
onto the end of them like so:
series.setName("My Data Series").setColour(Colour.BLACK).setLineVisible(false);
In Java and Kotlin, this is often neater when each chained method is on a new line:
series.setName("My Data Series")
.setColour(Colour.BLACK)
.setLineVisible(false);
You can chain like this straight after after creating the series too:
Series series = plot.createSeries()
.setName("Voltage")
.setLineWidth(1.0)
.setMarkerVisible(false)
.watch(results, 0, 1);
This can be a nice way of defining the series in a Plot
as it reads well and
keeps everything to do with the name/style/contents of your series to a single
statement.
Normally, when plotting data, the line drawn between points is simply a linenar
interpolation. However, you can tell a series to use a fitted line instead by
use of either polyFit(...)
or the more general fit(...)
methods.
For instance, if we wanted the line to instead be a third-order polynomial fit:
series.polyFit(3);
which would result in something like:
This fitting mode will also be applied to any sub-series already existing in your series or any created subsequently:
If you want a different fit then you can use the fit(...)
method instead. This
requires a Fitter
lambda expression which takes the plotted data and returns a
Fit
object. For more information about Fit
objects and fitting methods,
please refer to the "Mathematical Functions and Fittings" page.
Let's say we want to fit a Gaussian to the following points:
Recall that a Gaussian fit can be performed by using:
GaussianFit fit = Fitting.gaussianFit(data);
Therefore, we would want to give fit(...)
something like:
Java
series.fit(data -> Fitting.gaussianFit(data));
// Or, using method reference
series.fit(Fitting::gaussianFit);
Kotlin
series.fit { data -> Fitting.gaussianFit(data) }
series.fit { Fitting.gaussianFit(it) }
// Or, using method reference
series.fit(Fitting::gaussianFit)
Python
series.fit(lambda data: Fitting.gaussianFit(data))
# Or, using method reference
series.fit(Fitting.gaussianFit)
Which results in:
By default, points added to data series will be plotted in the order that they are added. Therefore, when the line is drawn (if visible) to connect the points, it will follow that same order.
This is important if you cannot guarantee that the data you want to plot will be in the correct order. For instance, if we had data of voltage vs time, and we wanted to plot time on the x-axis and voltage on the y-axis. If the data is not specified sorted by time, the line joining the points will jump back and forth between points and look messy:
Therefore, we want the plot to sort the data by its x-axis value before plotting. We can configure what the plot order our series by, through use of:
series.setPointOrder(Series.Ordering.NONE); // As it comes
series.setPointOrder(Series.Ordering.X_AXIS); // Ordered by x
series.setPointOrder(Series.Ordering.Y_AXIS); // Ordered by y
In our case, we want to order by the x-axis values, thus we'd write:
series.setPointOrder(Series.Ordering.X_AXIS);
resulting in:
If you are planning on recording a large amount of data, then you may well run
into memory issues. For example, if you are using a ResultList
to store data
then each data point will take up space in memory. This issue is compounded if
you then plot it because each point on the plot also takes up memory. For this
reason, JISA
provides the ResultStream
implementation of the ResultTable
which instead writes data directly to a file rather than keeping it in memory.
In the current version of JISA, plots will automatically cull points from the
plot as their density on the screen goes. For instance, if two points have
markers within a few pixels of each other, one of them will be culled.
Furthermore, Plot
objects now simply draw a rasterised image in the plotting
area, rather than working with individual polygon/path objects, thus leading to
significant memory savings.
However, the co-ordinates of each point that is plotted do need to be held in memory (including those culled, since they'd need to be plotted again if the user were to zoom in, for instance). This is done in a very memory-efficient way, but for very large sets of data, you may still run into out of memory issues. In these cases, a limit on the number of points can be set. When the total number of points exceeds this limit, the oldest points will be removed to compensate. To enable this, use:
series.setLimit(maxLimit);
To disable this after enabling it, just supply 0
as the limit:
series.setLimit(0);
This can be useful if you are logging something as a function of time, and only
really need to see that last
There are several ways of controlling the plot. These controls relate to things pertinent to the entire plot, not just a single series. These include:
- Axis limits and labels
- Setting how to display each axis
- How lines are drawn
You can manually set the limits on each axis like so:
myPlot.setXLimits(minX, maxX);
myPlot.setYLimits(minY, maxY);
// Or, individually
myPlot.setXMin(minX);
myPlot.setXMax(maxX);
myPlot.setYMin(minY);
myPlot.setYMax(maxY);
or, to return them to auto:
myPlot.autoRangeX();
myPlot.autoRangeY();
To check them:
double xMin = myPlot.getXMin();
double xMax = myPlot.getXMax();
double yMin = myPlot.getYMin();
double yMax = myPlot.getYMax();
Axes on Plot
objects can be one of three types:
- Linear
- Logarithmic
- Temporal (Linear)
These are set by using the set...AxisType(...)
methods:
// X Axis
myPlot.setXAxisType(Plot.AxisType.LINEAR);
myPlot.setXAxisType(Plot.AxisType.LOGARITHMIC);
myPlot.setXAxisType(Plot.AxisType.TIME);
// Y Axis
myPlot.setYAxisType(Plot.AxisType.LINEAR);
myPlot.setYAxisType(Plot.AxisType.LOGARITHMIC);
myPlot.setYAxisType(Plot.AxisType.TIME);
It is possible to let the user manually pan and zoom using their mouse by enabling mouse commands. This can be done by use of:
plot.setMouseEnabled(true); // Enabled
plot.setMouseEnabled(false); // Disabled
When enabled, this lets the user pan by holding CTRL
while dragging with their
primary button (normally left) or by dragging with the middle mouse button. They
can zoom by drawing a rectangle by dragging with the secondary/right mouse
button and can reset to automatic axis limits by double-clicking.
You can also set what should happen when a user clicks on the plot. This can be
done by use of addClickListener(...)
. This is used like so:
Java
plot.addClickListener((event, x, y) -> { /* code here */});
Kotlin
plot.addClickListener { event, x, y -> /* code here */}
Python
def onClick(event, x, y):
# Code here
plot.addClickListener(onClick)
Whenever the plot is clicked, the lambda will be called. The event
object
contains details about the click (which mouse button(s), how many clicks, etc),
while the x
and y
arguments will be co-ordinates on the plot that click
occurred at.
You can add toolbar buttons to a plot by use of the addToolbarButton(...)
method:
plot.addToolbarButton(buttonText, buttonAction);
where buttonText
is the text to be displayed on the button and buttonAction
is either a lambda function or method reference that will be run when the button
is clicked.
For instance, if we wanted to add a button that saves the plot to a fixed file-name when clicked:
Java
plot.addToolbarButton("Save", () -> {
plot.saveSVG("saved-plot.svg");
GUI.infoAlert("Plot saved!");
});
Kotlin
plot.addToolbarButton("Save") {
plot.saveSVG("saved-plot.svg")
GUI.infoAlert("Plot saved!")
}
Python
def savePlot():
plot.saveSVG("saved-plot.svg")
GUI.infoAlert("Plot saved!")
plot.addToolbarButton("Save", savePlot)
Calling addToolbarButton(...)
returns a Button
object that represents the
button you've just added:
Button button = plot.addToolbarButton(...);
x You can then use this object to do things like remove, hide, disable and change the text on the button:
button.setDisabled(true/false);
button.setVisible(true/false);
button.setText("New Text");
button.remove();
Plots can be saved as either png bitmap images, or svg vector graphics.
// As PNG file
myPlot.savePNG(fileName, width, height);
// As SVG file
myPlot.saveSVG(fileName, width, height);
// As PGFPlots tex code for a LaTeX document
myPlot.setTex(fileName);
For example, if we had the following plot:
and on it we called
myPlot.saveSVG("plot.svg", 600.0, 500.0);
then this will save the plot as an SVG file called plot.svg
with a size of
600x500 that looks like:
You can add a pre-made save button to your plot toolbar by using:
plot.addSaveButton("Button Text");
This will add a button to the toolbar with the specified button text that will open a plot save dialogue window when clicked.
- Getting Started
- Object Orientation
- Choosing a Language
- Using JISA in Java
- Using JISA in Python
- Using JISA in Kotlin
- Exceptions
- Functions as Objects
- Instrument Basics
- SMUs
- Thermometers (and old TCs)
- PID and Temperature Controllers
- Lock-Ins
- Power Supplies
- Pre-Amplifiers
- Writing New Drivers