Free cookie consent management tool by TermsFeed Policy Generator

source: trunk/sources/HeuristicLab.Problems.DataAnalysis.Views/3.4/Controls/FactorGradientChart.cs @ 14826

Last change on this file since 14826 was 14826, checked in by gkronber, 7 years ago

#2650: merged the factors branch into trunk

File size: 21.1 KB
Line 
1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2016 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22using System;
23using System.Collections;
24using System.Collections.Generic;
25using System.Drawing;
26using System.Linq;
27using System.Threading;
28using System.Threading.Tasks;
29using System.Windows.Forms;
30using System.Windows.Forms.DataVisualization.Charting;
31using HeuristicLab.Common;
32using HeuristicLab.MainForm.WindowsForms;
33using HeuristicLab.Visualization.ChartControlsExtensions;
34
35namespace HeuristicLab.Problems.DataAnalysis.Views {
36  public partial class FactorGradientChart : UserControl, IGradientChart {
37    private ModifiableDataset sharedFixedVariables; // used for syncronising variable values between charts
38    private ModifiableDataset internalDataset; // holds the x values for each point drawn
39
40    private CancellationTokenSource cancelCurrentRecalculateSource;
41
42    private readonly List<IRegressionSolution> solutions;
43    private readonly Dictionary<IRegressionSolution, Series> seriesCache;
44    private readonly Dictionary<IRegressionSolution, Series> ciSeriesCache;
45
46    #region Properties
47    public string XAxisTitle {
48      get { return chart.ChartAreas[0].AxisX.Title; }
49      set { chart.ChartAreas[0].AxisX.Title = value; }
50    }
51
52    public string YAxisTitle {
53      get { return chart.ChartAreas[0].AxisY.Title; }
54      set { chart.ChartAreas[0].AxisY.Title = value; }
55    }
56
57    public bool ShowLegend {
58      get { return chart.Legends[0].Enabled; }
59      set { chart.Legends[0].Enabled = value; }
60    }
61    public bool ShowCursor {
62      get { return chart.Annotations[0].Visible; }
63      set {
64        chart.Annotations[0].Visible = value;
65        if (!value) chart.Titles[0].Text = string.Empty;
66      }
67    }
68
69    private int yAxisTicks = 5;
70    public int YAxisTicks {
71      get { return yAxisTicks; }
72      set {
73        if (value != yAxisTicks) {
74          yAxisTicks = value;
75          SetupAxis(chart.ChartAreas[0].AxisY, yMin, yMax, YAxisTicks, FixedYAxisMin, FixedYAxisMax);
76          RecalculateInternalDataset();
77        }
78      }
79    }
80    private double? fixedYAxisMin;
81    public double? FixedYAxisMin {
82      get { return fixedYAxisMin; }
83      set {
84        if ((value.HasValue && fixedYAxisMin.HasValue && !value.Value.IsAlmost(fixedYAxisMin.Value)) || (value.HasValue != fixedYAxisMin.HasValue)) {
85          fixedYAxisMin = value;
86          SetupAxis(chart.ChartAreas[0].AxisY, yMin, yMax, YAxisTicks, FixedYAxisMin, FixedYAxisMax);
87        }
88      }
89    }
90    private double? fixedYAxisMax;
91    public double? FixedYAxisMax {
92      get { return fixedYAxisMax; }
93      set {
94        if ((value.HasValue && fixedYAxisMax.HasValue && !value.Value.IsAlmost(fixedYAxisMax.Value)) || (value.HasValue != fixedYAxisMax.HasValue)) {
95          fixedYAxisMax = value;
96          SetupAxis(chart.ChartAreas[0].AxisY, yMin, yMax, YAxisTicks, FixedYAxisMin, FixedYAxisMax);
97        }
98      }
99    }
100
101    private string freeVariable;
102    public string FreeVariable {
103      get { return freeVariable; }
104      set {
105        if (value == freeVariable) return;
106        if (solutions.Any(s => !s.ProblemData.Dataset.StringVariables.Contains(value))) {
107          throw new ArgumentException("Variable does not exist in the ProblemData of the Solutions.");
108        }
109        freeVariable = value;
110        RecalculateInternalDataset();
111      }
112    }
113
114    private double yMin;
115    public double YMin {
116      get { return yMin; }
117    }
118    private double yMax;
119    public double YMax {
120      get { return yMax; }
121    }
122
123    public bool IsZoomed {
124      get { return chart.ChartAreas[0].AxisX.ScaleView.IsZoomed; }
125    }
126
127    internal ElementPosition InnerPlotPosition {
128      get { return chart.ChartAreas[0].InnerPlotPosition; }
129    }
130    #endregion
131
132    private List<string> variableValues;
133
134    public event EventHandler ChartPostPaint;
135
136    public FactorGradientChart() {
137      InitializeComponent();
138
139      solutions = new List<IRegressionSolution>();
140      seriesCache = new Dictionary<IRegressionSolution, Series>();
141      ciSeriesCache = new Dictionary<IRegressionSolution, Series>();
142
143      // Configure axis
144      chart.CustomizeAllChartAreas();
145      chart.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;
146      chart.ChartAreas[0].AxisX.ScaleView.Zoomable = true;
147      chart.ChartAreas[0].CursorX.Interval = 0;
148
149      chart.ChartAreas[0].CursorY.IsUserSelectionEnabled = true;
150      chart.ChartAreas[0].AxisY.ScaleView.Zoomable = true;
151      chart.ChartAreas[0].CursorY.Interval = 0;
152
153      Disposed += GradientChart_Disposed;
154    }
155
156    private void GradientChart_Disposed(object sender, EventArgs e) {
157      if (cancelCurrentRecalculateSource != null)
158        cancelCurrentRecalculateSource.Cancel();
159    }
160
161    public void Configure(IEnumerable<IRegressionSolution> solutions, ModifiableDataset sharedFixedVariables, string freeVariable, IList<string> variableValues, bool initializeAxisRanges = true) {
162      if (!SolutionsCompatible(solutions))
163        throw new ArgumentException("Solutions are not compatible with the problem data.");
164      this.freeVariable = freeVariable;
165      this.variableValues = new List<string>(variableValues);
166
167      this.solutions.Clear();
168      this.solutions.AddRange(solutions);
169
170      // add an event such that whenever a value is changed in the shared dataset,
171      // this change is reflected in the internal dataset (where the value becomes a whole column)
172      if (this.sharedFixedVariables != null)
173        this.sharedFixedVariables.ItemChanged -= sharedFixedVariables_ItemChanged;
174      this.sharedFixedVariables = sharedFixedVariables;
175      this.sharedFixedVariables.ItemChanged += sharedFixedVariables_ItemChanged;
176
177      RecalculateInternalDataset();
178
179      chart.Series.Clear();
180      seriesCache.Clear();
181      ciSeriesCache.Clear();
182      foreach (var solution in this.solutions) {
183        var series = CreateSeries(solution);
184        seriesCache.Add(solution, series.Item1);
185        if (series.Item2 != null)
186          ciSeriesCache.Add(solution, series.Item2);
187      }
188
189      InitSeriesData();
190      OrderAndColorSeries();
191
192    }
193
194    public async Task RecalculateAsync(bool updateOnFinish = true, bool resetYAxis = true) {
195      if (IsDisposed
196        || sharedFixedVariables == null || !solutions.Any() || string.IsNullOrEmpty(freeVariable)
197        || !variableValues.Any())
198        return;
199
200      calculationPendingTimer.Start();
201
202      // cancel previous recalculate call
203      if (cancelCurrentRecalculateSource != null)
204        cancelCurrentRecalculateSource.Cancel();
205      cancelCurrentRecalculateSource = new CancellationTokenSource();
206      var cancellationToken = cancelCurrentRecalculateSource.Token;
207
208      // Update series
209      try {
210        var limits = await UpdateAllSeriesDataAsync(cancellationToken);
211
212        yMin = limits.Lower;
213        yMax = limits.Upper;
214        // Set y-axis
215        if (resetYAxis)
216          SetupAxis(chart.ChartAreas[0].AxisY, yMin, yMax, YAxisTicks, FixedYAxisMin, FixedYAxisMax);
217
218        calculationPendingTimer.Stop();
219        calculationPendingLabel.Visible = false;
220        if (updateOnFinish)
221          Update();
222      } catch (OperationCanceledException) { } catch (AggregateException ae) {
223        if (!ae.InnerExceptions.Any(e => e is OperationCanceledException))
224          throw;
225      }
226    }
227
228    public void UpdateTitlePosition() {
229      var title = chart.Titles[0];
230      var plotArea = InnerPlotPosition;
231
232      title.Visible = plotArea.Width != 0;
233
234      title.Position.X = plotArea.X + (plotArea.Width / 2);
235    }
236
237    private void SetupAxis(Axis axis, double minValue, double maxValue, int ticks, double? fixedAxisMin, double? fixedAxisMax) {
238      if (minValue < maxValue) {
239        double axisMin, axisMax, axisInterval;
240        ChartUtil.CalculateAxisInterval(minValue, maxValue, ticks, out axisMin, out axisMax, out axisInterval);
241        axis.Minimum = fixedAxisMin ?? axisMin;
242        axis.Maximum = fixedAxisMax ?? axisMax;
243        axis.Interval = (axis.Maximum - axis.Minimum) / ticks;
244      }
245
246      try {
247        chart.ChartAreas[0].RecalculateAxesScale();
248      } catch (InvalidOperationException) {
249        // Can occur if eg. axis min == axis max
250      }
251    }
252
253
254    private void RecalculateInternalDataset() {
255      if (sharedFixedVariables == null)
256        return;
257
258      var factorValues = new List<string>(variableValues);
259
260      var variables = sharedFixedVariables.VariableNames.ToList();
261      var values = new List<IList>();
262      foreach (var varName in variables) {
263        if (varName == FreeVariable) {
264          values.Add(factorValues);
265        } else if (sharedFixedVariables.VariableHasType<double>(varName)) {
266          values.Add(Enumerable.Repeat(sharedFixedVariables.GetDoubleValue(varName, 0), factorValues.Count).ToList());
267        } else if (sharedFixedVariables.VariableHasType<string>(varName)) {
268          values.Add(Enumerable.Repeat(sharedFixedVariables.GetStringValue(varName, 0), factorValues.Count).ToList());
269        }
270      }
271
272      internalDataset = new ModifiableDataset(variables, values);
273    }
274
275    private Tuple<Series, Series> CreateSeries(IRegressionSolution solution) {
276      var series = new Series {
277        ChartType = SeriesChartType.Column,
278        Name = solution.ProblemData.TargetVariable + " " + solutions.IndexOf(solution),
279        XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.String
280      };
281      series.LegendText = series.Name;
282
283      Series confidenceIntervalSeries = null;
284      confidenceIntervalSeries = new Series {
285        ChartType = SeriesChartType.BoxPlot,
286        XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.String,
287        Color = Color.Black,
288        YValuesPerPoint = 5,
289        Name = "95% Conf. Interval " + series.Name,
290        IsVisibleInLegend = false
291      };
292      return Tuple.Create(series, confidenceIntervalSeries);
293    }
294
295    private void OrderAndColorSeries() {
296      chart.SuspendRepaint();
297
298      chart.Series.Clear();
299      // Add mean series for applying palette colors
300      foreach (var solution in solutions) {
301        chart.Series.Add(seriesCache[solution]);
302      }
303
304      chart.Palette = ChartColorPalette.BrightPastel;
305      chart.ApplyPaletteColors();
306      chart.Palette = ChartColorPalette.None;
307
308      // Add confidence interval series after its coresponding series for correct z index
309      foreach (var solution in solutions) {
310        Series ciSeries;
311        if (ciSeriesCache.TryGetValue(solution, out ciSeries)) {
312          int idx = chart.Series.IndexOf(seriesCache[solution]);
313          chart.Series.Insert(idx + 1, ciSeries);
314        }
315      }
316
317      chart.ResumeRepaint(true);
318    }
319
320    private async Task<DoubleLimit> UpdateAllSeriesDataAsync(CancellationToken cancellationToken) {
321      var updateTasks = solutions.Select(solution => UpdateSeriesDataAsync(solution, cancellationToken));
322
323      double min = double.MaxValue, max = double.MinValue;
324      foreach (var update in updateTasks) {
325        var limit = await update;
326        if (limit.Lower < min) min = limit.Lower;
327        if (limit.Upper > max) max = limit.Upper;
328      }
329
330      return new DoubleLimit(min, max);
331    }
332
333    private Task<DoubleLimit> UpdateSeriesDataAsync(IRegressionSolution solution, CancellationToken cancellationToken) {
334      return Task.Run(() => {
335        var yvalues = solution.Model.GetEstimatedValues(internalDataset, Enumerable.Range(0, internalDataset.Rows)).ToList();
336
337        double min = double.MaxValue, max = double.MinValue;
338
339        var series = seriesCache[solution];
340        for (int i = 0; i < variableValues.Count; i++) {
341          series.Points[i].SetValueXY(variableValues[i], yvalues[i]);
342          if (yvalues[i] < min) min = yvalues[i];
343          if (yvalues[i] > max) max = yvalues[i];
344        }
345        chart.Invalidate();
346
347        cancellationToken.ThrowIfCancellationRequested();
348
349        var confidenceBoundSolution = solution as IConfidenceRegressionSolution;
350        if (confidenceBoundSolution != null) {
351          var confidenceIntervalSeries = ciSeriesCache[solution];
352          var variances = confidenceBoundSolution.Model.GetEstimatedVariances(internalDataset, Enumerable.Range(0, internalDataset.Rows)).ToList();
353          for (int i = 0; i < variableValues.Count; i++) {
354            var lower = yvalues[i] - 1.96 * Math.Sqrt(variances[i]);
355            var upper = yvalues[i] + 1.96 * Math.Sqrt(variances[i]);
356            confidenceIntervalSeries.Points[i].SetValueXY(variableValues[i], lower, upper, yvalues[i], yvalues[i], yvalues[i]);
357            if (lower < min) min = lower;
358            if (upper > max) max = upper;
359          }
360          chart.Invalidate();
361        }
362
363        cancellationToken.ThrowIfCancellationRequested();
364        return new DoubleLimit(min, max);
365      }, cancellationToken);
366    }
367
368    private void InitSeriesData() {
369      if (internalDataset == null)
370        return;
371
372      foreach (var solution in solutions)
373        InitSeriesData(solution, variableValues);
374    }
375
376    private void InitSeriesData(IRegressionSolution solution, IList<string> values) {
377
378      var series = seriesCache[solution];
379      series.Points.SuspendUpdates();
380      series.Points.Clear();
381      for (int i = 0; i < values.Count; i++) {
382        series.Points.AddXY(values[i], 0.0);
383        series.Points.Last().ToolTip = values[i];
384      }
385
386      UpdateAllSeriesStyles(variableValues.IndexOf(sharedFixedVariables.GetStringValue(FreeVariable, 0)));
387      series.Points.ResumeUpdates();
388
389      Series confidenceIntervalSeries;
390      if (ciSeriesCache.TryGetValue(solution, out confidenceIntervalSeries)) {
391        confidenceIntervalSeries.Points.SuspendUpdates();
392        confidenceIntervalSeries.Points.Clear();
393        for (int i = 0; i < values.Count; i++)
394          confidenceIntervalSeries.Points.AddXY(values[i], 0.0, 0.0, 0.0, 0.0, 0.0);
395        confidenceIntervalSeries.Points.ResumeUpdates();
396      }
397    }
398
399    public async Task AddSolutionAsync(IRegressionSolution solution) {
400      if (!SolutionsCompatible(solutions.Concat(new[] { solution })))
401        throw new ArgumentException("The solution is not compatible with the problem data.");
402      if (solutions.Contains(solution))
403        return;
404
405      solutions.Add(solution);
406
407      var series = CreateSeries(solution);
408      seriesCache.Add(solution, series.Item1);
409      if (series.Item2 != null)
410        ciSeriesCache.Add(solution, series.Item2);
411
412      InitSeriesData(solution, variableValues);
413      OrderAndColorSeries();
414
415      await RecalculateAsync();
416      var args = new EventArgs<IRegressionSolution>(solution);
417      OnSolutionAdded(this, args);
418    }
419
420    public async Task RemoveSolutionAsync(IRegressionSolution solution) {
421      throw new NotSupportedException();
422    }
423
424    private static bool SolutionsCompatible(IEnumerable<IRegressionSolution> solutions) {
425      var refSolution = solutions.First();
426      var refSolVars = refSolution.ProblemData.Dataset.VariableNames;
427      foreach (var solution in solutions.Skip(1)) {
428        var variables1 = solution.ProblemData.Dataset.VariableNames;
429        if (!variables1.All(refSolVars.Contains))
430          return false;
431
432        foreach (var factorVar in variables1.Where(solution.ProblemData.Dataset.VariableHasType<string>)) {
433          var distinctVals = refSolution.ProblemData.Dataset.GetStringValues(factorVar).Distinct();
434          if (solution.ProblemData.Dataset.GetStringValues(factorVar).Any(val => !distinctVals.Contains(val))) return false;
435        }
436      }
437      return true;
438    }
439
440    #region Events
441    public event EventHandler<EventArgs<IRegressionSolution>> SolutionAdded;
442    public void OnSolutionAdded(object sender, EventArgs<IRegressionSolution> args) {
443      var added = SolutionAdded;
444      if (added == null) return;
445      added(sender, args);
446    }
447
448    public event EventHandler<EventArgs<IRegressionSolution>> SolutionRemoved;
449    public void OnSolutionRemoved(object sender, EventArgs<IRegressionSolution> args) {
450      var removed = SolutionRemoved;
451      if (removed == null) return;
452      removed(sender, args);
453    }
454
455    public event EventHandler VariableValueChanged;
456    public void OnVariableValueChanged(object sender, EventArgs args) {
457      var changed = VariableValueChanged;
458      if (changed == null) return;
459      changed(sender, args);
460    }
461
462    public event EventHandler ZoomChanged;
463    public void OnZoomChanged(object sender, EventArgs args) {
464      var changed = ZoomChanged;
465      if (changed == null) return;
466      changed(sender, args);
467    }
468
469    private void sharedFixedVariables_ItemChanged(object o, EventArgs<int, int> e) {
470      if (o != sharedFixedVariables) return;
471      var variables = sharedFixedVariables.VariableNames.ToList();
472      var rowIndex = e.Value;
473      var columnIndex = e.Value2;
474
475      var variableName = variables[columnIndex];
476      if (variableName == FreeVariable) return;
477      if (internalDataset.VariableHasType<double>(variableName)) {
478        var v = sharedFixedVariables.GetDoubleValue(variableName, rowIndex);
479        var values = new List<double>(Enumerable.Repeat(v, internalDataset.Rows));
480        internalDataset.ReplaceVariable(variableName, values);
481      } else if (internalDataset.VariableHasType<string>(variableName)) {
482        var v = sharedFixedVariables.GetStringValue(variableName, rowIndex);
483        var values = new List<String>(Enumerable.Repeat(v, internalDataset.Rows));
484        internalDataset.ReplaceVariable(variableName, values);
485      } else {
486        // unsupported type
487        throw new NotSupportedException();
488      }
489    }
490
491    private async void chart_DragDrop(object sender, DragEventArgs e) {
492      var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
493      if (data != null) {
494        var solution = data as IRegressionSolution;
495        if (!solutions.Contains(solution))
496          await AddSolutionAsync(solution);
497      }
498    }
499    private void chart_DragEnter(object sender, DragEventArgs e) {
500      if (!e.Data.GetDataPresent(HeuristicLab.Common.Constants.DragDropDataFormat)) return;
501      e.Effect = DragDropEffects.None;
502
503      var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
504      var regressionSolution = data as IRegressionSolution;
505      if (regressionSolution != null) {
506        e.Effect = DragDropEffects.Copy;
507      }
508    }
509
510    private void calculationPendingTimer_Tick(object sender, EventArgs e) {
511      calculationPendingLabel.Visible = true;
512      Update();
513    }
514
515    private void chart_SelectionRangeChanged(object sender, CursorEventArgs e) {
516      OnZoomChanged(this, EventArgs.Empty);
517    }
518
519    private void chart_Resize(object sender, EventArgs e) {
520      UpdateTitlePosition();
521    }
522
523    private void chart_PostPaint(object sender, ChartPaintEventArgs e) {
524      if (ChartPostPaint != null)
525        ChartPostPaint(this, EventArgs.Empty);
526    }
527    #endregion
528
529    private void chart_MouseClick(object sender, MouseEventArgs e) {
530      var hitTestResult = chart.HitTest(e.X, e.Y, ChartElementType.DataPoint);
531      if (hitTestResult != null && hitTestResult.ChartElementType == ChartElementType.DataPoint) {
532        var series = hitTestResult.Series;
533        var dataPoint = (DataPoint)hitTestResult.Object;
534        var idx = series.Points.IndexOf(dataPoint);
535        UpdateSelectedValue(variableValues[idx]);
536
537        UpdateAllSeriesStyles(idx);
538      }
539    }
540
541    private void UpdateAllSeriesStyles(int selectedValueIndex) {
542      if (ShowCursor) {
543        chart.Titles[0].Text = FreeVariable + " : " + variableValues[selectedValueIndex];
544        chart.Update();
545      }
546      foreach (var s in seriesCache.Values) {
547        if (s.ChartType == SeriesChartType.Column)
548          for (int i = 0; i < s.Points.Count; i++) {
549            if (i != selectedValueIndex) {
550              s.Points[i].BorderDashStyle = ChartDashStyle.NotSet;
551            } else {
552              s.Points[i].BorderDashStyle = ChartDashStyle.Dash;
553              s.Points[i].BorderColor = Color.Red;
554            }
555          }
556      }
557    }
558
559    private void UpdateSelectedValue(string variableValue) {
560      sharedFixedVariables.SetVariableValue(variableValue, FreeVariable, 0);
561      OnVariableValueChanged(this, EventArgs.Empty);
562    }
563  }
564}
565
Note: See TracBrowser for help on using the repository browser.