#region License Information
/* HeuristicLab
* Copyright (C) 2002-2018 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
*
* This file is part of HeuristicLab.
*
* HeuristicLab is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* HeuristicLab is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with HeuristicLab. If not, see .
*/
#endregion
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
using HeuristicLab.Collections;
using HeuristicLab.Common;
using HeuristicLab.Core.Views;
using HeuristicLab.MainForm;
namespace HeuristicLab.Analysis.Views {
[View("ScatterPlot View")]
[Content(typeof(ScatterPlot), true)]
public partial class ScatterPlotView : NamedItemView, IConfigureableView {
protected List invisibleSeries;
protected Dictionary>, ScatterPlotDataRow> pointsRowsTable;
protected Dictionary seriesToRegressionSeriesTable;
private double xMin, xMax, yMin, yMax;
public new ScatterPlot Content {
get { return (ScatterPlot)base.Content; }
set { base.Content = value; }
}
public bool ShowName {
get { return nameTextBox.Visible; }
set {
if (nameTextBox.Visible != value) {
foreach (Control c in Controls) {
if (c == chart) continue;
c.Visible = value;
}
chart.Dock = value ? DockStyle.None : DockStyle.Fill;
}
}
}
public ScatterPlotView() {
InitializeComponent();
pointsRowsTable = new Dictionary>, ScatterPlotDataRow>();
seriesToRegressionSeriesTable = new Dictionary();
invisibleSeries = new List();
chart.CustomizeAllChartAreas();
chart.ChartAreas[0].CursorX.Interval = 1;
chart.ContextMenuStrip.Items.Add(configureToolStripMenuItem);
}
#region Event Handler Registration
protected override void DeregisterContentEvents() {
foreach (ScatterPlotDataRow row in Content.Rows)
DeregisterScatterPlotDataRowEvents(row);
Content.VisualPropertiesChanged -= new EventHandler(Content_VisualPropertiesChanged);
Content.Rows.ItemsAdded -= new CollectionItemsChangedEventHandler(Rows_ItemsAdded);
Content.Rows.ItemsRemoved -= new CollectionItemsChangedEventHandler(Rows_ItemsRemoved);
Content.Rows.ItemsReplaced -= new CollectionItemsChangedEventHandler(Rows_ItemsReplaced);
Content.Rows.CollectionReset -= new CollectionItemsChangedEventHandler(Rows_CollectionReset);
base.DeregisterContentEvents();
}
protected override void RegisterContentEvents() {
base.RegisterContentEvents();
Content.VisualPropertiesChanged += new EventHandler(Content_VisualPropertiesChanged);
Content.Rows.ItemsAdded += new CollectionItemsChangedEventHandler(Rows_ItemsAdded);
Content.Rows.ItemsRemoved += new CollectionItemsChangedEventHandler(Rows_ItemsRemoved);
Content.Rows.ItemsReplaced += new CollectionItemsChangedEventHandler(Rows_ItemsReplaced);
Content.Rows.CollectionReset += new CollectionItemsChangedEventHandler(Rows_CollectionReset);
}
protected virtual void RegisterScatterPlotDataRowEvents(ScatterPlotDataRow row) {
row.NameChanged += new EventHandler(Row_NameChanged);
row.VisualPropertiesChanged += new EventHandler(Row_VisualPropertiesChanged);
pointsRowsTable.Add(row.Points, row);
row.Points.ItemsAdded += new CollectionItemsChangedEventHandler>>(Points_ItemsAdded);
row.Points.ItemsRemoved += new CollectionItemsChangedEventHandler>>(Points_ItemsRemoved);
row.Points.ItemsReplaced += new CollectionItemsChangedEventHandler>>(Points_ItemsReplaced);
row.Points.CollectionReset += new CollectionItemsChangedEventHandler>>(Points_CollectionReset);
}
protected virtual void DeregisterScatterPlotDataRowEvents(ScatterPlotDataRow row) {
row.Points.ItemsAdded -= new CollectionItemsChangedEventHandler>>(Points_ItemsAdded);
row.Points.ItemsRemoved -= new CollectionItemsChangedEventHandler>>(Points_ItemsRemoved);
row.Points.ItemsReplaced -= new CollectionItemsChangedEventHandler>>(Points_ItemsReplaced);
row.Points.CollectionReset -= new CollectionItemsChangedEventHandler>>(Points_CollectionReset);
pointsRowsTable.Remove(row.Points);
row.VisualPropertiesChanged -= new EventHandler(Row_VisualPropertiesChanged);
row.NameChanged -= new EventHandler(Row_NameChanged);
}
#endregion
protected override void OnContentChanged() {
base.OnContentChanged();
invisibleSeries.Clear();
chart.Titles[0].Text = string.Empty;
chart.ChartAreas[0].AxisX.Title = string.Empty;
chart.ChartAreas[0].AxisY.Title = string.Empty;
chart.Series.Clear();
if (Content != null) {
chart.Titles[0].Text = Content.Name;
chart.Titles[0].Visible = !string.IsNullOrEmpty(Content.Name);
AddScatterPlotDataRows(Content.Rows);
ConfigureChartArea(chart.ChartAreas[0]);
RecalculateMinMaxPointValues();
RecalculateAxesScale(chart.ChartAreas[0]);
}
}
protected override void SetEnabledStateOfControls() {
base.SetEnabledStateOfControls();
chart.Enabled = Content != null;
}
public void ShowConfiguration() {
if (Content != null) {
using (ScatterPlotVisualPropertiesDialog dialog = new ScatterPlotVisualPropertiesDialog(Content)) {
dialog.ShowDialog(this);
}
} else MessageBox.Show("Nothing to configure.");
}
protected virtual void AddScatterPlotDataRows(IEnumerable rows) {
foreach (var row in rows) {
RegisterScatterPlotDataRowEvents(row);
Series series = new Series(row.Name) { Tag = row };
if (row.VisualProperties.DisplayName.Trim() != String.Empty) series.LegendText = row.VisualProperties.DisplayName;
else series.LegendText = row.Name;
var regressionSeries = new Series(row.Name + "_Regression") {
Tag = row,
ChartType = SeriesChartType.Line,
BorderDashStyle = ChartDashStyle.Dot,
IsVisibleInLegend = false,
Color = Color.Transparent // to avoid auto color assignment via color palette
};
seriesToRegressionSeriesTable.Add(series, regressionSeries);
ConfigureSeries(series, regressionSeries, row);
FillSeriesWithRowValues(series, row);
chart.Series.Add(series);
chart.Series.Add(regressionSeries);
FillRegressionSeries(regressionSeries, row);
}
ConfigureChartArea(chart.ChartAreas[0]);
RecalculateMinMaxPointValues();
RecalculateAxesScale(chart.ChartAreas[0]);
UpdateYCursorInterval();
UpdateRegressionSeriesColors();
}
protected virtual void RemoveScatterPlotDataRows(IEnumerable rows) {
foreach (var row in rows) {
DeregisterScatterPlotDataRowEvents(row);
Series series = chart.Series[row.Name];
chart.Series.Remove(series);
if (invisibleSeries.Contains(series))
invisibleSeries.Remove(series);
chart.Series.Remove(seriesToRegressionSeriesTable[series]);
seriesToRegressionSeriesTable.Remove(series);
}
RecalculateMinMaxPointValues();
RecalculateAxesScale(chart.ChartAreas[0]);
}
private void ConfigureSeries(Series series, Series regressionSeries, ScatterPlotDataRow row) {
series.BorderWidth = 1;
series.BorderDashStyle = ChartDashStyle.Solid;
series.BorderColor = Color.Empty;
if (row.VisualProperties.Color != Color.Empty)
series.Color = row.VisualProperties.Color;
else series.Color = Color.Empty;
series.IsVisibleInLegend = row.VisualProperties.IsVisibleInLegend;
series.ChartType = SeriesChartType.FastPoint;
series.MarkerSize = row.VisualProperties.PointSize;
series.MarkerStyle = ConvertPointStyle(row.VisualProperties.PointStyle);
series.XAxisType = AxisType.Primary;
series.YAxisType = AxisType.Primary;
if (row.VisualProperties.DisplayName.Trim() != String.Empty) series.LegendText = row.VisualProperties.DisplayName;
else series.LegendText = row.Name;
string xAxisTitle = string.IsNullOrEmpty(Content.VisualProperties.XAxisTitle)
? "X"
: Content.VisualProperties.XAxisTitle;
string yAxisTitle = string.IsNullOrEmpty(Content.VisualProperties.YAxisTitle)
? "Y"
: Content.VisualProperties.YAxisTitle;
series.ToolTip =
series.LegendText + Environment.NewLine +
xAxisTitle + " = " + "#VALX," + Environment.NewLine +
yAxisTitle + " = " + "#VAL";
regressionSeries.BorderWidth = Math.Max(1, row.VisualProperties.PointSize / 2);
regressionSeries.IsVisibleInLegend = row.VisualProperties.IsRegressionVisibleInLegend &&
row.VisualProperties.RegressionType != ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.None;
regressionSeries.LegendText = string.IsNullOrEmpty(row.VisualProperties.RegressionDisplayName)
? string.Format("{0}({1})", row.VisualProperties.RegressionType, row.Name)
: row.VisualProperties.RegressionDisplayName;
}
private void ConfigureChartArea(ChartArea area) {
if (Content.VisualProperties.TitleFont != null) chart.Titles[0].Font = Content.VisualProperties.TitleFont;
if (!Content.VisualProperties.TitleColor.IsEmpty) chart.Titles[0].ForeColor = Content.VisualProperties.TitleColor;
chart.Titles[0].Text = Content.VisualProperties.Title;
chart.Titles[0].Visible = !string.IsNullOrEmpty(Content.VisualProperties.Title);
if (Content.VisualProperties.AxisTitleFont != null) area.AxisX.TitleFont = Content.VisualProperties.AxisTitleFont;
if (!Content.VisualProperties.AxisTitleColor.IsEmpty) area.AxisX.TitleForeColor = Content.VisualProperties.AxisTitleColor;
area.AxisX.Title = Content.VisualProperties.XAxisTitle;
area.AxisX.MajorGrid.Enabled = Content.VisualProperties.XAxisGrid;
if (Content.VisualProperties.AxisTitleFont != null) area.AxisY.TitleFont = Content.VisualProperties.AxisTitleFont;
if (!Content.VisualProperties.AxisTitleColor.IsEmpty) area.AxisY.TitleForeColor = Content.VisualProperties.AxisTitleColor;
area.AxisY.Title = Content.VisualProperties.YAxisTitle;
area.AxisY.MajorGrid.Enabled = Content.VisualProperties.YAxisGrid;
}
private void RecalculateAxesScale(ChartArea area) {
area.AxisX.Minimum = CalculateMinBound(xMin);
area.AxisX.Maximum = CalculateMaxBound(xMax);
if (area.AxisX.Minimum == area.AxisX.Maximum) {
area.AxisX.Minimum = xMin - 0.5;
area.AxisX.Maximum = xMax + 0.5;
}
area.AxisY.Minimum = CalculateMinBound(yMin);
area.AxisY.Maximum = CalculateMaxBound(yMax);
if (area.AxisY.Minimum == area.AxisY.Maximum) {
area.AxisY.Minimum = yMin - 0.5;
area.AxisY.Maximum = yMax + 0.5;
}
if (xMax - xMin > 0) area.CursorX.Interval = Math.Pow(10, Math.Floor(Math.Log10(area.AxisX.Maximum - area.AxisX.Minimum) - 3));
else area.CursorX.Interval = 1;
area.AxisX.IsMarginVisible = false;
if (!Content.VisualProperties.XAxisMinimumAuto && !double.IsNaN(Content.VisualProperties.XAxisMinimumFixedValue)) area.AxisX.Minimum = Content.VisualProperties.XAxisMinimumFixedValue;
if (!Content.VisualProperties.XAxisMaximumAuto && !double.IsNaN(Content.VisualProperties.XAxisMaximumFixedValue)) area.AxisX.Maximum = Content.VisualProperties.XAxisMaximumFixedValue;
if (!Content.VisualProperties.YAxisMinimumAuto && !double.IsNaN(Content.VisualProperties.YAxisMinimumFixedValue)) area.AxisY.Minimum = Content.VisualProperties.YAxisMinimumFixedValue;
if (!Content.VisualProperties.YAxisMaximumAuto && !double.IsNaN(Content.VisualProperties.YAxisMaximumFixedValue)) area.AxisY.Maximum = Content.VisualProperties.YAxisMaximumFixedValue;
}
private static double CalculateMinBound(double min) {
if (min == 0) return 0;
var scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(min))));
return scale * (Math.Floor(min / scale));
}
private static double CalculateMaxBound(double max) {
if (max == 0) return 0;
var scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(max))));
return scale * (Math.Ceiling(max / scale));
}
protected virtual void UpdateYCursorInterval() {
double interestingValuesRange = (
from series in chart.Series
where series.Enabled
let values = (from point in series.Points
where !point.IsEmpty
select point.YValues[0]).DefaultIfEmpty(1.0)
let range = values.Max() - values.Min()
where range > 0.0
select range
).DefaultIfEmpty(1.0).Min();
double digits = (int)Math.Log10(interestingValuesRange) - 3;
double yZoomInterval = Math.Pow(10, digits);
this.chart.ChartAreas[0].CursorY.Interval = yZoomInterval;
}
protected void UpdateRegressionSeriesColors() {
chart.ApplyPaletteColors();
foreach (var row in Content.Rows) {
var series = chart.Series[row.Name];
var regressionSeries = seriesToRegressionSeriesTable[series];
regressionSeries.Color = series.Color;
}
}
#region Event Handlers
#region Content Event Handlers
protected override void Content_NameChanged(object sender, EventArgs e) {
if (InvokeRequired)
Invoke(new EventHandler(Content_NameChanged), sender, e);
else {
Content.VisualProperties.Title = Content.Name;
base.Content_NameChanged(sender, e);
}
}
private void Content_VisualPropertiesChanged(object sender, EventArgs e) {
if (InvokeRequired)
Invoke(new EventHandler(Content_VisualPropertiesChanged), sender, e);
else {
ConfigureChartArea(chart.ChartAreas[0]);
RecalculateAxesScale(chart.ChartAreas[0]); // axes min/max could have changed
}
}
#endregion
#region Rows Event Handlers
private void Rows_ItemsAdded(object sender, CollectionItemsChangedEventArgs e) {
if (InvokeRequired)
Invoke(new CollectionItemsChangedEventHandler(Rows_ItemsAdded), sender, e);
else {
AddScatterPlotDataRows(e.Items);
}
}
private void Rows_ItemsRemoved(object sender, CollectionItemsChangedEventArgs e) {
if (InvokeRequired)
Invoke(new CollectionItemsChangedEventHandler(Rows_ItemsRemoved), sender, e);
else {
RemoveScatterPlotDataRows(e.Items);
}
}
private void Rows_ItemsReplaced(object sender, CollectionItemsChangedEventArgs e) {
if (InvokeRequired)
Invoke(new CollectionItemsChangedEventHandler(Rows_ItemsReplaced), sender, e);
else {
RemoveScatterPlotDataRows(e.OldItems);
AddScatterPlotDataRows(e.Items);
}
}
private void Rows_CollectionReset(object sender, CollectionItemsChangedEventArgs e) {
if (InvokeRequired)
Invoke(new CollectionItemsChangedEventHandler(Rows_CollectionReset), sender, e);
else {
RemoveScatterPlotDataRows(e.OldItems);
AddScatterPlotDataRows(e.Items);
}
}
#endregion
#region Row Event Handlers
private void Row_VisualPropertiesChanged(object sender, EventArgs e) {
if (InvokeRequired)
Invoke(new EventHandler(Row_VisualPropertiesChanged), sender, e);
else {
ScatterPlotDataRow row = (ScatterPlotDataRow)sender;
Series series = chart.Series[row.Name];
Series regressionSeries = seriesToRegressionSeriesTable[series];
series.Points.Clear();
regressionSeries.Points.Clear();
ConfigureSeries(series, regressionSeries, row);
FillSeriesWithRowValues(series, row);
FillRegressionSeries(regressionSeries, row);
RecalculateMinMaxPointValues();
RecalculateAxesScale(chart.ChartAreas[0]);
UpdateRegressionSeriesColors();
}
}
private void Row_NameChanged(object sender, EventArgs e) {
if (InvokeRequired)
Invoke(new EventHandler(Row_NameChanged), sender, e);
else {
ScatterPlotDataRow row = (ScatterPlotDataRow)sender;
chart.Series[row.Name].Name = row.Name;
}
}
#endregion
#region Points Event Handlers
private void Points_ItemsAdded(object sender, CollectionItemsChangedEventArgs>> e) {
if (InvokeRequired)
Invoke(new CollectionItemsChangedEventHandler>>(Points_ItemsAdded), sender, e);
else {
ScatterPlotDataRow row = null;
pointsRowsTable.TryGetValue((IObservableList>)sender, out row);
if (row != null) {
Series rowSeries = chart.Series[row.Name];
Series regressionSeries = seriesToRegressionSeriesTable[rowSeries];
if (!invisibleSeries.Contains(rowSeries)) {
rowSeries.Points.Clear();
regressionSeries.Points.Clear();
FillSeriesWithRowValues(rowSeries, row);
FillRegressionSeries(regressionSeries, row);
RecalculateMinMaxPointValues();
RecalculateAxesScale(chart.ChartAreas[0]);
UpdateYCursorInterval();
}
}
}
}
private void Points_ItemsRemoved(object sender, CollectionItemsChangedEventArgs>> e) {
if (InvokeRequired)
Invoke(new CollectionItemsChangedEventHandler>>(Points_ItemsRemoved), sender, e);
else {
ScatterPlotDataRow row = null;
pointsRowsTable.TryGetValue((IObservableList>)sender, out row);
if (row != null) {
Series rowSeries = chart.Series[row.Name];
Series regressionSeries = seriesToRegressionSeriesTable[rowSeries];
if (!invisibleSeries.Contains(rowSeries)) {
rowSeries.Points.Clear();
regressionSeries.Points.Clear();
FillSeriesWithRowValues(rowSeries, row);
FillRegressionSeries(regressionSeries, row);
RecalculateMinMaxPointValues();
RecalculateAxesScale(chart.ChartAreas[0]);
UpdateYCursorInterval();
}
}
}
}
private void Points_ItemsReplaced(object sender, CollectionItemsChangedEventArgs>> e) {
if (InvokeRequired)
Invoke(new CollectionItemsChangedEventHandler>>(Points_ItemsReplaced), sender, e);
else {
ScatterPlotDataRow row = null;
pointsRowsTable.TryGetValue((IObservableList>)sender, out row);
if (row != null) {
Series rowSeries = chart.Series[row.Name];
Series regressionSeries = seriesToRegressionSeriesTable[rowSeries];
if (!invisibleSeries.Contains(rowSeries)) {
rowSeries.Points.Clear();
regressionSeries.Points.Clear();
FillSeriesWithRowValues(rowSeries, row);
FillRegressionSeries(regressionSeries, row);
RecalculateMinMaxPointValues();
RecalculateAxesScale(chart.ChartAreas[0]);
UpdateYCursorInterval();
}
}
}
}
private void Points_CollectionReset(object sender, CollectionItemsChangedEventArgs>> e) {
if (InvokeRequired)
Invoke(new CollectionItemsChangedEventHandler>>(Points_CollectionReset), sender, e);
else {
ScatterPlotDataRow row = null;
pointsRowsTable.TryGetValue((IObservableList>)sender, out row);
if (row != null) {
Series rowSeries = chart.Series[row.Name];
Series regressionSeries = seriesToRegressionSeriesTable[rowSeries];
if (!invisibleSeries.Contains(rowSeries)) {
rowSeries.Points.Clear();
regressionSeries.Points.Clear();
FillSeriesWithRowValues(rowSeries, row);
FillRegressionSeries(regressionSeries, row);
RecalculateMinMaxPointValues();
RecalculateAxesScale(chart.ChartAreas[0]);
UpdateYCursorInterval();
}
}
}
}
#endregion
private void configureToolStripMenuItem_Click(object sender, System.EventArgs e) {
ShowConfiguration();
}
#endregion
#region Chart Event Handlers
private void chart_MouseDoubleClick(object sender, MouseEventArgs e) {
HitTestResult result = chart.HitTest(e.X, e.Y, ChartElementType.DataPoint);
if (result.ChartElementType != ChartElementType.DataPoint) return;
var series = result.Series;
var dataPoint = series.Points[result.PointIndex];
var tag = dataPoint.Tag;
var content = tag as IContent;
if (tag == null) return;
if (content == null) return;
MainFormManager.MainForm.ShowContent(content);
}
private void chart_MouseDown(object sender, MouseEventArgs e) {
HitTestResult result = chart.HitTest(e.X, e.Y);
if (result.ChartElementType == ChartElementType.LegendItem) {
ToggleSeriesVisible(result.Series);
}
}
private void chart_MouseMove(object sender, MouseEventArgs e) {
HitTestResult result = chart.HitTest(e.X, e.Y);
if (result.ChartElementType == ChartElementType.LegendItem)
this.Cursor = Cursors.Hand;
else
this.Cursor = Cursors.Default;
}
private void chart_CustomizeLegend(object sender, CustomizeLegendEventArgs e) {
foreach (LegendItem legendItem in e.LegendItems) {
var series = chart.Series[legendItem.SeriesName];
if (series != null) {
bool seriesIsInvisible = invisibleSeries.Contains(series);
foreach (LegendCell cell in legendItem.Cells) {
cell.ForeColor = seriesIsInvisible ? Color.Gray : Color.Black;
}
}
}
}
#endregion
private void ToggleSeriesVisible(Series series) {
if (!invisibleSeries.Contains(series)) {
series.Points.Clear();
invisibleSeries.Add(series);
RecalculateMinMaxPointValues();
} else {
invisibleSeries.Remove(series);
if (Content != null) {
var row = (ScatterPlotDataRow)series.Tag;
if (seriesToRegressionSeriesTable.ContainsKey(series))
FillSeriesWithRowValues(series, row);
else
FillRegressionSeries(series, row);
RecalculateMinMaxPointValues();
this.chart.Legends[series.Legend].ForeColor = Color.Black;
RecalculateAxesScale(chart.ChartAreas[0]);
UpdateYCursorInterval();
}
}
}
private void RecalculateMinMaxPointValues() {
yMin = xMin = double.MaxValue;
yMax = xMax = double.MinValue;
foreach (var s in chart.Series.Where(x => x.Enabled)) {
foreach (var p in s.Points) {
double x = p.XValue, y = p.YValues[0];
if (xMin > x) xMin = x;
if (xMax < x) xMax = x;
if (yMin > y) yMin = y;
if (yMax < y) yMax = y;
}
}
}
private void FillSeriesWithRowValues(Series series, ScatterPlotDataRow row) {
bool zerosOnly = true;
for (int i = 0; i < row.Points.Count; i++) {
var value = row.Points[i];
DataPoint point = new DataPoint();
if (IsInvalidValue(value.X) || IsInvalidValue(value.Y))
point.IsEmpty = true;
else {
point.XValue = value.X;
point.YValues = new double[] { value.Y };
}
point.Tag = value.Tag;
series.Points.Add(point);
if (value.X != 0.0f)
zerosOnly = false;
}
if (zerosOnly) // if all x-values are zero, the x-values are interpreted as 1, 2, 3, ...
series.Points.Add(new DataPoint(1, 1) { IsEmpty = true });
double correlation = Correlation(row.Points);
series.LegendToolTip = string.Format("Correlation (R²) = {0:G4}", correlation * correlation);
}
private void FillRegressionSeries(Series regressionSeries, ScatterPlotDataRow row) {
if (row.VisualProperties.RegressionType == ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.None
|| invisibleSeries.Contains(regressionSeries))
return;
double[] coefficients;
if (!Fitting(row, out coefficients)) {
regressionSeries.LegendToolTip = "Could not calculate regression.";
return;
}
// Fill regrssion series
double min = row.Points.Min(p => p.X), max = row.Points.Max(p => p.X);
double range = max - min, delta = range / Math.Max(row.Points.Count - 1, 50);
if (range > double.Epsilon) {
for (double x = min; x <= max; x += delta) {
regressionSeries.Points.AddXY(x, Estimate(x, row, coefficients));
}
}
// Correlation
var data = row.Points.Select(p => new Point2D(p.Y, Estimate(p.X, row, coefficients)));
double correlation = Correlation(data.ToList());
regressionSeries.LegendToolTip = GetStringFormula(row, coefficients) + Environment.NewLine +
string.Format("Correlation (R²) = {0:G4}", correlation * correlation);
regressionSeries.ToolTip = GetStringFormula(row, coefficients);
}
#region Helpers
private MarkerStyle ConvertPointStyle(ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle pointStyle) {
switch (pointStyle) {
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Circle:
return MarkerStyle.Circle;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Cross:
return MarkerStyle.Cross;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Diamond:
return MarkerStyle.Diamond;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Square:
return MarkerStyle.Square;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Star4:
return MarkerStyle.Star4;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Star5:
return MarkerStyle.Star5;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Star6:
return MarkerStyle.Star6;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Star10:
return MarkerStyle.Star10;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Triangle:
return MarkerStyle.Triangle;
default:
return MarkerStyle.None;
}
}
protected static bool IsInvalidValue(double x) {
return double.IsNaN(x) || x < (double)decimal.MinValue || x > (double)decimal.MaxValue;
}
#endregion
#region Correlation and Fitting Helper
protected static double Correlation(IList> values) {
// sums of x, y, x squared etc.
double sx = 0.0;
double sy = 0.0;
double sxx = 0.0;
double syy = 0.0;
double sxy = 0.0;
int n = 0;
for (int i = 0; i < values.Count; i++) {
double x = values[i].X;
double y = values[i].Y;
if (IsInvalidValue(x) || IsInvalidValue(y))
continue;
sx += x;
sy += y;
sxx += x * x;
syy += y * y;
sxy += x * y;
n++;
}
// covariation
double cov = sxy / n - sx * sy / n / n;
// standard error of x
double sigmaX = Math.Sqrt(sxx / n - sx * sx / n / n);
// standard error of y
double sigmaY = Math.Sqrt(syy / n - sy * sy / n / n);
// correlation
return cov / sigmaX / sigmaY;
}
protected static bool Fitting(ScatterPlotDataRow row, out double[] coefficients) {
if (!IsValidRegressionData(row)) {
coefficients = new double[0];
return false;
}
var xs = row.Points.Select(p => p.X).ToList();
var ys = row.Points.Select(p => p.Y).ToList();
// Input transformations
double[,] matrix;
int nRows;
switch (row.VisualProperties.RegressionType) {
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Linear:
matrix = CreateMatrix(out nRows, ys, xs);
break;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Polynomial:
var xss = Enumerable.Range(1, row.VisualProperties.PolynomialRegressionOrder)
.Select(o => xs.Select(x => Math.Pow(x, o)).ToList())
.Reverse(); // higher order first
matrix = CreateMatrix(out nRows, ys, xss.ToArray());
break;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Exponential:
matrix = CreateMatrix(out nRows, ys.Select(y => Math.Log(y)).ToList(), xs);
break;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Power:
matrix = CreateMatrix(out nRows, ys.Select(y => Math.Log(y)).ToList(), xs.Select(x => Math.Log(x)).ToList());
break;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Logarithmic:
matrix = CreateMatrix(out nRows, ys, xs.Select(x => Math.Log(x)).ToList());
break;
default:
throw new ArgumentException("Unknown RegressionType: " + row.VisualProperties.RegressionType);
}
// Linear fitting
bool success = LinearFitting(matrix, nRows, out coefficients);
if (!success) return false;
// Output transformation
switch (row.VisualProperties.RegressionType) {
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Exponential:
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Power:
coefficients[1] = Math.Exp(coefficients[1]);
break;
}
return true;
}
protected static bool IsValidRegressionData(ScatterPlotDataRow row) {
// No invalid values allowed
for (int i = 0; i < row.Points.Count; i++) {
if (IsInvalidValue(row.Points[i].X) || IsInvalidValue(row.Points[i].Y))
return false;
}
// Exp, Power and Log Regression do not work with negative values
switch (row.VisualProperties.RegressionType) {
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Exponential:
for (int i = 0; i < row.Points.Count; i++) {
if (row.Points[i].Y <= 0)
return false;
}
break;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Power:
for (int i = 0; i < row.Points.Count; i++) {
if (row.Points[i].X <= 0 || row.Points[i].Y <= 0)
return false;
}
break;
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Logarithmic:
for (int i = 0; i < row.Points.Count; i++) {
if (row.Points[i].X <= 0)
return false;
}
break;
}
return true;
}
protected static double[,] CreateMatrix(out int nRows, IList ys, params IList[] xss) {
var matrix = new double[ys.Count, xss.Length + 1];
int rowIdx = 0;
for (int i = 0; i < ys.Count; i++) {
if (IsInvalidValue(ys[i]) || xss.Any(xs => IsInvalidValue(xs[i])))
continue;
for (int j = 0; j < xss.Length; j++) {
matrix[rowIdx, j] = xss[j][i];
}
matrix[rowIdx, xss.Length] = ys[i];
rowIdx++;
}
nRows = rowIdx;
return matrix;
}
protected static bool LinearFitting(double[,] xsy, int nRows, out double[] coefficients) {
int nFeatures = xsy.GetLength(1) - 1;
alglib.linearmodel lm;
alglib.lrreport ar;
int retVal;
alglib.lrbuild(xsy, nRows, nFeatures, out retVal, out lm, out ar);
if (retVal != 1) {
coefficients = new double[0];
return false;
}
alglib.lrunpack(lm, out coefficients, out nFeatures);
return true;
}
protected static double Estimate(double x, ScatterPlotDataRow row, double[] coefficients) {
switch (row.VisualProperties.RegressionType) {
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Linear:
return coefficients[0] * x + coefficients[1];
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Polynomial:
return coefficients
.Reverse() // to match index and order
.Select((c, o) => c * Math.Pow(x, o))
.Sum();
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Exponential:
return coefficients[1] * Math.Exp(coefficients[0] * x);
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Power:
return coefficients[1] * Math.Pow(x, coefficients[0]);
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Logarithmic:
return coefficients[0] * Math.Log(x) + coefficients[1];
default:
throw new ArgumentException("Unknown RegressionType: " + row.VisualProperties.RegressionType);
}
}
protected static string GetStringFormula(ScatterPlotDataRow row, double[] coefficients) {
switch (row.VisualProperties.RegressionType) {
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Linear:
return string.Format("{0:G4} x {1} {2:G4}", coefficients[0], Sign(coefficients[1]), Math.Abs(coefficients[1]));
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Polynomial:
var sb = new StringBuilder();
sb.AppendFormat("{0:G4}{1}", coefficients[0], PolyFactor(coefficients.Length - 1));
foreach (var x in coefficients
.Reverse() // match index and order
.Select((c, o) => new { c, o })
.Reverse() // higher order first
.Skip(1)) // highest order poly already added
sb.AppendFormat(" {0} {1:G4}{2}", Sign(x.c), Math.Abs(x.c), PolyFactor(x.o));
return sb.ToString();
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Exponential:
return string.Format("{0:G4} e^({1:G4} x)", coefficients[1], coefficients[0]);
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Power:
return string.Format("{0:G4} x^({1:G4})", coefficients[1], coefficients[0]);
case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Logarithmic:
return string.Format("{0:G4} ln(x) {1} {2:G4}", coefficients[0], Sign(coefficients[1]), Math.Abs(coefficients[1]));
default:
throw new ArgumentException("Unknown RegressionType: " + row.VisualProperties.RegressionType);
}
}
private static string Sign(double value) {
return value >= 0 ? "+" : "-";
}
private static string PolyFactor(int order) {
if (order == 0) return "";
if (order == 1) return " x";
if (order == 2) return " x²";
if (order == 3) return " x³";
return " x^" + order;
}
#endregion
}
}