| Title: | Download and Analyse Nighttime Lights Satellite Data |
|---|---|
| Description: | Downloads and processes nighttime lights satellite data from the 'NASA' 'VIIRS' Black Marble product suite (daily, monthly, and annual composites; Roman et al. (2018) <doi:10.1016/j.rse.2018.03.017>) served by the 'LAADS DAAC' archive <https://ladsweb.modaps.eosdis.nasa.gov>, and from the Indian Space Research Organisation's 'Bhuvan' nighttime lights portal. Provides zonal statistics over arbitrary polygons, returning tidy region-by-time panels, along with plotting helpers. |
| Authors: | Saket Choudhary [aut, cre] (ORCID: <https://orcid.org/0000-0001-5202-7633>) |
| Maintainer: | Saket Choudhary <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.2.0 |
| Built: | 2026-11-03 18:34:11 UTC |
| Source: | https://github.com/cran/lightson |
Fetches annual nighttime lights GeoTIFF tiles from the Bhuvan WMS service for a given spatial extent and converts the classed RGB map to a single-band radiance raster.
bhuvan_raster( region, years, force = FALSE, width = 1024, height = 1024, bbox_pad = c(0, 0, 0, 0) )bhuvan_raster( region, years, force = FALSE, width = 1024, height = 1024, bbox_pad = c(0, 0, 0, 0) )
region |
An |
years |
Integer vector of years to download (2012-2024). |
force |
Logical. If |
width, height
|
Pixel dimensions of the WMS request. Default 1024x1024. |
bbox_pad |
Numeric vector of length 1 or 4 (xmin, ymin, xmax, ymax
sides) in degrees, added to the region bounding box before the WMS
request. Useful when GADM boundaries clip disputed territories (e.g.
|
The Bhuvan WMS does not serve continuous radiance. It returns a map with four colour classes, which are converted to representative radiance values in nW/cm^2/sr:
| Colour | Class | Value |
| black | < 5 | 2.5 |
| blue-grey | 5 - 25 | 15 |
| cream | 26 - 200 | 112.5 |
| red | > 200 | 200 |
The first three values are class midpoints; the open-ended top class is set
to its floor, so the brightest areas are underestimated. Pixels that match
no class (the portal's watermark and its anti-aliased edges) are NA.
Treat the output as coarse, banded radiance: use VIIRS from
ntl_download() when you need continuous calibrated values.
A named list of single-band SpatRaster objects, keyed by year
(e.g., list("2020" = rast(...), "2021" = rast(...))). Errors if the WMS
fetch fails for any requested year.
Bhuvan NTL data from 2024 onwards is derived from VNP46A4 Collection 2.0, while 2012-2023 uses the legacy Collection 1.0. The two collections use different calibration and atmospheric correction algorithms, so values are not directly comparable across this boundary.
states <- get_india_admin("state") rasters <- bhuvan_raster(states, years = 2021:2022) panel <- extract_panel(rasters, states, id_col = "state_name")states <- get_india_admin("state") rasters <- bhuvan_raster(states, years = 2021:2022) panel <- extract_panel(rasters, states, id_col = "state_name")
Wraps the ISRO Bhuvan NTL REST API to return mean radiance per Indian state across years.
bhuvan_stats(years = NULL, state = NULL)bhuvan_stats(years = NULL, state = NULL)
years |
Integer vector of years to return. |
state |
Character. State name as returned by the API (e.g., |
For district-level or custom-geometry statistics, use bhuvan_raster()
together with extract_panel().
A tidy data frame with columns state (character), year
(integer), and mean_radiance (numeric). State names are normalised to
title case (e.g., "Tamil Nadu" not "TAMILNADU").
Bhuvan NTL data from 2024 onwards is derived from VNP46A4 Collection 2.0, while 2012-2023 uses the legacy Collection 1.0. The two collections use different calibration and atmospheric correction algorithms, so radiance estimates are not directly comparable across this boundary. Requests spanning both collections trigger a warning.
# All states, all years df <- bhuvan_stats() # Bihar only df <- bhuvan_stats(state = "BIHAR") # Multiple years df <- bhuvan_stats(years = 2015:2023)# All states, all years df <- bhuvan_stats() # Bihar only df <- bhuvan_stats(state = "BIHAR") # Multiple years df <- bhuvan_stats(years = 2015:2023)
Removes cached raster files downloaded by ntl_download() or bhuvan_raster().
cache_clear(source = NULL)cache_clear(source = NULL)
source |
One of |
Invisibly returns the number of files removed.
cache_clear("bhuvan")cache_clear("bhuvan")
Returns the bearer token used for VIIRS/Black Marble downloads from NASA LAADS DAAC.
earthdata_token(token = NULL)earthdata_token(token = NULL)
token |
Character string. If |
Getting a token (web):
Create an account at https://urs.earthdata.nasa.gov
Accept the MERIS and Sentinel EULAs under Profile > EULAs
Go to Profile > Generate Token, click "Generate Token", then "Show Token"
Copy the token (starts with EDL-) and store it
Supplying the token:
Set EARTHDATA_TOKEN environment variable (recommended: add to ~/.Renviron)
Or pass it directly as the token argument
Or set EARTHDATA_USERNAME + EARTHDATA_PASSWORD to fetch it automatically
Bhuvan NTL functions (bhuvan_stats(), bhuvan_raster()) do not require
any authentication.
The access token string, invisibly.
## Not run: # Option 1: token from Profile > Generate Token on urs.earthdata.nasa.gov Sys.setenv(EARTHDATA_TOKEN = "EDL-xxxxxxxxxxxxxxxxxxxx") token <- earthdata_token() # Option 2: username + password (fetches token automatically) Sys.setenv(EARTHDATA_USERNAME = "myuser", EARTHDATA_PASSWORD = "mypass") token <- earthdata_token() # Option 3: pass directly token <- earthdata_token("EDL-xxxxxxxxxxxxxxxxxxxx") ## End(Not run)## Not run: # Option 1: token from Profile > Generate Token on urs.earthdata.nasa.gov Sys.setenv(EARTHDATA_TOKEN = "EDL-xxxxxxxxxxxxxxxxxxxx") token <- earthdata_token() # Option 2: username + password (fetches token automatically) Sys.setenv(EARTHDATA_USERNAME = "myuser", EARTHDATA_PASSWORD = "mypass") token <- earthdata_token() # Option 3: pass directly token <- earthdata_token("EDL-xxxxxxxxxxxxxxxxxxxx") ## End(Not run)
Takes a named list of SpatRaster objects (one per time period, as returned by
ntl_download() or bhuvan_raster()) and an sf or SpatVector polygon
layer, and returns a tidy long-format data frame with one row per region and
time period. Annual names such as "2025" produce the original four-column
output. ISO-date names used by monthly and daily Black Marble additionally
produce a date column.
extract_panel(rasters, polygons, fun = "mean", id_col = NULL)extract_panel(rasters, polygons, fun = "mean", id_col = NULL)
rasters |
A named list of |
polygons |
An |
fun |
Aggregation function name passed to |
id_col |
Name of the column in |
The polygons argument accepts any sf object: GADM, bharatviz GeoJSONs,
LGD districts, or any shapefile loaded with sf::read_sf().
A data frame with columns:
Character. Region identifier from polygons.
Integer. Year, taken from the name of each raster.
Numeric. Aggregated radiance value.
Integer. Number of non-NA cells contributing to the stat.
r <- terra::rast( nrows = 10, ncols = 10, xmin = 0, xmax = 10, ymin = 0, ymax = 10, crs = "EPSG:4326", vals = 1:100 ) rasters <- list("2020" = r, "2021" = r * 2) square <- function(x0) { sf::st_polygon(list(cbind(c(x0, x0 + 5, x0 + 5, x0, x0), c(0, 0, 10, 10, 0)))) } regions <- sf::st_sf( region = c("west", "east"), geometry = sf::st_sfc(square(0), square(5), crs = 4326) ) extract_panel(rasters, regions, id_col = "region")r <- terra::rast( nrows = 10, ncols = 10, xmin = 0, xmax = 10, ymin = 0, ymax = 10, crs = "EPSG:4326", vals = 1:100 ) rasters <- list("2020" = r, "2021" = r * 2) square <- function(x0) { sf::st_polygon(list(cbind(c(x0, x0 + 5, x0 + 5, x0, x0), c(0, 0, 10, 10, 0)))) } regions <- sf::st_sf( region = c("west", "east"), geometry = sf::st_sfc(square(0), square(5), crs = 4326) ) extract_panel(rasters, regions, id_col = "region")
Returns Indian administrative boundaries as an sf object. Useful as the
polygons argument to extract_panel().
get_india_admin( level = c("country", "state", "district"), source = c("gadm", "bhuvan") )get_india_admin( level = c("country", "state", "district"), source = c("gadm", "bhuvan") )
level |
One of |
source |
One of |
State boundaries use a bundled LGD (Local Government Directory) dataset derived from the Survey of India.
For custom boundaries, load your own file with sf::read_sf() and pass it
directly to extract_panel().
An sf object.
# Bundled state boundaries (no download) states <- get_india_admin("state") head(states) # Downloads from GADM or Bhuvan districts <- get_india_admin("district") districts <- get_india_admin("district", source = "bhuvan")# Bundled state boundaries (no download) states <- get_india_admin("state") head(states) # Downloads from GADM or Bhuvan districts <- get_india_admin("district") districts <- get_india_admin("district", source = "bhuvan")
Downloads calibrated VIIRS Black Marble annual (VNP46A4), monthly
(VNP46A3), or daily (VNP46A2) rasters from NASA LAADS DAAC.
ntl_download( region, years = NULL, token, force = FALSE, product = c("annual", "monthly", "daily"), dates = NULL, quality = c("high", "all") )ntl_download( region, years = NULL, token, force = FALSE, product = c("annual", "monthly", "daily"), dates = NULL, quality = c("high", "all") )
region |
An |
years |
Integer years. Required for annual data; selects all twelve
months for monthly data when |
token |
NASA Earthdata bearer token from |
force |
Re-download cached output. |
product |
One of |
dates |
Date-like vector. Required for daily data. Monthly dates are normalised to the first day of their month. |
quality |
For daily data, |
Named list of cropped SpatRaster objects. Names are years for
annual data and ISO dates otherwise.
## Not run: token <- earthdata_token() annual <- ntl_download("IND", 2020:2025, token) monthly <- ntl_download("IND", 2025, token, product = "monthly") daily <- ntl_download("IND", token = token, product = "daily", dates = as.Date("2025-11-12") ) ## End(Not run)## Not run: token <- earthdata_token() annual <- ntl_download("IND", 2020:2025, token) monthly <- ntl_download("IND", 2025, token, product = "monthly") daily <- ntl_download("IND", token = token, product = "daily", dates = as.Date("2025-11-12") ) ## End(Not run)
Converts each group's values to an index where its baseline observation is 100. This is useful for comparing changes between sources with different units, but does not make their absolute levels comparable.
ntl_index( panel, baseline, group_cols = NULL, time_col = "year", value_col = "mean_radiance", index_col = "ntl_index" )ntl_index( panel, baseline, group_cols = NULL, time_col = "year", value_col = "mean_radiance", index_col = "ntl_index" )
panel |
Data frame containing nighttime-light observations. |
baseline |
Baseline value in |
group_cols |
Grouping columns. If omitted, uses |
time_col |
Time column name. Defaults to |
value_col |
Value column name. Defaults to |
index_col |
Name for the new index column. |
panel with an additional numeric index column.
panel <- data.frame( region_id = rep(c("a", "b"), each = 3), year = rep(2020:2022, 2), mean_radiance = c(10, 12, 15, 4, 4, 5) ) ntl_index(panel, baseline = 2020)panel <- data.frame( region_id = rep(c("a", "b"), each = 3), year = rep(2020:2022, 2), mean_radiance = c(10, 12, 15, 4, 4, 5) ) ntl_index(panel, baseline = 2020)
Aligns observations by region and time, then calculates within-region correlation between two sources. Correlation compares co-movement, not absolute levels or units.
ntl_source_agreement( x, y, id_col = "region_id", time_col = "year", value_col = "mean_radiance", method = c("pearson", "spearman", "kendall") )ntl_source_agreement( x, y, id_col = "region_id", time_col = "year", value_col = "mean_radiance", method = c("pearson", "spearman", "kendall") )
x, y
|
Panel data frames, such as outputs from |
id_col |
Region identifier column. |
time_col |
Shared time column. |
value_col |
Measurement column in both panels. |
method |
Correlation method passed to |
Data frame with region, correlation, and paired observation count.
x <- data.frame( region_id = rep(c("a", "b"), each = 3), year = rep(2020:2022, 2), mean_radiance = c(1, 2, 3, 2, 4, 6) ) y <- data.frame( region_id = rep(c("a", "b"), each = 3), year = rep(2020:2022, 2), mean_radiance = c(2, 4, 7, 6, 4, 2) ) ntl_source_agreement(x, y)x <- data.frame( region_id = rep(c("a", "b"), each = 3), year = rep(2020:2022, 2), mean_radiance = c(1, 2, 3, 2, 4, 6) ) y <- data.frame( region_id = rep(c("a", "b"), each = 3), year = rep(2020:2022, 2), mean_radiance = c(2, 4, 7, 6, 4, 2) ) ntl_source_agreement(x, y)
Renders a single SpatRaster as a map. Two palettes are available:
plot_ntl_map( raster, polygons = NULL, title = "Nighttime lights", caption = NULL, palette = c("glow", "bhuvan"), dark = TRUE, limits = NULL )plot_ntl_map( raster, polygons = NULL, title = "Nighttime lights", caption = NULL, palette = c("glow", "bhuvan"), dark = TRUE, limits = NULL )
raster |
A single-layer |
polygons |
Optional |
title |
Plot title. Default |
caption |
Optional caption string. |
palette |
One of |
dark |
Logical. If |
limits |
Numeric vector of length 2 for the continuous scale. Only used
when |
"glow" (default): continuous dark-olive-to-cream-white ramp modelled on
the NASA Black Marble composite. Suited to dark backgrounds and multi-panel
year comparisons.
"bhuvan": 4-class categorical legend matching the Bhuvan portal: black
(< 5 nW/cm2/sr), dark blue-grey (5-25), cream (26-200), red (> 200).
A ggplot2 object.
r <- terra::rast( nrows = 20, ncols = 20, xmin = 77, xmax = 78, ymin = 28, ymax = 29, crs = "EPSG:4326", vals = seq_len(400) / 2 ) plot_ntl_map(r) plot_ntl_map(r, palette = "bhuvan")r <- terra::rast( nrows = 20, ncols = 20, xmin = 77, xmax = 78, ymin = 28, ymax = 29, crs = "EPSG:4326", vals = seq_len(400) / 2 ) plot_ntl_map(r) plot_ntl_map(r, palette = "bhuvan")
Arranges one map per year into a patchwork grid with a shared title, subtitle, and caption. A shared radiance scale is computed from the 99.5th percentile across all rasters so panels are directly comparable.
plot_ntl_panel( rasters, polygons = NULL, title = "India at night", subtitle = NULL, caption = NULL, palette = c("glow", "bhuvan"), ncol = 3L, bg_colour = "#080c14", title_colour = "#CDD9E5", subtitle_colour = "#8B949E", year_colour = "#E8A87C" )plot_ntl_panel( rasters, polygons = NULL, title = "India at night", subtitle = NULL, caption = NULL, palette = c("glow", "bhuvan"), ncol = 3L, bg_colour = "#080c14", title_colour = "#CDD9E5", subtitle_colour = "#8B949E", year_colour = "#E8A87C" )
rasters |
Named list of |
polygons |
Optional |
title |
Overall plot title. Default |
subtitle |
Optional subtitle string. |
caption |
Optional caption string. |
palette |
One of |
ncol |
Number of columns in the grid. Default |
bg_colour |
Background colour of the assembled panel.
Default |
title_colour |
Title text colour. Default |
subtitle_colour |
Subtitle and caption text colour. Default |
year_colour |
Year label colour on each individual map.
Default |
Requires the patchwork package.
A patchwork object.
r <- terra::rast( nrows = 20, ncols = 20, xmin = 77, xmax = 78, ymin = 28, ymax = 29, crs = "EPSG:4326", vals = seq_len(400) / 2 ) plot_ntl_panel(list("2020" = r, "2021" = r * 1.2), title = "Synthetic example")r <- terra::rast( nrows = 20, ncols = 20, xmin = 77, xmax = 78, ymin = 28, ymax = 29, crs = "EPSG:4326", vals = seq_len(400) / 2 ) plot_ntl_panel(list("2020" = r, "2021" = r * 1.2), title = "Synthetic example")
Line chart of mean radiance over time for one or more regions. Accepts
output from extract_panel() or bhuvan_stats().
plot_ntl_trend( panel, region = NULL, title = "Nighttime lights radiance", subtitle = NULL, caption = NULL )plot_ntl_trend( panel, region = NULL, title = "Nighttime lights radiance", subtitle = NULL, caption = NULL )
panel |
A data frame as returned by |
region |
Character vector of region identifiers to plot. |
title |
Plot title. Default |
subtitle |
Optional subtitle string. |
caption |
Optional caption string. |
A ggplot2 object.
panel <- data.frame( region_id = rep(c("Lucknow", "Patna"), each = 4), year = rep(2018:2021, 2), mean_radiance = c(8.1, 8.6, 8.4, 9.2, 5.0, 5.6, 6.1, 6.9) ) plot_ntl_trend(panel) plot_ntl_trend(panel, region = "Patna")panel <- data.frame( region_id = rep(c("Lucknow", "Patna"), each = 4), year = rep(2018:2021, 2), mean_radiance = c(8.1, 8.6, 8.4, 9.2, 5.0, 5.6, 6.1, 6.9) ) plot_ntl_trend(panel) plot_ntl_trend(panel, region = "Patna")
A transparent-background theme_minimal() base tuned for NTL data: tight
grid, bold title, and optional overrides via .... Ink colour is a
single equal-contrast grey (#767676, ~4.5:1 against both pure black and
pure white) so the same rendered figure reads on light and dark pages
alike, since pkgdown bakes each figure to one static PNG at build time.
theme_ntl(base_size = 12, ...)theme_ntl(base_size = 12, ...)
base_size |
Base font size. Default |
... |
Additional |
A ggplot2 theme object.
library(ggplot2) ggplot(data.frame(year = 2012:2020, mean_radiance = 1:9), aes(year, mean_radiance)) + geom_line() + theme_ntl()library(ggplot2) ggplot(data.frame(year = 2012:2020, mean_radiance = 1:9), aes(year, mean_radiance)) + geom_line() + theme_ntl()