diff --git a/docs/notebooks/144_chart_features.ipynb b/docs/notebooks/144_chart_features.ipynb new file mode 100644 index 0000000000..b1e9f617e9 --- /dev/null +++ b/docs/notebooks/144_chart_features.ipynb @@ -0,0 +1,352 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\"Open\n", + "\n", + "**Feature and FeatureCollection Charts**\n", + "\n", + "Uncomment the following line to install [geemap](https://geemap.org) if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install -U geemap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import calendar\n", + "import ee\n", + "import geemap\n", + "from geemap import chart" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_initialize()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## feature_by_feature\n", + "\n", + "Features are plotted along the x-axis, labeled by values of a selected property. Series are represented by adjacent columns defined by a list of property names whose values are plotted along the y-axis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ecoregions = ee.FeatureCollection(\"projects/google/charts_feature_example\")\n", + "features = ecoregions.select(\"[0-9][0-9]_tmean|label\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_to_df(features)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x_property = \"label\"\n", + "y_properties = [str(x).zfill(2) + \"_tmean\" for x in range(1, 13)]\n", + "\n", + "labels = calendar.month_abbr[1:] # a list of month labels, e.g. ['Jan', 'Feb', ...]\n", + "\n", + "colors = [\n", + " \"#604791\",\n", + " \"#1d6b99\",\n", + " \"#39a8a7\",\n", + " \"#0f8755\",\n", + " \"#76b349\",\n", + " \"#f0af07\",\n", + " \"#e37d05\",\n", + " \"#cf513e\",\n", + " \"#96356f\",\n", + " \"#724173\",\n", + " \"#9c4f97\",\n", + " \"#696969\",\n", + "]\n", + "title = \"Average Monthly Temperature by Ecoregion\"\n", + "x_label = \"Ecoregion\"\n", + "y_label = \"Temperature\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.feature_by_feature(\n", + " features,\n", + " x_property,\n", + " y_properties,\n", + " colors=colors,\n", + " labels=labels,\n", + " title=title,\n", + " x_label=x_label,\n", + " y_label=y_label,\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/MZa99Vf.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## feature.by_property" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ecoregions = ee.FeatureCollection(\"projects/google/charts_feature_example\")\n", + "features = ecoregions.select(\"[0-9][0-9]_ppt|label\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_to_df(features)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "keys = [str(x).zfill(2) + \"_ppt\" for x in range(1, 13)]\n", + "values = calendar.month_abbr[1:] # a list of month labels, e.g. ['Jan', 'Feb', ...]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x_properties = dict(zip(keys, values))\n", + "series_property = \"label\"\n", + "title = \"Average Ecoregion Precipitation by Month\"\n", + "colors = [\"#f0af07\", \"#0f8755\", \"#76b349\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.feature_by_property(\n", + " features,\n", + " x_properties,\n", + " series_property,\n", + " title=title,\n", + " colors=colors,\n", + " x_label=\"Month\",\n", + " y_label=\"Precipitation (mm)\",\n", + " legend_location=\"top-left\",\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/6RhuUc7.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## feature_groups" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ecoregions = ee.FeatureCollection(\"projects/google/charts_feature_example\")\n", + "features = ecoregions.select(\"[0-9][0-9]_ppt|label\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "features = ee.FeatureCollection(\"projects/google/charts_feature_example\")\n", + "x_property = \"label\"\n", + "y_property = \"01_tmean\"\n", + "series_property = \"warm\"\n", + "title = \"Average January Temperature by Ecoregion\"\n", + "colors = [\"#cf513e\", \"#1d6b99\"]\n", + "labels = [\"Warm\", \"Cold\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "chart.feature_groups(\n", + " features,\n", + " x_property,\n", + " y_property,\n", + " series_property,\n", + " title=title,\n", + " colors=colors,\n", + " x_label=\"Ecoregion\",\n", + " y_label=\"January Temperature (°C)\",\n", + " legend_location=\"top-right\",\n", + " labels=labels,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/YFZlJtc.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## feature_histogram" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "source = ee.ImageCollection(\"OREGONSTATE/PRISM/Norm91m\").toBands()\n", + "region = ee.Geometry.Rectangle(-123.41, 40.43, -116.38, 45.14)\n", + "features = source.sample(region, 5000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_to_df(features.limit(5).select([\"07_ppt\"]))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "property = \"07_ppt\"\n", + "title = \"July Precipitation Distribution for NW USA\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.feature_histogram(\n", + " features,\n", + " property,\n", + " max_buckets=None,\n", + " title=title,\n", + " x_label=\"Precipitation (mm)\",\n", + " y_label=\"Pixel Count\",\n", + " colors=[\"#1d6b99\"],\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/ErIp7Oy.png)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "geo", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/notebooks/145_chart_image.ipynb b/docs/notebooks/145_chart_image.ipynb new file mode 100644 index 0000000000..09fa2b15eb --- /dev/null +++ b/docs/notebooks/145_chart_image.ipynb @@ -0,0 +1,304 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\"Open\n", + "\n", + "**Image Charts**\n", + "\n", + "Uncomment the following line to install [geemap](https://geemap.org) if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install -U geemap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import calendar\n", + "import ee\n", + "import geemap\n", + "from geemap import chart" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_initialize()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## image_by_region" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ecoregions = ee.FeatureCollection(\"projects/google/charts_feature_example\")\n", + "image = (\n", + " ee.ImageCollection(\"OREGONSTATE/PRISM/Norm91m\").toBands().select(\"[0-9][0-9]_tmean\")\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "labels = calendar.month_abbr[1:] # a list of month labels, e.g. ['Jan', 'Feb', ...]\n", + "title = \"Average Monthly Temperature by Ecoregion\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.image_by_region(\n", + " image,\n", + " ecoregions,\n", + " reducer=\"mean\",\n", + " scale=500,\n", + " x_property=\"label\",\n", + " title=title,\n", + " x_label=\"Ecoregion\",\n", + " y_label=\"Temperature\",\n", + " labels=labels,\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/y4rp3dK.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## image_regions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ecoregions = ee.FeatureCollection(\"projects/google/charts_feature_example\")\n", + "image = (\n", + " ee.ImageCollection(\"OREGONSTATE/PRISM/Norm91m\").toBands().select(\"[0-9][0-9]_ppt\")\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "keys = [str(x).zfill(2) + \"_ppt\" for x in range(1, 13)]\n", + "values = calendar.month_abbr[1:] # a list of month labels, e.g. ['Jan', 'Feb', ...]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x_properties = dict(zip(keys, values))\n", + "title = \"Average Ecoregion Precipitation by Month\"\n", + "colors = [\"#f0af07\", \"#0f8755\", \"#76b349\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.image_regions(\n", + " image,\n", + " ecoregions,\n", + " reducer=\"mean\",\n", + " scale=500,\n", + " series_property=\"label\",\n", + " x_labels=x_properties,\n", + " title=title,\n", + " colors=colors,\n", + " x_label=\"Month\",\n", + " y_label=\"Precipitation (mm)\",\n", + " legend_location=\"top-left\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/5WJVCNY.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## image_by_class" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ecoregions = ee.FeatureCollection(\"projects/google/charts_feature_example\")\n", + "\n", + "image = (\n", + " ee.ImageCollection(\"MODIS/061/MOD09A1\")\n", + " .filter(ee.Filter.date(\"2018-06-01\", \"2018-09-01\"))\n", + " .select(\"sur_refl_b0[0-7]\")\n", + " .mean()\n", + " .select([2, 3, 0, 1, 4, 5, 6])\n", + ")\n", + "\n", + "wavelengths = [469, 555, 655, 858, 1240, 1640, 2130]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.image_by_class(\n", + " image,\n", + " class_band=\"label\",\n", + " region=ecoregions,\n", + " reducer=\"MEAN\",\n", + " scale=500,\n", + " x_labels=wavelengths,\n", + " title=\"Ecoregion Spectral Signatures\",\n", + " x_label=\"Wavelength (nm)\",\n", + " y_label=\"Reflectance (x1e4)\",\n", + " colors=[\"#f0af07\", \"#0f8755\", \"#76b349\"],\n", + " legend_location=\"top-left\",\n", + " interpolation=\"basis\",\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/XqYHvBV.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## image_histogram" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "image = (\n", + " ee.ImageCollection(\"MODIS/061/MOD09A1\")\n", + " .filter(ee.Filter.date(\"2018-06-01\", \"2018-09-01\"))\n", + " .select([\"sur_refl_b01\", \"sur_refl_b02\", \"sur_refl_b06\"])\n", + " .mean()\n", + ")\n", + "\n", + "region = ee.Geometry.Rectangle([-112.60, 40.60, -111.18, 41.22])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.image_histogram(\n", + " image,\n", + " region,\n", + " scale=500,\n", + " max_buckets=200,\n", + " min_bucket_width=1.0,\n", + " max_raw=1000,\n", + " max_pixels=int(1e6),\n", + " title=\"MODIS SR Reflectance Histogram\",\n", + " labels=[\"Red\", \"NIR\", \"SWIR\"],\n", + " colors=[\"#cf513e\", \"#1d6b99\", \"#f0af07\"],\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/mY4yoYH.png)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "geo", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/notebooks/146_chart_image_collection.ipynb b/docs/notebooks/146_chart_image_collection.ipynb new file mode 100644 index 0000000000..0161bd2461 --- /dev/null +++ b/docs/notebooks/146_chart_image_collection.ipynb @@ -0,0 +1,419 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\"Open\n", + "\n", + "**ImageCollection Charts**\n", + "\n", + "Uncomment the following line to install [geemap](https://geemap.org) if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install -U geemap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ee\n", + "import geemap\n", + "from geemap import chart" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_initialize()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## image_series" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define the forest feature collection.\n", + "forest = ee.FeatureCollection(\"projects/google/charts_feature_example\").filter(\n", + " ee.Filter.eq(\"label\", \"Forest\")\n", + ")\n", + "\n", + "# Load MODIS vegetation indices data and subset a decade of images.\n", + "veg_indices = (\n", + " ee.ImageCollection(\"MODIS/061/MOD13A1\")\n", + " .filter(ee.Filter.date(\"2010-01-01\", \"2020-01-01\"))\n", + " .select([\"NDVI\", \"EVI\"])\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "title = \"Average Vegetation Index Value by Date for Forest\"\n", + "x_label = \"Year\"\n", + "y_label = \"Vegetation index (x1e4)\"\n", + "colors = [\"#e37d05\", \"#1d6b99\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.image_series(\n", + " veg_indices,\n", + " region=forest,\n", + " reducer=ee.Reducer.mean(),\n", + " scale=500,\n", + " x_property=\"system:time_start\",\n", + " chart_type=\"LineChart\",\n", + " x_cols=\"date\",\n", + " y_cols=[\"NDVI\", \"EVI\"],\n", + " colors=colors,\n", + " title=title,\n", + " x_label=x_label,\n", + " y_label=y_label,\n", + " legend_location=\"right\",\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/r9zSJh6.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## image_series_by_region" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import the example feature collection.\n", + "ecoregions = ee.FeatureCollection(\"projects/google/charts_feature_example\")\n", + "\n", + "# Load MODIS vegetation indices data and subset a decade of images.\n", + "veg_indices = (\n", + " ee.ImageCollection(\"MODIS/061/MOD13A1\")\n", + " .filter(ee.Filter.date(\"2010-01-01\", \"2020-01-01\"))\n", + " .select([\"NDVI\"])\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "title = \"Average NDVI Value by Date\"\n", + "x_label = \"Date\"\n", + "y_label = \"NDVI (x1e4)\"\n", + "x_cols = \"index\"\n", + "y_cols = [\"Desert\", \"Forest\", \"Grassland\"]\n", + "colors = [\"#f0af07\", \"#0f8755\", \"#76b349\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.image_series_by_region(\n", + " veg_indices,\n", + " regions=ecoregions,\n", + " reducer=ee.Reducer.mean(),\n", + " band=\"NDVI\",\n", + " scale=500,\n", + " x_property=\"system:time_start\",\n", + " series_property=\"label\",\n", + " chart_type=\"LineChart\",\n", + " x_cols=x_cols,\n", + " y_cols=y_cols,\n", + " title=title,\n", + " x_label=x_label,\n", + " y_label=y_label,\n", + " colors=colors,\n", + " stroke_width=3,\n", + " legend_location=\"bottom-left\",\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/rnILSfI.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## image_doy_series" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import the example feature collection and subset the grassland feature.\n", + "grassland = ee.FeatureCollection(\"projects/google/charts_feature_example\").filter(\n", + " ee.Filter.eq(\"label\", \"Grassland\")\n", + ")\n", + "\n", + "# Load MODIS vegetation indices data and subset a decade of images.\n", + "veg_indices = (\n", + " ee.ImageCollection(\"MODIS/061/MOD13A1\")\n", + " .filter(ee.Filter.date(\"2010-01-01\", \"2020-01-01\"))\n", + " .select([\"NDVI\", \"EVI\"])\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "title = \"Average Vegetation Index Value by Day of Year for Grassland\"\n", + "x_label = \"Day of Year\"\n", + "y_label = \"Vegetation Index (x1e4)\"\n", + "colors = [\"#f0af07\", \"#0f8755\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.image_doy_series(\n", + " image_collection=veg_indices,\n", + " region=grassland,\n", + " scale=500,\n", + " chart_type=\"LineChart\",\n", + " title=title,\n", + " x_label=x_label,\n", + " y_label=y_label,\n", + " colors=colors,\n", + " stroke_width=5,\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/F0z088e.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## image_doy_series_by_year" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import the example feature collection and subset the grassland feature.\n", + "grassland = ee.FeatureCollection(\"projects/google/charts_feature_example\").filter(\n", + " ee.Filter.eq(\"label\", \"Grassland\")\n", + ")\n", + "\n", + "# Load MODIS vegetation indices data and subset years 2012 and 2019.\n", + "veg_indices = (\n", + " ee.ImageCollection(\"MODIS/061/MOD13A1\")\n", + " .filter(\n", + " ee.Filter.Or(\n", + " ee.Filter.date(\"2012-01-01\", \"2013-01-01\"),\n", + " ee.Filter.date(\"2019-01-01\", \"2020-01-01\"),\n", + " )\n", + " )\n", + " .select([\"NDVI\", \"EVI\"])\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "title = \"Average Vegetation Index Value by Day of Year for Grassland\"\n", + "x_label = \"Day of Year\"\n", + "y_label = \"Vegetation Index (x1e4)\"\n", + "colors = [\"#e37d05\", \"#1d6b99\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.doy_series_by_year(\n", + " veg_indices,\n", + " band_name=\"NDVI\",\n", + " region=grassland,\n", + " scale=500,\n", + " chart_type=\"LineChart\",\n", + " colors=colors,\n", + " title=title,\n", + " x_label=x_label,\n", + " y_label=y_label,\n", + " stroke_width=5,\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/ui6zpbl.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## image_doy_series_by_region" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import the example feature collection and subset the grassland feature.\n", + "ecoregions = ee.FeatureCollection(\"projects/google/charts_feature_example\")\n", + "\n", + "# Load MODIS vegetation indices data and subset a decade of images.\n", + "veg_indices = (\n", + " ee.ImageCollection(\"MODIS/061/MOD13A1\")\n", + " .filter(ee.Filter.date(\"2010-01-01\", \"2020-01-01\"))\n", + " .select([\"NDVI\"])\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "title = \"Average Vegetation Index Value by Day of Year for Grassland\"\n", + "x_label = \"Day of Year\"\n", + "y_label = \"Vegetation Index (x1e4)\"\n", + "colors = [\"#f0af07\", \"#0f8755\", \"#76b349\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.image_doy_series_by_region(\n", + " veg_indices,\n", + " \"NDVI\",\n", + " ecoregions,\n", + " region_reducer=\"mean\",\n", + " scale=500,\n", + " year_reducer=ee.Reducer.mean(),\n", + " start_day=1,\n", + " end_day=366,\n", + " series_property=\"label\",\n", + " stroke_width=5,\n", + " chart_type=\"LineChart\",\n", + " title=title,\n", + " x_label=x_label,\n", + " y_label=y_label,\n", + " colors=colors,\n", + " legend_location=\"right\",\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/eGqGoRs.png)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "geo", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/notebooks/147_chart_array_list.ipynb b/docs/notebooks/147_chart_array_list.ipynb new file mode 100644 index 0000000000..5b9024c0ea --- /dev/null +++ b/docs/notebooks/147_chart_array_list.ipynb @@ -0,0 +1,377 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\"Open\n", + "\n", + "**Array and List Charts**\n", + "\n", + "Uncomment the following line to install [geemap](https://geemap.org) if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install -U geemap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ee\n", + "import geemap\n", + "from geemap import chart" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_initialize()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Scatter plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import the example feature collection and subset the forest feature.\n", + "forest = ee.FeatureCollection(\"projects/google/charts_feature_example\").filter(\n", + " ee.Filter.eq(\"label\", \"Forest\")\n", + ")\n", + "\n", + "# Define a MODIS surface reflectance composite.\n", + "modisSr = (\n", + " ee.ImageCollection(\"MODIS/061/MOD09A1\")\n", + " .filter(ee.Filter.date(\"2018-06-01\", \"2018-09-01\"))\n", + " .select(\"sur_refl_b0[0-7]\")\n", + " .mean()\n", + ")\n", + "\n", + "# Reduce MODIS reflectance bands by forest region; get a dictionary with\n", + "# band names as keys, pixel values as lists.\n", + "pixel_vals = modisSr.reduceRegion(\n", + " **{\"reducer\": ee.Reducer.toList(), \"geometry\": forest.geometry(), \"scale\": 2000}\n", + ")\n", + "\n", + "# Convert NIR and SWIR value lists to an array to be plotted along the y-axis.\n", + "y_values = pixel_vals.toArray([\"sur_refl_b02\", \"sur_refl_b06\"])\n", + "\n", + "\n", + "# Get the red band value list; to be plotted along the x-axis.\n", + "x_values = ee.List(pixel_vals.get(\"sur_refl_b01\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "title = \"Relationship Among Spectral Bands for Forest Pixels\"\n", + "colors = [\"rgba(29,107,153,0.4)\", \"rgba(207,81,62,0.4)\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.array_values(\n", + " y_values,\n", + " axis=1,\n", + " x_labels=x_values,\n", + " series_names=[\"NIR\", \"SWIR\"],\n", + " chart_type=\"ScatterChart\",\n", + " colors=colors,\n", + " title=title,\n", + " x_label=\"Red reflectance (x1e4)\",\n", + " y_label=\"NIR & SWIR reflectance (x1e4)\",\n", + " default_size=15,\n", + " xlim=(0, 800),\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/zkPlZIO.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x = ee.List(pixel_vals.get(\"sur_refl_b01\"))\n", + "y = ee.List(pixel_vals.get(\"sur_refl_b06\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.array_values(\n", + " y,\n", + " x_labels=x,\n", + " series_names=[\"SWIR\"],\n", + " chart_type=\"ScatterChart\",\n", + " colors=[\"rgba(207,81,62,0.4)\"],\n", + " title=title,\n", + " x_label=\"Red reflectance (x1e4)\",\n", + " y_label=\"SWIR reflectance (x1e4)\",\n", + " default_size=15,\n", + " xlim=(0, 800),\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/WHUHjH6.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ## Transect line plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define a line across the Olympic Peninsula, USA.\n", + "transect = ee.Geometry.LineString([[-122.8, 47.8], [-124.5, 47.8]])\n", + "\n", + "# Define a pixel coordinate image.\n", + "lat_lon_img = ee.Image.pixelLonLat()\n", + "\n", + "# Import a digital surface model and add latitude and longitude bands.\n", + "elev_img = ee.Image(\"USGS/SRTMGL1_003\").select(\"elevation\").addBands(lat_lon_img)\n", + "\n", + "# Reduce elevation and coordinate bands by transect line; get a dictionary with\n", + "# band names as keys, pixel values as lists.\n", + "elev_transect = elev_img.reduceRegion(\n", + " reducer=ee.Reducer.toList(),\n", + " geometry=transect,\n", + " scale=1000,\n", + ")\n", + "\n", + "# Get longitude and elevation value lists from the reduction dictionary.\n", + "lon = ee.List(elev_transect.get(\"longitude\"))\n", + "elev = ee.List(elev_transect.get(\"elevation\"))\n", + "\n", + "# Sort the longitude and elevation values by ascending longitude.\n", + "lon_sort = lon.sort(lon)\n", + "elev_sort = elev.sort(lon)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.array_values(\n", + " elev_sort,\n", + " x_labels=lon_sort,\n", + " series_names=[\"Elevation\"],\n", + " chart_type=\"AreaChart\",\n", + " colors=[\"#1d6b99\"],\n", + " title=\"Elevation Profile Across Longitude\",\n", + " x_label=\"Longitude\",\n", + " y_label=\"Elevation (m)\",\n", + " stroke_width=5,\n", + " fill=\"bottom\",\n", + " fill_opacities=[0.4],\n", + " ylim=(0, 2500),\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/k3XRita.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Metadata scatter plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import a Landsat 8 collection and filter to a single path/row.\n", + "col = ee.ImageCollection(\"LANDSAT/LC08/C02/T1_L2\").filter(\n", + " ee.Filter.expression(\"WRS_PATH == 45 && WRS_ROW == 30\")\n", + ")\n", + "\n", + "# Reduce image properties to a series of lists; one for each selected property.\n", + "propVals = col.reduceColumns(\n", + " reducer=ee.Reducer.toList().repeat(2),\n", + " selectors=[\"CLOUD_COVER\", \"GEOMETRIC_RMSE_MODEL\"],\n", + ").get(\"list\")\n", + "\n", + "# Get selected image property value lists; to be plotted along x and y axes.\n", + "x = ee.List(ee.List(propVals).get(0))\n", + "y = ee.List(ee.List(propVals).get(1))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "colors = [geemap.hex_to_rgba(\"#96356f\", 0.4)]\n", + "print(colors)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.array_values(\n", + " y,\n", + " x_labels=x,\n", + " series_names=[\"RMSE\"],\n", + " chart_type=\"ScatterChart\",\n", + " colors=colors,\n", + " title=\"Landsat 8 Image Collection Metadata (045030)\",\n", + " x_label=\"Cloud cover (%)\",\n", + " y_label=\"Geometric RMSE (m)\",\n", + " default_size=15,\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/3COY3xd.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Mapped function scatter & line plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "start = -2 * math.pi\n", + "end = 2 * math.pi\n", + "points = ee.List.sequence(start, end, None, 50)\n", + "\n", + "\n", + "def sin_func(val):\n", + " return ee.Number(val).sin()\n", + "\n", + "\n", + "values = points.map(sin_func)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.array_values(\n", + " values,\n", + " points,\n", + " chart_type=\"LineChart\",\n", + " colors=[\"#39a8a7\"],\n", + " title=\"Sine Function\",\n", + " x_label=\"radians\",\n", + " y_label=\"sin(x)\",\n", + " marker=\"circle\",\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/7qcxvey.png)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "geo", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/notebooks/148_chart_data_table.ipynb b/docs/notebooks/148_chart_data_table.ipynb new file mode 100644 index 0000000000..2f2a34dc88 --- /dev/null +++ b/docs/notebooks/148_chart_data_table.ipynb @@ -0,0 +1,363 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\"Open\n", + "\n", + "**DataTable Charts**\n", + "\n", + "Uncomment the following line to install [geemap](https://geemap.org) if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install -U geemap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ee\n", + "import geemap\n", + "from geemap import chart\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_initialize()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Manual DataTable chart" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = {\n", + " \"State\": [\"CA\", \"NY\", \"IL\", \"MI\", \"OR\"],\n", + " \"Population\": [37253956, 19378102, 12830632, 9883640, 3831074],\n", + "}\n", + "\n", + "df = pd.DataFrame(data)\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.Chart(\n", + " df,\n", + " x_cols=[\"State\"],\n", + " y_cols=[\"Population\"],\n", + " chart_type=\"ColumnChart\",\n", + " colors=[\"#1d6b99\"],\n", + " title=\"State Population (US census, 2010)\",\n", + " x_label=\"State\",\n", + " y_label=\"Population\",\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/vuxNmuh.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Computed DataTable chart" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import the example feature collection and subset the forest feature.\n", + "forest = ee.FeatureCollection(\"projects/google/charts_feature_example\").filter(\n", + " ee.Filter.eq(\"label\", \"Forest\")\n", + ")\n", + "\n", + "# Load MODIS vegetation indices data and subset a decade of images.\n", + "veg_indices = (\n", + " ee.ImageCollection(\"MODIS/061/MOD13A1\")\n", + " .filter(ee.Filter.date(\"2010-01-01\", \"2020-01-01\"))\n", + " .select([\"NDVI\", \"EVI\"])\n", + ")\n", + "\n", + "# Build a feature collection where each feature has a property that represents\n", + "# a DataFrame row.\n", + "\n", + "\n", + "def aggregate(img):\n", + " # Reduce the image to the mean of pixels intersecting the forest ecoregion.\n", + " stat = img.reduceRegion(\n", + " **{\"reducer\": ee.Reducer.mean(), \"geometry\": forest, \"scale\": 500}\n", + " )\n", + "\n", + " # Extract the reduction results along with the image date.\n", + " date = geemap.image_date(img)\n", + " evi = stat.get(\"EVI\")\n", + " ndvi = stat.get(\"NDVI\")\n", + "\n", + " # Make a list of observation attributes to define a row in the DataTable.\n", + " row = ee.List([date, evi, ndvi])\n", + "\n", + " # Return the row as a property of an ee.Feature.\n", + " return ee.Feature(None, {\"row\": row})\n", + "\n", + "\n", + "reduction_table = veg_indices.map(aggregate)\n", + "\n", + "# Aggregate the 'row' property from all features in the new feature collection\n", + "# to make a server-side 2-D list (DataTable).\n", + "data_table_server = reduction_table.aggregate_array(\"row\")\n", + "\n", + "# Define column names and properties for the DataTable. The order should\n", + "# correspond to the order in the construction of the 'row' property above.\n", + "column_header = ee.List([[\"Date\", \"EVI\", \"NDVI\"]])\n", + "\n", + "# Concatenate the column header to the table.\n", + "data_table_server = column_header.cat(data_table_server)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data_table = chart.DataTable(data_table_server, date_column=\"Date\")\n", + "data_table.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.Chart(\n", + " data_table,\n", + " chart_type=\"LineChart\",\n", + " x_cols=\"Date\",\n", + " y_cols=[\"EVI\", \"NDVI\"],\n", + " colors=[\"#e37d05\", \"#1d6b99\"],\n", + " title=\"Average Vegetation Index Value by Date for Forest\",\n", + " x_label=\"Date\",\n", + " y_label=\"Vegetation index (x1e4)\",\n", + " stroke_width=3,\n", + " legend_location=\"right\",\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/PWei7QC.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Interval chart" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define a point to extract an NDVI time series for.\n", + "geometry = ee.Geometry.Point([-121.679, 36.479])\n", + "\n", + "# Define a band of interest (NDVI), import the MODIS vegetation index dataset,\n", + "# and select the band.\n", + "band = \"NDVI\"\n", + "ndvi_col = ee.ImageCollection(\"MODIS/061/MOD13Q1\").select(band)\n", + "\n", + "# Map over the collection to add a day of year (doy) property to each image.\n", + "\n", + "\n", + "def set_doy(img):\n", + " doy = ee.Date(img.get(\"system:time_start\")).getRelative(\"day\", \"year\")\n", + " # Add 8 to day of year number so that the doy label represents the middle of\n", + " # the 16-day MODIS NDVI composite.\n", + " return img.set(\"doy\", ee.Number(doy).add(8))\n", + "\n", + "\n", + "ndvi_col = ndvi_col.map(set_doy)\n", + "\n", + "# Join all coincident day of year observations into a set of image collections.\n", + "distinct_doy = ndvi_col.filterDate(\"2013-01-01\", \"2014-01-01\")\n", + "filter = ee.Filter.equals(**{\"leftField\": \"doy\", \"rightField\": \"doy\"})\n", + "join = ee.Join.saveAll(\"doy_matches\")\n", + "join_col = ee.ImageCollection(join.apply(distinct_doy, ndvi_col, filter))\n", + "\n", + "# Calculate the absolute range, interquartile range, and median for the set\n", + "# of images composing each coincident doy observation group. The result is\n", + "# an image collection with an image representative per unique doy observation\n", + "# with bands that describe the 0, 25, 50, 75, 100 percentiles for the set of\n", + "# coincident doy images.\n", + "\n", + "\n", + "def cal_percentiles(img):\n", + " doyCol = ee.ImageCollection.fromImages(img.get(\"doy_matches\"))\n", + "\n", + " return doyCol.reduce(\n", + " ee.Reducer.percentile([0, 25, 50, 75, 100], [\"p0\", \"p25\", \"p50\", \"p75\", \"p100\"])\n", + " ).set({\"doy\": img.get(\"doy\")})\n", + "\n", + "\n", + "comp = ee.ImageCollection(join_col.map(cal_percentiles))\n", + "\n", + "# Extract the inter-annual NDVI doy percentile statistics for the\n", + "# point of interest per unique doy representative. The result is\n", + "# is a feature collection where each feature is a doy representative that\n", + "# contains a property (row) describing the respective inter-annual NDVI\n", + "# variance, formatted as a list of values.\n", + "\n", + "\n", + "def order_percentiles(img):\n", + " stats = ee.Dictionary(\n", + " img.reduceRegion(\n", + " **{\"reducer\": ee.Reducer.first(), \"geometry\": geometry, \"scale\": 250}\n", + " )\n", + " )\n", + "\n", + " # Order the percentile reduction elements according to how you want columns\n", + " # in the DataTable arranged (x-axis values need to be first).\n", + " row = ee.List(\n", + " [\n", + " img.get(\"doy\"),\n", + " stats.get(band + \"_p50\"),\n", + " stats.get(band + \"_p0\"),\n", + " stats.get(band + \"_p25\"),\n", + " stats.get(band + \"_p75\"),\n", + " stats.get(band + \"_p100\"),\n", + " ]\n", + " )\n", + "\n", + " # Return the row as a property of an ee.Feature.\n", + " return ee.Feature(None, {\"row\": row})\n", + "\n", + "\n", + "reduction_table = comp.map(order_percentiles)\n", + "\n", + "# Aggregate the 'row' properties to make a server-side 2-D array (DataTable).\n", + "data_table_server = reduction_table.aggregate_array(\"row\")\n", + "\n", + "# Define column names and properties for the DataTable. The order should\n", + "# correspond to the order in the construction of the 'row' property above.\n", + "column_header = ee.List([[\"DOY\", \"median\", \"p0\", \"p25\", \"p75\", \"p100\"]])\n", + "\n", + "# Concatenate the column header to the table.\n", + "data_table_server = column_header.cat(data_table_server)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = chart.DataTable(data_table_server)\n", + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = chart.Chart(\n", + " df,\n", + " chart_type=\"IntervalChart\",\n", + " x_cols=\"DOY\",\n", + " y_cols=[\"p0\", \"p25\", \"median\", \"p75\", \"p100\"],\n", + " title=\"Annual NDVI Time Series with Inter-Annual Variance\",\n", + " x_label=\"Day of Year\",\n", + " y_label=\"Vegetation index (x1e4)\",\n", + " stroke_width=1,\n", + " fill=\"between\",\n", + " fill_colors=[\"#b6d1c6\", \"#83b191\", \"#83b191\", \"#b6d1c6\"],\n", + " fill_opacities=[0.6] * 4,\n", + " labels=[\"p0\", \"p25\", \"median\", \"p75\", \"p100\"],\n", + " display_legend=True,\n", + " legend_location=\"top-right\",\n", + " ylim=(0, 10000),\n", + ")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/i8ZrGPR.png)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "geo", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/notebooks/63_charts.ipynb b/docs/notebooks/63_charts.ipynb index bb392ee003..e532da30df 100644 --- a/docs/notebooks/63_charts.ipynb +++ b/docs/notebooks/63_charts.ipynb @@ -148,7 +148,7 @@ "metadata": {}, "outputs": [], "source": [ - "chart.feature_byFeature(features, xProperty, yProperties, **options)" + "chart.feature_by_feature(features, xProperty, yProperties, **options)" ] }, { @@ -311,7 +311,7 @@ "metadata": {}, "outputs": [], "source": [ - "chart.feature_byProperty(features, xProperties, seriesProperty, **options)" + "chart.feature_by_property(features, xProperties, seriesProperty, **options)" ] }, { diff --git a/examples/notebooks/63_charts.ipynb b/examples/notebooks/63_charts.ipynb index bb392ee003..e532da30df 100644 --- a/examples/notebooks/63_charts.ipynb +++ b/examples/notebooks/63_charts.ipynb @@ -148,7 +148,7 @@ "metadata": {}, "outputs": [], "source": [ - "chart.feature_byFeature(features, xProperty, yProperties, **options)" + "chart.feature_by_feature(features, xProperty, yProperties, **options)" ] }, { @@ -311,7 +311,7 @@ "metadata": {}, "outputs": [], "source": [ - "chart.feature_byProperty(features, xProperties, seriesProperty, **options)" + "chart.feature_by_property(features, xProperties, seriesProperty, **options)" ] }, { diff --git a/geemap/chart.py b/geemap/chart.py index 1d2e8073c3..df005e82e3 100644 --- a/geemap/chart.py +++ b/geemap/chart.py @@ -1,4 +1,4 @@ -"""Module for creating charts for Earth Engine data. +"""Module for creating charts from Earth Engine data. """ # *******************************************************************************# @@ -9,18 +9,518 @@ import ee import pandas as pd import numpy as np +import bqplot as bq +import ipywidgets as widgets from bqplot import Tooltip from bqplot import pyplot as plt +from IPython.display import display +from .common import ee_to_df, zonal_stats, image_dates, hex_to_rgba -from .common import ee_to_df, zonal_stats +from typing import List, Optional, Union, Dict, Any, Tuple -from typing import Union + +class DataTable(pd.DataFrame): + + def __init__( + self, + data: Union[Dict[str, List[Any]], pd.DataFrame, None] = None, + date_column: Optional[str] = None, + date_format: Optional[str] = None, + **kwargs: Any, + ) -> None: + """ + Initializes the DataTable with data. + + Args: + data (Union[Dict[str, List[Any]], pd.DataFrame, None]): The input + data. If it's a dictionary, it will be converted to a DataFrame. + date_column (Optional[str]): The date column to convert to a DataFrame. + date_format (Optional[str]): The format of the date column. + **kwargs: Additional keyword arguments to pass to the pd.DataFrame + constructor. + """ + if isinstance(data, ee.FeatureCollection): + data = ee_to_df(data) + elif isinstance(data, ee.List): + data = data.getInfo() + kwargs["columns"] = data[0] + data = data[1:] + + super().__init__(data, **kwargs) + + if date_column is not None: + self[date_column] = pd.to_datetime( + self[date_column], format=date_format, errors="coerce" + ) + + +def transpose_df( + df: pd.DataFrame, + label_col: str, + index_name: str = None, + indexes: list = None, +) -> pd.DataFrame: + """ + Transposes a pandas DataFrame and optionally sets a new index name and + custom indexes. + + Args: + df (pd.DataFrame): The DataFrame to transpose. + label_col (str): The column to set as the index before transposing. + index_name (str, optional): The name to set for the index after + transposing. Defaults to None. + indexes (list, optional): A list of custom indexes to set after + transposing. The length of this list must match the number of rows + in the transposed DataFrame. Defaults to None. + + Returns: + pd.DataFrame: The transposed DataFrame. + + Raises: + ValueError: If `label_col` is not a column in the DataFrame. + ValueError: If the length of `indexes` does not match the number of + rows in the transposed DataFrame. + """ + # Check if the specified column exists in the DataFrame + if label_col not in df.columns: + raise ValueError(f"Column '{label_col}' not found in DataFrame") + + # Set the specified column as the index + transposed_df = df.set_index(label_col).transpose() + + # Set the index name if provided + if index_name: + transposed_df.columns.name = index_name + + # Set custom indexes if provided + if indexes: + if len(indexes) != len(transposed_df.index): + raise ValueError( + "Length of custom indexes must match the number of rows in the transposed DataFrame" + ) + transposed_df.index = indexes + + return transposed_df + + +def pivot_df(df: pd.DataFrame, index: str, columns: str, values: str) -> pd.DataFrame: + """ + Pivots a DataFrame using the specified index, columns, and values. + + Args: + df (pd.DataFrame): The DataFrame to pivot. + index (str): The column to use for the index. + columns (str): The column to use for the columns. + values (str): The column to use for the values. + + Returns: + pd.DataFrame: The pivoted DataFrame. + """ + df_pivot = df.pivot(index=index, columns=columns, values=values).reset_index() + df_pivot.columns = [index] + [f"{col}" for col in df_pivot.columns[1:]] + return df_pivot + + +def array_to_df( + y_values: Union[ee.Array, ee.List, List[List[float]]], + x_values: Optional[Union[ee.Array, ee.List, List[float]]] = None, + y_labels: Optional[List[str]] = None, + x_label: str = "x", + axis: int = 1, + **kwargs: Any, +) -> pd.DataFrame: + """ + Converts arrays or lists of y-values and optional x-values into a pandas DataFrame. + + Args: + y_values (Union[ee.Array, ee.List, List[List[float]]]): The y-values to convert. + x_values (Optional[Union[ee.Array, ee.List, List[float]]]): The x-values to convert. + Defaults to None. + y_labels (Optional[List[str]]): The labels for the y-values. Defaults to None. + x_label (str): The label for the x-values. Defaults to "x". + axis (int): The axis along which to transpose the y-values if needed. Defaults to 1. + **kwargs: Additional keyword arguments to pass to the pandas DataFrame constructor. + + Returns: + pd.DataFrame: The resulting DataFrame. + """ + + if isinstance(y_values, ee.Array) or isinstance(y_values, ee.List): + y_values = y_values.getInfo() + + if isinstance(x_values, ee.Array) or isinstance(x_values, ee.List): + x_values = x_values.getInfo() + + if axis == 0: + y_values = np.transpose(y_values) + + if x_values is None: + x_values = list(range(1, len(y_values[0]) + 1)) + + data = {x_label: x_values} + + if not isinstance(y_values[0], list): + y_values = [y_values] + + if y_labels is None: + y_labels = [ + f"y{str(i+1).zfill(len(str(len(y_values))))}" for i in range(len(y_values)) + ] + + if len(y_labels) != len(y_values): + raise ValueError("The length of y_labels must match the length of y_values.") + + for i, series in enumerate(y_labels): + data[series] = y_values[i] + + df = pd.DataFrame(data, **kwargs) + return df + + +class Chart: + """ + A class to create and display various types of charts from a data table. + + Attributes: + data_table (pd.DataFrame): The data to be displayed in the charts. + chart_type (str): The type of chart to create. Supported types are + 'ScatterChart', 'LineChart', 'ColumnChart', 'BarChart', 'PieChart', + 'AreaChart', and 'Table'. + chart: The bqplot Figure object for the chart. + """ + + def __init__( + self, + data_table: Union[Dict[str, List[Any]], pd.DataFrame], + chart_type: str = "LineChart", + x_cols: Optional[List[str]] = None, + y_cols: Optional[List[str]] = None, + colors: Optional[List[str]] = None, + title: Optional[str] = None, + x_label: Optional[str] = None, + y_label: Optional[str] = None, + **kwargs: Any, + ) -> None: + """ + Initializes the Chart with data. + + Args: + data_table (Union[Dict[str, List[Any]], pd.DataFrame]): A 2-D array of data. + If it's a dictionary, it will be converted to a DataFrame. + chart_type (str): The type of chart to create. Supported types are + 'ScatterChart', 'LineChart', 'ColumnChart', 'BarChart', + 'PieChart', 'AreaChart', and 'Table'. + x_cols (Optional[List[str]]): The columns to use for the x-axis. + Defaults to the first column. + y_cols (Optional[List[str]]): The columns to use for the y-axis. + Defaults to the second column. + colors (Optional[List[str]]): The colors to use for the chart. + Defaults to a predefined list of colors. + title (Optional[str]): The title of the chart. Defaults to the + chart type. + x_label (Optional[str]): The label for the x-axis. Defaults to an + empty string. + y_label (Optional[str]): The label for the y-axis. Defaults to an + empty string. + **kwargs: Additional keyword arguments to pass to the bqplot Figure + or mark objects. For axes_options, see + https://bqplot.github.io/bqplot/api/axes + """ + self.data_table = DataTable(data_table) + self.chart_type = chart_type + self.chart = None + self.title = title + self.x_label = x_label + self.y_label = y_label + self.x_cols = x_cols + self.y_cols = y_cols + self.colors = colors + self.xlim = kwargs.pop("xlim", None) + self.ylim = kwargs.pop("ylim", None) + + if title is not None: + kwargs["title"] = title + self.figure = plt.figure(**kwargs) + + if chart_type is not None: + self.set_chart_type(chart_type, **kwargs) + + def display(self) -> None: + """ + Display the chart without toolbar. + """ + self._set_plt_options() + display(self.figure) + + def save_png(self, filepath: str = "chart.png", scale: float = 1.0) -> None: + """ + Save the chart as a PNG image. + + Args: + filepath (str): The path to save the PNG image. Defaults to 'chart.png'. + scale (float): The scale factor for the image. Defaults to 1.0. + """ + self.figure.save_png(filepath, scale=scale) + + def _ipython_display_(self) -> None: + """ + Display the chart with toolbar. + """ + self._set_plt_options() + plt.show() + + def _set_plt_options(self) -> None: + """ + Set the title and labels for the chart. + """ + if self.title is not None: + self.figure.title = self.title + if self.x_label is not None: + plt.xlabel(self.x_label) + if self.y_label is not None: + plt.ylabel(self.y_label) + if self.xlim is not None: + plt.xlim(self.xlim[0], self.xlim[1]) + if self.ylim is not None: + plt.ylim(self.ylim[0], self.ylim[1]) + + def set_chart_type( + self, + chart_type: str, + clear: bool = True, + **kwargs: Any, + ) -> None: + """ + Sets the chart type and other chart properties. + + Args: + chart_type (str): The type of chart to create. Supported types are + 'ScatterChart', 'LineChart', 'ColumnChart', 'BarChart', + 'PieChart', 'AreaChart', and 'Table'. + clear (bool): Whether to clear the current chart before setting a new one. + Defaults to True. + **kwargs: Additional keyword arguments to pass to the bqplot Figure + or mark objects. + + Returns: + Chart: The Chart instance with the chart set. + """ + if clear: + plt.clear() + self.chart_type = chart_type + x_cols = self.x_cols + y_cols = self.y_cols + colors = self.colors + + if x_cols is None: + x_cols = [self.data_table.columns[0]] + if y_cols is None: + y_cols = [self.data_table.columns[1]] + + if isinstance(x_cols, str): + x_cols = [x_cols] + + if isinstance(y_cols, str): + y_cols = [y_cols] + + if len(x_cols) == 1 and len(y_cols) > 1: + x_cols = x_cols * len(y_cols) + + if "axes_options" not in kwargs: + kwargs["axes_options"] = { + "x": {"label_offset": "30px"}, + "y": {"label_offset": "40px"}, + } + + if chart_type == "PieChart": + if colors is None: + colors = [ + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", + ] # Default pie chart colors + else: + if colors is None: + colors = [ + "blue", + "orange", + "green", + "red", + "purple", + "brown", + ] # Default colors + + if chart_type == "IntervalChart": + + x = self.data_table[x_cols[0]] + y = [self.data_table[y_col] for y_col in y_cols] + if "fill" not in kwargs: + kwargs["fill"] = "between" + + self.chart = plt.plot( + x, + y, + colors=colors, + **kwargs, + ) + else: + for i, (x_col, y_col) in enumerate(zip(x_cols, y_cols)): + color = colors[i % len(colors)] + if "display_legend" not in kwargs and len(y_cols) > 1: + kwargs["display_legend"] = True + kwargs["labels"] = [y_col] + else: + kwargs["labels"] = [y_col] + + x = self.data_table[x_col] + y = self.data_table[y_col] + + if isinstance(x, pd.Series) and ( + not pd.api.types.is_datetime64_any_dtype(x) + ): + x = x.tolist() + if isinstance(y, pd.Series) and ( + not pd.api.types.is_datetime64_any_dtype(y) + ): + y = y.tolist() + + if chart_type == "ScatterChart": + self.chart = plt.scatter( + x, + y, + colors=[color], + **kwargs, + ) + elif chart_type == "LineChart": + self.chart = plt.plot( + x, + y, + colors=[color], + **kwargs, + ) + elif chart_type == "AreaChart": + if "fill" not in kwargs: + kwargs["fill"] = "bottom" + self.chart = plt.plot( + x, + y, + colors=[color], + **kwargs, + ) + elif chart_type == "ColumnChart": + self.chart = plt.bar( + x, + y, + colors=[color], + **kwargs, + ) + elif chart_type == "BarChart": + if "orientation" not in kwargs: + kwargs["orientation"] = "horizontal" + self.chart = plt.bar( + x, + y, + colors=[color], + **kwargs, + ) + elif chart_type == "AreaChart": + if "fill" not in kwargs: + kwargs["fill"] = "bottom" + self.chart = plt.plot( + x, + y, + colors=[color], + **kwargs, + ) + elif chart_type == "PieChart": + kwargs.pop("labels", None) + self.chart = plt.pie( + sizes=y, + labels=x, + colors=colors[: len(x)], + **kwargs, + ) + elif chart_type == "Table": + output = widgets.Output(**kwargs) + with output: + display(self.data_table) + output.layout = widgets.Layout(width="50%") + display(output) + else: + self.chart = plt.plot( + x, + y, + colors=[color], + **kwargs, + ) + + self._set_plt_options() + + def get_chart_type(self) -> Optional[str]: + """ + Get the current chart type. + + Returns: + Optional[str]: The current chart type, or None if no chart type is set. + """ + return self.chart_type + + def get_data_table(self) -> DataTable: + """ + Get the DataTable used by the chart. + + Returns: + DataTable: The DataTable instance containing the chart data. + """ + return self.data_table + + def set_data_table(self, data: Union[Dict[str, List[Any]], pd.DataFrame]) -> None: + """ + Set a new DataTable for the chart. + + Args: + data (Union[Dict[str, List[Any]], pd.DataFrame]): The new data to be + used for the chart. + """ + self.data_table = DataTable(data) + + def set_options(self, **options: Any) -> None: + """ + Set additional options for the chart. + + Args: + **options: Additional options to set for the chart. + """ + for key, value in options.items(): + setattr(self.figure, key, value) class BaseChartClass: """This should include everything a chart module requires to plot figures.""" - def __init__(self, features, default_labels, name, **kwargs): + def __init__( + self, + features: Union[ee.FeatureCollection, pd.DataFrame], + default_labels: List[str], + name: str, + **kwargs: Any, + ): + """ + Initializes the BaseChartClass with the given features, labels, and name. + + Args: + features (ee.FeatureCollection | pd.DataFrame): The features to plot. + default_labels (List[str]): The default labels for the chart. + name (str): The name of the chart. + **kwargs: Additional keyword arguments to set as attributes. + """ self.ylim = None self.xlim = None self.title = "" @@ -28,14 +528,31 @@ def __init__(self, features, default_labels, name, **kwargs): self.layout_width = None self.layout_height = None self.display_legend = True - self.xlabel = None - self.ylabel = None + self.x_label = None + self.y_label = None self.labels = default_labels self.width = None self.height = None - self.colors = "black" self.name = name + if isinstance(self.labels, list) and (len(self.labels) > 1): + self.colors = [ + "#604791", + "#1d6b99", + "#39a8a7", + "#0f8755", + "#76b349", + "#f0af07", + "#e37d05", + "#cf513e", + "#96356f", + "#724173", + "#9c4f97", + "#696969", + ] + else: + self.colors = "black" + if isinstance(features, ee.FeatureCollection): self.df = ee_to_df(features) elif isinstance(features, pd.DataFrame): @@ -45,33 +562,72 @@ def __init__(self, features, default_labels, name, **kwargs): setattr(self, key, value) @classmethod - def get_data(self): + def get_data(cls) -> None: + """ + Placeholder method to get data for the chart. + """ pass @classmethod - def plot_chart(self): + def plot_chart(cls) -> None: + """ + Placeholder method to plot the chart. + """ pass - def __repr__(self): + def __repr__(self) -> str: + """ + Returns the string representation of the chart. + + Returns: + str: The name of the chart. + """ return self.name class BarChart(BaseChartClass): """Create Bar Chart. All histogram/bar charts can use this object.""" - def __init__(self, features, default_labels, name, type="grouped", **kwargs): + def __init__( + self, + features: Union[ee.FeatureCollection, pd.DataFrame], + default_labels: List[str], + name: str, + type: str = "grouped", + **kwargs: Any, + ): + """ + Initializes the BarChart with the given features, labels, name, and type. + + Args: + features (ee.FeatureCollection | pd.DataFrame): The features to plot. + default_labels (List[str]): The default labels for the chart. + name (str): The name of the chart. + type (str, optional): The type of bar chart ('grouped' or 'stacked'). + Defaults to 'grouped'. + **kwargs: Additional keyword arguments to set as attributes. + """ super().__init__(features, default_labels, name, **kwargs) - self.type = type + self.type: str = type - def generate_tooltip(self): - if (self.xlabel is not None) and (self.ylabel is not None): + def generate_tooltip(self) -> None: + """ + Generates a tooltip for the bar chart. + """ + if (self.x_label is not None) and (self.y_label is not None): self.bar_chart.tooltip = Tooltip( - fields=["x", "y"], labels=[self.xlabel, self.ylabel] + fields=["x", "y"], labels=[self.x_label, self.y_label] ) else: self.bar_chart.tooltip = Tooltip(fields=["x", "y"]) - def get_ylim(self): + def get_ylim(self) -> Tuple[float, float]: + """ + Gets the y-axis limits for the bar chart. + + Returns: + Tuple[float, float]: The minimum and maximum y-axis limits. + """ if self.ylim: ylim_min, ylim_max = self.ylim[0], self.ylim[1] else: @@ -86,7 +642,10 @@ def get_ylim(self): ylim_max = ylim_max + 0.2 * (ylim_max - ylim_min) return (ylim_min, ylim_max) - def plot_chart(self): + def plot_chart(self) -> None: + """ + Plots the bar chart. + """ fig = plt.figure( title=self.title, legend_location=self.legend_location, @@ -101,8 +660,10 @@ def plot_chart(self): self.generate_tooltip() plt.ylim(*self.get_ylim()) - plt.xlabel(self.xlabel) - plt.ylabel(self.ylabel) + if self.x_label: + plt.xlabel(self.x_label) + if self.y_label: + plt.ylabel(self.y_label) if self.width: fig.layout.width = self.width @@ -115,128 +676,288 @@ def plot_chart(self): plt.show() +class LineChart(BarChart): + """A class to define variables and get_data method for a line chart.""" + + def __init__( + self, + features: Union[ee.FeatureCollection, pd.DataFrame], + labels: List[str], + name: str = "line.chart", + **kwargs: Any, + ): + """ + Initializes the LineChart with the given features, labels, and name. + + Args: + features (ee.FeatureCollection | pd.DataFrame): The features to plot. + labels (List[str]): The labels for the chart. + name (str, optional): The name of the chart. Defaults to 'line.chart'. + **kwargs: Additional keyword arguments to set as attributes. + """ + super().__init__(features, labels, name, **kwargs) + + def plot_chart(self) -> None: + """ + Plots the line chart. + """ + fig = plt.figure( + title=self.title, + legend_location=self.legend_location, + ) + + self.line_chart = plt.plot( + self.x_data, + self.y_data, + label=self.labels, + ) + + self.generate_tooltip() + plt.ylim(*self.get_ylim()) + if self.x_label: + plt.xlabel(self.x_label) + if self.y_label: + plt.ylabel(self.y_label) + + if self.width: + fig.layout.width = self.width + if self.height: + fig.layout.height = self.height + + plt.show() + + class Feature_ByFeature(BarChart): - """A object to define variables and get_data method.""" + """An object to define variables and get_data method for features by feature.""" def __init__( - self, features, xProperty, yProperties, name="feature.byFeature", **kwargs + self, + features: Union[ee.FeatureCollection, pd.DataFrame], + x_property: str, + y_properties: List[str], + name: str = "feature.byFeature", + **kwargs: Any, ): - default_labels = yProperties + """ + Initializes the Feature_ByFeature with the given features, x_property, + y_properties, and name. + + Args: + features (ee.FeatureCollection | pd.DataFrame): The features to plot. + x_property (str): The property to use for the x-axis. + y_properties (List[str]): The properties to use for the y-axis. + name (str, optional): The name of the chart. Defaults to + 'feature.byFeature'. + **kwargs: Additional keyword arguments to set as attributes. + """ + default_labels = y_properties super().__init__(features, default_labels, name, **kwargs) - self.x_data, self.y_data = self.get_data(xProperty, yProperties) - - def get_data(self, xProperty, yProperties): - x_data = list(self.df[xProperty]) - y_data = list(self.df[yProperties].values.T) + self.x_data, self.y_data = self.get_data(x_property, y_properties) + + def get_data( + self, x_property: str, y_properties: List[str] + ) -> Tuple[List[Any], List[Any]]: + """ + Gets the data for the chart. + + Args: + x_property (str): The property to use for the x-axis. + y_properties (List[str]): The properties to use for the y-axis. + + Returns: + Tuple[List[Any], List[Any]]: The x and y data for the chart. + """ + x_data = list(self.df[x_property]) + y_data = list(self.df[y_properties].values.T) return x_data, y_data class Feature_ByProperty(BarChart): - """A object to define variables and get_data method.""" + """An object to define variables and get_data method for features by property.""" def __init__( - self, features, xProperties, seriesProperty, name="feature.byProperty", **kwargs + self, + features: Union[ee.FeatureCollection, pd.DataFrame], + x_properties: Union[List[str], Dict[str, str]], + series_property: str, + name: str = "feature.byProperty", + **kwargs: Any, ): + """ + Initializes the Feature_ByProperty with the given features, x_properties, + series_property, and name. + + Args: + features (ee.FeatureCollection | pd.DataFrame): The features to plot. + x_properties (List[str] | Dict[str, str]): The properties to use for + the x-axis. + series_property (str): The property to use for labeling the series. + name (str, optional): The name of the chart. Defaults to + 'feature.byProperty'. + **kwargs: Additional keyword arguments to set as attributes. + + Raises: + Exception: If 'labels' is in kwargs. + """ default_labels = None super().__init__(features, default_labels, name, **kwargs) if "labels" in kwargs: raise Exception("Please remove labels in kwargs and try again.") - self.labels = list(self.df[seriesProperty]) - self.x_data, self.y_data = self.get_data(xProperties) - - def get_data(self, xProperties): - if isinstance(xProperties, list): - x_data = xProperties - y_data = self.df[xProperties].values - elif isinstance(xProperties, dict): - x_data = list(xProperties.values()) - y_data = self.df[list(xProperties.keys())].values + self.labels = list(self.df[series_property]) + self.x_data, self.y_data = self.get_data(x_properties) + + def get_data( + self, x_properties: Union[List[str], Dict[str, str]] + ) -> Tuple[List[Any], List[Any]]: + """ + Gets the data for the chart. + + Args: + x_properties (List[str] | Dict[str, str]): The properties to use for + the x-axis. + + Returns: + Tuple[List[Any], List[Any]]: The x and y data for the chart. + + Raises: + Exception: If x_properties is not a list or dictionary. + """ + if isinstance(x_properties, list): + x_data = x_properties + y_data = self.df[x_properties].values + elif isinstance(x_properties, dict): + x_data = list(x_properties.values()) + y_data = self.df[list(x_properties.keys())].values else: - raise Exception("xProperties must be a list or dictionary.") + raise Exception("x_properties must be a list or dictionary.") return x_data, y_data class Feature_Groups(BarChart): - """A object to define variables and get_data method.""" + """An object to define variables and get_data method for feature groups.""" def __init__( self, - features, - xProperty, - yProperty, - seriesProperty, - name="feature.groups", - type="stacked", - **kwargs, + features: Union[ee.FeatureCollection, pd.DataFrame], + x_property: str, + y_property: str, + series_property: str, + name: str = "feature.groups", + type: str = "stacked", + **kwargs: Any, ): + """ + Initializes the Feature_Groups with the given features, x_property, + y_property, series_property, name, and type. + + Args: + features (ee.FeatureCollection | pd.DataFrame): The features to plot. + x_property (str): The property to use for the x-axis. + y_property (str): The property to use for the y-axis. + series_property (str): The property to use for labeling the series. + name (str, optional): The name of the chart. Defaults to 'feature.groups'. + type (str, optional): The type of bar chart ('grouped' or 'stacked'). + Defaults to 'stacked'. + **kwargs: Additional keyword arguments to set as attributes. + """ df = ee_to_df(features) - self.unique_series_values = df[seriesProperty].unique().tolist() + self.unique_series_values = df[series_property].unique().tolist() default_labels = [str(x) for x in self.unique_series_values] - self.yProperty = yProperty + self.yProperty = y_property super().__init__(features, default_labels, name, type, **kwargs) - self.new_column_names = self.get_column_names(seriesProperty, yProperty) - self.x_data, self.y_data = self.get_data(xProperty, self.new_column_names) + self.new_column_names = self.get_column_names(series_property, y_property) + self.x_data, self.y_data = self.get_data(x_property, self.new_column_names) + + def get_column_names(self, series_property: str, y_property: str) -> List[str]: + """ + Gets the new column names for the DataFrame. - def get_column_names(self, seriesProperty, yProperty): + Args: + series_property (str): The property to use for labeling the series. + y_property (str): The property to use for the y-axis. + + Returns: + List[str]: The new column names. + """ new_column_names = [] for value in self.unique_series_values: - sample_filter = (self.df[seriesProperty] == value).map({True: 1, False: 0}) - column_name = str(yProperty) + "_" + str(value) - self.df[column_name] = self.df[yProperty] * sample_filter + sample_filter = (self.df[series_property] == value).map({True: 1, False: 0}) + column_name = str(y_property) + "_" + str(value) + self.df[column_name] = self.df[y_property] * sample_filter new_column_names.append(column_name) return new_column_names - def get_data(self, xProperty, new_column_names): - x_data = list(self.df[xProperty]) + def get_data( + self, x_property: str, new_column_names: List[str] + ) -> Tuple[List[Any], List[Any]]: + """ + Gets the data for the chart. + + Args: + x_property (str): The property to use for the x-axis. + new_column_names (List[str]): The new column names for the y-axis. + + Returns: + Tuple[List[Any], List[Any]]: The x and y data for the chart. + """ + x_data = list(self.df[x_property]) y_data = [self.df[x] for x in new_column_names] return x_data, y_data -def feature_byFeature( - features: ee.FeatureCollection, xProperty: str, yProperties: list, **kwargs -): - """Generates a Chart from a set of features. Plots the value of one or more properties for each feature. +def feature_by_feature( + features: ee.FeatureCollection, + x_property: str, + y_properties: List[str], + **kwargs: Any, +) -> None: + """ + Generates a Chart from a set of features. Plots the value of one or more + properties for each feature. Reference: https://developers.google.com/earth-engine/guides/charts_feature#uichartfeaturebyfeature Args: features (ee.FeatureCollection): The feature collection to generate a chart from. - xProperty (str): Features labeled by xProperty. - yProperties (list): Values of yProperties. + x_property (str): Features labeled by x_property. + y_properties (List[str]): Values of y_properties. + **kwargs: Additional keyword arguments to set as attributes. Raises: Exception: Errors when creating the chart. """ bar = Feature_ByFeature( - features=features, xProperty=xProperty, yProperties=yProperties, **kwargs + features=features, x_property=x_property, y_properties=y_properties, **kwargs ) try: bar.plot_chart() - except Exception as e: raise Exception(e) -def feature_byProperty( +def feature_by_property( features: ee.FeatureCollection, - xProperties: Union[list, dict], - seriesProperty: str, + x_properties: Union[list, dict], + series_property: str, **kwargs, ): - """Generates a Chart from a set of features. Plots property values of one or more features. + """Generates a Chart from a set of features. Plots property values of one or + more features. Reference: https://developers.google.com/earth-engine/guides/charts_feature#uichartfeaturebyproperty Args: features (ee.FeatureCollection): The features to include in the chart. - xProperties (list | dict): One of (1) a list of properties to be plotted on the x-axis; or - (2) a (property, label) dictionary specifying labels for properties to be used as values on the x-axis. - seriesProperty (str): The name of the property used to label each feature in the legend. + x_properties (list | dict): One of (1) a list of properties to be + plotted on the x-axis; or (2) a (property, label) dictionary + specifying labels for properties to be used as values on the x-axis. + series_property (str): The name of the property used to label each + feature in the legend. Raises: Exception: If the provided xProperties is not a list or dict. @@ -244,8 +965,8 @@ def feature_byProperty( """ bar = Feature_ByProperty( features=features, - xProperties=xProperties, - seriesProperty=seriesProperty, + x_properties=x_properties, + series_property=series_property, **kwargs, ) @@ -256,25 +977,36 @@ def feature_byProperty( raise Exception(e) -def feature_groups(features, xProperty, yProperty, seriesProperty, **kwargs): - """Generates a Chart from a set of features. +def feature_groups( + features: ee.FeatureCollection, + x_property: str, + y_property: str, + series_property: str, + **kwargs: Any, +) -> None: + """ + Generates a Chart from a set of features. Plots the value of one property for each feature. + Reference: https://developers.google.com/earth-engine/guides/charts_feature#uichartfeaturegroups + Args: features (ee.FeatureCollection): The feature collection to make a chart from. - xProperty (str): Features labeled by xProperty. - yProperty (str): Features labeled by yProperty. - seriesProperty (str): The property used to label each feature in the legend. + x_property (str): Features labeled by xProperty. + y_property (str): Features labeled by yProperty. + series_property (str): The property used to label each feature in the legend. + **kwargs: Additional keyword arguments to set as attributes. + Raises: Exception: Errors when creating the chart. """ bar = Feature_Groups( features=features, - xProperty=xProperty, - yProperty=yProperty, - seriesProperty=seriesProperty, + x_property=x_property, + y_property=y_property, + series_property=series_property, **kwargs, ) @@ -286,8 +1018,13 @@ def feature_groups(features, xProperty, yProperty, seriesProperty, **kwargs): def feature_histogram( - features, property, maxBuckets=None, minBucketWidth=None, show=True, **kwargs -): + features: ee.FeatureCollection, + property: str, + max_buckets: Optional[int] = None, + min_bucket_width: Optional[float] = None, + show: bool = True, + **kwargs: Any, +) -> Optional[Any]: """ Generates a Chart from a set of features. Computes and plots a histogram of the given property. @@ -298,16 +1035,23 @@ def feature_histogram( https://developers.google.com/earth-engine/guides/charts_feature#uichartfeaturehistogram Args: - features (ee.FeatureCollection): The features to include in the chart. - property (str): The name of the property to generate the histogram for. - maxBuckets (int, optional): The maximum number of buckets (bins) to use when building a histogram; - will be rounded up to a power of 2. - minBucketWidth (float, optional): The minimum histogram bucket width, or null to allow any power of 2. - show (bool, optional): Whether to show the chart. If not, it will return the bqplot chart object, which can be used to retrieve data for the chart. Defaults to True. + features (ee.FeatureCollection): The features to include in the chart. + property (str): The name of the property to generate the histogram for. + max_buckets (int, optional): The maximum number of buckets (bins) to use + when building a histogram; will be rounded up to a power of 2. + min_bucket_width (float, optional): The minimum histogram bucket width, + or null to allow any power of 2. + show (bool, optional): Whether to show the chart. If not, it will return + the bqplot chart object, which can be used to retrieve data for the + chart. Defaults to True. + **kwargs: Additional keyword arguments to set as attributes. Raises: Exception: If the provided xProperties is not a list or dict. Exception: If the chart fails to create. + + Returns: + Optional[Any]: The bqplot chart object if show is False, otherwise None. """ import math @@ -344,22 +1088,22 @@ def grow_bin(bin_size, ref): data_range = max_value - min_value - if not maxBuckets: + if not max_buckets: initial_bin_size = nextPowerOf2(data_range / pow(2, 8)) - if minBucketWidth: - if minBucketWidth < initial_bin_size: - bin_size = grow_bin(minBucketWidth, initial_bin_size) + if min_bucket_width: + if min_bucket_width < initial_bin_size: + bin_size = grow_bin(min_bucket_width, initial_bin_size) else: - bin_size = minBucketWidth + bin_size = min_bucket_width else: bin_size = initial_bin_size else: - initial_bin_size = math.ceil(data_range / nextPowerOf2(maxBuckets)) - if minBucketWidth: - if minBucketWidth < initial_bin_size: - bin_size = grow_bin(minBucketWidth, initial_bin_size) + initial_bin_size = math.ceil(data_range / nextPowerOf2(max_buckets)) + if min_bucket_width: + if min_bucket_width < initial_bin_size: + bin_size = grow_bin(min_bucket_width, initial_bin_size) else: - bin_size = minBucketWidth + bin_size = min_bucket_width else: bin_size = initial_bin_size @@ -389,20 +1133,20 @@ def grow_bin(bin_size, ref): if "height" in kwargs: fig.layout.height = kwargs["height"] - if "xlabel" not in kwargs: - xlabel = "" + if "x_label" not in kwargs: + x_label = "" else: - xlabel = kwargs["xlabel"] + x_label = kwargs["x_label"] - if "ylabel" not in kwargs: - ylabel = "" + if "y_label" not in kwargs: + y_label = "" else: - ylabel = kwargs["ylabel"] + y_label = kwargs["y_label"] histogram = plt.hist( sample=y_data, bins=num_bins, - axes_options={"count": {"label": ylabel}, "sample": {"label": xlabel}}, + axes_options={"count": {"label": y_label}, "sample": {"label": x_label}}, ) if "colors" in kwargs: @@ -416,10 +1160,10 @@ def grow_bin(bin_size, ref): else: histogram.stroke_width = 0 - if ("xlabel" in kwargs) and ("ylabel" in kwargs): + if ("x_label" in kwargs) and ("y_label" in kwargs): histogram.tooltip = Tooltip( fields=["midpoint", "count"], - labels=[kwargs["xlabel"], kwargs["ylabel"]], + labels=[kwargs["x_label"], kwargs["y_label"]], ) else: histogram.tooltip = Tooltip(fields=["midpoint", "count"]) @@ -433,82 +1177,851 @@ def grow_bin(bin_size, ref): raise Exception(e) -def image_byClass( - image, classBand, region, reducer, scale, classLabels, xLabels, **kwargs -): - # TODO - pass +def image_by_class( + image: ee.Image, + class_band: str, + region: Union[ee.Geometry, ee.FeatureCollection], + reducer: Union[str, ee.Reducer] = "MEAN", + scale: Optional[int] = None, + class_labels: Optional[List[str]] = None, + x_labels: Optional[List[str]] = None, + chart_type: str = "LineChart", + **kwargs: Any, +) -> Any: + """ + Generates a Chart from an image by class. Extracts and plots band values by class. + Args: + image (ee.Image): Image to extract band values from. + class_band (str): The band name to use as class labels. + region (ee.Geometry | ee.FeatureCollection): The region(s) to reduce. + reducer (str | ee.Reducer, optional): The reducer type for zonal statistics. Can + be one of 'mean', 'median', 'sum', 'min', 'max', etc. Defaults to 'MEAN'. + scale (int, optional): The scale in meters at which to perform the analysis. + class_labels (List[str], optional): List of class labels. + x_labels (List[str], optional): List of x-axis labels. + chart_type (str, optional): The type of chart to create. Supported types are + 'ScatterChart', 'LineChart', 'ColumnChart', 'BarChart', 'PieChart', + 'AreaChart', and 'Table'. Defaults to 'LineChart'. + **kwargs: Additional keyword arguments. + + Returns: + Any: The generated chart. + """ + fc = zonal_stats( + image, region, stat_type=reducer, scale=scale, verbose=False, return_fc=True + ) + bands = image.bandNames().getInfo() + df = ee_to_df(fc)[bands + [class_band]] -def image_byRegion(image, regions, reducer, scale, xProperty, **kwargs): - # TODO - pass + df_transposed = df.set_index(class_band).T + if x_labels is not None: + df_transposed["label"] = x_labels + else: + df_transposed["label"] = df_transposed.index -def image_doySeries( - imageCollection, - region, - regionReducer, - scale, - yearReducer, - startDay, - endDay, - **kwargs, -): - # TODO - pass - - -def image_doySeriesByRegion( - imageCollection, - bandName, - regions, - regionReducer, - scale, - yearReducer, - seriesProperty, - startDay, - endDay, - **kwargs, -): - # TODO - pass - - -def image_doySeriesByYear( - imageCollection, - bandName, - region, - regionReducer, - scale, - sameDayReducer, - startDay, - endDay, - **kwargs, -): - # TODO - pass + if class_labels is None: + y_cols = df_transposed.columns.tolist() + y_cols.remove("label") + else: + y_cols = class_labels + + fig = Chart( + df_transposed, chart_type=chart_type, x_cols="label", y_cols=y_cols, **kwargs + ) + return fig + + +def image_by_region( + image: ee.Image, + regions: Union[ee.FeatureCollection, ee.Geometry], + reducer: Union[str, ee.Reducer], + scale: int, + x_property: str, + **kwargs: Any, +) -> None: + """ + Generates a Chart from an image. Extracts and plots band values in one or more + regions in the image, with each band in a separate series. + + Args: + image (ee.Image): Image to extract band values from. + regions (ee.FeatureCollection | ee.Geometry): Regions to reduce. + Defaults to the image's footprint. + reducer (str | ee.Reducer): The reducer type for zonal statistics. Can + be one of 'mean', 'median', 'sum', 'min', 'max', etc. + scale (int): The scale in meters at which to perform the analysis. + x_property (str): The name of the property in the feature collection to + use as the x-axis values. + **kwargs: Additional keyword arguments to be passed to the + `feature_by_feature` function. + + Returns: + None + """ + + fc = zonal_stats( + image, regions, stat_type=reducer, scale=scale, verbose=False, return_fc=True + ) + bands = image.bandNames().getInfo() + df = ee_to_df(fc)[bands + [x_property]] + feature_by_feature(df, x_property, bands, **kwargs) + + +def image_doy_series( + image_collection: ee.ImageCollection, + region: Optional[Union[ee.Geometry, ee.FeatureCollection]] = None, + region_reducer: Optional[Union[str, ee.Reducer]] = None, + scale: Optional[int] = None, + year_reducer: Optional[Union[str, ee.Reducer]] = None, + start_day: int = 1, + end_day: int = 366, + chart_type: str = "LineChart", + colors: Optional[List[str]] = None, + title: Optional[str] = None, + x_label: Optional[str] = None, + y_label: Optional[str] = None, + **kwargs: Any, +) -> Chart: + """ + Generates a time series chart of an image collection for a specific region + over a range of days of the year. + + Args: + image_collection (ee.ImageCollection): The image collection to analyze. + region (Optional[Union[ee.Geometry, ee.FeatureCollection]]): The region + to reduce. + region_reducer (Optional[Union[str, ee.Reducer]]): The reducer type for + zonal statistics.Can be one of 'mean', 'median', 'sum', 'min', 'max', etc. + scale (Optional[int]): The scale in meters at which to perform the analysis. + year_reducer (Optional[Union[str, ee.Reducer]]): The reducer type for + yearly statistics. + start_day (int): The start day of the year. + end_day (int): The end day of the year. + chart_type (str): The type of chart to create. Supported types are + 'ScatterChart', 'LineChart', 'ColumnChart', 'BarChart', + 'PieChart', 'AreaChart', and 'Table'. + colors (Optional[List[str]]): The colors to use for the chart. + Defaults to a predefined list of colors. + title (Optional[str]): The title of the chart. Defaults to the + chart type. + x_label (Optional[str]): The label for the x-axis. Defaults to an + empty string. + y_label (Optional[str]): The label for the y-axis. Defaults to an + empty string. + **kwargs: Additional keyword arguments to pass to the bqplot Figure + or mark objects. For axes_options, see + https://bqplot.github.io/bqplot/api/axes + + Returns: + Chart: The generated chart. + """ + + # Function to add day-of-year ('doy') and year properties to each image. + def set_doys(collection): + def add_doy(img): + date = img.date() + year = date.get("year") + doy = date.getRelative("day", "year").floor().add(1) + return img.set({"doy": doy, "year": year}) + + return collection.map(add_doy) + + # Reduces images with the same day of year. + def group_by_doy(collection, start, end, reducer): + collection = set_doys(collection) + + doys = ee.FeatureCollection( + [ee.Feature(None, {"doy": i}) for i in range(start, end + 1)] + ) + + # Group images by their day of year. + filter = ee.Filter(ee.Filter.equals(leftField="doy", rightField="doy")) + joined = ee.Join.saveAll("matches").apply( + primary=doys, secondary=collection, condition=filter + ) + + # For each DoY, reduce images across years. + def reduce_images(doy): + images = ee.ImageCollection.fromImages(doy.get("matches")) + image = images.reduce(reducer) + return image.set( + { + "doy": doy.get("doy"), + "geo": images.geometry(), # // Retain geometry for future reduceRegion. + } + ) + + return ee.ImageCollection(joined.map(reduce_images)) + + # Set default values and filters if parameters are not provided. + region_reducer = region_reducer or ee.Reducer.mean() + year_reducer = year_reducer or ee.Reducer.mean() + + # Optionally filter the image collection by region. + filtered_collection = image_collection + if region: + filtered_collection = filtered_collection.filterBounds(region) + filtered_collection = set_doys(filtered_collection) + + doy_images = group_by_doy(filtered_collection, start_day, end_day, year_reducer) + + # For each DoY, reduce images across years within the region. + def reduce_doy_images(image): + region_for_image = region if region else image.get("geo") + dictionary = image.reduceRegion( + reducer=region_reducer, geometry=region_for_image, scale=scale + ) + + return ee.Feature(None, {"doy": image.get("doy")}).set(dictionary) + + reduced = ee.FeatureCollection(doy_images.map(reduce_doy_images)) + + df = ee_to_df(reduced) + df.columns = df.columns.str.replace(r"_.*", "", regex=True) + + x_cols = "doy" + y_cols = df.columns.tolist() + y_cols.remove("doy") + + fig = Chart( + df, + chart_type, + x_cols, + y_cols, + colors, + title, + x_label, + y_label, + **kwargs, + ) + return fig + + +def image_doy_series_by_region( + image_collection: ee.ImageCollection, + band_name: str, + regions: ee.FeatureCollection, + region_reducer: Optional[Union[str, ee.Reducer]] = None, + scale: Optional[int] = None, + year_reducer: Optional[Union[str, ee.Reducer]] = None, + series_property: Optional[str] = None, + start_day: int = 1, + end_day: int = 366, + chart_type: str = "LineChart", + colors: Optional[List[str]] = None, + title: Optional[str] = None, + x_label: Optional[str] = None, + y_label: Optional[str] = None, + **kwargs: Any, +) -> Chart: + """ + Generates a time series chart of an image collection for multiple regions + over a range of days of the year. + + Args: + image_collection (ee.ImageCollection): The image collection to analyze. + band_name (str): The name of the band to analyze. + regions (ee.FeatureCollection): The regions to analyze. + region_reducer (Optional[Union[str, ee.Reducer]]): The reducer type for + zonal statistics. + scale (Optional[int]): The scale in meters at which to perform the analysis. + year_reducer (Optional[Union[str, ee.Reducer]]): The reducer type for + yearly statistics. + series_property (Optional[str]): The property to use for labeling the series. + start_day (int): The start day of the year. + end_day (int): The end day of the year. + chart_type (str): The type of chart to create. Supported types are + 'ScatterChart', 'LineChart', 'ColumnChart', 'BarChart', + 'PieChart', 'AreaChart', and 'Table'. + colors (Optional[List[str]]): The colors to use for the chart. + Defaults to a predefined list of colors. + title (Optional[str]): The title of the chart. Defaults to the + chart type. + x_label (Optional[str]): The label for the x-axis. Defaults to an + empty string. + y_label (Optional[str]): The label for the y-axis. Defaults to an + empty string. + **kwargs: Additional keyword arguments to pass to the bqplot Figure + or mark objects. For axes_options, see + https://bqplot.github.io/bqplot/api/axes + + Returns: + Chart: The generated chart. + """ + + image_collection = image_collection.select(band_name) + + # Function to add day-of-year ('doy') and year properties to each image. + def set_doys(collection): + def add_doy(img): + date = img.date() + year = date.get("year") + doy = date.getRelative("day", "year").floor().add(1) + return img.set({"doy": doy, "year": year}) + + return collection.map(add_doy) + + # Reduces images with the same day of year. + def group_by_doy(collection, start, end, reducer): + collection = set_doys(collection) + + doys = ee.FeatureCollection( + [ee.Feature(None, {"doy": i}) for i in range(start, end + 1)] + ) + + # Group images by their day of year. + filter = ee.Filter(ee.Filter.equals(leftField="doy", rightField="doy")) + joined = ee.Join.saveAll("matches").apply( + primary=doys, secondary=collection, condition=filter + ) + + # For each DoY, reduce images across years. + def reduce_images(doy): + images = ee.ImageCollection.fromImages(doy.get("matches")) + image = images.reduce(reducer) + return image.set( + { + "doy": doy.get("doy"), + "geo": images.geometry(), # // Retain geometry for future reduceRegion. + } + ) + + return ee.ImageCollection(joined.map(reduce_images)) + + if year_reducer is None: + year_reducer = ee.Reducer.mean() + if region_reducer is None: + region_reducer = ee.Reducer.mean() + + doy_images = group_by_doy(image_collection, start_day, end_day, year_reducer) + + if series_property is None: + series_property = "system:index" + regions = regions.select([series_property]) + fc = zonal_stats( + doy_images.toBands(), + regions, + stat_type=region_reducer, + scale=scale, + verbose=False, + return_fc=True, + ) + df = ee_to_df(fc) + df = transpose_df(df, label_col=series_property, index_name="doy") + df["doy"] = df.index.str.split("_").str[0].astype(int) + df.sort_values("doy", inplace=True) + y_cols = df.columns.tolist() + y_cols.remove("doy") + + fig = Chart( + df, + chart_type, + "doy", + y_cols, + colors, + title, + x_label, + y_label, + **kwargs, + ) + return fig + + +def doy_series_by_year( + image_collection: ee.ImageCollection, + band_name: str, + region: Optional[Union[ee.Geometry, ee.FeatureCollection]] = None, + region_reducer: Optional[Union[str, ee.Reducer]] = None, + scale: Optional[int] = None, + same_day_reducer: Optional[Union[str, ee.Reducer]] = None, + start_day: int = 1, + end_day: int = 366, + chart_type: str = "LineChart", + colors: Optional[List[str]] = None, + title: Optional[str] = None, + x_label: Optional[str] = None, + y_label: Optional[str] = None, + **kwargs: Any, +) -> Chart: + """ + Generates a time series chart of an image collection for a specific region + over multiple years. + + Args: + image_collection (ee.ImageCollection): The image collection to analyze. + band_name (str): The name of the band to analyze. + region (Optional[Union[ee.Geometry, ee.FeatureCollection]]): The region + to analyze. + region_reducer (Optional[Union[str, ee.Reducer]]): The reducer type for + zonal statistics. + scale (Optional[int]): The scale in meters at which to perform the analysis. + same_day_reducer (Optional[Union[str, ee.Reducer]]): The reducer type + for daily statistics. + start_day (int): The start day of the year. + end_day (int): The end day of the year. + chart_type (str): The type of chart to create. Supported types are + 'ScatterChart', 'LineChart', 'ColumnChart', 'BarChart', + 'PieChart', 'AreaChart', and 'Table'. + colors (Optional[List[str]]): The colors to use for the chart. + Defaults to a predefined list of colors. + title (Optional[str]): The title of the chart. Defaults to the + chart type. + x_label (Optional[str]): The label for the x-axis. Defaults to an + empty string. + y_label (Optional[str]): The label for the y-axis. Defaults to an + empty string. + **kwargs: Additional keyword arguments to pass to the bqplot Figure + or mark objects. For axes_options, see + https://bqplot.github.io/bqplot/api/axes + + Returns: + Chart: The generated chart. + """ + + # Function to add day-of-year ('doy') and year properties to each image. + def set_doys(collection): + def add_doy(img): + date = img.date() + year = date.get("year") + doy = date.getRelative("day", "year").floor().add(1) + return img.set({"doy": doy, "year": year}) + + return collection.map(add_doy) + + # Set default values and filters if parameters are not provided. + region_reducer = region_reducer or ee.Reducer.mean() + same_day_reducer = same_day_reducer or ee.Reducer.mean() + + # Optionally filter the image collection by region. + filtered_collection = image_collection + if region: + filtered_collection = filtered_collection.filterBounds(region) + filtered_collection = set_doys(filtered_collection) + + # Filter image collection by day of year. + filtered_collection = filtered_collection.filter( + ee.Filter.calendarRange(start_day, end_day, "day_of_year") + ) + + # Generate a feature for each (doy, value, year) combination. + def create_feature(image): + value = ( + image.select(band_name) + .reduceRegion(reducer=region_reducer, geometry=region, scale=scale) + .get(band_name) + ) # Get the reduced value for the given band. + return ee.Feature( + None, {"doy": image.get("doy"), "year": image.get("year"), "value": value} + ) + + tuples = filtered_collection.map(create_feature) + + # Group by unique (doy, year) pairs. + distinct_doy_year = tuples.distinct(["doy", "year"]) + + # Join the original tuples with the distinct (doy, year) pairs. + filter = ee.Filter.And( + ee.Filter.equals(leftField="doy", rightField="doy"), + ee.Filter.equals(leftField="year", rightField="year"), + ) + joined = ee.Join.saveAll("matches").apply( + primary=distinct_doy_year, secondary=tuples, condition=filter + ) + + # For each (doy, year), reduce the values of the joined features. + def reduce_features(doy_year): + features = ee.FeatureCollection(ee.List(doy_year.get("matches"))) + value = features.aggregate_array("value").reduce(same_day_reducer) + return doy_year.set("value", value) + + reduced = joined.map(reduce_features) + + df = ee_to_df(reduced, columns=["doy", "year", "value"]) + df = pivot_df(df, index="doy", columns="year", values="value") + y_cols = df.columns.tolist()[1:] + x_cols = "doy" + + fig = Chart( + df, + chart_type, + x_cols, + y_cols, + colors, + title, + x_label, + y_label, + **kwargs, + ) + return fig def image_histogram( - image, region, scale, maxBuckets, minBucketWidth, maxRaw, maxPixels, **kwargs -): - # TODO - pass + image: ee.Image, + region: ee.Geometry, + scale: int, + max_buckets: int, + min_bucket_width: float, + max_raw: int, + max_pixels: int, + reducer_args: Dict[str, Any] = {}, + **kwargs: Dict[str, Any], +) -> bq.Figure: + """ + Creates a histogram for each band of the specified image within the given + region using bqplot. + Args: + image (ee.Image): The Earth Engine image for which to create histograms. + region (ee.Geometry): The region over which to calculate the histograms. + scale (int): The scale in meters of the calculation. + max_buckets (int): The maximum number of buckets in the histogram. + min_bucket_width (float): The minimum width of the buckets in the histogram. + max_raw (int): The maximum number of pixels to include in the histogram. + max_pixels (int): The maximum number of pixels to reduce. + reducer_args (Dict[str, Any]): Additional arguments to pass to the image.reduceRegion. + + Keyword Args: + colors (List[str]): Colors for the histograms of each band. + labels (List[str]): Labels for the histograms of each band. + title (str): Title of the combined histogram plot. + legend_location (str): Location of the legend in the plot. + + Returns: + bq.Figure: The bqplot figure containing the histograms. + """ + # Calculate the histogram data. + histogram = image.reduceRegion( + reducer=ee.Reducer.histogram( + maxBuckets=max_buckets, minBucketWidth=min_bucket_width, maxRaw=max_raw + ), + geometry=region, + scale=scale, + maxPixels=max_pixels, + **reducer_args, + ) -def image_regions(image, regions, reducer, scale, seriesProperty, xLabels, **kwargs): - # TODO - pass + histograms = { + band: histogram.get(band).getInfo() for band in image.bandNames().getInfo() + } + + # Create bqplot histograms for each band. + def create_histogram( + hist_data: Dict[str, Any], color: str, label: str + ) -> bq.Figure: + """ + Creates a bqplot histogram for the given histogram data. + + Args: + hist_data (dict): The histogram data. + color (str): The color of the histogram. + label (str): The label of the histogram. + + Returns: + bq.Figure: The bqplot figure for the histogram. + """ + x_data = np.array(hist_data["bucketMeans"]) + y_data = np.array(hist_data["histogram"]) + + x_sc = bq.LinearScale() + y_sc = bq.LinearScale() + + bar = bq.Bars( + x=x_data, + y=y_data, + scales={"x": x_sc, "y": y_sc}, + colors=[color], + display_legend=True, + labels=[label], + ) + ax_x = bq.Axis(scale=x_sc, label="Reflectance (x1e4)", tick_format="0.0f") + ax_y = bq.Axis( + scale=y_sc, orientation="vertical", label="Count", tick_format="0.0f" + ) -def image_series(imageCollection, region, reducer, scale, xProperty, **kwargs): - # TODO - pass + return bq.Figure(marks=[bar], axes=[ax_x, ax_y]) + # Define colors and labels for the bands. + band_colors = kwargs.get("colors", ["#cf513e", "#1d6b99", "#f0af07"]) + band_labels = kwargs.get("labels", image.bandNames().getInfo()) -def image_seriesByRegion( - imageCollection, regions, reducer, band, scale, xProperty, seriesProperty, **kwargs -): - # TODO - pass + # Create and combine histograms for each band. + histograms_fig = [] + for band, color, label in zip(histograms.keys(), band_colors, band_labels): + histograms_fig.append(create_histogram(histograms[band], color, label)) + + combined_fig = bq.Figure( + marks=[fig.marks[0] for fig in histograms_fig], + axes=histograms_fig[0].axes, + **kwargs, + ) + + for fig, label in zip(histograms_fig, band_labels): + fig.marks[0].labels = [label] + + combined_fig.legend_location = kwargs.get("legend_location", "top-right") + + return combined_fig + + +def image_regions( + image: ee.Image, + regions: Union[ee.FeatureCollection, ee.Geometry], + reducer: Union[str, ee.Reducer], + scale: int, + series_property: str, + x_labels: List[str], + **kwargs: Any, +) -> None: + """ + Generates a Chart from an image by regions. Extracts and plots band values + in multiple regions. + + Args: + image (ee.Image): Image to extract band values from. + regions (Union[ee.FeatureCollection, ee.Geometry]): Regions to reduce. + Defaults to the image's footprint. + reducer (Union[str, ee.Reducer]): The reducer type for zonal statistics. + Can be one of 'mean', 'median', 'sum', 'min', 'max', etc. + scale (int): The scale in meters at which to perform the analysis. + series_property (str): The property to use for labeling the series. + x_labels (List[str]): List of x-axis labels. + **kwargs: Additional keyword arguments. + + Returns: + bq.Figure: The bqplot figure. + """ + fc = zonal_stats( + image, regions, stat_type=reducer, scale=scale, verbose=False, return_fc=True + ) + bands = image.bandNames().getInfo() + fc = fc.select(bands + [series_property]) + return feature_by_property(fc, x_labels, series_property, **kwargs) + + +def image_series( + image_collection: ee.ImageCollection, + region: Union[ee.Geometry, ee.FeatureCollection], + reducer: Optional[Union[str, ee.Reducer]] = None, + scale: Optional[int] = None, + x_property: str = "system:time_start", + chart_type: str = "LineChart", + x_cols: Optional[List[str]] = None, + y_cols: Optional[List[str]] = None, + colors: Optional[List[str]] = None, + title: Optional[str] = None, + x_label: Optional[str] = None, + y_label: Optional[str] = None, + **kwargs: Any, +) -> Chart: + """ + Generates a time series chart of an image collection for a specific region. + + Args: + image_collection (ee.ImageCollection): The image collection to analyze. + region (Union[ee.Geometry, ee.FeatureCollection]): The region to reduce. + reducer (Optional[Union[str, ee.Reducer]]): The reducer to use. + scale (Optional[int]): The scale in meters at which to perform the analysis. + x_property (str): The name of the property to use as the x-axis values. + chart_type (str): The type of chart to create. Supported types are + 'ScatterChart', 'LineChart', 'ColumnChart', 'BarChart', + 'PieChart', 'AreaChart', and 'Table'. + x_cols (Optional[List[str]]): The columns to use for the x-axis. + Defaults to the first column. + y_cols (Optional[List[str]]): The columns to use for the y-axis. + Defaults to the second column. + colors (Optional[List[str]]): The colors to use for the chart. + Defaults to a predefined list of colors. + title (Optional[str]): The title of the chart. Defaults to the + chart type. + x_label (Optional[str]): The label for the x-axis. Defaults to an + empty string. + y_label (Optional[str]): The label for the y-axis. Defaults to an + empty string. + **kwargs: Additional keyword arguments to pass to the bqplot Figure + or mark objects. For axes_options, see + https://bqplot.github.io/bqplot/api/axes + + Returns: + Chart: The chart object. + """ + + if reducer is None: + reducer = ee.Reducer.mean() + + band_names = image_collection.first().bandNames().getInfo() + + # Function to reduce the region and get the mean for each image. + def get_stats(image): + stats = image.reduceRegion(reducer=reducer, geometry=region, scale=scale) + + results = {} + for band in band_names: + results[band] = stats.get(band) + + if x_property == "system:time_start" or x_property == "system:time_end": + results["date"] = image.date().format("YYYY-MM-dd") + else: + results[x_property] = image.get(x_property).getInfo() + + return ee.Feature(None, results) + + # Apply the function over the image collection. + fc = ee.FeatureCollection( + image_collection.map(get_stats).filter(ee.Filter.notNull(band_names)) + ) + df = ee_to_df(fc) + if "date" in df.columns: + df["date"] = pd.to_datetime(df["date"]) + + fig = Chart( + df, + chart_type, + x_cols, + y_cols, + colors, + title, + x_label, + y_label, + **kwargs, + ) + return fig + + +def image_series_by_region( + image_collection: ee.ImageCollection, + regions: Union[ee.FeatureCollection, ee.Geometry], + reducer: Optional[Union[str, ee.Reducer]] = None, + band: Optional[str] = None, + scale: Optional[int] = None, + x_property: str = "system:time_start", + series_property: str = "system:index", + chart_type: str = "LineChart", + x_cols: Optional[List[str]] = None, + y_cols: Optional[List[str]] = None, + colors: Optional[List[str]] = None, + title: Optional[str] = None, + x_label: Optional[str] = None, + y_label: Optional[str] = None, + **kwargs: Any, +) -> Chart: + """ + Generates a time series chart of an image collection for multiple regions. + + Args: + image_collection (ee.ImageCollection): The image collection to analyze. + regions (ee.FeatureCollection | ee.Geometry): The regions to reduce. + reducer (str | ee.Reducer): The reducer type for zonal statistics. + band (str): The name of the band to analyze. + scale (int): The scale in meters at which to perform the analysis. + x_property (str): The name of the property to use as the x-axis values. + series_property (str): The property to use for labeling the series. + chart_type (str): The type of chart to create. Supported types are + 'ScatterChart', 'LineChart', 'ColumnChart', 'BarChart', + 'PieChart', 'AreaChart', and 'Table'. + x_cols (Optional[List[str]]): The columns to use for the x-axis. + Defaults to the first column. + y_cols (Optional[List[str]]): The columns to use for the y-axis. + Defaults to the second column. + colors (Optional[List[str]]): The colors to use for the chart. + Defaults to a predefined list of colors. + title (Optional[str]): The title of the chart. Defaults to the + chart type. + x_label (Optional[str]): The label for the x-axis. Defaults to an + empty string. + y_label (Optional[str]): The label for the y-axis. Defaults to an + empty string. + **kwargs: Additional keyword arguments to pass to the bqplot Figure + or mark objects. For axes_options, see + https://bqplot.github.io/bqplot/api/axes + + Returns: + Chart: The chart object. + """ + if reducer is None: + reducer = ee.Reducer.mean() + + if band is None: + band = image_collection.first().bandNames().get(0).getInfo() + + image = image_collection.select(band).toBands() + + fc = zonal_stats( + image, regions, stat_type=reducer, scale=scale, verbose=False, return_fc=True + ) + columns = image.bandNames().getInfo() + [series_property] + df = ee_to_df(fc, columns=columns) + + headers = df[series_property].tolist() + df = df.drop(columns=[series_property]).T + df.columns = headers + + if x_property == "system:time_start" or x_property == "system:time_end": + indexes = image_dates(image_collection).getInfo() + df["index"] = pd.to_datetime(indexes) + + else: + indexes = image_collection.aggregate_array(x_property).getInfo() + df["index"] = indexes + + fig = Chart( + df, + chart_type, + x_cols, + y_cols, + colors, + title, + x_label, + y_label, + **kwargs, + ) + return fig + + +def array_values( + array: Union[ee.Array, ee.List, List[List[float]]], + x_labels: Optional[Union[ee.Array, ee.List, List[float]]] = None, + axis: int = 1, + series_names: Optional[List[str]] = None, + chart_type: str = "LineChart", + colors: Optional[List[str]] = None, + title: Optional[str] = None, + x_label: Optional[str] = None, + y_label: Optional[str] = None, + **kwargs: Any, +) -> Chart: + """ + Converts an array to a DataFrame and generates a chart. + + Args: + array (Union[ee.Array, ee.List, List[List[float]]]): The array to convert. + x_labels (Optional[Union[ee.Array, ee.List, List[float]]]): The labels + for the x-axis. Defaults to None. + axis (int): The axis along which to transpose the array if needed. Defaults to 1. + series_names (Optional[List[str]]): The names of the series. Defaults to None. + chart_type (str): The type of chart to create. Defaults to "LineChart". + colors (Optional[List[str]]): The colors to use for the chart. Defaults to None. + title (Optional[str]): The title of the chart. Defaults to None. + x_label (Optional[str]): The label for the x-axis. Defaults to None. + y_label (Optional[str]): The label for the y-axis. Defaults to None. + **kwargs: Additional keyword arguments to pass to the Chart constructor. + + Returns: + Chart: The generated chart. + """ + + df = array_to_df(array, x_values=x_labels, y_labels=series_names, axis=axis) + fig = Chart( + df, + x_cols=["x"], + y_cols=df.columns.tolist()[1:], + chart_type=chart_type, + colors=colors, + title=title, + x_label=x_label, + y_label=y_label, + **kwargs, + ) + return fig diff --git a/geemap/common.py b/geemap/common.py index d0c0a0b4e2..84b5a7b7e4 100644 --- a/geemap/common.py +++ b/geemap/common.py @@ -16280,3 +16280,24 @@ def xarray_to_raster(dataset, filename: str, **kwargs: Dict[str, Any]) -> None: dataset = dataset.rename(new_names) dataset.transpose(..., "y", "x").rio.to_raster(filename, **kwargs) + + +def hex_to_rgba(hex_color: str, opacity: float) -> str: + """ + Converts a hex color code to an RGBA color string. + + Args: + hex_color (str): The hex color code to convert. It can be in the format + '#RRGGBB' or 'RRGGBB'. + opacity (float): The opacity value for the RGBA color. It should be a + float between 0.0 (completely transparent) and 1.0 (completely opaque). + + Returns: + str: The RGBA color string in the format 'rgba(R, G, B, A)'. + """ + hex_color = hex_color.lstrip("#") + h_len = len(hex_color) + r, g, b = ( + int(hex_color[i : i + h_len // 3], 16) for i in range(0, h_len, h_len // 3) + ) + return f"rgba({r},{g},{b},{opacity})" diff --git a/mkdocs.yml b/mkdocs.yml index eb8149553e..d13ede44ae 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -77,7 +77,7 @@ extra: - icon: fontawesome/brands/twitter link: https://twitter.com/giswqs - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/qiushengwu + link: https://www.linkedin.com/in/giswqs - icon: fontawesome/brands/youtube link: https://youtube.com/@giswqs analytics: @@ -298,6 +298,11 @@ nav: - notebooks/141_image_array_viz.ipynb - notebooks/142_google_maps.ipynb - notebooks/143_precipitation_timelapse.ipynb + - notebooks/144_chart_features.ipynb + - notebooks/145_chart_image.ipynb + - notebooks/146_chart_image_collection.ipynb + - notebooks/147_chart_array_list.ipynb + - notebooks/148_chart_data_table.ipynb - notebooks/149_gemini.ipynb # - miscellaneous: # - notebooks/cartoee_colab.ipynb