holoviz / holoviews

With Holoviews, your data visualizes itself.
https://holoviews.org
BSD 3-Clause "New" or "Revised" License
2.7k stars 403 forks source link

LaTeX Support for Labels (Title, Axes, Ticks, Colorbar, etc.) #5740

Open cdeciampa opened 1 year ago

cdeciampa commented 1 year ago

Package versions:

As of Bokeh 3.0, LaTeX is now supported, but I don't think it's supported by holoviews yet. Using r"Upwelling Longwave Flux W$$m^{-2}$$" doesn't work for the colorbar label:

test_olr

And using that same label but for the title throws a KeyError:

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[28], line 1
----> 1 hv.save(mpas_raster_plot, '../figs/test_olr.png', dpi=300, center=False)

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/holoviews/util/__init__.py:807, in save(obj, filename, fmt, backend, resources, toolbar, title, **kwargs)
    805     if formats[-1] in supported:
    806         filename = '.'.join(formats[:-1])
--> 807 return renderer_obj.save(obj, filename, fmt=fmt, resources=resources,
    808                          title=title)

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/holoviews/plotting/renderer.py:594, in Renderer.save(self_or_cls, obj, basename, fmt, key, info, options, resources, title, **kwargs)
    592         if fmt in MIME_TYPES:
    593             basename = '.'.join([basename, fmt])
--> 594     plot.layout.save(basename, embed=True, resources=resources, title=title)
    595     return
    597 rendered = self_or_cls(plot, fmt)

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/panel/viewable.py:985, in Viewable.save(self, filename, title, resources, template, template_variables, embed, max_states, max_opts, embed_json, json_prefix, save_path, load_path, progress, embed_states, as_png, **kwargs)
    939 def save(
    940     self, filename: str | os.PathLike | IO, title: Optional[str] = None,
    941     resources: Resources | None = None, template: str | Template | None = None,
   (...)
    946     as_png: bool | None = None, **kwargs
    947 ) -> None:
    948     """
    949     Saves Panel objects to file.
    950 
   (...)
    983       string and ends with png.
    984     """
--> 985     return save(
    986         self, filename, title, resources, template,
    987         template_variables, embed, max_states, max_opts,
    988         embed_json, json_prefix, save_path, load_path, progress,
    989         embed_states, as_png, **kwargs
    990     )

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/panel/io/save.py:260, in save(panel, filename, title, resources, template, template_variables, embed, max_states, max_opts, embed_json, json_prefix, save_path, load_path, progress, embed_states, as_png, **kwargs)
    258     model = doc
    259 else:
--> 260     model = panel.get_root(doc, comm)
    261     if embed:
    262         embed_state(
    263             panel, model, doc, max_states, max_opts, embed_json,
    264             json_prefix, save_path, load_path, progress, embed_states
    265         )

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/panel/layout/base.py:286, in Panel.get_root(self, doc, comm, preprocess)
    282 def get_root(
    283     self, doc: Optional[Document] = None, comm: Optional[Comm] = None,
    284     preprocess: bool = True
    285 ) -> Model:
--> 286     root = super().get_root(doc, comm, preprocess)
    287     # ALERT: Find a better way to handle this
    288     if hasattr(root, 'styles') and 'overflow-x' in root.styles:

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/panel/viewable.py:658, in Renderable.get_root(self, doc, comm, preprocess)
    656 wrapper = self._design._wrapper(self)
    657 if wrapper is self:
--> 658     root = self._get_model(doc, comm=comm)
    659     if preprocess:
    660         self._preprocess(root)

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/panel/layout/base.py:170, in Panel._get_model(self, doc, root, parent, comm)
    168 root = root or model
    169 self._models[root.ref['id']] = (model, parent)
--> 170 objects, _ = self._get_objects(model, [], doc, root, comm)
    171 props = self._get_properties(doc)
    172 props[self._property_mapping['objects']] = objects

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/panel/layout/base.py:155, in Panel._get_objects(self, model, old_objects, doc, root, comm)
    153 else:
    154     try:
--> 155         child = pane._get_model(doc, root, model, comm)
    156     except RerenderError:
    157         return self._get_objects(model, current_objects[:i], doc, root, comm)

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/panel/pane/holoviews.py:380, in HoloViews._get_model(self, doc, root, parent, comm)
    378     plot = self.object
    379 else:
--> 380     plot = self._render(doc, comm, root)
    382 plot.pane = self
    383 backend = plot.renderer.backend

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/panel/pane/holoviews.py:461, in HoloViews._render(self, doc, comm, root)
    458     if comm:
    459         kwargs['comm'] = comm
--> 461 return renderer.get_plot(self.object, **kwargs)

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/holoviews/plotting/bokeh/renderer.py:70, in BokehRenderer.get_plot(self_or_cls, obj, doc, renderer, **kwargs)
     63 @bothmethod
     64 def get_plot(self_or_cls, obj, doc=None, renderer=None, **kwargs):
     65     """
     66     Given a HoloViews Viewable return a corresponding plot instance.
     67     Allows supplying a document attach the plot to, useful when
     68     combining the bokeh model with another plot.
     69     """
---> 70     plot = super().get_plot(obj, doc, renderer, **kwargs)
     71     if plot.document is None:
     72         plot.document = Document() if self_or_cls.notebook_context else curdoc()

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/holoviews/plotting/renderer.py:241, in Renderer.get_plot(self_or_cls, obj, doc, renderer, comm, **kwargs)
    238     defaults = [kd.default for kd in plot.dimensions]
    239     init_key = tuple(v if d is None else d for v, d in
    240                      zip(plot.keys[0], defaults))
--> 241     plot.update(init_key)
    242 else:
    243     plot = obj

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/holoviews/plotting/plot.py:936, in DimensionedPlot.update(self, key)
    934 def update(self, key):
    935     if len(self) == 1 and ((key == 0) or (key == self.keys[0])) and not self.drawn:
--> 936         return self.initialize_plot()
    937     item = self.__getitem__(key)
    938     self.traverse(lambda x: setattr(x, '_updated', True))

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/geoviews/plotting/bokeh/plot.py:106, in GeoPlot.initialize_plot(self, ranges, plot, plots, source)
    104 def initialize_plot(self, ranges=None, plot=None, plots=None, source=None):
    105     opts = {} if isinstance(self, HvOverlayPlot) else {'source': source}
--> 106     fig = super().initialize_plot(ranges, plot, plots, **opts)
    107     if self.geographic and self.show_bounds and not self.overlaid:
    108         from . import GeoShapePlot

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/holoviews/plotting/bokeh/element.py:2493, in OverlayPlot.initialize_plot(self, ranges, plot, plots)
   2491 self.tabs = self.tabs or any(isinstance(sp, TablePlot) for sp in self.subplots.values())
   2492 if plot is None and not self.tabs and not self.batched:
-> 2493     plot = self._init_plot(key, element, ranges=ranges, plots=plots)
   2494     self._init_axes(plot)
   2495 self.handles['plot'] = plot

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/holoviews/plotting/bokeh/element.py:498, in ElementPlot._init_plot(self, key, element, plots, ranges)
    495     properties['outline_line_alpha'] = 0
    497 if self.show_title and self.adjoined is None:
--> 498     title = self._format_title(key, separator=' ')
    499 else:
    500     title = ''

File /storage/work/c/cnd5285/mambaforge/envs/uviz/lib/python3.9/site-packages/holoviews/plotting/plot.py:490, in DimensionedPlot._format_title(self, key, dimensions, separator)
    486 def _format_title(self, key, dimensions=True, separator='\n'):
    487     label, group, type_name, dim_title = self._format_title_components(
    488         key, dimensions=True, separator='\n'
    489     )
--> 490     title = util.bytes_to_unicode(self.title).format(
    491         label=util.bytes_to_unicode(label),
    492         group=util.bytes_to_unicode(group),
    493         type=type_name,
    494         dimensions=dim_title
    495     )
    496     return title.strip(' \n')

KeyError: '-2'
ianthomas23 commented 1 year ago

A couple of observations:

  1. LaTeX support in Bokeh for ColorBars has only recently been committed and is not in a release yet. It will be in 3.2 which should be released soon.
  2. LaTeX support in Bokeh is limited to whole strings not substrings. Here is the simplest example working with bokeh 3.1.1 and holoviews 1.16.1:

<img width="930" alt="Screenshot 2023-06-05 at 11 23 37" src="https://github.com/holoviz/holoviews/assets/580326/281c6888-2d43-40f9-9b18-f2cde1094460">

The LaTeX substring issue is known and something we would like to fix, but I don't know if anyone is activately working on it.

I should also point out that when I first ran my simple example above I didn't see any LaTeX at all and I was seeing DOMException: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The image argument is a canvas element with a width or height of 0 errors in the javascript console which are related to https://github.com/bokeh/bokeh/issues/13139#issuecomment-1553315243. But now, with a cleanly installed conda environment I cannot reproduce those errors.

cdeciampa commented 1 year ago

Ah, okay, I didn't realize about 1., the documentation is a little confusing because further down the page, there's an example using a substring with div and paragraph widgets.

As for reproducing my error (which I think is what you meant), I didn't post a snippet of my code because I can't really share a condensed version. My research involves visualizing unstructured grid climate models (models that use unusual shapes as their grid, quasi-hexagons are shown above) and I use spatialpandas and holoviews.Polygons (and geoviews) that have been rasterized with datashader. Since all of the packages within holoviews are managed separately, my error could be related to holoviews' interaction with a different package that's not obvious within the traceback.

ianthomas23 commented 1 year ago

Ah, okay, I didn't realize about 1., the documentation is a little confusing because further down the page, there's an example using a substring with div and paragraph widgets.

Yes I can see that the documentation is confusing. There are really two separate implementations of LaTeX in Bokeh. For Div and Paragraph we can just defer support to the two HTML elements so the functionality here is as rich as would be in any HTML page using MathML, hence they support substrings. But for other Bokeh elements we are using bespoke code that doesn't yet support LaTeX substrings. The "two separate implementations" explanation is probably too much information for most readers although it would have helped you. It will all be clearer when all Bokeh elements support substrings which is what everyone expects.

As for reproducing my error (which I think is what you meant), I didn't post a snippet of my code ...

I didn't mean any criticism, I just wanted to post a minimal working example of LaTeX in HoloViews to confirm that something works and it is not completely broken. I agree that the error message you saw is not very informative!

cdeciampa commented 1 year ago

I somehow missed you explaining earlier that whole strings of LaTeX are already supported for titles and axes labels in the current version of holoviews. Using the same string you have for the title, r"$$\alpha \beta$$", I now get a different warning/error from bokeh specifically and the title doesn't plot:

WARNING:bokeh.io.export:The webdriver raised a TimeoutException while waiting for a 'bokeh:idle' event to signify that the layout has rendered. Something may have gone wrong.

test_olr-2

Here's the smallest snippet of my code I could manage and what the spatialpandas dataframe looks like:

Screenshot 2023-06-05 at 12 28 55 PM
# Plots polygons
hv_polys = hv.Polygons(ian_df, vdims=['faces']).opts(color='faces')

# Declares spatial plotting bounds
x_range = tuple((-90, -72.5))
y_range = tuple((20, 32.5))

# Sets Datashader options
datashader_kw = dict(aggregator='mean', precompute=True, 
                     x_range=x_range, y_range=y_range, pixel_ratio=10)

# Rasterizes polygons
raster_plot = hv.operation.datashader.rasterize(hv_polys, **datashader_kw) 

# Sets additional plotting options
font_opts = dict(fontsize=dict(title='35pt', clabel='50pt', cticks='40pt'))
cbar_opts = dict(height=40, border_line_width=3)
cmap_opts = dict(cmap=flut_cmap, clim=(cmin, cmax), colorbar=True, colorbar_position='bottom', 
                 clabel='Upwelling Longwave Flux [W/m^2]')
plot_kw = dict(width=w, height=h, xaxis=None, yaxis=None, title=r"$$\alpha \beta$$", 
               colorbar_opts=cbar_opts, **cmap_opts, **font_opts)

# Adds additional options to rasterized plot
out_plot = raster_plot.opts(**plot_kw)

# Sets up geoviews layer
coastline_kw = dict(scale='10m', line_color='#FFFFFF', line_width=2.0, width=w, height=h)
coastline_layer = gf.coastline(projection=ccrs.PlateCarree()).opts(**coastline_kw, xlim=x_range, ylim=y_range)

# Updates plot with geoviews layer
out_plot = out_plot * coastline_layer

# Outputs plot
hv.save(out_plot, '../figs/test_olr.png', dpi=300, center=False)
byquip commented 1 year ago

There seems to be bug with RadioButtonGroup labels. I tried to use Latex in one of the labels:

lab = r"$$\text{Dependency on }R_{core}$$"
cbg_dif_loss_depend = RadioButtonGroup(labels=['Dependency on N', lab],active=1)

And result:

image

hoxbro commented 1 year ago

@byquip please open an issue on Bokeh issue tracker if an existing issue do not already exists.

from bokeh.io import show, output_notebook
from bokeh.models import CustomJS, RadioButtonGroup

output_notebook()

lab = r"$$\text{Dependency on }R_{core}$$"
rbg = RadioButtonGroup(labels=['Dependency on N', lab],active=1)

show(rbg)

image

wen-jams commented 1 year ago

Hello, I've encountered a similar issue with Latex labels, where they won't even show up. Following @ianthomas23 simple example, I get this: image

Software versions: holoviews 1.17.0 bokeh 3.2.1 jupyterlab 3.6.3 jupyter_bokeh 3.0.7

sunilkpai commented 1 year ago

EDIT:

Screen Shot 2023-09-04 at 10 58 21 AM

For now, I found the following solution for my needs.

import holoviews as hv
hv.extension('bokeh')
import panel as pn
pn.extension('mathjax')

hv.Rectangles([[0, 0, 1, 1]]).opts(title=r'$$\alpha \beta$$', xlabel='$$y$$')

Let me know if it fixed it for everyone else :)

@ianthomas23 there appear to be some issues with the logic of enabling mathjax in holoviews for me. I would have expected it to work with something like:

hv.extension('bokeh', enable_mathjax=True)

But after poring through the code, the panel import is what fixed things...

OLD COMMENT:

For some reason (probably performance), mathjax appears to not be added to the templates in the HTML. I saved my plot as a standalone HTML and got this problem. The default template loading list appears to have changed in recent Bokeh versions to:

<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-3.2.2.min.js"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.2.2.min.js"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.2.2.min.js"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.2.2.min.js"></script>

which does not include mathjax.

So I needed to add

<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.2.2.min.js"></script>

Not sure how to make this the default for widgets generated in a notebook though. Any tips?

Software versions:

holoviews 1.17.1
bokeh 3.2.2
jupyterlab 4.0.5
jupyter_bokeh 3.0.7
param 1.13.0
hoxbro commented 1 year ago

The original problem by @cdeciampa, as far as I can see, is already supported or will need work done in Bokeh itself.

The second problem by @cdeciampa around hv.save not working needs a minimal, reproducible example (MRE) for me to be able to investigate. Please open a new issue if it is still a problem.

The problem by @ @byquip is a Bokeh feature request and is tracked on their issue tracker.

The problem described by @wen-jams and @sunilkpai should be fixed by #5904 and will be out in the next release, 1.18. Thank you for the investigation, @sunilkpai. I really, really appreciated it!

I think that was all of it and if not please open new issues.

wen-jams commented 1 year ago

@Hoxbro it seems like the fix in 1.18.0 is only working when running in panel serve and not for notebooks.

import holoviews as hv
import panel as pn

hv.extension("bokeh", enable_mathjax=True)

a = hv.Rectangles([[0, 0, 1, 1]]).opts(title=r"$$\alpha \beta$$", xlabel="$$y$$")

pn.panel(a).servable()

For example in Jupyterlab with the Panel preview, you can see the LaTeX is rendered in the widget but not inline: image

The pn.extension("mathjax") fix suggested by @sunilkpai didn't work for me, either.

However, saving the plot to HTML shows that mathjax is indeed included:

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-3.3.0.min.js"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.3.0.min.js"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.3.0.min.js"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.3.0.min.js"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.3.0.min.js"></script>
<script type="text/javascript" src="https://cdn.holoviz.org/panel/1.3.0/dist/panel.min.js"></script>

The LaTeX labels show up fine in the plot from HTML as well.

Software versions:

holoviews 1.18.0
bokeh 3.3.0
panel 1.3.0
jupyterlab 4.0.7
jupyter_bokeh 3.0.7
hoxbro commented 1 year ago

I think this is an upstream issue, as I also see the same problem using Bokeh: https://github.com/bokeh/bokeh/issues/13499