In the XamDataChart We want to be able to only display the marker to which the mouse's x-coordinate is closest to. In other words, we want to visually track the current x-location along the chart.
This idea is similar to Google Finance. It displays a circle on the chart.
In the attached picture, obviously three circles are displayed - one for each stock.
Hope this makes sense. Thanks in advance.
Adam
Hi ,
It doesnt work accurately if there are lots of Y variations over short x interval (lots of points over short X interval), the reason i found was not all the markers are created for all the points... for example i have 400 pts in the chart, but Markers count in TrackingGrid Items is always 50, so I am not getting the accurate marker for a particulate point. I am using splineSeries... I tries setting UseHighMarkerFiedlity also but it doesnt help
Here, try this version instead. The other version has some issues with WPF, and this version is simpler:The Xaml:
<UserControl.Resources> <local:TestData x:Key="data1" /> <local:TestData x:Key="data2" /> <DataTemplate x:Key="customTemplate"> <local:TrackingGrid x:Name="trackingGrid" Series="{Binding Series}" VisibilityItem="{Binding ElementName=visibilityItem}"> <Ellipse x:Name="seriesMarker" Stretch="Fill" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Fill="Transparent" Stroke="Transparent" StrokeThickness="0.5" MinWidth="15" MinHeight="15" > </Ellipse> <local:VisibilityItem x:Name="visibilityItem" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" Visibility="Collapsed"> <Ellipse Stretch="Fill" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Fill="{Binding ActualItemBrush}" Stroke="{Binding Series.ActualMarkerOutline}" StrokeThickness="0.5" MinWidth="15" MinHeight="15" > </Ellipse> </local:VisibilityItem> </local:TrackingGrid> </DataTemplate> </UserControl.Resources> <Grid x:Name="LayoutRoot" Background="White" > <ig:XamDataChart Name="xamDataChart1" MouseMove="XamDataChart_MouseMove"> <ig:XamDataChart.Axes> <ig:CategoryXAxis x:Name="xAxis" ItemsSource="{StaticResource data1}" > <ig:CategoryXAxis.LabelSettings > <ig:AxisLabelSettings Visibility="Collapsed" ></ig:AxisLabelSettings> </ig:CategoryXAxis.LabelSettings> </ig:CategoryXAxis> <ig:NumericYAxis x:Name="yAxis" > <ig:NumericYAxis.LabelSettings > <ig:AxisLabelSettings Visibility="Collapsed" ></ig:AxisLabelSettings> </ig:NumericYAxis.LabelSettings> </ig:NumericYAxis> </ig:XamDataChart.Axes> <ig:XamDataChart.Series> <ig:LineSeries x:Name="igLineSeries1" ItemsSource="{StaticResource data1}" XAxis="{Binding ElementName=xAxis}" YAxis="{Binding ElementName=yAxis}" ValueMemberPath="Value" MarkerType="Diamond" MarkerTemplate="{StaticResource customTemplate}"/> <ig:LineSeries x:Name="igLineSeries2" ItemsSource="{StaticResource data2}" XAxis="{Binding ElementName=xAxis}" YAxis="{Binding ElementName=yAxis}" ValueMemberPath="Value" MarkerType="Circle" MarkerTemplate="{StaticResource customTemplate}"/> </ig:XamDataChart.Series> </ig:XamDataChart> </Grid>
And the code behind:
public partial class MainPage : UserControl { public MainPage() { InitializeComponent(); } private void XamDataChart_MouseMove(object sender, MouseEventArgs e) { XamDataChart chart = sender as XamDataChart; if (chart == null) { return; } foreach (var series in chart.Series) { var seriesPos = e.GetPosition(series); if (seriesPos.X >= 0 && seriesPos.X < series.ActualWidth && seriesPos.Y >= 0 && seriesPos.Y < series.ActualHeight) { SelectClosest( series, seriesPos); } } } private static IEnumerable<DependencyObject> VisualDescendants( DependencyObject obj) { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++) { var child = VisualTreeHelper.GetChild(obj, i); yield return child; foreach (var subChild in VisualDescendants(child)) { yield return subChild; } } } public static void SelectClosest(Series series, Point point) { double minDist = double.PositiveInfinity; TrackingGrid closest = null; FrameworkElement closestContent = null; FrameworkElement beforeVisible = null; foreach (var grid in TrackingGrid.Items() .Where((i) => i.Series == series)) { double left = GetLeft(series, grid.Item); double dist = Math.Abs(point.X - left); var content = grid.VisibilityItem; if (content != null && content.Visibility == Visibility.Visible) { beforeVisible = content; content.Visibility = Visibility.Collapsed; } if (dist < minDist) { minDist = dist; closest = grid.Item; closestContent = content; } } if (closest != null) { if (closestContent != null) { closestContent.Visibility = Visibility.Visible; } } else { if (beforeVisible != null) { beforeVisible.Visibility = Visibility.Visible; } } } private static Marker FindMarker( FrameworkElement item) { while (item != null) { if (item is Marker) { return item as Marker; } item = VisualTreeHelper.GetParent(item) as FrameworkElement; } return null; } private static double GetLeft( Series series, FrameworkElement item) { Marker m = FindMarker(item); if (m == null) { return double.NaN; } if (m.Visibility == Visibility.Collapsed) { return double.NaN; } TranslateTransform t = m.RenderTransform as TranslateTransform; if (t == null) { return double.NaN; } return t.X + m.ActualWidth / 2.0; } } public class TrackingGrid : Grid { public static readonly DependencyProperty SeriesProperty = DependencyProperty.Register( "Series", typeof(Series), typeof(TrackingGrid), new PropertyMetadata(null, (o, e) => { (o as TrackingGrid) .OnSeriesChanged(e); })); private void OnSeriesChanged( DependencyPropertyChangedEventArgs e) { Refresh(); } public Series Series { get { return (Series)GetValue(SeriesProperty); } set { SetValue(SeriesProperty, value); } } public static readonly DependencyProperty VisibilityItemProperty = DependencyProperty.Register( "VisibilityItem", typeof(VisibilityItem), typeof(TrackingGrid), new PropertyMetadata(null, (o, e) => { (o as TrackingGrid) .OnVisibilityItemChanged(e); } )); private void OnVisibilityItemChanged( DependencyPropertyChangedEventArgs e) { Refresh(); } public VisibilityItem VisibilityItem { get { return (VisibilityItem)GetValue(VisibilityItemProperty); } set { SetValue(VisibilityItemProperty, value); } } public TrackingGrid() { this.Loaded += TrackingGrid_Loaded; this.Unloaded += TrackingGrid_Unloaded; } private void Refresh() { if (_items.ContainsKey(this)) { _items.Remove(this); } _items.Add(this, new ItemInfo() { Series = Series, Item = this, VisibilityItem = VisibilityItem }); } void TrackingGrid_Unloaded(object sender, RoutedEventArgs e) { _items.Remove(this); } void TrackingGrid_Loaded(object sender, RoutedEventArgs e) { Refresh(); } public class ItemInfo { public Series Series { get; set; } public TrackingGrid Item { get; set; } public VisibilityItem VisibilityItem { get; set; } } private static Dictionary<TrackingGrid, ItemInfo> _items = new Dictionary<TrackingGrid, ItemInfo>(); public static IEnumerable<ItemInfo> Items() { return _items.Values; } } public class VisibilityItem : ContentControl { } public class TestDataItem { public string Label { get; set; } public double Value { get; set; } } public class TestData : ObservableCollection<TestDataItem> { private static Random rand = new Random(); public TestData() { double curr = 0; for (int i = 0; i < 1000; i++) { if (rand.NextDouble() > .5) { curr += rand.NextDouble(); } else { curr -= rand.NextDouble(); } Add( new TestDataItem() { Label = i.ToString(), Value = curr }); } } }
Hope that helps!-Graham
Hi,
This solution depends on the crosshair event so make sure you have made the crosshairs visible but hidden. Also make sure the behavior is attached in the xaml like I did above.
-Graham
Thanks for the code. I'm having a hard time getting it to work. When I run it, I see the data and series, but no marker appears when I mouse over it. I have put breakpoints on teh OnValue2changed and OnValue1Changed methods, and they are never called.
I am interested in this more elegant solution.
Thanks,
Thank you for the answer. I will try it out when I get a chance. It appears more elegant than the solution that I used. I added a "dot" marker to the Series.RootCanvas.Children, and am positioning it on the canvas by using the GetScaledValue and GetUnscaledValue methods. Thank you!