2910 lines
94 KiB
C#
2910 lines
94 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using CSVStats;
|
|
using System.Threading.Tasks;
|
|
using System.Text;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Security.Cryptography;
|
|
using System.Xml.Linq;
|
|
using System.Collections;
|
|
|
|
namespace CSVTools
|
|
{
|
|
|
|
public class CsvToSvgLibVersion
|
|
{
|
|
private static string VersionString = "3.67";
|
|
|
|
public static string Get() { return VersionString; }
|
|
};
|
|
|
|
public enum ShowEventTextMode
|
|
{
|
|
Hide = 0,
|
|
ShowAuto = 1,
|
|
ShowAll = 2
|
|
};
|
|
|
|
public class GraphParams
|
|
{
|
|
public int width = 1800;
|
|
public int height = 550;
|
|
public string title = "";
|
|
public List<string> statNames = new List<string>();
|
|
public List<string> ignoreStats = new List<string>();
|
|
public int colorOffset = 0;
|
|
|
|
// Events
|
|
public List<string> showEventNames = new List<string>();
|
|
public ShowEventTextMode showEventNameTextMode = ShowEventTextMode.ShowAuto;
|
|
public List<string> highlightEventRegions = new List<string>();
|
|
|
|
// Smoothing
|
|
public bool smooth = false;
|
|
public float smoothKernelPercent = 2.0f;
|
|
public int smoothKernelSize = -1;
|
|
|
|
// Other flags
|
|
public bool showMetadata = true; //noMetadata
|
|
public bool graphOnly = false;
|
|
public bool interactive = false;
|
|
public bool snapToPeaks = true; // Interactive mode only
|
|
public int maxHierarchyDepth = -1;
|
|
public char hierarchySeparator = '/';
|
|
public List<string> hideStatPrefixes = new List<string>();
|
|
|
|
// Compression
|
|
public float compression = 0.0f;
|
|
public bool bFixedPointGraphs = true;
|
|
public float fixedPointPrecisionScale = 2.0f; // Scale applied to fixed point graphs. Values greater than one allow for subpixel accuracy
|
|
public int lineDecimalPlaces = 3; // Legacy: only used if bFixedPointGraphs is 0
|
|
|
|
// Stacking
|
|
public bool stacked = false;
|
|
public string stackTotalStat = "";
|
|
public bool stackedUnsorted = false;
|
|
|
|
// Percentiles
|
|
public bool percentileTop90 = false;
|
|
public bool percentileTop99 = false;
|
|
public bool percentile = false;
|
|
|
|
// X/Y range
|
|
public float minX = Range.Auto;
|
|
public float maxX = Range.Auto;
|
|
public float minY = Range.Auto;
|
|
public float maxY = Range.Auto;
|
|
|
|
// start graphing from this event. Note: this will cause frame numbers to start at zero (which is necessary for multiple graphs)
|
|
public string startEvent;
|
|
public int startEventOffset = 0;
|
|
public string endEvent;
|
|
public int endEventOffset = 0;
|
|
|
|
// Max auto Y range. Set to 0 to disable
|
|
public float maxAutoMaxY = 0.0f;
|
|
|
|
public float budget = float.MaxValue;
|
|
public float lineThickness = 1.0f;
|
|
|
|
// Thresholds
|
|
public float threshold = -float.MaxValue;
|
|
public float averageThreshold = -float.MaxValue;
|
|
|
|
public string embedText = "Created with CSVtoSVG "+ CsvToSvgLibVersion.Get();
|
|
public string uniqueId = "ID";
|
|
|
|
// Legend
|
|
public bool showAverages = false;
|
|
public bool showTotals = false;
|
|
public bool forceLegendSort = false;
|
|
public float legendAverageThreshold = -float.MaxValue;
|
|
public List<string> customLegendNames = new List<string>();
|
|
public List<string> csvColors = new List<string>();
|
|
|
|
// Process options (doesn't affect output)
|
|
public bool bSmoothMultithreaded = true;
|
|
public bool bPerfLog = false;
|
|
|
|
// Advanced params
|
|
public string themeName = "";
|
|
public int frameOffset = 0;
|
|
public float statMultiplier = 1.0f;
|
|
public string minFilterStatName = null;
|
|
public float minFilterStatValue = -float.MaxValue;
|
|
public bool filterOutZeros = false;
|
|
public bool discardLastFrame = true;
|
|
|
|
private bool bFinalized = false;
|
|
|
|
public void FinalizeSettings()
|
|
{
|
|
if (bFinalized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (percentile && (stacked || interactive))
|
|
{
|
|
throw new Exception("Warning: percentile graph not compatible with stacked & interactive");
|
|
}
|
|
|
|
// Make sure stack total stat is in the stat list. This may result in a duplicate, but that's fine
|
|
if (stacked)
|
|
{
|
|
if (stackTotalStat != "")
|
|
{
|
|
stackTotalStat = stackTotalStat.ToLower();
|
|
statNames.Add(stackTotalStat);
|
|
}
|
|
}
|
|
|
|
if ( percentileTop90 || percentileTop99 )
|
|
{
|
|
percentile = true;
|
|
}
|
|
|
|
themeName = themeName.ToLower();
|
|
|
|
if (maxX <= 0.0f)
|
|
{
|
|
maxX = Range.Auto;
|
|
}
|
|
if (maxY <= 0.0f)
|
|
{
|
|
maxY = Range.Auto;
|
|
}
|
|
if (maxAutoMaxY <= 0.0f)
|
|
{
|
|
// Clamp to 8m to prevent craziness
|
|
maxAutoMaxY = 8000000;
|
|
}
|
|
if (smoothKernelSize>0.0f)
|
|
{
|
|
smoothKernelPercent = 0.0f;
|
|
}
|
|
bFinalized = true;
|
|
}
|
|
|
|
|
|
public void DebugPrintProperties()
|
|
{
|
|
foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(this))
|
|
{
|
|
string name = descriptor.Name;
|
|
object value = descriptor.GetValue(this);
|
|
Console.WriteLine("{0}={1}", name, value);
|
|
}
|
|
}
|
|
};
|
|
|
|
public class CsvInfo
|
|
{
|
|
public CsvInfo(CsvStats csvStatsIn, string csvFilenameIn)
|
|
{
|
|
stats = csvStatsIn;
|
|
filename = csvFilenameIn;
|
|
}
|
|
public CsvStats stats;
|
|
public string filename;
|
|
};
|
|
|
|
class SvgFile
|
|
{
|
|
public SvgFile(string filename, string inUniqueId=null)
|
|
{
|
|
file = new System.IO.StreamWriter(filename);
|
|
idSuffix = "_" + inUniqueId;
|
|
sb = new StringBuilder();
|
|
}
|
|
|
|
public void WriteFast(string str)
|
|
{
|
|
sb.Append(str);
|
|
}
|
|
|
|
public void Write(string str)
|
|
{
|
|
sb.Append(str.Replace("<UNIQUE>", idSuffix));
|
|
}
|
|
|
|
public void WriteLineFast(string str)
|
|
{
|
|
sb.Append(str+"\n");
|
|
}
|
|
|
|
public void WriteLine(string str)
|
|
{
|
|
sb.Append(str.Replace("<UNIQUE>", idSuffix)+"\n");
|
|
Flush();
|
|
}
|
|
|
|
public void Flush()
|
|
{
|
|
file.Write(sb.ToString());
|
|
sb.Clear();
|
|
}
|
|
|
|
public void Close()
|
|
{
|
|
Flush();
|
|
file.Close();
|
|
file = null;
|
|
}
|
|
|
|
readonly string idSuffix;
|
|
System.IO.StreamWriter file = null;
|
|
StringBuilder sb = null;
|
|
//int bufferLineCount = 0;
|
|
}
|
|
|
|
|
|
|
|
class Rect
|
|
{
|
|
public Rect(float xIn, float yIn, float widthIn, float heightIn)
|
|
{
|
|
x = xIn; y = yIn; width = widthIn; height = heightIn;
|
|
}
|
|
public float x, y;
|
|
public float width, height;
|
|
};
|
|
|
|
class Range
|
|
{
|
|
public const float Auto = -100000.0f;
|
|
|
|
public Range() { }
|
|
public Range(float minx, float maxx, float miny, float maxy) { MinX = minx; MaxX = maxx; MinY = miny; MaxY = maxy; }
|
|
public float MinX, MaxX;
|
|
public float MinY, MaxY;
|
|
};
|
|
|
|
class Theme
|
|
{
|
|
// TODO: make this data driven
|
|
public Theme(string Name)
|
|
{
|
|
GraphColours = new Colour[32];
|
|
uint[] GraphColoursInt = null;
|
|
if (Name == "light")
|
|
{
|
|
BackgroundColour = new Colour(255, 255, 255);
|
|
BackgroundColourCentre = new Colour(255, 255, 255);
|
|
LineColour = new Colour(0, 0, 0);
|
|
|
|
GraphColoursInt = new uint[16]
|
|
{
|
|
0x0000C0, 0x8000C0, 0xFF4000, 0xC00000,
|
|
0x4040A0, 0x008080, 0x200080, 0x408060,
|
|
0x008040, 0x00008C, 0x60A880, 0x325000,
|
|
0xA040A0, 0x808000, 0x005050, 0x606060,
|
|
};
|
|
|
|
TextColour = new Colour(0, 0, 0);
|
|
MediumTextColour = new Colour(64, 64, 64);
|
|
MinorTextColour = new Colour(128, 128, 128);
|
|
AxisLineColour = new Colour(128, 128, 128);
|
|
MajorGridlineColour = new Colour(128, 128, 128);
|
|
MinorGridlineColour = new Colour(160, 160, 160);
|
|
BudgetLineColour = new Colour(0, 196, 0);
|
|
EventTextColour = new Colour(0);
|
|
BudgetLineThickness = 1.0f;
|
|
}
|
|
else if (Name == "pink")
|
|
{
|
|
BackgroundColour = new Colour(255, 128, 128);
|
|
BackgroundColourCentre = new Colour(255, 255, 255);
|
|
LineColour = new Colour(0, 0, 0);
|
|
GraphColoursInt = new uint[16]
|
|
{
|
|
0x8080FF, 0xFF8C8C, 0xFFFF8C, 0x20C0C0,
|
|
0x808000, 0xFF8C8C, 0x20FF8C, 0x408060,
|
|
0xFF8040, 0xFFFF8C, 0x60008C, 0x3250FF,
|
|
0x008000, 0x8C8CFF, 0xFF5050, 0x606060,
|
|
};
|
|
|
|
TextColour = new Colour(0, 0, 0);
|
|
MediumTextColour = new Colour(192, 192, 192);
|
|
MinorTextColour = new Colour(128, 128, 128);
|
|
AxisLineColour = new Colour(128, 128, 128);
|
|
MajorGridlineColour = new Colour(128, 128, 128);
|
|
MinorGridlineColour = new Colour(160, 160, 160);
|
|
BudgetLineColour = new Colour(0, 196, 0);
|
|
EventTextColour = new Colour(0);
|
|
BudgetLineThickness = 1.0f;
|
|
}
|
|
else // "dark"
|
|
{
|
|
BackgroundColour = new Colour(16, 16, 16);
|
|
BackgroundColourCentre = new Colour(80, 80, 80);
|
|
LineColour = new Colour(255, 255, 255);
|
|
|
|
GraphColoursInt = new uint[16]
|
|
{//0x11bbbb
|
|
0x0080FF, 0x66cdFF, 0xFF6600, 0xFFFF8C,
|
|
0x60f060, 0xFFFF00, 0x99CC00, 0xCC6600,
|
|
0xCC3300, 0xCCFF66, 0x60008C, 0x3250FF,
|
|
0x008000, 0x11bbbb, 0xFF5050, 0x606060,
|
|
};
|
|
for (int i = 0; i < 16; i++)
|
|
GraphColours[i] = new Colour(GraphColoursInt[i]);
|
|
|
|
TextColour = new Colour(255, 255, 255);
|
|
MediumTextColour = new Colour(192, 192, 192);
|
|
MinorTextColour = new Colour(128, 128, 128);
|
|
AxisLineColour = new Colour(128, 128, 128);
|
|
MajorGridlineColour = new Colour(128, 128, 128);
|
|
MinorGridlineColour = new Colour(96, 96, 96);
|
|
BudgetLineColour = new Colour(128, 255, 128);
|
|
EventTextColour = new Colour(255, 255, 255, 0.75f);
|
|
BudgetLineThickness = 0.5f;
|
|
}
|
|
|
|
for (int i = 0; i < GraphColours.Length; i++)
|
|
{
|
|
int repeat = i / GraphColoursInt.Length;
|
|
float alpha = 1.0f - (float)repeat * 0.25f;
|
|
GraphColours[i] = new Colour(GraphColoursInt[i % GraphColoursInt.Length], alpha);
|
|
|
|
}
|
|
|
|
}
|
|
public Colour BackgroundColour;
|
|
public Colour BackgroundColourCentre;
|
|
public Colour LineColour;
|
|
public Colour TextColour;
|
|
public Colour MinorTextColour;
|
|
public Colour MediumTextColour;
|
|
public Colour AxisLineColour;
|
|
public Colour MinorGridlineColour;
|
|
public Colour MajorGridlineColour;
|
|
public Colour BudgetLineColour;
|
|
public Colour EventTextColour;
|
|
public float BudgetLineThickness;
|
|
public Colour[] GraphColours;
|
|
};
|
|
|
|
|
|
public class GraphGenerator
|
|
{
|
|
List<CsvInfo> csvList;
|
|
|
|
public GraphGenerator(List<CsvInfo> csvsIn)
|
|
{
|
|
csvList = csvsIn;
|
|
}
|
|
|
|
public GraphGenerator(CsvStats csv, string filename)
|
|
{
|
|
csvList = new List<CsvInfo>();
|
|
csvList.Add(new CsvInfo(csv, filename));
|
|
}
|
|
|
|
public Task MakeGraphAsync(GraphParams graphParams, string svgFilename, bool bWriteErrorsToSvg, bool rethrowExceptions = true)
|
|
{
|
|
Action action = () =>
|
|
{
|
|
MakeGraph(graphParams, svgFilename, bWriteErrorsToSvg, rethrowExceptions);
|
|
};
|
|
return Task.Factory.StartNew(action);
|
|
}
|
|
|
|
public void MakeGraph(GraphParams graphParams, string svgFilename, bool bWriteErrorsToSvg, bool rethrowExceptions = true)
|
|
{
|
|
SvgFile svg = new SvgFile(svgFilename, graphParams.uniqueId);
|
|
try
|
|
{
|
|
graphParams.FinalizeSettings();
|
|
MakeGraphInternal(graphParams, svg);
|
|
}
|
|
catch (System.Exception e)
|
|
{
|
|
// Write the error to the SVG
|
|
string errorString = e.ToString();
|
|
if (bWriteErrorsToSvg)
|
|
{
|
|
errorString = errorString.Replace(" at", " at<br/>\n");
|
|
errorString += "<br/><br/>"+ graphParams.embedText.Replace("\n","<br/>");
|
|
float MessageWidth = graphParams.width - 20;
|
|
float MessageHeight = graphParams.height - 20;
|
|
|
|
svg.WriteLine("<switch>");
|
|
svg.WriteLine("<foreignObject x='10' y='10' color='#ffffff' font-size='12' width='" + MessageWidth + "' height='" + MessageHeight + "'><p xmlns='http://www.w3.org/1999/xhtml'>" + errorString + "</p></foreignObject>'");
|
|
svg.WriteLine("<text x='10' y='10' fill='rgb(255, 255, 255)' font-size='10' font-family='Helvetica' > ERROR: " + errorString + "</text>");
|
|
svg.WriteLine("</switch>");
|
|
svg.WriteLine("</svg>");
|
|
}
|
|
svg.Close();
|
|
if (rethrowExceptions)
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
svg.Close();
|
|
}
|
|
|
|
void MakeGraphInternal(GraphParams graphParams, SvgFile svg)
|
|
{
|
|
PerfLog perfLog = new PerfLog(graphParams.bPerfLog, "Graph:"+graphParams.title);
|
|
|
|
Rect dimensions = new Rect(0, 0, graphParams.width, graphParams.height);
|
|
Rect graphRect = new Rect(50, 42, dimensions.width - 100, dimensions.height - 115);
|
|
Theme theme = new Theme(graphParams.themeName);
|
|
List<string> statNames = new List<string>(graphParams.statNames);
|
|
string stackTotalStat = null;
|
|
bool stackTotalStatIsAutomatic = false;
|
|
|
|
if (graphParams.stacked)
|
|
{
|
|
stackTotalStat = graphParams.stackTotalStat;
|
|
}
|
|
|
|
svg.Write("<svg width='" + dimensions.width + "' height='" + dimensions.height + "' viewPort='0 0 " + dimensions.height + " " + dimensions.width + "' version='1.1' xmlns='http://www.w3.org/2000/svg'");
|
|
if (graphParams.interactive)
|
|
{
|
|
svg.Write(" onLoad='OnLoaded<UNIQUE>(evt)'");
|
|
}
|
|
svg.WriteLine(">");
|
|
|
|
if (graphParams.embedText != null)
|
|
{
|
|
svg.WriteLine("<![CDATA[ \n"+graphParams.embedText);
|
|
svg.WriteLine("]]>");
|
|
}
|
|
|
|
// Write defs
|
|
svg.WriteLine("<defs>");
|
|
svg.WriteLine("<radialGradient id='radialGradient<UNIQUE>'");
|
|
svg.WriteLine("fx='50%' fy='50%' r='65%'");
|
|
svg.WriteLine("spreadMethod='pad'>");
|
|
svg.WriteLine("<stop offset='0%' stop-color=" + theme.BackgroundColourCentre.SVGString() + " stop-opacity='1'/>");
|
|
svg.WriteLine("<stop offset='100%' stop-color=" + theme.BackgroundColour.SVGString() + " stop-opacity='1' />");
|
|
svg.WriteLine("</radialGradient>");
|
|
|
|
svg.WriteLine("<linearGradient id = 'linearGradient<UNIQUE>' x1 = '0%' y1 = '0%' x2 = '100%' y2 = '100%'>");
|
|
svg.WriteLine("<stop stop-color = 'black' offset = '0%'/>");
|
|
svg.WriteLine("<stop stop-color = 'white' offset = '100%'/>");
|
|
svg.WriteLine("</linearGradient>");
|
|
|
|
// Clip rect
|
|
svg.WriteLine("<clipPath id='graphClipRect<UNIQUE>'>");
|
|
// If we're using a fixed point scale for graphs, we need to apply it to the clip rect
|
|
float clipRectScale = (graphParams.bFixedPointGraphs && !graphParams.percentile) ? graphParams.fixedPointPrecisionScale : 1.0f;
|
|
svg.WriteLine("<rect x='" + graphRect.x*clipRectScale + "' y='" + graphRect.y*clipRectScale + "' width='" + graphRect.width*clipRectScale + "' height='" + graphRect.height*clipRectScale + "'/>");
|
|
svg.WriteLine("</clipPath>");
|
|
|
|
svg.WriteLine("<filter id='dropShadowFilter<UNIQUE>' x='-20%' width='130%' height='130%'>");
|
|
svg.WriteLine("<feOffset result='offOut' in='SourceAlpha' dx='-2' dy='2' />");
|
|
svg.WriteLine("<feGaussianBlur result='blurOut' in='offOut' stdDeviation='1.1' />");
|
|
svg.WriteLine("<feBlend in='SourceGraphic' in2='blurOut' mode='normal' />");
|
|
svg.WriteLine("</filter>");
|
|
|
|
svg.WriteLine("</defs>");
|
|
|
|
|
|
DrawGraphArea(svg, theme, theme.BackgroundColour, dimensions, true);
|
|
perfLog.LogTiming("Initialization");
|
|
|
|
// Generate the list of processed, filtered CSV stats for graphing
|
|
List<CsvStats> csvStatsList = new List<CsvStats>();
|
|
int currentColorOffset = graphParams.colorOffset;
|
|
int currentCustomLabelIndex = 0;
|
|
|
|
FrameRange frameRange = new FrameRange();
|
|
for ( int i=0; i<csvList.Count; i++ )
|
|
{
|
|
CsvInfo csvInfo = csvList[i];
|
|
FrameRange csvFrameRange;
|
|
CsvStats newCsvStats = ProcessCsvStats(csvInfo.stats, graphParams, out csvFrameRange);
|
|
Colour csvColorOverride = null;
|
|
|
|
// Do we have specific csv color overrides?
|
|
if(graphParams.csvColors.Count > 0)
|
|
{
|
|
int csvColorIndex = (i % graphParams.csvColors.Count);
|
|
// assign a color override for this CSV
|
|
csvColorOverride = new Colour(graphParams.csvColors[csvColorIndex]);
|
|
}
|
|
|
|
if ( csvFrameRange.isLimited() )
|
|
{
|
|
// If we have multiple CSVs and we're applying startEvent limiting then just truncate the start of the CSVs at the event so the frames line up
|
|
// This has the effect that frame numbers start from zero, but there is no good alternative given the CSV frame numbers will differ
|
|
if (csvList.Count > 1 )
|
|
{
|
|
if (csvFrameRange.start > 0)
|
|
{
|
|
newCsvStats.CropStats(csvFrameRange.start, int.MaxValue);
|
|
}
|
|
// Adjust the global frameRange end such that we're using the largest range for all CSVs
|
|
if (csvFrameRange.end != int.MaxValue)
|
|
{
|
|
// Offset end index because we're truncating the start
|
|
csvFrameRange.end -= csvFrameRange.start;
|
|
}
|
|
frameRange.end = (i == 0) ? csvFrameRange.end : Math.Max(frameRange.end, csvFrameRange.end);
|
|
}
|
|
// Otherwise set frameRange to use the first (only) csv's range and we'll modify minX/maxX, which keeps the axis numbers accurate
|
|
else
|
|
{
|
|
frameRange = csvFrameRange;
|
|
}
|
|
}
|
|
|
|
if (graphParams.stacked && stackTotalStat == "")
|
|
{
|
|
// Make a total stat by summing each frame
|
|
StatSamples totalStat = new StatSamples("Total");
|
|
totalStat.samples.Capacity = newCsvStats.SampleCount;
|
|
for (int j = 0; j < newCsvStats.SampleCount; j++)
|
|
{
|
|
float totalValue = 0.0f;
|
|
foreach (StatSamples stat in newCsvStats.Stats.Values)
|
|
{
|
|
totalValue += stat.samples[j];
|
|
}
|
|
totalStat.samples.Add(totalValue);
|
|
}
|
|
totalStat.ComputeAverageAndTotal();
|
|
totalStat.colour = new Colour(0x6E6E6E);
|
|
newCsvStats.AddStat(totalStat);
|
|
stackTotalStat = "total";
|
|
stackTotalStatIsAutomatic = true;
|
|
}
|
|
|
|
SetLegend(newCsvStats, csvInfo.filename, graphParams, csvList.Count > 1, ref currentCustomLabelIndex);
|
|
if (!graphParams.stacked)
|
|
{
|
|
currentColorOffset = AssignColours(newCsvStats, theme, false, currentColorOffset, csvColorOverride);
|
|
}
|
|
csvStatsList.Add(newCsvStats);
|
|
}
|
|
perfLog.LogTiming("ProcessCsvStats");
|
|
|
|
Range range = new Range(graphParams.minX, graphParams.maxX, graphParams.minY, graphParams.maxY);
|
|
|
|
// Apply the frameRange if necessary
|
|
if (frameRange.isLimited())
|
|
{
|
|
// MinX and MaxX are both relative to frameRange.start
|
|
range.MinX = (range.MinX == Range.Auto) ? frameRange.start : frameRange.start + range.MinX;
|
|
if (range.MaxX != Range.Auto)
|
|
{
|
|
range.MaxX += frameRange.start;
|
|
}
|
|
if (frameRange.end != int.MaxValue)
|
|
{
|
|
range.MaxX = (range.MaxX == Range.Auto) ? frameRange.end : Math.Min(range.MaxX, frameRange.end);
|
|
}
|
|
}
|
|
|
|
if (graphParams.smooth)
|
|
{
|
|
for (int i=0; i<csvStatsList.Count; i++)
|
|
{
|
|
int kernelSize = graphParams.smoothKernelSize;
|
|
if (graphParams.smoothKernelSize == -1)
|
|
{
|
|
float percent = graphParams.smoothKernelPercent;
|
|
percent = Math.Min(percent, 100.0f);
|
|
percent = Math.Max(percent, 0.0f);
|
|
float kernelSizeF = percent * 0.01f * (float)csvStatsList[i].SampleCount + 0.5f;
|
|
kernelSize = (int)kernelSizeF;
|
|
}
|
|
csvStatsList[i] = SmoothStats(csvStatsList[i], kernelSize, graphParams);
|
|
}
|
|
perfLog.LogTiming("SmoothStats");
|
|
}
|
|
|
|
// Compute the X range
|
|
range = ComputeAdjustedXRange(range, graphRect, csvStatsList);
|
|
|
|
// Recompute the averages based on the X range. This is necessary for the legend and accurate sorting
|
|
for (int i = 0; i < csvStatsList.Count; i++)
|
|
{
|
|
RecomputeStatAveragesForRange(csvStatsList[i], range);
|
|
}
|
|
perfLog.LogTiming("RecomputeStatAverages");
|
|
|
|
|
|
// Handle stacking. Note that if we're not stacking, unstackedCsvStats will be a copy of csvStats
|
|
List<CsvStats> unstackedCsvStats = new List<CsvStats>();
|
|
for (int i = 0; i < csvStatsList.Count; i++)
|
|
{
|
|
unstackedCsvStats.Add(csvStatsList[i]);
|
|
|
|
if (graphParams.stacked)
|
|
{
|
|
csvStatsList[i] = StackStats(csvStatsList[i], range, graphParams.stackedUnsorted, stackTotalStat, stackTotalStatIsAutomatic);
|
|
AssignColours(csvStatsList[i], theme, true, graphParams.colorOffset, null);
|
|
|
|
// Copy the colours to the unstacked stats
|
|
foreach (StatSamples samples in unstackedCsvStats[i].Stats.Values)
|
|
{
|
|
string statName = samples.Name;
|
|
if (stackTotalStatIsAutomatic == false && stackTotalStat != null && samples.Name.ToLower() == stackTotalStat.ToLower())
|
|
{
|
|
statName = "other";
|
|
}
|
|
samples.colour = csvStatsList[i].GetStat(statName).colour;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (graphParams.stacked) perfLog.LogTiming("StackStats");
|
|
|
|
// Adjust range
|
|
range = ComputeAdjustedYRange(range, graphRect, csvStatsList, graphParams.maxAutoMaxY);
|
|
|
|
if (graphParams.graphOnly)
|
|
{
|
|
graphRect = dimensions;
|
|
}
|
|
|
|
// Adjust thickness depending on sample density, smoothness etc
|
|
float thickness = graphParams.smooth ? 0.33f : 0.1f;
|
|
float thicknessMultiplier = (12000.0f / (range.MaxX - range.MinX)) * graphParams.lineThickness;
|
|
thicknessMultiplier *= (graphRect.width / 400.0f);
|
|
thickness *= thicknessMultiplier;
|
|
thickness = Math.Max(Math.Min(thickness, 1.5f), 0.11f);
|
|
|
|
string graphTitle = graphParams.title;
|
|
|
|
// Get the title
|
|
if (graphTitle.Length == 0 && statNames.Count == 1 && csvStatsList.Count > 0 && !statNames[0].Contains("*"))
|
|
{
|
|
// Use the stat name if we can find it, otherwise just take the param as-is
|
|
StatSamples stat = csvStatsList[0].GetStat(statNames[0]);
|
|
if (stat == null)
|
|
{
|
|
int idx = statNames[0].IndexOf('=');
|
|
if (idx > 0)
|
|
{
|
|
// If this is a stat expression and we've defined the name via the = operator then use that
|
|
graphTitle = statNames[0][..idx].Replace("(","").Replace(")","");
|
|
}
|
|
else
|
|
{
|
|
graphTitle = statNames[0];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
graphTitle = stat.Name;
|
|
}
|
|
}
|
|
|
|
// Combine and validate metadata
|
|
// Assign metadata based on the first CSV with metadata
|
|
CsvMetadata metaData = null;
|
|
|
|
if (graphParams.showMetadata && !graphParams.graphOnly)
|
|
{
|
|
foreach (CsvStats stats in csvStatsList)
|
|
{
|
|
if (stats.metaData != null)
|
|
{
|
|
metaData = stats.metaData.Clone();
|
|
break;
|
|
}
|
|
}
|
|
if (metaData != null)
|
|
{
|
|
// Combine all metadata
|
|
foreach (CsvStats stats in csvStatsList)
|
|
{
|
|
metaData.CombineAndValidate(stats.metaData);
|
|
}
|
|
}
|
|
}
|
|
perfLog.LogTiming("AdjustRangesAndMetadata");
|
|
|
|
// Draw the graphs
|
|
int csvIndex = 0;
|
|
svg.WriteLine("<g id='graphArea<UNIQUE>'>");
|
|
if (graphParams.percentile)
|
|
{
|
|
range.MinX = graphParams.percentileTop99 ? 99 : (graphParams.percentileTop90 ? 90 : 0);
|
|
range.MaxX = 100;
|
|
DrawGridLines(svg, theme, graphRect, range, graphParams, !graphParams.graphOnly, 1.0f, true);
|
|
csvIndex = 0;
|
|
foreach (CsvStats csvStat in csvStatsList)
|
|
{
|
|
foreach (StatSamples stat in csvStat.Stats.Values)
|
|
{
|
|
string statID = "Stat_" + csvIndex + "_" + stat.Name + "<UNIQUE>";
|
|
DrawPercentileGraph(svg, stat.samples, stat.colour, graphRect, range, statID);
|
|
}
|
|
csvIndex++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DrawGridLines(svg, theme, graphRect, range, graphParams, !graphParams.graphOnly, 1.0f, graphParams.stacked);
|
|
csvIndex = 0;
|
|
foreach (CsvStats csvStat in csvStatsList)
|
|
{
|
|
DrawEventLines(svg, theme, csvStat.Events, graphRect, range);
|
|
DrawEventHighlightRegions(svg, csvStat, graphRect, range, graphParams.highlightEventRegions);
|
|
|
|
int MaxSegments = 1;
|
|
if (graphParams.stacked)
|
|
{
|
|
MaxSegments = 4;
|
|
if (csvStat.Stats.Values.Count > 15)
|
|
{
|
|
MaxSegments = 8;
|
|
if (csvStat.Stats.Values.Count > 30)
|
|
{
|
|
MaxSegments = 32;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (StatSamples stat in csvStat.Stats.Values)
|
|
{
|
|
string statID = "Stat_" + csvIndex + "_" + stat.Name + "<UNIQUE>";
|
|
DrawGraph(svg, stat.samples, stat.colour, graphRect, range, thickness, statID, graphParams, MaxSegments);
|
|
}
|
|
|
|
if (graphParams.showEventNameTextMode != ShowEventTextMode.Hide)
|
|
{
|
|
Colour eventColour = theme.EventTextColour;
|
|
DrawEventText(svg, csvStat.Events, eventColour, graphRect, range, graphParams);
|
|
}
|
|
|
|
csvIndex++;
|
|
}
|
|
}
|
|
perfLog.LogTiming("DrawGraphs");
|
|
|
|
|
|
svg.WriteLine("</g>");
|
|
|
|
if (graphParams.stacked)
|
|
{
|
|
// If we're stacked, we need to redraw the grid lines without axis text
|
|
DrawGridLines(svg, theme, graphRect, range, graphParams, false, 0.75f, false);
|
|
}
|
|
|
|
// Draw legend, metadata and title
|
|
if (!graphParams.graphOnly)
|
|
{
|
|
DrawLegend(svg, theme, csvStatsList, graphParams, dimensions);
|
|
if (graphParams.smooth)
|
|
{
|
|
DrawText(svg, "(smoothed)", 50.0f, 33, 10.0f, dimensions, theme.MinorTextColour, "start");
|
|
}
|
|
DrawTitle(svg, theme, graphTitle, dimensions);
|
|
}
|
|
|
|
|
|
if (metaData != null)
|
|
{
|
|
DrawMetadata(svg, metaData, dimensions, theme.TextColour);
|
|
}
|
|
|
|
DrawText(svg, "CSVToSVG " + CsvToSvgLibVersion.Get(), dimensions.width - 102, 15, 7.0f, dimensions, theme.MinorTextColour);
|
|
|
|
perfLog.LogTiming("DrawText");
|
|
|
|
// Draw the interactive elements
|
|
if (graphParams.interactive)
|
|
{
|
|
AddInteractiveScripting(svg, theme, graphRect, range, unstackedCsvStats, graphParams);
|
|
perfLog.LogTiming("AddInteractiveScripting");
|
|
}
|
|
|
|
svg.WriteLine("</svg>");
|
|
perfLog.LogTotalTiming(false);
|
|
}
|
|
|
|
Range ComputeAdjustedYRange(Range range, Rect rect, List<CsvStats> csvStats, float maxAutoMaxY)
|
|
{
|
|
Range newRange = new Range(range.MinX, range.MaxX, range.MinY, range.MaxY);
|
|
|
|
if (range.MinY == Range.Auto)
|
|
{
|
|
newRange.MinY = 0.0f;
|
|
}
|
|
|
|
if (range.MaxY == Range.Auto)
|
|
{
|
|
float maxSample = -10000000.0f;
|
|
foreach (CsvStats stats in csvStats)
|
|
{
|
|
foreach (StatSamples samples in stats.Stats.Values)
|
|
{
|
|
maxSample = Math.Max(maxSample, samples.ComputeMaxValue(range.MinX == Range.Auto ? 0 : (int)range.MinX, range.MaxX == Range.Auto ? -1 : (int)range.MaxX));
|
|
}
|
|
}
|
|
newRange.MaxY = Math.Min(maxSample * 1.05f, maxAutoMaxY);
|
|
}
|
|
|
|
// Quantise based on yincrement
|
|
if (rect != null)
|
|
{
|
|
float yInc = GetYAxisIncrement(rect, newRange);
|
|
float difY = newRange.MaxY - newRange.MinY;
|
|
float newDifY = (int)(0.9999 + difY / yInc) * yInc;
|
|
newRange.MaxY = newRange.MinY + newDifY;
|
|
}
|
|
|
|
return newRange;
|
|
}
|
|
|
|
Range ComputeAdjustedXRange(Range range, Rect rect, List<CsvStats> csvStats)
|
|
{
|
|
Range newRange = new Range(range.MinX, range.MaxX, range.MinY, range.MaxY);
|
|
int maxNumSamples = 0;
|
|
foreach (CsvStats stats in csvStats)
|
|
{
|
|
foreach (StatSamples samples in stats.Stats.Values)
|
|
{
|
|
maxNumSamples = Math.Max(maxNumSamples, samples.samples.Count);
|
|
}
|
|
}
|
|
|
|
if (range.MinX == Range.Auto) newRange.MinX = 0;
|
|
if (range.MaxX == Range.Auto) newRange.MaxX = maxNumSamples;
|
|
|
|
// Quantize MinX and MaxX based on xincrement
|
|
if (rect != null)
|
|
{
|
|
float xInc = GetXAxisIncrement(rect, newRange);
|
|
if (newRange.MinX > 0)
|
|
{
|
|
newRange.MinX = (int)(range.MinX / xInc) * xInc;
|
|
}
|
|
float difX = newRange.MaxX - newRange.MinX;
|
|
float newDifX = (int)(0.9999 + difX / xInc) * xInc;
|
|
newRange.MaxX = newRange.MinX + newDifX;
|
|
}
|
|
|
|
return newRange;
|
|
}
|
|
|
|
class SmoothKernel
|
|
{
|
|
public SmoothKernel(int inKernelSize, float maxDownsampleMultiplier = 4.0f)
|
|
{
|
|
int maxDownsampleLevel = Math.Min(4, Math.Max(0,(int)Math.Log(maxDownsampleMultiplier, 2.0f)));
|
|
downsampleLevel = Math.Max( Math.Min( (int)Math.Log((double)inKernelSize / 20.0, 2.0), maxDownsampleLevel), 0);
|
|
|
|
kernelSize = inKernelSize >> downsampleLevel;
|
|
weights = new float[kernelSize];
|
|
float totalWeight = 0.0f;
|
|
for (int i = 0; i < kernelSize; i++)
|
|
{
|
|
double df = (double)i + 0.5;
|
|
double b = (double)kernelSize / 2;
|
|
double c = (double)kernelSize / 8;
|
|
double weight = Math.Exp(-(Math.Pow(df - b, 2.0) / Math.Pow(2 * c, 2.0)));
|
|
weights[i] = (float)weight;
|
|
totalWeight += (float)weight;
|
|
}
|
|
float oneOverTotalWeight = 1.0f / totalWeight;
|
|
for (int i = 0; i < kernelSize; i++)
|
|
{
|
|
weights[i] *= oneOverTotalWeight;
|
|
}
|
|
}
|
|
public StatSamples SmoothStatSamples(StatSamples sourceStatSamples)
|
|
{
|
|
StatSamples destStatSamples = new StatSamples(sourceStatSamples,false);
|
|
destStatSamples.samples.Capacity = sourceStatSamples.samples.Count;
|
|
|
|
// Downsample the source samples to the specified size
|
|
List<float> downsampledSamples = sourceStatSamples.samples;
|
|
for ( int i=0; i< downsampleLevel; i++)
|
|
{
|
|
int currentSampleCount = downsampledSamples.Count / 2;
|
|
List<float> nextLevelSamples = new List<float>(currentSampleCount);
|
|
for (int j=0; j< currentSampleCount; j++)
|
|
{
|
|
int prevLevelIndex = j * 2;
|
|
nextLevelSamples.Add( ( downsampledSamples[prevLevelIndex] + downsampledSamples[prevLevelIndex + 1] ) * 0.5f );
|
|
}
|
|
downsampledSamples = nextLevelSamples;
|
|
}
|
|
int downsampleScale = 1 << downsampleLevel;
|
|
|
|
int kernelSizeClamped = Math.Min(kernelSize, downsampledSamples.Count);
|
|
|
|
for (int i = 0; i < sourceStatSamples.samples.Count; i++)
|
|
{
|
|
float sum = 0.0f;
|
|
int startIndex = (i >> downsampleLevel) - kernelSizeClamped / 2;
|
|
int endIndex = (startIndex + kernelSizeClamped);
|
|
int startIndexClamped = Math.Max(0, startIndex);
|
|
int endIndexClamped = Math.Min(downsampledSamples.Count, endIndex);
|
|
int kernelStartIndex = startIndexClamped - startIndex;
|
|
int kernelIndex = kernelStartIndex;
|
|
|
|
// Smooth the in-range samples
|
|
for (int j = startIndexClamped; j < endIndexClamped; j++)
|
|
{
|
|
sum += downsampledSamples[j] * weights[kernelIndex];
|
|
kernelIndex++;
|
|
}
|
|
|
|
// Mirror the out of range samples < 0
|
|
if (startIndex < 0)
|
|
{
|
|
kernelIndex = kernelStartIndex - 1;
|
|
endIndexClamped = Math.Min(downsampledSamples.Count, -startIndex);
|
|
for (int j = startIndexClamped; j < endIndexClamped; j++)
|
|
{
|
|
sum += downsampledSamples[j] * weights[kernelIndex];
|
|
kernelIndex--;
|
|
}
|
|
}
|
|
// Mirror the out of range samples >= n
|
|
if (endIndex > downsampledSamples.Count)
|
|
{
|
|
kernelIndex = kernelSizeClamped - 1;
|
|
startIndexClamped = Math.Max(0, downsampledSamples.Count - endIndex + downsampledSamples.Count);
|
|
for (int j = startIndexClamped; j < endIndexClamped; j++)
|
|
{
|
|
sum += downsampledSamples[j] * weights[kernelIndex];
|
|
kernelIndex--;
|
|
}
|
|
}
|
|
|
|
destStatSamples.samples.Add(sum);
|
|
}
|
|
return destStatSamples;
|
|
}
|
|
|
|
int kernelSize;
|
|
int downsampleLevel;
|
|
float [] weights;
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CsvStats SmoothStats(CsvStats stats, int KernelSize, GraphParams graphParams)
|
|
{
|
|
bool bMultithreaded = graphParams.bSmoothMultithreaded;
|
|
|
|
// Compute the displayed sample count
|
|
int minX = Math.Max( (int)graphParams.minX, 0 );
|
|
int maxX = stats.SampleCount;
|
|
if (graphParams.maxX > 0)
|
|
{
|
|
maxX = Math.Min((int)graphParams.maxX, stats.SampleCount);
|
|
}
|
|
int sampleCount = maxX - minX;
|
|
|
|
// Compute a max downsample level based on the sample count
|
|
float maxDownsampleMultiplier = (float)sampleCount/graphParams.width;
|
|
|
|
// Compute Gaussian Weights
|
|
SmoothKernel kernel = new SmoothKernel(KernelSize, maxDownsampleMultiplier);
|
|
|
|
List<StatSamples> statSampleList = stats.Stats.Values.ToList();
|
|
// Add the stats to smoothstats before the parallel for, so we can preserve the order
|
|
|
|
StatSamples[] smoothSamplesArray = new StatSamples[stats.Stats.Count];
|
|
if (bMultithreaded)
|
|
{
|
|
int numThreads = Environment.ProcessorCount/2;
|
|
Parallel.For(0, stats.Stats.Values.Count, new ParallelOptions { MaxDegreeOfParallelism = numThreads }, i =>
|
|
{
|
|
smoothSamplesArray[i] = kernel.SmoothStatSamples(statSampleList[i]);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
for (int i=0; i< stats.Stats.Values.Count; i++)
|
|
{
|
|
smoothSamplesArray[i] = kernel.SmoothStatSamples(statSampleList[i]);
|
|
}
|
|
}
|
|
|
|
CsvStats smoothStats = new CsvStats();
|
|
foreach (StatSamples stat in smoothSamplesArray)
|
|
{
|
|
smoothStats.AddStat(stat);
|
|
}
|
|
|
|
smoothStats.metaData = stats.metaData;
|
|
smoothStats.Events = stats.Events;
|
|
return smoothStats;
|
|
}
|
|
|
|
void SetLegend(CsvStats csvStats, string csvFilename, GraphParams graphParams, bool UseFilename, ref int currentCustomLabelIndex)
|
|
{
|
|
foreach (StatSamples stat in csvStats.Stats.Values)
|
|
{
|
|
stat.LegendName = stat.Name;
|
|
if (UseFilename)
|
|
{
|
|
stat.LegendName = System.IO.Path.GetFileName(csvFilename);
|
|
}
|
|
if (graphParams.customLegendNames.Count > 0 && currentCustomLabelIndex < graphParams.customLegendNames.Count)
|
|
{
|
|
stat.LegendName = graphParams.customLegendNames[currentCustomLabelIndex];
|
|
currentCustomLabelIndex++;
|
|
}
|
|
foreach (string hideStatPrefix in graphParams.hideStatPrefixes)
|
|
{
|
|
if (hideStatPrefix.Length > 0)
|
|
{
|
|
if (stat.LegendName.ToLower().StartsWith(hideStatPrefix.ToLower()))
|
|
{
|
|
stat.LegendName = stat.LegendName.Substring(hideStatPrefix.Length);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int AssignColours(CsvStats stats, Theme theme, bool reverseOrder, int inColorOffset, Colour csvColorOverride)
|
|
{
|
|
int colorOffset = inColorOffset;
|
|
if (reverseOrder)
|
|
{
|
|
colorOffset = stats.Stats.Values.Count - 1;
|
|
}
|
|
|
|
foreach (StatSamples stat in stats.Stats.Values)
|
|
{
|
|
if(csvColorOverride == null)
|
|
{
|
|
if (stat.colour.alpha == 0.0f)
|
|
{
|
|
stat.colour = theme.GraphColours[colorOffset % theme.GraphColours.Length];
|
|
}
|
|
if (reverseOrder)
|
|
{
|
|
colorOffset--;
|
|
}
|
|
else
|
|
{
|
|
colorOffset++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
stat.colour = csvColorOverride;
|
|
}
|
|
}
|
|
return colorOffset;
|
|
}
|
|
|
|
CsvStats StackStats(CsvStats stats, Range range, bool stackedUnsorted, string stackTotalStat, bool bStackTotalStatIsAutomatic)
|
|
{
|
|
// Find our stack total stat
|
|
StatSamples totalStat = null;
|
|
string stackTotalStatLower = stackTotalStat.ToLower();
|
|
if (stackTotalStat.Length > 0)
|
|
{
|
|
foreach (StatSamples stat in stats.Stats.Values)
|
|
{
|
|
if (stat.Name.ToLower() == stackTotalStatLower)
|
|
{
|
|
totalStat = stat;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// sort largest->smallest (based on average)
|
|
List<StatSamples> StatSamplesSorted = new List<StatSamples>();
|
|
foreach (StatSamples stat in stats.Stats.Values)
|
|
{
|
|
if (stat != totalStat)
|
|
{
|
|
StatSamplesSorted.Add(stat);
|
|
}
|
|
}
|
|
|
|
if (!stackedUnsorted)
|
|
{
|
|
StatSamplesSorted.Sort();
|
|
}
|
|
|
|
StatSamples OtherStat = null;
|
|
StatSamples OtherUnstacked = null;
|
|
int rangeStart = Math.Max(0, (int)range.MinX);
|
|
int rangeEnd = (int)range.MaxX;
|
|
|
|
if (!bStackTotalStatIsAutomatic && totalStat != null)
|
|
{
|
|
OtherStat = new StatSamples("Other");
|
|
OtherStat.colour = new Colour(0x6E6E6E);
|
|
StatSamplesSorted.Add(OtherStat);
|
|
OtherStat.samples.AddRange(totalStat.samples);
|
|
OtherUnstacked = new StatSamples(OtherStat, false);
|
|
rangeEnd = Math.Min(OtherStat.samples.Count, (int)range.MaxX);
|
|
}
|
|
|
|
// Get the max count for all stats
|
|
int maxCount = 0;
|
|
foreach (StatSamples statSamples in StatSamplesSorted)
|
|
{
|
|
maxCount = Math.Max(statSamples.samples.Count, maxCount);
|
|
}
|
|
|
|
// Make the stats cumulative
|
|
float[] SumList = new float[maxCount];
|
|
List<StatSamples> StatSamplesList = new List<StatSamples>();
|
|
foreach (StatSamples srcStatSamples in StatSamplesSorted)
|
|
{
|
|
StatSamples destStatSamples = new StatSamples(srcStatSamples, false);
|
|
if (srcStatSamples == OtherStat)
|
|
{
|
|
for (int j = 0; j < srcStatSamples.samples.Count; j++)
|
|
{
|
|
// The previous value of value is the total. If there are no other stats, then the total is "other"
|
|
float value = Math.Max(srcStatSamples.samples[j] - SumList[j], 0.0f);
|
|
OtherUnstacked.samples.Add(value);
|
|
SumList[j] += value;
|
|
destStatSamples.samples.Add(SumList[j]);
|
|
}
|
|
// If this is the "other" stat, recompute the average based on the unstacked value and apply that to the dest stat (this is used in the legend)
|
|
OtherUnstacked.ComputeAverageAndTotal(rangeStart, rangeEnd);
|
|
destStatSamples.average = OtherUnstacked.average;
|
|
}
|
|
else
|
|
{
|
|
for (int j = 0; j < srcStatSamples.samples.Count; j++)
|
|
{
|
|
SumList[j] += srcStatSamples.samples[j];
|
|
destStatSamples.samples.Add(SumList[j]);
|
|
}
|
|
}
|
|
StatSamplesList.Add(destStatSamples);
|
|
}
|
|
|
|
// Copy out the list in reverse order (so they get rendered front->back)
|
|
CsvStats stackedStats = new CsvStats();
|
|
if (bStackTotalStatIsAutomatic)
|
|
{
|
|
stackedStats.AddStat(totalStat);
|
|
}
|
|
|
|
for (int i = StatSamplesList.Count - 1; i >= 0; i--)
|
|
{
|
|
stackedStats.AddStat(StatSamplesList[i]);
|
|
}
|
|
|
|
stackedStats.metaData = stats.metaData;
|
|
stackedStats.Events = stats.Events;
|
|
return stackedStats;
|
|
}
|
|
|
|
float GetXAxisIncrement(Rect rect, Range range)
|
|
{
|
|
float[] xIncrements = { 0.1f, 1.0f, 5.0f, 10.0f, 20.0f, 50.0f, 100.0f, 200.0f, 500.0f, 1000.0f, 2000.0f, 5000.0f, 10000.0f };
|
|
float xIncrement = xIncrements[0];
|
|
|
|
for (int i = 0; i < xIncrements.Length - 1; i++)
|
|
{
|
|
float gap = ToSvgXScale(xIncrement, rect, range);
|
|
if (gap < 25)
|
|
{
|
|
xIncrement = xIncrements[i + 1];
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
|
|
}
|
|
int xRange = (int)range.MaxX - (int)range.MinX;
|
|
//xIncrement = xIncrement - (xRange % (int)xIncrement);
|
|
return xIncrement;
|
|
}
|
|
|
|
float GetYAxisIncrement(Rect rect, Range range)
|
|
{
|
|
float[] yIncrements = { 0.5f, 1.0f, 2.0f, 5.0f, 10.0f, 20.0f, 50.0f, 100.0f, 200.0f, 500.0f, 1000.0f, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000, 1000000, 2000000, 5000000, 10000000 };
|
|
|
|
// Find a good increment
|
|
float yIncrement = yIncrements[0];
|
|
for (int i = 0; i < yIncrements.Length - 1; i++)
|
|
{
|
|
float gap = ToSvgYScale(yIncrement, rect, range);
|
|
if (gap < 25)
|
|
{
|
|
yIncrement = yIncrements[i + 1];
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
return yIncrement;
|
|
}
|
|
|
|
void DrawGridLines(SvgFile svg, Theme theme, Rect rect, Range range, GraphParams graphParams, bool bDrawAxisText, float alpha = 1.0f, bool extendLines = false)
|
|
{
|
|
float xIncrement = GetXAxisIncrement(rect, range);
|
|
float yIncrement = GetYAxisIncrement(rect, range);
|
|
float yScale = 1.0f;
|
|
|
|
// Draw 1ms grid lines
|
|
float minorYAxisIncrement = 1.0f;
|
|
if ((range.MaxY - range.MinY) > 100) minorYAxisIncrement = yIncrement / 10.0f;
|
|
minorYAxisIncrement = Math.Max(minorYAxisIncrement, yIncrement / 5.0f);
|
|
|
|
Colour colour = new Colour(theme.MinorGridlineColour);
|
|
colour.alpha = alpha;
|
|
|
|
// TODO: replace with <pattern>
|
|
svg.WriteLine("<g id='gridLines<UNIQUE>'>");
|
|
for (int i = 0; i < 5000; i++)
|
|
{
|
|
float y = range.MinY + (float)i * minorYAxisIncrement;
|
|
DrawHorizLine(svg, y, colour, rect, range);
|
|
if (y >= range.MaxY) break;
|
|
}
|
|
|
|
colour = new Colour(theme.MajorGridlineColour);
|
|
colour.alpha = (alpha + 3.0f) / 4.0f;
|
|
for (int i = 0; i < 2000; i++)
|
|
{
|
|
float y = range.MinY + (float)i * yIncrement;
|
|
DrawHorizLine(svg, y, colour, rect, range, false, 0.25f, false);
|
|
yScale += yIncrement;
|
|
if (y >= range.MaxY) break;
|
|
}
|
|
svg.WriteLine("</g>");
|
|
|
|
// Draw the axis text
|
|
if (bDrawAxisText)
|
|
{
|
|
svg.WriteLine("<g id='xAxis<UNIQUE>'>");
|
|
for (int i = 0; i < 2000; i++)
|
|
{
|
|
float x = range.MinX + i * xIncrement;
|
|
if (x > (range.MaxX)) break;
|
|
int displayFrame = (int)Math.Floor(x) + graphParams.frameOffset;
|
|
DrawHorizontalAxisText(svg, displayFrame.ToString(), x, theme.TextColour, rect, range);
|
|
}
|
|
svg.WriteLine("</g>");
|
|
|
|
svg.WriteLine("<g id='yAxis<UNIQUE>'>");
|
|
for (int i = 0; i < 2000; i++)
|
|
{
|
|
float y = range.MinY + (float)i * yIncrement;
|
|
// FIXME: This text should use theme.LineColour - using LineColour for now for consistency/historical reasons. Need to test
|
|
DrawVerticalAxisText(svg, y.ToString(), y, theme.LineColour, rect, range);
|
|
DrawVerticalAxisNotch(svg, y, theme.LineColour, rect, range);
|
|
if (y >= range.MaxY) break;
|
|
}
|
|
svg.WriteLine("</g>");
|
|
}
|
|
|
|
DrawVerticalLine(svg, 0, theme.AxisLineColour, rect, range, 0.5f);
|
|
DrawHorizLine(svg, 0.0f, theme.AxisLineColour, rect, range, false, 0.5f);
|
|
|
|
// Draw the budget line if there is one
|
|
if (graphParams.budget != float.MaxValue)
|
|
{
|
|
float budgetLineThickness = theme.BudgetLineThickness;
|
|
Colour budgetLineColour = new Colour(theme.BudgetLineColour);
|
|
bool dropShadow = false;
|
|
if (alpha != 1.0f)
|
|
{
|
|
dropShadow = true;
|
|
budgetLineThickness *= 2.0f;
|
|
budgetLineColour.r = (byte)((float)budgetLineColour.r * 0.9f);
|
|
budgetLineColour.g = (byte)((float)budgetLineColour.g * 0.9f);
|
|
budgetLineColour.b = (byte)((float)budgetLineColour.b * 0.9f);
|
|
}
|
|
DrawHorizLine(svg, graphParams.budget, budgetLineColour, rect, range, true, budgetLineThickness, dropShadow);
|
|
}
|
|
}
|
|
|
|
void DrawText(SvgFile svg, string text, float x, float y, float size, Rect rect, Colour colour, string anchor = "start", string font = "Helvetica", string id = "", bool dropShadow = false)
|
|
{
|
|
svg.WriteLine("<text x='" + (rect.x + x) + "' y='" + (rect.y + y) + "' fill=" + colour.SVGString() +
|
|
(id.Length > 0 ? " id='" + id + "'" : "") +
|
|
(dropShadow ? " filter='url(#dropShadowFilter)'" : "") +
|
|
" font-size='" + size + "' font-family='" + font + "' style='text-anchor: " + anchor + "'>" + text + "</text> >");
|
|
}
|
|
|
|
void DrawTitle(SvgFile svg, Theme theme, string title, Rect rect)
|
|
{
|
|
DrawText(svg, title, (float)(rect.x + rect.width * 0.5), 34, 14, rect, theme.TextColour, "middle", "Helvetica");
|
|
}
|
|
|
|
|
|
void DrawGraphArea(SvgFile svg, Theme theme, Colour colour, Rect rect, bool gradient, bool interactive = false)
|
|
{
|
|
//svg.WriteLine("<rect x='0' y='0' width='" + (dimensions.width) + "' height='" + (dimensions.height) + "' fill=" + colour.SVGString() + " stroke-width='1' stroke=" + theme.BackgroundColour.SVGString());
|
|
svg.WriteLine("<rect x='0' y='0' width='" + (rect.width) + "' height='" + (rect.height) + "' fill=" + colour.SVGString() + " stroke-width='1' stroke=" + theme.BackgroundColour.SVGString());
|
|
|
|
if (gradient)
|
|
{
|
|
svg.Write("style='fill:url(#radialGradient<UNIQUE>);' ");
|
|
}
|
|
if (interactive)
|
|
{
|
|
svg.Write(" onmousemove='OnGraphAreaClicked<UNIQUE>(evt)'");
|
|
//svg.Write(" onmousewheel='OnGraphAreaWheeled<UNIQUE>(evt)'");
|
|
}
|
|
svg.Write("/>");
|
|
}
|
|
|
|
float ToSvgX(float graphX, Rect rect, Range range)
|
|
{
|
|
float scaleX = rect.width / (range.MaxX - range.MinX);
|
|
return rect.x + ((float)graphX - range.MinX) * scaleX;
|
|
}
|
|
|
|
float ToSvgY(float graphY, Rect rect, Range range)
|
|
{
|
|
float svgY = rect.height - (graphY - range.MinY) / (range.MaxY - range.MinY) * rect.height + rect.y;
|
|
|
|
float scaleY = rect.height / (range.MaxY - range.MinY);
|
|
float svgY2 = rect.y + rect.height - (graphY - range.MinY) * scaleY;
|
|
return svgY;
|
|
}
|
|
|
|
float ToSvgXScale(float graphX, Rect rect, Range range)
|
|
{
|
|
float scaleX = rect.width / (range.MaxX - range.MinX);
|
|
return graphX * scaleX;
|
|
}
|
|
|
|
float ToSvgYScale(float graphY, Rect rect, Range range)
|
|
{
|
|
float scaleY = rect.height / (range.MaxY - range.MinY);
|
|
return graphY * scaleY;
|
|
}
|
|
|
|
float ToCsvXScale(float graphX, Rect rect, Range range)
|
|
{
|
|
float scaleX = rect.width / (range.MaxX - range.MinX);
|
|
return graphX / scaleX;
|
|
}
|
|
|
|
float ToCsvYScale(float graphY, Rect rect, Range range)
|
|
{
|
|
float scaleY = rect.height / (range.MaxY - range.MinY);
|
|
return graphY / scaleY;
|
|
}
|
|
|
|
float ToCsvX(float svgX, Rect rect, Range range)
|
|
{
|
|
return (svgX - rect.x) * (range.MaxX - range.MinX) / rect.width + range.MinX;
|
|
}
|
|
|
|
float ToCsvY(float svgY, Rect rect, Range range)
|
|
{
|
|
return (svgY - rect.y) * (range.MaxY - range.MinY) / rect.height + range.MinY;
|
|
}
|
|
|
|
struct Vec2
|
|
{
|
|
public Vec2(float inX, float inY) { X = inX; Y = inY; }
|
|
public float X;
|
|
public float Y;
|
|
};
|
|
|
|
class PointInfo
|
|
{
|
|
public void AddPoint(Vec2 pt)
|
|
{
|
|
Count++;
|
|
Max.X = Math.Max(pt.X, Max.X);
|
|
Max.Y = Math.Max(pt.Y, Max.Y);
|
|
Min.X = Math.Min(pt.X, Min.X);
|
|
Min.Y = Math.Min(pt.Y, Min.Y);
|
|
Total.X += pt.X;
|
|
Total.Y += pt.Y;
|
|
}
|
|
public Vec2 ComputeAvg()
|
|
{
|
|
Vec2 avg;
|
|
avg.X = Total.X / (float)Count;
|
|
avg.Y = Total.Y / (float)Count;
|
|
return avg;
|
|
}
|
|
public Vec2 Max = new Vec2(float.MinValue, float.MinValue);
|
|
public Vec2 Min = new Vec2(float.MaxValue, float.MaxValue);
|
|
public Vec2 Total = new Vec2(0, 0);
|
|
public int Count = 0;
|
|
};
|
|
|
|
Vec2[] RemoveRedundantPoints(Vec2[] RawPoints, float compressionThreshold, int passIndex)
|
|
{
|
|
//return RawPoints;
|
|
//compressionThreshold = 0;
|
|
if (RawPoints.Length == 0)
|
|
{
|
|
return RawPoints;
|
|
}
|
|
List<Vec2> StrippedPoints = new List<Vec2>(RawPoints.Length);
|
|
|
|
int offset = passIndex % 2;
|
|
|
|
// Add the first points
|
|
int i = 0;
|
|
for (i = 0; i <= offset; i++)
|
|
{
|
|
StrippedPoints.Add(RawPoints[i]);
|
|
}
|
|
for (; i < RawPoints.Length - 1; i += 2)
|
|
{
|
|
Vec2 p1 = RawPoints[i - 1];
|
|
Vec2 pCentre = RawPoints[i];
|
|
Vec2 p2 = RawPoints[i + 1];
|
|
|
|
// Interpolate to find where the line would be if we removed this point
|
|
float dX = p2.X - p1.X;
|
|
float centreDX = pCentre.X - p1.X;
|
|
float lerpValue = centreDX / dX;
|
|
float interpX = p1.X * (1.0f - lerpValue) + (p2.X * lerpValue);
|
|
float interpY = p1.Y * (1.0f - lerpValue) + (p2.Y * lerpValue);
|
|
|
|
//float distMh = Math.Abs(interpX - pCentre.X) + Math.Abs(interpY - pCentre.Y);
|
|
float distMh = Math.Abs(interpY - pCentre.Y);
|
|
if (distMh >= compressionThreshold)
|
|
{
|
|
StrippedPoints.Add(pCentre);
|
|
}
|
|
StrippedPoints.Add(p2);
|
|
}
|
|
// Add the last points
|
|
for (; i < RawPoints.Length; i++)
|
|
{
|
|
StrippedPoints.Add(RawPoints[i]);
|
|
}
|
|
return StrippedPoints.ToArray();
|
|
}
|
|
|
|
List<Vec2> CompressPoints(List<Vec2> RawPoints, float pixelsPerPoint, Rect rect, Range range)
|
|
{
|
|
if (RawPoints.Count < 3)
|
|
{
|
|
return RawPoints;
|
|
}
|
|
int numPointsAfterCompression = (int)((float)rect.width / pixelsPerPoint);
|
|
if (numPointsAfterCompression >= RawPoints.Count)
|
|
{
|
|
return RawPoints;
|
|
}
|
|
float NumOutputPointsPerInputPoint = (float)numPointsAfterCompression / (float)RawPoints.Count;
|
|
|
|
PointInfo[] pointInfoList = new PointInfo[numPointsAfterCompression];
|
|
for (int i = 0; i < numPointsAfterCompression; i++) pointInfoList[i] = new PointInfo();
|
|
|
|
// Compute average, min, max for every point
|
|
for (int i = 0; i < RawPoints.Count; i++)
|
|
{
|
|
Vec2 point = RawPoints[i];
|
|
float outPointX = (float)i * NumOutputPointsPerInputPoint;
|
|
int outPointIndex = Math.Min((int)(outPointX), numPointsAfterCompression - 1);
|
|
pointInfoList[outPointIndex].AddPoint(point);
|
|
}
|
|
|
|
Vec2[] CompressedPoints = new Vec2[numPointsAfterCompression];
|
|
CompressedPoints[0] = pointInfoList[0].ComputeAvg();
|
|
for (int i = 1; i < pointInfoList.Length - 1; i++)
|
|
{
|
|
PointInfo info = pointInfoList[i];
|
|
PointInfo last = pointInfoList[i - 1];
|
|
PointInfo next = pointInfoList[i + 1];
|
|
if (info.Min.Y <= last.Min.Y && info.Min.Y < next.Min.Y)
|
|
{
|
|
CompressedPoints[i] = info.Min;
|
|
}
|
|
else if (info.Max.Y >= last.Max.Y && info.Max.Y > next.Max.Y)
|
|
{
|
|
CompressedPoints[i] = info.Max;
|
|
}
|
|
else
|
|
{
|
|
CompressedPoints[i] = info.ComputeAvg();
|
|
}
|
|
}
|
|
CompressedPoints[pointInfoList.Length - 1]=pointInfoList.Last().ComputeAvg();
|
|
float compressionThreshold = pixelsPerPoint;
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
CompressedPoints = RemoveRedundantPoints(CompressedPoints, compressionThreshold, i);
|
|
}
|
|
return CompressedPoints.ToList();
|
|
}
|
|
|
|
void DrawGraph(SvgFile svg, List<float> samples, Colour colour, Rect rect, Range range, float thickness, string id, GraphParams graphParams, int MaxSegments=1)
|
|
{
|
|
int n = Math.Min(samples.Count, (int)(range.MaxX + 0.5));
|
|
int start = (int)(range.MinX + 0.5);
|
|
|
|
int numDecimalPlaces = graphParams.lineDecimalPlaces;
|
|
|
|
if (start + 1 < n)
|
|
{
|
|
List<Vec2> RawPoints = new List<Vec2>(n);
|
|
for (int i = start; i < n; i++)
|
|
{
|
|
float sample = samples[i];
|
|
float x = ToSvgX((float)i, rect, range);
|
|
float y = ToSvgY(sample, rect, range);
|
|
|
|
RawPoints.Add(new Vec2(x, y));
|
|
}
|
|
|
|
if (graphParams.compression > 0.0f)
|
|
{
|
|
RawPoints = CompressPoints(RawPoints, graphParams.compression, rect, range);
|
|
}
|
|
|
|
string idString = id.Length > 0 ? "id='" + id + "'" : "";
|
|
|
|
string fillString = "none";
|
|
if (graphParams.stacked)
|
|
{
|
|
thickness = Math.Min(thickness, 0.01f);
|
|
colour.alpha = 1.0f;
|
|
fillString = colour.SVGStringNoQuotes();
|
|
}
|
|
|
|
float graphScale = 1.0f;
|
|
if (graphParams.bFixedPointGraphs)
|
|
{
|
|
// Fixed point graphs are smaller because they don't include the decimal point, but we need to apply a scale to get subpixel accuracy
|
|
// We scale the polyline by the inverse of the scaling that we're applying to the points
|
|
graphScale = graphParams.fixedPointPrecisionScale;
|
|
float oneOverScale = 1.0f / graphScale;
|
|
|
|
// Divide the graph into segments to make it faster to render
|
|
float originSvgY = ToSvgY(0.0f, rect, range);
|
|
|
|
int numSegments = Math.Max( Math.Min(RawPoints.Count / 500, 1), MaxSegments);
|
|
int pointsPerSegment = RawPoints.Count / numSegments;
|
|
for ( int i=0;i<numSegments; i++)
|
|
{
|
|
int startIndex = i * pointsPerSegment;
|
|
int endIndex = startIndex + pointsPerSegment + 1;
|
|
if (i == numSegments - 1)
|
|
{
|
|
endIndex = RawPoints.Count;
|
|
}
|
|
|
|
string segmentIdString = id.Length > 0 ? "id='" + id + "_Seg-"+i+ "'" : "";
|
|
svg.WriteLine("<polyline " + segmentIdString + " transform='scale(" + oneOverScale + "," + oneOverScale + ")' points='");
|
|
|
|
for (int pointIndex=startIndex ; pointIndex < endIndex; pointIndex++)
|
|
{
|
|
Vec2 point = RawPoints[pointIndex];
|
|
int x = (int)Math.Round(point.X * graphScale, 0);
|
|
int y = (int)Math.Round(point.Y * graphScale, 0);
|
|
svg.WriteFast(" " + x + "," + y);
|
|
}
|
|
|
|
if (graphParams.stacked)
|
|
{
|
|
int firstX = (int)Math.Round(RawPoints[startIndex].X * graphScale);
|
|
int lastX = (int)Math.Round(RawPoints[endIndex - 1].X * graphScale);
|
|
int originY = (int)Math.Round(ToSvgY(0.0f, rect, range) * graphScale);
|
|
svg.WriteFast(" " + lastX + ","+ originY);
|
|
svg.WriteFast(" " + firstX + ","+ originY);
|
|
}
|
|
svg.WriteLine("' style='fill:" + fillString + ";stroke-width:" + thickness * graphScale + "; clip-path: url(#graphClipRect<UNIQUE>)' stroke=" + colour.SVGString() + "/>");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string formatStr = "0";
|
|
if (numDecimalPlaces > 0)
|
|
{
|
|
formatStr += ".";
|
|
for (int i = 0; i < numDecimalPlaces; i++)
|
|
{
|
|
formatStr += "0";
|
|
}
|
|
}
|
|
svg.WriteLine("<polyline " + idString + " points='");
|
|
foreach (Vec2 point in RawPoints)
|
|
{
|
|
float x = point.X;
|
|
float y = point.Y;
|
|
svg.WriteFast(" " + x.ToString(formatStr) + "," + y.ToString(formatStr));
|
|
}
|
|
|
|
if (graphParams.stacked)
|
|
{
|
|
float lastSample = samples[n - 1];
|
|
svg.WriteFast(" " + ToSvgX((float)n + 20, rect, range) * graphScale + "," + ToSvgY(lastSample, rect, range) * graphScale);
|
|
svg.WriteFast(" " + ToSvgX((float)n + 20, rect, range) * graphScale + "," + ToSvgY(0.0f, rect, range) * graphScale);
|
|
svg.WriteFast(" " + ToSvgX((float)n - 20, rect, range) * graphScale + "," + ToSvgY(0.0f, rect, range) * graphScale);
|
|
svg.WriteFast(" " + ToSvgX(start, rect, range) * graphScale + "," + ToSvgY(0.0f, rect, range) * graphScale );
|
|
}
|
|
svg.WriteLine("' style='fill:" + fillString + ";stroke-width:" + thickness * graphScale + "; clip-path: url(#graphClipRect<UNIQUE>)' stroke=" + colour.SVGString() + "/>");
|
|
}
|
|
}
|
|
}
|
|
void DrawPercentileGraph(SvgFile svg, List<float> samples, Colour colour, Rect rect, Range range, string id)
|
|
{
|
|
samples.Sort();
|
|
List<Vec2> RawPoints = new List<Vec2>();
|
|
const int NUM_SAMPLES = 100;
|
|
int Begin = (int)range.MinX;
|
|
int End = (int)range.MaxX;
|
|
int Delta = End - Begin;
|
|
float Percentile, xbase, x, y;
|
|
int Count = samples.Count;
|
|
for (int i = NUM_SAMPLES * Begin; i < NUM_SAMPLES * End; i += Delta)
|
|
{
|
|
int sampleIndex = Count * i / (NUM_SAMPLES * 100);
|
|
Percentile = samples[sampleIndex];
|
|
xbase = (float)i / NUM_SAMPLES;
|
|
x = ToSvgX(xbase, rect, range);
|
|
y = ToSvgY(Percentile, rect, range);
|
|
RawPoints.Add(new Vec2(x, y));
|
|
}
|
|
|
|
Percentile = samples[Count - 1];
|
|
xbase = 100.0F;
|
|
x = ToSvgX(xbase, rect, range);
|
|
y = ToSvgY(Percentile, rect, range);
|
|
RawPoints.Add(new Vec2(x, y));
|
|
string idString = id.Length > 0 ? "id='" + id + "'" : "";
|
|
svg.WriteLine("<polyline " + idString + " points='");
|
|
foreach (Vec2 point in RawPoints)
|
|
{
|
|
x = point.X;
|
|
y = point.Y;
|
|
|
|
svg.WriteFast(" " + x + "," + y);
|
|
}
|
|
svg.WriteLine("' style='fill:none;stroke-width:1.3; clip-path: url(#graphClipRect<UNIQUE>)' stroke=" + colour.SVGString() + "/>");
|
|
|
|
}
|
|
|
|
|
|
class CsvGraphEvent : CsvEvent
|
|
{
|
|
public CsvGraphEvent(CsvEvent ev, int inPriority)
|
|
{
|
|
base.Frame = ev.Frame;
|
|
base.Name = ev.Name;
|
|
count = 1;
|
|
priority = inPriority;
|
|
}
|
|
public int count;
|
|
public int priority;
|
|
};
|
|
|
|
private string truncateTextIfNeeded(string text, int maxLength)
|
|
{
|
|
if (text.Length <= maxLength)
|
|
{
|
|
return text;
|
|
}
|
|
return text.Substring(0, maxLength) + "...";
|
|
}
|
|
|
|
void DrawEventText(SvgFile svg, List<CsvEvent> events, Colour colour, Rect rect, Range range, GraphParams graphParams )
|
|
{
|
|
float LastEventX = -100000.0f;
|
|
int lastFrame = 0;
|
|
|
|
bool bAutoShowMode = graphParams.showEventNameTextMode == ShowEventTextMode.ShowAuto;
|
|
float mergeThreshold = bAutoShowMode ? 12.0f : 8.5f;
|
|
|
|
// Make a filtered list of events to display, grouping duplicates which are close together
|
|
List<CsvGraphEvent> filteredEvents = new List<CsvGraphEvent>();
|
|
CsvGraphEvent currentDisplayEvent = null;
|
|
foreach (CsvEvent ev in events)
|
|
{
|
|
// Only draw events which are in the range
|
|
int eventPriority = GetEventPriority(ev.Name, graphParams);
|
|
if (ev.Frame >= range.MinX && ev.Frame <= range.MaxX)
|
|
{
|
|
// Merge with the current display event?
|
|
bool bMerge = false;
|
|
if (currentDisplayEvent != null && ( ev.Name == currentDisplayEvent.Name || bAutoShowMode))
|
|
{
|
|
float DistToLastEvent = ToSvgX(ev.Frame, rect, range) - ToSvgX(currentDisplayEvent.Frame, rect, range);
|
|
if (DistToLastEvent <= mergeThreshold)
|
|
{
|
|
bMerge = true;
|
|
}
|
|
}
|
|
|
|
if (bMerge)
|
|
{
|
|
if ( bAutoShowMode )
|
|
{
|
|
// If this event is actually higher (numerically lower) priority then replace the current event
|
|
if (eventPriority < currentDisplayEvent.priority )
|
|
{
|
|
currentDisplayEvent.priority = eventPriority;
|
|
currentDisplayEvent.Name = ev.Name;
|
|
currentDisplayEvent.Frame = ev.Frame;
|
|
}
|
|
}
|
|
currentDisplayEvent.count++;
|
|
}
|
|
else
|
|
{
|
|
currentDisplayEvent = new CsvGraphEvent(ev, eventPriority);
|
|
filteredEvents.Add(currentDisplayEvent);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (graphParams.showEventNameTextMode == ShowEventTextMode.ShowAuto)
|
|
{
|
|
foreach (CsvGraphEvent ev in filteredEvents)
|
|
{
|
|
float eventX = ToSvgX(ev.Frame, rect, range);
|
|
string name = truncateTextIfNeeded(ev.Name,32);
|
|
|
|
if (ev.count > 1)
|
|
{
|
|
name += " +" + (ev.count-1);
|
|
}
|
|
|
|
float csvTextX = ToCsvX(eventX + 7, rect, range);
|
|
DrawHorizontalAxisText(svg, name, csvTextX, colour, rect, range, 10, true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (CsvGraphEvent ev in filteredEvents)
|
|
{
|
|
float eventX = ToSvgX(ev.Frame, rect, range);
|
|
string name = ev.Name;
|
|
|
|
if (ev.count > 1)
|
|
{
|
|
name += " × " + ev.count;
|
|
}
|
|
|
|
// Space out the events (allow at least 8 pixels between them)
|
|
if (eventX - LastEventX <= 8.5f)
|
|
{
|
|
// Add an arrow to indicate events were spaced out
|
|
name = "↷ " + name;
|
|
if (ev.count == 1)
|
|
{
|
|
name += " (+" + (ev.Frame - lastFrame) + ")";
|
|
}
|
|
eventX = LastEventX + 9.0f;
|
|
}
|
|
float csvTextX = ToCsvX(eventX + 7, rect, range);
|
|
|
|
DrawHorizontalAxisText(svg, name, csvTextX, colour, rect, range, 10, true);
|
|
LastEventX = eventX;
|
|
lastFrame = ev.Frame;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void DrawEventLines(SvgFile svg, Theme theme, List<CsvEvent> events, Rect rect, Range range)
|
|
{
|
|
float LastEventX = -100000.0f;
|
|
float LastDisplayedEventX = -100000.0f;
|
|
int i = 0;
|
|
foreach (CsvEvent ev in events)
|
|
{
|
|
// Only draw events which are in the range
|
|
if (ev.Frame >= range.MinX && ev.Frame <= range.MaxX)
|
|
{
|
|
float eventX = ToSvgX(ev.Frame, rect, range);
|
|
// Space out the events (allow at least 2 pixels between them)
|
|
if (eventX - LastDisplayedEventX > 3)
|
|
{
|
|
float alpha = (eventX - LastEventX < 1.0f) ? 0.5f : 1.0f;
|
|
DrawVerticalLine(svg, ev.Frame, theme.MajorGridlineColour, rect, range, alpha, true, true);
|
|
LastDisplayedEventX = eventX;
|
|
}
|
|
LastEventX = eventX;
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
|
|
|
|
void DrawEventHighlightRegions(SvgFile svg, CsvStats csvStats, Rect rect, Range range, List<string> highlightEventRegions )
|
|
{
|
|
// Draw event regions if requested
|
|
if (highlightEventRegions != null)
|
|
{
|
|
Colour highlightColour = new Colour(0xffffff, 0.1f);
|
|
float y0 = rect.y;
|
|
float y1 = rect.y + rect.height;
|
|
float height = y1 - y0;
|
|
|
|
int numPairs = highlightEventRegions.Count / 2;
|
|
if (highlightEventRegions.Count >= 2 && highlightEventRegions.Count % 2 != 2)
|
|
{
|
|
for (int i = 0; i < numPairs; i++)
|
|
{
|
|
string startName = highlightEventRegions[i * 2].ToLower().Trim();
|
|
string endName = highlightEventRegions[i * 2 + 1].ToLower().Trim();
|
|
|
|
if (endName == "{null}")
|
|
{
|
|
endName = null;
|
|
}
|
|
if (startName == "{null}")
|
|
{
|
|
startName = null;
|
|
}
|
|
|
|
List<int> startIndices = null;
|
|
List<int> endIndices = null;
|
|
csvStats.GetEventFrameIndexDelimiters(startName, endName, out startIndices, out endIndices);
|
|
for (int j = 0; j < startIndices.Count; j++)
|
|
{
|
|
float x0 = ToSvgX(startIndices[j], rect, range);
|
|
float x1 = ToSvgX(endIndices[j], rect, range);
|
|
float width = x1 - x0;
|
|
|
|
svg.WriteLine("<rect x='" + x0 + "' y='" + y0 + "' width='" + width + "' height='" + height + "' fill=" + highlightColour.SVGString() + "/>");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
void DrawVerticalAxisNotch(SvgFile svg, float sample, Colour colour, Rect rect, Range range, float thickness = 0.25f)
|
|
{
|
|
if (sample > range.MaxY) return;
|
|
if (sample < range.MinY) return;
|
|
|
|
float y = ToSvgY(sample, rect, range);
|
|
|
|
float x1 = rect.x - 2.5f;
|
|
float x2 = rect.x;
|
|
|
|
svg.WriteLineFast("<line x1='" + x1 + "' y1='" + y + "' x2='" + x2 + "' y2='" + y + "' style='fill:none;stroke-width:" + thickness + "'"
|
|
+ " stroke=" + colour.SVGString() + "/>");
|
|
}
|
|
|
|
void DrawHorizLine(SvgFile svg, float sample, Colour colour, Rect rect, Range range, bool dashed = false, float thickness = 0.25f, bool dropShadow = false)
|
|
{
|
|
if (sample > range.MaxY) return;
|
|
if (sample < range.MinY) return;
|
|
|
|
float y = ToSvgY(sample, rect, range);
|
|
|
|
float x1 = rect.x;
|
|
float x2 = rect.x + rect.width;
|
|
|
|
if (dropShadow)
|
|
{
|
|
Colour shadowColour = new Colour(0, 0, 0, 0.33f);
|
|
svg.WriteLineFast("<line x1='" + (x1 - 1) + "' y1='" + (y + 1) + "' x2='" + (x2 - 1) + "' y2='" + (y + 1) + "' style='fill:none;stroke-width:" + thickness * 1.5f + "'"
|
|
+ " stroke=" + shadowColour.SVGString() + ""
|
|
+ (dashed ? " stroke-dasharray='4,4' d='M5 10 l215 0' " : "")
|
|
+ "/>" );
|
|
}
|
|
|
|
svg.WriteLineFast("<line x1='" + x1 + "' y1='" + y + "' x2='" + x2 + "' y2='" + y + "' style='fill:none;stroke-width:" + thickness + "'"
|
|
+ " stroke=" + colour.SVGString() + ""
|
|
+ (dashed ? " stroke-dasharray='4,4' d='M5 10 l215 0' " : "")
|
|
// + (dropShadow ? "filter='url(#dropShadowFilter)'>" : "" )
|
|
+ "/>" );
|
|
}
|
|
|
|
void DrawVerticalAxisText(SvgFile svg, string text, float textY, Colour colour, Rect rect, Range range, float size = 10.0f)
|
|
{
|
|
float y = ToSvgY(textY, rect, range) + 4;
|
|
float x = rect.x - 10.0f;
|
|
svg.WriteLine("<text x='" + x + "' y='" + y + "' fill=" + colour.SVGString() + " font-size='" + size + "' font-family='Helvetica' style='text-anchor: end'>" + text + "</text> >");
|
|
}
|
|
|
|
|
|
void DrawHorizontalAxisText(SvgFile svg, string text, float textX, Colour colour, Rect rect, Range range, float size = 10.0f, bool top = false)
|
|
{
|
|
float scaleX = rect.width / (float)(range.MaxX - range.MinX - 1);
|
|
float x = ToSvgX(textX, rect, range) - 4;
|
|
float y = rect.y + rect.height + 10;
|
|
|
|
if (top)
|
|
{
|
|
y = rect.y + 5;
|
|
}
|
|
|
|
svg.WriteLine("<text x='" + y + "' y='" + (-x) + "' fill=" + colour.SVGString() + " font-size='" + size + "' font-family='Helvetica' transform='rotate(90)' >" + text + "</text>");
|
|
}
|
|
|
|
void DrawLegend(SvgFile svg, Theme theme, List<CsvStats> csvStats, GraphParams graphParams, Rect rect)
|
|
{
|
|
List<StatSamples> statSamples = new List<StatSamples>();
|
|
|
|
foreach (CsvStats csvStat in csvStats)
|
|
{
|
|
foreach (StatSamples stat in csvStat.Stats.Values)
|
|
{
|
|
statSamples.Add(stat);
|
|
}
|
|
}
|
|
|
|
// Stacked stats are already sorted in the right order
|
|
if (graphParams.forceLegendSort || ((graphParams.showAverages || graphParams.showTotals) && !graphParams.stacked))
|
|
{
|
|
statSamples.Sort();
|
|
}
|
|
|
|
|
|
float x = rect.width + rect.x - 60;
|
|
if (graphParams.showAverages || graphParams.showTotals)
|
|
{
|
|
x -= 30;
|
|
}
|
|
float y = 30; // rect.height - 25;
|
|
|
|
svg.WriteLine("<g id='LegendPanel<UNIQUE>' fill='rgba(255,0,0,0.3)'>");// transform='translate(5) rotate(45 50 50)'> ");
|
|
svg.WriteLine("<rect x='"+x+"' y='"+y+"' width='0' height='100' fill='rgba(0,0,0,0.3)' blend='1' id='legendPanelRect<UNIQUE>' rx='5' ry='5'/>");
|
|
|
|
// Check if the total is a fraction
|
|
bool legendValueIsWholeNumber = false;
|
|
if (graphParams.showTotals)
|
|
{
|
|
legendValueIsWholeNumber = true;
|
|
foreach (StatSamples stat in statSamples)
|
|
{
|
|
if (stat.total != Math.Floor(stat.total))
|
|
{
|
|
legendValueIsWholeNumber = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (StatSamples stat in statSamples)
|
|
{
|
|
if (stat.average > graphParams.legendAverageThreshold)
|
|
{
|
|
Colour colour = stat.colour;
|
|
svg.WriteLine("<text x='" + (x - 5) + "' y='" + y + "' fill=" + theme.TextColour.SVGString() + " font-size='10' font-family='Helvetica' style='text-anchor: end' filter='url(#dropShadowFilter)'>" + stat.LegendName + "</text>");
|
|
svg.WriteLine("<rect x='" + (x + 0) + "' y='" + (y - 8) + "' width='10' height='10' fill=" + colour.SVGString() + " filter='url(#dropShadowFilter)' stroke-width='1' stroke='" + theme.LineColour + "' />");
|
|
|
|
if (graphParams.showAverages || graphParams.showTotals)
|
|
{
|
|
float legendValue = graphParams.showTotals ? (float)stat.total : stat.average;
|
|
string formatString = "0.00";
|
|
if (graphParams.showTotals && legendValueIsWholeNumber)
|
|
{
|
|
formatString = "0";
|
|
}
|
|
svg.WriteLine("<text x='" + (x + 15) + "' y='" + y + "' fill=" + theme.TextColour.SVGString() + " font-size='10' font-family='Helvetica' style='text-anchor: start' filter='url(#dropShadowFilter)'>" + legendValue.ToString(formatString) + "</text>");
|
|
}
|
|
|
|
y += 16;
|
|
}
|
|
}
|
|
|
|
if (graphParams.showTotals || graphParams.showAverages)
|
|
{
|
|
string legendValueTypeString = graphParams.showTotals ? "(sum)" : "(avg)";
|
|
svg.WriteLine("<text x='" + (x + 15) + "' y='" + y + "' fill=" + theme.TextColour.SVGString() + " font-size='10' font-family='Helvetica' style='text-anchor: start' filter='url(#dropShadowFilter)'>" + legendValueTypeString + "</text>");
|
|
}
|
|
|
|
svg.WriteLine("</g>");
|
|
}
|
|
|
|
|
|
void DrawVerticalLine(SvgFile svg, float sampleX, Colour colour, Rect rect, Range range, float thickness = 0.25f, bool dashed = false, bool dropShadow = false, string id = "", bool noTranslate = false)
|
|
{
|
|
float x = noTranslate ? sampleX : ToSvgX(sampleX, rect, range);
|
|
float y1 = rect.y;
|
|
float y2 = rect.y + rect.height;
|
|
|
|
if (dropShadow)
|
|
{
|
|
Colour shadowColour = new Colour(0, 0, 0, 0.33f);
|
|
svg.WriteLine("<line x1='" + (x - 1) + "' y1='" + (y1 + 1) + "' x2='" + (x - 1) + "' y2='" + (y2 + 1) + "' style='fill:none;stroke-width:" + thickness * 1.5f + "' "
|
|
+ " stroke=" + shadowColour.SVGString() + ""
|
|
+ (dashed ? " stroke-dasharray='4,4' d='M5 10 l215 0' " : "")
|
|
+ (id.Length > 0 ? " id='" + id + "_dropShadow'" : "")
|
|
+ "/>");
|
|
}
|
|
|
|
svg.WriteLine("<line x1='" + x + "' y1='" + y1 + "' x2='" + x + "' y2='" + y2 + "' style='fill:none;stroke-width:" + thickness + "' "
|
|
+ " stroke=" + colour.SVGString() + ""
|
|
+ (dashed ? " stroke-dasharray='4,4' d='M5 10 l215 0' " : "")
|
|
+ (id.Length > 0 ? " id='" + id + "'" : "")
|
|
+ "/>");
|
|
|
|
}
|
|
float frac(float value)
|
|
{
|
|
return value - (float)Math.Truncate(value);
|
|
}
|
|
|
|
string GetStringHash8Char(string name)
|
|
{
|
|
using (SHA256 sha256 = SHA256.Create())
|
|
{
|
|
Encoding enc = Encoding.UTF8;
|
|
byte[] hash = sha256.ComputeHash(enc.GetBytes(name));
|
|
return BitConverter.ToString(hash).Replace("-", string.Empty).Substring(0, 8);
|
|
}
|
|
}
|
|
|
|
string GetJSStatName(string statName)
|
|
{
|
|
string newString = "_";
|
|
foreach (char c in statName)
|
|
{
|
|
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
|
|
{
|
|
newString += c;
|
|
}
|
|
else
|
|
{
|
|
newString += "_";
|
|
}
|
|
}
|
|
newString += "_" + GetStringHash8Char(statName);
|
|
return newString;
|
|
}
|
|
|
|
class InteractiveStatInfo
|
|
{
|
|
public string friendlyName;
|
|
public string jsVarName;
|
|
public string jsTextElementId;
|
|
public string jsGroupElementId;
|
|
public string graphID;
|
|
public Colour colour;
|
|
public StatSamples statSamples;
|
|
public StatSamples originalStatSamples;
|
|
};
|
|
|
|
void AddInteractiveScripting(SvgFile svg, Theme theme, Rect rect, Range range, List<CsvStats> csvStats, GraphParams graphParams )
|
|
{
|
|
bool bSnapToPeaks = graphParams.snapToPeaks && !graphParams.smooth;
|
|
|
|
// Todo: separate values, quantized frame pos, pass in X axis name
|
|
List<InteractiveStatInfo> interactiveStats = new List<InteractiveStatInfo>();
|
|
|
|
// Add the frame number stat
|
|
{
|
|
InteractiveStatInfo statInfo = new InteractiveStatInfo();
|
|
statInfo.friendlyName = "Frame";
|
|
statInfo.jsTextElementId = "csvFrameText<UNIQUE>";
|
|
statInfo.jsGroupElementId = "csvFrameGroup<UNIQUE>";
|
|
statInfo.statSamples = null;
|
|
interactiveStats.Add(statInfo);
|
|
}
|
|
|
|
for (int i = 0; i < csvStats.Count; i++)
|
|
{
|
|
foreach (StatSamples stat in csvStats[i].Stats.Values)
|
|
{
|
|
InteractiveStatInfo statInfo = new InteractiveStatInfo();
|
|
statInfo.colour = stat.colour;
|
|
statInfo.friendlyName = stat.LegendName;
|
|
statInfo.jsVarName = "statData_" + i + "_" + GetJSStatName(stat.Name) + "<UNIQUE>";
|
|
statInfo.graphID = "Stat_" + i + "_" + stat.Name + "<UNIQUE>";
|
|
statInfo.jsTextElementId = "csvStatText__" + i + GetJSStatName(stat.Name) + "<UNIQUE>";
|
|
statInfo.jsGroupElementId = "csvStatGroup__" + i + GetJSStatName(stat.Name) + "<UNIQUE>";
|
|
statInfo.originalStatSamples = stat;
|
|
statInfo.statSamples = new StatSamples(stat, false);
|
|
interactiveStats.Add(statInfo);
|
|
}
|
|
}
|
|
|
|
// Record the max value at each index
|
|
// TODO: strip out data outside the range (needs an offset as well as a multiplier)
|
|
int multiplier = 1;
|
|
float numStatsPerPixel = (float)(range.MaxX - range.MinX) / rect.width;
|
|
|
|
// Compute the unfiltered sample count
|
|
int rawSampleCount = 0;
|
|
foreach (InteractiveStatInfo statInfo in interactiveStats)
|
|
{
|
|
if (statInfo.originalStatSamples != null)
|
|
{
|
|
rawSampleCount = Math.Max(statInfo.originalStatSamples.samples.Count, rawSampleCount);
|
|
}
|
|
}
|
|
int filteredSampleCount = rawSampleCount;
|
|
|
|
// TODO: truncate the stats/events using minX/maxX to save memory, so filteredSampleCount is (int)(range.MaxX) - (int)(range.MinX)
|
|
// if not downsampling. MinX will require a bias as well as a multiplier in the javascript
|
|
|
|
// Downsample the stats if needed
|
|
if (numStatsPerPixel > 1)
|
|
{
|
|
multiplier = (int)numStatsPerPixel;
|
|
|
|
// Compute max value for each frame
|
|
List<float> maxValues = new List<float>(rawSampleCount);
|
|
foreach (InteractiveStatInfo statInfo in interactiveStats)
|
|
{
|
|
if (statInfo.originalStatSamples != null)
|
|
{
|
|
for (int i = 0; i < statInfo.originalStatSamples.samples.Count; i++)
|
|
{
|
|
float value = statInfo.originalStatSamples.samples[i];
|
|
if (i >= maxValues.Count)
|
|
{
|
|
maxValues.Add(value);
|
|
}
|
|
else
|
|
{
|
|
maxValues[i] = Math.Max(maxValues[i], value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
filteredSampleCount = maxValues.Count / multiplier;// (int)(range.MaxX) - (int)(range.MinX);
|
|
|
|
// Create the filtered stat array
|
|
int offset = multiplier / 2;
|
|
foreach (InteractiveStatInfo statInfo in interactiveStats)
|
|
{
|
|
if (statInfo.originalStatSamples == null)
|
|
{
|
|
continue;
|
|
}
|
|
for (int i = 0; i < filteredSampleCount; i++)
|
|
{
|
|
int srcStartIndex = Math.Max(i * multiplier - offset, 0);
|
|
int srcEndIndex = Math.Min(i * multiplier + offset + 1, maxValues.Count);
|
|
|
|
int bestIndex = -1;
|
|
float highestValue = float.MinValue;
|
|
// Find a good index based on nearby maxValues
|
|
for (int j = srcStartIndex; j < srcEndIndex; j++)
|
|
{
|
|
if (maxValues[j] > highestValue)
|
|
{
|
|
highestValue = maxValues[j];
|
|
bestIndex = j;
|
|
}
|
|
}
|
|
if (bestIndex < statInfo.originalStatSamples.samples.Count)
|
|
{
|
|
statInfo.statSamples.samples.Add(statInfo.originalStatSamples.samples[bestIndex]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (InteractiveStatInfo statInfo in interactiveStats)
|
|
{
|
|
statInfo.statSamples = statInfo.originalStatSamples;
|
|
}
|
|
}
|
|
|
|
float oneOverMultiplier = 1.0f / (float)multiplier;
|
|
|
|
// Generate the event list
|
|
List<CsvEvent> allEvents = new List<CsvEvent>();
|
|
for (int i = 0; i < csvStats.Count; i++)
|
|
{
|
|
foreach (CsvEvent ev in csvStats[i].Events)
|
|
{
|
|
allEvents.Add(ev);
|
|
}
|
|
}
|
|
|
|
|
|
// Create a hidden panel for storing all the stat group elements
|
|
svg.WriteLine("<g id='interactivePanelInnerHidden<UNIQUE>' visibility='collapse'>");
|
|
Rect zeroRect = new Rect(0, 0, 0, 0);
|
|
foreach (InteractiveStatInfo statInfo in interactiveStats)
|
|
{
|
|
svg.WriteLine("<g id='" + statInfo.jsGroupElementId + "' transform='translate(0,0)' visibility='inherit'>");
|
|
int textXOffset = -5;
|
|
if (statInfo.colour != null)
|
|
{
|
|
svg.WriteLine("<rect x='-16' y='0' width='10' height='10' fill=" + statInfo.colour.SVGString() + " filter='url(#dropShadowFilter)' stroke-width='1' stroke='" + theme.LineColour + "'/>");
|
|
textXOffset = -20;
|
|
}
|
|
DrawText(svg, statInfo.friendlyName, textXOffset, 9, 11, zeroRect, new Colour(255, 255, 255, 1.0f), "end", "Helvetica", "", true);
|
|
DrawText(svg, " ", 5, 9, 11, zeroRect, new Colour(255, 255, 255, 1.0f), "start", "Helvetica", statInfo.jsTextElementId, true);
|
|
|
|
svg.WriteLine("</g>");
|
|
}
|
|
svg.WriteLine("</g>"); // interactivePanelInnerHidden
|
|
|
|
svg.WriteLine("<g id='interactivePanel<UNIQUE>' visibility='hidden'>");
|
|
DrawVerticalLine(svg, 0, new Colour(255, 255, 255, 0.4f), rect, range, 1.0f, true, true, "", true);
|
|
svg.WriteLine("<g id='interactivePanelInnerWithRect<UNIQUE>'>");
|
|
svg.WriteLine("<rect x='0' y='0' width='100' height='100' fill='rgba(0,0,0,0.3)' blend='1' id='interactivePanelRect<UNIQUE>' rx='5' ry='5'/>");
|
|
svg.WriteLine("<g id='interactivePanelInner<UNIQUE>'>");
|
|
svg.WriteLine("</g>"); // interactivePanelInner
|
|
svg.WriteLine("</g>"); // interactivePanelInnerWithRect
|
|
|
|
svg.WriteLine("<g id='interactiveEventPanelInnerWithRect<UNIQUE>'>");
|
|
svg.WriteLine("<rect x='0' y='0' width='100' height='100' fill='rgba(0,0,0,0.3)' blend='1' id='interactiveEventPanelRect<UNIQUE>' rx='5' ry='5'/>");
|
|
svg.WriteLine("<g id='interactiveEventPanelInner<UNIQUE>'>");
|
|
|
|
float textOffsetY = rect.y - 20;
|
|
DrawText(svg, "Events", 0, 9+ textOffsetY, 11, zeroRect, new Colour(255, 255, 255, 1.0f), "start", "Helvetica", "", true);
|
|
svg.WriteLine("</g>"); // interactiveEventPanelInner
|
|
svg.WriteLine("</g>"); // interactiveEventPanelInnerWithRect
|
|
svg.WriteLine("</g>"); // interactivepanel
|
|
|
|
|
|
svg.WriteLine("<script type='application/ecmascript'> <![CDATA[");
|
|
|
|
// Write the event list
|
|
List<string> elementStrings = new List<string>();
|
|
for (int i = 0; i < allEvents.Count; i++)
|
|
{
|
|
CsvEvent ev = allEvents[i];
|
|
elementStrings.Add("{ text: '" + ev.Name + "', frame: " + ev.Frame.ToString() + "}");
|
|
}
|
|
svg.WriteLine("var allEvents<UNIQUE> = [" + String.Join(",", elementStrings) + "]");
|
|
|
|
// Write the event index list for each sample frame
|
|
List<int>[] filteredEventIndexLists = new List<int>[filteredSampleCount];
|
|
for (int i = 0; i < filteredEventIndexLists.Length; i++)
|
|
filteredEventIndexLists[i] = new List<int>();
|
|
|
|
int eventPaddingPixels=6;
|
|
for (int i = 0; i < allEvents.Count; i++)
|
|
{
|
|
int frame = allEvents[i].Frame;
|
|
int filteredFrameIndex = (int)Math.Round((float)frame * oneOverMultiplier);
|
|
|
|
for (int j= filteredFrameIndex - eventPaddingPixels; j< filteredFrameIndex + eventPaddingPixels; j++)
|
|
{
|
|
if (j >= 0 && j < filteredEventIndexLists.Length)
|
|
{
|
|
filteredEventIndexLists[j].Add(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
elementStrings = new List<string>();
|
|
for (int i = 0; i < filteredEventIndexLists.Length; i++)
|
|
{
|
|
List<int> eventIndexList = filteredEventIndexLists[i];
|
|
List<string> innerElementStrings = new List<string>();
|
|
foreach (int index in eventIndexList)
|
|
{
|
|
innerElementStrings.Add(index.ToString());
|
|
}
|
|
elementStrings.Add("["+String.Join(",", innerElementStrings)+"]");
|
|
}
|
|
svg.WriteLine("var eventIndexLists<UNIQUE> = [" + String.Join(",", elementStrings) + "]");
|
|
|
|
// Write the data array for each stat
|
|
foreach (InteractiveStatInfo statInfo in interactiveStats)
|
|
{
|
|
if (statInfo.statSamples != null)
|
|
{
|
|
svg.Write("var " + statInfo.jsVarName + " = [");
|
|
foreach (float value in statInfo.statSamples.samples)
|
|
{
|
|
float fracValue = frac(value);
|
|
// If this is very close to a whole number, write as a whole number to save 3 bytes
|
|
if (fracValue <= 0.005f || fracValue >= 0.995f)
|
|
{
|
|
svg.WriteFast((int)Math.Round(value, 0) + ",");
|
|
}
|
|
else
|
|
{
|
|
string str = value.ToString("0.00");
|
|
if (str[0] == '0' && str[1] == '.')
|
|
{
|
|
// Trim off the leading 0 before the decimal point - it's unnecessary
|
|
str = str.Substring(1);
|
|
}
|
|
if (str.Last() == '0')
|
|
{
|
|
// Trim off trailing 0s
|
|
str = str.Substring(0,str.Length-1);
|
|
}
|
|
svg.WriteFast(str + ",");
|
|
}
|
|
}
|
|
svg.WriteLine("]");
|
|
}
|
|
}
|
|
|
|
svg.WriteLine("function OnLoaded<UNIQUE>(evt)");
|
|
svg.WriteLine("{");
|
|
svg.WriteLine(" var legendPanel = document.getElementById('LegendPanel<UNIQUE>');");
|
|
svg.WriteLine(" var legendRect = document.getElementById('legendPanelRect<UNIQUE>');");
|
|
svg.WriteLine(" var legendBbox = legendPanel.getBBox();");
|
|
svg.WriteLine(" legendRect.setAttribute('width',legendBbox.width+80);");
|
|
svg.WriteLine(" legendRect.setAttribute('height',legendBbox.height+8);");
|
|
svg.WriteLine(" legendRect.setAttribute('x',legendRect.getAttribute('x')-legendBbox.width);");
|
|
//svg.WriteLine(" legendRect.setAttribute('y',legendBbox.y-8);");
|
|
svg.WriteLine("}");
|
|
|
|
if (bSnapToPeaks)
|
|
{
|
|
// Record the max value at each index
|
|
List<float> maxValues = new List<float>();
|
|
foreach (InteractiveStatInfo statInfo in interactiveStats)
|
|
{
|
|
if (statInfo.statSamples != null)
|
|
{
|
|
for (int i = 0; i < statInfo.statSamples.samples.Count; i++)
|
|
{
|
|
float value = statInfo.statSamples.samples[i];
|
|
if (i >= maxValues.Count)
|
|
{
|
|
maxValues.Add(value);
|
|
}
|
|
else
|
|
{
|
|
maxValues[i] = Math.Max(maxValues[i], value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
svg.Write("var maxValues<UNIQUE> = [");
|
|
foreach (float value in maxValues)
|
|
{
|
|
svg.WriteFast(value + ",");
|
|
}
|
|
svg.WriteLine("]");
|
|
|
|
svg.WriteLine("function FindInterestingFrame<UNIQUE>(x,range)");
|
|
svg.WriteLine("{");
|
|
svg.WriteLine(" range *= " + oneOverMultiplier + ";");
|
|
svg.WriteLine(" x = Math.round( x*" + oneOverMultiplier + " );");
|
|
svg.WriteLine(" halfRange = Math.round(range/2.0);");
|
|
svg.WriteLine(" startX = Math.round(Math.max(x-halfRange,0));");
|
|
svg.WriteLine(" endX = Math.round(Math.min(x+halfRange,maxValues<UNIQUE>.length));");
|
|
svg.WriteLine(" maxVal = 0.0;");
|
|
svg.WriteLine(" bestIndex = x;");
|
|
svg.WriteLine(" for ( i=startX;i<endX;i++) ");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" if (maxValues<UNIQUE>[i]>maxVal)");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" bestIndex=i;");
|
|
svg.WriteLine(" maxVal = maxValues<UNIQUE>[i];");
|
|
svg.WriteLine(" }");
|
|
svg.WriteLine(" }");
|
|
svg.WriteLine(" return Math.round( bestIndex *" + multiplier + ");");
|
|
svg.WriteLine("}");
|
|
svg.WriteLine(" ");
|
|
}
|
|
else
|
|
{
|
|
svg.WriteLine("function FindInterestingFrame<UNIQUE>(x,range) { return x; }");
|
|
}
|
|
|
|
|
|
|
|
|
|
svg.WriteLine("function GetGraphX<UNIQUE>(mouseX)");
|
|
svg.WriteLine("{");
|
|
svg.WriteLine(" return (mouseX - " + rect.x + ") * (" + range.MaxX + " - " + range.MinX + ") / " + rect.width + " + " + range.MinX + ";");
|
|
svg.WriteLine("}");
|
|
svg.WriteLine(" ");
|
|
|
|
svg.WriteLine("function compareSamples(a, b)");
|
|
svg.WriteLine("{");
|
|
svg.WriteLine(" if (a.isFrame)");
|
|
svg.WriteLine(" return -1;");
|
|
svg.WriteLine(" if (b.isFrame)");
|
|
svg.WriteLine(" return 1;");
|
|
svg.WriteLine(" if (a.value > b.value)");
|
|
svg.WriteLine(" return -1;");
|
|
svg.WriteLine(" if (a.value < b.value)");
|
|
svg.WriteLine(" return 1;");
|
|
svg.WriteLine(" return 0;");
|
|
svg.WriteLine("}");
|
|
|
|
svg.WriteLine("function ToSvgX<UNIQUE>(graphX)");
|
|
svg.WriteLine("{");
|
|
svg.WriteLine(" scaleX = " + rect.width / (range.MaxX - range.MinX) + ";");
|
|
svg.WriteLine(" return " + rect.x + " + (graphX - " + range.MinX + ") * scaleX;");
|
|
svg.WriteLine("}");
|
|
|
|
/*
|
|
svg.WriteLine("var graphView = { offsetX:0.0, offsetY:0.0, scaleX:1.0, scaleY:1.0 };");
|
|
svg.WriteLine("function OnGraphAreaWheeled<UNIQUE>(evt)");
|
|
svg.WriteLine("{");
|
|
svg.WriteLine(" var graphAreaElement = document.getElementById('graphAreaTransform');");
|
|
svg.WriteLine(" graphView.scaleX+=0.1;");
|
|
svg.WriteLine(" ");
|
|
svg.WriteLine(" graphAreaElement.setAttribute('transform','scale('+graphView.scaleX+','+graphView.scaleY+')')");
|
|
svg.WriteLine("}");
|
|
*/
|
|
|
|
int onePixelFrameCount = (int)(ToCsvXScale(1.0f, rect, range) * 8.0f);
|
|
float rightEdgeX = ToSvgX(range.MaxX, rect, range);
|
|
|
|
svg.WriteLine("function OnGraphAreaClicked<UNIQUE>(evt)");
|
|
svg.WriteLine("{");
|
|
svg.WriteLine(" graphX = GetGraphX<UNIQUE>(evt.offsetX); ");
|
|
svg.WriteLine(" var interactivePanel = document.getElementById('interactivePanel<UNIQUE>');");
|
|
svg.WriteLine(" var legendPanel = document.getElementById('LegendPanel<UNIQUE>');");
|
|
// Snap to an interesting frame (the max value under the pixel)
|
|
|
|
svg.WriteLine(" var frameNum = FindInterestingFrame<UNIQUE>( Math.round(graphX+0.5), " + onePixelFrameCount + " );");
|
|
svg.WriteLine(" if (frameNum >= " + range.MinX + " && frameNum < " + range.MaxX + ")");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" var xOffset = 0;");
|
|
svg.WriteLine(" var lineX = ToSvgX<UNIQUE>(frameNum);");
|
|
svg.WriteLine(" var textX = lineX + xOffset;");
|
|
svg.WriteLine(" var textY = " + textOffsetY);
|
|
|
|
svg.WriteLine(" var interactivePanelInner = document.getElementById('interactivePanelInner<UNIQUE>');");
|
|
svg.WriteLine(" legendPanel.setAttribute('visibility','hidden')");
|
|
svg.WriteLine(" interactivePanel.setAttribute('visibility','visible')");
|
|
svg.WriteLine(" interactivePanel.setAttribute('transform','translate('+textX+',0)')");
|
|
svg.WriteLine(" var dataIndex = Math.round( frameNum * " + oneOverMultiplier + " );");
|
|
svg.WriteLine(" dataIndex = Math.min( dataIndex, "+ (filteredSampleCount-1) +" ); ");
|
|
|
|
// Fill out the sample data array
|
|
svg.WriteLine(" var samples = [");
|
|
foreach (InteractiveStatInfo statInfo in interactiveStats)
|
|
{
|
|
string groupElementString = "document.getElementById('" + statInfo.jsGroupElementId + "')";
|
|
string textElementString = "document.getElementById('" + statInfo.jsTextElementId + "')";
|
|
if (statInfo.jsVarName == null)
|
|
{
|
|
svg.Write(" { value: frameNum+" + graphParams.frameOffset + ", name: '" + statInfo.friendlyName + "', colour: 'rgb(0,0,0)', isFrame:true");
|
|
}
|
|
else
|
|
{
|
|
string valueStr = statInfo.jsVarName + "[dataIndex]";
|
|
svg.Write(" { value: " + valueStr + ", name: '" + statInfo.friendlyName + "', colour: " + statInfo.colour.SVGString() + ", isFrame:false");
|
|
}
|
|
svg.WriteLine(", groupElement: " + groupElementString + ", textElement: " + textElementString + " },");
|
|
}
|
|
svg.WriteLine(" ];");
|
|
svg.WriteLine(" samples.sort(compareSamples);");
|
|
|
|
// Draw the interactive legend
|
|
// Move all elements from the visible rect into the hidden rect
|
|
svg.WriteLine(" var panelInner = document.getElementById('interactivePanelInner<UNIQUE>');");
|
|
svg.WriteLine(" var panelInnerHidden = document.getElementById('interactivePanelInnerHidden<UNIQUE>');");
|
|
svg.WriteLine(" while (panelInner.childNodes.length>0)");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" panelInnerHidden.appendChild(panelInner.childNodes[0]);");
|
|
svg.WriteLine(" }");
|
|
|
|
svg.WriteLine(" var maxSampleCountToDisplay="+(rect.height/15)+";");
|
|
svg.WriteLine(" for ( i=0;i<samples.length;i++ )");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" var groupElement = samples[i].groupElement;");
|
|
svg.WriteLine(" if ( samples[i].value > 0 && i<=maxSampleCountToDisplay)");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" groupElement.setAttribute('transform','translate(0,'+textY+')');");
|
|
svg.WriteLine(" var textElement = samples[i].textElement;");
|
|
svg.WriteLine(" var textNode = document.createTextNode(samples[i].value);");
|
|
svg.WriteLine(" textElement.replaceChild(textNode,textElement.childNodes[0]); ");
|
|
svg.WriteLine(" if (groupElement.parentNode != panelInner)");
|
|
svg.WriteLine(" panelInner.appendChild(groupElement);");
|
|
svg.WriteLine(" textY += 15;");
|
|
svg.WriteLine(" }");
|
|
svg.WriteLine(" else");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" if (groupElement.parentNode != panelInnerHidden)");
|
|
svg.WriteLine(" panelInnerHidden.appendChild(groupElement);");
|
|
svg.WriteLine(" }");
|
|
svg.WriteLine(" }");
|
|
|
|
// Display events
|
|
svg.WriteLine(" var eventPanelInner = document.getElementById('interactiveEventPanelInner<UNIQUE>');");
|
|
svg.WriteLine(" while (eventPanelInner.childNodes.length>2)");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" eventPanelInner.removeChild(eventPanelInner.lastChild);");
|
|
svg.WriteLine(" }");
|
|
|
|
svg.WriteLine(" var eventTextY = " + textOffsetY+"+15;");
|
|
svg.WriteLine(" var eventIndexList = eventIndexLists<UNIQUE>[dataIndex];");
|
|
svg.WriteLine(" for ( i=0;i<eventIndexList.length;i++ )");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" var index = eventIndexList[i];");
|
|
svg.WriteLine(" var event = allEvents<UNIQUE>[index];");
|
|
svg.WriteLine(" if ( eventTextY<=" + rect.height.ToString() + ")");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" var frameOffset = event.frame - frameNum;");
|
|
svg.WriteLine(" var frameOffsetStr = (frameOffset>0 ? '+' : '') +frameOffset.toString()");
|
|
svg.WriteLine(" var textElement = document.createElementNS('http://www.w3.org/2000/svg','text');");
|
|
svg.WriteLine(" textElement.setAttribute('transform','translate(0,'+eventTextY+')');");
|
|
svg.WriteLine(" textElement.innerHTML = event.text + ' (' + frameOffsetStr +')';");
|
|
svg.WriteLine(" textElement.setAttribute('fill','rgb(224, 224, 224)');");
|
|
svg.WriteLine(" textElement.setAttribute('filter','url(#dropShadowFilter)');");
|
|
svg.WriteLine(" textElement.setAttribute('stroke','CSVStats.Colour');");
|
|
svg.WriteLine(" textElement.setAttribute('font-size','10');");
|
|
svg.WriteLine(" textElement.setAttribute('font-family','Helvetica');");
|
|
svg.WriteLine(" textElement.setAttribute('style','text-anchor: start');");
|
|
svg.WriteLine(" textElement.setAttribute('x','0');");
|
|
svg.WriteLine(" textElement.setAttribute('y','9');");
|
|
svg.WriteLine(" textElement.setAttribute('visibility','inherit');");
|
|
svg.WriteLine(" eventPanelInner.appendChild(textElement);");
|
|
svg.WriteLine(" eventTextY += 12;");
|
|
svg.WriteLine(" }");
|
|
svg.WriteLine(" }");
|
|
|
|
// Set the panel rect dimensions based on the content
|
|
svg.WriteLine(" var panelRect = document.getElementById('interactivePanelRect<UNIQUE>');");
|
|
svg.WriteLine(" var bbox = interactivePanelInner.getBBox();");
|
|
svg.WriteLine(" panelRect.setAttribute('width',bbox.width+16);");
|
|
svg.WriteLine(" panelRect.setAttribute('height',textY+8);");
|
|
svg.WriteLine(" panelRect.setAttribute('x',bbox.x-8);");
|
|
svg.WriteLine(" panelRect.setAttribute('y',bbox.y-8);");
|
|
|
|
float maxX = rect.x + rect.width;
|
|
svg.WriteLine(" var panelOffset = 0;");
|
|
svg.WriteLine(" if ( bbox.x + textX < 0 )");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" panelOffset = -(bbox.x + textX);");
|
|
svg.WriteLine(" }");
|
|
svg.WriteLine(" else if ( bbox.x+bbox.width + textX > " + maxX + " ) ");
|
|
svg.WriteLine(" { ");
|
|
svg.WriteLine(" panelOffset = " + maxX + "-(bbox.x+bbox.width + textX);");
|
|
svg.WriteLine(" } ");
|
|
svg.WriteLine(" var interactivePanelInnerWithRect = document.getElementById('interactivePanelInnerWithRect<UNIQUE>');");
|
|
svg.WriteLine(" interactivePanelInnerWithRect.setAttribute('transform','translate('+panelOffset+',0)');");
|
|
|
|
// Move the event panel and set its visibility
|
|
svg.WriteLine(" var eventBbox = eventPanelInner.getBBox();");
|
|
svg.WriteLine(" if ( textX + eventBbox.width + bbox.width + bbox.x > " + maxX + " ) ");
|
|
svg.WriteLine(" panelOffset-=eventBbox.width + bbox.width+40;");
|
|
|
|
svg.WriteLine(" var eventPanel = document.getElementById('interactiveEventPanelInnerWithRect<UNIQUE>');");
|
|
svg.WriteLine(" if (eventIndexList.length>0)");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" eventPanel.setAttribute('visibility','inherit');");
|
|
svg.WriteLine(" eventPanel.setAttribute('transform','translate('+(bbox.width+bbox.x+20+panelOffset)+',0)');");
|
|
svg.WriteLine(" }");
|
|
svg.WriteLine(" else");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" eventPanel.setAttribute('visibility','hidden');");
|
|
svg.WriteLine(" }");
|
|
|
|
// Set the event panel rect size
|
|
svg.WriteLine(" var eventPanelRect = document.getElementById('interactiveEventPanelRect<UNIQUE>');");
|
|
svg.WriteLine(" eventPanelRect.setAttribute('width',eventBbox.width+16);");
|
|
svg.WriteLine(" eventPanelRect.setAttribute('height',eventTextY+8);");
|
|
svg.WriteLine(" eventPanelRect.setAttribute('x',eventBbox.x-8);");
|
|
svg.WriteLine(" eventPanelRect.setAttribute('y',eventBbox.y-8);");
|
|
svg.WriteLine(" }");
|
|
|
|
// If we moused outside the box then hide the interactive panel
|
|
svg.WriteLine(" else");
|
|
svg.WriteLine(" {");
|
|
svg.WriteLine(" interactivePanel.setAttribute('visibility','hidden')");
|
|
svg.WriteLine(" legendPanel.setAttribute('visibility','visible')");
|
|
svg.WriteLine(" }");
|
|
svg.WriteLine("}");
|
|
svg.WriteLine("]]> </script>");
|
|
|
|
Rect dimensions = new Rect(0, 0, graphParams.width, graphParams.height);
|
|
DrawGraphArea(svg, theme, new Colour(0, 0, 0, 0.0f), dimensions, false, true);
|
|
}
|
|
|
|
void DrawMetadata(SvgFile svg, CsvMetadata metadata, Rect rect, Colour colour)
|
|
{
|
|
float y = rect.height - 15;
|
|
|
|
string Platform = metadata.GetValue("platform", "[Unknown platform]");
|
|
string BuildConfiguration = metadata.GetValue("config", "[Unknown config]");
|
|
string BuildVersion = metadata.GetValue("buildversion", "[Unknown version]");
|
|
string SessionId = metadata.GetValue("sessionid", "");
|
|
string PlaylistId = metadata.GetValue("playlistid", "");
|
|
// If we have a device profile, write that out instead of the platform
|
|
Platform = metadata.GetValue("deviceprofile", Platform);
|
|
BuildVersion = metadata.GetValue("buildversion", BuildVersion);
|
|
|
|
string Commandline = metadata.GetValue("commandline", "");
|
|
svg.WriteLine("<text x='" + 10 + "' y='" + y + "' fill=" + colour.SVGString() + " font-size='9' font-family='Helvetica' >" + BuildConfiguration + " " + Platform + " " + BuildVersion + " " + SessionId + " " + PlaylistId + "</text>");
|
|
y += 10;
|
|
svg.WriteLine("<text x='" + 10 + "' y='" + y + "' fill=" + colour.SVGString() + " font-size='9' font-family='Courier New' >" + Commandline + "</text>");
|
|
}
|
|
|
|
bool IsEventShown(string eventString, GraphParams graphParams)
|
|
{
|
|
return GetEventPriority(eventString, graphParams) >= 0;
|
|
}
|
|
|
|
int GetEventPriority(string eventString, GraphParams graphParams)
|
|
{
|
|
if (graphParams.showEventNames == null)
|
|
{
|
|
return -1;
|
|
}
|
|
if (graphParams.showEventNames.Count == 0)
|
|
{
|
|
return -1;
|
|
}
|
|
if (eventString.Length == 0)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
eventString = eventString.ToLower();
|
|
int priority = 0;
|
|
foreach (string showEventName in graphParams.showEventNames)
|
|
{
|
|
string showEventNameLower = showEventName.ToLower();
|
|
if (showEventNameLower.EndsWith("*"))
|
|
{
|
|
int index = showEventNameLower.LastIndexOf('*');
|
|
string prefix = showEventNameLower.Substring(0, index);
|
|
if (eventString.StartsWith(prefix))
|
|
{
|
|
return priority;
|
|
}
|
|
}
|
|
else if (eventString == showEventNameLower)
|
|
{
|
|
return priority;
|
|
}
|
|
priority++;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
bool IsStatIgnored(string statName, GraphParams graphParams)
|
|
{
|
|
statName = statName.ToLower();
|
|
|
|
if (graphParams.maxHierarchyDepth != -1)
|
|
{
|
|
int Depth = 0;
|
|
foreach (char c in statName)
|
|
{
|
|
if (c == graphParams.hierarchySeparator)
|
|
{
|
|
Depth++;
|
|
if (Depth > graphParams.maxHierarchyDepth)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (graphParams.ignoreStats.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (string ignoreStat in graphParams.ignoreStats)
|
|
{
|
|
string ignoreStatLower = ignoreStat.ToLower();
|
|
if (ignoreStatLower.EndsWith("*"))
|
|
{
|
|
int index = ignoreStatLower.LastIndexOf('*');
|
|
string prefix = ignoreStatLower.Substring(0, index);
|
|
if (statName.StartsWith(prefix))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else if (statName == ignoreStatLower)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
class FrameRange
|
|
{
|
|
public bool isLimited()
|
|
{
|
|
return start > 0 || end < Int32.MaxValue;
|
|
}
|
|
|
|
public int start = 0;
|
|
public int end = Int32.MaxValue;
|
|
};
|
|
|
|
FrameRange GetEventTruncationFrameRange(CsvStats csvStats, GraphParams graphParams)
|
|
{
|
|
// Apply startEvent and endEvent truncation if requested
|
|
FrameRange frameRange = new FrameRange();
|
|
if (graphParams.startEvent != null)
|
|
{
|
|
foreach (CsvEvent ev in csvStats.Events)
|
|
{
|
|
if (CsvStats.DoesSearchStringMatch(ev.Name, graphParams.startEvent))
|
|
{
|
|
frameRange.start = Math.Clamp(ev.Frame + graphParams.startEventOffset, 0, csvStats.SampleCount - 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (graphParams.endEvent != null)
|
|
{
|
|
foreach (CsvEvent ev in csvStats.Events)
|
|
{
|
|
if (ev.Frame >= frameRange.start && CsvStats.DoesSearchStringMatch(ev.Name, graphParams.endEvent))
|
|
{
|
|
frameRange.end = Math.Clamp(ev.Frame + graphParams.endEventOffset, 0, csvStats.SampleCount - 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return frameRange;
|
|
}
|
|
|
|
CsvStats ProcessCsvStats(CsvStats csvStatsIn, GraphParams graphParams, out FrameRange frameRange)
|
|
{
|
|
CsvStats csvStats = new CsvStats(csvStatsIn, graphParams.statNames.ToArray());
|
|
|
|
// Compute any derived stats
|
|
foreach (string statStr in graphParams.statNames)
|
|
{
|
|
if (CsvStats.IsDerivedStatExpression(statStr))
|
|
{
|
|
StatSamples derivedStat = csvStatsIn.MakeDerivedStatFromExpression(statStr);
|
|
if (derivedStat != null)
|
|
{
|
|
csvStats.AddStat(derivedStat);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (graphParams.discardLastFrame)
|
|
{
|
|
foreach (StatSamples stat in csvStats.Stats.Values.ToArray())
|
|
{
|
|
if (stat.samples.Count > 0)
|
|
{
|
|
stat.samples.RemoveAt(stat.samples.Count - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process the minFilterStat
|
|
float minFilterStatValue = graphParams.minFilterStatValue;
|
|
StatSamples minFilterStat = null;
|
|
if (graphParams.minFilterStatName != null && minFilterStatValue > -float.MaxValue)
|
|
{
|
|
minFilterStat = csvStats.GetStat(graphParams.minFilterStatName);
|
|
if (minFilterStat != null)
|
|
{
|
|
for (int i = 0; i < minFilterStat.samples.Count; ++i)
|
|
{
|
|
if (minFilterStat.samples[i] < minFilterStatValue)
|
|
{
|
|
minFilterStat.samples[i] = 0.0f;
|
|
}
|
|
}
|
|
minFilterStat.ComputeAverageAndTotal();
|
|
}
|
|
else
|
|
{
|
|
// Need a proper stat name for the min filter to work
|
|
minFilterStatValue = -float.MaxValue;
|
|
}
|
|
}
|
|
|
|
// Filter out stats which are ignored etc
|
|
List<StatSamples> FilteredStats = new List<StatSamples>();
|
|
foreach (StatSamples stat in csvStats.Stats.Values.ToArray())
|
|
{
|
|
if (IsStatIgnored(stat.Name, graphParams))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (minFilterStatValue > -float.MaxValue)
|
|
{
|
|
if (stat != minFilterStat)
|
|
{
|
|
// Reset other stats when the filter stat entry was below the threshold
|
|
for (int i = 0; i < stat.samples.Count; i++)
|
|
{
|
|
if (minFilterStat.samples[i] == 0.0f)
|
|
{
|
|
stat.samples[i] = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (graphParams.filterOutZeros)
|
|
{
|
|
float lastNonZero = 0.0f;
|
|
for (int i = 0; i < stat.samples.Count; i++)
|
|
{
|
|
if (stat.samples[i] == 0.0f)
|
|
{
|
|
stat.samples[i] = lastNonZero;
|
|
}
|
|
else
|
|
{
|
|
lastNonZero = stat.samples[i];
|
|
}
|
|
}
|
|
stat.ComputeAverageAndTotal();
|
|
}
|
|
|
|
if (graphParams.statMultiplier != 1.0f)
|
|
{
|
|
for (int i = 0; i < stat.samples.Count; i++)
|
|
{
|
|
stat.samples[i] *= graphParams.statMultiplier;
|
|
}
|
|
}
|
|
|
|
// Filter out stats where the average below averageThreshold
|
|
if (graphParams.averageThreshold > -float.MaxValue)
|
|
{
|
|
if (stat.average < graphParams.averageThreshold)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Filter out stats below the threshold
|
|
if (graphParams.threshold > -float.MaxValue)
|
|
{
|
|
bool aboveThreshold = false;
|
|
foreach (float val in stat.samples)
|
|
{
|
|
if (val > graphParams.threshold)
|
|
{
|
|
aboveThreshold = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!aboveThreshold)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// LLM specific, spaces in LLM seem to be $32$.
|
|
// This is a temp fix until LLM outputs without $32$.
|
|
stat.Name = stat.Name.Replace("$32$", " ");
|
|
// If we get here, the stat wasn't filtered
|
|
FilteredStats.Add(stat);
|
|
}
|
|
|
|
// Have any stats actually been filtered? If so, replace the list
|
|
if (FilteredStats.Count < csvStats.Stats.Count)
|
|
{
|
|
csvStats.Stats.Clear();
|
|
foreach (StatSamples stat in FilteredStats)
|
|
{
|
|
csvStats.AddStat(stat);
|
|
}
|
|
}
|
|
|
|
frameRange = GetEventTruncationFrameRange(csvStats, graphParams);
|
|
|
|
// Filter out events
|
|
List<CsvEvent> FilteredEvents = new List<CsvEvent>();
|
|
foreach (CsvEvent ev in csvStats.Events)
|
|
{
|
|
if (IsEventShown(ev.Name, graphParams))
|
|
{
|
|
FilteredEvents.Add(ev);
|
|
}
|
|
}
|
|
csvStats.Events = FilteredEvents;
|
|
|
|
return csvStats;
|
|
}
|
|
|
|
void RecomputeStatAveragesForRange(CsvStats stats, Range range)
|
|
{
|
|
foreach (StatSamples stat in stats.Stats.Values.ToArray())
|
|
{
|
|
int rangeStart = Math.Max(0, (int)range.MinX);
|
|
int rangeEnd = Math.Min(stat.samples.Count, (int)range.MaxX);
|
|
|
|
stat.ComputeAverageAndTotal(rangeStart, rangeEnd);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
} |