Weight Tracker (Charts & Graphs)

Are you watching your weight? Weight Tracker enables you to weigh in as often as you like and provides several ways to visualize your progress. It is a pivot-based app with three pivot items:

  • list—The raw list of weigh-ins, with support for adding and deleting them. The weight trend between consecutive items is shown with up/down arrows.
  • graph—Plots your weight over time on a line chart, along with any number of goal weights that you can define on this app’s settings page. You can view the entire range of data, or narrow it down to a custom range.
  • progress—Summarizes your weight loss progress compared to your final weight loss goal. This dashboard view makes use of pie charts.

Although this is a pivot-based app, the purpose of this chapter is to explain how to include charts and graphs in your apps.

Charts & Graphs

The Silverlight team has created a very rich set of opensource controls for creating seven types of interactive charts: bar charts, line charts, pie charts, bubble charts, and more. These were created several years ago for desktop Silverlight and ship in the Silverlight Toolkit. At the time of this writing, these controls are not included in the Silverlight for Windows Phone Toolkit, so you must download them separately. Where you should download them, however, has been a source of confusion.

Currently, the best place to get the chart controls is directly from David Anson’s blog. David is the Microsoft developer responsible for these controls, and he was also kind enough to review this chapter. He provides “development releases” of the controls with the goal of exposing the latest functionality and bug fixes at a quicker pace than the official toolkit releases from Microsoft. His releases come with full source code as well as the compiled DLLs. The same source code can be compiled to work with Windows Phone, multiple versions of desktop Silverlight, as well as Windows Presentation Foundation (WPF).

If you’re uncomfortable with using this unofficial release, you can instead download the chart controls from the Silverlight Toolkit (the one intended for desktop computers). However, to make matters more confusing, you must not use the latest version of the Silverlight Toolkit. You can download the phonecompatible version of the Silverlight Toolkit (the November 2009 release for Silverlight 3) at http://bit.ly/sl3toolkit. The downside to using this version is that it is missing performance improvements and stacked series support (described later) that are present in David’s release.

Whichever route you take, you need to reference System.Windows.Controls. DataVisualization.Toolkit.dll, the assembly with all the chart-specific functionality. In David’s release, use the one under BinariesSilverlight3 inside the .zip file. If you install the November 2009 Silverlight Toolkit, you can find it in %ProgramFiles%Microsoft SDKs Silverlightv3.0ToolkitNov09Bin.

The chart controls from the Silverlight 4 Toolkit do not work on Windows Phone at the time of this writing!

Although the latest version of the Silverlight Toolkit contains the chart controls described in this chapter, you cannot currently use them. Windows Phone OS 7.0 ships with a custom version of Silverlight that is based on Silverlight 3, despite having a few additional features that were introduced in Silverlight 4. As a result,most Silverlight 2 or Silverlight 3 code can work on Windows Phone, but code written for Silverlight 4 may not. The Silverlight 4 version of the chart controls requires features that are not present on Windows Phone’s version of Silverlight, so attempting to use this version fails at run-time in hard-to-diagnose ways. Such incompatibilities between desktop Silverlight and Silverlight for Windows Phone should hopefully fade away in the future.

Regular Series

The primary element that enables charts is Chart from the System.Windows.Controls. DataVisualization.Charting namespace. To get different types of charts, you place different types of series in the Chart element. There are fifteen concrete series: seven regular ones and eight stacked ones. All of the regular ones are demonstrated in Table 29.1.

Each is shown with its default rendering under the light theme, which is decent (despite the use of phone-unfriendly gradients). The default rendering is not acceptable under the dark theme, however, as the legend text becomes white but the box’s background remains a light gradient.

From code-behind, each chart in Table 29.1 is given the same data context of three simple (X,Y) points as follows:

[code]

this.Chart.DataContext =
new Point[] { new Point(0, 2), new Point(1, 10), new Point(2, 6) };

[/code]

This way, each series can bind its ItemsSource property to this array with simple {Binding} syntax then specify each X property for its independent axis (the X axis in most chart types) and each Y property for its dependent axis (the Y axis in most chart types). You can directly manipulate a series without using data binding, but doing so from codebehind is a little awkward because you can’t access a series element by name.

TABLE 29.1 The Default Light-Theme Rendering of the Seven Nonstacked Chart Types, All with the Same Three-Point Data Source

Line Chart
Line Chart

Line Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:LineSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Area Chart
Area Chart

Area Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:AreaSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Bar Chart
Bar Chart

Bar Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:BarSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Column Chart
Column Chart

Column Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:ColumnSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Scatter Chart
Scatter Chart

Scatter Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:ScatterSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Bubble Chart
Bubble Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:BubbleSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Pie Chart
Pie Chart

Pie Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:PieSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

A single chart can contain multiple overlapping series, as with the following chart:

[code]

<charting:Chart x:Name=”Chart”>
<!– Data set #1 –>
<charting:AreaSeries IndependentValuePath=”X”
DependentValuePath=”Y”/>
<!– Data set #2 –>
<charting:AreaSeries IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Assigning each area series the following data produces the result in Figure 29.1 (under the light theme):

[code]

(this.Chart.Series[0] as AreaSeries).ItemsSource =
new Point[] { new Point(0, 2), new Point(1, 10), new Point(2, 6) };
(this.Chart.Series[1] as AreaSeries).ItemsSource =
new Point[] { new Point(0, 8), new Point(1, 1), new Point(2, 9) };

[/code]

FIGURE 29.1 Using two area series in the same chart.
FIGURE 29.1 Using two area series in the same chart.

Each series can even be a different type. Although it’s nonsensical in this case, Figure 29.2 combines all seven series from Table 29.1 into the same chart.

FIGURE 29.2 Using all seven types of nonstacked series in the same chart.
FIGURE 29.2 Using all seven types of nonstacked series in the same chart.

Stacked Series

In the Development Release 4 download of the chart controls, the first four series in Table 29.1 have two stacked counterparts: one that stacks the absolute values, and one that stacks relative values that always add up to 100%. Table 29.2 demonstrates these eight stacked series. They support multiple child series much like in Figure 29.1 but, as the name implies, each child get stacked rather than overlapped.

Each chart in Table 29.2 uses the following data context from code-behind:

[code]

this.Chart.DataContext = new Point[][] {
new Point[] { new Point(0, 2), new Point(1, 10), new Point(2, 6) }, // Series 1
new Point[] { new Point(0, 4), new Point(1, 5), new Point(2, 12) }, // Series 2
new Point[] { new Point(0, 8), new Point(1, 2.5), new Point(2, 3) } // Series 3
};

[/code]

A stacked series contains any number of series definitions. With this data context, the XAML snippets in Table 29.2 can bind three distinct series definitions to each Point[] element in the outermost array. When bound to an individual array of points, the assignments of IndependentValuePath and DependentValuePath work just like in the previous table.

TABLE 29.2 The Default Light-Theme Rendering of the Eight Stacked Chart Types, All with the Same Set of Three-Point Data Sources

Stacked Line Chart
Stacked Line Chart

Stacked Line Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:StackedLineSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:StackedLineSeries>
</charting:Chart>

[/code]

Stacked 100% Line Chart
Stacked 100% Line Chart

Stacked 100% Line Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:Stacked100LineSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Stacked100LineSeries>
</charting:Chart>

[/code]

Stacked Area Chart
Stacked Area Chart

Stacked Area Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:StackedAreaSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:StackedAreaSeries>
</charting:Chart>

[/code]

Stacked 100% Area Chart
Stacked 100% Area Chart

Stacked 100% Area Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:Stacked100AreaSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Stacked100AreaSeries>
</charting:Chart>

[/code]

Stacked Bar Chart
Stacked Bar Chart

Stacked Bar Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:StackedBarSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:StackedBarSeries>
</charting:Chart>

[/code]

Stacked 100% Bar Chart
Stacked 100% Bar Chart

Stacked 100% Bar Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:Stacked100BarSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Stacked100BarSeries>
</charting:Chart>

[/code]

Stacked Column Chart
Stacked Column Chart

Stacked Column Chart

[code]

<charting:StackedColumnSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:StackedColumnSeries>
</charting:Chart>

[/code]

Stacked 100% Column Chart
Stacked 100% Column Chart

Stacked 100% Column Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:Stacked100ColumnSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Stacked100ColumnSeries>
</charting:Chart>

[/code]

As you can see, getting reasonablelooking charts to render is fairly easy. The chart scales its axes appropriately, and even staggers axis labels if necessary (seen in Table 29.1). It applies different colors to the data automatically and has a lot of other intelligence not shown here. Chart, as well as each series object, also exposes numerous properties for formatting the chart area, legend, title, axes, data points, and more. The amount of possible customization is vast, although figuring out how to get your desired customizations (even simple-sounding things like hiding the legend or changing data colors) can be quite tricky. In the next section, Weight Tracker demonstrates how to perform several customizations on line charts and pie charts.

The Main Page

Besides the application bar and status bar, the main page contains the three-item pivot. These three items, described at the beginning of this chapter, are shown in Figure 29.3.

FIGURE 29.3 The three pivot items: list, graph, and progress.
FIGURE 29.3 The three pivot items: list, graph, and progress.

The User Interface

Listing 29.1 contains the XAML for the main page.

LISTING 29.1 MainPage.xaml—The User Interface for Weight Tracker’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:controls=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
xmlns:charting=”clr-namespace:
➥System.Windows.Controls.DataVisualization.Charting;
➥assembly=System.Windows.Controls.DataVisualization.Toolkit”
xmlns:chartingprimitives=”clr-namespace:
➥System.Windows.Controls.DataVisualization.Charting.Primitives;
➥assembly=System.Windows.Controls.DataVisualization.Toolkit”
xmlns:datavis=”clr-namespace:System.Windows.Controls.DataVisualization;
➥assembly=System.Windows.Controls.DataVisualization.Toolkit”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<!– The application bar, with two buttons and a menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings”
IconUri=”/Shared/Images/appbar.settings.png”
Click=”SettingsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<phone:PhoneApplicationPage.Resources>
<!– A custom style for Chart. It’s the default style from the toolkit, but
with no title/legend, reduced margins/padding, and fewer borders. –>
<Style x:Key=”ChartStyle” TargetType=”charting:Chart”>

</Style>
</phone:PhoneApplicationPage.Resources>
<!– The pivot is the root element –>
<controls:Pivot x:Name=”Pivot” Title=”WEIGHT TRACKER”
SelectionChanged=”Pivot_SelectionChanged”>
<!– Item 1: list –>
<controls:PivotItem Header=”list”>
<Grid Margin=”0,-24,0,0”>
<!– A translucent foreground-colored scale image behind the list –>
<Rectangle Opacity=”.2” Margin=”0,0,0,12” VerticalAlignment=”Bottom”
Width=”180” Height=”240” Fill=”{StaticResource PhoneForegroundBrush}”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/scale.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<!– The editable list, implemented as a user control –>
<local:WeighInEditableList x:Name=”EditableList” Collection=”{Binding}”/>
</Grid>
</controls:PivotItem>
<!– Item 2: graph –>
<controls:PivotItem Header=”graph”>
<Grid Margin=”0,-24,0,0”>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition/>
</Grid.RowDefinitions>
<!– The start date / end date pickers –>
<TextBlock Text=”Start date” Margin=”11,0,0,-5”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<toolkit:DatePicker x:Name=”StartGraphDatePicker” Grid.Row=”1”
ValueChanged=”GraphDatePicker_ValueChanged” local:Tilt.IsEnabled=”True”/>
<TextBlock Text=”End date” Grid.Column=”1” Margin=”11,0,0,-5”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<toolkit:DatePicker x:Name=”EndGraphDatePicker” Grid.Row=”1”
Grid.Column=”1” ValueChanged=”GraphDatePicker_ValueChanged”
local:Tilt.IsEnabled=”True”/>
<!– The line + scatter chart –>
<charting:Chart x:Name=”Chart” Style=”{StaticResource ChartStyle}”
Grid.ColumnSpan=”2” Grid.Row=”2”>
<!– Change the background of the plot area –>
<charting:Chart.PlotAreaStyle>
<Style TargetType=”Grid”>
<Setter Property=”Background”
Value=”{StaticResource PhoneBackgroundBrush}”/>
</Style>
</charting:Chart.PlotAreaStyle>
<!– An explicit X axis, so it can be customized –>
<charting:Chart.Axes>
<charting:DateTimeAxis ShowGridLines=”True” Orientation=”X”>
<charting:DateTimeAxis.AxisLabelStyle>
<Style TargetType=”charting:DateTimeAxisLabel”>
<Setter Property=”StringFormat” Value=”{}{0:M/d}” />
</Style>
</charting:DateTimeAxis.AxisLabelStyle>
</charting:DateTimeAxis>
</charting:Chart.Axes>
<!– The line series, for weigh-in data –>
<charting:LineSeries ItemsSource=”{Binding}”
IndependentValuePath=”Date”
DependentValuePath=”Weight”>
<!– A custom style for the line –>
<charting:LineSeries.PolylineStyle>
<Style TargetType=”Polyline”>
<Setter Property=”StrokeThickness” Value=”3”/>
</Style>
</charting:LineSeries.PolylineStyle>
<!– A custom style for data points –>
<charting:LineSeries.DataPointStyle>
<!– A copy of the default style, but with a new background,
border, width, height, and removal of the radial gradient –>
<Style TargetType=”charting:LineDataPoint”>
<Setter Property=”Background”
Value=”{StaticResource PhoneAccentBrush}”/>
<Setter Property=”BorderBrush”
Value=”{StaticResource PhoneForegroundColor}”/>
<Setter Property=”Width” Value=”10” />
<Setter Property=”Height” Value=”10” />

</Style>
</charting:LineSeries.DataPointStyle>
</charting:LineSeries>
<!– The scatter series, for goal weights –>
<charting:ScatterSeries IndependentValuePath=”Date”
DependentValuePath=”Weight”>
<!– A custom style for data points –>
<charting:ScatterSeries.DataPointStyle>
<Style TargetType=”charting:ScatterDataPoint”>
<!– Width and Height need to be set for the point
to render properly –>
<Setter Property=”Width” Value=”26”/>
<Setter Property=”Height” Value=”25”/>
<Setter Property=”Template”>
<Setter.Value>
<ControlTemplate TargetType=”charting:ScatterDataPoint”>
<!– A star image –>
<Rectangle Fill=”#FFC700”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/star.png”/>
</Rectangle.OpacityMask>
</Rectangle>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</charting:ScatterSeries.DataPointStyle>
</charting:ScatterSeries>
</charting:Chart>
</Grid>
</controls:PivotItem>
<!– Item 3: progress –>
<controls:PivotItem Header=”progress”>
<Grid>
<TextBlock x:Name=”NoDataTextBlock” Margin=”22,17,0,0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”>
You don’t have any goals set.<LineBreak/>Set one on the settings page.
</TextBlock>
<ScrollViewer x:Name=”DataDashboard” Margin=”0,-24,0,0”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!– The first pie chart –>
<charting:Chart x:Name=”WeightPieChart” Grid.Row=”1”
Style=”{StaticResource ChartStyle}”>
<!– Change the background of the plot area –>
<charting:Chart.PlotAreaStyle>
<Style TargetType=”Grid”>
<Setter Property=”Background”
Value=”{StaticResource PhoneBackgroundBrush}”/>
</Style>
</charting:Chart.PlotAreaStyle>
<!– The pie series –>
<charting:PieSeries ItemsSource=”{Binding}”>
<!– A custom two-color palette: the accent color for the first
piece, and the background color for the second piece –>
<charting:PieSeries.Palette>
<datavis:ResourceDictionaryCollection>
<!– Style for the first data point –>
<ResourceDictionary>
<Style x:Key=”DataPointStyle” TargetType=”Control”>
<Setter Property=”BorderBrush”
Value=”{StaticResource PhoneBackgroundBrush}”/>
<Setter Property=”Background”
Value=”{StaticResource PhoneAccentBrush}”/>
</Style>
</ResourceDictionary>
<!– Style for the second data point –>
<ResourceDictionary>
<Style x:Key=”DataPointStyle” TargetType=”Control”>
<Setter Property=”BorderBrush”
Value=”{StaticResource PhoneBackgroundBrush}”/>
<Setter Property=”Background”
Value=”{StaticResource PhoneBackgroundBrush}”/>
</Style>
</ResourceDictionary>
</datavis:ResourceDictionaryCollection>
</charting:PieSeries.Palette>
</charting:PieSeries>
</charting:Chart>
<!– The second pie chart –>
<charting:Chart x:Name=”TimePieChart” Grid.Row=”1” Grid.Column=”1”
Style=”{StaticResource ChartStyle}”>
… The contents are identical to the preceding pie chart …
</charting:Chart>
<!– Lots of text blocks –>
<TextBlock x:Name=”WeightLossPercentTextBlock” Text=”Weight loss”
Margin=”4” HorizontalAlignment=”Center”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBlock x:Name=”TimePercentTextBlock” Grid.Column=”1” Text=”Time”
Margin=”4” HorizontalAlignment=”Center”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBlock x:Name=”SummaryTextBlock” Grid.Row=”2” Grid.ColumnSpan=”2”
HorizontalAlignment=”Center” VerticalAlignment=”Top”
FontFamily=”Segoe WP Black” Margin=”0,0,0,8”
Style=”{StaticResource PhoneTextLargeStyle}”/>
<TextBlock Grid.Row=”3” Text=”Weight lost”
HorizontalAlignment=”Center” Style=”{StaticResource LabelStyle}”/>
<TextBlock x:Name=”WeightLostTextBlock” Grid.Row=”4”
HorizontalAlignment=”Center”
Style=”{StaticResource PhoneTextExtraLargeStyle}”/>
<TextBlock Grid.Row=”5” Text=”Weight to lose”
HorizontalAlignment=”Center” Style=”{StaticResource LabelStyle}”/>
<TextBlock x:Name=”WeightRemainingTextBlock” Grid.Row=”6”
HorizontalAlignment=”Center” Margin=”0,0,0,24” FontSize=”60”/>
<TextBlock Grid.Row=”3” Grid.Column=”1” Text=”Days elapsed”
HorizontalAlignment=”Center” Style=”{StaticResource LabelStyle}”/>
<TextBlock x:Name=”DaysElapsedTextBlock” Grid.Row=”4” Grid.Column=”1”
HorizontalAlignment=”Center”
Style=”{StaticResource PhoneTextExtraLargeStyle}”/>
<TextBlock Grid.Row=”5” Grid.Column=”1” Text=”Days remaining”
HorizontalAlignment=”Center”
Style=”{StaticResource LabelStyle}”/>
<TextBlock x:Name=”DaysRemainingTextBlock” Grid.Row=”6”
Grid.Column=”1” HorizontalAlignment=”Center”
Margin=”0,0,0,24” FontSize=”60”/>
</Grid>
</ScrollViewer>
</Grid>
</controls:PivotItem>
</controls:Pivot>
</phone:PhoneApplicationPage>

[/code]

  • The namespace for Chart and the series elements is included with a charting prefix. The chartingprimitives prefix is used inside this listing’s copy of Chart’s default style, and the datavis prefix is used for a ResourceDictionaryCollection type required by this listing’s pie chart customizations.
  • The custom style used by all three charts on this page (ChartStyle) is a copy of the Chart control’s default style with a few tweaks applied: removal of the title and legend, reduced margins and padding, and fewer borders.
  • Because each pivot item has a lot of information to fit on the page, the content in each one is given a –24 top margin to occupy some of the otherwise-blank space between the header and the content. You should be very careful when making changes such as this that violate design guidelines, as you risk having your app look “wrong” compared to other Windows Phone apps.
  • The editable list of weigh-ins in the first pivot item is implemented as a user control called WeighInEditableList. This control contains a list box bound to the value of its Collection property and includes the three controls displayed above the list box: the “Weight” text box, the “Date” text box, and the add button. The list box items also use a context menu to enable one-by-one deleting of items. This is packaged as a user control because it is also used by the settings page to enable viewing and editing of the list of goal weights. The source code for this user control is not shown in this chapter, but it is included in the Visual Studio project available to you.
  • Each weigh-in is represented by a WeighIn class that contains three read/write properties— Weight, Date, and Delta—as well as a few convenient readonly properties used by WeighInEditableList’s list box data template. The collection used by WeighInEditableList is a custom WeighInCollection class that derives from ObservableCollection<WeighIn>. It handles automatic sorting of the items in reverse chronological order, and it calculates the relevant Delta values (used to show the up/down arrows) whenever an item is added or removed.
  • The second pivot item contains a chart with two series: one for the list of weigh-ins, and one for the list of goal weights. The weigh-ins are shown with a line series, whereas the goal weights are shown with a scatter series (which looks just like a line series but with no lines connecting each dot). The items source for the scatter series is set in code-behind.
  • In addition to the custom ChartStyle style applied to the chart, several customizations are made to the chart itself as well as each series:
    • The chart is given a background that matches the page’s background. (Alternatively, this could have been done inside ChartStyle.)
    • The chart is given an explicit X axis, so three customizations can be made: showing vertical grid lines, changing the string format of each date so the year is not shown, and restricting the range of X values (done in code-behind). Notice that the type of the axis is DateTimeAxis, which includes special functionality for displaying DateTime values. Any .NET formatting string can be used for the StringFormat value.
    • The style of the lines and dots used by the line series have been changed in a number of ways. The line has been made thicker, the color of both has been made to match the current theme’s accent color, the radial gradient has been removed from each dot, and more.
    • The style of each data point in the scatter series has been changed to look like a star! This is done with a starimage opacity mask on a yellow rectangle.
  • The third and final pivot item contains two pie charts among a number of text blocks. The XAML for each pie chart looks identical, because the only difference is their data that is set in code-behind. The pie charts give themselves a page-colored background, just as the line and scatter chart does, and customizes the fills of its pie slices.

    When a chart chooses colors for data points, whether in the same series or across different series, it loops through a collection of styles assigned to its Palette property. This collection can be changed, and each series, which also has a Palette property, can have its own collection. Chart’s default style assigns 15 brushes to Palette (starting with blue, red, and green gradients, as seen in Tables 29.1 and 29.2, and Figure 29.1). Because the pie charts in this listing only ever contain two slices, and because we only want the first slice to be visible, these pie series use a two-brush palette, with the second brush matching the background.

To demonstrate their impact, Figure 29.4 displays this app’s line and scatter chart with various amounts of customizations removed. The second image shows the result of simply removing Style=”{StaticResource ChartStyle}” from the chart. The first image is the result of whittling the chart down to the following simplest version that still works without changing any additional code:

[code]

<!– The line + scatter chart –>
<charting:Chart x:Name=”Chart”
Grid.ColumnSpan=”2” Grid.Row=”2”>
<!– An explicit X axis, so its range can be restricted from code-behind –>
<charting:Chart.Axes>
<charting:DateTimeAxis Orientation=”X”/>
</charting:Chart.Axes>
<!– The line series, for weigh-in data –>
<charting:LineSeries ItemsSource=”{Binding}”
IndependentValuePath=”Date”
DependentValuePath=”Weight”/>
<!– The scatter series, for goal weights –>
<charting:ScatterSeries IndependentValuePath=”Date”
DependentValuePath=”Weight”/>
</charting:Chart>

[/code]

FIGURE 29.4 Some of the chart customizations are done by setting exposed properties, whereas others are done by changing the control template inside ChartStyle.
FIGURE 29.4 Some of the chart customizations are done by setting exposed properties, whereas others are done by changing the control template inside ChartStyle.

The Code-Behind

Listing 29.2 contains the code-behind for the main page, which makes the most of observable collections to keep all three pivot items up-to-date. This app manages two observable collections—one for the list of weigh-ins, and one for the list of goal weights. They are defined as follows in Settings.cs, along with a setting for remembering the main chart’s selected start date:

[code]

public static class Settings
{
public static readonly Setting<DateTime?> GraphStartDate =
new Setting<DateTime?>(“GraphStartDate”, null);
public static readonly Setting<WeighInCollection> WeighInList =
new Setting<WeighInCollection>(“WeighInList”, new WeighInCollection());
public static readonly Setting<WeighInCollection> GoalList =
new Setting<WeighInCollection>(“GoalList”, new WeighInCollection());
}

[/code]

LISTING 29.2 MainPage.xaml.cs—The Code-Behind for Weight Tracker’s Main Page

[code]

using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.DataVisualization.Charting;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
// Update the start/end dates of the main chart (done here so it
// doesn’t interfere with navigating back from the date pickers)
if (Settings.WeighInList.Value.Count + Settings.GoalList.Value.Count > 0)
{
// Restore the start date to the previously-used value,
// otherwise use the earliest date with data
if (Settings.GraphStartDate.Value != null)
this.StartGraphDatePicker.Value = Settings.GraphStartDate.Value;
else
this.StartGraphDatePicker.Value = GetEarliestDataPoint().Date;
// Don’t restore any customizations to the end date. Set it to the
// date of the last weigh-in or goal
this.EndGraphDatePicker.Value = GetLatestDataPoint().Date;
}
// Update the progress dashboard (the third pivot item)
UpdateProgress();
// Respond to changes in the two observable collections
Settings.WeighInList.Value.CollectionChanged += CollectionChanged;
Settings.GoalList.Value.CollectionChanged += CollectionChanged;
// Set the weigh-in list as the default data source for everything
this.DataContext = Settings.WeighInList.Value;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Fill out the text box with the most recent weight
if (this.EditableList.Text.Length == 0 &&
Settings.WeighInList.Value.Count > 0)
this.EditableList.Text = Settings.WeighInList.Value[0].Weight.ToString();
// When reactivated, restore the pivot selection
if (this.State.ContainsKey(“PivotSelectedIndex”))
this.Pivot.SelectedIndex = (int)this.State[“PivotSelectedIndex”];
// Set the goal list as the source for the scatter series on the main chart
(this.Chart.Series[1] as DataPointSeries).ItemsSource =
Settings.GoalList.Value;
}
void Pivot_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Remember the current pivot item for reactivation only
this.State[“PivotSelectedIndex”] = this.Pivot.SelectedIndex;
}
// Returns the earliest data point from either of the two lists
WeighIn GetEarliestDataPoint()
{
WeighIn earliest = null;
// Both lists are sorted in reverse chronological order
if (Settings.WeighInList.Value.Count > 0)
earliest = Settings.WeighInList.Value[Settings.WeighInList.Value.Count-1];
if (Settings.GoalList.Value.Count > 0)
{
WeighIn earliestGoal =
Settings.GoalList.Value[Settings.GoalList.Value.Count-1];
if (earliest == null || earliestGoal.Date < earliest.Date)
earliest = earliestGoal;
}
return earliest;
}
// Returns the latest data point from either of the two lists
WeighIn GetLatestDataPoint()
{
WeighIn latest = null;
// Both lists are sorted in reverse chronological order
if (Settings.WeighInList.Value.Count > 0)
latest = Settings.WeighInList.Value[0];
if (Settings.GoalList.Value.Count > 0)
{
WeighIn latestGoal = Settings.GoalList.Value[0];
if (latest == null || latestGoal.Date > latest.Date)
latest = latestGoal;
}
return latest;
}
// Called when either of the two observable collections changes
void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
WeighIn earliestDataPoint = GetEarliestDataPoint();
WeighIn latestDataPoint = GetLatestDataPoint();
// Potentially update the range of the main chart
if (earliestDataPoint != null && latestDataPoint != null)
{
// Check if a new earliest date was added. Only update start date if so.
if ((sender == Settings.WeighInList.Value &&
e.NewStartingIndex == Settings.WeighInList.Value.Count-1) ||
(sender == Settings.GoalList.Value &&
e.NewStartingIndex == Settings.GoalList.Value.Count-1))
this.StartGraphDatePicker.Value = earliestDataPoint.Date;
// Ensure the end date matches the end of the data
this.EndGraphDatePicker.Value = latestDataPoint.Date;
}
// Update the progress dashboard (the third pivot item)
UpdateProgress();
}
void UpdateProgress()
{
// Refresh all the data on this pivot item
this.NoDataTextBlock.Visibility = Settings.GoalList.Value.Count > 0 ?
Visibility.Collapsed : Visibility.Visible;
this.DataDashboard.Visibility = Settings.GoalList.Value.Count > 0 ?
Visibility.Visible : Visibility.Collapsed;
if (Settings.GoalList.Value.Count == 0)
return;
WeighIn earliestGoal =
Settings.GoalList.Value[Settings.GoalList.Value.Count-1];
WeighIn latestGoal = Settings.GoalList.Value[0];
int daysRemaining = 0;
double weightRemaining = latestGoal.Weight;
int daysElapsed = 0;
double weightLost = 0;
if (Settings.WeighInList.Value.Count > 0)
{
WeighIn earliestWeighIn =
Settings.WeighInList.Value[Settings.WeighInList.Value.Count-1];
WeighIn latestWeighIn = Settings.WeighInList.Value[0];
daysRemaining = (latestGoal.Date – latestWeighIn.Date).Days;
daysElapsed = (latestWeighIn.Date – earliestWeighIn.Date).Days;
weightLost = earliestWeighIn.Weight – latestWeighIn.Weight;
weightRemaining = latestWeighIn.Weight – latestGoal.Weight;
}
double weightPercent = weightLost / (weightRemaining + weightLost);
double timePercent = (double)daysElapsed / (daysRemaining + daysElapsed);
// Update text
this.WeightLossPercentTextBlock.Text =
“Weight loss: “ + (weightPercent * 100).ToString(“N0”) + “%”;
this.TimePercentTextBlock.Text =
“Time: “ + (timePercent * 100).ToString(“N0”) + “%”;
this.DaysElapsedTextBlock.Text = daysElapsed.ToString(“N0”);
this.DaysRemainingTextBlock.Text = daysRemaining.ToString(“N0”);
this.WeightLostTextBlock.Text = weightLost.ToString(“0.#”);
this.WeightRemainingTextBlock.Text = weightRemaining.ToString(“0.#”);
if (weightPercent > timePercent)
this.SummaryTextBlock.Text = “AHEAD OF SCHEDULE!”;
else if (weightPercent < timePercent)
this.SummaryTextBlock.Text = “FALLING BEHIND”;
else
this.SummaryTextBlock.Text = “ON TRACK”;
// Set the data for the two pie charts
this.WeightPieChart.DataContext =
new double[] { weightLost, weightRemaining };
this.TimePieChart.DataContext =
new int[] { daysElapsed, daysRemaining };
}
void GraphDatePicker_ValueChanged(object sender,
DateTimeValueChangedEventArgs e)
{
// Minimum must be <= maximum
if (this.StartGraphDatePicker.Value > this.EndGraphDatePicker.Value)
this.StartGraphDatePicker.Value = this.EndGraphDatePicker.Value;
// Update the range of the X axis
(this.Chart.Axes[0] as DateTimeAxis).Minimum =
this.StartGraphDatePicker.Value;
(this.Chart.Axes[0] as DateTimeAxis).Maximum =
this.EndGraphDatePicker.Value;
// Remember this new graph start date
Settings.GraphStartDate.Value = this.StartGraphDatePicker.Value;
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Weight Tracker”, UriKind.Relative));
}
}
}

[/code]

  • Although the page’s data context is set to the list of weigh-ins (leveraged by the first pivot item’s editable list and the second pivot item’s line series), this listing overrides the scatter series’ items source to the list of goals at the end of OnNavigatedTo. (DataPointSeries is a common base class of the seven non-stacked series classes.)
  • This page only remembers the currently selected pivot item in page state rather than isolated storage. That’s because when most users start a fresh instance, the first thing they probably want to do is record a new weigh-in.
  • At the end of UpdateProgress, each pie chart is given a simple two-number data source. The first number represents the amount of weight/time elapsed, and the second number represents the amount of weight/time remaining. This enables each pie chart to provide a visual representation of the percentages listed on the page.
  • To make the two date pickers filter the main chart, GraphDatePicker_ValueChanged sets the Minimum and Maximum properties on the chart’s explicit X axis.

The Settings Page

FIGURE 29.5 The settings page enables editing a list of goal weights by hosting the same user control as the main page.
FIGURE 29.5 The settings page enables editing a list of goal weights by hosting the same user control as the main page.

The settings page, shown in Figure 29.5, enables users to view, add, and delete date-based weight goals exactly how they view, add, and delete weigh-ins on the main page. That’s because it leverages the same WeighInEditableList user control. The page also contains a delete button for clearing all weigh-ins and all goals in bulk.

Because most of the functionality is provided by the WeighInEditableList user control, the implementation of the settings page is short and straightforward. Listing 29.3 contains its XAML and Listing 29.4 contains its codebehind. WeighInEditableList’s IsGoalList property being set to true is what makes a star appear next to each weight, rather than the up/down arrows seen on the main page.

LISTING 29.3 SettingsPage.xaml—The User Interface for Weight Tracker’s Settings Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.SettingsPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
<RowDefinition Height=”Auto”/>
</Grid.RowDefinitions>
<!– The standard settings header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SETTINGS” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”weight tracker”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<TextBlock Grid.Row=”1” Margin=”24,12,24,24”
Text=”Enter one or more weight goals.”/>
<!– A translucent foreground-colored star image behind the list –>
<Rectangle Grid.Row=”2” Opacity=”.2” Margin=”0,0,0,12” Width=”240”
Height=”240” VerticalAlignment=”Bottom”
Fill=”{StaticResource PhoneForegroundBrush}”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/bigStar.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<!– The editable list user control –>
<local:WeighInEditableList Grid.Row=”2” Collection=”{Binding}”
IsGoalList=”True” Margin=”12,0”/>
<!– Delete button –>
<Button x:Name=”DeleteButton” Grid.Row=”3” Content=”delete all data”
local:Tilt.IsEnabled=”True” Click=”DeleteButton_Click”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 29.4 SettingsPage.xaml.cs—The Code-Behind for Weight Tracker’s Settings Page

[code]

using System.Windows;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
// This time, the data source for the editable list is the goal list
this.DataContext = Settings.GoalList.Value;
}
void DeleteButton_Click(object sender, RoutedEventArgs e)
{
if (MessageBox.Show(
“Are you sure you want to clear all your weigh-ins and goals?”, “Delete”,
MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Settings.GoalList.Value.Clear();
Settings.WeighInList.Value.Clear();
}
}
}
}

[/code]

The Finished Product

Weight Tracker (Charts & Graphs)


Posted

in

by

Tags: