Font for japanese character in Matplotlib and Seaborn

Hello,

I am working on a app and I work in Japan so I need to be able to display Kanji. When I worked on my app in localhost I used : plt.rcParams['font.family'] = "Hiragino Sans" to display kanji on matplotlib and seaborn graph but when I deploy it online it doesn’t work and tell me that : " findfont: Font family 'Hiragino Sans' not found.".
I tried to add another font to my app and using the code below but it doesn’t work too.

font_path = "/app/streamlit_app/streamlit_app/Noto_Sans_JP/NotoSansJP-Regular.otf"
mpl.font_manager.FontManager().addfont(path=font_path)
font_prop = mpl.font_manager.FontProperties(fname=font_path)
font_prop.name = "Noto Sans JP-Regular"
mpl.font_manager.fontManager.ttflist.append(font_prop)
rcParams["font.family"] = "Noto Sans JP-Regular"

I welcome any help.

1 Like

Hey @Christophe,

Have you tried using the file’s GitHub URL rather than the path? I’d recommend following the example here.

Thank you, it worked to change the global font of the app but it didn’t work for my plot (matplotlib and seaborn plot).

Hi @Christophe :wave:

Here’s how you should set the font path instead:

# Set the font path
fpath = os.path.join(os.getcwd(), "NOTO_SANS_JP/NotoSansJP-Regular.otf")
prop = fm.FontProperties(fname=fpath)

And then when you’re creating your axes, xticks, title, etc, you should pass fontproperties=prop to those Axes objects. E.g. ax.set_title("日本語タイトル", fontproperties=prop).

Check out a live, working example here:

Expand for full app
import os

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import streamlit as st
from matplotlib import font_manager as fm

# Set the font path
fpath = os.path.join(os.getcwd(), "NOTO_SANS_JP/NotoSansJP-Regular.otf")
prop = fm.FontProperties(fname=fpath)

@st.cache_data
def load_data():
    # Create a dataframe containing japanese characters and plot it with pure matplotlib
    df = pd.DataFrame(
        {
            "x": np.arange(10),
            "y": ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"],
            "label": ["あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ"],
        }
    )

    return df

@st.cache_data
def plot_data(df):
    # Plot the dataframe
    fig, ax = plt.subplots(figsize=(10, 6))
    ax.plot(df["x"], df["y"], label=df["label"])
    ax.legend(prop=prop, loc="upper left")
    ax.set_title("日本語タイトル", fontproperties=prop)
    ax.set_xticks(df["x"])
    ax.set_xticklabels(df["label"], fontproperties=prop)
    ax.set_yticks(df["y"])
    ax.set_yticklabels(df["y"], fontproperties=prop)
    return fig

df = load_data()
fig = plot_data(df)
st.pyplot(fig)

# Now let's try to use seaborn to plot the same dataframe but as a scatter plot
@st.cache_data
def plot_data_seaborn(df):
    sns_fig, sns_ax = plt.subplots(figsize=(10, 6))
    sns.set(font=prop.get_name())
    sns.scatterplot(data=df, x="x", y="y", hue="label")
    handles, labels = sns_ax.get_legend_handles_labels()
    sns_ax.legend(handles, labels, prop=prop, loc="upper right")
    sns_ax.set_title("日本語タイトル", fontproperties=prop)
    sns_ax.set_xticks(df["x"])
    sns_ax.set_xticklabels(df["label"], fontproperties=prop)
    sns_ax.set_yticks(df["y"])
    sns_ax.set_yticklabels(df["y"], fontproperties=prop)
    return sns_fig


sns_fig = plot_data_seaborn(df)
st.pyplot(sns_fig)

https://custom-fonts-matplotlib.streamlit.app/ :point_down:

2 Likes

Thank you for your help, it is interesting but it still didn’t work in my case because I am using sns.pairplot where the x and y axis are already includes in the plot.
When I try your solution I have the message:

 corr_fig = sns.pairplot(st.session_state['data_file_selected'].iloc[:,:],corner=True)
  File "/home/appuser/venv/lib/python3.9/site-packages/seaborn/axisgrid.py", line 2114, in pairplot
    grid = PairGrid(data, vars=vars, x_vars=x_vars, y_vars=y_vars, hue=hue,
  File "/home/appuser/venv/lib/python3.9/site-packages/seaborn/axisgrid.py", line 1276, in __init__
    axes = fig.subplots(len(y_vars), len(x_vars),
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/figure.py", line 895, in subplots
    axs = gs.subplots(sharex=sharex, sharey=sharey, squeeze=squeeze,
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/gridspec.py", line 308, in subplots
    axarr[row, col] = figure.add_subplot(
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/figure.py", line 746, in add_subplot
    ax = subplot_class_factory(projection_class)(self, *args, **pkw)
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/axes/_subplots.py", line 34, in __init__
    self._axes_class.__init__(self, fig, [0, 0, 1, 1], **kwargs)
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/axes/_base.py", line 645, in __init__
    self._init_axis()
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/axes/_base.py", line 777, in _init_axis
    self.xaxis = maxis.XAxis(self)
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/axis.py", line 2190, in __init__
    super().__init__(*args, **kwargs)
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/_api/deprecation.py", line 454, in wrapper
    return func(*args, **kwargs)
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/axis.py", line 692, in __init__
    self.clear()
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/axis.py", line 877, in clear
    self._set_scale('linear')
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/axis.py", line 776, in _set_scale
    self._scale.set_default_locators_and_formatters(self)
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/scale.py", line 106, in set_default_locators_and_formatters
    axis.set_major_formatter(ScalarFormatter())
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/ticker.py", line 449, in __init__
    self.set_useMathText(useMathText)
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/ticker.py", line 564, in set_useMathText
    ufont = font_manager.findfont(
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/font_manager.py", line 1348, in findfont
    ret = self._findfont_cached(
  File "/home/appuser/venv/lib/python3.9/site-packages/matplotlib/font_manager.py", line 1474, in _findfont_cached
    + self.score_style(prop.get_style(), font.style)
AttributeError: 'FontProperties' object has no attribute 'style'

It is maybe very specific and I understand if you can’t help but what I don’t understand is that in localhost juste the line plt.rcParams[‘font.family’] = “Noto Sans JP Regular” is enough but while online it is so different.

I think the difference stems from the fact that in the local case, you have the relevant font installed on your local machine. Whereas it is not so trivial to install fonts on remote servers on which you don’t have root access – as is the case with Community Cloud.

Let me see if we can find a way to use custom fonts with sns.pairplot. Do you have a minimal reproducible example (not the entire app) of a seaborn pairplot with Japanese characters where the x and y axes are already included in the plot?

Using your code:

fpath=os.path.join(os.getcwd(),“Noto_Sans_JP/NotoSansJP-Regular.otf”)
prop = fm.FontProperties(fname=fpath)

def load_data():
df = pd.DataFrame(
{
“x”: np.arange(10),
“y”: np.arange(10),
“一” : np.arange(10),
“四” : np.arange(10),
“label”: [“あ”, “い”, “う”, “え”, “お”, “か”, “き”, “く”, “け”, “こ”],
}
)
return df
df = load_data()
corr_fig = sns.pairplot(df,corner=True)
st.pyplot(corr_fig)

When I do this the kanji doesn’t appear

1 Like

Hey @Christophe :wave:

Thank you so much for sharing a reproducible example! It’s important to understand that Seaborn is wrapper around matplotlib. It is much easier to set custom fonts for matplotlib because you’re aware of how the underlying chart is constructed.

In the case of sns.pairplot, you don’t just have one matplotlib.figure.Figure object returned. It’s non-trivial. The issue with the missing Japanese characters in the seaborn pairplot is caused by the fact that the pairplot method in seaborn creates multiple subplots, each with its own set of tick labels, axis labels, and legend. Therefore, to get the Japanese characters to display correctly in the pairplot, you need to set the font properties for each subplot separately. You can achieve this by iterating through the subplots and setting the font properties for each one.

The pairplot function returns a PairGrid object that can be used to access the individual subplots.

Note: When corner=True in sns.pairplot, the subplots of the pairplot are organized differently than when corner=False. In the case of corner=True, the diagonal plots are displayed separately and share a common y-axis. The off-diagonal plots are displayed in the upper-right triangle of the grid, while the lower-left triangle is left empty.

i.e. sns.pairplot returns a PairGrid object when corner=False, but returns a SubplotGrid object when corner=True. The PairGrid object has a g.axes attribute that is a 2D array of the subplots, whereas the SubplotGrid object does not have an axes attribute.

To handle both cases, we need to first check whether g has an axes attribute. If it does, we can iterate over g.axes.flat. If it does not, we can iterate over the g object directly, using the g.diag_axes and g.map_upper methods to access the diagonal and off-diagonal plots, respectively.

This code uses try and except statements to handle the NoneType errors that occur when subplots do not have xlabels or ylabels. If an AttributeError occurs while setting the font properties of a subplot, the code simply moves on to the next subplot, ignoring the one that caused the error:

import os

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import streamlit as st
from matplotlib import font_manager as fm

# Set the font path
fpath = os.path.join(os.getcwd(), "NOTO_SANS_JP/NotoSansJP-Regular.otf")
prop = fm.FontProperties(fname=fpath)

@st.cache_data
def load_data():
    df = pd.DataFrame(
        {
            "x": np.arange(10),
            "y": np.arange(10),
            "一": np.arange(10),
            "四": np.arange(10),
            "label": ["あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ"],
        }
    )
    return df

def create_pairplot(g):
    if g.axes is not None:
        axes = g.axes.flat
    else:
        axes = np.concatenate([g.diag_axes, g.map_upper().flat])
    for ax in axes:
        try:
            if ax.get_xlabel() is not None:
                ax.set_xlabel(ax.get_xlabel(), fontproperties=prop)
            if ax.get_xticklabels():
                ax.set_xticklabels(ax.get_xticklabels(), fontproperties=prop)
            if ax.get_ylabel() is not None:
                ax.set_ylabel(ax.get_ylabel(), fontproperties=prop)
            if ax.get_yticklabels():
                ax.set_yticklabels(ax.get_yticklabels(), fontproperties=prop)
            if ax.get_title() is not None:
                ax.set_title(ax.get_title(), fontproperties=prop)
        except AttributeError:
            pass

    return g

df = load_data()

col1, col2 = st.columns(2)
plot1 = create_pairplot(sns.pairplot(df, corner=False))
plot2 = create_pairplot(sns.pairplot(df, corner=True))
col1.pyplot(plot1.fig)
col2.pyplot(plot2.fig)

1 Like

Thank you very much for your help. It work well for sns.pairplot.

I found another way that work well too and it also work with sns.heatmap that I use too.

fpath = os.path.join(os.getcwd(), “streamlit_app/Noto_Sans_JP/NotoSansJP-Regular.otf”)
prop = fm.FontProperties(fname=fpath)
font_dir = [‘streamlit_app/Noto_Sans_JP’]
for font in fm.findSystemFonts(font_dir):
fm.fontManager.addfont(font)
rcParams[‘font.family’] = ‘Noto Sans JP’

And like this it work for everything without the function that you add.

Thank you a lot for your help and your time.

2 Likes

That’s genius! Glad you were able to figure out a general solution rather than having to create custom functions for each type of seaborn plot.

1 Like

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.