If there are multiple players on a chart is it possible to have a unified tooltip that displays the current Y value of each layer currently in contact with the vertical crosshair without having to have the cursor touching each layer individually?
You can adapt the code from this post: https://ko.infragistics.com/community/forums/f/retired-products-and-controls/39579/xamwebdatachart---crosshairs-position
to display the crosshair value for each series in the chart. The key would be to use the assigned x and y axis for each series to map from the world coordinate points to the axis values for each set of axes. Once you have resolved the values for each series, just store them somewhere that you can bind to from the tooltip. Would you like more info, or does this put you on the right path?
-Graham
With the release bits is there a more elegant solution to this or shold I still use the code from your previous post? In the post you linked to you indicated that the initial release did not have the bits to make this elegant.
Actually any example you have would be helpful. Since I am adding all series, axes, etc. dynamically from code I have difficulties getting the standard functionality to work much less something a bit more advanced like this.
Thanks!Mike
Mike,
This looks like a bug to me. I'll get back to you with the bug report number and a work around. Here's a teaser: we can use some of the code from my replies to your other post to determine the category item index.
I've reported this problem as a bug (35070), and below I'll recount a work-around based partly on the above and partly on my response to another of your posts. Note, for other series types that don't offset (like the column series does), you should set CategoryOffset = false.
The xaml:
<UserControl.Resources> <local:TestData x:Key="data" /> <local:TestData2 x:Key="data2" /> <DataTemplate x:Key="tooltipTemplate"> <Border BorderBrush="Gray" BorderThickness="1" CornerRadius="5" Background="White" IsHitTestVisible="False" Padding="5"> <ItemsControl ItemsSource="{Binding}"> <ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <ContentPresenter Content="{Binding}" ContentTemplate="{Binding Series.LegendItemBadgeTemplate}" /> <TextBlock Text="Series: " /> <TextBlock Text="{Binding Series.Title}" /> <TextBlock Text=" " /> <TextBlock Text="Value: " /> <TextBlock Text="{Binding Item.Value}" /> </StackPanel> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Border> </DataTemplate> </UserControl.Resources> <Grid x:Name="LayoutRoot" Background="White"> <igChart:XamDataChart SeriesCursorMouseMove="XamDataChart_SeriesCursorMouseMove" CrosshairVisibility="Visible"> <local:ChartBehaviors.HoveredCategoryItems> <local:HoveredCategoryItemsBehavior CategoryOffset="True"/> </local:ChartBehaviors.HoveredCategoryItems> <local:ChartBehaviors.FloatingTooltip> <local:FloatingTooltipBehavior TooltipTemplate="{StaticResource tooltipTemplate}" HoveredItems="{Binding Path=(local:ChartBehaviors.HoveredCategoryItems).HoveredItems}"/> </local:ChartBehaviors.FloatingTooltip> <igChart:XamDataChart.Axes> <igChart:CategoryXAxis x:Name="xAxis" ItemsSource="{StaticResource data}" Label="{}{Date}" > </igChart:CategoryXAxis> <igChart:NumericYAxis x:Name="yAxis" /> </igChart:XamDataChart.Axes> <igChart:XamDataChart.Series> <igChart:ColumnSeries x:Name="columnSeries" Title="Column 1" XAxis="{Binding ElementName=xAxis}" YAxis="{Binding ElementName=yAxis}" ItemsSource="{StaticResource data}" ValueMemberPath="Value" /> <igChart:ColumnSeries x:Name="columnSeries2" Title="Column 2" XAxis="{Binding ElementName=xAxis}" YAxis="{Binding ElementName=yAxis}" ItemsSource="{StaticResource data2}" ValueMemberPath="Value" /> </igChart:XamDataChart.Series> </igChart:XamDataChart> </Grid>
The code:
public class ChartBehaviors : DependencyObject { public static readonly DependencyProperty FloatingTooltipProperty = DependencyProperty.RegisterAttached("FloatingTooltip", typeof(FloatingTooltipBehavior), typeof(ChartBehaviors), new PropertyMetadata(null, (o, e) => FloatingTooltipChanged( o as XamDataChart, e.OldValue as FloatingTooltipBehavior, e.NewValue as FloatingTooltipBehavior))); public static FloatingTooltipBehavior GetFloatingTooltip( DependencyObject target) { return target.GetValue(FloatingTooltipProperty) as FloatingTooltipBehavior; } public static void SetFloatingTooltip( DependencyObject target, FloatingTooltipBehavior behavior) { target.SetValue(FloatingTooltipProperty, behavior); } private static void FloatingTooltipChanged( XamDataChart chart, FloatingTooltipBehavior oldValue, FloatingTooltipBehavior newValue) { if (chart == null) { return; } if (oldValue != null) { oldValue.OnDetach(chart); } if (newValue != null) { newValue.OnAttach(chart); } } public static readonly DependencyProperty HoveredCategoryItemsProperty = DependencyProperty.RegisterAttached("HoveredCategoryItems", typeof(HoveredCategoryItemsBehavior), typeof(ChartBehaviors), new PropertyMetadata(null, (o, e) => HoveredCategoryItemsChanged( o as XamDataChart, e.OldValue as HoveredCategoryItemsBehavior, e.NewValue as HoveredCategoryItemsBehavior))); public static HoveredCategoryItemsBehavior GetHoveredCategoryItems( DependencyObject target) { return target.GetValue(HoveredCategoryItemsProperty) as HoveredCategoryItemsBehavior; } public static void SetHoveredCategoryItems( DependencyObject target, HoveredCategoryItemsBehavior behavior) { target.SetValue(HoveredCategoryItemsProperty, behavior); } private static void HoveredCategoryItemsChanged( XamDataChart chart, HoveredCategoryItemsBehavior oldValue, HoveredCategoryItemsBehavior newValue) { if (chart == null) { return; } if (oldValue != null) { oldValue.OnDetach(chart); } if (newValue != null) { newValue.OnAttach(chart); } } } public class SeriesItemInfo : INotifyPropertyChanged { private Series _series; public Series Series { get { return _series; } set { _series = value; if (PropertyChanged != null) { PropertyChanged( this, new PropertyChangedEventArgs("Series")); } } } private object _item; public object Item { get { return _item; } set { _item = value; if (PropertyChanged != null) { PropertyChanged( this, new PropertyChangedEventArgs("Item")); } } } public event PropertyChangedEventHandler PropertyChanged; } public class SeriesItemInfoCollection : ObservableCollection<SeriesItemInfo> { public void UpdateSeriesItem( Series series, object item) { bool found = false; var indexes = from curr in this where curr.Series == series select this.IndexOf(curr); foreach (int index in indexes) { found = true; this[index].Item = item; } if (!found) { this.Add( new SeriesItemInfo() { Series = series, Item = item }); } } } public class HoveredCategoryItemsBehavior : FrameworkElement { public HoveredCategoryItemsBehavior() { HoveredItems = new SeriesItemInfoCollection(); } private XamDataChart _owner = null; public static readonly DependencyProperty HoveredItemsProperty = DependencyProperty.Register("HoveredItems", typeof(SeriesItemInfoCollection), typeof(HoveredCategoryItemsBehavior), new PropertyMetadata(null)); public SeriesItemInfoCollection HoveredItems { get { return (SeriesItemInfoCollection)GetValue(HoveredItemsProperty); } set { SetValue(HoveredItemsProperty, value); } } public bool CategoryOffset { get; set; } public void OnAttach(XamDataChart chart) { if (_owner != null) { OnDetach(_owner); } _owner = chart; chart.PropertyUpdated += Chart_PropertyUpdated; } public void OnDetach(XamDataChart chart) { if (_owner != chart) { return; } chart.PropertyUpdated -= Chart_PropertyUpdated; HoveredItems.Clear(); _owner = null; } private void GetInViewAxisBounds( CategoryAxisBase x, NumericYAxis y, Rect viewportRect, out double left, out double right, out double top, out double bottom) { left = x.GetUnscaledValue( viewportRect.Left, _owner.WindowRect, viewportRect); right = x.GetUnscaledValue( viewportRect.Right, _owner.WindowRect, viewportRect); top = y.GetUnscaledValue( viewportRect.Top, _owner.WindowRect, viewportRect); bottom = y.GetUnscaledValue( viewportRect.Bottom, _owner.WindowRect, viewportRect); } private Rect GetViewportRect( Series target, CategoryAxisBase x, NumericYAxis y) { double top = y.TransformToVisual(target) .Transform(new Point(0, 0)).Y; double bottom = y.TransformToVisual(target) .Transform(new Point(0, y.ActualHeight)).Y; double left = x.TransformToVisual(target) .Transform(new Point(0, 0)).X; double right = x.TransformToVisual(target) .Transform(new Point(x.ActualWidth, 0)).X; double width = right - left; double height = bottom - top; if (width > 0.0 && height > 0.0) { return new Rect(left, top, width, height); } return Rect.Empty; } private object GetItem(int index, Series series) { object item; if (series.ItemsSource is IList && index < ((IList)series.ItemsSource).Count) { item = ((IList)series.ItemsSource)[index]; } else { IEnumerator items = series.ItemsSource.GetEnumerator(); for (int i = 0; i < index; i++) { items.MoveNext(); } item = items.Current; } return item; } void Chart_PropertyUpdated( object sender, PropertyUpdatedEventArgs e) { if (e.PropertyName == "CrosshairPoint") { foreach (CategorySeries series in _owner.Series.Where((s) => s is CategorySeries)) { CategoryAxisBase x = series.XAxis; NumericYAxis y = series.YAxis; Rect viewportRect = GetViewportRect( series, x, y); Point p = (Point)e.NewValue; double left; double right; double top; double bottom; GetInViewAxisBounds(x, y, viewportRect, out left, out right, out top, out bottom); double windowX = (p.X - _owner.WindowRect.Left) / _owner.WindowRect.Width; double xAxisValue = left + (windowX * (right - left)); if (CategoryOffset) { xAxisValue -= .5; } xAxisValue = Math.Round(xAxisValue); object item = null; if (!double.IsInfinity(xAxisValue) && !double.IsNaN(xAxisValue) && xAxisValue >= 0 && series.ItemsSource != null) { int index = (int)xAxisValue; item = GetItem(index, series); if (item != null) { HoveredItems .UpdateSeriesItem(series, item); } } } } } } public class FloatingTooltipBehavior : FrameworkElement { public static readonly DependencyProperty HoveredItemsProperty = DependencyProperty.Register("HoveredItems", typeof(SeriesItemInfoCollection), typeof(FloatingTooltipBehavior), new PropertyMetadata(null)); public SeriesItemInfoCollection HoveredItems { get { return (SeriesItemInfoCollection)GetValue(HoveredItemsProperty); } set { SetValue(HoveredItemsProperty, value); } } private bool _isOverChart = false; private Popup _popup = new Popup(); private ContentControl _content = new ContentControl(); private Panel _container; private XamDataChart _owner = null; private DataTemplate _tooltipTemplate; public DataTemplate TooltipTemplate { get { return _tooltipTemplate; } set { _tooltipTemplate = value; _content.ContentTemplate = _tooltipTemplate; } } protected bool IsOverChart { get { return _isOverChart; } set { bool last = _isOverChart; _isOverChart = value; if (_isOverChart && !last) { ShowPopup(); } if (!_isOverChart && last) { HidePopup(); } } } private void HidePopup() { _popup.IsOpen = false; } private void ShowPopup() { _popup.IsOpen = true; } public void OnAttach(XamDataChart chart) { if (_owner != null) { OnDetach(_owner); } this.DataContext = chart; chart.MouseLeave += Chart_MouseLeave; chart.MouseMove += Chart_MouseMove; _popup.IsOpen = false; _popup.Child = _content; _content.ContentTemplate = TooltipTemplate; if (_content.Content == null) { _content.SetBinding( ContentControl.ContentProperty, new Binding("HoveredItems") { Source = this }); } if (chart.Parent != null && chart.Parent is Panel) { _container = chart.Parent as Panel; _container.Children.Add(_popup); } } public void OnDetach(XamDataChart chart) { if (_owner != chart) { return; } chart.MouseLeave -= Chart_MouseLeave; chart.MouseMove -= Chart_MouseMove; IsOverChart = false; if (_container != null) { _container.Children.Remove(_popup); } _owner = null; } void Chart_MouseMove(object sender, MouseEventArgs e) { SetPopupOffsets(e.GetPosition(_owner)); IsOverChart = true; } private void SetPopupOffsets(Point point) { _popup.VerticalOffset = point.Y; _popup.HorizontalOffset = point.X + 10; } void Chart_MouseLeave( object sender, MouseEventArgs e) { IsOverChart = false; } }
Hope this helps!
Graham,
I have switched over to the CategoryDateTimeXAxis and this behavior to get the y values for the current cursor x position no longer works. The GetUnscaledValue seems to be where the difference is. For some reason pulling the left and right values returns huge numbers (10 to the 14th power for example). I am unfamiliar with exactly how this function works so I wanted to check to see if you knew what I am missing.
Thanks,Mike
I have the same problem as Mike. It would be great if you could provide a solution.
Thanks,Hans
Mike and Hans,
You need a bit of a different approach for the CategoryDateTimeXAxis. The unscaled axis values represent ticks that you can convert into a DateTime. So if you do a new DateTime on the xAxisValue in the code above you get the actual DateTime value at the cursor.However, because of the nature of this type of Axis, that doesn't correlate to an index, letting you lookup the closest item in the collection.So you need a bit more code to get you from the DateTime value of the cursor to the represented source item.You will probably want to make some performance modifications depending on whether your source collection is sorted by date or massive.The changed xaml:
<local:HoveredCategoryItemsBehavior CategoryOffset="True"> <local:HoveredCategoryItemsBehavior.ItemComparer> <local:TestDataItemComparer /> </local:HoveredCategoryItemsBehavior.ItemComparer> </local:HoveredCategoryItemsBehavior>
The changed code:
public IComparer<object> ItemComparer { get; set; } void Chart_PropertyUpdated( object sender, PropertyUpdatedEventArgs e) { if (e.PropertyName == "CrosshairPoint") { foreach (CategorySeries series in _owner.Series.Where((s) => s is CategorySeries)) { CategoryAxisBase x = series.XAxis; NumericYAxis y = series.YAxis; Rect viewportRect = GetViewportRect( series, x, y); Point p = (Point)e.NewValue; double left; double right; double top; double bottom; GetInViewAxisBounds(x, y, viewportRect, out left, out right, out top, out bottom); double windowX = (p.X - _owner.WindowRect.Left) / _owner.WindowRect.Width; double xAxisValue = left + (windowX * (right - left)); if (CategoryOffset) { xAxisValue -= .5; } object item = null; if (!double.IsInfinity(xAxisValue) && !double.IsNaN(xAxisValue) && xAxisValue >= 0 && series.ItemsSource != null) { //note, you certainly want to do something more //efficient here, but its complexity depends on //your data and whether it is pre-sorted, etc. List<object> list = x.ItemsSource.OfType<object>().ToList(); list.Sort(ItemComparer); int index = list.BinarySearch(new DateTime((long)xAxisValue), ItemComparer); if (index < 0) { index = (~index) - 1; } //this should actually determine if index or //index + 1 is closer to the value we are searching for //leaving this as excercise for the reader. if (index > 0 && index < list.Count) { item = list[index]; } if (item != null) { HoveredItems .UpdateSeriesItem(series, item); } } } } }
The ItemComparer for the sample data:
public class TestDataItemComparer : IComparer<object> { public int Compare(object x, object y) { DateTime xDate = DateTime.MinValue; DateTime yDate = DateTime.MaxValue; if (x is TestDataItem) { xDate = (x as TestDataItem).Date; } if (y is TestDataItem) { yDate = (y as TestDataItem).Date; } if (x is DateTime) { xDate = (DateTime)x; } if (y is DateTime) { yDate = (DateTime)y; } return DateTime.Compare(xDate, yDate); } }
Hope this helps!-Graham
There are situations where you should not share a CategoryDateTimeXAxis in the same way that you should not really share a CategoryXAxis unless the series have the same labels and number of items. If you have the same number of items with the same dates assigned then you should be able to share the CategoryDateTimeXAxis. Sharing the NumericXAxis or NumericYAxis should not be a problem in those other scenarios as their range will just expand to accomodate all the series that they are assigned to.
Oh, I read that and then completely forgot about that issue. I am usging the new service release now so I removed that code. This works so far. Hopefully I won't run into any new issues.
If I am dynamically adding multiple series that all will have the same time period should I have them all use the same axes or should they all use independent x axes?
along with the multiplication your were curious about.
Thats applying the workaround I posted here: http://community.infragistics.com/forums/p/42507/242634.aspx#242634
Because I didn't know if you were running the RTM or the Service release (which I think has the fix for the CategoryDateTimeXAxis problem in question). You can rip out all the TimeTranslator stuff if you aren't having that problem.
Perhaps this is a stupid question but I just can't figure out why you multiply the ticks by 170000?