Plot annotations?
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:
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?
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...
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<ScatterPoints<IReadOnlyList<double>>>().Tag = "dataPoints";
// Add a LinearTrendLine with a Tag.
LinearTrendLine trendLine = new LinearTrendLine(data, plot.GetFirst<IContinuousCoordinateSystem>()) { 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<ScatterPoints<IReadOnlyList<double>>>().PresentationAttributes.Fill;
Brush trendLineBrush = plot.GetFirst<LinearTrendLine>().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("<b>R<sup>2</sup>:</b> " + 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., <script> elements) and manipulate it.
XmlDocument doc = container.SaveAsSVG();
// Now, the plot elements have been rendered with the specified tags.
// However, to ensure that each tag is unique, a suffix may have been
// added to the tag of individual graphics actions. For example,
// each point drawn by the ScatterPoints object is drawn with a tag
// of the form "dataPoints@<N>", where <N> is a number from 0 to
// 100 (corresponding to the index in the data array).
// We will use this to store the tags for the data points.
List<string> dataPointTags = new List<string>(data.Length);
// This will be used to store the tags for the trendline (there should
// normally only be one, but this is not guaranteed).
List<string> trendLineTags = new List<string>(1);
// This will be used for the point info box tags.
List<string> pointBoxTags = new List<string>();
// This will be used for the trendline info box tags.
List<string> tlBoxTags = new List<string>();
// You can get a list of all the tags that have been created in the
// plot using the GetTags method on the Graphics object.
foreach (string tag in container.Graphics.GetTags())
{
if (tag.StartsWith("dataPoints"))
{
dataPointTags.Add(tag);
}
else if (tag.StartsWith("trendLine"))
{
trendLineTags.Add(tag);
}
else if (tag.StartsWith("pointBox"))
{
pointBoxTags.Add(tag);
}
else if (tag.StartsWith("tlBox"))
{
tlBoxTags.Add(tag);
}
}
// We can now remove the placeholder text.
foreach (XmlElement element in doc.GetElementsByTagName("text").Cast<XmlElement>().Where(x => x.GetAttribute("id").StartsWith("removeMe")).ToList())
{
element.ParentNode.RemoveChild(element);
}
// Right now, the info boxes are shown on top of everything even
// when we are not hovering on a point. We should set the default
// visibility to hidden.
foreach (XmlElement element in doc.DocumentElement.SelectNodes("*").Cast<XmlElement>().Where(x => x.GetAttribute("id").StartsWith("pointBox") || x.GetAttribute("id").StartsWith("tlBox")))
{
element.SetAttribute("visibility", "hidden");
}
// Now we need some JavaScript for the interactivity. Start by creating a
// <script> element.
XmlElement scriptElement = doc.CreateElement("script", "http://www.w3.org/2000/svg");
scriptElement.SetAttribute("type", "text/javascript");
// JavaScript coding time! Of course, you can make this as complex as you want
// (e.g., by using a template script file included as an embedded resource).
StringBuilder javascriptCode = new StringBuilder();
// This code will be run when the document has finished loading.
javascriptCode.AppendLine("window.onload = function(event) {");
// Copy the data to the JS code. Here I'm using JSON arrays, but you could simply
// write out the arrays yourself.
javascriptCode.Append(" let data = JSON.parse(");
javascriptCode.Append(JsonSerializer.Serialize(JsonSerializer.Serialize(data)));
javascriptCode.AppendLine(");");
// Copy the data point tags to the JS code.
javascriptCode.Append(" let dataPointTags = JSON.parse(");
javascriptCode.Append(JsonSerializer.Serialize(JsonSerializer.Serialize(dataPointTags)));
javascriptCode.AppendLine(");");
// Copy the trendline tag(s) to the JS code.
javascriptCode.Append(" let trendLineTags = JSON.parse(");
javascriptCode.Append(JsonSerializer.Serialize(JsonSerializer.Serialize(trendLineTags)));
javascriptCode.AppendLine(");");
// Copy the point info box tag(s) to the JS code.
javascriptCode.Append(" let pointBoxTags = JSON.parse(");
javascriptCode.Append(JsonSerializer.Serialize(JsonSerializer.Serialize(pointBoxTags)));
javascriptCode.AppendLine(");");
// Copy the trendline info box tag(s) to the JS code.
javascriptCode.Append(" let tlBoxTags = JSON.parse(");
javascriptCode.Append(JsonSerializer.Serialize(JsonSerializer.Serialize(tlBoxTags)));
javascriptCode.AppendLine(");");
javascriptCode.AppendLine();
// Add event handlers to the data points.
javascriptCode.AppendLine(" for (let i = 0; i < dataPointTags.length; i++) {");
javascriptCode.AppendLine(" let tag = dataPointTags[i];");
javascriptCode.AppendLine(" let dataPoint = document.getElementById(tag);");
// Mouse enter event.
javascriptCode.AppendLine(" dataPoint.onmouseenter = function (mouseEvent) {");
// Change the colour of the point.
javascriptCode.AppendLine(" dataPoint.style.fill = \"#D55E00\";");
javascriptCode.AppendLine(" let pointIndex = parseInt(tag.substr(tag.indexOf(\"@\") + 1));");
// Find the coordinates of the point in screen space and add a small offset.
javascriptCode.AppendLine(" let coords = dataPoint.ownerSVGElement.createSVGPoint().matrixTransform(dataPoint.getScreenCTM());");
javascriptCode.AppendLine(" coords.x += 10;");
javascriptCode.AppendLine(" coords.y += 10;");
// Move around all the elements in the point info box.
javascriptCode.AppendLine(" for (let j = 0; j < pointBoxTags.length; j++) {");
javascriptCode.AppendLine(" let boxElement = document.getElementById(pointBoxTags[j]);");
// We need a null check because occasionally some VectSharp tags will not correspond
// to any SVG element (e.g., transforms).
javascriptCode.AppendLine(" if (boxElement != null) {");
// Show the element.
javascriptCode.AppendLine(" boxElement.style.visibility = \"visible\";");
// Find the coordinates of the point in the box elements's coordinate space.
javascriptCode.AppendLine(" let elementCoords = coords.matrixTransform(boxElement.ownerSVGElement.getScreenCTM().inverse());");
// Modify the box element's transform so that it appears in the correct position.
// boxElement.getAttribute("transform") is the "default" transform (which will ensure
// that all box elements are in the correct position with respect to each other).
javascriptCode.AppendLine(" boxElement.style.transform = boxElement.getAttribute(\"transform\") + \" translate(\" + elementCoords.x + \"px, \" + elementCoords.y + \"px)\";");
javascriptCode.AppendLine(" }");
javascriptCode.AppendLine(" }");
// Change the text of the labels. Note that the text will not be repositioned,
// so you need to be careful with how much space you leave around the placeholder.
// Also, if you use a glyph that is not included in the embedded fonts, a default
// font may be used instead.
javascriptCode.AppendLine(" document.getElementById(\"pointBox_TitleNumber\").innerHTML = pointIndex;");
javascriptCode.AppendLine(" document.getElementById(\"pointBox_xCoordValue\").innerHTML = data[pointIndex][0].toFixed(5);");
javascriptCode.AppendLine(" document.getElementById(\"pointBox_yCoordValue\").innerHTML = data[pointIndex][1].toFixed(5);");
javascriptCode.AppendLine(" };");
javascriptCode.AppendLine();
// Mouse leave event.
javascriptCode.AppendLine(" dataPoint.onmouseleave = function (mouseEvent) {");
// Reset the point fill colour.
javascriptCode.AppendLine(" dataPoint.style.fill = \"\";");
// Hide the point info box and reset its position.
javascriptCode.AppendLine(" for (let j = 0; j < pointBoxTags.length; j++) {");
javascriptCode.AppendLine(" let boxElement = document.getElementById(pointBoxTags[j]);");
javascriptCode.AppendLine(" if (boxElement != null) {");
javascriptCode.AppendLine(" boxElement.style.visibility = \"hidden\";");
javascriptCode.AppendLine(" boxElement.style.transform = boxElement.getAttribute(\"transform\");");
javascriptCode.AppendLine(" }");
javascriptCode.AppendLine(" }");
javascriptCode.AppendLine(" };");
javascriptCode.AppendLine(" }");
// Add event handlers to the trendline.
javascriptCode.AppendLine(" for (let i = 0; i < trendLineTags.length; i++) {");
javascriptCode.AppendLine(" let tag = trendLineTags[i];");
javascriptCode.AppendLine(" let trendLine = document.getElementById(tag);");
// Mouse enter event.
javascriptCode.AppendLine(" trendLine.onmouseenter = function (mouseEvent) {");
// Change the stroke colour of the trendline.
javascriptCode.AppendLine(" trendLine.style.stroke = \"#CC79A7\";");
// Find the coordinates of the current mouse position (plus a small offset) in the
// SVG coordinate space.
javascriptCode.AppendLine(" let coords = trendLine.ownerSVGElement.createSVGPoint();");
javascriptCode.AppendLine(" coords.x = mouseEvent.clientX + 10;");
javascriptCode.AppendLine(" coords.y = mouseEvent.clientY + 10;");
// Move around the elements in the trendline info box.
javascriptCode.AppendLine(" for (let j = 0; j < tlBoxTags.length; j++) {");
javascriptCode.AppendLine(" let boxElement = document.getElementById(tlBoxTags[j]);");
javascriptCode.AppendLine(" if (boxElement != null) {");
javascriptCode.AppendLine(" boxElement.style.visibility = \"visible\";");
javascriptCode.AppendLine(" let elementCoords = coords.matrixTransform(boxElement.ownerSVGElement.getScreenCTM().inverse());");
javascriptCode.AppendLine(" boxElement.style.transform = boxElement.getAttribute(\"transform\") + \" translate(\" + elementCoords.x + \"px, \" + elementCoords.y + \"px)\";");
javascriptCode.AppendLine(" }");
javascriptCode.AppendLine(" }");
javascriptCode.AppendLine(" };");
javascriptCode.AppendLine();
// Mouse leave event. Resets the stroke colour and hides the info box.
javascriptCode.AppendLine(" trendLine.onmouseleave = function (mouseEvent) {");
javascriptCode.AppendLine(" trendLine.style.stroke = \"\";");
javascriptCode.AppendLine(" for (let j = 0; j < tlBoxTags.length; j++) {");
javascriptCode.AppendLine(" let boxElement = document.getElementById(tlBoxTags[j]);");
javascriptCode.AppendLine(" if (boxElement != null) {");
javascriptCode.AppendLine(" boxElement.style.visibility = \"hidden\";");
javascriptCode.AppendLine(" boxElement.style.transform = boxElement.getAttribute(\"transform\");");
javascriptCode.AppendLine(" }");
javascriptCode.AppendLine(" }");
javascriptCode.AppendLine(" };");
javascriptCode.AppendLine(" }");
javascriptCode.AppendLine("};");
// Add the code to the script element.
scriptElement.InnerText = javascriptCode.ToString();
// Append the script to the SVG document.
doc.DocumentElement.AppendChild(scriptElement);
// Save the SVG to a file - you can use other overloads of this method to
// write it to a Stream or to a string.
doc.WriteSVGXML("plot.svg");
The only thing is that the doc.WriteSVGXML method used in the last line was not public, so you will need VectSharp.SVG 1.10.2-a1 to use it.
This is the result [GitHub will block the scripts, but if you right-click, download the SVG file, and open it locally it should work]:
This allows you to create a stand-alone interactive SVG; if you want to create an actual plot GUI, it might be a good idea to look at Avalonia. You can use VectSharp.Canvas to create an Avalonia Canvas object from the plot, which will make it easier to work with all the events etc. directly from C# code.
I hope this helps, let me know if you have any further questions!
It is quite convolved, but it works. Thank you very much for the example.
I'm closing this for now, feel free to reopen it or create a new issue if you still have any questions or problems!
Thank you very much for this code. It has opened a new world of possibilities for me.
With only this javascript:
window.svgInterop = window.svgInterop || {};
window.svgInterop.clientToSvgDataSpace = function (element, clientX, clientY) {
if (!element || (typeof element.getScreenCTM !== "function")) {
return null;
}
const ctm = element.getScreenCTM();
if (!ctm) {
return null;
}
const inverseCtm = ctm.inverse();
const svg = element.ownerSVGElement;
const point = svg.createSVGPoint();
point.x = clientX;
point.y = clientY;
const svgPoint = point.matrixTransform(inverseCtm);
return {
x: svgPoint.x,
y: svgPoint.y
};
};
And this blazor component:
public record SvgElementEventArgs(string Id, ElementReference ElementRef, MouseEventArgs MouseState);
public record SvgPoint(double X, double Y);
public class SvgRenderer : ComponentBase
{
private const string InteractiveElementClasses = "transition-all duration-150 ease-in-out origin-center hover:opacity-80";
[Parameter, EditorRequired] public XmlDocument Document { get; set; } = default!;
[Parameter] public string Class { get; set; } = InteractiveElementClasses;
[Parameter] public EventCallback<SvgElementEventArgs> OnElementMouseOver { get; set; }
[Parameter] public EventCallback<SvgElementEventArgs> OnElementMouseMove { get; set; }
[Parameter] public EventCallback<SvgElementEventArgs> OnElementMouseLeave { get; set; }
protected readonly Dictionary<string, ElementReference> _elementRefs = new();
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (Document?.DocumentElement != null) {
int sequence = 0;
RenderNode(Document.DocumentElement, builder, ref sequence);
}
}
#pragma warning disable ASP0006
private void RenderNode(XmlNode node, RenderTreeBuilder builder, ref int sequence)
{
switch (node.NodeType) {
case XmlNodeType.Element:
XmlElement element = (XmlElement)node;
builder.OpenElement(sequence++, element.Name);
string? originalClass = null;
foreach (XmlAttribute attr in element.Attributes) {
if (attr.Name.Equals("class", StringComparison.InvariantCultureIgnoreCase)) {
originalClass = attr.Value;
continue;
}
builder.AddAttribute(sequence++, attr.Name, attr.Value);
}
string id = element.GetAttribute("id");
if (!string.IsNullOrEmpty(id)) {
string finalClass = string.IsNullOrEmpty(originalClass) ? InteractiveElementClasses : $"{originalClass} {InteractiveElementClasses}";
builder.AddAttribute(sequence++, "class", finalClass);
builder.AddAttribute(sequence++, "onmouseover", EventCallback.Factory.Create<MouseEventArgs>(this,
(e) => HandleEvent(id, e, OnElementMouseOver)));
builder.AddAttribute(sequence++, "onmousemove", EventCallback.Factory.Create<MouseEventArgs>(this,
(e) => HandleEvent(id, e, OnElementMouseMove)));
builder.AddAttribute(sequence++, "onmouseleave",
EventCallback.Factory.Create<MouseEventArgs>(this,
(e) => HandleEvent(id, e, OnElementMouseLeave)));
builder.SetKey(id);
builder.AddElementReferenceCapture(sequence++, capturedRef => _elementRefs[id] = capturedRef);
} else if (originalClass != null) {
builder.AddAttribute(sequence++, "class", originalClass);
}
foreach (XmlNode childNode in element.ChildNodes) {
RenderNode(childNode, builder, ref sequence);
}
builder.CloseElement();
break;
case XmlNodeType.Text:
builder.AddMarkupContent(sequence++, node.Value);
return;
case XmlNodeType.CDATA:
builder.AddMarkupContent(sequence++, node.Value);
break;
case XmlNodeType.Comment:
builder.AddMarkupContent(sequence++, $"<!-- {node.Value} -->");
break;
default:
return;
}
}
#pragma warning restore ASP0006
private Task HandleEvent(string id, MouseEventArgs args, EventCallback<SvgElementEventArgs> callback)
{
if (_elementRefs.TryGetValue(id, out var elementRef)) {
return callback.InvokeAsync(new SvgElementEventArgs(id, elementRef, args));
}
return Task.CompletedTask;
}
}
I can bring some elements to life in blazor with very little work:
@if (_isTooltipVisible) {
<div class="absolute z-[999] rounded bg-slate-800 px-2 py-1 text-sm text-white shadow-lg pointer-events-none whitespace-nowrap"
style="@_tooltipStyle">
@_tooltipText
</div>
}
<div class="w-full lg:w-auto lg:grow h-auto lg:h-[500px]">
@if (svgTopView != null) {
<SvgRenderer Document="svgTopView" OnElementMouseOver="MouseOverTopView" OnElementMouseMove="MouseMoveTopView" OnElementMouseLeave="MouseLeaveTopView" />
}
</div>
private async Task MouseOverTopView(SvgElementEventArgs args)
{
_isTooltipVisible = true;
}
private async Task MouseMoveTopView(SvgElementEventArgs args)
{
if (tvCoordinateSystem == null) {
return;
}
SvgPoint? plotPoint = await JSRuntime.InvokeAsync<SvgPoint>("svgInterop.clientToSvgDataSpace", args.ElementRef, args.MouseState.ClientX, args.MouseState.ClientY);
if (plotPoint == null) {
return;
}
var vectSharpPlotPoint = new Point(plotPoint.X, plotPoint.Y);
double[] dataCoordinates = tvCoordinateSystem.ToDataCoordinates(vectSharpPlotPoint);
_isTooltipVisible = true;
_tooltipText = $"X: {dataCoordinates[0]:F2}, Y: {dataCoordinates[1]:F2}";
_tooltipStyle = string.Format(System.Globalization.CultureInfo.InvariantCulture,
"position: absolute; top: {0}px; left: {1}px;",
args.MouseState.ClientY + 15,
args.MouseState.ClientX + 15
);
StateHasChanged();
}
private void MouseLeaveTopView(SvgElementEventArgs args)
{
_isTooltipVisible = false;
}
The only issue I found with this approach is that I can't remove elements with id from the plot dynamically because if I do, the following error appears:
Unhandled exception rendering component: Unexpected frame type during RemoveOldFrame: ElementReferenceCapture
System.NotImplementedException: Unexpected frame type during RemoveOldFrame: ElementReferenceCapture
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.RemoveOldFrame(DiffContext& diffContext, Int32 oldFrameIndex)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(DiffContext& diffContext, Int32 oldFrameIndex, Int32 newFrameIndex)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(DiffContext& diffContext, Int32 oldFrameIndex, Int32 newFrameIndex)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
so my request would be to add support to something like linkDestinations in SaveAsSvg, but instead of for links, it can be to add custom attributes to the generated elements. The type should be Dictionary<string, Dictionary<string, string>>.
Imagine I want to add some attributes for a couple of paths I tagges as "path1" and "path2". I could do:
Dictionary<string, Dictionary<string, string>> customAttributes = new()
{
{ "path1", new Dictionary<string, string> {
{ "Visible", "true" },
{ "Locked", "false" }
}
},
{ "path2", new Dictionary<string, string> {
{ "Visible", "false" },
}
},
};
And then in my SvgRenderer, I could read those attributes and for example, for the invisible ones set display: none. That way I always generate the same objects but I can control their behaviour, and I don't run into the "Unexpected frame type during RemoveOldFrame: ElementReferenceCapture" exception.