/// <summary> /// Gets a URL for a coloured square to be used in the heatmap for a /// given PredictedObserved test. /// </summary> /// <param name="item">The item for which we want a colour.</param> private static string GetImageUrl(vPredictedObservedTests item) { Color colour = GetColour(item); string url = $"/APSIM.PerformanceTests/WebForm1.aspx?a={colour.A}&r={colour.R}&g={colour.G}&b={colour.B}"; #if DEBUG url = $"/WebForm1.aspx?a={colour.A}&r={colour.R}&g={colour.G}&b={colour.B}"; #endif return(url); }
/// <summary> /// Generates a colour for an item, for use in the heatmap. /// </summary> /// <param name="item">The item for which we need a colour.</param> private static Color GetColour(vPredictedObservedTests item) { double intensity = Math.Abs(((double)item.Current - (double)item.Accepted) / (double)item.Accepted); intensity = Math.Min(intensity, 1); // Upper bound = 1. if (item.IsImprovement != null && (bool)item.IsImprovement) { return(GetGreen(intensity)); } else if (item.PassedTest != null && (bool)item.PassedTest) { return(GetGreyscaleColour(GetColourIntensity(item))); } else { return(GetRed(1 - intensity)); // Darker red = worse, so invert the intensity. } }
/// <summary> /// Gets an estimation of the 'goodness' of an item's result, for use /// in the heatmap. /// </summary> /// <param name="item">The item for which we need an intensity.</param> /// <returns>A double in the range [0, 1].</returns> private static double GetColourIntensity(vPredictedObservedTests item) { switch (item.Test.ToUpper()) { case "N": return(item.Current == item.Accepted ? 1 : 0); case "NSE": return(NormaliseNse((double)item.Current)); case "R2": return((double)item.Current); case "RSR": return(NormaliseNse(1 - (double)item.Current)); default: throw new Exception($"unknown statistic: {item.Test}"); } }
/// <summary> /// Generates a table of heatmaps of the data. /// Each row represents stats for one model (wheat, barley, sorghum, etc.). /// Each column represents one particular statistic (r2, nse, etc.). /// </summary> /// <param name="poTestsList"></param> private void GenerateHeatmap(List <vPredictedObservedTests> poTestsList) { Table heatmapTable = new Table(); TableRow row; TableCell cell; ImageButton dataPoint; // This represents one data point (pixel) in a heatmap. HtmlGenericControl div; // This wraps the heatmap inside each cell. // First, create a row of column headers, containing the test names. // For now, we will not graph RMSE, as it is in the units of the variable it describes. string[] testNames = poTestsList.Select(po => po.Test).Distinct().Where(t => !t.Equals("RMSE", StringComparison.InvariantCultureIgnoreCase)).ToArray(); row = new TableRow(); // Left-most column contains model names. cell = new TableHeaderCell(); cell.Text = "Model Name"; row.Cells.Add(cell); // The remaining cells in the top row contain the test names (nse, r2, etc). foreach (string testName in testNames) { cell = new TableHeaderCell(); cell.Text = testName; row.Cells.Add(cell); } heatmapTable.Rows.Add(row); // Next, iterate over each model in the data. These will be our rows. foreach (var model in poTestsList.GroupBy(v => v.FileName)) { row = new TableRow(); // The first cell in each row will contain the model name. cell = new TableHeaderCell(); cell.Text = model.Key; row.Cells.Add(cell); foreach (var test in model.GroupBy(v => v.Test)) { // We don't want to generate a heatmap for every test. if (!testNames.Contains(test.Key)) { continue; } // Each cell contains a heatmap of data. We display this in // a square rather than a line, to conserve space. cell = new TableCell(); // Each heatmap goes inside a div, which goes inside a cell. // This means we can put a border around the heatmap, not the // cell, which can be bigger than the heatmap. Without the border, // it can be very difficult to see where the heatmaps start/end. div = new HtmlGenericControl("div"); div.Style.Add("border", "1px solid black"); div.Style.Add("display", "inline-block"); div.Style.Add("overflow", "hidden"); // Area of the heatmap will be the smallest square number which // is larger than the number of data points in the heatmap. // The length of each row will be the square root of this number. int rowLength = (int)Math.Floor(Math.Sqrt(test.Count())) + 1; // Our data contains many nullable doubles (ugh) so let's filter // them out before we start iteration, otherwise it will mess up // our indexing. List <vPredictedObservedTests> testWithoutNulls = test.Where(v => v.Current != null && v.Accepted != null).ToList(); for (int i = 0; i < testWithoutNulls.Count; i++) { vPredictedObservedTests item = testWithoutNulls[i]; dataPoint = new ImageButton(); dataPoint.ImageUrl = GetImageUrl(item); // Embed PO ID in the image. dataPoint.Attributes["POID"] = item.PredictedObservedDetailsID.ToString(); // The last item in each row needs to be "display: block;" // All other items need to be "float: left;" if (((i + 1) % rowLength) == 0) { dataPoint.Style.Add("display", "block"); } else { dataPoint.Style.Add("float", "left"); } dataPoint.ToolTip = item.Variable + " " + item.Test; dataPoint.Click += OnHeatmapPixelClicked; div.Controls.Add(dataPoint); } cell.Controls.Add(div); row.Cells.Add(cell); } heatmapTable.Rows.Add(row); } phHeatmap.Controls.Add(heatmapTable); }