[Matplotlib-users] Arbitrary artist data on SVG elements

Joshua Klein mobiusklein at gmail.com
Sat Apr 1 20:57:19 EDT 2017


I couldn’t find an example in the gallery, but just reading Figure.savefig,
and FigureCanvasBase.print_figure it was pretty clear how extra arguments
would flow to the backend, and that appropriately prefixed keyword
arguments would insulate the the high level API.

Since the preferred approach would be to just migrate this logic into the
SVG backend, it would also be a good opportunity to expose some of the
other parts of the SVG canvas that are otherwise left constant or
effectively constant by association e.g. height vs. viewBox dimensions,
setting other attributes on the <svg> element, and the inclusion of some
extra external components like including <defs> sections as described in
svg_filter_line <http://matplotlib.org/examples/misc/svg_filter_line.html>.

Proposed implementation would be:

   1. Add keyword arguments svg_gid_data and svg_attribs to
   FigureCanvasSVG._print_svg to be passed to RendererSVG, which will be
   expected to be Mapping-like objects.
   2. Add svg_gid_data and svg_attribs arguments to RendererSVG.__init__
   and attributes by the same name.
   3. When RendererSVG begins writing the <svg> tag, use the default values
   as written, and those key-value pairs of self.svg_attribs except for
   "defs" and "extra_content" keys.
   4. After completing the opening <svg> tag, if a "extra_content" key is
   in self.svg_attribs, this content will be written verbatim into the
   output stream, where malformed XML will produce invalid markup.
   5. If "defs" is in self.svg_attribs, the value will be written into the
   stream verbatim, (or map a dict of dicts to XML? Seems too much work for
   something I don’t know enough about).
   6. When RendererSVG begins rendering an artist, it will check if the
   artist has an assigned gid by calling Artist.get_gid, and if a gid is
   set, check self.svg_gid_data for additional data to include when opening
   the artist’s appropriate tag. No translation will be done so attribute
   names will be used as-is. This could be used to set on<event> handlers
   and set the class attribute, as well as adding data-<name> attributes
   for adding semantic data to the graphical elements.

I can also fix an omission in FigureCanvasSVG.print_svgz failing to
propagate **kwargs to _print_svg.

Thank you,
Joshua Klein

On Sat, Apr 1, 2017 at 4:30 PM, Thomas Caswell <tcaswell at gmail.com> wrote:

> Joshua,
> That is an interesting use case!
> I am hesitant to add this attribute to Artist because it is very specific
> to the SVG backend (none of the other backends would make use of this as
> far as I know). On the other hand, a generic way to use gid to add extra
> information in the SVG backend could be interesting.  I am pretty sure
> there are examples of optianal backend-specific kwargs going into
> `savefig`, what would the API for that look like?
> Tom
> On Wed, Mar 22, 2017 at 4:42 PM Joshua Klein <mobiusklein at gmail.com>
> wrote:
>> Hello,
>> I often embed figures as SVG graphics in web pages. As part of this
>> process, I usually do the following
>>    1.
>>    Set gids on artists and link that gid to a set of data describing
>>    that part of a graphic in an external dictionary. This includes things like
>>    setting the element’s class, extra contextual information, information that
>>    would be good to show in a tooltip, ids of related elements, and so forth.
>>    2.
>>    Serialize the figure into a file-like object, use an element tree
>>    implementation’s XMLID to get an element id map and Element objects
>>    3.
>>    Iterate over my data dictionary from (1) and set keys in the mapped
>>    Element’s attrib dictionary, using the id map from (2)
>>    4.
>>    Use the element tree implementation’s tostring function to serialize
>>    the updated Element objects back into a string and then send the string out
>>    as a response to a web request.
>>    5.
>>    After receiving the SVG string from the server on the client, add the
>>    SVG to the page’s DOM and then hang event handlers on it (or pre-specify
>>    delegated handlers) that use the added attributes to configure interactive
>>    behavior.
>> I looked at the Artist type and saw no good place to store “arbitrary
>> data”. Before I start working on this I wanted to know if anyone else had a
>> better solution. I would also like to know if the devs would be opposed to
>> a PR that adds an extra dictionary/attribute to every Artist instance
>> created.
>> Another alternative solution would be to find a way to push my dictionary
>> mapping gids to extra attributes into the SVGRenderer and have it pass
>> them as **extras to XMLWriter.element when it processes individual
>> artists.
>> Here’s a generic example of what I do currently:
>> def plot_with_extras_for_svg(*data, **kwargs):
>>     # Do the plotting, generating the id-linked data in `id_mapper`
>>     ax, id_mapper = plot_my_data(*data, **kwargs)
>>     xlim = ax.get_xlim()
>>     ylim = ax.get_ylim()
>>     # compute the total space used in both dimensions when dealing with
>>     # negative axis bounds
>>     x_size = sum(map(abs, xlim))
>>     y_size = sum(map(abs, ylim))
>>     # Map the used axis space to the drawable region dimensions
>>     aspect_ratio = x_size / y_size
>>     canvas_x = 8.
>>     canvas_y = canvas_x / aspect_ratio
>>     # Configure the artist to draw within the new drawable region bounds
>>     fig = ax.get_figure()
>>     fig.tight_layout(pad=0.2)
>>     fig.patch.set_visible(False)
>>     fig.set_figwidth(canvas_x)
>>     fig.set_figheight(canvas_y)
>>     ax.patch.set_visible(False)
>>     # Perform the first serialization
>>     buff = StringIO()
>>     fig.savefig(buff, format='svg')
>>     # Parse XML buffer from `buff` and configure tag attributes
>>     root, ids = ET.XMLID(buff.getvalue())
>>     root.attrib['class'] = 'plot-class-svg'
>>     for id, attributes in id_mapper.items():
>>         element = ids[id]
>>         element.attrib.update({("data-" + k): str(v)
>>                                for k, v in attributes.items()})
>>         element.attrib['class'] = id.rsplit('-')[0]
>>     # More drawable space shenanigans
>>     min_x, min_y, max_x, max_y = map(int, root.attrib["viewBox"].split(" "))
>>     min_x += 100
>>     max_x += 200
>>     view_box = ' '.join(map(str, (min_x, min_y, max_x, max_y)))
>>     root.attrib["viewBox"] = view_box
>>     width = float(root.attrib["width"][:-2]) * 1.75
>>     root.attrib["width"] = "100%"
>>     height = width / (aspect_ratio)
>>     root.attrib["height"] = "%dpt" % (height * 1.2)
>>     root.attrib["preserveAspectRatio"] = "xMinYMin meet"
>>     # Second serialization
>>     svg = ET.tostring(root)
>>     plt.close(fig)
>>     return svg
>> Thank you
>>>> _______________________________________________
>> Matplotlib-users mailing list
>> Matplotlib-users at python.org
>> https://mail.python.org/mailman/listinfo/matplotlib-users
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/matplotlib-users/attachments/20170401/7bf53e49/attachment-0001.html>

More information about the Matplotlib-users mailing list