Example 2: 2D Plotting
Introduction
In this tutorial, we will equip the macro module we created in the previous tutorial with a responsive and interactable panel to plot grayscale distributions of single slices as well as defined sequences of slices in 2D.
Steps to do
Open the module definition folder of your macro module and the related .script file in MATE. Then activate the Preview as shown below:
Drag the small Preview window to the bottom right corner of your window where it does not bother you. We will now be adding contents to be displayed there.
Adding the following code to your .script file will open a panel window if the macro module is clicked. This new panel window contains a Matplotlib canvas where the plots will be displayed later on as well as two prepared boxes that we will add functions to in the next step.
BaseNetwork.script
Window {
Category {
Horizontal {
Vertical {
expandY = True
expandX = False
Box {
title= "Single Slice"
}
Box {
title = "Sequence"
}
Empty {
expandY = True
}
}
Box {
MatplotlibCanvas {
expandY = True
expandX = True
name = canvas
useToolBar = True
}
expandY = True
expandX = True
}
}
}
}
Letting a box expand on the x- or y-axis or adding an empty object do so contributes to the panel looking a certain way and helps the positioning of the elements. You can also try to vary the positioning by adding or removing expand-statements or moving boxes from a vertical to a horizontal alignment. Hover over the boxes in the preview to explore the concept.
Now, we need to identify which module parameters we want to be able to access from the panel of our macro:
To plot a slice or a defined sequence of slices, we need to be able to set a start and an end.
Go back into your MeVisLab workspace, right click your BaseNetwork
module and choose “Show Internal Network”.
We now know that we will need SubImage.z
and SubImage.sz
to define the start and end of a sequence.
But there are a few other module parameters that must be set beforehand to make sure the data we extract to plot later is compareable and correct.
To do so, we will be defining a “setDefaults” function for our module. Open the .py file and add the code below.
BaseNetwork.py
def setDefaults():
ctx.field("SubImage.fullSize").touch()
ctx.field("SubImage.autoApply").value = True
ctx.field("Histogram.updateMode").value = "AutoUpdate"
ctx.field("Histogram.xRange").value = "Dynamic Min/Max"
ctx.field("Histogram.useZeroAsBinCenter").value = False
ctx.field("Histogram.binSize").value = 5.0
ctx.field("Histogram.backgroundValue").value = False
ctx.field("Histogram.curveType").value = "Area"
ctx.field("Histogram.useStepFunction").value = True
ctx.field("Histogram.curveStyle").value = 7
As it is also incredibly important, that the values of the parameters we are referencing are regularly updated, we will be setting some global values containing those values.
BaseNetwork.py
startSlice = None
endSlice = None
bins = None
def updateSlices():
global startSlice, endSlice, bins
startSlice = int(ctx.field("SubImage.z").value)
endSlice = int(ctx.field("SubImage.sz").value)
bins = ctx.field("Histogram.binSize").value
Make sure that the variable declarations as none are put above the “setDefaults” function and add the execution of the “updateSlices()” function into the “setDefaults” function, like so:
BaseNetwork.py
def setDefaults():
ctx.field("Histogram.xRange").value = "Dynamic Min/Max"
ctx.field("Histogram.useZeroAsBinCenter").value = False
ctx.field("Histogram.binSize").value = 5.0
ctx.field("Histogram.backgroundValue").value = False
ctx.field("Histogram.curveType").value = "Area"
ctx.field("Histogram.useStepFunction").value = True
ctx.field("Histogram.curveStyle").value = 7
ctx.field("SubImage.fullSize").touch()
ctx.field("SubImage.autoApply").value = True
ctx.field("Histogram.updateMode").value = "AutoUpdate"
updateSlices()
Now we are ensuring, that the “setDefaults” function and therefore also the “updateSlices” function are executed everytime the panel is opened by setting “setDefaults” as a wake up command.
BaseNetwork.script
Commands {
source = $(LOCAL)/BaseNetwork.py
wakeupCommand = "setDefaults"
}
And we add field listeners, so that the field values that we are working with are updated everytime they are changed.
BaseNetwork.script
Commands {
source = $(LOCAL)/BaseNetwork.py
wakeupCommand = "setDefaults"
FieldListener {
listenField = "SubImage.sz"
listenField = "SubImage.z"
listenField = "Histogram.binSize"
command = "updateSlices"
}
}
To see if all of this is working, we need to embed fields into our panel. Put this inside of the box titled “Single Slice”:
BaseNetwork.script
Field "SubImage.sz" {
title = "Plot slice"
}
Button {
title = "in 2D"
command = "singleSlice2D"
}
Button {
title = "in 3D"
command = "click3D"
}
Empty {}
And then add this to your box titled “Sequence”:
BaseNetwork.script
Field "SubImage.z" {
title = "From slice"
}
Field "SubImage.sz" {
title = "To slice"
}
Button {
title = "Plot 2D"
command = "click2D"
}
Button {
title = "Plot 3D"
command = "click3D"
}
Lastly, put this under your two boxes, but above the empty element in the vertical alignment:
BaseNetwork.script
Field "Histogram.binSize" {
title = "Bin size"
}
If you followed all of the listed steps, your panel preview should look like this and display all the current parameter values.
We can now work on the functions that visualize the data as plots on the Matplotlib canvas. You will have noticed how all of the buttons in the .script file have a command. Whenever that button is clicked, its designated command is executed. However, for any of the functions referenced via command to work, we need one that ensures, that the plots are shown on the integrated Matplotlib canvas. We will start with that one.
BaseNetwork.py
def clearFigure():
control = ctx.control("canvas").object()
control.figure().clear()
Now that this is prepared and ready, we can add the functions to extract the data:
BaseNetwork.py
def getX():
x = ctx.field("Histogram.outputHistogramCurve").object().getXValues()
stringx = ",".join([str(i) for i in x])
xValues = stringx.split(",")
return [float(s) for s in xValues]
def getY():
y = ctx.field("Histogram.outputHistogramCurve").object().getYValues()
stringy = ",".join([str(i) for i in y])
yValues = stringy.split(",")
return [float(s) for s in yValues]
And lastly enable the plotting of a single slice as well as a sequence in 2D through our panel by adding the code below.
BaseNetwork.py
def singleSlice2D():
global endSlice
lastSlice = endSlice
ctx.field("SubImage.z").value = endSlice
click2D()
ctx.field("SubImage.z").value = lastSlice
def plotSequence():
clearFigure()
figure = ctx.control("canvas").object().figure()
values = [i for i in range(startSlice, endSlice + 1)]
if len(values) <= 4:
# adapt the height of the subplot to the number of plots
sub = 100 * len(values) + 11
for i in values:
subplot = figure.add_subplot(sub)
sub += 1
ctx.field("SubImage.z").value = i
ctx.field("SubImage.sz").value = i
subplot.bar(getX(), getY(), bins, color='r', label=f'Slice {i}')
subplot.legend([f'Slice {i}'])
else:
subplot = figure.add_subplot()
for i in values:
ctx.field("SubImage.z").value = i
ctx.field("SubImage.sz").value = i
subplot.plot(getX(), getY(), bins)
subplot.legend([f'Slice {i}' for i in values])
ctx.field("SubImage.z").value = values[0]
figure.canvas.draw()
def click2D():
clearFigure()
figure = ctx.control("canvas").object().figure()
if startSlice == endSlice:
subplot = figure.add_subplot(111)
subplot.bar(getX(), getY(), bins, color='b', label=f"Slice {endSlice}")
subplot.legend()
subplot.plot()
figure.canvas.draw()
else:
plotSequence()
You should now be able to reproduce results like these:
You can download the .py file below if you want.
Summary
- Functions are connected to fields of the panel via commands
- The panel preview in MATE can be used to alter positioning of panel components without touching the code
- An “expand” statement can help the positioning of components in the panel