problems rendering an SVG? #1866

Closed
jonathan-taylor opened this Issue Jun 6, 2012 · 33 comments

Projects

None yet
@jonathan-taylor

In a local branch, I seem to have run into a possible rendering problem with the notebook. The branch is:

https://github.com/jonathan-taylor/ipython/tree/Rdevice

The last cell of docs/notebooks/examples/rmagic_extension.ipynb is mangled in my browser but when I extract the svg from the .ipynb file and view it with chrome it looks fine.

I used the following script to extract the svg data from the notebook:

import simplejson
r = simplejson.load(open('docs/examples/notebooks/rmagic_extension.ipynb'))
svgcell = r.values()[1][0]['cells'][-2]
svgdata = svgcell['outputs'][0]
raw_svg = '\n'.join(svgdata['svg'] )
file('last_plot.svg','w').write(raw_svg)
@fperez
IPython member

@takluyver, since you're poking into the R magic machinery, perhaps you have some ideas? I'd also like to ping @stefanv, who also saw some SVG weirdness with the octave magic, in case this is similar. Unfortunately right now I'm busy finishing a report that's due so I can't look at this code quite right now.

@takluyver
IPython member

No immediate ideas, but I'll try to remember to look at it.

@stefanv

I can reproduce the problem, but it's not the same one I had with the Octave backend. I can't see anything obvious causing it.

@stefanv

It turns out that SVGs embedded the way IPython currently does all live inside the same DOM. This means that when any single SVG modifies styles, those changes are propagated to the rest. One solution is to encode SVGs the same way PNGs and JPEGs are, as base64 encoded data chunks, and add them to an <img> tag.

I've followed this approach here, and it now renders correctly:

https://github.com/stefanv/ipython/tree/svg_as_image_blob

The above changes made me realize that there's special casing and duplicate code for png/jpeg, so we may want to look at cleaning that up as well eventually.

@takluyver
IPython member

Would that mean they're also saved in the ipynb file as base64 encoded data? It seems a bit ugly to turn XML into unreadable base64 strings, although I guess SVG from plotting isn't going to be very readable anyway.

@stefanv

Yes, unfortunately.

@jonathan-taylor

Let me see if I understand: with multiple SVG files in the document, when one changes some of its xml representation (say, its axis labels), the browser propagates these changes to the next SVG plot?

@fperez
IPython member

@jonathan-taylor, yes apparently at least some style elements do persist that way. Unfortunately as I pointed out to @stefanv, I'm not sure encoding is a good idea: it seems to kill the scriptability support of SVG, and that would immediately rule out things like interactive d3.js figures. That sounds like throwing away the baby with the bathwater.

We should try to find a solution to this that doesn't preclude things like interactive d3.js svg objects. @minrk, you know a lot about graphics stuff, any thoughts on this front?

@satra

before i open a new issue, is the following related:

i'm trying to display some svg and possibly missing something obvious. the command executes without error but nothing is displayed.

from IPython.core.display import display_svg
display_svg(url='http://en.wikipedia.org/wiki/File:Correlation_examples2.svg')

sys info:

{'commit_hash': '5308c36',
 'commit_source': 'installation',
 'default_encoding': 'UTF-8',
 'ipython_path': '/software/virtualenvs.EPD/7.2/devpype/lib/python2.7/site-packages/IPython',
 'ipython_version': '0.14.dev',
 'os_name': 'posix',
 'platform': 'Darwin-12.0.0-x86_64-i386-64bit',
 'sys_executable': '/software/virtualenvs.EPD/7.2/devpype/bin/python',
 'sys_platform': 'darwin',
 'sys_version': '2.7.2 |CUSTOM| (default, Sep  7 2011, 16:31:15) \n[GCC 4.0.1 (Apple Inc. build 5493)]'}
chrome-dev
@lgautier

I reached this page when investigating a plotting issue with the ipython notebook and rpy2: it turns out to be the same problem.

Shouldn't SVG rendering the last possible rendering option by default, until the problem is resolved ? (that is iPython would try first to display PNG, JPEG, or other possible alternatives).

@Carreau
IPython member

This would be a shame to deactivate svg if they are resolution independent. we can try to wrap them in a IFrame.

In the meantime you can locally monkey patch notebook from js console/custom.js:

IPython.OutputArea.display_order has the following display order by default ['javascript','html','latex','svg','png','jpeg','text']; you can mess with it if you wish.

Just set it to another value and new output will follow the new preference.

@lgautier

Right now we have resolution-independent figures, only garbled as soon as there is more than one figure in a notebook. ;-)

I'd trade those for PNGs with a reasonable resolution any day.

@stefanv

@lgautier Yes, multiple figures in a notebook is such specialized use, isn't it :)

@m3nu

I'm seeing the issue as well. The dev-version of igraph renders SVG and the second plot has messed-up labels.

@pablooliveira

This annoys me so I have looked a bit into this issue.
Yet I'm not sure this is a priority right now given http://mail.scipy.org/pipermail/ipython-dev/2012-June/009754.html

A simple test reproducing the problem is

<html>
<head>
</head>
<body>

    <!-- First inline SVG with inline style block -->
    <svg width="10cm" height="5cm" viewBox="0 0 1000 500">
    <defs>
    <style><![CDATA[

        rect {
            fill: red;
            stroke: blue;
            stroke-width: 3
        }
        ]]></style>
    </defs>
    <rect x="200" y="100" width="600" height="300"/>
    </svg>

    <!-- Second SVG -->
    <svg width="10cm" height="5cm" viewBox="0 0 1000 500">
    <rect x="200" y="100" width="600" height="300"/>
    </svg>

</body>
</html>

Both rectangles are red because the style of the first svg leaks into the second svg.
This is correct according to the w3c specification at http://www.w3.org/TR/SVG/styling.html#Scope
which reads:

Stand-alone SVG content textually included in an XML document
There is a single parse tree, using multiple namespaces; one or more subtrees are in the SVG namespace. Style sheets defined anywhere within the XML document (in style elements or style attributes, or in external style sheets linked with the style sheet processing instruction) apply across the entire document, including those parts of it in the SVG namespace. To get different styling for the SVG part, use the ‘style’ attribute, or put an ‘id’ on the ‘svg’ element and use contextual CSS selectors, or use XSL selectors.

So what can we do about it ? I have explored the following solutions:

1) Wrap the svg into an img or object tag. The inconvenient, as mentioned by @fperez, is that we loose the nice xml representation of the svg.

2) Wrap the svg in an iframe as mentioned by @Carreau. The inconvenient is that we have to serve the svg data from another url.

3) One could imagine injecting the style using javascript after the css is computed for the other elements.
Yet from what I've looked, it seems it's not possible to change the inline style attribute using javascript.

4) One could follow the w3c recommendation and add an unique id selector per svg figure in front of every inline style rule. This would provide a unique namespace for each svg figure. The problem is that this requires parsing the css inline block which seems hard and error prone.

5) Resetting the css adding a set of default css rules before each style block is very hard because of css inheritance rules.

6) The ideal solution would be to use the <style scoped > html 5 extension. (http://www.w3.org/TR/html- markup/style.html#style.attrs.scoped). The scoped attribute restricts the css scope to be local.

I think approach 6 is probably the more elegant one. The problem is that scoped is planned but not yet included into SVG2 specification (http://www.w3.org/TR/SVG2/styling.html#Scope). Also html scoped is
still very experimental and not supported in major browsers.

I have successfully experimented the following approach:

  • extract the inline style attribute from the svg and wrap it into an enclosing div using </li> <li>use this Jquery plugin (<a href="https://github.com/thingsinjars/jQuery-Scoped-CSS-plugin">https://github.com/thingsinjars/jQuery-Scoped-CSS-plugin</a>) which adds support for style scoped to current browsers</li> </ul> <p>Using this the above code would become:</p> <pre lang="html"><code>&lt;html&gt; &lt;head&gt; &lt;script src=&quot;http://code.jquery.com/jquery-1.9.1.js&quot;&gt;&lt;/script&gt; &lt;script src=&quot;jquery.scoped.js&quot;&gt;&lt;/script&gt; &lt;/head&gt; &lt;body&gt; &lt;div&gt; &lt;style scoped &gt; rect { fill: red; stroke: blue; stroke-width: 3 } &lt;/style&gt; &lt;svg width=&quot;10cm&quot; height=&quot;5cm&quot; viewBox=&quot;0 0 1000 500&quot;&gt; &lt;defs&gt; &lt;/defs&gt; &lt;rect x=&quot;200&quot; y=&quot;100&quot; width=&quot;600&quot; height=&quot;300&quot;/&gt; &lt;/svg&gt; &lt;/div&gt; &lt;svg width=&quot;10cm&quot; height=&quot;5cm&quot; viewBox=&quot;0 0 1000 500&quot;&gt; &lt;rect x=&quot;200&quot; y=&quot;100&quot; width=&quot;600&quot; height=&quot;300&quot;/&gt; &lt;/svg&gt; &lt;script&gt; $.scoped(); &lt;/script&gt; &lt;/body&gt; &lt;/html&gt; </code></pre> <p>which renders correctly.</p> <p>Another maybe more pragmatical solution would be to embed svg into an iframe or object for now, and use inline svg when browsers are ready.</p> <p>What do you think ?<br> Is there any interest in fixing this issue now or should we wait for the svg support to mature ?</p> <p>Thanks,</p> <p>Pablo</p>
@stefanv

When you write "not supported in major browsers", are you saying that that the scoped style fix won't help at the moment?

@pablooliveira

See this table http://caniuse.com/style-scoped for the full details. In a nutshell, current versions of Chrome and IE do not support scoped style.

But this jquery plugin https://github.com/thingsinjars/jQuery-Scoped-CSS-plugin plugin enables scoped style on current browser (while disabling itself on browsers with scoped support).

@stefanv

The jQuery plugin then sounds like a viable approach to me.

@Carreau
IPython member
@pablooliveira pablooliveira added a commit to pablooliveira/ipython that referenced this issue Sep 19, 2013
@pablooliveira pablooliveira display: scope SVG's style elements so they do not leak
When multiple SVG figures are inlined in the same dom, they
share the same css namespace. This patch, scopes each SVG's style
CSS rules so they do not leak into other figures.

Should help fixing #1866
2217951
@pablooliveira

Actually, I realized #4236 does not fix the initial issue (rendering https://github.com/jonathan-taylor/ipython/tree/Rdevice). My reduced test case is too simple and possibly exhibits a different problem. Something else is going on with the original example, but I have no clue for the moment.

EDIT:

It seems the initial issue in https://github.com/jonathan-taylor/ipython/tree/Rdevice is not due to style leaks, but to xlink:href reused identifiers.

The SVG spec allows to reuse elements defined inside a <defs> section by inserting a <use> element (http://www.w3.org/TR/SVG/struct.html#UseElement).

The use element references the origin element thanks to a xlink:href attribute.
For example:

 <defs>
    <rect id="MyRect" width="60" height="10"/>
  </defs>
  ...
  <use x="20" y="10" xlink:href="#MyRect" />

The problem in the rmagic_extension.ipynb notebook is that different SVG figures are redefining the same ids.
Therefore the xlink:href has multiple targets.

Making the ids and xlink:href unique (for example prepending a unique hash) solves the issue.

Nevertheless this is getting hairy, maybe wrapping the svg in an img/iframe/object is the best option for now ?

@ellisonbg
IPython member

The basic message here is that SVG is just like any other HTML - ids have to be global on the page. I consider it to be a bug in other's libraries if they are writing SVG like this. It would be the same if pandas was writing HTML tables that couldn't be on the page with other pandas HTML tables. I would tell them to 1) use classes instead of ids or 2) use ids that are always unique on a page. I am -1 on the iframe fix.

@minrk
IPython member

I think the argument is that most of the time, SVGs are treated like images - they don't all exist on the same page, but are sourced from separate URLs, avoiding any conflict. it's a bit unusual that we inline SVGs rather than treating them individually as separate documents, which any normal webpage would do.

@pablooliveira

Yes, @minrk is right: most of the time SVGs are treated as images so the programs producing SVGs do not need to produce unique ids. Even Inkscape may produce SVGs with colliding ids.

This means that, right now, SVG support in IPython notebook is broken in most non-trivial usages.
While one could debate that it's upstream responsibility to generate unique ids [*], I do not think that is a pragmatic solution in the mean time.

[*] I'm not sure it's upstream responsibility to guarantee a clean namespace (btw, non trivial problem, would you use UUID ?) but the application that inlines multiple contents.

@ellisonbg
IPython member

I agree that is the traditional model of SVG. But in the modern web, where the Notebook lives, SVG in understodd to be "just part of the DOM". Libraries such as d3 fully embrace this way of thinking about SVG. I know that IPython can't completely ignore the old way of think about SVGs, I am just not ready to throw all SVG in iframes, as it seriously retricts the more modern usages cases by sandboxing SVG away from the rest of the code on the page.

@takluyver
IPython member
@pablooliveira

@ellisonbg: I agree, but to encourage the new Web SVG we should encourage adoption of SVG by providing solid support.

We could by default inline SVGs and provide a compatibility mode for old SVGs.
For example with

SVG(url="xxx", scoped=True). 

What do you think?

@stefanv
@ellisonbg
IPython member
@pablooliveira

@stefanv:

Another option could be to parse the SVGs and replace IDs with some image specific identifier + the old ID.

At first I though this was the way to go (see ~11 comments above :) ), but now I think it's a bad idea because:

1) It has the same downside that the iframe, it breaks interaction from other elements in the page such as global CSS or Javascript. That is because the CSS ID rules do not match anymore, and the Javascript cannot
access the elements anymore, because the IDs have been dynamically rewritten.
At least with the iframe solution we can embed d3.js code or CSS rules in the
iframe and they still work on the SVG.

2) The implementation is much more error prone and costly than the iframe: it requires parsing the SVG and there are many tricky cases for IDs.

@Carreau
IPython member

Agreed that this is suboptimal to search and replace ids.

I think this would be easier to "fix" once we have the plug-able mechanism for arbitrary mimetype.
It would be easier to dispatch to another handler based on some metadata.

We should also try to think that as a language agnostic issue.

@pablooliveira

I updated #4250 to disable SVG iframe wrapping by default.
But it can be enabled by the user by calling the code.display.SVG constructor with scoped=True.

I agree with @Carreau that using a special mimetype for this would be nice, whenever support for that comes.

In the meantime, the proposed patch adds a class descriptor to the svg named "ipython-scoped".
It fixes the SVG issues without being very intruding in my opinion.

@Carreau

We should also try to think that as a language agnostic issue.

Could you please clarify this point?

@Carreau
IPython member

We should also try to think that as a language agnostic issue.
Could you please clarify this point?

The notebook format as well as the message specs try to make as little assumption as possible from the fact that
the kernel is IPython. In particular the biggest use of SVG is made by the Julia kernel and their plotting library that by default send SVG. They probably can't (or don't want) to modify the SVG on the fly to add classes (especially if thoses classes start with IPython.

So knowing that usually you can send from the kernel (<- did not write python) either (data) or (data,metadata). Could you think of a mechanism that allow to decide wether svg should be scoped/iframed/embeded/whatever.

You can add more question to that like :
- Is this mechanism also easy applicable to nbconvert (and the different format it can output) ?
- Does this modify data (if it does then it is probably bad as other application might try to use data and crash on them.) ?
- Can I fallback gracefully if my application does not support metadata in ipynb file ?

@ellisonbg
IPython member
@pablooliveira pablooliveira added a commit to pablooliveira/ipython that referenced this issue Oct 29, 2013
@pablooliveira pablooliveira outputarea.js: Wrap inline SVGs inside an iframe
When multiple inline SVGs are included in a single document,
they share the same parse tree. Therefore, style collisions and
use id collisions can occur and upset the rendering.

This patch wraps each SVG inside an individual iframe, ensuring
that SVG's declarations do not collide.

(The SVG representation is kept as XML and not converted to a binary
format, so I do not think this approach precludes the use of d3.js)

Tested on:
* Chrome Version 29.0.1547.57 Debian 7.1 (217859)
* Mozilla/5.0 (X11; Linux x86_64; rv:17.0) Gecko/20130806 Firefox/17.0 Iceweasel/17.0.8

Closes #1866
333abbe
@ivanov ivanov closed this in #4250 Nov 11, 2013
@minrk minrk added this to the 2.0 milestone Mar 26, 2014
@mattvonrocketstein mattvonrocketstein pushed a commit to mattvonrocketstein/ipython that referenced this issue Nov 3, 2014
@pablooliveira pablooliveira outputarea.js: Wrap inline SVGs inside an iframe
When multiple inline SVGs are included in a single document,
they share the same parse tree. Therefore, style collisions and
use id collisions can occur and upset the rendering.

This patch wraps each SVG inside an individual iframe, ensuring
that SVG's declarations do not collide.

(The SVG representation is kept as XML and not converted to a binary
format, so I do not think this approach precludes the use of d3.js)

Tested on:
* Chrome Version 29.0.1547.57 Debian 7.1 (217859)
* Mozilla/5.0 (X11; Linux x86_64; rv:17.0) Gecko/20130806 Firefox/17.0 Iceweasel/17.0.8

Closes #1866
f218f71
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment