├── .gitignore ├── LICENSE ├── README.md ├── block.png ├── braille.png ├── gamma.png ├── matplotlib_terminal ├── __init__.py └── backend.py ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/**/usage.statistics.xml 10 | .idea/**/dictionaries 11 | .idea/**/shelf 12 | 13 | # Generated files 14 | .idea/**/contentModel.xml 15 | 16 | # Sensitive or high-churn files 17 | .idea/**/dataSources/ 18 | .idea/**/dataSources.ids 19 | .idea/**/dataSources.local.xml 20 | .idea/**/sqlDataSources.xml 21 | .idea/**/dynamic.xml 22 | .idea/**/uiDesigner.xml 23 | .idea/**/dbnavigator.xml 24 | 25 | # Gradle 26 | .idea/**/gradle.xml 27 | .idea/**/libraries 28 | 29 | # Gradle and Maven with auto-import 30 | # When using Gradle or Maven with auto-import, you should exclude module files, 31 | # since they will be recreated, and may cause churn. Uncomment if using 32 | # auto-import. 33 | # .idea/artifacts 34 | # .idea/compiler.xml 35 | # .idea/jarRepositories.xml 36 | # .idea/modules.xml 37 | # .idea/*.iml 38 | # .idea/modules 39 | # *.iml 40 | # *.ipr 41 | 42 | # CMake 43 | cmake-build-*/ 44 | 45 | # Mongo Explorer plugin 46 | .idea/**/mongoSettings.xml 47 | 48 | # File-based project format 49 | *.iws 50 | 51 | # IntelliJ 52 | out/ 53 | 54 | # mpeltonen/sbt-idea plugin 55 | .idea_modules/ 56 | 57 | # JIRA plugin 58 | atlassian-ide-plugin.xml 59 | 60 | # Cursive Clojure plugin 61 | .idea/replstate.xml 62 | 63 | # Crashlytics plugin (for Android Studio and IntelliJ) 64 | com_crashlytics_export_strings.xml 65 | crashlytics.properties 66 | crashlytics-build.properties 67 | fabric.properties 68 | 69 | # Editor-based Rest Client 70 | .idea/httpRequests 71 | 72 | # Android studio 3.1+ serialized cache file 73 | .idea/caches/build_file_checksums.ser 74 | 75 | ### Python template 76 | # Byte-compiled / optimized / DLL files 77 | __pycache__/ 78 | *.py[cod] 79 | *$py.class 80 | 81 | # C extensions 82 | *.so 83 | 84 | # Distribution / packaging 85 | .Python 86 | build/ 87 | develop-eggs/ 88 | dist/ 89 | downloads/ 90 | eggs/ 91 | .eggs/ 92 | lib/ 93 | lib64/ 94 | parts/ 95 | sdist/ 96 | var/ 97 | wheels/ 98 | pip-wheel-metadata/ 99 | share/python-wheels/ 100 | *.egg-info/ 101 | .installed.cfg 102 | *.egg 103 | MANIFEST 104 | 105 | # PyInstaller 106 | # Usually these files are written by a python script from a template 107 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 108 | *.manifest 109 | *.spec 110 | 111 | # Installer logs 112 | pip-log.txt 113 | pip-delete-this-directory.txt 114 | 115 | # Unit test / coverage reports 116 | htmlcov/ 117 | .tox/ 118 | .nox/ 119 | .coverage 120 | .coverage.* 121 | .cache 122 | nosetests.xml 123 | coverage.xml 124 | *.cover 125 | *.py,cover 126 | .hypothesis/ 127 | .pytest_cache/ 128 | cover/ 129 | 130 | # Translations 131 | *.mo 132 | *.pot 133 | 134 | # Django stuff: 135 | *.log 136 | local_settings.py 137 | db.sqlite3 138 | db.sqlite3-journal 139 | 140 | # Flask stuff: 141 | instance/ 142 | .webassets-cache 143 | 144 | # Scrapy stuff: 145 | .scrapy 146 | 147 | # Sphinx documentation 148 | docs/_build/ 149 | 150 | # PyBuilder 151 | .pybuilder/ 152 | target/ 153 | 154 | # Jupyter Notebook 155 | .ipynb_checkpoints 156 | 157 | # IPython 158 | profile_default/ 159 | ipython_config.py 160 | 161 | # pyenv 162 | # For a library or package, you might want to ignore these files since the code is 163 | # intended to run in multiple environments; otherwise, check them in: 164 | # .python-version 165 | 166 | # pipenv 167 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 168 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 169 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 170 | # install all needed dependencies. 171 | #Pipfile.lock 172 | 173 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 174 | __pypackages__/ 175 | 176 | # Celery stuff 177 | celerybeat-schedule 178 | celerybeat.pid 179 | 180 | # SageMath parsed files 181 | *.sage.py 182 | 183 | # Environments 184 | .env 185 | .venv 186 | env/ 187 | venv/ 188 | ENV/ 189 | env.bak/ 190 | venv.bak/ 191 | 192 | # Spyder project settings 193 | .spyderproject 194 | .spyproject 195 | 196 | # Rope project settings 197 | .ropeproject 198 | 199 | # mkdocs documentation 200 | /site 201 | 202 | # mypy 203 | .mypy_cache/ 204 | .dmypy.json 205 | dmypy.json 206 | 207 | # Pyre type checker 208 | .pyre/ 209 | 210 | # pytype static type analyzer 211 | .pytype/ 212 | 213 | # Cython debug symbols 214 | cython_debug/ 215 | 216 | ### Linux template 217 | *~ 218 | 219 | # temporary files which can be created if a process still has a handle open of a deleted file 220 | .fuse_hidden* 221 | 222 | # KDE directory preferences 223 | .directory 224 | 225 | # Linux trash folder which might appear on any partition or disk 226 | .Trash-* 227 | 228 | # .nfs files are created when an open file is removed but is still being accessed 229 | .nfs* 230 | 231 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Maciej Matraszek 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # matplotlib-terminal 2 | Matplotlib backend to plot in terminal using [matrach/img2unicode](https://github.com/matrach/img2unicode) 3 | 4 | This is in proof of concept stage, so stay tuned! 5 | The library is optimized for Gnome Terminal with Ubuntu Mono font. 6 | Nevertheless `'block'` and `'braille'` renderers should work with most modern terminals. 7 | 8 | Install it with: 9 | ```sh 10 | $ pip install matplotlib-terminal 11 | ``` 12 | 13 | To speed ~10x up the ``braille`` and ``gamma`` renderers, install an optional dependency of ``img2unicode``: 14 | ```sh 15 | $ pip install 'img2unicode[n2]' 16 | ``` 17 | 18 | ## Alternatives, which operate on raster output 19 | - https://github.com/jktr/matplotlib-backend-notcurses 20 | - https://github.com/jktr/matplotlib-backend-kitty 21 | 22 | ## Usage: 23 | ```python 24 | import matplotlib_terminal 25 | import matplotlib.pyplot as plt 26 | # Or in short: 27 | # from matplotlib_terminal import plt 28 | 29 | 30 | plt.plot([0, 1], [0, 1]) 31 | plt.plot([1, 0], [0, 1], lw=3) 32 | plt.scatter([0], [.5]) 33 | 34 | plt.show() 35 | plt.show('gamma') # Use RendererGamma-fast/noblock from img2unicode renderer 36 | plt.show('block') # Use Renderer-fast/block from img2unicode, dual color! 37 | plt.show('braille') # Use RendererGamma-fast/braille from img2unicode renderer 38 | plt.close() 39 | ``` 40 | 41 | ## Sample results 42 | Gamma renderer: 43 | ![gamma renderer](gamma.png) 44 | Block renderer: 45 | ![block renderer](block.png) 46 | Braille renderer: 47 | ![braille renderer](braille.png) 48 | 49 | ## TODO 50 | 51 | - [ ] figure out how to configure the lib in matplotlib-ish way (eg. rcParams) 52 | - [ ] allow to specify figure size in terms of cells 53 | - [ ] compare with alternatives 54 | - [ ] auto select backend from alternatives 55 | -------------------------------------------------------------------------------- /block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrach/matplotlib-terminal/51b6ca771bdbe6d9f255dc41779736d181f0f9d2/block.png -------------------------------------------------------------------------------- /braille.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrach/matplotlib-terminal/51b6ca771bdbe6d9f255dc41779736d181f0f9d2/braille.png -------------------------------------------------------------------------------- /gamma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrach/matplotlib-terminal/51b6ca771bdbe6d9f255dc41779736d181f0f9d2/gamma.png -------------------------------------------------------------------------------- /matplotlib_terminal/__init__.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | matplotlib.use('module://matplotlib_terminal.backend') 3 | 4 | import matplotlib.pyplot as plt 5 | plt.style.use('dark_background') 6 | plt.style.use({ 7 | 'figure.figsize': (11, 8), 8 | 'figure.autolayout': True, 9 | 'lines.linewidth': 2, 10 | 'axes.edgecolor': 'white', 11 | 'xtick.major.size': 6, 12 | 'xtick.major.width': 3, 13 | 'xtick.direction': 'out', 14 | 'ytick.major.size': 6, 15 | 'ytick.major.width': 3, 16 | 'ytick.direction': 'out', 17 | 18 | 'font.family': 'monospace', 19 | 'font.size': 12, 20 | 'font.monospace': 'Ubuntu Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Computer Modern Typewriter, Andale Mono, Nimbus Mono L, Courier New, Courier, Fixed, Terminal, monospace', 21 | 22 | 23 | 'legend.fancybox': False, 24 | 'legend.edgecolor': 'white', 25 | 'legend.fontsize': 'medium', 26 | 27 | # 'terminal.unicode_latex': True, 28 | }) 29 | -------------------------------------------------------------------------------- /matplotlib_terminal/backend.py: -------------------------------------------------------------------------------- 1 | import unicodedata 2 | import sys 3 | import logging 4 | 5 | import PIL.Image 6 | import numpy as np 7 | from matplotlib._pylab_helpers import Gcf 8 | from matplotlib.backend_bases import FigureCanvasBase 9 | from matplotlib.backends.backend_agg import RendererAgg 10 | 11 | import img2unicode 12 | 13 | 14 | class MyRenderer(RendererAgg): 15 | def __init__(self, *args): 16 | super().__init__(*args) 17 | self.texts = [] 18 | 19 | def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): 20 | size = prop.get_size_in_points() 21 | logging.debug("Draw text %s", (gc, x, y, s, prop, angle, ismath, mtext, size, gc.get_rgb())) 22 | if prop.get_size_in_points() > 18: 23 | return super().draw_text(gc, x, y, s, prop, angle, ismath, mtext) 24 | else: 25 | self.texts.append( 26 | (x, y, s, size, ismath, np.array(gc.get_rgb()[:3]))) 27 | 28 | circ_numr = ord('❶') 29 | circ_num = ord('①') 30 | 31 | circ_letc = ord('Ⓐ') 32 | circ_let = ord('ⓐ') 33 | par_let = ord('⒜') 34 | par_num = ord('⑴') 35 | serif_num = ord('') 36 | serif_letc = ord('') 37 | num2_0 = ord('') 38 | sup_num_0 = ord('') 39 | sub_num_0 = ord('') 40 | wide_ascii = ord('!') 41 | 42 | 43 | optimizers = { 44 | 'gamma': img2unicode.GammaRenderer(img2unicode.BestGammaOptimizer(True, 'no_block'), max_h=60, max_w=180, allow_upscale=True), 45 | 'block': img2unicode.Renderer(img2unicode.FastGenericDualOptimizer('block'), max_h=60, max_w=180, allow_upscale=True), 46 | 'braille': img2unicode.GammaRenderer(img2unicode.BestGammaOptimizer(True, 'braille'), max_h=60, max_w=180, allow_upscale=True), 47 | } 48 | 49 | class FigureCanvasUnicodeAgg(FigureCanvasBase): 50 | def __init__(self, figure, *args, **kwargs): 51 | # print("USING MY CUSTOM FIGURE") 52 | self.ua_figure = figure 53 | self.R = optimizers['gamma'] 54 | super().__init__(figure, *args, **kwargs) 55 | 56 | def draw(self, rendering='gamma'): 57 | self.R = optimizers[rendering] 58 | w, h = self.get_width_height() 59 | self.renderer = MyRenderer(w, h, self.ua_figure.dpi) 60 | with self.renderer.lock: 61 | self.ua_figure.draw(self.renderer) 62 | super().draw() 63 | 64 | arr = np.asarray(self.renderer.buffer_rgba()) 65 | img = PIL.Image.fromarray(arr, 'RGBA') 66 | logging.debug("Max h %s ,w %s", h, w) 67 | # Hax 68 | self.R.max_h = h/16 69 | self.R.max_w = w/8 70 | self.chars, self.fores, self.backs = self.R.render_numpy(img) 71 | self.substitute_text(max(w/self.R.max_w, h/self.R.max_h/2)) 72 | 73 | def substitute_text(self, scale): 74 | from img2unicode import unicodeit 75 | for x, y, s, size, ismath, col in self.renderer.texts: 76 | xi, yi = int(round(x // scale)), int(round(y // (scale*2) )) 77 | if ismath and True: 78 | s = unicodeit.replace(s[1:-1]) 79 | for i, c in enumerate(s): 80 | char = ord(c) 81 | if yi >= self.chars.shape[0] or xi+i >= self.chars.shape[1]: 82 | continue 83 | # if size < 6 and '0' <= c <= '9': 84 | # if c == '0': 85 | # char -= 9 86 | # char += sub_num_0 - ord('0') 87 | if size < 8: 88 | if '0' <= c <= '9': 89 | char += serif_num - ord('0') 90 | c2 = c.upper() 91 | if 'A' <= c2 <= 'Z': 92 | char = ord(c2) + serif_letc - ord('A') 93 | if size > 14: 94 | if '!' <= c <= chr(125): 95 | char += wide_ascii - ord('!') 96 | elif c == ' ': 97 | char = 0x3000 98 | if unicodedata.east_asian_width(chr(char)) == 'F': 99 | i *= 2 100 | if xi+i+1 >= self.chars.shape[1]: 101 | continue 102 | self.chars[yi, xi + i + 1] = ord('\N{ZERO WIDTH SPACE}') 103 | self.chars[yi, xi + i] = char 104 | self.fores[yi, xi + i] = col * 255 # np.array((1, 1, 1)) 105 | 106 | 107 | def print_terminal(self, rendering='gamma'): 108 | self.draw(rendering=rendering) 109 | self.R.print_to_terminal(sys.stdout, self.chars, self.fores, self.backs, 110 | **({'sentinel':''} if rendering == 'block' else {})) 111 | 112 | def get_default_filetype(self): 113 | return 'txt' 114 | 115 | def print_txt(self, filename_or_obj, *args, rendering='gamma', **kwargs): 116 | self.draw(rendering=rendering) 117 | self.R.print_to_terminal(filename_or_obj, self.chars, self.fores, self.backs, 118 | **({'sentinel':''} if rendering == 'block' else {})) 119 | 120 | 121 | def get_renderer(self, cleared=False): 122 | return 123 | 124 | def show(rendering='gamma'): 125 | # print("SHOWING!!!") 126 | for manager in Gcf.get_all_fig_managers(): 127 | manager.canvas.print_terminal(rendering=rendering) 128 | 129 | # Expected by matplotlib.use 130 | FigureCanvas = FigureCanvasUnicodeAgg 131 | show = show 132 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup(name='matplotlib-terminal', 7 | version='0.1a4', 8 | description='Render matplotlib plots in terminal.', 9 | long_description=long_description, 10 | long_description_content_type="text/markdown", 11 | url='https://github.com/matrach/matplotlib-terminal', 12 | author='Maciej Matraszek', 13 | author_email='matraszek.maciej@gmail.com', 14 | license='MIT', 15 | packages=find_packages(), 16 | install_requires=[ 17 | 'matplotlib', 18 | 'img2unicode>=0.1a8', 19 | ], 20 | extras_require={ 21 | 'develop': [ 22 | 'pytest', 23 | 'pytest-cov', 24 | 'sphinx', 25 | 'sphinx_autodoc_typehints', 26 | ] 27 | }, 28 | classifiers=[ 29 | "Programming Language :: Python :: 3", 30 | "License :: OSI Approved :: MIT License", 31 | "Operating System :: OS Independent", 32 | ], 33 | python_requires='>=3.6', 34 | zip_safe=False) 35 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib_terminal 3 | import matplotlib.pyplot as plt 4 | fig = plt.figure() 5 | 6 | plt.plot([0, 1], [0, 1]) 7 | plt.plot([1, 0], [0, 1], lw=3) 8 | plt.scatter([0], [.5]) 9 | 10 | plt.show('gamma') 11 | plt.show('block') 12 | plt.show('braille') 13 | plt.close() 14 | 15 | 16 | ax = plt.gca() 17 | 18 | plt.text(0.4, 0.1, "bar555", size=5,) 19 | plt.text(0.5, 0.1, "bar6", size=6,) 20 | plt.text(0.6, 0.2, "bar8", size=8,) 21 | plt.text(0.7, 0.3, "bar10", size=10,) 22 | plt.text(0.8, 0.5, "bar14", size=14,) 23 | plt.text(0.8, 0.8, "bar16", size=16,) 24 | plt.text(0.9, 0.8, "bar18", size=18,) 25 | plt.text(0.8, 0.6, "bar20", size=20,) 26 | plt.text(0.4, 0.2, "bar30", size=30,) 27 | plt.text(0.3, 0.6, "bar50", size=50,) 28 | 29 | plt.text(-0.5, 0.4, "$\sum_{k=0}^n k+1$", size=30,) 30 | plt.text(-0.8, 0.3, "$\sum_{k=0}^n x_k+1$", size=12,) 31 | 32 | plt.plot([0, 1], [0, 1], c='g', label='alamakota', lw=4) 33 | x = np.linspace(0, 1, 400) 34 | plt.plot(x, np.sin(1/x), c='r', label='sin 1/x', lw=4) 35 | 36 | 37 | # im = plt.imshow(template, extent=[0, 1, 0, 1]) 38 | # plt.colorbar(im) 39 | 40 | 41 | delta = 0.025 42 | x = np.arange(-1.0, 1.0, delta) 43 | y = np.arange(-1.0, 1.0, delta) 44 | X, Y = np.meshgrid(x, y) 45 | Z1 = np.exp(-X**2 - Y**2) 46 | Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) 47 | Z = (Z1 - Z2) * 2 48 | CS = ax.contour(X, Y, Z) 49 | ax.clabel(CS, inline=1, fontsize=10) 50 | ax.set_title('Simplest default with labels') 51 | 52 | plt.legend() 53 | plt.title('Some random plotting') 54 | plt.suptitle('Testing') 55 | 56 | plt.show('gamma') 57 | plt.show('block') 58 | plt.show('braille') 59 | plt.close() 60 | 61 | 62 | category_names = ['Strongly disagree', 'Disagree', 63 | 'Neither agree nor disagree', 'Agree', 'Strongly agree'] 64 | results = { 65 | 'Question 1': [10, 15, 17, 32, 26], 66 | 'Question 2': [26, 22, 29, 10, 13], 67 | 'Question 3': [35, 37, 7, 2, 19], 68 | 'Question 4': [32, 11, 9, 15, 33], 69 | 'Question 5': [21, 29, 5, 5, 40], 70 | 'Question 6': [8, 19, 5, 30, 38] 71 | } 72 | 73 | plt.style.use('dark_background') 74 | def survey(results, category_names, fig, ax): 75 | """ 76 | Parameters 77 | ---------- 78 | results : dict 79 | A mapping from question labels to a list of answers per category. 80 | It is assumed all lists contain the same number of entries and that 81 | it matches the length of *category_names*. 82 | category_names : list of str 83 | The category labels. 84 | """ 85 | labels = list(results.keys()) 86 | data = np.array(list(results.values())) 87 | data_cum = data.cumsum(axis=1) 88 | category_colors = plt.get_cmap('RdYlGn')( 89 | np.linspace(0.15, 0.85, data.shape[1])) 90 | 91 | ax.invert_yaxis() 92 | ax.xaxis.set_visible(False) 93 | ax.set_xlim(0, np.sum(data, axis=1).max()) 94 | 95 | for i, (colname, color) in enumerate(zip(category_names, category_colors)): 96 | widths = data[:, i] 97 | starts = data_cum[:, i] - widths 98 | ax.barh(labels, widths, left=starts, height=0.5, 99 | label=colname, color=color) 100 | xcenters = starts + widths / 2 101 | 102 | r, g, b, _ = color 103 | text_color = 'white' if r * g * b < 0.5 else 'darkgrey' 104 | for y, (x, c) in enumerate(zip(xcenters, widths)): 105 | ax.text(x, y, str(int(c)), ha='center', va='center', 106 | color=text_color) 107 | ax.legend(ncol=len(category_names), bbox_to_anchor=(0, 1), 108 | loc='lower left') 109 | 110 | return fig, ax 111 | 112 | fig = plt.figure() 113 | ax = plt.gca() 114 | survey(results, category_names, fig, ax) 115 | 116 | plt.show() 117 | plt.show('block') 118 | plt.show('braille') 119 | plt.savefig('xxx.png') 120 | plt.savefig('xxx.txt') 121 | 122 | plt.close() 123 | plt.text(0, 0, r'$\sum_{i=0}^t (\frac{\sum_{k=0}^p c_{i,p}\cdot s_p}{|c_i|}nc_i + \frac{\sum_{k=0}^p (1-c_{i,p})\cdot s_p}{|1-c_i|}n(1-c_i)- s)^2$', fontsize=50) 124 | plt.axis('off') 125 | plt.show() 126 | --------------------------------------------------------------------------------