from accessiplot.utils.chart_type import ChartTypes
from accessiplot.detection.handler import DetectionHandler
from matplotlib.colors import to_rgb
import warnings
__all__ = [
'is_contrast_ratio_below_threshold',
'calculate_contrast_ratios_from_ax',
'calculate_contrast_ratio_histogram',
'calculate_contrast_ratio_lines',
'calculate_contrast_ratio',
'calculate_relative_luminance',
'normalize'
]
DELIMITER = "_---_"
BACKGROUND = "BACKGROUND"
[docs]def is_contrast_ratio_below_threshold(contrast_ratio: float, threshold: float = 2.5):
"""
Pass in a contrast ratio and it will detect if it is below a limit.
Parameters
----------
contrast_ratio : float
The contrast ratio calculated using the WCAG 2.0 definition.
threshold : float
Threshold to detect if the contrast ratio is below it.
Returns
-------
is_below_threshold: bool
Result of comparison of threshold and contrast ratio.
"""
# TODO: This needs a bit more love to detect what threshold to use automatically by context
is_below_threshold = False
if contrast_ratio < threshold:
is_below_threshold = True
return is_below_threshold
[docs]def calculate_contrast_ratios_from_ax(dh: DetectionHandler):
"""
Generates dictionaries of the contrast_ratios_by_index and detections.
Also a list of colors_by_index is generated to keep track of the colors
that are associated with the contrast ratios.
It determines the processing logic based on the chart type from the
DetectionHandler object.
Parameters
----------
dh : accessiplot.detection.handler.DetectionHandler
A DetectionHandler object that contains the axes object and histogram object if necessary.
Returns
-------
contrast_ratios_by_index: dict
Dictionary where the key is the indices of the lines/background being compared
and the value is the contrast ratio as a float.
colors_by_index: list
List where each element is a tuple of `(r, g, b)` where `r`, `g`, and `b` are
normalized from the 0->255 value down to a 0->1 value.
detections: dict
Dictionary where the key is the indices of the lines/background being compared
and the value is the contrast ratio as a float.
"""
contrast_ratios_by_index = {}
colors_by_index = []
detections = {}
if dh.chart_type == ChartTypes.LINE_CHART.name:
contrast_ratios_by_index, colors_by_index, detections = calculate_contrast_ratio_lines(dh)
elif dh.chart_type == ChartTypes.HISTOGRAM.name:
contrast_ratios_by_index, colors_by_index, detections = calculate_contrast_ratio_histogram(dh)
else:
warnings.warn(f"chart_type:{dh.chart_type} is unsupported! Returning dummy values for analysis")
return contrast_ratios_by_index, colors_by_index, detections
def calculate_contrast_ratio_histogram(dh: DetectionHandler):
"""
Generates a dictionary of contrast ratios from an Axes object,
the corresponding colors of the lines and background, and
a dictionary of comparisons that have a contrast ratio below
a given threshold.
1) Get color of all bins as rgb and append to list
2) Get color of the background and append to list
3) Do an n**2 comparison of colors in the above list and generate contrast ratios.
These are stored as key/value mappings where the key is `<index_color1>_<index_color2>`.
Parameters
----------
dh : accessiplot.detection.handler.DetectionHandler
A DetectionHandler object that contains the axes object and histogram object if necessary.
Returns
-------
contrast_ratios_by_index: dict
Dictionary where the key is the indices of the lines/background being compared
and the value is the contrast ratio as a float.
line_colors: list
List where each element is a tuple of `(r, g, b)` where `r`, `g`, and `b` are
normalized from the 0->255 value down to a 0->1 value.
detections: dict
Dictionary where the key is the indices of the lines/background being compared
and the value is the contrast ratio as a float.
"""
if dh.histogram is None:
warnings.warn("The DetectionHandler object does not have histogram metadata associated with it."
"Skipping histogram processing.")
return {}, [], {}
colors = []
for rect in dh.histogram[2]:
rect_color = rect.get_facecolor()
if rect_color not in colors:
colors.append(to_rgb(rect_color))
colors.append(to_rgb(dh.ax.get_facecolor()))
contrast_ratios_by_index = {}
detections = {}
for i in range(len(colors)):
for j in range(len(colors)):
contrast_ratios_by_index[f'{i}{DELIMITER}{j}'] = \
calculate_contrast_ratio(colors[i], colors[j])
for key in contrast_ratios_by_index.keys():
ind_str1, ind_str2 = key.split(DELIMITER)
if ind_str1 == ind_str2:
continue # Don't do self-analysis for contrast ratio.
if is_contrast_ratio_below_threshold(contrast_ratios_by_index[key]):
# TODO: Need to do better handling of threshold based on plot
detections[key] = contrast_ratios_by_index[key]
return contrast_ratios_by_index, colors, detections
def calculate_contrast_ratio_lines(dh: DetectionHandler):
"""
Generates a dictionary of contrast ratios from an Axes object,
the corresponding colors of the lines and background, and
a dictionary of comparisons that have a contrast ratio below
a given threshold.
1) Get color of all lines as rgb and append to list
2) Get color of the background and append to list
3) Do an n**2 comparison of colors in the above list and generate contrast ratios.
These are stored as key/value mappings where the key is `<index_color1>_<index_color2>`.
Parameters
----------
dh : accessiplot.detection.handler.DetectionHandler
A DetectionHandler object that contains the axes object and histogram object if necessary.
Returns
-------
contrast_ratios_by_index: dict
Dictionary where the key is the indices of the lines/background being compared
and the value is the contrast ratio as a float.
line_colors: list
List where each element is a tuple of `(r, g, b)` where `r`, `g`, and `b` are
normalized from the 0->255 value down to a 0->1 value.
detections: dict
Dictionary where the key is the indices of the lines/background being compared
and the value is the contrast ratio as a float.
"""
lines = dh.ax.lines
colors = [to_rgb(line.get_color()) for line in lines]
colors.append(to_rgb(dh.ax.get_facecolor()))
contrast_ratios_by_index = {}
detections = {}
for i in range(len(colors)):
for j in range(len(colors)):
if i != len(lines):
left = lines[i].get_label()
else:
left = BACKGROUND
if j != len(lines):
right = lines[j].get_label()
else:
right = BACKGROUND
key = f'{left}{DELIMITER}{right}'
contrast_ratios_by_index[key] = \
calculate_contrast_ratio(colors[i], colors[j])
for key in contrast_ratios_by_index.keys():
ind_str1, ind_str2 = key.split(DELIMITER)
if ind_str1 == ind_str2:
continue # Don't do self-analysis for contrast ratio.
if is_contrast_ratio_below_threshold(contrast_ratios_by_index[key]):
# TODO: Need to do better handling of threshold based on plot
detections[key] = contrast_ratios_by_index[key]
return contrast_ratios_by_index, colors, detections
[docs]def calculate_contrast_ratio(rgb1, rgb2):
"""
Calculates the contrast ratio given two tuples of normalized rgb values.
These values must be in the range of [0,1].
Parameters
----------
rgb1: tuple
A tuple of 3 values that are [0,1] which represent the normalized RGB
values converted from the 8-bit 0-255 representation.
rgb2: tuple
A tuple of 3 values that are [0,1] which represent the normalized RGB
values converted from the 8-bit 0-255 representation.
Returns
-------
contrast_ratio: float
Contrast ratio calculated from the relative luminance of two
normalized rgb tuples of values.
References
----------
.. [1] https://www.w3.org/TR/WCAG20/#relativeluminancedef
.. [2] https://contrast-ratio.com/
.. [3] https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html
"""
l1 = calculate_relative_luminance(rgb1)
l2 = calculate_relative_luminance(rgb2)
if l1 > l2:
contrast_ratio = (l1 + 0.05) / (l2 + 0.05)
else:
contrast_ratio = (l2 + 0.05) / (l1 + 0.05)
return contrast_ratio
[docs]def calculate_relative_luminance(rgb: tuple):
"""
Calculates the relative luminance given a tuple of normalized rgb values.
These values must be in the range of [0,1]. This is used to calculate
contrast ratios.
Parameters
----------
rgb: tuple
A tuple of 3 values that are [0,1] which represent the scaled RGB
values converted from the 8-bit 0-255 representation.
Returns
-------
relative_luminance: float
The light intensity of a given color value.
Notes
-----
Observing the coefficients in the formula helps to determine the
contributing effects of `r`, `g`, and `b` to relative luminance. We
can see that `g` gives the greatest contribution to relative luminance.
Raises
------
ValueError
Raises if `r`, `g`, or `b` are not in a normalized [0,1] form.
References
----------
.. [1] https://www.w3.org/TR/WCAG20/#relativeluminancedef
"""
r, g, b = rgb
# Check to make sure the appropriate normalized rgb value is passed in.
if (not 0.0 <= r <= 1.0) or (not 0.0 <= g <= 1.0) or (not 0.0 <= b <= 1.0):
raise ValueError(f"The values of r:{r}, g:{g}, or b:{b} are not all within [0,1]!")
r = normalize(r)
g = normalize(g)
b = normalize(b)
return 0.2126 * r + 0.7152 * g + 0.0722 * b
[docs]def normalize(primary_color: float):
"""
Normalize the , G, or B value using the WCAG 2.0 formula defined
for calculating the relative luminance.
Parameters
----------
primary_color: float
Value for r, g, or b in the scaled in the range [0,1]
Returns
-------
normalized_color: float
Normalizes the value based on if it is `>0.03928` or not.
References
----------
.. [1] https://www.w3.org/TR/WCAG20/#relativeluminancedef
"""
if primary_color <= 0.03928:
normalized_color = primary_color / 12.92
else:
normalized_color = ((primary_color + 0.055) / 1.055) ** 2.4
return normalized_color