Quantcast

Arbitrary artist data on SVG elements

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
6 messages Options
Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Arbitrary artist data on SVG elements

Joshua Klein

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
[hidden email]
https://mail.python.org/mailman/listinfo/matplotlib-users
Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Arbitrary artist data on SVG elements

tcaswell
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 <[hidden email]> 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
[hidden email]
https://mail.python.org/mailman/listinfo/matplotlib-users

_______________________________________________
Matplotlib-users mailing list
[hidden email]
https://mail.python.org/mailman/listinfo/matplotlib-users
Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Arbitrary artist data on SVG elements

Joshua Klein

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.

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 <[hidden email]> 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 <[hidden email]> 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
[hidden email]
https://mail.python.org/mailman/listinfo/matplotlib-users


_______________________________________________
Matplotlib-users mailing list
[hidden email]
https://mail.python.org/mailman/listinfo/matplotlib-users
Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Arbitrary artist data on SVG elements

tcaswell
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 <[hidden email]> 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.

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 <[hidden email]> 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 <[hidden email]> 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
[hidden email]
https://mail.python.org/mailman/listinfo/matplotlib-users


_______________________________________________
Matplotlib-users mailing list
[hidden email]
https://mail.python.org/mailman/listinfo/matplotlib-users
Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Arbitrary artist data on SVG elements

Joshua Klein

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 <[hidden email]> 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 <[hidden email]> 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.

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 <[hidden email]> 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 <[hidden email]> 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
[hidden email]
https://mail.python.org/mailman/listinfo/matplotlib-users



_______________________________________________
Matplotlib-users mailing list
[hidden email]
https://mail.python.org/mailman/listinfo/matplotlib-users
Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Arbitrary artist data on SVG elements

Joshua Klein

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 <[hidden email]> 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 <[hidden email]> 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 <[hidden email]> 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.

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 <[hidden email]> 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 <[hidden email]> 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
[hidden email]
https://mail.python.org/mailman/listinfo/matplotlib-users




_______________________________________________
Matplotlib-users mailing list
[hidden email]
https://mail.python.org/mailman/listinfo/matplotlib-users
Loading...