Constrain aspect ratio in WindowsForms DataVisualization Chart
Solution 1:
This is a good question but unfortunately there is no simple solution like locking the two Axes
or setting one value..
Let's start by looking at the relevant players:
The
Chart
control has an innerSize
calledClientSize
, which is theChart.Size
minus the borders. Both sizes are measured in pixels.Inside there may be one or more
ChartAreas
. Each has aPosition
which is of typeElementPosition
.Inside each
ChartArea
the is an area which is used for the actual drawing of the points; it is calledInnerPlotPosition
.
The InnerPlotPosition property defines the rectangle within a chart area element that is used for plotting data; it excludes tick marks, axis labels, and so forth.
The coordinates used for this property (0,0 to 100,100) are related to the ChartArea object, and not to the entire Chart.
The InnerPlotPosition property can be used to align multiple chart areas. However, if one chart area has tick marks and axis labels and another one does not, their axis lines cannot be aligned.
- Both
ChartArea.Position
andChartArea.InnerPlotPosition
contain not just the location but also the size of the areas; all values are in percent of the outer area, ieChartArea.InnerPlotPosition
is relative to theChartArea.Position
andChartArea.Position
is relative to theChart.ClientSize
. All percentages go from0-100
.
So the ChartArea
includes Labels
and Legends
as well as Axes
and TickMarks
..
What we want is to find a way to make the InnerPlotArea
square, i.e. have the same width and height in pixels. The percentages won't do!
Let's start with a few simple calculations; if these are the data we have..:
// we'll work with one ChartArea only..:
ChartArea ca = chart1.ChartAreas[0];
ElementPosition cap = ca.Position;
ElementPosition ipp = ca.InnerPlotPosition;
.. then these are the pixel sizes of the two areas:
// chartarea pixel size:
Size CaSize = new Size( (int)( cap.Width * chart1.ClientSize.Width / 100f),
(int)( cap.Height * chart1.ClientSize.Height / 100f));
// InnerPlotArea pixel size:
Size IppSize = new Size((int)(ipp.Width * CaSize.Width / 100f),
(int)(ipp.Height * CaSize.Height / 100f));
Ideally we would like the InnerPlotArea
to be square; since can't very well let the smaller side grow (or else the chart would overdraw,) we need to shrink the larger one. So the new pixel size of the InnerPlotArea
is
int ippNewSide = Math.Min(IppSize.Width, IppSize.Height);
What next? Since the Chart.Size
has just been set, we don't want to mess with it. Nor should we mess with the ChartArea
: It still needs space to hold the Legend
etc..
So we change the size of the InnerPlotArea
..:
First create a class level variable to store the original values of the InnerPlotPosition
:
ElementPosition ipp0 = null;
We will need it to keep the original percentages, i.e. the margins in order to use them when calculating the new ones. When we adapt the chart the then current ones will already have been changed/distorted..
Then we create a function to make the InnerPlotArea
square, which wraps it all up:
void makeSquare(Chart chart)
{
ChartArea ca = chart.ChartAreas[0];
// store the original value:
if (ipp0 == null) ipp0 = ca.InnerPlotPosition;
// get the current chart area :
ElementPosition cap = ca.Position;
// get both area sizes in pixels:
Size CaSize = new Size( (int)( cap.Width * chart1.ClientSize.Width / 100f),
(int)( cap.Height * chart1.ClientSize.Height / 100f));
Size IppSize = new Size((int)(ipp0.Width * CaSize.Width / 100f),
(int)(ipp0.Height * CaSize.Height / 100f));
// we need to use the smaller side:
int ippNewSide = Math.Min(IppSize.Width, IppSize.Height);
// calculate the scaling factors
float px = ipp0.Width / IppSize.Width * ippNewSide;
float py = ipp0.Height / IppSize.Height * ippNewSide;
// use one or the other:
if (IppSize.Width < IppSize.Height)
ca.InnerPlotPosition = new ElementPosition(ipp0.X, ipp0.Y, ipp0.Width, py);
else
ca.InnerPlotPosition = new ElementPosition(ipp0.X, ipp0.Y, px, ipp0.Height);
}
You would call the function after or during resizing.
private void chart1_Resize(object sender, EventArgs e)
{
makeSquare(chart1);
}
Here the function is at work:
The original size:
Squeezed a little:
And made square again:
Note how the green ChartArea
reserves enough space for the Labels
and the Legend
and how the automatic scaling for the axes still works.. But the X-Axis labels now don't fit in one row. Also note how the ChartArea.BackColor
actually is the color of the InnerPlotArea
only!
Note that you may have to refresh the variable ipp0
to reflect the changed percentages, after making modification to the ChartArea
layout like enlarging or moving or removing Legends
or changing the size or angle of Labels
etc..
Of course you can modify the function to pass in any other ratio to keep instead of keeping the plot area a square..