├── pyproject.toml ├── numpy_html ├── __init__.py ├── formatter.py └── renderer.py ├── README.md └── LICENSE.md /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool] 2 | [tool.poetry] 3 | name = "numpy-html" 4 | version = "1.0.0" 5 | description = "A simple table renderer for numpy arrays. Provides a rich display hook for use with Jupyter Lab / Notebook." 6 | license = "MIT" 7 | classifiers = ["Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Framework :: Jupyter", "Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent"] 8 | authors = ["Angus Hollands "] 9 | readme = "README.md" 10 | 11 | [tool.poetry.dependencies] 12 | python = ">=3.6" 13 | numpy = "*" 14 | -------------------------------------------------------------------------------- /numpy_html/__init__.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from .formatter import array_to_html 3 | 4 | 5 | def register_formatter(ipython, cls): 6 | html_formatter = ipython.display_formatter.formatters["text/html"] 7 | html_formatter.for_type(cls, array_to_html) 8 | 9 | 10 | def unregister_formatter(ipython, cls): 11 | html_formatter = ipython.display_formatter.formatters["text/html"] 12 | html_formatter.pop(cls) 13 | 14 | 15 | def load_ipython_extension(ipython): 16 | register_formatter(ipython, np.ndarray) 17 | 18 | 19 | def unload_ipython_extension(ipython): 20 | unregister_formatter(ipython, np.ndarray) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # numpy-html 2 | [![pypi-badge][]][pypi] 3 | 4 | [pypi-badge]: https://img.shields.io/pypi/v/numpy-html 5 | [pypi]: https://pypi.org/project/numpy-html 6 | 7 | A simple table renderer for numpy arrays. Provides a rich display hook for use with Jupyter Lab / Notebook. Inspired by [xtensor](https://github.com/QuantStack/xtensor). 8 | 9 | ## Installation 10 | `pip install numpy-html` 11 | 12 | ## Example inside Jupyter 13 | ```python 14 | %load_ext numpy_html 15 | import numpy as np 16 | 17 | np.set_printoptions(threshold=5, edgeitems=2) 18 | np.arange(49).reshape(7, 7) 19 | ``` 20 | | 0 | 1 | ⋯ | 5 | 6 | 21 | |:--: |:--: |:-: |:--: |:--: | 22 | | 7 | 8 | ⋯ | 12 | 13 | 23 | | ⋮ | ⋮ | ⋱ | ⋮ | ⋮ | 24 | | 35 | 36 | ⋯ | 40 | 41 | 25 | | 42 | 43 | ⋯ | 47 | 48 | 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Angus Hollands 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /numpy_html/formatter.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from contextlib import contextmanager 3 | 4 | import numpy as np 5 | 6 | from .renderer import render_table, ITEM_TYPE, INDEX_TYPE, TemplateItem 7 | 8 | # Type of elements or templates 9 | ITEMS_TYPE = typing.Iterable[ITEM_TYPE] 10 | 11 | 12 | def format_index(index: INDEX_TYPE) -> typing.Union[INDEX_TYPE, int]: 13 | """Format the index tuple corresponding to a particular array element. Return the 14 | 15 | :param index: tuple of integers representing the array index 16 | :return: sole entry of `index` if a tuple of length 1, otherwise `index` 17 | """ 18 | if len(index) == 1: 19 | return index[0] 20 | return index 21 | 22 | 23 | def format_items(items: ITEMS_TYPE, format_element: typing.Callable[..., str], **format_kwargs) -> typing.Iterator[str]: 24 | """Yield the formatted strings for the given items. Those items which are not templates are yielded directly. 25 | 26 | :param items: iterable of templates or strings 27 | :param format_element: array element formatter 28 | :param format_kwargs: additional keyword arguments for element formatter 29 | :return: Iterator of formatted strings 30 | """ 31 | for item in items: 32 | if isinstance(item, str): 33 | yield item 34 | else: 35 | # We have a string template element, yield formatted string (using formatter and template) 36 | template, index, element = item 37 | yield template.format(format_index(index), format_element(element, **format_kwargs)) 38 | 39 | 40 | def fixed_format_element_npy(x, max_width: int = None) -> str: 41 | """Fixed with formatter using numpy.array2string. If `max_width` is None, then return the formatted element 42 | directly, otherwise left pad such that the final string has length `max_width`. 43 | 44 | :param x: element to render 45 | :param max_width: width of maximum element (predetermined) 46 | :return: formatted string 47 | """ 48 | # Use numpy to format element 49 | x_str = np.array2string(x) 50 | if max_width is None: 51 | return x_str 52 | 53 | # Return padded left-aligned string 54 | return f"{x_str:{max_width}}" 55 | 56 | 57 | def fixed_format_items(items: ITEMS_TYPE) -> typing.List[str]: 58 | """Format items using a fixed with formatter. 59 | 60 | :param items: iterable of templates or strings 61 | :return: formatted string 62 | """ 63 | items = list(items) 64 | 65 | with np.printoptions(floatmode="maxprec"): 66 | template_lengths = [len(fixed_format_element_npy(t.item)) for t in items if isinstance(t, TemplateItem)] 67 | try: 68 | max_width = max(template_lengths) 69 | except ValueError: 70 | max_width = None 71 | return [*format_items(items, fixed_format_element_npy, max_width=max_width)] 72 | 73 | 74 | def array_to_html( 75 | array: np.ndarray, formatter: typing.Callable[..., typing.List[str]] = fixed_format_items, **formatter_kwargs 76 | ) -> str: 77 | """Render NumPy array as an HTML table. 78 | 79 | :param array: ndarray object 80 | :param formatter: items formatter 81 | :param formatter_kwargs: keyword arguments for items formatter 82 | :return: HTML string 83 | """ 84 | print_options = np.get_printoptions() 85 | edge_items = print_options["edgeitems"] 86 | threshold = print_options["threshold"] 87 | 88 | if array.size < threshold: 89 | edge_items = 0 90 | 91 | items = render_table((), array, edge_items) 92 | return "\n".join(formatter(items, **formatter_kwargs)) 93 | -------------------------------------------------------------------------------- /numpy_html/renderer.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import numpy as np 4 | 5 | 6 | TD_ITEM_HTML_TEMPLATE = '
{}
' 7 | 8 | ELLIPSIS_CELL_HTML_HORIZONTAL = TD_ITEM_HTML_TEMPLATE.format("element(s) elided", "\u2026") 9 | ELLIPSIS_CELL_HTML_VERTICAL = TD_ITEM_HTML_TEMPLATE.format("element(s) elided", "\u22EE") 10 | ELLIPSIS_CELL_HTML_DIAGONAL = TD_ITEM_HTML_TEMPLATE.format("element(s) elided", "\u22F1") 11 | EMPTY_CELL_HTML = TD_ITEM_HTML_TEMPLATE.format("empty array", "\u2800") 12 | 13 | INDEX_TYPE = typing.Tuple[int, ...] 14 | 15 | 16 | class TemplateItem(typing.NamedTuple): 17 | template: str 18 | index: INDEX_TYPE 19 | item: typing.Any 20 | 21 | 22 | ITEM_TYPE = typing.Union[TemplateItem, str] 23 | ITEM_GENERATOR_TYPE = typing.Iterator[ITEM_TYPE] 24 | SUMMARY_RENDERER_TYPE = typing.Callable[ 25 | [INDEX_TYPE, np.ndarray, int], typing.Iterator[str] 26 | ] 27 | ITEM_RENDERER_TYPE = typing.Callable[[INDEX_TYPE, np.ndarray, int], ITEM_GENERATOR_TYPE] 28 | 29 | 30 | def make_constant_renderer(const: str) -> SUMMARY_RENDERER_TYPE: 31 | """Factory function for a single ellipsis renderer. 32 | 33 | :param const: constant string 34 | :return: generator which produces string 35 | """ 36 | 37 | def wrapper( 38 | index: INDEX_TYPE, array: np.ndarray, edge_items: int 39 | ) -> typing.Iterator[str]: 40 | yield const 41 | 42 | return wrapper 43 | 44 | 45 | def ellipsis_renderer_2d( 46 | index: INDEX_TYPE, array: np.ndarray, edge_items: int 47 | ) -> typing.Iterator[str]: 48 | n, m = array.shape 49 | yield "" 50 | if m > 2 * edge_items: 51 | for i in range(edge_items): 52 | yield ELLIPSIS_CELL_HTML_VERTICAL 53 | 54 | yield ELLIPSIS_CELL_HTML_DIAGONAL 55 | 56 | for i in range(edge_items): 57 | yield ELLIPSIS_CELL_HTML_VERTICAL 58 | else: 59 | for i in range(m): 60 | yield ELLIPSIS_CELL_HTML_VERTICAL 61 | yield "" 62 | 63 | 64 | def extend_index(index: INDEX_TYPE, coordinate: int) -> INDEX_TYPE: 65 | return (*index, coordinate) 66 | 67 | 68 | def render_array_items_summarized( 69 | item_renderer: ITEM_RENDERER_TYPE, 70 | summary_renderer: ITEM_RENDERER_TYPE, 71 | index: INDEX_TYPE, 72 | array: np.ndarray, 73 | edge_items: int, 74 | ) -> ITEM_GENERATOR_TYPE: 75 | """Render array, summarising the inner items that have indices between `edge_items` and `len(array)-edge_items`. 76 | 77 | :param item_renderer: item renderer 78 | :param summary_renderer: summary item renderer 79 | :param index: index 80 | :param array: array to render 81 | :param edge_items: number of edge items when summarising 82 | :return: 83 | """ 84 | for i, item in enumerate(array[:edge_items]): 85 | yield from item_renderer(extend_index(index, i), item, edge_items) 86 | 87 | yield from summary_renderer(index, array, edge_items) 88 | 89 | for i, item in enumerate(array[-edge_items:], start=len(array) - edge_items): 90 | yield from item_renderer(extend_index(index, i), item, edge_items) 91 | 92 | 93 | def render_array_items( 94 | item_renderer: ITEM_RENDERER_TYPE, 95 | summary_renderer: ITEM_RENDERER_TYPE, 96 | index: INDEX_TYPE, 97 | array: np.ndarray, 98 | edge_items: int, 99 | ) -> ITEM_GENERATOR_TYPE: 100 | """Render array, dispatching to `render_array_summarised` if required. 101 | 102 | :param item_renderer: item renderer 103 | :param summary_renderer: summary item renderer 104 | :param index: index 105 | :param array: array to render 106 | :param edge_items: number of edge items when summarising 107 | :return: 108 | """ 109 | if edge_items and len(array) > 2 * edge_items: 110 | yield from render_array_items_summarized( 111 | item_renderer, summary_renderer, index, array, edge_items 112 | ) 113 | else: 114 | for i, item in enumerate(array): 115 | yield from item_renderer(extend_index(index, i), item, edge_items) 116 | 117 | 118 | def render_array_0d(index: INDEX_TYPE, item, edge_items: int) -> ITEM_GENERATOR_TYPE: 119 | yield TemplateItem(f"{TD_ITEM_HTML_TEMPLATE}", index, item) 120 | 121 | 122 | def render_row_1d(index: INDEX_TYPE, row, edge_items: int) -> ITEM_GENERATOR_TYPE: 123 | yield TemplateItem(f"{TD_ITEM_HTML_TEMPLATE}", index, row) 124 | 125 | 126 | def render_array_1d( 127 | index: INDEX_TYPE, array: np.ndarray, edge_items: int 128 | ) -> ITEM_GENERATOR_TYPE: 129 | # Special case empty 1D arrays 130 | if not array.shape[0]: 131 | renderer = make_constant_renderer(EMPTY_CELL_HTML) 132 | return renderer(index, array, edge_items) 133 | 134 | return render_array_items( 135 | render_row_1d, 136 | make_constant_renderer(f"{ELLIPSIS_CELL_HTML_VERTICAL}"), 137 | index, 138 | array, 139 | edge_items, 140 | ) 141 | 142 | 143 | def render_elem_2d(index: INDEX_TYPE, item, edge_items: int) -> ITEM_GENERATOR_TYPE: 144 | yield TemplateItem(TD_ITEM_HTML_TEMPLATE, index, item) 145 | 146 | 147 | def render_row_2d( 148 | index: INDEX_TYPE, row: np.ndarray, edge_items: int 149 | ) -> ITEM_GENERATOR_TYPE: 150 | yield "" 151 | yield from render_array_items( 152 | render_elem_2d, 153 | make_constant_renderer(ELLIPSIS_CELL_HTML_HORIZONTAL), 154 | index, 155 | row, 156 | edge_items, 157 | ) 158 | yield "" 159 | 160 | 161 | def render_array_2d( 162 | index: INDEX_TYPE, array: np.ndarray, edge_items: int 163 | ) -> ITEM_GENERATOR_TYPE: 164 | yield from render_array_items( 165 | render_row_2d, ellipsis_renderer_2d, index, array, edge_items 166 | ) 167 | 168 | 169 | def render_row_nd( 170 | index: INDEX_TYPE, row: np.ndarray, edge_items: int 171 | ) -> ITEM_GENERATOR_TYPE: 172 | yield "" 173 | yield from render_table(index, row, edge_items) 174 | yield "" 175 | 176 | 177 | def render_array_nd( 178 | index: INDEX_TYPE, array: np.ndarray, edge_items: int 179 | ) -> ITEM_GENERATOR_TYPE: 180 | yield from render_array_items( 181 | render_row_nd, 182 | make_constant_renderer(ELLIPSIS_CELL_HTML_VERTICAL), 183 | index, 184 | array, 185 | edge_items, 186 | ) 187 | 188 | 189 | _shape_length_to_renderer = {0: render_array_0d, 1: render_array_1d, 2: render_array_2d} 190 | 191 | 192 | def render_array( 193 | index: INDEX_TYPE, array: np.ndarray, edge_items: int 194 | ) -> ITEM_GENERATOR_TYPE: 195 | renderer = _shape_length_to_renderer.get(len(array.shape), render_array_nd) 196 | return renderer(index, array, edge_items) 197 | 198 | 199 | def render_table(index: INDEX_TYPE, array, edge_items: int) -> ITEM_GENERATOR_TYPE: 200 | yield "" 201 | yield from render_array(index, array, edge_items) 202 | yield "
" 203 | --------------------------------------------------------------------------------