Plot aspect ratio control

If a plot has the same units on both X and Y axes, could there be an option to ensure that the scale factors on the axes are the same and a curve, or colour plot, maintains its correct aspect ratio? This would ideally apply both on screen and when printed. This could be either by rescaling one axis or by changing the layer shape/size, introducing extra blank space on the data’s short axis rather than cropping the long one.

My present use case is for beam camera images (cm on both axes).

Better still, could we optionally set an absolute scale factor in printed cm / screen pixels per 1 axis unit, so I can print the image at its actual size or magnified/reduced by a known amount? Or be able to export a Colour Fill plot at exactly one image pixel per workspace bin?

Hi James,

There’s an option on the Format/Plot dialog called “Keep aspect ratio on window resize” (below), which maintains the image’s aspect ratio on resizing. Is this similar to what you need, or did you mean an option on the plot command to ensure the scale factors of X and Y are the same when the graph is first plotted? (It could then automatically set the option below to ensure the aspect ratio stays the same).

When exporting an image, there’s an option for “Keep aspect ratio” when exporting to PDF, EPS and PS formats (under “advanced” in the save dialog from right click/Export). The only page sizes available under “Custom page size” are standard A0-9, B0-9 though. Would adding an option for “actual size” help here? It could have a box to enter the magnification factor (fractional for reduction).

This would still need a way of setting a scale factor for the axes, though, so that one unit was a set length or number of pixels. Perhaps if it was possible to set the layer size (as above) in cm as well as pixels - then if the axes were 0-10 cm, the layer size could be set to 10x10 cm?

Hi Tom,

I meant the second option, so it chooses the same data units/pixel ratio on both axes and keeps them equal following subsequent resizing of the window.

I note that on the Mantid I have on my PC (3.7.20160725.927) I don’t have the option you highlighted. The other “Keep aspect ratio” is ticked but only enforces keeping the ratio when adjusting the size numbers next to it, not window resizing.

The layer size in pixels here includes border space for axis ticks, tick numbers and titles, and (for example) changing the precision of the Y tick labels moves the left axis in or out to make space, changing the data’s scale factor. Resizing, with a fixed aspect ratio for the outer frame, would change the data’s aspect ratio if the axis fonts require a constant number of pixels. A better option would be to specify the size, position and aspect ratio of the axes frame and let the ticks, etc. stick out beyond the edges. Origin does it that way.

I can imagine being able to choose three options for what happens on resizing:
1: As now, no aspect ratio set. The axes cover the same numeric range and everything stretches.
2. The axes keep their units/pixel equal to each other and either the layer no longer fills the window, leaving blank space top+bottom or left+right as the case may be, or the range of one axis extends so that the layer still fills the window, so showing more of the data in one direction. Opening the axis dialog “scale” tab in this state should have a message implying “display axis extended to fix aspect ratio” as changes to the numbers might not affect the graph.
3. The units/pixel is fixed to a specified number. Either the layer doesn’t fill the window, or is cropped at the edges / has scroll bars, or the data axes adjust to show more or less of the data at the same scale. The axis scale tab would only have the “From” value editable, with “To” calculated from the window size.

Now we come to the problem of printing, and guessing the size of the plot. Should Mantid assume a certain number of pixels/cm, or ask the OS for a value, or can I enter a units/cm value for printing as well as a units/pixel one for on screen use? And how do I ensure the plot is laid out correctly for printing (e.g. legends not overlapping any data points) if working on a small laptop screen?

Hi James,

Thanks. The box I highlighted is only visible for Spectrograms (2D color fill plots) and ticking it does something like your option 2. This option isn’t exposed to Python yet, though, so doing this might be a good first step - in this case we would also need a way to specify in the plot command that the scales should be the same for both axes.

I think that using matplotlib might be an easier way to do this, as there is an aspect='equal' option you can pass to the plot command, and you can specify the figure size. I tweaked the script in another forum post Export publication quality images - #3 by Nina to plot a workspace with the right aspect ratio and export the figure at the right size (from which it can be printed). Let me know if this helps, or if it would be better to add the ability to do this to Mantidplot:

import numpy as np
import matplotlib.pyplot as plt

CM = 0.393701 # inches
PAD = 1.08 # for axis labels etc
DPI = 170 # change this for your monitor

# ------------------------------------------------------------------------------
# Create figure
# ------------------------------------------------------------------------------
def CreateFigure3D(wksp, title, xlabel, ylabel,
                   rasterized=False, aspect='auto'):
    # prepare data for treatment by matplotlib
    # Signal values (really the Z axis)
    intensity = wksp.extractY()
    xmin, xmax = wksp.readX(0)[0], wksp.readX(0)[-1]
    xstep = wksp.readX(0)[1] - xmin
    x = np.arange(xmin, xmax + xstep/2.,xstep)
    ymin, ymax = wksp.getAxis(1).getMin(), wksp.getAxis(1).getMax()
    ystep = wksp.getAxis(1).getValue(1) - ymin
    y = np.arange(ymin, ymax + ystep/2.,ystep)
    xx, yy = np.meshgrid(x, y)

    fig, ax1 = gui_cmd(plt.subplots, 1, 1, dpi=DPI)
    ax1.set_aspect(aspect)
    cmesh = gui_cmd(ax1.pcolormesh, xx, yy, intensity)
    cmesh.set_rasterized(rasterized)
    ax1.set_title(title)
    ax1.set_xlabel(xlabel)
    ax1.set_ylabel(ylabel)
    return fig

# ------------------------------------------------------------------------------
# Create a test workspace for demo - replace with actual workspace
# ------------------------------------------------------------------------------
def createTestWS(nPoints):
    ws=WorkspaceFactory.create("Workspace2D", XLength=nPoints + 1, YLength=nPoints, NVectors=nPoints)
    for i in range(nPoints):
        for j in range(nPoints):
            ws.dataX(i)[j]=(j - 2.0)*1.0
            ws.dataY(i)[j]=(i - 2.0)**2 + (j - 2.0)**2
        ws.dataX(i)[nPoints] = 3.0

    # X axis
    xAxis = ws.getAxis(0)
    xAxis.setUnit("Label").setLabel('X', 'cm')

    # Y axis
    ba = BinEdgeAxis.create(nPoints + 1)
    for i in range(nPoints + 1):
        ba.setValue(i, (i - 2.0))
    ws.replaceAxis(1, ba)
    ws.setYUnitLabel("Y")
    ws.setYUnit("cm")

    return ws
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Get x axis units
# ------------------------------------------------------------------------------
def getXUnits(workspace):
    unit = workspace.getAxis(0).getUnit()
    label_x = unit.caption()
    if label_x != "":
        label_x = label_x + " (" + unit.symbol().ascii() + ")"
    return label_x

# ------------------------------------------------------------------------------
# Get y axis units
# ------------------------------------------------------------------------------
def getYUnits(workspace):
    return workspace.YUnitLabel() + " (" + workspace.YUnit() + ")"
# ------------------------------------------------------------------------------

# Create workspace
histo = createTestWS(5)

# Plot using matplotlib
fig = CreateFigure3D(histo, xlabel=getXUnits(histo),
                     ylabel=getYUnits(histo), title=histo.name(),
                     rasterized=False, aspect='equal') # equal aspect ratio

# Set the figure to the correct size.
# Size includes axis labels etc so must include "padding" space for these.
fig.set_size_inches(5*CM + PAD, 5*CM + PAD, forward=True)
fig.tight_layout(pad=PAD)

# To display the plot:
gui_cmd(fig.show)
# To save to file
fig.savefig(filename=r'C:\test_save3d.eps', dpi=fig.dpi)`

Hi Tom,

Yes, the “Keep aspect ratio on window resize” is there on my 2D plots. If I tick it, then on the next resize the window snaps to 1:1 aspect ratio, whatever it had been, and maintains that when subsequently resized, using the X range only so either cropped top/bottom or white space top/bottom. That’s good. Could we have the option on all plot types?

But if I overlay line data on a colour plot, the resulting graph no longer has the tick box and resizing it squashes the data (keeping the lines and image aligned with each other):
g=plot2D(img)
g2=plotSpectrum(EllipseCurves,[0,1,2])
mergePlots(g,g2)
(note the reverse merge option mergePlots(g2,g) hides the colour fill image leaving just the lines)

I tried your matplotlib code with one of my beam camera images (520 spectra of 696 bins, 80 pixels/cm on both axes). It came out with a 1:1 aspect ratio on the printer but scaled down by a factor 0.63. I then modified the line
fig.set_size_inches(696.0/80.0CM + PAD, 520.0/80.0CM + PAD, forward=True)
and it looks fairly close to actual size. (I printed the .eps from Ghostscript and used the WIndows GDI driver the first time and Postscript printer the second, but the preview images in the Ghostscript window looked different).

However the .eps file is 71 Mbytes and took ages to generate and print!

Hi James,

I’ve opened an issue to add the “keep aspect ratio on window resize” option for all plot types, and also expose it to Python.

When merging data from a color fill plot into a 1D graph, I think the result is still treated as a 1D graph and so the box is not shown. If the option is made available for all plot types then that should fix that problem.

For the large EPS file, you could try using rasterized=True in the CreateFigure3D command, as I’ve seen this can reduce the file size in some cases - though I think this makes more difference for a set of lines than a plot of pixels. Sometimes converting to PDF using something like epstopdf can reduce the file size - is there a particular file format you need the output to be in?

Thanks for opening the issue.

I don’t need any specific format just yet. Your example code happened to generate .eps and I then wanted to find out the scale when printed.

Two use cases are getting a publication quality image in a format that can be included in a journal article or other document (most likely written in LaTeX in my case) and getting a bitmap, ideally with pixels matching data bins, that can then be processed further, for example making a movie. For the first, eps or pdf are suitable. The colour fill plot’s data could be converted to an “image” of one pixel to one cell to be included on the page at an appropriate scale factor while the axes, titles, etc are included as lines and text. At the moment it’s probably drawing each cell in the bitmap as an individual coloured square or rectangle. What I don’t want is for Mantid or matplotlib to render the whole page into a bitmap at some arbitrary resolution and then export that in what claims to be a vector file.