/// <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);
        }