arklumpus / VectSharp

A light library for C# vector graphics
GNU Lesser General Public License v3.0
221 stars 22 forks source link

Plot annotations? #71

Open emumanu opened 3 weeks ago

emumanu commented 3 weeks ago

Hello there!

Nice project you have. Also, it is refreshing to see that you don't have thousands of unresolved open issues. That talks a lot about the quality of the project.

I have been successfully using the VectSharp.Plot nuget package. I can add links to my SVG files, but the only thing I'm really missing to get rid of other chart libraries are the annotations when you hover the mouse over the graph. I'm talking about something like this:

image

It is nice to be able to display custom data (not only the x and y values) when hovering over the graph or data points. I know I will need some javascript/webassembly for that, but is there any plan to support something like that?

arklumpus commented 3 weeks ago

Hi, I'm glad you like VectSharp!

I think it should already be possible to do what you want; the trick is to use "tags". When you draw something specifying a tag that is not null, the tag becomes the id of the graphics element in the SVG document. You can then use these ids to access the elements e.g. from JavaScript code.

Here is a long and detailed example... ```CSharp using MathNet.Numerics.Distributions; using System.Text; using System.Text.Json; using System.Xml; using VectSharp; using VectSharp.Plots; using VectSharp.SVG; // Generate some random data. double[][] data = Enumerable.Range(0, 101).Select(x => new double[] { x, 2 * x + 3 + Normal.Sample(0, 10) }).ToArray(); // Create a scatter plot. Plot plot = Plot.Create.ScatterPlot(data); // Get the ScatterPoint objects that draws the point and give it a Tag. plot.GetFirst>>().Tag = "dataPoints"; // Add a LinearTrendLine with a Tag. LinearTrendLine trendLine = new LinearTrendLine(data, plot.GetFirst()) { Tag = "trendLine" }; plot.AddPlotElement(trendLine); // Render the plot to a page. Page renderedPlot = plot.Render(); Page container = new Page(renderedPlot.Width, renderedPlot.Height); container.Graphics.DrawGraphics(0, 0, renderedPlot.Graphics); // There are multiple ways in which you could display things on top of the plot. // For example, you could generate new DOM elements within the JavaScript code, // or if you are using Blazor/WebAssembly you could generate a new SVG image // on the fly. For this example, I'm drawing a placeholder element whose contents // will be updated by the JS code. Brush dataPointBrush = plot.GetFirst>>().PresentationAttributes.Fill; Brush trendLineBrush = plot.GetFirst().PresentationAttributes.Stroke; Font fntBold = new Font(FontFamily.ResolveFontFamily(FontFamily.StandardFontFamilies.HelveticaBold), 10); Font fnt = new Font(FontFamily.ResolveFontFamily(FontFamily.StandardFontFamilies.Helvetica), 10); // Fancy shadow. Graphics pointBoxShadow = new Graphics(); pointBoxShadow.FillRectangle(3, 3, 80, 45, Colours.Black.WithAlpha(0.5)); container.Graphics.DrawGraphics(0, 0, pointBoxShadow, new VectSharp.Filters.GaussianBlurFilter(3), tag: "pointBox_Shadow"); // An info box showing X and Y coordinates. container.Graphics.FillRectangle(0, 0, 80, 45, Colours.White, tag: "pointBox_BG"); container.Graphics.StrokeRectangle(0, 0, 80, 45, dataPointBrush, tag: "pointBox_Border"); container.Graphics.FillRectangle(0, 0, 80, 14, dataPointBrush, tag: "pointBox_TitleBG"); container.Graphics.FillText(5, 10, "Data point:", fntBold, Colours.White, textBaseline: TextBaselines.Baseline, tag: "pointBox_Title"); container.Graphics.FillText(5 + fntBold.MeasureText("Data point:").Width + 5, 10, "0", fntBold, Colours.White, textBaseline: TextBaselines.Baseline, tag: "pointBox_TitleNumber"); container.Graphics.FillText(5, 25, "X:", fntBold, Colours.Black, textBaseline: TextBaselines.Baseline, tag: "pointBox_xCoordLabel"); container.Graphics.FillText(20, 25, "0", fnt, Colours.Black, textBaseline: TextBaselines.Baseline, tag: "pointBox_xCoordValue"); container.Graphics.FillText(5, 39, "Y:", fntBold, Colours.Black, textBaseline: TextBaselines.Baseline, tag: "pointBox_yCoordLabel"); container.Graphics.FillText(20, 39, "0", fnt, Colours.Black, textBaseline: TextBaselines.Baseline, tag: "pointBox_yCoordValue"); // Let's create an info box for the trendline (this will have fixed text). string trendLineEquation = "y = " + trendLine.Slope.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture) + " x " + (trendLine.Intercept > 0 ? "+ " : "- ") + Math.Abs(trendLine.Intercept).ToString("0.00", System.Globalization.CultureInfo.InvariantCulture); double averageY = data.Select(v => v[1]).Average(); double rSquared = 1 - data.Select(v => v[1] - (trendLine.Slope * v[0] + trendLine.Intercept)).Select(x => x * x).Sum() / data.Select(v => v[1] - averageY).Select(x => x * x).Sum(); // Fancy shadow #2. Graphics tlBoxShadow = new Graphics(); tlBoxShadow.FillRectangle(3, 3, fnt.MeasureText(trendLineEquation).Width + 10, 45, Colours.Black.WithAlpha(0.5)); container.Graphics.DrawGraphics(0, 0, tlBoxShadow, new VectSharp.Filters.GaussianBlurFilter(3), tag: "tlBox_Shadow"); // An info box for the trendline. container.Graphics.FillRectangle(0, 0, fnt.MeasureText(trendLineEquation).Width + 10, 45, Colours.White, tag: "tlBox_BG"); container.Graphics.StrokeRectangle(0, 0, fnt.MeasureText(trendLineEquation).Width + 10, 45, trendLineBrush, tag: "tlBox_Border"); container.Graphics.FillRectangle(0, 0, fnt.MeasureText(trendLineEquation).Width + 10, 13, trendLineBrush, tag: "tlBox_TitleBG"); container.Graphics.FillText(5, 10, "Trendline", fntBold, Colours.White, textBaseline: TextBaselines.Baseline, tag: "tlBox_Title"); container.Graphics.FillText(5, 25, trendLineEquation, fnt, Colours.Black, textBaseline: TextBaselines.Baseline, tag: "tlBox_equation"); container.Graphics.FillText(5, 39, FormattedText.Format("R2: " + rSquared.ToString("0.000", System.Globalization.CultureInfo.InvariantCulture), FontFamily.StandardFontFamilies.Helvetica, fnt.FontSize), Colours.Black, textBaseline: TextBaselines.Baseline, tag: "tlBox_R2"); // Finally, let's add some other text elements to make sure that the embedded fonts // contain all the glyphs we may need. We will remove these elements from the // rendered SVG later. Alternatively, you could use TextOptions.EmbedFonts to // embed the full font rather than a subset of glyphs. container.Graphics.FillText(0, 0, "-0123456789.", fntBold, Colours.Fuchsia, tag: "removeMe_1"); container.Graphics.FillText(0, 0, "-0123456789.", fnt, Colours.Fuchsia, tag: "removeMe_2"); // You can render the Page to an SVG XmlDocument, so that you can add nodes // to it (e.g., Githubissues.
  • Githubissues is a development platform for aggregating issues.