import plotnine as p9
import numpy as np
[docs]class text:
def __init__(self, label, element_text=None, theme=None):
"""
create a new text object
Arguments
---------
label : string
text label with desirable format (e.g. sympy, etc.)
element_text : plotnine.themes.elements.element_text
element object from plotnine, can also be added later
theme : plotnine.theme object
theme object from plotnine, can be associated later
should only provide a theme or element_text (as a theme
contains an element_text)
Notes
-----
This class leverages matplotlib to create the text (given that
matplotlib can create path-style objects for text if the individual
is worried that the svg text visually won't be preserved - this is also
annoying that this is the default).
Note the function use the ``text`` attribute - NOT the
``plot_title`` attribute if a theme is provided.
"""
self.label = label
if element_text is not None and theme is not None:
raise ValueError("please provide only a theme or element_text, "+
"not both")
self.element_text = None # prep initialization
self._clean_element_text(element_text)
self.theme = theme
def _clean_element_text(self, element_text):
"""
cleans element_text to make sure any element_text() is a
p9.themes.themeable, not just a p9.theme.elements.element_text
Arguments
---------
element_text : p9's element_text
element text added to or initialized with the text object. Can
be None, a p9.theme.elements.element_text or a p9.themes.themeable
Notes
-----
updates i place
"""
if element_text is None:
element_text = None
if isinstance(element_text, p9.themes.elements.element_text):
et_themeable = p9.theme(text = element_text
).themeables.get("text")
element_text = et_themeable
else:
element_text = element_text
if self.element_text is not None:
self.element_text.merge(element_text)
else:
self.element_text = element_text
return None # just a reminder
def __add__(self, other):
"""
add element_text or theme to update format
TODO: make this work w.r.t patchwork approaches (edge cases #8)
Arguments
---------
other : plotnine.themes.elements.element_text or theme
theme or element_text to define the attributes of the text.
Notes
-----
Note the function use the ``text`` attribute - NOT the ``plot_title``
attribute if a theme is provided.
"""
if not isinstance(other, p9.themes.themeable.element_text) and \
not isinstance(other, p9.theme):
raise ValueError("text objects are only allowed to be combined "+
"with element_text objects.")
# need to update theme or element_text...
if isinstance(other, p9.themes.themeable.element_text):
self._clean_element_text(other)
# update theme if it already exists
# (if not we'll update it when it's required)
if self.theme is not None:
self.theme += p9.theme(text = self.element_text.theme_element)
if isinstance(other, p9.theme):
if self.theme is None:
self.theme = other
else:
self.theme.add_theme(other)
new_element_text = self.theme.themeables.get("text")
self._clean_element_text(new_element_text)
return self
def _provide_complete_theme(self):
"""
It should be here that the current global theme is accessed, and thing are completed...
"""
if self.theme is None:
current_theme = p9.theme_get()
# and update with our element_text
if self.element_text is not None:
# problem here... (need to update themeable.get("text") instead)
current_theme += p9.theme(text=self.element_text.theme_element)
else:
current_theme = self.theme
return current_theme
def _inner_prep(self):
"""
Internal function to create matplotlib figure with text and
provide a bounding box for the location in the plot
Returns
-------
fig : matplotlib.figure.Figure
figure with text at (0,0), no axes
bbox : matplotlib.transforms.Bbox
bbox location of text in figure
"""
# apply theme/element_text correctly ------
# mirrors code in p9.ggplot.draw_title() and
# p9.themeable.plot_title.apply_figure()
# code references:
# - https://github.com/has2k1/plotnine/blob/9fbb5f77c8a8fb8c522eb88d4274cd2fa4f8dd88/plotnine/ggplot.py#L545
# - https://github.com/has2k1/plotnine/blob/6c82cdc20d6f81c96772da73fc07a672a0a0a6ef/plotnine/themes/themeable.py#L361
#
# collect desirable theme and properties --------
theme = self._provide_complete_theme()
text_themeable = theme.themeables.get('text')
properties = text_themeable.properties.copy()
# create text and apply ----------
# https://stackoverflow.com/questions/24581194/matplotlib-text-bounding-box-dimensions
fig, ax = plt.subplots()
fig.set_dpi(96)
txt = fig.text(x=0.000, y=0.000, s=self.label)
with suppress(KeyError):
del properties['margin']
with suppress(KeyError):
txt.set(**properties)
bbox = txt.get_window_extent(fig.canvas.get_renderer())
# remove background structure from mpl ----------
# help from: https://stackoverflow.com/questions/21687571/matplotlib-remove-patches-from-figure
fig.axes.pop()
plt.axis('off')
# bbox aids in cutting all but the desired image
return fig, bbox
def _create_svg_object(self, width=None, height=None):
"""
Internal to create svg object (with text in correct location
and correct image size)
Arguments
---------
width : float
width of desired output (in inches)
height : float
height of desired output (in inches)
Returns
-------
svg_obj : svgutils.transform.SVGFigure
svg representation of text with correct format and image size
"""
fig, bbox = self._inner_prep()
if width is not None:
width = to_pt(width, "in")
if height is not None:
height = to_pt(height, "in")
# get original matplotlib image ------------
fid = io.StringIO()
fig.savefig(fid, bbox_inches=fig.bbox_inches, format='svg')
fid.seek(0)
image_string = fid.read()
img = sg.fromstring(image_string)
img_size = transform_size((img.width, img.height))
# location correction for alignment and margins -------
current_theme = self._provide_complete_theme()
ha_str = current_theme.themeables.get("text").properties.get("ha")
va_str = current_theme.themeables.get("text").properties.get("va")
margin_dict = current_theme.themeables.get("text").properties.get("margin")
margin_dict_pt = {"t": to_pt(margin_dict["t"], margin_dict["units"]),
"b": to_pt(margin_dict["b"], margin_dict["units"]),
"l": to_pt(margin_dict["l"], margin_dict["units"]),
"r": to_pt(margin_dict["r"], margin_dict["units"])}
if width is None:
width = to_pt(bbox.width, 'px') + margin_dict_pt["l"] + margin_dict_pt["r"]
if height is None:
height = to_pt(bbox.height, 'px') + margin_dict_pt["t"] + margin_dict_pt["b"]
if ha_str == "center":
x_shift = width/2 - to_pt(bbox.width, "px") /2
elif ha_str == "right":
x_shift = width - to_pt(bbox.width, "px") - margin_dict_pt["r"]
else: # ha_str == "left"
x_shift = margin_dict_pt["l"]
if va_str == "center":
y_shift = height/2 - to_pt(bbox.height, "px") /2
elif va_str == "bottom":
y_shift = height - to_pt(bbox.height, "px") - margin_dict_pt["b"]
else: # va_str = "top"
y_shift = margin_dict_pt["t"]
# prep translated and smaller canvas image ----------
# note that bbox coordinates are with respect to a bottom left axis
# (aka cartesian)
# and then we need to convert it to svg's top left axis
svg_bb_top_left_corner = (to_pt(bbox.x0,
units = "px"), # x_min
img_size[1] - to_pt(bbox.y1,
units = "px")) # height - y_max
new_image_size = (str(width)+"pt",
str(height)+"pt")
# need to declare the viewBox for some reason...
new_image_size_string_val = [re.sub("pt", "", val)
for val in new_image_size]
new_viewBox = "0 0 %s %s" % (new_image_size_string_val[0],
new_image_size_string_val[1])
img_root = img.getroot()
img_root.moveto(x = -1*svg_bb_top_left_corner[0] + x_shift,
y = -1*svg_bb_top_left_corner[1] + y_shift)
img_root_str = img_root.tostr()
new_image = sg.SVGFigure()
new_image.set_size(new_image_size)
new_image.root.set("viewBox", new_viewBox)
# remove patch (lxml.etree) ----------
# img_root2_lxml = etree.fromstring(img_root_str)
# parent = img_root2_lxml.findall(".//{http://www.w3.org/2000/svg}g[@id=\"patch_1\"]")[0]
# to_remove = parent.getchildren()[0]
# parent.remove(to_remove)
# img_root2_str = etree.tostring(img_root2_lxml)
# img2 = sg.fromstring(img_root2_str.decode("utf-8"))
# remove path (xml.etree.ElementTree) ---------
img_root2_xml = ET.fromstring(img_root_str)
parent = img_root2_xml.findall(".//{http://www.w3.org/2000/svg}g[@id=\"patch_1\"]")[0]
to_remove = img_root2_xml.findall(".//{http://www.w3.org/2000/svg}path")[0]
parent.remove(to_remove)
img_root2_xml_str = ET.tostring(img_root2_xml)
img2 = sg.fromstring(img_root2_xml_str.decode("utf-8"))
new_image.append(img2)
# closing plot
plt.close()
return new_image
[docs] def save(self, filename, width=None, height=None):
"""
save text object as image in minimal size object
Arguments
---------
filename : str
width : float
in inches (if None, then tight (w.r.t. to margins))
height : float
in inches (if None, then tight (w.r.t. to margins))
"""
svg_obj = self._create_svg_object(width=width, height=height)
svg_obj.save(filename)
plt.close() # do we need this?