MS Chart Rectangular Annotation width in percent and not pixel - c#

why is it that the MS chart rectangular annotation width is in percent and not pixel like msdn says it is? https://msdn.microsoft.com/en-us/library/system.windows.forms.datavisualization.charting.annotation.width(v=vs.110).aspx
This is what msdn says
Gets or sets the width, in pixels, of an annotation.
I'd like to set my width to be pixels like it states. Am I missing something here?

Congratulations, you have found a bug in the documentation! Easy to spot simply by following the link to the Annotation.Height docs..
The dimensions of an Annotation, as many others in a Chart control, are indeed given in percentages. This has the advantage that they quite cleverly grow and shrink with the Chart control just like many other elements.
So if you double the width of the chart you basically double the space for the shown DataPoints and if your Annotation went over a 1/3 of the width before it will still do that after you have resized the chart..
Which is nice.
But not what you wanted..
So to set the size to a fixed size in pixels you need to do this:
Calculate the size you want in percentages and set it
Repeat whenever you resize the chart or its layout
The real problem is the calculation: If you look at the Chart.ClientSize you get the size in pixels and to get n pixels you need to do something like this:
float WidthInPercent = 100f * n / Chart.ClientSize.width;
This however does not take into account the various elements your Chart probably has : The Size of an Annotation is not really calculated as direct percentages of the Chart's size.
Instead it is calulated as percentages of the InnerPlotPosition of the ChartArea CA.
InnerPlotPosition by default is set to Auto so accessing its values, e.g. CA.InnerPlotPosition.Width will return 0f; but you can get at the (current!!) values by doing this:
RectangleF IPP = CA.InnerPlotPosition.ToRectangleF();
The same goes for the Size/Position of the ChartArea itself:
RectangleF CAP = CA.Position.ToRectangleF();
Now you can combine these percentages with the (current!) Chart.ClientSize to find out which percentage you need to achieve a certain pixel size..
Note that these values will change when resizing because the outer extras, like Legend and Axis and Labels etc. will not resize, so their relative sizes will grow or shink in relation to their containing elements..
So you need to recalculate upon each Resize event, or, better: write a function to do it for you which you can call wheneber needed..
The result is an Annotation that will (pretty much, due to rounding) maintain its size, no matter how you resize the Chart..
Here are some helpful functions:
This one returns the current ClientRectangle of a ChartArea in pixels
RectangleF ChartAreaClientRectangle(Chart chart, ChartArea CA)
{
RectangleF CAR = CA.Position.ToRectangleF();
float pw = chart.ClientSize.Width / 100f;
float ph = chart.ClientSize.Height / 100f;
return new RectangleF(pw * CAR.X, ph * CAR.Y, pw * CAR.Width, ph * CAR.Height);
}
This one is similar and returns the current ClientRectangle of a ChartArea's InnerplotPosition in pixels:
RectangleF InnerPlotPositionClientRectangle(Chart chart, ChartArea CA)
{
RectangleF IPP = CA.InnerPlotPosition.ToRectangleF();
RectangleF CArp = ChartAreaClientRectangle(chart, CA);
float pw = CArp.Width / 100f;
float ph = CArp.Height / 100f;
return new RectangleF(CArp.X + pw * IPP.X, CArp.Y + ph * IPP.Y,
pw * IPP.Width, ph * IPP.Height);
}
Finally one that converts a size in pixels to one in percentages, again valid only currently, i.e. until the next changes in size or layout..:
SizeF Pixels2Percent( ChartArea CA, int w, int h)
{
RectangleF IPPR = InnerPlotPositionClientRectangle(chart1, CA);
float pw = 100f * w / IPPR.Width ;
float ph = 100f * h / IPPR.Height ;
return new SizeF(pw, ph);
}
Lets have a look at the result before and after some resizing:
As you can see the size stay the same.
Also note the colored rectangles I draw in the Paint event to demostrate the new functions!
Here is the Paint event:
private void chart1_Paint(object sender, PaintEventArgs e)
{
ChartArea CA = chart1.ChartAreas[0];
e.Graphics.DrawRectangle(Pens.Violet,
Rectangle.Round(ChartAreaClientRectangle(chart1, CA)));
e.Graphics.DrawRectangle(Pens.LimeGreen,
Rectangle.Round(InnerPlotPositionClientRectangle(chart1, CA)));
}
Here is the Resize event:
private void chart1_Resize(object sender, EventArgs e)
{
sizeAnn(ra, new Size(24, 36));
}
And here the sizing function:
void sizeAnn(RectangleAnnotation ra, Size sz)
{
ChartArea CA = chart1.ChartAreas[0];
SizeF pixelPercent = Pixels2Percent(CA, sz.Width, sz.Height);
ra.Width = pixelPercent.Width;
ra.Height = pixelPercent.Height;
}

Yes, we cannot set it in % or px as the property value is set to double.
Secondly, pixle = H multiplied by W. but you are only setting the width of chart then how can you set it in pixle? It fundamentally not possible. I hope you will understand what I mean.

I know the OP wanted to set the width in pixels, but in case some people (like me) want to set the width neither in pixels nor in %, but according to the actual values, just set :
annotation.IsSizeAlwaysRelative = False
annotation.Width = whatEverValue

Related

Constrain aspect ratio in WindowsForms DataVisualization Chart

Using the charting control from System.Windows.Forms.DataVisualization.Charting.Chart, I am making a scatter plot.
How can I constrain it so that the scale of the X axis is the same as the scale of the Y axis?
Simply setting the control itself to be square is insufficient, because it has internal margins for drawing and labeling the axes which are not equal.
I could pick a specific size and tweak it to be square, but it needs to be both square and resizable.
I've searched high and low in the documentation and in the property browser, but I can't find anything or think of any ways to do it in the resize event.
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 inner Size called ClientSize, which is the Chart.Size minus the borders. Both sizes are measured in pixels.
Inside there may be one or more ChartAreas. Each has a Position which is of type ElementPosition.
Inside each ChartArea the is an area which is used for the actual drawing of the points; it is called InnerPlotPosition.
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 and ChartArea.InnerPlotPosition contain not just the location but also the size of the areas; all values are in percent of the outer area, ie ChartArea.InnerPlotPosition is relative to the ChartArea.Position and ChartArea.Position is relative to the Chart.ClientSize. All percentages go from 0-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..

Nicer AxisArrowStyle arrows for my Chart.Axes

I answered a question on how to set up a Chart to look like a regular mathematical graph, i.e. with the axes centered and with nice arrows to the top and to the right..
However I found the built-in AxisArrowStyle.Triangle to be rather big and found no way to make it smaller.
Lines - A line-shaped arrow is used for the relevant axis.
None - No arrow is used for the relevant axis.
SharpTriangle - A sharp triangular arrow is used for the relevant axis.
Triangle - A triangular arrow is used for the relevant axis.
Here is the original look of it:
So how can we fix this?
The Chart's axis.AxisArrowStyle enumeration doesn't let us pick a smaller arrow, only a slimmer one.
So we need to draw it ourselves:
Here is a simple but effective piece of code that achieves just that:
private void chart1_PrePaint(object sender, ChartPaintEventArgs e)
{
if (e.ChartElement.ToString().StartsWith("ChartArea-") )
{
// get the positions of the axes' ends:
ChartArea CA = chart1.ChartAreas[0];
float xx = (float)CA.AxisX.ValueToPixelPosition(CA.AxisX.Maximum);
float xy = (float)CA.AxisY.ValueToPixelPosition(CA.AxisY.Crossing);
float yx = (float)CA.AxisX.ValueToPixelPosition(CA.AxisX.Crossing);
float yy = (float)CA.AxisY.ValueToPixelPosition(CA.AxisY.Maximum);
// a simple arrowhead geometry:
int arrowSize = 18; // size in pixels
Point[] arrowPoints = new Point[3] { new Point(-arrowSize, -arrowSize / 2),
new Point(-arrowSize, arrowSize / 2), Point.Empty };
// draw the two arrows by moving and/or rotating the graphics object:
e.ChartGraphics.Graphics.TranslateTransform(xx + arrowSize, xy);
e.ChartGraphics.Graphics.FillPolygon(Brushes.Black, arrowPoints);
e.ChartGraphics.Graphics.TranslateTransform(yx -xx -arrowSize, yy -xy -arrowSize);
e.ChartGraphics.Graphics.RotateTransform(-90);
e.ChartGraphics.Graphics.FillPolygon(Brushes.Black, arrowPoints);
e.ChartGraphics.Graphics.ResetTransform();
}
}

UWP: Calculate Transformation based on ScrollViewer

I have a windows universal app where I am rendering a scene with DirectX. I want to make use of the Scrollviewer and therefore I render my scene behind the Scrollviewer and want to calculate the scene transformation based on the Scrollviewer. It works fine so far, especially the translation and scrolling. But when I zoom in, the scene jumps around in two special situations:
The scene had enough space and was centered and now scrolling is required.
The opposite direction.
More or less I use the following code:
float zoom = scrollViewer.ZoomFactor;
float inverseZoom = 1f / scrollViewer.ZoomFactor;
float scaledContentW = Document.Size.X * scrollViewer.ZoomFactor;
float scaledContentH = Document.Size.Y * scrollViewer.ZoomFactor;
float translateX;
float translateY;
if (scaledContentW < scrollViewer.ViewportWidth)
{
translateX = ((float)scrollViewer.ViewportWidth * inverseZoom - Document.Size.X) * 0.5f;
}
else
{
translateX = -inverseZoom * (float)scrollViewer.HorizontalOffset;
}
if (scaledContentH < scrollViewer.ViewportHeight)
{
translateY = ((float)scrollViewer.ViewportHeight * inverseZoom - Document.Size.Y) * 0.5f;
}
else
{
translateY = -inverseZoom * (float)scrollViewer.VerticalOffset;
}
float visibleX = inverseZoom * (float)scrollViewer.HorizontalOffset;
float visibleY = inverseZoom * (float)scrollViewer.VerticalOffset; ;
float visibleW = Math.Min(Document.Size.X, inverseZoom * (float)scrollViewer.ViewportWidth);
float visibleH = Math.Min(Document.Size.Y, inverseZoom * (float)scrollViewer.ViewportHeight);
Rect2 visibleRect = new Rect2(visibleX, visibleY, visibleW, visibleH);
transform =
Matrix3x2.CreateTranslation(
translateX,
translateY) *
Matrix3x2.CreateScale(zoom);
You can get an example here: https://github.com/SebastianStehle/Win2DZoomTest
To be sure that my eyes are not broken I was zooming around and have written the translation and zoom values to a file. You can see it here:
https://www.dropbox.com/s/9ak6ohg4zb1mnxa/Test.png?dl=0
The meaning of the columns is the following:
Column 1: The computed zoom value of the transformation matrix (M11) = ScrollViewer.ZoomFactor
Column 2: The computed x offset of the matrix (See above)
Column 3: The x value of the result of matrix * vector (500, 500), here: Colum1 * 500 + Column2
You see, that the matrix values look good, but when applying the transformation you get this little jump to the right for some milliseconds. One theory was, that the viewport might change because the scrollbar becomes visible. But this is not the case. I also tried fixed values here, made the scrollbars visible and even created a custom template for the scrollviewer with no scrollbars at all.
Btw: This is a cross post, I also asked the question here: https://github.com/Microsoft/Win2D/issues/125
You see this behavior because when you zoom bigger than the ScrollViewer's size, the zoom center point is moved. To fix this, you just need to subscribe to the ScrollViewer's LayoutUpdated event and inside the handler, manually keep its content in the center.
private void ScrollViewer_LayoutUpdated(object sender, object e)
{
this.ScrollViewer.ChangeView(this.ScrollViewer.ScrollableWidth / 2, this.ScrollViewer.ScrollableHeight / 2, this.ScrollViewer.ZoomFactor, true);
}
This should fix the jumpy movement on the two drawed Rectangles from Win2D.
Update
Just to prove my point, the jumpy behavior is most likely due to unusual translate x and/or y value change when the content size goes over the size of the ScrollViewer. So I wrote some code to log these values on the screen as shown below -
...
this.Test1.Text += ((float)translateX).ToString() + " ";
this.Test2.Text += ((float)translateY).ToString() + " ";
session.Transform =
Matrix3x2.CreateTranslation(
(float)translateX,
(float)translateY) *
Matrix3x2.CreateScale((float)zoom);
Now look at the numbers on the image above. What I did was I tried zooming in until the jumpy scene occurred. See the highlighted translate y value? It is slightly greater than its previous value, which is against the declining trend.
So to fix this, you will need to be able to skip these unusual values caused by ScrollViewer.

Resizing an image in cm C#

I have an requirement that asks for an image with 10 X 6,88 cm.
I know that I can't simple convert from cm to pixels, cause one pixel size depends on the user display resolution.
I would like to know if there is a way to resize an image to have that size in cm. (I need to keep the image extension also. e.g.: can't convert it to a pdf or other extension)
It really depends on in which resolution the user will print the image (sizes in cm makes little sense other than when printed). If the user wants to make a print in, say 200 dpi, then the image would need to be (10 / 2.54 * 200) by (6.88 / 2.54 * 200) pixels (the division with 2.54 is needed to convert between cm and inches). Which resolution that is needed is highly dependent on what kind of image it is, and the quality requirements of the user.
So just saying "I want to resize to X by Y cm" does not really make sense.
For a code sample on how to make the actual resize once you have figured out the needed size of the image, this SO answer should cover your needs.
Actually, you have to differentiate between the images size on the screen, and the images size on the printout.
usually, you find the formula:
inches = pixels / dpi
so it follows:
pixel = inches * dpi
This is for print, actually.
For the display, replace dpi with ppi, and there you are.
For those (like me) that are not familiar with inches:
inches = pixels / dpi
pixel = inches * dpi
1 centimeter = 0.393700787 inch
pixel = cm * 0.393700787 * dpi
This routine will calculate the pixel-size to have the image display X-cm on the monitor.
But on the printer, you don't have it that easy, since you can't get the DPI as easy as the PPI (bmp.HorizontalResolution & bmp.VerticalResolution).
public static int Cm2Pixel(double WidthInCm)
{
double HeightInCm = WidthInCm;
return Cm2Pixel(WidthInCm, HeightInCm).Width;
} // End Function Cm2Pixel
public static System.Drawing.Size Cm2Pixel(double WidthInCm, double HeightInCm)
{
float sngWidth = (float)WidthInCm; //cm
float sngHeight = (float)HeightInCm; //cm
using (System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(1, 1))
{
sngWidth *= 0.393700787f * bmp.HorizontalResolution; // x-Axis pixel
sngHeight *= 0.393700787f * bmp.VerticalResolution; // y-Axis pixel
}
return new System.Drawing.Size((int)sngWidth, (int)sngHeight);
} // End Function Cm2Pixel
usage would go like this:
public System.Drawing.Image Generate(string Text, int CodeSize)
{
int minSize = Cm2Pixel(2.5); // 100;
if (CodeSize < minSize)
CodeSize = minSize;
if (string.IsNullOrEmpty(Text))
{
System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(CodeSize, CodeSize);
using (System.Drawing.Graphics gfx = System.Drawing.Graphics.FromImage(bmp))
{
gfx.Clear(System.Drawing.Color.Black);
using(System.Drawing.Font fnt = new System.Drawing.Font("Verdana", 12, System.Drawing.FontStyle.Bold))
{
double y = CodeSize / 2.0 - fnt.Size;
gfx.DrawString("No Data", fnt, System.Drawing.Brushes.White, 5, (int)y, System.Drawing.StringFormat.GenericTypographic);
} // End Using fnt
} // End using gfx
return bmp;
} // End if (string.IsNullOrEmpty(Text))
...[Generate QR-Code]
return [Generated QR-Code]
}
Image file formats like JPG and TIFF have an EXIF header which has information like horizontal and vertical DPI.
Thus if you get an image that has this metadata, you could verify the printable size.
double DPC = Image_DPI * 0.393700787;
double widthInCm = Image_Width * DPC;
double heightInCm = Image_Height * DPC;
if (widthInCm <= 10 && heightInCm <= 6.88) // do stuff
If you need to resize images to never exceed these printable dimensions, you could do it the other way around, and calculate a DPI ratio that lets the image of dimensions W x H fit within 10cm x 6.88cm bounds.
Kind of what Fredrik is saying:
I would take a nice DPI and require the image to be that resolution or bigger (but is the same aspect ratio) and when exporting/printing the image, resize the image to the DPI used by the other program/printer...
It might be as simple as this: most images store the number of pixels per inch in them. Figure out the number of pixels in each dimension of your image, and divide that by the number of inches (convert from cm). Then use the original bits, just modify the field for the number of pixels per inch (or, more commonly, dots per inch).
So your picture needs to be 3.93" x 2.71". If your image is 393px x 271px, you would set the dpi to 100x100. If your image is 39px x 27px, you would set the dpi to 10x10.
Though probably you'll have to do some resizing, as explained by other answers. :)

Transform pixel height of image to printing size used by graphics object

As part of a print procedure of my application I'm trying to print a list of images scaled down to a specified width and placed one below the other. The problem is I can not figure out how to transform the height in pixels of the images to the height in the units used by the graphics object during printing. How do I calculate the imageHeightPrint variable correctly?
This code snippet is the part of the image printing loop that scales down the image and calculates it's height and the placement of the next image.
Image image = Image.FromStream(imageStream);
// Get proportional correct height
int imageHeight = image.Height * imageWidth / image.Width;
Image imageToPrint = image.GetThumbnailImage(imageWidth, imageHeight, null, IntPtr.Zero);
float imageHeightPrint = e.Graphics.DpiY * imageToPrint.Height / imageToPrint.VerticalResolution;
e.Graphics.DrawImage(imageToPrint, e.MarginBounds.Left, yPos);
yPos += imageHeightPrint;
I found the correct solution my self after dissecting the documentation.
This line:
float imageHeightPrint = e.Graphics.DpiY * imageToPrint.Height / imageToPrint.VerticalResolution;
Should be changed into this:
float imageHeightPrint = imageToPrint.Height /
imageToPrint.VerticalResolution * 100;
The biggest thing I missed was that the height-in-print should be in hundredths of an inch.

Categories

Resources