#region License Information
/* HeuristicLab
* Copyright (C) 2002-2016 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.Globalization;
using System.Linq;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
using HeuristicLab.Common;
using HeuristicLab.Visualization.ChartControlsExtensions;
namespace HeuristicLab.Problems.DataAnalysis.Views {
public partial class GradientChart : EnhancedChart {
private ModifiableDataset internalDataset;
public bool ShowLegend { get; set; }
private bool useMedianValues;
public bool UseMedianValues {
get { return useMedianValues; }
set {
if (value == useMedianValues) return;
useMedianValues = value;
OnChartPropertyChanged(this, EventArgs.Empty);
UpdateChart();
}
}
private int row;
public int Row {
get { return row; }
set {
if (row == value) return;
row = value;
OnChartPropertyChanged(this, EventArgs.Empty);
UpdateChart();
}
}
private double min;
public double Min {
get { return min; }
set {
if (value.IsAlmost(min)) return;
min = value;
OnChartPropertyChanged(this, EventArgs.Empty);
UpdateChart();
}
}
private double max;
public double Max {
get { return max; }
set {
if (value.IsAlmost(max)) return;
max = value;
OnChartPropertyChanged(this, EventArgs.Empty);
UpdateChart();
}
}
private int points;
public int Points {
get { return points; }
set {
if (value == points) return;
points = value;
OnChartPropertyChanged(this, EventArgs.Empty);
UpdateChart();
}
}
private IRegressionProblemData problemData;
public IRegressionProblemData ProblemData {
get { return problemData; }
set {
if (!SolutionsCompatibleWithProblemData(value, solutionList))
throw new ArgumentException("The problem data provided does not contain all the variables required by the solutions.");
problemData = value;
UpdateInternalDataset();
UpdateChart();
}
}
public string Target {
get { return Solutions.First().ProblemData.TargetVariable; }
}
private string variable;
public string Variable {
get { return variable; }
set {
if (variable == value) return;
if (!ProblemData.Dataset.DoubleVariables.Contains(value))
throw new ArgumentException("The variable must be present in the problem dataset.");
OnChartPropertyChanged(this, EventArgs.Empty);
variable = value;
var values = ProblemData.Dataset.GetReadOnlyDoubleValues(variable);
min = values.Min();
max = values.Max();
UpdateChart();
}
}
private List solutionList;
public IEnumerable Solutions {
get { return solutionList; }
set {
if (!value.Any())
throw new ArgumentException("At least one solution must be provided.");
if (SolutionsCompatibleWithProblemData(problemData, value))
solutionList = new List(value);
else
throw new ArgumentException("The provided solution collection is not compatible with the existing problem data.");
UpdateChart();
}
}
public VerticalLineAnnotation VerticalLineAnnotation {
get { return (VerticalLineAnnotation)Annotations.SingleOrDefault(x => x is VerticalLineAnnotation); }
}
public GradientChart() {
InitializeComponent();
RegisterEvents();
}
public void AddSolution(IRegressionSolution solution) {
if (!SolutionsCompatibleWithProblemData(problemData, new[] { solution })) {
throw new ArgumentException("The solution is not compatible with the problem data.");
}
solutionList.Add(solution);
UpdateChart();
}
public void RemoveSolution(IRegressionSolution solution) {
var removed = solutionList.RemoveAll(x => x == solution);
if (removed > 0)
UpdateChart();
}
private static bool SolutionsCompatibleWithProblemData(IRegressionProblemData pd, IEnumerable solutions) {
if (pd == null || !solutions.Any()) return true;
if (solutions.Any(x => x.ProblemData.TargetVariable != pd.TargetVariable)) return false;
var variables = new HashSet(pd.Dataset.DoubleVariables);
return solutions.SelectMany(x => x.ProblemData.Dataset.DoubleVariables).All(variables.Contains);
}
public void Configure(IEnumerable solutions, IRegressionProblemData pd, double min, double max, int points) {
if (!SolutionsCompatibleWithProblemData(pd, solutions))
throw new ArgumentException("Solutions are not compatible with the problem data.");
this.solutionList = new List(solutions);
this.problemData = pd;
this.variable = pd.Dataset.DoubleVariables.First();
this.min = min;
this.max = max;
this.points = points;
UpdateInternalDataset();
UpdateChart();
}
public void Configure(IEnumerable solutions, IRegressionProblemData pd, ModifiableDataset dataset, string variable, double min, double max, int points) {
if (!SolutionsCompatibleWithProblemData(pd, solutions))
throw new ArgumentException("Solutions are not compatible with the problem data.");
this.solutionList = new List(solutions);
this.problemData = pd;
this.variable = variable;
this.internalDataset = dataset;
this.min = min;
this.max = max;
this.points = points;
}
public void UpdateChart() {
// throw exceptions?
if (internalDataset == null || solutionList == null || !solutionList.Any())
return;
if (min.IsAlmost(max) || min > max || points == 0)
return;
Series.Clear();
var vla = VerticalLineAnnotation;
vla.Visible = true;
Annotations.Clear();
Annotations.Add(vla);
double axisMin, axisMax, axisInterval;
// calculate X-axis interval
ChartUtil.CalculateAxisInterval(min, max, 5, out axisMin, out axisMax, out axisInterval);
var axis = ChartAreas[0].AxisX;
axis.Minimum = axisMin;
axis.Maximum = axisMax;
axis.Interval = axisInterval;
for (int i = 0; i < solutionList.Count; ++i) {
var solution = solutionList[i];
var series = PlotSeries(solution);
series.Name = Target + " " + i;
Series.Add(series);
var p = series.Points.Last();
vla.X = p.XValue;
}
// calculate Y-axis interval
double ymin = 0, ymax = 0;
foreach (var v in Series[0].Points.Select(x => x.YValues[0])) {
if (ymin > v) ymin = v;
if (ymax < v) ymax = v;
}
ChartUtil.CalculateAxisInterval(ymin, ymax, 5, out axisMin, out axisMax, out axisInterval);
axis = ChartAreas[0].AxisY;
axis.Minimum = axisMin;
axis.Maximum = axisMax;
axis.Interval = axisInterval;
ChartAreas[0].AxisX.Title = Variable + " : " + vla.X.ToString("N3", CultureInfo.CurrentCulture); // set axis titlet
AddStripLines(); // add strip lines
if (ShowLegend)
AddLegends();
}
private void UpdateInternalDataset() {
var variables = ProblemData.Dataset.DoubleVariables.ToList();
var variableValues = new List[variables.Count];
if (UseMedianValues) {
for (int i = 0; i < variables.Count; ++i) {
var median = ProblemData.Dataset.GetDoubleValues(variables[i], ProblemData.TrainingIndices).Median();
variableValues[i] = new List { median };
}
} else {
for (int i = 0; i < variables.Count; ++i) {
var variableValue = ProblemData.Dataset.GetDoubleValue(variables[i], Row);
variableValues[i] = new List { variableValue };
}
}
internalDataset = new ModifiableDataset(variables, variableValues);
}
private double GetEstimatedValue(IRegressionSolution solution, double x) {
var v = internalDataset.GetDoubleValue(Variable, 0);
internalDataset.SetVariableValue(x, Variable, 0);
var y = solution.Model.GetEstimatedValues(internalDataset, new[] { 0 }).Single();
internalDataset.SetVariableValue(v, Variable, 0);
return y;
}
private Series PlotSeries(IRegressionSolution solution) {
var v = internalDataset.GetDoubleValue(variable, 0);
var series = new Series { ChartType = SeriesChartType.Point };
var step = (max - min) / points;
var axisX = ChartAreas[0].AxisX;
axisX.Title = Variable + " : " + v.ToString("N3", CultureInfo.CurrentCulture);
var axisY = ChartAreas[0].AxisY;
axisY.Title = Target;
double y;
// lefthand section outside of the training range
for (double x = axisX.Minimum; x < min; x += step) {
y = GetEstimatedValue(solution, x);
series.Points.Add(new DataPoint(x, y) { MarkerSize = 2, MarkerColor = Color.Orange });
}
// points in the trainig range
for (double x = min; x < max; x += step) {
y = GetEstimatedValue(solution, x);
series.Points.Add(new DataPoint(x, y) { MarkerSize = 2 });
}
// righthand section outside of the training range
for (double x = max; x < axisX.Maximum; x += step) {
y = GetEstimatedValue(solution, x);
series.Points.Add(new DataPoint(x, y) { MarkerSize = 2, MarkerColor = Color.Orange });
}
y = GetEstimatedValue(solution, v);
series.Points.Add(new DataPoint(v, y) { MarkerSize = 5, MarkerColor = Color.Red });
series.IsVisibleInLegend = true;
return series;
}
private void AddLegends() {
Legends.Clear();
var legend = new Legend();
legend.Alignment = StringAlignment.Center;
legend.LegendStyle = LegendStyle.Row;
legend.Docking = Docking.Top;
Legends.Add(legend);
foreach (var s in Series) {
s.Legend = legend.Name;
}
}
private void AddStripLines() {
var axisX = ChartAreas[0].AxisX;
axisX.StripLines.Clear();
axisX.StripLines.Add(new StripLine { BackColor = Color.FromArgb(30, Color.Green), IntervalOffset = axisX.Minimum, StripWidth = min - axisX.Minimum });
axisX.StripLines.Add(new StripLine { BackColor = Color.FromArgb(30, Color.Green), IntervalOffset = max, StripWidth = axisX.Maximum - max });
}
private void RegisterEvents() {
AnnotationPositionChanging += chart_AnnotationPositionChanging;
MouseMove += chart_MouseMove;
FormatNumber += chart_FormatNumber;
}
#region events
public event EventHandler VariableValueChanged;
public void OnVariableValueChanged(object sender, EventArgs args) {
var changed = VariableValueChanged;
if (changed == null) return;
changed(sender, args);
}
public event EventHandler ChartPropertyChanged;
public void OnChartPropertyChanged(object sender, EventArgs args) {
var changed = ChartPropertyChanged;
if (changed == null) return;
changed(sender, args);
}
private void chart_AnnotationPositionChanged(object sender, EventArgs e) {
var annotation = VerticalLineAnnotation;
var x = annotation.X;
internalDataset.SetVariableValue(x, Variable, 0);
for (int i = 0; i < solutionList.Count; ++i) {
var y = GetEstimatedValue(solutionList[i], x);
var s = Series[i];
var n = s.Points.Count;
s.Points[n - 1] = new DataPoint(x, y) { MarkerColor = Color.Red, MarkerSize = 5 };
}
ChartAreas[0].AxisX.Title = Variable + " : " + x.ToString("N3", CultureInfo.CurrentCulture);
Update();
OnVariableValueChanged(this, EventArgs.Empty);
}
private void chart_AnnotationPositionChanging(object sender, AnnotationPositionChangingEventArgs e) {
var step = (max - min) / points;
e.NewLocationX = step * (long)Math.Round(e.NewLocationX / step);
var axisX = ChartAreas[0].AxisX;
if (e.NewLocationX > axisX.Maximum)
e.NewLocationX = axisX.Maximum;
if (e.NewLocationX < axisX.Minimum)
e.NewLocationX = axisX.Minimum;
// var x = e.NewLocationX;
// internalDataset.SetVariableValue(x, Variable, 0);
// for (int i = 0; i < solutionList.Count; ++i) {
// var y = GetEstimatedValue(solutionList[i], x);
// var s = Series[i];
// var n = s.Points.Count;
// s.Points[n - 1] = new DataPoint(x, y) { MarkerColor = Color.Red, MarkerSize = 5 };
// }
// ChartAreas[0].AxisX.Title = Variable + " : " + x.ToString("N3", CultureInfo.CurrentCulture);
// Update();
// OnVariableValueChanged(this, EventArgs.Empty);
}
private void chart_MouseMove(object sender, MouseEventArgs e) {
this.Cursor = HitTest(e.X, e.Y).ChartElementType == ChartElementType.Annotation ? Cursors.VSplit : Cursors.Default;
}
private void chart_FormatNumber(object sender, FormatNumberEventArgs e) {
if (e.ElementType == ChartElementType.AxisLabels) {
switch (e.Format) {
case "CustomAxisXFormat":
break;
case "CustomAxisYFormat":
var v = e.Value;
e.LocalizedValue = string.Format("{0,5}", v);
break;
default:
break;
}
}
}
private void GradientChart_DragDrop(object sender, DragEventArgs e) {
var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
if (data != null) {
var solution = data as IRegressionSolution;
if (!Solutions.Contains(solution))
AddSolution(solution);
}
}
private void GradientChart_DragEnter(object sender, DragEventArgs e) {
if (!e.Data.GetDataPresent(HeuristicLab.Common.Constants.DragDropDataFormat)) return;
e.Effect = DragDropEffects.None;
var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
var regressionSolution = data as IRegressionSolution;
if (regressionSolution != null) {
e.Effect = DragDropEffects.Copy;
}
}
#endregion
}
}