I'm implementing a simple treemap in Python using Squarify.
I'm plotting the artist name with it's percentage of streams in the considered song chart, the bigger/darker the square, the higher is the value.
My code is the following:
dataGoals = sort_by_streams[sort_by_streams["Streams"]>1]
#Utilise matplotlib to scale our stream number between the min and max, then assign this scale to our values.
norm = matplotlib.colors.Normalize(vmin=min(dataGoals.Streams), vmax=max(dataGoals.Streams))
colors = [matplotlib.cm.Blues(norm(value)) for value in dataGoals.Streams]
#Create our plot and resize it.
fig1 = plt.figure()
ax = fig1.add_subplot()
fig1.set_size_inches(16, 4.5)
#Use squarify to plot our data, label it and add colours. We add an alpha layer to ensure black labels show through
labels = ["%s\n%.2f" % (label) for label in zip(dataGoals.Artist, dataGoals.Streams)]
squarify.plot(label=labels,sizes=dataGoals.Streams, color = colors, alpha=.7, bar_kwargs=dict(linewidth=0.5, edgecolor="#222222"),text_kwargs={'fontsize':15})
plt.title("Streams Percentage",fontsize=23,fontweight="bold")
#Remove our axes and display the plot
plt.axis('off')
plt.show()
And this is the result:
As you might notice, the labels of the smaller squares overlaps and go out of the borders. Is there a way to automatically resize the label in order to fit the square?
EDIT: I tried to implement the autowrap function of matplotlib with the following code: squarify.plot(label=labels,sizes=dataGoals.Streams, color = colors, alpha=.7, bar_kwargs=dict(linewidth=0.5, edgecolor="#222222"),text_kwargs={'fontsize':20, 'wrap':True})
but this doesn't solve my problem, my text labels still go out of bounds.
I have the same problem when trying to draw a treemap with squarify
. After some search, I come up with a solution, which seems to work as expected.
import matplotlib.patches as mpatches
import matplotlib.text as mtext
# Refrence https://stackoverflow.com/questions/48079364/wrapping-text-not-working-in-matplotlib
# and https://stackoverflow.com/questions/50742503/how-do-i-get-the-height-of-a-wrapped-text-in-matplotlib
class WrapText(mtext.Text):
def __init__(self,
x=0, y=0, text='',
width=0,
**kwargs):
mtext.Text.__init__(self,
x=x, y=y, text=text,
wrap=True,
**kwargs)
self.width = width # in screen pixels. You could do scaling first
def _get_wrap_line_width(self):
return self.width
def get_lines_num(self):
return len(self._get_wrapped_text().split('\n'))
class WrapAnnotation(mtext.Annotation):
def __init__(self,
text, xy,
width, **kwargs):
mtext.Annotation.__init__(self,
text=text,
xy=xy,
wrap=True,
**kwargs)
self.width = width
def _get_wrap_line_width(self):
return self.width
def get_lines_num(self):
return len(self._get_wrapped_text().split('\n'))
def text_with_autofit(self, txt, xy, width, height, *,
transform=None,
ha='center', va='center',
wrap=False, show_rect=False,
min_size=1, adjust=0,
**kwargs):
if transform is None:
if isinstance(self, Axes):
transform = self.transData
if isinstance(self, Figure):
transform = self.transFigure
x_data = {'center': (xy[0] - width/2, xy[0] + width/2),
'left': (xy[0], xy[0] + width),
'right': (xy[0] - width, xy[0])}
y_data = {'center': (xy[1] - height/2, xy[1] + height/2),
'bottom': (xy[1], xy[1] + height),
'top': (xy[1] - height, xy[1])}
(x0, y0) = transform.transform((x_data[ha][0], y_data[va][0]))
(x1, y1) = transform.transform((x_data[ha][1], y_data[va][1]))
# rectange region size to constrain the text
rect_width = x1 - x0
rect_height = y1- y0
fig = self.get_figure() if isinstance(self, Axes) else self
dpi = fig.dpi
rect_height_inch = rect_height / dpi
fontsize = rect_height_inch * 72
if isinstance(self, Figure):
if not wrap:
text = self.text(*xy, txt, ha=ha, va=va, transform=transform,
fontsize=min_size,
**kwargs)
else:
fontsize /= 2
text = WrapText(*xy, txt, width=rect_width, ha=ha, va=va,
transform=transform, fontsize=fontsize,
**kwargs)
self.add_artist(text)
if isinstance(self, Axes):
if not wrap:
text = self.annotate(txt, xy, ha=ha, va=va, xycoords=transform,
fontsize=min_size,
**kwargs)
else:
fontsize /= 2
text = WrapAnnotation(txt, xy, ha=ha, va=va, xycoords=transform,
fontsize=fontsize, width=rect_width,
**kwargs)
self.add_artist(text)
while fontsize > min_size:
text.set_fontsize(fontsize)
bbox = text.get_window_extent(fig.canvas.get_renderer())
bbox_width = bbox.width / text.get_lines_num() if wrap else bbox.width
if bbox_width <= rect_width:
while bbox_width <= rect_width:
fontsize += 1
text.set_fontsize(fontsize)
bbox = text.get_window_extent(fig.canvas.get_renderer())
bbox_width = bbox.width / text.get_lines_num() if wrap else bbox.width
else:
fontsize = fontsize - 1
text.set_fontsize(fontsize)
break;
fontsize /= 2
if fig.get_constrained_layout():
c_fontsize = fontsize + adjust + 0.5
text.set_fontsize(c_fontsize if c_fontsize > min_size else min_size)
if fig.get_tight_layout():
c_fontsize = fontsize + adjust
text.set_fontsize(c_fontsize if c_fontsize > min_size else min_size)
if show_rect and isinstance(self, Axes):
rect = mpatches.Rectangle((x_data[ha][0], y_data[va][0]),
width, height, fill=False, ls='--')
self.add_patch(rect)
return text
This function supports auto-fitting text into a box. If wrap
is True
, then the text will be auto-wrapped according to the size of the box.
The following is the figure with auto-fitting (grow=True
) and auto-wraping (wrap=True
)
The data is G20 from treemapify, which is an excellent R's package to plot a treemap.
Figure with auto-fitting and auto-wraping:
The basic process of auto-fitting is setting the font size according to the height of the box, comparing text width with the box's width and decreasing the font size until the text width is less than box's width.
As for auto-wrapping, the underlying process depends on the built-in auto-wrap in matplotlib by setting wrap=True
. The process of auto-adjusting the fontsize is same.
However, the process of auto-fitting is a little slow. I hope some one can figure out some more efficient algorithm of auto-fitting.
Hope this function can help you.