Currently my software uses the HitTest() method of a chart object in MSCharts but as I scale this up to more and more data points on my chart combined with other factors this can have a massive performance hit.
I was wondering if there any alternatives that you know of to provide the same functionality ( get the X Coordinate on the chart for the cursor position ) but without the performance hit as hit testing seems to be a very brute force way of obtaining my answer.
My chart is created from the class System.Windows.Forms.DataVisualization.Charting.Chart
Edit for clarity: I need to find the position of a line on my chart to use it for other calculations.
Had the same performance issue with a mousewheel event.
Here is my solution:
To get the axes values of the current mouse position:
double posX = Math.Round(currentArea.AxisX.PixelPositionToValue(e.X));
double posY = Math.Round(currentArea.AxisY.PixelPositionToValue(e.Y));
Taken from Showing Mouse Axis Coordinates on Chart Control with a little change to get it more accurate.
But you should check before, that the mouse is in a ChartArea, else it will throw you an Exception.
To get the ChatElement on which the mouse points:
// Gets the ChartArea that the mouse points
private ChartArea mouseinChartArea(Chart source, Point e)
{
double relativeX = (double)e.X * 100 / source.Width;
double relativeY = (double)e.Y * 100 / source.Height;
foreach (ChartArea ca in source.ChartAreas)
{
if (relativeX > ca.Position.X && relativeX < ca.Position.Right &&
relativeY > ca.Position.Y && relativeY < ca.Position.Bottom)
return ca;
}
return null;
}
// for my purpose, returns an axis. But you can return anything
private Axis findAxisforZooming(Chart source, Point e)
{
ChartArea currentArea = mouseinChartArea(source, new Point(e.X, e.Y)); // Check if inside
if (currentArea == null)
return null;
double axisXfontSize = currentArea.AxisX.LabelAutoFitMinFontSize + ((double)source.Width / SystemInformation.PrimaryMonitorSize.Width)
* (currentArea.AxisX.LabelAutoFitMaxFontSize - currentArea.AxisX.LabelAutoFitMinFontSize);
double axisYfontSize = currentArea.AxisY.LabelAutoFitMinFontSize + ((double)source.Height / SystemInformation.PrimaryMonitorSize.Height)
* (currentArea.AxisY.LabelAutoFitMaxFontSize - currentArea.AxisY.LabelAutoFitMinFontSize);
double axisYfontHeightSize = (axisYfontSize - currentArea.AxisY.LabelStyle.Font.Size) + currentArea.AxisY.LabelStyle.Font.Height;
Graphics g = this.CreateGraphics();
if (currentArea.AxisX.LabelStyle.Font.Unit == GraphicsUnit.Point)
axisXfontSize = axisXfontSize * g.DpiX / 72;
if (currentArea.AxisY.LabelStyle.Font.Unit == GraphicsUnit.Point)
axisYfontHeightSize = axisYfontHeightSize * g.DpiX / 72;
g.Dispose();
// Replacing the SystemInformation.PrimaryMonitorSize with the source.Width / Height will give the accurate TickMarks size.
// But it doens't count for the gab between the tickMarks and the axis lables (so by replacing, it give a good proximity with the gab)
int axisYTickMarks = (int)Math.Round(currentArea.AxisY.MajorTickMark.Size / 100 * SystemInformation.PrimaryMonitorSize.Width); // source.Width;
int axisXTickMarks = (int)Math.Round(currentArea.AxisX.MajorTickMark.Size / 100 * SystemInformation.PrimaryMonitorSize.Height); // source.Height;
int leftInnerPlot = (int)Math.Round(currentArea.Position.X / 100 * source.Width +
currentArea.InnerPlotPosition.X / 100 * currentArea.Position.Width / 100 * source.Width);
int rightInnerPlot = (int)Math.Round(currentArea.Position.X / 100 * this.chart1.Width +
currentArea.InnerPlotPosition.Right / 100 * currentArea.Position.Width / 100 * source.Width);
int topInnerPlot = (int)Math.Round(currentArea.Position.Y / 100 * this.chart1.Height +
currentArea.InnerPlotPosition.Y / 100 * currentArea.Position.Height / 100 * source.Height);
int bottomInnerPlot = (int)Math.Round(currentArea.Position.Y / 100 * source.Height +
currentArea.InnerPlotPosition.Bottom / 100 * currentArea.Position.Height / 100 * source.Height);
// Now you got the boundaries of every important ChartElement.
// Only left to check if the mouse is within your desire ChartElement,
// like the following:
bottomInnerPlot += axisXTickMarks + (int)Math.Round(axisXfontSize); // Include AxisX
if (e.X > leftInnerPlot && e.X < rightInnerPlot &&
e.Y > topInnerPlot && e.Y < bottomInnerPlot) // return AxisX if inside the InnerPlot area or on AxisX
return currentArea.AxisX;
else if (e.X > (leftInnerPlot - axisYTickMarks - (int)Math.Round(axisYfontHeightSize)) && e.X < rightInnerPlot &&
e.Y > topInnerPlot && e.Y < bottomInnerPlot) // return AxisY if on AxisY only
return currentArea.AxisY;
return null;
}
As it can be seen, the code is longer than HitTest(). But the run time is shorter.
Related
I'm very new to c# and running into a problem while trying to program and visualize the Mandelbrotset.
I have created a 400 by 400 panel and want to use this to graph the set. I want my graph to go from -2 to 2 on both axes so I'm using a scale of 0.01. When looking at my code, I think the paint method should work; the coordinates are converted in the correct way it seems. The problem is that the graph is not fully shown on the panel while running. (0,0) is somewhere in the lower right corner, removing much of the coordinates in that corner.
Below is the function used to draw the graph on the panel. Did I make a mistake in coding the coordinates this way? Or am I misunderstanding how coordinates work in a panel?
for (int xco =0; xco<400; xco++)
{
for (int yco=0; yco<400; yco++)
{
double x = (xco - 200) * scale;
double y = (yco - 200) * scale;
int mandel = 0;
double fx = x, fy = y;
double distance = 0;
while ((mandel<max) && (distance<2))
{
double fx1 = fx;
fx = fx * fx - fy * fy + x;
fy = 2 * fx1 * fy + y;
distance = Math.Sqrt(fx * fx + fy * fy);
mandel++;
}
if (mandel%2==1)
pea.Graphics.FillRectangle(Brushes.White, xco, yco, xco + 1, yco + 1);
else
pea.Graphics.FillRectangle(Brushes.Black, xco, yco, xco + 1, yco + 1);
}
}
I am drawing a circle of _radius = 50 pixels in the center of the form:
g.FillEllipse(Brushes.Red, this.ClientRectangle.Width / 2 - _radius / 2, this.ClientRectangle.Height / 2 - _radius / 2, _radius, _radius);
Now I want to check if the user clicked in the form.
if (e.Button == MouseButtons.Left)
{
int w = this.ClientRectangle.Width;
int h = this.ClientRectangle.Height;
double distance = Math.Sqrt((w/2 - e.Location.X) ^ 2 + (h/2 - e.Location.Y) ^ 2);
....
if (distance <_radius)
return true;
else
return false;
}
Now I am ending up with wrong values. For instance if I click on the edge of the circle I at times get distance of ~10 or NaN at times. What am I doing wrong here?
You're performing integer division, which is coarser than floating-point division.
^ is not the "power-to" operator, it's the bitwise XOR operator, which is probably not what you want. Use Math.Pow or x*x instead.
You can simplify the last statement by simply doing return distance < _radius.
Try this:
Single w = this.ClientRectangle.Width;
Single h = this.ClientRectangle.Height;
Single distanceX = w / 2f - e.Location.X;
Single distanceY = h / 2f - e.Location.Y;
Single distance = Math.Sqrt( distanceX * distanceX + distanceY * distanceY );
return distance < this._radius;
(This code does not change any assumptions about the location of the circle).
Good evening, I know on the web there are similar questions and a few tutorials about it, but I'd like you to check my code and correct it. I mean, I'd like to know what's wrong with my project.
I have to draw a parabola graph given its equation on my main panel.
I also must include two buttons, zoom in and zoom out, which are used to reduce and enlarge the "view" panel's view (and so the parabola).
I was recommended to use a scale var.
This is my code:
note: x0, y0 are panel_main x center, y center.
I have x, y that are used to determine x,y from the equation.
xpc, ypc are converted for the window scale (so are pixels).
xmin, xmax are the extreme values that, with a certain scale, stay on the panel
I hope you can give me a hint, thanks a lot!
public void DisegnaParabola()
{
Graphics gs = panel_main.CreateGraphics();
pen.Color = Color.Black;
scale = (x0*2) / zoom; //Pixels equivalent to 1x or 1y
n_punti = (x0*2) / scale; //Number of x math points that are visible in window
xmin = -(n_punti / 2);
xmax = n_punti / 2;
precision = 1 / scale; //Increment of x to have 1px
if (asse_parabola.SelectedIndex == 0) //if Y axis
{
for (double i = xmin + precision; i < xmax; i += precision)
{
rifx = i - precision; //Old points
rifxpc = rifx * scale;
rify = (a * Math.Pow(rifx, 2)) + b * rifx + c;
rifypc = y0 - (rify * scale);
x = i; //New points
y = (a * Math.Pow(x, 2)) + b * x + c;
ypc = y0 - (y * scale);
gs.DrawLine(pen, (float)rifxpc, (float)rifypc, (float)xpc, (float)ypc);
}
}
else
{
scale = (y0*2) / zoom; //Pixels for 1y
n_punti = (y0*2) / scale; //Numbers of y in the window
ymin = -(n_punti / 2);
ymax = n_punti / 2;
for(double i=ymin+precision; i<ymax; i+=precision)
{
rify = y - precision;
rifypc = (y0*2) - rify * scale;
rifx = (a * Math.Pow(rify, 2)) + b * rify + c;
rifxpc = x0 + (rifx * scale);
y = i;
x = (a * Math.Pow(y, 2)) + b * y + c;
xpc = x0 + (x * scale);
gs.DrawLine(pen, (float)rifypc, (float)rifxpc, (float)ypc, (float)xpc);
}
}
lbl_canc.Visible = true;
}
Your question actually consists of several tasks and as usual the key is to take and break those apart..
One issue is getting the data, I will leave the details to you but show how to sparate it from the rest.
The next issue is to scale the data. I'll show you how to avoid this one altogether and scale the drawing tool instead.
And the third one is to draw them to a display surface. As you'll see this is really simple once the other issues are taken care of.
Let's start with the most important step: Collecting the data. You try to create and scale and draw them all in the same piece of code. This has many disadvantages..
Let's first collect the data in a suitable structure:
List<PointF> points = new List<PointF>();
List<T> is the collection of choice most of the time; certainly much nicer than arrays! In some method you should fill that list with your data, calculated from some formula.
Here is an example:
List<PointF> getPoints(float start, float end, int count, float ymax)
{
List<PointF> points = new List<PointF>();
float deltaX = (end - start) / count;
for (int i = 0; i < count; i++)
{
float x = i * deltaX;
// insert your own formula(s) here!
float y = ymax + (float)Math.Sin(x * somefactor) * ymax;
points.Add(new PointF(x, y));
}
return points;
}
Now for the second important part: How to scale the data? This can be done either when creating them; but again, separating the two taks makes them both a lot simpler.
So here is a function that, instead of scaling the data scales the Graphics object we will use to plot them:
void ScaleGraphics(Graphics g, List<PointF> data)
{
float xmax = data.Select(x => x.X).Max();
float ymax = data.Select(x => x.Y).Max();
float xmin = data.Select(x => x.X).Min();
float ymin = data.Select(x => x.Y).Min();
float width = Math.Abs(xmax - xmin);
float height = Math.Abs(ymax - ymin);
var vr = g.VisibleClipBounds;
g.ScaleTransform(vr.Width / width, vr.Height / height);
}
This method makes sure that all the data in our list will fit into the drawing surface. If you want to restrict them to a different size you can pass it in and change the code accordingly..
Finally we need to do the actual drawing. We do that where we should, that is in the Paint event of our drawing surface control..:
private void panel1_Paint(object sender, PaintEventArgs e)
{
if (points.Count < 2) return; // no lines to draw, yet
ScaleGraphics(e.Graphics, points);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
using ( Pen pen = new Pen(Color.Blue )
{ Width = 1.5f , LineJoin = LineJoin.Round, MiterLimit = 1f} )
e.Graphics.DrawLines(pen, points.ToArray());
}
The title for this post was quite hard to think of, so if you can think of a more descriptive title please tell me. Anyway, my problem is quite specific and requires some simple maths knowledge. I am writing a C# WinForms application which is a bit like the old 'xeyes' Linux application. It basically is a set of eyes which follow around your mouse cursor. This may sound easy at first, however can get rather complicated if you're a perfectionist like me :P. This is my code so far (only the paint method, that is called on an interval of 16).
int lx = 35;
int ly = 50;
int rx;
int ry;
int wx = Location.X + Width / 2;
int wy = Location.Y + Height / 2;
Rectangle bounds = Screen.FromControl(this).Bounds;
// Calculate X
float tempX = (mx - wx) / (float)(bounds.Width / 2);
// Calculate Y
float tempY = (my - wy) / (float)(bounds.Height / 2);
// Draw eyes
e.Graphics.FillEllipse(Brushes.LightGray, 10, 10, 70, 100);
e.Graphics.FillEllipse(Brushes.LightGray, 90, 10, 70, 100);
// Draw pupils (this only draws the left one)
e.Graphics.FillEllipse(Brushes.Black, lx += (int)(25 * tempX), ly += (int)(40 * tempY), 20, 20);
Now this does work at a basic level, however sometimes this can happen if the user puts the cursor at 0,0.
Now my question is how to fix this? What would the IF statement be to check where the mouse pointer is, and then reduce the pupil X depending on that?
Thanks.
Edit: This is where I get the mouse positions (my and mx):
private void timer_Tick(object sender, EventArgs e)
{
mx = Cursor.Position.X;
my = Cursor.Position.Y;
Invalidate();
}
The timer is started in the eyes_Load event and the interval is 16.
Edit 2: Final solution: http://pastebin.com/fT5HfiQR
Modelling the eyeball as the following ellipse:
Its equation is:
And that of the line joining its center and the cursor:
(don't worry about the singularity)
We can then solve to get the intersection point:
Where
Now you can calculate the distance to the eyeball's edge, by dividing the distance from the center to the cursor by sigma. What remains is just interpolating to cap the position of the pupil:
The if statement you want is then
(N.B. for math-mo's out there the above was a slight simplification, which assumes your ellipse is not too narrow; the exact solution is non-analytical)
EDIT: my tests in VB.NET:
EDIT 2: C# port
PointF Bound(double xc, double yc, double w, double h, double xm, double ym, double r)
{
double dx = xm - xc, dy = ym - yc;
if (Math.Abs(dx) > 0.001 && Math.Abs(dy) > 0.001)
{
double dx2 = dx * dx, dy2 = dy * dy;
double sig = 1.0 / Math.Sqrt(dx2 / (w * w * 0.25) + dy2 / (h * h * 0.25));
double d = Math.Sqrt(dx2 + dy2), e = d * sig;
if (d > e - r)
{
double ratio = (e - r) / d;
return new PointF((float)(xc + dx * ratio),
(float)(yc + dy * ratio));
}
}
return new PointF((float)xm, (float)ym);
}
xc, yc: Center coordinates of the ellipse
w, h: Width and height of the ellipse
xm, ym: Mouse coordinates
r: Radius of the circle you wanna constrain (the pupil)
Returns: The point where you wanna place the center of the circle
EDIT 3: Many thanks to Quinchilion for the following optimization (gawd damn this smacked me hard in the face)
PointF Bound(double xc, double yc, double w, double h, double xm, double ym, double r)
{
double x = (xm - xc) / (w - r);
double y = (ym - yc) / (h - r);
double dot = x*x + y*y;
if (dot > 1) {
double mag = 1.0 / Math.Sqrt(dot);
x *= mag; y *= mag;
}
return new PointF((float)(x * (w - r) + xc), (float)(y * (h - r) + yc));
}
I know this question has been asked a few times before, and I have read various posts about this. However I am struggling to get this to work.
bool isClicked()
{
Vector2 origLoc = Location;
Matrix rotationMatrix = Matrix.CreateRotationZ(-Rotation);
Location = new Vector2(0 -(Texture.Width/2), 0 - (Texture.Height/2));
Vector2 rotatedPoint = new Vector2(Game1.mouseState.X, Game1.mouseState.Y);
rotatedPoint = Vector2.Transform(rotatedPoint, rotationMatrix);
if (Game1.mouseState.LeftButton == ButtonState.Pressed &&
rotatedPoint.X > Location.X &&
rotatedPoint.X < Location.X + Texture.Width &&
rotatedPoint.Y > Location.Y &&
rotatedPoint.Y < Location.Y + Texture.Height)
{
Location = origLoc;
return true;
}
Location = origLoc;
return false;
}
Let point P(x,y), and rectangle A(x1,y1), B(x2,y2), C(x3,y3), D(x4,y4).
Calculate the sum of areas of △APD, △DPC, △CPB, △PBA.
If this sum is greater than the area of the rectangle:
Then point P(x,y) is outside the rectangle.
Else it is in or on the rectangle.
The area of each triangle can be calculated using only coordinates with this formula:
Assuming the three points are: A(x,y), B(x,y), C(x,y)...
Area = abs( (Bx * Ay - Ax * By) + (Cx * By - Bx * Cy) + (Ax * Cy - Cx * Ay) ) / 2
I assume that Location is the rectangle's rotation center. If not, please update your answer with an appropriate figure.
What you want to do is expressing the mouse location in the local system of the rectangle. Therfore, you could do the following:
bool isClicked()
{
Matrix rotationMatrix = Matrix.CreateRotationZ(-Rotation);
//difference vector from rotation center to mouse
var localMouse = new Vector2(Game1.mouseState.X, Game1.mouseState.Y) - Location;
//now rotate the mouse
localMouse = Vector2.Transform(localMouse, rotationMatrix);
if (Game1.mouseState.LeftButton == ButtonState.Pressed &&
rotatedPoint.X > -Texture.Width / 2 &&
rotatedPoint.X < Texture.Width / 2 &&
rotatedPoint.Y > -Texture.Height / 2 &&
rotatedPoint.Y < Texture.Height / 2)
{
return true;
}
return false;
}
Additionally, you may want to move the check if the mouse is pressed to the beginning of the method. If it is not pressed, you don't have to calculate the transformation etc.