├── .gitattributes ├── .gitignore ├── Examples.ipynb ├── LICENSE ├── MANIFEST.in ├── README.md ├── data ├── LICENSE ├── README.md └── mt_bruno_elevation.csv ├── docs └── images │ ├── line.png │ ├── scatter.png │ ├── surface.png │ └── voxels.png ├── plotnine3d ├── __init__.py ├── geoms.py ├── labels.py ├── plot.py └── py.typed ├── setup.cfg └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ipynb linguist-vendored 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Michał Krassowski 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.md plotnine3d/py.typed -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plotnine3d 2 | 3 | 3D geoms for [plotnine](https://github.com/has2k1/plotnine) (grammar of graphics in Python). 4 | 5 | Status: experimental. Please leave feedback; pull requests welcome. 6 | 7 | 8 | ### Examples 9 | 10 | Please refer to the [notebook with examples](https://github.com/krassowski/plotnine3d/blob/main/Examples.ipynb) for more details on data preparation. 11 | 12 | #### Surface 13 | 14 | 15 | ```python 16 | ( 17 | ggplot_3d(mt_bruno_long) 18 | + geom_polygon_3d(size=0.01) 19 | + aes(x='x', y='y', z='height') 20 | + theme_minimal() 21 | ) 22 | ``` 23 | 24 | ![surface](https://raw.githubusercontent.com/krassowski/plotnine3d/main/docs/images/surface.png) 25 | 26 | #### Scatter 27 | 28 | ```python 29 | ( 30 | ggplot_3d(mtcars) 31 | + aes( 32 | x='hp', y='disp', z='mpg', 33 | shape='transmission', 34 | fill='transmission' 35 | ) 36 | + theme_minimal() 37 | + scale_shape_manual(values={'automatic': 'o', 'manual': '^'}) 38 | + geom_point_3d(stroke=0.25, size=3, color='black') 39 | + scale_fill_manual(values={'automatic': 'orange', 'manual': 'blue'}) 40 | ) 41 | ``` 42 | 43 | ![scatter](https://raw.githubusercontent.com/krassowski/plotnine3d/main/docs/images/scatter.png) 44 | 45 | #### Voxels 46 | 47 | ```python 48 | ( 49 | ggplot_3d(voxels_long) 50 | + aes(x='x', y='y', z='z', fill='object') 51 | + geom_voxel_3d(size=0.01) 52 | + theme_minimal() 53 | + ylim(0, 8) 54 | + xlim(0, 8) 55 | + scale_fill_manual(values={ 56 | 'link': 'red', 57 | 'cube1': 'blue', 58 | 'cube2': 'green' 59 | }) 60 | ) 61 | ``` 62 | 63 | 64 | ![voxels](https://raw.githubusercontent.com/krassowski/plotnine3d/main/docs/images/voxels.png) 65 | 66 | #### Line 67 | 68 | ```python 69 | ( 70 | ggplot_3d(data) 71 | + aes(x='x', y='y', z='z', color='z') 72 | + geom_line_3d(size=2) 73 | + theme_minimal() 74 | ) 75 | ``` 76 | 77 | 78 | ![line](https://raw.githubusercontent.com/krassowski/plotnine3d/main/docs/images/line.png) 79 | 80 | 81 | ### Installation 82 | 83 | Installation from PyPI: 84 | 85 | ``` 86 | pip install plotnine3d 87 | ``` 88 | -------------------------------------------------------------------------------- /data/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Plotly 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. -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | ## Datasets used in examples 2 | 3 | - `mt_bruno_elevation.csv` from https://github.com/plotly/datasets -------------------------------------------------------------------------------- /data/mt_bruno_elevation.csv: -------------------------------------------------------------------------------- 1 | ,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23 2 | 0,27.80985,49.61936,83.08067,116.6632,130.414,150.7206,220.1871,156.1536,148.6416,203.7845,206.0386,107.1618,68.36975,45.3359,49.96142,21.89279,17.02552,11.74317,14.75226,13.6671,5.677561,3.31234,1.156517,-0.147662 3 | 1,27.71966,48.55022,65.21374,95.27666,116.9964,133.9056,152.3412,151.934,160.1139,179.5327,147.6184,170.3943,121.8194,52.58537,33.08871,38.40972,44.24843,69.5786,4.019351,3.050024,3.039719,2.996142,2.967954,1.999594 4 | 2,30.4267,33.47752,44.80953,62.47495,77.43523,104.2153,102.7393,137.0004,186.0706,219.3173,181.7615,120.9154,143.1835,82.40501,48.47132,74.71461,60.0909,7.073525,6.089851,6.53745,6.666096,7.306965,5.73684,3.625628 5 | 3,16.66549,30.1086,39.96952,44.12225,59.57512,77.56929,106.8925,166.5539,175.2381,185.2815,154.5056,83.0433,62.61732,62.33167,60.55916,55.92124,15.17284,8.248324,36.68087,61.93413,20.26867,68.58819,46.49812,0.2360095 6 | 4,8.815617,18.3516,8.658275,27.5859,48.62691,60.18013,91.3286,145.7109,116.0653,106.2662,68.69447,53.10596,37.92797,47.95942,47.42691,69.20731,44.95468,29.17197,17.91674,16.25515,14.65559,17.26048,31.22245,46.71704 7 | 5,6.628881,10.41339,24.81939,26.08952,30.1605,52.30802,64.71007,76.30823,84.63686,99.4324,62.52132,46.81647,55.76606,82.4099,140.2647,81.26501,56.45756,30.42164,17.28782,8.302431,2.981626,2.698536,5.886086,5.268358 8 | 6,21.83975,6.63927,18.97085,32.89204,43.15014,62.86014,104.6657,130.2294,114.8494,106.9873,61.89647,55.55682,86.80986,89.27802,122.4221,123.9698,109.0952,98.41956,77.61374,32.49031,14.67344,7.370775,0.03711011,0.6423392 9 | 7,53.34303,26.79797,6.63927,10.88787,17.2044,56.18116,79.70141,90.8453,98.27675,80.87243,74.7931,75.54661,73.4373,74.11694,68.1749,46.24076,39.93857,31.21653,36.88335,40.02525,117.4297,12.70328,1.729771,0.0 10 | 8,25.66785,63.05717,22.1414,17.074,41.74483,60.27227,81.42432,114.444,102.3234,101.7878,111.031,119.2309,114.0777,110.5296,59.19355,42.47175,14.63598,6.944074,6.944075,27.74936,0.0,0.0,0.09449376,0.07732264 11 | 9,12.827,69.20554,46.76293,13.96517,33.88744,61.82613,84.74799,121.122,145.2741,153.1797,204.786,227.9242,236.3038,228.3655,79.34425,25.93483,6.944074,6.944074,6.944075,7.553681,0.0,0.0,0.0,0.0 12 | 10,0.0,68.66396,59.0435,33.35762,47.45282,57.8355,78.91689,107.8275,168.0053,130.9597,212.5541,165.8122,210.2429,181.1713,189.7617,137.3378,84.65395,8.677168,6.956576,8.468093,0.0,0.0,0.0,0.0 13 | 11,0.0,95.17499,80.03818,59.89862,39.58476,50.28058,63.81641,80.61302,66.37824,198.7651,244.3467,294.2474,264.3517,176.4082,60.21857,77.41475,53.16981,56.16393,6.949235,7.531059,3.780177,0.0,0.0,0.0 14 | 12,0.0,134.9879,130.3696,96.86325,75.70494,58.86466,57.20374,55.18837,78.128,108.5582,154.3774,319.1686,372.8826,275.4655,130.2632,54.93822,25.49719,8.047439,8.084393,5.115252,5.678269,0.0,0.0,0.0 15 | 13,0.0,48.08919,142.5558,140.3777,154.7261,87.9361,58.11092,52.83869,67.14822,83.66798,118.9242,150.0681,272.9709,341.1366,238.664,190.2,116.8943,91.48672,14.0157,42.29277,5.115252,0.0,0.0,0.0 16 | 14,0.0,54.1941,146.3839,99.48143,96.19411,102.9473,76.14089,57.7844,47.0402,64.36799,84.23767,162.7181,121.3275,213.1646,328.482,285.4489,283.8319,212.815,164.549,92.29631,7.244015,1.167,0.0,0.0 17 | 15,0.0,6.919659,195.1709,132.5253,135.2341,89.85069,89.45549,60.29967,50.33806,39.17583,59.06854,74.52159,84.93402,187.1219,123.9673,103.7027,128.986,165.1283,249.7054,95.39966,10.00284,2.39255,0.0,0.0 18 | 16,0.0,21.73871,123.1339,176.7414,158.2698,137.235,105.3089,86.63255,53.11591,29.03865,30.40539,39.04902,49.23405,63.27853,111.4215,101.1956,40.00962,59.84565,74.51253,17.06316,2.435141,2.287471,-0.0003636982,0.0 19 | 17,0.0,0.0,62.04672,136.3122,201.7952,168.1343,95.2046,58.90624,46.94091,49.27053,37.10416,17.97011,30.93697,33.39257,44.03077,55.64542,78.22423,14.42782,9.954997,7.768213,13.0254,21.73166,2.156372,0.5317867 20 | 18,0.0,0.0,79.62993,139.6978,173.167,192.8718,196.3499,144.6611,106.5424,57.16653,41.16107,32.12764,13.8566,10.91772,12.07177,22.38254,24.72105,6.803666,4.200841,16.46857,15.70744,33.96221,7.575688,-0.04880907 21 | 19,0.0,0.0,33.2664,57.53643,167.2241,196.4833,194.7966,182.1884,119.6961,73.02113,48.36549,33.74652,26.2379,16.3578,6.811293,6.63927,6.639271,8.468093,6.194273,3.591233,3.81486,8.600739,5.21889,0.0 22 | 20,0.0,0.0,29.77937,54.97282,144.7995,207.4904,165.3432,171.4047,174.9216,100.2733,61.46441,50.19171,26.08209,17.18218,8.468093,6.63927,6.334467,6.334467,5.666687,4.272203,0.0,0.0,0.0,0.0 23 | 21,0.0,0.0,31.409,132.7418,185.5796,121.8299,185.3841,160.6566,116.1478,118.1078,141.7946,65.56351,48.84066,23.13864,18.12932,10.28531,6.029663,6.044627,5.694764,3.739085,3.896037,0.0,0.0,0.0 24 | 22,0.0,0.0,19.58994,42.30355,96.26777,187.1207,179.6626,221.3898,154.2617,142.1604,148.5737,67.17937,40.69044,39.74512,26.10166,14.48469,8.65873,3.896037,3.571392,3.896037,3.896037,3.896037,1.077756,0.0 25 | 23,0.001229679,3.008948,5.909858,33.50574,104.3341,152.2165,198.1988,191.841,228.7349,168.1041,144.2759,110.7436,57.65214,42.63504,27.91891,15.41052,8.056102,3.90283,3.879774,3.936718,3.968634,0.1236256,3.985531,-0.1835741 26 | 24,0.0,5.626141,7.676256,63.16226,45.99762,79.56688,227.311,203.9287,172.5618,177.1462,140.4554,123.9905,110.346,65.12319,34.31887,24.5278,9.561069,3.334991,5.590495,5.487353,5.909499,5.868994,5.833817,3.568177 27 | -------------------------------------------------------------------------------- /docs/images/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krassowski/plotnine3d/d7500269722496509356633f2cb0d358d552dc9c/docs/images/line.png -------------------------------------------------------------------------------- /docs/images/scatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krassowski/plotnine3d/d7500269722496509356633f2cb0d358d552dc9c/docs/images/scatter.png -------------------------------------------------------------------------------- /docs/images/surface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krassowski/plotnine3d/d7500269722496509356633f2cb0d358d552dc9c/docs/images/surface.png -------------------------------------------------------------------------------- /docs/images/voxels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krassowski/plotnine3d/d7500269722496509356633f2cb0d358d552dc9c/docs/images/voxels.png -------------------------------------------------------------------------------- /plotnine3d/__init__.py: -------------------------------------------------------------------------------- 1 | from .geoms import geom_point_3d, geom_polygon_3d, geom_line_3d, geom_voxel_3d, geom_text_3d, geom_label_3d 2 | from .plot import ggplot_3d 3 | from .labels import zlab 4 | 5 | 6 | __version__ = '0.0.6' 7 | 8 | 9 | __all__ = [ 10 | 'geom_line_3d', 11 | 'geom_polygon_3d', 12 | 'geom_point_3d', 13 | 'geom_voxel_3d', 14 | 'geom_label_3d', 15 | 'geom_text_3d', 16 | 'zlab', 17 | 'ggplot_3d' 18 | ] 19 | -------------------------------------------------------------------------------- /plotnine3d/geoms.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import packaging 3 | from warnings import warn 4 | from plotnine import geom_polygon, geom_point, geom_path, geom_text, geom_label 5 | from plotnine.utils import to_rgba, SIZE_FACTOR 6 | from plotnine.geoms.geom_path import _get_joinstyle 7 | import numpy as np 8 | 9 | 10 | class geom_line_3d(geom_path): 11 | REQUIRED_AES = { 12 | 'x', 'y', 'z' 13 | } 14 | 15 | @staticmethod 16 | def draw_group(data, panel_params, coord, ax, **params): 17 | data = coord.transform(data, panel_params, munch=True) 18 | data['size'] *= SIZE_FACTOR 19 | constant = params.pop('constant', data['group'].nunique() == 1) 20 | join_style = _get_joinstyle(data, params) 21 | 22 | if constant: 23 | color = to_rgba(data['color'].iloc[0], data['alpha'].iloc[0]) 24 | 25 | ax.plot( 26 | xs=data['x'].values, 27 | ys=data['y'].values, 28 | zs=data['z'].values, 29 | color=color, 30 | linewidth=data['size'].iloc[0], 31 | linestyle=data['linetype'].iloc[0], 32 | zorder=params['zorder'], 33 | rasterized=params['raster'], 34 | **join_style 35 | ) 36 | else: 37 | # TODO: this is rather inefficient, geom_path has a better 38 | # segmentation implementation (rather than point-by-point) 39 | color = to_rgba(data['color'], data['alpha']) 40 | 41 | x = data['x'].values 42 | y = data['y'].values 43 | z = data['z'].values 44 | 45 | for i in range(len(data) - 1): 46 | ax.plot( 47 | x[i:i + 2], 48 | y[i:i + 2], 49 | z[i:i + 2], 50 | color=color[i], 51 | linewidth=data['size'].iloc[i], 52 | linestyle=data['linetype'].iloc[i], 53 | zorder=params['zorder'], 54 | rasterized=params['raster'], 55 | **join_style 56 | ) 57 | 58 | 59 | class geom_polygon_3d(geom_polygon): 60 | REQUIRED_AES = {'x', 'y', 'z'} 61 | DEFAULT_PARAMS = { 62 | 'lightsource': None, 63 | 'antialiased': True, 64 | 'shade': True, 65 | **geom_polygon.DEFAULT_PARAMS 66 | } 67 | 68 | @staticmethod 69 | def draw_group(data, panel_params, coord, ax, **params): 70 | data = coord.transform(data, panel_params, munch=True) 71 | data['size'] *= SIZE_FACTOR 72 | 73 | grouper = data.groupby('group', sort=False) 74 | for i, (group, df) in enumerate(grouper): 75 | fill = to_rgba(df['fill'], df['alpha']) 76 | 77 | ax.plot_trisurf( 78 | df['x'].values, 79 | df['y'].values, 80 | df['z'].values, 81 | facecolors=fill if any(fill) else 'none', 82 | edgecolors=df['color'] if any(df['color']) else 'none', 83 | linestyles=df['linetype'], 84 | linewidths=df['size'], 85 | zorder=params['zorder'], 86 | rasterized=params['raster'], 87 | antialiased=params['antialiased'], 88 | lightsource=params['lightsource'], 89 | shade=params['shade'], 90 | ) 91 | 92 | 93 | class geom_voxel_3d(geom_point): 94 | REQUIRED_AES = {'x', 'y', 'z'} 95 | DEFAULT_AES = { 96 | **geom_point.DEFAULT_AES, 97 | 'fill': 'blue', 98 | 'shape': 's' 99 | } 100 | 101 | @staticmethod 102 | def draw_group(data, panel_params, coord, ax, **params): 103 | data = coord.transform(data, panel_params, munch=True) 104 | data['size'] *= SIZE_FACTOR 105 | grouper = data.groupby('group', sort=False) 106 | for i, (group, df) in enumerate(grouper): 107 | fill = to_rgba(df['fill'], df['alpha']) 108 | 109 | voxel_grid = np.zeros(( 110 | df['x'].max() + 1, 111 | df['y'].max() + 1, 112 | df['z'].max() + 1 113 | )) 114 | voxel_grid[df['x'], df['y'], df['z']] = 1 115 | fills = np.empty(voxel_grid.shape, dtype=object) 116 | fills[df['x'], df['y'], df['z']] = fill 117 | 118 | colors = np.empty(voxel_grid.shape, dtype=object) 119 | colors[df['x'], df['y'], df['z']] = df['color'] 120 | 121 | ax.voxels( 122 | filled=voxel_grid, 123 | facecolors=fills, 124 | edgecolors=colors, 125 | zorder=params['zorder'], 126 | ) 127 | 128 | 129 | class geom_point_3d(geom_point): 130 | REQUIRED_AES = {'x', 'y', 'z'} 131 | DEFAULT_PARAMS = { 132 | 'depthshade': True, 133 | **geom_point.DEFAULT_PARAMS 134 | } 135 | 136 | @staticmethod 137 | def draw_unit(data, panel_params, coord, ax, **params): 138 | size = ((data['size'] + data['stroke'])**2) * np.pi 139 | stroke = data['stroke'] * SIZE_FACTOR 140 | color = to_rgba(data['color'], data['alpha']) 141 | 142 | if all(c is None for c in data['fill']): 143 | fill = color 144 | else: 145 | fill = to_rgba(data['fill'], data['alpha']) 146 | 147 | # https://github.com/matplotlib/matplotlib/issues/23433 148 | stroke_set = set(stroke) 149 | 150 | if len(stroke_set) > 1: 151 | version = importlib.metadata.version('matplotlib') 152 | required = packaging.version.parse('3.6.0') 153 | detected = packaging.version.parse(version) 154 | supports_array_stroke = detected >= required 155 | if not supports_array_stroke: 156 | warn( 157 | 'Variable stroke values in `geom_point_3d` require' 158 | f' matplotlib v3.6.0+ (detected matplotlib: {mpl_version}).' 159 | ) 160 | stroke = next(iter(stroke_set)) 161 | else: 162 | stroke = next(iter(stroke_set)) 163 | 164 | ax.scatter3D( 165 | data['x'], 166 | data['y'], 167 | data['z'], 168 | s=size, 169 | facecolor=fill, 170 | edgecolor=color, 171 | marker=data.loc[0, 'shape'], 172 | linewidths=stroke, 173 | depthshade=params['depthshade'] 174 | ) 175 | 176 | @staticmethod 177 | def draw_group(data, panel_params, coord, ax, **params): 178 | data = coord.transform(data, panel_params) 179 | units = 'shape' 180 | for _, udata in data.groupby(units, dropna=False): 181 | udata.reset_index(inplace=True, drop=True) 182 | geom_point_3d.draw_unit(udata, panel_params, coord, ax, **params) 183 | 184 | 185 | class geom_text_3d(geom_text): 186 | """ 187 | This class is reusing the code of `plotnine.geom_text` which is licensed under: 188 | 189 | The MIT License (MIT) 190 | 191 | Copyright (c) 2022 Hassan Kibirige 192 | 193 | Permission is hereby granted, free of charge, to any person obtaining a copy 194 | of this software and associated documentation files (the "Software"), to deal 195 | in the Software without restriction, including without limitation the rights 196 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 197 | copies of the Software, and to permit persons to whom the Software is 198 | furnished to do so, subject to the following conditions: 199 | 200 | The above copyright notice and this permission notice shall be included in all 201 | copies or substantial portions of the Software. 202 | 203 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 204 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 205 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 206 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 207 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 208 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 209 | SOFTWARE. 210 | """ 211 | 212 | REQUIRED_AES = { 213 | 'x', 'y', 'z', 214 | 'label' 215 | } 216 | DEFAULT_AES = { 217 | 'zdir': None, 218 | **geom_text.DEFAULT_AES 219 | } 220 | 221 | @staticmethod 222 | def draw_group(data, panel_params, coord, ax, **params): 223 | data = coord.transform(data, panel_params) 224 | 225 | # Bind color and alpha 226 | color = to_rgba(data['color'], data['alpha']) 227 | 228 | # Create a dataframe for the plotting data required 229 | # adding custom "params" mappings 230 | 231 | df = data[['x', 'y', 'z', 'size']].copy() 232 | df['zdir'] = data['zdir'] 233 | df['s'] = data['label'] 234 | df['rotation'] = data['angle'] 235 | df['linespacing'] = data['lineheight'] 236 | df['color'] = color 237 | df['ha'] = data['ha'] 238 | df['va'] = data['va'] 239 | df['family'] = params['family'] 240 | df['fontweight'] = params['fontweight'] 241 | df['fontstyle'] = params['fontstyle'] 242 | df['zorder'] = params['zorder'] 243 | df['rasterized'] = params['raster'] 244 | df['clip_on'] = True 245 | 246 | # 'boxstyle' indicates geom_label so we need an MPL bbox 247 | draw_label = 'boxstyle' in params 248 | if draw_label: 249 | fill = to_rgba(data.pop('fill'), data['alpha']) 250 | if isinstance(fill, tuple): 251 | fill = [list(fill)] * len(data['x']) 252 | df['facecolor'] = fill 253 | 254 | if params['boxstyle'] in ('round', 'round4'): 255 | boxstyle = '{},pad={},rounding_size={}'.format( 256 | params['boxstyle'], 257 | params['label_padding'], 258 | params['label_r']) 259 | elif params['boxstyle'] in ('roundtooth', 'sawtooth'): 260 | boxstyle = '{},pad={},tooth_size={}'.format( 261 | params['boxstyle'], 262 | params['label_padding'], 263 | params['tooth_size']) 264 | else: 265 | boxstyle = '{},pad={}'.format( 266 | params['boxstyle'], 267 | params['label_padding']) 268 | bbox = {'linewidth': params['label_size'], 269 | 'boxstyle': boxstyle} 270 | else: 271 | bbox = {} 272 | 273 | texts = [] 274 | 275 | # For labels add a bbox 276 | for i in range(len(data)): 277 | kw = df.iloc[i].to_dict() 278 | if draw_label: 279 | kw['bbox'] = bbox 280 | kw['bbox']['edgecolor'] = params['boxcolor'] or kw['color'] 281 | kw['bbox']['facecolor'] = kw.pop('facecolor') 282 | 283 | text_elem = ax.text(**kw) 284 | texts.append(text_elem) 285 | if params['path_effects']: 286 | text_elem.set_path_effects(params['path_effects']) 287 | 288 | 289 | class geom_label_3d(geom_text_3d): 290 | DEFAULT_AES = { 291 | **geom_text_3d.DEFAULT_AES, 292 | **geom_label.DEFAULT_AES 293 | } 294 | DEFAULT_PARAMS = { 295 | **geom_text_3d.DEFAULT_PARAMS, 296 | **geom_label.DEFAULT_PARAMS 297 | } 298 | 299 | @staticmethod 300 | def draw_legend(data, da, lyr): 301 | return geom_label.draw_legend(data, da, lyr) 302 | -------------------------------------------------------------------------------- /plotnine3d/labels.py: -------------------------------------------------------------------------------- 1 | from plotnine.labels import labs 2 | 3 | 4 | class zlab(labs): 5 | """ 6 | Create z-axis label 7 | Parameters 8 | ---------- 9 | zlab : str 10 | z-axis label 11 | """ 12 | 13 | def __init__(self, zlab): 14 | if zlab is None: 15 | raise PlotnineError( 16 | "Arguments to zlab cannot be None") 17 | self.labels = {'z': zlab} 18 | -------------------------------------------------------------------------------- /plotnine3d/plot.py: -------------------------------------------------------------------------------- 1 | from plotnine import ggplot 2 | import matplotlib.pyplot as plt 3 | 4 | 5 | 6 | class ggplot_3d(ggplot): 7 | 8 | def _create_figure(self): 9 | figure = plt.figure() 10 | axs = [plt.axes(projection='3d')] 11 | 12 | figure._themeable = {} 13 | self.figure = figure 14 | self.axs = axs 15 | return figure, axs 16 | 17 | def _draw_labels(self): 18 | ax = self.axs[0] 19 | ax.set_xlabel(self.layout.xlabel(self.labels)) 20 | ax.set_ylabel(self.layout.ylabel(self.labels)) 21 | ax.set_zlabel(self.labels.get('z', 'z')) 22 | 23 | def __repr__(self): 24 | super().__repr__() 25 | return '' 26 | -------------------------------------------------------------------------------- /plotnine3d/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krassowski/plotnine3d/d7500269722496509356633f2cb0d358d552dc9c/plotnine3d/py.typed -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = plotnine3d 3 | description = 3D geoms for plotnine (grammar of graphics in Python) 4 | long_description = file: ./README.md 5 | long_description_content_type = text/markdown 6 | project_urls = 7 | Bug Tracker = https://github.com/krassowski/plotnine3d/issues 8 | Source Code = https://github.com/krassowski/plotnine3d 9 | license = MIT license 10 | license_file = LICENSE 11 | author = Michał Krassowski 12 | version = attr: plotnine3d.__version__ 13 | classifiers = 14 | Intended Audience :: Science/Research 15 | Intended Audience :: Education 16 | License :: OSI Approved :: MIT License 17 | Operating System :: Microsoft :: Windows 18 | Operating System :: Unix 19 | Operating System :: MacOS 20 | Programming Language :: Python :: 3 :: Only 21 | Programming Language :: Python :: 3.8 22 | Programming Language :: Python :: 3.9 23 | Programming Language :: Python :: 3.10 24 | Framework :: Matplotlib 25 | Topic :: Scientific/Engineering :: Visualization 26 | 27 | [options] 28 | packages = find: 29 | python_requires = >=3.8 30 | 31 | install_requires = 32 | matplotlib 33 | packaging 34 | plotnine >=0.9.0 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | 4 | setuptools.setup( 5 | name='plotnine3d', 6 | package_data={ 7 | 'plotnine3d': ['py.typed'] 8 | } 9 | ) 10 | --------------------------------------------------------------------------------