├── .editorconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CHANGELOG.md ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── assets └── example.gif ├── keplergl_cli ├── __init__.py ├── cli.py ├── keplergl_cli.py └── keplergl_config.json ├── requirements.txt ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── test_kepler_quickvis.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.json] 14 | indent_size = 2 15 | 16 | [*.bat] 17 | indent_style = tab 18 | end_of_line = crlf 19 | 20 | [LICENSE] 21 | insert_final_newline = false 22 | 23 | [Makefile] 24 | indent_style = tab 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * Kepler Quickvis version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 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 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # IDE settings 107 | .vscode/ 108 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | python: 5 | - 3.8 6 | - 3.7 7 | - 3.6 8 | - 3.5 9 | 10 | # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 11 | install: pip install -U tox-travis 12 | 13 | # Command to run tests, e.g. python setup.py test 14 | script: tox 15 | 16 | 17 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Kyle Barron 9 | 10 | Contributors 11 | ------------ 12 | 13 | None yet. Why not be the first? 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.3 (2022-06-27) 4 | 5 | - Revert usage of `__geo_interface__` 6 | 7 | ## 0.3.2 (2022-06-27) 8 | 9 | - Use `__geo_interface__` when possible 10 | - Respect `open_browser=False` 11 | - Fix centering map 12 | 13 | ## 0.3.1 (2020-02-26) 14 | 15 | - Fix for stdin 16 | 17 | ## 0.3.0 (2020-02-26) 18 | 19 | - Support GeoJSON on stdin 20 | - Rename `keplergl_quickvis` to `keplergl_cli` 21 | - Rename CLI entry point to `kepler` 22 | - CLI option for which layers from file to display 23 | 24 | ## 0.2.0 (2019-12-09) 25 | 26 | - Automatically attempt to reproject to EPSG 4326 27 | 28 | ## 0.1.0 (2019-12-05) 29 | 30 | - First release on PyPI. 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every little bit 8 | helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/kylebarron/kepler_quickvis/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 30 | wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | Kepler Quickvis could always use more documentation, whether as part of the 42 | official Kepler Quickvis docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/kylebarron/kepler_quickvis/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `kepler_quickvis` for local development. 61 | 62 | 1. Fork the `kepler_quickvis` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/kepler_quickvis.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv kepler_quickvis 70 | $ cd kepler_quickvis/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the 80 | tests, including testing other Python versions with tox:: 81 | 82 | $ flake8 kepler_quickvis tests 83 | $ python setup.py test or pytest 84 | $ tox 85 | 86 | To get flake8 and tox, just pip install them into your virtualenv. 87 | 88 | 6. Commit your changes and push your branch to GitHub:: 89 | 90 | $ git add . 91 | $ git commit -m "Your detailed description of your changes." 92 | $ git push origin name-of-your-bugfix-or-feature 93 | 94 | 7. Submit a pull request through the GitHub website. 95 | 96 | Pull Request Guidelines 97 | ----------------------- 98 | 99 | Before you submit a pull request, check that it meets these guidelines: 100 | 101 | 1. The pull request should include tests. 102 | 2. If the pull request adds functionality, the docs should be updated. Put 103 | your new functionality into a function with a docstring, and add the 104 | feature to the list in README.rst. 105 | 3. The pull request should work for Python 3.5, 3.6, 3.7 and 3.8, and for PyPy. Check 106 | https://travis-ci.org/kylebarron/kepler_quickvis/pull_requests 107 | and make sure that the tests pass for all supported Python versions. 108 | 109 | Tips 110 | ---- 111 | 112 | To run a subset of tests:: 113 | 114 | 115 | $ python -m unittest tests.test_kepler_quickvis 116 | 117 | Deploying 118 | --------- 119 | 120 | A reminder for the maintainers on how to deploy. 121 | Make sure all your changes are committed (including an entry in HISTORY.rst). 122 | Then run:: 123 | 124 | $ bump2version patch # possible: major / minor / patch 125 | $ git push 126 | $ git push --tags 127 | 128 | Travis will then deploy to PyPI if tests pass. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019, Kyle Barron 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 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | include README.md 3 | include LICENSE 4 | include requirements.txt 5 | include requirements_dev.txt 6 | include keplergl_cli/keplergl_config.json 7 | 8 | recursive-include tests * 9 | recursive-exclude * __pycache__ 10 | recursive-exclude * *.py[co] 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | from urllib.request import pathname2url 8 | 9 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 10 | endef 11 | export BROWSER_PYSCRIPT 12 | 13 | define PRINT_HELP_PYSCRIPT 14 | import re, sys 15 | 16 | for line in sys.stdin: 17 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 18 | if match: 19 | target, help = match.groups() 20 | print("%-20s %s" % (target, help)) 21 | endef 22 | export PRINT_HELP_PYSCRIPT 23 | 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 30 | 31 | clean-build: ## remove build artifacts 32 | rm -fr build/ 33 | rm -fr dist/ 34 | rm -fr .eggs/ 35 | find . -name '*.egg-info' -exec rm -fr {} + 36 | find . -name '*.egg' -exec rm -f {} + 37 | 38 | clean-pyc: ## remove Python file artifacts 39 | find . -name '*.pyc' -exec rm -f {} + 40 | find . -name '*.pyo' -exec rm -f {} + 41 | find . -name '*~' -exec rm -f {} + 42 | find . -name '__pycache__' -exec rm -fr {} + 43 | 44 | clean-test: ## remove test and coverage artifacts 45 | rm -fr .tox/ 46 | rm -f .coverage 47 | rm -fr htmlcov/ 48 | rm -fr .pytest_cache 49 | 50 | lint: ## check style with flake8 51 | flake8 kepler_quickvis tests 52 | 53 | test: ## run tests quickly with the default Python 54 | python setup.py test 55 | 56 | test-all: ## run tests on every Python version with tox 57 | tox 58 | 59 | coverage: ## check code coverage quickly with the default Python 60 | coverage run --source kepler_quickvis setup.py test 61 | coverage report -m 62 | coverage html 63 | $(BROWSER) htmlcov/index.html 64 | 65 | docs: ## generate Sphinx HTML documentation, including API docs 66 | rm -f docs/kepler_quickvis.rst 67 | rm -f docs/modules.rst 68 | sphinx-apidoc -o docs/ kepler_quickvis 69 | $(MAKE) -C docs clean 70 | $(MAKE) -C docs html 71 | $(BROWSER) docs/_build/html/index.html 72 | 73 | servedocs: docs ## compile the docs watching for changes 74 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 75 | 76 | release: dist ## package and upload a release 77 | twine upload dist/* 78 | 79 | dist: clean ## builds source and wheel package 80 | python setup.py sdist 81 | python setup.py bdist_wheel 82 | ls -l dist 83 | 84 | install: clean ## install the package to the active Python's site-packages 85 | python setup.py install 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # keplergl_cli 2 | 3 | A CLI and Python API for quickly viewing geospatial data in Kepler.gl. 4 | 5 | ## Overview 6 | 7 | Uber's open-source [kepler.gl](https://kepler.gl/) is a great browser-based 8 | platform for interactively visualizing geospatial data. The `keplergl` Python package's [included 9 | documentation](https://github.com/keplergl/kepler.gl/blob/master/docs/keplergl-jupyter/user-guide.md) 10 | is almost entirely directed at use within Jupyter, and it took a little bit of 11 | work to figure out how to use it from a non-Jupyter Python environment. 12 | 13 | This package is a simple wrapper to quickly get your data into kepler.gl. From 14 | the command line, it's as simple as: 15 | 16 | ``` 17 | export MAPBOX_API_KEY=... 18 | keplergl data1.geojson data2.shp data3.gdb 19 | cat data.geojson | keplergl 20 | ``` 21 | 22 | from Python: 23 | 24 | ```py 25 | from keplergl_cli import Visualize 26 | Visualize(data) 27 | ``` 28 | 29 | ![Example gif](https://raw.githubusercontent.com/kylebarron/keplergl_cli/master/assets/example.gif) 30 | 31 | ## Features 32 | 33 | - One-line data visualization 34 | - Automatically converts Shapely objects to GeoJSON 35 | - Supports piped GeoJSON input 36 | - No configuration needed 37 | 38 | ## Install 39 | 40 | **Mapbox API key**: in order to display Mapbox-hosted maps, you need to provide 41 | a Mapbox API key. Go to [Mapbox.com](https://account.mapbox.com/access-tokens) 42 | to get an API key. 43 | 44 | **Package install**: 45 | 46 | ``` 47 | pip install keplergl_cli 48 | ``` 49 | 50 | This package has dependencies on `geojson`, `shapely`, and `geopandas`. If you 51 | get errors when installing this package through pip, it may be easier to first 52 | install dependencies through Conda, then install this package. I.e.: 53 | 54 | ``` 55 | conda install geojson shapely geopandas -c conda-forge 56 | pip install keplergl_cli 57 | ``` 58 | 59 | ## Usage 60 | 61 | ### CLI 62 | 63 | The CLI is installed under the name `kepler`: 64 | 65 | ``` 66 | export MAPBOX_API_KEY=... 67 | kepler --style=outdoors data.geojson 68 | kepler --style=dark data1.geojson shapefile.shp geodatabase.gdb -l layer1 -l layer2 69 | cat data.geojson | kepler 70 | ``` 71 | 72 | You can add `export MAPBOX_API_KEY` to your `.bashrc` or `.zshrc` to not have to 73 | run that step each time. 74 | 75 | You can supply filename paths to data in any [vector format readable by 76 | GeoPandas/GDAL](https://gdal.org/drivers/vector/index.html). Alternatively you 77 | can supply GeoJSON or newline-delimited GeoJSON on stdin. 78 | 79 | Supply `--help` to see the CLI's help menu: 80 | 81 | ``` 82 | > kepler --help 83 | 84 | Usage: kepler [OPTIONS] FILES... 85 | 86 | Interactively view geospatial data using kepler.gl 87 | 88 | Options: 89 | -l, --layer TEXT Layer names. If not provided, will display all layers 90 | --api_key TEXT Mapbox API Key. Must be provided on the command line or 91 | exist in the MAPBOX_API_KEY environment variable. 92 | --style TEXT Mapbox style. Accepted values are: streets, outdoors, 93 | light, dark, satellite, satellite-streets, or a custom 94 | style URL. [default: streets] 95 | --help Show this message and exit. 96 | ``` 97 | 98 | ### Python API 99 | 100 | Simplest usage: 101 | 102 | ```py 103 | import geopandas as gpd 104 | from keplergl_cli import Visualize 105 | 106 | # Create your geospatial objects 107 | gdf = gpd.GeoDataFrame(...) 108 | 109 | # Visualize one or multiple objects at a time 110 | Visualize(gdf, api_key=MAPBOX_API_KEY) 111 | Visualize([gdf, shapely_object, geojson_string], api_key=MAPBOX_API_KEY) 112 | ``` 113 | 114 | More detail over the objects in your map: 115 | 116 | ```py 117 | from keplergl_cli import Visualize 118 | vis = Visualize(api_key=MAPBOX_API_KEY) 119 | vis.add_data(data=data, names='name of layer') 120 | vis.add_data(data=data2, names='name of layer') 121 | html_path = vis.render(open_browser=True, read_only=False) 122 | ``` 123 | 124 | **Visualize** 125 | 126 | ```py 127 | Visualize(data=None, names=None, read_only=False, api_key=None, style=None) 128 | ``` 129 | 130 | - `data` (either `None`, a single data object, or a list of data objects): 131 | 132 | A data object may be a GeoDataFrame from the 133 | [GeoPandas](http://geopandas.org/) library, any geometry from the 134 | [Shapely](https://shapely.readthedocs.io/en/stable/manual.html) library, any 135 | object from the [GeoJSON](https://github.com/jazzband/geojson) library, or 136 | any GeoJSON `str` or `dict`. You can also provide a CSV file as a 137 | string or a Pandas DataFrame if the DataFrame has `Latitude` and `Longitude` 138 | columns. Full documentation on the accepted data formats is 139 | [here](https://github.com/keplergl/kepler.gl/blob/master/docs/keplergl-jupyter/user-guide.md#3-data-format). 140 | 141 | You can provide either a single data object, or an iterable containing 142 | multiple allowed data objects. 143 | 144 | If data is not `None`, then Visualize(data) will perform all steps, including 145 | rendering the data to an HTML file and opening it in a new browser tab. 146 | 147 | - `names` (either `None`, a string, or a list of strings): 148 | 149 | This defines the names shown for each layer in Kepler.gl. If `None`, the 150 | layers will be named `data_0`, `data_1`, and so on. Otherwise, if `data` is 151 | a single object, `names` should be a string, and if `data` is an iterable, 152 | then `names` should be an iterable of strings. 153 | 154 | - `read_only` (`boolean`): If `True`, hides side panel to disable map customization 155 | - `api_key` (`string`): Mapbox API key. Go to [Mapbox.com](https://account.mapbox.com/access-tokens) 156 | to get an API key. If not provided, the `MAPBOX_API_KEY` environment 157 | variable must be set, or the `style_url` must point to a `style.json` file 158 | that does not use Mapbox map tiles. 159 | - `style` (`string`): The basemap style to use. Standard Mapbox options are: 160 | 161 | - `streets` 162 | - `outdoors` 163 | - `light` 164 | - `dark` 165 | - `satellite` 166 | - `satellite-streets` 167 | 168 | The default is `streets`. Alternatively, you can supply a path to a custom 169 | style. A custom style created from Mapbox Studio should have a url that 170 | starts with `mapbox://`. Otherwise, a custom style using third-party map 171 | tiles should be a URL to a JSON file that conforms to the [Mapbox Style 172 | Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/). 173 | 174 | **Visualize.add_data()** 175 | 176 | ```py 177 | Visualize.add_data(data, names=None): 178 | ``` 179 | 180 | - `data` (either a single data object, or a list of data objects): 181 | 182 | A data object may be a GeoDataFrame from the 183 | [GeoPandas](http://geopandas.org/) library, any geometry from the 184 | [Shapely](https://shapely.readthedocs.io/en/stable/manual.html) library, any 185 | object from the [GeoJSON](https://github.com/jazzband/geojson) library, or 186 | any GeoJSON string or dictionary. You can also provide a CSV file as a 187 | string or a Pandas DataFrame if the DataFrame has `Latitude` and `Longitude` 188 | columns. Full documentation on the accepted data formats is 189 | [here](https://github.com/keplergl/kepler.gl/blob/master/docs/keplergl-jupyter/user-guide.md#3-data-format). 190 | 191 | You can provide either a single data object, or an iterable containing 192 | multiple allowed data objects. 193 | 194 | - `names` (either `None`, a string, or a list of strings): 195 | 196 | This defines the names shown for each layer in Kepler.gl. If `None`, the 197 | layers will be named `data_0`, `data_1`, and so on. Otherwise, if `data` is 198 | a single object, `names` should be a string, and if `data` is an iterable, 199 | then `names` should be an iterable of strings. 200 | 201 | **Visualize.render()** 202 | 203 | ```py 204 | Visualize.render(open_browser=True, read_only=False) 205 | ``` 206 | 207 | - `read_only` (`boolean`): If `True`, hides side panel to disable map customization 208 | - `open_browser` (`boolean`): If `True`, opens the saved HTML file in the default browser 209 | 210 | ## Troubleshooting 211 | 212 | The most common reasons why a map is not displayed are: 213 | 214 | - Missing Mapbox API Key: in order to display Mapbox-hosted maps, you need get [an API key from Mapbox](https://account.mapbox.com/access-tokens) to pass an API key 215 | - Data projection: Kepler.gl works only with data projected into standard WGS84 (latitude, longitude) coordinates. If you have your data in a projected coordinate system, first reproject your data into WGS84 (EPGS 4326), then try again. The CLI attempts to automatically reproject into EPSG 4326, but the Python library doesn't. 216 | 217 | If your data seems to be "floating" above the map, this is likely because your 218 | input data have Z coordinates, so kepler.gl displays them in 3-dimensional 219 | space. 220 | -------------------------------------------------------------------------------- /assets/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylebarron/keplergl_cli/6af215be3763c63ee1a049b119841b1afaf48a46/assets/example.gif -------------------------------------------------------------------------------- /keplergl_cli/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for keplergl_cli.""" 2 | 3 | __author__ = """Kyle Barron""" 4 | __email__ = 'kylebarron2@gmail.com' 5 | __version__ = '0.3.3' 6 | 7 | from .keplergl_cli import Visualize 8 | -------------------------------------------------------------------------------- /keplergl_cli/cli.py: -------------------------------------------------------------------------------- 1 | """Console script for keplergl_cli.""" 2 | import json 3 | # Some imports are loaded conditionally later to try to make the CLI more 4 | # responsive 5 | import sys 6 | from pathlib import Path 7 | 8 | import click 9 | import fiona 10 | import geojson 11 | import geopandas as gpd 12 | 13 | from .keplergl_cli import Visualize 14 | 15 | 16 | # https://stackoverflow.com/a/45845513 17 | def get_stdin(ctx, param, value): 18 | if not value and not click.get_text_stream('stdin').isatty(): 19 | return (click.get_text_stream('stdin').read().strip(), ) 20 | else: 21 | return value 22 | 23 | 24 | @click.command() 25 | @click.option( 26 | '-l', 27 | '--layer', 28 | type=str, 29 | default=None, 30 | required=False, 31 | multiple=True, 32 | help='Layer names. If not provided, will display all layers') 33 | @click.option( 34 | '--api_key', 35 | type=str, 36 | default=None, 37 | help= 38 | 'Mapbox API Key. Must be provided on the command line or exist in the MAPBOX_API_KEY environment variable.' 39 | ) 40 | @click.option( 41 | '--style', 42 | type=str, 43 | default='streets', 44 | show_default=True, 45 | help= 46 | 'Mapbox style. Accepted values are: streets, outdoors, light, dark, satellite, satellite-streets, or a custom style URL.' 47 | ) 48 | @click.option('--center-map/--no-center-map', type=bool, default=True, show_default=True, help='Whether to center map.') 49 | @click.argument('files', nargs=-1, callback=get_stdin, type=str) 50 | def main(layer, api_key, style, center_map, files): 51 | """Interactively view geospatial data using kepler.gl""" 52 | vis = Visualize(api_key=api_key, style=style) 53 | 54 | # For each file, try to load data with GeoPandas 55 | for item in files: 56 | # If body exists as a path; assume it represents a Path 57 | try: 58 | path = Path(item) 59 | if path.exists(): 60 | layers = fiona.listlayers(path) 61 | 62 | if layer: 63 | layers = [x for x in layers if x in layer] 64 | 65 | click.echo(f'Loading layers from {path}') 66 | for l in layers: 67 | click.echo(l) 68 | vis.add_data(*load_file(path, l)) 69 | # layer = ('NHDFlowline', ) 70 | continue 71 | 72 | except OSError: 73 | # Otherwise, it should be GeoJSON 74 | # First try to parse entire stdin as single GeoJSON 75 | # If this fails, assume it's newline delimited where each feature is 76 | # on a single line 77 | try: 78 | vis.add_data(geojson.loads(item)) 79 | 80 | except json.JSONDecodeError: 81 | lines = item.split('\n') 82 | features = [geojson.loads(l) for l in lines] 83 | vis.add_data(geojson.FeatureCollection(features)) 84 | 85 | vis.render(open_browser=True, read_only=False, center_map=center_map) 86 | return 0 87 | 88 | 89 | def load_file(path, layer=None): 90 | """Load geospatial data at path 91 | 92 | Loads data with GeoPandas; reprojects to 4326 93 | """ 94 | layer_name = layer or path.stem 95 | gdf = gpd.read_file(path, layer=layer) 96 | 97 | # Remove null geometries 98 | gdf = gdf[gdf.geometry.notna()] 99 | 100 | # Try to automatically reproject to epsg 4326 101 | # For some reason, it takes forever to call gdf.crs, so I don't want 102 | # to first check the crs, then reproject. Anyways, reprojecting from 103 | # epsg 4326 to epsg 4326 should be instant 104 | try: 105 | gdf = gdf.to_crs(epsg=4326) 106 | except: 107 | pass 108 | 109 | return gdf, layer_name 110 | 111 | 112 | if __name__ == "__main__": 113 | sys.exit(main()) # pragma: no cover 114 | -------------------------------------------------------------------------------- /keplergl_cli/keplergl_cli.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import tempfile 4 | import webbrowser 5 | 6 | import geojson 7 | import shapely.geometry 8 | from keplergl import KeplerGl 9 | from pkg_resources import resource_filename 10 | from shapely.geometry import mapping 11 | 12 | SHAPELY_GEOJSON_CLASSES = [ 13 | shapely.geometry.LineString, 14 | shapely.geometry.LinearRing, 15 | shapely.geometry.MultiLineString, 16 | shapely.geometry.MultiPoint, 17 | shapely.geometry.MultiPolygon, 18 | shapely.geometry.Point, 19 | shapely.geometry.Polygon, 20 | geojson.Feature, 21 | geojson.FeatureCollection, 22 | geojson.GeoJSON, 23 | geojson.GeoJSONEncoder, 24 | geojson.GeometryCollection, 25 | geojson.LineString, 26 | geojson.MultiLineString, 27 | geojson.MultiPoint, 28 | geojson.MultiPolygon, 29 | geojson.Point, 30 | geojson.Polygon 31 | ] # yapf: disable 32 | 33 | 34 | class Visualize: 35 | """Quickly visualize data in browser over Mapbox tiles with the help of the AMAZING kepler.gl. 36 | """ 37 | def __init__( 38 | self, 39 | data=None, 40 | names=None, 41 | read_only=False, 42 | api_key=None, 43 | style=None): 44 | """Visualize data using kepler.gl 45 | 46 | Args: 47 | data Optional[Union[List[]]]: 48 | either None, a List of data objects, or a single data object. If 49 | data is not None, then Visualize(data) will perform all steps, 50 | including rendering and opening a browser. 51 | """ 52 | super(Visualize, self).__init__() 53 | 54 | if api_key is not None: 55 | self.MAPBOX_API_KEY = api_key 56 | else: 57 | self.MAPBOX_API_KEY = os.getenv('MAPBOX_API_KEY') 58 | msg = 'Warning: api_key not provided and MAPBOX_API_KEY ' 59 | msg += 'environment variable not set.\nMap may not display.' 60 | if self.MAPBOX_API_KEY is None: 61 | print(msg) 62 | 63 | config = self.config(style=style) 64 | self.map = KeplerGl(config=config) 65 | 66 | if data is not None: 67 | self.add_data(data=data, names=names) 68 | self.html_path = self.render(read_only=read_only) 69 | 70 | def config(self, style=None): 71 | """Load kepler.gl config and insert Mapbox API Key""" 72 | 73 | config_file = resource_filename( 74 | 'keplergl_cli', 'keplergl_config.json') 75 | 76 | # First load config file as string, replace {MAPBOX_API_KEY} with the 77 | # actual api key, then parse as JSON 78 | with open(config_file) as f: 79 | text = f.read() 80 | 81 | text = text.replace('{MAPBOX_API_KEY}', self.MAPBOX_API_KEY) 82 | keplergl_config = json.loads(text) 83 | 84 | # If style_url is not None, replace existing value 85 | standard_styles = [ 86 | 'streets', 87 | 'outdoors', 88 | 'light', 89 | 'dark', 90 | 'satellite', 91 | 'satellite-streets', 92 | ] 93 | if style is not None: 94 | style = style.lower() 95 | if style in standard_styles: 96 | # Just change the name of the mapStyle.StyleType key 97 | keplergl_config['config']['config']['mapStyle']['styleType'] = style 98 | else: 99 | # Add a new style with that url 100 | d = { 101 | 'accessToken': self.MAPBOX_API_KEY, 102 | 'custom': True, 103 | 'id': 'custom', 104 | 'label': 'Custom map style', 105 | 'url': style 106 | } 107 | keplergl_config['config']['config']['mapStyle']['mapStyles']['custom'] = d 108 | keplergl_config['config']['config']['mapStyle']['styleType'] = 'custom' 109 | 110 | # Remove map state in the hope that it'll auto-center based on data 111 | # keplergl_config['config']['config'].pop('mapState') 112 | return keplergl_config['config'] 113 | 114 | def add_data(self, data, names=None): 115 | """Add data to kepler map 116 | 117 | Data should be either GeoJSON or GeoDataFrame. Kepler isn't aware of the 118 | geojson or shapely package, so if I supply an object from one of these 119 | libraries, first convert it to a GeoJSON dict. 120 | """ 121 | # Make `data` iterable 122 | if not isinstance(data, list): 123 | data = [data] 124 | 125 | # Make `names` iterable and of the same length as `data` 126 | if isinstance(names, list): 127 | # Already iterable, make sure they're the same length 128 | msg = 'data and names are iterables of different length' 129 | assert len(data) == len(names), msg 130 | else: 131 | # `names` not iterable, make sure it's the same length as `data` 132 | name_stub = 'data' if names is None else names 133 | names = [f'{name_stub}_{x}' for x in range(len(data))] 134 | 135 | for datum, name in zip(data, names): 136 | # TODO: revisit using __geo_interface__ 137 | # This was reported to have issues when piping in data 138 | # if getattr(datum, '__geo_interface__'): 139 | # datum = datum.__geo_interface__ 140 | if any(isinstance(datum, c) for c in SHAPELY_GEOJSON_CLASSES): 141 | datum = dict(mapping(datum)) 142 | 143 | self.map.add_data(data=datum, name=name) 144 | 145 | def render(self, open_browser=True, read_only=False, center_map=True): 146 | """Export kepler.gl map to HTML file and open in Chrome 147 | """ 148 | # Generate path to a temporary file 149 | path = os.path.join(tempfile.mkdtemp(), 'vis.html') 150 | self.map.save_to_html(file_name=path, read_only=read_only, center_map=center_map) 151 | 152 | # Open saved HTML file in new tab in default browser 153 | if open_browser: 154 | webbrowser.open_new_tab('file://' + path) 155 | 156 | return path 157 | -------------------------------------------------------------------------------- /keplergl_cli/keplergl_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "datasets": [], 3 | "config": { 4 | "version": "v1", 5 | "config": { 6 | "visState": { 7 | "filters": [], 8 | "layers": [], 9 | "interactionConfig": { 10 | "tooltip": { 11 | "fieldsToShow": {}, 12 | "enabled": true 13 | }, 14 | "brush": { 15 | "size": 2, 16 | "enabled": false 17 | } 18 | }, 19 | "layerBlending": "normal", 20 | "splitMaps": [], 21 | "animationConfig": { 22 | "currentTime": null, 23 | "speed": 1 24 | } 25 | }, 26 | "mapState": { 27 | "bearing": 0, 28 | "dragRotate": false, 29 | "latitude": 37.75043, 30 | "longitude": -122.34679, 31 | "pitch": 0, 32 | "zoom": 9, 33 | "isSplit": false 34 | }, 35 | "mapStyle": { 36 | "styleType": "streets", 37 | "topLayerGroups": {}, 38 | "visibleLayerGroups": { 39 | "label": true, 40 | "road": true, 41 | "building": true, 42 | "water": true, 43 | "land": true 44 | }, 45 | "threeDBuildingColor": [ 46 | 194.6103322548211, 47 | 191.81688250953655, 48 | 185.2988331038727 49 | ], 50 | "mapStyles": { 51 | "streets": { 52 | "accessToken": "{MAPBOX_API_KEY}", 53 | "custom": true, 54 | "icon": "https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/-122.3391,37.7922,9,0,0/400x300?access_token={MAPBOX_API_KEY}&logo=false&attribution=false", 55 | "id": "streets", 56 | "label": "Mapbox Streets", 57 | "url": "mapbox://styles/mapbox/streets-v11" 58 | }, 59 | "outdoors": { 60 | "accessToken": "{MAPBOX_API_KEY}", 61 | "custom": true, 62 | "icon": "https://api.mapbox.com/styles/v1/mapbox/outdoors-v11/static/-122.3391,37.7922,9,0,0/400x300?access_token={MAPBOX_API_KEY}&logo=false&attribution=false", 63 | "id": "outdoors", 64 | "label": "Mapbox Outdoors", 65 | "url": "mapbox://styles/mapbox/outdoors-v11" 66 | }, 67 | "light": { 68 | "accessToken": "{MAPBOX_API_KEY}", 69 | "custom": true, 70 | "icon": "https://api.mapbox.com/styles/v1/mapbox/light-v10/static/-122.3391,37.7922,9,0,0/400x300?access_token={MAPBOX_API_KEY}&logo=false&attribution=false", 71 | "id": "light", 72 | "label": "Mapbox Light", 73 | "url": "mapbox://styles/mapbox/light-v10" 74 | }, 75 | "dark": { 76 | "accessToken": "{MAPBOX_API_KEY}", 77 | "custom": true, 78 | "icon": "https://api.mapbox.com/styles/v1/mapbox/dark-v10/static/-122.3391,37.7922,9,0,0/400x300?access_token={MAPBOX_API_KEY}&logo=false&attribution=false", 79 | "id": "dark", 80 | "label": "Mapbox Dark", 81 | "url": "mapbox://styles/mapbox/dark-v10" 82 | }, 83 | "satellite": { 84 | "accessToken": "{MAPBOX_API_KEY}", 85 | "custom": true, 86 | "icon": "https://api.mapbox.com/styles/v1/mapbox/satellite-v9/static/-122.3391,37.7922,9,0,0/400x300?access_token={MAPBOX_API_KEY}&logo=false&attribution=false", 87 | "id": "satellite", 88 | "label": "Mapbox Satellite", 89 | "url": "mapbox://styles/mapbox/satellite-v9" 90 | }, 91 | "satellite-streets": { 92 | "accessToken": "{MAPBOX_API_KEY}", 93 | "custom": true, 94 | "icon": "https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v11/static/-122.3391,37.7922,9,0,0/400x300?access_token={MAPBOX_API_KEY}&logo=false&attribution=false", 95 | "id": "satellite-streets", 96 | "label": "Mapbox Satellite Streets", 97 | "url": "mapbox://styles/mapbox/satellite-streets-v11" 98 | } 99 | } 100 | } 101 | } 102 | }, 103 | "info": { 104 | "app": "kepler.gl", 105 | "created_at": "Tue Oct 29 2019 17:15:02 GMT+0100 (Central European Standard Time)" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | keplergl>=0.3.2 2 | geojson>=2.5.0 3 | shapely>=1.6.4 4 | geopandas>=0.6.0 5 | click>=7.0 6 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pip==21.1 2 | bump2version==0.5.11 3 | wheel==0.33.6 4 | watchdog==0.9.0 5 | flake8==3.7.8 6 | tox==3.14.0 7 | coverage==4.5.4 8 | Sphinx==1.8.5 9 | twine==1.14.0 10 | Click==7.0 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.3.3 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:keplergl_cli/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | 20 | [aliases] 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """The setup script.""" 4 | 5 | from setuptools import find_packages, setup 6 | 7 | with open('README.md') as f: 8 | readme = f.read() 9 | 10 | with open('CHANGELOG.md') as history_file: 11 | history = history_file.read() 12 | 13 | with open('requirements.txt') as requirements_file: 14 | requirements = requirements_file.readlines() 15 | requirements = [x[:-1] for x in requirements] 16 | 17 | with open('requirements_dev.txt') as test_requirements_file: 18 | test_requirements = test_requirements_file.readlines() 19 | test_requirements = [x[:-1] for x in test_requirements] 20 | 21 | setup_requirements = ['setuptools >= 38.6.0', 'twine >= 1.11.0'] 22 | 23 | setup( 24 | author="Kyle Barron", 25 | author_email='kylebarron2@gmail.com', 26 | python_requires='>=3.5', 27 | classifiers=[ 28 | 'Development Status :: 4 - Beta', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Natural Language :: English', 32 | 'Programming Language :: Python :: 3', 33 | 'Programming Language :: Python :: 3.5', 34 | 'Programming Language :: Python :: 3.6', 35 | 'Programming Language :: Python :: 3.7', 36 | 'Programming Language :: Python :: 3.8', 37 | ], 38 | description="Description ", 39 | entry_points={ 40 | 'console_scripts': [ 41 | 'keplergl_quickvis=keplergl_cli.cli:main', 42 | 'kepler=keplergl_cli.cli:main', 43 | ], 44 | }, 45 | install_requires=requirements, 46 | license="MIT license", 47 | long_description=readme + '\n\n' + history, 48 | long_description_content_type='text/markdown', 49 | include_package_data=True, 50 | keywords=['keplergl', 'mapbox', 'cli'], 51 | name='keplergl_cli', 52 | packages=find_packages(include=['keplergl_cli', 'keplergl_cli.*']), 53 | package_data={'keplergl_cli': ['keplergl_config.json']}, 54 | setup_requires=setup_requirements, 55 | test_suite='tests', 56 | tests_require=test_requirements, 57 | url='https://github.com/kylebarron/keplergl_cli', 58 | version='0.3.3', 59 | zip_safe=False, 60 | ) 61 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit test package for kepler_quickvis.""" 2 | -------------------------------------------------------------------------------- /tests/test_kepler_quickvis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests for `kepler_quickvis` package.""" 4 | 5 | 6 | import unittest 7 | from click.testing import CliRunner 8 | 9 | from kepler_quickvis import kepler_quickvis 10 | from kepler_quickvis import cli 11 | 12 | 13 | class TestKepler_quickvis(unittest.TestCase): 14 | """Tests for `kepler_quickvis` package.""" 15 | 16 | def setUp(self): 17 | """Set up test fixtures, if any.""" 18 | 19 | def tearDown(self): 20 | """Tear down test fixtures, if any.""" 21 | 22 | def test_000_something(self): 23 | """Test something.""" 24 | 25 | def test_command_line_interface(self): 26 | """Test the CLI.""" 27 | runner = CliRunner() 28 | result = runner.invoke(cli.main) 29 | assert result.exit_code == 0 30 | assert 'kepler_quickvis.cli.main' in result.output 31 | help_result = runner.invoke(cli.main, ['--help']) 32 | assert help_result.exit_code == 0 33 | assert '--help Show this message and exit.' in help_result.output 34 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35, py36, py37, py38, flake8 3 | 4 | [travis] 5 | python = 6 | 3.8: py38 7 | 3.7: py37 8 | 3.6: py36 9 | 3.5: py35 10 | 11 | [testenv:flake8] 12 | basepython = python 13 | deps = flake8 14 | commands = flake8 kepler_quickvis 15 | 16 | [testenv] 17 | setenv = 18 | PYTHONPATH = {toxinidir} 19 | 20 | commands = python setup.py test 21 | --------------------------------------------------------------------------------