Search code examples
pythonplotbokehholoviews

Better Scientific notation for Bokeh/Holoviews


I am trying to make a dashboard with holoviews/bokeh and some numbers are rather large. This triggers the scientific notation ticks (ex.1.00e+4 ... 2.00e+5). I don't like the looks of that so I would like to implement one of 2 things and can't seem to get either to work.

Option 1: Display ticks in the form of 2.0x105, or even 2.0E5.
Option 2: I can divide my data by 104 and change my axis labels to say "X x104.

For option 1, I can't figure out how to do this with a Bokeh tickformatter to give to holoviews. It looks like I need to use the CustomJSTickFormatter, but I do not know javascript so this doesn't help me.

For option 2: Every time I try to use LaTeX or MathML as a label string, it truncates the label and fails to render it.

Here's my minimal reproducible code:
bokeh.__version__ = 3.6.1
holoviews.__version__ = 1.20.0

x = np.linspace(start=0, stop=100000, num=25)
y = np.linspace(start=0, stop=35, num=25)
df_test = pd.DataFrame({'x':x, 'y': y, 'x2':x/10000})
hv.Points(df_test, ['x2', 'y']).opts(xlabel=r'$$\text{ This is messed up } \times 10^6$$')

Output with Option 2:
BokehOutput_Opt2

Option 2 is probably my preferred solution if I can get it to render correctly.


Solution

  • Well, given James's answer, I came up with a workaround for my needs...Unicode!

    
        import holoviews as hv
        
        # create a dictionary to convert numbers to superscripts
        super_dict = {1:'\u00b1',
                      2:'\u00b2',
                      3:'\u00b2',
                      4:'\u2074',
                      5:'\u2075',
                      6:'\u2076',
                      7:'\u2077',
                      8:'\u2078',
                      9:'\u2079',
                      0:'\u2070'}
        
        # use some math to find the closest power of 10 that is below your
        # desired max. In my case, I wanted 10 rather than 1. One could also
        # probably modify this use %3 to cut off at some reasonable thousand
        def simplify_data(data, col):
            import math
            import numpy as np
            logscale = math.floor(np.log10(data[col].max()))
            new_data = data[col]/(10**(logscale-1))
            new_label = f'{col}/10{super_dict[logscale-1]}'
            data.loc[:, [new_label]] = new_data
            return data, new_label
        
        # create synthetic data
        x = np.linspace(start=0, stop=200000, num=25)
        y = np.linspace(start=0, stop=35, num=25)
        df_test = pd.DataFrame({'x':x, 'y': y})
    
        # run my new function to generate my scaled variable
        df_test, new_col = simplify_data(df_test, 'x')
        hv.Points(df_test, [new_col, 'y'])
        # the .opts(xlabel=...) is now not needed either
    

    enter image description here