Metadata-Version: 2.5
Name: prettyvoronoi
Version: 0.1.0
Summary: Interactive weighted Voronoi treemaps for Python notebooks
Author: prettyvoronoi contributors
License-Expression: MIT
License-File: LICENSE
License-File: THIRD_PARTY_LICENSES.md
Requires-Python: >=3.10
Requires-Dist: anywidget>=0.9
Requires-Dist: traitlets>=5.14
Provides-Extra: data
Requires-Dist: pandas>=2.0; extra == 'data'
Provides-Extra: demo
Requires-Dist: altair>=5.4; extra == 'demo'
Requires-Dist: country-converter>=1.3; extra == 'demo'
Requires-Dist: marimo>=0.14; extra == 'demo'
Requires-Dist: pandas>=2.0; extra == 'demo'
Provides-Extra: jupyter
Requires-Dist: altair>=5.4; extra == 'jupyter'
Requires-Dist: ipykernel>=6.29; extra == 'jupyter'
Requires-Dist: ipywidgets>=8.1; extra == 'jupyter'
Requires-Dist: jupyterlab>=4.2; extra == 'jupyter'
Requires-Dist: pandas>=2.0; extra == 'jupyter'
Provides-Extra: polars
Requires-Dist: polars>=1.0; extra == 'polars'
Provides-Extra: test
Requires-Dist: altair>=5.4; extra == 'test'
Requires-Dist: pandas>=2.0; extra == 'test'
Requires-Dist: polars>=1.0; extra == 'test'
Requires-Dist: pytest>=8.0; extra == 'test'
Description-Content-Type: text/markdown

# prettyvoronoi

**World explorer**
[![Open in molab](https://marimo.io/molab-shield.svg)](https://molab.marimo.io/github/fabioscantamburlo/prettyvoronoi/blob/main/examples/marimo_world_demo.py/server)

**Stock maps & image themes**
[![Open in molab](https://marimo.io/molab-shield.svg)](https://molab.marimo.io/github/fabioscantamburlo/prettyvoronoi/blob/main/examples/marimo_gallery.py/server)

<p align="center">
  <img
    src="docs/assets/interaction-demo.gif"
    alt="Animated prettyvoronoi demo showing zooming, panning, hierarchy selection, and country flag backgrounds"
    width="920"
  >
</p>

<p align="center">
  <em>Zoom, pan, select hierarchy levels, and move between overview and detail.</em>
</p>

- Interactive weighted Voronoi treemaps for Marimo and JupyterLab.
- Turn dataframe rows into cells whose areas represent a numeric variable.
- Use color to show a second measure without changing cell area.
- Arrange cells into multilevel categorical groups.
- Send country, group, and multilevel selections back to Python.
- Turn those selections into reactive companion charts, tables, or slide-ready stories.
- Zoom, pan, focus groups, select several regions, and enter full screen.
- Drop your own images onto cells or use bundled SVG country flags.
- Reuse your own SVGs or images as categorical themes without copying them into every row.
- Switch to an all-rectangular stock-map view for portfolios and company data.
- Automatically combine visually unresolvable cells into drillable **Other**
  regions without losing their underlying dataframe rows.

## Install

```bash
pip install prettyvoronoi
```

That is the minimal installation: the widget accepts lists of records and
column mappings without installing a dataframe library. Add only the dataframe
integration you use:

```bash
pip install "prettyvoronoi[data]"    # pandas and as_frame=True dataset loaders
pip install "prettyvoronoi[polars]"  # Polars dataframe input
```

Marimo, JupyterLab, Altair, test tools, and both dataframe libraries are kept
out of the default installation.

## Run an example

### Marimo explorer

```bash
uv sync --extra demo
uv run marimo edit examples/marimo_world_demo.py
```

The Marimo app exposes the full experience as live controls. Try these
combinations:

- Land area grouped by continent, with CO₂ emissions as a heat gradient.
- Population grouped by continent and language family.
- GDP grouped by currency, with country flags as cell backgrounds.
- Full-screen mode with additive selection to build a filtered dataframe.
- A selection-driven country ranking that updates below the Voronoi map.

The companion gallery demonstrates company and energy use cases:

```bash
uv run marimo edit examples/marimo_gallery.py
```

Choose the company view for a familiar rectangular stock map, or the energy
view to reuse local SVG icons for fossil, solar-and-wind, renewable, and nuclear
cells. Selecting a company group creates a market-value ranking; selecting an
energy region creates a stacked generation-mix chart.

### JupyterLab notebook

```bash
uv sync --extra jupyter
uv run jupyter lab examples/jupyterlab_world_demo.ipynb
```

The ready-to-run [JupyterLab notebook](examples/jupyterlab_world_demo.ipynb)
uses the same widget and bundled data. Its `USE_FLAGS` switch demonstrates the
two intended visual modes: a numeric gradient or crisp SVG flag backgrounds.
Zoom, pan, drill-down, full screen, image dropping, and hierarchy selection
work in both notebook environments.

[The JupyterLab gallery](examples/jupyterlab_gallery.ipynb) contains the same
company stock-map and category-image examples.

```bash
uv run jupyter lab examples/jupyterlab_gallery.ipynb
```

Both notebooks attach a live observer to `selected_rows`. The companion chart
refreshes immediately after a widget selection, without rerunning a cell.
Their shared presentation helpers live in
[`examples/narrative_charts.py`](examples/narrative_charts.py), so the ranking
and stacked-chart patterns can be copied into another notebook or slide app.

Small countries that cannot be drawn meaningfully at the current chart size are
combined into dashed **Other (n)** regions. Double-click **Other** to explore
those countries in a dedicated view. Selecting the combined region returns all
of its underlying dataframe rows.

## Load the example data

The example [world dataset](src/prettyvoronoi/datasets/data/world.csv) is
packaged with the library and has an sklearn-style loader:

```python
from prettyvoronoi import load_companies, load_energy_mix, load_world

dataset = load_world(as_frame=True)
countries = dataset.data

dataset.feature_names
dataset.DESCR

companies = load_companies(as_frame=True).data
energy_by_source = load_energy_mix(as_frame=True).data
```

The dependency-free default returns a list of row dictionaries. Pass an
explicit target when you want a feature/target split:

```python
X, population = load_world(
    as_frame=True,
    target="Population",
    return_X_y=True,
)
```

### Dataset credit and AI-data disclaimer

The underlying country indicators are credited to
[World Bank Open Data](https://data.worldbank.org/). The CSV bundled with this
project is an **AI-modified derivative**, not an unchanged World Bank download:
missing language and currency values were inferred with AI assistance, labels
were standardized, and visualization-oriented grouping fields were added.
These modifications have not been verified or endorsed by the World Bank.
Treat the dataset as demonstration data rather than an authoritative source.

The bundled companies and energy datasets are **AI-generated demonstration
data** and have not been independently verified. Do not use them as current or
authoritative financial, investment, engineering, or policy data.

## Create a chart

```python
from prettyvoronoi import VoronoiTreemap, load_world

countries = load_world(as_frame=True).data

chart = VoronoiTreemap(
    countries,
    values="Population",
    groups=["Official language", "Currency-Code"],
    label="Country",
    id_column="Abbreviation",
    color_by="Birth Rate",
    color_range=["#edf8e9", "#15803d"],
    shape="circle",
    width=1000,
    aspect_ratio=16 / 9,
    fit_viewport=True,
    tiny_cells="auto",
    selection_mode="multiple",
    sync_selection_records=True,
    tooltip=["Country", "Population", "Birth Rate"],
)

chart
```

`values` determines cell area. `groups` can contain any number of categorical
columns and defines the visible hierarchy. `color_by` adds an independent
numeric gradient without changing area.

<p align="center">
  <img
    src="docs/assets/voronoi-gradient.png"
    alt="Circular weighted Voronoi treemap where land area controls cell size and birth rate controls a green gradient"
    width="720"
  >
</p>

<p align="center">
  <em>Land area controls cell size while birth rate independently controls color intensity.</em>
</p>

Supported Voronoi boundaries are `circle`, `ellipse`, `rectangle`, `square`,
`hexagon`, `diamond`, and `triangle`. Use `shape="stock-shape"` when every
group and leaf should be a rectangle:

```python
from prettyvoronoi import VoronoiTreemap, load_companies

companies = load_companies(as_frame=True).data

stock_map = VoronoiTreemap(
    companies,
    values="Market Cap ($B)",
    groups=["Sector", "Industry"],
    label="Company",
    id_column="Ticker",
    color_by="YoY Revenue Growth (%)",
    shape="stock-shape",
    fit_viewport=True,
)
```

This view behaves like the familiar stock-market map: rectangle area represents
the selected value, while nested rectangles preserve sector and industry.
Selection, zoom, full screen, tooltips, and downstream dataframe filtering work
exactly as in the Voronoi views. Because rectangular subdivision is exact, this
is also the safest layout when sibling values differ by several orders of
magnitude.

## Build a downstream workflow from selections

### Marimo

```python
import marimo as mo

voronoi = mo.ui.anywidget(chart)
voronoi
```

Selections are reactive, so another Marimo cell can immediately use them:

```python
selected_countries = countries.iloc[voronoi.selected_rows]
selected_countries
```

The examples go one step further and turn the selected dataframe into an
Altair chart. This creates a useful presentation flow:

1. Use the Voronoi or stock map as the visual overview.
2. Select one leaf, several leaves, or a complete hierarchy group.
3. Let a familiar bar or stacked chart explain the selected subset precisely.
4. Reuse that same subset for a table, model, export, or following slide.

The widget exposes several useful views of the same selection:

```python
voronoi.selected_node  # last group or leaf you interacted with
voronoi.selected_nodes  # all selected hierarchy nodes
voronoi.selected_rows  # deduplicated dataframe row positions
voronoi.selected_records  # row dictionaries when synchronization is enabled
```

Single-click replaces the current selection. Ctrl/Cmd/Shift-click toggles a
node, while **Select many** makes additive selection comfortable without a
keyboard. Countries and groups at every visible hierarchy level are selectable.

### JupyterLab

The same synchronized traits are available on the widget instance. A normal
trait observer can update another output immediately:

```python
def update_story(change):
    selected = countries.iloc[change["new"]]
    # Render a chart, table, or narrative from selected.

chart.observe(update_story, names="selected_rows")
```

The bundled Jupyter notebooks include a complete live companion-chart output.
Selecting an **Other** region returns all of its source rows; double-click it
first when you want to select the tiny countries individually.

## Explore the chart

- Scroll or pinch to zoom around the pointer.
- Drag anywhere to pan.
- Double-click a cell or group to focus it.
- Double-click at maximum zoom to return to the full view.
- Use **Show all**, `0`, or `Escape` to reset.
- Use **Full screen** for a presentation-sized chart.

The outer shape, major groups, inner groups, and leaf cells use progressively
lighter boundaries. Each group boundary combines light and dark contrast, so
the hierarchy remains readable over both flags and gradients.

Set `fit_viewport=True` to use most of the notebook height even before entering
full screen. `zoom_sensitivity` controls the wheel and trackpad speed, and
`max_zoom` limits how far users can zoom.

## Keep tiny cells honest

```python
chart = VoronoiTreemap(
    countries,
    values="Land Area(Km2)",
    groups=["Official language"],
    label="Country",
    tiny_cells="auto",
)
```

Automatic mode only groups sibling leaves that are too small to represent well.
It never changes the source dataframe or loses selection data. The default size
threshold adapts to the chart; override it with `tiny_cell_area=48` when you
want a consistent visual target. `max_other_share=0.15` limits how much of a
parent can be folded into one **Other** region. Use `tiny_cells="show"` when
seeing every cell in the overview is more important than resolution-aware
grouping.

Weighted Voronoi layout is iterative. For highly skewed values, start with
`tiny_cells="auto"`; if an area warning or layout failure remains, switch to
`shape="stock-shape"` for exact rectangular areas. The chart reports an area
warning if a visible polygon differs too much from the value it should
represent, so extreme datasets do not fail silently.

## Add color or images

Use a gradient to compare a second measure:

```python
chart = VoronoiTreemap(
    countries,
    values="Land Area(Km2)",
    color_by="Birth Rate",
    color_range=["#fff7bc", "#d7301f"],
    show_color_legend=True,
)
```

Use a dataframe column containing image URLs or data URIs for cell backgrounds:

```python
from prettyvoronoi import flag_theme

countries["Flag"] = countries["Abbreviation"].fillna("").map(flag_theme)

chart = VoronoiTreemap(
    countries,
    values="Population",
    label="Country",
    image="Flag",
    image_mode="cover",  # also "contain" or "stretch"
    allow_image_drop=True,
)
```

The world demo uses bundled SVG flags, which remain sharp while zooming. With
image dropping enabled, drag an SVG, PNG, JPEG, WebP, or GIF directly onto a
leaf to replace its background. Dropped images are returned through
`custom_images` and can be removed from the widget header.

Color gradients and image backgrounds compete for attention, so choosing an
image column pauses the numeric gradient by default and uses a neutral backing
behind the images. Enable `color_with_images=True` only when combining both is
intentional. A missing numeric color value also gets the neutral backing rather
than an unrelated categorical color.

For repeated categories, map the category column directly to local files, URLs,
or data URIs. Each image is embedded and sent to the browser once:

```python
from pathlib import Path

from prettyvoronoi import VoronoiTreemap, load_energy_mix

energy = load_energy_mix(as_frame=True).data
icons = Path("my-energy-icons")

chart = VoronoiTreemap(
    energy,
    values="Generation (TWh)",
    groups=["Continent", "Country"],
    label="Energy Source",
    id_column="Cell ID",
    image="Energy Source",
    image_map={
        "Fossil fuels": icons / "fossil.svg",
        "Solar & wind": icons / "solar-wind.svg",
        "Hydro & other renewables": icons / "renewables.svg",
        "Nuclear": icons / "nuclear.svg",
    },
    image_mode="contain",
    image_padding=0.12,
)
```

SVG is ideal for deep zoom. Unmapped categories keep a neutral background, and
a dropped image temporarily overrides the mapped theme for that individual
cell. `image_uri(...)` is also available when you need to prepare a standalone
local image for another dataframe workflow.

## Project notes

The [blueprint](docs/blueprint.md) describes the product direction, and the
[implementation plan](docs/IMPLEMENTATION_PLAN.md) tracks completed and future
work.

Non-rectangular layouts are powered by Franck Lebeau's permissively licensed
[`d3-voronoi-treemap`](https://github.com/Kcnarf/d3-voronoi-treemap),
[`d3-voronoi-map`](https://github.com/Kcnarf/d3-voronoi-map), and
[`d3-weighted-voronoi`](https://github.com/Kcnarf/d3-weighted-voronoi), built on
[D3](https://d3js.org/). Full bundled-component notices are in
[`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md).

## Test the project

Install the development tools once and enable the Git hook:

```bash
uv sync --all-extras
uv run pre-commit install
```

Every commit now fixes and checks Python with Ruff, strips notebook outputs and
execution counts, and catches malformed configuration, merge markers, private
keys, and whitespace problems. Run the complete hook set manually with:

```bash
uv run pre-commit run --all-files
```

Run the project test suites separately:

```bash
npm --prefix frontend install
npm test
npm run build

uv run pytest
uv run marimo check examples/marimo_world_demo.py examples/marimo_gallery.py

# Starts a local JupyterLab and browser and tests the world notebook
npm run test:jupyterlab
```

The live JupyterLab smoke test expects `google-chrome` to be available on the
development machine.
