using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
using ReputationLib;
namespace Reputationator
{
//////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// Represents a reputation chart.
/// </summary>
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 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 name)
{
this.ShowTrendLine = false;
InitSeriesColors();
this.ChartObj = new Chart();
this.ChartObj.Name = name;
this.ChartObj.Dock = System.Windows.Forms.DockStyle.Fill;
this.ChartObj.Visible = false;
this.ChartObj.Tag = this;
this.ChartObj.Location = new System.Drawing.Point(0, 0);
this.ChartObj.TabIndex = 1;
this.ChartObj.Text = name;
this.ChartObj.Palette = ChartColorPalette.BrightPastel;
this.ChartObj.AntiAliasing = AntiAliasingStyles.All;
this.ChartObj.Series.Clear();
this.ChartObj.ChartAreas.Clear();
this.ChartObj.Legends.Clear();
// add the chart area
ChartArea area = new ChartArea("ChartArea1");
this.ChartObj.ChartAreas.Add(area);
// add the legend
Legend legend = new Legend("Legend1");
this.ChartObj.Legends.Add(legend);
}
//--------------------------------------------------------------------------------
/// <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);
}
if (this.ChartObj.Titles.Count == 0)
{
this.ChartObj.Titles.Add(new Title(title, Docking.Top, m_titleFont, Color.Black));
}
else
{
this.ChartObj.Titles[0].Text = 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(RepScraper scraper, string comboTimePeriod, DateTime dateFrom, DateTime dateTo, DisplayCategories categories)
{
throw new NotImplementedException("You MUST use an overridden version of this method in your derived class.");
}
//--------------------------------------------------------------------------------
/// <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, Color.DarkRed);
m_seriesColors.Add(RepCategory.Authority, Color.DarkGreen);
m_seriesColors.Add(RepCategory.Debator, Color.Magenta);
m_seriesColors.Add(RepCategory.Editor, Color.Goldenrod);
m_seriesColors.Add(RepCategory.Enquirer, Color.Purple);
m_seriesColors.Add(RepCategory.Organiser, Color.Olive);
m_seriesColors.Add(RepCategory.Participant, Color.LightSteelBlue);
m_seriesColors.Add(RepCategory.Total, Color.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(Series series, int categoryCount)
{
if (this.ShowTrendLine)
{
int pointCount = series.Points.Count;
Series trend = new Series("Trend", pointCount);
trend.ChartArea = this.ChartObj.ChartAreas[0].Name;
trend.ChartType = SeriesChartType.Line;
trend.XValueType = ChartValueType.DateTime;
trend.YValueType = ChartValueType.Double;
trend.Color = (categoryCount > 1) ? series.Color : Color.Black;
double[] points = new double[pointCount];
for (int i = 0; i < series.Points.Count; i++)
{
DataPoint point = series.Points[i];
points[i] = point.YValues[0];
}
double a = 0;
double b = 0;
Globals.Regress(points, ref a, ref b);
for (int i = 0; i < pointCount; i++)
{
double yield = a + b * i;
trend.Points.AddXY(series.Points[i].XValue, yield);
}
trend.Name = string.Format("{0} Trend",series.Name);
this.ChartObj.Series.Add(trend);
}
}
}
}