Metadata-Version: 2.4
Name: climate-canvas
Version: 0.1.0
Summary: A python command line application and package for plotting climate impact assessment response surfaces and other climate change scenario visualizations.
Project-URL: repository, https://github.com/JohnRushKucharski/climate-canvas
Author-email: John Kucharski <johnkucharski@gmail.com>
License-Expression: GPL-3.0-or-later
License-File: LICENSE.txt
Keywords: climate,climate-change,decision-scaling,impact-assessment,response-surface,visualization
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Python: >=3.12
Requires-Dist: matplotlib<4.0.0,>=3.9.1.post1
Requires-Dist: numpy<3.0.0,>=2.0.1
Requires-Dist: openpyxl<4.0.0,>=3.1.5
Requires-Dist: pandas<3.0.0,>=2.2.2
Requires-Dist: scipy<2.0.0,>=1.14.0
Requires-Dist: typer<0.28.0,>=0.12.3
Description-Content-Type: text/markdown

# climate-canvas

Python package and command line interface (CLI) for plotting climate impact study response surfaces and other climate change scenario visualizations.

![plot](./img/complex_interp.png)

Example: The figure above is created by running the ``uv run climate-canvas response examples\complex_surface.csv --interp`` climate-canvas CLI command on the *complex_surface.csv* data distributed with the climate-canvas program.

## Installation Instructions

#### System Requirements

climate-canvas requires python 3.12+. It aims to be multi-platform and has been run on Windows 11 and MacOS 14 and 15.

#### Clone or Fork climate-canvas from GitHub
The climate-canvas source code can be found here: https://github.com/JohnRushKucharski/climate-canvas is available under the GNU Version 3 General Public License.

It can be cloned or forked by following the normal cloning or forking instructions, which are available here: https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository and here: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo.


#### Installation with uv

climate-canvas is developed with [uv](https://docs.astral.sh/uv/), which can be used to simplify the installation process.

To install uv, follow the instructions here: https://docs.astral.sh/uv/getting-started/installation/.

Once uv is installed, use your favorite shell to go to the location of the local climate-canvas repository, e.g.

``
cd <PATH_TO_LOCAL>\climate-canvas
``

Next run:

``
uv sync --group test
``

This will create a python virtual environment (``.venv``) containing all the required climate-canvas dependencies, without affecting your system's global python environment.

The climate-canvas program should be ready for use as either a python package or command line utility. To test the command line interface (CLI) type the following command into your shell:

``
uv run climate-canvas --help
``

This should return help instructions for the climate-canvas CLI.

## Basic Usage

The climate-canvas program can be extended as a python package or run through a command line interface (CLI).

The current version (0.1.0) only supports 2D climate response surface style plots. These are two-dimensional contour plots that show the impact of two variables plotted on the x and y axes on a response (i.e. z axis) variable whose values are plotted using contour lines and a colorbar. An example is shown below.

![plot](./img/exscenario_nointerp.png)

#### Command Line Interface

The example above is created **response** command. From a shell run:

``
uv run climate-canvas response --help
``

To view help for the **response** command, i.e.:

![plot](./img/response_help.png)

As the response help document describes the **response** command requires that a path to a *.csv* file containing plotting data be specified. Two example, data files are provided in the repository's *examples/* directory. The following command, using on of these example files, reproduces the figure above:

``
uv run climate-canvas response examples\scenario_data.csv
``

The *--interp* flag can be used to bi-linearly interpolate between z-axis values. For example, running the following command (using the same data from the figure above) with the *--interp* flag produces the figure below:

``
uv run climate-canvas response examples\scenario_data.csv --interp
``

![plot](./img/exscenario_interp.png)

The *--fillin* flag extends *--interp* to fill in grid cells that bilinear interpolation leaves
blank because one or more of the cell's four corners is missing. It has no effect unless *--interp*
is also set. For example:

``
uv run climate-canvas response examples\great_lakes\kingston_shoal_low_water_grid.csv --interp --fillin
``

##### The math behind `--interp` and `--fillin`

This section works through the exact arithmetic behind `--interp` (bilinear interpolation) and
`--fillin` (Delaunay-linear fallback for missing data), using the smallest possible data sets —
2x2 grids with z-values between 0 and 1 — so every number can be checked by hand.

**1. Bilinear interpolation on a complete grid — `examples/simple.csv`**

This file defines a full 2x2 grid (no missing values):

| z    | x=0 | x=1 |
|------|-----|-----|
| y=0  | 0   | 1   |
| y=1  | 1   | 1   |

Bilinear interpolation first interpolates along x at each of the two known y-rows, then
interpolates the result along y. For a query point `(x, y)`, with `px = x` and `py = y` (since the
grid spans exactly 0 to 1 here, the interpolation weight equals the coordinate):

```
z_x,y0 = z00*(1-px) + z01*px      # interpolate along x at y=0
z_x,y1 = z10*(1-px) + z11*px      # interpolate along x at y=1
z_x,y  = z_x,y0*(1-py) + z_x,y1*py  # interpolate the two results along y
```

At the midpoint `(0.5, 0.5)`:

```
z_x,y0 = 0*(1-0.5) + 1*0.5 = 0.5
z_x,y1 = 1*(1-0.5) + 1*0.5 = 1.0
z_x,y  = 0.5*(1-0.5) + 1.0*0.5 = 0.75
```

Running `uv run climate-canvas response examples\simple.csv --interp` reproduces this: every
point in the unit square is filled with the plane implied by these four corners (no blank cells,
since the one cell in this grid has all four corners known — `--fillin` has no effect here).
Without `--interp`, only the 4 known grid points are plotted (as 4 flat-colored quarter-cells);
no interpolation is performed between them.

**2. A missing corner — `examples/simple_3points.csv`**

This file is identical, except the top-right corner is missing:

| z    | x=0 | x=1  |
|------|-----|------|
| y=0  | 0   | 1    |
| y=1  | 1   | *NaN* |

There is only one cell in this grid, and one of its four corners is unknown, so:

- **Without `--interp`**: the 3 known points are plotted as flat-colored quarter-cells; the
  `(1, 1)` corner's quarter-cell is blank. No interpolation happens.
- **With `--interp` only** (`--interp`, no `--fillin`): bilinear interpolation strictly requires
  all four corners (see `interpolate_2d` in `data_utilities.py`) — with one corner missing, the
  *entire* cell returns `NaN`. The plot is blank everywhere except the 3 exact known grid points
  (which are always re-inserted into the resampled grid). See
  `examples/simple_3points_interp.png`.
- **With `--interp --fillin`**: the missing cell falls back to `delaunay_fill()`, which
  triangulates only the 3 known points — `(0, 0, z=0)`, `(1, 0, z=1)`, `(0, 1, z=1)` — into a
  single triangle and fits the plane `z = a + bx + cy` through them:

  ```
  (0,0): a           = 0   ->  a = 0
  (1,0): a + b       = 1   ->  b = 1
  (0,1): a     + c   = 1   ->  c = 1

  z = x + y
  ```

  This plane is only used *inside* the triangle's convex hull, i.e. where `x + y <= 1`:
  - `(0.25, 0.25)` -> `z = 0.25 + 0.25 = 0.5`
  - `(0.5, 0.5)` (on the hull's edge, between the two known `z=1` points) -> `z = 0.5 + 0.5 = 1.0`
    (matches both known neighbors, as expected on their connecting edge)
  - `(0.75, 0.75)` -> `x + y = 1.5 > 1`, **outside** the hull, so this stays `NaN` (blank) — the
    same as the exact missing corner `(1, 1)` itself, which has `x + y = 2 > 1`.

  So `--fillin` fills the lower-left triangle (`x + y <= 1`, nearest the 3 known points) with the
  plane above, while the upper-right triangle (nearest the missing corner) stays blank. See
  `examples/simple_3points_interp_fillin.png` next to `examples/simple_3points_interp.png` for the
  before/after.

Because the bilinear surface (example 1) and the Delaunay plane (example 2) are fit independently,
a visible seam can appear where a `--fillin` triangle borders a complete bilinear cell (the two
surfaces aren't generally coplanar there). See
[`docs/adr/0002-delaunay-linear-fillin-for-missing-grid-points.md`](./docs/adr/0002-delaunay-linear-fillin-for-missing-grid-points.md)
for the full rationale and this trade-off.

As the help documentation shows titles for the figure, x, y, and z axes can be added as optional arguments.

The *--threshold* option sets the z-value that becomes the colormap's center (yellow) color, splitting
the color range asymmetrically around it (and adjusting the contour levels to match). If omitted, it
defaults to the midpoint of the data's z-value range. Use *--color-map* to change the matplotlib
colormap (defaults to *RdBu*), and *--color-map-ticks* to set explicit colorbar tick values. If
*--threshold* is given but *--color-map-ticks* is not, the colorbar ticks default to the same
contour levels used for the black contour lines, since matplotlib's default tick locator is
linear in data value and can otherwise cluster ticks into one visually-compressed half of the
colorbar when threshold is far from the z-range's midpoint. E.g.:

``
uv run climate-canvas response examples\scenario_data.csv --threshold 0.2 --color-map RdYlBu
``

#### Multiple Response Surfaces (`response-surfaces-grid` Command)

The **response-surfaces-grid** command plots several response surfaces as subplots of one
figure, arranged in a grid. Every subplot is rendered through the exact same pathway as the
**response** command, so a subplot always matches what a standalone **response** call would
draw for the same data/options.

Unlike **response**, which takes a *.csv* file, **response-surfaces-grid** takes a single Excel
(*.xlsx*) workbook containing:

- a `main_config` sheet: figure-wide options (key in column A, value in column B) --
  `suptitle`, `sharex`, `sharey`, `shared_colorbar`, `color_map`, `threshold`,
  `color_map_ticks`, `output_directory`.
- a `main_layout` sheet: column numbers across row 1 (`B1:K1`), row numbers down column A
  (`A2:A11`, max 10x10), and a subplot id in each interior cell. The same id repeated in
  adjacent cells makes that subplot span that rectangular block of grid cells.
- one `<id>_config` sheet per subplot: the same options as **response** (`interpolate`,
  `fillin`, `xlabel`, `ylabel`, `zlabel`, `title`, `threshold`, `color_map`,
  `color_map_ticks`), same key-in-column-A/value-in-column-B shape as `main_config`.
- one `<id>_grid` sheet per subplot: that subplot's x/y/z data, in the same grid shape as the
  CSV format **response** reads.

`shared_colorbar` (default `false`) controls whether every subplot uses its own
`color_map`/`threshold`/`color_map_ticks` and its own colorbar (the default, matching a
standalone **response** call exactly for each subplot), or all subplots share one color scale
(`main_config`'s own `color_map`/`threshold`/`color_map_ticks`) and one colorbar for the whole
figure. `output_directory`, if set, saves the figure as a PNG named after the workbook's
filename in that directory.

A runnable, pre-filled template workbook is provided at
`examples/response_surfaces_grid_template.xlsx` (built by
`examples/build_response_surfaces_grid_template.py`). Run it with:

``
uv run climate-canvas response-surfaces-grid examples\response_surfaces_grid_template.xlsx
``

![plot](./img/response_surfaces_grid_template.png)

See
[`docs/adr/0003-excel-driven-response-surfaces-grid-command.md`](./docs/adr/0003-excel-driven-response-surfaces-grid-command.md)
and `CONTEXT.md` for the full schema/rationale.

#### Python API

`plot_response_surface` (in `climate_canvas.plots_utilities`) can also be called directly as a
library function, e.g. from another package's CLI. It accepts several optional parameters not
exposed by the `response` CLI command:

- `save_path` (`Path | None`, default `None`): when provided, saves the figure to this path.
- `show` (`bool`, default `True`): when `False`, skips the interactive `plt.show()` window
  (useful for batch/headless plotting, e.g. saving one plot per component in a loop). The
  figure is always closed after the call to avoid leaking matplotlib figures.
- `threshold` (`float | None`, default `None`): z-value that becomes the colormap's center color.
  Defaults to the midpoint of the z-value range if `None` or outside that range.
- `color_map` (`str`, default `'RdBu'`): matplotlib colormap name.
- `color_map_ticks` (`list[float] | None`, default `None`): explicit colorbar tick values. If
  `None` and `threshold` is given, defaults to the contour levels (5 below/above threshold plus
  threshold itself) instead of matplotlib's automatic tick locator.

The third element of the `labels` tuple (z label) is rendered as the colorbar's label, in
addition to `labels[0]`/`labels[1]` being used as the x/y axis labels.

```python
from climate_canvas.plots_utilities import plot_response_surface

plot_response_surface(xs, ys, zs, labels=('Precip Delta (%)', 'Temp Delta (C)', 'portion'),
                      save_path='surface.png', show=False, threshold=0.2)
```

`plot_response_surfaces_grid` (also in `climate_canvas.plots_utilities`) reads a workbook path
and renders the multi-subplot figure described above:

```python
from climate_canvas.plots_utilities import plot_response_surfaces_grid

plot_response_surfaces_grid('examples/response_surfaces_grid_template.xlsx', show=False)
```

