#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 } }