Generic types are great, but it has traditionally been difficult to do math with them. Consider the simple task where you want to accept a generic array and return its sum. With .NET 6 (and features currently still in preview), this got much easier!
publicstatic T Sum<T>(T[] values) where T : INumber<T>
{
T sum = T.Zero;
for (int i = 0; i < values.Length; i++)
sum += values[i];
return sum;
}
Note that the generic math function above is equivalent in speed to one that accepts and returns double[], while a method which accepts a generic but calls Convert.ToDouble() every time is about 3x slower than both options:
// this code works on older versions of .NET but is about 3x slowerpublicstaticdouble SumGenericToDouble<T>(T[] values)
{
double sum = 0;
for (int i = 0; i < values.Length; i++)
sum += Convert.ToDouble(values[i]);
return sum;
}
I recently had the need to determine if a point is inside a rotated rectangle. This need arose when I wanted to make a rotated rectangular textbox draggable, but I wanted to determine if the mouse was over the rectangle. I know the rectangle’s location, size, and rotation, and the position of the mouse cursor, and my goal is to tell if the mouse is inside the rotated rectangle. In this example I’ll use Maui.Graphics to render a test image in a Windows Forms application (with SkiaSharp and OpenGL), but the same could be achieved with System.Drawing or other similar 2D graphics libraries.
I started just knowing the width and height of my rectangle. I created an array of points representing its corners.
To determine if a given point is inside the rotated rectangle I called this method which accepts the point of interest and an array containing the four corners of the rotated rectangle.
In practice you’ll probably want to use a more intelligent data structure than a 4-element Pointf[] when calling these functions.
The points in the array are clockwise, but I assume this method will work regardless of the order of the points in the array.
At the very end of IsPointInsideRectangle() the final decision is made based on a distance being less than a given value. It’s true that the cursor will be inside the rectangle if the distance is exactly zero, but with the possible accumulation of floating-point math errors this seemed like a safer option.
I recently had the need to create a smoothed curve from a series of X/Y data points in a C# application. I achieved this using cubic spline interpolation. I prefer this strategy because I can control the exact number of points in the output curve, and the generated curve (given sufficient points) will pass through the original data making it excellent for data smoothing applications.
The code below is an adaptation of original work by Ryan Seghers (links below) that I modified to narrow its scope, support double types, use modern language features, and operate statelessly in a functional style with all static methods.
It targets .NET Standard 2.0 so it can be used in .NET Framework and .NET Core applications.
Input Xs and Ys must be the same length but do not need to be ordered.
The interpolated curve may have any number of points (not just even multiples of the input length), and may even have fewer points than the original data.
Users cannot define start or end slopes so the curve generated is a natural spline.
publicstaticclassCubic{
/// <summary>/// Generate a smooth (interpolated) curve that follows the path of the given X/Y points/// </summary>publicstatic (double[] xs, double[] ys) InterpolateXY(double[] xs, double[] ys, int count)
{
if (xs isnull || ys isnull || xs.Length != ys.Length)
thrownew ArgumentException($"{nameof(xs)} and {nameof(ys)} must have same length");
int inputPointCount = xs.Length;
double[] inputDistances = newdouble[inputPointCount];
for (int i = 1; i < inputPointCount; i++)
{
double dx = xs[i] - xs[i - 1];
double dy = ys[i] - ys[i - 1];
double distance = Math.Sqrt(dx * dx + dy * dy);
inputDistances[i] = inputDistances[i - 1] + distance;
}
double meanDistance = inputDistances.Last() / (count - 1);
double[] evenDistances = Enumerable.Range(0, count).Select(x => x * meanDistance).ToArray();
double[] xsOut = Interpolate(inputDistances, xs, evenDistances);
double[] ysOut = Interpolate(inputDistances, ys, evenDistances);
return (xsOut, ysOut);
}
privatestaticdouble[] Interpolate(double[] xOrig, double[] yOrig, double[] xInterp)
{
(double[] a, double[] b) = FitMatrix(xOrig, yOrig);
double[] yInterp = newdouble[xInterp.Length];
for (int i = 0; i < yInterp.Length; i++)
{
int j;
for (j = 0; j < xOrig.Length - 2; j++)
if (xInterp[i] <= xOrig[j + 1])
break;
double dx = xOrig[j + 1] - xOrig[j];
double t = (xInterp[i] - xOrig[j]) / dx;
double y = (1 - t) * yOrig[j] + t * yOrig[j + 1] +
t * (1 - t) * (a[j] * (1 - t) + b[j] * t);
yInterp[i] = y;
}
return yInterp;
}
privatestatic (double[] a, double[] b) FitMatrix(double[] x, double[] y)
{
int n = x.Length;
double[] a = newdouble[n - 1];
double[] b = newdouble[n - 1];
double[] r = newdouble[n];
double[] A = newdouble[n];
double[] B = newdouble[n];
double[] C = newdouble[n];
double dx1, dx2, dy1, dy2;
dx1 = x[1] - x[0];
C[0] = 1.0f / dx1;
B[0] = 2.0f * C[0];
r[0] = 3 * (y[1] - y[0]) / (dx1 * dx1);
for (int i = 1; i < n - 1; i++)
{
dx1 = x[i] - x[i - 1];
dx2 = x[i + 1] - x[i];
A[i] = 1.0f / dx1;
C[i] = 1.0f / dx2;
B[i] = 2.0f * (A[i] + C[i]);
dy1 = y[i] - y[i - 1];
dy2 = y[i + 1] - y[i];
r[i] = 3 * (dy1 / (dx1 * dx1) + dy2 / (dx2 * dx2));
}
dx1 = x[n - 1] - x[n - 2];
dy1 = y[n - 1] - y[n - 2];
A[n - 1] = 1.0f / dx1;
B[n - 1] = 2.0f * A[n - 1];
r[n - 1] = 3 * (dy1 / (dx1 * dx1));
double[] cPrime = newdouble[n];
cPrime[0] = C[0] / B[0];
for (int i = 1; i < n; i++)
cPrime[i] = C[i] / (B[i] - cPrime[i - 1] * A[i]);
double[] dPrime = newdouble[n];
dPrime[0] = r[0] / B[0];
for (int i = 1; i < n; i++)
dPrime[i] = (r[i] - dPrime[i - 1] * A[i]) / (B[i] - cPrime[i - 1] * A[i]);
double[] k = newdouble[n];
k[n - 1] = dPrime[n - 1];
for (int i = n - 2; i >= 0; i--)
k[i] = dPrime[i] - cPrime[i] * k[i + 1];
for (int i = 1; i < n; i++)
{
dx1 = x[i] - x[i - 1];
dy1 = y[i] - y[i - 1];
a[i - 1] = k[i - 1] * dx1 - dy1;
b[i - 1] = -k[i] * dx1 + dy1;
}
return (a, b);
}
}
This sample .NET 6 console application uses the class above to create a smoothed (interpolated) curve from a set of random X/Y points. It then plots the original data and the interpolated curve using ScottPlot.
// generate sample data using a random walkRandom rand = new(1268);
int pointCount = 20;
double[] xs1 = newdouble[pointCount];
double[] ys1 = newdouble[pointCount];
for (int i = 1; i < pointCount; i++)
{
xs1[i] = xs1[i - 1] + rand.NextDouble() - .5;
ys1[i] = ys1[i - 1] + rand.NextDouble() - .5;
}
// Use cubic interpolation to smooth the original data(double[] xs2, double[] ys2) = Cubic.InterpolateXY(xs1, ys1, 200);
// Plot the original vs. interpolated datavar plt = new ScottPlot.Plot(600, 400);
plt.AddScatter(xs1, ys1, label: "original", markerSize: 7);
plt.AddScatter(xs2, ys2, label: "interpolated", markerSize: 3);
plt.Legend();
plt.SaveFig("interpolation.png");
There are many different methods that can smooth data. Common methods include Bézier splines, Catmull-Rom splines, corner-cutting Chaikin curves, and Cubic splines. I recently implemented these strageies to include with ScottPlot (a MIT-licensed 2D plotting library for .NET). Visit ScottPlot.net to find the source code for that project and search for the Interpolation namespace.
A set of X/Y points can be interpolated such that they are evenly spaced on the X axis. This 1D interpolation can be used to change the sampling rate of time series data. For more information about 1D interpolation see my other blog post: Resample Time Series Data using Cubic Spline Interpolation
Many project websites and readmes have status badges that display build status, project details, or code metrics. badgen.net and shields.io are popular services for dynamically generating status badges as SVG files using HTTP requests. This article demonstrates how I use C# and Microsoft.Maui.Graphics to build status badges from NDepend static analysis reports. Source code for this project is available on GitHub.
NDepend can analyze a code base at different points in time and display code metric trends. See NDepend: Trend Monitoring for a full description. These metrics are stored in an XML file available in the HTML build folder.
The XML file contains many Root/MetricIndex/Metric elements that describe each metric and its units. This can be parsed to obtain the Name and Unit for each metric.
<Root><MetricIndex><MetricName="# New Issues since Baseline"Unit="issues"/><MetricName="# Issues Fixed since Baseline"Unit="issues"/><MetricName="# Issues Worsened since Baseline"Unit="issues"/><MetricName="# Issues with severity Blocker"Unit="issues"/><MetricName="# Issues with severity Critical"Unit="issues"/><MetricName="# Issues with severity High"Unit="issues"/><MetricName="# Issues with severity Medium"Unit="issues"/> ...
</MetricIndex></Root>
The XML file contains multiple Root/M/R elements that contain the value of each metric at a distinct time point. Numerical metrics have been converted to strings separated by the | character. Metric values for each time point are in the same order as the metric index.
I’ve already written how to make status badges with C# and Maui.Graphics, but that strategy only generates PNG files. For this project I also chose to generate SVG files. Rather than discuss that in detail, I’ll just show to the source code for an example SVG file.
It is important to note that in order to know the image width I must measure the string width. In HTML environments this could be done with vanilla Javascript, but in a C# environment I reached for Microsoft.Maui.Graphics (see how to MeasureString() with Maui.Graphics).
<svgxmlns='http://www.w3.org/2000/svg'xmlns:xlink='http://www.w3.org/1999/xlink'width='237'height='20'role='img'aria-label='languages: 5'><title>Average # Lines of Code for Types: 25.96</title><linearGradientid='s'x2='0'y2='100%'><!-- linear gradient to use for the background shadow --><stopoffset='0'stop-color='#bbb'stop-opacity='.1'/><stopoffset='1'stop-opacity='.1'/></linearGradient><clipPathid='r'><!-- clip to a rectangle with rounded edges --><rectwidth='237'height='20'rx='3'fill='#fff'/></clipPath><gclip-path='url(#r)'><!-- left background --><rectwidth='195'height='20'fill='#555'/><!-- right background --><rectx='195'width='42'height='20'fill='#007ec6'/><!-- background shadow --><rectwidth='237'height='20'fill='url(#s)'/></g><gfill='#FFF'text-anchor='center'font-family='Verdana,Geneva,DejaVu Sans,sans-serif'text-rendering='geometricPrecision'font-size='110'><!-- left text semitransparent shadow then white text --><textaria-hidden='true'x='40'y='150'fill='#010101'fill-opacity='.3'transform='scale(.1)'textLength='1854'>Average # Lines of Code for Types</text><textx='40'y='140'transform='scale(.1)'fill='#FFF'textLength='1854'>Average # Lines of Code for Types</text><!-- right text semitransparent shadow then white text --><textaria-hidden='true'x='1994'y='150'fill='#010101'fill-opacity='.3'transform='scale(.1)'textLength='300'>25.96</text><textx='1994'y='140'transform='scale(.1)'fill='#FFF'textLength='300'>25.96</text></g></svg>
One day Maui.Graphics may offer SVG export support (issue #103) but for now generating these files discretely isn’t too bad.
Status badges are popular decorators on GitHub readme pages and project websites.Badgen.net and shields.io are popular HTTP APIs for dynamically generating SVG status badges. In this article we will use the new Microsoft.Maui.Graphics package to generate status badges from a C# console application. This application can be downloaded: BadgeApp.zip
Microsoft.Maui.Graphics natively supports image scaling. This allows you to create large badges without any loss in quality that would come from creating a small badge and resizing the bitmap.