It’s definitely possible with Plotly and layout images with a little bit of maths and fiddling!
Here’s my copy of the above chart with plotly
import numpy as np
import pandas as pd
import plotly.express as px
values = pd.Series(
{
"Other renewables": 1614,
"Biofuels": 1102,
"Solar": 1793,
"Wind": 3540,
"Hydro": 10455,
}
)
height = 450
width = 1000
font_size = 16
bar_gap = 0.2
max_icons = 110
margin_x = 20
bg_color = "#142229"
plot_aspect_ratio = (width - (2 * margin_x)) / (height - 150 - font_size)
bar_width = (width - 180) / len(values) * (1 - bar_gap)
bar_max_height = (height - 150 - font_size)
magnitude = 10 ** np.floor(np.log(values.max() / max_icons) / np.log(10))
icon_magnitude = np.ceil((values.max() / max_icons) / magnitude) * magnitude
max_n_icons = np.ceil(values.max() / icon_magnitude)
possible_n_cols = np.arange(4, 15)
n_cols = possible_n_cols[
np.argmin(np.abs(np.ceil(max_n_icons / possible_n_cols) / possible_n_cols - bar_max_height / bar_width))
]
df = (
values.to_frame("value")
.rename_axis("key")
.reset_index()
.assign(
text_height=(np.ceil(values / icon_magnitude / n_cols) * icon_magnitude * n_cols).to_numpy(),
pos=lambda df: list(range(len(values))),
)
)
fig = (
px.bar(
df,
x="pos",
y="text_height",
template="plotly_dark",
width=width,
height=height,
custom_data=["value"],
)
.update_traces(
marker={"color": "rgba(0,0,0,0)", "line_width": 0},
texttemplate="%{customdata[0]:,.0f} TWh", textposition="outside",
hovertemplate="%{customdata[0]:,.0f} TWh",
)
.update_yaxes(showgrid=False, title=None, showticklabels=False, constraintoward="bottom", zeroline=False)
.update_xaxes(showgrid=False, title=None, tickvals=list(range(len(values))), ticktext=values.index.to_list())
.update_layout(
yaxis_scaleanchor="x",
yaxis_scaleratio=len(values)/plot_aspect_ratio/values.max(),
showlegend=False,
bargap=bar_gap,
margin_pad=10,
margin_l=margin_x,
margin_r=margin_x,
paper_bgcolor=bg_color,
plot_bgcolor=bg_color,
)
)
fig.add_annotation(
xref="paper",
yref="paper",
xanchor="left",
yanchor="top",
xshift=margin_x,
yshift=40,
x=0,
y=1,
text="Renewable energy across the world",
ax=0,
ay=0,
font={"size": 20, "color": "#fff"}
)
fig.add_annotation(
xref="paper",
yref="paper",
xanchor="left",
yanchor="top",
xshift=margin_x,
yshift=12,
x=0,
y=1,
text="Primary energy consumption of renewable energy sources, 2019",
ax=0,
ay=0,
font={"size": 16, "color": "#fff"}
)
fig.add_annotation(
xref="paper",
yref="paper",
xanchor="left",
yanchor="top",
xshift=margin_x,
yshift=-12,
x=0,
y=1,
text="Source: Our world in data",
ax=0,
ay=0,
font={"size": 12, "color": "#fff"}
)
sizex = (1 - bar_gap) / n_cols
sizey = values.max() / np.ceil(max_n_icons / n_cols)
icons = {
"Solar": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 256 256'%3E%3Cpath fill='%23FF6D00' d='M120 40V16a8 8 0 0 1 16 0v24a8 8 0 0 1-16 0m8 24a64 64 0 1 0 64 64a64.07 64.07 0 0 0-64-64m-69.66 5.66a8 8 0 0 0 11.32-11.32l-16-16a8 8 0 0 0-11.32 11.32Zm0 116.68l-16 16a8 8 0 0 0 11.32 11.32l16-16a8 8 0 0 0-11.32-11.32M192 72a8 8 0 0 0 5.66-2.34l16-16a8 8 0 0 0-11.32-11.32l-16 16A8 8 0 0 0 192 72m5.66 114.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32-11.32ZM48 128a8 8 0 0 0-8-8H16a8 8 0 0 0 0 16h24a8 8 0 0 0 8-8m80 80a8 8 0 0 0-8 8v24a8 8 0 0 0 16 0v-24a8 8 0 0 0-8-8m112-88h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16'/%3E%3C/svg%3E",
"Wind": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='%232DB958' d='M12 2c3.292 0 6 2.435 6 5.5c0 1.337-.515 2.554-1.369 3.5H21a1 1 0 0 1 1 1c0 3.292-2.435 6-5.5 6c-1.336 0-2.553-.515-3.5-1.368V21a1 1 0 0 1-1 1c-3.292 0-6-2.435-6-5.5c0-1.336.515-2.553 1.368-3.5H3a1 1 0 0 1-1-1c0-3.292 2.435-6 5.5-6c1.337 0 2.554.515 3.5 1.369V3a1 1 0 0 1 1-1'/%3E%3C/svg%3E",
"Biofuels": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='%23FF4136' d='m22 3.41l-.12-1.26l-1.2.4a13.84 13.84 0 0 1-6.41.64a11.87 11.87 0 0 0-6.68.9A7.23 7.23 0 0 0 3.3 9.5a9 9 0 0 0 .39 4.58a16.6 16.6 0 0 1 1.18-2.2a9.85 9.85 0 0 1 4.07-3.43a11.16 11.16 0 0 1 5.06-1A12.08 12.08 0 0 0 9.34 9.2a9.48 9.48 0 0 0-1.86 1.53a11.38 11.38 0 0 0-1.39 1.91a16.39 16.39 0 0 0-1.57 4.54A26.42 26.42 0 0 0 4 22h2a30.69 30.69 0 0 1 .59-4.32a9.25 9.25 0 0 0 4.52 1.11a11 11 0 0 0 4.28-.87C23 14.67 22 3.86 22 3.41'/%3E%3C/svg%3E",
"Hydro": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 512 512'%3E%3Cpath fill='%232ECEC4' d='M265.12 60.12a12 12 0 0 0-18.23 0C215.23 97.15 112 225.17 112 320c0 88.37 55.64 144 144 144s144-55.63 144-144c0-94.83-103.23-222.85-134.88-259.88M272 412a12 12 0 0 1-11.34-16a11.89 11.89 0 0 1 11.41-8A60.06 60.06 0 0 0 332 328.07a11.89 11.89 0 0 1 8-11.41A12 12 0 0 1 356 328a84.09 84.09 0 0 1-84 84'/%3E%3C/svg%3E",
"Other renewables": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='%23C658C0' fill-rule='evenodd' d='M9.586 2.586A2 2 0 0 1 11 2h2a2 2 0 0 1 2 2v.089l.473.196l.063-.063a2 2 0 0 1 2.828 0l1.414 1.414a2 2 0 0 1 0 2.827l-.063.064l.196.473H20a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2h-.089l-.196.473l.063.063a2 2 0 0 1 0 2.828l-1.414 1.414a2 2 0 0 1-2.828 0l-.063-.063l-.473.196V20a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-.089l-.473-.196l-.063.063a2 2 0 0 1-2.828 0l-1.414-1.414a2 2 0 0 1 0-2.827l.063-.064L4.089 15H4a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h.09l.195-.473l-.063-.063a2 2 0 0 1 0-2.828l1.414-1.414a2 2 0 0 1 2.827 0l.064.063L9 4.089V4a2 2 0 0 1 .586-1.414M8 12a4 4 0 1 1 8 0a4 4 0 0 1-8 0' clip-rule='evenodd'/%3E%3C/svg%3E",
}
fig.add_layout_image(
source=icons.get("Hydro"),
xref="paper",
yref="paper",
xanchor="left",
yanchor="top",
x=20 / width,
y=1 - 60 / height,
sizex=20 / width,
sizey=20 / height,
sizing="contain",
opacity=1,
layer="above"
)
fig.add_annotation(
xref="paper",
yref="paper",
xanchor="left",
yanchor="top",
xshift=margin_x + 20,
yshift=-40,
x=0,
y=1,
text=f"= {icon_magnitude} TWh",
ax=0,
ay=0,
font={"size": 12, "color": "#fff"}
)
for i, (k, v) in enumerate(values.items()):
for j in np.arange(0, np.ceil(v / icon_magnitude)):
x, y = j % n_cols, j // n_cols
fig.add_layout_image(
source=icons.get(k),
xref="x",
yref="y",
xanchor="center",
yanchor="top",
x=i - (1 - bar_gap) / 2 + (x + 1 / 2) * sizex,
y=(y + 1) * sizey,
sizex=sizex * 0.9,
sizey=sizey * 0.9,
sizing="contain",
opacity=1,
layer="above"
)
if j == np.ceil(v / icon_magnitude) - 1:
fig.add_shape(
type="rect",
x0=i - (1 - bar_gap) / 2 + x * sizex,
x1=i - (1 - bar_gap) / 2 + (x + 1) * sizex,
y0=(y + 1) * sizey,
y1=(y + 1 - (np.ceil(v / icon_magnitude) - v / icon_magnitude)) * sizey,
fillcolor=bg_color,
line_width=0,
)
fig.show()