Skip to content
William Wood edited this page Nov 30, 2022 · 52 revisions

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.

Contents

Creating a Plot

Top ↑

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:

Quickly Plotting a ResultTable

Top ↑

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:

  1. Voltage vs Time
  2. Current vs Time
  3. 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).

Data Series

Top ↑

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))

Error Bars

Top ↑

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:

Plotting Data in a ResultTable

Top ↑

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

Plotting Two Columns Against Each Other

Top ↑

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}.

Two Columns with Error Bars

Top ↑

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:

Split Data into Sub-Series

Top ↑

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))

Filtering

Top ↑

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.

Plotting Functions of Columns

Top ↑

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)
)

Styling Series

Top ↑

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.

Series Colour

Top ↑

To set the colour of a series, use setColour(...) like so:

series.setColour(Colour.PURPLE);

Colour Sequence for Sub-Series

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.

Marker Visibility, Shape and Size

Top ↑

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);

Line Visibility and Width

Top ↑

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);

Chaining Series Methods

Top ↑

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.

Fitted Lines

Top ↑

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:

Point Ordering

Top ↑

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:

Working with Large Data Sets

Top ↑

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 $x$ seconds of it, for instance.

Controlling Plots

Top ↑

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

Axis Limits

Top ↑

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();

Axis Types

Top ↑

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);

User Interaction

Top ↑

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.

Toolbar Buttons

Top ↑

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();

Saving Plots

Top ↑

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:

Save Buttons

Top ↑

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.

Clone this wiki locally