214 lines
8.6 KiB
C#
214 lines
8.6 KiB
C#
#nullable enable
|
|
using ImGuiNET;
|
|
using T3.Core.DataTypes.Vector;
|
|
using T3.Core.Stats;
|
|
using T3.Core.Utils;
|
|
using T3.Editor.Gui.Styling;
|
|
using T3.Editor.Gui.UiHelpers;
|
|
|
|
namespace T3.Editor.Gui.Windows.Analyze;
|
|
|
|
/// <summary>
|
|
/// Renders a <see cref="RollingMetric"/> as an overlaid histogram (bars in the background)
|
|
/// and plot line (samples over time in the foreground). Pure draw helpers — no layout logic,
|
|
/// no internal state. Caller is responsible for allocating rect space (e.g. via <c>ImGui.Dummy</c>).
|
|
/// </summary>
|
|
internal static class MetricGraphView
|
|
{
|
|
/// <summary>
|
|
/// Draws histogram + plot line into <paramref name="rect"/>.
|
|
/// </summary>
|
|
/// <param name="scratch">Reusable array of at least <c>metric.Capacity</c> floats, owned by the caller.</param>
|
|
/// <param name="plotMaxY">Upper bound of the plot-line Y axis. Samples above this clamp to the top.</param>
|
|
/// <param name="flashDurationSec">Histogram bars flash toward <paramref name="flashColor"/> for this long after being incremented.</param>
|
|
/// <summary>
|
|
/// Draws plot line + histogram. Returns the hovered histogram bucket index, or -1 when the
|
|
/// mouse is not over any bucket. Caller uses the return value to render a per-bucket tooltip.
|
|
/// </summary>
|
|
public static int DrawGraph(ImDrawListPtr drawList, ImRect rect,
|
|
RollingMetric metric, float[] scratch,
|
|
Color barColor, Color flashColor, Color lineColor,
|
|
double now, float plotMaxY,
|
|
string? axisLeftLabel = null, string? axisMidLabel = null, string? axisRightLabel = null,
|
|
float flashDurationSec = 0.25f)
|
|
{
|
|
if (metric.Count == 0)
|
|
return -1;
|
|
|
|
var size = rect.GetSize();
|
|
if (size.X <= 0 || size.Y <= 0)
|
|
return -1;
|
|
|
|
// Layout: plot line (top), histogram (middle), mean-indicator triangle, optional axis labels (bottom).
|
|
const float meanIndicatorHeight = 5f;
|
|
var hasAxisLabels = axisLeftLabel != null || axisMidLabel != null || axisRightLabel != null;
|
|
float axisLabelHeight = 0f;
|
|
if (hasAxisLabels)
|
|
{
|
|
ImGui.PushFont(Fonts.FontSmall);
|
|
axisLabelHeight = ImGui.GetTextLineHeight() + 1f;
|
|
ImGui.PopFont();
|
|
}
|
|
|
|
var workingHeight = size.Y - meanIndicatorHeight - axisLabelHeight;
|
|
if (workingHeight < 6f)
|
|
return -1;
|
|
|
|
var midY = rect.Min.Y + workingHeight * 0.5f;
|
|
var bottomY = rect.Min.Y + workingHeight;
|
|
var plotRect = new ImRect(rect.Min, new Vector2(rect.Max.X, midY));
|
|
var histoRect = new ImRect(new Vector2(rect.Min.X, midY), new Vector2(rect.Max.X, bottomY));
|
|
|
|
// Hover detection runs before the draw so the histogram can highlight the selected range.
|
|
var hoverRect = new ImRect(histoRect.Min, new Vector2(histoRect.Max.X, rect.Max.Y));
|
|
var hoveredBucket = -1;
|
|
if (ImGui.IsMouseHoveringRect(hoverRect.Min, hoverRect.Max))
|
|
{
|
|
var histoBarWidth = histoRect.GetWidth() / metric.BucketCount;
|
|
if (histoBarWidth > 0)
|
|
{
|
|
var idx = (int)((ImGui.GetMousePos().X - histoRect.Min.X) / histoBarWidth);
|
|
if (idx >= 0 && idx < metric.BucketCount)
|
|
hoveredBucket = idx;
|
|
}
|
|
}
|
|
|
|
// Holding Shift while hovering enables cumulative selection (this bucket and everything right of it).
|
|
var highlightFromBucket = hoveredBucket >= 0 && ImGui.GetIO().KeyShift ? hoveredBucket : -1;
|
|
|
|
DrawPlotLine(drawList, plotRect, metric, scratch, lineColor, plotMaxY);
|
|
DrawHistogram(drawList, histoRect, metric, now, barColor, flashColor, lineColor, flashDurationSec, highlightFromBucket);
|
|
|
|
if (hasAxisLabels)
|
|
{
|
|
ImGui.PushFont(Fonts.FontSmall);
|
|
var labelY = bottomY + meanIndicatorHeight + 1f;
|
|
var labelColor = (uint)UiColors.BackgroundFull.Fade(0.8f);
|
|
|
|
if (axisLeftLabel != null)
|
|
drawList.AddText(new Vector2(rect.Min.X, labelY), labelColor, axisLeftLabel);
|
|
|
|
if (axisMidLabel != null)
|
|
{
|
|
var midLabelSize = ImGui.CalcTextSize(axisMidLabel);
|
|
drawList.AddText(new Vector2(rect.GetCenter().X - midLabelSize.X * 0.5f, labelY), labelColor, axisMidLabel);
|
|
}
|
|
|
|
if (axisRightLabel != null)
|
|
{
|
|
var rightLabelSize = ImGui.CalcTextSize(axisRightLabel);
|
|
drawList.AddText(new Vector2(rect.Max.X - rightLabelSize.X, labelY), labelColor, axisRightLabel);
|
|
}
|
|
ImGui.PopFont();
|
|
}
|
|
|
|
return hoveredBucket;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Histogram bars + mean-indicator triangle just below the baseline. Bar heights use a log curve
|
|
/// so tall bars don't dominate; empty buckets render as a single transparent line.
|
|
/// When <paramref name="highlightFromBucket"/> is non-negative, buckets at or right of that index
|
|
/// are tinted toward <see cref="UiColors.StatusActivated"/> — used for the Shift-cumulative selection.
|
|
/// </summary>
|
|
private static void DrawHistogram(ImDrawListPtr drawList, ImRect rect, RollingMetric metric, double now,
|
|
Color barColor, Color flashColor, Color meanColor, float flashDurationSec = 1,
|
|
int highlightFromBucket = -1)
|
|
{
|
|
var slots = metric.Slots;
|
|
var bucketCount = slots.Length;
|
|
if (bucketCount == 0)
|
|
return;
|
|
|
|
var size = rect.GetSize();
|
|
if (size.X <= 0 || size.Y <= 0)
|
|
return;
|
|
|
|
var barWidth = size.X / bucketCount;
|
|
var heightScale = size.Y / LogHeight(metric.Capacity); // full window height == LogHeight(capacity)
|
|
var y1 = rect.Max.Y;
|
|
|
|
// Baseline
|
|
drawList.AddLine(new Vector2(rect.Min.X, y1 - 0.5f),
|
|
new Vector2(rect.Max.X, y1 +1),
|
|
UiColors.BackgroundFull.Fade(0.5f));
|
|
|
|
|
|
for (var i = 0; i < bucketCount; i++)
|
|
{
|
|
var slot = slots[i];
|
|
if (slot.CountRecent == 0)
|
|
continue;
|
|
|
|
var x0 = rect.Min.X + i * barWidth;
|
|
var x1 = rect.Min.X + (i + 1) * barWidth - 2f;
|
|
var h = (LogHeight(slot.CountRecent) * heightScale).ClampMin(1);
|
|
var fadeFactor = (float)((now - slot.LastIncrementTime) / flashDurationSec).Clamp(0, 1);
|
|
var color = Color.Mix(flashColor, barColor, fadeFactor);
|
|
|
|
if (highlightFromBucket >= 0 && i >= highlightFromBucket)
|
|
color = Color.Mix(color, UiColors.StatusActivated, 0.6f);
|
|
|
|
drawList.AddRectFilled(new Vector2(x0, y1 - h), new Vector2(x1, y1), color);
|
|
}
|
|
|
|
|
|
// Mean indicator: small triangle below the histogram, pointing up at the bucket containing the mean.
|
|
var mean = metric.Average;
|
|
var meanBucket = 0;
|
|
for (var i = 0; i < bucketCount; i++)
|
|
{
|
|
if (slots[i].ValueRangeMin <= mean)
|
|
meanBucket = i;
|
|
else
|
|
break;
|
|
}
|
|
var meanX = rect.Min.X + (meanBucket + 0.5f) * barWidth;
|
|
var tipY = y1 + 1f;
|
|
drawList.AddTriangleFilled(
|
|
new Vector2(meanX, tipY),
|
|
new Vector2(meanX + 3f, tipY + 4f),
|
|
new Vector2(meanX - 3f, tipY + 4f),
|
|
meanColor);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Plot-line only. Samples are laid out left-to-right in chronological order.
|
|
/// </summary>
|
|
public static void DrawPlotLine(ImDrawListPtr drawList, ImRect rect, RollingMetric metric, float[] scratch,
|
|
Color lineColor, float plotMaxY)
|
|
{
|
|
var samples = metric.AsOrderedSpan(scratch);
|
|
var count = samples.Length;
|
|
if (count < 2)
|
|
return;
|
|
|
|
var size = rect.GetSize();
|
|
if (size.X <= 0 || size.Y <= 0)
|
|
return;
|
|
|
|
if (plotMaxY <= 0f)
|
|
return;
|
|
|
|
var stepX = size.X / (count - 1);
|
|
var baseY = rect.Max.Y;
|
|
var height = size.Y;
|
|
uint color = lineColor;
|
|
|
|
var prev = new Vector2(rect.Min.X, baseY - Math.Clamp(samples[0] / plotMaxY, 0f, 1f) * height);
|
|
for (var i = 1; i < count; i++)
|
|
{
|
|
var x = rect.Min.X + i * stepX;
|
|
var y = baseY - Math.Clamp(samples[i] / plotMaxY, 0f, 1f) * height - 3;
|
|
var next = new Vector2(x, y);
|
|
drawList.AddLine(prev, next, color);
|
|
prev = next;
|
|
}
|
|
|
|
// Marker at the latest sample.
|
|
drawList.AddCircleFilled(prev, 2f, color);
|
|
}
|
|
|
|
/// <summary>Exponentially-compressed bar-height mapping. count=0 → 0, grows as log10(1+count).</summary>
|
|
private static float LogHeight(int count) => count <= 0 ? 0f : MathF.Log10(1 + count) * 15f;
|
|
} |