Skip to content

Commit 9a3e98f

Browse files
Add sp.use_chinese() one-line fix for Chinese text rendering
Auto-detects the best Chinese font on any platform: - macOS: PingFang SC/HK, Songti SC, Hiragino Sans GB - Windows: Microsoft YaHei, SimHei, SimSun - Linux: Noto CJK, WenQuanYi Usage: call sp.use_chinese() before plotting. Supports 'auto', 'serif', 'sans', or a specific font name. Also sets axes.unicode_minus=False. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7d0e3d1 commit 9a3e98f

3 files changed

Lines changed: 109 additions & 2 deletions

File tree

src/statspai/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
from .postestimation import margins, marginsplot, test, lincom
5353
from .diagnostics import oster_bounds, mccrary_test, diagnose, het_test, reset_test, vif, sensemakr, rddensity, hausman_test, anderson_rubin_test, evalue, evalue_from_result
5454
from .inference import wild_cluster_bootstrap, aipw, ri_test
55-
from .plots import binscatter, set_theme, list_themes, interactive, get_code
55+
from .plots import binscatter, set_theme, list_themes, use_chinese, interactive, get_code
5656
from .utils import label_var, label_vars, get_label, get_labels, describe, pwcorr, winsor, read_data
5757
from .gmm import xtabond
5858
from .metalearners import metalearner, SLearner, TLearner, XLearner, RLearner, DRLearner
@@ -137,6 +137,7 @@
137137
"binscatter",
138138
"set_theme",
139139
"list_themes",
140+
"use_chinese",
140141
"interactive",
141142
"get_code",
142143
# Utils

src/statspai/plots/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@
1111
"""
1212

1313
from .binscatter import binscatter
14-
from .themes import set_theme, list_themes
14+
from .themes import set_theme, list_themes, use_chinese
1515
from .interactive import interactive, get_code, FigureEditor
1616

1717
__all__ = [
1818
'binscatter',
1919
'set_theme',
2020
'list_themes',
21+
'use_chinese',
2122
'interactive',
2223
'get_code',
2324
'FigureEditor',

src/statspai/plots/themes.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,111 @@ def _get_cn_serif_fonts() -> list:
157157
return found
158158

159159

160+
def use_chinese(style: str = 'auto') -> str:
161+
"""
162+
One-line fix for Chinese text rendering in matplotlib.
163+
164+
Call this **before** creating any plots. Automatically detects
165+
the best Chinese font on your system (macOS, Windows, Linux).
166+
167+
Parameters
168+
----------
169+
style : str, default 'auto'
170+
- ``'auto'``: auto-detect the best available Chinese font
171+
- ``'serif'``: prefer serif fonts (宋体 Songti SC, SimSun)
172+
- ``'sans'``: prefer sans-serif fonts (苹方 PingFang, 黑体 SimHei)
173+
- Any specific font name, e.g. ``'Songti SC'``, ``'SimHei'``
174+
175+
Returns
176+
-------
177+
str
178+
The font name that was configured.
179+
180+
Examples
181+
--------
182+
>>> import statspai as sp
183+
>>> sp.use_chinese() # auto-detect best font
184+
>>> sp.use_chinese('serif') # prefer 宋体
185+
>>> sp.use_chinese('sans') # prefer 黑体/苹方
186+
>>> sp.use_chinese('Kaiti SC') # use specific font
187+
"""
188+
try:
189+
import matplotlib as mpl
190+
from matplotlib.font_manager import fontManager, findfont, FontProperties
191+
except ImportError:
192+
raise ImportError("matplotlib required. Install: pip install matplotlib")
193+
194+
available = {f.name for f in fontManager.ttflist}
195+
196+
# Priority lists for each platform
197+
serif_priority = [
198+
'Songti SC', 'Noto Serif CJK SC', 'SimSun', 'STSong',
199+
'Hiragino Mincho ProN', 'AR PL UMing CN',
200+
]
201+
sans_priority = [
202+
'PingFang SC', 'PingFang HK', 'Hiragino Sans GB',
203+
'Microsoft YaHei', 'SimHei', 'Noto Sans CJK SC',
204+
'Heiti TC', 'STHeiti', 'WenQuanYi Micro Hei',
205+
'Hiragino Sans',
206+
]
207+
all_priority = sans_priority + serif_priority + ['Arial Unicode MS']
208+
209+
chosen = None
210+
211+
if style == 'auto':
212+
for font in all_priority:
213+
if font in available:
214+
chosen = font
215+
break
216+
elif style == 'serif':
217+
for font in serif_priority:
218+
if font in available:
219+
chosen = font
220+
break
221+
elif style == 'sans':
222+
for font in sans_priority:
223+
if font in available:
224+
chosen = font
225+
break
226+
elif style in available:
227+
chosen = style
228+
else:
229+
# Try as-is, matplotlib will warn if not found
230+
chosen = style
231+
232+
if chosen is None:
233+
import warnings
234+
warnings.warn(
235+
"No Chinese font found on this system. "
236+
"Install one of: Noto CJK, SimSun, PingFang, "
237+
"Microsoft YaHei, or WenQuanYi.",
238+
UserWarning, stacklevel=2,
239+
)
240+
return ''
241+
242+
# Determine family
243+
_serif_names = ('Song', 'Serif', 'Mincho', 'Ming', 'STSong', 'Noto Serif')
244+
if any(kw in chosen for kw in _serif_names):
245+
family = 'serif'
246+
mpl.rcParams['font.family'] = 'serif'
247+
current = list(mpl.rcParams.get('font.serif', []))
248+
if chosen not in current:
249+
current.insert(0, chosen)
250+
mpl.rcParams['font.serif'] = current
251+
else:
252+
family = 'sans-serif'
253+
mpl.rcParams['font.family'] = 'sans-serif'
254+
current = list(mpl.rcParams.get('font.sans-serif', []))
255+
if chosen not in current:
256+
current.insert(0, chosen)
257+
mpl.rcParams['font.sans-serif'] = current
258+
259+
# Fix minus sign
260+
mpl.rcParams['axes.unicode_minus'] = False
261+
262+
return chosen
263+
264+
160265
def set_theme(
161266
name: str = 'academic',
162267
palette: Optional[str] = None,

0 commit comments

Comments
 (0)