Blog

Introducing Annotations

Chart annotations are visual elements that can be used to mark points or areas of some significance. For example, in stock charts they can be used to draw trend lines, callout text boxes, highlighted regions, cycle lines and many more. In this blog post, we will be introducing the annotations provided by the premium version of Visiblox charts.


Trend Lines

Cycle Lines

Highlighted region with callout textbox

Basic Concepts

In order to better understand how annotations work in Visiblox, we will start by introducing some basic concepts. Every chart contains an Annotations Collection which holds all the annotations plotted against the chart. To add or remove annotations from the chart, constitutes - among others - the equivalent action on the Annotations Collection.

Annotations wouldn't be very useful (or fun!) if we couldn't interact with them - drag them around the plot area, resize them or edit the text. Interaction can very easily be enabled. You just need to set IsInteractionEnabled to true and ClickMode to Drag. And to make interaction even easier, grippers appear at the edges of the annotations when you mouse over them.

Interaction with annotations, includes creating and deleting them. The creation process is a bit more complicated and will be covered later in this blog post. Deletion is much easier; in fact, you only need to set ClickMode to Delete and the cursor will turn into an eraser. To delete an annotation programmatically, all you need to do is remove it from the Annotations Collection.

Default Annotations in Visiblox

Visiblox charts provide a set of out-of-the-box annotations. You can, of course, add your own custom annotations, but that area will be covered in another blog post. So, here we go:

  • LineAnnotation
  • RectangleAnnotation and SquareAnnotation
  • EllipseAnnotation and CircleAnnotation
  • HorizontalLineAnnotation and HorizontalLineWithValueAnnotation
  • VerticalLineAnnotation and VerticalLineWithValueAnnotation
  • TextAnnotation
  • CalloutTextAnnotation
  • EventAnnotation

Creating Annotations

As promised, in this section we will be covering the creation process. There are two ways to add annotations to your chart, either interactively (using the AnnotationBehaviour) or programmatically. Let's take a closer look:

Creating Annotations Interactively

Adding the AnnotationBehaviour to your chart will allow the users to draw annotations to the chart themselves. The type of annotation that will be added is defined by the AnnotationFactory property which by default is set to an instance of a DefaultAnnotationFactory. The DefaultAnnotationFactory exposes the AnnotationMode property which can be set to define the type of annotation to draw. The AnnotationMode property is of type BuiltinAnnotationMode which is an enum containing all the default annotation types. This property can easily be bound to a dropdown box or buttons to allow the user to select the type of annotation. This can be summarized in the following diagram:

If this seemed a little too complicated, don't worry! Hopefully, the next example will clarify everything for you!

Creating Annotations Interactively: Example (Building An Annotation Toolbar)

In this section we will create an annotation toolbar in order to demonstrate the AnnotationBehaviour. Let's start by analysing the components our toolbar will contain:

  • A combobox containing all the default annotation types (AnnotationType). Its SelectedItem value is bound to the AnnotationMode of the AnnotationFactory.
  • Radio buttons to allow the user to add (AddAnnotation), delete (DeleteAnnotations) or take no action (NoAnnotationAction) upon the annotations. If the add action is selected, then the type of annotation added will be the selected type from the AnnotationType combobox.
  • A checkbox to enable or disable interaction with the annotations (AnnotationInteractionEnabledCheckbox)
  • A button that deletes all the annotations (DeleteAllAnnotationsButton)
  • A button that deletes the selected annotation (DeleteSelectedAnnotationsButton)

Below is the xaml for the example:

<Grid x:Name="LayoutRoot" Background="White">
    <Grid.RowDefinitions>
        <RowDefinition Height="300" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="540" />
        <ColumnDefinition Width="180" />
    </Grid.ColumnDefinitions>
 
    <charts:Chart x:Name="MainChart" Width="520" Height="300" HorizontalAlignment="Left" VerticalAlignment="Stretch" LegendVisibility="Collapsed">            
    </charts:Chart>
 
    <Border Grid.Column="1" BorderThickness="1" VerticalAlignment="Center" BorderBrush="Black" Background="White">
        <Grid>
            <StackPanel Orientation="Vertical" Margin="10,10,10,10">
                <TextBlock Text="Click Action:" />
                <StackPanel Orientation="Horizontal">
                    <RadioButton GroupName="AnnotationAction" Content="Add:" Margin="0,0,5,0" x:Name="AddAnnotation" Checked="AddAnnotation_Checked" />
                    <ComboBox Name="AnnotationType" DropDownOpened="AnnotationType_DropDownOpened" />
                </StackPanel>
                <RadioButton GroupName="AnnotationAction" Content="Delete" Margin="0,0,5,0" x:Name="DeleteAnnotations" Checked="DeleteAnnotations_Checked" Unchecked="DeleteAnnotations_Unchecked" />
                <RadioButton GroupName="AnnotationAction" Content="None" Margin="0,0,5,0" x:Name="NoAnnotationAction" Checked="NoAnnotationAction_Checked" />
                <CheckBox Margin="0,10,0,0" Content="Enable interaction" x:Name="AnnotationInteractionEnabledCheckbox" Checked="AnnotationInteractionEnabledCheckbox_Checked" Unchecked="AnnotationInteractionEnabledCheckbox_Unchecked" IsChecked="True"/>
                <StackPanel Orientation="Horizontal" >
                    <TextBlock Text="Delete: "  VerticalAlignment="Center"/>
                    <Button Content="All" x:Name="DeleteAllAnnotationsButton" Click="DeleteAllAnnotationsButton_Click" Width="40" />
                    <Button Content="Selected" x:Name="DeleteSelectedAnnotationsButton" Click="DeleteSelectedAnnotationsButton_Click" Width="60" />
                </StackPanel>
            </StackPanel>
        </Grid>
    </Border>
</Grid>

Moving on to the code behind, we need a property of type BuiltinAnnotationMode in order to bind it to the SelectedItem of the AnnotationType combobox. When this property's value is changed, the AnnotationMode of the AnnotationFactory needs to change.

private BuiltinAnnotationMode _annotationMode;
public BuiltinAnnotationMode AnnotationBehaviourMode
{
     get { return _annotationMode; }
     set
    {
         if (_annotationMode == value) { return; }
         _annotationMode = value;
         ((DefaultAnnotationFactory)_annotationBehaviour.AnnotationFactory).AnnotationMode = _annotationMode;
     }
}

Similarly, we add the property AnnotationInteractionEnabled which sets the IsInteractionEnabled value of all the annotations on the chart.

private bool _annotationInteractionEnabled = true;
public bool AnnotationInteractionEnabled
{
    get { return _annotationInteractionEnabled; }
    set
    {
        if (_annotationInteractionEnabled != value)
        {
            _annotationInteractionEnabled = value;
            if (MainChart != null)
                MainChart.Annotations.ToList<IAnnotation>().ForEach(a => a.IsInteractionEnabled = _annotationInteractionEnabled);
        }
    }
}

Upon initialization, we first add a line series with some random data. Then, we instantiate the private field _annotationBehaviour and set it as the chart behaviour. We also populate the AnnotationType combobox and set the two-way binding with the AnnotationBehaviourMode property.

private void InitializeData()
{
    Random _rnd = new Random();
    var ds = new DataSeries<DateTime, double>();
    for (int i = 0; i < 10; i++)
    {
        ds.Add(new DataPoint<DateTime, double>(DateTime.Now.AddDays(i), _rnd.Next(10, 50)));
    }
    var lineSeries = new LineSeries() { ShowPoints = true, DataSeries = ds };
    MainChart.Series.Add(lineSeries);
}

private void InitializeToolbar()
{            
    _annotationBehaviour = new AnnotationBehaviour { IsEnabled = false };
    MainChart.Behaviour = _annotationBehaviour;

    AnnotationType.ItemsSource = GetValues<BuiltinAnnotationMode>();
    AnnotationType.SetBinding(ComboBox.SelectedItemProperty, new Binding
    {
        Source = this,
        Path = new PropertyPath("AnnotationBehaviourMode"),
        Mode = BindingMode.TwoWay
    });
}

Finally, we implement the event handlers for the toolbar components. The AddAnnotation_Checked event handler is invoked when the AddAnnotation radio button is selected. In that case, we need to make sure that the _annotationBehaviour is enabled so the user can add annotations.

private void AddAnnotation_Checked(object sender, RoutedEventArgs e)
{
    _annotationBehaviour.IsEnabled = true;
}

The following handler is invoked when the AnnotationType combo box is opened. This event handler is added in order to synchronize the AnnotationType combo box with the AddAnnotation radio button, so the user does not need to select an item from the combo box and separately select the Add radio button.

private void AnnotationType_DropDownOpened(object sender, EventArgs e)
{
    AddAnnotation.IsChecked = true;
}

When the NoAnnotationAction radio button is selected we need to disable the _annotationBehaviours the user can't add annotations.

private void NoAnnotationAction_Checked(object sender, RoutedEventArgs e)
{
    _annotationBehaviour.IsEnabled = false;
}

As previously discussed, deleting annotations is a manipulation action so in order to allow users to delete annotations, AnnotationInteractionEnabled must be true and the ClickMode must be set to Delete. We also need to disable the _annotationBehaviour as it is only needed when we add annotations.

private void DeleteAnnotations_Checked(object sender, RoutedEventArgs e)
{
    _annotationBehaviour.IsEnabled = false;
    AnnotationInteractionEnabled = true;
    AnnotationInteractionEnabledCheckbox.IsChecked = true;
    MainChart.Annotations.ToList<IAnnotation>().ForEach(a => a.ClickMode = AnnotationClickMode.Delete);
}

Unchecking the DeleteAnnotations radio button means that either AddAnnotation has been selected or NoAnnotationAction. Since the _annotationBehaviour is taken care from their event handlers, we only need to change the ClickMode to Drag.

private void DeleteAnnotations_Unchecked(object sender, RoutedEventArgs e)
{
    MainChart.Annotations.ToList<IAnnotation>().ForEach(a => a.ClickMode = AnnotationClickMode.Drag);
}

When the AnnotationInteractionEnabledCheckbox is checked, we need to enable interaction with all the annotations. Setting the AnnotationInteractionEnabled property to true will take care of that.

private void AnnotationInteractionEnabledCheckbox_Checked(object sender, RoutedEventArgs e)
{
    AnnotationInteractionEnabled = true;
}

When the AnnotationInteractionEnabledCheckbox is unchecked, we need to disable interaction with all the annotations. Also, since we cannot delete annotations if IsInteractionEnabled is false, we make sure that the DeleteAnnotations radio button is not selected

private void AnnotationInteractionEnabledCheckbox_Unchecked(object sender, RoutedEventArgs e)
{
    AnnotationInteractionEnabled = false;
    if (DeleteAnnotations.IsChecked.Value)
    {
        NoAnnotationAction.IsChecked = true;
    }
}

The following event handlers are invoked when the DeleteAllAnnotationsButton or DeleteSelectedAnnotationsButton buttons are clicked. In the first case, we need to remove all the annotations from the chart and in the second case, we find all the selected annotation - if any - and remove them.

private void DeleteAllAnnotationsButton_Click(object sender, RoutedEventArgs e)
{
    MainChart.Annotations.Clear();
}
private void DeleteSelectedAnnotationsButton_Click(object sender, RoutedEventArgs e)
{
    foreach (var a in MainChart.Annotations.Where(a => a.IsSelected).ToList())
    {
        MainChart.Annotations.Remove(a);
    }
}

Putting it all together, we get the application displayed below. Enjoy!

Creating Annotations programmatically

Each annotation is represented by a UI element (a line, a rectangle, a text box etc) displayed in an arbitrary position of the plot area. This position is defined by a set of points which may differ from annotation to annotation. For example, a RectangleAnnotation is defined by the top left and bottom right point while the HorizontalLineAnnotation is defined by one point which represents the Y value of the line.

From everything we've discussed so far, I am sure you already expect how we can add annotations programmatically! Well, you will need to call the appropriate constructor specifying the required points and add the created annotation to the Annotations Collection. For example, the constructor of a RectangleAnnotation is RectangleAnnotation(IDataPoint topLeftPoint, IDataPoint bottomRightPoint) and for the HorizontalLineAnnotation HorizontalLineAnnotation(IDataPoint point).

In the next section, we will be taking a closer look to this process through an example. To make it a tad bit more interesting, we will create a live chart which adds annotations when certain criteria are met.

Creating Annotations Programmatically: Example (Creating A Live Chart With Annotations)

First, we add a chart in our xaml code.

<UserControl.Resources>
    <charts:DoubleRange Minimum="0" Maximum="100" x:Key="yAxisRange" />
</UserControl.Resources>
 
<Grid x:Name="LayoutRoot" Background="White">
    <charts:Chart Name="chart" Width="600" Height="400" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="White" FontFamily="Arial" LegendVisibility="Collapsed">
        <charts:Chart.XAxis>
            <charts:DateTimeAxis ShowMajorGridlines="False" ShowMinorTicks="False" />
        </charts:Chart.XAxis>
        <charts:Chart.YAxis>
            <charts:LinearAxis ShowMajorGridlines="False" Range="{StaticResource yAxisRange}" />
        </charts:Chart.YAxis>
        <charts:Chart.Series>
            <charts:LineSeries />
        </charts:Chart.Series>
    </charts:Chart>
</Grid>

After the chart is loaded, we need to add some initial data to the chart.

private void InitilizeData()
{
    // Set the starting date a year earlier
    this._currentTime = DateTime.Now.AddYears(-1);
    this._lastHour = _currentTime;

    var data = new DataSeries<DateTime, double>();
    chart.Series[0].DataSeries = data;

    // Create initial data for the chart and assign it to it
    for (int i = 0; i < _MaxPoints; i++)
    {
        _currentTime = _currentTime.AddMinutes(1);
        double YValue = rand.Next(10, 80);
        var dataPoint = new DataPoint<DateTime, double>(_currentTime, YValue);
        data.Add(dataPoint);
        _lastPointCounter++;

        AddAnnotations(dataPoint);
    }
}

For every point added to the chart, AddAnnotations()is invoked. If certain criteria are met, one or more annotations attached to the new point will be added.

private void AddAnnotations(DataPoint<DateTime, double> dataPoint)
{
    if (_currentTime == _lastHour.AddHours(1))
    {
        AddEventMarker(dataPoint);
        _lastHour = _currentTime;
    }

    if (dataPoint.Y > 75)
    {
        AddCalloutAnnotation(dataPoint);
    }

    if (_lastPointCounter > 30)
    {
        AddEllipseAnnotation(dataPoint);
        _lastPointCounter = 0;
    }
}

A EventAnnotation is added every hour. The constructor arguments refer to the top left corner of the rectangle and the text inside the annotation.

void AddEventMarker(IDataPoint dataPoint)
{
    EventAnnotation marker = new EventAnnotation(dataPoint, dataPoint.X.ToString());
    AddAnnotation(marker);
}

A CalloutTextAnnotation is added if the Y value of the added datapoint is over 75. The first argument of the constructor is the start point of the line, the second is the top left corner of the text block and the third is the text.

void AddCalloutAnnotation(IDataPoint dataPoint)
{            
    CalloutTextAnnotation callout = new CalloutTextAnnotation(dataPoint, new DataPoint<DateTime, double>(DateTime.Parse(dataPoint.X.ToString()).AddMinutes(1), (double)dataPoint.Y + 5), dataPoint.Y.ToString());
    AddAnnotation(callout);
}

A EllipseAnnotation is added every 30 points. The constructor arguments are the top left and bottom right points of the rectangle enclosing the ellipse.

void AddEllipseAnnotation(IDataPoint dataPoint)
{
    EllipseAnnotation ellipse = new EllipseAnnotation(new DataPoint<DateTime, double>(DateTime.Parse(dataPoint.X.ToString()).AddMinutes(-5), (double)dataPoint.Y + 5), dataPoint);
    AddAnnotation(ellipse);
}

For every annotation, make sure the X and Y axes are set to the chart's X and Y axes and add them to the Annotations collection.

void AddAnnotation(IAnnotation annotation)
{
    annotation.IsInteractionEnabled = false;
    annotation.XAxis = chart.XAxis;
    annotation.YAxis = chart.YAxis;
    chart.Annotations.Add(annotation);
}

After initialising the DataSeries, a HorizontalLineAnnotation is added on value 25 of the YAxis. Since the HorizontalLineAnnotation is infinite, it only takes into consideration the Y value of the datapoint and it is added once.

void AddHorizontalAnnotation()
{
    HorizontalLineAnnotation horizAnnot = new HorizontalLineAnnotation(new DataPoint<DateTime, double>(_currentTime, 25));
    AddAnnotation(horizAnnot);
}

The chart updates regularly by removing the datapoint at the beginning and adding a new datapoint at the end. For every datapoint added, AddAnnotations() is called so one or more annotations might be added as well.

void UpdateChart()
{
    if (((FrameworkElement)chart.Parent).ActualWidth == 0)
    {
        return;
        // If the chart is no longer displayed, stop running so that it can be garbage collected. 
    }
    DataSeries<DateTime, double> priceData = (DataSeries<DateTime, double>)chart.Series[0].DataSeries;
    double YValue = rand.Next(10, 80);
    var dataPoint = new DataPoint<DateTime, double>(_currentTime, YValue);
    priceData.Add(dataPoint);
    _lastPointCounter++;

    AddAnnotations(dataPoint);

    _currentTime = _currentTime.AddMinutes(1);            

    if (priceData.Count > _MaxPoints)
    {
        priceData.RemoveAt(0);
        RemoveHiddenAnnotations(priceData[0]);
    }
    _lastUpdated = DateTime.Now;
}

The following event handler is invoked every time the Silverlight framework renders a frame. If enough time has passed (_tickInterval milliseconds), then update the chart.

void CompositionTarget_Rendering(object sender, EventArgs e)
{
    if ((DateTime.Now - _lastUpdated).TotalMilliseconds > _tickInterval)
    {
        UpdateChart();
    }
}

As previously discussed, the data initialization takes place after the chart has loaded. Then, we add the HorizontalLineAnnotation and submit the event handler that will update the chart.

private void chart_Loaded(object sender, RoutedEventArgs e)
{
     InitilizeData();
     AddHorizontalAnnotation();
     CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering);
}

Finally, annotations are removed from the Annotations collections if they are no longer visible. As the chart updates and points are removed from the plot area, we need to clean up any annotations attached to those points.

private void RemoveHiddenAnnotations(DataPoint<DateTime, double> firstVisiblePoint)
{
    AnnotationCollection removedAnnotations = new AnnotationCollection();

    foreach (IAnnotation annotation in chart.Annotations)
    {
        if ((annotation as HorizontalLineAnnotation) != null)
            continue;
        foreach(IObservableDataPoint point in annotation.Points){
            if (DateTime.Parse(point.X.ToString()) < firstVisiblePoint.X)
                removedAnnotations.Add(annotation);
        }
    }
    foreach (IAnnotation annotation in removedAnnotations)
    {
        chart.Annotations.Remove(annotation);
    }
} 

Putting it all together, we get the application displayed below. The source code for the example can be downloaded here, but you will need to add a reference to the premium Visiblox dll to compile it.

Summary

In this blog post, we introduced annotations and how we can interact with them. We looked closely into creating and deleting them, either programmatically or by using the built-in behaviour, and provided examples for each case. You can download the source code of both examples from here. Note that you will need to reference the Premium Visiblox dll to run them.

Blog posts providing a closer look to styling annotations and extending them to provide custom annotations are coming up next. Stay tuned!

UPDATE: a blog post covering how to style annotations has been published!

Comments

This a great post about annotations. I am currently writing a program that uses annotations. These annotations need to be interactive, like the first example. However, I just want to use the Callout annotation. So, I don't need the toolbar. I have attempted to set the annotation mode to Callout but with no luck. It is defaulted to line through the DefaultAnnotationFactory. How can I adjust the annotation mode to callout without the toolbar?
 
Posted by Wolfgang Emmons
You should be able to set the annotation to use by setting the AnnotationMode property on the DefaultAnnotationFactory. Something like: (annotationBehaviour.AnnotationFactory as DefaultAnnotationFactory).AnnotationMode = BuiltinAnnotationMode.Callout; If that isn't working for you, could you email me some code that reproduces what you're trying to do and I can take a look?
 
Posted by Jesse
Can I create a vertical line annotation in xaml and bind the XValue to a property?
 
Posted by Alex
Hi Alex, You can't directly declare an annotation in XAML because they don't have a default constructor. But you can get around this by sub-classing the annotation like so:
public class MyAnnotation : VerticalLineAnnotation { public MyAnnotation() : base(new DataPoint<double, double>(0, 0)) { } }

You'll also need to add a style for this annotation (or it won't display). You can inherit the default like so:

<ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="pack://application:,,,/Visiblox.Charts;component/generic.DefaultTheme.xaml" /> </ResourceDictionary.MergedDictionaries> <Style TargetType="local:MyAnnotation" BasedOn="{StaticResource DefaultVerticalLineAnnotationStyle}" /> </ResourceDictionary>

Once you've done this you can set up the binding like normal:

<charts:Chart.Annotations> <local:MyAnnotation XValue="{Binding MyValue}" /> </charts:Chart.Annotations>

Kind regards,

William

 
Posted by William

Post a comment