#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.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
using HeuristicLab.Common;
using HeuristicLab.MainForm.WindowsForms;
using HeuristicLab.Visualization.ChartControlsExtensions;
namespace HeuristicLab.Problems.DataAnalysis.Views {
public partial class GradientChart : UserControl {
private ModifiableDataset sharedFixedVariables; // used for syncronising variable values between charts
private ModifiableDataset internalDataset; // holds the x values for each point drawn
public bool ShowLegend {
get { return chart.Legends[0].Enabled; }
set { chart.Legends[0].Enabled = value; }
}
public bool ShowXAxisLabel {
get { return chart.ChartAreas[0].AxisX.Enabled == AxisEnabled.True; }
set { chart.ChartAreas[0].AxisX.Enabled = value ? AxisEnabled.True : AxisEnabled.False; }
}
public bool ShowYAxisLabel {
get { return chart.ChartAreas[0].AxisY.Enabled == AxisEnabled.True; }
set { chart.ChartAreas[0].AxisY.Enabled = value ? AxisEnabled.True : AxisEnabled.False; }
}
public bool ShowCursor {
get { return chart.Annotations[0].Visible; }
set { chart.Annotations[0].Visible = value; }
}
private int xAxisTicks = 5;
public int XAxisTicks {
get { return xAxisTicks; }
set { if (xAxisTicks != value) { xAxisTicks = value; UpdateChartAsync(); } }
}
private int yAxisTicks = 5;
public int YXAxisTicks {
get { return yAxisTicks; }
set { if (yAxisTicks != value) { yAxisTicks = value; UpdateChartAsync(); } }
}
private double trainingMin = double.MinValue;
public double TrainingMin {
get { return trainingMin; }
set { if (!value.IsAlmost(trainingMin)) { trainingMin = value; UpdateChartAsync(); } }
}
private double trainingMax = double.MaxValue;
public double TrainingMax {
get { return trainingMax; }
set { if (!value.IsAlmost(trainingMax)) { trainingMax = value; UpdateChartAsync(); } }
}
private int drawingSteps = 1000;
public int DrawingSteps {
get { return drawingSteps; }
set { if (value != drawingSteps) { drawingSteps = value; UpdateChartAsync(); } }
}
private string freeVariable;
public string FreeVariable {
get { return freeVariable; }
set {
if (value == freeVariable) return;
if (solutions.Any(s => !s.ProblemData.Dataset.DoubleVariables.Contains(value))) {
throw new ArgumentException("Variable does not exist in the ProblemData of the Solutions.");
}
freeVariable = value;
RecalculateInternalDataset();
UpdateChartAsync();
}
}
private readonly List solutions = new List();
public IEnumerable Solutions {
get { return solutions; }
}
private VerticalLineAnnotation VerticalLineAnnotation {
get { return (VerticalLineAnnotation)chart.Annotations.SingleOrDefault(x => x is VerticalLineAnnotation); }
}
public GradientChart() {
InitializeComponent();
// Configure axis
chart.CustomizeAllChartAreas();
chart.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;
chart.ChartAreas[0].AxisX.ScaleView.Zoomable = true;
chart.ChartAreas[0].CursorX.Interval = 0;
chart.ChartAreas[0].CursorY.IsUserSelectionEnabled = true;
chart.ChartAreas[0].AxisY.ScaleView.Zoomable = true;
chart.ChartAreas[0].CursorY.Interval = 0;
}
public void Configure(IEnumerable solutions, ModifiableDataset sharedFixedVariables, string freeVariable, int drawingSteps) {
if (!SolutionsCompatible(solutions))
throw new ArgumentException("Solutions are not compatible with the problem data.");
this.solutions.Clear();
this.solutions.AddRange(solutions);
this.freeVariable = freeVariable;
this.drawingSteps = drawingSteps;
// add an event such that whenever a value is changed in the shared dataset,
// this change is reflected in the internal dataset (where the value becomes a whole column)
if (this.sharedFixedVariables != null)
this.sharedFixedVariables.ItemChanged -= sharedFixedVariables_ItemChanged;
this.sharedFixedVariables = sharedFixedVariables;
this.sharedFixedVariables.ItemChanged += sharedFixedVariables_ItemChanged;
RecalculateTrainingLimits();
RecalculateInternalDataset();
}
private void sharedFixedVariables_ItemChanged(object o, EventArgs e) {
if (o != sharedFixedVariables) return;
var variables = sharedFixedVariables.DoubleVariables.ToList();
var rowIndex = e.Value;
var columnIndex = e.Value2;
var variableName = variables[columnIndex];
if (variableName == FreeVariable) return;
var v = sharedFixedVariables.GetDoubleValue(variableName, rowIndex);
var values = new List(Enumerable.Repeat(v, DrawingSteps));
internalDataset.ReplaceVariable(variableName, values);
}
private void RecalculateInternalDataset() {
// we expand the range in order to get nice tick intervals on the x axis
double xmin, xmax, xinterval;
ChartUtil.CalculateAxisInterval(trainingMin, trainingMax, XAxisTicks, out xmin, out xmax, out xinterval);
double step = (xmax - xmin) / drawingSteps;
var xvalues = new List();
for (int i = 0; i < drawingSteps; i++)
xvalues.Add(xmin + i * step);
var variables = sharedFixedVariables.DoubleVariables.ToList();
internalDataset = new ModifiableDataset(variables,
variables.Select(x => x == FreeVariable
? xvalues
: Enumerable.Repeat(sharedFixedVariables.GetDoubleValue(x, 0), xvalues.Count).ToList()
)
);
}
private void RecalculateTrainingLimits() {
trainingMin = solutions.Select(s => s.ProblemData.Dataset.GetDoubleValues(freeVariable, s.ProblemData.TrainingIndices).Min()).Max();
trainingMax = solutions.Select(s => s.ProblemData.Dataset.GetDoubleValues(freeVariable, s.ProblemData.TrainingIndices).Max()).Min();
}
public async Task UpdateChartAsync() {
// throw exceptions?
if (sharedFixedVariables == null || solutions == null || !solutions.Any())
return;
if (trainingMin.IsAlmost(trainingMax) || trainingMin > trainingMax || drawingSteps == 0)
return;
// Set cursor
var defaultValue = sharedFixedVariables.GetDoubleValue(freeVariable, 0);
VerticalLineAnnotation.X = defaultValue;
// Calculate X-axis interval
double axisMin, axisMax, axisInterval;
ChartUtil.CalculateAxisInterval(trainingMin, trainingMax, XAxisTicks, out axisMin, out axisMax, out axisInterval);
var axis = chart.ChartAreas[0].AxisX;
axis.Minimum = axisMin;
axis.Maximum = axisMax;
axis.Interval = axisInterval;
// Create series
var seriesDict = new Dictionary();
for (int i = 0; i < solutions.Count; ++i) {
var solution = solutions[i];
var series = await CreateSeriesAsync(solution);
series.Item1.Tag = i; // for sorting
var meanSeries = series.Item1;
var confidenceIntervalSeries = series.Item2;
meanSeries.Name = solution.ProblemData.TargetVariable + " " + i;
seriesDict.Add(meanSeries, confidenceIntervalSeries);
if (confidenceIntervalSeries != null)
confidenceIntervalSeries.Name = "95% Conf. Interval " + meanSeries.Name;
}
chart.SuspendRepaint();
chart.Series.Clear();
// Add mean series for applying palette colors
foreach (var series in seriesDict.Keys.OrderBy(s => (int)s.Tag)) {
series.LegendText = series.Name;
chart.Series.Add(series);
}
chart.Palette = ChartColorPalette.BrightPastel;
chart.ApplyPaletteColors();
chart.Palette = ChartColorPalette.None;
foreach (var series in seriesDict.OrderBy(s => (int)s.Key.Tag)) {
if (series.Value == null) continue;
int idx = chart.Series.IndexOf(series.Key);
series.Value.Color = Color.FromArgb(40, series.Key.Color);
series.Value.IsVisibleInLegend = false;
chart.Series.Insert(idx, series.Value);
}
chart.ResumeRepaint(true);
// calculate Y-axis interval
//double ymin = 0, ymax = 0;
//foreach (var vs in chart.Series.SelectMany(series => series.Points.Select(s => s.YValues))) {
// for (int index = 0; index < vs.Length; index++) {
// var v = vs[index];
// if (ymin > v) ymin = v;
// if (ymax < v) ymax = v;
// }
//}
//ChartUtil.CalculateAxisInterval(ymin, ymax, YXAxisTicks, out axisMin, out axisMax, out axisInterval);
//axis = chart.ChartAreas[0].AxisY;
//axis.Minimum = axisMin;
//axis.Maximum = axisMax;
//axis.Interval = axisInterval;
//chart.ChartAreas[0].RecalculateAxesScale();
// set axis title
chart.ChartAreas[0].AxisX.Title = FreeVariable + " : " + defaultValue.ToString("N3", CultureInfo.CurrentCulture);
UpdateStripLines();
}
private Task> CreateSeriesAsync(IRegressionSolution solution) {
return Task.Run(() => {
var xvalues = internalDataset.GetDoubleValues(FreeVariable).ToList();
var yvalues = solution.Model.GetEstimatedValues(internalDataset, Enumerable.Range(0, internalDataset.Rows)).ToList();
var series = new Series { ChartType = SeriesChartType.Line };
series.Points.DataBindXY(xvalues, yvalues);
var confidenceBoundSolution = solution as IConfidenceBoundRegressionSolution;
Series confidenceIntervalSeries = null;
if (confidenceBoundSolution != null) {
var variances = confidenceBoundSolution.Model.GetEstimatedVariances(internalDataset, Enumerable.Range(0, internalDataset.Rows)).ToList();
var lower = yvalues.Zip(variances, (m, s2) => m - 1.96 * Math.Sqrt(s2)).ToList();
var upper = yvalues.Zip(variances, (m, s2) => m + 1.96 * Math.Sqrt(s2)).ToList();
confidenceIntervalSeries = new Series { ChartType = SeriesChartType.Range, YValuesPerPoint = 2 };
confidenceIntervalSeries.Points.DataBindXY(xvalues, lower, upper);
}
return Tuple.Create(series, confidenceIntervalSeries);
});
}
public void AddSolution(IRegressionSolution solution) {
if (!SolutionsCompatible(solutions.Concat(new[] { solution })))
throw new ArgumentException("The solution is not compatible with the problem data.");
if (solutions.Contains(solution)) return;
solutions.Add(solution);
RecalculateTrainingLimits();
UpdateChartAsync();
}
public void RemoveSolution(IRegressionSolution solution) {
if (!solutions.Remove(solution)) return;
RecalculateTrainingLimits();
UpdateChartAsync();
}
private static bool SolutionsCompatible(IEnumerable solutions) {
foreach (var solution1 in solutions) {
var variables1 = solution1.ProblemData.Dataset.DoubleVariables;
foreach (var solution2 in solutions) {
if (solution1 == solution2)
continue;
var variables2 = solution2.ProblemData.Dataset.DoubleVariables;
if (!variables1.All(variables2.Contains))
return false;
}
}
return true;
}
private void UpdateStripLines() {
var axisX = chart.ChartAreas[0].AxisX;
var lowerStripLine = axisX.StripLines[0];
var upperStripLine = axisX.StripLines[1];
lowerStripLine.IntervalOffset = axisX.Minimum;
lowerStripLine.StripWidth = trainingMin - axisX.Minimum;
upperStripLine.IntervalOffset = trainingMax;
upperStripLine.StripWidth = axisX.Maximum - trainingMax;
}
#region events
public event EventHandler VariableValueChanged;
public void OnVariableValueChanged(object sender, EventArgs args) {
var changed = VariableValueChanged;
if (changed == null) return;
changed(sender, args);
}
private void chart_AnnotationPositionChanged(object sender, EventArgs e) {
var annotation = VerticalLineAnnotation;
var x = annotation.X;
sharedFixedVariables.SetVariableValue(x, FreeVariable, 0);
chart.ChartAreas[0].AxisX.Title = FreeVariable + " : " + x.ToString("N3", CultureInfo.CurrentCulture);
chart.Update();
OnVariableValueChanged(this, EventArgs.Empty);
}
private void chart_AnnotationPositionChanging(object sender, AnnotationPositionChangingEventArgs e) {
//var step = (trainingMax - trainingMin) / drawingSteps;
//e.NewLocationX = step * (long)Math.Round(e.NewLocationX / step);
//var axisX = chart.ChartAreas[0].AxisX;
//if (e.NewLocationX > axisX.Maximum)
// e.NewLocationX = axisX.Maximum;
//if (e.NewLocationX < axisX.Minimum)
// e.NewLocationX = axisX.Minimum;
var annotation = VerticalLineAnnotation;
var x = annotation.X;
sharedFixedVariables.SetVariableValue(x, FreeVariable, 0);
chart.ChartAreas[0].AxisX.Title = FreeVariable + " : " + x.ToString("N3", CultureInfo.CurrentCulture);
chart.Update();
OnVariableValueChanged(this, EventArgs.Empty);
}
private void chart_MouseMove(object sender, MouseEventArgs e) {
chart.Cursor = chart.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
}
}