Skip to content

Commit 40541c9

Browse files
committed
Merge commit 'bb9aae4b3f7e6eb89a3ebde0eccf41f7982f9db3' into text-overhaul-figures-per-commit
2 parents 078806c + bb9aae4 commit 40541c9

File tree

16 files changed

+432
-139
lines changed

16 files changed

+432
-139
lines changed

.github/workflows/cibuildwheel.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ jobs:
141141
path: dist/
142142

143143
- name: Build wheels for CPython 3.13
144-
uses: pypa/cibuildwheel@faf86a6ed7efa889faf6996aa23820831055001a # v2.23.3
144+
uses: pypa/cibuildwheel@5f22145df44122af0f5a201f93cf0207171beca7 # v3.0.0
145145
with:
146146
package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }}
147147
env:
@@ -152,15 +152,15 @@ jobs:
152152
CIBW_ARCHS: ${{ matrix.cibw_archs }}
153153

154154
- name: Build wheels for CPython 3.12
155-
uses: pypa/cibuildwheel@faf86a6ed7efa889faf6996aa23820831055001a # v2.23.3
155+
uses: pypa/cibuildwheel@5f22145df44122af0f5a201f93cf0207171beca7 # v3.0.0
156156
with:
157157
package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }}
158158
env:
159159
CIBW_BUILD: "cp312-*"
160160
CIBW_ARCHS: ${{ matrix.cibw_archs }}
161161

162162
- name: Build wheels for CPython 3.11
163-
uses: pypa/cibuildwheel@faf86a6ed7efa889faf6996aa23820831055001a # v2.23.3
163+
uses: pypa/cibuildwheel@5f22145df44122af0f5a201f93cf0207171beca7 # v3.0.0
164164
with:
165165
package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }}
166166
env:
@@ -169,7 +169,7 @@ jobs:
169169

170170

171171
- name: Build wheels for PyPy
172-
uses: pypa/cibuildwheel@faf86a6ed7efa889faf6996aa23820831055001a # v2.23.3
172+
uses: pypa/cibuildwheel@5f22145df44122af0f5a201f93cf0207171beca7 # v3.0.0
173173
with:
174174
package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }}
175175
env:
@@ -208,7 +208,7 @@ jobs:
208208
run: ls dist
209209

210210
- name: Generate artifact attestation for sdist and wheel
211-
uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
211+
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
212212
with:
213213
subject-path: dist/matplotlib-*
214214

.github/workflows/codeql-analysis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
persist-credentials: false
3333

3434
- name: Initialize CodeQL
35-
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
35+
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
3636
with:
3737
languages: ${{ matrix.language }}
3838

@@ -43,4 +43,4 @@ jobs:
4343
pip install --user -v .
4444
4545
- name: Perform CodeQL Analysis
46-
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
46+
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0

galleries/examples/pie_and_polar_charts/polar_demo.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,41 @@
44
==========
55
66
Demo of a line plot on a polar axis.
7+
8+
The second plot shows the same data, but with the radial axis starting at r=1
9+
and the angular axis starting at 0 degrees and ending at 225 degrees. Setting
10+
the origin of the radial axis to 0 allows the radial ticks to be placed at the
11+
same location as the first plot.
712
"""
813
import matplotlib.pyplot as plt
914
import numpy as np
1015

1116
r = np.arange(0, 2, 0.01)
1217
theta = 2 * np.pi * r
1318

14-
fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
19+
fig, axs = plt.subplots(2, 1, figsize=(5, 8), subplot_kw={'projection': 'polar'},
20+
layout='constrained')
21+
ax = axs[0]
1522
ax.plot(theta, r)
1623
ax.set_rmax(2)
17-
ax.set_rticks([0.5, 1, 1.5, 2]) # Less radial ticks
24+
ax.set_rticks([0.5, 1, 1.5, 2]) # Fewer radial ticks
1825
ax.set_rlabel_position(-22.5) # Move radial labels away from plotted line
1926
ax.grid(True)
2027

2128
ax.set_title("A line plot on a polar axis", va='bottom')
29+
30+
ax = axs[1]
31+
ax.plot(theta, r)
32+
ax.set_rmax(2)
33+
ax.set_rmin(1) # Change the radial axis to only go from 1 to 2
34+
ax.set_rorigin(0) # Set the origin of the radial axis to 0
35+
ax.set_thetamin(0)
36+
ax.set_thetamax(225)
37+
ax.set_rticks([1, 1.5, 2]) # Fewer radial ticks
38+
ax.set_rlabel_position(-22.5) # Move radial labels away from plotted line
39+
40+
ax.grid(True)
41+
ax.set_title("Same plot, but with reduced axis limits", va='bottom')
2242
plt.show()
2343

2444
# %%
@@ -32,6 +52,8 @@
3252
# - `matplotlib.projections.polar`
3353
# - `matplotlib.projections.polar.PolarAxes`
3454
# - `matplotlib.projections.polar.PolarAxes.set_rticks`
55+
# - `matplotlib.projections.polar.PolarAxes.set_rmin`
56+
# - `matplotlib.projections.polar.PolarAxes.set_rorigin`
3557
# - `matplotlib.projections.polar.PolarAxes.set_rmax`
3658
# - `matplotlib.projections.polar.PolarAxes.set_rlabel_position`
3759
#

galleries/users_explain/text/fonts.py

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,35 @@
2727
Matplotlib supports three font specifications (in addition to pdf 'core fonts',
2828
which are explained later in the guide):
2929
30-
.. list-table:: Type of Fonts
31-
:header-rows: 1
32-
33-
* - Type 1 (PDF)
34-
- Type 3 (PDF/PS)
35-
- TrueType (PDF)
36-
* - One of the oldest types, introduced by Adobe
37-
- Similar to Type 1 in terms of introduction
38-
- Newer than previous types, used commonly today, introduced by Apple
39-
* - Restricted subset of PostScript, charstrings are in bytecode
40-
- Full PostScript language, allows embedding arbitrary code
41-
(in theory, even render fractals when rasterizing!)
42-
- Include a virtual machine that can execute code!
43-
* - These fonts support font hinting
44-
- Do not support font hinting
45-
- Hinting supported (virtual machine processes the "hints")
46-
* - Non-subsetted through Matplotlib
47-
- Subsetted via external module ttconv
48-
- Subsetted via external module
49-
`fontTools <https://github.com/fonttools/fonttools>`__
30+
.. table:: Type of Fonts
31+
32+
+--------------------------+----------------------------+----------------------------+
33+
| Type 1 (PDF with usetex) | Type 3 (PDF/PS) | TrueType (PDF) |
34+
+==========================+============================+============================+
35+
| One of the oldest types, | Similar to Type 1 in | Newer than previous types, |
36+
| introduced by Adobe | terms of introduction | used commonly today, |
37+
| | | introduced by Apple |
38+
+--------------------------+----------------------------+----------------------------+
39+
| Restricted subset of | Full PostScript language, | Includes a virtual machine |
40+
| PostScript, charstrings | allows embedding arbitrary | that can execute code! |
41+
| are in bytecode | code (in theory, even | |
42+
| | render fractals when | |
43+
| | rasterizing!) | |
44+
+--------------------------+----------------------------+----------------------------+
45+
| Supports font | Does not support font | Supports font hinting |
46+
| hinting | hinting | (virtual machine processes |
47+
| | | the "hints") |
48+
+--------------------------+----------------------------+----------------------------+
49+
| Subsetted by code in | Subsetted via external module |
50+
| `matplotlib._type1font` | `fontTools <https://github.com/fonttools/fonttools>`__ |
51+
+--------------------------+----------------------------+----------------------------+
5052
5153
.. note::
5254
5355
Adobe disabled__ support for authoring with Type 1 fonts in January 2023.
56+
Matplotlib uses Type 1 fonts for compatibility with TeX; when the usetex
57+
feature is used with the PDF backend, Matplotlib reads the fonts used by
58+
the TeX engine, which are usually Type 1.
5459
5560
__ https://helpx.adobe.com/fonts/kb/postscript-type-1-fonts-end-of-support.html
5661
@@ -83,14 +88,12 @@
8388
files, particularly with fonts with many glyphs such as those that support CJK
8489
(Chinese/Japanese/Korean).
8590
86-
The solution to this problem is to subset the fonts used in the document and
87-
only embed the glyphs actually used. This gets both vector text and small
88-
files sizes. Computing the subset of the font required and writing the new
89-
(reduced) font are both complex problem and thus Matplotlib relies on
90-
`fontTools <https://fonttools.readthedocs.io/en/latest/>`__ and a vendored fork
91-
of ttconv.
92-
93-
Currently Type 3, Type 42, and TrueType fonts are subsetted. Type 1 fonts are not.
91+
To keep the output size reasonable while using vector fonts,
92+
Matplotlib embeds only the glyphs that are actually used in the document.
93+
This is known as font subsetting.
94+
Computing the font subset and writing the reduced font are both complex problems,
95+
which Matplotlib solves in most cases by using the
96+
`fontTools <https://fonttools.readthedocs.io/en/latest/>`__ library.
9497
9598
Core Fonts
9699
^^^^^^^^^^

lib/matplotlib/cbook.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2228,6 +2228,9 @@ def _g_sig_digits(value, delta):
22282228
Return the number of significant digits to %g-format *value*, assuming that
22292229
it is known with an error of *delta*.
22302230
"""
2231+
# For inf or nan, the precision doesn't matter.
2232+
if not math.isfinite(value):
2233+
return 0
22312234
if delta == 0:
22322235
if value == 0:
22332236
# if both value and delta are 0, np.spacing below returns 5e-324
@@ -2241,11 +2244,10 @@ def _g_sig_digits(value, delta):
22412244
# digits before the decimal point (floor(log10(45.67)) + 1 = 2): the total
22422245
# is 4 significant digits. A value of 0 contributes 1 "digit" before the
22432246
# decimal point.
2244-
# For inf or nan, the precision doesn't matter.
22452247
return max(
22462248
0,
22472249
(math.floor(math.log10(abs(value))) + 1 if value else 1)
2248-
- math.floor(math.log10(delta))) if math.isfinite(value) else 0
2250+
- math.floor(math.log10(delta)))
22492251

22502252

22512253
def _unikey_or_keysym_to_mplkey(unikey, keysym):

lib/matplotlib/sphinxext/plot_directive.py

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@
4747
4848
The ``.. plot::`` directive supports the following options:
4949
50+
``:filename-prefix:`` : str
51+
The base name (without the extension) of the outputted image and script
52+
files. The default is to use the same name as the input script, or the
53+
name of the RST document if no script is provided. The filename-prefix for
54+
each plot directive must be unique.
55+
5056
``:format:`` : {'python', 'doctest'}
5157
The format of the input. If unset, the format is auto-detected.
5258
@@ -163,8 +169,10 @@
163169
be customized by changing the *plot_template*. See the source of
164170
:doc:`/api/sphinxext_plot_directive_api` for the templates defined in *TEMPLATE*
165171
and *TEMPLATE_SRCSET*.
172+
166173
"""
167174

175+
from collections import defaultdict
168176
import contextlib
169177
import doctest
170178
from io import StringIO
@@ -182,6 +190,7 @@
182190
from docutils.parsers.rst.directives.images import Image
183191
import jinja2 # Sphinx dependency.
184192

193+
from sphinx.environment.collectors import EnvironmentCollector
185194
from sphinx.errors import ExtensionError
186195

187196
import matplotlib
@@ -265,6 +274,7 @@ class PlotDirective(Directive):
265274
'scale': directives.nonnegative_int,
266275
'align': Image.align,
267276
'class': directives.class_option,
277+
'filename-prefix': directives.unchanged,
268278
'include-source': _option_boolean,
269279
'show-source-link': _option_boolean,
270280
'format': _option_format,
@@ -312,9 +322,35 @@ def setup(app):
312322
app.connect('build-finished', _copy_css_file)
313323
metadata = {'parallel_read_safe': True, 'parallel_write_safe': True,
314324
'version': matplotlib.__version__}
325+
app.connect('builder-inited', init_filename_registry)
326+
app.add_env_collector(_FilenameCollector)
315327
return metadata
316328

317329

330+
# -----------------------------------------------------------------------------
331+
# Handle Duplicate Filenames
332+
# -----------------------------------------------------------------------------
333+
334+
def init_filename_registry(app):
335+
env = app.builder.env
336+
if not hasattr(env, 'mpl_plot_image_basenames'):
337+
env.mpl_plot_image_basenames = defaultdict(set)
338+
339+
340+
class _FilenameCollector(EnvironmentCollector):
341+
def process_doc(self, app, doctree):
342+
pass
343+
344+
def clear_doc(self, app, env, docname):
345+
if docname in env.mpl_plot_image_basenames:
346+
del env.mpl_plot_image_basenames[docname]
347+
348+
def merge_other(self, app, env, docnames, other):
349+
for docname in other.mpl_plot_image_basenames:
350+
env.mpl_plot_image_basenames[docname].update(
351+
other.mpl_plot_image_basenames[docname])
352+
353+
318354
# -----------------------------------------------------------------------------
319355
# Doctest handling
320356
# -----------------------------------------------------------------------------
@@ -600,6 +636,25 @@ def _parse_srcset(entries):
600636
return srcset
601637

602638

639+
def check_output_base_name(env, output_base):
640+
docname = env.docname
641+
642+
if '.' in output_base or '/' in output_base or '\\' in output_base:
643+
raise PlotError(
644+
f"The filename-prefix '{output_base}' is invalid. "
645+
f"It must not contain dots or slashes.")
646+
647+
for d in env.mpl_plot_image_basenames:
648+
if output_base in env.mpl_plot_image_basenames[d]:
649+
if d == docname:
650+
raise PlotError(
651+
f"The filename-prefix {output_base!r} is used multiple times.")
652+
raise PlotError(f"The filename-prefix {output_base!r} is used multiple"
653+
f"times (it is also used in {env.doc2path(d)}).")
654+
655+
env.mpl_plot_image_basenames[docname].add(output_base)
656+
657+
603658
def render_figures(code, code_path, output_dir, output_base, context,
604659
function_name, config, context_reset=False,
605660
close_figs=False,
@@ -722,7 +777,8 @@ def render_figures(code, code_path, output_dir, output_base, context,
722777

723778
def run(arguments, content, options, state_machine, state, lineno):
724779
document = state_machine.document
725-
config = document.settings.env.config
780+
env = document.settings.env
781+
config = env.config
726782
nofigs = 'nofigs' in options
727783

728784
if config.plot_srcset and setup.app.builder.name == 'singlehtml':
@@ -734,6 +790,7 @@ def run(arguments, content, options, state_machine, state, lineno):
734790

735791
options.setdefault('include-source', config.plot_include_source)
736792
options.setdefault('show-source-link', config.plot_html_show_source_link)
793+
options.setdefault('filename-prefix', None)
737794

738795
if 'class' in options:
739796
# classes are parsed into a list of string, and output by simply
@@ -775,14 +832,22 @@ def run(arguments, content, options, state_machine, state, lineno):
775832
function_name = None
776833

777834
code = Path(source_file_name).read_text(encoding='utf-8')
778-
output_base = os.path.basename(source_file_name)
835+
if options['filename-prefix']:
836+
output_base = options['filename-prefix']
837+
check_output_base_name(env, output_base)
838+
else:
839+
output_base = os.path.basename(source_file_name)
779840
else:
780841
source_file_name = rst_file
781842
code = textwrap.dedent("\n".join(map(str, content)))
782-
counter = document.attributes.get('_plot_counter', 0) + 1
783-
document.attributes['_plot_counter'] = counter
784-
base, ext = os.path.splitext(os.path.basename(source_file_name))
785-
output_base = '%s-%d.py' % (base, counter)
843+
if options['filename-prefix']:
844+
output_base = options['filename-prefix']
845+
check_output_base_name(env, output_base)
846+
else:
847+
base, ext = os.path.splitext(os.path.basename(source_file_name))
848+
counter = document.attributes.get('_plot_counter', 0) + 1
849+
document.attributes['_plot_counter'] = counter
850+
output_base = '%s-%d.py' % (base, counter)
786851
function_name = None
787852
caption = options.get('caption', '')
788853

@@ -846,7 +911,7 @@ def run(arguments, content, options, state_machine, state, lineno):
846911

847912
# save script (if necessary)
848913
if options['show-source-link']:
849-
Path(build_dir, output_base + source_ext).write_text(
914+
Path(build_dir, output_base + (source_ext or '.py')).write_text(
850915
doctest.script_from_examples(code)
851916
if source_file_name == rst_file and is_doctest
852917
else code,
@@ -906,7 +971,7 @@ def run(arguments, content, options, state_machine, state, lineno):
906971
# Not-None src_name signals the need for a source download in the
907972
# generated html
908973
if j == 0 and options['show-source-link']:
909-
src_name = output_base + source_ext
974+
src_name = output_base + (source_ext or '.py')
910975
else:
911976
src_name = None
912977
if config.plot_srcset:

0 commit comments

Comments
 (0)