In Data Drought in the Humid Tropics: How to Overcome the Cloud Barrier in Greenhouse Gas Remote Sensing, we took a deep dive on the impact of clouds on data coverrage and revisit times, with a focus on the humid tropics, where we see the lowest data yields in all current greenhouse gas missions GRL paper.
Here, we provide a simple interactive example for cloud statistics from our data in the manuscript under review. You can choose different locations on the map and see the cloud-free likelihood statistics for a 200 or 2000m footprint and an according time-series. Note that the coverage is not fully global due to the computation demand of computing these likelihoods from 10m Sentinel-2 data.
viewof point = {
const width = 600.0
const height = width / 2;
const context = DOM.context2d(width, height);
const projection = d3.geoEqualEarth().fitSize([width, height], {type: "Sphere"});
const path = d3.geoPath(projection, context);
let mousedown = false;
context.beginPath(), path(graticule), context.strokeStyle = "#ccc", context.stroke();
context.beginPath(), path(land), context.fill();
context.beginPath(), path(sphere), context.strokeStyle = "#000", context.stroke();
context.lineWidth = 2, context.strokeStyle = "#f00";
const image = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
function render(coordinates) {
context.canvas.value = coordinates;
context.clearRect(0, 0, width, height);
context.putImageData(image, 0, 0);
context.beginPath(), path({type: "Point", coordinates}), context.stroke();
}
context.canvas.onmousedown = event => {
mousedown = true;
context.canvas.onmousemove(event);
};
context.canvas.onmousemove = (event) => {
if (!mousedown) return;
const rect = context.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
render(projection.invert([x, y]));
context.canvas.dispatchEvent(new CustomEvent("input"));
};
context.canvas.onmouseup = event => {
mousedown = false;
};
render([-55, 0]);
return context.canvas;
}
html`
<p class="mt-3 mb-4">
<strong>You picked:</strong> Latitude ${Math.round(point[1])}, Longitude ${Math.round(point[0])}
</p>
`
viewof slider = rangeSlider({
min: 0,
max: d3.timeDay.count(start, end),
step: 1,
format: d => formatDate(d3.timeDay.offset(start, d)),
})
html`<label class="form-label mt-3">Number of bins</label>`
fileUrl = `https://raw.githubusercontent.com/cfranken/s2_data/main/files/S2_${Math.round(point[1])}.0_${Math.round(point[0])}.0.csv`;
//fileUrl = `/data/subset_data.csv`;
function calculateLogBins(start, end, nBins) {
const logStart = Math.log10(start);
const logEnd = Math.log10(end);
const bins = Array.from({length: nBins + 1}, (_, i) =>
Math.pow(10, logStart + (logEnd - logStart) * i / nBins)
);
return bins;
}
// Custom tick format function
function customTickFormat(d) {
if (d < 0.1) return d.toFixed(2); // Display 1 as '1'
if (d < 1) return d.toFixed(1); // Display 0.01 as '0.01'
return d; // Default formatting for other values
}
Below is a histogram of the cloud-free fractions for 200m footprints (blue) and 2000m footprints (red) on a logarithmic scale (from 0.1 to 100%)
2000m
200m
plot1 = Plot.plot({
width,
x: {
type: "log",
base:10,
transform: d => Math.max(.01, 100*d)
},
y: {grid: true},
style: {
fontSize: "12px"},
color: {legend: true},
marks: [
Plot.rectY(filtered, Plot.binX({y: "count"}, {x: "cf_200", fill: "steelblue", fillOpacity: 0.5,thresholds: binBoundaries1, tip: "xy" })),
Plot.rectY(filtered, Plot.binX({y: "count"}, {x: "cf_2000", fill: "tomato", fillOpacity: 0.5,thresholds: binBoundaries1, tip: "xy" })),
Plot.ruleY([0])
]
})
Below is the corresponding time-series in the same colors (You can hover over the data to get exact values at each time-step). You can find that there are extended times in the humid tropics, where larger pixels hamper data collection, specifically in the wet season. Try out regions in the Amazon or Indonesia, for example. Outside the humid tropics, the differences are far less severe, as large-scale cloud cover without gaps can persist, and so do larger areas without clouds. Thus, in many areas, footprints larger than 2km can be perfectly fine (e.g. with TROPOMI).
However, if you want to improve understanding in the humid tropics, fine spatial resolution is key!
plot2 = Plot.plot({
width,
y: { grid: true, label: "Likelihood of cloud free pixel",domain:[0,1.099] },
color: { legend: true },
style: { fontSize: "12px" },
marginBottom: 37,
marks: [
Plot.axisX({ ticks: d3.utcMonth.every(3) }),
Plot.lineY(filtered, {
x: "Date",
y: "cf_200",
stroke: "steelblue", // Custom line color
fillOpacity: 0.75,
marker: "circle",
strokeWidth: 1.2 // Thin line
}),
Plot.lineY(filtered, {
x: "Date",
y: "cf_2000",
stroke: "tomato", // Custom line color
fillOpacity: 0.75,
marker: "circle",
strokeWidth: 1.5
}),
//Plot.ruleX(filtered, Plot.pointerX({x: "Date", py: "cf_2000", stroke: "black",fillOpacity: 0.75,strokeWidth: 0.8 })),
Plot.dot(filtered, Plot.pointerX({x: "Date", y: "cf_200", stroke: "blue"})),
Plot.dot(filtered, Plot.pointerX({x: "Date", y: "cf_2000", stroke: "red"})),
Plot.text(filtered, Plot.pointerX({px: "Date", py: "cf_2000", dy: +0.15, frameAnchor: "top-left", fontVariant: "tabular-nums", text: (d) => [`Date ${Plot.formatIsoDate(d.Date)}`, `200m ${d.cf_200?.toFixed(4)}`, `2000m ${d.cf_2000?.toFixed(4)}`].join(" ")})),
Plot.ruleY([0])
]
})
---
comments: false
page-layout: custom
format:
html:
margin-top: 0em
margin-bottom: 0em
minimal: true
smooth-scroll: true
fig-responsive: true
toc: false
echo: false
keep-hidden: true
code-tools: true
---
:::: {.hero-small .hero-gradient .hero-clouds}
::: {.container}
# **Clouds**
:::
::::
::::: {.container}
:::: {.about .px-7 .py-5 style="border-bottom: 1px solid #C8C7C7;"}
# The impact of cloud on GHG remote sensing is strongly dependent on the footprint size.
In **Data Drought in the Humid Tropics: How to Overcome the Cloud Barrier in Greenhouse Gas Remote Sensing**, we took a deep dive on the impact of clouds on data coverrage and revisit times, with a focus on the humid tropics, where we see the lowest data yields in all current greenhouse gas missions [GRL paper](https://doi.org/10.1029/2024GL108791).
Here, we provide a simple interactive example for cloud statistics from our data in the manuscript under review. You can choose different locations on the map and see the cloud-free likelihood statistics for a 200 or 2000m footprint and an according time-series. Note that the coverage is not fully global due to the computation demand of computing these likelihoods from 10m Sentinel-2 data.
::::
:::: {.plots .px-4 .py-5}
::: {.globe-section}
```{ojs}
//| panel: input
html`
<p class="mb-4">
Empty paragraph for now
</p>
`
viewof point = {
const width = 600.0
const height = width / 2;
const context = DOM.context2d(width, height);
const projection = d3.geoEqualEarth().fitSize([width, height], {type: "Sphere"});
const path = d3.geoPath(projection, context);
let mousedown = false;
context.beginPath(), path(graticule), context.strokeStyle = "#ccc", context.stroke();
context.beginPath(), path(land), context.fill();
context.beginPath(), path(sphere), context.strokeStyle = "#000", context.stroke();
context.lineWidth = 2, context.strokeStyle = "#f00";
const image = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
function render(coordinates) {
context.canvas.value = coordinates;
context.clearRect(0, 0, width, height);
context.putImageData(image, 0, 0);
context.beginPath(), path({type: "Point", coordinates}), context.stroke();
}
context.canvas.onmousedown = event => {
mousedown = true;
context.canvas.onmousemove(event);
};
context.canvas.onmousemove = (event) => {
if (!mousedown) return;
const rect = context.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
render(projection.invert([x, y]));
context.canvas.dispatchEvent(new CustomEvent("input"));
};
context.canvas.onmouseup = event => {
mousedown = false;
};
render([-55, 0]);
return context.canvas;
}
html`
<p class="mt-3 mb-4">
<strong>You picked:</strong> Latitude ${Math.round(point[1])}, Longitude ${Math.round(point[0])}
</p>
`
html`<label class="form-label">Pick a Date Range</label>`
viewof slider = rangeSlider({
min: 0,
max: d3.timeDay.count(start, end),
step: 1,
format: d => formatDate(d3.timeDay.offset(start, d)),
})
html`<label class="form-label mt-3">Number of bins</label>`
viewof thresh = Inputs.range(
[5, 50],
{value: 17, step: 2}
)
```
:::
```{ojs}
start = new Date(2018, 0)
end = new Date(2022,0)
```
```{ojs}
formatDate = d3.timeFormat("%Y-%m-%d")
```
```{ojs}
fileUrl = `https://raw.githubusercontent.com/cfranken/s2_data/main/files/S2_${Math.round(point[1])}.0_${Math.round(point[0])}.0.csv`;
//fileUrl = `/data/subset_data.csv`;
```
```{ojs}
binBoundaries1 = calculateLogBins(0.0001, 1.01, thresh);
```
```{ojs}
function calculateLogBins(start, end, nBins) {
const logStart = Math.log10(start);
const logEnd = Math.log10(end);
const bins = Array.from({length: nBins + 1}, (_, i) =>
Math.pow(10, logStart + (logEnd - logStart) * i / nBins)
);
return bins;
}
```
```{ojs}
// Custom tick format function
function customTickFormat(d) {
if (d < 0.1) return d.toFixed(2); // Display 1 as '1'
if (d < 1) return d.toFixed(1); // Display 0.01 as '0.01'
return d; // Default formatting for other values
}
```
::: {.panel-tabset}
## Plot
Below is a histogram of the cloud-free fractions for 200m footprints (blue) and 2000m footprints (red) on a logarithmic scale (from 0.1 to 100%)
<!-- For creating legend -->
::::: {.d-flex .gap-4 .my-2}
:::: {.d-flex .align-items-center .gap-2}
::: {style="width: 1.5rem; height: 1.5rem; background-color: #FFB2A4;"}
:::
2000m
::::
:::: {.d-flex .align-items-center .gap-2 .ps-4}
::: {style="width: 1.5rem; height: 1.5rem; background-color: #A2C0D8;"}
:::
200m
::::
:::::
```{ojs}
plot1 = Plot.plot({
width,
x: {
type: "log",
base:10,
transform: d => Math.max(.01, 100*d)
},
y: {grid: true},
style: {
fontSize: "12px"},
color: {legend: true},
marks: [
Plot.rectY(filtered, Plot.binX({y: "count"}, {x: "cf_200", fill: "steelblue", fillOpacity: 0.5,thresholds: binBoundaries1, tip: "xy" })),
Plot.rectY(filtered, Plot.binX({y: "count"}, {x: "cf_2000", fill: "tomato", fillOpacity: 0.5,thresholds: binBoundaries1, tip: "xy" })),
Plot.ruleY([0])
]
})
```
Below is the corresponding time-series in the same colors (You can hover over the data to get exact values at each time-step). You can find that there are extended times in the humid tropics, where larger pixels hamper data collection, specifically in the wet season. Try out regions in the Amazon or Indonesia, for example. Outside the humid tropics, the differences are far less severe, as large-scale cloud cover without gaps can persist, and so do larger areas without clouds. Thus, in many areas, footprints larger than 2km can be perfectly fine (e.g. with TROPOMI).
However, if you want to improve understanding in the humid tropics, fine spatial resolution is key!
```{ojs}
plot2 = Plot.plot({
width,
y: { grid: true, label: "Likelihood of cloud free pixel",domain:[0,1.099] },
color: { legend: true },
style: { fontSize: "12px" },
marginBottom: 37,
marks: [
Plot.axisX({ ticks: d3.utcMonth.every(3) }),
Plot.lineY(filtered, {
x: "Date",
y: "cf_200",
stroke: "steelblue", // Custom line color
fillOpacity: 0.75,
marker: "circle",
strokeWidth: 1.2 // Thin line
}),
Plot.lineY(filtered, {
x: "Date",
y: "cf_2000",
stroke: "tomato", // Custom line color
fillOpacity: 0.75,
marker: "circle",
strokeWidth: 1.5
}),
//Plot.ruleX(filtered, Plot.pointerX({x: "Date", py: "cf_2000", stroke: "black",fillOpacity: 0.75,strokeWidth: 0.8 })),
Plot.dot(filtered, Plot.pointerX({x: "Date", y: "cf_200", stroke: "blue"})),
Plot.dot(filtered, Plot.pointerX({x: "Date", y: "cf_2000", stroke: "red"})),
Plot.text(filtered, Plot.pointerX({px: "Date", py: "cf_2000", dy: +0.15, frameAnchor: "top-left", fontVariant: "tabular-nums", text: (d) => [`Date ${Plot.formatIsoDate(d.Date)}`, `200m ${d.cf_200?.toFixed(4)}`, `2000m ${d.cf_2000?.toFixed(4)}`].join(" ")})),
Plot.ruleY([0])
]
})
```
:::
```{ojs}
//data = FileAttachment("data/subset_data.csv").csv({ typed: true })
//data = d3.csv("https://raw.githubusercontent.com/cfranken/s2_data/main/files/1.0_-55.0.csv")
//print(fileUrl)
data = d3.csv(fileUrl, d3.autoType)
//data = FileAttachment("data/subset_data.csv").csv()
```
```{ojs}
startDate = d3.timeDay.offset(start, slider[0])
stopDate = d3.timeDay.offset(start, slider[1])
```
```{ojs}
filtered = data.filter(function(s2) {
return s2.Date >= startDate && s2.Date <= stopDate;
})
```
```{ojs}
sphere = ({type: "Sphere"})
```
```{ojs}
graticule = d3.geoGraticule10()
```
```{ojs}
land = topojson.feature(world, world.objects.land)
```
```{ojs}
world = FileAttachment("data/land-50m.json").json()
```
```{ojs}
topojson = require("topojson-client@3")
```
```{ojs}
import { rangeSlider as rangeSlider } from '@mootari/range-slider'
```
```{ojs}
import {Plot} from "@mkfreeman/plot-tooltip"
```
::::
:::::
Closing tropical data gaps to resolve global carbon-budget uncertainties