Skip to content

Commit

Permalink
Merge #1133
Browse files Browse the repository at this point in the history
1133: Change HTML repr away from LaTeX to a Sparse/Dask-like repr r=hgrecco a=jthielen

While I've never been a big fan of Pint's LaTeX repr in JupyterLab/Jupyter Notebook due to working with large arrays that would crash the renderer, I've recently been working a lot with various wrapping combinations of xarray, Pint, Dask, and Sparse, in which Pint's LaTeX repr performs particularly poorly. Even if Pint's repr were elided (#765), it would still poorly nest with the other HTML reprs.

This motivated me to implement an alternative HTML repr for Pint using a simple HTML table similar to what both Dask and Sparse use. As shown in [this demonstration notebook](https://nbviewer.jupyter.org/urls/dl.dropbox.com/s/jlgqm6s92kzvagi/pint_html_repr_demo.ipynb), this allows much better nesting of HTML reprs. A quick example image is below:

![](https://www.meteor.iastate.edu/~jthielen/Screenshot%20from%202020-07-14%2017-07-51.png)

This doesn't quite take care of [xarray's unit repr discussions](pydata/xarray#2773) (since this only shows up when expanding the data in a dataset), but I still think it is a worthwhile improvement for the expanded view. @keewis do you have any thoughts on this particular interaction?

Marking as a WIP/draft for now to see if this is a welcome format for the HTML repr first before I dig too much into optimizing the implementation and writing tests for it.

- [x] Closes #654 (even though it was already closed in favor of #765, I think the are related but separate issues)
- [x] Executed ``black -t py36 . && isort -rc . && flake8`` with no errors
- [x] The change is fully covered by automated unit tests
- [x] Documented in docs/ as appropriate
- [x] Added an entry to the CHANGES file

Tagging @nbren12, @hgrecco, @jondoesntgit, @natezb, @ipcoder, @keewis, and @TomNicholas for input based on past issues (#654, #765, and xarray-contrib/pint-xarray#6).


Co-authored-by: Jon Thielen <github@jont.cc>
  • Loading branch information
bors[bot] and jthielen authored Aug 17, 2020
2 parents 3d99581 + f510bee commit dea623a
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 73 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ test/

# test csv which should be user generated
notebooks/pandas_test.csv

# dask stuff
dask-worker-space
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Pint Changelog
0.15 (unreleased)
-----------------

- Change `Quantity` and `Unit` HTML (i.e., Jupyter notebook) repr away from LaTeX to a
simpler, more performant pretty-text and table based repr inspired by Sparse and Dask.
(Issue #654)
- Implement Dask collection interface to support Pint Quantity wrapped Dask arrays.
- Started automatically testing examples in the documentation
- Fixed right operand power for dimensionless Quantity to reflect numpy behavior. (Issue #1136)
Expand Down
8 changes: 4 additions & 4 deletions docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -319,10 +319,10 @@ Pint also supports `f-strings`_ from python>=3.6 :
>>> print(f'The str is {accel:~.3e}')
The str is 1.300e+00 m / s ** 2
>>> print(f'The str is {accel:~H}') # HTML format (displays well in Jupyter)
The str is \[1.3\ m/{s}^{2}\]
The str is 1.3 m/s<sup>2</sup>

But Pint also extends the standard formatting capabilities for unicode and
LaTeX representations:
But Pint also extends the standard formatting capabilities for unicode, LaTeX, and HTML
representations:

.. doctest::

Expand All @@ -335,7 +335,7 @@ LaTeX representations:
'The LaTeX representation is 1.3\\ \\frac{\\mathrm{meter}}{\\mathrm{second}^{2}}'
>>> # HTML print - good for Jupyter notebooks
>>> 'The HTML representation is {:H}'.format(accel)
'The HTML representation is \\[1.3\\ meter/{second}^{2}\\]'
'The HTML representation is 1.3 meter/second<sup>2</sup>'

If you want to use abbreviated unit names, prefix the specification with `~`:

Expand Down
8 changes: 2 additions & 6 deletions pint/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def _pretty_fmt_exponent(num):
"single_denominator": True,
"product_fmt": r" ",
"division_fmt": r"{}/{}",
"power_fmt": "{{{}}}^{{{}}}", # braces superscript whole exponent
"power_fmt": r"{}<sup>{}</sup>",
"parentheses_fmt": r"({})",
},
"": { # Default format.
Expand Down Expand Up @@ -270,12 +270,8 @@ def format_unit(unit, spec, **kwspec):
(r"\mathrm{{{}}}".format(u.replace("_", r"\_")), p) for u, p in unit.items()
]
return formatter(rm, **fmt).replace("[", "{").replace("]", "}")
elif spec == "H":
# HTML (Jupyter Notebook)
rm = [(u.replace("_", r"\_"), p) for u, p in unit.items()]
return formatter(rm, **fmt)
else:
# Plain text
# HTML and Text
return formatter(unit.items(), **fmt)


Expand Down
12 changes: 4 additions & 8 deletions pint/measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def __format__(self, spec):
if "L" in newspec and "S" in newspec:
mag = mag.replace("(", r"\left(").replace(")", r"\right)")

if "L" in newspec or "H" in spec:
if "L" in newspec:
space = r"\ "
else:
space = " "
Expand All @@ -158,14 +158,10 @@ def __format__(self, spec):

if "H" in spec:
# Fix exponential format
mag = re.sub(r"\)e\+0?(\d+)", r")×10^{\1}", mag)
mag = re.sub(r"\)e-0?(\d+)", r")×10^{-\1}", mag)
mag = re.sub(r"\)e\+0?(\d+)", r")×10<sup>\1</sup>", mag)
mag = re.sub(r"\)e-0?(\d+)", r")×10<sup>-\1</sup>", mag)

assert ustr[:2] == r"\["
assert ustr[-2:] == r"\]"
return r"\[" + mag + space + ustr[2:]
else:
return mag + space + ustr
return mag + space + ustr


_Measurement = Measurement
Expand Down
79 changes: 52 additions & 27 deletions pint/quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
from .formatting import (
_pretty_fmt_exponent,
ndarray_to_latex,
ndarray_to_latex_parts,
remove_custom_flags,
siunitx_format_unit,
)
Expand All @@ -66,6 +65,7 @@
SharedRegistryObject,
UnitsContainer,
infer_base_unit,
iterable,
logger,
to_units_container,
)
Expand Down Expand Up @@ -311,48 +311,72 @@ def __format__(self, spec):

spec = spec or self.default_format

if "L" in spec:
allf = plain_allf = r"{}\ {}"
else:
allf = plain_allf = "{} {}"

# If Compact is selected, do it at the beginning
if "#" in spec:
spec = spec.replace("#", "")
obj = self.to_compact()
else:
obj = self

# the LaTeX siunitx code
if "L" in spec:
allf = plain_allf = r"{}\ {}"
elif "H" in spec:
allf = plain_allf = "{} {}"
if iterable(obj.magnitude):
# Use HTML table instead of plain text template for array-likes
allf = (
"<table><tbody>"
"<tr><th>Magnitude</th>"
"<td style='text-align:left;'>{}</td></tr>"
"<tr><th>Units</th><td style='text-align:left;'>{}</td></tr>"
"</tbody></table>"
)
else:
allf = plain_allf = "{} {}"

if "Lx" in spec:
# the LaTeX siunitx code
spec = spec.replace("Lx", "")
# TODO: add support for extracting options
opts = ""
ustr = siunitx_format_unit(obj.units)
allf = r"\SI[%s]{{{}}}{{{}}}" % opts
elif "H" in spec:
ustr = format(obj.units, spec)
assert ustr[:2] == r"\["
assert ustr[-2:] == r"\]"
ustr = ustr[2:-2]
allf = r"\[{}\ {}\]"
else:
# Hand off to unit formatting
ustr = format(obj.units, spec)

mspec = remove_custom_flags(spec)
if isinstance(self.magnitude, ndarray):
if "H" in spec:
# HTML formatting
if hasattr(obj.magnitude, "_repr_html_"):
# If magnitude has an HTML repr, nest it within Pint's
mstr = obj.magnitude._repr_html_()
else:
if isinstance(self.magnitude, ndarray):
# Use custom ndarray text formatting with monospace font
formatter = "{{:{}}}".format(mspec)
with printoptions(formatter={"float_kind": formatter.format}):
mstr = (
"<pre>"
+ format(obj.magnitude).replace("\n", "<br>")
+ "</pre>"
)
elif not iterable(obj.magnitude):
# Use plain text for scalars
mstr = format(obj.magnitude, mspec)
else:
# Use monospace font for other array-likes
mstr = (
"<pre>"
+ format(obj.magnitude, mspec).replace("\n", "<br>")
+ "</pre>"
)
elif isinstance(self.magnitude, ndarray):
if "L" in spec:
# Use ndarray LaTeX special formatting
mstr = ndarray_to_latex(obj.magnitude, mspec)
elif "H" in spec:
allf = r"\[{} {}\]"
# this is required to have the magnitude and unit in the same line
parts = ndarray_to_latex_parts(obj.magnitude, mspec)

if len(parts) > 1:
return "\n".join(allf.format(part, ustr) for part in parts)

mstr = parts[0]
else:
# Use custom ndarray text formatting
formatter = "{{:{}}}".format(mspec)
with printoptions(formatter={"float_kind": formatter.format}):
mstr = format(obj.magnitude).replace("\n", "")
Expand All @@ -361,13 +385,14 @@ def __format__(self, spec):

if "L" in spec:
mstr = self._exp_pattern.sub(r"\1\\times 10^{\2\3}", mstr)
elif "H" in spec:
mstr = self._exp_pattern.sub(r"\1×10^{\2\3}", mstr)
elif "P" in spec:
elif "H" in spec or "P" in spec:
m = self._exp_pattern.match(mstr)
_exp_formatter = (
_pretty_fmt_exponent if "P" in spec else lambda s: f"<sup>{s}</sup>"
)
if m:
exp = int(m.group(2) + m.group(3))
mstr = self._exp_pattern.sub(r"\1×10" + _pretty_fmt_exponent(exp), mstr)
mstr = self._exp_pattern.sub(r"\1×10" + _exp_formatter(exp), mstr)

if allf == plain_allf and ustr.startswith("1 /"):
# Write e.g. "3 / s" instead of "3 1 / s"
Expand Down
16 changes: 8 additions & 8 deletions pint/testsuite/test_measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ def test_format(self):
("{!r}", "<Measurement(4.0, 0.1, second ** 2)>"),
("{:P}", "(4.00 ± 0.10) second²"),
("{:L}", r"\left(4.00 \pm 0.10\right)\ \mathrm{second}^{2}"),
("{:H}", r"\[(4.00 &plusmn; 0.10)\ {second}^{2}\]"),
("{:H}", "(4.00 &plusmn; 0.10) second<sup>2</sup>"),
("{:C}", "(4.00+/-0.10) second**2"),
("{:Lx}", r"\SI{4.00 +- 0.10}{\second\squared}"),
("{:.1f}", "(4.0 +/- 0.1) second ** 2"),
("{:.1fP}", "(4.0 ± 0.1) second²"),
("{:.1fL}", r"\left(4.0 \pm 0.1\right)\ \mathrm{second}^{2}"),
("{:.1fH}", r"\[(4.0 &plusmn; 0.1)\ {second}^{2}\]"),
("{:.1fH}", "(4.0 &plusmn; 0.1) second<sup>2</sup>"),
("{:.1fC}", "(4.0+/-0.1) second**2"),
("{:.1fLx}", r"\SI{4.0 +- 0.1}{\second\squared}"),
):
Expand All @@ -70,7 +70,7 @@ def test_format_paru(self):
("{:.3uS}", "0.2000(100) second ** 2"),
("{:.3uSP}", "0.2000(100) second²"),
("{:.3uSL}", r"0.2000\left(100\right)\ \mathrm{second}^{2}"),
("{:.3uSH}", r"\[0.2000(100)\ {second}^{2}\]"),
("{:.3uSH}", "0.2000(100) second<sup>2</sup>"),
("{:.3uSC}", "0.2000(100) second**2"),
):
with self.subTest(spec):
Expand All @@ -84,7 +84,7 @@ def test_format_u(self):
("{:.3u}", "(0.2000 +/- 0.0100) second ** 2"),
("{:.3uP}", "(0.2000 ± 0.0100) second²"),
("{:.3uL}", r"\left(0.2000 \pm 0.0100\right)\ \mathrm{second}^{2}"),
("{:.3uH}", r"\[(0.2000 &plusmn; 0.0100)\ {second}^{2}\]"),
("{:.3uH}", "(0.2000 &plusmn; 0.0100) second<sup>2</sup>"),
("{:.3uC}", "(0.2000+/-0.0100) second**2"),
("{:.3uLx}", r"\SI{0.2000 +- 0.0100}{\second\squared}",),
("{:.1uLx}", r"\SI{0.20 +- 0.01}{\second\squared}"),
Expand All @@ -101,7 +101,7 @@ def test_format_percu(self):
("{:.1u%}", "(20 +/- 1)% second ** 2"),
("{:.1u%P}", "(20 ± 1)% second²"),
("{:.1u%L}", r"\left(20 \pm 1\right) \%\ \mathrm{second}^{2}"),
("{:.1u%H}", r"\[(20 &plusmn; 1)%\ {second}^{2}\]"),
("{:.1u%H}", "(20 &plusmn; 1)% second<sup>2</sup>"),
("{:.1u%C}", "(20+/-1)% second**2"),
):
with self.subTest(spec):
Expand All @@ -117,7 +117,7 @@ def test_format_perce(self):
"{:.1ueL}",
r"\left(2.0 \pm 0.1\right) \times 10^{-1}\ \mathrm{second}^{2}",
),
("{:.1ueH}", r"\[(2.0 &plusmn; 0.1)×10^{-1}\ {second}^{2}\]"),
("{:.1ueH}", "(2.0 &plusmn; 0.1)×10<sup>-1</sup> second<sup>2</sup>"),
("{:.1ueC}", "(2.0+/-0.1)e-01 second**2"),
):
with self.subTest(spec):
Expand All @@ -132,7 +132,7 @@ def test_format_exponential_pos(self):
("{!r}", "<Measurement(4e+20, 1e+19, second ** 2)>"),
("{:P}", "(4.00 ± 0.10)×10²⁰ second²"),
("{:L}", r"\left(4.00 \pm 0.10\right) \times 10^{20}\ \mathrm{second}^{2}"),
("{:H}", r"\[(4.00 &plusmn; 0.10)×10^{20}\ {second}^{2}\]"),
("{:H}", "(4.00 &plusmn; 0.10)×10<sup>20</sup> second<sup>2</sup>"),
("{:C}", "(4.00+/-0.10)e+20 second**2"),
("{:Lx}", r"\SI{4.00 +- 0.10 e+20}{\second\squared}"),
):
Expand All @@ -149,7 +149,7 @@ def test_format_exponential_neg(self):
"{:L}",
r"\left(4.00 \pm 0.10\right) \times 10^{-20}\ \mathrm{second}^{2}",
),
("{:H}", r"\[(4.00 &plusmn; 0.10)×10^{-20}\ {second}^{2}\]"),
("{:H}", "(4.00 &plusmn; 0.10)×10<sup>-20</sup> second<sup>2</sup>"),
("{:C}", "(4.00+/-0.10)e-20 second**2"),
("{:Lx}", r"\SI{4.00 +- 0.10 e-20}{\second\squared}"),
):
Expand Down
25 changes: 17 additions & 8 deletions pint/testsuite/test_quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,15 @@ def test_quantity_format(self):
r"4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}",
),
("{:P}", "4.12345678 kilogram·meter²/second"),
("{:H}", r"\[4.12345678\ kilogram\ {meter}^{2}/second\]"),
("{:H}", "4.12345678 kilogram meter<sup>2</sup>/second"),
("{:C}", "4.12345678 kilogram*meter**2/second"),
("{:~}", "4.12345678 kg * m ** 2 / s"),
(
"{:L~}",
r"4.12345678\ \frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}",
),
("{:P~}", "4.12345678 kg·m²/s"),
("{:H~}", r"\[4.12345678\ kg\ {m}^{2}/s\]"),
("{:H~}", "4.12345678 kg m<sup>2</sup>/s"),
("{:C~}", "4.12345678 kg*m**2/s"),
("{:Lx}", r"\SI[]{4.12345678}{\kilo\gram\meter\squared\per\second}"),
):
Expand Down Expand Up @@ -176,6 +176,15 @@ def test_quantity_array_format(self):
),
("{:.2f~P}", "[0.00 1.00 10000000.00 1000000000000.00 nan inf] kg·m²"),
("{:g~P}", "[1e-16 1 1e+07 1e+12 nan inf] kg·m²"),
(
"{:.2f~H}",
(
"<table><tbody><tr><th>Magnitude</th><td style='text-align:left;'>"
"<pre>[0.00 1.00 10000000.00 1000000000000.00 nan inf]</pre></td></tr>"
"<tr><th>Units</th><td style='text-align:left;'>kg m<sup>2</sup></td></tr>"
"</tbody></table>"
),
),
):
with self.subTest(spec):
self.assertEqual(spec.format(x), result)
Expand Down Expand Up @@ -209,12 +218,12 @@ def test_default_formatting(self):
r"4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}",
),
("P", "4.12345678 kilogram·meter²/second"),
("H", r"\[4.12345678\ kilogram\ {meter}^{2}/second\]"),
("H", "4.12345678 kilogram meter<sup>2</sup>/second"),
("C", "4.12345678 kilogram*meter**2/second"),
("~", "4.12345678 kg * m ** 2 / s"),
("L~", r"4.12345678\ \frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}"),
("P~", "4.12345678 kg·m²/s"),
("H~", r"\[4.12345678\ kg\ {m}^{2}/s\]"),
("H~", "4.12345678 kg m<sup>2</sup>/s"),
("C~", "4.12345678 kg*m**2/s"),
):
with self.subTest(spec):
Expand All @@ -224,12 +233,12 @@ def test_default_formatting(self):
def test_exponent_formatting(self):
ureg = UnitRegistry()
x = ureg.Quantity(1e20, "meter")
self.assertEqual(f"{x:~H}", r"\[1×10^{20}\ m\]")
self.assertEqual(f"{x:~H}", r"1×10<sup>20</sup> m")
self.assertEqual(f"{x:~L}", r"1\times 10^{20}\ \mathrm{m}")
self.assertEqual(f"{x:~P}", r"1×10²⁰ m")

x /= 1e40
self.assertEqual(f"{x:~H}", r"\[1×10^{-20}\ m\]")
self.assertEqual(f"{x:~H}", r"1×10<sup>-20</sup> m")
self.assertEqual(f"{x:~L}", r"1\times 10^{-20}\ \mathrm{m}")
self.assertEqual(f"{x:~P}", r"1×10⁻²⁰ m")

Expand All @@ -250,7 +259,7 @@ def pretty(cls, data):

ureg = UnitRegistry()
x = 3.5 * ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1))
self.assertEqual(x._repr_html_(), r"\[3.5\ kilogram\ {meter}^{2}/second\]")
self.assertEqual(x._repr_html_(), "3.5 kilogram meter<sup>2</sup>/second")
self.assertEqual(
x._repr_latex_(),
r"$3.5\ \frac{\mathrm{kilogram} \cdot "
Expand All @@ -259,7 +268,7 @@ def pretty(cls, data):
x._repr_pretty_(Pretty, False)
self.assertEqual("".join(alltext), "3.5 kilogram·meter²/second")
ureg.default_format = "~"
self.assertEqual(x._repr_html_(), r"\[3.5\ kg\ {m}^{2}/s\]")
self.assertEqual(x._repr_html_(), "3.5 kg m<sup>2</sup>/s")
self.assertEqual(
x._repr_latex_(),
r"$3.5\ \frac{\mathrm{kg} \cdot " r"\mathrm{m}^{2}}{\mathrm{s}}$",
Expand Down
Loading

0 comments on commit dea623a

Please sign in to comment.