Title: Let the user click on graph points in WPF and C#
This example shows how you can let a user click on graph points to display information about the data. (It extends the example Draw a graph with rotated text in WPF and C#.)
When the user moves the mouse over a point on the graph, the program changes its cursor to an upward pointing arrow. If the user clicks there, the program displays the data point's X and Y coordinates. Note that the values are in world coordinates, not the pixels of device coordinates.
The first step is figuring out whether the mouse is over a data point. The program uses the following FindDataPoint method to find the data point at a particular device coordinate location.
// The data.
private PointCollection[] DataPoints = new PointCollection[3];
private Brush[] DataBrushes =
{ Brushes.Red, Brushes.Green, Brushes.Blue };
// Find the data point at this device coordinate location.
// Return data_set = -1 if there is no point at this location.
private void FindDataPoint(Point location,
out int data_set, out int point_number)
{
// Check each data set.
for (data_set = 0; data_set < DataPoints.Length; data_set++)
{
// Check this data set.
for (point_number = 0;
point_number < DataPoints[data_set].Count;
point_number++)
{
// See how far the location is from the data point.
Point data_point = DataPoints[data_set][point_number];
Vector vector = location - data_point;
double dist = vector.Length;
if (dist < 3) return;
}
}
// We didn't find a point at this location.
data_set = -1;
point_number = -1;
}
The method loops through the program's DataPoints collections. The code loops through each PointCollection object's points.
For each point, the code uses subtraction to get a Vector pointing from the target location to the data point. A Vector is like an arrow stretching between two points. It's length gives the distance between the points. (You could calculate the distance yourself, but this is a bit easier.)
If the distance between the target point and the data point is less than 3 pixels, the method returns. At that point, the out variables data_set and point_number tell which point is at the target location.
If the method loops through all of the data and doesn't find a point at the target location, its sets data_set and point_number to -1 to indicate that there is no data point at the location.
This method is a bit awkward. It would be easier to use a foreach loop to iterate over the PointCollection objects and another to loop through their points, and then make the method return a Point. I used for loops with indices instead for two reasons.
First, Point is a structure not a class, so the method couldn't return null to indicate that it didn't find a point. It would need to return a "magic value" like a point with both coordinates set to double.MinValue or something. That would also be awkward, but probably acceptable.
The second reason is that I wanted to method to return both the point at the target location and the index of the DataPoints collection so the program knows which color the data point has. When you click on a data point, the following MouseUp event handler draws an ellipse using the point's color over the data point and labels it.
// To mark a clicked point.
private Ellipse DataEllipse = null;
private Label DataLabel = null;
// See if the mouse is over a data point.
private void canGraph_MouseUp(
object sender, MouseButtonEventArgs e)
{
// Find the data point at the mouse's location.
Point mouse_location = e.GetPosition(canGraph);
int data_set, point_number;
FindDataPoint(mouse_location, out data_set, out point_number);
if (data_set < 0) return;
Point data_point = DataPoints[data_set][point_number];
// Make the data ellipse if we haven't already.
if (DataEllipse == null)
{
DataEllipse = new Ellipse();
DataEllipse.Fill = null;
DataEllipse.StrokeThickness = 1;
DataEllipse.Width = 7;
DataEllipse.Height = 7;
canGraph.Children.Add(DataEllipse);
}
// Color and position the ellipse.
DataEllipse.Stroke = DataBrushes[data_set];
Canvas.SetLeft(DataEllipse, data_point.X - 3);
Canvas.SetTop(DataEllipse, data_point.Y - 3);
// Make the data label if we haven't already.
if (DataLabel == null)
{
DataLabel = new Label();
DataLabel.FontSize = 12;
canGraph.Children.Add(DataLabel);
}
// Convert the data values back into world coordinates.
Point world_point = DtoW(data_point);
// Set the data label's text and position it.
DataLabel.Content = "(" +
world_point.X.ToString("0.0") + ", " +
world_point.Y.ToString("0.0") + ")";
DataLabel.Measure(new Size(double.MaxValue, double.MaxValue));
Canvas.SetLeft(DataLabel, data_point.X + 4);
Canvas.SetTop(DataLabel,
data_point.Y - DataLabel.DesiredSize.Height);
}
This event handler uses the FindDataPoint method to get the data point under the mouse. If there is a point at that location, the code creates an Ellipse (if it wasn't previously created). It gives the Ellipse the same color as the data point, and positions it to center it over the data point.
Next the code uses the DtoW method (see the post Use transformations to draw a graph in WPF and C# for more information about the program's transformations) to convert the data point's coordinates from pixels (device coordinates) into world coordinates. It then creates a Label (if it hasn't previously created it), sets its text, and positions it above and to the right of the data point.
The last part of the program changes the cursor to the up arrow when the mouse is over a data point. The following MouseMove event handler handles this.
// Change the mouse cursor appropriately.
private void canGraph_MouseMove(object sender, MouseEventArgs e)
{
// Find the data point at the mouse's location.
Point mouse_location = e.GetPosition(canGraph);
int data_set, point_number;
FindDataPoint(mouse_location, out data_set, out point_number);
// Display the appropriate cursor.
if (data_set < 0)
canGraph.Cursor = null;
else
canGraph.Cursor = Cursors.UpArrow;
}
This event handler calls the FindDataPoint method to see if a point is at the mouse's location. If there is no point there, the code sets the cursor to null, which makes the default cursor appear. If there is a point under the mouse, the code sets the cursor to the up arrow cursor.
Download the example to experiment with it and to see additional details.
|