Hi,
We are evaluating XamDataChart and considering to purchase the product. However, there is a single feature that we could not find support for.
My question is does the chart support annotations? (I mean user drawing trend line, fibonacci retracements, etc.)?
According to https://ko.infragistics.com/community/forums/f/ultimate-ui-for-wpf/50063/annotations-on-charts/263567#263567, this feature was not supported in 2010. Was the support added?
If yes, could you please provide a code snippet how to use it.
if no, could you please provide some guidance on how to implement it: what Canvas to use (e.g. custom series, or do you have smth built in), how to attach event handlers (to the main chart or to a canvas), how to make the annotations work in sync with built-in zooming / panning capabilities?
Thank you,
Adam K
Adam,
Here's an example of how you could implement Fibonacci Retracements as an overlay to the chart:
Xaml:
<Window.Resources> <local:TestData x:Key="data" /> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <igChart:XamDataChart x:Name="theChart" VerticalZoomable="True" HorizontalZoomable="True"> <igChart:XamDataChart.Axes> <igChart:CategoryXAxis x:Name="xAxis" ItemsSource="{StaticResource data}" > <igChart:CategoryXAxis.LabelSettings> <igChart:AxisLabelSettings Visibility="Collapsed" /> </igChart:CategoryXAxis.LabelSettings> </igChart:CategoryXAxis> <igChart:NumericYAxis x:Name="yAxis" /> </igChart:XamDataChart.Axes> <igChart:XamDataChart.Series> <igChart:FinancialPriceSeries x:Name="priceSeries" XAxis="{Binding ElementName=xAxis}" YAxis="{Binding ElementName=yAxis}" ItemsSource="{StaticResource data}" OpenMemberPath="Open" CloseMemberPath="Close" HighMemberPath="High" LowMemberPath="Low" DisplayType="Candlestick"/> </igChart:XamDataChart.Series> </igChart:XamDataChart> <local:FibonacciOverlay x:Name="overlay" Chart="{Binding ElementName=theChart}" /> <Button Grid.Row="1" x:Name="startAnnotation" Click="startAnnotation_Click" Content="Start Annotation" /> </Grid>
And code behind:
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void startAnnotation_Click(object sender, RoutedEventArgs e) { overlay.ShouldCatchInput = true; } } public class TestData : ObservableCollection<TestDataItem> { private Random _rand = new Random(); public TestData() { var curr = 10.0; var currHigh = 10.0; var currLow = 10.0; var currOpen = 10.0; var currClose = 10.0; for (var i = 0; i < 10000; i++) { if (_rand.NextDouble() > .5) { curr += _rand.NextDouble() * 2.0; } else { curr -= _rand.NextDouble() * 2.0; } if (_rand.NextDouble() > .5) { currOpen = curr + _rand.NextDouble() * 2.0; currClose = curr - _rand.NextDouble() * 2.0; currHigh = currOpen + _rand.NextDouble() * 2.0; currLow = currClose - _rand.NextDouble() * 2.0; } else { currOpen -= _rand.NextDouble() * 2.0; currClose += _rand.NextDouble() * 2.0; currHigh = currClose + _rand.NextDouble() * 2.0; currLow = currOpen - _rand.NextDouble() * 2.0; } Add(new TestDataItem() { Open= currOpen, High=currHigh, Close=currClose, Low= currLow }); } } } public class TestDataItem { public double Open { get; set; } public double High { get; set; } public double Low { get; set; } public double Close { get; set; } } public class ChartOverlay : ContentControl { protected Canvas _overlayCanvas = new Canvas(); public ChartOverlay() { Viewport = Rect.Empty; HorizontalContentAlignment = System.Windows.HorizontalAlignment.Stretch; VerticalContentAlignment = System.Windows.VerticalAlignment.Stretch; Content = _overlayCanvas; } public XamDataChart Chart { get { return (XamDataChart)GetValue(ChartProperty); } set { SetValue(ChartProperty, value); } } public static readonly DependencyProperty ChartProperty = DependencyProperty.Register( "Chart", typeof(XamDataChart), typeof(ChartOverlay), new PropertyMetadata( null, (o, e) => (o as ChartOverlay).OnChartChanged( (XamDataChart)e.OldValue, (XamDataChart)e.NewValue))); private void OnChartChanged(XamDataChart oldChart, XamDataChart newChart) { if (oldChart != null) { Detach(oldChart); } if (newChart != null) { Attach(newChart); } } protected XamDataChart _chart = null; private void Attach(XamDataChart newChart) { _chart = newChart; newChart.RefreshCompleted += RefreshCompleted; } protected Rect Viewport { get; set; } private void RefreshCompleted(object sender, EventArgs e) { Viewport = GetChartViewport(); DoRefresh(); } private Rect GetChartViewport() { if (_chart == null) { return Rect.Empty; } var eles = _chart.Axes.OfType<FrameworkElement>() .Concat(_chart.Series.OfType<FrameworkElement>()); if (!eles.Any()) { return Rect.Empty; } var first = eles.First(); Point topLeft; try { topLeft = first.TransformToVisual(this).Transform(new Point(0, 0)); } catch (Exception e) { return Rect.Empty; } return new Rect( topLeft.X, topLeft.Y, _chart.ViewportRect.Width, _chart.ViewportRect.Height); } protected void DoRefresh() { DoRefreshOverride(); } protected virtual void DoRefreshOverride() { } private void Detach(XamDataChart oldChart) { oldChart.RefreshCompleted -= RefreshCompleted; } public bool ShouldCatchInput { get { return (bool)GetValue(ShouldCatchInputProperty); } set { SetValue(ShouldCatchInputProperty, value); } } public static readonly DependencyProperty ShouldCatchInputProperty = DependencyProperty.Register( "ShouldCatchInput", typeof(bool), typeof(ChartOverlay), new PropertyMetadata(false, (o, e) => (o as ChartOverlay) .OnShouldCatchInputChanged( (bool)e.OldValue, (bool)e.NewValue))); private void OnShouldCatchInputChanged( bool oldValue, bool newValue) { if (newValue) { _overlayCanvas.Background = new SolidColorBrush(Color.FromArgb(20, 0, 0, 0)); } else { _overlayCanvas.Background = null; } } } public class FibonacciOverlay : ChartOverlay { public FibonacciOverlay() { this.MouseDown += FibonacciOverlay_MouseDown; this.MouseUp += FibonacciOverlay_MouseUp; this.MouseMove += FibonacciOverlay_MouseMove; _fibCanvas = new Canvas(); _fibCanvas.IsHitTestVisible = false; } private bool _isDragging = false; private bool _isPlaced = false; private Canvas _fibCanvas; protected CategoryXAxis XAxis { get { return _chart.Axes.OfType<CategoryXAxis>().First(); } } protected NumericYAxis YAxis { get { return _chart.Axes.OfType<NumericYAxis>().First(); } } void FibonacciOverlay_MouseMove(object sender, MouseEventArgs e) { if (_isDragging) { var pos = e.GetPosition(XAxis); System.Diagnostics.Debug.WriteLine(pos); var scaled = GetUnscaled(pos); var pointX = Math.Min(_fibAnchor.X, scaled.X); var pointY = Math.Max(_fibAnchor.Y, scaled.Y); _fibWidth = Math.Abs(_fibAnchor.X - scaled.X); _fibHeight = Math.Abs(_fibAnchor.Y - scaled.Y); _fibPoint = new Point(pointX, pointY); DoRefresh(); e.Handled = true; } } void FibonacciOverlay_MouseUp(object sender, MouseButtonEventArgs e) { if (_isDragging) { _isDragging = false; ShouldCatchInput = false; DoRefresh(); ReleaseMouseCapture(); } } private Point _fibPoint { get; set; } private Point _fibAnchor { get; set; } private double _fibWidth { get; set; } private double _fibHeight { get; set; } void FibonacciOverlay_MouseDown(object sender, MouseButtonEventArgs e) { if (_chart == null) { return; } e.Handled = true; if (_isPlaced) { _overlayCanvas.Children.Remove(_fibCanvas); } if (this.CaptureMouse()) { _isDragging = true; _overlayCanvas.Children.Add(_fibCanvas); _isPlaced = true; Point pos = e.GetPosition(XAxis); _fibAnchor = GetUnscaled(pos); _fibPoint = GetUnscaled(pos); DoRefresh(); } } private double GetUnscaledX(double x) { return XAxis.UnscaleValue(x); } private double GetUnscaledY(double y) { return YAxis.UnscaleValue(y); } private Point GetUnscaled(Point pos) { return new Point( GetUnscaledX(pos.X), GetUnscaledY(pos.Y)); } protected override void DoRefreshOverride() { base.DoRefreshOverride(); if (Viewport.IsEmpty) { return; } var x = XAxis.ScaleValue(_fibPoint.X); var y = YAxis.ScaleValue(_fibPoint.Y); var right = XAxis.ScaleValue(_fibPoint.X + _fibWidth); var bottom = YAxis.ScaleValue(_fibPoint.Y - _fibHeight); Canvas.SetLeft(_fibCanvas, Viewport.Left + x); Canvas.SetTop(_fibCanvas, Viewport.Top + y); var viewport = new Rect(0, 0, right - x, bottom - y); SetClipRectangle(); RefreshFibVisual(viewport); } private void SetClipRectangle() { _overlayCanvas.Clip = new RectangleGeometry() { Rect = Viewport }; } private List<FibEntry> Entries { get; set; } private void RefreshFibVisual(Rect viewport) { if (_fibCanvas.Children.Count == 0) { List<double> values = new List<double>() { 0.0, 23.6, 38.2, 100.0 }; Entries = new List<FibEntry>(); foreach (var val in values) { Entries.Add( new FibEntry() { Line = new Line() { Stroke = new SolidColorBrush(Colors.Red), StrokeThickness = 1 }, TextBlock = new TextBlock() { Text = val.ToString(), Foreground = new SolidColorBrush(Colors.Red), }, Value = val }); } foreach (var entry in Entries) { _fibCanvas.Children.Add(entry.Line); _fibCanvas.Children.Add(entry.TextBlock); entry.TextBlock.InvalidateMeasure(); entry.TextBlock.Measure( new Size(double.PositiveInfinity, double.PositiveInfinity)); } } if (viewport.Width == 0 || viewport.Height == 0 || double.IsNaN(viewport.Width) || double.IsNaN(viewport.Height)) { return; } foreach (var entry in Entries) { var yPos = viewport.Bottom - (viewport.Height * entry.Value / 100.0); var xLeft = viewport.Left; var xRight = viewport.Right; if (double.IsNaN(yPos) || double.IsNaN(xLeft) || double.IsNaN(xRight)) { continue; } entry.Line.X1 = xLeft; entry.Line.X2 = xRight; entry.Line.Y1 = yPos; entry.Line.Y2 = yPos; Canvas.SetTop(entry.TextBlock, yPos - entry.TextBlock.DesiredSize.Height); Canvas.SetLeft(entry.TextBlock, xRight - entry.TextBlock.DesiredSize.Width); } } } public class FibEntry { public Line Line { get; set; } public TextBlock TextBlock { get; set; } public double Value { get; set; } }
Which lets you do something like this by clicking on the Start Annotation button and then drawing the annotation on the chart.
Hope this helps!
-Graham
Graham,
Thank you for the detailed answer. I will give it a try.
The best example would be to show how to implement Fibonacci retracements (similar to the video here:http://www.youtube.com/watch?v=Rhr7hBa-rWQ).
Sincerely,
Adam
There are still no native annotations in the chart. But if you look at the two links I shared in that post you linked to, you will see two different samples of how you can achieve annotations in application code. In the most recent versions of the chart, you're life is made a bit easier by the fact that the chart will raise an event called RefeshCompleted, whenever it updates its visual (this will happen as the zoom/pan changes, etc). So it should just be sufficient to listen for that event to know when to update your objects that are annotating the chart.
I would recommend overlaying a canvas on the chart. Methods on the axes of the chart (ScaleValue UnscaleValue) should help you move from the axis values into the pixel space of the chart, or back from a mouse over coordinate into the axis values you are concerned with.
For some annotations it may be worthwhile investigating doing a custom series type. But depending on the version you are targetting, and your requirements, it may turn out some of the functionality you need is sealed.
Can you provide more information about what you are trying to accomplish? I may be able to help out with an example. Also, I'd recommend submitting feature requests for the types of annotations you would find most useful.