sagemath / sage

Main repository of SageMath. Now open for Issues and Pull Requests.
https://www.sagemath.org
Other
1.19k stars 412 forks source link

Missing scientific labels #34233

Open kcrisman opened 1 year ago

kcrisman commented 1 year ago

See this sage-devel discussion for the problem. Compare

sage:  plot(x^2,(x,0,5000))  # missing display of 1e7 above y-axis
sage:  plot(x^2,(x,50,5000))  # missing display of 1e7 above y-axis
sage:  plot(x^2,(x,100,5000))  # display of 1e7 above y-axis is visible

gh-niranjankm has identified the problem as the use of SelectiveFormatter, in particular skip_values=[0]

Related:

CC: @niranjankm

Component: graphics

Issue created by migration from https://trac.sagemath.org/ticket/34233

slel commented 1 year ago

Description changed:

--- 
+++ 
@@ -9,5 +9,12 @@
 gh-niranjankm has identified the problem as the use of `SelectiveFormatter`, in particular `skip_values=[0]`

 Related:
-* 30983
-* #7964
+
+- #31447: list_plot labels don't indicate exponential notation by default
+- #30983: Fix tick label rendering in 2D plots, eg 4.0e6 displays as 4.0
+- #7964: axis labels in weird scientific notation
+- [Ask Sage question 63469: Plot drops exponent when scientific notation appears on the y-axis](https://ask.sagemath.org/question/63469)
+- [​Ask Sage question 54462: Some 2-d plot questions](https://ask.sagemath.org/question/54462)
+- [Ask Sage question 54361: Plot ratio of Bessel functions](https://ask.sagemath.org/question/54361)
+- [2022-07 sage-devel discussion: Display of scale multiplier in scientific notation (the e notation for powers of ten) is missing for plots starting with 0](https://groups.google.com/g/sage-devel/c/s39WysaG0fI)
+
ea007ec0-dfc6-4661-b6de-0fd1ed448353 commented 1 year ago
comment:2

See: https://stackoverflow.com/a/73371333/18152652

ea007ec0-dfc6-4661-b6de-0fd1ed448353 commented 1 year ago
comment:3

The fix is as follows:

STEP 1: Add following function to sage.plot.plot.py

def CustomScalarFormatter(replace_values=([],[])):
    """
    This matplotlib formatter selectively replaces the given tick labels.
    Takes a tuple or list of two lists as argument. First list labels will be replaced by second list labels.
    Note that the first label entries are cumpulsorily int or float values. No strings.
    No such restrictions for the second list. Because they will be anyway converted to strings in the end.

    EXAMPLES:

    ::

        sage: from sage.plot.plot import CustomScalarFormatter
        sage: import matplotlib.pyplot as plt
        sage: import numpy as np
        sage: z = np.linspace(0, 5000, 100)
        sage: fig, ax = plt.subplots()
        sage: xmajorformatter = CustomScalarFormatter(replace_values=([2000,0],['$x_0$','']))
        sage: ymajorformatter = CustomScalarFormatter(replace_values=([1E7,0],['$y_0$','']))
        sage: ax.xaxis.set_major_formatter(xmajorformatter)
        sage: ax.yaxis.set_major_formatter(ymajorformatter)
        sage: ax.plot(z,z**2)
        sage: plt.show()

    ::

        sage: from sage.plot.plot import CustomScalarFormatter
        sage: from matplotlib import font_manager
        sage: plot(x^2, (x,100,5000), tick_formatter = [ CustomScalarFormatter(replace_values=[[2000,0],['$x_0$','']]), CustomScalarFormatter(replace_values=[[1E7,0],['$y_0$','']]) ])

    """

    from matplotlib.ticker import ScalarFormatter

    class _CustomScalarFormatter(ScalarFormatter):
        def __init__(self, useOffset=None, useMathText=None, useLocale=None, replace_values=([],[])):
            super().__init__(useOffset=None, useMathText=None, useLocale=None)
            self.replace_values = replace_values

        def __call__(self, x, pos=None):
            """
            Return the format for tick value *x* at position *pos*.
            """
            if len(self.locs) == 0:
                return ''
            #elif x == 0:
            #    return ''
            elif x in self.replace_values[0]:
                idx = self.replace_values[0].index(x)
                return str(self.replace_values[1][idx])
            else:
                xp = (x - self.offset) / (10. ** self.orderOfMagnitude)
                if abs(xp) < 1e-8:
                    xp = 0
                return self._format_maybe_minus_and_locale(self.format, xp)

    return _CustomScalarFormatter(replace_values=replace_values)

I have verified the examples in docstring but check once the formatting.

STEP 2: Replace following lines in sage.plot.graphics.py

                from sage.plot.plot import SelectiveFormatter
                subplot.yaxis.set_major_formatter(SelectiveFormatter(
                    subplot.yaxis.get_major_formatter(), skip_values=[0]))
                subplot.xaxis.set_major_formatter(SelectiveFormatter(
                    subplot.xaxis.get_major_formatter(), skip_values=[0]))

by

                from sage.plot.plot import CustomScalarFormatter
                subplot.yaxis.set_major_formatter(CustomScalarFormatter(replace_values=([0],[''])))
                subplot.xaxis.set_major_formatter(CustomScalarFormatter(replace_values=([0],[''])))

STEP 3: (Optional) To have nice display of scientific label as x107 instead of 1e7 (say for example) by default add following line in sage.plot.graphics.py

        rcParams['axes.formatter.use_mathtext'] = True

just after

        from matplotlib import rcParams

in function

    def matplotlib(self, filename=None,

Very sorry, now I am far from setting up and working with sage git due to my upcoming exams and also since i am new to git and i have to explore and learn some more about git. Please somebody take up this task of getting these into sage.

egourgoulhon commented 1 year ago
comment:5

FWIW, a new report for this bug: https://groups.google.com/g/sage-support/c/zH_4KsKCscQ

williamstein commented 4 months ago

I think this should be a blocker. After all the impact is that the y-axis labels are incorrect. I just got a report of this on our support system from a confused user.

C(p) = 90000*p/(100-p)
show('For p=0.85, cost =  ', C(85))
show('For p=0.90, cost =  ', C(90))
plot(C(p), (p,0,100), ymin=0, ymax=1000000)

The y range of this plot looks like it is between 0 and 1. However it is actually between 0 and 1 million:

image

If you include frame=True it is a little clearer maybe what is going wrong:

image

Perhaps this is a sufficiently serious bug to be considered a "blocker", since it is a mathematically incorrect output, which is highly likely to be seen by our largest group of users (beginners), and seriously confuse them?

dimpase commented 4 months ago

I thought it's a problem of plotting near a singular point, but no, with (p,0,95) it looks just as bad. To compare, sympy+mathplotlib (from Anaconda, or from PyPI)

from sympy import symbols
from sympy.plotting import plot
p=symbols('p')
x=symbols('x')
C=90000*p/(100-p)
p=plot(C, (p,0,99), ymin=0, ymax=1000000) # with 99 or 100 the graph is too steep, below it's 95

p

That's on cocalc - not sure why I couldn't do a screenshot with Firefox, or save an image, so this is a photo of the screen.

dimpase commented 4 months ago

I can confirm that https://github.com/sagemath/sage/issues/34233#issuecomment-1418180812 provides a fix. I'll make a PR.

kcrisman commented 4 months ago

37502 does fix this as such, though see the related problems it causes in its current version (likely quite fixable).