using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using HC_APTBS.Models;
namespace HC_APTBS.Services.Impl
{
///
/// Generates SVG chart markup for embedding in QuestPDF reports via the
/// .Svg() API. All rendering is done as raw SVG strings — no
/// SkiaSharp dependency required at PDF-generation time.
///
internal static class ReportChartRenderer
{
// ── Layout constants (SVG user-units, mapped to points in the PDF) ────
private const float LeftMargin = 48f;
private const float RightMargin = 12f;
private const float TopMargin = 22f;
private const float BottomMargin = 24f;
private const float LegendHeight = 14f;
// ── Public API ────────────────────────────────────────────────────────
///
/// Produces an SVG string depicting a line chart of individual
/// measurement samples with a horizontal target line, shaded tolerance
/// band, average line, and axis labels.
///
/// Chart width in points.
/// Chart height in points.
/// Individual timestamped measurements.
/// Expected / setpoint value.
/// Acceptable deviation (±).
/// Computed average of all samples.
/// Whether the parameter passed the tolerance check.
/// Chart title label (e.g. "1000 RPM 40°C — QDelivery").
/// A complete SVG document string.
public static string RenderMeasurementChart(
float width,
float height,
IReadOnlyList samples,
double targetValue,
double tolerance,
double average,
bool passed,
string label)
{
if (samples.Count < 2)
return RenderInsufficientData(width, height, label);
var sb = new StringBuilder();
sb.AppendLine(SvgHeader(width, height));
// Plot area bounds.
float plotLeft = LeftMargin;
float plotRight = width - RightMargin;
float plotTop = TopMargin + LegendHeight;
float plotBottom = height - BottomMargin;
float plotW = plotRight - plotLeft;
float plotH = plotBottom - plotTop;
// Y-axis range: encompass all samples and the tolerance band, with padding.
double minVal = samples.Min(s => s.Value);
double maxVal = samples.Max(s => s.Value);
double bandLo = targetValue - tolerance;
double bandHi = targetValue + tolerance;
minVal = Math.Min(minVal, bandLo);
maxVal = Math.Max(maxVal, bandHi);
double range = maxVal - minVal;
if (range < 0.001) range = 1.0;
double padding = range * 0.12;
double yMin = minVal - padding;
double yMax = maxVal + padding;
double yRange = yMax - yMin;
// ── Background & border ──────────────────────────────────────────
sb.AppendLine(Rect(0, 0, width, height, "#FFFFFF", null, 0));
sb.AppendLine(Rect(plotLeft, plotTop, plotW, plotH, null, Col(ReportTheme.ChartGrid), 1));
// ── Grid lines ───────────────────────────────────────────────────
const int gridCount = 5;
double gridStep = yRange / gridCount;
for (int i = 0; i <= gridCount; i++)
{
double val = yMin + gridStep * i;
float y = ValueToY(val, plotTop, plotH, yMin, yRange);
sb.AppendLine(Line(plotLeft, y, plotRight, y, Col(ReportTheme.ChartGrid), 0.5f, null));
string text = val.ToString("F1", CultureInfo.InvariantCulture);
sb.AppendLine(Text(plotLeft - 4f, y + 3f, text, Col(ReportTheme.AxisLabel), 7f, "end"));
}
// ── Tolerance band ───────────────────────────────────────────────
float bandTopY = Math.Max(ValueToY(bandHi, plotTop, plotH, yMin, yRange), plotTop);
float bandBotY = Math.Min(ValueToY(bandLo, plotTop, plotH, yMin, yRange), plotBottom);
sb.AppendLine(Rect(plotLeft, bandTopY, plotW, bandBotY - bandTopY,
ColAlpha(ReportTheme.ToleranceBand), null, 0));
// ── Target line (dashed amber) ───────────────────────────────────
float targetY = ValueToY(targetValue, plotTop, plotH, yMin, yRange);
sb.AppendLine(Line(plotLeft, targetY, plotRight, targetY,
Col(ReportTheme.TargetLine), 1.5f, "6,3"));
// ── Average line (dashed green/red) ──────────────────────────────
float avgY = ValueToY(average, plotTop, plotH, yMin, yRange);
string avgCol = passed ? Col(ReportTheme.AvgPassLine) : Col(ReportTheme.AvgFailLine);
sb.AppendLine(Line(plotLeft, avgY, plotRight, avgY, avgCol, 1.2f, "4,3"));
// ── Data polyline ────────────────────────────────────────────────
var points = new StringBuilder();
for (int i = 0; i < samples.Count; i++)
{
float x = plotLeft + (plotW * i / (samples.Count - 1));
float y = ValueToY(samples[i].Value, plotTop, plotH, yMin, yRange);
if (i > 0) points.Append(' ');
points.Append(F(x)).Append(',').Append(F(y));
}
sb.Append($"");
// ── Sample dots (only if ≤ 60 samples) ──────────────────────────
if (samples.Count <= 60)
{
for (int i = 0; i < samples.Count; i++)
{
float x = plotLeft + (plotW * i / (samples.Count - 1));
float y = ValueToY(samples[i].Value, plotTop, plotH, yMin, yRange);
sb.AppendLine($"");
}
}
// ── X-axis labels ────────────────────────────────────────────────
int xLabelStep = Math.Max(1, samples.Count / 8);
for (int i = 0; i < samples.Count; i += xLabelStep)
{
float x = plotLeft + (plotW * i / (samples.Count - 1));
sb.AppendLine(Text(x, plotBottom + 12f, (i + 1).ToString(),
Col(ReportTheme.AxisLabel), 7f, "middle"));
}
// ── Title label (top-left) ───────────────────────────────────────
sb.AppendLine(Text(plotLeft, TopMargin, EscapeXml(label), "#1B2A4A", 9f, "start"));
// ── Legend (top-right) ────────────────────────────────────────────
string targetText = $"Target: {targetValue.ToString("F2", CultureInfo.InvariantCulture)}";
string avgText = $"Avg: {average.ToString("F2", CultureInfo.InvariantCulture)}";
sb.AppendLine(Text(plotRight - 80f, TopMargin, targetText,
Col(ReportTheme.TargetLine), 7f, "start"));
sb.AppendLine(Text(plotRight, TopMargin, avgText, avgCol, 7f, "end"));
sb.AppendLine("");
return sb.ToString();
}
///
/// Produces an SVG string depicting a large PASS/FAIL verdict badge.
///
public static string RenderVerdictBadge(float width, float height, bool passed)
{
var sb = new StringBuilder();
sb.AppendLine(SvgHeader(width, height));
string bgColor = passed ? "#2E7D32" : "#C62828";
sb.AppendLine($"");
string text = passed ? "PASS" : "FAIL";
float fontSize = height * 0.45f;
sb.AppendLine($"{text}");
sb.AppendLine("");
return sb.ToString();
}
// ── Private helpers ───────────────────────────────────────────────────
/// Maps a data value to the Y coordinate (inverted — higher values = lower Y).
private static float ValueToY(double value, float plotTop, float plotH, double yMin, double yRange)
=> plotTop + (float)((1.0 - (value - yMin) / yRange) * plotH);
/// Renders a placeholder SVG when fewer than 2 samples are available.
private static string RenderInsufficientData(float width, float height, string label)
{
var sb = new StringBuilder();
sb.AppendLine(SvgHeader(width, height));
sb.AppendLine(Rect(0, 0, width, height, "#F5F7FA", Col(ReportTheme.ChartGrid), 1));
sb.AppendLine(Text(12f, 18f, EscapeXml(label), "#1B2A4A", 9f, "start"));
sb.AppendLine(Text(width / 2, height / 2 + 4f,
"Insufficient sample data for chart", Col(ReportTheme.AxisLabel), 10f, "middle"));
sb.AppendLine("");
return sb.ToString();
}
// ── SVG primitives ────────────────────────────────────────────────────
/// SVG document header.
private static string SvgHeader(float w, float h)
=> $"