Resize canvas to get real aspect ratio of TH2 or TGraph

I a have a TCanvas containing either a 2D histogram or TGraph / TGraph2D.

I am writing a function to resize the canvas, so that the unit length in the x and y axes occupies the same number of pixels, i.e. to get a real aspect ratio in the monitor compared to the spatial dimensions.

// 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"

/**
 * \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
 * \return false if error is encountered, true otherwise
 * \note It resizes the width of the canvas, not the height, as you normally have more space in your monitor horizontally than vertically
 * \note Resize the canvas in Y before calling this function if you want a larger vertical height 
 * \note Call this function AFTER drawing AND zooming (SetUserRange) your TGraph or Histogram, otherwise it cannot infer your actual axes length
 * 
 */
bool SetRealAspectRatio(TCanvas* const c)
{
	if(!c)
	{
		cout << "Error in SetRealAspectRatio: canvas is NULL";
		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 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 << "\tOriginal Canvas" << endl;
		
		c->SetCanvasSize(npy*ratio, npy);
		c->SetWindowSize((bnpx-npx)+npy*ratio, bnpy);
	}
	
	//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 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" << ratio << "\t" << (double)npx/npy << "\tModified Canvas" << endl;
		
		//Check accuracy +/-1 pixel due to rounding
		if(abs(npy*ratio - 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
}

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("default_ratio.png");
   
   SetRealAspectRatio(c);
   c->SaveAs("real_aspect_ratio.png");
}

I have the following questions:
[ul]
[li] Is there an easier (default) way to do what I want? Something like c->SetRealAspectRatio()?[/li]
[li] Am I handling correctly the window border?[/li]
[li] Is the approach also valid for TGraph and TGraph2D?[/li]
[li] If I run it in batch mode, my original canvas is 2 pixels smaller in each dimension. 696x472 vs 698x474. This is also visible in the png output. Why does this happen?[/li][/ul]




I think the end of the header doc here root.cern.ch/doc/master/classTCanvas.html
Is what you are looking for.

If I call in my previous script c->SetCanvasSize(472*3/5.,472), the x:y axes have not a ratio 3:5, just the canvas. See attached file, with following code, which I run in batch mode:

// 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"

/**
 * \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
 * \return false if error is encountered, true otherwise
 * \note It resizes the width of the canvas, not the height, as you normally have more space in your monitor horizontally than vertically
 * \note Resize the canvas in Y before calling this function if you want a larger vertical height
  * \note Call this function AFTER drawing AND zooming (SetUserRange) your TGraph or Histogram, otherwise it cannot infer your actual axes length
 * \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)
{
	if(!c)
	{
		cout << "Error in SetRealAspectRatio: canvas is NULL";
		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 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 << "\tOriginal Canvas" << endl;
		
		c->SetCanvasSize(npy*ratio, npy);
		c->SetWindowSize((bnpx-npx)+npy*ratio, bnpy);
	}
	
	//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 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" << ratio << "\t" << (double)npx/npy << "\tModified Canvas" << endl;
		
		//Check accuracy +/-1 pixel due to rounding
		if(abs(npy*ratio - 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
}

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("default_ratio.png");
   
   
   SetRealAspectRatio(c);
   c->SaveAs("real_aspect_ratio.png");

   c = new TCanvas("c2","c2");
   c->SetGridx();
   c->SetGridy();
   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->SetCanvasSize(472.*(6-3)/(-2-(-7)),472);
   c->SaveAs("not_correct_ratio.png");
   
	//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 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(npy*ratio - npx)<2)
		{
			cout << "Resizing finished successfully." << endl;
		}
		else
		{
			cout << "Resizing failed." << endl;
		}
	}
}

The difference is subtle, but it’s there:

[ul]
[li] real_aspect_ratio --> png canvas is 280x472 (factor 1.69 > 5/3), but the axes are exactly 5:3[/li]
[li] non_correct_ratio --> png canvas is 283x472 (factor 1.67==5/3), but the axes are NOT exactly 5:3 [/li][/ul]

In other words, a w:h canvas size does not correlate with a w:h axes ratio exactly. This is why I wrote the SetRealAspectRatio function.

See also output of provided script comparing ratiox/y with CanvX/CanvY:

WindX  WindY      CanvX  CanvY  x1      y1        x2     y2      ratiox/y  CanvX/CanvY
700      500      283     472   2.625   -7.65     6.375  -1.35   0.595238  0.599576
Resizing failed.

ratiox/y should be 0.599=3/5 and not 0.595.

Do you want us to include it in ROOT ?

Yes, something like c->SetRealAspectRatio() would be perfect. Let me know if I need to format or restyle my code suggestion.

Another question, why is a default canvas in batch mode 2 pixels smaller than in gui mode? I get 696x472 vs 698x474. This is also visible in the png output.

In non Batch mode the first designers of TCanavs decide that the canvas size should include th window decoration… surely that was not the best choice… In batch there is no decoration.

I will look how to integrate your code.

Basically your code is:

   {
      //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 << "\tOriginal Canvas" << endl;
      
      c->SetCanvasSize(npy*ratio, npy);
      c->SetWindowSize((bnpx-npx)+npy*ratio, bnpy);
   }

Right ?

The rest seems to be only verification we do not want in the final code. I am a bit puzzle because your method has only the canvas as parameter … it means that if we put it in TCanvas it will have no parameter. I would expect that it should have at least one … the aspect ratio … and may be a way to indicate what is the length we take as reference: the width or the heigh ? …

Did you think about canvases with multiple sub-pads?
How do you want to make sure that sub-pads get “real aspect ratios”?

Thanks for your comments.

No, the function sets the REAL aspect ratio, i.e what you would see in reality, if axes x and y are in mm. If my histogram is from 0 to 1 in x and 0 to 2 in y, I call the function, which looks automatically at the axes lengths and infers that you need an aspect ratio of 2:1 to have a PROPORTIONAL representation of reality. If my axes are from 0.234 to 0.586 and y from 2.189 and 3.679, I do not want to bother what should be the aspect ratio, I just want that it is the “real” one, and the function does it for me. So no need for calculating previously the ratio and passing it as argument.

Of course, one could think of another function that sets not a real, but a desired aspect ratio. I would however separate them clearly by name.

SetRealAspectRatio();//Resizes your canvas so that unit lengths in x and y occupy same number of pixels
SetFrameAspectRatio(factor);//Resizes your canvas so that the number of pixels of your x axis is "factor" times your y axis (in n of pixels)

And each function should be implemented differently. I could provide a code suggestion for the second type.

Yes, that’s a good idea, an extra (defaulted) parameter like SetRealAspectRatio(const Int_t axis=1)

Good point. I will think about it by using the c->SetTop|Bottom|Left|RightMargin() function, i.e. modifying the actual frame size inside the subpad, as the size of the subpad itself should not be modified.

I di not realised that you assume the axis are in mm. So that’s very specific. I would propose you post your macro as a contribution here.
viewforum.php?f=12

Well, I do not assume that they are in mm, it was just an example.

I just assume that the x and y axes have the same units.

For example, x / ns versus y in /ns, or x / MeV vs y / MeV. I think this is a rather general situation, both axes in same units.

Ah ok, fine … lets nevertheless post your macro in the part of the forum dedicated to new functionality (as I proposed) to see if there is some interest for it.

May be something to add in the help of your macro by the way …

Thanks for your suggestions.

The thread goes then on at:

By the way, there was a problem in my previous function if the left-right margins and top-bottom were not equivalent. I have corrected this in the follow-up thread, so ignore the previous code here.