I provide now an extended version, including an extra function to resize your canvas in order to obtain not a real, but a custom desired aspect ratio of your axes (a screen pixel ratio, i.e. units and numbers of your axes are completely ignored).
Implementation as
c->SetFrameAspectRatio(const Double_t frameRatio, const Int_t axis=1);
would be nice.
// Run with root -l -n test_ratio.cpp+
// Alternatively: root -l -n test_ratio.cpp+ -b -q
#include "TCanvas.h"
#include "TH2F.h"
#include "TF2.h"
#include "TROOT.h"
#include "TFrame.h"
#include "TMath.h"
Int_t countpads(TVirtualPad *pad) {
//count the number of pads in pad
if (!pad) return 0;
Int_t npads = 0;
TObject *obj;
TIter next(pad->GetListOfPrimitives());
while ((obj = next())) {
if (obj->InheritsFrom(TVirtualPad::Class())) npads++;
}
return npads;
//Reference https://root.cern.ch/root/roottalk/roottalk02/0654.html
}
/**
* \brief Function to resize a canvas so that a 2D histogram or TGraph / TGraph2D are shown in real aspect ratio
* \param c pointer to a TCanvas
* \param axis 1 for resizing horizontally (x-axis) in order to get real aspect ratio, 2 for the resizing vertically (y-axis)
* \return false if error is encountered, true otherwise
* \note For defining the concept real aspect ratio, it is assumed that x and y axes are in same units, e.g. both in MeV or both in ns
* \note You can resize either the the width of the canvas or the height, but not both at the same time
* \note Resize the canvas before calling this function, if you want a larger height or width of reference if you call with parameter axis= 1 or 2, respectively
* \note Call this function AFTER drawing AND zooming (SetUserRange) your TGraph or Histogram, otherwise it cannot infer your actual axes lengths
* \note This function ensures that the TFrame has a real aspect ratio, this does not mean that the full pad (i.e. the canvas or png output) including margins has exactly the same ratio
* \note This function does not work if canvas is divided in several subpads
* \note Still to implement, if your CRT screen has non-square pixels, then the function does not work. One should specify then the screen pixel ratio (pixelSizeY / pixelSizeX), and the function returns a correctly resized canvas so that the aspect ratio is real in your current screen. See https://en.wikipedia.org/wiki/Pixel_aspect_ratio
* \see https://root-forum.cern.ch/t/resize-canvas-to-get-real-aspect-ratio-of-th2-or-tgraph/20718/1
*/
bool SetRealAspectRatio(TCanvas* const c, const Int_t axis = 1/*, const Double_t screenPixelRatio=1.*/)
{
if(!c)
{
cout << "Error in SetRealAspectRatio: canvas is NULL" << endl;
return false;
}
if(countpads(c)>0)
{
cout << "Error in SetRealAspectRatio: function not implemented yet for multiple subpads." << endl;
return false;
}
{
//Get the current min-max values if SetUserRange has been called
c->Update();
const Double_t xmin = c->GetUxmin();
const Double_t xmax = c->GetUxmax();
const Double_t ymin = c->GetUymin();
const Double_t ymax = c->GetUymax();
//Get the length of zoomed x and y axes
const Double_t xlength = xmax - xmin;
const Double_t ylength = ymax - ymin;
const Double_t ratio = xlength/ylength;
//Get how many pixels are occupied by the canvas
const Int_t npx = c->GetWw();
const Int_t npy = c->GetWh();
//Get x-y coordinates at the edges of the canvas (extrapolating outside the axes, NOT at the edges of the histogram)
const Double_t x1 = c->GetX1();
const Double_t y1 = c->GetY1();
const Double_t x2 = c->GetX2();
const Double_t y2 = c->GetY2();
//Get the length of extrapolated x and y axes
const Double_t xlength2 = x2 - x1;
const Double_t ylength2 = y2 - y1;
const Double_t ratio2 = xlength2/ylength2;
//Get now number of pixels including window borders
const Int_t bnpx = c->GetWindowWidth();
const Int_t bnpy = c->GetWindowHeight();
cout << "WindX\tWindY\tCanvX\tCanvY\tx1\ty1\tx2\ty2\tratiox/y\tCanvX/CanvY" << endl;
cout << bnpx << "\t" << bnpy << "\t" << npx << "\t" << npy << "\t" << x1 << "\t" << y1 << "\t" << x2 << "\t" << y2 << "\t" << ratio2 << "\t" << (double)npx/npy << "\tOriginal Canvas" << endl;
if(axis==1)
{
c->SetCanvasSize(TMath::Nint(npy*ratio2), npy);
c->SetWindowSize((bnpx-npx)+TMath::Nint(npy*ratio2), bnpy);
}
else if(axis==2)
{
c->SetCanvasSize(npx, TMath::Nint(npx/ratio2));
c->SetWindowSize(bnpx, (bnpy-npy)+TMath::Nint(npx/ratio2));
}
else
{
cout << "Error in SetRealAspectRatio: axis value " << axis << " is neither 1 (resize along x-axis) nor 2 (resize along y-axis).";
return false;
}
}
//Check now that resizing has worked
{
//Get the current min-max values if SetUserRange has been called
c->Update();
const Double_t xmin = c->GetUxmin();
const Double_t xmax = c->GetUxmax();
const Double_t ymin = c->GetUymin();
const Double_t ymax = c->GetUymax();
//Get the length of zoomed x and y axes
const Double_t xlength = xmax - xmin;
const Double_t ylength = ymax - ymin;
const Double_t ratio = xlength/ylength;
//Get how many pixels are occupied by the canvas
const Int_t npx = c->GetWw();
const Int_t npy = c->GetWh();
//Get x-y coordinates at the edges of the canvas (extrapolating outside the axes, NOT at the edges of the histogram)
const Double_t x1 = c->GetX1();
const Double_t y1 = c->GetY1();
const Double_t x2 = c->GetX2();
const Double_t y2 = c->GetY2();
//Get the length of extrapolated x and y axes
const Double_t xlength2 = x2 - x1;
const Double_t ylength2 = y2 - y1;
const Double_t ratio2 = xlength2/ylength2;
//Get now number of pixels including window borders
const Int_t bnpx = c->GetWindowWidth();
const Int_t bnpy = c->GetWindowHeight();
cout << bnpx << "\t" << bnpy << "\t" << npx << "\t" << npy << "\t" << x1 << "\t" << y1 << "\t" << x2 << "\t" << y2 << "\t" << ratio2 << "\t" << (double)npx/npy << "\tModified Canvas" << endl;
//Check accuracy +/-1 pixel due to rounding
if(abs(TMath::Nint(npy*ratio2) - npx)<2)
{
cout << "Resizing finished successfully." << endl;
return true;
}
else
{
cout << "Resizing failed." << endl;
return false;
}
}
// References:
// https://root.cern.ch/root/roottalk/roottalk01/3676.html
// https://root-forum.cern.ch/t/making-the-both-axes-square-on-the-pad/4325/1
}
/**
* \brief Function to resize a canvas so that a 2D histogram or TGraph / TGraph2D (i.e. the) are shown in a custom ratio
* \param c pointer to a TCanvas
* \param frameRatio the desired size of y axis in pixels divided by size of the x axis in pixels
* \param axis 1 for resizing horizontally (x-axis) in order to get real aspect ratio, 2 for the resizing vertically (y-axis)
* \return false if error is encountered, true otherwise
* \note For defining the concept frame aspect ratio, you refer to the length of x and y axes in pixels, independently of their units
* \note You can resize either the the width of the canvas or the height, but not both at the same time
* \note Resize the canvas before calling this function, if you want a larger height or width of reference if you call with parameter axis= 1 or 2, respectively
* \note Call this function AFTER drawing AND zooming (SetUserRange) your TGraph or Histogram, otherwise it cannot infer your actual axes lengths
* \note This function ensures that the TFrame has the specified ratio, this does not mean that the full pad (i.e. the canvas or png output) including margins has exactly the same ratio
* \note This function does not work if canvas is divided in several subpads
* \note Beware: this function does not take into account your axes units and numbers. It just takes into account the screen pixels!
* \see https://root-forum.cern.ch/t/resize-canvas-to-get-real-aspect-ratio-of-th2-or-tgraph/20718/1
*/
bool SetFrameAspectRatio(TCanvas* const c, const Double_t frameRatio, const Int_t axis = 1)
{
if(!c)
{
cout << "Error in SetFrameAspectRatio: canvas is NULL" << endl;
return false;
}
if(countpads(c)>0)
{
cout << "Error in SetFrameAspectRatio: function not implemented yet for multiple subpads." << endl;
return false;
}
{
//Get the current min-max values if SetUserRange has been called
c->Update();
const Double_t xmin = c->GetUxmin();
const Double_t xmax = c->GetUxmax();
const Double_t ymin = c->GetUymin();
const Double_t ymax = c->GetUymax();
//Get the length of zoomed x and y axes
const Double_t xlength = xmax - xmin;
const Double_t ylength = ymax - ymin;
const Double_t ratio = xlength/ylength;
//Get how many pixels are occupied by the canvas
const Int_t npx = c->GetWw();
const Int_t npy = c->GetWh();
//Get pixel coordinates at the edges of the TFrame (figure axes)
const Int_t nx1 = c->XtoPixel(xmin);
const Int_t ny1 = c->YtoPixel(ymin);
const Int_t nx2 = c->XtoPixel(xmax);
const Int_t ny2 = c->YtoPixel(ymax);
//Get the length of x and y axes in pixels
const Int_t nxlength2 = abs(nx2 - nx1);
const Int_t nylength2 = abs(ny2 - ny1);
const Double_t ratio2 = (Double_t)nxlength2/nylength2;
//Get now number of pixels including window borders
const Int_t bnpx = c->GetWindowWidth();
const Int_t bnpy = c->GetWindowHeight();
cout << "WindX\tWindY\tCanvX\tCanvY\tx1\ty1\tx2\ty2\tratiox/y\tCanvX/CanvY" << endl;
cout << bnpx << "\t" << bnpy << "\t" << npx << "\t" << npy << "\t" << nx1 << "\t" << ny1 << "\t" << nx2 << "\t" << ny2 << "\t" << ratio2 << "\t" << (double)npx/npy << "\tOriginal Canvas" << endl;
if(axis==1)
{
c->SetCanvasSize(TMath::Nint(npx/frameRatio/ratio2), npy);
c->SetWindowSize((bnpx-npx)+TMath::Nint(npx/frameRatio/ratio2), bnpy);
}
else if(axis==2)
{
c->SetCanvasSize(npx, TMath::Nint(npy*frameRatio*ratio2));
c->SetWindowSize(bnpx, (bnpy-npy)+TMath::Nint(npy*frameRatio*ratio2));
}
else
{
cout << "Error in SetFrameAspectRatio: axis value " << axis << " is neither 1 (resize along x-axis) nor 2 (resize along y-axis).";
return false;
}
}
//Check now that resizing has worked
{
//Get the current min-max values if SetUserRange has been called
c->Update();
const Double_t xmin = c->GetUxmin();
const Double_t xmax = c->GetUxmax();
const Double_t ymin = c->GetUymin();
const Double_t ymax = c->GetUymax();
//Get the length of zoomed x and y axes
const Double_t xlength = xmax - xmin;
const Double_t ylength = ymax - ymin;
const Double_t ratio = xlength/ylength;
//Get how many pixels are occupied by the canvas
const Int_t npx = c->GetWw();
const Int_t npy = c->GetWh();
//Get pixel coordinates at the edges of the TFrame (figure axes)
const Int_t nx1 = c->XtoPixel(xmin);
const Int_t ny1 = c->YtoPixel(ymin);
const Int_t nx2 = c->XtoPixel(xmax);
const Int_t ny2 = c->YtoPixel(ymax);
//Get the length of x and y axes in pixels
const Int_t nxlength2 = abs(nx2 - nx1);
const Int_t nylength2 = abs(ny2 - ny1);
const Double_t ratio2 = (Double_t)nxlength2/nylength2;
//Get now number of pixels including window borders
const Int_t bnpx = c->GetWindowWidth();
const Int_t bnpy = c->GetWindowHeight();
cout << bnpx << "\t" << bnpy << "\t" << npx << "\t" << npy << "\t" << nx1 << "\t" << ny1 << "\t" << nx2 << "\t" << ny2 << "\t" << ratio2 << "\t" << (double)npx/npy << "\tModified Canvas" << endl;
//Check accuracy +/-1 pixel due to rounding
if(abs(TMath::Nint(nxlength2*frameRatio) - nylength2)<2)
{
cout << "Resizing finished successfully." << endl;
return true;
}
else
{
cout << "Resizing failed." << " " << nxlength2 << " " << nylength2 << endl;
return false;
}
}
// References:
// https://root.cern.ch/root/roottalk/roottalk01/3676.html
// https://root-forum.cern.ch/t/making-the-both-axes-square-on-the-pad/4325/1
}
void test_ratio()
{
TF2 *xyg = new TF2("xyg","xygaus",0,10,0,10);
xyg->SetParameters(1,4.5,0.5,-4.5,0.5); //amplitude, meanx,sigmax,meany,sigmay
TCanvas* c = new TCanvas("c","c");
c->SetGridx();
c->SetGridy();
TH2* h2 = new TH2F("h2","h2", 50,0,10, 100,-8,-1);
h2->GetXaxis()->SetTitle("x / mm");
h2->GetYaxis()->SetTitle("y / mm");
h2->GetXaxis()->SetRangeUser(3,6);
h2->GetYaxis()->SetRangeUser(-7,-2);
h2->FillRandom("xyg",1000000);
h2->Draw("COLZ");
c->SaveAs("test_ratio_default.png");
SetRealAspectRatio(c);
c->SaveAs("test_ratio_real_x.png");
SetRealAspectRatio(c,2);
c->SaveAs("test_ratio_real_x_y.png");
c = new TCanvas("cy","c");
c->SetGridx();
c->SetGridy();
h2 = new TH2F("hy","h2", 50,0,10, 100,-8,-1);
h2->GetXaxis()->SetTitle("x / mm");
h2->GetYaxis()->SetTitle("y / mm");
h2->GetXaxis()->SetRangeUser(3,6);
h2->GetYaxis()->SetRangeUser(-7,-2);
h2->FillRandom("xyg",1000000);
h2->Draw("COLZ");
SetRealAspectRatio(c,2);
c->SaveAs("test_ratio_real_y.png");
c = new TCanvas("c2","c2");
c->SetGridx();
c->SetGridy();
h2 = new TH2F("h2b","h2", 50,0,10, 100,-8,-1);
h2->GetXaxis()->SetTitle("x / mm");
h2->GetYaxis()->SetTitle("y / mm");
h2->GetXaxis()->SetRangeUser(3,6);
h2->GetYaxis()->SetRangeUser(-7,-2);
h2->FillRandom("xyg",1000000);
h2->Draw("COLZ");
c->SetCanvasSize(472.*(6-3)/(-2-(-7)),472);
//Check now that resizing did not work
{
//Get the current min-max values if SetUserRange has been called
c->Update();
const Double_t xmin = c->GetUxmin();
const Double_t xmax = c->GetUxmax();
const Double_t ymin = c->GetUymin();
const Double_t ymax = c->GetUymax();
//Get the length of zoomed x and y axes
const Double_t xlength = xmax - xmin;
const Double_t ylength = ymax - ymin;
const Double_t ratio = xlength/ylength;
//Get how many pixels are occupied by the canvas
const Int_t npx = c->GetWw();
const Int_t npy = c->GetWh();
//Get x-y coordinates at the edges of the canvas (extrapolating outside the axes, NOT at the edges of the histogram)
const Double_t x1 = c->GetX1();
const Double_t y1 = c->GetY1();
const Double_t x2 = c->GetX2();
const Double_t y2 = c->GetY2();
//Get now number of pixels including window borders
const Int_t bnpx = c->GetWindowWidth();
const Int_t bnpy = c->GetWindowHeight();
cout << "WindX\tWindY\tCanvX\tCanvY\tx1\ty1\tx2\ty2\tratiox/y\tCanvX/CanvY" << endl;
cout << bnpx << "\t" << bnpy << "\t" << npx << "\t" << npy << "\t" << x1 << "\t" << y1 << "\t" << x2 << "\t" << y2 << "\t" << ratio << "\t" << (double)npx/npy << "\tModified Canvas" << endl;
//Check accuracy +/-1 pixel due to rounding
if(abs(TMath::Nint(npy*ratio) - npx)<2)
{
cout << "Resizing finished successfully." << endl;
}
else
{
cout << "Resizing failed." << endl;
}
}
c->SaveAs("test_ratio_incorrect.png");
c = new TCanvas("c3","c3");
c->SetGridx();
c->SetGridy();
h2 = new TH2F("h3","h3", 50,0,10, 100,-8,-1);
h2->GetXaxis()->SetTitle("x / mm");
h2->GetYaxis()->SetTitle("y / mm");
h2->GetXaxis()->SetRangeUser(3,6);
h2->GetYaxis()->SetRangeUser(-7,-2);
h2->FillRandom("xyg",1000000);
h2->Draw("COLZ");
SetFrameAspectRatio(c,2.0);
c->SaveAs("test_ratio_frame_2_x.png");
c = new TCanvas("c4","c4");
c->SetGridx();
c->SetGridy();
h2 = new TH2F("h4","h3", 50,0,10, 100,-8,-1);
h2->GetXaxis()->SetTitle("x / mm");
h2->GetYaxis()->SetTitle("y / mm");
h2->GetXaxis()->SetRangeUser(3,6);
h2->GetYaxis()->SetRangeUser(-7,-2);
h2->FillRandom("xyg",1000000);
h2->Draw("COLZ");
SetFrameAspectRatio(c,2.0,2);
c->SaveAs("test_ratio_frame_2_y.png");
}
I have added a guard for multiple subpads. One may think about resizing the TFrame in these cases instead of the TCanvas itself, and checking that the new TFrame is contained within the subpad.