fix: null-safe string reader, chart V2 label collision avoidance
- ChartDataServiceV2: aggiunto helper ToStr() per GetString null-safe su colonne nullable (Sottostante, NomeCFT); pattern analogo a ToDecimal - SkiaChartRendererV2: collision avoidance label margine destro — tutti i label (barriere, strike, autocall, end-label serie) raccolti in lista, ordinati per Y e distribuiti con spacing minimo 13px (push-down + clamp-up) prima del disegno - CLAUDE.md: documentati i due fix e la root cause cedlab_Chart_UL1 divide-by-zero su Strike=0 (fix DB: NULLIF(cu.Strike,0)) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,12 +60,12 @@ public class ChartDataServiceV2 : IChartDataServiceV2
|
||||
BarrieraCoupon = ToDecimal(r, "BarrieraCoupon"),
|
||||
BarrieraCapitalePerc = ToDecimal(r, "BarrieraCapitalePerc"),
|
||||
BarrieraCapitale = ToDecimal(r, "BarrieraCapitale"),
|
||||
Sottostante = r.GetString(r.GetOrdinal("Sottostante")),
|
||||
Sottostante = ToStr(r, "Sottostante"),
|
||||
IsWorstOf = r.GetInt32(r.GetOrdinal("IsWorstOf")),
|
||||
PriceWorst = ToDecimal(r, "PriceWorst"),
|
||||
PriceWorstPerc = ToDecimal(r, "PriceWorstPerc"),
|
||||
NumPrezziCFT = r.GetInt32(r.GetOrdinal("NumPrezziCFT")),
|
||||
NomeCFT = r.GetString(r.GetOrdinal("NomeCFT")),
|
||||
NomeCFT = ToStr(r, "NomeCFT"),
|
||||
TriggerAutocallPerc = ToDecimal(r, "TriggerAutocallPerc"),
|
||||
AutocallValue = ToDecimal(r, "AutocallValue"),
|
||||
});
|
||||
@@ -134,4 +134,10 @@ public class ChartDataServiceV2 : IChartDataServiceV2
|
||||
if (r.IsDBNull(ord)) return 0m;
|
||||
return Convert.ToDecimal(r.GetValue(ord));
|
||||
}
|
||||
|
||||
private static string ToStr(SqlDataReader r, string column)
|
||||
{
|
||||
int ord = r.GetOrdinal(column);
|
||||
return r.IsDBNull(ord) ? string.Empty : r.GetString(ord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,12 +79,13 @@ public static class SkiaChartRendererV2
|
||||
|
||||
// ── Linee costanti con label ───────────────────────────────────
|
||||
var constLegend = new List<(string name, SKColor color, bool dashed, float thickness)>();
|
||||
var rightLabels = new List<(float Y, string Text, SKColor Color)>();
|
||||
|
||||
string bcLabel = data.GlobalMeta.BarrieraCouponPerc == data.GlobalMeta.BarrieraCapitalePerc
|
||||
? $"Barriera {data.GlobalMeta.BarrieraCapitalePerc:0}% ({data.GlobalMeta.BarrieraCapitale:0.00})"
|
||||
: $"Barriera Capitale {data.GlobalMeta.BarrieraCapitalePerc:0}% ({data.GlobalMeta.BarrieraCapitale:0.00})";
|
||||
DrawHorizontalLineWithLabel(canvas, plotArea, minY, maxY,
|
||||
(float)data.GlobalMeta.BarrieraCapitalePerc, CapitaleColor, 1.5f, false, bcLabel);
|
||||
(float)data.GlobalMeta.BarrieraCapitalePerc, CapitaleColor, 1.5f, false, bcLabel, rightLabels);
|
||||
constLegend.Add((bcLabel, CapitaleColor, false, 1.5f));
|
||||
|
||||
if (data.GlobalMeta.BarrieraCouponPerc != data.GlobalMeta.BarrieraCapitalePerc
|
||||
@@ -92,13 +93,13 @@ public static class SkiaChartRendererV2
|
||||
{
|
||||
string bkLabel = $"Barriera Coupon {data.GlobalMeta.BarrieraCouponPerc:0}% ({data.GlobalMeta.BarrieraCoupon:0.00})";
|
||||
DrawHorizontalLineWithLabel(canvas, plotArea, minY, maxY,
|
||||
(float)data.GlobalMeta.BarrieraCouponPerc, CouponColor, 1.5f, false, bkLabel);
|
||||
(float)data.GlobalMeta.BarrieraCouponPerc, CouponColor, 1.5f, false, bkLabel, rightLabels);
|
||||
constLegend.Add((bkLabel, CouponColor, false, 1.5f));
|
||||
}
|
||||
|
||||
string strikeLabel = $"Strike 100% ({data.GlobalMeta.Strike:0.00})";
|
||||
DrawHorizontalLineWithLabel(canvas, plotArea, minY, maxY,
|
||||
100f, StrikeColor, 1.5f, false, strikeLabel);
|
||||
100f, StrikeColor, 1.5f, false, strikeLabel, rightLabels);
|
||||
constLegend.Add((strikeLabel, StrikeColor, false, 1.5f));
|
||||
|
||||
bool showAutocall = data.GlobalMeta.TriggerAutocallPerc != 0
|
||||
@@ -109,7 +110,7 @@ public static class SkiaChartRendererV2
|
||||
{
|
||||
string taLabel = $"Trigger Autocall {data.GlobalMeta.TriggerAutocallPerc:0}% ({data.GlobalMeta.AutocallValue:0.00})";
|
||||
DrawHorizontalLineWithLabel(canvas, plotArea, minY, maxY,
|
||||
(float)data.GlobalMeta.TriggerAutocallPerc, AutocallColor, 1.5f, false, taLabel);
|
||||
(float)data.GlobalMeta.TriggerAutocallPerc, AutocallColor, 1.5f, false, taLabel, rightLabels);
|
||||
constLegend.Add((taLabel, AutocallColor, false, 1.5f));
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ public static class SkiaChartRendererV2
|
||||
{
|
||||
string pwLabel = $"{worstOf.Sottostante} ({worstOf.PriceWorst:0.00})";
|
||||
DrawHorizontalLineWithLabel(canvas, plotArea, minY, maxY,
|
||||
(float)worstOf.PriceWorstPerc, PrezzoWorstColor, 1f, true, pwLabel);
|
||||
(float)worstOf.PriceWorstPerc, PrezzoWorstColor, 1f, true, pwLabel, rightLabels);
|
||||
}
|
||||
|
||||
// ── Serie ──────────────────────────────────────────────────────
|
||||
@@ -159,10 +160,12 @@ public static class SkiaChartRendererV2
|
||||
}
|
||||
|
||||
DrawSeriesV2(canvas, plotArea, ulPoints, minDate, maxDate, minY, maxY, color, thickness);
|
||||
DrawSeriesEndLabel(canvas, plotArea, ulPoints, minDate, maxDate, minY, maxY, color, endLabel);
|
||||
DrawSeriesEndLabel(plotArea, ulPoints, minDate, maxDate, minY, maxY, color, endLabel, rightLabels);
|
||||
seriesLegend.Add((seriesLabel, color, false, thickness));
|
||||
}
|
||||
|
||||
DrawRightLabels(canvas, plotArea, rightLabels);
|
||||
|
||||
// Bordo area plot
|
||||
using var borderPaint = new SKPaint
|
||||
{
|
||||
@@ -372,19 +375,17 @@ public static class SkiaChartRendererV2
|
||||
// Label al termine di una serie (estremo destro)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private static void DrawSeriesEndLabel(SKCanvas canvas, SKRect area,
|
||||
private static void DrawSeriesEndLabel(SKRect area,
|
||||
List<ChartSeriesPoint> points,
|
||||
DateTime minDate, DateTime maxDate, double minY, double maxY,
|
||||
SKColor color, string label)
|
||||
SKColor color, string label,
|
||||
List<(float Y, string Text, SKColor Color)> rightLabels)
|
||||
{
|
||||
if (points.Count == 0) return;
|
||||
var last = points.Last();
|
||||
float y = ValueToY((double)last.Performance, area, minY, maxY);
|
||||
y = Math.Max(area.Top, Math.Min(area.Bottom, y));
|
||||
|
||||
var font = CreateFont(9.5f);
|
||||
using var textPaint = new SKPaint { Color = color, IsAntialias = true };
|
||||
canvas.DrawText(label, area.Right + 5, y + 4, SKTextAlign.Left, font, textPaint);
|
||||
rightLabels.Add((y + 4, label, color));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -393,7 +394,8 @@ public static class SkiaChartRendererV2
|
||||
|
||||
private static void DrawHorizontalLineWithLabel(SKCanvas canvas, SKRect area,
|
||||
double minY, double maxY,
|
||||
float value, SKColor color, float thickness, bool dashed, string label)
|
||||
float value, SKColor color, float thickness, bool dashed, string label,
|
||||
List<(float Y, string Text, SKColor Color)> rightLabels)
|
||||
{
|
||||
float y = ValueToY(value, area, minY, maxY);
|
||||
if (y < area.Top || y > area.Bottom) return;
|
||||
@@ -407,10 +409,42 @@ public static class SkiaChartRendererV2
|
||||
linePaint.PathEffect = SKPathEffect.CreateDash(new[] { 8f, 4f }, 0);
|
||||
|
||||
canvas.DrawLine(area.Left, y, area.Right, y, linePaint);
|
||||
rightLabels.Add((y + 4, label, color));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Label destra — collision avoidance
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private static void DrawRightLabels(SKCanvas canvas, SKRect area,
|
||||
List<(float Y, string Text, SKColor Color)> labels)
|
||||
{
|
||||
if (labels.Count == 0) return;
|
||||
|
||||
const float minSpacing = 13f;
|
||||
|
||||
var items = labels.OrderBy(l => l.Y).ToList();
|
||||
float[] ys = items.Select(l => l.Y).ToArray();
|
||||
|
||||
// Passata discendente: sposta in basso le label troppo vicine
|
||||
for (int i = 1; i < ys.Length; i++)
|
||||
if (ys[i] < ys[i - 1] + minSpacing)
|
||||
ys[i] = ys[i - 1] + minSpacing;
|
||||
|
||||
// Passata risalente: se qualcuna è finita sotto il bordo, riporta su
|
||||
float maxAllowed = area.Bottom + 4f;
|
||||
for (int i = ys.Length - 1; i >= 0; i--)
|
||||
{
|
||||
if (ys[i] > maxAllowed) ys[i] = maxAllowed;
|
||||
maxAllowed = ys[i] - minSpacing;
|
||||
}
|
||||
|
||||
var font = CreateFont(9.5f);
|
||||
using var textPaint = new SKPaint { Color = color, IsAntialias = true };
|
||||
canvas.DrawText(label, area.Right + 5, y + 4, SKTextAlign.Left, font, textPaint);
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
using var textPaint = new SKPaint { Color = items[i].Color, IsAntialias = true };
|
||||
canvas.DrawText(items[i].Text, area.Right + 5, ys[i], SKTextAlign.Left, font, textPaint);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user