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
Adam,
Here's one way of approaching this. Hope it helps you out! The xaml:
<UserControl.Resources> <local:TestData x:Key="data1" /> <local:TestData x:Key="data2" /> <DataTemplate x:Key="customTemplate"> <Grid> <Grid.Resources> <local:EqualChecker x:Key="checker" Value1="{Binding Path=Series.(local:SeriesExtensions.CrosshairItem)}" Value2="{Binding Path=Item}"/> <local:VisibilityConverter x:Key="converter" /> <local:VisibleItemsRegisterer x:Key="registerer" Item="{Binding Path=Item}" /> </Grid.Resources> <Ellipse Stretch="Fill" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Fill="Transparent" Stroke="Transparent" StrokeThickness="0.5" MinWidth="10" MinHeight="10" > </Ellipse> <Ellipse Stretch="Fill" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Fill="{Binding ActualItemBrush}" Stroke="{Binding Series.ActualMarkerOutline}" StrokeThickness="0.5" MinWidth="10" MinHeight="10" Visibility="{Binding Path=Result, Source={StaticResource checker}, Converter={StaticResource converter}}" > </Ellipse> </Grid> </DataTemplate> <Style x:Key="crosshairStyle" TargetType="Line"> <Style.Setters> <Setter Property="Visibility" Value="Collapsed" /> </Style.Setters> </Style> </UserControl.Resources> <Grid x:Name="LayoutRoot" Background="White"> <ig:XamDataChart Name="xamDataChart1" CrosshairVisibility="Visible" CrosshairLineStyle="{StaticResource crosshairStyle}" SeriesCursorMouseMove="xamDataChart1_SeriesCursorMouseMove"> <ig:XamDataChart.Axes> <ig:CategoryXAxis x:Name="xAxis" ItemsSource="{StaticResource data1}" /> <ig:NumericYAxis x:Name="yAxis" /> </ig:XamDataChart.Axes> <ig:XamDataChart.Series> <ig:LineSeries x:Name="testLine" ItemsSource="{StaticResource data1}" XAxis="{Binding ElementName=xAxis}" YAxis="{Binding ElementName=yAxis}" ValueMemberPath="Value" MarkerTemplate="{StaticResource customTemplate}"/> <ig:LineSeries x:Name="testLine2" ItemsSource="{StaticResource data2}" XAxis="{Binding ElementName=xAxis}" YAxis="{Binding ElementName=yAxis}" ValueMemberPath="Value" MarkerTemplate="{StaticResource customTemplate}"/> </ig:XamDataChart.Series> </ig:XamDataChart> </Grid>
And the code behind:
public partial class MainPage : UserControl { public MainPage() { InitializeComponent(); } private void xamDataChart1_SeriesCursorMouseMove( object sender, Infragistics.Controls.Charts.ChartCursorEventArgs e) { if (e.Item != null && e.Series != null && VisibleItemsRegisterer.ItemVisible(e.Item)) { SeriesExtensions.SetCrosshairItem( e.Series, e.Item); } } } public class SeriesExtensions { public static readonly DependencyProperty CrosshairItemProperty = DependencyProperty.RegisterAttached( "CrosshairItem", typeof(object), typeof(SeriesExtensions), new PropertyMetadata(null)); public static object GetCrosshairItem( DependencyObject target) { return target.GetValue(CrosshairItemProperty); } public static void SetCrosshairItem( DependencyObject target, object value) { target.SetValue(CrosshairItemProperty, value); } } public class VisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (targetType == typeof(Visibility) && value is bool) { if ((bool)value) { return Visibility.Visible; } } return Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } public class VisibleItemsRegisterer : DependencyObject { private static Dictionary<object, object> _items = new Dictionary<object, object>(); public static bool ItemVisible(object item) { return _items.ContainsKey(item); } public static readonly DependencyProperty ItemProperty = DependencyProperty.Register( "Item", typeof(object), typeof(VisibleItemsRegisterer), new PropertyMetadata(null, (o, e) => (o as VisibleItemsRegisterer).OnItemChanged(e))); public object Item { get { return GetValue(ItemProperty); } set { SetValue(ItemProperty, value); } } private void OnItemChanged( DependencyPropertyChangedEventArgs e) { if (e.OldValue != null && _items.ContainsKey(e.OldValue)) { _items.Remove(e.OldValue); } if (e.NewValue != null && !_items.ContainsKey(e.NewValue)) { _items.Add(e.NewValue, e.NewValue); } } } public class EqualChecker : DependencyObject { public static readonly DependencyProperty Value1Property = DependencyProperty.Register( "Value1", typeof(object), typeof(EqualChecker), new PropertyMetadata(null, (o, e) => (o as EqualChecker).OnValue1Changed(e))); public object Value1 { get { return GetValue(Value1Property); } set { SetValue(Value1Property, value); } } private void OnValue1Changed( DependencyPropertyChangedEventArgs e) { Result = Value1 == Value2; } public static readonly DependencyProperty Value2Property = DependencyProperty.Register( "Value2", typeof(object), typeof(EqualChecker), new PropertyMetadata(null, (o, e) => (o as EqualChecker).OnValue2Changed(e))); public object Value2 { get { return GetValue(Value2Property); } set { SetValue(Value2Property, value); } } private void OnValue2Changed( DependencyPropertyChangedEventArgs e) { Result = Value1 == Value2; } public static readonly DependencyProperty ResultProperty = DependencyProperty.Register( "Result", typeof(object), typeof(EqualChecker), new PropertyMetadata(null, (o, e) => { })); public object Result { get { return GetValue(ResultProperty); } set { SetValue(ResultProperty, value); } } } 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 }); } } }
Let me know if you have any questions.-Graham
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!
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>
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,