SciChart® the market leader in Fast WPF Charts, WPF 3D Charts, and now iOS Charting & Android Chart Components

Providing Annotations via The ChartModifier API

This article hails from the days of v1.3.x where Annotations were not native to SciChart. We’re keeping it for posterities sake, however, SciChart now has a much improved Annotations API in v1.5 of SciChart. So do take this article as a bit of history and use the new Annotations API instead!

One of the more hotly requested features of SciChart is the ability to add custom Annotations. While this feature is currently in Beta and the API will be improved for subsequent versions of SciChart, this is already possible with the current release. This article shows you how to do just that with the current released version of SciChart (v1.2.1).

SciChart ships with a ChartModifier API, which allows you, the end-user to extend interactivity of the chart as you see fit. The ChartModifier API allows you to subscribe to mouse events, render update events, overlay UIElements on the chart, interact with chart Axes and convert data values to pixel coordinates and back. With these building blocks you can do almost anything that WPF and Silverlight allow.

The Use-Case

User X wants to analyse some data. They want to display a large dataset (>10k points) and interact with the chart by pointing and clicking. In particular they want to highlight areas of interest with text/lines as if the chart were being edited in a paint program. Finally, they want to export the annotated chart to an image file to include in reports.

Ok you say, so where shall we start? We start by creating a chart with some data to render.

Xaml

<SciChart:SciChartSurface x:Name="sciChart" Margin="10">

    <SciChart:SciChartSurface.RenderableSeries>
        <SciChart:FastLineRenderableSeries SeriesColor="SteelBlue" ResamplingMode="None"/>
    </SciChart:SciChartSurface.RenderableSeries>

    <SciChart:SciChartSurface.XAxis>
        <SciChart:NumericAxis TextFormatting="0.000E+0"/>
    </SciChart:SciChartSurface.XAxis>

    <SciChart:SciChartSurface.YAxis>
        <SciChart:NumericAxis/>
    </SciChart:SciChartSurface.YAxis>

    <SciChart:SciChartSurface.ChartModifier>
        <SciChart:ModifierGroup>
            <SciChart:RubberBandXyZoomModifier x:Name="zoomModifier" IsXAxisOnly="True" IsEnabled="True"/>
            <SciChart:ZoomPanModifier x:Name="panModifier" IsEnabled="False"/>
            <SciChart:XAxisDragModifier/>
            <SciChart:YAxisDragModifier/>
        </SciChart:ModifierGroup>
    </SciChart:SciChartSurface.ChartModifier>
</SciChart:SciChartSurface>

Code

private void PopulateChart()
{
	// This example assumes our data has been populated in arrays
	double[] yValues;
	double[] xValues;

	// Suspend drawing
	using (var updateSuspender = sciChart.SuspendUpdates())
	{
		// Create dataset
		var dataSeriesSet = new DataSeriesSet();
		sciChart.DataSet = dataSeriesSet;

		// Add series
		var tdSeries = dataSeriesSet.AddSeries();
		tdSeries.SeriesName = &quot;Time Domain&quot;;

		// Append data
		tdSeries.Append(xValues, yValues);

		// On every zoom extents, the Y Range will expand by 10% below and 10% above the data range
		sciChart.YAxis.GrowBy = new DoubleRange(0.1, 0.1);

		// Zoom extents
		sciChart.ZoomExtents();
	}
	// redraws on exit of this block (disposal of ISuspendable type)
}

The above code creates a SciChartSurface instance with Numeric X and Y Axes. It creates a single line series and adds a dataset with single DataSeries. The DataSeries is populated with double values. Notice that the SciChartSurface.ChartModifier property is filled with a ModifierGroup containing multiple chart modifiers. These provide our zooming, panning and axis dragging behavior.

    <!-- ... -->
    <SciChart:SciChartSurface.ChartModifier>
        <SciChart:ModifierGroup>
		    <WpfApplication1:TextAnnotationModifier x:Name="annotationModifier"/>
            <SciChart:RubberBandXyZoomModifier x:Name="zoomModifier" IsXAxisOnly="True" IsEnabled="True"/>
            <SciChart:ZoomPanModifier x:Name="panModifier" IsEnabled="False"/>
            <SciChart:XAxisDragModifier/>
            <SciChart:YAxisDragModifier/>
        </SciChart:ModifierGroup>
    </SciChart:SciChartSurface.ChartModifier>

What we will do in the next section is create a custom modifier to add our annotations.

Creating the custom AnnotationModifier

First of all, we need to create a base class for our annotations. This will be AnnotationControl and simply inherits FrameworkElement. Later we will override OnRender to perform the drawing, but for now leave this class as-is. AnnotationControl stores the X,Y data values (not mouse coordinates) where we wish to draw our annotation. These will be used to update the annotation position as the chart is zoomed, panned or otherwise redrawn.

public class AnnotationControl : FrameworkElement
{
	public double XData1 { get; set; }
	public double XData2 { get; set; }
	public double YData1 { get; set; }
	public double YData2 { get; set; }
	public string Text { get; set; }
}

Next we need to create a custom chart modifier to draw our annotations. This will be called AnnotationsModifier and extends ChartModifierBase.

public class AnnotationsModifier : ChartModifierBase
{
private readonly IList _annotations = new List();

// Occurs when the modifier is attached to the parent surface
public override void OnAttached()
{
	base.OnAttached();
	Services.GetService().Subscribe(OnScichartSurfaceRendered);
	}

	public override void OnModifierDoubleClick(ModifierMouseArgs e)
	{
		base.OnModifierDoubleClick(e);
		AddAnnotation(e.MousePoint);
	}

	// Called each time the SciChartSurface is rendered
	private void OnScichartSurfaceRendered(SciChartRenderedMessage obj)
	{
		UpdateAnnotations();
	}

	private void UpdateAnnotations()
	{
		foreach(var annotation in _annotations)
		{
			double xCoord1 = XAxis.GetCoordinate(annotation.XData1);
			double yCoord1 = YAxis.GetCoordinate(annotation.YData1);

			Canvas.SetLeft(annotation, xCoord1);
			Canvas.SetTop(annotation, yCoord1);

			annotation.InvalidateVisual();
		}
	}

	private void AddAnnotation(Point mousePoint)
	{
		var annotation = new AnnotationControl();

		annotation.XData1 = (double)XAxis.HitTest(mousePoint).DataValue;
		annotation.YData1 = (double)YAxis.HitTest(mousePoint).DataValue;

		annotation.Text = &quot;Hello World!&quot;;

		_annotations.Add(annotation);
		ModifierSurface.Children.Add(annotation);
		UpdateAnnotations();
	}
}

AnnotationsModifier overrides the OnAttached() method, called whenever a Chart Modifier is attached to a parent SciChartSurface. Here it subscribes to the SciChartRendered event, which is fired immediately before the end of a render pass. Events are weak events provided by TinyMessenger, meaning they don’t need to be unsubscribed to prevent memory leaks.

AnnotationsModifier also overrides OnModifierMouseDoubleClick. This provides a cross-platform mouse double click event which hooks into the SciChart eventing system. Here we will create an annotation.

The next two methods of interest are AddAnnotation() and UpdateAnnotations().

AddAnnotation() creates an AnnotationControl and adds it to the ModifierSurface. This is a canvas which is overlaid on top of the chart pane and allows UIElements to be added and manipulated. Notice that AddAnnotation() first converts the X,Y mouse coordinates into X,Y data coordinates for storage on the AnnotationControl.

// ...
private void AddAnnotation(Point mousePoint)
{
	var annotation = new AnnotationControl();

	// Convert X,Y mouse points to data coordinates and store
	annotation.XData1 = (double)XAxis.HitTest(mousePoint).DataValue;
	annotation.YData1 = (double)YAxis.HitTest(mousePoint).DataValue;
	annotation.Text = &quot;Hello World!&quot;;

	_annotations.Add(annotation);
	ModifierSurface.Children.Add(annotation);
	UpdateAnnotations();
}

UpdateAnnotations() iterates over all AnnotationControls currently on the chart ModifierSurface. These are repositioned by converting their previously stored X,Y data coordinates into X,Y pixel coordinates. UpdateAnnotations occurs every time the parent SciChartSurface is rendered or if a new annotation is added.

// ...
private void UpdateAnnotations()
{
	foreach(var annotation in _annotations)
	{
		// Convert Data coordinates to X,Y pixel coords
		double xCoord1 = XAxis.GetCoordinate(annotation.XData1);
		double yCoord1 = YAxis.GetCoordinate(annotation.YData1);

		Canvas.SetLeft(annotation, xCoord1);
		Canvas.SetTop(annotation, yCoord1);

		annotation.InvalidateVisual();
	}
}

Custom Rendering of our Annotation

Finally, we need to update our AnnotationControl to render some text. To do this we are going to override OnRender.

Note that OnRender is a WPF only feature. If you wish to create annotations in Silverlight, this can be done, but you will need to generate the annotation control using simple UIElement derived classes, e.g. Line, Rectangle, TextBlock.

public class AnnotationControl : FrameworkElement
{
	private Pen _linePen;
	private SolidColorBrush _textBrush;
	private Typeface _typeFace;

	public TextAnnotationControl()
	{
		_textBrush = new SolidColorBrush(Colors.DarkSlateGray);
		_linePen = new Pen(_textBrush, 2);
		_typeFace = new Typeface(
		new FontFamily(&quot;Arial&quot;),
		FontStyles.Normal,
		FontWeights.Normal,
		FontStretches.Normal);
	}

	public double XData1 { get; set; }
	public double XData2 { get; set; }
	public double YData1 { get; set; }
	public double YData2 { get; set; }
	public string Text { get; set; }

	protected override void OnRender(DrawingContext drawingContext)
	{
		base.OnRender(drawingContext);

		var formattedText = new FormattedText(Text,
			CultureInfo.InvariantCulture,
			FlowDirection.LeftToRight,
			_typeFace,
		10.0,
		_textBrush);

		// Annotation will be positioned by the parent AnnotationsModifier
		// Just draw the text at an offset of 0,0 (Note: can adjust offset
		// if required)
		drawingContext.DrawText(formattedText, new Point(0,0));
	}
}

The above code simply draws some text to the screen at an offset of 0,0. If you wish to modify this offset, for intance to account for the height or alignment of the text string, then do so here.

Putting it all together

Custom Annotations

Custom Annotations

The above code snippets hopefully demonstrate how you can add custom annotations to your SciChart projects. To simplify this process we’ve created a Demo Application which you can download and compile. This requires the SciChart binaries which you can get by signing up to our Newsletter.

The demo application includes a toolbar at the top of the chart to set zoom, pan mode and also allows you to take a screenshot. Screenshot functionality is provided by the enclosed ScreenshotUtil, which uses the WPF RenderTargetBitmap class to perform this action.

Leave a Reply