I am displaying ScatterSeries data and it happens routinely that some of the data points in these series will overlap - either they are "really close" or actually exist at the same X/Y position.
Right now I use the SeriesMouseLeftButtonUp argument data to determine which series/item was clicked and that works well except for the situation I mentioned above. What I'd like to be able to do is do some sort of hit test and do something meaningful with those overlapping points such as displaying them in another pop-up chart so that the user can choose among the points.
Do you have ideas about how I might perform a hit test? I may have overlooked an example of this situation that was posted elsewhere. I'm assuming I'm not the only person to have to handle this sort of thing.
I'll get back to you on this in a short while. Should be able to show you a neat sample.
-Graham
On a side note, if you enable zooming on the chart, a zoom in would let the user distinguish between the overlapping points also.
Yes, that's true (I am using the zooming feature on the chart in this instance and it works really well.) except, of course, where the points really do have the same x/y values (which unfortunately happens sometimes with my data).
Regards,
Matt
Yes, that's exactly the sort of thing I was wanting to do. Thanks - it is very helpful.
Is this the sort of thing you were going for? You mentioned a drilldown chart, but that still wouldnt help if two points shared the same value. Let me know if you were shooting for something else.
Hope this helps!
Here is just one way of going about it. Other ways involve translating from the clicked point back to axis values and examining the data source bound to the series for the closest points, but this way lets you deal with pixel values which is what you are most concerned with seeing as how the problem is when the points are occluding each other.
You can adjust the nearness value to control how close the points need to be in pixel distance before being pulled into the list. I did this in Silverlight before noticing you were doing WPF there. You may need to set the grid containing the chart as the PlacementTarget of the popup in WPF before this works there, or something of that nature.
<UserControl.Resources> <local:TestData x:Key="data" /> </UserControl.Resources> <Grid x:Name="LayoutRoot" Background="White"> <Grid x:Name="chartContainer"> <igChart:XamDataChart x:Name="theChart" SeriesMouseLeftButtonUp="theChart_SeriesMouseLeftButtonUp"> <igChart:XamDataChart.Axes> <igChart:NumericXAxis x:Name="xAxis" /> <igChart:NumericYAxis x:Name="yAxis" /> </igChart:XamDataChart.Axes> <igChart:XamDataChart.Series> <igChart:ScatterSeries x:Name="scatter" XAxis="{Binding ElementName=xAxis}" YAxis="{Binding ElementName=yAxis}" XMemberPath="X" YMemberPath="Y" ItemsSource="{StaticResource data}" /> </igChart:XamDataChart.Series> </igChart:XamDataChart> <Popup x:Name="thePopup" IsOpen="False"> <Border Background="White" BorderThickness="1" BorderBrush="Gray"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition /> </Grid.RowDefinitions> <TextBlock Text="X" Grid.Row="0" Grid.Column="1" MouseLeftButtonUp="TextBlock_MouseLeftButtonUp"/> <ItemsControl ItemsSource="{Binding}" Grid.Row="1" Margin="5"> <ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <ContentControl ContentTemplate="{Binding Series.LegendItemBadgeTemplate}" Content="{Binding}" /> <TextBlock Text="X=" /> <TextBlock Text="{Binding Item.X}" /> <TextBlock Text=", Y=" /> <TextBlock Text="{Binding Item.Y}" /> </StackPanel> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Grid> </Border> </Popup> </Grid> </Grid>
And the code behind:
public partial class MainPage : UserControl { public MainPage() { InitializeComponent(); } private const double Nearness = 10.0; private void theChart_SeriesMouseLeftButtonUp( object sender, DataChartMouseButtonEventArgs e) { Point pos = e.GetPosition(chartContainer); var hitItems = GetHitItems(e); thePopup.DataContext = hitItems; thePopup.HorizontalOffset = pos.X; thePopup.VerticalOffset = pos.Y; thePopup.IsOpen = true; } private List<DataContext> GetHitItems(DataChartMouseButtonEventArgs e) { if (e.Series == null || e.Item == null) { return new List<DataContext>(); } var markers = from marker in e.Series .VisualDescendantsOfType<Marker>() where marker.Visibility == Visibility.Visible select marker; var hit = (from marker in markers where marker.Content is DataContext && (marker.Content as DataContext).Item == e.Item && (marker.Content as DataContext).Series == e.Series select marker).FirstOrDefault(); if (hit == null) { return new List<DataContext>(); } var hitPoint = hit.TransformToVisual(e.Series) .Transform(new Point(0, 0)); var markerPositions = from marker in markers select new Tuple<Marker, Point>( marker, marker.TransformToVisual(e.Series) .Transform(new Point(0, 0))); var nearnessSquared = Nearness * Nearness; var closeMarkers = from markerAndPos in markerPositions where Math.Pow(markerAndPos.Item2.X - hitPoint.X, 2) + Math.Pow(markerAndPos.Item2.Y - hitPoint.Y, 2) < nearnessSquared orderby Math.Pow(markerAndPos.Item2.X - hitPoint.X, 2) + Math.Pow(markerAndPos.Item2.Y - hitPoint.Y, 2) select markerAndPos.Item1; List<DataContext> items = (from marker in closeMarkers where marker.Content is DataContext select marker.Content as DataContext) .ToList(); return items; } private void TextBlock_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { thePopup.IsOpen = false; } } public static class VisualTree { public static IEnumerable<DependencyObject> VisualDescendants( this DependencyObject parent) { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) { var current = VisualTreeHelper.GetChild(parent, i); yield return current; foreach (var child in current.VisualDescendants()) { yield return child; } } } public static IEnumerable<T> VisualDescendantsOfType<T>( this DependencyObject parent) { return parent.VisualDescendants().OfType<T>(); } } public class TestData : ObservableCollection<TestDataItem> { public TestData() { Add(new TestDataItem() { X = 1, Y = 1 }); Add(new TestDataItem() { X = 2, Y = 2 }); Add(new TestDataItem() { X = 3, Y = 3 }); Add(new TestDataItem() { X = 3.01, Y = 3.01 }); Add(new TestDataItem() { X = 3.02, Y = 3.02 }); Add(new TestDataItem() { X = 4, Y = 4 }); Add(new TestDataItem() { X = 5, Y = 5 }); Add(new TestDataItem() { X = 6, Y = 6 }); } } public class TestDataItem { public double X { get; set; } public double Y { get; set; } }
Yes, I'll see how I can help you out for that case.