Index: /stable/HeuristicLab 3.3.sln
===================================================================
--- /stable/HeuristicLab 3.3.sln	(revision 15219)
+++ /stable/HeuristicLab 3.3.sln	(revision 15220)
@@ -94,4 +94,5 @@
 		{B94FFB82-43D4-40AB-9980-B03470ADF221} = {B94FFB82-43D4-40AB-9980-B03470ADF221}
 		{06D4A186-9319-48A0-BADE-A2058D462EEA} = {06D4A186-9319-48A0-BADE-A2058D462EEA}
+		{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9} = {4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}
 		{5B9B9E8C-2706-43C2-80B8-A08341E431F7} = {5B9B9E8C-2706-43C2-80B8-A08341E431F7}
 		{997F018D-AEA2-4F21-9301-82FAF6A5612D} = {997F018D-AEA2-4F21-9301-82FAF6A5612D}
@@ -448,4 +449,6 @@
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HeuristicLab.Problems.BinPacking-3.3", "HeuristicLab.Problems.BinPacking\3.3\HeuristicLab.Problems.BinPacking-3.3.csproj", "{F8A55094-3CD5-4034-B0CA-5BD7FFB016D4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HeuristicLab.Algorithms.DataAnalysis.Glmnet-3.4", "HeuristicLab.Algorithms.DataAnalysis.Glmnet\3.4\HeuristicLab.Algorithms.DataAnalysis.Glmnet-3.4.csproj", "{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HeuristicLab.Problems.Instances.DIMACS-3.3", "HeuristicLab.Problems.Instances.DIMACS\3.3\HeuristicLab.Problems.Instances.DIMACS-3.3.csproj", "{9943FF23-8619-459C-B84A-E7FBDCBA04E6}"
@@ -2179,4 +2182,16 @@
 		{F8A55094-3CD5-4034-B0CA-5BD7FFB016D4}.Release|x86.ActiveCfg = Release|x86
 		{F8A55094-3CD5-4034-B0CA-5BD7FFB016D4}.Release|x86.Build.0 = Release|x86
+		{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}.Debug|x64.ActiveCfg = Debug|x64
+		{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}.Debug|x64.Build.0 = Debug|x64
+		{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}.Debug|x86.ActiveCfg = Debug|x86
+		{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}.Debug|x86.Build.0 = Debug|x86
+		{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}.Release|Any CPU.Build.0 = Release|Any CPU
+		{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}.Release|x64.ActiveCfg = Release|x64
+		{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}.Release|x64.Build.0 = Release|x64
+		{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}.Release|x86.ActiveCfg = Release|x86
+		{4C7F6D8A-B279-4898-ACD6-7BE39111DEF9}.Release|x86.Build.0 = Release|x86
 		{9943FF23-8619-459C-B84A-E7FBDCBA04E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{9943FF23-8619-459C-B84A-E7FBDCBA04E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
Index: /stable/HeuristicLab.Analysis.Views/3.3/GanttDataView.Designer.cs
===================================================================
--- /stable/HeuristicLab.Analysis.Views/3.3/GanttDataView.Designer.cs	(revision 15220)
+++ /stable/HeuristicLab.Analysis.Views/3.3/GanttDataView.Designer.cs	(revision 15220)
@@ -0,0 +1,75 @@
+﻿#region License Information
+
+/* HeuristicLab
+ * Copyright (C) 2002-2017 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 <http://www.gnu.org/licenses/>.
+ */
+
+#endregion
+
+namespace HeuristicLab.Analysis.Views {
+  partial class GanttDataView {
+    /// <summary> 
+    /// Required designer variable.
+    /// </summary>
+    private System.ComponentModel.IContainer components = null;
+
+    /// <summary> 
+    /// Clean up any resources being used.
+    /// </summary>
+    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+    protected override void Dispose(bool disposing) {
+      if (disposing && (components != null)) {
+        components.Dispose();
+      }
+      base.Dispose(disposing);
+    }
+
+    #region Component Designer generated code
+
+    /// <summary> 
+    /// Required method for Designer support - do not modify 
+    /// the contents of this method with the code editor.
+    /// </summary>
+    private void InitializeComponent() {
+      this.ganttChart = new HeuristicLab.Visualization.ChartControlsExtensions.GanttChart();
+      this.SuspendLayout();
+      // 
+      // ganttChart
+      // 
+      this.ganttChart.Dock = System.Windows.Forms.DockStyle.Fill;
+      this.ganttChart.Location = new System.Drawing.Point(0, 0);
+      this.ganttChart.Name = "ganttChart";
+      this.ganttChart.Size = new System.Drawing.Size(367, 249);
+      this.ganttChart.TabIndex = 0;
+      // 
+      // GanttDataView
+      // 
+      this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
+      this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+      this.Controls.Add(this.ganttChart);
+      this.Name = "GanttDataView";
+      this.Size = new System.Drawing.Size(367, 249);
+      this.ResumeLayout(false);
+
+    }
+
+    #endregion
+
+    private Visualization.ChartControlsExtensions.GanttChart ganttChart;
+  }
+}
Index: /stable/HeuristicLab.Analysis.Views/3.3/GanttDataView.cs
===================================================================
--- /stable/HeuristicLab.Analysis.Views/3.3/GanttDataView.cs	(revision 15220)
+++ /stable/HeuristicLab.Analysis.Views/3.3/GanttDataView.cs	(revision 15220)
@@ -0,0 +1,177 @@
+﻿#region License Information
+
+/* HeuristicLab
+ * Copyright (C) 2002-2017 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 <http://www.gnu.org/licenses/>.
+ */
+
+#endregion
+
+using System;
+using System.Drawing;
+using System.Linq;
+using HeuristicLab.Collections;
+using HeuristicLab.Core.Views;
+using HeuristicLab.MainForm;
+
+namespace HeuristicLab.Analysis.Views {
+  [View("Gantt Data View")]
+  [Content(typeof(GanttData), IsDefaultView = true)]
+  public partial class GanttDataView : ItemView {
+
+    public new GanttData Content {
+      get { return (GanttData)base.Content; }
+      set { base.Content = value; }
+    }
+
+    public GanttDataView() {
+      InitializeComponent();
+    }
+
+    #region De/Register Content Events
+    protected override void RegisterContentEvents() {
+      base.RegisterContentEvents();
+      Content.Rows.ItemsAdded += GanttRowsOnChanged;
+      Content.Rows.ItemsRemoved += GanttRowsOnRemoved;
+      Content.Rows.ItemsReplaced += GanttRowsOnChanged;
+      Content.Rows.CollectionReset += GanttRowsOnChanged;
+      Content.CategoryColors.ItemsAdded += GanttCategoryColorsOnChanged;
+      Content.CategoryColors.ItemsRemoved += GanttCategoryColorsOnChanged;
+      Content.CategoryColors.ItemsReplaced += GanttCategoryColorsOnChanged;
+      Content.CategoryColors.CollectionReset += GanttCategoryColorsOnChanged;
+      foreach (var row in Content.Rows)
+        RegisterGanttRowEvents(row);
+    }
+
+    private void RegisterGanttRowEvents(GanttRow row) {
+      row.Items.ItemsAdded += GanttRowItemsOnChanged;
+      row.Items.ItemsReplaced += GanttRowItemsOnChanged;
+      row.Items.ItemsRemoved += GanttRowItemsOnRemoved;
+      row.Items.CollectionReset += GanttRowItemsOnChanged;
+      foreach (var item in row.Items)
+        RegisterGanttRowItemEvents(item);
+    }
+
+    private void RegisterGanttRowItemEvents(GanttItem item) {
+      item.PropertyChanged += GanttRowItemsItemOnChanged;
+    }
+
+    protected override void DeregisterContentEvents() {
+      base.DeregisterContentEvents();
+      Content.Rows.ItemsAdded -= GanttRowsOnChanged;
+      Content.Rows.ItemsRemoved -= GanttRowsOnRemoved;
+      Content.Rows.ItemsReplaced -= GanttRowsOnChanged;
+      Content.Rows.CollectionReset -= GanttRowsOnChanged;
+      Content.CategoryColors.ItemsAdded -= GanttCategoryColorsOnChanged;
+      Content.CategoryColors.ItemsRemoved -= GanttCategoryColorsOnChanged;
+      Content.CategoryColors.ItemsReplaced -= GanttCategoryColorsOnChanged;
+      Content.CategoryColors.CollectionReset -= GanttCategoryColorsOnChanged;
+      foreach (var row in Content.Rows)
+        DeregisterGanttRowEvents(row);
+    }
+
+    private void DeregisterGanttRowEvents(GanttRow row) {
+      row.Items.ItemsAdded -= GanttRowItemsOnChanged;
+      row.Items.ItemsReplaced -= GanttRowItemsOnChanged;
+      row.Items.ItemsRemoved -= GanttRowItemsOnRemoved;
+      row.Items.CollectionReset -= GanttRowItemsOnChanged;
+      foreach (var item in row.Items)
+        DeregisterGanttRowItemEvents(item);
+    }
+
+    private void DeregisterGanttRowItemEvents(GanttItem item) {
+      item.PropertyChanged -= GanttRowItemsItemOnChanged;
+    }
+    #endregion
+
+    protected override void OnContentChanged() {
+      base.OnContentChanged();
+
+      UpdateVisualization();
+    }
+
+    protected override void SetEnabledStateOfControls() {
+      base.SetEnabledStateOfControls();
+      ganttChart.Enabled = !Locked && !ReadOnly && Content != null;
+    }
+
+    private void UpdateVisualization() {
+      ganttChart.Reset();
+
+      if (Content == null) return;
+
+      var colors = (KnownColor[])Enum.GetValues(typeof(KnownColor));
+
+      var categories = Content.Rows.SelectMany(x => x.Items.Select(y => y.Category ?? string.Empty)).Distinct();
+      foreach (var cat in categories) {
+        Color color;
+        if (!Content.CategoryColors.TryGetValue(cat, out color)) {
+          // if no color is defined, a random color is chosen for each category
+          var rand = new Random(cat.GetHashCode());
+          color = Color.FromKnownColor(colors[rand.Next(colors.Length)]);
+        }
+        ganttChart.AddCategory(cat, color);
+      }
+      
+      ganttChart.Hide();
+      ganttChart.Title = string.IsNullOrWhiteSpace(Content.Name) ? null : Content.Name;
+      foreach (var row in Content.Rows) {
+        foreach (var item in row.Items) {
+          ganttChart.AddData(row.Name, item.Category ?? string.Empty, item.StartDate, item.EndDate, item.ToolTip, item.ShowLabel);
+        }
+      }
+      ganttChart.Show();
+    }
+
+    #region Content Event Handlers
+    private void GanttRowsOnChanged(object sender, CollectionItemsChangedEventArgs<GanttRow> e) {
+      foreach (var row in e.Items)
+        RegisterGanttRowEvents(row);
+      foreach (var row in e.OldItems)
+        DeregisterGanttRowEvents(row);
+
+      UpdateVisualization();
+    }
+    private void GanttRowsOnRemoved(object sender, CollectionItemsChangedEventArgs<GanttRow> e) {
+      foreach (var row in e.Items)
+        DeregisterGanttRowEvents(row);
+
+      UpdateVisualization();
+    }
+    private void GanttCategoryColorsOnChanged(object sender, EventArgs e) {
+      UpdateVisualization();
+    }
+    private void GanttRowItemsOnChanged(object sender, CollectionItemsChangedEventArgs<IndexedItem<GanttItem>> e) {
+      foreach (var item in e.Items)
+        RegisterGanttRowItemEvents(item.Value);
+      foreach (var item in e.OldItems)
+        DeregisterGanttRowItemEvents(item.Value);
+
+      UpdateVisualization();
+    }
+    private void GanttRowItemsOnRemoved(object sender, CollectionItemsChangedEventArgs<IndexedItem<GanttItem>> e) {
+      foreach (var item in e.Items)
+        DeregisterGanttRowItemEvents(item.Value);
+
+      UpdateVisualization();
+    }
+    private void GanttRowItemsItemOnChanged(object sender, EventArgs e) {
+      UpdateVisualization();
+    }
+    #endregion
+  }
+}
Index: /stable/HeuristicLab.Analysis.Views/3.3/HeuristicLab.Analysis.Views-3.3.csproj
===================================================================
--- /stable/HeuristicLab.Analysis.Views/3.3/HeuristicLab.Analysis.Views-3.3.csproj	(revision 15219)
+++ /stable/HeuristicLab.Analysis.Views/3.3/HeuristicLab.Analysis.Views-3.3.csproj	(revision 15220)
@@ -132,4 +132,16 @@
       <DependentUpon>DataTableView.cs</DependentUpon>
     </Compile>
+    <Compile Include="GanttDataView.cs">
+      <SubType>UserControl</SubType>
+    </Compile>
+    <Compile Include="GanttDataView.Designer.cs">
+      <DependentUpon>GanttDataView.cs</DependentUpon>
+    </Compile>
+    <Compile Include="IndexedDataTableView.cs">
+      <SubType>UserControl</SubType>
+    </Compile>
+    <Compile Include="IndexedDataTableView.Designer.cs">
+      <DependentUpon>IndexedDataTableView.cs</DependentUpon>
+    </Compile>
     <Compile Include="ScatterPlotView.cs">
       <SubType>UserControl</SubType>
Index: /stable/HeuristicLab.Analysis.Views/3.3/IndexedDataTableView.Designer.cs
===================================================================
--- /stable/HeuristicLab.Analysis.Views/3.3/IndexedDataTableView.Designer.cs	(revision 15220)
+++ /stable/HeuristicLab.Analysis.Views/3.3/IndexedDataTableView.Designer.cs	(revision 15220)
@@ -0,0 +1,125 @@
+#region License Information
+/* HeuristicLab
+ * Copyright (C) 2002-2015 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 <http://www.gnu.org/licenses/>.
+ */
+#endregion
+
+namespace HeuristicLab.Analysis.Views {
+  partial class IndexedDataTableView<T> {
+    /// <summary> 
+    /// Required designer variable.
+    /// </summary>
+    private System.ComponentModel.IContainer components = null;
+
+    /// <summary> 
+    /// Clean up any resources being used.
+    /// </summary>
+    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+    protected override void Dispose(bool disposing) {
+      if (disposing) {
+        if (components != null) components.Dispose();
+      }
+      base.Dispose(disposing);
+    }
+
+    #region Component Designer generated code
+
+    /// <summary> 
+    /// Required method for Designer support - do not modify 
+    /// the contents of this method with the code editor.
+    /// </summary>
+    private void InitializeComponent() {
+      System.Windows.Forms.DataVisualization.Charting.ChartArea chartArea1 = new System.Windows.Forms.DataVisualization.Charting.ChartArea();
+      System.Windows.Forms.DataVisualization.Charting.Legend legend1 = new System.Windows.Forms.DataVisualization.Charting.Legend();
+      System.Windows.Forms.DataVisualization.Charting.Series series1 = new System.Windows.Forms.DataVisualization.Charting.Series();
+      System.Windows.Forms.DataVisualization.Charting.Title title1 = new System.Windows.Forms.DataVisualization.Charting.Title();
+      this.chart = new HeuristicLab.Visualization.ChartControlsExtensions.EnhancedChart();
+      ((System.ComponentModel.ISupportInitialize)(this.errorProvider)).BeginInit();
+      ((System.ComponentModel.ISupportInitialize)(this.chart)).BeginInit();
+      this.SuspendLayout();
+      // 
+      // nameTextBox
+      // 
+      this.errorProvider.SetIconAlignment(this.nameTextBox, System.Windows.Forms.ErrorIconAlignment.MiddleLeft);
+      this.errorProvider.SetIconPadding(this.nameTextBox, 2);
+      this.nameTextBox.Location = new System.Drawing.Point(55, 0);
+      this.nameTextBox.Size = new System.Drawing.Size(279, 20);
+      // 
+      // infoLabel
+      // 
+      this.infoLabel.Location = new System.Drawing.Point(340, 3);
+      // 
+      // chart
+      // 
+      this.chart.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
+                  | System.Windows.Forms.AnchorStyles.Left)
+                  | System.Windows.Forms.AnchorStyles.Right)));
+      this.chart.BorderlineColor = System.Drawing.Color.Black;
+      this.chart.BorderlineDashStyle = System.Windows.Forms.DataVisualization.Charting.ChartDashStyle.Solid;
+      chartArea1.AxisX.Minimum = 0D;
+      chartArea1.CursorX.IsUserEnabled = true;
+      chartArea1.CursorX.IsUserSelectionEnabled = true;
+      chartArea1.CursorY.IsUserEnabled = true;
+      chartArea1.CursorY.IsUserSelectionEnabled = true;
+      chartArea1.Name = "Default";
+      this.chart.ChartAreas.Add(chartArea1);
+      legend1.Alignment = System.Drawing.StringAlignment.Center;
+      legend1.Docking = System.Windows.Forms.DataVisualization.Charting.Docking.Top;
+      legend1.Name = "Default";
+      this.chart.Legends.Add(legend1);
+      this.chart.Location = new System.Drawing.Point(0, 26);
+      this.chart.Name = "chart";
+      series1.ChartArea = "Default";
+      series1.Legend = "Default";
+      series1.Name = "Default";
+      this.chart.Series.Add(series1);
+      this.chart.Size = new System.Drawing.Size(359, 248);
+      this.chart.TabIndex = 3;
+      this.chart.Text = "chart";
+      title1.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
+      title1.Name = "Default";
+      title1.Text = "Title";
+      this.chart.Titles.Add(title1);
+      this.chart.CustomizeLegend += new System.EventHandler<System.Windows.Forms.DataVisualization.Charting.CustomizeLegendEventArgs>(this.chart_CustomizeLegend);
+      this.chart.MouseDown += new System.Windows.Forms.MouseEventHandler(this.chart_MouseDown);
+      this.chart.MouseMove += new System.Windows.Forms.MouseEventHandler(this.chart_MouseMove);
+      // 
+      // DataTableView
+      // 
+      this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+      this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+      this.Controls.Add(this.chart);
+      this.Name = "DataTableView";
+      this.Size = new System.Drawing.Size(359, 274);
+      this.Controls.SetChildIndex(this.infoLabel, 0);
+      this.Controls.SetChildIndex(this.chart, 0);
+      this.Controls.SetChildIndex(this.nameLabel, 0);
+      this.Controls.SetChildIndex(this.nameTextBox, 0);
+      ((System.ComponentModel.ISupportInitialize)(this.errorProvider)).EndInit();
+      ((System.ComponentModel.ISupportInitialize)(this.chart)).EndInit();
+      this.ResumeLayout(false);
+      this.PerformLayout();
+
+    }
+
+    #endregion
+
+    protected HeuristicLab.Visualization.ChartControlsExtensions.EnhancedChart chart;
+
+  }
+}
Index: /stable/HeuristicLab.Analysis.Views/3.3/IndexedDataTableView.cs
===================================================================
--- /stable/HeuristicLab.Analysis.Views/3.3/IndexedDataTableView.cs	(revision 15220)
+++ /stable/HeuristicLab.Analysis.Views/3.3/IndexedDataTableView.cs	(revision 15220)
@@ -0,0 +1,609 @@
+#region License Information
+/* HeuristicLab
+ * Copyright (C) 2002-2015 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 <http://www.gnu.org/licenses/>.
+ */
+#endregion
+
+using HeuristicLab.Collections;
+using HeuristicLab.Core.Views;
+using HeuristicLab.MainForm;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Windows.Forms;
+using System.Windows.Forms.DataVisualization.Charting;
+
+namespace HeuristicLab.Analysis.Views {
+  [View("IndexedDataTable View")]
+  [Content(typeof(IndexedDataTable<>), true)]
+  public partial class IndexedDataTableView<T> : NamedItemView {
+    protected List<Series> invisibleSeries;
+    protected Dictionary<IObservableList<Tuple<T, double>>, IndexedDataRow<T>> valuesRowsTable;
+
+    public new IndexedDataTable<T> Content {
+      get { return (IndexedDataTable<T>)base.Content; }
+      set { base.Content = value; }
+    }
+
+    public IndexedDataTableView() {
+      InitializeComponent();
+      valuesRowsTable = new Dictionary<IObservableList<Tuple<T, double>>, IndexedDataRow<T>>();
+      invisibleSeries = new List<Series>();
+      chart.CustomizeAllChartAreas();
+      chart.ChartAreas[0].CursorX.Interval = 1;
+      chart.SuppressExceptions = true;
+    }
+
+    #region Event Handler Registration
+    protected override void DeregisterContentEvents() {
+      foreach (var row in Content.Rows)
+        DeregisterDataRowEvents(row);
+      Content.VisualPropertiesChanged -= new EventHandler(Content_VisualPropertiesChanged);
+      Content.Rows.ItemsAdded -= new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(Rows_ItemsAdded);
+      Content.Rows.ItemsRemoved -= new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(Rows_ItemsRemoved);
+      Content.Rows.ItemsReplaced -= new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(Rows_ItemsReplaced);
+      Content.Rows.CollectionReset -= new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(Rows_CollectionReset);
+      base.DeregisterContentEvents();
+    }
+
+    protected override void RegisterContentEvents() {
+      base.RegisterContentEvents();
+      Content.VisualPropertiesChanged += new EventHandler(Content_VisualPropertiesChanged);
+      Content.Rows.ItemsAdded += new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(Rows_ItemsAdded);
+      Content.Rows.ItemsRemoved += new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(Rows_ItemsRemoved);
+      Content.Rows.ItemsReplaced += new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(Rows_ItemsReplaced);
+      Content.Rows.CollectionReset += new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(Rows_CollectionReset);
+      foreach (var row in Content.Rows)
+        RegisterDataRowEvents(row);
+    }
+
+    /// <summary>
+    /// Automatically called for every existing data row and whenever a data row is added
+    /// to the data table. Do not call this method directly.
+    /// </summary>
+    /// <param name="row">The DataRow that was added.</param>
+    protected virtual void RegisterDataRowEvents(IndexedDataRow<T> row) {
+      row.NameChanged += new EventHandler(Row_NameChanged);
+      row.VisualPropertiesChanged += new EventHandler(Row_VisualPropertiesChanged);
+      valuesRowsTable.Add(row.Values, row);
+      row.Values.ItemsAdded += new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_ItemsAdded);
+      row.Values.ItemsRemoved += new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_ItemsRemoved);
+      row.Values.ItemsReplaced += new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_ItemsReplaced);
+      row.Values.ItemsMoved += new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_ItemsMoved);
+      row.Values.CollectionReset += new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_CollectionReset);
+    }
+
+    /// <summary>
+    /// Automatically called for every data row that is removed from the DataTable. Do
+    /// not directly call this method.
+    /// </summary>
+    /// <param name="row">The DataRow that was removed.</param>
+    protected virtual void DeregisterDataRowEvents(IndexedDataRow<T> row) {
+      row.Values.ItemsAdded -= new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_ItemsAdded);
+      row.Values.ItemsRemoved -= new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_ItemsRemoved);
+      row.Values.ItemsReplaced -= new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_ItemsReplaced);
+      row.Values.ItemsMoved -= new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_ItemsMoved);
+      row.Values.CollectionReset -= new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_CollectionReset);
+      valuesRowsTable.Remove(row.Values);
+      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.ChartAreas[0].AxisY2.Title = string.Empty;
+      chart.ChartAreas[0].AxisX.IsLogarithmic = false;
+      chart.ChartAreas[0].AxisX2.IsLogarithmic = false;
+      chart.ChartAreas[0].AxisY.IsLogarithmic = false;
+      chart.ChartAreas[0].AxisY2.IsLogarithmic = false;
+      chart.Series.Clear();
+      if (Content != null) {
+        chart.Titles[0].Text = Content.Name;
+        foreach (var row in Content.Rows)
+          AddDataRow(row);
+        ConfigureChartArea(chart.ChartAreas[0]);
+        RecalculateAxesScale(chart.ChartAreas[0]);
+      }
+    }
+
+    protected override void SetEnabledStateOfControls() {
+      base.SetEnabledStateOfControls();
+      chart.Enabled = Content != null;
+    }
+
+    /// <summary>
+    /// Add the DataRow as a series to the chart.
+    /// </summary>
+    /// <param name="row">DataRow to add as series to the chart.</param>
+    protected virtual void AddDataRow(IndexedDataRow<T> row) {
+      if (row.Values.Count == 0) return;
+
+      Series series = new Series(row.Name);
+      if (row.VisualProperties.DisplayName.Trim() != String.Empty) series.LegendText = row.VisualProperties.DisplayName;
+      else series.LegendText = row.Name;
+      ConfigureSeries(series, row);
+      FillSeriesWithRowValues(series, row);
+
+      chart.Series.Add(series);
+      ConfigureChartArea(chart.ChartAreas[0]);
+      RecalculateAxesScale(chart.ChartAreas[0]);
+      UpdateYCursorInterval();
+    }
+
+    private void ConfigureSeries(Series series, IndexedDataRow<T> row) {
+      RemoveCustomPropertyIfExists(series, "PointWidth");
+      series.BorderWidth = 1;
+      series.BorderDashStyle = ChartDashStyle.Solid;
+      series.BorderColor = Color.Empty;
+
+      series.Color = row.VisualProperties.Color;
+      series.IsVisibleInLegend = row.VisualProperties.IsVisibleInLegend;
+
+      series.SmartLabelStyle.Enabled = true;
+      series.SmartLabelStyle.AllowOutsidePlotArea = LabelOutsidePlotAreaStyle.No;
+      series.SmartLabelStyle.CalloutLineAnchorCapStyle = LineAnchorCapStyle.None;
+      series.SmartLabelStyle.CalloutLineColor = series.Color;
+      series.SmartLabelStyle.CalloutLineWidth = 2;
+      series.SmartLabelStyle.CalloutStyle = LabelCalloutStyle.Underlined;
+      series.SmartLabelStyle.IsOverlappedHidden = false;
+      series.SmartLabelStyle.MaxMovingDistance = 200;
+
+      switch (row.VisualProperties.ChartType) {
+        case DataRowVisualProperties.DataRowChartType.Line:
+          series.ChartType = SeriesChartType.FastLine;
+          series.BorderWidth = row.VisualProperties.LineWidth;
+          series.BorderDashStyle = ConvertLineStyle(row.VisualProperties.LineStyle);
+          break;
+        case DataRowVisualProperties.DataRowChartType.Bars:
+          // Bar is incompatible with anything but Bar and StackedBar*
+          if (!chart.Series.Any(x => x.ChartType != SeriesChartType.Bar && x.ChartType != SeriesChartType.StackedBar && x.ChartType != SeriesChartType.StackedBar100)) {
+            series.ChartType = SeriesChartType.Bar;
+            chart.ChartAreas[0].AxisX.Interval = 1;
+          } else {
+            series.ChartType = SeriesChartType.FastPoint; //default
+            row.VisualProperties.ChartType = DataRowVisualProperties.DataRowChartType.Points;
+          }
+          break;
+        case DataRowVisualProperties.DataRowChartType.Columns:
+          series.ChartType = SeriesChartType.Column;
+          break;
+        case DataRowVisualProperties.DataRowChartType.Points:
+          series.ChartType = SeriesChartType.FastPoint;
+          break;
+        case DataRowVisualProperties.DataRowChartType.StepLine:
+          series.ChartType = SeriesChartType.StepLine;
+          series.BorderWidth = row.VisualProperties.LineWidth;
+          series.BorderDashStyle = ConvertLineStyle(row.VisualProperties.LineStyle);
+          break;
+        default:
+          series.ChartType = SeriesChartType.FastPoint;
+          break;
+      }
+      series.YAxisType = row.VisualProperties.SecondYAxis ? AxisType.Secondary : AxisType.Primary;
+      series.XAxisType = row.VisualProperties.SecondXAxis ? AxisType.Secondary : AxisType.Primary;
+      if (row.VisualProperties.DisplayName.Trim() != String.Empty) series.LegendText = row.VisualProperties.DisplayName;
+      else series.LegendText = row.Name;
+      series.ToolTip = series.LegendText + " X = #VALX, Y = #VALY";
+    }
+
+    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;
+
+      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;
+
+      if (Content.VisualProperties.AxisTitleFont != null) area.AxisX2.TitleFont = Content.VisualProperties.AxisTitleFont;
+      if (!Content.VisualProperties.AxisTitleColor.IsEmpty) area.AxisX2.TitleForeColor = Content.VisualProperties.AxisTitleColor;
+      area.AxisX2.Title = Content.VisualProperties.SecondXAxisTitle;
+
+      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;
+
+      if (Content.VisualProperties.AxisTitleFont != null) area.AxisY2.TitleFont = Content.VisualProperties.AxisTitleFont;
+      if (!Content.VisualProperties.AxisTitleColor.IsEmpty) area.AxisY2.TitleForeColor = Content.VisualProperties.AxisTitleColor;
+      area.AxisY2.Title = Content.VisualProperties.SecondYAxisTitle;
+
+      if (typeof(T).Equals(typeof(DateTime))) {
+        area.AxisX.IntervalType = DateTimeIntervalType.Hours;
+        area.AxisX.LabelStyle.Format = "dd.MM.yyyy HH:mm";
+        area.AxisX.IntervalAutoMode = IntervalAutoMode.VariableCount;
+      }
+    }
+
+    private void RecalculateAxesScale(ChartArea area) {
+      // Reset the axes bounds so that RecalculateAxesScale() will assign new bounds
+      foreach (Axis a in area.Axes) {
+        a.Minimum = double.NaN;
+        a.Maximum = double.NaN;
+      }
+
+      if (chart.Series.Any(x => x.Enabled && x.XAxisType == AxisType.Primary && (x.Points.Count == 0 || x.Points.Any(y => y.XValue <= 0))))
+        area.AxisX.IsLogarithmic = false;
+      else area.AxisX.IsLogarithmic = Content.VisualProperties.XAxisLogScale;
+      if (chart.Series.Any(x => x.Enabled && x.XAxisType == AxisType.Secondary && (x.Points.Count == 0 || x.Points.Any(y => y.XValue <= 0))))
+        area.AxisX2.IsLogarithmic = false;
+      else area.AxisX2.IsLogarithmic = Content.VisualProperties.SecondXAxisLogScale;
+
+      if (chart.Series.Any(x => x.Enabled && x.YAxisType == AxisType.Primary && (x.Points.Count == 0 || x.Points.Any(y => y.YValues.Min() <= 0))))
+        area.AxisY.IsLogarithmic = false;
+      else area.AxisY.IsLogarithmic = Content.VisualProperties.YAxisLogScale;
+      if (chart.Series.Any(x => x.Enabled && x.YAxisType == AxisType.Secondary && (x.Points.Count == 0 || x.Points.Any(y => y.YValues.Min() <= 0))))
+        area.AxisY2.IsLogarithmic = false;
+      else area.AxisY2.IsLogarithmic = Content.VisualProperties.SecondYAxisLogScale;
+
+      area.RecalculateAxesScale();
+      area.AxisX.IsMarginVisible = false;
+      area.AxisX2.IsMarginVisible = false;
+
+      area.AxisX.IsStartedFromZero = Content.Rows.Where(x => !x.VisualProperties.SecondXAxis).Any(x => x.VisualProperties.StartIndexZero);
+      area.AxisX2.IsStartedFromZero = Content.Rows.Where(x => x.VisualProperties.SecondXAxis).Any(x => x.VisualProperties.StartIndexZero);
+
+      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.SecondXAxisMinimumAuto && !double.IsNaN(Content.VisualProperties.SecondXAxisMinimumFixedValue)) area.AxisX2.Minimum = Content.VisualProperties.SecondXAxisMinimumFixedValue;
+      if (!Content.VisualProperties.SecondXAxisMaximumAuto && !double.IsNaN(Content.VisualProperties.SecondXAxisMaximumFixedValue)) area.AxisX2.Maximum = Content.VisualProperties.SecondXAxisMaximumFixedValue;
+      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;
+      if (!Content.VisualProperties.SecondYAxisMinimumAuto && !double.IsNaN(Content.VisualProperties.SecondYAxisMinimumFixedValue)) area.AxisY2.Minimum = Content.VisualProperties.SecondYAxisMinimumFixedValue;
+      if (!Content.VisualProperties.SecondYAxisMaximumAuto && !double.IsNaN(Content.VisualProperties.SecondYAxisMaximumFixedValue)) area.AxisY2.Maximum = Content.VisualProperties.SecondYAxisMaximumFixedValue;
+      if (area.AxisX.Minimum >= area.AxisX.Maximum) area.AxisX.Maximum = area.AxisX.Minimum + 1;
+      if (area.AxisX2.Minimum >= area.AxisX2.Maximum) area.AxisX2.Maximum = area.AxisX2.Minimum + 1;
+      if (area.AxisY.Minimum >= area.AxisY.Maximum) area.AxisY.Maximum = area.AxisY.Minimum + 1;
+      if (area.AxisY2.Minimum >= area.AxisY2.Maximum) area.AxisY2.Maximum = area.AxisY2.Minimum + 1;
+    }
+
+    /// <summary>
+    /// Set the Y Cursor interval to visible points of enabled series.
+    /// </summary>
+    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;
+    }
+
+
+    /// <summary>
+    /// Remove the corresponding series for a certain DataRow.
+    /// </summary>
+    /// <param name="row">DataRow which series should be removed.</param>
+    protected virtual void RemoveDataRow(IndexedDataRow<T> row) {
+      if (chart.Series.All(x => x.Name != row.Name)) return;
+      Series series = chart.Series[row.Name];
+      chart.Series.Remove(series);
+      if (invisibleSeries.Contains(series))
+        invisibleSeries.Remove(series);
+      RecalculateAxesScale(chart.ChartAreas[0]);
+    }
+
+    #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 {
+        chart.Titles[0].Text = 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<IndexedDataRow<T>> e) {
+      if (InvokeRequired)
+        Invoke(new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(Rows_ItemsAdded), sender, e);
+      else {
+        foreach (var row in e.Items) {
+          AddDataRow(row);
+          RegisterDataRowEvents(row);
+        }
+      }
+    }
+    private void Rows_ItemsRemoved(object sender, CollectionItemsChangedEventArgs<IndexedDataRow<T>> e) {
+      if (InvokeRequired)
+        Invoke(new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(Rows_ItemsRemoved), sender, e);
+      else {
+        foreach (var row in e.Items) {
+          DeregisterDataRowEvents(row);
+          RemoveDataRow(row);
+        }
+      }
+    }
+    private void Rows_ItemsReplaced(object sender, CollectionItemsChangedEventArgs<IndexedDataRow<T>> e) {
+      if (InvokeRequired)
+        Invoke(new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(Rows_ItemsReplaced), sender, e);
+      else {
+        foreach (var row in e.OldItems) {
+          DeregisterDataRowEvents(row);
+          RemoveDataRow(row);
+        }
+        foreach (var row in e.Items) {
+          AddDataRow(row);
+          RegisterDataRowEvents(row);
+        }
+      }
+    }
+    private void Rows_CollectionReset(object sender, CollectionItemsChangedEventArgs<IndexedDataRow<T>> e) {
+      if (InvokeRequired)
+        Invoke(new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(Rows_CollectionReset), sender, e);
+      else {
+        foreach (var row in e.OldItems) {
+          DeregisterDataRowEvents(row);
+          RemoveDataRow(row);
+        }
+        foreach (var row in e.Items) {
+          AddDataRow(row);
+          RegisterDataRowEvents(row);
+        }
+      }
+    }
+    #endregion
+    #region Row Event Handlers
+    private void Row_VisualPropertiesChanged(object sender, EventArgs e) {
+      if (InvokeRequired)
+        Invoke(new EventHandler(Row_VisualPropertiesChanged), sender, e);
+      else {
+        var row = (IndexedDataRow<T>)sender;
+        Series series = chart.Series[row.Name];
+        series.Points.Clear();
+        ConfigureSeries(series, row);
+        FillSeriesWithRowValues(series, row);
+        RecalculateAxesScale(chart.ChartAreas[0]);
+      }
+    }
+    private void Row_NameChanged(object sender, EventArgs e) {
+      if (InvokeRequired)
+        Invoke(new EventHandler(Row_NameChanged), sender, e);
+      else {
+        var row = (IndexedDataRow<T>)sender;
+        chart.Series[row.Name].Name = row.Name;
+      }
+    }
+    #endregion
+    #region Values Event Handlers
+    private void Values_ItemsAdded(object sender, CollectionItemsChangedEventArgs<IndexedItem<Tuple<T, double>>> e) {
+      if (InvokeRequired)
+        Invoke(new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_ItemsAdded), sender, e);
+      else {
+        IndexedDataRow<T> row = null;
+        valuesRowsTable.TryGetValue((IObservableList<Tuple<T, double>>)sender, out row);
+        if (row != null) {
+          Series rowSeries = chart.Series[row.Name];
+          if (!invisibleSeries.Contains(rowSeries)) {
+            rowSeries.Points.Clear();
+            FillSeriesWithRowValues(rowSeries, row);
+            RecalculateAxesScale(chart.ChartAreas[0]);
+            UpdateYCursorInterval();
+          }
+        } else AddDataRow((IndexedDataRow<T>)sender);
+      }
+    }
+    private void Values_ItemsRemoved(object sender, CollectionItemsChangedEventArgs<IndexedItem<Tuple<T, double>>> e) {
+      if (InvokeRequired)
+        Invoke(new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_ItemsRemoved), sender, e);
+      else {
+        IndexedDataRow<T> row = null;
+        valuesRowsTable.TryGetValue((IObservableList<Tuple<T, double>>)sender, out row);
+        if (row != null) {
+          if (row.Values.Count == 0) {
+            RemoveDataRow(row);
+          } else {
+            Series rowSeries = chart.Series[row.Name];
+            if (!invisibleSeries.Contains(rowSeries)) {
+              rowSeries.Points.Clear();
+              FillSeriesWithRowValues(rowSeries, row);
+              RecalculateAxesScale(chart.ChartAreas[0]);
+              UpdateYCursorInterval();
+            }
+          }
+        }
+      }
+    }
+    private void Values_ItemsReplaced(object sender, CollectionItemsChangedEventArgs<IndexedItem<Tuple<T, double>>> e) {
+      if (InvokeRequired)
+        Invoke(new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_ItemsReplaced), sender, e);
+      else {
+        IndexedDataRow<T> row = null;
+        valuesRowsTable.TryGetValue((IObservableList<Tuple<T, double>>)sender, out row);
+        if (row != null) {
+          if (row.Values.Count == 0) {
+            RemoveDataRow(row);
+          } else {
+            Series rowSeries = chart.Series[row.Name];
+            if (!invisibleSeries.Contains(rowSeries)) {
+              if (row.VisualProperties.ChartType == DataRowVisualProperties.DataRowChartType.Histogram) {
+                rowSeries.Points.Clear();
+                FillSeriesWithRowValues(rowSeries, row);
+              } else {
+                foreach (IndexedItem<Tuple<T, double>> item in e.Items) {
+                  rowSeries.Points[item.Index].SetValueXY(item.Value.Item1, item.Value.Item2);
+                }
+              }
+              RecalculateAxesScale(chart.ChartAreas[0]);
+              UpdateYCursorInterval();
+            }
+          }
+        }
+      }
+    }
+    private void Values_ItemsMoved(object sender, CollectionItemsChangedEventArgs<IndexedItem<Tuple<T, double>>> e) {
+      if (InvokeRequired)
+        Invoke(new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_ItemsMoved), sender, e);
+      else {
+        IndexedDataRow<T> row = null;
+        valuesRowsTable.TryGetValue((IObservableList<Tuple<T, double>>)sender, out row);
+        if (row != null) {
+          Series rowSeries = chart.Series[row.Name];
+          if (!invisibleSeries.Contains(rowSeries)) {
+            rowSeries.Points.Clear();
+            FillSeriesWithRowValues(rowSeries, row);
+            RecalculateAxesScale(chart.ChartAreas[0]);
+            UpdateYCursorInterval();
+          }
+        }
+      }
+    }
+
+    private void Values_CollectionReset(object sender, CollectionItemsChangedEventArgs<IndexedItem<Tuple<T, double>>> e) {
+      if (InvokeRequired)
+        Invoke(new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(Values_CollectionReset), sender, e);
+      else {
+        IndexedDataRow<T> row = null;
+        valuesRowsTable.TryGetValue((IObservableList<Tuple<T, double>>)sender, out row);
+        if (row != null) {
+          if (row.Values.Count == 0) {
+            RemoveDataRow(row);
+          } else {
+            Series rowSeries = chart.Series[row.Name];
+            if (!invisibleSeries.Contains(rowSeries)) {
+              rowSeries.Points.Clear();
+              FillSeriesWithRowValues(rowSeries, row);
+              RecalculateAxesScale(chart.ChartAreas[0]);
+              UpdateYCursorInterval();
+            }
+          }
+        }
+      }
+    }
+    #endregion
+    #endregion
+
+    #region Chart Event Handlers
+    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);
+      } else {
+        invisibleSeries.Remove(series);
+        if (Content != null) {
+
+          var row = (from r in Content.Rows
+                     where r.Name == series.Name
+                     select r).Single();
+          FillSeriesWithRowValues(series, row);
+          this.chart.Legends[series.Legend].ForeColor = Color.Black;
+          RecalculateAxesScale(chart.ChartAreas[0]);
+          UpdateYCursorInterval();
+        }
+      }
+    }
+
+    private void FillSeriesWithRowValues(Series series, IndexedDataRow<T> row) {
+      for (int i = 0; i < row.Values.Count; i++) {
+        var value = row.Values[i];
+        if (IsInvalidValue(value.Item2)) continue;
+        var point = new DataPoint();
+        point.SetValueXY(value.Item1, value.Item2);
+        if (i == row.Values.Count - 1) {
+          point.Label = series.Name;
+          point.MarkerStyle = MarkerStyle.Cross;
+          point.MarkerSize = 15;
+          point.MarkerBorderWidth = 1;
+        }
+        series.Points.Add(point);
+      }
+    }
+
+    #region Helpers
+    protected void RemoveCustomPropertyIfExists(Series series, string property) {
+      if (series.IsCustomPropertySet(property)) series.DeleteCustomProperty(property);
+    }
+
+    private ChartDashStyle ConvertLineStyle(DataRowVisualProperties.DataRowLineStyle dataRowLineStyle) {
+      switch (dataRowLineStyle) {
+        case DataRowVisualProperties.DataRowLineStyle.Dash:
+          return ChartDashStyle.Dash;
+        case DataRowVisualProperties.DataRowLineStyle.DashDot:
+          return ChartDashStyle.DashDot;
+        case DataRowVisualProperties.DataRowLineStyle.DashDotDot:
+          return ChartDashStyle.DashDotDot;
+        case DataRowVisualProperties.DataRowLineStyle.Dot:
+          return ChartDashStyle.Dot;
+        case DataRowVisualProperties.DataRowLineStyle.NotSet:
+          return ChartDashStyle.NotSet;
+        case DataRowVisualProperties.DataRowLineStyle.Solid:
+          return ChartDashStyle.Solid;
+        default:
+          return ChartDashStyle.NotSet;
+      }
+    }
+
+    /// <summary>
+    /// Determines whether a double value can be displayed (converted to Decimal and not an NaN).
+    /// </summary>
+    /// <param name="x">The number to check.</param>
+    /// <returns><code>true</code> if the value can be safely shwon in the chart,
+    /// <code>false</code> otherwise.</returns>
+    protected static bool IsInvalidValue(double x) {
+      return double.IsNaN(x) || x < (double)decimal.MinValue || x > (double)decimal.MaxValue;
+    }
+    #endregion
+  }
+}
Index: /stable/HeuristicLab.Analysis/3.3/DataVisualization/GanttData.cs
===================================================================
--- /stable/HeuristicLab.Analysis/3.3/DataVisualization/GanttData.cs	(revision 15220)
+++ /stable/HeuristicLab.Analysis/3.3/DataVisualization/GanttData.cs	(revision 15220)
@@ -0,0 +1,188 @@
+﻿#region License Information
+
+/* HeuristicLab
+ * Copyright (C) 2002-2017 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 <http://www.gnu.org/licenses/>.
+ */
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using HeuristicLab.Collections;
+using HeuristicLab.Common;
+using HeuristicLab.Core;
+using HeuristicLab.Persistence.Default.CompositeSerializers.Storable;
+
+namespace HeuristicLab.Analysis {
+  [Item("Gantt Data", "Data of a Gantt visualization")]
+  [StorableClass]
+  public class GanttData : NamedItem {
+
+    [Storable]
+    private NamedItemCollection<GanttRow> rows;
+    public NamedItemCollection<GanttRow> Rows { get { return rows; } }
+
+    [Storable]
+    private ObservableDictionary<string, Color> categoryColors;
+    public ObservableDictionary<string, Color> CategoryColors { get { return categoryColors; } }
+
+    [StorableConstructor]
+    protected GanttData(bool deserializing) : base(deserializing) {}
+    protected GanttData(GanttData original, Cloner cloner)
+      : base(original, cloner) {
+      rows = cloner.Clone(original.rows);
+      categoryColors = new ObservableDictionary<string, Color>(original.categoryColors);
+    }
+    public GanttData() : this("Gantt Data", "Data of a Gantt visualization") { }
+    public GanttData(string name) : this(name, string.Empty) { }
+    public GanttData(string name, string description) : base(name, description) {
+      rows = new NamedItemCollection<GanttRow>();
+      categoryColors = new ObservableDictionary<string, Color>();
+    }
+    public GanttData(IEnumerable<GanttRow> rows) : this("Gantt Data", rows) { }
+    public GanttData(string name, IEnumerable<GanttRow> rows) : this(name, string.Empty, rows) { }
+    public GanttData(string name, string description, IEnumerable<GanttRow> rows) : base(name, description) {
+      this.rows = new NamedItemCollection<GanttRow>(rows);
+      categoryColors = new ObservableDictionary<string, Color>();
+    }
+
+    public override IDeepCloneable Clone(Cloner cloner) {
+      return new GanttData(this, cloner);
+    }
+  }
+
+  [Item("Gantt Row", "Row of a Gantt chart")]
+  [StorableClass]
+  public class GanttRow : NamedItem {
+    [Storable]
+    private ItemList<GanttItem> items;
+    public ItemList<GanttItem> Items { get { return items; } }
+
+    [StorableConstructor]
+    protected GanttRow(bool deserializing) : base(deserializing) {}
+    protected GanttRow(GanttRow original, Cloner cloner)
+      : base(original, cloner) {
+      items = cloner.Clone(original.items);
+    }
+    public GanttRow() : this("Gantt Row", "Row of a Gantt chart") { }
+    public GanttRow(string name) : this(name, string.Empty) { }
+    public GanttRow(string name, string description) : base(name, description) {
+      items = new ItemList<GanttItem>();
+    }
+    public GanttRow(IEnumerable<GanttItem> items) : this("Gantt Row", items) { }
+    public GanttRow(string name, IEnumerable<GanttItem> items) : this(name, string.Empty, items) { }
+    public GanttRow(string name, string description, IEnumerable<GanttItem> items) : base(name, description) {
+      this.items = new ItemList<GanttItem>(items);
+    }
+
+    public override IDeepCloneable Clone(Cloner cloner) {
+      return new GanttRow(this, cloner);
+    }
+  }
+
+  [Item("Gantt Item", "Item of a Gantt chart row")]
+  [StorableClass]
+  public class GanttItem : Item, INotifyPropertyChanged {
+
+    [Storable]
+    private DateTime startDate;
+    public DateTime StartDate {
+      get { return startDate; }
+      set {
+        if (startDate == value) return;
+        startDate = value;
+        OnPropertyChanged("StartDate");
+      }
+    }
+
+    [Storable]
+    private DateTime endDate;
+    public DateTime EndDate {
+      get { return endDate; }
+      set {
+        if (endDate == value) return;
+        endDate = value;
+        OnPropertyChanged("EndDate");
+      }
+    }
+
+    [Storable]
+    private string toolTip;
+    public string ToolTip {
+      get { return toolTip; }
+      set {
+        if (toolTip == value) return;
+        toolTip = value;
+        OnPropertyChanged("ToolTip");
+      }
+    }
+
+    [Storable]
+    private string category;
+    public string Category {
+      get { return category; }
+      set {
+        if (category == value) return;
+        category = value;
+        OnPropertyChanged("Category");
+      }
+    }
+
+    [Storable]
+    private bool showLabel;
+    public bool ShowLabel {
+      get { return showLabel; }
+      set {
+        if (showLabel == value) return;
+        showLabel = value;
+        OnPropertyChanged("ShowLabel");
+      }
+    }
+
+    [StorableConstructor]
+    protected GanttItem(bool deserializing) : base(deserializing) { }
+    protected GanttItem(GanttItem original, Cloner cloner)
+      : base(original, cloner) {
+      startDate = original.startDate;
+      endDate = original.endDate;
+      toolTip = original.toolTip;
+      category = original.category;
+      showLabel = original.showLabel;
+    }
+    public GanttItem() : this(DateTime.Now, DateTime.Now) { }
+    public GanttItem(DateTime start, DateTime end) {
+      startDate = start;
+      endDate = end;
+      toolTip = string.Empty;
+      category = string.Empty;
+      showLabel = false;
+    }
+
+    public override IDeepCloneable Clone(Cloner cloner) {
+      return new GanttItem(this, cloner);
+    }
+
+    public event PropertyChangedEventHandler PropertyChanged;
+    protected virtual void OnPropertyChanged(string propertyName) {
+      var handler = PropertyChanged;
+      if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
+    }
+  }
+}
Index: /stable/HeuristicLab.Analysis/3.3/DataVisualization/IndexedDataRow.cs
===================================================================
--- /stable/HeuristicLab.Analysis/3.3/DataVisualization/IndexedDataRow.cs	(revision 15220)
+++ /stable/HeuristicLab.Analysis/3.3/DataVisualization/IndexedDataRow.cs	(revision 15220)
@@ -0,0 +1,112 @@
+﻿#region License Information
+/* HeuristicLab
+ * Copyright (C) 2002-2015 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 <http://www.gnu.org/licenses/>.
+ */
+#endregion
+
+using HeuristicLab.Collections;
+using HeuristicLab.Common;
+using HeuristicLab.Core;
+using HeuristicLab.Persistence.Default.CompositeSerializers.Storable;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+
+namespace HeuristicLab.Analysis {
+  [Item("IndexedDataRow", "A data row that contains a series of points.")]
+  [StorableClass]
+  public class IndexedDataRow<T> : NamedItem {
+
+    private DataRowVisualProperties visualProperties;
+    public DataRowVisualProperties VisualProperties {
+      get { return visualProperties; }
+      set {
+        if (visualProperties != value) {
+          if (value == null) throw new ArgumentNullException("VisualProperties");
+          if (visualProperties != null) visualProperties.PropertyChanged -= new PropertyChangedEventHandler(VisualProperties_PropertyChanged);
+          visualProperties = value;
+          visualProperties.PropertyChanged += new PropertyChangedEventHandler(VisualProperties_PropertyChanged);
+          OnVisualPropertiesChanged();
+        }
+      }
+    }
+    private ObservableList<Tuple<T, double>> values;
+    public ObservableList<Tuple<T, double>> Values {
+      get { return values; }
+    }
+
+    #region Persistence Properties
+    [Storable(Name = "visualProperties")]
+    private DataRowVisualProperties StorableVisualProperties {
+      get { return visualProperties; }
+      set { visualProperties = value; }
+    }
+    [Storable(Name = "values")]
+    private IEnumerable<Tuple<T, double>> StorableValues {
+      get { return values; }
+      set { values = new ObservableList<Tuple<T, double>>(value); }
+    }
+    #endregion
+
+    [StorableConstructor]
+    protected IndexedDataRow(bool deserializing) : base(deserializing) { }
+    protected IndexedDataRow(IndexedDataRow<T> original, Cloner cloner)
+      : base(original, cloner) {
+      values = new ObservableList<Tuple<T, double>>(original.values.Select(x => Tuple.Create<T, double>(x.Item1, x.Item2)).ToList());
+      VisualProperties = cloner.Clone(original.visualProperties);
+    }
+    public IndexedDataRow() {
+      values = new ObservableList<Tuple<T, double>>();
+      VisualProperties = new DataRowVisualProperties();
+    }
+    public IndexedDataRow(string name)
+      : base(name) {
+      values = new ObservableList<Tuple<T, double>>();
+      VisualProperties = new DataRowVisualProperties(name);
+    }
+    public IndexedDataRow(string name, string description)
+      : base(name, description) {
+      values = new ObservableList<Tuple<T, double>>();
+      VisualProperties = new DataRowVisualProperties(name);
+    }
+    public IndexedDataRow(string name, string description, IEnumerable<Tuple<T, double>> values)
+      : base(name, description) {
+      this.values = new ObservableList<Tuple<T, double>>(values);
+      VisualProperties = new DataRowVisualProperties(name);
+    }
+
+    public override IDeepCloneable Clone(Cloner cloner) {
+      return new IndexedDataRow<T>(this, cloner);
+    }
+
+    public event EventHandler VisualPropertiesChanged;
+    protected virtual void OnVisualPropertiesChanged() {
+      EventHandler handler = VisualPropertiesChanged;
+      if (handler != null) handler(this, EventArgs.Empty);
+    }
+
+    private void VisualProperties_PropertyChanged(object sender, PropertyChangedEventArgs e) {
+      OnVisualPropertiesChanged();
+    }
+    protected override void OnNameChanged() {
+      base.OnNameChanged();
+      VisualProperties.DisplayName = Name;
+    }
+  }
+}
Index: /stable/HeuristicLab.Analysis/3.3/DataVisualization/IndexedDataTable.cs
===================================================================
--- /stable/HeuristicLab.Analysis/3.3/DataVisualization/IndexedDataTable.cs	(revision 15220)
+++ /stable/HeuristicLab.Analysis/3.3/DataVisualization/IndexedDataTable.cs	(revision 15220)
@@ -0,0 +1,279 @@
+﻿#region License Information
+/* HeuristicLab
+ * Copyright (C) 2002-2015 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 <http://www.gnu.org/licenses/>.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Linq;
+using HeuristicLab.Collections;
+using HeuristicLab.Common;
+using HeuristicLab.Core;
+using HeuristicLab.Data;
+using HeuristicLab.Persistence.Default.CompositeSerializers.Storable;
+
+namespace HeuristicLab.Analysis {
+  [Item("IndexedDataTable", "A data table where the points are also given with a certain index.")]
+  [StorableClass]
+  public class IndexedDataTable<T> : NamedItem, IStringConvertibleMatrix {
+    public static new Image StaticItemImage {
+      get { return HeuristicLab.Common.Resources.VSImageLibrary.Performance; }
+    }
+
+    private DataTableVisualProperties visualProperties;
+    public DataTableVisualProperties VisualProperties {
+      get { return visualProperties; }
+      set {
+        if (visualProperties != value) {
+          if (value == null) throw new ArgumentNullException("VisualProperties");
+          if (visualProperties != null) visualProperties.PropertyChanged -= new PropertyChangedEventHandler(VisualProperties_PropertyChanged);
+          visualProperties = value;
+          visualProperties.PropertyChanged += new PropertyChangedEventHandler(VisualProperties_PropertyChanged);
+          OnVisualPropertiesChanged();
+        }
+      }
+    }
+
+    private NamedItemCollection<IndexedDataRow<T>> rows;
+    public NamedItemCollection<IndexedDataRow<T>> Rows {
+      get { return rows; }
+    }
+
+    #region Persistence Properties
+    [Storable(Name = "VisualProperties")]
+    private DataTableVisualProperties StorableVisualProperties {
+      get { return visualProperties; }
+      set {
+        visualProperties = value;
+        visualProperties.PropertyChanged += new PropertyChangedEventHandler(VisualProperties_PropertyChanged);
+      }
+    }
+    [Storable(Name = "rows")]
+    private IEnumerable<IndexedDataRow<T>> StorableRows {
+      get { return rows; }
+      set { rows = new NamedItemCollection<IndexedDataRow<T>>(value); }
+    }
+    #endregion
+
+    [StorableConstructor]
+    protected IndexedDataTable(bool deserializing) : base(deserializing) { }
+    protected IndexedDataTable(IndexedDataTable<T> original, Cloner cloner)
+      : base(original, cloner) {
+      VisualProperties = (DataTableVisualProperties)cloner.Clone(original.visualProperties);
+      rows = cloner.Clone(original.rows);
+      this.RegisterRowsEvents();
+    }
+    public IndexedDataTable()
+      : base() {
+      VisualProperties = new DataTableVisualProperties();
+      rows = new NamedItemCollection<IndexedDataRow<T>>();
+      this.RegisterRowsEvents();
+    }
+    public IndexedDataTable(string name)
+      : base(name) {
+      VisualProperties = new DataTableVisualProperties();
+      rows = new NamedItemCollection<IndexedDataRow<T>>();
+      this.RegisterRowsEvents();
+    }
+    public IndexedDataTable(string name, string description)
+      : base(name, description) {
+      VisualProperties = new DataTableVisualProperties();
+      rows = new NamedItemCollection<IndexedDataRow<T>>();
+      this.RegisterRowsEvents();
+    }
+
+    public override IDeepCloneable Clone(Cloner cloner) {
+      return new IndexedDataTable<T>(this, cloner);
+    }
+
+    public event EventHandler VisualPropertiesChanged;
+    protected virtual void OnVisualPropertiesChanged() {
+      var handler = VisualPropertiesChanged;
+      if (handler != null) handler(this, EventArgs.Empty);
+    }
+
+    private void VisualProperties_PropertyChanged(object sender, PropertyChangedEventArgs e) {
+      OnVisualPropertiesChanged();
+    }
+
+    private void RegisterRowsEvents() {
+      rows.ItemsAdded += new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(RowsOnItemsAdded);
+      rows.ItemsRemoved += new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(RowsOnItemsRemoved);
+      rows.ItemsReplaced += new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(RowsOnItemsReplaced);
+      rows.CollectionReset += new CollectionItemsChangedEventHandler<IndexedDataRow<T>>(RowsOnCollectionReset);
+      foreach (var row in Rows) RegisterRowEvents(row);
+    }
+    protected virtual void RowsOnItemsAdded(object sender, CollectionItemsChangedEventArgs<IndexedDataRow<T>> e) {
+      foreach (var row in e.Items)
+        this.RegisterRowEvents(row);
+
+      this.OnColumnsChanged();
+      this.OnColumnNamesChanged();
+      this.OnReset();
+    }
+    protected virtual void RowsOnItemsRemoved(object sender, CollectionItemsChangedEventArgs<IndexedDataRow<T>> e) {
+      foreach (var row in e.Items)
+        this.DeregisterRowEvents(row);
+
+      this.OnColumnsChanged();
+      this.OnColumnNamesChanged();
+      this.OnReset();
+    }
+    protected virtual void RowsOnItemsReplaced(object sender, CollectionItemsChangedEventArgs<IndexedDataRow<T>> e) {
+      foreach (var row in e.OldItems)
+        this.DeregisterRowEvents(row);
+      foreach (var row in e.Items)
+        this.RegisterRowEvents(row);
+
+      this.OnColumnsChanged();
+      this.OnColumnNamesChanged();
+      this.OnReset();
+    }
+    protected virtual void RowsOnCollectionReset(object sender, CollectionItemsChangedEventArgs<IndexedDataRow<T>> e) {
+      foreach (var row in e.OldItems)
+        this.DeregisterRowEvents(row);
+      foreach (var row in e.Items)
+        this.RegisterRowEvents(row);
+
+      if (e.OldItems.Count() != e.Items.Count())
+        this.OnColumnsChanged();
+      this.OnColumnNamesChanged();
+      this.OnReset();
+    }
+
+    private void RegisterRowEvents(IndexedDataRow<T> row) {
+      row.Values.ItemsAdded += new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(ValuesOnItemsAdded);
+      row.Values.ItemsMoved += new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(ValuesOnItemsMoved);
+      row.Values.ItemsRemoved += new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(ValuesOnItemsRemoved);
+      row.Values.ItemsReplaced += new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(ValuesOnItemsReplaced);
+      row.Values.CollectionReset += new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(ValuesOnCollectionReset);
+    }
+    private void DeregisterRowEvents(IndexedDataRow<T> row) {
+      row.Values.ItemsAdded -= new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(ValuesOnItemsAdded);
+      row.Values.ItemsMoved -= new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(ValuesOnItemsMoved);
+      row.Values.ItemsRemoved -= new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(ValuesOnItemsRemoved);
+      row.Values.ItemsReplaced -= new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(ValuesOnItemsReplaced);
+      row.Values.CollectionReset -= new CollectionItemsChangedEventHandler<IndexedItem<Tuple<T, double>>>(ValuesOnCollectionReset);
+    }
+
+    protected virtual void ValuesOnItemsAdded(object sender, CollectionItemsChangedEventArgs<IndexedItem<Tuple<T, double>>> e) {
+      this.OnReset();
+    }
+    protected virtual void ValuesOnItemsMoved(object sender, CollectionItemsChangedEventArgs<IndexedItem<Tuple<T, double>>> e) {
+      this.OnReset();
+    }
+    protected virtual void ValuesOnItemsRemoved(object sender, CollectionItemsChangedEventArgs<IndexedItem<Tuple<T, double>>> e) {
+      this.OnReset();
+    }
+    protected virtual void ValuesOnItemsReplaced(object sender, CollectionItemsChangedEventArgs<IndexedItem<Tuple<T, double>>> e) {
+      this.OnReset();
+    }
+    protected virtual void ValuesOnCollectionReset(object sender, CollectionItemsChangedEventArgs<IndexedItem<Tuple<T, double>>> e) {
+      this.OnReset();
+    }
+
+    #region IStringConvertibleMatrix Members
+
+    int IStringConvertibleMatrix.Rows {
+      get { return rows.Count == 0 ? 0 : rows.Max(r => r.Values.Count); }
+      set { throw new NotSupportedException(); }
+    }
+    int IStringConvertibleMatrix.Columns {
+      get { return rows.Count * 2; }
+      set { throw new NotSupportedException(); }
+    }
+    IEnumerable<string> IStringConvertibleMatrix.ColumnNames {
+      get { return rows.Select(r => new string[] { r.Name + " Index", r.Name }).SelectMany(x => x); }
+      set { throw new NotSupportedException(); }
+    }
+    IEnumerable<string> IStringConvertibleMatrix.RowNames {
+      get { return new List<string>(); }
+      set { throw new NotSupportedException(); }
+    }
+
+    bool IStringConvertibleMatrix.SortableView {
+      get { return true; }
+      set { throw new NotSupportedException(); }
+    }
+    bool IStringConvertibleMatrix.ReadOnly {
+      get { return true; }
+    }
+
+    string IStringConvertibleMatrix.GetValue(int rowIndex, int columnIndex) {
+      if (columnIndex < rows.Count * 2) {
+        string columnName = ((IStringConvertibleMatrix)this).ColumnNames.ElementAt(columnIndex);
+        if (!rows.ContainsKey(columnName) && columnName.Length > " Index".Length) {
+          columnName = columnName.Substring(0, columnName.Length - " Index".Length);
+          if (rows.ContainsKey(columnName) && rowIndex < rows[columnName].Values.Count)
+            return rows[columnName].Values[rowIndex].Item1.ToString();
+          else return string.Empty;
+        } else if (rows.ContainsKey(columnName) && rowIndex < rows[columnName].Values.Count)
+          return rows[columnName].Values[rowIndex].Item2.ToString();
+      }
+      return string.Empty;
+    }
+
+    bool IStringConvertibleMatrix.Validate(string value, out string errorMessage) {
+      throw new NotSupportedException();
+    }
+    bool IStringConvertibleMatrix.SetValue(string value, int rowIndex, int columnIndex) {
+      throw new NotSupportedException();
+    }
+
+    public event EventHandler<EventArgs<int, int>> ItemChanged;
+    protected virtual void OnItemChanged(int rowIndex, int columnIndex) {
+      var handler = ItemChanged;
+      if (handler != null) handler(this, new EventArgs<int, int>(rowIndex, columnIndex));
+      OnToStringChanged();
+    }
+    public event EventHandler Reset;
+    protected virtual void OnReset() {
+      var handler = Reset;
+      if (handler != null) handler(this, EventArgs.Empty);
+    }
+    public event EventHandler ColumnsChanged;
+    protected virtual void OnColumnsChanged() {
+      var handler = ColumnsChanged;
+      if (handler != null) handler(this, EventArgs.Empty);
+    }
+    public event EventHandler RowsChanged;
+    protected virtual void OnRowsChanged() {
+      var handler = RowsChanged;
+      if (handler != null) handler(this, EventArgs.Empty);
+    }
+    public event EventHandler ColumnNamesChanged;
+    protected virtual void OnColumnNamesChanged() {
+      var handler = ColumnNamesChanged;
+      if (handler != null) handler(this, EventArgs.Empty);
+    }
+    public event EventHandler RowNamesChanged;
+    protected virtual void OnRowNamesChanged() {
+      var handler = RowNamesChanged;
+      if (handler != null) handler(this, EventArgs.Empty);
+    }
+    public event EventHandler SortableViewChanged;
+    protected virtual void OnSortableViewChanged() {
+      var handler = SortableViewChanged;
+      if (handler != null) handler(this, EventArgs.Empty);
+    }
+    #endregion
+  }
+}
Index: /stable/HeuristicLab.Analysis/3.3/HeuristicLab.Analysis-3.3.csproj
===================================================================
--- /stable/HeuristicLab.Analysis/3.3/HeuristicLab.Analysis-3.3.csproj	(revision 15219)
+++ /stable/HeuristicLab.Analysis/3.3/HeuristicLab.Analysis-3.3.csproj	(revision 15220)
@@ -131,4 +131,7 @@
     <Compile Include="AlleleFrequencyAnalysis\AlleleFrequencyCollectionHistory.cs" />
     <Compile Include="BestScopeSolutionAnalyzer.cs" />
+    <Compile Include="DataVisualization\GanttData.cs" />
+    <Compile Include="DataVisualization\IndexedDataRow.cs" />
+    <Compile Include="DataVisualization\IndexedDataTable.cs" />
     <Compile Include="DataVisualization\ScatterPlotDataRowVisualProperties.cs" />
     <Compile Include="DataVisualization\ScatterPlotDataRow.cs" />
@@ -154,4 +157,5 @@
     <Compile Include="QualityAnalysis\BestAverageWorstQualityCalculator.cs" />
     <Compile Include="QualityAnalysis\BestQualityMemorizer.cs" />
+    <Compile Include="QualityAnalysis\ExpectedRuntimeHelper.cs" />
     <Compile Include="QualityAnalysis\QualityAnalyzer.cs" />
     <Compile Include="QualityAnalysis\QualityDifferenceCalculator.cs" />
@@ -159,4 +163,6 @@
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="QualityAnalysis\QualityDistributionAnalyzer.cs" />
+    <Compile Include="QualityAnalysis\QualityPerClockAnalyzer.cs" />
+    <Compile Include="QualityAnalysis\QualityPerEvaluationsAnalyzer.cs" />
     <Compile Include="QualityAnalysis\ScaledQualityDifferenceAnalyzer.cs" />
     <Compile Include="Statistics\BonferroniHolm.cs" />
Index: /stable/HeuristicLab.Analysis/3.3/QualityAnalysis/ExpectedRuntimeHelper.cs
===================================================================
--- /stable/HeuristicLab.Analysis/3.3/QualityAnalysis/ExpectedRuntimeHelper.cs	(revision 15220)
+++ /stable/HeuristicLab.Analysis/3.3/QualityAnalysis/ExpectedRuntimeHelper.cs	(revision 15220)
@@ -0,0 +1,62 @@
+﻿using HeuristicLab.Common;
+using HeuristicLab.Optimization;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+
+namespace HeuristicLab.Analysis {
+  public static class ExpectedRuntimeHelper {
+    public static ErtCalculationResult CalculateErt(IEnumerable<IEnumerable<Tuple<double, double>>> convGraphs, double target, bool maximization) {
+      var successful = new List<double>();
+      var unsuccessful = new List<double>();
+      foreach (var graph in convGraphs) {
+        var targetAchieved = false;
+        var lastEffort = double.MaxValue;
+        foreach (var v in graph) {
+          if (maximization && v.Item2 >= target || !maximization && v.Item2 <= target) {
+            successful.Add(v.Item1);
+            targetAchieved = true;
+            break;
+          }
+          lastEffort = v.Item1;
+        }
+        if (!targetAchieved) unsuccessful.Add(lastEffort);
+      }
+
+      var ert = double.PositiveInfinity;
+
+      var nRuns = successful.Count + unsuccessful.Count;
+      if (successful.Count > 0) {
+        var succAvg = successful.Average();
+        var succDev = successful.StandardDeviation() + 1e-7;
+        successful.RemoveAll(x => x < succAvg - 2 * succDev);
+        unsuccessful.RemoveAll(x => x < succAvg - 2 * succDev);
+        nRuns = successful.Count + unsuccessful.Count;
+
+        ert = successful.Average() / (successful.Count / (double)nRuns);
+      }
+      return new ErtCalculationResult(successful.Count, nRuns, ert);
+    }
+
+    public static ErtCalculationResult CalculateErt(List<IRun> runs, string indexedDataTableName, double target, bool maximization) {
+      return CalculateErt(runs.Select(r => ((IndexedDataTable<double>)r.Results[indexedDataTableName]).Rows.First().Values), target, maximization);
+    }
+  }
+
+  public struct ErtCalculationResult {
+    public readonly int SuccessfulRuns;
+    public readonly int TotalRuns;
+    public readonly double ExpectedRuntime;
+
+    public ErtCalculationResult(int successful, int total, double ert) {
+      SuccessfulRuns = successful;
+      TotalRuns = total;
+      ExpectedRuntime = ert;
+    }
+
+    public override string ToString() {
+      return ExpectedRuntime.ToString("##,0.0", CultureInfo.CurrentCulture.NumberFormat);
+    }
+  }
+}
Index: /stable/HeuristicLab.Analysis/3.3/QualityAnalysis/QualityPerClockAnalyzer.cs
===================================================================
--- /stable/HeuristicLab.Analysis/3.3/QualityAnalysis/QualityPerClockAnalyzer.cs	(revision 15220)
+++ /stable/HeuristicLab.Analysis/3.3/QualityAnalysis/QualityPerClockAnalyzer.cs	(revision 15220)
@@ -0,0 +1,108 @@
+﻿#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 <http://www.gnu.org/licenses/>.
+ */
+#endregion
+
+using System;
+using System.Linq;
+using HeuristicLab.Common;
+using HeuristicLab.Core;
+using HeuristicLab.Data;
+using HeuristicLab.Operators;
+using HeuristicLab.Optimization;
+using HeuristicLab.Parameters;
+using HeuristicLab.Persistence.Default.CompositeSerializers.Storable;
+
+namespace HeuristicLab.Analysis {
+  [Item("QualityPerClockAnalyzer", @"Creates a plot of the solution quality with respect to the elapsed wall-clock time.")]
+  [StorableClass]
+  public class QualityPerClockAnalyzer : SingleSuccessorOperator, IAnalyzer, ISingleObjectiveOperator {
+    public virtual bool EnabledByDefault {
+      get { return false; }
+    }
+
+    public ILookupParameter<DoubleValue> BestQualityParameter {
+      get { return (ILookupParameter<DoubleValue>)Parameters["BestQuality"]; }
+    }
+
+    public ILookupParameter<DateTimeValue> LastUpdateTimeParameter {
+      get { return (ILookupParameter<DateTimeValue>)Parameters["LastUpdateTime"]; }
+    }
+
+    public IResultParameter<IndexedDataTable<double>> QualityPerClockParameter {
+      get { return (IResultParameter<IndexedDataTable<double>>)Parameters["QualityPerClock"]; }
+    }
+
+    [StorableConstructor]
+    protected QualityPerClockAnalyzer(bool deserializing) : base(deserializing) { }
+    protected QualityPerClockAnalyzer(QualityPerClockAnalyzer original, Cloner cloner) : base(original, cloner) { }
+    public QualityPerClockAnalyzer()
+      : base() {
+      Parameters.Add(new LookupParameter<DoubleValue>("BestQuality", "The quality value that should be compared."));
+      Parameters.Add(new LookupParameter<DateTimeValue>("LastUpdateTime", "The time the analyzer was last run."));
+      Parameters.Add(new ResultParameter<IndexedDataTable<double>>("QualityPerClock", "Data table containing the first hitting graph with elapsed wall clock time (in seconds) as the x-axis."));
+      QualityPerClockParameter.DefaultValue = new IndexedDataTable<double>("Quality per Clock") {
+        VisualProperties = {
+          XAxisTitle = "Elapsed time [s]",
+          YAxisTitle = "Quality"
+        },
+        Rows = { new IndexedDataRow<double>("First-hit Graph") { VisualProperties = {
+          ChartType = DataRowVisualProperties.DataRowChartType.StepLine,
+          LineWidth = 2
+        } } }
+      };
+    }
+
+    public override IDeepCloneable Clone(Cloner cloner) {
+      return new QualityPerClockAnalyzer(this, cloner);
+    }
+
+    public override IOperation Apply() {
+      var bestQuality = BestQualityParameter.ActualValue.Value;
+      var dataTable = QualityPerClockParameter.ActualValue;
+      var values = dataTable.Rows["First-hit Graph"].Values;
+
+      var lastUpdateTime = LastUpdateTimeParameter.ActualValue;
+      if (lastUpdateTime == null) {
+        lastUpdateTime = new DateTimeValue(DateTime.UtcNow.AddMilliseconds(-1));
+        LastUpdateTimeParameter.ActualValue = lastUpdateTime;
+      }
+
+      var now = DateTime.UtcNow;
+      var runtimeSoFar = (now - lastUpdateTime.Value).TotalSeconds + (values.Count > 0 ? values.Last().Item1 : 0);
+      lastUpdateTime.Value = now;
+      var newEntry = Tuple.Create(runtimeSoFar, bestQuality);
+
+      if (values.Count == 0) {
+        values.Add(newEntry); // record the first data
+        values.Add(Tuple.Create(runtimeSoFar, bestQuality)); // last entry records max number of evaluations
+        return base.Apply();
+      }
+
+      var improvement = values.Last().Item2 != bestQuality;
+      if (improvement) {
+        values[values.Count - 1] = newEntry; // record the improvement
+        values.Add(Tuple.Create(runtimeSoFar, bestQuality)); // last entry records max number of evaluations
+      } else {
+        values[values.Count - 1] = Tuple.Create(runtimeSoFar, bestQuality);
+      }
+      return base.Apply();
+    }
+  }
+}
Index: /stable/HeuristicLab.Analysis/3.3/QualityAnalysis/QualityPerEvaluationsAnalyzer.cs
===================================================================
--- /stable/HeuristicLab.Analysis/3.3/QualityAnalysis/QualityPerEvaluationsAnalyzer.cs	(revision 15220)
+++ /stable/HeuristicLab.Analysis/3.3/QualityAnalysis/QualityPerEvaluationsAnalyzer.cs	(revision 15220)
@@ -0,0 +1,113 @@
+﻿#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 <http://www.gnu.org/licenses/>.
+ */
+#endregion
+
+using System;
+using System.Linq;
+using HeuristicLab.Common;
+using HeuristicLab.Core;
+using HeuristicLab.Data;
+using HeuristicLab.Operators;
+using HeuristicLab.Optimization;
+using HeuristicLab.Parameters;
+using HeuristicLab.Persistence.Default.CompositeSerializers.Storable;
+
+namespace HeuristicLab.Analysis {
+  [Item("QualityPerEvaluationsAnalyzer", @"Creates a plot of the solution quality with respect to the number of evaluated solutions.")]
+  [StorableClass]
+  public class QualityPerEvaluationsAnalyzer : SingleSuccessorOperator, IAnalyzer, ISingleObjectiveOperator {
+    public virtual bool EnabledByDefault {
+      get { return false; }
+    }
+
+    public ILookupParameter<DoubleValue> BestQualityParameter {
+      get { return (ILookupParameter<DoubleValue>)Parameters["BestQuality"]; }
+    }
+    public ILookupParameter<IntValue> EvaluatedSolutionsParameter {
+      get { return (ILookupParameter<IntValue>)Parameters["EvaluatedSolutions"]; }
+    }
+    public ILookupParameter<IntValue> EvaluatedMovesParameter {
+      get { return (ILookupParameter<IntValue>)Parameters["EvaluatedMoves"]; }
+    }
+    public IValueLookupParameter<DoubleValue> MoveCostPerSolutionParameter {
+      get { return (IValueLookupParameter<DoubleValue>)Parameters["MoveCostPerSolution"]; }
+    }
+    public IResultParameter<IndexedDataTable<double>> QualityPerEvaluationsParameter {
+      get { return (IResultParameter<IndexedDataTable<double>>)Parameters["QualityPerEvaluations"]; }
+    }
+
+    [StorableConstructor]
+    protected QualityPerEvaluationsAnalyzer(bool deserializing) : base(deserializing) { }
+    protected QualityPerEvaluationsAnalyzer(QualityPerEvaluationsAnalyzer original, Cloner cloner) : base(original, cloner) { }
+    public QualityPerEvaluationsAnalyzer()
+      : base() {
+      Parameters.Add(new LookupParameter<DoubleValue>("BestQuality", "The quality value that should be compared."));
+      Parameters.Add(new LookupParameter<IntValue>("EvaluatedSolutions", "The number of evaluated solutions."));
+      Parameters.Add(new LookupParameter<IntValue>("EvaluatedMoves", "The number of evaluated moves."));
+      Parameters.Add(new ValueLookupParameter<DoubleValue>("MoveCostPerSolution", "The cost to evaluate a move as a ratio to the cost of evaluating a solution.", new DoubleValue(1)));
+      Parameters.Add(new ResultParameter<IndexedDataTable<double>>("QualityPerEvaluations", "Data table containing the first hitting graph with evaluations as the x-axis."));
+      QualityPerEvaluationsParameter.DefaultValue = new IndexedDataTable<double>("Quality per Evaluations") {
+        VisualProperties = {
+          XAxisTitle = "Evaluations",
+          YAxisTitle = "Quality"
+        },
+        Rows = { new IndexedDataRow<double>("First-hit Graph") { VisualProperties = {
+          ChartType = DataRowVisualProperties.DataRowChartType.StepLine,
+          LineWidth = 2
+        } } }
+      };
+    }
+
+    public override IDeepCloneable Clone(Cloner cloner) {
+      return new QualityPerEvaluationsAnalyzer(this, cloner);
+    }
+
+    public override IOperation Apply() {
+      var bestQuality = BestQualityParameter.ActualValue.Value;
+      var evalSols = EvaluatedSolutionsParameter.ActualValue;
+      var evalMoves = EvaluatedMovesParameter.ActualValue;
+      var evaluations = 0.0;
+      if (evalSols != null) evaluations += evalSols.Value;
+      if (evalMoves != null) evaluations += evalMoves.Value * MoveCostPerSolutionParameter.ActualValue.Value;
+
+      if (evaluations > 0) {
+        var dataTable = QualityPerEvaluationsParameter.ActualValue;
+        var values = dataTable.Rows["First-hit Graph"].Values;
+
+        var newEntry = Tuple.Create(evaluations, bestQuality);
+
+        if (values.Count == 0) {
+          values.Add(newEntry); // record the first data
+          values.Add(Tuple.Create(evaluations, bestQuality)); // last entry records max number of evaluations
+          return base.Apply();
+        }
+
+        var improvement = values.Last().Item2 != bestQuality;
+        if (improvement) {
+          values[values.Count - 1] = newEntry; // record the improvement
+          values.Add(Tuple.Create(evaluations, bestQuality)); // last entry records max number of evaluations
+        } else {
+          values[values.Count - 1] = Tuple.Create(evaluations, bestQuality); // the last entry is updated
+        }
+      }
+      return base.Apply();
+    }
+  }
+}
Index: able/HeuristicLab.Common/3.3/EnumerableStatisticExtensions.cs
===================================================================
--- /stable/HeuristicLab.Common/3.3/EnumerableStatisticExtensions.cs	(revision 15219)
+++ 	(revision )
@@ -1,239 +1,0 @@
-#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 <http://www.gnu.org/licenses/>.
- */
-#endregion
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.Contracts;
-using System.Linq;
-
-namespace HeuristicLab.Common {
-  public static class EnumerableStatisticExtensions {
-    /// <summary>
-    /// Calculates the median element of the enumeration.
-    /// </summary>
-    /// <param name="values"></param>
-    /// <returns></returns>
-    public static double Median(this IEnumerable<double> values) {
-      // See unit tests for comparison with naive implementation
-      return Quantile(values, 0.5);
-    }
-
-    /// <summary>
-    /// Calculates the alpha-quantile element of the enumeration.
-    /// </summary>
-    /// <param name="values"></param>
-    /// <returns></returns>
-    public static double Quantile(this IEnumerable<double> values, double alpha) {
-      // See unit tests for comparison with naive implementation
-      double[] valuesArr = values.ToArray();
-      int n = valuesArr.Length;
-      if (n == 0) throw new InvalidOperationException("Enumeration contains no elements.");
-
-      // "When N is even, statistics books define the median as the arithmetic mean of the elements k = N/2 
-      // and k = N/2 + 1 (that is, N/2 from the bottom and N/2 from the top). 
-      // If you accept such pedantry, you must perform two separate selections to find these elements."
-
-      // return the element at Math.Ceiling (if n*alpha is fractional) or the average of two elements if n*alpha is integer.
-      var pos = n * alpha;
-      Contract.Assert(pos >= 0);
-      Contract.Assert(pos < n);
-      bool isInteger = Math.Round(pos).IsAlmost(pos);
-      if (isInteger) {
-        return 0.5 * (Select((int)pos - 1, valuesArr) + Select((int)pos, valuesArr));
-      } else {
-        return Select((int)Math.Ceiling(pos) - 1, valuesArr);
-      }
-    }
-
-    // Numerical Recipes in C++, §8.5 Selecting the Mth Largest, O(n)
-    // Given k in [0..n-1] returns an array value from array arr[0..n-1] such that k array values are 
-    // less than or equal to the one returned. The input array will be rearranged to have this value in 
-    // location arr[k], with all smaller elements moved to arr[0..k-1] (in arbitrary order) and all 
-    // larger elements in arr[k+1..n-1] (also in arbitrary order).
-    // 
-    // Could be changed to Select<T> where T is IComparable but in this case is significantly slower for double values
-    private static double Select(int k, double[] arr) {
-      Contract.Assert(arr.GetLowerBound(0) == 0);
-      Contract.Assert(k >= 0 && k < arr.Length);
-      int i, ir, j, l, mid, n = arr.Length;
-      double a;
-      l = 0;
-      ir = n - 1;
-      for (; ; ) {
-        if (ir <= l + 1) {
-          // Active partition contains 1 or 2 elements.
-          if (ir == l + 1 && arr[ir] < arr[l]) {
-            // if (ir == l + 1 && arr[ir].CompareTo(arr[l]) < 0) {
-            // Case of 2 elements.
-            // SWAP(arr[l], arr[ir]);
-            double temp = arr[l];
-            arr[l] = arr[ir];
-            arr[ir] = temp;
-          }
-          return arr[k];
-        } else {
-          mid = (l + ir) >> 1; // Choose median of left, center, and right elements
-          {
-            // SWAP(arr[mid], arr[l + 1]); // as partitioning element a. Also
-            double temp = arr[mid];
-            arr[mid] = arr[l + 1];
-            arr[l + 1] = temp;
-          }
-
-          if (arr[l] > arr[ir]) {
-            // if (arr[l].CompareTo(arr[ir]) > 0) {  // rearrange so that arr[l] arr[ir] <= arr[l+1],
-            // SWAP(arr[l], arr[ir]); . arr[ir] >= arr[l+1]
-            double temp = arr[l];
-            arr[l] = arr[ir];
-            arr[ir] = temp;
-          }
-
-          if (arr[l + 1] > arr[ir]) {
-            // if (arr[l + 1].CompareTo(arr[ir]) > 0) {
-            // SWAP(arr[l + 1], arr[ir]);
-            double temp = arr[l + 1];
-            arr[l + 1] = arr[ir];
-            arr[ir] = temp;
-          }
-          if (arr[l] > arr[l + 1]) {
-            //if (arr[l].CompareTo(arr[l + 1]) > 0) {
-            // SWAP(arr[l], arr[l + 1]);
-            double temp = arr[l];
-            arr[l] = arr[l + 1];
-            arr[l + 1] = temp;
-
-          }
-          i = l + 1; // Initialize pointers for partitioning.
-          j = ir;
-          a = arr[l + 1]; // Partitioning element.
-          for (; ; ) { // Beginning of innermost loop.
-            do i++; while (arr[i] < a /* arr[i].CompareTo(a) < 0 */); // Scan up to find element > a.
-            do j--; while (arr[j] > a /* arr[j].CompareTo(a) > 0 */); // Scan down to find element < a.
-            if (j < i) break; // Pointers crossed. Partitioning complete.
-            {
-              // SWAP(arr[i], arr[j]);
-              double temp = arr[i];
-              arr[i] = arr[j];
-              arr[j] = temp;
-            }
-          } // End of innermost loop.
-          arr[l + 1] = arr[j]; // Insert partitioning element.
-          arr[j] = a;
-          if (j >= k) ir = j - 1; // Keep active the partition that contains the
-          if (j <= k) l = i; // kth element.
-        }
-      }
-    }
-
-    /// <summary>
-    /// Calculates the range (max - min) of the enumeration.
-    /// </summary>
-    /// <param name="values"></param>
-    /// <returns></returns>
-    public static double Range(this IEnumerable<double> values) {
-      double min = double.PositiveInfinity;
-      double max = double.NegativeInfinity;
-      int i = 0;
-      foreach (var e in values) {
-        if (min > e) min = e;
-        if (max < e) max = e;
-        i++;
-      }
-      if (i < 1) throw new ArgumentException("The enumerable must contain at least two elements", "values");
-      return max - min;
-    }
-
-
-    /// <summary>
-    /// Calculates the sample standard deviation of values.
-    /// </summary>
-    /// <param name="values"></param>
-    /// <returns></returns>
-    public static double StandardDeviation(this IEnumerable<double> values) {
-      return Math.Sqrt(Variance(values));
-    }
-
-    /// <summary>
-    /// Calculates the population standard deviation of values.
-    /// </summary>
-    /// <param name="values"></param>
-    /// <returns></returns>
-    public static double StandardDeviationPop(this IEnumerable<double> values) {
-      return Math.Sqrt(VariancePop(values));
-    }
-
-    /// <summary>
-    /// Calculates the sample variance of values. (sum (x - x_mean)² / (n-1))
-    /// </summary>
-    /// <param name="values"></param>
-    /// <returns></returns>
-    public static double Variance(this IEnumerable<double> values) {
-      return Variance(values, true);
-    }
-
-    /// <summary>
-    /// Calculates the population variance of values. (sum (x - x_mean)² / n)
-    /// </summary>
-    /// <param name="values"></param>
-    /// <returns></returns>
-    public static double VariancePop(this IEnumerable<double> values) {
-      return Variance(values, false);
-    }
-
-    private static double Variance(IEnumerable<double> values, bool sampleVariance) {
-      int m_n = 0;
-      double m_oldM = 0.0;
-      double m_newM = 0.0;
-      double m_oldS = 0.0;
-      double m_newS = 0.0;
-      foreach (double x in values) {
-        m_n++;
-        if (m_n == 1) {
-          m_oldM = m_newM = x;
-          m_oldS = 0.0;
-        } else {
-          m_newM = m_oldM + (x - m_oldM) / m_n;
-          m_newS = m_oldS + (x - m_oldM) * (x - m_newM);
-
-          // set up for next iteration
-          m_oldM = m_newM;
-          m_oldS = m_newS;
-        }
-      }
-
-      if (m_n == 0) return double.NaN;
-      if (m_n == 1) return 0.0;
-
-      if (sampleVariance) return m_newS / (m_n - 1);
-      else return m_newS / m_n;
-    }
-
-    public static IEnumerable<double> LimitToRange(this IEnumerable<double> values, double min, double max) {
-      if (min > max) throw new ArgumentException(string.Format("Minimum {0} is larger than maximum {1}.", min, max));
-      foreach (var x in values) {
-        if (double.IsNaN(x)) yield return (max + min) / 2.0;
-        else if (x < min) yield return min;
-        else if (x > max) yield return max;
-        else yield return x;
-      }
-    }
-  }
-}
Index: /stable/HeuristicLab.Common/3.3/HeuristicLab.Common-3.3.csproj
===================================================================
--- /stable/HeuristicLab.Common/3.3/HeuristicLab.Common-3.3.csproj	(revision 15219)
+++ /stable/HeuristicLab.Common/3.3/HeuristicLab.Common-3.3.csproj	(revision 15220)
@@ -131,4 +131,5 @@
     <Compile Include="Constants.cs" />
     <Compile Include="ArrayExtensions.cs" />
+    <Compile Include="Statistics\EmpiricalCumulativeDistributionFunction.cs" />
     <Compile Include="ListExtensions.cs" />
     <Compile Include="Point2D.cs" />
@@ -139,5 +140,5 @@
     <Compile Include="DeepCloneable.cs" />
     <Compile Include="Content\IContent.cs" />
-    <Compile Include="EnumerableStatisticExtensions.cs" />
+    <Compile Include="Statistics\EnumerableStatisticExtensions.cs" />
     <Compile Include="IDeepCloneable.cs" />
     <Compile Include="Plugin.cs" />
Index: /stable/HeuristicLab.Common/3.3/Point2D.cs
===================================================================
--- /stable/HeuristicLab.Common/3.3/Point2D.cs	(revision 15219)
+++ /stable/HeuristicLab.Common/3.3/Point2D.cs	(revision 15220)
@@ -54,4 +54,8 @@
     }
 
+    public static Point2D<T> Create(T x, T y, object tag = null) {
+      return new Point2D<T>(x, y, tag);
+    }
+
     public static bool operator ==(Point2D<T> left, Point2D<T> right) {
       return left.x.Equals(right.x) && left.y.Equals(right.y) && left.tag == right.tag;
Index: /stable/HeuristicLab.Common/3.3/Statistics/EmpiricalCumulativeDistributionFunction.cs
===================================================================
--- /stable/HeuristicLab.Common/3.3/Statistics/EmpiricalCumulativeDistributionFunction.cs	(revision 15220)
+++ /stable/HeuristicLab.Common/3.3/Statistics/EmpiricalCumulativeDistributionFunction.cs	(revision 15220)
@@ -0,0 +1,130 @@
+﻿#region License Information
+/* HeuristicLab
+ * Copyright (C) 2002-2017 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 <http://www.gnu.org/licenses/>.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace HeuristicLab.Common {
+  public class EmpiricalCumulativeDistributionFunction {
+    private static readonly AbscissaComparer abscissaComparer = new AbscissaComparer();
+    private static readonly OrdinateComparer ordinateComparer = new OrdinateComparer();
+
+    private List<Point2D<double>> ecdf;
+    public IEnumerable<Point2D<double>> SupportingPoints {
+      get { return ecdf; }
+    }
+    
+    public EmpiricalCumulativeDistributionFunction(IList<double> sample) {
+      ecdf = new List<Point2D<double>>();
+
+      var len = sample.Count;
+      var cumulative = 0;
+      var localcumulative = 0;
+      var prev = double.NaN;
+      foreach (var p in sample.OrderBy(x => x)) {
+        if (double.IsNaN(p) || double.IsInfinity(p)) continue;
+        if (!double.IsNaN(prev) && prev < p) {
+          cumulative += localcumulative;
+          localcumulative = 0;
+          ecdf.Add(Point2D<double>.Create(prev, cumulative / (double)len));
+        }
+        prev = p;
+        localcumulative++;
+      }
+      if (!double.IsNaN(prev)) {
+        cumulative += localcumulative;
+        ecdf.Add(Point2D<double>.Create(prev, cumulative / (double)len));
+      }
+    }
+    public EmpiricalCumulativeDistributionFunction(IEnumerable<Point2D<double>> ecdf) {
+      this.ecdf = new List<Point2D<double>>();
+      var prev = Point2D<double>.Empty;
+      foreach (var point in ecdf) {
+        if (point.Y < 0 || point.Y > 1 || double.IsNaN(point.X) || double.IsInfinity(point.X)
+          || point.IsEmpty || (!prev.IsEmpty && (point.X <= prev.X || point.Y <= prev.Y)))
+          throw new ArgumentException("Invalid supporting points of a cumulative distribution function. Must be strictly monotonically increasing in both X and Y with X in R and Y in [0;1].", "ecdf");
+
+        this.ecdf.Add(point);
+        prev = point;
+      }
+    }
+
+    public double Evaluate(double x) {
+      if (ecdf.Count == 0) return double.NaN;
+      if (x < ecdf[0].X) return 0;
+      var last = ecdf[ecdf.Count - 1];
+      if (x >= last.X) return last.Y;
+
+      var index = ecdf.BinarySearch(Point2D<double>.Create(x, 0), abscissaComparer);
+      if (index >= 0) return ecdf[index].Y;
+      return ecdf[~index - 1].Y;
+    }
+
+    public double InterpolateLinear(double x) {
+      if (ecdf.Count == 0) return double.NaN;
+      if (x < ecdf[0].X) return 0;
+      var last = ecdf[ecdf.Count - 1];
+      if (x >= last.X) return last.Y;
+
+      var index = ecdf.BinarySearch(Point2D<double>.Create(x, 0), abscissaComparer);
+      if (index >= 0) return ecdf[index].Y;
+      var prev = ecdf[~index - 1];
+      var next = ecdf[~index];
+      return prev.Y + (next.Y - prev.Y) * ((x - prev.X) / (next.X - prev.X));
+    }
+
+    public double InterpolateNearest(double x) {
+      if (ecdf.Count == 0) return double.NaN;
+      if (x < ecdf[0].X) return 0;
+      var last = ecdf[ecdf.Count - 1];
+      if (x >= last.X) return last.Y;
+
+      var index = ecdf.BinarySearch(Point2D<double>.Create(x, 0), abscissaComparer);
+      if (index >= 0) return ecdf[index].Y;
+      var prev = ecdf[~index - 1];
+      var next = ecdf[~index];
+      if (x - prev.X < next.X - x) return prev.Y;
+      return next.Y;
+    }
+
+    public double Inverse(double y) {
+      if (ecdf.Count == 0) return double.NaN;
+      if (y < 0 || y > 1) throw new ArgumentException("parameter must be in interval [0;1]", "y");
+      if (ecdf[ecdf.Count - 1].Y < y) return double.PositiveInfinity;
+      var index = ecdf.BinarySearch(Point2D<double>.Create(0, y), ordinateComparer);
+      if (index >= 0) return ecdf[index].X;
+      return ecdf[Math.Max(~index - 1, 0)].X;
+    }
+
+    private class AbscissaComparer : Comparer<Point2D<double>> {
+      public override int Compare(Point2D<double> x, Point2D<double> y) {
+        return x.X.CompareTo(y.X);
+      }
+    }
+
+    private class OrdinateComparer : Comparer<Point2D<double>> {
+      public override int Compare(Point2D<double> x, Point2D<double> y) {
+        return x.Y.CompareTo(y.Y);
+      }
+    }
+  }
+}
Index: /stable/HeuristicLab.Common/3.3/Statistics/EnumerableStatisticExtensions.cs
===================================================================
--- /stable/HeuristicLab.Common/3.3/Statistics/EnumerableStatisticExtensions.cs	(revision 15220)
+++ /stable/HeuristicLab.Common/3.3/Statistics/EnumerableStatisticExtensions.cs	(revision 15220)
@@ -0,0 +1,239 @@
+#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 <http://www.gnu.org/licenses/>.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Linq;
+
+namespace HeuristicLab.Common {
+  public static class EnumerableStatisticExtensions {
+    /// <summary>
+    /// Calculates the median element of the enumeration.
+    /// </summary>
+    /// <param name="values"></param>
+    /// <returns></returns>
+    public static double Median(this IEnumerable<double> values) {
+      // See unit tests for comparison with naive implementation
+      return Quantile(values, 0.5);
+    }
+
+    /// <summary>
+    /// Calculates the alpha-quantile element of the enumeration.
+    /// </summary>
+    /// <param name="values"></param>
+    /// <returns></returns>
+    public static double Quantile(this IEnumerable<double> values, double alpha) {
+      // See unit tests for comparison with naive implementation
+      double[] valuesArr = values.ToArray();
+      int n = valuesArr.Length;
+      if (n == 0) throw new InvalidOperationException("Enumeration contains no elements.");
+
+      // "When N is even, statistics books define the median as the arithmetic mean of the elements k = N/2 
+      // and k = N/2 + 1 (that is, N/2 from the bottom and N/2 from the top). 
+      // If you accept such pedantry, you must perform two separate selections to find these elements."
+
+      // return the element at Math.Ceiling (if n*alpha is fractional) or the average of two elements if n*alpha is integer.
+      var pos = n * alpha;
+      Contract.Assert(pos >= 0);
+      Contract.Assert(pos < n);
+      bool isInteger = Math.Round(pos).IsAlmost(pos);
+      if (isInteger) {
+        return 0.5 * (Select((int)pos - 1, valuesArr) + Select((int)pos, valuesArr));
+      } else {
+        return Select((int)Math.Ceiling(pos) - 1, valuesArr);
+      }
+    }
+
+    // Numerical Recipes in C++, §8.5 Selecting the Mth Largest, O(n)
+    // Given k in [0..n-1] returns an array value from array arr[0..n-1] such that k array values are 
+    // less than or equal to the one returned. The input array will be rearranged to have this value in 
+    // location arr[k], with all smaller elements moved to arr[0..k-1] (in arbitrary order) and all 
+    // larger elements in arr[k+1..n-1] (also in arbitrary order).
+    // 
+    // Could be changed to Select<T> where T is IComparable but in this case is significantly slower for double values
+    private static double Select(int k, double[] arr) {
+      Contract.Assert(arr.GetLowerBound(0) == 0);
+      Contract.Assert(k >= 0 && k < arr.Length);
+      int i, ir, j, l, mid, n = arr.Length;
+      double a;
+      l = 0;
+      ir = n - 1;
+      for (; ; ) {
+        if (ir <= l + 1) {
+          // Active partition contains 1 or 2 elements.
+          if (ir == l + 1 && arr[ir] < arr[l]) {
+            // if (ir == l + 1 && arr[ir].CompareTo(arr[l]) < 0) {
+            // Case of 2 elements.
+            // SWAP(arr[l], arr[ir]);
+            double temp = arr[l];
+            arr[l] = arr[ir];
+            arr[ir] = temp;
+          }
+          return arr[k];
+        } else {
+          mid = (l + ir) >> 1; // Choose median of left, center, and right elements
+          {
+            // SWAP(arr[mid], arr[l + 1]); // as partitioning element a. Also
+            double temp = arr[mid];
+            arr[mid] = arr[l + 1];
+            arr[l + 1] = temp;
+          }
+
+          if (arr[l] > arr[ir]) {
+            // if (arr[l].CompareTo(arr[ir]) > 0) {  // rearrange so that arr[l] arr[ir] <= arr[l+1],
+            // SWAP(arr[l], arr[ir]); . arr[ir] >= arr[l+1]
+            double temp = arr[l];
+            arr[l] = arr[ir];
+            arr[ir] = temp;
+          }
+
+          if (arr[l + 1] > arr[ir]) {
+            // if (arr[l + 1].CompareTo(arr[ir]) > 0) {
+            // SWAP(arr[l + 1], arr[ir]);
+            double temp = arr[l + 1];
+            arr[l + 1] = arr[ir];
+            arr[ir] = temp;
+          }
+          if (arr[l] > arr[l + 1]) {
+            //if (arr[l].CompareTo(arr[l + 1]) > 0) {
+            // SWAP(arr[l], arr[l + 1]);
+            double temp = arr[l];
+            arr[l] = arr[l + 1];
+            arr[l + 1] = temp;
+
+          }
+          i = l + 1; // Initialize pointers for partitioning.
+          j = ir;
+          a = arr[l + 1]; // Partitioning element.
+          for (; ; ) { // Beginning of innermost loop.
+            do i++; while (arr[i] < a /* arr[i].CompareTo(a) < 0 */); // Scan up to find element > a.
+            do j--; while (arr[j] > a /* arr[j].CompareTo(a) > 0 */); // Scan down to find element < a.
+            if (j < i) break; // Pointers crossed. Partitioning complete.
+            {
+              // SWAP(arr[i], arr[j]);
+              double temp = arr[i];
+              arr[i] = arr[j];
+              arr[j] = temp;
+            }
+          } // End of innermost loop.
+          arr[l + 1] = arr[j]; // Insert partitioning element.
+          arr[j] = a;
+          if (j >= k) ir = j - 1; // Keep active the partition that contains the
+          if (j <= k) l = i; // kth element.
+        }
+      }
+    }
+
+    /// <summary>
+    /// Calculates the range (max - min) of the enumeration.
+    /// </summary>
+    /// <param name="values"></param>
+    /// <returns></returns>
+    public static double Range(this IEnumerable<double> values) {
+      double min = double.PositiveInfinity;
+      double max = double.NegativeInfinity;
+      int i = 0;
+      foreach (var e in values) {
+        if (min > e) min = e;
+        if (max < e) max = e;
+        i++;
+      }
+      if (i < 1) throw new ArgumentException("The enumerable must contain at least two elements", "values");
+      return max - min;
+    }
+
+
+    /// <summary>
+    /// Calculates the sample standard deviation of values.
+    /// </summary>
+    /// <param name="values"></param>
+    /// <returns></returns>
+    public static double StandardDeviation(this IEnumerable<double> values) {
+      return Math.Sqrt(Variance(values));
+    }
+
+    /// <summary>
+    /// Calculates the population standard deviation of values.
+    /// </summary>
+    /// <param name="values"></param>
+    /// <returns></returns>
+    public static double StandardDeviationPop(this IEnumerable<double> values) {
+      return Math.Sqrt(VariancePop(values));
+    }
+
+    /// <summary>
+    /// Calculates the sample variance of values. (sum (x - x_mean)² / (n-1))
+    /// </summary>
+    /// <param name="values"></param>
+    /// <returns></returns>
+    public static double Variance(this IEnumerable<double> values) {
+      return Variance(values, true);
+    }
+
+    /// <summary>
+    /// Calculates the population variance of values. (sum (x - x_mean)² / n)
+    /// </summary>
+    /// <param name="values"></param>
+    /// <returns></returns>
+    public static double VariancePop(this IEnumerable<double> values) {
+      return Variance(values, false);
+    }
+
+    private static double Variance(IEnumerable<double> values, bool sampleVariance) {
+      int m_n = 0;
+      double m_oldM = 0.0;
+      double m_newM = 0.0;
+      double m_oldS = 0.0;
+      double m_newS = 0.0;
+      foreach (double x in values) {
+        m_n++;
+        if (m_n == 1) {
+          m_oldM = m_newM = x;
+          m_oldS = 0.0;
+        } else {
+          m_newM = m_oldM + (x - m_oldM) / m_n;
+          m_newS = m_oldS + (x - m_oldM) * (x - m_newM);
+
+          // set up for next iteration
+          m_oldM = m_newM;
+          m_oldS = m_newS;
+        }
+      }
+
+      if (m_n == 0) return double.NaN;
+      if (m_n == 1) return 0.0;
+
+      if (sampleVariance) return m_newS / (m_n - 1);
+      else return m_newS / m_n;
+    }
+
+    public static IEnumerable<double> LimitToRange(this IEnumerable<double> values, double min, double max) {
+      if (min > max) throw new ArgumentException(string.Format("Minimum {0} is larger than maximum {1}.", min, max));
+      foreach (var x in values) {
+        if (double.IsNaN(x)) yield return (max + min) / 2.0;
+        else if (x < min) yield return min;
+        else if (x > max) yield return max;
+        else yield return x;
+      }
+    }
+  }
+}
Index: /stable/HeuristicLab.Optimization.Views/3.3/HeuristicLab.Optimization.Views-3.3.csproj
===================================================================
--- /stable/HeuristicLab.Optimization.Views/3.3/HeuristicLab.Optimization.Views-3.3.csproj	(revision 15219)
+++ /stable/HeuristicLab.Optimization.Views/3.3/HeuristicLab.Optimization.Views-3.3.csproj	(revision 15220)
@@ -269,4 +269,10 @@
       <DependentUpon>RunCollectionEqualityConstraintView.cs</DependentUpon>
     </Compile>
+    <Compile Include="RunCollectionViews\RunCollectionRLDView.cs">
+      <SubType>UserControl</SubType>
+    </Compile>
+    <Compile Include="RunCollectionViews\RunCollectionRLDView.Designer.cs">
+      <DependentUpon>RunCollectionRLDView.cs</DependentUpon>
+    </Compile>
     <Compile Include="RunCollectionViews\RunCollectionTableView.cs">
       <SubType>UserControl</SubType>
Index: /stable/HeuristicLab.Optimization.Views/3.3/RunCollectionViews/RunCollectionRLDView.Designer.cs
===================================================================
--- /stable/HeuristicLab.Optimization.Views/3.3/RunCollectionViews/RunCollectionRLDView.Designer.cs	(revision 15220)
+++ /stable/HeuristicLab.Optimization.Views/3.3/RunCollectionViews/RunCollectionRLDView.Designer.cs	(revision 15220)
@@ -0,0 +1,510 @@
+﻿#region License Information
+/* HeuristicLab
+ * Copyright (C) 2002-2015 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 <http://www.gnu.org/licenses/>.
+ */
+#endregion
+namespace HeuristicLab.Optimization.Views {
+  partial class RunCollectionRLDView {
+    /// <summary> 
+    /// Required designer variable.
+    /// </summary>
+    private System.ComponentModel.IContainer components = null;
+
+    /// <summary> 
+    /// Clean up any resources being used.
+    /// </summary>
+    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+    protected override void Dispose(bool disposing) {
+      if (disposing && (components != null)) {
+        components.Dispose();
+      }
+      base.Dispose(disposing);
+    }
+
+    #region Component Designer generated code
+
+    /// <summary> 
+    /// Required method for Designer support - do not modify 
+    /// the contents of this method with the code editor.
+    /// </summary>
+    private void InitializeComponent() {
+      this.components = new System.ComponentModel.Container();
+      System.Windows.Forms.DataVisualization.Charting.ChartArea chartArea2 = new System.Windows.Forms.DataVisualization.Charting.ChartArea();
+      System.Windows.Forms.DataVisualization.Charting.Legend legend2 = new System.Windows.Forms.DataVisualization.Charting.Legend();
+      this.dataTableComboBox = new System.Windows.Forms.ComboBox();
+      this.dataTableLabel = new System.Windows.Forms.Label();
+      this.groupLabel = new System.Windows.Forms.Label();
+      this.groupComboBox = new System.Windows.Forms.ComboBox();
+      this.targetLogScalingCheckBox = new System.Windows.Forms.CheckBox();
+      this.targetsTextBox = new System.Windows.Forms.TextBox();
+      this.targetsLabel = new System.Windows.Forms.Label();
+      this.errorProvider = new System.Windows.Forms.ErrorProvider(this.components);
+      this.addTargetsAsResultButton = new System.Windows.Forms.Button();
+      this.generateTargetsButton = new System.Windows.Forms.Button();
+      this.toolTip = new System.Windows.Forms.ToolTip(this.components);
+      this.budgetsLabel = new System.Windows.Forms.Label();
+      this.budgetsTextBox = new System.Windows.Forms.TextBox();
+      this.addBudgetsAsResultButton = new System.Windows.Forms.Button();
+      this.aggregateTargetsCheckBox = new System.Windows.Forms.CheckBox();
+      this.tabControl = new System.Windows.Forms.TabControl();
+      this.byTargetTabPage = new System.Windows.Forms.TabPage();
+      this.relativeOrAbsoluteComboBox = new System.Windows.Forms.ComboBox();
+      this.targetChart = new HeuristicLab.Visualization.ChartControlsExtensions.EnhancedChart();
+      this.showLabelsCheckBox = new System.Windows.Forms.CheckBox();
+      this.markerCheckBox = new System.Windows.Forms.CheckBox();
+      this.boundShadingCheckBox = new System.Windows.Forms.CheckBox();
+      this.byCostTabPage = new System.Windows.Forms.TabPage();
+      this.byCostViewHost = new HeuristicLab.MainForm.WindowsForms.ViewHost();
+      this.budgetLogScalingCheckBox = new System.Windows.Forms.CheckBox();
+      this.generateBudgetsButton = new System.Windows.Forms.Button();
+      this.byTableTabPage = new System.Windows.Forms.TabPage();
+      this.ertTableView = new HeuristicLab.Data.Views.StringConvertibleMatrixView();
+      this.problemComboBox = new System.Windows.Forms.ComboBox();
+      this.problemLabel = new System.Windows.Forms.Label();
+      ((System.ComponentModel.ISupportInitialize)(this.errorProvider)).BeginInit();
+      this.tabControl.SuspendLayout();
+      this.byTargetTabPage.SuspendLayout();
+      ((System.ComponentModel.ISupportInitialize)(this.targetChart)).BeginInit();
+      this.byCostTabPage.SuspendLayout();
+      this.byTableTabPage.SuspendLayout();
+      this.SuspendLayout();
+      // 
+      // dataTableComboBox
+      // 
+      this.dataTableComboBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+      this.dataTableComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+      this.dataTableComboBox.FormattingEnabled = true;
+      this.dataTableComboBox.Location = new System.Drawing.Point(69, 57);
+      this.dataTableComboBox.Name = "dataTableComboBox";
+      this.dataTableComboBox.Size = new System.Drawing.Size(582, 21);
+      this.dataTableComboBox.TabIndex = 5;
+      this.dataTableComboBox.SelectedIndexChanged += new System.EventHandler(this.dataTableComboBox_SelectedIndexChanged);
+      // 
+      // dataTableLabel
+      // 
+      this.dataTableLabel.AutoSize = true;
+      this.dataTableLabel.Location = new System.Drawing.Point(3, 60);
+      this.dataTableLabel.Name = "dataTableLabel";
+      this.dataTableLabel.Size = new System.Drawing.Size(60, 13);
+      this.dataTableLabel.TabIndex = 4;
+      this.dataTableLabel.Text = "DataTable:";
+      // 
+      // groupLabel
+      // 
+      this.groupLabel.AutoSize = true;
+      this.groupLabel.Location = new System.Drawing.Point(3, 6);
+      this.groupLabel.Name = "groupLabel";
+      this.groupLabel.Size = new System.Drawing.Size(39, 13);
+      this.groupLabel.TabIndex = 0;
+      this.groupLabel.Text = "Group:";
+      // 
+      // groupComboBox
+      // 
+      this.groupComboBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+      this.groupComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+      this.groupComboBox.FormattingEnabled = true;
+      this.groupComboBox.Location = new System.Drawing.Point(69, 3);
+      this.groupComboBox.Name = "groupComboBox";
+      this.groupComboBox.Size = new System.Drawing.Size(582, 21);
+      this.groupComboBox.TabIndex = 1;
+      this.groupComboBox.SelectedIndexChanged += new System.EventHandler(this.groupComboBox_SelectedIndexChanged);
+      // 
+      // targetLogScalingCheckBox
+      // 
+      this.targetLogScalingCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
+      this.targetLogScalingCheckBox.AutoSize = true;
+      this.targetLogScalingCheckBox.Location = new System.Drawing.Point(9, 364);
+      this.targetLogScalingCheckBox.Name = "targetLogScalingCheckBox";
+      this.targetLogScalingCheckBox.Size = new System.Drawing.Size(112, 17);
+      this.targetLogScalingCheckBox.TabIndex = 6;
+      this.targetLogScalingCheckBox.Text = "logarithmic scaling";
+      this.targetLogScalingCheckBox.UseVisualStyleBackColor = true;
+      this.targetLogScalingCheckBox.CheckedChanged += new System.EventHandler(this.logScalingCheckBox_CheckedChanged);
+      // 
+      // targetsTextBox
+      // 
+      this.targetsTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+      this.targetsTextBox.Location = new System.Drawing.Point(59, 8);
+      this.targetsTextBox.Name = "targetsTextBox";
+      this.targetsTextBox.Size = new System.Drawing.Size(204, 20);
+      this.targetsTextBox.TabIndex = 1;
+      this.toolTip.SetToolTip(this.targetsTextBox, "The order of the targets is important, first to-hit targets\r\nshould be given firs" +
+        "t. The sequence should be monotonous.\r\n\r\nTargets should be separated by semicolo" +
+        "n, tab or space.");
+      this.targetsTextBox.Validating += new System.ComponentModel.CancelEventHandler(this.targetsTextBox_Validating);
+      // 
+      // targetsLabel
+      // 
+      this.targetsLabel.AutoSize = true;
+      this.targetsLabel.Location = new System.Drawing.Point(6, 11);
+      this.targetsLabel.Name = "targetsLabel";
+      this.targetsLabel.Size = new System.Drawing.Size(46, 13);
+      this.targetsLabel.TabIndex = 0;
+      this.targetsLabel.Text = "Targets:";
+      // 
+      // errorProvider
+      // 
+      this.errorProvider.ContainerControl = this;
+      // 
+      // addTargetsAsResultButton
+      // 
+      this.addTargetsAsResultButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
+      this.addTargetsAsResultButton.Location = new System.Drawing.Point(551, 6);
+      this.addTargetsAsResultButton.Name = "addTargetsAsResultButton";
+      this.addTargetsAsResultButton.Size = new System.Drawing.Size(89, 23);
+      this.addTargetsAsResultButton.TabIndex = 4;
+      this.addTargetsAsResultButton.Text = "Add as Result";
+      this.addTargetsAsResultButton.UseVisualStyleBackColor = true;
+      this.addTargetsAsResultButton.Click += new System.EventHandler(this.addTargetsAsResultButton_Click);
+      // 
+      // generateTargetsButton
+      // 
+      this.generateTargetsButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
+      this.generateTargetsButton.Location = new System.Drawing.Point(456, 6);
+      this.generateTargetsButton.Name = "generateTargetsButton";
+      this.generateTargetsButton.Size = new System.Drawing.Size(89, 23);
+      this.generateTargetsButton.TabIndex = 3;
+      this.generateTargetsButton.Text = "Generate...";
+      this.generateTargetsButton.UseVisualStyleBackColor = true;
+      this.generateTargetsButton.Click += new System.EventHandler(this.generateTargetsButton_Click);
+      // 
+      // budgetsLabel
+      // 
+      this.budgetsLabel.AutoSize = true;
+      this.budgetsLabel.Location = new System.Drawing.Point(6, 11);
+      this.budgetsLabel.Name = "budgetsLabel";
+      this.budgetsLabel.Size = new System.Drawing.Size(49, 13);
+      this.budgetsLabel.TabIndex = 1;
+      this.budgetsLabel.Text = "Budgets:";
+      // 
+      // budgetsTextBox
+      // 
+      this.budgetsTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+      this.budgetsTextBox.Location = new System.Drawing.Point(59, 8);
+      this.budgetsTextBox.Name = "budgetsTextBox";
+      this.budgetsTextBox.Size = new System.Drawing.Size(391, 20);
+      this.budgetsTextBox.TabIndex = 6;
+      this.budgetsTextBox.Validating += new System.ComponentModel.CancelEventHandler(this.budgetsTextBox_Validating);
+      // 
+      // addBudgetsAsResultButton
+      // 
+      this.addBudgetsAsResultButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
+      this.addBudgetsAsResultButton.Location = new System.Drawing.Point(551, 6);
+      this.addBudgetsAsResultButton.Name = "addBudgetsAsResultButton";
+      this.addBudgetsAsResultButton.Size = new System.Drawing.Size(89, 23);
+      this.addBudgetsAsResultButton.TabIndex = 7;
+      this.addBudgetsAsResultButton.Text = "Add as Result";
+      this.addBudgetsAsResultButton.UseVisualStyleBackColor = true;
+      this.addBudgetsAsResultButton.Click += new System.EventHandler(this.addBudgetsAsResultButton_Click);
+      // 
+      // aggregateTargetsCheckBox
+      // 
+      this.aggregateTargetsCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
+      this.aggregateTargetsCheckBox.AutoSize = true;
+      this.aggregateTargetsCheckBox.Checked = true;
+      this.aggregateTargetsCheckBox.CheckState = System.Windows.Forms.CheckState.Checked;
+      this.aggregateTargetsCheckBox.Location = new System.Drawing.Point(374, 10);
+      this.aggregateTargetsCheckBox.Name = "aggregateTargetsCheckBox";
+      this.aggregateTargetsCheckBox.Size = new System.Drawing.Size(74, 17);
+      this.aggregateTargetsCheckBox.TabIndex = 2;
+      this.aggregateTargetsCheckBox.Text = "aggregate";
+      this.aggregateTargetsCheckBox.UseVisualStyleBackColor = true;
+      this.aggregateTargetsCheckBox.CheckedChanged += new System.EventHandler(this.aggregateTargetsCheckBox_CheckedChanged);
+      // 
+      // tabControl
+      // 
+      this.tabControl.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
+            | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+      this.tabControl.Controls.Add(this.byTargetTabPage);
+      this.tabControl.Controls.Add(this.byCostTabPage);
+      this.tabControl.Controls.Add(this.byTableTabPage);
+      this.tabControl.Location = new System.Drawing.Point(0, 84);
+      this.tabControl.Name = "tabControl";
+      this.tabControl.SelectedIndex = 0;
+      this.tabControl.Size = new System.Drawing.Size(654, 413);
+      this.tabControl.TabIndex = 6;
+      // 
+      // byTargetTabPage
+      // 
+      this.byTargetTabPage.Controls.Add(this.relativeOrAbsoluteComboBox);
+      this.byTargetTabPage.Controls.Add(this.targetChart);
+      this.byTargetTabPage.Controls.Add(this.showLabelsCheckBox);
+      this.byTargetTabPage.Controls.Add(this.markerCheckBox);
+      this.byTargetTabPage.Controls.Add(this.boundShadingCheckBox);
+      this.byTargetTabPage.Controls.Add(this.targetLogScalingCheckBox);
+      this.byTargetTabPage.Controls.Add(this.targetsLabel);
+      this.byTargetTabPage.Controls.Add(this.aggregateTargetsCheckBox);
+      this.byTargetTabPage.Controls.Add(this.targetsTextBox);
+      this.byTargetTabPage.Controls.Add(this.generateTargetsButton);
+      this.byTargetTabPage.Controls.Add(this.addTargetsAsResultButton);
+      this.byTargetTabPage.Location = new System.Drawing.Point(4, 22);
+      this.byTargetTabPage.Name = "byTargetTabPage";
+      this.byTargetTabPage.Padding = new System.Windows.Forms.Padding(3);
+      this.byTargetTabPage.Size = new System.Drawing.Size(646, 387);
+      this.byTargetTabPage.TabIndex = 0;
+      this.byTargetTabPage.Text = "Performance by Target";
+      this.byTargetTabPage.UseVisualStyleBackColor = true;
+      // 
+      // relativeOrAbsoluteComboBox
+      // 
+      this.relativeOrAbsoluteComboBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
+      this.relativeOrAbsoluteComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+      this.relativeOrAbsoluteComboBox.FormattingEnabled = true;
+      this.relativeOrAbsoluteComboBox.Items.AddRange(new object[] {
+            "relative",
+            "absolute"});
+      this.relativeOrAbsoluteComboBox.Location = new System.Drawing.Point(269, 8);
+      this.relativeOrAbsoluteComboBox.Name = "relativeOrAbsoluteComboBox";
+      this.relativeOrAbsoluteComboBox.Size = new System.Drawing.Size(99, 21);
+      this.relativeOrAbsoluteComboBox.TabIndex = 8;
+      this.relativeOrAbsoluteComboBox.SelectedIndexChanged += new System.EventHandler(this.relativeOrAbsoluteComboBox_SelectedIndexChanged);
+      // 
+      // targetChart
+      // 
+      this.targetChart.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
+            | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+      chartArea2.AxisX.IsStartedFromZero = false;
+      chartArea2.AxisX.MinorGrid.Enabled = true;
+      chartArea2.AxisX.MinorGrid.LineColor = System.Drawing.Color.WhiteSmoke;
+      chartArea2.AxisX.MinorGrid.LineDashStyle = System.Windows.Forms.DataVisualization.Charting.ChartDashStyle.Dash;
+      chartArea2.AxisY.Maximum = 1D;
+      chartArea2.AxisY.Minimum = 0D;
+      chartArea2.AxisY.MinorGrid.Enabled = true;
+      chartArea2.AxisY.MinorGrid.LineColor = System.Drawing.Color.WhiteSmoke;
+      chartArea2.AxisY.MinorGrid.LineDashStyle = System.Windows.Forms.DataVisualization.Charting.ChartDashStyle.Dash;
+      chartArea2.Name = "ChartArea1";
+      this.targetChart.ChartAreas.Add(chartArea2);
+      legend2.Alignment = System.Drawing.StringAlignment.Center;
+      legend2.Docking = System.Windows.Forms.DataVisualization.Charting.Docking.Top;
+      legend2.Name = "Legend1";
+      this.targetChart.Legends.Add(legend2);
+      this.targetChart.Location = new System.Drawing.Point(6, 34);
+      this.targetChart.Name = "targetChart";
+      this.targetChart.Size = new System.Drawing.Size(634, 324);
+      this.targetChart.SuppressExceptions = true;
+      this.targetChart.TabIndex = 7;
+      this.targetChart.Text = "enhancedChart1";
+      this.targetChart.CustomizeLegend += new System.EventHandler<System.Windows.Forms.DataVisualization.Charting.CustomizeLegendEventArgs>(this.chart_CustomizeLegend);
+      this.targetChart.MouseDown += new System.Windows.Forms.MouseEventHandler(this.chart_MouseDown);
+      this.targetChart.MouseMove += new System.Windows.Forms.MouseEventHandler(this.chart_MouseMove);
+      // 
+      // showLabelsCheckBox
+      // 
+      this.showLabelsCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
+      this.showLabelsCheckBox.AutoSize = true;
+      this.showLabelsCheckBox.Checked = true;
+      this.showLabelsCheckBox.CheckState = System.Windows.Forms.CheckState.Checked;
+      this.showLabelsCheckBox.Location = new System.Drawing.Point(329, 364);
+      this.showLabelsCheckBox.Name = "showLabelsCheckBox";
+      this.showLabelsCheckBox.Size = new System.Drawing.Size(81, 17);
+      this.showLabelsCheckBox.TabIndex = 6;
+      this.showLabelsCheckBox.Text = "show labels";
+      this.showLabelsCheckBox.UseVisualStyleBackColor = true;
+      this.showLabelsCheckBox.CheckedChanged += new System.EventHandler(this.showLabelsCheckBox_CheckedChanged);
+      // 
+      // markerCheckBox
+      // 
+      this.markerCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
+      this.markerCheckBox.AutoSize = true;
+      this.markerCheckBox.Location = new System.Drawing.Point(229, 364);
+      this.markerCheckBox.Name = "markerCheckBox";
+      this.markerCheckBox.Size = new System.Drawing.Size(91, 17);
+      this.markerCheckBox.TabIndex = 6;
+      this.markerCheckBox.Text = "show markers";
+      this.markerCheckBox.UseVisualStyleBackColor = true;
+      this.markerCheckBox.CheckedChanged += new System.EventHandler(this.markerCheckBox_CheckedChanged);
+      // 
+      // boundShadingCheckBox
+      // 
+      this.boundShadingCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
+      this.boundShadingCheckBox.AutoSize = true;
+      this.boundShadingCheckBox.Checked = true;
+      this.boundShadingCheckBox.CheckState = System.Windows.Forms.CheckState.Checked;
+      this.boundShadingCheckBox.Location = new System.Drawing.Point(127, 364);
+      this.boundShadingCheckBox.Name = "boundShadingCheckBox";
+      this.boundShadingCheckBox.Size = new System.Drawing.Size(96, 17);
+      this.boundShadingCheckBox.TabIndex = 6;
+      this.boundShadingCheckBox.Text = "bound shading";
+      this.boundShadingCheckBox.UseVisualStyleBackColor = true;
+      this.boundShadingCheckBox.CheckedChanged += new System.EventHandler(this.boundShadingCheckBox_CheckedChanged);
+      // 
+      // byCostTabPage
+      // 
+      this.byCostTabPage.Controls.Add(this.byCostViewHost);
+      this.byCostTabPage.Controls.Add(this.budgetLogScalingCheckBox);
+      this.byCostTabPage.Controls.Add(this.generateBudgetsButton);
+      this.byCostTabPage.Controls.Add(this.budgetsLabel);
+      this.byCostTabPage.Controls.Add(this.addBudgetsAsResultButton);
+      this.byCostTabPage.Controls.Add(this.budgetsTextBox);
+      this.byCostTabPage.Location = new System.Drawing.Point(4, 22);
+      this.byCostTabPage.Name = "byCostTabPage";
+      this.byCostTabPage.Padding = new System.Windows.Forms.Padding(3);
+      this.byCostTabPage.Size = new System.Drawing.Size(646, 387);
+      this.byCostTabPage.TabIndex = 1;
+      this.byCostTabPage.Text = "Performance by Cost";
+      this.byCostTabPage.UseVisualStyleBackColor = true;
+      // 
+      // byCostViewHost
+      // 
+      this.byCostViewHost.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
+            | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+      this.byCostViewHost.Caption = "View";
+      this.byCostViewHost.Content = null;
+      this.byCostViewHost.Enabled = false;
+      this.byCostViewHost.Location = new System.Drawing.Point(6, 34);
+      this.byCostViewHost.Name = "byCostViewHost";
+      this.byCostViewHost.ReadOnly = false;
+      this.byCostViewHost.Size = new System.Drawing.Size(634, 324);
+      this.byCostViewHost.TabIndex = 12;
+      this.byCostViewHost.ViewsLabelVisible = true;
+      this.byCostViewHost.ViewType = null;
+      // 
+      // budgetLogScalingCheckBox
+      // 
+      this.budgetLogScalingCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
+      this.budgetLogScalingCheckBox.AutoSize = true;
+      this.budgetLogScalingCheckBox.Location = new System.Drawing.Point(9, 364);
+      this.budgetLogScalingCheckBox.Name = "budgetLogScalingCheckBox";
+      this.budgetLogScalingCheckBox.Size = new System.Drawing.Size(112, 17);
+      this.budgetLogScalingCheckBox.TabIndex = 11;
+      this.budgetLogScalingCheckBox.Text = "logarithmic scaling";
+      this.budgetLogScalingCheckBox.UseVisualStyleBackColor = true;
+      this.budgetLogScalingCheckBox.CheckedChanged += new System.EventHandler(this.logScalingCheckBox_CheckedChanged);
+      // 
+      // generateBudgetsButton
+      // 
+      this.generateBudgetsButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
+      this.generateBudgetsButton.Location = new System.Drawing.Point(456, 6);
+      this.generateBudgetsButton.Name = "generateBudgetsButton";
+      this.generateBudgetsButton.Size = new System.Drawing.Size(89, 23);
+      this.generateBudgetsButton.TabIndex = 9;
+      this.generateBudgetsButton.Text = "Generate...";
+      this.generateBudgetsButton.UseVisualStyleBackColor = true;
+      this.generateBudgetsButton.Click += new System.EventHandler(this.generateBudgetsButton_Click);
+      // 
+      // byTableTabPage
+      // 
+      this.byTableTabPage.Controls.Add(this.ertTableView);
+      this.byTableTabPage.Location = new System.Drawing.Point(4, 22);
+      this.byTableTabPage.Name = "byTableTabPage";
+      this.byTableTabPage.Padding = new System.Windows.Forms.Padding(3);
+      this.byTableTabPage.Size = new System.Drawing.Size(646, 387);
+      this.byTableTabPage.TabIndex = 2;
+      this.byTableTabPage.Text = "Expected Runtime Tables";
+      this.byTableTabPage.UseVisualStyleBackColor = true;
+      // 
+      // ertTableView
+      // 
+      this.ertTableView.Caption = "StringConvertibleMatrix View";
+      this.ertTableView.Content = null;
+      this.ertTableView.Dock = System.Windows.Forms.DockStyle.Fill;
+      this.ertTableView.Location = new System.Drawing.Point(3, 3);
+      this.ertTableView.Name = "ertTableView";
+      this.ertTableView.ReadOnly = false;
+      this.ertTableView.ShowRowsAndColumnsTextBox = false;
+      this.ertTableView.ShowStatisticalInformation = false;
+      this.ertTableView.Size = new System.Drawing.Size(640, 381);
+      this.ertTableView.TabIndex = 0;
+      // 
+      // problemComboBox
+      // 
+      this.problemComboBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+      this.problemComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+      this.problemComboBox.FormattingEnabled = true;
+      this.problemComboBox.Location = new System.Drawing.Point(69, 30);
+      this.problemComboBox.Name = "problemComboBox";
+      this.problemComboBox.Size = new System.Drawing.Size(582, 21);
+      this.problemComboBox.TabIndex = 3;
+      this.problemComboBox.SelectedIndexChanged += new System.EventHandler(this.problemComboBox_SelectedIndexChanged);
+      // 
+      // problemLabel
+      // 
+      this.problemLabel.AutoSize = true;
+      this.problemLabel.Location = new System.Drawing.Point(3, 33);
+      this.problemLabel.Name = "problemLabel";
+      this.problemLabel.Size = new System.Drawing.Size(48, 13);
+      this.problemLabel.TabIndex = 2;
+      this.problemLabel.Text = "Problem:";
+      // 
+      // RunCollectionRLDView
+      // 
+      this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Inherit;
+      this.Controls.Add(this.tabControl);
+      this.Controls.Add(this.groupComboBox);
+      this.Controls.Add(this.groupLabel);
+      this.Controls.Add(this.problemLabel);
+      this.Controls.Add(this.dataTableLabel);
+      this.Controls.Add(this.problemComboBox);
+      this.Controls.Add(this.dataTableComboBox);
+      this.Name = "RunCollectionRLDView";
+      this.Size = new System.Drawing.Size(654, 497);
+      ((System.ComponentModel.ISupportInitialize)(this.errorProvider)).EndInit();
+      this.tabControl.ResumeLayout(false);
+      this.byTargetTabPage.ResumeLayout(false);
+      this.byTargetTabPage.PerformLayout();
+      ((System.ComponentModel.ISupportInitialize)(this.targetChart)).EndInit();
+      this.byCostTabPage.ResumeLayout(false);
+      this.byCostTabPage.PerformLayout();
+      this.byTableTabPage.ResumeLayout(false);
+      this.ResumeLayout(false);
+      this.PerformLayout();
+
+    }
+
+    #endregion
+
+    private System.Windows.Forms.ComboBox dataTableComboBox;
+    private System.Windows.Forms.Label dataTableLabel;
+    private System.Windows.Forms.Label groupLabel;
+    private System.Windows.Forms.ComboBox groupComboBox;
+    private System.Windows.Forms.CheckBox targetLogScalingCheckBox;
+    private System.Windows.Forms.TextBox targetsTextBox;
+    private System.Windows.Forms.Label targetsLabel;
+    protected System.Windows.Forms.ErrorProvider errorProvider;
+    private System.Windows.Forms.Button addTargetsAsResultButton;
+    private System.Windows.Forms.Button generateTargetsButton;
+    private System.Windows.Forms.ToolTip toolTip;
+    private System.Windows.Forms.Button addBudgetsAsResultButton;
+    private System.Windows.Forms.TextBox budgetsTextBox;
+    private System.Windows.Forms.Label budgetsLabel;
+    private System.Windows.Forms.CheckBox aggregateTargetsCheckBox;
+    private System.Windows.Forms.TabControl tabControl;
+    private System.Windows.Forms.TabPage byTargetTabPage;
+    private System.Windows.Forms.TabPage byCostTabPage;
+    private System.Windows.Forms.TabPage byTableTabPage;
+    private System.Windows.Forms.Button generateBudgetsButton;
+    private System.Windows.Forms.CheckBox budgetLogScalingCheckBox;
+    private System.Windows.Forms.Label problemLabel;
+    private System.Windows.Forms.ComboBox problemComboBox;
+    private Data.Views.StringConvertibleMatrixView ertTableView;
+    private Visualization.ChartControlsExtensions.EnhancedChart targetChart;
+    private MainForm.WindowsForms.ViewHost byCostViewHost;
+    private System.Windows.Forms.CheckBox boundShadingCheckBox;
+    private System.Windows.Forms.CheckBox markerCheckBox;
+    private System.Windows.Forms.ComboBox relativeOrAbsoluteComboBox;
+    private System.Windows.Forms.CheckBox showLabelsCheckBox;
+  }
+}
Index: /stable/HeuristicLab.Optimization.Views/3.3/RunCollectionViews/RunCollectionRLDView.cs
===================================================================
--- /stable/HeuristicLab.Optimization.Views/3.3/RunCollectionViews/RunCollectionRLDView.cs	(revision 15220)
+++ /stable/HeuristicLab.Optimization.Views/3.3/RunCollectionViews/RunCollectionRLDView.cs	(revision 15220)
@@ -0,0 +1,1405 @@
+﻿#region License Information
+/* HeuristicLab
+ * Copyright (C) 2002-2017 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 <http://www.gnu.org/licenses/>.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Globalization;
+using System.Linq;
+using System.Windows.Forms;
+using System.Windows.Forms.DataVisualization.Charting;
+using HeuristicLab.Analysis;
+using HeuristicLab.Collections;
+using HeuristicLab.Core;
+using HeuristicLab.Core.Views;
+using HeuristicLab.Data;
+using HeuristicLab.MainForm;
+using HeuristicLab.MainForm.WindowsForms;
+
+namespace HeuristicLab.Optimization.Views {
+  [View("Run-length Distribution View")]
+  [Content(typeof(RunCollection), false)]
+  public partial class RunCollectionRLDView : ItemView {
+    private List<Series> invisibleTargetSeries;
+
+    private const string AllInstances = "All Instances";
+
+    private static readonly Color[] colors = new[] {
+      Color.FromArgb(0x40, 0x6A, 0xB7),
+      Color.FromArgb(0xB1, 0x6D, 0x01),
+      Color.FromArgb(0x4E, 0x8A, 0x06),
+      Color.FromArgb(0x75, 0x50, 0x7B),
+      Color.FromArgb(0x72, 0x9F, 0xCF),
+      Color.FromArgb(0xA4, 0x00, 0x00),
+      Color.FromArgb(0xAD, 0x7F, 0xA8),
+      Color.FromArgb(0x29, 0x50, 0xCF),
+      Color.FromArgb(0x90, 0xB0, 0x60),
+      Color.FromArgb(0xF5, 0x89, 0x30),
+      Color.FromArgb(0x55, 0x57, 0x53),
+      Color.FromArgb(0xEF, 0x59, 0x59),
+      Color.FromArgb(0xED, 0xD4, 0x30),
+      Color.FromArgb(0x63, 0xC2, 0x16),
+    };
+    private static readonly ChartDashStyle[] lineStyles = new[] {
+      ChartDashStyle.Solid,
+      ChartDashStyle.Dash,
+      ChartDashStyle.DashDot,
+      ChartDashStyle.Dot
+    };
+    private static readonly DataRowVisualProperties.DataRowLineStyle[] hlLineStyles = new[] {
+      DataRowVisualProperties.DataRowLineStyle.Solid,
+      DataRowVisualProperties.DataRowLineStyle.Dash,
+      DataRowVisualProperties.DataRowLineStyle.DashDot,
+      DataRowVisualProperties.DataRowLineStyle.Dot
+    };
+
+    public new RunCollection Content {
+      get { return (RunCollection)base.Content; }
+      set { base.Content = value; }
+    }
+
+    private double[] targets;
+    private double[] budgets;
+    private bool targetsAreRelative = true;
+    private bool showLabelsInTargetChart = true;
+    private readonly BindingList<ProblemInstance> problems;
+
+    private bool suppressUpdates;
+    private readonly IndexedDataTable<double> byCostDataTable;
+    public IndexedDataTable<double> ByCostDataTable {
+      get { return byCostDataTable; }
+    }
+
+    public RunCollectionRLDView() {
+      InitializeComponent();
+      invisibleTargetSeries = new List<Series>();
+
+      targetChart.CustomizeAllChartAreas();
+      targetChart.ChartAreas[0].CursorX.Interval = 1;
+      targetChart.SuppressExceptions = true;
+      byCostDataTable = new IndexedDataTable<double>("ECDF by Cost", "A data table containing the ECDF of function values (relative to best-known).") {
+        VisualProperties = {
+          YAxisTitle = "Proportion of runs",
+          YAxisMinimumFixedValue = 0,
+          YAxisMinimumAuto = false,
+          YAxisMaximumFixedValue = 1,
+          YAxisMaximumAuto = false
+        }
+      };
+      byCostViewHost.Content = byCostDataTable;
+      suppressUpdates = true;
+      relativeOrAbsoluteComboBox.SelectedItem = targetsAreRelative ? "relative" : "absolute";
+      problems = new BindingList<ProblemInstance>();
+      problemComboBox.DataSource = new BindingSource() { DataSource = problems };
+      problemComboBox.DataBindings.DefaultDataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged;
+      suppressUpdates = false;
+    }
+
+    #region Content events
+    protected override void RegisterContentEvents() {
+      base.RegisterContentEvents();
+      Content.ItemsAdded += Content_ItemsAdded;
+      Content.ItemsRemoved += Content_ItemsRemoved;
+      Content.CollectionReset += Content_CollectionReset;
+      Content.UpdateOfRunsInProgressChanged += Content_UpdateOfRunsInProgressChanged;
+      Content.OptimizerNameChanged += Content_AlgorithmNameChanged;
+    }
+    protected override void DeregisterContentEvents() {
+      Content.ItemsAdded -= Content_ItemsAdded;
+      Content.ItemsRemoved -= Content_ItemsRemoved;
+      Content.CollectionReset -= Content_CollectionReset;
+      Content.UpdateOfRunsInProgressChanged -= Content_UpdateOfRunsInProgressChanged;
+      Content.OptimizerNameChanged -= Content_AlgorithmNameChanged;
+      base.DeregisterContentEvents();
+    }
+
+    private void Content_ItemsAdded(object sender, CollectionItemsChangedEventArgs<IRun> e) {
+      if (suppressUpdates) return;
+      if (InvokeRequired) {
+        Invoke(new CollectionItemsChangedEventHandler<IRun>(Content_ItemsAdded), sender, e);
+        return;
+      }
+      UpdateGroupAndProblemComboBox();
+      UpdateDataTableComboBox();
+      foreach (var run in e.Items)
+        RegisterRunEvents(run);
+    }
+    private void Content_ItemsRemoved(object sender, CollectionItemsChangedEventArgs<IRun> e) {
+      if (suppressUpdates) return;
+      if (InvokeRequired) {
+        Invoke(new CollectionItemsChangedEventHandler<IRun>(Content_ItemsRemoved), sender, e);
+        return;
+      }
+      UpdateGroupAndProblemComboBox();
+      UpdateDataTableComboBox();
+      foreach (var run in e.Items)
+        DeregisterRunEvents(run);
+    }
+    private void Content_CollectionReset(object sender, CollectionItemsChangedEventArgs<IRun> e) {
+      if (suppressUpdates) return;
+      if (InvokeRequired) {
+        Invoke(new CollectionItemsChangedEventHandler<IRun>(Content_CollectionReset), sender, e);
+        return;
+      }
+      UpdateGroupAndProblemComboBox();
+      UpdateDataTableComboBox();
+      foreach (var run in e.OldItems)
+        DeregisterRunEvents(run);
+    }
+    private void Content_AlgorithmNameChanged(object sender, EventArgs e) {
+      if (InvokeRequired)
+        Invoke(new EventHandler(Content_AlgorithmNameChanged), sender, e);
+      else UpdateCaption();
+    }
+    private void Content_UpdateOfRunsInProgressChanged(object sender, EventArgs e) {
+      if (InvokeRequired) {
+        Invoke(new EventHandler(Content_UpdateOfRunsInProgressChanged), sender, e);
+        return;
+      }
+      suppressUpdates = Content.UpdateOfRunsInProgress;
+      if (!suppressUpdates) {
+        UpdateDataTableComboBox();
+        UpdateGroupAndProblemComboBox();
+        UpdateRuns();
+      }
+    }
+
+    private void RegisterRunEvents(IRun run) {
+      run.PropertyChanged += run_PropertyChanged;
+    }
+    private void DeregisterRunEvents(IRun run) {
+      run.PropertyChanged -= run_PropertyChanged;
+    }
+    private void run_PropertyChanged(object sender, PropertyChangedEventArgs e) {
+      if (suppressUpdates) return;
+      if (InvokeRequired) {
+        Invoke((Action<object, PropertyChangedEventArgs>)run_PropertyChanged, sender, e);
+      } else {
+        if (e.PropertyName == "Visible")
+          UpdateRuns();
+      }
+    }
+    #endregion
+
+    protected override void OnContentChanged() {
+      base.OnContentChanged();
+      dataTableComboBox.Items.Clear();
+      groupComboBox.Items.Clear();
+      targetChart.ChartAreas[0].AxisX.IsLogarithmic = false;
+      targetChart.Series.Clear();
+      invisibleTargetSeries.Clear();
+      byCostDataTable.VisualProperties.XAxisLogScale = false;
+      byCostDataTable.Rows.Clear();
+
+      UpdateCaption();
+      if (Content != null) {
+        UpdateGroupAndProblemComboBox();
+        UpdateDataTableComboBox();
+      }
+    }
+
+
+    private void UpdateGroupAndProblemComboBox() {
+      var selectedGroupItem = (string)groupComboBox.SelectedItem;
+
+      var groupings = Content.ParameterNames.OrderBy(x => x).ToArray();
+      groupComboBox.Items.Clear();
+      groupComboBox.Items.Add(AllInstances);
+      groupComboBox.Items.AddRange(groupings);
+      if (selectedGroupItem != null && groupComboBox.Items.Contains(selectedGroupItem)) {
+        groupComboBox.SelectedItem = selectedGroupItem;
+      } else if (groupComboBox.Items.Count > 0) {
+        groupComboBox.SelectedItem = groupComboBox.Items[0];
+      }
+
+      var table = (string)dataTableComboBox.SelectedItem;
+      var problemDict = CalculateBestTargetPerProblemInstance(table);
+
+      var problemTypesDifferent = problemDict.Keys.Select(x => x.ProblemType).Where(x => !string.IsNullOrEmpty(x)).Distinct().Count() > 1;
+      var problemNamesDifferent = problemDict.Keys.Select(x => x.ProblemName).Where(x => !string.IsNullOrEmpty(x)).Distinct().Count() > 1;
+      var evaluatorDifferent = problemDict.Keys.Select(x => x.Evaluator).Where(x => !string.IsNullOrEmpty(x)).Distinct().Count() > 1;
+      var maximizationDifferent = problemDict.Keys.Select(x => x.Maximization).Distinct().Count() > 1;
+      var allEqual = !problemTypesDifferent && !problemNamesDifferent && !evaluatorDifferent && !maximizationDifferent;
+
+      var selectedProblemItem = (ProblemInstance)problemComboBox.SelectedItem;
+      problemComboBox.DataSource = null;
+      problemComboBox.Items.Clear();
+      problems.Clear();
+      problems.Add(ProblemInstance.MatchAll);
+      problemComboBox.DataSource = new BindingSource() { DataSource = problems };
+      problemComboBox.DataBindings.DefaultDataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged;
+      if (problems[0].Equals(selectedProblemItem)) problemComboBox.SelectedItem = problems[0];
+      foreach (var p in problemDict.ToList()) {
+        p.Key.BestKnownQuality = p.Value;
+        p.Key.DisplayProblemType = problemTypesDifferent;
+        p.Key.DisplayProblemName = problemNamesDifferent || allEqual;
+        p.Key.DisplayEvaluator = evaluatorDifferent;
+        p.Key.DisplayMaximization = maximizationDifferent;
+        problems.Add(p.Key);
+        if (p.Key.Equals(selectedProblemItem)) problemComboBox.SelectedItem = p.Key;
+      }
+      SetEnabledStateOfControls();
+    }
+
+    private void UpdateDataTableComboBox() {
+      string selectedItem = (string)dataTableComboBox.SelectedItem;
+
+      dataTableComboBox.Items.Clear();
+      var dataTables = (from run in Content
+                        from result in run.Results
+                        where result.Value is IndexedDataTable<double>
+                        select result.Key).Distinct().ToArray();
+
+      dataTableComboBox.Items.AddRange(dataTables);
+      if (selectedItem != null && dataTableComboBox.Items.Contains(selectedItem)) {
+        dataTableComboBox.SelectedItem = selectedItem;
+      } else if (dataTableComboBox.Items.Count > 0) {
+        dataTableComboBox.SelectedItem = dataTableComboBox.Items[0];
+      }
+    }
+
+    protected override void SetEnabledStateOfControls() {
+      base.SetEnabledStateOfControls();
+      groupComboBox.Enabled = Content != null;
+      problemComboBox.Enabled = Content != null && problemComboBox.Items.Count > 1;
+      dataTableComboBox.Enabled = Content != null && dataTableComboBox.Items.Count > 1;
+      addTargetsAsResultButton.Enabled = Content != null && targets != null && dataTableComboBox.SelectedIndex >= 0;
+      addBudgetsAsResultButton.Enabled = Content != null && budgets != null && dataTableComboBox.SelectedIndex >= 0;
+      generateTargetsButton.Enabled = targets != null;
+    }
+
+    private IEnumerable<AlgorithmInstance> GroupRuns() {
+      var table = (string)dataTableComboBox.SelectedItem;
+      if (string.IsNullOrEmpty(table)) yield break;
+
+      var selectedGroup = (string)groupComboBox.SelectedItem;
+      if (string.IsNullOrEmpty(selectedGroup)) yield break;
+
+      var selectedProblem = (ProblemInstance)problemComboBox.SelectedItem;
+      if (selectedProblem == null) yield break;
+      
+      foreach (var x in (from r in Content
+                         where (selectedGroup == AllInstances || r.Parameters.ContainsKey(selectedGroup))
+                           && selectedProblem.Match(r)
+                           && r.Results.ContainsKey(table)
+                           && r.Visible
+                         let key = selectedGroup == AllInstances ? AllInstances : r.Parameters[selectedGroup].ToString()
+                         group r by key into g
+                         select new AlgorithmInstance(g.Key, g, problems))) {
+        yield return x;
+      }
+    }
+
+    #region Performance analysis by (multiple) target(s)
+    private void UpdateResultsByTarget() {
+      // necessary to reset log scale -> empty chart cannot use log scaling
+      targetChart.ChartAreas[0].AxisX.IsLogarithmic = false;
+      targetChart.Series.Clear();
+      invisibleTargetSeries.Clear();
+      
+      var table = (string)dataTableComboBox.SelectedItem;
+      if (string.IsNullOrEmpty(table)) return;
+
+      if (targets == null) GenerateDefaultTargets();
+
+      var algInstances = GroupRuns().ToList();
+      if (algInstances.Count == 0) return;
+
+      var xAxisTitles = new HashSet<string>();
+
+      // hits describes the number of target hits at a certain time for a certain group
+      var hits = new Dictionary<string, SortedList<double, int>>();
+      // misses describes the number of target misses after a certain time for a certain group
+      // for instance when a run ends, but has not achieved all targets, misses describes
+      // how many targets have been left open at the point when the run ended
+      var misses = new Dictionary<string, SortedList<double, int>>();
+      var totalRuns = new Dictionary<string, int>();
+
+      var aggregate = aggregateTargetsCheckBox.Checked;
+      double minEff = double.MaxValue, maxEff = double.MinValue;
+      foreach (var alg in algInstances) {
+        var noRuns = 0;
+        SortedList<double, int> epdfHits = null, epdfMisses = null;
+        if (aggregate) {
+          hits[alg.Name] = epdfHits = new SortedList<double, int>();
+          misses[alg.Name] = epdfMisses = new SortedList<double, int>();
+        }
+        foreach (var problem in alg.GetProblemInstances()) {
+          var max = problem.IsMaximization();
+          var absTargets = GetAbsoluteTargets(problem).ToArray();
+          foreach (var run in alg.GetRuns(problem)) {
+            noRuns++;
+            var resultsTable = (IndexedDataTable<double>)run.Results[table];
+            xAxisTitles.Add(resultsTable.VisualProperties.XAxisTitle);
+
+            var efforts = absTargets.Select(t => GetEffortToHitTarget(resultsTable.Rows.First().Values, t, max)).ToArray();
+            minEff = Math.Min(minEff, efforts.Min(x => x.Item2));
+            maxEff = Math.Max(maxEff, efforts.Max(x => x.Item2));
+            for (var idx = 0; idx < efforts.Length; idx++) {
+              var e = efforts[idx];
+              if (!aggregate) {
+                var key = alg.Name + "@" + (targetsAreRelative
+                            ? (targets[idx] * 100).ToString(CultureInfo.CurrentCulture.NumberFormat) + "%"
+                            : targets[idx].ToString(CultureInfo.CurrentCulture.NumberFormat));
+                if (!hits.TryGetValue(key, out epdfHits))
+                  hits[key] = epdfHits = new SortedList<double, int>();
+                if (!misses.TryGetValue(key, out epdfMisses))
+                  misses[key] = epdfMisses = new SortedList<double, int>();
+                totalRuns[key] = noRuns;
+              };
+              var list = e.Item1 ? epdfHits : epdfMisses;
+              int v;
+              if (list.TryGetValue(e.Item2, out v))
+                list[e.Item2] = v + 1;
+              else list[e.Item2] = 1;
+            }
+          }
+        }
+        if (aggregate) totalRuns[alg.Name] = noRuns;
+      }
+
+      UpdateTargetChartAxisXBounds(minEff, maxEff);
+
+      DrawTargetsEcdf(hits, misses, totalRuns);
+
+      if (targets.Length == 1) {
+        if (targetsAreRelative)
+          targetChart.ChartAreas[0].AxisY.Title = "Probability to be " + (targets[0] * 100) + "% worse than best";
+        else targetChart.ChartAreas[0].AxisY.Title = "Probability to reach at least a fitness of " + targets[0];
+      } else targetChart.ChartAreas[0].AxisY.Title = "Proportion of reached targets";
+      targetChart.ChartAreas[0].AxisX.Title = string.Join(" / ", xAxisTitles);
+      targetChart.ChartAreas[0].AxisX.IsLogarithmic = CanDisplayLogarithmic();
+      targetChart.ChartAreas[0].CursorY.Interval = 0.05;
+
+      UpdateErtTables(algInstances);
+    }
+
+    private void DrawTargetsEcdf(Dictionary<string, SortedList<double, int>> hits, Dictionary<string, SortedList<double, int>> misses, Dictionary<string, int> noRuns) {
+      var colorCount = 0;
+      var lineStyleCount = 0;
+      
+      var showMarkers = markerCheckBox.Checked;
+      foreach (var list in hits) {
+        var row = new Series(list.Key) {
+          ChartType = SeriesChartType.StepLine,
+          BorderWidth = 3,
+          Color = colors[colorCount],
+          BorderDashStyle = lineStyles[lineStyleCount],
+        };
+        var rowShade = new Series(list.Key + "-range") {
+          IsVisibleInLegend = false,
+          ChartType = SeriesChartType.Range,
+          Color = Color.FromArgb(32, colors[colorCount]),
+          YValuesPerPoint = 2
+        };
+
+        var ecdf = 0.0;
+        var missedecdf = 0.0;
+        var iter = misses[list.Key].GetEnumerator();
+        var moreMisses = iter.MoveNext();
+        var totalTargets = noRuns[list.Key];
+        if (aggregateTargetsCheckBox.Checked) totalTargets *= targets.Length;
+        var movingTargets = totalTargets;
+        var labelPrinted = false;
+        foreach (var h in list.Value) {
+          var prevmissedecdf = missedecdf;
+          while (moreMisses && iter.Current.Key <= h.Key) {
+            if (!labelPrinted && row.Points.Count > 0) {
+              var point = row.Points.Last();
+              if (showLabelsInTargetChart)
+                point.Label = row.Name;
+              point.MarkerStyle = MarkerStyle.Cross;
+              point.MarkerBorderWidth = 1;
+              point.MarkerSize = 10;
+              labelPrinted = true;
+              rowShade.Points.Add(new DataPoint(point.XValue, new[] {ecdf / totalTargets, (ecdf + missedecdf) / totalTargets}));
+            }
+            missedecdf += iter.Current.Value;
+            movingTargets -= iter.Current.Value;
+            if (row.Points.Count > 0 && row.Points.Last().XValue == iter.Current.Key) {
+              row.Points.Last().SetValueY(ecdf / movingTargets);
+              row.Points.Last().Color = Color.FromArgb(255 - (int)Math.Floor(255 * (prevmissedecdf / totalTargets)), colors[colorCount]);
+            } else {
+              var dp = new DataPoint(iter.Current.Key, ecdf / movingTargets) {
+                Color = Color.FromArgb(255 - (int)Math.Floor(255 * (prevmissedecdf / totalTargets)), colors[colorCount])
+              };
+              if (showMarkers) {
+                dp.MarkerStyle = MarkerStyle.Circle;
+                dp.MarkerBorderWidth = 1;
+                dp.MarkerSize = 5;
+              }
+              row.Points.Add(dp);
+              prevmissedecdf = missedecdf;
+            }
+            if (boundShadingCheckBox.Checked) {
+              if (rowShade.Points.Count > 0 && rowShade.Points.Last().XValue == iter.Current.Key)
+                rowShade.Points.Last().SetValueY(ecdf / totalTargets, (ecdf + missedecdf) / totalTargets);
+              else rowShade.Points.Add(new DataPoint(iter.Current.Key, new[] {ecdf / totalTargets, (ecdf + missedecdf) / totalTargets}));
+            }
+            moreMisses = iter.MoveNext();
+            if (!labelPrinted) {
+              var point = row.Points.Last();
+              if (showLabelsInTargetChart)
+                point.Label = row.Name;
+              point.MarkerStyle = MarkerStyle.Cross;
+              point.MarkerBorderWidth = 1;
+              point.MarkerSize = 10;
+              labelPrinted = true;
+            }
+          }
+          ecdf += h.Value;
+          if (row.Points.Count > 0 && row.Points.Last().XValue == h.Key) {
+            row.Points.Last().SetValueY(ecdf / movingTargets);
+            row.Points.Last().Color = Color.FromArgb(255 - (int)Math.Floor(255 * (missedecdf / totalTargets)), colors[colorCount]);
+          } else {
+            var dp = new DataPoint(h.Key, ecdf / movingTargets) {
+              Color = Color.FromArgb(255 - (int)Math.Floor(255 * (missedecdf / totalTargets)), colors[colorCount])
+            };
+            if (showMarkers) {
+              dp.MarkerStyle = MarkerStyle.Circle;
+              dp.MarkerBorderWidth = 1;
+              dp.MarkerSize = 5;
+            }
+            row.Points.Add(dp);
+          }
+          if (missedecdf > 0 && boundShadingCheckBox.Checked) {
+            if (rowShade.Points.Count > 0 && rowShade.Points.Last().XValue == h.Key)
+              rowShade.Points.Last().SetValueY(ecdf / totalTargets, (ecdf + missedecdf) / totalTargets);
+            else rowShade.Points.Add(new DataPoint(h.Key, new[] {ecdf / totalTargets, (ecdf + missedecdf) / totalTargets}));
+          }
+        }
+
+        while (moreMisses) {
+          // if there are misses beyond the last hit we extend the shaded area
+          missedecdf += iter.Current.Value;
+          var dp = new DataPoint(iter.Current.Key, ecdf / movingTargets) {
+            Color = Color.FromArgb(255 - (int)Math.Floor(255 * (missedecdf / totalTargets)), colors[colorCount])
+          };
+          if (showMarkers) {
+            dp.MarkerStyle = MarkerStyle.Circle;
+            dp.MarkerBorderWidth = 1;
+            dp.MarkerSize = 5;
+          }
+          row.Points.Add(dp);
+          if (boundShadingCheckBox.Checked) {
+            rowShade.Points.Add(new DataPoint(iter.Current.Key, new[] {ecdf / totalTargets, (ecdf + missedecdf) / totalTargets}));
+          }
+          moreMisses = iter.MoveNext();
+
+          if (!labelPrinted && row.Points.Count > 0) {
+            var point = row.Points.Last();
+            if (showLabelsInTargetChart)
+              point.Label = row.Name;
+            point.MarkerStyle = MarkerStyle.Cross;
+            point.MarkerBorderWidth = 1;
+            point.MarkerSize = 10;
+            labelPrinted = true;
+          }
+        }
+
+        if (!labelPrinted) {
+          var point = row.Points.Last();
+          if (showLabelsInTargetChart)
+            point.Label = row.Name;
+          point.MarkerStyle = MarkerStyle.Cross;
+          point.MarkerBorderWidth = 1;
+          point.MarkerSize = 10;
+          rowShade.Points.Add(new DataPoint(point.XValue, new[] {ecdf / totalTargets, (ecdf + missedecdf) / totalTargets}));
+          labelPrinted = true;
+        }
+
+        ConfigureSeries(row);
+        targetChart.Series.Add(rowShade);
+        targetChart.Series.Add(row);
+
+        colorCount = (colorCount + 1) % colors.Length;
+        if (colorCount == 0) lineStyleCount = (lineStyleCount + 1) % lineStyles.Length;
+      }
+    }
+
+    private void UpdateTargetChartAxisXBounds(double minEff, double maxEff) {
+      var minZeros = (int)Math.Floor(Math.Log10(minEff));
+      var maxZeros = (int)Math.Floor(Math.Log10(maxEff));
+      var axisMin = (decimal)Math.Pow(10, minZeros);
+      var axisMax = (decimal)Math.Pow(10, maxZeros);
+      if (!targetLogScalingCheckBox.Checked) {
+        var minAdd = (decimal)Math.Pow(10, minZeros - 1) * 2;
+        var maxAdd = (decimal)Math.Pow(10, maxZeros - 1) * 2;
+        while (axisMin + minAdd < (decimal)minEff) axisMin += minAdd;
+        while (axisMax <= (decimal)maxEff) axisMax += maxAdd;
+      } else axisMax = (decimal)Math.Pow(10, (int)Math.Ceiling(Math.Log10(maxEff)));
+      targetChart.ChartAreas[0].AxisX.Minimum = (double)axisMin;
+      targetChart.ChartAreas[0].AxisX.Maximum = (double)axisMax;
+    }
+
+    private IEnumerable<double> GetAbsoluteTargets(ProblemInstance pInstance) {
+      if (!targetsAreRelative) return targets;
+      var maximization = pInstance.IsMaximization();
+      var bestKnown = pInstance.BestKnownQuality;
+      if (double.IsNaN(bestKnown)) throw new ArgumentException("Problem instance does not have a defined best - known quality.");
+      IEnumerable<double> tmp = null;
+      if (bestKnown > 0) {
+        tmp = targets.Select(x => (maximization ? (1 - x) : (1 + x)) * bestKnown);
+      } else if (bestKnown < 0) {
+        tmp = targets.Select(x => (!maximization ? (1 - x) : (1 + x)) * bestKnown);
+      } else {
+        // relative to 0 is impossible
+        tmp = targets;
+      }
+      return tmp;
+    }
+
+    private double[] GetAbsoluteTargetsWorstToBest(ProblemInstance pInstance) {
+      if (double.IsNaN(pInstance.BestKnownQuality)) throw new ArgumentException("Problem instance does not have a defined best-known quality.");
+      var absTargets = GetAbsoluteTargets(pInstance);
+      return (pInstance.IsMaximization()
+        ? absTargets.OrderBy(x => x) : absTargets.OrderByDescending(x => x)).ToArray();
+    }
+
+    private void GenerateDefaultTargets() {
+      targets = new[] { 0.1, 0.095, 0.09, 0.085, 0.08, 0.075, 0.07, 0.065, 0.06, 0.055, 0.05, 0.045, 0.04, 0.035, 0.03, 0.025, 0.02, 0.015, 0.01, 0.005, 0 };
+      SynchronizeTargetTextBox();
+    }
+
+    private Tuple<bool, double> GetEffortToHitTarget(
+        ObservableList<Tuple<double, double>> convergenceGraph,
+        double absTarget, bool maximization) {
+      if (convergenceGraph.Count == 0)
+        throw new ArgumentException("Convergence graph is empty.", "convergenceGraph");
+      
+      var index = convergenceGraph.BinarySearch(Tuple.Create(0.0, absTarget), new TargetComparer(maximization));
+      if (index >= 0) {
+        return Tuple.Create(true, convergenceGraph[index].Item1);
+      } else {
+        index = ~index;
+        if (index >= convergenceGraph.Count)
+          return Tuple.Create(false, convergenceGraph.Last().Item1);
+        return Tuple.Create(true, convergenceGraph[index].Item1);
+      }
+    }
+
+    private void UpdateErtTables(List<AlgorithmInstance> algorithmInstances) {
+      ertTableView.Content = null;
+      var columns = targets.Length + 1;
+      var totalRows = algorithmInstances.Count * algorithmInstances.Max(x => x.GetNumberOfProblemInstances()) + algorithmInstances.Max(x => x.GetNumberOfProblemInstances());
+      var matrix = new StringMatrix(totalRows, columns);
+      var rowNames = new List<string>();
+      matrix.ColumnNames = targets.Select(x => targetsAreRelative ? (100 * x).ToString() + "%" : x.ToString())
+        .Concat(new[] { "#succ" }).ToList();
+      var rowCount = 0;
+
+      var tableName = (string)dataTableComboBox.SelectedItem;
+      if (string.IsNullOrEmpty(tableName)) return;
+      
+      var problems = algorithmInstances.SelectMany(x => x.GetProblemInstances()).Distinct().ToList();
+
+      foreach (var problem in problems) {
+        var max = problem.IsMaximization();
+        rowNames.Add(problem.ToString());
+        var absTargets = GetAbsoluteTargetsWorstToBest(problem);
+        if (targetsAreRelative) {
+          // print out the absolute target values
+          for (var i = 0; i < absTargets.Length; i++) {
+            matrix[rowCount, i] = absTargets[i].ToString("##,0.0", CultureInfo.CurrentCulture.NumberFormat);
+          }
+        }
+        rowCount++;
+
+        foreach (var alg in algorithmInstances) {
+          rowNames.Add(alg.Name);
+          var runs = alg.GetRuns(problem).ToList();
+          if (runs.Count == 0) {
+            matrix[rowCount, columns - 1] = "N/A";
+            rowCount++;
+            continue;
+          }
+          var result = default(ErtCalculationResult);
+          for (var i = 0; i < absTargets.Length; i++) {
+            result = ExpectedRuntimeHelper.CalculateErt(runs, tableName, absTargets[i], max);
+            matrix[rowCount, i] = result.ToString();
+          }
+          matrix[rowCount, columns - 1] = targets.Length > 0 ? result.SuccessfulRuns + "/" + result.TotalRuns : "-";
+          rowCount++;
+        }
+      }
+      matrix.RowNames = rowNames;
+      ertTableView.Content = matrix;
+      ertTableView.DataGridView.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells);
+    }
+    #endregion
+
+    #region Performance analysis by (multiple) budget(s)
+    private void UpdateResultsByCost() {
+      // necessary to reset log scale -> empty chart cannot use log scaling
+      byCostDataTable.VisualProperties.XAxisLogScale = false;
+      byCostDataTable.Rows.Clear();
+
+      var table = (string)dataTableComboBox.SelectedItem;
+      if (string.IsNullOrEmpty(table)) return;
+
+      if (budgets == null) GenerateDefaultBudgets(table);
+
+      var algInstances = GroupRuns().ToList();
+      if (algInstances.Count == 0) return;
+
+      var colorCount = 0;
+      var lineStyleCount = 0;
+      
+      foreach (var alg in algInstances) {
+        var hits = new Dictionary<string, SortedList<double, double>>();
+
+        foreach (var problem in alg.GetProblemInstances()) {
+          foreach (var run in alg.GetRuns(problem)) {
+            var resultsTable = (IndexedDataTable<double>)run.Results[table];
+
+            CalculateHitsForEachBudget(hits, resultsTable.Rows.First(), problem, alg.Name);
+          }
+        }
+
+        foreach (var list in hits) {
+          var row = new IndexedDataRow<double>(list.Key) {
+            VisualProperties = {
+              ChartType = DataRowVisualProperties.DataRowChartType.StepLine,
+              LineWidth = 2,
+              Color = colors[colorCount],
+              LineStyle = hlLineStyles[lineStyleCount],
+              StartIndexZero = false
+            }
+          };
+
+          var total = 0.0;
+          var count = list.Value.Count;
+          foreach (var h in list.Value) {
+            total += h.Value;
+            row.Values.Add(Tuple.Create(h.Key, total / (double)count));
+          }
+
+          byCostDataTable.Rows.Add(row);
+        }
+        colorCount = (colorCount + 1) % colors.Length;
+        if (colorCount == 0) lineStyleCount = (lineStyleCount + 1) % lineStyles.Length;
+      }
+
+      byCostDataTable.VisualProperties.XAxisTitle = "Targets to Best-Known Ratio";
+      byCostDataTable.VisualProperties.XAxisLogScale = byCostDataTable.Rows.Count > 0 && budgetLogScalingCheckBox.Checked;
+    }
+
+    private void GenerateDefaultBudgets(string table) {
+      var runs = Content;
+      var min = runs.Select(x => ((IndexedDataTable<double>)x.Results[table]).Rows.First().Values.Select(y => y.Item1).Min()).Min();
+      var max = runs.Select(x => ((IndexedDataTable<double>)x.Results[table]).Rows.First().Values.Select(y => y.Item1).Max()).Max();
+      var points = 3;
+      budgets = Enumerable.Range(1, points).Select(x => min + (x / (double)points) * (max - min)).ToArray();
+      suppressBudgetsEvents = true;
+      budgetsTextBox.Text = string.Join(" ; ", budgets);
+      suppressBudgetsEvents = false;
+    }
+
+    private void CalculateHitsForEachBudget(Dictionary<string, SortedList<double, double>> hits, IndexedDataRow<double> row, ProblemInstance problem, string groupName) {
+      var max = problem.IsMaximization();
+      var prevIndex = 0;
+      foreach (var b in budgets) {
+        var key = groupName + "-" + b;
+        var index = row.Values.BinarySearch(prevIndex, row.Values.Count - prevIndex, Tuple.Create(b, 0.0), new CostComparer());
+        if (index < 0) {
+          index = ~index;
+          if (index >= row.Values.Count) break; // the run wasn't long enough to use up budget b (or any subsequent larger one)
+        }
+        if (!hits.ContainsKey(key)) hits.Add(key, new SortedList<double, double>());
+        var v = row.Values[index];
+        var relTgt = CalculateRelativeDifference(max, problem.BestKnownQuality, v.Item2) + 1;
+        if (hits[key].ContainsKey(relTgt))
+          hits[key][relTgt]++;
+        else hits[key][relTgt] = 1.0;
+        prevIndex = index;
+      }
+    }
+    #endregion
+
+    private void UpdateCaption() {
+      Caption = Content != null ? Content.OptimizerName + " RLD View" : ViewAttribute.GetViewName(GetType());
+    }
+
+    private void SynchronizeTargetTextBox() {
+      if (InvokeRequired) Invoke((Action)SynchronizeTargetTextBox);
+      else {
+        suppressTargetsEvents = true;
+        try {
+          if (targetsAreRelative)
+            targetsTextBox.Text = string.Join("% ; ", targets.Select(x => x * 100)) + "%";
+          else targetsTextBox.Text = string.Join(" ; ", targets);
+        } finally { suppressTargetsEvents = false; }
+      }
+    }
+
+    private void groupComboBox_SelectedIndexChanged(object sender, EventArgs e) {
+      UpdateRuns();
+      SetEnabledStateOfControls();
+    }
+    private void problemComboBox_SelectedIndexChanged(object sender, EventArgs e) {
+      UpdateRuns();
+      SetEnabledStateOfControls();
+    }
+    private void dataTableComboBox_SelectedIndexChanged(object sender, EventArgs e) {
+      if (dataTableComboBox.SelectedIndex >= 0)
+        GenerateDefaultBudgets((string)dataTableComboBox.SelectedItem);
+      UpdateBestKnownQualities();
+      UpdateRuns();
+      SetEnabledStateOfControls();
+    }
+
+    private void logScalingCheckBox_CheckedChanged(object sender, EventArgs e) {
+      UpdateResultsByTarget();
+      byCostDataTable.VisualProperties.XAxisLogScale = byCostDataTable.Rows.Count > 0 && budgetLogScalingCheckBox.Checked;
+    }
+
+    private void boundShadingCheckBox_CheckedChanged(object sender, EventArgs e) {
+      UpdateResultsByTarget();
+    }
+
+    #region Event handlers for target analysis
+    private bool suppressTargetsEvents;
+    private void targetsTextBox_Validating(object sender, CancelEventArgs e) {
+      if (suppressTargetsEvents) return;
+      var targetStrings = targetsTextBox.Text.Split(new[] { '%', ';', '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries);
+      var targetList = new List<decimal>();
+      foreach (var ts in targetStrings) {
+        decimal t;
+        if (!decimal.TryParse(ts, out t)) {
+          errorProvider.SetError(targetsTextBox, "Not all targets can be parsed: " + ts);
+          e.Cancel = true;
+          return;
+        }
+        if (targetsAreRelative)
+          targetList.Add(t / 100);
+        else targetList.Add(t);
+      }
+      if (targetList.Count == 0) {
+        errorProvider.SetError(targetsTextBox, "Give at least one target value!");
+        e.Cancel = true;
+        return;
+      }
+      e.Cancel = false;
+      errorProvider.SetError(targetsTextBox, null);
+      targets = targetsAreRelative ? targetList.Select(x => (double)x).OrderByDescending(x => x).ToArray() : targetList.Select(x => (double)x).ToArray();
+
+      SynchronizeTargetTextBox();
+      UpdateResultsByTarget();
+      SetEnabledStateOfControls();
+    }
+
+    private void aggregateTargetsCheckBox_CheckedChanged(object sender, EventArgs e) {
+      SuspendRepaint();
+      try {
+        UpdateResultsByTarget();
+      } finally { ResumeRepaint(true); }
+    }
+
+    private void relativeOrAbsoluteComboBox_SelectedIndexChanged(object sender, EventArgs e) {
+      if (suppressUpdates) return;
+      var pd = (ProblemInstance)problemComboBox.SelectedItem;
+      if (!double.IsNaN(pd.BestKnownQuality)) {
+        var max = pd.IsMaximization();
+        if (targetsAreRelative) targets = GetAbsoluteTargets(pd).ToArray();
+        else {
+          // Rounding to 5 digits since it's certainly appropriate for this application
+          if (pd.BestKnownQuality > 0) {
+            targets = targets.Select(x => Math.Round(max ? 1.0 - (x / pd.BestKnownQuality) : (x / pd.BestKnownQuality) - 1.0, 5)).ToArray();
+          } else if (pd.BestKnownQuality < 0) {
+            targets = targets.Select(x => Math.Round(!max ? 1.0 - (x / pd.BestKnownQuality) : (x / pd.BestKnownQuality) - 1.0, 5)).ToArray();
+          }
+        }
+      }
+      targetsAreRelative = (string)relativeOrAbsoluteComboBox.SelectedItem == "relative";
+      SynchronizeTargetTextBox();
+
+      try {
+        SuspendRepaint();
+        UpdateResultsByTarget();
+      } finally { ResumeRepaint(true); }
+    }
+
+    private void generateTargetsButton_Click(object sender, EventArgs e) {
+      if (targets == null) return;
+      decimal max = 10, min = 0, count = 10;
+      max = (decimal)targets.Max();
+      min = (decimal)targets.Min();
+      count = targets.Length - 1;
+      if (targetsAreRelative) {
+        max *= 100;
+        min *= 100;
+      }
+      using (var dialog = new DefineArithmeticProgressionDialog(false, min, max, (max - min) / count)) {
+        if (dialog.ShowDialog() == DialogResult.OK) {
+          if (dialog.Values.Any()) {
+            targets = targetsAreRelative
+              ? dialog.Values.OrderByDescending(x => x).Select(x => (double)x / 100.0).ToArray()
+              : dialog.Values.Select(x => (double)x).ToArray();
+
+            SynchronizeTargetTextBox();
+            UpdateResultsByTarget();
+            SetEnabledStateOfControls();
+          }
+        }
+      }
+    }
+
+    private void addTargetsAsResultButton_Click(object sender, EventArgs e) {
+      var table = (string)dataTableComboBox.SelectedItem;
+      if (string.IsNullOrEmpty(table)) return;
+      
+      foreach (var run in Content) {
+        if (!run.Results.ContainsKey(table)) continue;
+        var resultsTable = (IndexedDataTable<double>)run.Results[table];
+        var values = resultsTable.Rows.First().Values;
+        var pd = new ProblemInstance(run);
+        pd = problems.Single(x => x.Equals(pd));
+        var max = pd.IsMaximization();
+        var absTargets = GetAbsoluteTargetsWorstToBest(pd);
+
+        var prevIndex = 0;
+        for (var i = 0; i < absTargets.Length; i++) {
+          var absTarget = absTargets[i];
+          var index = values.BinarySearch(prevIndex, values.Count - prevIndex, Tuple.Create(0.0, absTarget), new TargetComparer(max));
+          if (index < 0) {
+            index = ~index;
+            if (index >= values.Count) break; // the target (and subsequent ones) wasn't achieved
+          }
+          var target = targetsAreRelative ? (targets[i] * 100) : absTarget;
+          run.Results[table + (targetsAreRelative ? ".RelTarget " : ".AbsTarget ") + target + (targetsAreRelative ? "%" : string.Empty)] = new DoubleValue(values[index].Item1);
+          prevIndex = index;
+        }
+      }
+    }
+
+    private void markerCheckBox_CheckedChanged(object sender, EventArgs e) {
+      SuspendRepaint();
+      try {
+        UpdateResultsByTarget();
+      } finally { ResumeRepaint(true); }
+    }
+
+    private void showLabelsCheckBox_CheckedChanged(object sender, EventArgs e) {
+      showLabelsInTargetChart = showLabelsCheckBox.Checked;
+      SuspendRepaint();
+      try {
+        UpdateResultsByTarget();
+      } finally { ResumeRepaint(true); }
+    }
+    #endregion
+
+    #region Event handlers for cost analysis
+    private bool suppressBudgetsEvents;
+    private void budgetsTextBox_Validating(object sender, CancelEventArgs e) {
+      if (suppressBudgetsEvents) return;
+      var budgetStrings = budgetsTextBox.Text.Split(new[] { ';', '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries);
+      var budgetList = new List<double>();
+      foreach (var ts in budgetStrings) {
+        double b;
+        if (!double.TryParse(ts, out b)) {
+          errorProvider.SetError(budgetsTextBox, "Not all budgets can be parsed: " + ts);
+          e.Cancel = true;
+          return;
+        }
+        budgetList.Add(b);
+      }
+      if (budgetList.Count == 0) {
+        errorProvider.SetError(budgetsTextBox, "Give at least one budget value!");
+        e.Cancel = true;
+        return;
+      }
+      e.Cancel = false;
+      errorProvider.SetError(budgetsTextBox, null);
+      budgets = budgetList.OrderBy(x => x).ToArray();
+      try {
+        suppressBudgetsEvents = true;
+        budgetsTextBox.Text = string.Join(" ; ", budgets);
+      } finally { suppressBudgetsEvents = false; }
+      UpdateResultsByCost();
+      SetEnabledStateOfControls();
+    }
+
+    private void generateBudgetsButton_Click(object sender, EventArgs e) {
+      decimal max = 1, min = 0, count = 10;
+      if (budgets != null) {
+        max = (decimal)budgets.Max();
+        min = (decimal)budgets.Min();
+        count = budgets.Length;
+      } else if (Content.Count > 0 && dataTableComboBox.SelectedIndex >= 0) {
+        var table = (string)dataTableComboBox.SelectedItem;
+        min = (decimal)Content.Where(x => x.Results.ContainsKey(table)).Select(x => ((IndexedDataTable<double>)x.Results[table]).Rows.First().Values.Min(y => y.Item1)).Min();
+        max = (decimal)Content.Where(x => x.Results.ContainsKey(table)).Select(x => ((IndexedDataTable<double>)x.Results[table]).Rows.First().Values.Max(y => y.Item1)).Max();
+        count = 3;
+      }
+      using (var dialog = new DefineArithmeticProgressionDialog(false, min, max, (max - min) / count)) {
+        if (dialog.ShowDialog() == DialogResult.OK) {
+          if (dialog.Values.Any()) {
+            budgets = dialog.Values.OrderBy(x => x).Select(x => (double)x).ToArray();
+
+            try {
+              suppressBudgetsEvents = true;
+              budgetsTextBox.Text = string.Join(" ; ", budgets);
+            } finally { suppressBudgetsEvents = false; }
+
+            UpdateResultsByCost();
+            SetEnabledStateOfControls();
+          }
+        }
+      }
+    }
+
+    private void addBudgetsAsResultButton_Click(object sender, EventArgs e) {
+      var table = (string)dataTableComboBox.SelectedItem;
+
+      foreach (var run in Content) {
+        if (!run.Results.ContainsKey(table)) continue;
+        var resultsTable = (IndexedDataTable<double>)run.Results[table];
+        var values = resultsTable.Rows.First().Values;
+        var pd = new ProblemInstance(run);
+        pd = problems.Single(x => x.Equals(pd));
+
+        var prevIndex = 0;
+        foreach (var b in budgets) {
+          var index = values.BinarySearch(prevIndex, values.Count - prevIndex, Tuple.Create(b, 0.0), new CostComparer());
+          if (index < 0) {
+            index = ~index;
+            if (index >= values.Count) break; // the run wasn't long enough to use up budget b (or any subsequent larger one)
+          }
+          var v = values[index];
+          var tgt = targetsAreRelative ? CalculateRelativeDifference(pd.IsMaximization(), pd.BestKnownQuality, v.Item2) : v.Item2;
+          run.Results[table + (targetsAreRelative ? ".CostForRelTarget " : ".CostForAbsTarget ") + b] = new DoubleValue(tgt);
+          prevIndex = index;
+        }
+      }
+    }
+    #endregion
+
+    #region Helpers
+    private double CalculateRelativeDifference(bool maximization, double bestKnown, double fit) {
+      if (bestKnown == 0) {
+        // no relative difference with respect to bestKnown possible
+        return maximization ? -fit : fit;
+      }
+      var absDiff = (fit - bestKnown);
+      var relDiff = absDiff / bestKnown;
+      if (maximization) {
+        return bestKnown > 0 ? -relDiff : relDiff;
+      } else {
+        return bestKnown > 0 ? relDiff : -relDiff;
+      }
+    }
+
+    private Dictionary<ProblemInstance, double> CalculateBestTargetPerProblemInstance(string table) {
+      if (table == null) table = string.Empty;
+      return (from r in Content
+              where r.Visible
+              let pd = new ProblemInstance(r)
+              let target = r.Parameters.ContainsKey("BestKnownQuality")
+                           && r.Parameters["BestKnownQuality"] is DoubleValue
+                ? ((DoubleValue)r.Parameters["BestKnownQuality"]).Value
+                : (r.Results.ContainsKey(table) && r.Results[table] is IndexedDataTable<double>
+                  ? ((IndexedDataTable<double>)r.Results[table]).Rows.First().Values.Last().Item2
+                  : double.NaN)
+              group target by pd into g
+              select new { Problem = g.Key, Target = g.Any(x => !double.IsNaN(x)) ? (g.Key.IsMaximization() ? g.Max() : g.Where(x => !double.IsNaN(x)).Min()) : double.NaN })
+        .ToDictionary(x => x.Problem, x => x.Target);
+    }
+
+    private void UpdateRuns() {
+      if (InvokeRequired) {
+        Invoke((Action)UpdateRuns);
+        return;
+      }
+      SuspendRepaint();
+      try {
+        UpdateResultsByTarget();
+        UpdateResultsByCost();
+      } finally { ResumeRepaint(true); }
+    }
+
+    private void UpdateBestKnownQualities() {
+      var table = (string)dataTableComboBox.SelectedItem;
+      if (string.IsNullOrEmpty(table)) return;
+
+      var targetsPerProblem = CalculateBestTargetPerProblemInstance(table);
+      foreach (var pd in problems) {
+        double bkq;
+        if (targetsPerProblem.TryGetValue(pd, out bkq))
+          pd.BestKnownQuality = bkq;
+        else pd.BestKnownQuality = double.NaN;
+      }
+    }
+    #endregion
+
+    private void ConfigureSeries(Series series) {
+      series.SmartLabelStyle.Enabled = showLabelsInTargetChart;
+      series.SmartLabelStyle.AllowOutsidePlotArea = LabelOutsidePlotAreaStyle.No;
+      series.SmartLabelStyle.CalloutLineAnchorCapStyle = LineAnchorCapStyle.None;
+      series.SmartLabelStyle.CalloutLineColor = series.Color;
+      series.SmartLabelStyle.CalloutLineWidth = 2;
+      series.SmartLabelStyle.CalloutStyle = LabelCalloutStyle.Underlined;
+      series.SmartLabelStyle.IsOverlappedHidden = false;
+      series.SmartLabelStyle.MaxMovingDistance = 200;
+      series.ToolTip = series.LegendText + " X = #VALX, Y = #VALY";
+    }
+
+    private void chart_MouseDown(object sender, MouseEventArgs e) {
+      HitTestResult result = targetChart.HitTest(e.X, e.Y);
+      if (result.ChartElementType == ChartElementType.LegendItem) {
+        ToggleTargetChartSeriesVisible(result.Series);
+      }
+    }
+    private void chart_MouseMove(object sender, MouseEventArgs e) {
+      HitTestResult result = targetChart.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 = targetChart.Series[legendItem.SeriesName];
+        if (series != null) {
+          bool seriesIsInvisible = invisibleTargetSeries.Any(x => x.Name == series.Name);
+          foreach (LegendCell cell in legendItem.Cells) {
+            cell.ForeColor = seriesIsInvisible ? Color.Gray : Color.Black;
+          }
+        }
+      }
+    }
+
+    private void ToggleTargetChartSeriesVisible(Series series) {
+      var indexList = invisibleTargetSeries.FindIndex(x => x.Name == series.Name);
+      var indexChart = targetChart.Series.IndexOf(series);
+      if (targetChart.Series.Count == 1) targetChart.ChartAreas[0].AxisX.IsLogarithmic = false;
+      targetChart.Series.RemoveAt(indexChart);
+      var s = indexList >= 0 ? invisibleTargetSeries[indexList] : new Series(series.Name) {
+        Color = series.Color,
+        ChartType = series.ChartType,
+        BorderWidth = series.BorderWidth,
+        BorderDashStyle = series.BorderDashStyle
+      };
+      if (indexList < 0) {
+        // hide
+        invisibleTargetSeries.Add(series);
+        var shadeSeries = targetChart.Series.FirstOrDefault(x => x.Name == series.Name + "-range");
+        if (shadeSeries != null) {
+          if (targetChart.Series.Count == 1) targetChart.ChartAreas[0].AxisX.IsLogarithmic = false;
+          targetChart.Series.Remove(shadeSeries);
+          invisibleTargetSeries.Add(shadeSeries);
+          indexChart--;
+        }
+      } else {
+        // show
+        invisibleTargetSeries.RemoveAt(indexList);
+        var shadeSeries = invisibleTargetSeries.FirstOrDefault(x => x.Name == series.Name + "-range");
+        if (shadeSeries != null) {
+          invisibleTargetSeries.Remove(shadeSeries);
+          InsertOrAddSeries(indexChart, shadeSeries);
+          indexChart++;
+        }
+      }
+      InsertOrAddSeries(indexChart, s);
+      targetChart.ChartAreas[0].AxisX.IsLogarithmic = CanDisplayLogarithmic();
+    }
+
+    private bool CanDisplayLogarithmic() {
+      return targetLogScalingCheckBox.Checked
+        && targetChart.Series.Count > 0 // must have a series
+        && targetChart.Series.Any(x => x.Points.Count > 0) // at least one series must have points
+        && targetChart.Series.All(s => s.Points.All(p => p.XValue > 0)); // all points must be positive
+    }
+
+    private void InsertOrAddSeries(int index, Series s) {
+      if (targetChart.Series.Count <= index)
+        targetChart.Series.Add(s);
+      else targetChart.Series.Insert(index, s);
+    }
+
+    private class AlgorithmInstance : INotifyPropertyChanged {
+      private string name;
+      public string Name {
+        get { return name; }
+        set {
+          if (name == value) return;
+          name = value;
+          OnPropertyChanged("Name");
+        }
+      }
+
+      private Dictionary<ProblemInstance, List<IRun>> performanceData;
+
+      public int GetNumberOfProblemInstances() {
+        return performanceData.Count;
+      }
+
+      public IEnumerable<ProblemInstance> GetProblemInstances() {
+        return performanceData.Keys;
+      }
+
+      public int GetNumberOfRuns(ProblemInstance p) {
+        if (p == ProblemInstance.MatchAll) return performanceData.Select(x => x.Value.Count).Sum();
+        List<IRun> runs;
+        if (performanceData.TryGetValue(p, out runs))
+          return runs.Count;
+
+        return 0;
+      }
+
+      public IEnumerable<IRun> GetRuns(ProblemInstance p) {
+        if (p == ProblemInstance.MatchAll) return performanceData.SelectMany(x => x.Value);
+        List<IRun> runs;
+        if (performanceData.TryGetValue(p, out runs))
+          return runs;
+
+        return Enumerable.Empty<IRun>();
+      }
+
+      public AlgorithmInstance(string name, IEnumerable<IRun> runs, IEnumerable<ProblemInstance> problems) {
+        this.name = name;
+
+        var pDict = problems.ToDictionary(r => r, _ => new List<IRun>());
+        foreach (var y in runs) {
+          var pd = new ProblemInstance(y);
+          List<IRun> l;
+          if (pDict.TryGetValue(pd, out l)) l.Add(y);
+        }
+        performanceData = pDict.Where(x => x.Value.Count > 0).ToDictionary(x => x.Key, x => x.Value);
+      }
+
+      public override bool Equals(object obj) {
+        var other = obj as AlgorithmInstance;
+        if (other == null) return false;
+        return name == other.name;
+      }
+
+      public override int GetHashCode() {
+        return name.GetHashCode();
+      }
+
+      public event PropertyChangedEventHandler PropertyChanged;
+      protected virtual void OnPropertyChanged(string propertyName = null) {
+        var handler = PropertyChanged;
+        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
+      }
+    }
+
+    private class ProblemInstance : INotifyPropertyChanged {
+      private readonly bool matchAll;
+      public static readonly ProblemInstance MatchAll = new ProblemInstance() {
+        ProblemName = "All with Best-Known"
+      };
+
+      private ProblemInstance() {
+        ProblemType = string.Empty;
+        ProblemName = string.Empty;
+        Evaluator = string.Empty;
+        Maximization = string.Empty;
+        DisplayProblemType = false;
+        DisplayProblemName = false;
+        DisplayEvaluator = false;
+        DisplayMaximization = false;
+        matchAll = true;
+        BestKnownQuality = double.NaN;
+      }
+
+      public ProblemInstance(IRun run) {
+        ProblemType = GetStringValueOrEmpty(run, "Problem Type");
+        ProblemName = GetStringValueOrEmpty(run, "Problem Name");
+        Evaluator = GetStringValueOrEmpty(run, "Evaluator");
+        Maximization = GetMaximizationValueOrEmpty(run, "Maximization");
+        DisplayProblemType = !string.IsNullOrEmpty(ProblemType);
+        DisplayProblemName = !string.IsNullOrEmpty(ProblemName);
+        DisplayEvaluator = !string.IsNullOrEmpty(Evaluator);
+        DisplayMaximization = !string.IsNullOrEmpty(Maximization);
+        matchAll = false;
+        BestKnownQuality = GetDoubleValueOrNaN(run, "BestKnownQuality");
+      }
+
+      private bool displayProblemType;
+      public bool DisplayProblemType {
+        get { return displayProblemType; }
+        set {
+          if (displayProblemType == value) return;
+          displayProblemType = value;
+          OnPropertyChanged("DisplayProblemType");
+        }
+      }
+      private string problemType;
+      public string ProblemType {
+        get { return problemType; }
+        set {
+          if (problemType == value) return;
+          problemType = value;
+          OnPropertyChanged("ProblemType");
+        }
+      }
+      private bool displayProblemName;
+      public bool DisplayProblemName {
+        get { return displayProblemName; }
+        set {
+          if (displayProblemName == value) return;
+          displayProblemName = value;
+          OnPropertyChanged("DisplayProblemName");
+        }
+      }
+      private string problemName;
+      public string ProblemName {
+        get { return problemName; }
+        set {
+          if (problemName == value) return;
+          problemName = value;
+          OnPropertyChanged("ProblemName");
+        }
+      }
+      private bool displayEvaluator;
+      public bool DisplayEvaluator {
+        get { return displayEvaluator; }
+        set {
+          if (displayEvaluator == value) return;
+          displayEvaluator = value;
+          OnPropertyChanged("DisplayEvaluator");
+        }
+      }
+      private string evaluator;
+      public string Evaluator {
+        get { return evaluator; }
+        set {
+          if (evaluator == value) return;
+          evaluator = value;
+          OnPropertyChanged("Evaluator");
+        }
+      }
+      private bool displayMaximization;
+      public bool DisplayMaximization {
+        get { return displayMaximization; }
+        set {
+          if (displayMaximization == value) return;
+          displayMaximization = value;
+          OnPropertyChanged("DisplayMaximization");
+        }
+      }
+      private string maximization;
+      public string Maximization {
+        get { return maximization; }
+        set {
+          if (maximization == value) return;
+          maximization = value;
+          OnPropertyChanged("Maximization");
+        }
+      }
+      private double bestKnownQuality;
+      public double BestKnownQuality {
+        get { return bestKnownQuality; }
+        set {
+          if (bestKnownQuality == value) return;
+          bestKnownQuality = value;
+          OnPropertyChanged("BestKnownQuality");
+        }
+      }
+
+      public bool IsMaximization() {
+        return Maximization == "MAX";
+      }
+
+      public bool Match(IRun run) {
+        return matchAll ||
+               GetStringValueOrEmpty(run, "Problem Type") == ProblemType
+               && GetStringValueOrEmpty(run, "Problem Name") == ProblemName
+               && GetStringValueOrEmpty(run, "Evaluator") == Evaluator
+               && GetMaximizationValueOrEmpty(run, "Maximization") == Maximization;
+      }
+
+      private double GetDoubleValueOrNaN(IRun run, string key) {
+        IItem param;
+        if (run.Parameters.TryGetValue(key, out param)) {
+          var dv = param as DoubleValue;
+          return dv != null ? dv.Value : double.NaN;
+        }
+        return double.NaN;
+      }
+
+      private string GetStringValueOrEmpty(IRun run, string key) {
+        IItem param;
+        if (run.Parameters.TryGetValue(key, out param)) {
+          var sv = param as StringValue;
+          return sv != null ? sv.Value : string.Empty;
+        }
+        return string.Empty;
+      }
+
+      private string GetMaximizationValueOrEmpty(IRun run, string key) {
+        IItem param;
+        if (run.Parameters.TryGetValue(key, out param)) {
+          var bv = param as BoolValue;
+          return bv != null ? (bv.Value ? "MAX" : "MIN") : string.Empty;
+        }
+        return string.Empty;
+      }
+
+      public override bool Equals(object obj) {
+        var other = obj as ProblemInstance;
+        if (other == null) return false;
+        return ProblemType == other.ProblemType
+               && ProblemName == other.ProblemName
+               && Evaluator == other.Evaluator
+               && Maximization == other.Maximization;
+      }
+
+      public override int GetHashCode() {
+        return ProblemType.GetHashCode() ^ ProblemName.GetHashCode() ^ Evaluator.GetHashCode() ^ Maximization.GetHashCode();
+      }
+
+      public override string ToString() {
+        return string.Join("  --  ", new[] {
+          (DisplayProblemType ? ProblemType : string.Empty),
+          (DisplayProblemName ? ProblemName : string.Empty),
+          (DisplayEvaluator ? Evaluator : string.Empty),
+          (DisplayMaximization ? Maximization : string.Empty),
+          !double.IsNaN(BestKnownQuality) ? BestKnownQuality.ToString(CultureInfo.CurrentCulture.NumberFormat) : string.Empty }.Where(x => !string.IsNullOrEmpty(x)));
+      }
+
+      public event PropertyChangedEventHandler PropertyChanged;
+      protected virtual void OnPropertyChanged(string propertyName = null) {
+        var handler = PropertyChanged;
+        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
+      }
+    }
+
+    private class CostComparer : Comparer<Tuple<double, double>> {
+      public override int Compare(Tuple<double, double> x, Tuple<double, double> y) {
+        return x.Item1.CompareTo(y.Item1);
+      }
+    }
+
+    private class TargetComparer : Comparer<Tuple<double, double>> {
+      public bool Maximization { get; private set; }
+      public TargetComparer(bool maximization) {
+        Maximization = maximization;
+      }
+
+      public override int Compare(Tuple<double, double> x, Tuple<double, double> y) {
+        return Maximization ? x.Item2.CompareTo(y.Item2) : y.Item2.CompareTo(x.Item2);
+      }
+    }
+  }
+}
Index: /stable/HeuristicLab.Visualization.ChartControlsExtensions/3.3/GanttChart.Designer.cs
===================================================================
--- /stable/HeuristicLab.Visualization.ChartControlsExtensions/3.3/GanttChart.Designer.cs	(revision 15219)
+++ /stable/HeuristicLab.Visualization.ChartControlsExtensions/3.3/GanttChart.Designer.cs	(revision 15220)
@@ -45,7 +45,9 @@
     /// </summary>
     private void InitializeComponent() {
+      this.components = new System.ComponentModel.Container();
       System.Windows.Forms.DataVisualization.Charting.ChartArea chartArea1 = new System.Windows.Forms.DataVisualization.Charting.ChartArea();
       System.Windows.Forms.DataVisualization.Charting.Legend legend1 = new System.Windows.Forms.DataVisualization.Charting.Legend();
       System.Windows.Forms.DataVisualization.Charting.Series series1 = new System.Windows.Forms.DataVisualization.Charting.Series();
+      System.Windows.Forms.DataVisualization.Charting.Title title1 = new System.Windows.Forms.DataVisualization.Charting.Title();
       this.chart = new HeuristicLab.Visualization.ChartControlsExtensions.EnhancedChart();
       ((System.ComponentModel.ISupportInitialize)(this.chart)).BeginInit();
@@ -54,7 +56,7 @@
       // chart
       // 
-      this.chart.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
-                  | System.Windows.Forms.AnchorStyles.Left)
-                  | System.Windows.Forms.AnchorStyles.Right)));
+      this.chart.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
+            | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
       chartArea1.AxisX.ScaleView.SmallScrollMinSizeType = System.Windows.Forms.DataVisualization.Charting.DateTimeIntervalType.Milliseconds;
       chartArea1.AxisY.ScaleView.SmallScrollMinSizeType = System.Windows.Forms.DataVisualization.Charting.DateTimeIntervalType.Milliseconds;
@@ -79,8 +81,12 @@
       this.chart.TabIndex = 0;
       this.chart.Text = "chart";
+      title1.Font = new System.Drawing.Font("Microsoft Sans Serif", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
+      title1.Name = "Gantt Title";
+      title1.Text = "Gantt Title";
+      title1.Visible = false;
+      this.chart.Titles.Add(title1);
       // 
       // GanttChart
       // 
-      this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
       this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Inherit;
       this.Controls.Add(this.chart);
Index: /stable/HeuristicLab.Visualization.ChartControlsExtensions/3.3/GanttChart.cs
===================================================================
--- /stable/HeuristicLab.Visualization.ChartControlsExtensions/3.3/GanttChart.cs	(revision 15219)
+++ /stable/HeuristicLab.Visualization.ChartControlsExtensions/3.3/GanttChart.cs	(revision 15220)
@@ -32,4 +32,12 @@
     private IDictionary<string, Color> categories = new Dictionary<string, Color>();
     private IDictionary<string, int> rowNames = new Dictionary<string, int>();
+
+    public string Title {
+      get { return chart.Titles[0].Text; }
+      set {
+        chart.Titles[0].Text = value ?? string.Empty;
+        chart.Titles[0].Visible = value != null;
+      }
+    }
 
     public GanttChart() {
