using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Controls.DataVisualization;
using System.Windows.Controls.DataVisualization.Charting;
using ReputationLib;
namespace RepWPF
{
//////////////////////////////////////////////////////////////////////////////////////
public class RepChart
{
/// <summary>
/// Contains the colors used for each chart series. By default, we use
/// CodeProject's colors.
/// </summary>
public static Dictionary<RepCategory, Color> m_seriesColors = new Dictionary<RepCategory, Color>();
/// <summary>
/// The font used for the chart title
/// </summary>
//protected Font m_titleFont = new Font("Arial", 12, FontStyle.Bold | FontStyle.Italic, GraphicsUnit.Pixel);
/// <summary>
/// The DataVisualization.Charting.Chart object for this RepChart
/// </summary>
public Chart ChartObj { get; private set; }
/// <summary>
/// Event handler for the "No data" event
/// </summary>
public event NoDataEventHandler NoDataEvent = delegate { };
protected DateTime m_dateFrom;
protected DateTime m_dateTo;
public string TitlePrefix {get; set; }
public bool ShowTrendLine { get; set; }
//--------------------------------------------------------------------------------
/// <summary>
/// Constructor - sets default values for many of the chart's properties, and
/// creates several objects necessary to bring a given chart to life.
/// </summary>
/// <param name="name"></param>
public RepChart(string titlePrefix, ChartNames chartObjName)
{
this.ShowTrendLine = false;
this.TitlePrefix = titlePrefix;
InitSeriesColors();
this.ChartObj = new Chart();
this.ChartObj.Name = chartObjName.ToString();
this.ChartObj.Tag = this;
this.ChartObj.TabIndex = 1;
this.ChartObj.Series.Clear();
}
//--------------------------------------------------------------------------------
/// <summary>
/// Sets the title for the chart object. By default, the chart title is the text
/// that appears in the chart category combo box on the form. To make it more
/// meaningful, use this method to describe the represented time period.
/// </summary>
/// <param name="period">A string that represents data added to the name of the
/// chart that indicates the covered date period. You should set the string to
/// whatever is needed to make sense such as putting the word "from", or "for"
/// BEFORE the actual time period. </param>
protected void SetTitle(string period)
{
string title = this.ChartObj.Name;
if (!string.IsNullOrEmpty(period))
{
title = string.Format("{0} {1}", this.ChartObj.Name, period);
}
this.ChartObj.Title = title;
}
//--------------------------------------------------------------------------------
/// <summary>
/// The base PopulateChart method. Using this base instance with throw a
/// NotImplementedException.
/// </summary>
/// <param name="scraper"></param>
/// <param name="comboTimePeriod"></param>
/// <param name="dateFrom"></param>
/// <param name="dateTo"></param>
/// <param name="categories"></param>
public virtual void PopulateChart(string comboTimePeriod, DateTime dateFrom, DateTime dateTo, DisplayCategories categories)
{
this.ChartObj.Series.Clear();
}
//--------------------------------------------------------------------------------
/// <summary>
/// sets the colors used by the various series. This base version uss colors
/// similar to what are used on the CodeProject web site. If you want to set
/// different colors, override this method.
/// </summary>
protected virtual void InitSeriesColors()
{
if (m_seriesColors.Count == 0)
{
m_seriesColors.Add(RepCategory.Author, Colors.DarkRed);
m_seriesColors.Add(RepCategory.Authority, Colors.DarkGreen);
m_seriesColors.Add(RepCategory.Debator, Colors.Magenta);
m_seriesColors.Add(RepCategory.Editor, Colors.Goldenrod);
m_seriesColors.Add(RepCategory.Enquirer, Colors.Purple);
m_seriesColors.Add(RepCategory.Organiser, Colors.Olive);
m_seriesColors.Add(RepCategory.Participant, Colors.LightSteelBlue);
m_seriesColors.Add(RepCategory.Total, Colors.Orange);
}
}
//--------------------------------------------------------------------------------
/// <summary>
/// Retrieves a list of reputation items based on the specified time period
/// category, the reputation category, and the from/to dates.
/// </summary>
/// <param name="repItems">The list of reputation items to process</param>
/// <param name="comboTimePeriod">The selected time perio (current week, current year, etc)</param>
/// <param name="category">The reputation category to pull items for</param>
/// <param name="dateFrom">The starting date of the specified time period</param>
/// <param name="dateTo">The ending date of the specified time period</param>
/// <returns></returns>
protected virtual List<RepItem> GetSeriesList(RepItemCollection repItems, string comboTimePeriod, RepCategory category, DateTime dateFrom, DateTime dateTo)
{
// exception throws
// we have to have reputation items
if (repItems.Count <= 0)
{
throw new Exception(string.Format("No reputation items found. ({0} / {1})", comboTimePeriod, category.ToString()));
}
// there are no points associated with the Unknown reputaion category
if (category == RepCategory.Unknown)
{
throw new Exception(string.Format("Invalid reputation cateory. ({0} / {1})", comboTimePeriod, category.ToString()));
}
// if this is for a specific time period, the dateFrom parameter must be
// valid (ticks > 0)
if (comboTimePeriod == "Specified Period" && dateFrom.Ticks == 0)
{
throw new Exception(string.Format("Invalid start date (dateFrom) used for retrieving series data. ({0} / {1})", comboTimePeriod, category.ToString()));
}
// the "dateTo" must be set (ticks > 0) - we don't actually check to make
// sure it's >= the from date unless this is for a "Specified Period"
if (dateTo.Ticks == 0)
{
throw new Exception(string.Format("Invalid end date (dateTo) used for retrieving series data. ({0} / {1})", comboTimePeriod, category.ToString()));
}
// so we can avoid a smart-ass programmer trying something like setting the
// dateTo parameter to a value that would result in a dateFrom being less
// than the lowest possible date (ticks = 0), we provide one last sanity
// check before proceeding, making sure that the dateFrom is really set to
// 0 ticks (but only if we're not doing a "specified period"
if (comboTimePeriod != "Specified Period" && dateFrom.Ticks > 0)
{
dateFrom.AddTicks(dateFrom.Ticks * -1);
}
// when we calculate the dateFrom, we do a Math.Max on the resulting ticks
// to mack sure we're not going to generate an invalid date
switch (comboTimePeriod)
{
case "Current Week":
dateFrom = dateFrom.AddTicks(Math.Max(0, dateTo.WeekStartDate().Ticks));
break;
case "Current Month":
dateFrom = dateFrom.AddTicks(Math.Max(0, (dateTo.AddDays((dateTo.Day - 1) * -1).Ticks)));
break;
case "Current Year":
dateFrom = dateFrom.AddTicks(Math.Max(0, (dateTo.AddDays((dateTo.DayOfYear - 1) * -1).Ticks)));
break;
case "Last 7 days":
dateFrom = dateFrom.AddTicks(Math.Max(0, (dateTo.AddDays(-7)).Ticks));
break;
case "Last 30 days":
dateFrom = dateFrom.AddTicks(Math.Max(0, (dateTo.AddDays(-30)).Ticks));
break;
case "Last 365 days":
dateFrom = dateFrom.AddTicks(Math.Max(0, (dateTo.AddDays(-365)).Ticks));
break;
case "Specified Period":
if (dateTo < dateFrom)
{
throw new Exception(string.Format("End date (dateTo) cannot be ealier than start date (dateFrom). ({0} / {1})", comboTimePeriod, category.ToString()));
}
break;
}
m_dateTo = dateTo;
m_dateFrom = dateFrom;
// now that we have our from/to dates we can retrieve/return the specified items
List<RepItem> list = null;
list = (from item in repItems
where ((item.Category == category) && (item.TimeScraped.Between(dateFrom, dateTo, true)))
select item).ToList();
return list;
}
//--------------------------------------------------------------------------------
/// <summary>
/// determines the highest value to be represented on the chart, and essentially
/// allows that value to be the next highest possible delta (if the actual
/// highest value on the chart is 950, the highest value shown will be 1000.
/// </summary>
/// <param name="maxY"></param>
/// <returns></returns>
protected virtual int NormalizeMaxY(int maxY)
{
int thousands = (int)Math.Ceiling(maxY / 1000d);
if (thousands > 100)
{
thousands += (50 - (thousands % 50));
}
else if (thousands > 10)
{
thousands += (5 - (thousands % 5));
}
maxY = 1000 * thousands;
return maxY;
}
//--------------------------------------------------------------------------------
/// <summary>
/// Determines the Y-Axis interval based on the max possible value on the Y-Axis.
/// </summary>
/// <param name="maxY"></param>
/// <returns></returns>
protected virtual int NormalizeInterval(int maxY)
{
int interval = 0;
if (maxY > 100000)
{
interval = 50000;
}
else if (maxY > 10000)
{
interval = 10000;
}
else if (maxY > 1000)
{
interval = 1000;
}
else
{
interval = 100;
}
return interval;
}
//--------------------------------------------------------------------------------
/// <summary>
/// Raises the No Data event that signifies that the resulting list of repitems
/// is empty, and no chart can be rendered.
/// </summary>
protected void RaiseNoDataEvent()
{
NoDataEvent(this, null);
}
//--------------------------------------------------------------------------------
/// <summary>
/// Calculates the trend line based on the specified parent series, and creates a
/// series for the chart.
/// </summary>
/// <param name="series"></param>
/// <param name="categoryCount"></param>
protected void CalculateTrendLine(List<RepItem> data, int categoryCount)
{
if (this.ShowTrendLine)
{
LineSeries series = new LineSeries();
List<RepItem> trendItems = new List<RepItem>();
int pointCount = data.Count;
// create a points array for the Regress method
double[] points = new double[pointCount];
for (int i = 0; i < pointCount; i++)
{
points[i] = (this.TitlePrefix == "Daily Changes") ? data[i].ChangeValue : data[i].Value;
}
double a = 0;
double b = 0;
//calculation the regression results
Globals.Regress(points, ref a, ref b);
for (int i = 0; i < pointCount; i++)
{
trendItems.Add(data[i].Clone());
double yield = (a + b) * i;
if (this.TitlePrefix == "Accumulated By Day")
{
trendItems[i].ChangeValue = Convert.ToInt32(yield);
}
else
{
trendItems[i].Value = Convert.ToInt32(yield);
}
}
SolidColorBrush brush = new SolidColorBrush((categoryCount > 1) ? m_seriesColors[trendItems[0].Category] : Colors.Black);
MakeDataLine(trendItems, trendItems[0].Category.ToString(), "TimeScraped", (this.TitlePrefix == "Accumulated By Day") ? "ChangeValue" : "Value", brush);
}
}
//--------------------------------------------------------------------------------
protected void MakeChartAxes()
{
TimeSpan span = this.m_dateTo - this.m_dateFrom;
string format = "{0:MMM yyyy}";
DateTimeIntervalType intervalType = DateTimeIntervalType.Months;
if (span.TotalDays <= 7)
{
format = "{0:ddd}";
intervalType = DateTimeIntervalType.Days;
}
else if (span.TotalDays <= 30)
{
format = "{0:MMM dd}";
intervalType = DateTimeIntervalType.Weeks;
}
Style xLabelStyle = new Style(typeof(AxisLabel));
xLabelStyle.Setters.Add(new Setter() { Property = AxisLabel.StringFormatProperty, Value = format });
LinearAxis yAxis = new LinearAxis()
{
Name = "ChartYAxis",
Orientation = AxisOrientation.Y,
Location = AxisLocation.Left,
ShowGridLines = true,
};
CategoryAxis xCategory = new CategoryAxis()
{
Name = "ChartXCategoryAxis",
Orientation = AxisOrientation.X,
Location = AxisLocation.Bottom,
ShowGridLines = false,
AxisLabelStyle = xLabelStyle,
};
DateTimeAxis xAxis = new DateTimeAxis()
{
Name = "ChartXAxis",
Orientation = AxisOrientation.X,
Location = AxisLocation.Bottom,
ShowGridLines = false,
Interval = 1,
AxisLabelStyle = xLabelStyle,
IntervalType = intervalType
};
this.ChartObj.Axes.Clear();
this.ChartObj.Axes.Add(yAxis);
this.ChartObj.Axes.Add(xAxis);
this.ChartObj.Axes.Add(xCategory);
}
//--------------------------------------------------------------------------------
/// <summary>
/// Makes a column series for the specifed data.
/// </summary>
/// <param name="data"></param>
/// <param name="seriesTitle"></param>
/// <param name="xProperty"></param>
/// <param name="yProperty"></param>
/// <param name="colorBrush"></param>
protected void MakeColumn(List<RepItem> data, string seriesTitle, string xProperty, string yProperty, Brush colorBrush)
{
ColumnSeries series = new ColumnSeries()
{
ItemsSource = data,
Title = seriesTitle,
DependentValueBinding = new System.Windows.Data.Binding(yProperty),
IndependentValueBinding = new System.Windows.Data.Binding(xProperty)
};
this.ChartObj.Series.Add(series);
try
{
Style columnStyle = new Style(typeof(ColumnDataPoint));
SetStyleProperty(columnStyle, ColumnDataPoint.BackgroundProperty, colorBrush);
SetStyleProperty(columnStyle, ColumnDataPoint.BorderThicknessProperty, new Thickness(0));
ControlTemplate template = Application.Current.Resources["ColumnSeriesTemplate"] as ControlTemplate;
if (template != null)
{
SetStyleProperty(columnStyle, ColumnDataPoint.TemplateProperty, template);
}
series.DataPointStyle = columnStyle;
series.TransitionDuration = new TimeSpan(0);
}
catch (Exception ex)
{
if (ex != null) { }
}
}
//--------------------------------------------------------------------------------
/// <summary>
/// Sets tthe specified property to the specified value, and adds that setter to
/// the specified style.
/// </summary>
/// <param name="style"></param>
/// <param name="property"></param>
/// <param name="value"></param>
private void SetStyleProperty(Style style, DependencyProperty property, object value)
{
Setter setter = new Setter(property, value);
style.Setters.Add(setter);
}
//--------------------------------------------------------------------------------
/// <summary>
/// Sets the series styles for regular data lines.
/// </summary>
/// <param name="series">The series to set the styles on</param>
/// <param name="seriesTitle">The series title (as it would appear in the legend)</param>
/// <param name="xProperty">The data property used for the x-axis</param>
/// <param name="yProperty">The data property used for the y-axis</param>
/// <param name="colorBrush">The color of the line</param>
protected void MakeDataLine(List<RepItem> data, string seriesTitle, string xProperty, string yProperty, Brush colorBrush)
{
LineSeries series = new LineSeries()
{
ItemsSource = data,
Title = seriesTitle,
DependentValueBinding = new System.Windows.Data.Binding(yProperty),
IndependentValueBinding = new System.Windows.Data.Binding(xProperty)
};
this.ChartObj.Series.Add(series);
try
{
Style style = new Style(typeof(LineDataPoint));
SetStyleProperty(style, LineDataPoint.BackgroundProperty, colorBrush);
string templateName = (this.ShowTrendLine) ? "TrendLineSeriesTemplate" : "LineSeriesTemplate";
ControlTemplate template = Application.Current.Resources[templateName] as ControlTemplate;
if (template != null)
{
SetStyleProperty(style, ColumnDataPoint.TemplateProperty, template);
}
series.DataPointStyle = style;
series.TransitionDuration = new TimeSpan(0);
}
catch (Exception ex)
{
if (ex != null) {}
}
}
//--------------------------------------------------------------------------------
/// <summary>
/// Creates a pie chart series.
/// </summary>
/// <param name="data"></param>
/// <param name="seriesTitle"></param>
/// <param name="yProperty"></param>
protected void MakePie(List<RepItem> data, string seriesTitle, string yProperty)
{
DisplayCategories categories = new DisplayCategories();
foreach(RepItem item in data)
{
categories.Add(item.Category);
}
System.Collections.ObjectModel.Collection<ResourceDictionary> palette = MakePalette(categories);
//this.ChartObj.Palette = palette;
// add our custom color palette
PieSeries series = new PieSeries()
{
ItemsSource = data,
Title = seriesTitle,
DependentValueBinding = new System.Windows.Data.Binding(yProperty),
IndependentValueBinding = new System.Windows.Data.Binding("Category"),
TransitionDuration = new TimeSpan(0),
};
Style style = new Style(typeof(PieDataPoint));
ControlTemplate template = Application.Current.Resources["PieDataTemplate"] as ControlTemplate;
if (template != null)
{
SetStyleProperty(style, ColumnDataPoint.TemplateProperty, template);
}
series.DataPointStyle = style;
Style legendStyle = new Style(typeof(LegendItem));
ControlTemplate legendTemplate = Application.Current.Resources["PieLegendTemplate"] as ControlTemplate;
if (legendTemplate != null)
{
SetStyleProperty(legendStyle, LegendItem.TemplateProperty, legendTemplate);
}
series.LegendItemStyle = legendStyle;
series.Palette = palette;
this.ChartObj.Series.Add(series);
}
//--------------------------------------------------------------------------------
/// <summary>
/// Default MakePalette method (for line and column charts). The pie chart has an
/// overloaded version of this method.
/// </summary>
/// <param name="list"></param>
/// <returns></returns>
protected System.Collections.ObjectModel.Collection<ResourceDictionary> MakePalette(DisplayCategories list)
{
// allocate the collection
System.Collections.ObjectModel.Collection<ResourceDictionary> palette = new System.Collections.ObjectModel.Collection<ResourceDictionary>();
// cycle through the specified categories
foreach (RepCategory item in list)
{
// create a resource dictonary
ResourceDictionary rd = new ResourceDictionary();
// create a style based on Control (as opposed to PieDataPoint)
Style style = new Style(typeof(Control));
// get ready to create the appropriate brush
SolidColorBrush brush = null;
switch (item)
{
case RepCategory.Author : brush = new SolidColorBrush(Colors.DarkRed); break;
case RepCategory.Authority : brush = new SolidColorBrush(Colors.DarkGreen); break;
case RepCategory.Debator : brush = new SolidColorBrush(Colors.Magenta); break;
case RepCategory.Editor : brush = new SolidColorBrush(Colors.Goldenrod); break;
case RepCategory.Enquirer : brush = new SolidColorBrush(Colors.Purple); break;
case RepCategory.Organiser : brush = new SolidColorBrush(Colors.Olive); break;
case RepCategory.Participant : brush = new SolidColorBrush(Colors.LightSteelBlue); break;
}
// set the background for the style to be the brush we created
style.Setters.Add(new Setter()
{
Property = Control.BackgroundProperty,
Value = brush
});
// add the style to the resouorce dictionary
rd.Add("DataPointStyle", style);
//add the resource dictionary to the palettte
palette.Add(rd);
}
// return the palette
return palette;
// what a pain in the ass, but wait until you see the axis code for column and line charts...
}
}
}