Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add wai-aria property to the artist accessibility annotations. #21328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions doc/api/artist_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,21 @@ Miscellaneous
Artist.get_in_layout
Artist.stale


Accessibility
-------------


.. autosummary::
:template: autosummary.rst
:toctree: _as_gen
:nosignatures:

Artist.get_aria
Artist.set_aria
Artist.update_aria


Functions
=========

Expand Down
23 changes: 23 additions & 0 deletions doc/users/next_whats_new/aria.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
All ``Arist`` now carry wai-aria data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
All ``Arist`` now carry wai-aria data
All ``Artist``s now carry wai-aria data

-------------------------------------
Comment on lines +1 to +2
Copy link
Member

@QuLogic QuLogic Nov 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
All ``Arist`` now carry wai-aria data
-------------------------------------
All ``Artist`` now carry wai-aria data
--------------------------------------


It is now possible to attach `wai-aria
<https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles>`__ role
information to any `~matplotlib.artist.Artist`. These roles are the
industry standard for providing accessibility mark up on the web. This
information can be used by downstream applications for providing accessible
descriptions of visualizations. Best practices in the space are still
developing, but by providing a mechanism to store and access this information
we will enable this development.

There are three methods provided:

- `~matplotlib.artist.Artist.set_aria` which will completely replace any existing roles.
- `~matplotlib.artist.Artist.update_aria` which will update the current roles in-place.
- `~matplotlib.artist.Artist.get_aria` which will return a copy of the current roles.

We currently do no validation on either the keys or the values.


Matplotlib will use the ``'aria-label'`` role when saving svg output if it is
provided.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be helpful to have an explicit example along with this that shows the structure of the dict() that you're expecting.

aria_dict = {"aria-label": "This is my cool figure"}

But, then we see this in roles: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/textbox_role

  role="textbox"
  contenteditable="true"
  aria-placeholder="5-digit zipcode"
  aria-labelledby="txtboxLabel"

where I'm a bit confused on whether you have a nested dict here or something else?

30 changes: 30 additions & 0 deletions lib/matplotlib/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ def __init__(self):
self._clippath = None
self._clipon = True
self._label = ''
self._aria = {}
self._picker = None
self._rasterized = False
self._agg_filter = None
Expand Down Expand Up @@ -1085,6 +1086,35 @@ def set_in_layout(self, in_layout):
"""
self._in_layout = in_layout

def get_aria(self):
"""Return any ARIA properties assigned to the artist"""
return dict(self._aria)

def set_aria(self, aria):
"""
Set ARIA properties to the artist.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This docstring should define or reference what ARIA means.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should include link.


A primary use of this method is to attach aria-label to the artist to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this clearer that aria-description for raster backends (for alt text).

provide alt text to figures.

Parameters
----------
aria : dict

"""
# TODO validation
if not isinstance(aria, dict):
if aria is not None:
raise TypeError(
f'aria must be dict or None, not {type(aria)}')
Comment on lines +1106 to +1109
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use _api.check_isinstance.

self._aria = aria
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add validation to known aria attributes


def update_aria(self, **aria):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want update_aria or only get/set like most of our other methods.

I think a user could then do something like: a.set_aria(a.get_aria() | new_aria_dict)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming back to this, I am leaning to dropping this method. I can think of two ways you might want to update ("replace what is there" vs "if there is not something there, set these").

I think it is better to leave just set meaning "replace all aria with this dict" and let the user do what ever they want to generate the updated dictionary.

"""Update WAI-ARIA properties on the artist."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only one that says WAI-ARIA instead of ARIA.

# TODO validation
for k, v in aria.items():
self._aria[k] = v
Comment on lines +1115 to +1116
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should get a test?


def get_label(self):
"""Return the label used for this artist in the legend."""
return self._label
Expand Down
12 changes: 9 additions & 3 deletions lib/matplotlib/backends/backend_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ def _check_is_iterable_of_str(infos, key):

class RendererSVG(RendererBase):
def __init__(self, width, height, svgwriter, basename=None, image_dpi=72,
*, metadata=None):
*, metadata=None, aria=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a safe extension of the API as we only use this additional optional key in this file where we know which class we are going to be calling. I do not know if other file formats also support aria roles (as they are more of an html thing and tend to live on the DOM outside of what ever we generate). If it is more general, we can push to other backends and up to base, otherwise I think we should keep this slight extension local to the svg backend.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider ripping this out.

self.width = width
self.height = height
self.writer = XMLWriter(svgwriter)
Expand All @@ -337,6 +337,7 @@ def __init__(self, width, height, svgwriter, basename=None, image_dpi=72,
self._hatchd = {}
self._has_gouraud = False
self._n_gradients = 0
self._aria = dict(aria or {})

super().__init__()
self._glyph_map = dict()
Expand All @@ -350,7 +351,9 @@ def __init__(self, width, height, svgwriter, basename=None, image_dpi=72,
viewBox='0 0 %s %s' % (str_width, str_height),
xmlns="http://www.w3.org/2000/svg",
version="1.1",
attrib={'xmlns:xlink': "http://www.w3.org/1999/xlink"})
attrib={'xmlns:xlink': "http://www.w3.org/1999/xlink"},
**{k: self._aria[k] for k in ['aria-label'] if k in self._aria}
)
self._write_metadata(metadata)
self._write_default_style()

Expand Down Expand Up @@ -1373,9 +1376,12 @@ def print_svg(self, filename, *, bbox_inches_restore=None, metadata=None):
self.figure.dpi = 72
width, height = self.figure.get_size_inches()
w, h = width * 72, height * 72
aria = self.figure.get_aria()
renderer = MixedModeRenderer(
self.figure, width, height, dpi,
RendererSVG(w, h, fh, image_dpi=dpi, metadata=metadata),
RendererSVG(
w, h, fh, image_dpi=dpi, metadata=metadata, aria=aria
),
bbox_inches_restore=bbox_inches_restore)
self.figure.draw(renderer)
renderer.finalize()
Expand Down
7 changes: 7 additions & 0 deletions lib/matplotlib/tests/test_artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,3 +562,10 @@ def draw(self, renderer, extra):

assert 'aardvark' == art.draw(renderer, 'aardvark')
assert 'aardvark' == art.draw(renderer, extra='aardvark')


def test_set_aria():
art = martist.Artist()
with pytest.raises(TypeError, match='^aria must be dict'):
art.set_aria([])
art.set_aria(dict(label="artist alt text"))
17 changes: 17 additions & 0 deletions lib/matplotlib/tests/test_backend_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,3 +609,20 @@ def test_svg_font_string(font_str, include_generic):

assert font_info == f"{size}px {font_str}"
assert text_count == len(ax.texts)


def test_aria():
fig, ax = plt.subplots()

with BytesIO() as fd:
fig.savefig(fd, format="svg")
buf = fd.getvalue()

assert b'aria-label' not in buf

fig.set_aria({'aria-label': 'A test of inserting the label'})
with BytesIO() as fd:
fig.savefig(fd, format="svg")
buf = fd.getvalue()

assert b'aria-label' in buf