Metadata-Version: 2.4
Name: orama
Version: 1.1.0
Summary: Declarative, metadata-driven charting library for Python
Project-URL: Repository, https://gitlab.com/Kencho1/orama
Project-URL: Issues, https://gitlab.com/Kencho1/orama/-/issues
Author: Jesús Alonso Abad
License: MIT License
        
        Copyright (c) 2026 Jesús Alonso Abad
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: charts,dataframes,declarative,plotly,polars,visualization
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: armonia~=1.1
Requires-Dist: babel~=2.17
Requires-Dist: fields-metadata>=0.1
Requires-Dist: pandas~=2.3
Requires-Dist: plotly[express]~=6.3
Requires-Dist: polars~=1.33
Requires-Dist: polychromos~=1.4
Requires-Dist: pyarrow~=22.0
Requires-Dist: pycountry>=24.6
Requires-Dist: therismos>=0.1
Provides-Extra: demo
Requires-Dist: flask-wtf~=1.2; extra == 'demo'
Requires-Dist: flask~=3.1; extra == 'demo'
Requires-Dist: wtforms~=3.2; extra == 'demo'
Provides-Extra: docs
Requires-Dist: sphinx~=8.0; extra == 'docs'
Provides-Extra: test
Requires-Dist: mypy~=1.8; extra == 'test'
Requires-Dist: pytest-cov~=6.0; extra == 'test'
Requires-Dist: pytest~=8.3; extra == 'test'
Requires-Dist: ruff; extra == 'test'
Requires-Dist: tox-uv~=1.0; extra == 'test'
Requires-Dist: tox~=4.0; extra == 'test'
Provides-Extra: web
Requires-Dist: wtforms~=3.2; extra == 'web'
Description-Content-Type: text/markdown

# orama

> _**όραμα**_
>
> Greek; noun
>
> Vision, goal.

Declarative, metadata-driven charting library for Python

`orama` produces Plotly figures from structured Polars DataFrames using a pluggable strategy
pattern, `fields-metadata`-driven field discovery, `therismos`-based query descriptors, and
composable color strategies (using `polychromos` and `armonia`). It is designed to be embedded 
in any Python application regardless of domain model or data-access layer.

---

## Overview

`orama` sits between your data layer and your presentation layer. You describe *which* fields
to group and aggregate; `orama` builds the corresponding `QueryParams` for you to execute,
then turns the resulting Polars DataFrames into fully themed, i18n-aware Plotly figures.

### What orama does

- Provides a catalogue of chart strategies: bar, grouped bar, line, categorical line, pie,
  sunburst, heatmap, boxplot, grouped boxplot, and choropleth map.
- Derives query parameters (group-by fields, aggregations, expected schema) directly from the
  strategy configuration.
- Renders themed, i18n-aware `go.Figure` objects from the query results you supply.

### What orama does NOT do

- Execute queries or fetch data — callers are responsible for running the queries and returning
  the results as Polars DataFrames.
- Provide a web layer — dynamic WTForms form generation and HTTP serialization are available
  via `orama[web]` (`orama.web`).

---

## Installation

```bash
pip install orama
```

**Requirements:** Python ≥ 3.11

**Optional extras:**

| Extra | Installs |
|-------|----------|
| `orama[web]` | WTForms (web binding for `orama.web`) |
| `orama[demo]` | `orama[web]` + Flask, Flask-WTF (for the demo application) |

---

## Core Concepts

### Three-step workflow

Every chart follows the same workflow regardless of chart type:

```
1. Extract field metadata
       fields-metadata MetadataExtractor
       ──────────────────────────────────►  dict[str, FieldMetadata]
                                                        │
2. Instantiate a FigureStrategy                         │
       FigureStrategy(fields_metadata, ...)  ◄──────────┘
                        │
3. Get query params, execute, build
       strategy.get_query_params()     ──►  dict[str, QueryParams]  (you execute these)
       strategy.build(dfs, tr, theme)  ──►  FigureView  ← honours pre/post callbacks
       strategy.plot(dfs, tr, theme)   ──►  FigureView  ← raw render, no callbacks
```

**Step 1 — Extract field metadata.**
Use `fields-metadata`'s `MetadataExtractor` on your domain model to produce a
`dict[str, FieldMetadata]` (a `FieldsMetadataMap`). This map drives field validation inside
each strategy: categorical fields go to categorical variables, numeric fields go to numeric
variables, and so on.

**Step 2 — Instantiate a strategy.**
Choose the appropriate `FigureStrategy` subclass and pass it the field metadata, your chosen
variables, options, and optional color rules. The constructor validates all inputs immediately.

**Step 3 — Query, then plot.**
Call `get_query_params()` to obtain a `dict[str, QueryParams]`. Execute each query using your
data layer (MongoDB, Polars, SQL — orama is agnostic) and collect the results as Polars
DataFrames in a dict keyed by the same names. Pass that dict to `plot(dfs, tr, theme)` to
obtain a `go.Figure`.

---

## Quick Start

The following example renders a simple bar chart from a toy Polars DataFrame.

```python
from dataclasses import dataclass

import polars as pl
from babel.support import NullTranslations
from fields_metadata import MetadataExtractor
from polychromos.color import HSLColor

from orama.strategies.bar import BarChartStrategy
from orama.theme import Theme
from orama.variables import Aggregation

# ── Step 1: extract field metadata ────────────────────────────────────────────

@dataclass
class SalesRecord:
    region: str    # categorical — accepted by CategoricalVariable
    revenue: float # numeric    — accepted by NumericalAggregationVariable

fields_metadata = MetadataExtractor(SalesRecord).extract()

# ── Step 2: instantiate the strategy ──────────────────────────────────────────

strategy = BarChartStrategy(
    fields_metadata=fields_metadata,
    category_variables=["region"],
    aggregation=Aggregation(
        name="total_revenue",   # column name in the result DataFrame
        function="sum",         # therismos AggregationFunction value
        field="revenue",        # source field to aggregate
    ),
)

# ── Step 3: get query params, execute, and plot ───────────────────────────────

query_params = strategy.get_query_params()
# query_params["main"].group_fields   == ["region"]
# query_params["main"].aggregations   == [Aggregation(id="total_revenue", ...)]
# query_params["main"].dataframe_schema expresses the expected Polars schema

# Execute query with your data layer; here we build a toy DataFrame directly:
df = pl.DataFrame({
    "region": ["North", "South", "East", "West"],
    "total_revenue": [120.5, 98.3, 145.2, 87.6],
})

theme = Theme(colors={
    "background": HSLColor.from_hex("#ffffff"),
    "primary_color_normal": HSLColor.from_hex("#3b82f6"),
    "secondary_color_normal": HSLColor.from_hex("#f59e0b"),
})

fig = strategy.plot(
    dfs={"main": df},
    tr=NullTranslations(),
    theme=theme,
)
fig.show()
```

The `Aggregation.function` string must be a valid value of `therismos.grouping.AggregationFunction`
(e.g. `"count"`, `"sum"`, `"average"`, `"min"`, `"max"`). Refer to the therismos documentation
for the complete list.

---

## Labels

Every strategy accepts three universal text parameters and a set of variable-specific label
parameters. All are optional strings that default to `None`; when `None`, each strategy
computes a meaningful default from its configured variables.

| Parameter | Scope | Description |
|-----------|-------|-------------|
| `title` | universal | Figure title. Defaults to a generated description of the chart. |
| `subtitle` | universal | Figure subtitle rendered below the title (Plotly 5.15+). |
| `caption` | universal | Caption intended for `<figcaption>` in an HTML `<figure>` wrapper. Not applied to the Plotly figure — returned in `FigureView.caption`. |
| `description` | universal | Free multiline text explaining how to interpret the figure. Not applied to the Plotly figure — returned in `FigureView.description`. |
| `category_label` | bar, grouped bar, pie, boxplot | Label for the category axis / legend. |
| `value_label` | bar, grouped bar, line, categorical line, pie, sunburst, heatmap, boxplot, grouped boxplot, choropleth map | Label for the value axis / colorbar. |
| `group_label` | grouped bar, grouped boxplot | Legend title for the grouping variable. |
| `country_label` | choropleth map | Label for the country dimension. |
| `date_label` | line | X-axis label for the date dimension. |
| `category_label` | line | Legend title for the category dimension (only shown when `category_variable` is set). |
| `x_label` | categorical line, heatmap | X-axis label. |
| `series_label` | categorical line | Legend title for the series dimension (only shown when `series_variable` is set). |
| `y_label` | heatmap | Y-axis label. |
| `path_label` | sunburst | Reserved for future use. |

```python
from orama.strategies.bar import BarChartStrategy
from orama.variables import Aggregation

strategy = BarChartStrategy(
    fields_metadata=fields_metadata,
    category_variables=["region"],
    aggregation=Aggregation(name="total_revenue", function="sum", field="revenue"),
    # Universal labels
    title="Revenue by Region",
    subtitle="All regions · Q1 2025",
    caption="Source: Internal sales data. Figures in EUR.",
    description="Revenue is computed as the sum of invoiced amounts net of VAT.\nOnly regions with at least one transaction are shown.",
    # Variable-specific labels
    category_label="Region",
    value_label="Total Revenue (€)",
)

view = strategy.plot(dfs={"main": df}, tr=NullTranslations(), theme=theme)

# Render with caption and/or description
html = view.figure.to_html(full_html=False)
if view.caption:
    html = f"<figure>{html}<figcaption>{view.caption}</figcaption></figure>"
# view.description is available for custom rendering (e.g., a tooltip or popover)
```

### Unit annotations

All strategies recognise `FieldMetadata.extra["unit"]` and automatically append the unit to
the auto-generated default value label. When no explicit `value_label` is provided, the
label becomes `"<default label> (<unit>)"` — for example `"Sum of revenue (EUR)"`.

The unit is **not** appended when the caller provides an explicit `value_label`; the
annotation only affects the fallback.

```python
from fields_metadata import FieldMetadata, MetadataExtractor

metadata = MetadataExtractor().extract(MyModel)

# Annotate numeric fields with their units after extraction
metadata["revenue"].extra["unit"] = "EUR"
metadata["tax_rate"].extra["unit"] = "%"

# Now any strategy using `revenue` as its aggregation field will automatically
# display e.g. "Sum of revenue (EUR)" on the value axis when no explicit
# value_label is passed.
strategy = BarChartStrategy(
    fields_metadata=metadata,
    category_variables=["region"],
    aggregation=Aggregation(name="total_revenue", function="sum", field="revenue"),
)
# value axis label → "Sum of revenue (EUR)"
```

---

## Callbacks

Two optional callbacks can be passed at construction time to hook into the rendering
pipeline. Use `build()` — the recommended entry point — to honour them. `plot()` remains
available as a raw render with no side-effects.

```python
FigureStrategy(
    ...,
    pre_build_callback=fn,   # fires before plot()
    post_build_callback=fn,  # fires after plot()
)
view = strategy.build(dfs, tr, theme)
```

### `pre_build_callback`

Fires **before** `plot()`. The DataFrames in the payload are the aggregated result of
`get_query_params()` — only the columns the plot will use — making them safe to pass
directly to an LLM. Mutations to `payload["dfs"]` change what gets plotted.

**Payload structure:**

```
payload
├── "dfs"      dict[str, pl.DataFrame]   Aggregated DataFrames (replace entries to change plotted data)
└── "metadata"
    ├── "strategy"     str            e.g. "BarChartStrategy"
    ├── "title"        str | None
    ├── "subtitle"     str | None
    ├── "caption"      str | None
    ├── "description"  str | None
    └── "color_rules"  list[dict]
        └── each: {"selector_type": str, "assigner_type": str, "colors": list[str]}
```

### `post_build_callback`

Fires **after** `plot()`. Can mutate `payload["figure"]` or overwrite
`payload["caption"]` / `payload["description"]` before `FigureView` is returned.

**Payload structure:**

```
payload
├── "figure"       go.Figure
├── "caption"      str | None       Assign to override FigureView.caption
├── "description"  str | None       Assign to override FigureView.description
├── "metadata"     (same structure as pre_build_callback)
└── "data"
    └── "<query_key>"
        ├── "columns"  list[str]
        └── "rows"     list[dict]
```

### Examples

**Example 1 — pre-build: LLM-generated data storytelling:**

```python
def storytelling_callback(payload):
    rows = payload["dfs"]["main"].to_dicts()
    meta = payload["metadata"]
    narrative = llm_client.generate(
        f"Write a one-sentence insight for a '{meta['title']}' chart: {rows}"
    )
    payload["metadata"]["description"] = narrative

strategy = BarChartStrategy(
    ...,
    title="Revenue by Region",
    pre_build_callback=storytelling_callback,
)
view = strategy.build(dfs={"main": df}, tr=tr, theme=theme)
```

Note: because `pre_build_callback` fires before the figure is built, text written into
`payload["metadata"]["description"]` is not automatically propagated to the `FigureView`.
Use `post_build_callback` for that — or combine both: write the narrative in the
pre-callback (where you have the raw data) and read it in the post-callback to set
`payload["description"]`.

**Example 2 — post-build: override caption and description:**

```python
def annotate(payload):
    payload["description"] = "Automatically generated insight."
    payload["caption"] = "Source: internal CRM data."

strategy = BarChartStrategy(..., post_build_callback=annotate)
view = strategy.build(dfs={"main": df}, tr=tr, theme=theme)
# view.caption == "Source: internal CRM data."
```

**Example 3 — combined pre + post for full storytelling pipeline:**

```python
_narrative = {}

def read_data(payload):
    rows = payload["dfs"]["main"].to_dicts()
    meta = payload["metadata"]
    _narrative["text"] = llm_client.generate(
        f"Summarise this '{meta['title']}' chart in one sentence: {rows}"
    )

def write_story(payload):
    payload["description"] = _narrative.get("text", "")

strategy = BarChartStrategy(
    ...,
    title="Revenue by Region",
    pre_build_callback=read_data,
    post_build_callback=write_story,
)
view = strategy.build(dfs={"main": df}, tr=tr, theme=theme)
```

---

## Chart Types Reference

| Strategy | Description |
|----------|-------------|
| `BarChartStrategy` | Single-series bar chart (vertical or horizontal) |
| `GroupedBarChartStrategy` | Multi-series grouped or stacked bar chart |
| `LineChartStrategy` | Time-series line chart grouped by calendar year/month |
| `CategoricalLineChartStrategy` | Line chart over an ordered categorical X axis |
| `PieChartStrategy` | Pie / doughnut chart |
| `SunburstChartStrategy` | Multi-level hierarchical sunburst chart |
| `HeatMapChartStrategy` | Two-dimensional categorical heat map |
| `BoxplotChartStrategy` | Boxplot distribution chart per category |
| `GroupedBoxplotChartStrategy` | Side-by-side boxplots grouped by a second categorical variable |
| `ChoroplethMapChartStrategy` | Country-level choropleth world map |

All strategies share the same base constructor signature and the same `plot(dfs, tr, theme)`
call, so they are fully interchangeable from the caller's perspective.

---

### BarChartStrategy

Represents categorical data with rectangular bars whose heights are proportional to
the aggregated values. Bars may be vertical or horizontal.

```python
from orama.strategies.bar import BarChartStrategy
from orama.variables import Aggregation
from orama.enums import SortOrder

strategy = BarChartStrategy(
    fields_metadata=fields_metadata,
    category_variables=["region"],          # list[str] — one or more categorical fields
    aggregation=Aggregation(
        name="count",
        function="count",
        field=None,                         # None for count-only aggregations
    ),
    sort_by_value=SortOrder.DESCENDING,     # SortOrder.NONE | ASCENDING | DESCENDING
    horizontal=False,                       # bool — render bars horizontally
    min_value=None,                         # float | None — axis lower bound
    max_value=None,                         # float | None — axis upper bound
    mean_category_name="",                  # str — add a mean bar with this label, or ""
    mean_annotation=False,                  # bool — draw a mean line annotation
    color_rules=None,                       # list[ColorRule] | None
    title=None,                             # str | None — figure title
    subtitle=None,                          # str | None — figure subtitle
    caption=None,                           # str | None — figure caption (for <figcaption>)
    category_label=None,                    # str | None — X/Y axis label for the category
    value_label=None,                       # str | None — X/Y axis label for the value
)
```

`get_query_params()` returns a single `"main"` key.  When `mean_category_name` or
`mean_annotation` is set, a `__total_count__` aggregation is added automatically.

**Supported color selectors:** `ColorSelectorStrategy`, `ColorSelectByCategoryName`,
`ColorSelectValuesBelow`, `ColorSelectValuesAbove`, `ColorSelectMaxValue`,
`ColorSelectMinValue`, `ColorSelectValuesBelowMean`, `ColorSelectValuesAboveMean`.

**Supported color assigners:** `ColorAssignerByCategory`, `ColorAssignerByValue`.

---

### GroupedBarChartStrategy

Extends bar charts with a grouping variable that splits each category into separate
sub-bars. Supports stacked, relative (100 %), and horizontal layouts.

```python
from orama.strategies.grouped_bar import GroupedBarChartStrategy

strategy = GroupedBarChartStrategy(
    fields_metadata=fields_metadata,
    category_variables=["region"],
    group_variable="product_line",          # str — single categorical field for groups
    aggregation=Aggregation(name="revenue", function="sum", field="revenue"),
    stacked=False,                          # bool — stack bars instead of grouping
    relative=False,                         # bool — relative % (requires stacked=True)
    horizontal=False,
    color_rules=None,
    title=None,                             # str | None
    subtitle=None,                          # str | None
    caption=None,                           # str | None
    category_label=None,                    # str | None — axis label for the category
    value_label=None,                       # str | None — axis label for the value
    group_label=None,                       # str | None — legend title for the grouping variable
)
```

**Supported color selectors:** `ColorSelectorStrategy`, `ColorSelectByCategoryName`.

**Supported color assigners:** `ColorAssignerByGroup`.

---

### LineChartStrategy

Plots one line per category value over a time axis grouped by calendar year and month.
The `date_variable` must be a datetime field whose `__year` and `__month` derived fields
are present in the `fields_metadata` map.

```python
from orama.strategies.line import LineChartStrategy

strategy = LineChartStrategy(
    fields_metadata=fields_metadata,
    date_variable="created_at",             # str — a date or datetime field (DateTimeVariable)
    aggregation=Aggregation(name="count", function="count"),
    category_variable="status",             # str | None — splits data into lines
    min_value=None,
    max_value=None,
    color_rules=None,
    title=None,                             # str | None
    subtitle=None,                          # str | None
    caption=None,                           # str | None
    date_label=None,                        # str | None — X-axis label for the date dimension
    value_label=None,                       # str | None — Y-axis label for the value
    category_label=None,                    # str | None — legend title (when category_variable is set)
)
```

`get_query_params()` groups by `created_at__year`, `created_at__month`, and `status`.

**Supported color selectors:** `ColorSelectorStrategy`, `ColorSelectByCategoryName`.

**Supported color assigners:** `ColorAssignerByCategory`.

---

### CategoricalLineChartStrategy

Plots lines over an ordered categorical X axis (e.g. fiscal quarters, ordinal labels).
Unlike `LineChartStrategy`, the X axis is not calendar-based.

```python
from orama.strategies.categorical_line import CategoricalLineChartStrategy

strategy = CategoricalLineChartStrategy(
    fields_metadata=fields_metadata,
    x_variables=["year", "quarter"],        # list[str] — ordered categorical X axis
    aggregation=Aggregation(name="revenue", function="sum", field="revenue"),
    series_variable="region",              # str | None — optional series split
    color_rules=None,
    title=None,                             # str | None
    subtitle=None,                          # str | None
    caption=None,                           # str | None
    x_label=None,                           # str | None — X-axis label
    value_label=None,                       # str | None — Y-axis label for the value
    series_label=None,                      # str | None — legend title (when series_variable is set)
)
```

**Supported color selectors:** `ColorSelectorStrategy`, `ColorSelectByCategoryName`.

**Supported color assigners:** `ColorAssignerByCategory`.

---

### PieChartStrategy

Circular chart where arc length is proportional to the aggregated value. Setting
`hole > 0` produces a doughnut chart.

```python
from orama.strategies.pie import PieChartStrategy

strategy = PieChartStrategy(
    fields_metadata=fields_metadata,
    category_variables=["region"],
    aggregation=Aggregation(name="count", function="count"),
    sort_by_value=SortOrder.NONE,
    hole=0.4,           # float in [0, 0.9] — doughnut hole fraction; 0 = full pie
    inner_overall=0.0,  # float — font size for the overall value in the hole; 0 = hidden
    color_rules=None,
    title=None,         # str | None
    subtitle=None,      # str | None
    caption=None,       # str | None
    category_label=None,  # str | None — reserved for future legend/hover use
    value_label=None,     # str | None — reserved for future legend/hover use
)
```

When `hole > 0` and `inner_overall > 0`, `get_query_params()` returns both a `"main"` and
an `"overall"` key.

**Supported color selectors:** `ColorSelectorStrategy`, `ColorSelectByCategoryName`,
`ColorSelectValuesBelow`, `ColorSelectValuesAbove`, `ColorSelectMaxValue`, `ColorSelectMinValue`.

**Supported color assigners:** `ColorAssignerByCategory`, `ColorAssignerByValue`.

---

### SunburstChartStrategy

Multi-level pie chart (sunburst) for hierarchical data. Each entry in `path_variables`
represents one ring, innermost first.

```python
from orama.strategies.sunburst import SunburstChartStrategy

strategy = SunburstChartStrategy(
    fields_metadata=fields_metadata,
    path_variables=["continent", "country"],  # list[str] — hierarchical path, innermost first
    aggregation=Aggregation(name="count", function="count"),
    sort_by_value=SortOrder.NONE,
    color_rules=None,
    title=None,       # str | None
    subtitle=None,    # str | None
    caption=None,     # str | None
    path_label=None,  # str | None — reserved for future use
    value_label=None, # str | None — reserved for future use
)
```

**Supported color selectors:** `ColorSelectorStrategy`, `ColorSelectByCategoryName`,
`ColorSelectValuesBelow`, `ColorSelectValuesAbove`, `ColorSelectMaxValue`, `ColorSelectMinValue`.

**Supported color assigners:** `ColorAssignerByCategory`, `ColorAssignerByValue`.

---

### HeatMapChartStrategy

Two-dimensional matrix colored by aggregated value. Accepts at most one `ColorRule`.

```python
from orama.strategies.heatmap import HeatMapChartStrategy

strategy = HeatMapChartStrategy(
    fields_metadata=fields_metadata,
    x_category_variables=["month"],
    y_category_variables=["region"],
    aggregation=Aggregation(name="revenue", function="sum", field="revenue"),
    display_values=True,    # bool — annotate each cell with its numeric value
    color_rules=None,
    title=None,         # str | None
    subtitle=None,      # str | None
    caption=None,       # str | None
    x_label=None,       # str | None — X-axis label
    y_label=None,       # str | None — Y-axis label
    value_label=None,   # str | None — colorbar title
)
```

**Supported color selectors:** `ColorSelectorStrategy` (all entries — select the scale).

**Supported color assigners:** `ColorAssignerByValue` (defines the color scale and optional
range bounds via `range_min`, `range_max`, `range_center_at`).

---

### BoxplotChartStrategy

Statistical distribution chart showing Q1, median, Q3, and computed IQR fences per
category. An optional overall boxplot can be added.

```python
from orama.strategies.boxplot import BoxplotChartStrategy

strategy = BoxplotChartStrategy(
    fields_metadata=fields_metadata,
    category_variables=["region"],
    value_variable="revenue",       # str — a NonDerivedNumericalVariable field
    horizontal=False,
    overall_category_name="",       # str — label for an optional overall boxplot, or ""
    display_mean=False,             # bool — show mean marker
    display_sd=False,               # bool — show standard deviation (requires display_mean)
    color_rules=None,
    title=None,                     # str | None
    subtitle=None,                  # str | None
    caption=None,                   # str | None
    category_label=None,            # str | None — axis label for the category
    value_label=None,               # str | None — axis label for the value
)
```

`get_query_params()` aggregates Q1, median, Q3, mean, standard deviation, min, and max.
When `overall_category_name` is set, a second `"overall"` query is added.

**Supported color selectors:** `ColorSelectorStrategy`, `ColorSelectByCategoryName`,
`ColorSelectValuesBelow`, `ColorSelectValuesAbove`, `ColorSelectMaxValue`,
`ColorSelectMinValue`, `ColorSelectValuesBelowMean`, `ColorSelectValuesAboveMean`.

**Supported color assigners:** `ColorAssignerByCategory`, `ColorAssignerByValue`.

---

### GroupedBoxplotChartStrategy

Extends the standard boxplot with an additional grouping dimension, producing
side-by-side boxplots per category — one per unique value of the grouping variable.

```python
from orama.strategies.grouped_boxplot import GroupedBoxplotChartStrategy

strategy = GroupedBoxplotChartStrategy(
    fields_metadata=fields_metadata,
    category_variables=["region"],      # list[str] — multicategorical
    value_variable="revenue",           # str — NonDerivedNumericalVariable field
    group_variable="product_line",      # str — single categorical field for groups
    horizontal=False,
    display_mean=False,
    display_sd=False,                   # bool — requires display_mean=True
    color_rules=None,
    title=None,                         # str | None
    subtitle=None,                      # str | None
    caption=None,                       # str | None
    category_label=None,                # str | None — axis label for the category
    value_label=None,                   # str | None — axis label for the value
    group_label=None,                   # str | None — legend title for the grouping variable
)
```

`get_query_params()` returns a single `"main"` key.

**Supported color selectors:** `ColorSelectorStrategy`, `ColorSelectByCategoryName`.

**Supported color assigners:** `ColorAssignerByGroup`.

---

### ChoroplethMapChartStrategy

Renders country-level data on an interactive world map using ISO 3166-1 alpha-2
country codes. No Mapbox token is required.

```python
from orama.strategies.choropleth_map import ChoroplethMapChartStrategy
from orama.enums import GeoProjection
from orama.variables import Aggregation

strategy = ChoroplethMapChartStrategy(
    fields_metadata=fields_metadata,
    country_variable="country_code",    # str — CountryCodeVariable (ISO 3166-1 alpha-2)
    aggregation=Aggregation(name="count", function="count"),
    map_style=GeoProjection.EQUIRECTANGULAR,  # GeoProjection enum
    fit_bounds=False,                   # bool — auto-zoom to countries with data
    color_rules=None,
    title=None,                         # str | None
    subtitle=None,                      # str | None
    caption=None,                       # str | None
    country_label=None,                 # str | None — label for the country dimension
    value_label=None,                   # str | None — colorbar title
)
```

`get_query_params()` returns a single `"main"` key.

**Supported color selectors:** `ColorSelectorStrategy`, `ColorSelectByCategoryName`,
`ColorSelectValuesBelow`, `ColorSelectValuesAbove`, `ColorSelectMaxValue`,
`ColorSelectMinValue`, `ColorSelectValuesBelowMean`, `ColorSelectValuesAboveMean`.

**Supported color assigners:** `ColorAssignerByCategory`, `ColorAssignerByValue`.

---

## Variables

Variables describe the roles that DataFrame fields play in a chart. Each strategy
declares a fixed `_variables` map; callers select fields from their metadata to fill
those roles.

### `Aggregation` dataclass

```python
from orama.variables import Aggregation

Aggregation(
    name="revenue_sum",   # str — output column name in the result DataFrame
    function="sum",       # str — therismos AggregationFunction value
    field="revenue",      # str | None — source field; None for count-only aggregations
)
```

### Variable types

| Class | Candidate fields |
|-------|-----------------|
| `CategoricalVariable` | Fields where `FieldMetadata.categorical` is `True` |
| `NumericalVariable` | Fields where `FieldMetadata.numeric` is `True` |
| `NonDerivedNumericalVariable` | Numeric fields that are not derived (`FieldMetadata.derived` is `False`) |
| `NumericalAggregationVariable` | Same as `NonDerivedNumericalVariable`; used as aggregation targets |
| `DateTimeVariable` | `date` or `datetime` fields for which `{field}__year` and `{field}__month` derived fields are present in the metadata map; or standalone integer fields annotated with `suggested_validation` of `"year"` or `"month"` |
| `CountryCodeVariable` | Fields with `extra["suggested_validation"] == "iso_a2"` (ISO 3166-1 alpha-2 country codes) |

All variable types validate the fields passed by the caller at strategy construction time and
raise `ValueError` if an invalid field is supplied.

---

## Options

Options tune chart appearance and behaviour. Each strategy declares a fixed `_options` map
and validates option values at construction time.

| Class | Python type | Notes |
|-------|-------------|-------|
| `BooleanOption` | `bool` | Default: `False` unless declared otherwise |
| `FloatOption` | `float \| int` | Supports optional `min_value` / `max_value` bounds |
| `IntOption` | `int` | Supports optional `min_value` / `max_value` bounds |
| `StringOption` | `str` | Default: `None` unless declared otherwise |
| `SortOrderOption` | `SortOrder` | Default: `SortOrder.NONE` |
| `LinePlacementOption` | `LinePlacement` | Default: `LinePlacement.OVERLAPPING` |
| `LineInterpolationOption` | `LineInterpolation` | Default: `LineInterpolation.LINEAR` |
| `LineSortOrderOption` | `LineSortOrder` | Default: `LineSortOrder.NONE` |
| `GeoProjectionOption` | `GeoProjection` | Default: `GeoProjection.NATURAL_EARTH` |

---

## Enums

### `SortOrder`

```python
from orama.enums import SortOrder

SortOrder.NONE        # 0 — no sorting applied
SortOrder.ASCENDING   # 1
SortOrder.DESCENDING  # -1
```

### `LinePlacement`

Controls how multiple line series are positioned relative to each other.

```python
from orama.enums import LinePlacement

LinePlacement.OVERLAPPING       # lines drawn on top of each other (default)
LinePlacement.STACKED           # lines stacked additively
LinePlacement.STACKED_RELATIVE  # lines stacked as relative percentages (100 %)
LinePlacement.RIDGELINE         # lines offset vertically (ridgeline / joy plot)
```

### `LineInterpolation`

Controls the interpolation mode between data points on a line trace.

```python
from orama.enums import LineInterpolation

LineInterpolation.LINEAR   # straight segments between points (default)
LineInterpolation.SPLINE   # smooth curved interpolation
LineInterpolation.STEP_HV  # horizontal then vertical step ("hv")
LineInterpolation.STEP_VH  # vertical then horizontal step ("vh")
```

### `LineSortOrder`

Controls how line series or categories are sorted in the legend.

```python
from orama.enums import LineSortOrder

LineSortOrder.NONE              # no sorting (default)
LineSortOrder.ALPHABETICAL_ASC  # alphabetically ascending
LineSortOrder.ALPHABETICAL_DESC # alphabetically descending
LineSortOrder.FIRST_VALUE_ASC   # ascending by first data-point value
LineSortOrder.FIRST_VALUE_DESC  # descending by first data-point value
LineSortOrder.LAST_VALUE_ASC    # ascending by last data-point value
LineSortOrder.LAST_VALUE_DESC   # descending by last data-point value
```

### `GeoProjection`

Controls the map projection used by `ChoroplethMapChartStrategy`.

```python
from orama.enums import GeoProjection

GeoProjection.NATURAL_EARTH    # Natural Earth (default for GeoProjectionOption)
GeoProjection.MERCATOR         # Mercator
GeoProjection.ORTHOGRAPHIC     # Orthographic
GeoProjection.EQUIRECTANGULAR  # Equirectangular (default in ChoroplethMapChartStrategy)
GeoProjection.ROBINSON         # Robinson
GeoProjection.KAVRAYSKIY7      # Kavrayskiy VII
```

---

## Color System

Every color rule is a `ColorRule` — a pair of one **selector** (which rows to affect) and
one **assigner** (what colors to apply to those rows). Rules are applied in order; later
rules override earlier ones for the selected rows.

```python
from orama.color import (
    ColorRule,
    ColorSelectMaxValue,
    ColorAssignerByCategory,
)
from polychromos.color import HSLColor

rule = ColorRule(
    assigner=ColorAssignerByCategory(
        anchor_colors=[HSLColor.from_hex("#ef4444")],
    ),
    selector=ColorSelectMaxValue(),  # highlight only the maximum bar
)

strategy = BarChartStrategy(
    fields_metadata=fields_metadata,
    category_variables=["region"],
    aggregation=Aggregation(name="count", function="count"),
    color_rules=[rule],
)
```

### Selectors

| Class | Selects |
|-------|---------|
| `ColorSelectorStrategy` | All entries (use as first rule to override defaults) |
| `ColorSelectByCategoryName(names)` | Entries whose category name is in `names` |
| `ColorSelectValuesBelow(threshold)` | Entries with value ≤ threshold |
| `ColorSelectValuesAbove(threshold)` | Entries with value ≥ threshold |
| `ColorSelectMinValue()` | Entry/entries with the minimum value |
| `ColorSelectMaxValue()` | Entry/entries with the maximum value |
| `ColorSelectValuesBelowMean()` | Entries with value ≤ weighted mean (bar charts only) |
| `ColorSelectValuesAboveMean()` | Entries with value ≥ weighted mean (bar charts only) |

### Assigners

| Class | Assigns |
|-------|---------|
| `ColorAssignerByCategory(anchor_colors, shuffle=False)` | One color per distinct category, interpolated from anchor colors; `shuffle=True` alternates colors across categories |
| `ColorAssignerByGroup(anchor_colors, shuffle=False)` | One color per distinct group; `shuffle=True` alternates colors across groups |
| `ColorAssignerByValue(anchor_colors, range_min, range_max, range_center_at)` | Continuous color scale mapped to values; optionally centered at a pivot |

`anchor_colors` is an `HSLColorSequence` from `polychromos`. When two colors are provided and
more entries exist, colors are interpolated via cylindrical shortest-path. When more than two
colors are provided, a multi-stop color scale is constructed.

Each strategy documents which selectors and assigners it accepts. Passing an unsupported
combination raises `ValueError` at construction time.

---

## Theming

`Theme` wraps an armonia `Theme` and exposes colors to chart strategies.

```python
from orama.theme import Theme
from polychromos.color import HSLColor

theme = Theme(colors={
    # Required for all charts
    "background":             HSLColor.from_hex("#1e293b"),
    "primary_color_normal":   HSLColor.from_hex("#3b82f6"),
    # Optional — used by strategies and computed color variants
    "secondary_color_normal": HSLColor.from_hex("#f59e0b"),
    "accent_color_normal":    HSLColor.from_hex("#10b981"),
})
```

When `"background"` is provided, the following computed colors are automatically registered:

- `background_inverted`, `background_inverted_mild`, `background_inverted_milder`, `background_inverted_faint`
- `{prefix}_color_softer`, `{prefix}_color_stronger` for each of `primary`, `secondary`, `accent`

Use `theme.get_color(name)` to retrieve any named or computed color as an `HSLColor`.
Use `theme.get_all_colors()` to list all registered colors.

The theme is passed to `strategy.plot(dfs, tr, theme)` and applied to every figure via a
Plotly template derived from `plotly_white`.

---

## Internationalization

All strategies accept a `babel.support.Translations` object as the `tr` argument in `plot()`.
It is used to translate month names and day-of-week labels in time-based charts.

When internationalization is not needed, pass `NullTranslations`:

```python
from babel.support import NullTranslations

fig = strategy.plot(dfs={"main": df}, tr=NullTranslations(), theme=theme)
```

To load real translations, use Babel's standard `Translations.load()`:

```python
from babel.support import Translations

tr = Translations.load("locale", locales=["fr"])
fig = strategy.plot(dfs={"main": df}, tr=tr, theme=theme)
```

---

## Strategy Introspection

Every strategy class exposes a `describe()` class method that returns a
`FigureStrategyDescription` without requiring an instance:

```python
from orama.strategies.bar import BarChartStrategy
from orama.strategies.base import FigureStrategyDescription

desc: FigureStrategyDescription = BarChartStrategy.describe(fields_metadata)

desc.name          # "Bar Chart"
desc.description   # human-readable strategy description
desc.variables     # dict[str, dict[str, Any]] — variable descriptors
desc.options       # dict[str, dict[str, Any]] — option descriptors
desc.colors        # ColorStrategyDescription — supported selectors and assigners
```

Each variable descriptor includes `type`, `name`, `description`, `map_to`,
`candidate_fields`, and (for categorical variables) `multicategorical`, `ordered`,
`optional`. Each option descriptor includes `type`, `name`, `description`, `map_to`,
`default_value`, and `value_type`.

`get_query_params()` returns a `dict[str, QueryParams]`. Inspecting each
`QueryParams` reveals the `group_fields`, `aggregations`, and `dataframe_schema` that
your data layer must satisfy:

```python
params = strategy.get_query_params()
for query_name, qp in params.items():
    print(query_name, qp.group_fields, qp.dataframe_schema)
```

---

## Modebar Configuration

Every strategy exposes a `modebar_config()` instance method that returns a serialisable
Plotly config dict. The web layer passes this to `Figure.to_html(config=...)` when
rendering each figure. By default the config:

- Hides the Plotly logo (`displaylogo: False`).
- Removes the box-select and lasso tools (`modeBarButtonsToRemove`).

The demo additionally injects a **Download SVG** button next to the default PNG button
via a `post_script` using `Plotly.react()` (JavaScript callbacks cannot be serialised to
JSON and therefore cannot be part of the Python config dict).

Override `modebar_config()` in a subclass to customise the modebar for a specific strategy:

```python
class MyBarChartStrategy(BarChartStrategy):
    def modebar_config(self) -> dict:
        cfg = super().modebar_config()
        cfg["modeBarButtonsToRemove"].append("zoom2d")
        return cfg
```

---

## Web Layer

`orama.web.FigureView` binds a WTForms form to a strategy class and provides
serialization, deserialization, and instantiation from form data.

```python
from orama.web.figureview import FigureView
from orama.strategies.bar import BarChartStrategy

# Create from HTTP form data (POST)
view = FigureView(
    figure_strategy=BarChartStrategy,
    fields_metadata=fields_metadata,
    dataset_name="sales",
    formdata=request.form,
)

# Instantiate the strategy from the submitted form
strategy = view.instantiate_figure_strategy(theme)

# Serialize to a JSON-serializable dict (for persistence)
state = view.serialize()

# Reconstruct from a previously serialized dict
view = FigureView.deserialize(
    data=state,
    strategy_registry={"BarChartStrategy": BarChartStrategy},
    fields_metadata_registry={"sales": fields_metadata},
)
```

The full web binding — dynamic WTForms `Form` subclasses, color rule forms, and HTTP
serialization — is available via `pip install 'orama[web]'`.

---

## Dependencies

| Package | Version | Role |
|---------|---------|------|
| `plotly[express]` | ~6.3 | Primary rendering backend (`go.Figure`) |
| `polars` | ~1.33 | DataFrame manipulation and schema validation |
| `pandas` | ~2.3 | Plotly bridge (`df.to_pandas()` at plot boundary) |
| `pyarrow` | ~22.0 | Polars/Pandas interchange |
| `fields-metadata` | ≥0.1 | Field discovery and metadata (`FieldMetadata`, `MetadataExtractor`) |
| `therismos` | ≥0.1 | `GroupSpec`, `AggregationSpec`, `SortSpec` |
| `polychromos` | ~1.4 | Color manipulation: HSL, palettes, sequences, scales |
| `armonia` | ~1.1 | Theme color composition and computed color functions |
| `babel` | ~2.17 | i18n and translations |

---

## License

This project is licensed under the MIT license.
