dinsdag 15 september 2009

Freeing the axes of the Microsoft toolkit charting control

Introduction

Most charting tools produce the same kind of charts. A chart has a vertical axis on the left (or right) and a horizontal axis on the bottom (or top). You can add more axes but they are stuck to the sides of the chart and keep occupying the whole length of the chart area. In medical applications we want to have charts like the one below. For the interested: The first five signals are EEG signals, (the electrical activity in the brain). Below the EEG signals are the patients breathing signal, the blood pressure and the heartbeat (ECG) signal.
Eeg

The signals are stacked on top of each other. If one signal gets big it can reach the space of the signal below or above it. So there’s one plot area for all signals. As I said before all free WPF chart tools I know don’t have the option of creating a chart like the picture above. In my job as a programmer in a hospital I often need charts like these to display medical data. In this blog post I want to talk about my first attempt to create a chart like the one above.

Microsoft’s Charting Control

In June 2009 Microsoft released a charting control as part of their WPF toolkit. Because they released the source code of the toolkit it is easy to extend the functionality of the original toolkit. The main class of the charting control is the Chart class. This class has a property called ChartArea of type EdgePanel that is derived from the Panel class. The EdgePanel class overrides the ArrangeOverride and MeasureOverride methods of the Panel class to arrange the axes. In my first attempt to create a chart like the EEG chart above I created a new class called StackedPanel and tried to arrange the axes on the left on top of each other. The EdgePanel class was then replaced with the StackedPanel class. I asked a question about it here.
Later I noticed that all classes derived from Axis in the toolkit can be used without the need for any other classes of the toolkit. So you can for instance take the LinearAxis class set it’s location, orientation, minimum and maximum values, put it on a canvas and it displays a nice looking axis. That made me decide to write my own simple chart class. To keep things simple I just wanted to use the classes derived from the base Axis class of Microsoft’s Charting Control. I considered using the LineSeries class to show the data,  but that would make my Chart class more complicated. I decided to create my own FastLineSeries class because I need a class that can show large amounts of data anyway and the LineSeries in Microsoft’s class uses a PolyLine class to draw it’s data. There are faster methods to draw lines in WPF then with a PolyLine.
The source code that comes with this article contains a solution with three small programs and a library. The contents of the solution are:
  • Thumbs: A small demonstration program described in the paragraph below.
  • FreeAxes: A program that allows you to drag and resize the all axes.
  • StackedAxes: A program that stacks all vertical axes.
  • Wpf.Axes: A library that the FreeAxes and StackedAxes programs use.

Sukram’s WPF Diagram Designer

I wanted to be able to drag the axes of my program around and resize them. The plot area should adapt to the location and the size of the axes. There is a very nice series of articles on the CodeProject site by Sukram called WPF Diagram Designer. The first article of this series describes how you can move and resize any control that you put in a ContentControl. The window below shows the first program I created for testing purposes. The black rectangles represent the axes and the gray border is the plot area. In the upper left corner is a checkbox. If you select the checkbox the rectangles can be resized. If you deselect the checkbox the rectangles only can be moved. If you move the axes around you see that the plot area changes according to the position and size of the axes.
Thumbs
The FakeAxis class in this test program is derived from the Control class and implements the IAxis interface of the Microsoft Chart Control. The most important part of the code is shown below:
    public class FakeAxis : Control, IAxis
    {
        static FakeAxis()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(FakeAxis), new FrameworkPropertyMetadata(typeof(FakeAxis)));
        }
 
        /// <summary>
        /// Instantiates a new instance of the Axis class.
        /// </summary>
        public FakeAxis()
        {
            RegisteredListeners = new ObservableCollection<IAxisListener>();
            this.RegisteredListeners.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(RegisteredListeners_CollectionChanged);
            LayoutUpdated += new EventHandler(Axis_LayoutUpdated);
        }
 
        void Axis_LayoutUpdated(object sender, EventArgs e)
        {
            Invalidate();
        }
 
        internal void Invalidate()
        {
            OnInvalidated(new RoutedEventArgs());
        }
 
        /// <summary>
        /// Raises the invalidated event.
        /// </summary>
        /// <param name="args">Information about the event.</param>
        protected virtual void OnInvalidated(RoutedEventArgs args)
        {
            foreach (IAxisListener listener in RegisteredListeners)
            {
                listener.AxisInvalidated(this);
            }
        }
The IAxis interface has a property called RegisteredListeners. The plot area implements the IAxisListener interface and its AxisInvalidated method is called when an Axis changes its position or size. When the chart canvas is initialized all components that implement the IAxisListener interface are added to the RegisteredListeners collection of the axes:
        void ChartCanvas_Initialized(object sender, EventArgs e)
        {
            ChartArea area = Children.OfType<ChartArea>().First();
            IEnumerable<ContentControl> ctrls = Children.OfType<ContentControl>();
            IEnumerable<FakeAxis> axes = ctrls.Select(c => c.Content).OfType<FakeAxis>();
            foreach (FakeAxis axis in axes)
            {
                area.Axes.Add(axis);
                axis.RegisteredListeners.Add(area);
                axis.InvalidateVisual();
            }
        }

The class library for the stacked axes and for freely movable axes

Below is a class diagram of library. The FreePanel class you can use to move axes freely around it’s canvas. The StackedPanel class will stack the available axes with vertical orientation on top of each other. The FastLineSeries is for drawing the line series.
image
I used an msdn article by Charles Petzold as the starting point for my FastLineSeries class. I won’t describe the inner workings of  this class. If you want to read more about fast drawing in WPF you can read the article. I changed Petzold’s code so instead of displaying a scatter plot it displays a line plot now. This class I named GeometryPlotVisuals. The FastLineSeries derives form the GeometryPlotVisuals class and implements the IAxisListener interface. The AxisInvalidated method of the FastLineSeries looks like this:

        public void AxisInvalidated(IAxis axis)
        {
            double scaleY = calculateScale(DependentAxis as NumericAxis);
            Scale.ScaleY = -scaleY;
            Scale.ScaleX = calculateScale(IndependentAxis as NumericAxis);
            Translate.Y = DependentAxis.ActualHeight;
            Translate.X = 0;
            Clip = SeriesClip;
        }
 
        private double calculateScale(NumericAxis axis){
            UnitValue? max = axis.GetPlotAreaCoordinate(axis.Maximum);
            UnitValue? min = axis.GetPlotAreaCoordinate(axis.Minimum);
            return (max.Value.Value - min.Value.Value) / (axis.Maximum.Value - axis.Minimum.Value);
        }
The FastLineSeries uses the ScaleTransform and TranslateTransform classes to position the line series.  I’m not really sure about using the ScaleTransform to position the series because it also changes the thickness of the line. I’ll probably change this in the future.
The last line of the AxisInvalidated method sets the clipping region of the chart area so that values outside of the chart area become invisible.
ChartPanel is the base class for the classes FreePanel and StackedPanel. Because Sukram’s Diagram Designer classes need a Canvas I had to use the Canvas class as it’s base class and not the Panel class. The class searches for axes and series that are placed on it’s canvas and adds them to the Axes and Series ObservableCollections.

An example program with movable and resizable axes

I was surprised how easy it was to display an axis of Microsoft’s chart control. I just had to replace the axis I created in my test program with the LinearAxis class of the chart control.
                                  <chart:LinearAxis x:Key="xAxis" 
                              Orientation="X"  
                              Location="Bottom" 
                              Minimum="0"  
                              Maximum="100" 
                              Interval="10" 
                              IsHitTestVisible="False"/>



Below is my definition for the x axis of the program. The Style property of the ContentControl uses a static resource called DesignerItemStyle. This style is created by Sukram to give drag and drop and resize capabilities to a ContentControl. Just put the axis in a ContentControl and you can move it around and resize it:
                <ContentControl Width="402" 
                                Height="50" 
                                Padding="1" 
                                Canvas.Left="55" 
                                Canvas.Top="329" 
                                Style="{StaticResource DesignerItemStyle}" 
                                Content="{StaticResource xAxis}"></ContentControl>
Every time you move or resize an axis that is placed on a FreePanel canvas. The MeasureOverride and ArrangeOverride methods are called. In the FreePanel class we don’t need the MeasureOverride method and only use the ArrangeOverride method. The ArrangeOverride method calculates the new location  and calls the AxisInvalidated method for all the FastLineSeries the class holds.

        protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)
        {
            if (this._vertCtrls == null || this._horzCtrls == null)
                return finalSize;
            vertAxesPositions.Clear(); horzAxesPositions.Clear();
            // fill the dictionary with left positions
            foreach (ContentControl child in _horzCtrls)
                horzAxesPositions.Add(
                    new KeyValuePair<UIElement, double>(child.Content as UIElement, Canvas.GetLeft(child)));
            // fill the dictionary with top positions
            foreach (ContentControl child in _vertCtrls)
                vertAxesPositions.Add(
                    new KeyValuePair<UIElement, double>(child.Content as UIElement, Canvas.GetTop(child)));
 
            positionSeries();
 
            return base.ArrangeOverride(finalSize);
        }
 
        /// <summary>
        /// Positions the series.
        /// </summary>
        void positionSeries()
        {
            // look for the minimum left position of the axes
            double left = _horzCtrls.Select(p => Canvas.GetLeft(p)).Min();
            // look for the minimum top position of the axes
            double top = _vertCtrls.Select(p => Canvas.GetTop(p)).Min();
            // look for the maximum bottom position of the axes
            double bottom = _vertCtrls.Select(p => Canvas.GetTop(p) + p.Height).Max();
            // look for the maximum right position of the axes
            double right = _horzCtrls.Select(p => Canvas.GetLeft(p) + p.Width).Max();
 
            if (Series != null)
                foreach (FastLineSeries child in Series)
                {
                    if (child.IndependentAxis == null || child.DependentAxis == null)
                        continue;
                    double curX = horzAxesPositions[child.IndependentAxis];
                    double curY = vertAxesPositions[child.DependentAxis];
                    Canvas.SetLeft(child, curX);
                    Canvas.SetTop(child, curY);
                    child.SeriesClip = new RectangleGeometry(
                       new Rect(
                           0,
                           top - curY,
                           right - left,
                           bottom - top)); ;
                    child.AxisInvalidated(null);
                }
        }

A Stacked chart

The stacked chart needed more complicated MeasureOverride and ArrangeOverride methods. First because it needs to look at the size of the panel and change the size of the axes accordingly. Also I added a Proportion attached property that made things more complicated. The Proportion property allows you to set the size of an axis based on a given percentage. For instance, if you have two vertical axes where one has a Proportion value of 0.25 and the other of 0.75 the first only occupies 25% of the total vertical space available. Alec Bryte has a nice blog article about it that you can find here.

Remarks

The program described was created just as a proof of principle for myself. I can now create axes and move them around. The attached series adapts to the size and position of the axes. I know that there is still a lot of room for improvement. There’s still a lot of work to do before I have something that can display medical data like the first picture in this article.
As I said in the introduction in my first attempt I tried to create a stacked chart with the LineSeries of the toolkit. That turned out to be very complicated, but I’m still looking at it. Here are some of the things I still have to do:
  • Integrate my chart class more closely with the WPF toolkit’s Chart Control.
  • Be able to display all data series types of the toolkit in my chart.
  • Derive my FastLineSeries from a Series abstract class of the toolkit.
I hope to get some feedback on how to improve the program and proceed further with this.
The source code for the program you can download here. The WPF toolkit isn’t included in the zip file. You can download the WPF toolkit here. Download the WPFToolkitBinariesAndSource zip file and unzip it. Install the toolkit by running the installation program in the binaries file. Include the “Controls.DataVisualization.Toolkit.csproj” that is in the folder “WPFToolkitBinariesAndSource\Toolkit-release\DataVisualization” of the toolkit and make a reference to the project in all the projects of the solution.

This program was created in the Clinical Neurophysiology department of the Leiden University Medical Centre in the Netherlands. Shout it kick it on DotNetKicks.com

2 opmerkingen:

Anoniem zei

Having trouble building ...

This is not being resolved:



Any suggestions?

Robert zei

This article has been on my blog for months. Nobody said they had problems with compiling. Are you sure you installed or included the june 2009 of the wpf toolkit. Do you see any warnings when you open the preferences?