# Phase 1

## Goal:

create a python library called matelot on top of seaborn that can discriminate plots on color brightness.

## Proposal:

Implement a function named lineplot that takes same arguments as seaborn lineplot function plus an extra argument `brightness: ColumnName | _Vector | None = None`.

If the brightness argument is passed, `hue` must also be provided — raise a clear `ValueError` if not.

The given DataSource (data argument) should be copied (`data.copy()`) and extended with an extra column like this:

`data["_matelot_combined"] = df.apply(lambda x: str(x[brightness]) + " " + str(x[hue]), axis=1)`

Using `"_matelot_combined"` as the column name avoids collisions with existing columns in the user's data. Both values are cast to `str` to handle numeric or categorical column types.

### Palette generation

The base palette is resolved from the `palette` argument if provided, otherwise from `sns.color_palette()` which returns the current active seaborn palette as a list of RGB tuples.

One base color is assigned per unique `hue` value by cycling over the base palette with `itertools.cycle`, mirroring seaborn's native behavior when there are more hue values than palette colors.

For each base color, N brightness shades are generated — one per unique value in the `brightness` column. Shades are computed by:

1. Converting the base RGB color to HLS using Python's `colorsys` module.
2. Starting from the base color's lightness, generating N lightness values by alternating left (darker) and right (lighter) in steps of 0.1: the base is always included, then `base - 0.1`, `base + 0.1`, `base - 0.2`, `base + 0.2`, etc. Values are clamped to [0.0, 0.8]; if a boundary is reached it is repeated. Sorted brightness values map consistently (lowest → darkest shade, highest → lightest).
3. Converting each HLS variant back to RGB.

The result is a dict mapping each `_matelot_combined` value to its computed RGB color, passed as the `palette` argument to seaborn.

### Argument handling

- If `data` is not a `pd.DataFrame`, raise a `TypeError` with a clear message. Seaborn's broader input support (dicts, etc.) is out of scope for Phase 1.
- If `hue_order` is passed, it is intercepted and rewritten into `_matelot_combined` space: for each value in `hue_order`, all corresponding `_matelot_combined` entries are expanded in sorted `brightness` order. The original `hue_order` is not forwarded to seaborn.

Seaborn lineplot is then called with the extended dataset, `hue="_matelot_combined"`, the created palette dict, and all other arguments untouched.

# Phase 2

## Goal:

Extend matelot to support boxplots.

## Proposal:

Implement `matelot.boxplot` using the same `_matelot_combined` column and palette generation strategy as Phase 1. The function signature mirrors `seaborn.boxplot` exactly, plus the `brightness` argument.

The same validation rules apply:
- If `data` is not a `pd.DataFrame`, raise a `TypeError` with a clear message.
- If `brightness` is provided but `hue` is not, raise a `ValueError`.

`hue_order` interception follows the same rewriting logic as Phase 1: intercepted and expanded into `_matelot_combined` space, not forwarded to seaborn.

The `_matelot_combined` column construction and palette generation (HLS shading, clamping to [0.0, 0.8], alternating left/right expansion) are shared with Phase 1 — extract into a common internal module if not already done.

# Phase 3

## Goal:

Make svg lineplots interactive by:
1. allowing users to show point values when overing the line

## Proposal:
Use `uuid.uuid4()` to generate a unique id per `lineplot` call (instead of a static module-level counter).

Add an `annotate: bool = False` parameter to lineplot function.

In lineplot, if `annotate` is true, after calling seaborn lineplot, set a unique gid to each line and add annotations in a single pass.
Suggestion:
```
fmt = mpl.ticker.EngFormatter(unit="", places=2)
for line in ax.get_lines():
    if line.get_label().startswith("_child"):
        line.set_gid(f'_matelot_line_{uid}_{line.get_label()}')
        for (i, (x, y)) in enumerate(list(zip(line.get_xdata(), line.get_ydata()))):
            annotation = ax.annotate(
                fmt(y),
                (x, y + abs(y) * 0.1),
                bbox=dict(boxstyle='round, pad=.2',
                    fc=(1., 1., 1.),
                    ec=(.8, .8, .8), lw=1,
                    zorder=1)
            )
            annotation.set_gid(f'_matelot_tooltip_{uid}_{line.get_label()}_{i}')
```

The SVG transformation logic is extracted into a private `_save_interactived_svg(figure, fname, **kwargs)` helper shared by both flavors below.

### Explicit flavor

Create a class `InteractiveFigure` that contains a matplotlib Figure.

Add an `interactive` function to matelot that takes a Figure as param and returns an InteractiveFigure.

`InteractiveFigure` has a `savefig` method that takes the same parameters as matplotlib `savefig` except `format` (and any other inapplicable params). It delegates to `_save_interactived_svg`.

### Implicit flavor (pyplot-style)

Add a module-level `matelot.savefig` function with the same signature as `InteractiveFigure.savefig`. It implicitly operates on `plt.gcf()` and delegates to `_save_interactived_svg`.

### `_save_interactived_svg` implementation

`_save_interactived_svg` will do something like that:
```
f = BytesIO()
figure.savefig(f, format="svg", ...)

tree, xmlid = ET.XMLID(f.getvalue())
tree.set('onload', 'init(event)')

for id, elem in xmlid.items():
    if id.startswith('_matelot_tooltip_'):
        elem.set('visibility', 'hidden')
    if id.startswith('_matelot_line_'):
        elem.set('onmouseover', f'ShowTooltip(\'{id.replace("_matelot_line_", "_matelot_tooltip_")}\')')
        elem.set('onmouseout', f'HideTooltip(\'{id.replace("_matelot_line_", "_matelot_tooltip_")}\')')

script = """
    <script type="text/ecmascript">
    <![CDATA[

    function init(event) {
        if ( window.svgDocument == null ) {
            svgDocument = event.target.ownerDocument;
            }
        }

    function ShowTooltip(id) {
        for (const el of svgDocument.getElementsByTagName('*')) {
            if (el.id.startsWith(id)) {
                el.setAttribute('visibility', "visible")
            }
        }
    }

    function HideTooltip(id) {
        for (const el of svgDocument.getElementsByTagName('*')) {
            if (el.id.startsWith(id)) {
                el.setAttribute('visibility', "hidden")
            }
        }
    }

    ]]>
    </script>
    """

tree.insert(0, ET.XML(script))
ET.ElementTree(tree).write(fname)

```
