[Matplotlib-users] Arbitrary artist data on SVG elements

Joshua Klein mobiusklein at gmail.com
Mon Apr 3 13:34:14 EDT 2017


The described functionality has been implemented and unit tests written to
exercise the new code-paths at
https://github.com/mobiusklein/matplotlib/tree/svg-extra-data, and is
passing the unit test suite on both Py2 and Py3. Before submitting a pull
request I want to document what new features are available, but I’m not
sure where is appropriate. The Figure.savefig and pyplot.savefig docstrings
don’t describe any particular keyword argument forwarding for individual
backends, and the only equivalent interface that is backend-specific that I
am aware of is PdfPages, which instantiates a whole new object that takes
over the figure-saving process.

Most of the names in backend_svg are unmentioned in the documentation
anyway since they don’t have docstrings, instead using something that looks
like doxygen comments above classes and functions. Should I migrate these
to use NumPy-style docstrings so that Sphinx can pick them up, and then
describe the additional features in the module-level and print_svg
docstrings?

Thank you,
Joshua Klein
​

On Sat, Apr 1, 2017 at 10:39 PM, Joshua Klein <mobiusklein at gmail.com> wrote:

> 5 could just be folded into 4 just by writing defs as raw XML. As for
> validating the extra content XML, it’s possible to validate the markup
> syntax, but it’s not easy to validate the SVG semantics of the markup.
> Syntactic validation can be carried out just using the standard library’s
> xml.etree.ElementTree.fromstring for detection, wrapped in a try-except
> to catch the initial error and raise a more informative error.
>>
> On Sat, Apr 1, 2017 at 10:02 PM, Thomas Caswell <tcaswell at gmail.com>
> wrote:
>
>> That seems reasonable (but I don't know the svg backend super well).
>>
>> One concern in the special keys in `svg_attribs`,  would reserving the
>> keys 'defs' and 'extra_content' get in the way of users?  It may be better
>> to do this as 4 parameters.
>>
>> For 5 if you are not sure maybe just skip it for now?
>>
>> For 4 is it possible/reasonable to validate the xml before we write it?
>>
>> On Sat, Apr 1, 2017 at 8:57 PM Joshua Klein <mobiusklein at gmail.com>
>> wrote:
>>
>>> Hello,
>>>
>>> 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/20170403/15ccd151/attachment-0001.html>


More information about the Matplotlib-users mailing list