Skip to main content Skip to footer

Creating Colored “Alarm Zones” in C1Chart for XAML

In this post I describe a custom implementation of colored bands, or 'Alarm Zones,' in the C1Chart control, and I provide coded solutions for both WPF and Silverlight. The same code can also be applied to Windows Store and Windows Phone apps as well.

What is an Alarm Zone?

Alarm Zones are a series of bands or shapes that can be placed behind the plotted data, but in front of the chart background. Generally, Alarm Zones are used in much the same manner as grid lines, but the ability to modify the Alarm Zones allows them to be more useful and visually appealing. Alarm zones are also known more simply as colored bands or regions. Chart_AlarmZone5

Custom Implementation

While the XAML C1Chart control does not have built-in support for alarm zones like the WinForms version, accomplishing the same feature is quite possible. Every XAML version of C1Chart supports polygon chart types and they allow you to combine any number of different chart types in a single plot. For instance you can very easily create an alarm zone by simply adding a PolygonFilled data series to your chart like below.


<c1:C1Chart Name="c1Chart1"  
            ChartType="XYPlot"  
            Palette="Standard">  
    <c1:C1Chart.Data>  
        <c1:ChartData>  
            <c1:XYDataSeries ChartType="PolygonFilled"  
                             XValues="3 3 7 6 5"  
                             Values="8 17 17 15 8"/>  
            <c1:XYDataSeries Label="S1"  
                             Values="5 12 18 14 6 7 21 15 12 19"  
                             XValues="5 4 3 1 2 6 7 9 8 5" />  
        </c1:ChartData>  
    </c1:C1Chart.Data>  
    <c1:C1ChartLegend />  
</c1:C1Chart>  

Chart_PolygonSeries The problem here is that you have to map out in your head the X,Y coordinates for each point. Plus, if you want a zone to extend to "infinity" you must know ahead of time what the maximum value of the axis is. It would be a lot easier if we could just provide a few extents for our alarm zone and let the data series figure out the exact coordinates for each point. We can do this by writing a custom class, AlarmZone, which extends the XYDataSeries class. Take a look at the WPF definition of AlarmZone.cs below. Copy this to your project and follow along below. I've also provided a Silverlight C# version and a VB version for WPF below. Or you can download full samples at the bottom of this blog post. AlarmZone.cs (WPF)


using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Windows.Media;  
using System.Windows.Controls;  
using C1.WPF.C1Chart;  

namespace Chart\_AlarmZones\_WPF_CS  
{  
    public class AlarmZone : XYDataSeries  
    {  
        #region "constructor"  

        public AlarmZone()  
        {  
            this.LayoutUpdated += new EventHandler(AlarmZone_LayoutUpdated);  
            this.ChartType = C1.WPF.C1Chart.ChartType.PolygonFilled;  
            this.XValues = new DoubleCollection();  
            this.Values = new DoubleCollection();  
            UpdateLegend();  
            Update();  
        }  

        #endregion  

        #region "members"  

        private C1Chart _chart;  
        public C1Chart Chart  
        {  
            get { return _chart; }  
            set { _chart = value; }  
        }  

        private double? _lowerExtent = null;  
        public double? LowerExtent  
        {  
            get { return _lowerExtent; }  
            set  
            {  
                _lowerExtent = value;  
                Update();  
            }  
        }  

        private double? _upperExtent = null;  
        public double? UpperExtent  
        {  
            get { return _upperExtent; }  
            set  
            {  
                _upperExtent = value;  
                Update();  
            }  
        }  

        private double? _near = null;  
        public double? Near  
        {  
            get { return _near; }  
            set  
            {  
                _near = value;  
                Update();  
            }  
        }  

        private double? _far = null;  
        public double? Far  
        {  
            get { return _far; }  
            set  
            {  
                _far = value;  
                Update();  
            }  
        }  

        private bool _showInLegend = false;  
        public bool ShowInLegend  
        {  
            get { return _showInLegend; }  
            set  
            {  
                _showInLegend = value;  
                UpdateLegend();  
            }  
        }  

        #endregion  

        #region "implementation"  

        public void Update()  
        {  
            if (\_near != null && \_far != null)  
            {  
                this.XValues.Clear();  
                this.XValues.Add((double)_near);  
                this.XValues.Add((double)_far);  
                this.XValues.Add((double)_far);  
                this.XValues.Add((double)_near);  
            }  

            if(\_lowerExtent != null && \_upperExtent != null)  
            {  
                this.Values.Clear();  
                this.Values.Add((double)_lowerExtent);  
                this.Values.Add((double)_lowerExtent);  
                this.Values.Add((double)_upperExtent);  
                this.Values.Add((double)_upperExtent);  
            }  
        }  

        public void UpdateLegend()  
        {  
            if (_showInLegend)  
            {  
                this.Display = SeriesDisplay.SkipNaN;  
            }  
            else  
            {  
                this.Display = SeriesDisplay.HideLegend;  
            }  
        }  

        private void chart_LayoutUpdated(object sender, EventArgs e)  
        {  
            if (this.Chart != null)  
            {  
                if (this.Chart.View != null)  
                {  
                    // if extent is null, set to axis bounds  
                    if (_near == null)  
                    {  
                        _near = Chart.View.AxisX.ActualMin;  
                        Update();  
                    }  
                    if (_far == null)  
                    {  
                        _far = Chart.View.AxisX.ActualMax;  
                        Update();  
                    }  
                    if (_upperExtent == null)  
                    {  
                        _upperExtent = Chart.View.AxisY.ActualMax;  
                        Update();  
                    }  
                    if (_lowerExtent == null)  
                    {  
                        _lowerExtent = Chart.View.AxisY.ActualMin;  
                        Update();  
                    }  

                }  
            }  
        }  

        // obtains the parent chart control so we can later get axis bounds  
        void AlarmZone_LayoutUpdated(object sender, EventArgs e)  
        {  
            if (this.Parent != null && this.Chart == null)  
            {  
                Canvas c = this.Parent as Canvas;  
                if (c != null)  
                {  
                    Canvas cv = c.Parent as Canvas;  
                    if (cv != null)  
                    {  
                        Border b = cv.Parent as Border;  
                        if (b != null)  
                        {  
                            Grid g = b.Parent as Grid;  
                            if (g != null)  
                            {  
                                C1Chart chart = g.Parent as C1Chart;  
                                if (chart != null)  
                                {  
                                    this.Chart = chart;  
                                    this.Chart.LayoutUpdated += chart_LayoutUpdated;  
                                }  
                            }  
                        }  

                    }  
                }  

            }  
        }  

        #endregion  

    }  
}  

AlarmZone.cs (Silverlight/WinRT version) AlarmZone.vb (WPF) The AlarmZone class exposes 4 key properties: Near, Far, UpperExtent and LowerExtent. We’re using the same language here as the WinForms version. These properties are translated into the data points for the polygon series, but now it’s much easier to define multiple alarm zones because we only need to provide up to 4 values. As you can see in the diagram below, these 4 properties form the bounds of our alarm zone. Chart_AlarmZone2 The color of our zone is determined by the ConnectionFill property which is inherited from the XYDataSeries. We can take advantage of anything that the underlying data series supports, such as appearance and legend properties, without having to write this code ourselves. After you’ve added the AlarmZone class to your project, build and add a local xml namespace to your XAML file.


xmlns:local="clr-namespace:<Project Namespace>"  

Now you can add some AlarmZones to your chart by adding them like data series. To create a red block, like in the diagram above, you would set all four properties, Near, Far, LowerExtent and UpperExtent, such as below.


<c1:C1Chart Name="c1Chart1"  
            ChartType="XYPlot"  
            Palette="Standard">  
    <c1:C1Chart.Data>  
        <c1:ChartData>  
            <local:AlarmZone Near="3"  
                             Far="7"  
                             LowerExtent="10"  
                             UpperExtent="18"  
                             ShowInLegend="True"  
                             Label="Alarm"  
                             ConnectionFill="#79FF0000"/>  
            <c1:XYDataSeries Label="S1"  
                             Values="5 12 18 14 6 7 21 15 12 19"  
                             XValues="5 4 3 1 2 6 7 9 8 5" />  
        </c1:ChartData>  
    </c1:C1Chart.Data>  
    <c1:C1ChartLegend />  
</c1:C1Chart>  

We’ve also added a ShowInLegend property which allows you to label the zone in the legend. This property simply uses the underlying Display property on the XYDataSeries to determine if the series displays in the legend. What’s neat about this custom AlarmZone implementation is that it can also extend zones to the bounds of the axis, regardless of the actual data in the chart. If you notice, the Near, Far, UpperExtent and LowerExtent properties are all Nullable. If you don’t explicitly set any of these values the alarm zone will extend to the axis bounds in that direction. For example, to create some vertical bands that extend from the bottom of the Y-axis to the top, you can simply just set the Near and Far properties for each zone.


<c1:C1Chart Name="c1Chart1" ChartType="XYPlot">  
    <c1:C1Chart.Data>  
        <c1:ChartData>  
            <local:AlarmZone Far="3"  
                             ConnectionFill="#96FF0000"  
                             ShowInLegend="True"  
                             Label="Poor"/>  
            <local:AlarmZone Near="3"  
                             Far="6"  
                             ConnectionFill="#96FFFF00"  
                             ShowInLegend="True"  
                             Label="Medium"/>  
            <local:AlarmZone Near="6"  
                             ConnectionFill="#96008000"  
                             ShowInLegend="True"  
                             Label="Good"/>  
            <c1:XYDataSeries Label="S1"  
                             Values="5 12 18 14 6 7 21 15 12 19"  
                             XValues="5 4 3 1 2 6 7 9 8 5" />  
        </c1:ChartData>  
    </c1:C1Chart.Data>  
    <c1:C1ChartLegend />  
</c1:C1Chart>  

Chart_AlarmZone1 By not setting UpperExtent and LowerExtent, the zones will extend completely in those directions. Notice we don't even have to set the Near property for the red zone, or the far property for the green zone because these will extend to "infinity" along the x-axis. The magic here is in the AlarmZone implementation code. If any of these values are null, the alarm zone gets the actual axis min or max from the parent chart and uses that value.


// SNIPPET: if upper extent is null, set to axis max  
if (_upperExtent == null)  
{  
    _upperExtent = Chart.View.AxisY.ActualMax;  
    Update();  
}  
...  

In order to obtain the parent chart, the AlarmZone finds it by cycling through its parent when it's first initialized. This is where the code slightly differs between WPF and the other XAML platforms because the visual tree hierarchy is a bit different. So make sure to use the correct version of AlarmZone I’ve posted above.

Conclusion

This sample demonstrates how we’re able to extend and add this missing functionality to C1Chart by using existing features. By using a polygon data series as a base we can create our own ‘Alarm Zone’ definition that can be added to any existing chart solution. You can download a full sample for WPF (C#/VB) or Silverlight (C#) below. Chart_AlarmZones_WPF_CS.zip Chart_AlarmZones_WPF_VB.zip ChartAlarmZones_Silverlight_CS.zip

ComponentOne Product Manager Greg Lutz

Greg Lutz

comments powered by Disqus