This is an automated email from the ASF dual-hosted git repository.

jiayu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sedona.git


The following commit(s) were added to refs/heads/master by this push:
     new d4d01104 [SEDONA-323] Add keplergl wrapper (#898)
d4d01104 is described below

commit d4d01104f83c87cb1e387ee773ffd7131359c6fe
Author: Nilesh Gajwani <[email protected]>
AuthorDate: Mon Jul 17 22:59:51 2023 -0700

    [SEDONA-323] Add keplergl wrapper (#898)
---
 ...eSedonaSQL_SpatialJoin_AirportsPerCountry.ipynb | 346 +++++++++++++++++++--
 docs/image/sedona_customization.gif                | Bin 0 -> 675205 bytes
 docs/tutorial/sql.md                               |  75 +++++
 python/Pipfile                                     |   1 +
 python/sedona/maps/SedonaKepler.py                 |  69 ++++
 python/sedona/maps/__init__.py                     |  16 +
 python/sedona/spark/__init__.py                    |   3 +-
 python/tests/maps/__init__.py                      |  16 +
 .../tests/maps/test_sedonakepler_visualization.py  | 201 ++++++++++++
 9 files changed, 707 insertions(+), 20 deletions(-)

diff --git a/binder/ApacheSedonaSQL_SpatialJoin_AirportsPerCountry.ipynb 
b/binder/ApacheSedonaSQL_SpatialJoin_AirportsPerCountry.ipynb
index f1d883f5..225e6ad8 100644
--- a/binder/ApacheSedonaSQL_SpatialJoin_AirportsPerCountry.ipynb
+++ b/binder/ApacheSedonaSQL_SpatialJoin_AirportsPerCountry.ipynb
@@ -37,9 +37,9 @@
     "\n",
     "\n",
     "from sedona.spark import *\n",
+    "from keplergl import KeplerGl\n",
     "from utilities import getConfig\n",
-    "\n",
-    "from keplergl import KeplerGl"
+    "\n"
    ]
   },
   {
@@ -51,9 +51,48 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 2,
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      ":: loading settings :: url = 
jar:file:/Users/nileshgajwani/Desktop/spark/spark-3.4.0-bin-hadoop3/jars/ivy-2.5.1.jar!/org/apache/ivy/core/settings/ivysettings.xml\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "Ivy Default Cache set to: /Users/nileshgajwani/.ivy2/cache\n",
+      "The jars for the packages stored in: /Users/nileshgajwani/.ivy2/jars\n",
+      "org.apache.sedona#sedona-spark-shaded-3.0_2.12 added as a dependency\n",
+      "org.datasyslab#geotools-wrapper added as a dependency\n",
+      ":: resolving dependencies :: 
org.apache.spark#spark-submit-parent-2ebc22b4-bd08-4a3f-a2dc-bd50e2f0f728;1.0\n",
+      "\tconfs: [default]\n",
+      "\tfound org.apache.sedona#sedona-spark-shaded-3.0_2.12;1.4.1 in 
central\n",
+      "\tfound org.datasyslab#geotools-wrapper;1.4.0-28.2 in central\n",
+      ":: resolution report :: resolve 85ms :: artifacts dl 3ms\n",
+      "\t:: modules in use:\n",
+      "\torg.apache.sedona#sedona-spark-shaded-3.0_2.12;1.4.1 from central in 
[default]\n",
+      "\torg.datasyslab#geotools-wrapper;1.4.0-28.2 from central in 
[default]\n",
+      
"\t---------------------------------------------------------------------\n",
+      "\t|                  |            modules            ||   artifacts   
|\n",
+      "\t|       conf       | number| search|dwnlded|evicted|| 
number|dwnlded|\n",
+      
"\t---------------------------------------------------------------------\n",
+      "\t|      default     |   2   |   0   |   0   |   0   ||   2   |   0   
|\n",
+      
"\t---------------------------------------------------------------------\n",
+      ":: retrieving :: 
org.apache.spark#spark-submit-parent-2ebc22b4-bd08-4a3f-a2dc-bd50e2f0f728\n",
+      "\tconfs: [default]\n",
+      "\t0 artifacts copied, 2 already retrieved (0kB/2ms)\n",
+      "23/07/12 14:17:39 WARN NativeCodeLoader: Unable to load native-hadoop 
library for your platform... using builtin-java classes where applicable\n",
+      "Setting default log level to \"WARN\".\n",
+      "To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use 
setLogLevel(newLevel).\n",
+      "23/07/12 14:17:43 WARN SimpleFunctionRegistry: The function st_affine 
replaced a previously registered function.\n"
+     ]
+    }
+   ],
    "source": [
     "config = SedonaContext.builder() .\\\n",
     "    config('spark.jars.packages',\n",
@@ -76,9 +115,120 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 3,
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "root\n",
+      " |-- geometry: geometry (nullable = true)\n",
+      " |-- featurecla: string (nullable = true)\n",
+      " |-- scalerank: string (nullable = true)\n",
+      " |-- LABELRANK: string (nullable = true)\n",
+      " |-- SOVEREIGNT: string (nullable = true)\n",
+      " |-- SOV_A3: string (nullable = true)\n",
+      " |-- ADM0_DIF: string (nullable = true)\n",
+      " |-- LEVEL: string (nullable = true)\n",
+      " |-- TYPE: string (nullable = true)\n",
+      " |-- ADMIN: string (nullable = true)\n",
+      " |-- ADM0_A3: string (nullable = true)\n",
+      " |-- GEOU_DIF: string (nullable = true)\n",
+      " |-- GEOUNIT: string (nullable = true)\n",
+      " |-- GU_A3: string (nullable = true)\n",
+      " |-- SU_DIF: string (nullable = true)\n",
+      " |-- SUBUNIT: string (nullable = true)\n",
+      " |-- SU_A3: string (nullable = true)\n",
+      " |-- BRK_DIFF: string (nullable = true)\n",
+      " |-- NAME: string (nullable = true)\n",
+      " |-- NAME_LONG: string (nullable = true)\n",
+      " |-- BRK_A3: string (nullable = true)\n",
+      " |-- BRK_NAME: string (nullable = true)\n",
+      " |-- BRK_GROUP: string (nullable = true)\n",
+      " |-- ABBREV: string (nullable = true)\n",
+      " |-- POSTAL: string (nullable = true)\n",
+      " |-- FORMAL_EN: string (nullable = true)\n",
+      " |-- FORMAL_FR: string (nullable = true)\n",
+      " |-- NAME_CIAWF: string (nullable = true)\n",
+      " |-- NOTE_ADM0: string (nullable = true)\n",
+      " |-- NOTE_BRK: string (nullable = true)\n",
+      " |-- NAME_SORT: string (nullable = true)\n",
+      " |-- NAME_ALT: string (nullable = true)\n",
+      " |-- MAPCOLOR7: string (nullable = true)\n",
+      " |-- MAPCOLOR8: string (nullable = true)\n",
+      " |-- MAPCOLOR9: string (nullable = true)\n",
+      " |-- MAPCOLOR13: string (nullable = true)\n",
+      " |-- POP_EST: string (nullable = true)\n",
+      " |-- POP_RANK: string (nullable = true)\n",
+      " |-- GDP_MD_EST: string (nullable = true)\n",
+      " |-- POP_YEAR: string (nullable = true)\n",
+      " |-- LASTCENSUS: string (nullable = true)\n",
+      " |-- GDP_YEAR: string (nullable = true)\n",
+      " |-- ECONOMY: string (nullable = true)\n",
+      " |-- INCOME_GRP: string (nullable = true)\n",
+      " |-- WIKIPEDIA: string (nullable = true)\n",
+      " |-- FIPS_10_: string (nullable = true)\n",
+      " |-- ISO_A2: string (nullable = true)\n",
+      " |-- ISO_A3: string (nullable = true)\n",
+      " |-- ISO_A3_EH: string (nullable = true)\n",
+      " |-- ISO_N3: string (nullable = true)\n",
+      " |-- UN_A3: string (nullable = true)\n",
+      " |-- WB_A2: string (nullable = true)\n",
+      " |-- WB_A3: string (nullable = true)\n",
+      " |-- WOE_ID: string (nullable = true)\n",
+      " |-- WOE_ID_EH: string (nullable = true)\n",
+      " |-- WOE_NOTE: string (nullable = true)\n",
+      " |-- ADM0_A3_IS: string (nullable = true)\n",
+      " |-- ADM0_A3_US: string (nullable = true)\n",
+      " |-- ADM0_A3_UN: string (nullable = true)\n",
+      " |-- ADM0_A3_WB: string (nullable = true)\n",
+      " |-- CONTINENT: string (nullable = true)\n",
+      " |-- REGION_UN: string (nullable = true)\n",
+      " |-- SUBREGION: string (nullable = true)\n",
+      " |-- REGION_WB: string (nullable = true)\n",
+      " |-- NAME_LEN: string (nullable = true)\n",
+      " |-- LONG_LEN: string (nullable = true)\n",
+      " |-- ABBREV_LEN: string (nullable = true)\n",
+      " |-- TINY: string (nullable = true)\n",
+      " |-- HOMEPART: string (nullable = true)\n",
+      " |-- MIN_ZOOM: string (nullable = true)\n",
+      " |-- MIN_LABEL: string (nullable = true)\n",
+      " |-- MAX_LABEL: string (nullable = true)\n",
+      " |-- NE_ID: string (nullable = true)\n",
+      " |-- WIKIDATAID: string (nullable = true)\n",
+      " |-- NAME_AR: string (nullable = true)\n",
+      " |-- NAME_BN: string (nullable = true)\n",
+      " |-- NAME_DE: string (nullable = true)\n",
+      " |-- NAME_EN: string (nullable = true)\n",
+      " |-- NAME_ES: string (nullable = true)\n",
+      " |-- NAME_FR: string (nullable = true)\n",
+      " |-- NAME_EL: string (nullable = true)\n",
+      " |-- NAME_HI: string (nullable = true)\n",
+      " |-- NAME_HU: string (nullable = true)\n",
+      " |-- NAME_ID: string (nullable = true)\n",
+      " |-- NAME_IT: string (nullable = true)\n",
+      " |-- NAME_JA: string (nullable = true)\n",
+      " |-- NAME_KO: string (nullable = true)\n",
+      " |-- NAME_NL: string (nullable = true)\n",
+      " |-- NAME_PL: string (nullable = true)\n",
+      " |-- NAME_PT: string (nullable = true)\n",
+      " |-- NAME_RU: string (nullable = true)\n",
+      " |-- NAME_SV: string (nullable = true)\n",
+      " |-- NAME_TR: string (nullable = true)\n",
+      " |-- NAME_VI: string (nullable = true)\n",
+      " |-- NAME_ZH: string (nullable = true)\n",
+      "\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "23/07/12 14:17:43 WARN package: Truncated the string representation of 
a plan since it was too large. This behavior can be adjusted by setting 
'spark.sql.debug.maxToStringFields'.\n"
+     ]
+    }
+   ],
    "source": [
     "countries = ShapefileReader.readToGeometryRDD(sc, 
\"data/ne_50m_admin_0_countries_lakes/\")\n",
     "countries_df = Adapter.toDf(countries, sedona)\n",
@@ -96,9 +246,29 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 4,
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "root\n",
+      " |-- geometry: geometry (nullable = true)\n",
+      " |-- scalerank: string (nullable = true)\n",
+      " |-- featurecla: string (nullable = true)\n",
+      " |-- type: string (nullable = true)\n",
+      " |-- name: string (nullable = true)\n",
+      " |-- abbrev: string (nullable = true)\n",
+      " |-- location: string (nullable = true)\n",
+      " |-- gps_code: string (nullable = true)\n",
+      " |-- iata_code: string (nullable = true)\n",
+      " |-- wikipedia: string (nullable = true)\n",
+      " |-- natlscale: string (nullable = true)\n",
+      "\n"
+     ]
+    }
+   ],
    "source": [
     "airports = ShapefileReader.readToGeometryRDD(sc, 
\"data/ne_50m_airports/\")\n",
     "airports_df = Adapter.toDf(airports, sedona)\n",
@@ -115,7 +285,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 5,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -131,9 +301,17 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 6,
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "[('3.0', '2.12', '1.4.1')]\n"
+     ]
+    }
+   ],
    "source": [
     "airports_rdd = Adapter.toSpatialRdd(airports_df, \"geometry\")\n",
     "# Drop the duplicate name column in countries_df\n",
@@ -170,9 +348,75 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 7,
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "23/07/12 14:17:44 WARN JoinQuery: UseIndex is true, but no index 
exists. Will build index on the fly.\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      
"+--------------------+--------------------+--------------------+--------------------+\n",
+      "|        country_geom|             NAME_EN|        airport_geom|        
        name|\n",
+      
"+--------------------+--------------------+--------------------+--------------------+\n",
+      "|MULTIPOLYGON (((1...|Taiwan           ...|POINT (121.231370...|Taoyuan 
         ...|\n",
+      "|MULTIPOLYGON (((5...|Netherlands      ...|POINT 
(4.76437693...|Schiphol         ...|\n",
+      "|POLYGON ((103.969...|Singapore        ...|POINT 
(103.986413...|Singapore Changi ...|\n",
+      "|MULTIPOLYGON (((-...|United Kingdom   ...|POINT (-0.4531566...|London 
Heathrow  ...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT 
(-149.98172...|Anchorage Int'l  ...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT 
(-84.425397...|Hartsfield-Jackso...|\n",
+      "|MULTIPOLYGON (((1...|People's Republic...|POINT (116.588174...|Beijing 
Capital  ...|\n",
+      "|MULTIPOLYGON (((-...|Colombia         ...|POINT 
(-74.143371...|Eldorado Int'l   ...|\n",
+      "|MULTIPOLYGON (((6...|India            ...|POINT 
(72.8745639...|Chhatrapati Shiva...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT (-71.016406...|Gen E L 
Logan Int...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT 
(-76.668642...|Baltimore-Washing...|\n",
+      "|POLYGON ((36.8713...|Egypt            ...|POINT (31.3997430...|Cairo 
Int'l      ...|\n",
+      "|POLYGON ((-2.2196...|Morocco          ...|POINT 
(-7.6632188...|Casablanca-Anfa  ...|\n",
+      "|MULTIPOLYGON (((-...|Venezuela        ...|POINT (-67.005748...|Simon 
Bolivar Int...|\n",
+      "|MULTIPOLYGON (((2...|South Africa     ...|POINT (18.5976565...|Cape 
Town Int'l  ...|\n",
+      "|MULTIPOLYGON (((1...|People's Republic...|POINT 
(103.956136...|Chengdushuang Liu...|\n",
+      "|MULTIPOLYGON (((6...|India            ...|POINT (77.0878362...|Indira 
Gandhi Int...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT (-104.67379...|Denver 
Int'l     ...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT 
(-97.040371...|Dallas-Ft. Worth ...|\n",
+      "|MULTIPOLYGON (((1...|Thailand         ...|POINT (100.602578...|Don 
Muang Int'l  ...|\n",
+      
"+--------------------+--------------------+--------------------+--------------------+\n",
+      "only showing top 20 rows\n",
+      "\n",
+      
"+--------------------+--------------------+--------------------+--------------------+\n",
+      "|        country_geom|             NAME_EN|        airport_geom|        
        name|\n",
+      
"+--------------------+--------------------+--------------------+--------------------+\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT (-80.145258...|Fort 
Lauderdale H...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT (-80.278971...|Miami 
Int'l      ...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT (-95.333704...|George 
Bush Inter...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT (-90.256693...|New 
Orleans Int'l...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT (-81.307371...|Orlando 
Int'l    ...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT (-82.534824...|Tampa 
Int'l      ...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT (-112.01363...|Sky 
Harbor Int'l ...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT (-118.40246...|Los 
Angeles Int'l...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT (-116.97547...|General 
Abelardo ...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT 
(-97.040371...|Dallas-Ft. Worth ...|\n",
+      "|MULTIPOLYGON (((-...|United States of ...|POINT 
(-84.425397...|Hartsfield-Jackso...|\n",
+      "|POLYGON ((-69.965...|Peru             ...|POINT (-77.107565...|Jorge 
Chavez     ...|\n",
+      "|MULTIPOLYGON (((-...|Panama           ...|POINT (-79.387134...|Tocumen 
Int'l    ...|\n",
+      "|POLYGON ((-83.157...|Nicaragua        ...|POINT (-86.171284...|Augusto 
Cesar San...|\n",
+      "|MULTIPOLYGON (((-...|Mexico           ...|POINT (-96.183570...|Gen. 
Heriberto Ja...|\n",
+      "|MULTIPOLYGON (((-...|Mexico           ...|POINT (-106.27001...|General 
Rafael Bu...|\n",
+      "|MULTIPOLYGON (((-...|Mexico           ...|POINT (-99.754508...|General 
Juan N Al...|\n",
+      "|MULTIPOLYGON (((-...|Mexico           ...|POINT (-99.570649...|Jose 
Maria Morelo...|\n",
+      "|MULTIPOLYGON (((-...|Mexico           ...|POINT (-98.375759...|Puebla  
         ...|\n",
+      "|MULTIPOLYGON (((-...|Mexico           ...|POINT (-99.082607...|Lic 
Benito Juarez...|\n",
+      
"+--------------------+--------------------+--------------------+--------------------+\n",
+      "only showing top 20 rows\n",
+      "\n"
+     ]
+    }
+   ],
    "source": [
     "# The result of SQL API\n",
     "result.show()\n",
@@ -189,11 +433,44 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 8,
    "metadata": {
     "scrolled": true
    },
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "+--------------------+--------------------+------------+\n",
+      "|             NAME_EN|        country_geom|AirportCount|\n",
+      "+--------------------+--------------------+------------+\n",
+      "|Cuba             ...|MULTIPOLYGON (((-...|           1|\n",
+      "|Mexico           ...|MULTIPOLYGON (((-...|          12|\n",
+      "|Panama           ...|MULTIPOLYGON (((-...|           1|\n",
+      "|Nicaragua        ...|POLYGON ((-83.157...|           1|\n",
+      "|Honduras         ...|MULTIPOLYGON (((-...|           1|\n",
+      "|Colombia         ...|MULTIPOLYGON (((-...|           4|\n",
+      "|United States of ...|MULTIPOLYGON (((-...|          35|\n",
+      "|Ecuador          ...|MULTIPOLYGON (((-...|           1|\n",
+      "|The Bahamas      ...|MULTIPOLYGON (((-...|           1|\n",
+      "|Peru             ...|POLYGON ((-69.965...|           1|\n",
+      "|Guatemala        ...|POLYGON ((-92.235...|           1|\n",
+      "|Canada           ...|MULTIPOLYGON (((-...|          15|\n",
+      "|Venezuela        ...|MULTIPOLYGON (((-...|           3|\n",
+      "|Argentina        ...|MULTIPOLYGON (((-...|           3|\n",
+      "|Bolivia          ...|MULTIPOLYGON (((-...|           2|\n",
+      "|Paraguay         ...|POLYGON ((-58.159...|           1|\n",
+      "|Benin            ...|POLYGON ((1.62265...|           1|\n",
+      "|Guinea           ...|POLYGON ((-10.283...|           1|\n",
+      "|Chile            ...|MULTIPOLYGON (((-...|           5|\n",
+      "|Nigeria          ...|MULTIPOLYGON (((7...|           3|\n",
+      "+--------------------+--------------------+------------+\n",
+      "only showing top 20 rows\n",
+      "\n"
+     ]
+    }
+   ],
    "source": [
     "# result.createOrReplaceTempView(\"result\")\n",
     "result2.createOrReplaceTempView(\"result\")\n",
@@ -210,16 +487,47 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 9,
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "User Guide: https://docs.kepler.gl/docs/keplergl-jupyter\n";
+     ]
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "0646646608754887811eee12e5516d16",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "KeplerGl(config={'version': 'v1', 'config': {'visState': {'filters': 
[], 'layers': [{'id': 'ikzru0t', 'type': …"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
    "source": [
     "df = groupedresult.toPandas()\n",
     "gdf = gpd.GeoDataFrame(df, 
geometry=\"country_geom\").rename(columns={'country_geom':'geometry'})\n",
     "\n",
-    "map_1 = KeplerGl(data={\"AirportCount\": gdf}, config=getConfig())\n",
-    "map_1"
+    "map = KeplerGl(data={\"AirportCount\": gdf}, config=getConfig())\n",
+    "map"
    ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [],
+   "source": []
   }
  ],
  "metadata": {
diff --git a/docs/image/sedona_customization.gif 
b/docs/image/sedona_customization.gif
new file mode 100644
index 00000000..2b6903a2
Binary files /dev/null and b/docs/image/sedona_customization.gif differ
diff --git a/docs/tutorial/sql.md b/docs/tutorial/sql.md
index aa98b379..8621d153 100644
--- a/docs/tutorial/sql.md
+++ b/docs/tutorial/sql.md
@@ -505,6 +505,81 @@ The details of a join query is available here [Join 
query](../api/sql/Optimizer.
 
 There are lots of other functions can be combined with these queries. Please 
read [SedonaSQL functions](../api/sql/Function.md) and [SedonaSQL aggregate 
functions](../api/sql/AggregateFunction.md).
 
+## Visualize query results
+
+==Sedona >= 1.5.0==
+
+
+Spatial query results can be visualized in Jupyter lab/notebook using 
SedonaKepler. 
+
+SedonaKepler exposes APIs to create interactive and customizable map 
visualizations using [KeplerGl](https://kepler.gl/).
+
+### Creating a map object using SedonaKepler.create_map
+
+SedonaKepler exposes a create_map API with the following signature:
+
+```python
+create_map(df: SedonaDataFrame=None, name: str='unnamed', geometry_col: 
str='geometry', config: dict=None) -> map
+```
+
+The parameter 'name' is used to associate the passed SedonaDataFrame in the 
map object and any config applied to the map is linked to this name. It is 
recommended you pass a unique identifier to the dataframe here.
+
+The parameter 'geometry_col' is used to identify the geometry containing 
column. This is required if the column has a name other than the standard 
'geometry'.
+
+!!!Note
+       Failure to pass the correct geometry column name (if it has a name 
other than 'geometry') will result in a failure to create a map object.
+
+If no SedonaDataFrame object is passed, an empty map (with config applied if 
passed) is returned. A SedonaDataFrame can be added later using the method 
`add_df`
+
+A map config can be passed optionally to apply pre-apply customizations to the 
map.
+
+!!!Note 
+       The map config references every customization with the name assigned to 
the SedonaDataFrame being displayed, if there is a mismatch in the name, the 
config will not be applied to the map object.
+
+
+!!! abstract "Example usage (Referenced from Sedona Jupyter examples)"
+
+       === "Python"
+               ```python
+               map = SedonaKepler.create_map(df=groupedresult, 
name="AirportCount", geometry_col="country_geom")
+               map
+               ```
+
+### Adding SedonaDataFrame to a map object using SedonaKepler.add_df
+SedonaKepler exposes a add_df API with the following signature:
+
+```python
+add_df(map, df: SedonaDataFrame, name: str='unnamed', geometry_col='geometry')
+```
+
+This API can be used to add a SedonaDataFrame to an already created map 
object. The map object passed is directly mutated and nothing is returned.
+
+The parameters name and geometry_col have the same conditions as 'create_map'
+
+!!!Tip
+       This method can be used to add multiple dataframes to a map object to 
be able to visualize them together.
+
+!!! abstract "Example usage (Referenced from Sedona Jupyter examples)"
+       === "Python"
+               ```python
+               map = SedonaKepler.create_map()
+               SedonaKepler.add_df(map, groupedresult, name="AirportCount", 
geometry_col="country_geom")
+               map
+               ```
+
+### Setting a config via the map 
+A map rendered by accessing the map object created by SedonaKepler includes a 
config panel which can be used to customize the map
+
+<img src="../../image/sedona_customization.gif" width="1000">
+
+
+### Saving and setting config
+
+A map object's current config can be accessed by accessing its 'config' 
attribute like `map.config`. This config can be saved for future use or use 
across notebooks if the exact same map is to be rendered everytime.
+
+!!!Note
+       The map config references each applied customization with the name 
given to the dataframe and hence will work only on maps with the same name of 
dataframe supplied.
+       For more details refer to keplerGl documentation 
[here](https://docs.kepler.gl/docs/keplergl-jupyter#6.-match-config-with-data)
 ## Save to permanent storage
 
 To save a Spatial DataFrame to some permanent storage such as Hive tables and 
HDFS, you can simply convert each geometry in the Geometry type column back to 
a plain String and save the plain DataFrame to wherever you want.
diff --git a/python/Pipfile b/python/Pipfile
index e1aeb8c1..e23af0bf 100644
--- a/python/Pipfile
+++ b/python/Pipfile
@@ -17,6 +17,7 @@ geopandas="<=0.10.2"
 pyspark=">=2.3.0"
 attrs="*"
 pyarrow="*"
+keplergl = "==0.3.2"
 
 [requires]
 python_version = "3.7"
diff --git a/python/sedona/maps/SedonaKepler.py 
b/python/sedona/maps/SedonaKepler.py
new file mode 100644
index 00000000..c70ee726
--- /dev/null
+++ b/python/sedona/maps/SedonaKepler.py
@@ -0,0 +1,69 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+from keplergl import KeplerGl
+import geopandas as gpd
+
+
+class SedonaKepler:
+
+    @classmethod
+    def create_map(cls, df=None, name="unnamed", geometry_col="geometry", 
config=None):
+        """
+        Creates a map visualization using kepler, optionally taking a sedona 
dataFrame as data input
+        :param df: [Optional] SedonaDataFrame to plot on the map
+        :param name: [Optional] Name to be associated with the given 
dataframe, if a df is passed with no name, a default name of 'unnamed' is set 
for it.
+        :param geometry_col: [Optional] Custom name of geometry column in the 
sedona data frame,
+                            if no name is provided, it is assumed that the 
column has the default name 'geometry'.
+        :param config: [Optional] A map config to be applied to the rendered 
map
+        :return: A map object
+        """
+        kepler_map = KeplerGl()
+        if df is not None:
+            SedonaKepler.add_df(kepler_map, df, name, geometry_col)
+
+        if config is not None:
+            kepler_map.config = config
+
+        return kepler_map
+
+    @classmethod
+    def add_df(cls, kepler_map, df, name="unnamed", geometry_col="geometry"):
+        """
+        Adds a SedonaDataFrame to a given map object.
+        :param kepler_map: Map object to add SedonaDataFrame to
+        :param df: SedonaDataFrame to add
+        :param name: [Optional] Name to assign to the dataframe, default name 
assigned is 'unnamed'
+        :param geometry_col: [Optional] Custom name of geometry_column if any, 
if no name is provided, a default name of 'geometry' is assumed.
+        :return: Does not return anything, adds df directly to the given map 
object
+        """
+        geo_df = SedonaKepler._convert_to_gdf(df, geometry_col)
+        kepler_map.add_data(geo_df, name=name)
+
+    @classmethod
+    def _convert_to_gdf(cls, df, geometry_col="geometry"):
+        """
+        Converts a SedonaDataFrame to a GeoPandasDataFrame and also renames 
geometry column to a standard name of 'geometry'
+        :param df: SedonaDataFrame to convert
+        :param geometry_col: [Optional]
+        :return:
+        """
+        pandas_df = df.toPandas()
+        geo_df = gpd.GeoDataFrame(pandas_df, geometry=geometry_col)
+        if geometry_col != "geometry":
+            geo_df = geo_df.rename(columns={geometry_col: "geometry"})
+        return geo_df
diff --git a/python/sedona/maps/__init__.py b/python/sedona/maps/__init__.py
new file mode 100644
index 00000000..a67d5ea2
--- /dev/null
+++ b/python/sedona/maps/__init__.py
@@ -0,0 +1,16 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
diff --git a/python/sedona/spark/__init__.py b/python/sedona/spark/__init__.py
index 3cf6e8fa..011a1940 100644
--- a/python/sedona/spark/__init__.py
+++ b/python/sedona/spark/__init__.py
@@ -38,4 +38,5 @@ from sedona.utils.adapter import Adapter
 from sedona.utils import KryoSerializer
 from sedona.utils import SedonaKryoRegistrator
 from sedona.register import SedonaRegistrator
-from sedona.spark.SedonaContext import SedonaContext
\ No newline at end of file
+from sedona.spark.SedonaContext import SedonaContext
+from sedona.maps.SedonaKepler import SedonaKepler
\ No newline at end of file
diff --git a/python/tests/maps/__init__.py b/python/tests/maps/__init__.py
new file mode 100644
index 00000000..38cc50c2
--- /dev/null
+++ b/python/tests/maps/__init__.py
@@ -0,0 +1,16 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
\ No newline at end of file
diff --git a/python/tests/maps/test_sedonakepler_visualization.py 
b/python/tests/maps/test_sedonakepler_visualization.py
new file mode 100644
index 00000000..a5144e90
--- /dev/null
+++ b/python/tests/maps/test_sedonakepler_visualization.py
@@ -0,0 +1,201 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+from keplergl import KeplerGl
+from sedona.maps.SedonaKepler import SedonaKepler
+from tests.test_base import TestBase
+from tests import mixed_wkt_geometry_input_location
+from tests import csv_point_input_location
+import geopandas as gpd
+
+
+class TestVisualization(TestBase):
+    """ _repr_html() creates a html encoded string of the current map data, 
can be used to assert data equality """
+
+    def test_basic_map_creation(self):
+        sedona_kepler_map = SedonaKepler.create_map()
+        kepler_map = KeplerGl()
+        assert sedona_kepler_map.config == kepler_map.config
+
+    def test_map_creation_with_df(self):
+        polygon_wkt_df = self.spark.read.format("csv"). \
+            option("delimiter", "\t"). \
+            option("header", "false"). \
+            load(mixed_wkt_geometry_input_location)
+
+        polygon_wkt_df.createOrReplaceTempView("polygontable")
+        polygon_df = self.spark.sql("select ST_GeomFromWKT(polygontable._c0) 
as countyshape from polygontable")
+        polygon_gdf = gpd.GeoDataFrame(data=polygon_df.toPandas(), 
geometry="countyshape")
+        polygon_gdf_renamed = polygon_gdf.rename(columns={"countyshape": 
"geometry"})
+
+        sedona_kepler_map = SedonaKepler.create_map(df=polygon_df, 
name="data_1", geometry_col="countyshape")
+        kepler_map = KeplerGl()
+        kepler_map.add_data(data=polygon_gdf_renamed, name="data_1")
+
+        assert sedona_kepler_map._repr_html_() == kepler_map._repr_html_()
+        assert sedona_kepler_map.config == kepler_map.config
+
+    def test_df_addition(self):
+        polygon_wkt_df = self.spark.read.format("csv"). \
+            option("delimiter", "\t"). \
+            option("header", "false"). \
+            load(mixed_wkt_geometry_input_location)
+
+        polygon_wkt_df.createOrReplaceTempView("polygontable")
+
+        polygon_df = self.spark.sql("select ST_GeomFromWKT(polygontable._c0) 
as countyshape from polygontable")
+        polygon_gdf = gpd.GeoDataFrame(data=polygon_df.toPandas(), 
geometry="countyshape")
+        polygon_gdf_renamed = polygon_gdf.rename(columns={"countyshape": 
"geometry"})
+
+        sedona_kepler_empty_map = SedonaKepler.create_map()
+        SedonaKepler.add_df(sedona_kepler_empty_map, polygon_df, 
name="data_1", geometry_col="countyshape")
+
+        kepler_map = KeplerGl()
+        kepler_map.add_data(polygon_gdf_renamed, name="data_1")
+
+        assert sedona_kepler_empty_map._repr_html_() == 
kepler_map._repr_html_()
+        assert sedona_kepler_empty_map.config == kepler_map.config
+
+    def test_adding_multiple_datasets(self):
+        config = {'version': 'v1',
+                  'config': {'visState': {'filters': [],
+                                          'layers': [{'id': 'ikzru0t',
+                                                      'type': 'geojson',
+                                                      'config': {'dataId': 
'AirportCount',
+                                                                 'label': 
'AirportCount',
+                                                                 'color': 
[218, 112, 191],
+                                                                 
'highlightColor': [252, 242, 26, 255],
+                                                                 'columns': 
{'geojson': 'geometry'},
+                                                                 'isVisible': 
True,
+                                                                 'visConfig': 
{'opacity': 0.8,
+                                                                               
'strokeOpacity': 0.8,
+                                                                               
'thickness': 0.5,
+                                                                               
'strokeColor': [18, 92, 119],
+                                                                               
'colorRange': {
+                                                                               
    'name': 'Uber Viz Sequential 6',
+                                                                               
    'type': 'sequential',
+                                                                               
    'category': 'Uber',
+                                                                               
    'colors': ['#E6FAFA',
+                                                                               
               '#C1E5E6',
+                                                                               
               '#9DD0D4',
+                                                                               
               '#75BBC1',
+                                                                               
               '#4BA7AF',
+                                                                               
               '#00939C',
+                                                                               
               '#108188',
+                                                                               
               '#0E7077']},
+                                                                               
'strokeColorRange': {
+                                                                               
    'name': 'Global Warming',
+                                                                               
    'type': 'sequential',
+                                                                               
    'category': 'Uber',
+                                                                               
    'colors': ['#5A1846',
+                                                                               
               '#900C3F',
+                                                                               
               '#C70039',
+                                                                               
               '#E3611C',
+                                                                               
               '#F1920E',
+                                                                               
               '#FFC300']},
+                                                                               
'radius': 10,
+                                                                               
'sizeRange': [0, 10],
+                                                                               
'radiusRange': [0, 50],
+                                                                               
'heightRange': [0, 500],
+                                                                               
'elevationScale': 5,
+                                                                               
'enableElevationZoomFactor': True,
+                                                                               
'stroked': False,
+                                                                               
'filled': True,
+                                                                               
'enable3d': False,
+                                                                               
'wireframe': False},
+                                                                 'hidden': 
False,
+                                                                 'textLabel': 
[{'field': None,
+                                                                               
 'color': [255, 255, 255],
+                                                                               
 'size': 18,
+                                                                               
 'offset': [0, 0],
+                                                                               
 'anchor': 'start',
+                                                                               
 'alignment': 'center'}]},
+                                                      'visualChannels': 
{'colorField': {'name': 'AirportCount',
+                                                                               
         'type': 'integer'},
+                                                                         
'colorScale': 'quantize',
+                                                                         
'strokeColorField': None,
+                                                                         
'strokeColorScale': 'quantile',
+                                                                         
'sizeField': None,
+                                                                         
'sizeScale': 'linear',
+                                                                         
'heightField': None,
+                                                                         
'heightScale': 'linear',
+                                                                         
'radiusField': None,
+                                                                         
'radiusScale': 'linear'}}],
+                                          'interactionConfig': {
+                                              'tooltip': {'fieldsToShow': 
{'AirportCount': [{'name': 'NAME_EN',
+                                                                               
              'format': None},
+                                                                               
             {'name': 'AirportCount',
+                                                                               
              'format': None}]},
+                                                          'compareMode': False,
+                                                          'compareType': 
'absolute',
+                                                          'enabled': True},
+                                              'brush': {'size': 0.5, 
'enabled': False},
+                                              'geocoder': {'enabled': False},
+                                              'coordinate': {'enabled': 
False}},
+                                          'layerBlending': 'normal',
+                                          'splitMaps': [],
+                                          'animationConfig': {'currentTime': 
None, 'speed': 1}},
+                             'mapState': {'bearing': 0,
+                                          'dragRotate': False,
+                                          'latitude': 56.422456606624316,
+                                          'longitude': 9.778836615231771,
+                                          'pitch': 0,
+                                          'zoom': 0.4214991225736964,
+                                          'isSplit': False},
+                             'mapStyle': {'styleType': 'dark',
+                                          'topLayerGroups': {},
+                                          'visibleLayerGroups': {'label': True,
+                                                                 'road': True,
+                                                                 'border': 
False,
+                                                                 'building': 
True,
+                                                                 'water': True,
+                                                                 'land': True,
+                                                                 '3d 
building': False},
+                                          'threeDBuildingColor': 
[9.665468314072013,
+                                                                  
17.18305478057247,
+                                                                  
31.1442867897876],
+                                          'mapStyles': {}}}}
+        polygon_wkt_df = self.spark.read.format("csv"). \
+            option("delimiter", "\t"). \
+            option("header", "false"). \
+            load(mixed_wkt_geometry_input_location)
+
+        point_csv_df = self.spark.read.format("csv"). \
+            option("delimiter", ","). \
+            option("header", "false"). \
+            load(csv_point_input_location)
+
+        point_csv_df.createOrReplaceTempView("pointtable")
+        point_df = self.spark.sql("select ST_Point(cast(pointtable._c0 as 
Decimal(24,20)), cast(pointtable._c1 as Decimal(24,20))) as arealandmark from 
pointtable")
+        polygon_wkt_df.createOrReplaceTempView("polygontable")
+        polygon_df = self.spark.sql("select ST_GeomFromWKT(polygontable._c0) 
as countyshape from polygontable")
+
+        sedona_kepler_map = SedonaKepler.create_map(df=polygon_df, 
name="data_1", geometry_col="countyshape", config=config)
+        # SedonaKepler.add_df(sedona_kepler_map, polygon_df, "data_1", 
"countyshape")
+        SedonaKepler.add_df(sedona_kepler_map, point_df, name="data_2", 
geometry_col="arealandmark")
+
+        polygon_gdf = gpd.GeoDataFrame(data=polygon_df.toPandas(), 
geometry="countyshape")
+        polygon_gdf_renamed = polygon_gdf.rename(columns={"countyshape": 
"geometry"})
+        point_gdf = gpd.GeoDataFrame(data=point_df.toPandas(), 
geometry="arealandmark")
+        point_gdf_renamed = point_gdf.rename(columns={"arealandmark": 
"geometry"})
+
+        kepler_map = KeplerGl(config=config)
+        kepler_map.add_data(polygon_gdf_renamed, "data_1")
+        kepler_map.add_data(point_gdf_renamed, name="data_2")
+
+        assert sedona_kepler_map._repr_html_() == kepler_map._repr_html_()
+        assert sedona_kepler_map.config == kepler_map.config


Reply via email to