Metadata-Version: 2.4
Name: weighted-voronoi
Version: 0.1.0
Summary: Weighted Voronoi diagrams, maps and treemaps in pure Python -- no JavaScript engine required
Author-email: Elad Noor <elad.noor@weizmann.ac.il>
License-Expression: MIT
Project-URL: Homepage, https://gitlab.com/milo-lab-public/weighted-voronoi
Project-URL: Repository, https://gitlab.com/milo-lab-public/weighted-voronoi
Project-URL: Issues, https://gitlab.com/milo-lab-public/weighted-voronoi/-/issues
Project-URL: Origin project, https://gitlab.com/milo-lab-public/voronoi-treemap-webapp
Keywords: voronoi,treemap,power-diagram,laguerre,visualization,proteomap
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Topic :: Multimedia :: Graphics
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: weighted_voronoi/THIRD-PARTY-LICENSES.md
Requires-Dist: numpy>=1.20
Requires-Dist: scipy>=1.7
Requires-Dist: pandas>=1.0
Requires-Dist: matplotlib>=3.5
Requires-Dist: tqdm>=4.60
Provides-Extra: excel
Requires-Dist: openpyxl>=3.1; extra == "excel"
Provides-Extra: notebook
Requires-Dist: ipykernel>=6.29.5; extra == "notebook"
Requires-Dist: nbclient>=0.10; extra == "notebook"
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Requires-Dist: openpyxl>=3.1; extra == "test"
Dynamic: license-file

weighted-voronoi
================

Voronoi treemaps from a spreadsheet, in Python and nothing else.

A treemap that tiles a square with cells whose *areas* are proportional to your
weights, nested down a hierarchy — and where the cells are polygons rather than
rectangles. Pure Python on top of numpy, scipy, pandas and matplotlib: no
external tools to install, nothing to configure.

    pip install weighted-voronoi

Excel input needs one extra (quote it — zsh treats the brackets as a glob):

    pip install 'weighted-voronoi[excel]'

Quick start
-----------

```python
from weighted_voronoi import treemap

fig = treemap("examples/ecoli_proteome_test.csv", label_level="Level2")
fig.savefig("treemap.png", dpi=200)
```

`treemap()` takes a path (CSV, TSV or Excel), a file object, CSV/TSV text, or
a DataFrame, and returns a matplotlib `Figure` — so it saves to PNG/SVG/PDF,
composes into a larger figure, and displays on its own in a notebook.

A worked example lives in [examples/quickstart.ipynb](examples/quickstart.ipynb),
executed so its figures render without running anything. It covers label
levels, styling, DataFrame input, reading the layout back as data, seeds and
progress reporting.

Input format
------------

One row per leaf, with the hierarchy spelled out in columns:

| Column | Meaning |
| --- | --- |
| `Level1`..`LevelN` | the groups the leaf belongs to, outermost first (up to 10 levels) |
| `Name` | the leaf's own name |
| `Weight` | a positive number; cell areas follow it |
| `Color` | optional — `#1f77b4`, `rgb()`/`rgba()`, `hsl()`/`hsla()`, or a CSS color name |

Rows sharing a level path are grouped, and a group's weight is the sum of its
children. Rows with a non-positive weight are dropped, and a blank level cell
becomes a group named `(none)`.

Colors are per row. Rows that leave the column blank get one derived from their
position in the hierarchy — distinct hues per `Level1` group, varied within it —
and a group's own swatch is the weight-averaged color of the tiles beneath it.

Command line
------------

```
weighted-voronoi examples/ecoli_proteome_test.csv -o map.png --level Level2 --label-color contrast
weighted-voronoi examples/microbe_test.xlsx       -o map.svg --borders none --font "Liberation Serif"
weighted-voronoi workbook.xlsx --sheet costs -o map.pdf --random-seed
```

`weighted_voronoi` and `python -m weighted_voronoi` do the same thing. Under
uv, without installing anything:

```
uvx --from weighted-voronoi weighted-voronoi data.csv -o map.png
```

Output format follows the `-o` extension: `.png`, `.svg`, `.pdf` or `.eps`.

| Flag | Meaning |
| --- | --- |
| `--level Level2` / `--level Name` / `--level 2` | which depth to label |
| `--label-color white\|black\|contrast` | `contrast` picks per label, from the tile's luminance |
| `--borders white\|black\|none` | `none` lets tiles touch |
| `--font "DejaVu Serif"` | three sans and three serif families, all freely licensed |
| `--seed N`, `--random-seed` | reshuffle the layout |
| `--no-legend`, `--no-frame` | drop the legend column, drop the frame |
| `--sheet` | which sheet of an Excel workbook |
| `--size-inches`, `--dpi` | output size |
| `--iterations`, `--convergence` | trade layout quality against time |

`--help` lists them all.

A large file takes a few seconds to lay out, so there is a progress bar with an
estimate of the time left. It appears only when stderr is a terminal, and
`--no-progress` suppresses it. If a map is wanted sooner than exact,
`--iterations 20` (from the default 50) roughly halves the time, at the price of
the worst-fitted tiles drifting from 0.05% to 0.3% of the map.

Rendering options
-----------------

`render()`, and the corresponding `treemap()` keywords:

| Argument | Values |
| --- | --- |
| `label_level` | `None`/`"Name"` for the leaves, a level column name, or a 1-based depth |
| `label_color` | `"white"`, `"black"`, `"contrast"` (per label, from the tile's luminance) |
| `borders` | `"white"`, `"black"`, `None` (tiles touch) |
| `font` | a key of `FONT_STACKS` — three sans, three serif, all freely licensed |
| `legend`, `map_inches`, `legend_inches`, `dpi`, `margin`, `frame` | figure geometry |

Labels are sized to the room their own cell actually has at the anchor, and
dropped when they would come out unreadably small. The default font is DejaVu
Sans because matplotlib ships it: the figure looks the same on a machine with
no fonts installed at all.

There is no interactivity — no hover tooltips, no rebuild button. Pass a
different `seed` to reshuffle, and re-render to change options. Tile shares are
available programmatically: every record from `compute_layout()` carries
`value` and its `path`.

Using the layout on its own
---------------------------

`weighted_voronoi.layout` is useful without any of the spreadsheet machinery
above. It needs only numpy and scipy — matplotlib is imported lazily, when
something asks to draw.

```python
import numpy as np
from weighted_voronoi.layout import power_diagram, voronoi_map, voronoi_treemap

square = [[0, 0], [0, 900], [900, 900], [900, 0]]

# A power (Laguerre) diagram: one cell per site, heavier sites take more room
# from their neighbours.
cells = power_diagram(np.array([[300., 450.], [600., 450.]]),
                      np.array([5000., 1000.]), square)

# Cells whose *areas* follow the weights, found by relaxation.
result = voronoi_map([1.0, 2.0, 4.0, 8.0], square, seed=0)
result["cells"], result["converged"], result["area_error_ratio"]

# The same, recursively, down a hierarchy of dicts.
records = voronoi_treemap({"name": "root", "children": [...]}, square, seed=0)

# Anything slow enough to be worth watching takes a progress callback.
records = voronoi_treemap(tree, square, seed=0,
                          progress=lambda done, total: print(done, "of", total))
```

Nothing in there knows about spreadsheets, colors or pandas. Cell areas land
within 0.2% of the weights they were asked for, and every loop is bounded — no
input hangs the layout, and no seed needs retrying.

Speed is set by the number of nodes: 14 nodes lay out in 0.07 s, 1080 in
about 5 s.

In the browser
--------------

**[milo-lab-public.gitlab.io/weighted-voronoi][pages]** is this package running
under [Pyodide](https://pyodide.org/): pick a spreadsheet and the layout is
computed by `weighted_voronoi.layout` in WebAssembly. The file never leaves your
machine — it is read in the browser. Exports are SVG, PNG and PDF.

[pages]: https://milo-lab-public.gitlab.io/weighted-voronoi/

It is a demonstration, not a replacement: the page downloads ~86 MB of wheels,
and WebAssembly runs the layout about 2.6× slower again — a 1000-leaf file
takes roughly 12 s. Small maps are comfortable.

The page is [`index.html`](index.html), which Pages publishes from the default
branch. It is generated from the package, not written by hand:

```
python tools/build_web.py
```

The markup lives beside the builder in `tools/template.html`. The builder pastes
the package's own modules in verbatim, and `tests/test_web_page.py` regenerates
and compares, so the committed page cannot fall behind the source.

Tests
-----

```
pip install 'weighted-voronoi[test]'
python -m pytest tests -q
```

Without that extra the suite still passes; only the Excel tests skip.

`tests/golden/reference.json` holds expected layout outputs, captured once and
checked in; `test_reference_parity.py` checks against them.
`tests/golden/regenerate.py` says what regenerating it takes, and refuses to run
rather than guessing. Never edit it by hand.

Releasing
---------

Tagging is the whole procedure. CI runs the suite on every push, and on a `v*`
tag it builds a wheel and an sdist, checks them, and uploads to PyPI via
[Trusted Publishing][tp] — no token stored here.

```
# bump `version` in pyproject.toml first -- CI refuses a tag that disagrees
git tag v0.1.1 && git push origin v0.1.1
```

[tp]: https://docs.pypi.org/trusted-publishers/

Licensing
---------

This package's own code is MIT, in [LICENSE](LICENSE). `layout/` is a derived
work: `power.py` and `clip.py` port **d3-weighted-voronoi**, `relax.py` ports
**d3-voronoi-map**, `treemap.py` ports **d3-voronoi-treemap** — all BSD
3-Clause, Copyright (c) 2018, LEBEAU Franck — and `clip.py` is additionally
derived from **D3** (BSD 3-Clause, Copyright 2010-2017 Mike Bostock).

No JavaScript is redistributed here, but a port is a derived work, so the
notices travel with it: see
[weighted_voronoi/THIRD-PARTY-LICENSES.md](weighted_voronoi/THIRD-PARTY-LICENSES.md),
which ships inside the installed package and in the wheel.

`parsing.py` and `drawing.py` are copies of files in the origin project's
[`python/proteomap/`][origin-python]. Nothing enforces that they stay copies —
if you change the parsing rules in one place, change them in all three.

[origin-python]: https://gitlab.com/milo-lab-public/voronoi-treemap-webapp/-/tree/main/python
