Chart: PixelPositionToValue not accurate when finding x,y coordinates - c#

I am trying to display the X and Y coordinates of the chart data in a small display. Everything works well but the data shown isn't accurate.
Here is the code below:
var results = chart1.HitTest(e.X, e.Y, false, ChartElementType.PlottingArea);
foreach (var result in results)
{
if (result.ChartElementType == ChartElementType.PlottingArea)
{
yValue = chart1.ChartAreas[0].AxisY2.PixelPositionToValue(e.Y);
xValue = chart1.ChartAreas[0].AxisX2.PixelPositionToValue(e.X);
}
}
if (OverlapcheckBox1.Checked)
{
int val = Convert.ToInt16(yValue / 24);
yValue = yValue - 24 * val;
}
if (Cursor1checkBox.Checked && ClickMouse)
{
V1textBox1.Text = string.Concat(string.Concat(yValue).ToString());
}
if (Cursor2checkBox.Checked && ClickMouse)
{
V2textBox2.Text = string.Concat(string.Concat(yValue).ToString());
}
The image shows cursor at 10 but the value in V1 is 9.88
And an image:

Unless your mouse has amazing accuracy you would never see an exact 10.000. You could round it off:
private void Chart1_MouseClick(object sender, MouseEventArgs e) {
double yValue = chart1.ChartAreas[0].AxisY.PixelPositionToValue(e.Y);
yValue = Math.Round(yValue, 0);
}
Or perhaps you wish to find DataPoints near the cursor click position?
private void Chart1_MouseClick(object sender, MouseEventArgs e) {
HitTestResult result = chart1.HitTest(e.X, e.Y);
if (result.ChartElementType == ChartElementType.DataPoint) {
DataPoint point = (DataPoint)result.Object;
double yValue = point.YValues[0];
}
}

Related

How to draw moving axis table

I'm trying to draw an axis table (x-y) in WPF from code-behind; and I want to give it drag and drop option which can see more of the axis table.
I had created static axis but I don't know how to create a dynamic one?
Can anybody help me with this stuff ?
Thanks.
for (int i = 10; i < 400; i+=10)
{
Line a = new Line();
a.X1 = 0;
a.Y1 = i;
a.X2 = canGraph.Width;
a.Y2 = a.Y1;
a.Stroke = System.Windows.Media.Brushes.Black;
a.StrokeThickness = 0.5;
canGraph.Children.Add(a);
Line b = new Line();
b.X1 = i;
b.Y1 = 0;
b.X2 = i;
b.Y2 = canGraph.Height;
b.Stroke = System.Windows.Media.Brushes.Black;
b.StrokeThickness = 0.5;
canGraph.Children.Add(b);
if (i % 50 == 0)
{
a.StrokeThickness = 1;
b.StrokeThickness = 1;
}
if (i == 200)
{
a.StrokeThickness = 2;
b.StrokeThickness = 2;
}
}
This should get you started. Add event handler(s) to your main axis and canGraph -
...
if (i == 200)
{
a.StrokeThickness = 2;
b.StrokeThickness = 2;
a.MouseLeftButtonDown += A_MouseLeftButtonDown;
}
}
canGraph.MouseLeftButtonUp += CanGraph_MouseLeftButtonUp;
canGraph.MouseMove += CanGraph_MouseMove;
Add following methods -
Line _selectedAxis = null;
private void CanGraph_MouseMove(object sender, MouseEventArgs e)
{
if (_selectedAxis != null)
{
var line = _selectedAxis;
var pos = e.GetPosition(line);
textBlock.Text = $"({pos.X}, {pos.Y})";
line.Y1 = pos.Y;
line.Y2 = pos.Y;
}
}
private void CanGraph_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_selectedAxis = null;
}
private void A_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var line = sender as Line;
_selectedAxis = line;
}
Now hold you main horizontal axis and drag it.
You can do the same for vertical axis as well.
For Zooming
Initialize canGraph.RenderTransform with ScaleTransform and subscribe to MouseWheel event. Note RenderTransformOrigin is set to (0.5, 0.5) to zoom from center instead of top left (default) -
canGraph.RenderTransformOrigin = new Point(0.5, 0.5);
canGraph.RenderTransform = new ScaleTransform();
canGraph.MouseWheel += CanGraph_MouseWheel;
And the function -
private void CanGraph_MouseWheel(object sender, MouseWheelEventArgs e)
{
var transform = canGraph.RenderTransform as ScaleTransform;
var factor = transform.ScaleX;
factor += (e.Delta > 0 ? 1 : (factor == 1 ? 0 : -1));
transform.ScaleX = factor;
transform.ScaleY = factor;
}
I'm guessing you have added Line type object to draw axes, then gave it to window content.
Then just add events, like MouseLeftButtonDown event, or MouseMove event.Add appropriate methods.
Change your object positions on MouseMove event, like:
(For a certain line)
private void MouseMoveMethod(object sender, MouseEventArgs e)
{
var obj = sender as Line;
obj.X1 = e.GetPosition(this).X; //Line start x coordinate
obj.Y1 = e.GetPosition(this).Y; //Line start y coordinate
...
}

Creating list from multiple list?

I'm not sure if I'm going about this right. What I need is after the user creates all of their lines, could be 1 could be 10. I can calculate the length of those lines, take the zone that line appeared in and add it to a list.
So in the end you have for example
Length Location
2 1
4 2
3 1
8 1
Afterwards I will be adding this data to their respective columns on the oracle server. Is a list appropriate? I get an out of bounds error currently on Zone1 and distfinal. If I just do one line then I get a length calc but an out of bound error on Zone1
List<string> Zone1 = new List<string>();
private Point p1, p2;
List<Point> p1List = new List<Point>();
List<Point> p2List = new List<Point>();
Dictionary<string, int> Void = new Dictionary<string, int>();
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button.Equals(MouseButtons.Left))
{
if (p1.X == 0)
{
p1.X = e.X;
p1.Y = e.Y;
var color = zoneMap1.GetPixel(e.X, e.Y);
if (color == Color.FromArgb(0, 0, 255))
{
//MessageBox.Show("Zone 1");
Zone1.Add("1");
}
else if (color == Color.FromArgb(0, 255, 0))
{
//MessageBox.Show("Zone 2");
Zone1.Add("2");
}
}
else
{
p2.X = e.X;
p2.Y = e.Y;
p1List.Add(p1);
p2List.Add(p2);
Invalidate();
pictureBox1.Refresh();
p1.X = 0;
}
}
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
using (var p = new Pen(Color.Red, 5))
{
for (int x = 0; x < p1List.Count; x++)
{
e.Graphics.DrawLine(p, p1List[x], p2List[x]);
}
}
}
With what I have here, assuming I am going about this correctly, the errors occur with Void.Add(Zone1[i], distfinal);
Ultimately I'd like to just create all the lines. Then use the below button to create the example I gave at the top.
private void btnCalc_Click(object sender, EventArgs e)
{
for (int i = 0; i < p1List.Count; i++)
{
if (p1List.Count != 0 && p2List.Count != 0)
{
dist = (Convert.ToInt32(Math.Pow(p1.X - p1.Y, 2) + Math.Pow(p2.X - p2.Y, 2)));
int distfinal = (dist % 32);
Void.Add(Zone1[i], distfinal);
}
else
{
MessageBox.Show("You must first create a line");
}
}
}
Well, learned Tuple :)
This fixed it.
var list = new List<Tuple<string, int>>();
list.Add(new Tuple<string, int>(Zone1[i], distfinal));

Getting DataPoint with mouse X coordinate only

I need to access the DataPoint via X coordinate, but I can't seem to find any solutions for it. HitTest() is out, since it also needs Y coordinate (cannot hit anything if Y is 0). I know I could select the DataPoints via Linq and the X coordinate, but I was wondering if there are better solutions for this?
More specifically I'm trying to show Candlestick data (Open, Low, High, Close and DateTime) with a TextAnnotation, which follows the CursorX (it's anchored above my X-axis, which is located at the bottom of my chart), and my CursorX is intervalled so it is always in the center of a candle.
What I have so far, is a MouseMove function, which updates my CursorX, but lacks the data access and label update:
PointF _mouse = new PointF();
TextAnnotation _mouseLabel;
void UpdateMouseCursor(object sender, MouseEventArgs e)
{
if( _mouseLabel == null )
return;
_mouse.X = e.Location.X;
_mouse.Y = e.Location.Y;
var cursorX = chart1.ChartAreas[0].CursorX;
cursorX.SetCursorPixelPosition( _mouse, true );
_mouseLabel.X = cursorX.Position;
}
CursorX and _mouseLabel setup:
public void SetupMouse(double interval, DateTimeIntervalType intervalType)
{
chart1.ChartAreas[0].CursorX.IntervalType = intervalType;
chart1.ChartAreas[0].CursorX.Interval = interval;
chart1.ChartAreas[0].CursorX.LineColor = Color.FromArgb(128, 128,128,128);
if( _mouseLabel == null )
{
_mouseLabel = new TextAnnotation();
_mouseLabel.Text = "WOOHOO!";
_mouseLabel.AnchorY = 85;
_mouseLabel.AxisX = chart1.ChartAreas[0].AxisX;
chart1.Annotations.Add( _mouseLabel );
}
}
The visuals:

Prevent ToolStripMenuItems from jumping to second screen

I have an application that is mostly operated through NotifyIcon's ContextMenuStrip
There are multiple levels of ToolStripMenuItems and the user can go through them.
The problem is, that when the user has two screen, the MenuItems jump to second screen when no space is available. like so:
How can I force them to stay on the same screen? I've tried to search through the web but couldn't find an appropriate answer.
Here is a sample piece of code i'm using to test this senario:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
var resources = new ComponentResourceManager(typeof(Form1));
var notifyIcon1 = new NotifyIcon(components);
var contextMenuStrip1 = new ContextMenuStrip(components);
var level1ToolStripMenuItem = new ToolStripMenuItem("level 1 drop down");
var level2ToolStripMenuItem = new ToolStripMenuItem("level 2 drop down");
var level3ToolStripMenuItem = new ToolStripMenuItem("level 3 drop down");
notifyIcon1.ContextMenuStrip = contextMenuStrip1;
notifyIcon1.Icon = ((Icon)(resources.GetObject("notifyIcon1.Icon")));
notifyIcon1.Visible = true;
level2ToolStripMenuItem.DropDownItems.Add(level3ToolStripMenuItem);
level1ToolStripMenuItem.DropDownItems.Add(level2ToolStripMenuItem);
contextMenuStrip1.Items.Add(level1ToolStripMenuItem);
}
}
It is not easy, but you can write code in the DropDownOpening event to look at where the menu is at (its bounds), the current screen, and then set the DropDownDirection of the ToolStripMenuItem:
private void submenu_DropDownOpening(object sender, EventArgs e)
{
ToolStripMenuItem menuItem = sender as ToolStripMenuItem;
if (menuItem.HasDropDownItems == false)
{
return; // not a drop down item
}
// Current bounds of the current monitor
Rectangle Bounds = menuItem.GetCurrentParent().Bounds;
Screen CurrentScreen = Screen.FromPoint(Bounds.Location);
// Look how big our children are:
int MaxWidth = 0;
foreach (ToolStripMenuItem subitem in menuItem.DropDownItems)
{
MaxWidth = Math.Max(subitem.Width, MaxWidth);
}
MaxWidth += 10; // Add a little wiggle room
int FarRight = Bounds.Right + MaxWidth;
int CurrentMonitorRight = CurrentScreen.Bounds.Right;
if (FarRight > CurrentMonitorRight)
{
menuItem.DropDownDirection = ToolStripDropDownDirection.Left;
}
else
{
menuItem.DropDownDirection = ToolStripDropDownDirection.Right;
}
}
Also, make sure you have the DropDownOpening event hooked up (you would really need to add this to every menu item):
level1ToolStripMenuItem += submenu_DropDownOpening;
I have solved it this way:
For the ContextMenuStrip itself to open on a desired screen, I created a ContextMenuStripEx with the following methods:
protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified)
{
Rectangle dropDownBounds = new Rectangle(x, y, width, height);
dropDownBounds = ConstrainToBounds(Screen.FromPoint(dropDownBounds.Location).Bounds, dropDownBounds);
base.SetBoundsCore(dropDownBounds.X, dropDownBounds.Y, dropDownBounds.Width, dropDownBounds.Height, specified);
}
internal static Rectangle ConstrainToBounds(Rectangle constrainingBounds, Rectangle bounds)
{
if (!constrainingBounds.Contains(bounds))
{
bounds.Size = new Size(Math.Min(constrainingBounds.Width - 2, bounds.Width), Math.Min(constrainingBounds.Height - 2, bounds.Height));
if (bounds.Right > constrainingBounds.Right)
{
bounds.X = constrainingBounds.Right - bounds.Width;
}
else if (bounds.Left < constrainingBounds.Left)
{
bounds.X = constrainingBounds.Left;
}
if (bounds.Bottom > constrainingBounds.Bottom)
{
bounds.Y = constrainingBounds.Bottom - 1 - bounds.Height;
}
else if (bounds.Top < constrainingBounds.Top)
{
bounds.Y = constrainingBounds.Top;
}
}
return bounds;
}
(ConstrainToBounds method is taken from the base class ToolStripDropDown via Reflector)
for the nested MenuItems to open on the same screen as ContextMenuStrip, I created a ToolStripMenuItemEx (which derives from ToolStripMenuItem). In my case it looks like this:
private ToolStripDropDownDirection? originalToolStripDropDownDirection;
protected override void OnDropDownShow(EventArgs e)
{
base.OnDropDownShow(e);
if (!Screen.FromControl(this.Owner).Equals(Screen.FromPoint(this.DropDownLocation)))
{
if (!originalToolStripDropDownDirection.HasValue)
originalToolStripDropDownDirection = this.DropDownDirection;
this.DropDownDirection = originalToolStripDropDownDirection.Value == ToolStripDropDownDirection.Left ? ToolStripDropDownDirection.Right : ToolStripDropDownDirection.Left;
}
}
The code of #David does not fix if the menu is opened in the left side of second screen. I have improved that code to work on all screen corner.
private void subMenu_DropDownOpening(object sender, EventArgs e)
{
ToolStripMenuItem mnuItem = sender as ToolStripMenuItem;
if (mnuItem.HasDropDownItems == false)
{
return; // not a drop down item
}
//get position of current menu item
var pos = new Point(mnuItem.GetCurrentParent().Left, mnuItem.GetCurrentParent().Top);
// Current bounds of the current monitor
Rectangle bounds = Screen.GetWorkingArea(pos);
Screen currentScreen = Screen.FromPoint(pos);
// Find the width of sub-menu
int maxWidth = 0;
foreach (var subItem in mnuItem.DropDownItems)
{
if (subItem.GetType() == typeof(ToolStripMenuItem))
{
var mnu = (ToolStripMenuItem) subItem;
maxWidth = Math.Max(mnu.Width, maxWidth);
}
}
maxWidth += 10; // Add a little wiggle room
int farRight = pos.X + mnuMain.Width + maxWidth;
int farLeft = pos.X - maxWidth;
//get left and right distance to compare
int leftGap = farLeft - currentScreen.Bounds.Left;
int rightGap = currentScreen.Bounds.Right - farRight;
if (leftGap >= rightGap)
{
mnuItem.DropDownDirection = ToolStripDropDownDirection.Left;
}
else
{
mnuItem.DropDownDirection = ToolStripDropDownDirection.Right;
}
}
I did not try the solution by tombam. But since the others didn't seem to work, I came up with this simple solution:
private void MenuDropDownOpening(object sender, EventArgs e)
{
var menuItem = sender as ToolStripDropDownButton;
if (menuItem == null || menuItem.HasDropDownItems == false)
return; // not a drop down item
// Current bounds of the current monitor
var upperRightCornerOfMenuInScreenCoordinates = menuItem.GetCurrentParent().PointToScreen(new Point(menuItem.Bounds.Right, menuItem.Bounds.Top));
var currentScreen = Screen.FromPoint(upperRightCornerOfMenuInScreenCoordinates);
// Get width of widest child item (skip separators!)
var maxWidth = menuItem.DropDownItems.OfType<ToolStripMenuItem>().Select(m => m.Width).Max();
var farRight = upperRightCornerOfMenuInScreenCoordinates.X + maxWidth;
var currentMonitorRight = currentScreen.Bounds.Right;
menuItem.DropDownDirection = farRight > currentMonitorRight ? ToolStripDropDownDirection.Left :
ToolStripDropDownDirection.Right;
}
Note that in my world, I was not concerned about multiple levels of cascading menus (as in the OP), so I did not test my solution in that scenario. But this works correctly for a single ToolStripDropDownButton on a ToolStrip.

C# MS Chart Control - two questions

So I've made this range bar chart with the MS Chart Control. I have two questions:
How can I implement an event handler for when the user double clicks a series? I can't see one anywhere.
For some reason the scrollbar on my X Axis (which, amusingly, the Chart Control seems to think is the Y Axis...) seems to be partially transparent for some reason...can anyone shed any light as to why that might be?
Here's my code so far, bastardised from a PDF I found somewhere on the net (yeah, I know, it's messy, needs tidying up):
private void PopulateGantt()
{
foreach (Job jobThis in lstJobs)
{
if ((jobThis.HireFrom != null) && (jobThis.HireTo != null))
{
string xlabel = string.Empty;
double xordinal = 0;
double yplot1 = 0;
double yplot2 = 0;
yplot1 = (double)((DateTime)jobThis.HireFrom).ToOADate();
yplot2 = (double)((DateTime)jobThis.HireTo).ToOADate()+1;
// Use a different series for each datapoint
Series seriesInstance = new Series();
// For Gantt charts, we want a RangeBar graph type
seriesInstance.ChartType = SeriesChartType.RangeBar;
// Have a start and end date so plotting 2 points on the y-axis
seriesInstance.YValuesPerPoint = 2;
// We want to draw datapoint side by side (night is day?)
seriesInstance.CustomProperties = "DrawSideBySide=false";
// Add the datapoint to the series, specifying tooltiptext, colorand label
xordinal = lstJobs.IndexOf(jobThis); //(double)itemIndex;
seriesInstance.Points.AddXY(xordinal, yplot1, yplot2);
//seriesInstance.Points[0].Color = resourceColor;
seriesInstance.Points[0].AxisLabel = xlabel;
seriesInstance.Label = jobThis.Number + jobThis.Type + " - " + jobThis.ClientCompanyName;
seriesInstance.Points[0].ToolTip = jobThis.Number + jobThis.Type +
"\r\n\r\n" + jobThis.ClientCompanyName +
"\r\n\r\n" + jobThis.BriefDescription;
seriesList.Add(seriesInstance);
}
chtHC.Series.Clear();
foreach (Series plotSeries in seriesList)
{
chtHC.Series.Add(plotSeries);
}
// Force x-axis to show each task or resource
chtHC.ChartAreas[0].AxisX.Interval = 1;
// Set y-axis to show each day of the month
chtHC.ChartAreas[0].AxisY.Interval = 1;
// Set x-axis to show in reversed order so dates are displayed leftto-right
//chtHC.ChartAreas[0].AxisY.IsReversed = true;
//chtHC.ChartAreas[0].AxisX
// Set other y-axis properties
chtHC.ChartAreas[0].AxisY.IsStartedFromZero = false;
chtHC.ChartAreas[0].AxisY.IsMarginVisible = false;
chtHC.ChartAreas[0].AxisY.IntervalType = DateTimeIntervalType.Days;
// Set the y-axis labels
DateTime? datFirst = null;// = new DateTime();
DateTime? datLast = null; //= new DateTime();
//datFirst = (DateTime)lstJobs[0].HireFrom;
foreach (Job jobFirst in lstJobs)
{
if (jobFirst.HireFrom != null)
{
if (datFirst == null)
{
datFirst = (DateTime)jobFirst.HireFrom;
}
else
{
if (jobFirst.HireFrom < datFirst)
{
datFirst = (DateTime)jobFirst.HireFrom;
}
}
}
if (jobFirst.HireTo != null)
{
if (datLast == null)
{
datLast = (DateTime)jobFirst.HireTo;
}
else
{
if (jobFirst.HireTo > datLast)
{
datLast = (DateTime)jobFirst.HireTo;
}
}
}
}
if ((datFirst != null))
{
//datLast = ((DateTime)datFirst).AddDays(21);
chtHC.ChartAreas[0].AxisY.Minimum = ((DateTime)datFirst).AddDays(-1).ToOADate();
chtHC.ChartAreas[0].AxisY.Maximum = ((DateTime)datLast).AddDays(+1).ToOADate();
}
chtHC.ChartAreas[0].CursorY.AutoScroll = true;
chtHC.ChartAreas[0].AxisY.ScaleView.Zoomable = true;
chtHC.ChartAreas[0].AxisY.ScaleView.SizeType = DateTimeIntervalType.Days;
//chtHC.ChartAreas[0].AxisY.LabelStyle.Format = "MMM dd ddd";
//chtHC.ChartAreas[0].AxisY.LabelStyle.Format = "ddd MMM dd";
chtHC.ChartAreas[0].AxisY.LabelStyle.Format = "ddd dd/MM/yyyy";
//chtHC.ChartAreas[0].AxisX.// Force redraw of chart
chtHC.ChartAreas[0].AxisY.ScaleView.Zoom(chtHC.ChartAreas[0].AxisY.Minimum, chtHC.ChartAreas[0].AxisY.Minimum+21);
chtHC.ChartAreas[0].AxisY.ScrollBar.ButtonStyle = ScrollBarButtonStyles.SmallScroll;
chtHC.ChartAreas[0].AxisY.ScaleView.SmallScrollSize = 1;
chtHC.Update();
}
}
There's no specific event able to handle datapoint clicks, but you can use MouseClick event plus HitTest method, e.g.:
void chart1_MouseClick(object sender, MouseEventArgs e)
{
var pos = e.Location;
var results = chart1.HitTest(pos.X, pos.Y,false, ChartElementType.DataPoint);
foreach (var result in results)
{
if (result.ChartElementType == ChartElementType.DataPoint)
{
// use result.Series etc...
}
}
}
the chart has also a double click event
private void chart_MouseDoubleClick(object sender, MouseEventArgs e)

Categories