Skip to content

Add custom math fallback - #16509

Merged
timhoffm merged 1 commit into
matplotlib:masterfrom
andrzejnovak:fallbackto
Mar 3, 2020
Merged

timhoffm merged 1 commit into
matplotlib:masterfrom
andrzejnovak:fallbackto

Conversation

@andrzejnovak

@andrzejnovak andrzejnovak commented Feb 14, 2020 •

Copy link
Copy Markdown
Contributor

PR Summary

Follow-up on #11644

I've switched the logic so it's more transparent when mathtext.fallback is by default 'cm' . The original mathtext.fallback_to_cm defaults to None, but either True or False will raise a warning and override the mathtext.fallback setting.

PR Checklist

  • Has Pytest style unit tests
  • Code is Flake 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@andrzejnovak

Copy link
Copy Markdown
Contributor Author

@anntzer This should about do it. Seems mpl ships only with DejaVu Sans/STIX so I couldn't test a real figure against an incomplete font that would have to get substituted.

Unsure what's failing the docs though

Comment thread tutorials/text/mathtext.py Resolved
@anntzer

anntzer commented Feb 15, 2020 •

Copy link
Copy Markdown
Contributor

@geib7592 Can you please comment on this PR? Thanks!

@anntzer

anntzer commented Feb 15, 2020

Copy link
Copy Markdown
Contributor

Looking a bit below, it appears that DejaVuFonts (the actual default) already implements its own fallback solution, which goes to stix, not not cm. If you set mathtext.fallback, does that also override the fallback for dejavu?

I guess this suggests that the proper solution may instead to make mathtext.fontset accept a list of fonts, falling back from one to the next (so e.g. mathtext.fontset: dejavusans, stix or custom, cm or whatnot -- where custom is again defined by mathtext.{cal,rm,tt,it,bf,sf}).

Comment thread lib/matplotlib/mathtext.py Resolved
@andrzejnovak

Copy link
Copy Markdown
Contributor Author

Looking a bit below, it appears that DejaVuFonts (the actual default) already implements its own fallback solution, which goes to stix, not not cm. If you set mathtext.fallback, does that also override the fallback for dejavu?

I guess this suggests that the proper solution may instead to make mathtext.fontset accept a list of fonts, falling back from one to the next (so e.g. mathtext.fontset: dejavusans, stix or custom, cm or whatnot -- where custom is again defined by mathtext.{cal,rm,tt,it,bf,sf}).

IIUC fallback_to_cm only came to effect when using a math.text = custom and the behaviour should not be changed in this PR.

I agree it would be nicer to have a fallback list, but that seems like it would require major code changes, meanwhile the functionality in this PR (or the original PR indeed) is good enough.

Though it could be easily extended to also so StixSansFonts.

@geib7592

Copy link
Copy Markdown

Hello, it's me from #8619. I'm excited to see someone working on this issue. It seems that since matplotlib ships with stix and dejavu sans, adding a fallback to dajavu sans (and stix sans) could be another (very similar) PR, and in my opinion, it would be worthwhile for flexibility in using custom sans-serif fonts.

Allowing a custom fallback font is likely more work, but would be useful for cases I pointed out in #8619. (@anntzer, your suggestion for having mathtext.fontset be a list seems to preclude the future possibility of having a custom fallback font)

I think @anntzer also suggested I add my two cents about why this PR is important. Since posting the feature request in 2017, I've thought a fair amount about how to typeset publication-quality scientific figures. Currently, I don't think any free (or even non-free) software does it perfectly, and it's an area where, with a few well chosen tweaks, matplotlib could really excel.

For example, major scientific publishing companies like Nature and Science explicitly prefer Helvetica for the text of their figures. For Greek letters and mathematical symbols, they use a similarly weighted serif font used in their main text. This PR as it stands really helps move in the right direction; the example in #11644 shows how to use Helvetica with a fallback to stix, which gets closer to Nature's guideline.

I'd love to see more development toward industry standard design principles in the future with similar tweaks to the mathtext and latex engines. If the matplotlib devs are interested, I'd be happy to discuss and get more involved.

Thanks for all your work!

@anntzer

anntzer commented Feb 15, 2020

Copy link
Copy Markdown
Contributor

I'm fine with not doing the most general approach; I just want the various fallback options and their interaction to be properly documented (e.g. whether mathtext.fallback affects dejavu fallback as well).

your suggestion for having mathtext.fontset be a list seems to preclude the future possibility of having a custom fallback font

why?

@geib7592

Copy link
Copy Markdown

Maybe I misunderstood, but right now custom mathfont requires mathtext.fontset=custom, so if you also wanted a custom fallback, you'd need a second custom setting, like allowing mathtext.{cal,rm,tt,it,bf,sf} to be lists too, where missing characters are sourced down the list.

@anntzer

anntzer commented Feb 15, 2020

Copy link
Copy Markdown
Contributor

Ah yes, you want to fallback from one custom font to another one...
I guess making all of mathtext.cal/rm/... lists too would work indeed? (not that it has to be done in this PR)

@anntzer

anntzer commented Feb 16, 2020

Copy link
Copy Markdown
Contributor

If you look a bit below, class DejaVuFonts has

class DejaVuFonts(UnicodeFonts):
    use_cmex = False

    def __init__(self, *args, **kwargs):
        # This must come first so the backend's owner is set correctly
        if isinstance(self, DejaVuSerifFonts):
            self.cm_fallback = StixFonts(*args, **kwargs)
        else:
            self.cm_fallback = StixSansFonts(*args, **kwargs)
        self.bakoma = BakomaFonts(*args, **kwargs)
        TruetypeFonts.__init__(self, *args, **kwargs)

(the point I was mentioning above)

I'm actually now a bit confused by why DejaVuFonts inherits from UnicodeFonts but calls TruetypeFonts.__init__ in its __init__, and am not even sure anymore that DejaVuFonts.cm_fallback is ever used?

@andrzejnovak

Copy link
Copy Markdown
Contributor Author

I think we all agree that what we want is a fallback list for math as well. However, since that's major surgery, especially since the default fonts have a bunch of no-warnings/no-docs fallback which should likely be overhauled, we should open this as a new issue for a new PR.

This PR bring major improvement in just being able to have a sans-serif math fallback for custom fonts. Fulfilling most of what was requested in the 3 separate issues #8619, #11644 and #16492

Re some details, I added the option to explicitly ask for stixsans but actually have trouble finding characters that would be missing in a default font (say Arial) and would be different between stix and stixsans, which seem to differ for regular alphabet chars, but not so much for math symbols. (Basically I can't test is)

Otherwise, if we can agree on this functionality I'll update the tut/whats_new etc...

Comment thread matplotlibrc.template Resolved
Comment thread lib/matplotlib/rcsetup.py Resolved
@anntzer

anntzer commented Feb 18, 2020

Copy link
Copy Markdown
Contributor

I think you can add a test by picking some math strings that include a character that's not present in e.g. dejavusans but present in cm/stix/stixsans, and then saving a svg file with that string and rcParams["svg.fonttype"] = None, with fallback configured to cm/stix/stixsans/nofallback, which will saves the individual glyphs; you can then check that fallback correctly occurred (or not, if not falling back).

@andrzejnovak

Copy link
Copy Markdown
Contributor Author

Ok, I did the SVG check and the fallbacks work fine, the issue is just that some of the math characters are the same for both stix/stixsans e.g.("#STIXGeneral-Regular-10938") In reality it looks like it won't matter if the math fallback is stix or stixsans

Comment thread lib/matplotlib/mathtext.py Resolved
Comment thread lib/matplotlib/mathtext.py Resolved
@anntzer

anntzer commented Feb 19, 2020

Copy link
Copy Markdown
Contributor

It's fine if the test doesn't cover all cases, it would still be an improvement over the current situation.

Comment thread lib/matplotlib/mathtext.py Outdated

def __init__(self, *args, **kwargs):
self._stix_fallback = StixFonts(*args, **kwargs)
self.name = "Computer Modern"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the hope was that you would not have to hardcode this but could instead infer it from the FT2Font object, but I guess that somehow didn't work?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm doing it wrong, but the only reasonable way I found was self.cm_fallback.default_font_prop.get_family() but that just returns ['sans-serif'] which doesn't help here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or fontmap which returns sth like {'cal': '/home/anovak/matplotlib/lib/matplotlib/mpl-data/fonts/ttf/cmsy10.ttf', ... 'cmsy10': '/home/anovak/matplotlib/lib/matplotlib/mpl- data/fonts/ttf/cmex10.ttf', 'cmex10': '/home/anovak/matplotlib/lib/matplotlib/mpl-data/fonts/ttf/cmex10.ttf'} which would need some extra matching overhead to spit out a useful message.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was basically considering something like

                if (fontname in ('it', 'regular')
                        and isinstance(self.cm_fallback, StixFonts)):
                    fontname = 'rm'

                g = self.cm_fallback._get_glyph(
                    fontname, font_class, sym, fontsize)
                _log.warning("Substituting with a symbol from {}".format(g[0].family_name))
                # or .postscript_name
                return g

As it turns out, while this would work for stix fallback and most other fonts (I guess), this would fail specifically for computer modern fallback because cmmi10.ttf has poor metadata (it lists its family as "cmmi10"...), but I guess you can always special-case cm; at least this should work for "all" other fallbacks...

@andrzejnovak andrzejnovak Feb 20, 2020 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah nice, that's quite fun, because you can report per symbol. Checkout:

import matplotlib as mpl
test_str = r'a$b\AA\Finv\breve\gimel\succnapprox$'

mpl.rcParams['mathtext.fontset'] = 'custom'
mpl.rcParams['mathtext.rm'] = 'Courier New'
mpl.rcParams['mathtext.it'] = 'Courier New:italic'
mpl.rcParams['mathtext.bf'] = 'Courier New:bold'

fig, ax = plt.subplots(figsize=(8, 8))

mpl.rcParams['mathtext.fallback'] = 'cm'
txt = fig.text(.5, .5, test_str, fontsize=40, ha='center')

Output:

Substituting with a \Finv from STIXGeneral
Substituting with a \gimel from STIXGeneral
Substituting with a \combiningbreve from Computer Modern
Substituting with a \succnapprox from STIXGeneral
Substituting with a \Finv from STIXGeneral
Substituting with a \gimel from STIXGeneral
Substituting with a \combiningbreve from Computer Modern
Substituting with a \succnapprox from STIXGeneral

Can't figure out why is it doing two loops though. It will do yet another loop if a giure is saved, which is probably too many warnings to show.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anntzer yeah, warnings fail tests

@anntzer anntzer Feb 20, 2020 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use recwarn or pytest.warns (https://docs.pytest.org/en/5.3.5/warnings.html#assertwarnings) to catch them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point was that you actually want to emit a test here, but in the tests you can check that the warning is correctly thrown using pytest.warns.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better just go back to logging, than changing all those tests

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's fine with me too, I don't think the extra logging is too bad(?)

Comment thread lib/matplotlib/mathtext.py Resolved
Comment thread lib/matplotlib/mathtext.py Resolved
Comment thread lib/matplotlib/mathtext.py Resolved
Comment thread lib/matplotlib/mathtext.py Resolved
@andrzejnovak

Copy link
Copy Markdown
Contributor Author

@anntzer that should be it

@anntzer

anntzer commented Feb 24, 2020

Copy link
Copy Markdown
Contributor

A test (per #16509 (comment)) would be nice.

Comment thread lib/matplotlib/tests/test_mathtext.py Resolved
Comment thread lib/matplotlib/tests/test_mathtext.py Resolved
Comment thread lib/matplotlib/tests/test_mathtext.py Resolved
@anntzer

anntzer commented Feb 25, 2020

Copy link
Copy Markdown
Contributor

lgtm, can you please squash?

@andrzejnovak

andrzejnovak commented Feb 25, 2020 •

Copy link
Copy Markdown
Contributor Author

phew I'm just gonna drop this here when I inevitably need it again https://blog.appsignal.com/2016/09/27/git-rebasing-strategies.html

@anntzer

anntzer commented Feb 25, 2020

Copy link
Copy Markdown
Contributor

looks like you got it to work :)

@andrzejnovak

Copy link
Copy Markdown
Contributor Author

what's failing the codecov?

@anntzer

anntzer commented Feb 25, 2020

Copy link
Copy Markdown
Contributor

It's not failing?

@anntzer

anntzer commented Feb 25, 2020

Copy link
Copy Markdown
Contributor

flake8 is unhappy though

./lib/matplotlib/rcsetup.py:443:25: E128 continuation line under-indented for visual indent
2390./lib/matplotlib/rcsetup.py:444:25: E128 continuation line under-indented for visual indent
23912     E128 continuation line under-indented for visual indent

@andrzejnovak

Copy link
Copy Markdown
Contributor Author

It's not failing?

image

somehow it's transient?

@anntzer

anntzer commented Feb 25, 2020

Copy link
Copy Markdown
Contributor

Now it's back.
It's pretty flaky anyways (they likely have some race conditions against when the CI run finishes), so we don't overly worry about it.

@andrzejnovak
andrzejnovak force-pushed the fallbackto branch 3 times, most recently from 8c6734f to 5424ff5 Compare February 26, 2020 09:40

@anntzer anntzer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conditional on CI

@andrzejnovak

Copy link
Copy Markdown
Contributor Author

@anntzer what about the codecov

@anntzer

anntzer commented Mar 2, 2020

Copy link
Copy Markdown
Contributor

it's fine.

Comment thread lib/matplotlib/mathtext.py Resolved
Comment thread lib/matplotlib/mathtext.py Resolved
@timhoffm
timhoffm merged commit 09436b4 into matplotlib:master Mar 3, 2020
@QuLogic QuLogic changed the title Add custom math fallback [WIP] Add custom math fallback Mar 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants