Skip to content

Commit

Permalink
Add a KaTeX and MathJax based LaTeX pane (#311)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored Mar 17, 2019
1 parent f9d1f61 commit 16f32ea
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 78 deletions.
115 changes: 115 additions & 0 deletions examples/gallery/panes/LaTeX.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"pn.extension('katex', 'mathjax')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``LaTeX`` pane allows rendering LaTeX equations as HTML. It uses either [MathJax](https://www.mathjax.org) or [KaTeX](https://katex.org/) depending on the defined renderer, by default it will use the renderer loaded in the extension (e.g. ``pn.extension('katex')``) and will default to KaTeX.\n",
"\n",
"#### Parameters:\n",
"\n",
"For layout and styling related parameters see the [layout user guide](../../user_guide/Layout.ipynb).\n",
"\n",
"* **``options``** (list or dict): A list or dictionary of options to select from\n",
"* **``renderer``** (object): The current value, must be one of the option values\n",
"* **``style``** (dict): Dictionary specifying CSS styles\n",
"\n",
"___"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A ``LaTeX`` pane will render any object with a ``_repr_latex_`` method as well as SymPy expressions, or any string containing HTML. Any ``LaTeX`` section should be wrapped in `$` or ``\\(`` and ``\\(`` delimiters, e.g.:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"latex = pn.pane.LaTeX('The LaTeX pane supports two delimiters: $LaTeX$ and \\(LaTeX\\)', style={'font-size': '18pt'}, width=800)\n",
"latex"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``LaTeX`` pane can be updated like other panes:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"latex.object = '$\\sum_{j}{\\sum_{i}{a*w_{j, i}}}$'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And can also be composed like any other pane:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"maxwell = pn.pane.LaTeX(r\"\"\"\n",
"$\\begin{aligned}\n",
" \\nabla \\times \\vec{\\mathbf{B}} -\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{E}}}{\\partial t} & = \\frac{4\\pi}{c}\\vec{\\mathbf{j}} \\\\\n",
" \\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\\n",
" \\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\\n",
" \\nabla \\cdot \\vec{\\mathbf{B}} & = 0\n",
"\\end{aligned}\n",
"$\"\"\", style={'font-size': '24pt'})\n",
"\n",
"cauchy_schwarz = pn.pane.LaTeX(object=r\"\"\"\n",
"$\\left( \\sum_{k=1}^n a_k b_k \\right)^2 \\leq \\left( \\sum_{k=1}^n a_k^2 \\right) \\left( \\sum_{k=1}^n b_k^2 \\right)$\n",
"\"\"\", style={'font-size': '24pt'})\n",
"\n",
"cross_product = pn.pane.LaTeX(object=r\"\"\"\n",
"$\\mathbf{V}_1 \\times \\mathbf{V}_2 = \\begin{vmatrix}\n",
"\\mathbf{i} & \\mathbf{j} & \\mathbf{k} \\\\\n",
"\\frac{\\partial X}{\\partial u} & \\frac{\\partial Y}{\\partial u} & 0 \\\\\n",
"\\frac{\\partial X}{\\partial v} & \\frac{\\partial Y}{\\partial v} & 0\n",
"\\end{vmatrix}\n",
"$\"\"\", style={'font-size': '24pt'})\n",
"\n",
"spacer = pn.Spacer(width=50)\n",
"\n",
"pn.Column(\n",
" pn.pane.Markdown('# The KaTeX Pane'),\n",
" pn.Row(maxwell, spacer, cross_product, spacer, cauchy_schwarz)\n",
")"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
4 changes: 3 additions & 1 deletion panel/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ class panel_extension(_pyviz_extension):

_loaded = False

_imports = {'plotly': 'panel.models.plotly',
_imports = {'katex': 'panel.models.katex',
'mathjax': 'panel.models.mathjax',
'plotly': 'panel.models.plotly',
'vega': 'panel.models.vega'}

def __call__(self, *args, **params):
Expand Down
24 changes: 24 additions & 0 deletions panel/models/katex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
Defines a custom KaTeX bokeh model to render text using KaTeX.
"""
import os

from bokeh.models import Markup

from ..util import CUSTOM_MODELS


class KaTeX(Markup):
"""
A bokeh model that renders text using KaTeX.
"""

__javascript__ = ["https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.js",
"https://cdn.jsdelivr.net/npm/katex@0.10.1/dist/contrib/auto-render.min.js"]

__css__ = ["https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.css"]

__implementation__ = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'katex.ts')


CUSTOM_MODELS['panel.models.katex.KaTeX'] = KaTeX
42 changes: 42 additions & 0 deletions panel/models/katex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as p from "core/properties"
import {Markup, MarkupView} from "models/widgets/markup"

export class KaTeXView extends MarkupView {
model: KaTeX

render(): void {
super.render();
this.markup_el.innerHTML = this.model.text;
(window as any).renderMathInElement(this.el, {
delimiters: [
{left: "$$", right: "$$", display: true},
{left: "\\[", right: "\\]", display: true},
{left: "$", right: "$", display: false},
{left: "\\(", right: "\\)", display: false}
]
})
}
}

export namespace KaTeX {
export type Attrs = p.AttrsOf<Props>
export type Props = Markup.Props & {
text: p.Property<string>
}
}

export interface KaTeX extends KaTeX.Attrs {}

export class KaTeX extends Markup {
properties: KaTeX.Props

constructor(attrs?: Partial<KaTeX.Attrs>) {
super(attrs)
}

static initClass(): void {
this.prototype.type = "KaTeX"
this.prototype.default_view = KaTeXView
}
}
KaTeX.initClass()
21 changes: 21 additions & 0 deletions panel/models/mathjax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
Defines a custom MathJax bokeh model to render text using MathJax.
"""
import os

from bokeh.models import Markup

from ..util import CUSTOM_MODELS


class MathJax(Markup):
"""
A bokeh model that renders text using MathJax.
"""

__javascript__ = ["https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/latest.js?config=TeX-MML-AM_CHTML"]

__implementation__ = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'mathjax.ts')


CUSTOM_MODELS['panel.models.mathjax.MathJax'] = MathJax
44 changes: 44 additions & 0 deletions panel/models/mathjax.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as p from "core/properties"
import {Markup, MarkupView} from "models/widgets/markup"

export class MathJaxView extends MarkupView {
model: MathJax
private _hub: any

initialize(): void {
super.initialize()
this._hub = (window as any).MathJax.Hub;
this._hub.Config({
tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']]}
});
}

render(): void {
super.render();
this.markup_el.innerHTML = this.model.text;
this._hub.Queue(["Typeset", this._hub, this.markup_el]);
}
}

export namespace MathJax {
export type Attrs = p.AttrsOf<Props>
export type Props = Markup.Props & {
text: p.Property<string>
}
}

export interface MathJax extends MathJax.Attrs {}

export class MathJax extends Markup {
properties: MathJax.Props

constructor(attrs?: Partial<MathJax.Attrs>) {
super(attrs)
}

static initClass(): void {
this.prototype.type = "MathJax"
this.prototype.default_view = MathJaxView
}
}
MathJax.initClass()
107 changes: 36 additions & 71 deletions panel/pane/equation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,49 +10,9 @@

import param

from .image import PNG
from pyviz_comms import JupyterComm


def latex_to_img(text, size=25, dpi=100):
"""
Returns PIL image for LaTeX equation text, using matplotlib's rendering.
Usage: latex_to_img(r'$\frac(x}{y^2}$')
From https://stackoverflow.com/questions/1381741.
"""
import matplotlib.pyplot as plt
from PIL import Image, ImageChops
import io

buf = io.BytesIO()
with plt.rc_context({'text.usetex': False, 'mathtext.fontset': 'stix'}):
fig = plt.figure()
ax = fig.add_subplot(111)
ax.axis('off')
ax.text(0.05, 0.5, '{text}'.format(text=text), size=size)
fig.set_dpi(dpi)
fig.canvas.print_figure(buf)
plt.close(fig)

im = Image.open(buf)
bg = Image.new(im.mode, im.size, (255, 255, 255, 255))
diff = ImageChops.difference(im, bg)
diff = ImageChops.add(diff, diff, 2.0, -100)
bbox = diff.getbbox()
return im.crop(bbox)


def make_transparent(img, bg=(255, 255, 255, 255)):
"""Given a PIL image, makes the specified background color transparent."""
img = img.convert("RGBA")
clear = bg[0:3]+(0,)
pixdata = img.load()

width, height = img.size
for y in range(height):
for x in range(width):
if pixdata[x,y] == bg:
pixdata[x,y] = clear
return img
from .markup import DivPaneBase


def is_sympy_expr(obj):
Expand All @@ -64,50 +24,55 @@ def is_sympy_expr(obj):
return False


class LaTeX(PNG):
"""
Matplotlib-based LaTeX-syntax equation.
Requires matplotlib and pillow.
See https://matplotlib.org/users/mathtext.html for what is supported.
"""
class LaTeX(DivPaneBase):

renderer = param.ObjectSelector(default=None, objects=['katex', 'mathjax'], doc="""
The JS renderer used to render the LaTeX expression.""")

# Priority is dependent on the data type
priority = None

size = param.Number(default=25, bounds=(1, 100), doc="""
Size of the rendered equation.""")

dpi = param.Number(default=72, bounds=(1, 1900), doc="""
Resolution per inch for the rendered equation.""")

_rerender_params = ['object', 'size', 'dpi']

@classmethod
def applies(cls, obj):
if is_sympy_expr(obj) or hasattr(obj, '_repr_latex_'):
try:
import matplotlib, PIL # noqa
except ImportError:
return False
return 0.05
elif isinstance(obj, string_types):
return None
else:
return False

def _imgshape(self, data):
"""Calculate and return image width,height"""
w, h = super(LaTeX, self)._imgshape(data)
w, h = (w/self.dpi), (h/self.dpi)
return int(w*72), int(h*72)

def _img(self):
obj = self.object # Default: LaTeX string

def _get_model_type(self, comm):
module = self.renderer
if module is None:
if 'panel.models.mathjax' in sys.modules and 'panel.models.katex' not in sys.modules:
module = 'mathjax'
else:
module = 'katex'
model = 'KaTeX' if module == 'katex' else 'MathJax'
if 'panel.models.'+module not in sys.modules:
if isinstance(comm, JupyterComm):
self.param.warning('{model} model was not imported on instantiation '
'and may not render in a notebook. Restart '
'the notebook kernel and ensure you load '
'it as part of the extension using:'
'\n\npn.extension(\'{module}\')\n'.format(
module=module, model=model))
__import__('panel.models.'+module)
return getattr(sys.modules['panel.models.'+module], model)

def _get_model(self, doc, root=None, parent=None, comm=None):
model = self._get_model_type(comm)(**self._get_properties())
if root is None:
root = model
self._models[root.ref['id']] = (model, parent)
return model

def _get_properties(self):
properties = super(LaTeX, self)._get_properties()
obj = self.object
if hasattr(obj, '_repr_latex_'):
obj = obj._repr_latex_()
elif is_sympy_expr(obj):
import sympy
obj = r'$'+sympy.latex(obj)+'$'

return make_transparent(latex_to_img(obj, self.size, self.dpi))._repr_png_()
return dict(properties, text=obj)
Loading

0 comments on commit 16f32ea

Please sign in to comment.