• Home
  • Team
  • Science
  • Publications
  • Visual Stories
    • CH4
    • Clouds
  • News

Clouds

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.

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.

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}
)
start = new Date(2018, 0)
end   = new Date(2022,0)
formatDate = d3.timeFormat("%Y-%m-%d")
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`;
binBoundaries1 = calculateLogBins(0.0001, 1.01, thresh);
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
}
  • 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%)

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])
  ]
})
//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()
startDate = d3.timeDay.offset(start, slider[0])
stopDate  = d3.timeDay.offset(start, slider[1])
filtered = data.filter(function(s2) {
  return s2.Date >= startDate && s2.Date <= stopDate;
})
sphere = ({type: "Sphere"})
graticule = d3.geoGraticule10()
land = topojson.feature(world, world.objects.land)
world = FileAttachment("data/land-50m.json").json()
topojson = require("topojson-client@3")
import { rangeSlider as rangeSlider } from '@mootari/range-slider'
import {Plot} from "@mkfreeman/plot-tooltip"
Source Code
---
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


Lead Institution


Instrument and
Mission Management

© 2025 California Institute of Technology. All Rights Reserved.


Spacecraft and
Mission Operations