Wednesday, March 25, 2009

Writing Your Own Silverlight Chart Series (Part 2): Implementing the Series

This is the second part in a series of three that explains how you can add series to Silverlight Charts.   We'll look at how Silverlight Chart's rich hierarchy of interfaces allow developers to create series that can play well with all the built-in series and axes, as well as custom series and axes written by other parties.  Once again...

BewareSilverlight charts is still in preview mode and there may be breaking changes in the future despite our best efforts to avoid them.

Last time we learn how to structure our series in such a way that designers can customize its appearance.  Now we can get down to the business of implementing our stock series.  The StockSeries class is responsible for the following:

  • Acquiring a data point style
  • Adding a legend item
  • Binding data points to a data source
  • Acquiring axes
  • Plotting the Data Points

Acquiring a Data Point Style

When we plot multiple series in the same Chart we want each of them to have different colors so that we can tell them apart.

ChartingIntroduction-Mar09-LineSeriesOnCategoryAxis

To ensure that our StockSeries uses a different color than the other series in the chart it must retrieve the next available style from its series host.  A series host contains series and provides them with various services.  Currently the only object that implements the ISeriesHost interface in Silverlight Charts is the Chart object.  Let's take a look at the Chart hierarchy.

blogimage12

When a series is inserted into a Chart the SeriesHost property of the series is set to that Chart.  Conversely when a series is removed from a chart its SeriesHost property is set to null.  When the SeriesHost property changes a protected method OnSeriesHostPropertyChanged is invoked.  We can override this method in our StockSeries class if we want to do something when the series is added or removed from a Chart. 

We should acquire a style from our series host when...

1.  The series host changes

2.  The RefreshStyles method of the base "Series" class is called

public class StockSeries : Series, IRequireGlobalSeriesIndex { private Style DataPointStyle { get; set; } protected override void OnSeriesHostPropertyChanged(ISeriesHost oldValue, ISeriesHost newValue) { AcquireDataPointStyle(); base.OnSeriesHostPropertyChanged(oldValue, newValue); } public override void RefreshStyles() { AcquireDataPointStyle(); Refresh(); } private void AcquireDataPointStyle() { if (SeriesHost != null) { using (IEnumerator<Style> styleEnumerator = SeriesHost.GetStylesWithTargetType(typeof(StockDataPoint), true)) { if (styleEnumerator.MoveNext()) { DataPointStyle = styleEnumerator.Current; } } } } // snip... }

In the example above we're using the IStyleDispenser's extension method GetStylesWithTargetType (in System.Windows.Controls.DataVisualization) to retrieve an IEnumerator of styles appropriate for our data point type. 

newValue.GetStylesWithTargetType(typeof(StockDataPoint), true))

The "true" indicates that we are willing to accept styles with a target type that is an ancestor of our data point's type.  Due to the fact that all of the styles in the Chart's default style palette target Control - which StockDataPoint inherits from - we can be sure our series will get a style when we insert it into a Chart.

Adding a Legend Item

Now that we've acquired a style we can use for our data points the time is right to insert a legend item into the Legend.  We'll add a private property to store an instance of the LegendItem class and initialize it in the constructor.  In order to ensure that it is inserted into the Legend we'll add it to the series LegendItems collection (inherited from base class Series). UI elements inserted into the legend items collection of a series are automatically inserted into the legend by the series host.

public class StockSeries : Series { // snip... private LegendItem LegendItem { get; set; } public StockSeries() { this.DefaultStyleKey = typeof(StockSeries); this.LegendItem = new LegendItem(); this.LegendItems.Add(LegendItem); } }

A well-behaved series ensures the following:

  • If it does not have a title its legend item should display the series index
  • If it has a title the legend item should display it
  • Its legend item should have a visual appearance similar to its data points

Displaying the Series Index

If the series does not have a title it should display its series index in the LegendItem. Therefore we must determine what the index of our series is.  We can try and figure out our index by counting the series in our series host collection, but this approach is complicated because we would also have to monitor the series collection of our series host for any changes.

Thankfully we can let the series host do the work for us by implementing IRequireGlobalSeriesIndex.  This is our way of signaling to the series host that this series uses an index and needs to be informed when that index changes. In addition to implementing this interface we'll also add a property to store the global series index value.

public class StockSeries : Series, IRequireGlobalSeriesIndex { private int GlobalSeriesIndex { get; set; } private LegendItem LegendItem { get; set; } public void GlobalSeriesIndexChanged(int globalIndex) { GlobalSeriesIndex = globalIndex; UpdateLegendItem(); } private void UpdateLegendItem() { if (GlobalSeriesIndex != -1) { LegendItem.Content = string.Format("Series {0}", GlobalSeriesIndex + 1); } } // snip... }

Now if we add our series to a chart we should be able to see our legend item!

blogimage15

Displaying the Series Title in the Legend Item

If a series title is set it's good practice to display it in the legend item instead of the index.  We could set the content of our legend item to the series Title property when we create the legend item, but we also need to update our legend item if the title changes.  We can listen for changes to the title property by overriding the protected OnTitleChanged method.  Let's also modify our private UpdateLegendItem method to check if the Title property is set before resorting to using the series index.

public class StockSeries : Series, IRequireGlobalSeriesIndex { private void UpdateLegendItem() { if (Title != null) { LegendItem.Content = this.Title; } else if (GlobalSeriesIndex != -1) { LegendItem.Content = string.Format("Series {0}", GlobalSeriesIndex+1); } } protected override void OnTitleChanged(object oldValue, object newValue) { UpdateLegendItem(); base.OnTitleChanged(oldValue, newValue); } // snip... }

Now if we specify a custom series title we can see it in the Legend!

<charting:Chart> <local:StockSeries Title="My First Series" /> </charting:Chart>

my first series

Styling the Legend Item

Ideally we'd like our legend item to be the same color as our data points.  We can accomplish this by creating an instance of StockSeriesDataPoint, applying the data point style we retrieved from the series host, and setting the data point to be the data context of our legend item.  The best place to do this is in our AcquireDataPointStyle method.

private void AcquireDataPointStyle() { if (SeriesHost != null) { using (IEnumerator<Style> styleEnumerator = SeriesHost.GetStylesWithTargetType(typeof(StockDataPoint), true)) { if (styleEnumerator.MoveNext()) { DataPointStyle = styleEnumerator.Current; this.LegendItem.DataContext = new StockDataPoint { Style = DataPointStyle }; } } } }

Now we can see that the legend item has a color.

my first series in color

So how does this work?  The default template for the LegendItem binds to the background color and border brush properties of its DataContext.  This approach means that we can expose a LegendItemStyle property and give designers the ability to replace our legend item's template while still retaining certain aspects of the data point style (such as the color).  I'll leave that as an exercise for the reader.

Binding Data Points to a Data Source

We'd like to be able to bind our series directly to objects in our data source.  Let's add string properties for the DatePath, HighPath, LowPath, and ClosePath to our series.  We'll store the path's in binding objects which we'll use to bind the properties in our data points to the properties in our data source objects.  We expose string properties for our paths instead of bindings because strings are easier to work with in Blend.

private Binding dateBinding = new Binding(); public string DateBinding { get { return dateBinding.Path.Path; } set { dateBinding.Path = new PropertyPath(value); } } private Binding lowBinding = new Binding(); public string LowPath { get { return lowBinding.Path.Path; } set { lowBinding.Path = new PropertyPath(value); } } private Binding highBinding = new Binding(); public string HighPath { get { return highBinding.Path.Path; } set { highBinding.Path = new PropertyPath(value); } } private Binding closeBinding = new Binding(); public string ClosePath { get { return closeBinding.Path.Path; } set { closeBinding.Path = new PropertyPath(value); } }

Let's follow the ItemsControl model and add an ItemsSource dependency property of type IEnumerable.  When the ItemsSource property changes we'll create a data point for each object and add the data points to a dictionary using the object as the key.  We'll use a helper method called CreateStockDataPoint to create the data point, apply our data point style, and add the bindings to it.

private StockDataPoint CreateStockDataPoint(object value) { var stockDataPoint = new StockDataPoint { DataContext = value }; stockDataPoint.Style = DataPointStyle; stockDataPoint.SetBinding(StockDataPoint.DateProperty, dateBinding); stockDataPoint.SetBinding(StockDataPoint.HighProperty, highBinding); stockDataPoint.SetBinding(StockDataPoint.LowProperty, lowBinding); stockDataPoint.SetBinding(StockDataPoint.CloseProperty, closeBinding); return stockDataPoint; } private Dictionary<object, StockDataPoint> _dataPoints = new Dictionary<object, StockDataPoint>(); public IEnumerable ItemsSource { get { return GetValue(ItemsSourceProperty) as IEnumerable; } set { SetValue(ItemsSourceProperty, value); } } public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register( "ItemsSource", typeof(IEnumerable), typeof(StockSeries), new PropertyMetadata(null, OnItemsSourcePropertyChanged)); private static void OnItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { StockSeries source = (StockSeries)d; IEnumerable oldValue = (IEnumerable)e.OldValue; IEnumerable newValue = (IEnumerable)e.NewValue; source.OnItemsSourcePropertyChanged(oldValue, newValue); } protected virtual void OnItemsSourcePropertyChanged(IEnumerable oldValue, IEnumerable newValue) { Refresh(); } public override void Refresh() { if (ItemsSource != null) { this.PlotArea.Children.Clear(); _dataPoints = ItemsSource .Cast<object>() .Select(obj => CreateStockDataPoint(obj)) .ToDictionary(dataPoint => dataPoint.DataContext); foreach (StockDataPoint dataPoint in _dataPoints.Values) { this.PlotArea.Children.Add(dataPoint); } } }

That's all it takes to create our data points and bind them to an underlying data source!

Creating Some Sample Data To Plot

We'll need some sample data if we want to see our series in action.  First we'll need a class to hold our sample data.

public class HighLowClose { public DateTime Date { get; set; } public double High { get; set; } public double Low { get; set; } public double Close { get; set; } }

That was easy.  Thanks automatic properties!  Now let's create a set of data and set it to the ItemsSource property of our series.

var date = new DateTime(2008, 1, 1); StockSeries stockSeries = chart.Series[0] as StockSeries; stockSeries.DatePath = "Date"; stockSeries.HighPath = "High"; stockSeries.LowPath = "Low"; stockSeries.ClosePath = "Close"; stockSeries.ItemsSource = new[] { new HighLowClose { Date = date, High = 23.0, Low = 18.0, Close = 22.0 }, new HighLowClose { Date = date.AddDays(1), High = 26.0, Low = 22.0, Close = 22.0 }, new HighLowClose { Date = date.AddDays(2), High = 33.0, Low = 30.0, Close = 32.0 }, new HighLowClose { Date = date.AddDays(3), High = 44.0, Low = 42.0, Close = 43.0 }, new HighLowClose { Date = date.AddDays(4), High = 37.0, Low = 35.0, Close = 36.0 }, new HighLowClose { Date = date.AddDays(5), High = 49.0, Low = 43.0, Close = 48.0 }, };

Now we're ready to starting plotting our data points.

Acquiring Axes

Listening for Changes

To plot our data points we'll need axes, but first we need to ensure that our series can receive messages from them.  The series needs to be able to listen for changes in the axes such as a change in the range or size.  This way it can respond to these changes by updating the positions of its data points accordingly.  A series can listen for axis changes by implementing the IAxisListener interface.

[TemplatePart(Name = "PlotArea", Type = typeof(Canvas))] public class StockSeries : Series, IAxisListener, IRequireGlobalSeriesIndex { // snip... public void AxisInvalidated(IAxis axis) { } }

The AxisInvalidated method is called by an axis when it changes.  Eventually we will add some code to update our data points to this method.  For the time being we'll leave it empty though.

Acquiring Axes

Now that we can listen for changes in an axis we want to acquire axes that we can use.  Where do get them from though? 

A well-behaved series does the following when it is added to a series host:

  • Looks for suitable axes in its series host's axes collection
  • If no axes are found it creates the axes it needs and adds them to its series host's collection of axes
  • Adds itself to each axes' collection of registered listeners

A well-behaved series does the following when it is removed from a series host:

  • Removes itself from each axes' collection of registered listeners

If we follow the steps above our stock series can share the same axes with other series.  We will also ensure that developers won't have to explicitly add appropriate axes to the chart's axes collection before they can use our series. 

There are three axes available in Silverlight Charts:

  • LinearAxis
  • DateTimeAxis
  • CategoryAxis

One approach would be too look for a horizontal DateTimeAxis and a vertical LinearAxis in the series host's axes collection when the SeriesHost property changes.  While this approach would work it would be sub-optimal.  In addition to allowing custom series, Silverlight Charts also allows custom axes.  We'd like our series to be able to use any axes added in the future (ex. LogarithmicAxis) as well as axes added by third-parties.  Let's take a look at the axis hierarchy.

blogimage10

You might be feeling a little overwhelmed but don't worry :-).  We're primarily interested in the interfaces and I'll explain those one by one. 

Let's take a look at the axes acquisition code in our StockSeries class.

private IAxis IndependentAxis { get; set; } private IRangeAxis DependentAxis { get; set; } protected override void OnSeriesHostPropertyChanged(ISeriesHost oldValue, ISeriesHost newValue) { if (newValue != null) { this.IndependentAxis = newValue.Axes .OfType<IAxis>() .Where(axis => axis.CanPlot(DateTime.Now) && axis.Orientation == AxisOrientation.X) .FirstOrDefault(); if (this.IndependentAxis == null) { IndependentAxis = new DateTimeAxis { Orientation = AxisOrientation.X }; newValue.Axes.Add(IndependentAxis); } this.IndependentAxis.RegisteredListeners.Add(this); this.DependentAxis = newValue.Axes .OfType<IRangeAxis>() .Where(rangeAxis => rangeAxis.CanPlot(0.0) && rangeAxis.Orientation == AxisOrientation.Y) .FirstOrDefault(); if (this.DependentAxis == null) { DependentAxis = new LinearAxis { Orientation = AxisOrientation.Y }; newValue.Axes.Add(DependentAxis); } this.DependentAxis.RegisteredListeners.Add(this); } else { if (this.IndependentAxis != null) { this.IndependentAxis.RegisteredListeners.Remove(this); } if (this.DependentAxis != null) { this.DependentAxis.RegisteredListeners.Remove(this); } } AcquireDataPointStyle(); base.OnSeriesHostPropertyChanged(oldValue, newValue); }

One immediate side-effect of this general approach is that we can use a CategoryAxis on the horizontal in addition to a DateTimeAxis because category axes can plot dates as well.

When we acquire an axis we add ourselves to its registered listeners collection.  When the axis changes it will call the AxisInvalidated method on all its registered and we'll get the opportunity to update our data points.  Now we're ready for the main event...

Plotting the Data Points

When the "Refresh" method of the base Series class is called our series is expected to retrieve data from its data source and plot its data points.  Let's create an "UpdateDataPoints" method which will plot a sequence of data points and invoke it in the "Refresh" method. 

public override void Refresh() { if (IndependentAxis != null && DependentAxis != null && PlotArea != null && ItemsSource != null) { this.PlotArea.Children.Clear(); _dataPoints = ItemsSource .Cast<object>() .Select(obj => CreateStockDataPoint(obj)) .ToDictionary(dataPoint => dataPoint.DataContext); foreach (StockDataPoint dataPoint in _dataPoints.Values) { this.PlotArea.Children.Add(dataPoint); } this.Dispatcher.BeginInvoke(() => UpdateDataPoints(_dataPoints.Values)); } }

Notice that instead of updating the data points synchronously we use BeginInvoke.  This will delay the process until after a layout pass has occurred.  This gives Silverlight/WPF the opportunity to apply the template to our data point controls.  This is important as we will need to know the final width of the data points in order to center them along the X axis.  Now let's write the methods that do the important work of positioning the data points. 

private void UpdateDataPoints(IEnumerable<StockDataPoint> dataPoints) { foreach (var dataPoint in dataPoints) { UpdateDataPoint(dataPoint); } } private void UpdateDataPoint(StockDataPoint dataPoint) { // GetPlotAreaCoordinate returns a nullable value because the value // may or may not be present on the axis. var dateCoordinateUnitValue = IndependentAxis.GetPlotAreaCoordinate(dataPoint.Date); if (dateCoordinateUnitValue != null && dateCoordinateUnitValue.Value.Unit == Unit.Pixels) { var nullableHighCoordinateUnitValue = DependentAxis.GetPlotAreaCoordinate(dataPoint.High); var nullableLowCoordinateUnitValue = DependentAxis.GetPlotAreaCoordinate(dataPoint.Low); var nullableCloseCoordinateUnitValue = DependentAxis.GetPlotAreaCoordinate(dataPoint.Close); if (nullableHighCoordinateUnitValue != null && nullableHighCoordinateUnitValue.Value.Unit == Unit.Pixels && nullableLowCoordinateUnitValue != null && nullableLowCoordinateUnitValue.Value.Unit == Unit.Pixels && nullableCloseCoordinateUnitValue != null && nullableCloseCoordinateUnitValue.Value.Unit == Unit.Pixels) { // Subtract all Y coordinates from the height of the plot // area canvas to invert the Y axis. The ensures that // larger values appear higher than smaller ones. var highPixelValue = PlotArea.ActualHeight - nullableHighCoordinateUnitValue.Value.Value; var lowPixelValue = PlotArea.ActualHeight - nullableLowCoordinateUnitValue.Value.Value; var closePixelValue = PlotArea.ActualHeight - nullableCloseCoordinateUnitValue.Value.Value; var dataPointHeight = lowPixelValue - highPixelValue; var closeCoordinate = closePixelValue - highPixelValue; dataPoint.CloseCoordinate = closeCoordinate; dataPoint.Height = dataPointHeight; // center our stock data point on the X point Canvas.SetLeft(dataPoint, dateCoordinateUnitValue.Value.Value - (dataPoint.ActualWidth / 2.0)); Canvas.SetTop(dataPoint, highPixelValue); } }

The GetPlotAreaCoordinate method of the IAxis type is important to understand.  Rather than a double this method returns a nullable UnitValue object.  This value is nullable because a given value may not exist on an axis.  For example negative values cannot exist on a logarithmic axis.  Also certain values may or may not be present on a category axis.

The UnitValue object is composed of a double Value property and an enum Unit property value which can be either Pixels or Degrees.  Although there aren't current any axes in Silverlight Charts that return Degree values this may change in the future to support radial axes.  For the time being our series will only render pixel values.

Now we're ready to implement our AxisInvalidated method.  All we need to do is call UpdateDataPoints!

public void AxisInvalidated(IAxis axis) { //Refresh(); UpdateDataPoints(_dataPoints.Values); }

Given the following XAML...

<charting:Chart x:Name="chart"> <charting:Chart.Axes> <charting:DateTimeAxis Orientation="X" /> <charting:LinearAxis Orientation="Y" /> </charting:Chart.Axes> <local:StockSeries Title="My First Series" /> </charting:Chart>

...we should be able to see our series.  Drumroll please...

emptyseries

Oops.  So what went wrong?  Why didn't our data points get plotted?

In fact our data points were plotted, it's just that they are outside of the visible range of the Y axis.  Notice that the range on the Y axis is 0 to 1.  Turns out that just as an axis needs to communicate with a series when it changes, a series must also communicate information to its axes.  In this case the series must communicate its data range to the axes so that the axes can select an appropriate range.

Helping the Axis Pick a Better Range

In order to get the axis to choose a range that is appropriate for our data we'll implement IRangeProvider.  Let's take a look at how an IRangeProvider interacts with an IRangeAxis.

rangeaxis

We implement the IRangeProvider.GetRange method so the axis can query the series to determine an appropriate range to display.  When a range consumer requests a range it passes itself to the GetRange method.  This allows us to determine which axis is requesting the range, the dependent axis or the independent axis.

public class StockSeries : Series, IRequireGlobalSeriesIndex, IAxisListener, IRangeProvider { //snip... public Range<IComparable> GetRange(IRangeConsumer rangeConsumer) { if (_dataPoints.Any()) { if (rangeConsumer == IndependentAxis) { DateTime minimum = _dataPoints.Values.Select(dataPoint => dataPoint.Date).Min(); DateTime maximum = _dataPoints.Values.Select(dataPoint => dataPoint.Date).Max(); return new Range<IComparable>(minimum, maximum); } else if (rangeConsumer == DependentAxis) { double minimum = _dataPoints.Values.Select(dataPoint => dataPoint.Low).Min(); double maximum = _dataPoints.Values.Select(dataPoint => dataPoint.High).Max(); return new Range<IComparable>(minimum, maximum); } } return new Range<IComparable>(); } }

Now the range axis can determine the best range to use to display our series data.  That's only half of the equation though.  The series also needs to be able to inform the axis that its range has changed.  The best time to do this is in the Refresh method after we load our data.

public override void Refresh() { if (IndependentAxis != null && DependentAxis != null && PlotArea != null && ItemsSource != null) { // snip... this.Dispatcher.BeginInvoke( () => { UpdateDataPoints(_dataPoints.Values); { { IRangeConsumer rangeConsumer = IndependentAxis as IRangeConsumer; if (rangeConsumer != null) { rangeConsumer.RangeChanged(this, GetRange(rangeConsumer)); } } { IRangeConsumer rangeConsumer = DependentAxis as IRangeConsumer; if (rangeConsumer != null) { rangeConsumer.RangeChanged(this, GetRange(rangeConsumer)); } } } }); } }

Now we should be able to see our series in action...

seriesinaction

Hmmm, looks okay.  Our data points have got a nice looking gradient and they've been plotted properly. 

The problem is that our far left and far right data points stretch outside of the chart.  The axis is doing its job and picking a range that encompasses all our data.  The problem is that our data points have a display width in addition to a data value. 

What we'd really like is for the axis to pick a range large enough so that both our data and our visual objects will fit inside of the series.  So how do we do that?

Making Room With ValueMargins

In order to help an axis balance display concerns and data concerns Silverlight Charts provides a ValueMargin object.  A value margin is a data value with a high and low margin value in pixels.  When an axis is provided with value margins in addition to a range it will try and find a range large enough such that a series data and its graphical objects can be displayed inside of the Chart.

The IValueMarginProvider and IValueMarginConsumer interfaces work in a way very similar to the IRangeProvider and IRangeConsumer interfaces:

valuemargins

Let's implement IValueMarginProvider.

public class StockSeries : Series, IRequireGlobalSeriesIndex, IAxisListener, IRangeProvider, IValueMarginProvider { //snip... public IEnumerable<ValueMargin> GetValueMargins(IValueMarginConsumer consumer) { // We only need to worry about value margins on the X axis. // On the Y axis the top and bottom of the data point always // correspond to the value location on the axis. if (consumer == IndependentAxis && consumer is IRangeAxis) { if (_dataPoints.Values.Any()) { // We return the width of only minimum and maximum data // points. The other information is unnecessary. StockDataPoint minimumStockDataPoint = EnumerableFunctions.Min(_dataPoints.Values, dataPoint => dataPoint.Date); StockDataPoint maximumStockDataPoint = EnumerableFunctions.Max(_dataPoints.Values, dataPoint => dataPoint.Date); var halfWidth = minimumStockDataPoint.Width / 2.0; return new[] { new ValueMargin(minimumStockDataPoint.Date, halfWidth, halfWidth), new ValueMargin(maximumStockDataPoint.Date, halfWidth, halfWidth), }; } } return new ValueMargin[] { }; } }

Notice that we're only worried about returning value margins to the independent (X) axis.  That's because the top and bottom of a data point on a stock chart always line up with a value on the axis (lest they be misleading).

To make life easier I've written some helper functions: Min and Max.  These functions work by accepting a function that they apply to each data point.  The function returns a value which is used to compare the data points.  The data point with the largest or smallest computed value is returned.

That wasn't too tough.  Now let's take a look at our series...

finished

Success!  There's only one more thing to do...

Supporting a Category Axis

Remember that we accept any axis on the horizontal, not just a DateTimeAxis.  This means that we can use an instance of the CategoryAxis class for our independent axis.  A category axis is similar to a range axis in that it needs to know what values to include.  As a result our series must inform the category axis which discrete values it intends to plot.  Predictably this is accomplished using the familiar Provider/Consumer model.

dataconsumer

Let's implement IDataProvider...

public class StockSeries : Series, IRequireGlobalSeriesIndex, IAxisListener, IRangeProvider, IValueMarginProvider, IDataProvider { public IEnumerable<object> GetData(IDataConsumer axis) { if (axis == IndependentAxis) { return _dataPoints.Values.Select(dataPoint => dataPoint.Date).Cast<object>(); } else if (axis == DependentAxis) { return _dataPoints.Values .SelectMany(dataPoint => new[] { dataPoint.Close, dataPoint.High, dataPoint.Low }).Cast<object>(); } return new object[] { }; } //snip... }

Just as we must inform the range axes we're using that the range has changed after loading new data we must also inform any axes that implement IDataConsumer (such as axes that implement ICategoryAxis) that our data has changed.  Let's revisit our Refresh method one last time.

public override void Refresh() { // snip... this.Dispatcher.BeginInvoke( () => { UpdateDataPoints(_dataPoints.Values); { { IRangeConsumer rangeConsumer = IndependentAxis as IRangeConsumer; if (rangeConsumer != null) { rangeConsumer.RangeChanged(this, GetRange(rangeConsumer)); } IDataConsumer dataConsumer = IndependentAxis as IDataConsumer; if (dataConsumer != null) { dataConsumer.DataChanged(this, GetData(dataConsumer)); } } { IRangeConsumer rangeConsumer = DependentAxis as IRangeConsumer; if (rangeConsumer != null) { rangeConsumer.RangeChanged(this, GetRange(rangeConsumer)); } IDataConsumer dataConsumer = DependentAxis as IDataConsumer; if (dataConsumer != null) { dataConsumer.DataChanged(this, GetData(dataConsumer)); } } } }); }

Now let's try and use a category axis with our stock series.  To make things interesting let's reverse the order of the dates on the axis (something a DateTimeAxis doesn't support).

<charting:Chart x:Name="chart"> <charting:Chart.Axes> <charting:CategoryAxis Orientation="X" SortOrder="Descending"> <charting:CategoryAxis.AxisLabelStyle> <Style TargetType="charting:AxisLabel"> <Setter Property="StringFormat" Value="{}{0:MM/dd/yy}" /> </Style> </charting:CategoryAxis.AxisLabelStyle> </charting:CategoryAxis> <charting:LinearAxis Orientation="Y" /> </charting:Chart.Axes> <local:StockSeries Title="My First Series" /> </charting:Chart>

Now let's take a look at our Series...

categoryaxis

A New Series For Silverlight Charts

As you can see Silverlight Charts has a very rich set of interfaces you can use to smoothly integrate custom series and axes.  With roughly 450 lines of code we just added the last series missing from Excel to Silverlight Charts.  The ball's in your court.  We're eager to see what the community comes up with.

Next Time: Making our Stock Series dynamic!

Thursday, March 19, 2009

Bug: Explicit Styles Replaced by ISM in Silverlight3 Version of Toolkit

As many of you are aware we've just released a new version of the Silverlight Toolkit.  Rest assured it is a high-quality release that is chock full of great new controls and compelling features.  Unfortunately a bug in the SL3 version of ISM slipped through.  This bug causes ISM to replace styles you've explicitly set.  This issue also affects the themes. 

Given the following XAML...

<Grid theming:ImplicitStyleManager.ApplyMode="Auto"> <Grid.Resources> <Style TargetType="Button"> <Setter Property="Foreground" Value="Green" /> </Style> <Style x:Key="customStyle" TargetType="Button"> <Setter Property="Foreground" Value="Red" /> </Style> </Grid.Resources> <Button Content="This should be red." Style="{StaticResource customStyle}" /> </Grid>

...ISM should ensure that the style explicitly set on the button takes precedence over the other style in the Grid's resource dictionary.  Unfortunately it replaces the explicit style with the implicit one:

shouldbered

The good news is that you can replace the references to the System.Windows.Controls.Theming.*.dlls in the Silverlight 3 Toolkit with the ones in the newest version of the Silveright 2 Toolkit (available here) and everything will work just fine.  You will still get access to all the new themes and bug fixes.  In fact there is no difference between these two assemblies other than this bug.

Sorry for the inconvenience.

Writing Your Own Silverlight Chart Series (Part 1): Making Designers Happy

In the latest release of Silverlight Charts, new features took a backseat to stabilizing our architecture and improving performance.  For the intrepid developer the time is right to take the plunge and start experimenting with your own series.

BewareSilverlight charts is still in preview mode and there may be breaking changes in the future despite our best efforts to avoid them.

In this series of blog posts I will explain how to implement your own series in Silverlight charts.  I will assume you already know how to create Silverlight controls and are familiar with Silverlight/WPF concepts such as the visual tree and templating.  If you aren't comfortable with these concepts I advise you to drop by www.silverlight.net and make your way through the excellent tutorials.

Introducing the Stock Series

One of the series that is available in Excel but is currently missing from Silverlight Charts is the Stock Series.  The stock series displays a high, low, and close value on a given day.  Here's an example from Excel:

blogimage0

Let's see if we can implement the stock series using the extensibility points provided by Silverlight Charts.

"Designers, Designers, Designers!"

One of the biggest benefits of Silverlight is that control developers can decouple a control's model and its visual appearance.  This means that designers can use tools like Expression Blend to give our controls the professional treatment.  Finally design-challenged developers can create glassy, beveled controls that impress our clients, friends, and family. 

Our goal is to take advantage of Silverlight to give the designer as much flexibility as possible to customize our series' appearance.  This post explains how to do this.

Setting up the Project

First create a new Silverlight project and import the System.Windows.Controls.DataVisualization.Toolkit.dll from the newest version of the Siverlight Toolkit.

blogimage1 

Now we're ready to start adding the files we need for our Stock Series.

Creating the StockSeries Control

One of the things to keep in mind about Silverlight Charts is that everything is a control.  In addition to the Chart control which acts as a container, each individual series and the axes they use are also controls.  This is great news for designers because they can completely change the visual appearance of a control by replacing its template.  It's also great news for developers because the Control base class provides a variety of useful methods and properties.

Since our StockSeries is a control we need to go through the same steps required to create any control in Silverlight:

1.  Add the StockSeries Class

using System.Windows.Controls.DataVisualization.Charting; namespace CustomSeries { public class StockSeries : Series { public StockSeries() { this.DefaultStyleKey = typeof(StockSeries); } public override void Refresh() { } } }

Our StockSeries inherits from the abstract Series base class, which in turn inherits from Control.  The only method we need to implement is the "Refresh" method.  When this method is called a series is expected to go back to its data source and render everything from scratch.  We'll get to this method later.

2.  Add a Default Style for the StockSeries Control.

a) Add a folder called "Themes" to the root of the project.

blogimage2

b) Add a new XAML file called "generic.xaml" under the root.

blogimage3

c) Add a resource dictionary with a default style for the StockSeries class to the "generic.xaml" file

<ResourceDictionary xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:local="clr-namespace:CustomSeries"> <Style TargetType="local:StockSeries"> </Style> </ResourceDictionary>

d) Set the build action of "generic.xaml" to "Resource".

blogimage4 

3.  Add a Default Template for the StockSeries class

A control's template contains the objects used to render its visuals.  Since a StockSeries plots objects in two dimensional space it makes sense to include a Canvas in its template.  The StockSeries will use this canvas to arrange its data points. 

In order to add a template to our StockSeries control we must add a setter to the default style in the "generic.xaml" file.

<Style TargetType="local:StockSeries"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:StockSeries"> <Canvas x:Name="PlotArea" /> </ControlTemplate> </Setter.Value> </Setter> </Style>

Now that we've added the canvas to our StockSeries template we will declare that it is a "Template Part" by adding the TemplatePart attribute to the top of our class.

[TemplatePart(Name = "PlotArea", Type = typeof(Canvas))] public class StockSeries : Series
"What's a Template Part?"

A Template Part is an object that must be present for a control to operate properly.  The StockSeries can't do very much without a canvas to plot its points in, right?  Adding the TemplatePart attribute above to our class is our way of saying "Hey designers!  You can replace the template to this class if you want, but make sure to add a Canvas with the name 'PlotArea' in there somewhere."

4.  Grabbing the PlotArea Canvas Object

When our template is applied we'll need to retrieve a reference to the our "PlotArea" Canvas object so that we can use it to lay out our data points.  We'll store this reference in a private property so that we can use it later.

private Canvas PlotArea {get; set;}

We retrieve a reference to the canvas object when our template is applied so we can be sure that it exists.

public override void OnApplyTemplate() { base.OnApplyTemplate(); this.PlotArea = GetTemplateChild("PlotArea") as Canvas; }

Now we've created a StockSeries that can be customized by a designer!  They can change the background color, add borders, embellish it with images, or whatever. 

Customizing the template for the StockSeries is nice, but keep in mind that the series is really just a container.  The real fun is in customizing the appearance of the data points.  In order to give designers this ability we'll create another control for our data points.

Creating the StockDataPoint Control

The StockDataPoint looks something like this:

blogimage5

Let's create a simple default template for our StockDataPoint that a designer can replace later.  The series will set the height of our stock data point and we want to ensure that our template scales gracefully to any size.  Let's start by creating a grid, splitting it into three columns, and inserting a rectangle in the middle.  This gives us the following visual appearance:

blogimage6

Now let's add another grid on top and divide in into four columns. 

blogimage7

We want to insert our line into the second column so that sticks out of the left-hand side of the rectangle.  We would also like it to stretch to the full width of the column it is in.  Unfortunately if we set the line to stretch it will do so vertically and horizontally which means that it will always appear in the center of the column like so:

blogimage8

This won't do because we need to be able to change the vertical position of the line based on the "Close" value.  To accomplish this we nest the line inside of another grid.  By setting the vertical alignment of this grid to top and the vertical alignment of the line to bottom we can control the vertical position of the line by adjusting the height of the inner grid.

blogimage9

Voila.  It's not too pretty but that's not important because a designer can replace it with something really impressive later. 

Let's take a look at the default style XAML in "generic.xaml."

<Style TargetType="local:StockDataPoint"> <Setter Property="Width" Value="10" /> <Setter Property="Background" Value="Beige" /> <Setter Property="Foreground" Value="Black" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:StockDataPoint"> <Grid Width="{TemplateBinding Width}" Height="{TemplateBinding Height}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> </Grid.RowDefinitions> <Rectangle Grid.Column="1" Grid.Row="0" Fill="{TemplateBinding Background}" Stroke="{TemplateBinding Foreground}" /> </Grid> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> </Grid.RowDefinitions> <Grid VerticalAlignment="Top" Grid.Row="0" Grid.Column="1" Height="{TemplateBinding CloseCoordinate}"> <Line VerticalAlignment="Bottom" X1="0" Y1="0" Stretch="Fill" X2="1" Y2="0" Stroke="{TemplateBinding Foreground}" StrokeThickness="3"> </Line> </Grid> </Grid> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>

We expect the StockSeries to size and place the data point such that the top is at the high position and the bottom is at the low position.  The series will also set the "CloseCoordinate" property of our StockDataPoint to the correct value when it updates a data point.  This will control the vertical position of the line.

Now let's create a StockDataPoint class which inherits from Control:

public class StockDataPoint : Control { public StockDataPoint() { this.DefaultStyleKey = typeof(StockDataPoint); } public DateTime Date { get { return (DateTime)GetValue(DateProperty); } set { SetValue(DateProperty, value); } } public static readonly DependencyProperty DateProperty = DependencyProperty.Register( "Date", typeof(DateTime), typeof(StockSeriesDataPoint), new PropertyMetadata(default(DateTime))); public double High { get { return (double)GetValue(HighProperty); } set { SetValue(HighProperty, value); } } public static readonly DependencyProperty HighProperty = DependencyProperty.Register( "High", typeof(double), typeof(StockSeriesDataPoint), new PropertyMetadata(0.0)); public double Low { get { return (double)GetValue(LowProperty); } set { SetValue(LowProperty, value); } } public static readonly DependencyProperty LowProperty = DependencyProperty.Register( "Low", typeof(double), typeof(StockSeriesDataPoint), new PropertyMetadata(0.0)); public double Close { get { return (double)GetValue(CloseProperty); } set { SetValue(CloseProperty, value); } } public static readonly DependencyProperty CloseProperty = DependencyProperty.Register( "Close", typeof(double), typeof(StockSeriesDataPoint), new PropertyMetadata(0.0)); public double CloseCoordinate { get { return (double)GetValue(CloseCoordinateProperty); } set { SetValue(CloseCoordinateProperty, value); } } public static readonly DependencyProperty CloseCoordinateProperty = DependencyProperty.Register( "CloseCoordinate", typeof(double), typeof(StockSeriesDataPoint), new PropertyMetadata(0.0)); }

The "Date" property stores the date on which the data in the data point was collected.  This value will be used to determine where the data point appears on the X axis.  It is sometimes called the independent value.

The "High", "Low", and "Close" values are called dependent values, because they depend on the date on which they are taken. 

The "CloseCoordinate" property is the vertical coordinate relative to the top of the data point where the line in the data point located.  Our StockSeries class will set this value to the location of the Close property as reported by the Y axis.

A Designers Dream

Now we've empowered designers by giving them ability to completely change the appearance of our series and data point controls using Microsoft Expression Blend.  If you're lucky enough to have an in-house designer they could even get started right away while you finished implementing the series.

Coming in Part 2:  We implement the StockSeries class and starting charting some data!

Coming in Part 3: We make our StockSeries dynamic, responding to changes in the underlying data source with smooth animations.

Saturday, February 21, 2009

Flex's Achilles Heel

When we evaluate rich Internet platforms we tend to focus on the tip of the iceberg, even more so than in conventional software.  After all rich Internet applications are about graphics, animation, and other multimedia right?  By these measures it it is hard to deny that Silverlight and Flex are very evenly matched - for now.  However Flex developers are beginning to notice that Flex lacks a feature that will become increasingly important: Threading

Retrofitting Flex to make it a suitable platform for multithreaded programming is a very hard problem because of its heritage as a vector animation platform .  Assuming that Adobe is willing to go through this painful step the job of making Flex a hospitable platform for multithreaded programing will be far from complete.  Why?

Making it  possible to do threading is a platform problem. Making it easy is a language problem.

Microsoft made a key decision to develop their own programming languages and they've evolved them in ways that make them extremely well-suited to multi-threaded programming.  The results are pretty spectacular.  Here are two examples (coming soon to Silverlight):

1.  Parallel Linq

Parallel Linq is Linq provider that will execute a query in parallel.  In some cases parallelizing a query can be as easy as adding "AsParallel" to it like so:

var q = from x in list.AsParallel() where x < 3300 select x;

It just doesn't get much easier than this.

It is interesting to note that it is the programming language that makes this possible.  Parallel Linq relies on C# and VB.net's ability to convert Linq expressions into data.  The Parallel Linq library then analyzes the expression at run-time and determines the best way of executing it.

2.  F#

F# is an extended version of OCaml.  Microsoft research added a feature to the language which makes it incredibly easy to write asynchronous applications.  This is important given that Rich Internet applications spend a whole lot of time waiting on IO.

Let's take a look at an example of F# function which synchronously retrieves the HTML of a web page:

let getHtml(url:string) = let req = WebRequest.Create(url) use resp = req.GetResponseAsync() use stream = resp.GetResponseStream() use reader = new StreamReader(stream) reader.ReadToEnd()

You can think of the "let" key word is equivalent to C#'s "var" and the "use" keyword as equivalent to C#'s "using."  Now let's take a look at what it takes to make this function asynchronous.

let getHtml(url:string) = async { let req = WebRequest.Create(url) use! resp = req.GetResponseAsync() use stream = resp.GetResponseStream() use reader = new StreamReader(stream) return reader.ReadToEnd() }

Can you spot the differences?  The obvious one is the "async."  Less obvious is the "!" in from of the use keyword.  Whenever a "!" is placed in front of a keyword inside of an "async", the operation is executed on another thread and the rest of the computation is suspended until the operation finishes.  When the operation finishes control jumps from one thread to another and the rest of the computation is executed.  No threading code necessary.

Once again it's the language that makes this possible.  How's it done?

In certain contexts F# allows you to overload language keywords and have a custom function execute them instead of the programming language.  This makes it possible to do very complex types of transformations.  In this case the execution of "use!" is handled by a Use method on an instance of the AsyncBuilder class.  It turns out that"async" isn't a keyword, it is actually just an instance of type AsyncBuilder!

This is somewhat similar to implementing the query pattern in C#.The principle difference is that F# supports many more keywords.  To find out more check out Don Syme's talk on asynchronous workflows on Channel 9.

A Blessing in Disguise

A few months ago there was quite a bit of controversy when efforts towards JavaScript standardization fell apart.  In retrospect I believe that this is the best thing that could've happened to Adobe.  They are now free to extend JavaScript to make it a more suitable language for multi-threaded programming.

The real question is: Will they? 

I've been disappointed by the evolution of JavaScript.  Many of the features recently added seem to be inspired by Python, which is not surprising given the similarities between the two languages.  Although Python is a great language for doing dynamic, imperative development it is certainly not my first choice for doing functional programming, and by extension multi-threaded programming.  Unfortunately Javascript seems to have inherited some of Python's mistakes.  For example Javascript adopted Python's iterator model which is inferior to that of .NET's Enumerable model but that's another blog post entirely. :-)

In the interest of making multi-threaded programming (and a host of other problems in different domains) easier I'd add two features to ActionScript:

1.  Code-to-data transformations (which makes Parallel Linq possible)

2. Monad comprehensions (which F# uses to transform synchronous code into asynchronous code)

If Flex stays a one language platform and Actionscript continues to evolve in Python's direction instead of Haskell's direction I see a grim future ahead for the platform.  I hope I'm wrong because competition is a healthy thing :-).

About Me

My photo
I'm a software developer who started programming at age 16 and never saw any reason to stop. I'm working on the Presentation Platform Controls team at Microsoft. My primary interests are functional programming, and Rich Internet Applications.