A lot of times we use matplotlib to plot images and we would like to display them on a HTML page simply for rendering as PDF for download or just for display.
Fortunately this is possible for django backend, here is an example for plotting a horizontal barchart in HTML.
from io import BytesIO
import base64
from matplotlib.figure import Figure
from matplotlib.ticker import MaxNLocator
import seaborn as sns
class HorizontalBarChart:
"""Plots a horizontal barchat for displaying in html"""
@staticmethod
def _set_annotation(fig, rect, max_bar_width):
"""Annotate the value of each bar in the barchart"""
# Rectangle widths are already integer-valued but are floating
# type, so it helps to remove the trailing decimal point and 0 by
# converting width to int type
total = int(rect.get_width())
annotation_inside_bar_params = dict(
xytext=(-5, 0),
color='white',
ha='right', # Horizontal align
)
annotation_to_right_of_bar_params = dict(
xytext=(5, 0),
color='black',
ha='left', # Horizontal align
)
if max_bar_width == 0:
# Empty plot, default annotation to the right of bar
annotation_params = annotation_to_right_of_bar_params
else:
# We default all annotations inside the bar
annotation_params = annotation_inside_bar_params
current_bar_width_percentage = int(total / max_bar_width * 100)
if current_bar_width_percentage < 10:
# current bar size is smaller than 10% of the longest bar, means that
# it may not be wide enough to print its value inside
annotation_params = annotation_to_right_of_bar_params
yloc = rect.get_y() + rect.get_height() / 2
fig.annotate(
total,
xy=(total, yloc),
textcoords="offset points",
va='center',
weight='bold',
clip_on=True,
**annotation_params,
)
@staticmethod
def _to_data_url(fig):
"""
Create a data url for matplotlib Figure for embed in html
How to use it:
In python:
chart = HorizontalBarChart()._to_data_url(data)
In html:
<img src="{{ chart }}">
"""
buf = BytesIO() # Save it to a temporary buffer.
fig.savefig(buf, format='png')
data = base64.b64encode(buf.getbuffer()).decode("ascii")
return f"data:image/png;base64,{data}"
@classmethod
def get_image_url(cls, data, title=None, with_annotation=False):
""" Generate a barplot for given data
:param dict[int] data: input data to plot
:param str title: title for the chart
:param bool with_annotation:
if set, annotate the value of each bar in the chart so its easier to see
defaults to False
:return str: data url of the generated barchart, easy to render in html
"""
x = list(data.values())
y = list(data.keys())
# Create a Figure constructor so it saves to in-memory buffers
fig = Figure(figsize=(10, 5))
# Set temporary plot themes for each plot
with sns.plotting_context("talk"): # Bigger fonts
with sns.axes_style("whitegrid"): # White grid background
ax = fig.subplots()
ax = sns.barplot(x=x, y=y, color='#2b7bba', ax=ax) # Mono color for bars
if title:
ax.set_title(title)
# Make sure we use integer for axis
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
if with_annotation:
max_bar_width = max([rect.get_width() for rect in ax.patches])
# Annotation the value of each bar for a better presentation
for rect in ax.patches:
cls._set_annotation(ax, rect, max_bar_width)
fig.set_tight_layout(tight=True) # less white margines
return cls._to_data_url(fig)
Here is the html template code:
<picture>
<img src="{{ your_generated_chart }}" style="width: 100%;">
</picture>
The key implementations here:
base64
io stream so its in the memory buffer instead of on diskFigure
object in Matplotlib so its thread safeReference: