SVG export from a vega-lite api chart: error on opening the file

I would like to display a vega-lite-api generated chart and be able to save it as an SVG file or a PNG file.

I go for instance to https://observablehq.com/@ericmauviere/seasonality-of-weddings-in-france
which displays at the top an SVG vega-lite bar chart

then try and export via the download SVG item in the Observable cell menu.
When i open the file (in a browser like Chrome or Edge) i get:

This page contains the following errors:
error on line 1 at column 226: Attribute xmlns redefined
Below is a rendering of the page up to the first error.

And indeed, this appears repeated in the svg root node:
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"

Actually, vega-lite SVG renderer generates these attributes, and it looks like the Observable SVG export functionality add them a second time.

There is also an error on PNG export (via the cell menu) ā€˜Oops, downloading failed. Try again?ā€™

Eventually, i try the custom SVG and PNG export showcased in https://observablehq.com/@mbostock/saving-svg.

The ā€˜Save as SVGā€™ button generates a proper SVG file,
The ā€˜Save as PNGā€™ button doesnā€™t do anything

1 Like

You are right, removing the 2nd xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fixes the svg downloaded via the

The SVG element is the 1st child of the evol_weddings. Mikes saving-svg just saves the whole div cell but that mostly works.

This problem has come up before. The cause is described in this post: How to create or download high resolution figure using Vega-Lite rather than SVG?

1 Like

Would it be worth removing those attributes, so observable can save the svg correctly, until observable has a fix? I guess that depends on the project.

like soā€¦

evol_weddings = {
let ev = await vl.markBar()
  .data(agregats.total_annuel.objects())
  .encode(
    vl.x().fieldO('AN').axis({ labelAngle: 0, labelOverlap: true }).title(null),
    vl.y().fieldQ('TOT').title(null),
    vl.color().value('#1e9b9e '),
    vl.tooltip().fieldQ('TOT')
  )
  .title({
    text: ['Evolution of total nb. of marriages','France'],
    anchor: 'start', frame: 'group'
  })
  .width(cwidth - 70)
  .render({renderer: 'svg'});
  
  let svg = ev.querySelector('svg');
  svg.removeAttribute("xmlns");
  svg.removeAttribute("xmlns:xlink")
  
return ev
}
1 Like

Thanks @hellonearthis for that fix, that i will use! It eliminates as well the PNG export error.

To summarise at this point:
Both Vega lite and Observable teams use different encodings to set xmlns attributes in the SVG, in the end, they pile up, which causes the reopening issue.
No matter which encoding process is the most academic, it would just be appropriate for both parties to agree on the same.

I donā€™t know how I can help with this, except to relay this analysis on vega-lite-api github, so do iā€¦

1 Like

I have to nitpick here: This is not about encodings, but about XML namespaces.

As far as I could tell, Vegaā€™s way of setting the attribute is perfectly valid. So itā€™s on Observable to fix their Download function by checking for the presence of the attribute.

1 Like

This is a bug in Vega, but we could workaround it.

In HTML, the SVG element is known as a ā€œforeign elementā€. When parsing static HTML, the HTML specification defines that certain attributes on foreign elements are implicitly assigned namespaces. Relevant here, the xmlns and xmlns:xlink attributes are implicitly in the XMLNS namespace.

https://html.spec.whatwg.org/dev/syntax.html#attributes-2

This implicit namespacing happens only when parsing HTML (e.g., when assigning to the innerHTML of a template element). It does not apply when element.setAttribute is called directly, which is how Vega is setting these attributes.

Vega is currently doing this:

element.setAttribute("xmlns", "http://www.w3.org/2000/svg");
element.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");

It should be doing this, which is what Observable does:

element.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.w3.org/2000/svg");
element.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");

The bug in Vega is not obvious because these attributes are not needed to display an SVG element correctly in the browser, and when serializing, the namespaces are dropped anyway (and when the serialized SVG is read, the namespaces are once again implied). But the issue is that Vega is not setting the namespace correctly, so when Observable does set the attributes with namespaces, you get duplicate attributes when serializing. We can fix this by removing the non-namespaced attributes as described above.

3 Likes

Thank you very much for the explanation. Would it be possible please for you and the Vega team to just discuss this directly and adopt the same convention? I gather that you know Jeff Heer. If this a bug on their side, i suppose they would gladly agree to fix it. I have posted an issue on the vega-lite-api Github, but with no answer so farā€¦

Iā€™ve opened a PR here:

1 Like

Thank you very much!

Problem solved with last vega-lite-api version ! Thanks once again @mbostock and Jeffrey Heer

1 Like