Powered by AppSignal & Oban Pro

Earthmover CNG 2025: Datacubes and Chunk-Based Computation

earthmover_datacube.livemd

Earthmover CNG 2025: Datacubes and Chunk-Based Computation

Mix.install([
  {:ex_zarr, "~> 1.2"},
  # {:ex_zarr, path: Path.join(__DIR__, "../../..")}
  {:nx, "~> 0.9"},
  {:kino, "~> 0.13"},
  {:kino_vega_lite, "~> 0.1"}
])

What is a Datacube

A datacube is a multi-dimensional array representing spatiotemporal data. The term "cube" emphasizes organization along three or more axes, typically including:

  • Spatial dimensions: X and Y coordinates (longitude, latitude, or pixels)
  • Temporal dimension: Time (dates, timestamps, or sequence numbers)
  • Additional dimensions: Spectral bands, elevation levels, scenarios, or variables

Common Applications:

Earth Observation:

  • Satellite imagery time series
  • Climate model outputs
  • Weather forecasts
  • Land use classification

Scientific Analysis:

  • Medical imaging (3D + time + modality)
  • Oceanography (depth + lat + lon + time)
  • Atmospheric science (pressure levels + spatial + time)

Key Characteristics:

  1. Regular Grid: Data aligned to consistent spatial/temporal coordinates
  2. Multi-Dimensional: Typically 3D (space + time) or 4D (space + time + bands)
  3. Large Scale: Often too large for memory, requiring chunked access
  4. Analysis-Ready: Preprocessed, calibrated, and georeferenced
  5. Interoperable: Standard formats and metadata conventions

Why Zarr for Datacubes:

  • Chunked Storage: Selective access to spatial/temporal subsets
  • Compression: Reduce storage for repetitive patterns
  • Cloud-Native: Direct access from object storage (S3, GCS)
  • Parallel I/O: Multiple processes read/write simultaneously
  • Flexible Layout: Optimize chunks for access patterns

This notebook demonstrates datacube construction, chunk-based computation, and analysis workflows using ExZarr.

Building a 4D Zarr Structure

We'll create a 4D datacube representing multi-spectral satellite imagery:

Dimensions (demo-scale; same layout as a full-resolution cube):

  • Time: 12 months (monthly composites)
  • Band: 4 spectral bands (Red, Green, Blue, Near-Infrared)
  • Y: 128 pixels (latitude/northing)
  • X: 128 pixels (longitude/easting)

Total size: 12 × 4 × 128 × 128 ≈ 786K pixels

Note: Prefer ExZarr.Array.stream_chunks/2 for 4D reductions. Multi-chunk get_slice on ≥3D arrays currently falls back to element-wise assembly and is too slow for large spatial tiles.

defmodule DatacubeHelpers do
  def format_number(n) when n >= 1_000_000, do: "#{Float.round(n / 1_000_000, 1)}M"
  def format_number(n) when n >= 1_000, do: "#{Float.round(n / 1_000, 1)}K"
  def format_number(n), do: "#{n}"

  def get_slice(array, ranges) when is_tuple(ranges) do
    ranges_list = Tuple.to_list(ranges)
    start = ranges_list |> Enum.map(& &1.first) |> List.to_tuple()
    stop = ranges_list |> Enum.map(&(&1.last + 1)) |> List.to_tuple()
    ExZarr.Array.get_slice(array, start: start, stop: stop)
  end

  def get_slice_tensor(array, ranges) when is_tuple(ranges) do
    with {:ok, binary} <- get_slice(array, ranges) do
      shape =
        ranges
        |> Tuple.to_list()
        |> Enum.map(fn r -> r.last - r.first + 1 end)
        |> List.to_tuple()

      type =
        case array.dtype do
          :uint16 -> {:u, 16}
          :uint8 -> {:u, 8}
          :float32 -> {:f, 32}
          :float64 -> {:f, 64}
          :int32 -> {:s, 32}
          other -> other
        end

      {:ok, binary_to_tensor(binary, shape, type)}
    end
  end

  def write_tensor!(array, tensor) do
    # Chunk via Nx slices + write_stream — avoids slow ND set_slice splitting
    chunk_shape = Tuple.to_list(array.chunks)
    array_shape = Tuple.to_list(Nx.shape(tensor))

    chunk_counts =
      Enum.zip(array_shape, chunk_shape)
      |> Enum.map(fn {dim, chunk} -> div(dim + chunk - 1, chunk) end)

    indices =
      chunk_counts
      |> Enum.map(&((0)..(&1 - 1)))
      |> cartesian_product()

    chunks =
      Enum.map(indices, fn index_list ->
        ranges =
          Enum.zip([index_list, chunk_shape, array_shape])
          |> Enum.map(fn {idx, chunk, dim} ->
            start = idx * chunk
            stop = min(start + chunk, dim) - 1
            start..stop
          end)

        {List.to_tuple(index_list), Nx.to_binary(tensor[ranges])}
      end)

    {:ok, _} = ExZarr.Array.write_stream(array, chunks)
    :ok
  end

  defp cartesian_product([]), do: [[]]

  defp cartesian_product([range | rest]) do
    for i <- range, combo <- cartesian_product(rest), do: [i | combo]
  end

  def binary_to_tensor(binary, shape, type) do
    binary
    |> Nx.from_binary(type)
    |> Nx.reshape(shape)
  end
end

# Define datacube structure (demo scale — scale up shape/chunks for production)
time_steps = 12
bands = 4
height = 128
width = 128

shape = {time_steps, bands, height, width}

# Chunk strategy: one month, all bands, 64×64 spatial tiles
chunks = {1, 4, 64, 64}

IO.puts("Datacube specification:")
IO.puts("  Shape: #{inspect(shape)}")
IO.puts("  Chunks: #{inspect(chunks)}")
IO.puts("  Total elements: #{DatacubeHelpers.format_number(time_steps * bands * height * width)}")
IO.puts("  Chunk size: #{DatacubeHelpers.format_number(1 * 4 * 64 * 64)} elements")
IO.puts("  Number of chunks: #{div(time_steps, 1) * div(bands, 4) * div(height, 64) * div(width, 64)}")

19:54:11.888 [info] init got unexpected: {:io_request, #PID<0.99.0>, #Reference<0.624519451.972554244.56896>,
 {:put_chars, :unicode,
  "Failed to write log message to stdout, trying stderr\n"}}
Datacube specification:
  Shape: {12, 4, 128, 128}
  Chunks: {1, 4, 64, 64}
  Total elements: 786.4K
  Chunk size: 16.4K elements
  Number of chunks: 48
:ok
# Create the datacube array
{:ok, datacube} =
  ExZarr.create(
    shape: shape,
    chunks: chunks,
    dtype: :uint16,
    storage: :memory
  )

# Add metadata
datacube_attrs = %{
  "title" => "Multi-Spectral Satellite Datacube",
  "description" => "Monthly composites for 12 months, 4 spectral bands",
  "dimensions" => ["time", "band", "y", "x"],
  "coordinates" => %{
    "time" => "months 0-11 (Jan-Dec 2024)",
    "band" => "0=Red, 1=Green, 2=Blue, 3=NIR",
    "y" => "128 pixels (north-south)",
    "x" => "128 pixels (east-west)"
  },
  "spatial_resolution" => "30 meters",
  "crs" => "EPSG:32610",
  "units" => "digital numbers (0-65535)",
  "created_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}

# Attributes kept in `datacube_attrs` for this notebook (shown below)

IO.puts("Datacube created with metadata")
Datacube created with metadata
:ok
# Generate synthetic satellite imagery data
# Simulates realistic patterns: spatial autocorrelation, seasonal variation, spectral characteristics

defmodule DatacubeGenerator do
  @moduledoc """
  Generate synthetic multi-spectral satellite imagery (Nx-vectorized).
  """

  def generate_datacube(time_steps, bands, height, width) do
    IO.puts("Generating synthetic datacube data...")

    for t <- 0..(time_steps - 1) do
      IO.write("\rGenerating month #{t + 1}/#{time_steps}...")

      # Generate all bands for this time step
      band_data =
        for b <- 0..(bands - 1) do
          generate_band(t, b, height, width)
        end
        |> Nx.stack()

      band_data
    end
    |> Nx.stack()
    |> tap(fn _ -> IO.puts(" Done!") end)
  end

  defp generate_band(time, band, height, width) do
    base_value = band_base_value(band) * 1.0
    seasonal_factor = seasonal_variation(time, band)

    y_idx = Nx.iota({height, width}, axis: 0) |> Nx.as_type({:f, 32})
    x_idx = Nx.iota({height, width}, axis: 1) |> Nx.as_type({:f, 32})

    terrain =
      y_idx
      |> Nx.multiply(:math.pi() / 100)
      |> Nx.sin()
      |> Nx.multiply(x_idx |> Nx.multiply(:math.pi() / 100) |> Nx.cos())

    patches =
      y_idx
      |> Nx.multiply(:math.pi() / 25)
      |> Nx.sin()
      |> Nx.multiply(x_idx |> Nx.multiply(:math.pi() / 25) |> Nx.sin())

    key = Nx.Random.key(time * 17 + band + 1)
    {noise, _} = Nx.Random.uniform(key, shape: {height, width}, type: {:f, 32})
    noise = noise |> Nx.subtract(0.5) |> Nx.multiply(500.0)

    base_value
    |> Nx.add(Nx.multiply(Nx.divide(y_idx, height * 1.0), 2000.0))
    |> Nx.add(Nx.multiply(Nx.divide(x_idx, width * 1.0), 1000.0))
    |> Nx.add(Nx.multiply(terrain, 3000.0))
    |> Nx.add(Nx.multiply(patches, 2000.0))
    |> Nx.add(seasonal_factor * 5000.0)
    |> Nx.add(noise)
    |> Nx.max(0.0)
    |> Nx.min(65_535.0)
    |> Nx.round()
    |> Nx.as_type({:u, 16})
  end

  defp band_base_value(0), do: 8000
  defp band_base_value(1), do: 10_000
  defp band_base_value(2), do: 7000
  defp band_base_value(3), do: 15_000

  defp seasonal_variation(month, band) do
    angle = month * :math.pi() / 6

    case band do
      0 -> -:math.cos(angle) * 0.3
      3 -> :math.cos(angle) * 0.5
      _ -> :math.cos(angle) * 0.1
    end
  end
end

# Generate and write data
datacube_data =
  DatacubeGenerator.generate_datacube(time_steps, bands, height, width)

DatacubeHelpers.write_tensor!(datacube, datacube_data)

IO.puts("\nDatacube populated with synthetic satellite imagery")
IO.puts("Data type: #{inspect(Nx.type(datacube_data))}")
IO.puts("Data shape: #{inspect(Nx.shape(datacube_data))}")
Generating synthetic datacube data...
Generating month 12/12... Done!

19:54:19.040 [debug] loaded module at Elixir.ExZarr.Codecs.ZigCodecs

Datacube populated with synthetic satellite imagery
Data type: {:u, 16}
Data shape: {12, 4, 128, 128}
:ok
# Inspect datacube structure
metadata = datacube.metadata
attrs = datacube_attrs

datacube_info = """
## Datacube Structure

**Dimensions:** #{inspect(metadata.shape)}
- Time: #{elem(metadata.shape, 0)} months
- Bands: #{elem(metadata.shape, 1)} spectral bands
- Y: #{elem(metadata.shape, 2)} pixels
- X: #{elem(metadata.shape, 3)} pixels

**Chunks:** #{inspect(metadata.chunks)}
- Temporal: #{elem(metadata.chunks, 0)} month(s) per chunk
- Spectral: #{elem(metadata.chunks, 1)} band(s) per chunk
- Spatial: #{elem(metadata.chunks, 2)}×#{elem(metadata.chunks, 3)} pixels per chunk

**Storage:**
- Data type: #{metadata.dtype}
- Compressor: #{inspect(metadata.compressor)}
- Bytes per element: 2
- Uncompressed size: #{elem(metadata.shape, 0) * elem(metadata.shape, 1) * elem(metadata.shape, 2) * elem(metadata.shape, 3) * 2 / 1_048_576 |> Float.round(2)} MB

**Metadata:**
- Title: #{attrs["title"]}
- Spatial resolution: #{attrs["spatial_resolution"]}
- Coordinate system: #{attrs["crs"]}
"""

Kino.Markdown.new(datacube_info)

Chunk-Aware Computation

Chunk-aware algorithms process data in blocks, enabling computation on datasets larger than memory. Benefits:

  1. Memory Efficiency: Process one chunk at a time
  2. Parallelism: Independent chunks can be processed concurrently
  3. I/O Optimization: Read only required chunks
  4. Scalability: Handle arbitrarily large datasets

Strategy:

  • Prefer ExZarr.Array.stream_chunks/2 for full-cube reductions
  • Decode each chunk binary with Nx (skip multi-chunk 4D get_slice)
  • Aggregate into the result grid
  • Move to the next chunk

Let's compute statistics chunk-by-chunk:

defmodule ChunkProcessor do
  @moduledoc """
  Process Zarr datacube chunk-by-chunk.
  """

  def compute_temporal_mean(datacube) do
    metadata = datacube.metadata
    {time_steps, bands, height, width} = metadata.shape
    {_t_chunk, _b_chunk, y_chunk, x_chunk} = metadata.chunks

    IO.puts("Computing temporal mean across #{time_steps} time steps...")
    IO.puts("Streaming #{div(height, y_chunk) * div(width, x_chunk)} spatial tiles via stream_chunks/2")

    {sums, counts, done} =
      datacube
      |> ExZarr.Array.stream_chunks(concurrency: 4, ordered: false)
      |> Enum.reduce({%{}, %{}, 0}, fn {index, binary}, {sums, counts, done} ->
        {_t, _b, y_i, x_i} = index
        key = {y_i, x_i}

        tile =
          binary
          |> Nx.from_binary({:u, 16})
          |> Nx.reshape({bands, y_chunk, x_chunk})
          |> Nx.as_type({:f, 32})

        sums = Map.update(sums, key, tile, fn existing -> Nx.add(existing, tile) end)
        counts = Map.update(counts, key, 1, &(&1 + 1))
        done = done + 1

        if rem(done, 4) == 0, do: IO.write("\rProcessed #{done} chunks...")

        {sums, counts, done}
      end)

    IO.write("\rProcessed #{done} chunks...")

    result =
      Enum.reduce(sums, Nx.broadcast(0.0, {bands, height, width}) |> Nx.as_type({:f, 32}), fn
        {{y_i, x_i}, sum_tile}, acc ->
          mean = Nx.divide(sum_tile, counts[{y_i, x_i}])
          Nx.put_slice(acc, [0, y_i * y_chunk, x_i * x_chunk], mean)
      end)

    IO.puts(" Done!")
    result
  end

  def compute_ndvi_time_series(datacube, y_pixel, x_pixel) do
    metadata = datacube.metadata
    {time_steps, _bands, _height, _width} = metadata.shape
    {_t_chunk, _b_chunk, y_chunk, x_chunk} = metadata.chunks

    IO.puts("Computing NDVI time series at pixel (#{y_pixel}, #{x_pixel})...")

    y_i = div(y_pixel, y_chunk)
    x_i = div(x_pixel, x_chunk)
    y_off = rem(y_pixel, y_chunk)
    x_off = rem(x_pixel, x_chunk)

    by_time =
      datacube
      |> ExZarr.Array.stream_chunks(
        concurrency: 4,
        ordered: false,
        filter: fn
          {_t, _b, ^y_i, ^x_i} -> true
          _ -> false
        end
      )
      |> Enum.reduce(%{}, fn {{t, _b, _, _}, binary}, acc ->
        tile =
          binary
          |> Nx.from_binary({:u, 16})
          |> Nx.reshape({4, y_chunk, x_chunk})

        red = tile[[0, y_off, x_off]] |> Nx.as_type({:f, 32}) |> Nx.to_number()
        nir = tile[[3, y_off, x_off]] |> Nx.as_type({:f, 32}) |> Nx.to_number()
        Map.put(acc, t, {red, nir})
      end)

    ndvi_values =
      for t <- 0..(time_steps - 1) do
        {red, nir} = Map.fetch!(by_time, t)
        denom = nir + red
        if denom == 0.0, do: 0.0, else: (nir - red) / denom
      end

    IO.puts("NDVI computation complete")
    ndvi_values
  end

  def chunk_statistics(datacube) do
    metadata = datacube.metadata
    {time_steps, bands, height, width} = metadata.shape
    {t_chunk, b_chunk, y_chunk, x_chunk} = metadata.chunks

    num_t_chunks = div(time_steps, t_chunk)
    num_y_chunks = div(height, y_chunk)
    num_x_chunks = div(width, x_chunk)
    total_chunks = num_t_chunks * div(bands, b_chunk) * num_y_chunks * num_x_chunks

    IO.puts("Analyzing chunk access patterns...")
    IO.puts("Total chunks: #{total_chunks}")
    IO.puts("  Temporal chunks: #{num_t_chunks}")
    IO.puts("  Spatial chunks (Y): #{num_y_chunks}")
    IO.puts("  Spatial chunks (X): #{num_x_chunks}")

    %{
      total_chunks: total_chunks,
      temporal_chunks: num_t_chunks,
      spatial_y_chunks: num_y_chunks,
      spatial_x_chunks: num_x_chunks,
      elements_per_chunk: t_chunk * b_chunk * y_chunk * x_chunk,
      bytes_per_chunk: t_chunk * b_chunk * y_chunk * x_chunk * 2
    }
  end
end

# Compute chunk statistics
stats = ChunkProcessor.chunk_statistics(datacube)

chunk_stats = """
## Chunk Analysis

**Total Chunks:** #{stats.total_chunks}

**Chunk Distribution:**
- Temporal: #{stats.temporal_chunks} chunks (#{elem(metadata.chunks, 0)} month(s) each)
- Spatial Y: #{stats.spatial_y_chunks} chunks (#{elem(metadata.chunks, 2)} pixels each)
- Spatial X: #{stats.spatial_x_chunks} chunks (#{elem(metadata.chunks, 3)} pixels each)

**Chunk Size:**
- Elements: #{DatacubeHelpers.format_number(stats.elements_per_chunk)}
- Bytes: #{Float.round(stats.bytes_per_chunk / 1024, 1)} KB

**Access Patterns:**

**Temporal Query** (one location, all times):
- Chunks accessed: #{stats.temporal_chunks}
- Data read: #{Float.round(stats.temporal_chunks * stats.bytes_per_chunk / 1024, 1)} KB

**Spatial Query** (one time, spatial region):
- Example: #{elem(metadata.chunks, 2)}×#{elem(metadata.chunks, 3)} region
- Chunks accessed: 1
- Data read: #{Float.round(stats.bytes_per_chunk / 1024, 1)} KB

**Full Scan** (all data):
- Chunks accessed: #{stats.total_chunks}
- Data read: #{Float.round(stats.total_chunks * stats.bytes_per_chunk / 1_048_576, 1)} MB
"""

Kino.Markdown.new(chunk_stats)
Analyzing chunk access patterns...
Total chunks: 48
  Temporal chunks: 12
  Spatial chunks (Y): 2
  Spatial chunks (X): 2
# Compute temporal mean (chunk-by-chunk processing)
temporal_mean = ChunkProcessor.compute_temporal_mean(datacube)

IO.puts("\nTemporal mean computed:")
IO.puts("  Result shape: #{inspect(Nx.shape(temporal_mean))}")
IO.puts("  Result type: #{inspect(Nx.type(temporal_mean))}")
Computing temporal mean across 12 time steps...
Streaming 4 spatial tiles via stream_chunks/2
Processed 48 chunks... Done!

Temporal mean computed:
  Result shape: {4, 128, 128}
  Result type: {:f, 32}
:ok

Zonal Statistics Example

Zonal statistics compute aggregates over spatial regions (zones). Common in:

  • Agricultural monitoring (field-level statistics)
  • Urban planning (neighborhood metrics)
  • Ecological studies (habitat analysis)
  • Disaster response (affected area assessment)

Example: Compute mean NDVI by elevation zone

defmodule ZonalStats do
  @moduledoc """
  Compute zonal statistics on datacube.
  """

  def compute_zonal_mean(datacube, zone_map) do
    metadata = datacube.metadata
    {time_steps, _bands, _height, _width} = metadata.shape
    {_t_chunk, _b_chunk, y_chunk, x_chunk} = metadata.chunks
    last_time = time_steps - 1

    zones =
      zone_map
      |> Nx.flatten()
      |> Nx.to_flat_list()
      |> Enum.uniq()
      |> Enum.sort()

    IO.puts("Computing zonal statistics for #{length(zones)} zones...")

    zone_accumulators =
      Map.new(zones, fn zone ->
        {zone, %{sum_ndvi: 0.0, count: 0}}
      end)

    zone_results =
      datacube
      |> ExZarr.Array.stream_chunks(
        concurrency: 4,
        ordered: false,
        filter: fn
          {^last_time, _b, _y, _x} -> true
          _ -> false
        end
      )
      |> Enum.reduce(zone_accumulators, fn {{_t, _b, y_i, x_i}, binary}, acc ->
        y_start = y_i * y_chunk
        x_start = x_i * x_chunk

        tile =
          binary
          |> Nx.from_binary({:u, 16})
          |> Nx.reshape({4, y_chunk, x_chunk})
          |> Nx.as_type({:f, 32})

        red = tile[0]
        nir = tile[3]
        denom = Nx.add(nir, red)

        ndvi =
          Nx.select(
            Nx.equal(denom, 0),
            0.0,
            Nx.divide(Nx.subtract(nir, red), denom)
          )

        zone_chunk = Nx.slice(zone_map, [y_start, x_start], [y_chunk, x_chunk])

        for y <- 0..(y_chunk - 1), x <- 0..(x_chunk - 1), reduce: acc do
          zone_acc ->
            zone_id = Nx.to_number(zone_chunk[y][x])
            ndvi_value = Nx.to_number(ndvi[y][x])

            Map.update!(zone_acc, zone_id, fn zone_data ->
              %{
                zone_data
                | sum_ndvi: zone_data.sum_ndvi + ndvi_value,
                  count: zone_data.count + 1
              }
            end)
        end
      end)

    IO.puts(" Done!")

    Map.new(zone_results, fn {zone, data} ->
      mean_ndvi = if data.count > 0, do: data.sum_ndvi / data.count, else: 0.0
      {zone, %{mean_ndvi: mean_ndvi, pixel_count: data.count}}
    end)
  end

  def create_elevation_zones(height, width, num_zones \\ 5) do
    # Create synthetic elevation zones (bands across the image)
    for y <- 0..(height - 1), x <- 0..(width - 1) do
      # Assign zone based on Y coordinate (horizontal bands)
      div(y * num_zones, height)
    end
    |> Nx.tensor(type: {:s, 32})
    |> Nx.reshape({height, width})
  end
end

# Create elevation zones
IO.puts("Creating elevation zone map...")
zone_map = ZonalStats.create_elevation_zones(height, width, 5)

IO.puts("Zone map created: #{inspect(Nx.shape(zone_map))}")
IO.puts("Zones: 0 (low elevation) to 4 (high elevation)")
warning: variable "x" is unused (if the variable is not meant to be used, prefix it with an underscore)
└─ work/ExZarr/docs/livebooks/earthmover_datacube.livemd#cell:zejctdp3zizkzzkl:84: ZonalStats.create_elevation_zones/3

Creating elevation zone map...
Zone map created: {128, 128}
Zones: 0 (low elevation) to 4 (high elevation)
:ok
# Compute zonal statistics
zonal_results = ZonalStats.compute_zonal_mean(datacube, zone_map)

elevation_labels = %{
  0 => "Low",
  1 => "Medium-Low",
  2 => "Medium",
  3 => "Medium-High",
  4 => "High"
}

zonal_table_data =
  zonal_results
  |> Enum.sort_by(fn {zone, _data} -> zone end)
  |> Enum.map(fn {zone, data} ->
    %{
      "Zone" => zone,
      "Elevation" => Map.get(elevation_labels, zone, "Unknown"),
      "Mean NDVI" => Float.round(data.mean_ndvi, 3),
      "Pixels" => data.pixel_count
    }
  end)

Kino.DataTable.new(zonal_table_data)
Computing zonal statistics for 5 zones...
 Done!
[%{"Elevation" => "Low", "Mean NDVI" => 0.421, "Pixels" => 3328, "Zone" => 0}, %{"Elevation" => "Medium-Low", "Mean NDVI" => 0.433, "Pixels" => 3328, "Zone" => 1}, %{"Elevation" => "Medium", "Mean NDVI" => 0.409, "Pixels" => 3200, "Zone" => 2}, %{"Elevation" => "Medium-High", "Mean NDVI" => 0.39, "Pixels" => 3328, "Zone" => 3}, %{"Elevation" => "High", "Mean NDVI" => 0.362, "Pixels" => 3200, "Zone" => 4}]
# Compute NDVI time series for a sample location
sample_y = div(height, 2)
sample_x = div(width, 2)

ndvi_time_series = ChunkProcessor.compute_ndvi_time_series(datacube, sample_y, sample_x)

IO.puts("NDVI Time Series at (#{sample_y}, #{sample_x}):")
IO.inspect(Enum.map(ndvi_time_series, &Float.round(&1, 3)))
Computing NDVI time series at pixel (64, 64)...
NDVI computation complete
NDVI Time Series at (64, 64):
[0.392, 0.365, 0.316, 0.25, 0.191, 0.125, 0.101, 0.123, 0.191, 0.252, 0.328,
 0.38]
[0.392, 0.365, 0.316, 0.25, 0.191, 0.125, 0.101, 0.123, 0.191, 0.252, 0.328, 0.38]

Key Observations:

  1. Chunk-by-chunk processing enables computation on large datasets
  2. Spatial chunking optimizes for region-based queries
  3. Temporal chunking optimizes for time series analysis
  4. Zone-based aggregation reduces many pixels to summary statistics

Stop any still-running old compute_temporal_mean cell, then re-evaluate from the datacube-creation cells (shape is now 128×128; mean uses stream_chunks).

Visualization: Spatial Map

Visualize a spatial slice of the datacube using VegaLite.

alias VegaLite, as: Vl

# Extract spatial slice via stream_chunks (avoids slow 4D get_slice assembly)
last_time = time_steps - 1
{_t_c, _b_c, y_c, x_c} = metadata.chunks

spatial_2d =
  datacube
  |> ExZarr.Array.stream_chunks(
    concurrency: 4,
    ordered: false,
    filter: fn
      {^last_time, _b, _y, _x} -> true
      _ -> false
    end
  )
  |> Enum.reduce(Nx.broadcast(0, {height, width}) |> Nx.as_type({:u, 16}), fn
    {{_t, _b, y_i, x_i}, binary}, acc ->
      tile =
        binary
        |> Nx.from_binary({:u, 16})
        |> Nx.reshape({4, y_c, x_c})

      Nx.put_slice(acc, [y_i * y_c, x_i * x_c], tile[0])
  end)

# Downsample for visualization
downsample_factor = 4
viz_height = div(height, downsample_factor)
viz_width = div(width, downsample_factor)

viz_data =
  for y <- 0..(viz_height - 1), x <- 0..(viz_width - 1) do
    orig_y = y * downsample_factor
    orig_x = x * downsample_factor
    value = Nx.to_number(spatial_2d[[orig_y, orig_x]])

    %{x: x, y: y, value: value}
  end

Vl.new(width: 500, height: 500, title: "Red Band - Month 12 (Downsampled 4x)")
|> Vl.data_from_values(viz_data)
|> Vl.mark(:rect)
|> Vl.encode_field(:x, "x", type: :ordinal, title: "X (pixels / 4)")
|> Vl.encode_field(:y, "y", type: :ordinal, title: "Y (pixels / 4)", sort: "descending")
|> Vl.encode_field(:color, "value",
  type: :quantitative,
  scale: [scheme: "viridis"],
  title: "DN"
)
{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","data":{"values":[{"value":6601,"x":0,"y":0},{"value":6708,"x":1,"y":0},{"value":6788,"x":2,"y":0},{"value":6799,"x":3,"y":0},{"value":6819,"x":4,"y":0},{"value":7037,"x":5,"y":0},{"value":6670,"x":6,"y":0},{"value":7000,"x":7,"y":0},{"value":7070,"x":8,"y":0},{"value":6849,"x":9,"y":0},{"value":7136,"x":10,"y":0},{"value":7026,"x":11,"y":0},{"value":6976,"x":12,"y":0},{"value":7227,"x":13,"y":0},{"value":7322,"x":14,"y":0},{"value":7101,"x":15,"y":0},{"value":6978,"x":16,"y":0},{"value":7061,"x":17,"y":0},{"value":7431,"x":18,"y":0},{"value":7491,"x":19,"y":0},{"value":7094,"x":20,"y":0},{"value":7211,"x":21,"y":0},{"value":7190,"x":22,"y":0},{"value":7376,"x":23,"y":0},{"value":7651,"x":24,"y":0},{"value":7416,"x":25,"y":0},{"value":7585,"x":26,"y":0},{"value":7596,"x":27,"y":0},{"value":7383,"x":28,"y":0},{"value":7581,"x":29,"y":0},{"value":7485,"x":30,"y":0},{"value":7464,"x":31,"y":0},{"value":7248,"x":0,"y":1},{"value":7546,"x":1,"y":1},{"value":7931,"x":2,"y":1},{"value":8302,"x":3,"y":1},{"value":7964,"x":4,"y":1},{"value":7700,"x":5,"y":1},{"value":7187,"x":6,"y":1},{"value":7107,"x":7,"y":1},{"value":6528,"x":8,"y":1},{"value":6258,"x":9,"y":1},{"value":6047,"x":10,"y":1},{"value":6310,"x":11,"y":1},{"value":7158,"x":12,"y":1},{"value":7543,"x":13,"y":1},{"value":7566,"x":14,"y":1},{"value":7813,"x":15,"y":1},{"value":8125,"x":16,"y":1},{"value":7753,"x":17,"y":1},{"value":7371,"x":18,"y":1},{"value":6907,"x":19,"y":1},{"value":6366,"x":20,"y":1},{"value":6269,"x":21,"y":1},{"value":6279,"x":22,"y":1},{"value":6518,"x":23,"y":1},{"value":6445,"x":24,"y":1},{"value":7317,"x":25,"y":1},{"value":7589,"x":26,"y":1},{"value":7875,"x":27,"y":1},{"value":8295,"x":28,"y":1},{"value":8234,"x":29,"y":1},{"value":8135,"x":30,"y":1},{"value":7546,"x":31,"y":1},{"value":7433,"x":0,"y":2},{"value":8213,"x":1,"y":2},{"value":9132,"x":2,"y":2},{"value":9317,"x":3,"y":2},{"value":9157,"x":4,"y":2},{"value":8364,"x":5,"y":2},{"value":7801,"x":6,"y":2},{"value":6665,"x":7,"y":2},{"value":6209,"x":8,"y":2},{"value":5949,"x":9,"y":2},{"value":5690,"x":10,"y":2},{"value":6226,"x":11,"y":2},{"value":6660,"x":12,"y":2},{"value":7748,"x":13,"y":2},{"value":8039,"x":14,"y":2},{"value":8544,"x":15,"y":2},{"value":8786,"x":16,"y":2},{"value":8407,"x":17,"y":2},{"value":7729,"x":18,"y":2},{"value":6863,"x":19,"y":2},{"value":6104,"x":20,"y":2},{"value":5420,"x":21,"y":2},{"value":5149,"x":22,"y":2},{"value":5502,"x":23,"y":2},{"value":6208,"x":24,"y":2},{"value":6893,"x":25,"y":2},{"value":7863,"x":26,"y":2},{"value":8371,"x":27,"y":2},{"value":8799,"x":28,"y":2},{"value":8487,"x":29,"y":2},{"value":8262,"x":30,"y":2},{"value":7653,"x":31,"y":2},{"value":7999,"x":0,"y":3},{"value":8955,"x":1,"y":3},{"value":9762,"x":2,"y":3},{"value":10078,"x":3,"y":3},{"value":9794,"x":4,"y":3},{"value":9178,"x":5,"y":3},{"value":7966,"x":6,"y":3},{"value":7279,"x":7,"y":3},{"value":6166,"x":8,"y":3},{"value":5468,"x":9,"y":3},{"value":5465,"x":10,"y":3},{"value":6162,"x":11,"y":3},{"value":6673,"x":12,"y":3},{"value":7574,"x":13,"y":3},{"value":8645,"x":14,"y":3},{"value":8821,"x":15,"y":3},{"value":8789,"x":16,"y":3},{"value":8373,"x":17,"y":3},{"value":7417,"x":18,"y":3},{"value":6244,"x":19,"y":3},{"value":5519,"x":20,"y":3},{"value":4831,"x":21,"y":3},{"value":4330,"x":22,"y":3},{"value":4616,"x":23,"y":3},{"value":5347,"x":24,"y":3},{"value":6722,"x":25,"y":3},{"value":7735,"x":26,"y":3},{"value":8572,"x":27,"y":3},{"value":8636,"x":28,"y":3},{"value":8485,"x":29,"y":3},{"value":7889,"x":30,"y":3},{"value":7134,"x":31,"y":3},{"value":8509,"x":0,"y":4},{"value":9496,"x":1,"y":4},{"value":10007,"x":2,"y":4},{"value":10077,"x":3,"y":4},{"value":9903,"x":4,"y":4},{"value":9406,"x":5,"y":4},{"value":8356,"x":6,"y":4},{"value":7406,"x":7,"y":4},{"value":6583,"x":8,"y":4},{"value":6120,"x":9,"y":4},{"value":6076,"x":10,"y":4},{"value":6198,"x":11,"y":4},{"value":7091,"x":12,"y":4},{"value":7848,"x":13,"y":4},{"value":8399,"x":14,"y":4},{"value":8928,"x":15,"y":4},{"value":8425,"x":16,"y":4},{"value":7879,"x":17,"y":4},{"value":7332,"x":18,"y":4},{"value":6077,"x":19,"y":4},{"value":5250,"x":20,"y":4},{"value":4812,"x":21,"y":4},{"value":4534,"x":22,"y":4},{"value":4640,"x":23,"y":4},{"value":5631,"x":24,"y":4},{"value":6360,"x":25,"y":4},{"value":7421,"x":26,"y":4},{"value":7805,"x":27,"y":4},{"value":8128,"x":28,"y":4},{"value":8438,"x":29,"y":4},{"value":7538,"x":30,"y":4},{"value":6992,"x":31,"y":4},{"value":8598,"x":0,"y":5},{"value":9258,"x":1,"y":5},{"value":9687,"x":2,"y":5},{"value":10003,"x":3,"y":5},{"value":9993,"x":4,"y":5},{"value":9242,"x":5,"y":5},{"value":8438,"x":6,"y":5},{"value":7746,"x":7,"y":5},{"value":7377,"x":8,"y":5},{"value":6832,"x":9,"y":5},{"value":6828,"x":10,"y":5},{"value":6890,"x":11,"y":5},{"value":7211,"x":12,"y":5},{"value":7509,"x":13,"y":5},{"value":8022,"x":14,"y":5},{"value":8198,"x":15,"y":5},{"value":7929,"x":16,"y":5},{"value":7402,"x":17,"y":5},{"value":7038,"x":18,"y":5},{"value":5986,"x":19,"y":5},{"value":5555,"x":20,"y":5},{"value":5004,"x":21,"y":5},{"value":4906,"x":22,"y":5},{"value":4884,"x":23,"y":5},{"value":5517,"x":24,"y":5},{"value":5923,"x":25,"y":5},{"value":6575,"x":26,"y":5},{"value":7095,"x":27,"y":5},{"value":7502,"x":28,"y":5},{"value":7234,"x":29,"y":5},{"value":7185,"x":30,"y":5},{"value":6861,"x":31,"y":5},{"value":8935,"x":0,"y":6},{"value":9479,"x":1,"y":6},{"value":9555,"x":2,"y":6},{"value":9279,"x":3,"y":6},{"value":9188,"x":4,"y":6},{"value":9046,"x":5,"y":6},{"value":8930,"x":6,"y":6},{"value":8397,"x":7,"y":6},{"value":8405,"x":8,"y":6},{"value":7770,"x":9,"y":6},{"value":8006,"x":10,"y":6},{"value":7880,"x":11,"y":6},{"value":7536,"x":12,"y":6},{"value":7305,"x":13,"y":6},{"value":7439,"x":14,"y":6},{"value":6935,"x":15,"y":6},{"value":7198,"x":16,"y":6},{"value":6862,"x":17,"y":6},{"value":6223,"x":18,"y":6},{"value":6237,"x":19,"y":6},{"value":6107,"x":20,"y":6},{"value":5806,"x":21,"y":6},{"value":5543,"x":22,"y":6},{"value":5617,"x":23,"y":6},{"value":5771,"x":24,"y":6},{"value":6002,"x":25,"y":6},{"value":5738,"x":26,"y":6},{"value":5911,"x":27,"y":6},{"value":6535,"x":28,"y":6},{"value":6538,"x":29,"y":6},{"value":6428,"x":30,"y":6},{"value":6593,"x":31,"y":6},{"value":9241,"x":0,"y":7},{"value":8894,"x":1,"y":7},{"value":8995,"x":2,"y":7},{"value":8638,"x":3,"y":7},{"value":8508,"x":4,"y":7},{"value":8564,"x":5,"y":7},{"value":8745,"x":6,"y":7},{"value":8864,"x":7,"y":7},{"value":9001,"x":8,"y":7},{"value":9148,"x":9,"y":7},{"value":8718,"x":10,"y":7},{"value":8418,"x":11,"y":7},{"value":7811,"x":12,"y":7},{"value":6988,"x":13,"y":7},{"value":6698,"x":14,"y":7},{"value":5992,"x":15,"y":7},{"value":6093,"x":16,"y":7},{"value":5931,"x":17,"y":7},{"value":5838,"x":18,"y":7},{"value":5971,"x":19,"y":7},{"value":6222,"x":20,"y":7},{"value":6404,"x":21,"y":7},{"value":6275,"x":22,"y":7},{"value":6260,"x":23,"y":7},{"value":6141,"x":24,"y":7},{"value":5608,"x":25,"y":7},{"value":5200,"x":26,"y":7},{"value":5248,"x":27,"y":7},{"value":4956,"x":28,"y":7},{"value":5200,"x":29,"y":7},{"value":5942,"x":30,"y":7},{"value":6549,"x":31,"y":7},{"value":9503,"x":0,"y":8},{"value":8838,"x":1,"y":8},{"value":8438,"x":2,"y":8},{"value":8211,"x":3,"y":8},{"value":8289,"x":4,"y":8},{"value":8553,"x":5,"y":8},{"value":9006,"x":6,"y":8},{"value":9540,"x":7,"y":8},{"value":10202,"x":8,"y":8},{"value":9904,"x":9,"y":8},{"value":9816,"x":10,"y":8},{"value":9167,"x":11,"y":8},{"value":7906,"x":12,"y":8},{"value":7224,"x":13,"y":8},{"value":6325,"x":14,"y":8},{"value":5327,"x":15,"y":8},{"value":5253,"x":16,"y":8},{"value":5062,"x":17,"y":8},{"value":5589,"x":18,"y":8},{"value":5975,"x":19,"y":8},{"value":6632,"x":20,"y":8},{"value":6931,"x":21,"y":8},{"value":7149,"x":22,"y":8},{"value":6545,"x":23,"y":8},{"value":6303,"x":24,"y":8},{"value":5242,"x":25,"y":8},{"value":4525,"x":26,"y":8},{"value":4102,"x":27,"y":8},{"value":4242,"x":28,"y":8},{"value":4366,"x":29,"y":8},{"value":5196,"x":30,"y":8},{"value":6132,"x":31,"y":8},{"value":9850,"x":0,"y":9},{"value":9098,"x":1,"y":9},{"value":8082,"x":2,"y":9},{"value":7883,"x":3,"y":9},{"value":8020,"x":4,"y":9},{"value":8365,"x":5,"y":9},{"value":9196,"x":6,"y":9},{"value":10034,"x":7,"y":9},{"value":10612,"x":8,"y":9},{"value":10875,"x":9,"y":9},{"value":10475,"x":10,"y":9},{"value":9499,"x":11,"y":9},{"value":8096,"x":12,"y":9},{"value":7058,"x":13,"y":9},{"value":5612,"x":14,"y":9},{"value":5251,"x":15,"y":9},{"value":4633,"x":16,"y":9},{"value":5038,"x":17,"y":9},{"value":5154,"x":18,"y":9},{"value":6329,"x":19,"y":9},{"value":6770,"x":20,"y":9},{"value":7357,"x":21,"y":9},{"value":7417,"x":22,"y":9},{"value":7169,"x":23,"y":9},{"value":6020,"x":24,"y":9},{"value":5390,"x":25,"y":9},{"value":4388,"x":26,"y":9},{"value":3742,"x":27,"y":9},{"value":3524,"x":28,"y":9},{"value":3886,"x":29,"y":9},{"value":4703,"x":30,"y":9},{"value":5974,"x":31,"y":9},{"value":10141,"x":0,"y":10},{"value":9440,"x":1,"y":10},{"value":8740,"x":2,"y":10},{"value":8205,"x":3,"y":10},{"value":8433,"x":4,"y":10},{"value":8801,"x":5,"y":10},{"value":9394,"x":6,"y":10},{"value":10114,"x":7,"y":10},{"value":10540,"x":8,"y":10},{"value":10487,"x":9,"y":10},{"value":10403,"x":10,"y":10},{"value":9321,"x":11,"y":10},{"value":8547,"x":12,"y":10},{"value":6916,"x":13,"y":10},{"value":5743,"x":14,"y":10},{"value":5255,"x":15,"y":10},{"value":4991,"x":16,"y":10},{"value":4672,"x":17,"y":10},{"value":5431,"x":18,"y":10},{"value":6041,"x":19,"y":10},{"value":6568,"x":20,"y":10},{"value":7445,"x":21,"y":10},{"value":7357,"x":22,"y":10},{"value":7123,"x":23,"y":10},{"value":6017,"x":24,"y":10},{"value":5193,"x":25,"y":10},{"value":4253,"x":26,"y":10},{"value":3685,"x":27,"y":10},{"value":3660,"x":28,"y":10},{"value":4172,"x":29,"y":10},{"value":4718,"x":30,"y":10},{"value":5844,"x":31,"y":10},{"value":10121,"x":0,"y":11},{"value":9850,"x":1,"y":11},{"value":8918,"x":2,"y":11},{"value":8860,"x":3,"y":11},{"value":8974,"x":4,"y":11},{"value":9319,"x":5,"y":11},{"value":9657,"x":6,"y":11},{"value":9786,"x":7,"y":11},{"value":10416,"x":8,"y":11},{"value":10048,"x":9,"y":11},{"value":9839,"x":10,"y":11},{"value":9239,"x":11,"y":11},{"value":8295,"x":12,"y":11},{"value":7505,"x":13,"y":11},{"value":6460,"x":14,"y":11},{"value":5731,"x":15,"y":11},{"value":5527,"x":16,"y":11},{"value":5197,"x":17,"y":11},{"value":5424,"x":18,"y":11},{"value":6066,"x":19,"y":11},{"value":6669,"x":20,"y":11},{"value":6891,"x":21,"y":11},{"value":6463,"x":22,"y":11},{"value":6204,"x":23,"y":11},{"value":6025,"x":24,"y":11},{"value":5038,"x":25,"y":11},{"value":4492,"x":26,"y":11},{"value":4059,"x":27,"y":11},{"value":4292,"x":28,"y":11},{"value":4429,"x":29,"y":11},{"value":4963,"x":30,"y":11},{"value":5944,"x":31,"y":11},{"value":10625,"x":0,"y":12},{"value":10069,"x":1,"y":12},{"value":9872,"x":2,"y":12},{"value":9764,"x":3,"y":12},{"value":9824,"x":4,"y":12},{"value":9525,"x":5,"y":12},{"value":9517,"x":6,"y":12},{"value":9843,"x":7,"y":12},{"value":9870,"x":8,"y":12},{"value":9643,"x":9,"y":12},{"value":9083,"x":10,"y":12},{"value":8616,"x":11,"y":12},{"value":8132,"x":12,"y":12},{"value":7297,"x":13,"y":12},{"value":7170,"x":14,"y":12},{"value":6391,"x":15,"y":12},{"value":6431,"x":16,"y":12},{"value":6004,"x":17,"y":12},{"value":5770,"x":18,"y":12},{"value":6022,"x":19,"y":12},{"value":5734,"x":20,"y":12},{"value":5786,"x":21,"y":12},{"value":5888,"x":22,"y":12},{"value":5734,"x":23,"y":12},{"value":5264,"x":24,"y":12},{"value":5127,"x":25,"y":12},{"value":4956,"x":26,"y":12},{"value":4879,"x":27,"y":12},{"value":5007,"x":28,"y":12},{"value":5041,"x":29,"y":12},{"value":5909,"x":30,"y":12},{"value":6340,"x":31,"y":12},{"value":10558,"x":0,"y":13},{"value":10732,"x":1,"y":13},{"value":10711,"x":2,"y":13},{"value":11128,"x":3,"y":13},{"value":10572,"x":4,"y":13},{"value":10616,"x":5,"y":13},{"value":10164,"x":6,"y":13},{"value":9321,"x":7,"y":13},{"value":8833,"x":8,"y":13},{"value":8521,"x":9,"y":13},{"value":8087,"x":10,"y":13},{"value":8083,"x":11,"y":13},{"value":7818,"x":12,"y":13},{"value":7840,"x":13,"y":13},{"value":7537,"x":14,"y":13},{"value":7299,"x":15,"y":13},{"value":7431,"x":16,"y":13},{"value":7019,"x":17,"y":13},{"value":6329,"x":18,"y":13},{"value":6067,"x":19,"y":13},{"value":5623,"x":20,"y":13},{"value":5258,"x":21,"y":13},{"value":5032,"x":22,"y":13},{"value":4743,"x":23,"y":13},{"value":5160,"x":24,"y":13},{"value":5258,"x":25,"y":13},{"value":5765,"x":26,"y":13},{"value":6099,"x":27,"y":13},{"value":6020,"x":28,"y":13},{"value":6084,"x":29,"y":13},{"value":6113,"x":30,"y":13},{"value":6267,"x":31,"y":13},{"value":10434,"x":0,"y":14},{"value":11007,"x":1,"y":14},{"value":11581,"x":2,"y":14},{"value":11595,"x":3,"y":14},{"value":11445,"x":4,"y":14},{"value":10810,"x":5,"y":14},{"value":10005,"x":6,"y":14},{"value":9417,"x":7,"y":14},{"value":8334,"x":8,"y":14},{"value":7770,"x":9,"y":14},{"value":7674,"x":10,"y":14},{"value":7498,"x":11,"y":14},{"value":7725,"x":12,"y":14},{"value":8254,"x":13,"y":14},{"value":8618,"x":14,"y":14},{"value":8292,"x":15,"y":14},{"value":8057,"x":16,"y":14},{"value":7495,"x":17,"y":14},{"value":6837,"x":18,"y":14},{"value":5789,"x":19,"y":14},{"value":4820,"x":20,"y":14},{"value":4219,"x":21,"y":14},{"value":4266,"x":22,"y":14},{"value":4139,"x":23,"y":14},{"value":4607,"x":24,"y":14},{"value":5500,"x":25,"y":14},{"value":6035,"x":26,"y":14},{"value":6876,"x":27,"y":14},{"value":7020,"x":28,"y":14},{"value":7065,"x":29,"y":14},{"value":6982,"x":30,"y":14},{"value":6612,"x":31,"y":14},{"value":10645,"x":0,"y":15},{"value":11457,"x":1,"y":15},{"value":12145,"x":2,"y":15},{"value":12192,"x":3,"y":15},{"value":11864,"x":4,"y":15},{"value":11453,"x":5,"y":15},{"value":9992,"x":6,"y":15},{"value":8796,"x":7,"y":15},{"value":7713,"x":8,"y":15},{"value":7092,"x":9,"y":15},{"value":6783,"x":10,"y":15},{"value":7271,"x":11,"y":15},{"value":7934,"x":12,"y":15},{"value":8391,"x":13,"y":15},{"value":8772,"x":14,"y":15},{"value":9078,"x":15,"y":15},{"value":8834,"x":16,"y":15},{"value":8012,"x":17,"y":15},{"value":6996,"x":18,"y":15},{"value":6073,"x":19,"y":15},{"value":4938,"x":20,"y":15},{"value":3965,"x":21,"y":15},{"value":3773,"x":22,"y":15},{"value":3838,"x":23,"y":15},{"value":4689,"x":24,"y":15},{"value":5642,"x":25,"y":15},{"value":6332,"x":26,"y":15},{"value":7544,"x":27,"y":15},{"value":8006,"x":28,"y":15},{"value":7730,"x":29,"y":15},{"value":7154,"x":30,"y":15},{"value":6687,"x":31,"y":15},{"value":10419,"x":0,"y":16},{"value":11606,"x":1,"y":16},{"value":12129,"x":2,"y":16},{"value":12373,"x":3,"y":16},{"value":11957,"x":4,"y":16},{"value":11440,"x":5,"y":16},{"value":10267,"x":6,"y":16},{"value":9115,"x":7,"y":16},{"value":8064,"x":8,"y":16},{"value":7347,"x":9,"y":16},{"value":6949,"x":10,"y":16},{"value":7031,"x":11,"y":16},{"value":7896,"x":12,"y":16},{"value":8564,"x":13,"y":16},{"value":9097,"x":14,"y":16},{"value":9049,"x":15,"y":16},{"value":8772,"x":16,"y":16},{"value":8413,"x":17,"y":16},{"value":7484,"x":18,"y":16},{"value":6107,"x":19,"y":16},{"value":5173,"x":20,"y":16},{"value":4200,"x":21,"y":16},{"value":4047,"x":22,"y":16},{"value":4305,"x":23,"y":16},{"value":5009,"x":24,"y":16},{"value":5807,"x":25,"y":16},{"value":6905,"x":26,"y":16},{"value":7542,"x":27,"y":16},{"value":7880,"x":28,"y":16},{"value":7844,"x":29,"y":16},{"value":7587,"x":30,"y":16},{"value":7044,"x":31,"y":16},{"value":10404,"x":0,"y":17},{"value":11252,"x":1,"y":17},{"value":11340,"x":2,"y":17},{"value":11532,"x":3,"y":17},{"value":11614,"x":4,"y":17},{"value":10996,"x":5,"y":17},{"value":10004,"x":6,"y":17},{"value":9216,"x":7,"y":17},{"value":8064,"x":8,"y":17},{"value":7433,"x":9,"y":17},{"value":7636,"x":10,"y":17},{"value":7716,"x":11,"y":17},{"value":7799,"x":12,"y":17},{"value":8315,"x":13,"y":17},{"value":8856,"x":14,"y":17},{"value":8724,"x":15,"y":17},{"value":8900,"x":16,"y":17},{"value":8178,"x":17,"y":17},{"value":7499,"x":18,"y":17},{"value":6392,"x":19,"y":17},{"value":5316,"x":20,"y":17},{"value":4743,"x":21,"y":17},{"value":4691,"x":22,"y":17},{"value":4935,"x":23,"y":17},{"value":5299,"x":24,"y":17},{"value":5767,"x":25,"y":17},{"value":6842,"x":26,"y":17},{"value":7523,"x":27,"y":17},{"value":7846,"x":28,"y":17},{"value":7717,"x":29,"y":17},{"value":7632,"x":30,"y":17},{"value":6836,"x":31,"y":17},{"value":10237,"x":0,"y":18},{"value":10598,"x":1,"y":18},{"value":10839,"x":2,"y":18},{"value":10784,"x":3,"y":18},{"value":10884,"x":4,"y":18},{"value":10431,"x":5,"y":18},{"value":9753,"x":6,"y":18},{"value":9203,"x":7,"y":18},{"value":8755,"x":8,"y":18},{"value":8580,"x":9,"y":18},{"value":8264,"x":10,"y":18},{"value":8147,"x":11,"y":18},{"value":8323,"x":12,"y":18},{"value":8461,"x":13,"y":18},{"value":8361,"x":14,"y":18},{"value":8187,"x":15,"y":18},{"value":7873,"x":16,"y":18},{"value":7538,"x":17,"y":18},{"value":7054,"x":18,"y":18},{"value":6461,"x":19,"y":18},{"value":5975,"x":20,"y":18},{"value":5938,"x":21,"y":18},{"value":5602,"x":22,"y":18},{"value":5439,"x":23,"y":18},{"value":5970,"x":24,"y":18},{"value":6210,"x":25,"y":18},{"value":6546,"x":26,"y":18},{"value":6887,"x":27,"y":18},{"value":7316,"x":28,"y":18},{"value":7472,"x":29,"y":18},{"value":7565,"x":30,"y":18},{"value":7204,"x":31,"y":18},{"value":9692,"x":0,"y":19},{"value":9907,"x":1,"y":19},{"value":9672,"x":2,"y":19},{"value":9510,"x":3,"y":19},{"value":9616,"x":4,"y":19},{"value":9784,"x":5,"y":19},{"value":9630,"x":6,"y":19},{"value":9364,"x":7,"y":19},{"value":9399,"x":8,"y":19},{"value":9080,"x":9,"y":19},{"value":9212,"x":10,"y":19},{"value":8946,"x":11,"y":19},{"value":8452,"x":12,"y":19},{"value":8151,"x":13,"y":19},{"value":7676,"x":14,"y":19},{"value":7680,"x":15,"y":19},{"value":7313,"x":16,"y":19},{"value":7269,"x":17,"y":19},{"value":7253,"x":18,"y":19},{"value":7255,"x":19,"y":19},{"value":6936,"x":20,"y":19},{"value":6913,"x":21,"y":19},{"value":6887,"x":22,"y":19},{"value":6872,"x":23,"y":19},{"value":6490,"x":24,"y":19},{"value":6696,"x":25,"y":19},{"value":6584,"x":26,"y":19},{"value":6535,"x":27,"y":19},{"value":6461,"x":28,"y":19},{"value":6764,"x":29,"y":19},{"value":7164,"x":30,"y":19},{"value":7270,"x":31,"y":19},{"value":9629,"x":0,"y":20},{"value":9383,"x":1,"y":20},{"value":8503,"x":2,"y":20},{"value":8735,"x":3,"y":20},{"value":8498,"x":4,"y":20},{"value":9067,"x":5,"y":20},{"value":9384,"x":6,"y":20},{"value":9860,"x":7,"y":20},{"value":10284,"x":8,"y":20},{"value":10377,"x":9,"y":20},{"value":9934,"x":10,"y":20},{"value":9517,"x":11,"y":20},{"value":8859,"x":12,"y":20},{"value":7830,"x":13,"y":20},{"value":7496,"x":14,"y":20},{"value":6979,"x":15,"y":20},{"value":6308,"x":16,"y":20},{"value":6745,"x":17,"y":20},{"value":7180,"x":18,"y":20},{"value":7501,"x":19,"y":20},{"value":7959,"x":20,"y":20},{"value":8251,"x":21,"y":20},{"value":8197,"x":22,"y":20},{"value":8044,"x":23,"y":20},{"value":7500,"x":24,"y":20},{"value":6734,"x":25,"y":20},{"value":6443,"x":26,"y":20},{"value":6102,"x":27,"y":20},{"value":5918,"x":28,"y":20},{"value":6034,"x":29,"y":20},{"value":6691,"x":30,"y":20},{"value":7506,"x":31,"y":20},{"value":9645,"x":0,"y":21},{"value":8697,"x":1,"y":21},{"value":7967,"x":2,"y":21},{"value":7418,"x":3,"y":21},{"value":7557,"x":4,"y":21},{"value":8038,"x":5,"y":21},{"value":8788,"x":6,"y":21},{"value":9961,"x":7,"y":21},{"value":10615,"x":8,"y":21},{"value":10518,"x":9,"y":21},{"value":10286,"x":10,"y":21},{"value":9970,"x":11,"y":21},{"value":9029,"x":12,"y":21},{"value":7671,"x":13,"y":21},{"value":7110,"x":14,"y":21},{"value":6310,"x":15,"y":21},{"value":6112,"x":16,"y":21},{"value":6421,"x":17,"y":21},{"value":6858,"x":18,"y":21},{"value":7809,"x":19,"y":21},{"value":8767,"x":20,"y":21},{"value":8843,"x":21,"y":21},{"value":9371,"x":22,"y":21},{"value":8652,"x":23,"y":21},{"value":8417,"x":24,"y":21},{"value":7254,"x":25,"y":21},{"value":6427,"x":26,"y":21},{"value":5995,"x":27,"y":21},{"value":5719,"x":28,"y":21},{"value":6079,"x":29,"y":21},{"value":6648,"x":30,"y":21},{"value":7831,"x":31,"y":21},{"value":9059,"x":0,"y":22},{"value":8096,"x":1,"y":22},{"value":7446,"x":2,"y":22},{"value":7343,"x":3,"y":22},{"value":7291,"x":4,"y":22},{"value":8031,"x":5,"y":22},{"value":8602,"x":6,"y":22},{"value":9973,"x":7,"y":22},{"value":10401,"x":8,"y":22},{"value":10839,"x":9,"y":22},{"value":10731,"x":10,"y":22},{"value":9995,"x":11,"y":22},{"value":8798,"x":12,"y":22},{"value":7844,"x":13,"y":22},{"value":6894,"x":14,"y":22},{"value":6290,"x":15,"y":22},{"value":5969,"x":16,"y":22},{"value":6724,"x":17,"y":22},{"value":7208,"x":18,"y":22},{"value":7952,"x":19,"y":22},{"value":8877,"x":20,"y":22},{"value":9792,"x":21,"y":22},{"value":9607,"x":22,"y":22},{"value":9556,"x":23,"y":22},{"value":8495,"x":24,"y":22},{"value":7683,"x":25,"y":22},{"value":6611,"x":26,"y":22},{"value":6367,"x":27,"y":22},{"value":5868,"x":28,"y":22},{"value":6428,"x":29,"y":22},{"value":6889,"x":30,"y":22},{"value":7885,"x":31,"y":22},{"value":9112,"x":0,"y":23},{"value":7993,"x":1,"y":23},{"value":7272,"x":2,"y":23},{"value":7423,"x":3,"y":23},{"value":7575,"x":4,"y":23},{"value":7987,"x":5,"y":23},{"value":8468,"x":6,"y":23},{"value":9490,"x":7,"y":23},{"value":10255,"x":8,"y":23},{"value":10297,"x":9,"y":23},{"value":10166,"x":10,"y":23},{"value":9759,"x":11,"y":23},{"value":9189,"x":12,"y":23},{"value":8170,"x":13,"y":23},{"value":7453,"x":14,"y":23},{"value":7019,"x":15,"y":23},{"value":6492,"x":16,"y":23},{"value":7192,"x":17,"y":23},{"value":7365,"x":18,"y":23},{"value":8487,"x":19,"y":23},{"value":9340,"x":20,"y":23},{"value":9663,"x":21,"y":23},{"value":9931,"x":22,"y":23},{"value":9546,"x":23,"y":23},{"value":9006,"x":24,"y":23},{"value":8023,"x":25,"y":23},{"value":7502,"x":26,"y":23},{"value":7067,"x":27,"y":23},{"value":6546,"x":28,"y":23},{"value":6996,"x":29,"y":23},{"value":7491,"x":30,"y":23},{"value":8571,"x":31,"y":23},{"value":8423,"x":0,"y":24},{"value":8107,"x":1,"y":24},{"value":7857,"x":2,"y":24},{"value":7785,"x":3,"y":24},{"value":7676,"x":4,"y":24},{"value":8249,"x":5,"y":24},{"value":8727,"x":6,"y":24},{"value":8838,"x":7,"y":24},{"value":9240,"x":8,"y":24},{"value":9656,"x":9,"y":24},{"value":9444,"x":10,"y":24},{"value":9387,"x":11,"y":24},{"value":8966,"x":12,"y":24},{"value":8573,"x":13,"y":24},{"value":7793,"x":14,"y":24},{"value":7474,"x":15,"y":24},{"value":7404,"x":16,"y":24},{"value":7573,"x":17,"y":24},{"value":8334,"x":18,"y":24},{"value":8465,"x":19,"y":24},{"value":9305,"x":20,"y":24},{"value":9580,"x":21,"y":24},{"value":9314,"x":22,"y":24},{"value":9418,"x":23,"y":24},{"value":9163,"x":24,"y":24},{"value":8550,"x":25,"y":24},{"value":8375,"x":26,"y":24},{"value":8073,"x":27,"y":24},{"value":7685,"x":28,"y":24},{"value":7994,"x":29,"y":24},{"value":8385,"x":30,"y":24},{"value":8945,"x":31,"y":24},{"value":8163,"x":0,"y":25},{"value":8156,"x":1,"y":25},{"value":8537,"x":2,"y":25},{"value":8376,"x":3,"y":25},{"value":8444,"x":4,"y":25},{"value":8594,"x":5,"y":25},{"value":8363,"x":6,"y":25},{"value":8660,"x":7,"y":25},{"value":8337,"x":8,"y":25},{"value":8421,"x":9,"y":25},{"value":8494,"x":10,"y":25},{"value":8641,"x":11,"y":25},{"value":8754,"x":12,"y":25},{"value":8681,"x":13,"y":25},{"value":8762,"x":14,"y":25},{"value":8526,"x":15,"y":25},{"value":8912,"x":16,"y":25},{"value":8896,"x":17,"y":25},{"value":9069,"x":18,"y":25},{"value":8629,"x":19,"y":25},{"value":8850,"x":20,"y":25},{"value":8875,"x":21,"y":25},{"value":9008,"x":22,"y":25},{"value":9068,"x":23,"y":25},{"value":9071,"x":24,"y":25},{"value":8964,"x":25,"y":25},{"value":9056,"x":26,"y":25},{"value":9159,"x":27,"y":25},{"value":9049,"x":28,"y":25},{"value":9368,"x":29,"y":25},{"value":9384,"x":30,"y":25},{"value":9179,"x":31,"y":25},{"value":7931,"x":0,"y":26},{"value":8460,"x":1,"y":26},{"value":8960,"x":2,"y":26},{"value":8814,"x":3,"y":26},{"value":9217,"x":4,"y":26},{"value":8939,"x":5,"y":26},{"value":8574,"x":6,"y":26},{"value":7913,"x":7,"y":26},{"value":7594,"x":8,"y":26},{"value":7433,"x":9,"y":26},{"value":7622,"x":10,"y":26},{"value":7890,"x":11,"y":26},{"value":8402,"x":12,"y":26},{"value":9188,"x":13,"y":26},{"value":9493,"x":14,"y":26},{"value":9939,"x":15,"y":26},{"value":9875,"x":16,"y":26},{"value":9560,"x":17,"y":26},{"value":9508,"x":18,"y":26},{"value":9057,"x":19,"y":26},{"value":8722,"x":20,"y":26},{"value":8291,"x":21,"y":26},{"value":8260,"x":22,"y":26},{"value":8633,"x":23,"y":26},{"value":9010,"x":24,"y":26},{"value":9454,"x":25,"y":26},{"value":10177,"x":26,"y":26},{"value":10462,"x":27,"y":26},{"value":10567,"x":28,"y":26},{"value":10628,"x":29,"y":26},{"value":10161,"x":30,"y":26},{"value":9809,"x":31,"y":26},{"value":7659,"x":0,"y":27},{"value":8581,"x":1,"y":27},{"value":9316,"x":2,"y":27},{"value":9495,"x":3,"y":27},{"value":9567,"x":4,"y":27},{"value":9023,"x":5,"y":27},{"value":8025,"x":6,"y":27},{"value":7706,"x":7,"y":27},{"value":7091,"x":8,"y":27},{"value":6743,"x":9,"y":27},{"value":6773,"x":10,"y":27},{"value":7628,"x":11,"y":27},{"value":8050,"x":12,"y":27},{"value":9161,"x":13,"y":27},{"value":10291,"x":14,"y":27},{"value":10881,"x":15,"y":27},{"value":11027,"x":16,"y":27},{"value":10431,"x":17,"y":27},{"value":10183,"x":18,"y":27},{"value":9217,"x":19,"y":27},{"value":8823,"x":20,"y":27},{"value":8291,"x":21,"y":27},{"value":8168,"x":22,"y":27},{"value":8196,"x":23,"y":27},{"value":9300,"x":24,"y":27},{"value":9891,"x":25,"y":27},{"value":10715,"x":26,"y":27},{"value":11186,"x":27,"y":27},{"value":11699,"x":28,"y":27},{"value":11314,"x":29,"y":27},{"value":10835,"x":30,"y":27},{"value":10315,"x":31,"y":27},{"value":7259,"x":0,"y":28},{"value":8574,"x":1,"y":28},{"value":8929,"x":2,"y":28},{"value":9574,"x":3,"y":28},{"value":9543,"x":4,"y":28},{"value":8776,"x":5,"y":28},{"value":8251,"x":6,"y":28},{"value":7186,"x":7,"y":28},{"value":6428,"x":8,"y":28},{"value":6310,"x":9,"y":28},{"value":6557,"x":10,"y":28},{"value":7231,"x":11,"y":28},{"value":8345,"x":12,"y":28},{"value":9523,"x":13,"y":28},{"value":10325,"x":14,"y":28},{"value":11122,"x":15,"y":28},{"value":11261,"x":16,"y":28},{"value":11174,"x":17,"y":28},{"value":10567,"x":18,"y":28},{"value":9575,"x":19,"y":28},{"value":8647,"x":20,"y":28},{"value":8107,"x":21,"y":28},{"value":8340,"x":22,"y":28},{"value":8612,"x":23,"y":28},{"value":9569,"x":24,"y":28},{"value":10417,"x":25,"y":28},{"value":11409,"x":26,"y":28},{"value":11872,"x":27,"y":28},{"value":12178,"x":28,"y":28},{"value":11942,"x":29,"y":28},{"value":11382,"x":30,"y":28},{"value":10365,"x":31,"y":28},{"value":7164,"x":0,"y":29},{"value":7977,"x":1,"y":29},{"value":8737,"x":2,"y":29},{"value":9127,"x":3,"y":29},{"value":9007,"x":4,"y":29},{"value":8765,"x":5,"y":29},{"value":7673,"x":6,"y":29},{"value":7301,"x":7,"y":29},{"value":6700,"x":8,"y":29},{"value":6347,"x":9,"y":29},{"value":6525,"x":10,"y":29},{"value":7436,"x":11,"y":29},{"value":8589,"x":12,"y":29},{"value":9437,"x":13,"y":29},{"value":10480,"x":14,"y":29},{"value":11047,"x":15,"y":29},{"value":11623,"x":16,"y":29},{"value":11023,"x":17,"y":29},{"value":10479,"x":18,"y":29},{"value":9902,"x":19,"y":29},{"value":9437,"x":20,"y":29},{"value":8666,"x":21,"y":29},{"value":8787,"x":22,"y":29},{"value":9052,"x":23,"y":29},{"value":9609,"x":24,"y":29},{"value":10933,"x":25,"y":29},{"value":11749,"x":26,"y":29},{"value":12477,"x":27,"y":29},{"value":12496,"x":28,"y":29},{"value":12370,"x":29,"y":29},{"value":11779,"x":30,"y":29},{"value":10825,"x":31,"y":29},{"value":6930,"x":0,"y":30},{"value":7272,"x":1,"y":30},{"value":7863,"x":2,"y":30},{"value":8175,"x":3,"y":30},{"value":8030,"x":4,"y":30},{"value":7859,"x":5,"y":30},{"value":7405,"x":6,"y":30},{"value":7079,"x":7,"y":30},{"value":7052,"x":8,"y":30},{"value":7188,"x":9,"y":30},{"value":7235,"x":10,"y":30},{"value":7962,"x":11,"y":30},{"value":8570,"x":12,"y":30},{"value":9393,"x":13,"y":30},{"value":10238,"x":14,"y":30},{"value":10711,"x":15,"y":30},{"value":10805,"x":16,"y":30},{"value":10991,"x":17,"y":30},{"value":10550,"x":18,"y":30},{"value":10157,"x":19,"y":30},{"value":10052,"x":20,"y":30},{"value":9757,"x":21,"y":30},{"value":9787,"x":22,"y":30},{"value":9841,"x":23,"y":30},{"value":10574,"x":24,"y":30},{"value":11312,"x":25,"y":30},{"value":11896,"x":26,"y":30},{"value":12078,"x":27,"y":30},{"value":12224,"x":28,"y":30},{"value":12172,"x":29,"y":30},{"value":11777,"x":30,"y":30},{"value":11210,"x":31,"y":30},{"value":6657,"x":0,"y":31},{"value":6582,"x":1,"y":31},{"value":6708,"x":2,"y":31},{"value":7205,"x":3,"y":31},{"value":7103,"x":4,"y":31},{"value":7402,"x":5,"y":31},{"value":7554,"x":6,"y":31},{"value":7306,"x":7,"y":31},{"value":7625,"x":8,"y":31},{"value":7896,"x":9,"y":31},{"value":8023,"x":10,"y":31},{"value":8387,"x":11,"y":31},{"value":8735,"x":12,"y":31},{"value":9450,"x":13,"y":31},{"value":9629,"x":14,"y":31},{"value":9740,"x":15,"y":31},{"value":10272,"x":16,"y":31},{"value":10284,"x":17,"y":31},{"value":10571,"x":18,"y":31},{"value":10541,"x":19,"y":31},{"value":10918,"x":20,"y":31},{"value":10966,"x":21,"y":31},{"value":11097,"x":22,"y":31},{"value":10886,"x":23,"y":31},{"value":11114,"x":24,"y":31},{"value":11515,"x":25,"y":31},{"value":11795,"x":26,"y":31},{"value":11900,"x":27,"y":31},{"value":11822,"x":28,"y":31},{"value":11769,"x":29,"y":31},{"value":11201,"x":30,"y":31},{"value":11140,"x":31,"y":31}]},"encoding":{"color":{"field":"value","scale":{"scheme":"viridis"},"title":"DN","type":"quantitative"},"x":{"field":"x","title":"X (pixels / 4)","type":"ordinal"},"y":{"field":"y","sort":"descending","title":"Y (pixels / 4)","type":"ordinal"}},"height":500,"mark":"rect","title":"Red Band - Month 12 (Downsampled 4x)","width":500}
# Visualize temporal mean (Green band)
green_band = 1
green_mean_slice = temporal_mean[green_band]

# Downsample
viz_mean_data =
  for y <- 0..(viz_height - 1), x <- 0..(viz_width - 1) do
    orig_y = y * downsample_factor
    orig_x = x * downsample_factor
    value = Nx.to_number(green_mean_slice[orig_y][orig_x])

    %{x: x, y: y, value: value}
  end

Vl.new(width: 500, height: 500, title: "Green Band - Temporal Mean (Downsampled 4x)")
|> Vl.data_from_values(viz_mean_data)
|> Vl.mark(:rect)
|> Vl.encode_field(:x, "x", type: :ordinal, title: "X (pixels / 4)")
|> Vl.encode_field(:y, "y", type: :ordinal, title: "Y (pixels / 4)", sort: "descending")
|> Vl.encode_field(:color, "value",
  type: :quantitative,
  scale: [scheme: "greens"],
  title: "Mean DN"
)
{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","data":{"values":[{"value":9982.8330078125,"x":0,"y":0},{"value":10038.5,"x":1,"y":0},{"value":10019.6669921875,"x":2,"y":0},{"value":10073.5830078125,"x":3,"y":0},{"value":10155.1669921875,"x":4,"y":0},{"value":10212.1669921875,"x":5,"y":0},{"value":10221.25,"x":6,"y":0},{"value":10216.1669921875,"x":7,"y":0},{"value":10261.5,"x":8,"y":0},{"value":10229.75,"x":9,"y":0},{"value":10317.25,"x":10,"y":0},{"value":10344.3330078125,"x":11,"y":0},{"value":10381.9169921875,"x":12,"y":0},{"value":10444.8330078125,"x":13,"y":0},{"value":10436.75,"x":14,"y":0},{"value":10453.0,"x":15,"y":0},{"value":10524.3330078125,"x":16,"y":0},{"value":10510.4169921875,"x":17,"y":0},{"value":10632.8330078125,"x":18,"y":0},{"value":10543.25,"x":19,"y":0},{"value":10678.3330078125,"x":20,"y":0},{"value":10673.4169921875,"x":21,"y":0},{"value":10673.8330078125,"x":22,"y":0},{"value":10681.3330078125,"x":23,"y":0},{"value":10761.75,"x":24,"y":0},{"value":10831.8330078125,"x":25,"y":0},{"value":10805.5830078125,"x":26,"y":0},{"value":10830.6669921875,"x":27,"y":0},{"value":10882.1669921875,"x":28,"y":0},{"value":10865.0830078125,"x":29,"y":0},{"value":10904.25,"x":30,"y":0},{"value":10971.0,"x":31,"y":0},{"value":10470.9169921875,"x":0,"y":1},{"value":10940.8330078125,"x":1,"y":1},{"value":11325.75,"x":2,"y":1},{"value":11459.0,"x":3,"y":1},{"value":11398.6669921875,"x":4,"y":1},{"value":11079.5830078125,"x":5,"y":1},{"value":10623.3330078125,"x":6,"y":1},{"value":10178.0,"x":7,"y":1},{"value":9767.5,"x":8,"y":1},{"value":9568.5,"x":9,"y":1},{"value":9572.6669921875,"x":10,"y":1},{"value":9827.1669921875,"x":11,"y":1},{"value":10188.1669921875,"x":12,"y":1},{"value":10682.75,"x":13,"y":1},{"value":11067.4169921875,"x":14,"y":1},{"value":11378.25,"x":15,"y":1},{"value":11306.1669921875,"x":16,"y":1},{"value":11107.75,"x":17,"y":1},{"value":10746.75,"x":18,"y":1},{"value":10212.5,"x":19,"y":1},{"value":9894.75,"x":20,"y":1},{"value":9523.0,"x":21,"y":1},{"value":9484.1669921875,"x":22,"y":1},{"value":9622.6669921875,"x":23,"y":1},{"value":9943.75,"x":24,"y":1},{"value":10486.8330078125,"x":25,"y":1},{"value":10954.5,"x":26,"y":1},{"value":11319.0,"x":27,"y":1},{"value":11540.6669921875,"x":28,"y":1},{"value":11532.0830078125,"x":29,"y":1},{"value":11192.9169921875,"x":30,"y":1},{"value":10902.0830078125,"x":31,"y":1},{"value":10858.75,"x":0,"y":2},{"value":11760.4169921875,"x":1,"y":2},{"value":12305.3330078125,"x":2,"y":2},{"value":12637.6669921875,"x":3,"y":2},{"value":12406.8330078125,"x":4,"y":2},{"value":11873.6669921875,"x":5,"y":2},{"value":11087.0,"x":6,"y":2},{"value":10132.25,"x":7,"y":2},{"value":9438.9169921875,"x":8,"y":2},{"value":9086.6669921875,"x":9,"y":2},{"value":8965.1669921875,"x":10,"y":2},{"value":9393.8330078125,"x":11,"y":2},{"value":10129.9169921875,"x":12,"y":2},{"value":10850.8330078125,"x":13,"y":2},{"value":11596.3330078125,"x":14,"y":2},{"value":11937.5830078125,"x":15,"y":2},{"value":11951.0,"x":16,"y":2},{"value":11558.8330078125,"x":17,"y":2},{"value":10882.3330078125,"x":18,"y":2},{"value":9945.1669921875,"x":19,"y":2},{"value":9204.5,"x":20,"y":2},{"value":8626.5,"x":21,"y":2},{"value":8413.5,"x":22,"y":2},{"value":8709.1669921875,"x":23,"y":2},{"value":9304.25,"x":24,"y":2},{"value":10139.75,"x":25,"y":2},{"value":11056.6669921875,"x":26,"y":2},{"value":11685.5830078125,"x":27,"y":2},{"value":12066.0830078125,"x":28,"y":2},{"value":11859.5830078125,"x":29,"y":2},{"value":11565.6669921875,"x":30,"y":2},{"value":10698.8330078125,"x":31,"y":2},{"value":11395.0,"x":0,"y":3},{"value":12251.25,"x":1,"y":3},{"value":13072.6669921875,"x":2,"y":3},{"value":13317.75,"x":3,"y":3},{"value":13068.6669921875,"x":4,"y":3},{"value":12478.1669921875,"x":5,"y":3},{"value":11397.8330078125,"x":6,"y":3},{"value":10382.1669921875,"x":7,"y":3},{"value":9515.9169921875,"x":8,"y":3},{"value":9017.0830078125,"x":9,"y":3},{"value":8967.1669921875,"x":10,"y":3},{"value":9343.25,"x":11,"y":3},{"value":10181.8330078125,"x":12,"y":3},{"value":10996.0,"x":13,"y":3},{"value":11796.5,"x":14,"y":3},{"value":12187.6669921875,"x":15,"y":3},{"value":12170.0830078125,"x":16,"y":3},{"value":11693.5,"x":17,"y":3},{"value":10782.5830078125,"x":18,"y":3},{"value":9728.4169921875,"x":19,"y":3},{"value":8773.3330078125,"x":20,"y":3},{"value":8087.91650390625,"x":21,"y":3},{"value":7904.58349609375,"x":22,"y":3},{"value":8121.75,"x":23,"y":3},{"value":8880.1669921875,"x":24,"y":3},{"value":9851.3330078125,"x":25,"y":3},{"value":10859.0830078125,"x":26,"y":3},{"value":11649.0830078125,"x":27,"y":3},{"value":12035.3330078125,"x":28,"y":3},{"value":11961.0,"x":29,"y":3},{"value":11395.8330078125,"x":30,"y":3},{"value":10661.0,"x":31,"y":3},{"value":11691.5830078125,"x":0,"y":4},{"value":12620.75,"x":1,"y":4},{"value":13291.0,"x":2,"y":4},{"value":13537.0830078125,"x":3,"y":4},{"value":13292.5,"x":4,"y":4},{"value":12595.1669921875,"x":5,"y":4},{"value":11689.5830078125,"x":6,"y":4},{"value":10649.1669921875,"x":7,"y":4},{"value":9924.1669921875,"x":8,"y":4},{"value":9318.5830078125,"x":9,"y":4},{"value":9194.8330078125,"x":10,"y":4},{"value":9590.0830078125,"x":11,"y":4},{"value":10257.3330078125,"x":12,"y":4},{"value":11014.1669921875,"x":13,"y":4},{"value":11594.25,"x":14,"y":4},{"value":11948.75,"x":15,"y":4},{"value":11931.0830078125,"x":16,"y":4},{"value":11402.3330078125,"x":17,"y":4},{"value":10530.25,"x":18,"y":4},{"value":9587.5,"x":19,"y":4},{"value":8582.0830078125,"x":20,"y":4},{"value":8050.58349609375,"x":21,"y":4},{"value":7826.5,"x":22,"y":4},{"value":8083.08349609375,"x":23,"y":4},{"value":8696.4169921875,"x":24,"y":4},{"value":9579.25,"x":25,"y":4},{"value":10468.9169921875,"x":26,"y":4},{"value":11230.25,"x":27,"y":4},{"value":11637.5,"x":28,"y":4},{"value":11546.6669921875,"x":29,"y":4},{"value":11078.0830078125,"x":30,"y":4},{"value":10398.75,"x":31,"y":4},{"value":12100.3330078125,"x":0,"y":5},{"value":12682.5,"x":1,"y":5},{"value":13079.6669921875,"x":2,"y":5},{"value":13232.3330078125,"x":3,"y":5},{"value":13058.5830078125,"x":4,"y":5},{"value":12511.4169921875,"x":5,"y":5},{"value":11964.0,"x":6,"y":5},{"value":11205.75,"x":7,"y":5},{"value":10593.75,"x":8,"y":5},{"value":10273.8330078125,"x":9,"y":5},{"value":10065.3330078125,"x":10,"y":5},{"value":10161.1669921875,"x":11,"y":5},{"value":10477.3330078125,"x":12,"y":5},{"value":10934.4169921875,"x":13,"y":5},{"value":11173.6669921875,"x":14,"y":5},{"value":11374.5830078125,"x":15,"y":5},{"value":11262.0,"x":16,"y":5},{"value":10807.6669921875,"x":17,"y":5},{"value":10143.0,"x":18,"y":5},{"value":9377.1669921875,"x":19,"y":5},{"value":8819.0830078125,"x":20,"y":5},{"value":8362.25,"x":21,"y":5},{"value":8250.6669921875,"x":22,"y":5},{"value":8350.25,"x":23,"y":5},{"value":8687.4169921875,"x":24,"y":5},{"value":9279.8330078125,"x":25,"y":5},{"value":9995.0,"x":26,"y":5},{"value":10455.5,"x":27,"y":5},{"value":10711.9169921875,"x":28,"y":5},{"value":10733.1669921875,"x":29,"y":5},{"value":10498.0830078125,"x":30,"y":5},{"value":10107.6669921875,"x":31,"y":5},{"value":12382.5830078125,"x":0,"y":6},{"value":12613.1669921875,"x":1,"y":6},{"value":12602.0,"x":2,"y":6},{"value":12639.5830078125,"x":3,"y":6},{"value":12496.9169921875,"x":4,"y":6},{"value":12354.0830078125,"x":5,"y":6},{"value":12052.1669921875,"x":6,"y":6},{"value":11826.0830078125,"x":7,"y":6},{"value":11486.4169921875,"x":8,"y":6},{"value":11313.25,"x":9,"y":6},{"value":11120.8330078125,"x":10,"y":6},{"value":10944.8330078125,"x":11,"y":6},{"value":10726.6669921875,"x":12,"y":6},{"value":10718.0830078125,"x":13,"y":6},{"value":10527.8330078125,"x":14,"y":6},{"value":10456.1669921875,"x":15,"y":6},{"value":10329.4169921875,"x":16,"y":6},{"value":9989.8330078125,"x":17,"y":6},{"value":9751.1669921875,"x":18,"y":6},{"value":9392.5830078125,"x":19,"y":6},{"value":9148.75,"x":20,"y":6},{"value":9042.3330078125,"x":21,"y":6},{"value":8944.8330078125,"x":22,"y":6},{"value":8919.9169921875,"x":23,"y":6},{"value":8949.6669921875,"x":24,"y":6},{"value":9063.5,"x":25,"y":6},{"value":9276.5830078125,"x":26,"y":6},{"value":9418.8330078125,"x":27,"y":6},{"value":9574.25,"x":28,"y":6},{"value":9740.25,"x":29,"y":6},{"value":9824.5,"x":30,"y":6},{"value":9812.75,"x":31,"y":6},{"value":12739.5,"x":0,"y":7},{"value":12418.0830078125,"x":1,"y":7},{"value":12160.9169921875,"x":2,"y":7},{"value":11939.5830078125,"x":3,"y":7},{"value":11904.8330078125,"x":4,"y":7},{"value":12103.9169921875,"x":5,"y":7},{"value":12204.0830078125,"x":6,"y":7},{"value":12350.3330078125,"x":7,"y":7},{"value":12479.5830078125,"x":8,"y":7},{"value":12511.5830078125,"x":9,"y":7},{"value":12189.5,"x":10,"y":7},{"value":11704.0,"x":11,"y":7},{"value":11174.9169921875,"x":12,"y":7},{"value":10498.4169921875,"x":13,"y":7},{"value":9921.4169921875,"x":14,"y":7},{"value":9435.5,"x":15,"y":7},{"value":9232.75,"x":16,"y":7},{"value":9172.25,"x":17,"y":7},{"value":9199.75,"x":18,"y":7},{"value":9435.0,"x":19,"y":7},{"value":9625.5,"x":20,"y":7},{"value":9733.0,"x":21,"y":7},{"value":9638.25,"x":22,"y":7},{"value":9536.5,"x":23,"y":7},{"value":9251.25,"x":24,"y":7},{"value":8950.0,"x":25,"y":7},{"value":8600.75,"x":26,"y":7},{"value":8414.0,"x":27,"y":7},{"value":8413.25,"x":28,"y":7},{"value":8701.25,"x":29,"y":7},{"value":9048.9169921875,"x":30,"y":7},{"value":9628.25,"x":31,"y":7},{"value":13115.5,"x":0,"y":8},{"value":12275.5,"x":1,"y":8},{"value":11696.5,"x":2,"y":8},{"value":11361.25,"x":3,"y":8},{"value":11454.75,"x":4,"y":8},{"value":11798.0830078125,"x":5,"y":8},{"value":12345.6669921875,"x":6,"y":8},{"value":12822.8330078125,"x":7,"y":8},{"value":13255.9169921875,"x":8,"y":8},{"value":13336.0,"x":9,"y":8},{"value":13034.6669921875,"x":10,"y":8},{"value":12344.1669921875,"x":11,"y":8},{"value":11381.25,"x":12,"y":8},{"value":10352.4169921875,"x":13,"y":8},{"value":9335.0830078125,"x":14,"y":8},{"value":8783.5830078125,"x":15,"y":8},{"value":8453.3330078125,"x":16,"y":8},{"value":8454.9169921875,"x":17,"y":8},{"value":8896.9169921875,"x":18,"y":8},{"value":9472.3330078125,"x":19,"y":8},{"value":9968.0830078125,"x":20,"y":8},{"value":10285.8330078125,"x":21,"y":8},{"value":10348.1669921875,"x":22,"y":8},{"value":9986.3330078125,"x":23,"y":8},{"value":9489.3330078125,"x":24,"y":8},{"value":8739.4169921875,"x":25,"y":8},{"value":8042.0,"x":26,"y":8},{"value":7576.91650390625,"x":27,"y":8},{"value":7543.66650390625,"x":28,"y":8},{"value":7822.91650390625,"x":29,"y":8},{"value":8465.4169921875,"x":30,"y":8},{"value":9409.6669921875,"x":31,"y":8},{"value":13325.3330078125,"x":0,"y":9},{"value":12314.25,"x":1,"y":9},{"value":11596.75,"x":2,"y":9},{"value":11228.5,"x":3,"y":9},{"value":11299.0,"x":4,"y":9},{"value":11782.9169921875,"x":5,"y":9},{"value":12474.9169921875,"x":6,"y":9},{"value":13164.3330078125,"x":7,"y":9},{"value":13771.1669921875,"x":8,"y":9},{"value":13854.75,"x":9,"y":9},{"value":13533.8330078125,"x":10,"y":9},{"value":12815.75,"x":11,"y":9},{"value":11547.5830078125,"x":12,"y":9},{"value":10326.4169921875,"x":13,"y":9},{"value":9132.8330078125,"x":14,"y":9},{"value":8336.9169921875,"x":15,"y":9},{"value":7907.5,"x":16,"y":9},{"value":8115.58349609375,"x":17,"y":9},{"value":8634.75,"x":18,"y":9},{"value":9470.25,"x":19,"y":9},{"value":10159.0,"x":20,"y":9},{"value":10541.5830078125,"x":21,"y":9},{"value":10729.0830078125,"x":22,"y":9},{"value":10327.4169921875,"x":23,"y":9},{"value":9619.9169921875,"x":24,"y":9},{"value":8680.25,"x":25,"y":9},{"value":7712.33349609375,"x":26,"y":9},{"value":7152.83349609375,"x":27,"y":9},{"value":6941.83349609375,"x":28,"y":9},{"value":7333.5,"x":29,"y":9},{"value":8164.16650390625,"x":30,"y":9},{"value":9343.25,"x":31,"y":9},{"value":13436.1669921875,"x":0,"y":10},{"value":12639.5830078125,"x":1,"y":10},{"value":11854.25,"x":2,"y":10},{"value":11500.5,"x":3,"y":10},{"value":11524.8330078125,"x":4,"y":10},{"value":11955.9169921875,"x":5,"y":10},{"value":12617.3330078125,"x":6,"y":10},{"value":13419.5830078125,"x":7,"y":10},{"value":13865.5,"x":8,"y":10},{"value":13960.1669921875,"x":9,"y":10},{"value":13657.75,"x":10,"y":10},{"value":12872.6669921875,"x":11,"y":10},{"value":11617.0830078125,"x":12,"y":10},{"value":10421.75,"x":13,"y":10},{"value":9168.0,"x":14,"y":10},{"value":8419.0830078125,"x":15,"y":10},{"value":8079.75,"x":16,"y":10},{"value":8091.25,"x":17,"y":10},{"value":8688.8330078125,"x":18,"y":10},{"value":9351.5830078125,"x":19,"y":10},{"value":10073.0,"x":20,"y":10},{"value":10481.0830078125,"x":21,"y":10},{"value":10594.1669921875,"x":22,"y":10},{"value":10188.0,"x":23,"y":10},{"value":9523.5,"x":24,"y":10},{"value":8560.5830078125,"x":25,"y":10},{"value":7651.91650390625,"x":26,"y":10},{"value":7072.0,"x":27,"y":10},{"value":7000.75,"x":28,"y":10},{"value":7323.58349609375,"x":29,"y":10},{"value":8167.0,"x":30,"y":10},{"value":9275.5,"x":31,"y":10},{"value":13621.75,"x":0,"y":11},{"value":12995.6669921875,"x":1,"y":11},{"value":12437.75,"x":2,"y":11},{"value":12134.4169921875,"x":3,"y":11},{"value":12173.4169921875,"x":4,"y":11},{"value":12423.6669921875,"x":5,"y":11},{"value":12764.6669921875,"x":6,"y":11},{"value":13296.8330078125,"x":7,"y":11},{"value":13517.8330078125,"x":8,"y":11},{"value":13609.8330078125,"x":9,"y":11},{"value":13239.25,"x":10,"y":11},{"value":12475.5830078125,"x":11,"y":11},{"value":11619.9169921875,"x":12,"y":11},{"value":10553.0830078125,"x":13,"y":11},{"value":9698.25,"x":14,"y":11},{"value":9021.4169921875,"x":15,"y":11},{"value":8552.9169921875,"x":16,"y":11},{"value":8671.6669921875,"x":17,"y":11},{"value":8900.6669921875,"x":18,"y":11},{"value":9350.6669921875,"x":19,"y":11},{"value":9725.0830078125,"x":20,"y":11},{"value":10009.75,"x":21,"y":11},{"value":10013.0,"x":22,"y":11},{"value":9641.4169921875,"x":23,"y":11},{"value":9153.9169921875,"x":24,"y":11},{"value":8544.1669921875,"x":25,"y":11},{"value":7871.91650390625,"x":26,"y":11},{"value":7526.58349609375,"x":27,"y":11},{"value":7554.08349609375,"x":28,"y":11},{"value":7788.83349609375,"x":29,"y":11},{"value":8431.5830078125,"x":30,"y":11},{"value":9359.4169921875,"x":31,"y":11},{"value":13760.25,"x":0,"y":12},{"value":13510.25,"x":1,"y":12},{"value":13313.75,"x":2,"y":12},{"value":13146.5830078125,"x":3,"y":12},{"value":13124.75,"x":4,"y":12},{"value":13015.4169921875,"x":5,"y":12},{"value":13111.25,"x":6,"y":12},{"value":13031.3330078125,"x":7,"y":12},{"value":12966.75,"x":8,"y":12},{"value":12774.8330078125,"x":9,"y":12},{"value":12452.6669921875,"x":10,"y":12},{"value":11935.75,"x":11,"y":12},{"value":11468.4169921875,"x":12,"y":12},{"value":10829.4169921875,"x":13,"y":12},{"value":10268.0,"x":14,"y":12},{"value":9906.4169921875,"x":15,"y":12},{"value":9528.0,"x":16,"y":12},{"value":9261.75,"x":17,"y":12},{"value":9193.0830078125,"x":18,"y":12},{"value":9275.25,"x":19,"y":12},{"value":9162.0830078125,"x":20,"y":12},{"value":9277.4169921875,"x":21,"y":12},{"value":9155.0830078125,"x":22,"y":12},{"value":9006.8330078125,"x":23,"y":12},{"value":8744.1669921875,"x":24,"y":12},{"value":8603.6669921875,"x":25,"y":12},{"value":8292.5,"x":26,"y":12},{"value":8198.9169921875,"x":27,"y":12},{"value":8400.5,"x":28,"y":12},{"value":8621.6669921875,"x":29,"y":12},{"value":8978.6669921875,"x":30,"y":12},{"value":9384.5,"x":31,"y":12},{"value":13804.5830078125,"x":0,"y":13},{"value":14103.9169921875,"x":1,"y":13},{"value":14216.0,"x":2,"y":13},{"value":14210.5830078125,"x":3,"y":13},{"value":13990.6669921875,"x":4,"y":13},{"value":13708.5830078125,"x":5,"y":13},{"value":13276.8330078125,"x":6,"y":13},{"value":12743.8330078125,"x":7,"y":13},{"value":12253.9169921875,"x":8,"y":13},{"value":11834.5,"x":9,"y":13},{"value":11599.4169921875,"x":10,"y":13},{"value":11396.75,"x":11,"y":13},{"value":11251.6669921875,"x":12,"y":13},{"value":11083.8330078125,"x":13,"y":13},{"value":11037.25,"x":14,"y":13},{"value":10910.6669921875,"x":15,"y":13},{"value":10515.1669921875,"x":16,"y":13},{"value":10069.9169921875,"x":17,"y":13},{"value":9607.1669921875,"x":18,"y":13},{"value":9139.75,"x":19,"y":13},{"value":8663.25,"x":20,"y":13},{"value":8301.0830078125,"x":21,"y":13},{"value":8265.75,"x":22,"y":13},{"value":8227.6669921875,"x":23,"y":13},{"value":8405.25,"x":24,"y":13},{"value":8613.6669921875,"x":25,"y":13},{"value":8874.75,"x":26,"y":13},{"value":9212.4169921875,"x":27,"y":13},{"value":9358.75,"x":28,"y":13},{"value":9574.0,"x":29,"y":13},{"value":9561.75,"x":30,"y":13},{"value":9737.5,"x":31,"y":13},{"value":13831.1669921875,"x":0,"y":14},{"value":14463.0,"x":1,"y":14},{"value":14986.9169921875,"x":2,"y":14},{"value":15089.8330078125,"x":3,"y":14},{"value":14860.25,"x":4,"y":14},{"value":14168.25,"x":5,"y":14},{"value":13373.8330078125,"x":6,"y":14},{"value":12447.5830078125,"x":7,"y":14},{"value":11689.1669921875,"x":8,"y":14},{"value":11143.4169921875,"x":9,"y":14},{"value":10743.9169921875,"x":10,"y":14},{"value":10797.6669921875,"x":11,"y":14},{"value":11104.25,"x":12,"y":14},{"value":11394.6669921875,"x":13,"y":14},{"value":11749.4169921875,"x":14,"y":14},{"value":11724.9169921875,"x":15,"y":14},{"value":11426.1669921875,"x":16,"y":14},{"value":10915.9169921875,"x":17,"y":14},{"value":10059.1669921875,"x":18,"y":14},{"value":9151.8330078125,"x":19,"y":14},{"value":8337.5830078125,"x":20,"y":14},{"value":7715.91650390625,"x":21,"y":14},{"value":7424.33349609375,"x":22,"y":14},{"value":7624.66650390625,"x":23,"y":14},{"value":8118.91650390625,"x":24,"y":14},{"value":8763.4169921875,"x":25,"y":14},{"value":9423.6669921875,"x":26,"y":14},{"value":9954.6669921875,"x":27,"y":14},{"value":10412.1669921875,"x":28,"y":14},{"value":10352.25,"x":29,"y":14},{"value":10215.4169921875,"x":30,"y":14},{"value":9865.3330078125,"x":31,"y":14},{"value":13808.1669921875,"x":0,"y":15},{"value":14701.5830078125,"x":1,"y":15},{"value":15398.5830078125,"x":2,"y":15},{"value":15555.5830078125,"x":3,"y":15},{"value":15215.9169921875,"x":4,"y":15},{"value":14576.5,"x":5,"y":15},{"value":13431.5830078125,"x":6,"y":15},{"value":12216.9169921875,"x":7,"y":15},{"value":11355.0830078125,"x":8,"y":15},{"value":10503.6669921875,"x":9,"y":15},{"value":10336.25,"x":10,"y":15},{"value":10547.5830078125,"x":11,"y":15},{"value":11046.25,"x":12,"y":15},{"value":11707.25,"x":13,"y":15},{"value":12151.25,"x":14,"y":15},{"value":12325.4169921875,"x":15,"y":15},{"value":12079.6669921875,"x":16,"y":15},{"value":11428.9169921875,"x":17,"y":15},{"value":10363.1669921875,"x":18,"y":15},{"value":9165.75,"x":19,"y":15},{"value":8063.33349609375,"x":20,"y":15},{"value":7389.0,"x":21,"y":15},{"value":7096.5,"x":22,"y":15},{"value":7176.41650390625,"x":23,"y":15},{"value":7947.66650390625,"x":24,"y":15},{"value":8845.4169921875,"x":25,"y":15},{"value":9810.1669921875,"x":26,"y":15},{"value":10638.5830078125,"x":27,"y":15},{"value":11066.4169921875,"x":28,"y":15},{"value":11021.4169921875,"x":29,"y":15},{"value":10677.75,"x":30,"y":15},{"value":10125.0,"x":31,"y":15},{"value":13730.25,"x":0,"y":16},{"value":14676.3330078125,"x":1,"y":16},{"value":15358.0830078125,"x":2,"y":16},{"value":15537.3330078125,"x":3,"y":16},{"value":15255.25,"x":4,"y":16},{"value":14441.8330078125,"x":5,"y":16},{"value":13405.6669921875,"x":6,"y":16},{"value":12273.3330078125,"x":7,"y":16},{"value":11225.4169921875,"x":8,"y":16},{"value":10467.1669921875,"x":9,"y":16},{"value":10298.5,"x":10,"y":16},{"value":10529.9169921875,"x":11,"y":16},{"value":11013.25,"x":12,"y":16},{"value":11678.8330078125,"x":13,"y":16},{"value":12189.5,"x":14,"y":16},{"value":12475.4169921875,"x":15,"y":16},{"value":12254.8330078125,"x":16,"y":16},{"value":11560.6669921875,"x":17,"y":16},{"value":10492.5830078125,"x":18,"y":16},{"value":9341.0,"x":19,"y":16},{"value":8274.5,"x":20,"y":16},{"value":7445.08349609375,"x":21,"y":16},{"value":7272.75,"x":22,"y":16},{"value":7434.66650390625,"x":23,"y":16},{"value":8092.66650390625,"x":24,"y":16},{"value":9051.5830078125,"x":25,"y":16},{"value":10046.75,"x":26,"y":16},{"value":10820.9169921875,"x":27,"y":16},{"value":11292.0830078125,"x":28,"y":16},{"value":11265.3330078125,"x":29,"y":16},{"value":10915.3330078125,"x":30,"y":16},{"value":10217.8330078125,"x":31,"y":16},{"value":13728.1669921875,"x":0,"y":17},{"value":14442.0830078125,"x":1,"y":17},{"value":14841.6669921875,"x":2,"y":17},{"value":15066.1669921875,"x":3,"y":17},{"value":14757.25,"x":4,"y":17},{"value":14167.75,"x":5,"y":17},{"value":13338.0830078125,"x":6,"y":17},{"value":12364.8330078125,"x":7,"y":17},{"value":11533.8330078125,"x":8,"y":17},{"value":10921.0830078125,"x":9,"y":17},{"value":10716.5830078125,"x":10,"y":17},{"value":10876.8330078125,"x":11,"y":17},{"value":11212.5,"x":12,"y":17},{"value":11697.4169921875,"x":13,"y":17},{"value":12061.3330078125,"x":14,"y":17},{"value":12264.75,"x":15,"y":17},{"value":11998.3330078125,"x":16,"y":17},{"value":11385.75,"x":17,"y":17},{"value":10544.5,"x":18,"y":17},{"value":9588.5830078125,"x":19,"y":17},{"value":8653.6669921875,"x":20,"y":17},{"value":8106.75,"x":21,"y":17},{"value":7855.33349609375,"x":22,"y":17},{"value":7999.91650390625,"x":23,"y":17},{"value":8573.3330078125,"x":24,"y":17},{"value":9267.4169921875,"x":25,"y":17},{"value":10049.8330078125,"x":26,"y":17},{"value":10754.75,"x":27,"y":17},{"value":11050.3330078125,"x":28,"y":17},{"value":11178.0,"x":29,"y":17},{"value":10824.0830078125,"x":30,"y":17},{"value":10380.75,"x":31,"y":17},{"value":13514.25,"x":0,"y":18},{"value":13801.6669921875,"x":1,"y":18},{"value":14084.0830078125,"x":2,"y":18},{"value":14139.6669921875,"x":3,"y":18},{"value":13958.5830078125,"x":4,"y":18},{"value":13564.8330078125,"x":5,"y":18},{"value":13070.0830078125,"x":6,"y":18},{"value":12505.3330078125,"x":7,"y":18},{"value":12043.8330078125,"x":8,"y":18},{"value":11659.25,"x":9,"y":18},{"value":11467.4169921875,"x":10,"y":18},{"value":11407.8330078125,"x":11,"y":18},{"value":11441.8330078125,"x":12,"y":18},{"value":11645.75,"x":13,"y":18},{"value":11713.4169921875,"x":14,"y":18},{"value":11525.75,"x":15,"y":18},{"value":11332.3330078125,"x":16,"y":18},{"value":10999.1669921875,"x":17,"y":18},{"value":10517.5,"x":18,"y":18},{"value":9910.4169921875,"x":19,"y":18},{"value":9362.75,"x":20,"y":18},{"value":9078.25,"x":21,"y":18},{"value":8975.4169921875,"x":22,"y":18},{"value":9000.6669921875,"x":23,"y":18},{"value":9224.75,"x":24,"y":18},{"value":9557.0830078125,"x":25,"y":18},{"value":9995.3330078125,"x":26,"y":18},{"value":10288.5830078125,"x":27,"y":18},{"value":10530.4169921875,"x":28,"y":18},{"value":10690.25,"x":29,"y":18},{"value":10585.3330078125,"x":30,"y":18},{"value":10499.1669921875,"x":31,"y":18},{"value":13254.0,"x":0,"y":19},{"value":13178.4169921875,"x":1,"y":19},{"value":12992.3330078125,"x":2,"y":19},{"value":12981.8330078125,"x":3,"y":19},{"value":12842.9169921875,"x":4,"y":19},{"value":12907.3330078125,"x":5,"y":19},{"value":12810.0830078125,"x":6,"y":19},{"value":12826.8330078125,"x":7,"y":19},{"value":12790.5830078125,"x":8,"y":19},{"value":12586.9169921875,"x":9,"y":19},{"value":12308.9169921875,"x":10,"y":19},{"value":12084.9169921875,"x":11,"y":19},{"value":11720.5830078125,"x":12,"y":19},{"value":11425.9169921875,"x":13,"y":19},{"value":11049.0830078125,"x":14,"y":19},{"value":10818.5,"x":15,"y":19},{"value":10615.25,"x":16,"y":19},{"value":10369.9169921875,"x":17,"y":19},{"value":10391.25,"x":18,"y":19},{"value":10364.8330078125,"x":19,"y":19},{"value":10319.5,"x":20,"y":19},{"value":10340.0,"x":21,"y":19},{"value":10235.0,"x":22,"y":19},{"value":10176.0,"x":23,"y":19},{"value":10072.25,"x":24,"y":19},{"value":9882.8330078125,"x":25,"y":19},{"value":9862.8330078125,"x":26,"y":19},{"value":9879.75,"x":27,"y":19},{"value":9860.9169921875,"x":28,"y":19},{"value":10128.3330078125,"x":29,"y":19},{"value":10393.1669921875,"x":30,"y":19},{"value":10649.75,"x":31,"y":19},{"value":13019.8330078125,"x":0,"y":20},{"value":12413.75,"x":1,"y":20},{"value":12053.5830078125,"x":2,"y":20},{"value":11832.1669921875,"x":3,"y":20},{"value":11860.4169921875,"x":4,"y":20},{"value":12099.3330078125,"x":5,"y":20},{"value":12604.75,"x":6,"y":20},{"value":13090.5,"x":7,"y":20},{"value":13269.3330078125,"x":8,"y":20},{"value":13430.3330078125,"x":9,"y":20},{"value":13267.5830078125,"x":10,"y":20},{"value":12658.6669921875,"x":11,"y":20},{"value":12018.5,"x":12,"y":20},{"value":11237.0,"x":13,"y":20},{"value":10524.3330078125,"x":14,"y":20},{"value":10104.0830078125,"x":15,"y":20},{"value":9846.1669921875,"x":16,"y":20},{"value":9854.25,"x":17,"y":20},{"value":10209.9169921875,"x":18,"y":20},{"value":10689.5830078125,"x":19,"y":20},{"value":11168.75,"x":20,"y":20},{"value":11428.5,"x":21,"y":20},{"value":11496.5,"x":22,"y":20},{"value":11226.9169921875,"x":23,"y":20},{"value":10856.75,"x":24,"y":20},{"value":10290.1669921875,"x":25,"y":20},{"value":9719.75,"x":26,"y":20},{"value":9382.0830078125,"x":27,"y":20},{"value":9292.3330078125,"x":28,"y":20},{"value":9589.4169921875,"x":29,"y":20},{"value":10022.75,"x":30,"y":20},{"value":10746.3330078125,"x":31,"y":20},{"value":12784.75,"x":0,"y":21},{"value":11903.3330078125,"x":1,"y":21},{"value":11243.5,"x":2,"y":21},{"value":10886.75,"x":3,"y":21},{"value":11075.5830078125,"x":4,"y":21},{"value":11598.5,"x":5,"y":21},{"value":12356.0830078125,"x":6,"y":21},{"value":13161.4169921875,"x":7,"y":21},{"value":13704.25,"x":8,"y":21},{"value":14056.3330078125,"x":9,"y":21},{"value":13796.25,"x":10,"y":21},{"value":13218.0830078125,"x":11,"y":21},{"value":12210.3330078125,"x":12,"y":21},{"value":11113.5830078125,"x":13,"y":21},{"value":10241.0830078125,"x":14,"y":21},{"value":9666.0830078125,"x":15,"y":21},{"value":9460.1669921875,"x":16,"y":21},{"value":9717.1669921875,"x":17,"y":21},{"value":10186.9169921875,"x":18,"y":21},{"value":11077.4169921875,"x":19,"y":21},{"value":11812.5830078125,"x":20,"y":21},{"value":12435.1669921875,"x":21,"y":21},{"value":12489.0830078125,"x":22,"y":21},{"value":12130.1669921875,"x":23,"y":21},{"value":11485.5830078125,"x":24,"y":21},{"value":10720.8330078125,"x":25,"y":21},{"value":9852.8330078125,"x":26,"y":21},{"value":9151.0,"x":27,"y":21},{"value":8977.25,"x":28,"y":21},{"value":9312.8330078125,"x":29,"y":21},{"value":9923.6669921875,"x":30,"y":21},{"value":10973.0830078125,"x":31,"y":21},{"value":12450.6669921875,"x":0,"y":22},{"value":11552.5830078125,"x":1,"y":22},{"value":10828.5,"x":2,"y":22},{"value":10481.75,"x":3,"y":22},{"value":10712.0,"x":4,"y":22},{"value":11173.8330078125,"x":5,"y":22},{"value":12151.8330078125,"x":6,"y":22},{"value":13042.4169921875,"x":7,"y":22},{"value":13692.25,"x":8,"y":22},{"value":14028.25,"x":9,"y":22},{"value":13930.3330078125,"x":10,"y":22},{"value":13259.6669921875,"x":11,"y":22},{"value":12314.75,"x":12,"y":22},{"value":11181.0,"x":13,"y":22},{"value":10272.5830078125,"x":14,"y":22},{"value":9593.75,"x":15,"y":22},{"value":9471.5,"x":16,"y":22},{"value":9744.75,"x":17,"y":22},{"value":10481.4169921875,"x":18,"y":22},{"value":11407.0,"x":19,"y":22},{"value":12247.0,"x":20,"y":22},{"value":12940.3330078125,"x":21,"y":22},{"value":12994.5830078125,"x":22,"y":22},{"value":12662.0830078125,"x":23,"y":22},{"value":11997.5830078125,"x":24,"y":22},{"value":11026.0,"x":25,"y":22},{"value":10178.0,"x":26,"y":22},{"value":9459.1669921875,"x":27,"y":22},{"value":9203.5,"x":28,"y":22},{"value":9496.6669921875,"x":29,"y":22},{"value":10235.0,"x":30,"y":22},{"value":11304.1669921875,"x":31,"y":22},{"value":12267.8330078125,"x":0,"y":23},{"value":11407.3330078125,"x":1,"y":23},{"value":10887.0,"x":2,"y":23},{"value":10554.9169921875,"x":3,"y":23},{"value":10729.6669921875,"x":4,"y":23},{"value":11194.0830078125,"x":5,"y":23},{"value":11902.0830078125,"x":6,"y":23},{"value":12717.5,"x":7,"y":23},{"value":13416.4169921875,"x":8,"y":23},{"value":13695.8330078125,"x":9,"y":23},{"value":13616.4169921875,"x":10,"y":23},{"value":13106.0830078125,"x":11,"y":23},{"value":12232.0830078125,"x":12,"y":23},{"value":11436.8330078125,"x":13,"y":23},{"value":10557.1669921875,"x":14,"y":23},{"value":10148.5,"x":15,"y":23},{"value":9923.9169921875,"x":16,"y":23},{"value":10254.0,"x":17,"y":23},{"value":10929.0,"x":18,"y":23},{"value":11733.3330078125,"x":19,"y":23},{"value":12379.8330078125,"x":20,"y":23},{"value":12919.9169921875,"x":21,"y":23},{"value":13080.3330078125,"x":22,"y":23},{"value":12840.75,"x":23,"y":23},{"value":12243.5,"x":24,"y":23},{"value":11531.0,"x":25,"y":23},{"value":10655.0,"x":26,"y":23},{"value":10109.3330078125,"x":27,"y":23},{"value":9934.5830078125,"x":28,"y":23},{"value":10217.5,"x":29,"y":23},{"value":10853.25,"x":30,"y":23},{"value":11621.1669921875,"x":31,"y":23},{"value":11866.5830078125,"x":0,"y":24},{"value":11447.1669921875,"x":1,"y":24},{"value":11096.5830078125,"x":2,"y":24},{"value":11010.1669921875,"x":3,"y":24},{"value":11099.1669921875,"x":4,"y":24},{"value":11423.75,"x":5,"y":24},{"value":11830.5830078125,"x":6,"y":24},{"value":12339.75,"x":7,"y":24},{"value":12713.25,"x":8,"y":24},{"value":12906.25,"x":9,"y":24},{"value":12792.3330078125,"x":10,"y":24},{"value":12557.0,"x":11,"y":24},{"value":12097.0,"x":12,"y":24},{"value":11583.4169921875,"x":13,"y":24},{"value":11228.25,"x":14,"y":24},{"value":10917.4169921875,"x":15,"y":24},{"value":10906.75,"x":16,"y":24},{"value":11024.0830078125,"x":17,"y":24},{"value":11389.0,"x":18,"y":24},{"value":11944.0830078125,"x":19,"y":24},{"value":12352.5830078125,"x":20,"y":24},{"value":12679.75,"x":21,"y":24},{"value":12806.9169921875,"x":22,"y":24},{"value":12592.6669921875,"x":23,"y":24},{"value":12346.0,"x":24,"y":24},{"value":11899.0,"x":25,"y":24},{"value":11456.5830078125,"x":26,"y":24},{"value":11224.1669921875,"x":27,"y":24},{"value":11118.8330078125,"x":28,"y":24},{"value":11246.9169921875,"x":29,"y":24},{"value":11566.5,"x":30,"y":24},{"value":12089.3330078125,"x":31,"y":24},{"value":11570.3330078125,"x":0,"y":25},{"value":11629.25,"x":1,"y":25},{"value":11607.75,"x":2,"y":25},{"value":11769.5830078125,"x":3,"y":25},{"value":11705.6669921875,"x":4,"y":25},{"value":11669.75,"x":5,"y":25},{"value":11683.9169921875,"x":6,"y":25},{"value":11741.25,"x":7,"y":25},{"value":11847.0,"x":8,"y":25},{"value":11870.5,"x":9,"y":25},{"value":11936.6669921875,"x":10,"y":25},{"value":11899.8330078125,"x":11,"y":25},{"value":11963.25,"x":12,"y":25},{"value":12075.6669921875,"x":13,"y":25},{"value":12028.25,"x":14,"y":25},{"value":11939.25,"x":15,"y":25},{"value":12045.8330078125,"x":16,"y":25},{"value":12067.75,"x":17,"y":25},{"value":12131.1669921875,"x":18,"y":25},{"value":12170.6669921875,"x":19,"y":25},{"value":12222.9169921875,"x":20,"y":25},{"value":12233.25,"x":21,"y":25},{"value":12251.1669921875,"x":22,"y":25},{"value":12265.1669921875,"x":23,"y":25},{"value":12271.8330078125,"x":24,"y":25},{"value":12307.1669921875,"x":25,"y":25},{"value":12421.5830078125,"x":26,"y":25},{"value":12402.0830078125,"x":27,"y":25},{"value":12469.75,"x":28,"y":25},{"value":12472.9169921875,"x":29,"y":25},{"value":12540.4169921875,"x":30,"y":25},{"value":12434.5,"x":31,"y":25},{"value":11229.6669921875,"x":0,"y":26},{"value":11679.1669921875,"x":1,"y":26},{"value":12151.0830078125,"x":2,"y":26},{"value":12294.4169921875,"x":3,"y":26},{"value":12330.9169921875,"x":4,"y":26},{"value":12052.1669921875,"x":5,"y":26},{"value":11695.75,"x":6,"y":26},{"value":11248.25,"x":7,"y":26},{"value":10899.5830078125,"x":8,"y":26},{"value":10846.5830078125,"x":9,"y":26},{"value":10913.25,"x":10,"y":26},{"value":11189.5,"x":11,"y":26},{"value":11783.25,"x":12,"y":26},{"value":12316.3330078125,"x":13,"y":26},{"value":12794.5830078125,"x":14,"y":26},{"value":13060.0830078125,"x":15,"y":26},{"value":13159.8330078125,"x":16,"y":26},{"value":13067.5830078125,"x":17,"y":26},{"value":12743.5,"x":18,"y":26},{"value":12328.0830078125,"x":19,"y":26},{"value":11974.4169921875,"x":20,"y":26},{"value":11811.25,"x":21,"y":26},{"value":11654.5830078125,"x":22,"y":26},{"value":11901.9169921875,"x":23,"y":26},{"value":12286.1669921875,"x":24,"y":26},{"value":12750.8330078125,"x":25,"y":26},{"value":13286.1669921875,"x":26,"y":26},{"value":13666.25,"x":27,"y":26},{"value":13807.3330078125,"x":28,"y":26},{"value":13734.75,"x":29,"y":26},{"value":13453.5830078125,"x":30,"y":26},{"value":12946.9169921875,"x":31,"y":26},{"value":10945.75,"x":0,"y":27},{"value":11782.0830078125,"x":1,"y":27},{"value":12367.6669921875,"x":2,"y":27},{"value":12830.25,"x":3,"y":27},{"value":12668.9169921875,"x":4,"y":27},{"value":12211.75,"x":5,"y":27},{"value":11465.4169921875,"x":6,"y":27},{"value":10816.5,"x":7,"y":27},{"value":10301.5830078125,"x":8,"y":27},{"value":10022.4169921875,"x":9,"y":27},{"value":10226.0,"x":10,"y":27},{"value":10740.75,"x":11,"y":27},{"value":11604.4169921875,"x":12,"y":27},{"value":12563.1669921875,"x":13,"y":27},{"value":13428.25,"x":14,"y":27},{"value":13992.0830078125,"x":15,"y":27},{"value":14080.25,"x":16,"y":27},{"value":13893.75,"x":17,"y":27},{"value":13358.5,"x":18,"y":27},{"value":12611.1669921875,"x":19,"y":27},{"value":11967.25,"x":20,"y":27},{"value":11441.4169921875,"x":21,"y":27},{"value":11386.75,"x":22,"y":27},{"value":11752.6669921875,"x":23,"y":27},{"value":12375.8330078125,"x":24,"y":27},{"value":13213.3330078125,"x":25,"y":27},{"value":14095.0,"x":26,"y":27},{"value":14608.8330078125,"x":27,"y":27},{"value":14904.75,"x":28,"y":27},{"value":14785.8330078125,"x":29,"y":27},{"value":14214.75,"x":30,"y":27},{"value":13366.0,"x":31,"y":27},{"value":10589.0,"x":0,"y":28},{"value":11649.8330078125,"x":1,"y":28},{"value":12454.9169921875,"x":2,"y":28},{"value":12774.75,"x":3,"y":28},{"value":12724.25,"x":4,"y":28},{"value":12137.6669921875,"x":5,"y":28},{"value":11381.9169921875,"x":6,"y":28},{"value":10506.5,"x":7,"y":28},{"value":9785.9169921875,"x":8,"y":28},{"value":9590.0,"x":9,"y":28},{"value":9873.25,"x":10,"y":28},{"value":10484.75,"x":11,"y":28},{"value":11538.5830078125,"x":12,"y":28},{"value":12719.75,"x":13,"y":28},{"value":13761.25,"x":14,"y":28},{"value":14410.6669921875,"x":15,"y":28},{"value":14729.5830078125,"x":16,"y":28},{"value":14452.25,"x":17,"y":28},{"value":13791.0830078125,"x":18,"y":28},{"value":12922.1669921875,"x":19,"y":28},{"value":12037.3330078125,"x":20,"y":28},{"value":11573.0830078125,"x":21,"y":28},{"value":11524.5830078125,"x":22,"y":28},{"value":11805.5830078125,"x":23,"y":28},{"value":12666.8330078125,"x":24,"y":28},{"value":13615.25,"x":25,"y":28},{"value":14637.0830078125,"x":26,"y":28},{"value":15387.0,"x":27,"y":28},{"value":15712.0,"x":28,"y":28},{"value":15392.8330078125,"x":29,"y":28},{"value":14731.8330078125,"x":30,"y":28},{"value":1.38e4,"x":31,"y":28},{"value":10349.75,"x":0,"y":29},{"value":11355.0,"x":1,"y":29},{"value":12006.5,"x":2,"y":29},{"value":12311.75,"x":3,"y":29},{"value":12341.1669921875,"x":4,"y":29},{"value":11858.9169921875,"x":5,"y":29},{"value":11183.5,"x":6,"y":29},{"value":10481.75,"x":7,"y":29},{"value":9872.6669921875,"x":8,"y":29},{"value":9674.6669921875,"x":9,"y":29},{"value":9925.8330078125,"x":10,"y":29},{"value":10664.8330078125,"x":11,"y":29},{"value":11681.3330078125,"x":12,"y":29},{"value":12787.1669921875,"x":13,"y":29},{"value":13782.3330078125,"x":14,"y":29},{"value":14414.6669921875,"x":15,"y":29},{"value":14734.1669921875,"x":16,"y":29},{"value":14490.25,"x":17,"y":29},{"value":13928.8330078125,"x":18,"y":29},{"value":13280.5,"x":19,"y":29},{"value":12528.5830078125,"x":20,"y":29},{"value":12152.0830078125,"x":21,"y":29},{"value":12024.8330078125,"x":22,"y":29},{"value":12401.0,"x":23,"y":29},{"value":13139.75,"x":24,"y":29},{"value":14041.6669921875,"x":25,"y":29},{"value":14860.25,"x":26,"y":29},{"value":15616.4169921875,"x":27,"y":29},{"value":15803.1669921875,"x":28,"y":29},{"value":15657.0,"x":29,"y":29},{"value":14939.5830078125,"x":30,"y":29},{"value":14063.1669921875,"x":31,"y":29},{"value":10065.75,"x":0,"y":30},{"value":10747.6669921875,"x":1,"y":30},{"value":11138.8330078125,"x":2,"y":30},{"value":11486.5,"x":3,"y":30},{"value":11528.0830078125,"x":4,"y":30},{"value":11353.1669921875,"x":5,"y":30},{"value":10897.0,"x":6,"y":30},{"value":10468.75,"x":7,"y":30},{"value":10236.8330078125,"x":8,"y":30},{"value":10226.3330078125,"x":9,"y":30},{"value":10534.75,"x":10,"y":30},{"value":11072.5830078125,"x":11,"y":30},{"value":11762.4169921875,"x":12,"y":30},{"value":12697.9169921875,"x":13,"y":30},{"value":13475.0,"x":14,"y":30},{"value":14017.0,"x":15,"y":30},{"value":14327.8330078125,"x":16,"y":30},{"value":14194.1669921875,"x":17,"y":30},{"value":14011.1669921875,"x":18,"y":30},{"value":13639.8330078125,"x":19,"y":30},{"value":13278.5830078125,"x":20,"y":30},{"value":12988.8330078125,"x":21,"y":30},{"value":13014.8330078125,"x":22,"y":30},{"value":13323.75,"x":23,"y":30},{"value":13839.75,"x":24,"y":30},{"value":14364.3330078125,"x":25,"y":30},{"value":14964.9169921875,"x":26,"y":30},{"value":15455.5,"x":27,"y":30},{"value":15595.0830078125,"x":28,"y":30},{"value":15414.5,"x":29,"y":30},{"value":14984.3330078125,"x":30,"y":30},{"value":14227.8330078125,"x":31,"y":30},{"value":9903.4169921875,"x":0,"y":31},{"value":10073.5830078125,"x":1,"y":31},{"value":10197.25,"x":2,"y":31},{"value":10370.1669921875,"x":3,"y":31},{"value":10447.25,"x":4,"y":31},{"value":10662.75,"x":5,"y":31},{"value":10658.25,"x":6,"y":31},{"value":10793.0,"x":7,"y":31},{"value":10842.5830078125,"x":8,"y":31},{"value":11100.5,"x":9,"y":31},{"value":11418.3330078125,"x":10,"y":31},{"value":11693.8330078125,"x":11,"y":31},{"value":12068.8330078125,"x":12,"y":31},{"value":12534.5830078125,"x":13,"y":31},{"value":12971.0,"x":14,"y":31},{"value":13268.25,"x":15,"y":31},{"value":13587.9169921875,"x":16,"y":31},{"value":13872.9169921875,"x":17,"y":31},{"value":13899.0,"x":18,"y":31},{"value":13967.6669921875,"x":19,"y":31},{"value":14106.5,"x":20,"y":31},{"value":14220.75,"x":21,"y":31},{"value":14241.0830078125,"x":22,"y":31},{"value":14445.5830078125,"x":23,"y":31},{"value":14617.9169921875,"x":24,"y":31},{"value":14818.3330078125,"x":25,"y":31},{"value":14898.5,"x":26,"y":31},{"value":14984.1669921875,"x":27,"y":31},{"value":15002.75,"x":28,"y":31},{"value":14961.0830078125,"x":29,"y":31},{"value":14693.4169921875,"x":30,"y":31},{"value":14463.4169921875,"x":31,"y":31}]},"encoding":{"color":{"field":"value","scale":{"scheme":"greens"},"title":"Mean DN","type":"quantitative"},"x":{"field":"x","title":"X (pixels / 4)","type":"ordinal"},"y":{"field":"y","sort":"descending","title":"Y (pixels / 4)","type":"ordinal"}},"height":500,"mark":"rect","title":"Green Band - Temporal Mean (Downsampled 4x)","width":500}
# Visualize NDVI time series
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

ndvi_chart_data =
  ndvi_time_series
  |> Enum.with_index()
  |> Enum.map(fn {ndvi, idx} ->
    %{month: Enum.at(months, idx), ndvi: ndvi}
  end)

Vl.new(width: 600, height: 300, title: "NDVI Time Series - Center Pixel")
|> Vl.data_from_values(ndvi_chart_data)
|> Vl.mark(:line, point: true)
|> Vl.encode_field(:x, "month", type: :ordinal, title: "Month")
|> Vl.encode_field(:y, "ndvi", type: :quantitative, title: "NDVI", scale: [domain: [-1, 1]])
{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","data":{"values":[{"month":"Jan","ndvi":0.3922714553890837},{"month":"Feb","ndvi":0.3649206238198475},{"month":"Mar","ndvi":0.3157110417258019},{"month":"Apr","ndvi":0.24962630792227206},{"month":"May","ndvi":0.19052013732511353},{"month":"Jun","ndvi":0.12483544589461015},{"month":"Jul","ndvi":0.10060906835100383},{"month":"Aug","ndvi":0.12332239785266925},{"month":"Sep","ndvi":0.1909693763504955},{"month":"Oct","ndvi":0.2516347072220998},{"month":"Nov","ndvi":0.3277174105404736},{"month":"Dec","ndvi":0.380464722084893}]},"encoding":{"x":{"field":"month","title":"Month","type":"ordinal"},"y":{"field":"ndvi","scale":{"domain":[-1,1]},"title":"NDVI","type":"quantitative"}},"height":300,"mark":{"point":true,"type":"line"},"title":"NDVI Time Series - Center Pixel","width":600}

Future Directions

Icechunk: Versioning for Zarr

What is Icechunk?

Icechunk provides version control for Zarr datastores, similar to Git for code:

  • Commit snapshots of data state
  • Branch for parallel workflows
  • Merge changes from multiple sources
  • Time-travel to historical versions

Benefits:

  1. Reproducibility: Exact data version used in analysis
  2. Collaboration: Multiple users edit without conflicts
  3. Provenance: Track data lineage and transformations
  4. Experimentation: Test changes without affecting production

Current Status:

Icechunk is in active development (Python implementation available). ExZarr integration would require:

  • Version metadata management
  • Snapshot coordination with storage backend
  • Merge conflict resolution strategies
  • Transaction support for atomic updates

Simulating Versioning with Groups:

Without Icechunk, you can simulate versioning using Zarr groups:

# Simulate version control with groups
{:ok, versions_root} = ExZarr.Group.create("/versions", storage: :memory)

# Create version 1: baseline
{:ok, v1_group} = ExZarr.Group.create_group(versions_root, "v1_baseline")

{:ok, v1_array} =
  ExZarr.Group.create_array(v1_group, "data",
    shape: {100, 100},
    chunks: {50, 50},
    dtype: :float32
  )

key = Nx.Random.key(42)
{v1_data, _key} = Nx.Random.uniform(key, shape: {100, 100}, type: :f32)
DatacubeHelpers.write_tensor!(v1_array, v1_data)

v1_attrs = %{
  "version" => "1.0",
  "created_at" => DateTime.utc_now() |> DateTime.to_iso8601(),
  "description" => "Baseline version",
  "author" => "analyst@example.com"
}

v1_group = Enum.reduce(v1_attrs, v1_group, fn {k, v}, g -> ExZarr.Group.set_attr(g, k, v) end)

# Create version 2: processed
{:ok, v2_group} = ExZarr.Group.create_group(versions_root, "v2_processed")

{:ok, v2_array} =
  ExZarr.Group.create_array(v2_group, "data",
    shape: {100, 100},
    chunks: {50, 50},
    dtype: :float32
  )

# Apply transformation (e.g., normalization)
v2_data = Nx.divide(v1_data, Nx.mean(v1_data))
DatacubeHelpers.write_tensor!(v2_array, v2_data)

v2_attrs = %{
  "version" => "2.0",
  "created_at" => DateTime.utc_now() |> DateTime.to_iso8601(),
  "description" => "Normalized version",
  "parent_version" => "1.0",
  "author" => "analyst@example.com"
}

v2_group = Enum.reduce(v2_attrs, v2_group, fn {k, v}, g -> ExZarr.Group.set_attr(g, k, v) end)

IO.puts("Created version hierarchy:")
IO.puts("  - v1_baseline: original data")
IO.puts("  - v2_processed: normalized")
Created version hierarchy:
  - v1_baseline: original data
  - v2_processed: normalized
:ok

Limitations of Group-Based Versioning:

  • No automatic diff/merge
  • No space efficiency (full copies)
  • Manual provenance tracking
  • No transaction guarantees

Zarr v3

ExZarr Fully Supports Zarr v3:

ExZarr provides production-ready support for Zarr v3 with all core features implemented:

  1. Unified Codec Pipeline: Composable compression/encoding stages
  2. Improved Metadata: Flexible zarr.json format
  3. Hierarchical Chunk Storage: c/ directory structure
  4. Automatic Version Detection: Seamlessly work with v2 and v3 arrays
  5. Python Interoperability: Full compatibility with zarr-python 3.x

Creating v3 Arrays:

# Create a Zarr v3 array (fully supported)
{:ok, v3_array} = ExZarr.create(
  shape: {10_000, 10_000},
  chunks: {100, 100},
  dtype: :float32,
  codecs: [
    %{name: "bytes"},
    %{name: "gzip", configuration: %{level: 5}}
  ],
  zarr_version: 3,
  storage: :memory
)

IO.puts("Zarr v3 array created successfully")
Zarr v3 array created successfully
:ok

v3 Advanced Features:

ExZarr v3 includes:

  • Unified codec pipeline with configurable stages
  • Improved metadata with embedded attributes
  • Slash-separated chunk keys for better organization
  • Automatic conversion from v2-style configuration

Sharding (v3 Extension):

Sharding is a v3 extension that groups multiple chunks into single objects:

# Conceptual sharding structure (extension feature)
v3_sharding_concept = %{
  "shape" => {10_000, 10_000},
  "chunks" => {100, 100},
  "shards" => {1000, 1000},
  "chunks_per_shard" => {10, 10},
  "benefit" => "100 chunks stored in 1 S3 object instead of 100",
  "use_case" => "Reduce LIST operations on object storage"
}

IO.inspect(v3_sharding_concept, label: "Zarr v3 Sharding Concept")
Zarr v3 Sharding Concept: %{
  "benefit" => "100 chunks stored in 1 S3 object instead of 100",
  "chunks" => {100, 100},
  "chunks_per_shard" => {10, 10},
  "shape" => {10000, 10000},
  "shards" => {1000, 1000},
  "use_case" => "Reduce LIST operations on object storage"
}
%{
  "benefit" => "100 chunks stored in 1 S3 object instead of 100",
  "chunks" => {100, 100},
  "chunks_per_shard" => {10, 10},
  "shape" => {10000, 10000},
  "shards" => {1000, 1000},
  "use_case" => "Reduce LIST operations on object storage"
}

Note: Sharding is an optional v3 extension. Core v3 features are fully implemented in ExZarr.

Version Selection:

  • Use v3 for new projects (recommended)
  • Use v2 for compatibility with older Python tools
  • Both versions fully supported and production-ready
  • Automatic detection when opening existing arrays

Performance Characteristics

Understanding performance trade-offs in datacube operations:

defmodule PerformanceAnalysis do
  def analyze_access_patterns(shape, chunks) do
    {time_steps, bands, height, width} = shape
    {t_chunk, b_chunk, y_chunk, x_chunk} = chunks

    """
    ## Access Pattern Performance Analysis

    **Dataset:** #{time_steps}t × #{bands}b × #{height}y × #{width}x
    **Chunks:** #{t_chunk}t × #{b_chunk}b × #{y_chunk}y × #{x_chunk}x

    ### Pattern 1: Single Pixel Time Series
    - **Goal:** Extract all time steps for one spatial location
    - **Chunks accessed:** #{time_steps}
    - **Data read:** #{time_steps * b_chunk * y_chunk * x_chunk * 2} bytes
    - **Efficiency:** Low (reads full spatial chunks for 1 pixel)
    - **Optimization:** Use smaller spatial chunks or rechunk for time series access

    ### Pattern 2: Single Time Slice
    - **Goal:** Extract one time step, all spatial pixels
    - **Chunks accessed:** #{div(height, y_chunk) * div(width, x_chunk)}
    - **Data read:** #{div(height, y_chunk) * div(width, x_chunk) * t_chunk * b_chunk * y_chunk * x_chunk * 2} bytes
    - **Efficiency:** High (spatial chunks align with query)
    - **Use case:** Spatial analysis, mapping

    ### Pattern 3: Spatial Subset, All Times
    - **Goal:** 500×500 region across all time steps
    - **Chunks accessed:** #{time_steps * div(500, y_chunk) * div(500, x_chunk)}
    - **Data read:** ~#{time_steps * div(500, y_chunk) * div(500, x_chunk) * t_chunk * b_chunk * y_chunk * x_chunk * 2} bytes
    - **Efficiency:** Medium (balanced spatial/temporal access)
    - **Use case:** Regional time series analysis

    ### Pattern 4: Full Scan
    - **Goal:** Process entire datacube
    - **Chunks accessed:** #{div(time_steps, t_chunk) * div(height, y_chunk) * div(width, x_chunk)}
    - **Data read:** All (#{time_steps * bands * height * width * 2} bytes)
    - **Efficiency:** High (sequential chunk access)
    - **Use case:** Global statistics, model training

    ### Recommendations:

    **For Time Series Analysis:**
    - Use temporal chunks = 1
    - Larger spatial chunks acceptable
    - Consider rechunking if switching workflows

    **For Spatial Analysis:**
    - Use spatial chunks matching query size
    - Temporal chunks = 1 or all time steps
    - Optimize for single-time access

    **For Mixed Workflows:**
    - Balanced chunks (current configuration)
    - Accept moderate inefficiency for flexibility
    - Consider multiple representations (spatial + temporal optimized)
    """
  end
end

PerformanceAnalysis.analyze_access_patterns(shape, chunks)
|> Kino.Markdown.new()

Summary

This notebook demonstrated datacube construction and chunk-based computation:

Key Concepts:

  1. Datacube Structure: 4D arrays (time × band × y × x) for spatiotemporal data
  2. Chunk Strategy: Optimize for access patterns (spatial vs temporal queries)
  3. Chunk-Based Computation: Process large datasets incrementally
  4. Zonal Statistics: Aggregate over spatial regions
  5. Access Pattern Analysis: Understand performance implications

Chunk-Aware Processing:

  • Enables datasets larger than memory
  • Supports parallel computation
  • Reduces I/O by reading only required chunks
  • Requires explicit iteration over chunk grid

Performance Considerations:

  • Chunk size affects both memory and I/O efficiency
  • Access patterns should match chunk layout
  • Cloud storage amplifies chunking impact (latency, cost)
  • Consider multiple representations for different workflows

Future Directions:

  • Icechunk: Version control and provenance tracking
  • Zarr v3 Sharding Extension: Multi-chunk objects for reduced object count
  • Distributed Computing: Integration with GenStage/Flow
  • Custom Codecs: Domain-specific compression algorithms

Next Steps:

  • Experiment with chunk sizes for your data
  • Profile access patterns in real workflows
  • Build reusable computation pipelines
  • Integrate with cloud storage backends
  • Explore distributed processing patterns

Open Questions

Optimal Chunking:

How do you choose chunk sizes for datasets with mixed access patterns?

Versioning Strategy:

What version control model best fits scientific datacube workflows?

Distributed Processing:

How can GenServer/GenStage coordinate parallel chunk processing across nodes?

Compression:

Which codecs work best for different satellite sensor types?

Cloud Optimization:

How do you minimize S3 request costs while maintaining good performance?

Interoperability:

How can ExZarr datacubes integrate with Python tools like Xarray, Dask, and Pangeo?

Explore these questions as you build production datacube systems with ExZarr.