diff --git a/.issueflows/03-solved-issues/issue181_original.md b/.issueflows/03-solved-issues/issue181_original.md new file mode 100644 index 0000000..935ea80 --- /dev/null +++ b/.issueflows/03-solved-issues/issue181_original.md @@ -0,0 +1,13 @@ +# Issue #181 — turning off mute by group reduces interpretability + +Source: https://github.com/cellpy/cellpy-simple-gui/issues/181 + +Here is an example with mute by group on: (screenshot — cells of one group share +a colour) + +Here is after turning it off: (screenshot — every cell has its own colour) + +It is almost impossible to get an impression of what group each cells belong +to. This is however very important information. Propose to keep cells in the +same group with the same color also when mute by group is off. Consider option +for using a gradient within group (saturation). diff --git a/.issueflows/03-solved-issues/issue181_plan.md b/.issueflows/03-solved-issues/issue181_plan.md new file mode 100644 index 0000000..cae974a --- /dev/null +++ b/.issueflows/03-solved-issues/issue181_plan.md @@ -0,0 +1,37 @@ +# Plan — #181 keep group colours when "Mute by group" is off + +## Findings + +- cellpy colours per-cell traces by **group** whenever `group_cells` is on, + independent of `group_legend_muting`; muting only decides what goes on + `legendgroup` (the group number, or the cell name). +- The app's own colorway pass (`collect._apply_colorway`, used by the `safe` + and `muted` schemes) keys colours by `legendgroup or name`. With muting off + that key is the cell name, so every cell gets its own colour — the issue. +- With muting on, cells of one group are drawn in **identical** colours (cellpy + removes the sub-group markers), so the members cannot be told apart either. +- `library.PALETTE` (= the `safe` scheme) also paints the sidebar swatches by + `(group - 1) % len`; the plot used order of appearance instead. + +## Approach + +Colour by group, shade by member — in both muting modes, for every scheme: + +1. `collect.cell_groups(records)` → `{batch key or group name: group}`; + `plotting` passes it to `figure_json(cell_groups=…)` for the summary + (ungrouped) and the Cycles pane (`per_cycle` layout; `per_cell`/`film` + colour by cycle and are left alone). +2. `_apply_colorway(fig, scheme, cell_groups=…)`: traces are bucketed by group + via name / legendgroup lookup. A group's base colour is the scheme colour at + `(group - 1) % len` (so `safe` matches the sidebar swatches, and colours + stay put when a group disappears) or, for the `cellpy` scheme, the colour + cellpy already gave the group. Members of a multi-cell group get a lightness + gradient around the base (`_shade_series`, HLS, clamped so nothing goes + white/black); singletons keep the base exactly. Ungrouped traces keep the + previous order-of-appearance behaviour. Colouring moves ahead of legend + truncation in `_restyle` so truncated names cannot collide. +3. Tests: muting off keeps one hue per group (safe scheme); members of a group + get distinct shades of the same hue in both muting modes; `cellpy` scheme + shades from cellpy's own group colour; `per_cell` layout untouched; `safe` + base colour equals the sidebar swatch. +4. Design doc (`group-legend-muting.md` / `plot-appearance.md`) + README line. diff --git a/.issueflows/03-solved-issues/issue181_status.md b/.issueflows/03-solved-issues/issue181_status.md new file mode 100644 index 0000000..a200b05 --- /dev/null +++ b/.issueflows/03-solved-issues/issue181_status.md @@ -0,0 +1,40 @@ +# Status — #181 keep group colours when "Mute by group" is off + +- [x] Done + +## What was done + +- `collect.cell_groups(records)` maps trace names (batch keys and group names) + to group ids; `plotting.summary_figure` and `plotting.cycles_figure` + (`per_cycle` layout) pass it to `figure_json(cell_groups=…)`. +- `collect._apply_colorway` buckets per-cell traces by group and paints each + group with one hue — the scheme colour at `(group - 1) % len` (so the `safe` + scheme matches the sidebar swatches) or, for the `cellpy` scheme, the colour + cellpy already gave the group — fanned into lightness shades per member via + the new `_shade_series` / `_parse_color` / `_trace_color` / `_paint` + helpers. Singletons keep the base colour exactly. Traces outside any group + (group averages, `per_cell` facets coloured by cycle) keep the previous + order-of-appearance colouring. +- Colouring now runs before legend truncation in `_restyle`, so truncated + names cannot collide. +- Result is identical with **Mute by group** on or off; muting only decides + what a legend click toggles. + +## Tests + +- `tests/test_core.py` (#181 section): `cell_groups` map, `_shade_series`, + summary colours follow groups for every scheme × both muting modes, both + muting modes give identical colours, Cycles pane follows groups while + `per_cell` stays coloured by cycle, group-average traces keep their swatch + colours. Full suite green. + +## Docs + +- `.issueflows/04-designs-and-guides/plot-appearance.md` and + `group-legend-muting.md` updated; README feature line; `llms-full.txt` + regenerated. + +## Remaining + +- Nothing. A saturation (rather than lightness) gradient was considered and + rejected: muted base colours lose their identity when desaturated further. diff --git a/.issueflows/01-current-issues/issue187_original.md b/.issueflows/03-solved-issues/issue187_original.md similarity index 100% rename from .issueflows/01-current-issues/issue187_original.md rename to .issueflows/03-solved-issues/issue187_original.md diff --git a/.issueflows/01-current-issues/issue187_plan.md b/.issueflows/03-solved-issues/issue187_plan.md similarity index 100% rename from .issueflows/01-current-issues/issue187_plan.md rename to .issueflows/03-solved-issues/issue187_plan.md diff --git a/.issueflows/01-current-issues/issue187_status.md b/.issueflows/03-solved-issues/issue187_status.md similarity index 100% rename from .issueflows/01-current-issues/issue187_status.md rename to .issueflows/03-solved-issues/issue187_status.md diff --git a/.issueflows/04-designs-and-guides/group-legend-muting.md b/.issueflows/04-designs-and-guides/group-legend-muting.md index 99d6040..2926bbf 100644 --- a/.issueflows/04-designs-and-guides/group-legend-muting.md +++ b/.issueflows/04-designs-and-guides/group-legend-muting.md @@ -17,6 +17,15 @@ series. The app did not expose the knob. - Do not expose a separate `group_cells` checkbox; muting alone is enough. - Cell explorer stays out of scope. +## Colours do not depend on muting (#181) + +Turning **Mute by group** off used to recolour the figure: the app colorway was +keyed by Plotly `legendgroup`, which cellpy sets to the group id (muting on) or +the cell name (muting off), so each cell got its own colour and group identity +was lost. Since #181 the colorway is keyed by `collect.cell_groups(records)`: +cells of one group share a hue and differ by lightness, in both muting modes. +See [`plot-appearance.md`](plot-appearance.md). + ## Alternatives considered - Hide the control when N/A — rejected; disable + tooltip keeps layout stable. diff --git a/.issueflows/04-designs-and-guides/plot-appearance.md b/.issueflows/04-designs-and-guides/plot-appearance.md index 9e520a3..1c9f504 100644 --- a/.issueflows/04-designs-and-guides/plot-appearance.md +++ b/.issueflows/04-designs-and-guides/plot-appearance.md @@ -22,6 +22,16 @@ light/dark shell hook. - **Persistence:** `localStorage` keys `csg-figure-theme`, `csg-color-scheme`. Existing Light/Dark choices in localStorage stay respected. - **Cell-list swatches** stay on `PALETTE` and do not follow the plot scheme (v1). +- **Colour by group, shade by member (#181):** for per-cell traces + `_apply_colorway` takes a `cell_groups` map (`collect.cell_groups(records)`, + trace name → group id) and paints every group with one hue — scheme colour + index `(group - 1)` (so `safe` matches the sidebar swatches) or, for the + `cellpy` scheme, the colour cellpy already gave the group — fanned into + lightness shades per member (`_shade_series`, HLS lightness ±0.11 per + member, capped at ±0.275). The result is identical whether **Mute by + group** is on or off; muting only decides what a legend click toggles. + Group-average traces (`group_cells=False`) and `per_cell` cycle facets + (coloured by cycle) are left to the plain order-of-appearance pass. - Keep legend truncation + colorway in app `_restyle`. As of cellpy **2.1.1.post4** (#801), paper/plot/font colors and panel height go through `layout_updates` / `height_per_panel` (`collect._inject_app_chrome`); facet diff --git a/README.md b/README.md index 201f5f7..6c6460b 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ - **Cell list** — rename, group, name the groups, select, filter, sort, remove. - **Projects** — save and reopen the loaded cells, grouping, group names, labels, and selection. Unsaved edits show as `name*`. - **Export** — CSV, Excel, Parquet, JSON; figures as PNG, SVG, PDF. -- **Light and dark themes.** +- **Light and dark themes.** Plot colour schemes colour by group (one hue per group, a shade per cell) whether or not legend muting is by group. - **Background loading** with progress. - **Instruments** from the installed cellpy, including each loader's sub-models. - **Developer mode** — every summary family cellpy registers, raw traces, diagnostics (`run --dev`). diff --git a/llms-full.txt b/llms-full.txt index 96b2780..604a596 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -132,7 +132,7 @@ FILE: README.md - **Cell list** — rename, group, name the groups, select, filter, sort, remove. - **Projects** — save and reopen the loaded cells, grouping, group names, labels, and selection. Unsaved edits show as `name*`. - **Export** — CSV, Excel, Parquet, JSON; figures as PNG, SVG, PDF. -- **Light and dark themes.** +- **Light and dark themes.** Plot colour schemes colour by group (one hue per group, a shade per cell) whether or not legend muting is by group. - **Background loading** with progress. - **Instruments** from the installed cellpy, including each loader's sub-models. - **Developer mode** — every summary family cellpy registers, raw traces, diagnostics (`run --dev`). diff --git a/src/cellpy_simple_gui/core/collect.py b/src/cellpy_simple_gui/core/collect.py index c33595a..514e6a1 100644 --- a/src/cellpy_simple_gui/core/collect.py +++ b/src/cellpy_simple_gui/core/collect.py @@ -12,6 +12,7 @@ from __future__ import annotations +import colorsys import io import logging @@ -109,6 +110,20 @@ def group_titles(records: list[CellRecord]) -> dict[str, str]: return {str(rec.group): rec.group_label for rec in records if rec.group_label} +def cell_groups(records: list[CellRecord]) -> dict[str, int]: + """Legend key → group number, for colouring traces by group (#181). + + Per-cell traces are named after the batch key; group-averaged traces are + named after the group (``group_name``). Both resolve here so the colour + pass can find a trace's group whatever ``legendgroup`` carries. + """ + out: dict[str, int] = {} + for key, rec in zip(batch_keys(records), records, strict=True): + out[key] = rec.group + out.setdefault(rec.group_name(), rec.group) + return out + + def _apply_group_titles(fig, titles: dict[str, str]) -> None: for tr in fig.data: group = getattr(tr, "legendgroup", None) @@ -878,6 +893,7 @@ def figure_json( overlay: bool = False, warnings: list[str] | None = None, group_titles: dict[str, str] | None = None, + cell_groups: dict[str, int] | None = None, **plot_kwargs, ) -> str: # spread (mean ± std band) only makes sense once actually group-averaged. @@ -905,7 +921,12 @@ def figure_json( fig = _overlay_facets( fig, height=opts["height_per_panel"] + opts["figure_border_height"] ) - _restyle(fig, figure_theme=figure_theme, color_scheme=color_scheme) + _restyle( + fig, + figure_theme=figure_theme, + color_scheme=color_scheme, + cell_groups=cell_groups, + ) if group_titles and not is_grouped(collection): _apply_group_titles(fig, group_titles) if y_ranges: @@ -1366,30 +1387,132 @@ def _hex_to_rgba(color: str, alpha: float = 0.28) -> str: return color -def _apply_colorway(fig, color_scheme: str) -> None: - """Cycle a discrete colorway across legend series (name / legendgroup).""" +def _parse_color(color) -> tuple[int, int, int] | None: + """``#rrggbb`` / ``rgb(…)`` / ``rgba(…)`` → ``(r, g, b)``; None when unknown.""" + if not isinstance(color, str): + return None + c = color.strip() + try: + if c.startswith("#") and len(c) == 7: + return tuple(int(c[i : i + 2], 16) for i in (1, 3, 5)) # type: ignore[return-value] + if c.startswith("rgb"): + parts = c[c.index("(") + 1 : c.rindex(")")].split(",")[:3] + return tuple(int(float(p)) for p in parts) # type: ignore[return-value] + except (ValueError, IndexError): + return None + return None + + +def _shade_series(base: str, n: int) -> list[str]: + """``n`` shades of ``base`` — same hue, lightness spread around it (#181). + + One member keeps the base colour exactly, so a singleton group matches + its sidebar swatch. Several members fan out from darker to lighter, the + span growing with the count and clamped so no shade turns black or white. + """ + if n <= 1: + return [base] + rgb = _parse_color(base) + if rgb is None: + return [base] * n + h, lum, sat = colorsys.rgb_to_hls(*(v / 255 for v in rgb)) + span = min(0.55, 0.22 * (n - 1)) + out: list[str] = [] + for i in range(n): + t = i / (n - 1) + l_i = min(0.86, max(0.2, lum + (t - 0.5) * span)) + r, g, b = colorsys.hls_to_rgb(h, l_i, sat) + out.append(f"#{round(r * 255):02x}{round(g * 255):02x}{round(b * 255):02x}") + return out + + +def _trace_color(tr) -> str | None: + line = getattr(tr, "line", None) + color = getattr(line, "color", None) if line is not None else None + if isinstance(color, str): + return color + marker = getattr(tr, "marker", None) + color = getattr(marker, "color", None) if marker is not None else None + return color if isinstance(color, str) else None + + +def _paint(tr, color: str) -> None: + try: + if getattr(tr, "line", None) is not None: + tr.line.color = color + if getattr(tr, "marker", None) is not None: + tr.marker.color = color + # Spread bands need alpha in fillcolor; tr.opacity is ignored for + # fills or washes out the mean line when fill+line share a trace. + fill = getattr(tr, "fill", None) + if fill and fill != "none": + tr.fillcolor = _hex_to_rgba(color, 0.28) + except Exception: # noqa: BLE001 - per-trace color is best-effort + pass + + +def _apply_colorway( + fig, color_scheme: str, *, cell_groups: dict[str, int] | None = None +) -> None: + """Colour legend series: by group when the groups are known, else per series. + + With ``cell_groups`` (#181) every trace that resolves to a group — by name + or ``legendgroup`` — takes that group's colour, whichever way legend muting + is set: the scheme colour at ``group - 1`` (so ``safe`` matches the sidebar + swatches) or, for the ``cellpy`` scheme, the colour cellpy already gave the + group. Members of a multi-cell group become shades of that colour so they + stay distinguishable. Traces outside any group cycle the scheme in order + of appearance, as before; without a scheme they are left to cellpy. + """ colors = COLOR_SCHEMES.get(color_scheme) - if not colors: + if not colors and not cell_groups: return - series_key: dict[str, int] = {} + + def series_of(tr) -> str: + # A trace named after a cell is that cell's series even when legend + # muting puts the whole group on one legendgroup. + name = getattr(tr, "name", None) + if cell_groups and name is not None and str(name) in cell_groups: + return str(name) + return str(getattr(tr, "legendgroup", None) or name or id(tr)) + + def group_of(tr) -> int | None: + if not cell_groups: + return None + for attr in ("name", "legendgroup"): + value = getattr(tr, attr, None) + if value is not None and str(value) in cell_groups: + return cell_groups[str(value)] + return None + + # group → series key → traces, all in order of appearance + grouped: dict[int, dict[str, list]] = {} + loose: list[tuple[str, object]] = [] for tr in fig.data: - key = getattr(tr, "legendgroup", None) or getattr(tr, "name", None) or id(tr) - key = str(key) - if key not in series_key: - series_key[key] = len(series_key) - color = colors[series_key[key] % len(colors)] - try: - if getattr(tr, "line", None) is not None: - tr.line.color = color - if getattr(tr, "marker", None) is not None: - tr.marker.color = color - # Spread bands need alpha in fillcolor; tr.opacity is ignored for - # fills or washes out the mean line when fill+line share a trace. - fill = getattr(tr, "fill", None) - if fill and fill != "none": - tr.fillcolor = _hex_to_rgba(color, 0.28) - except Exception: # noqa: BLE001 - per-trace color is best-effort - continue + g = group_of(tr) + if g is None: + loose.append((series_of(tr), tr)) + else: + grouped.setdefault(g, {}).setdefault(series_of(tr), []).append(tr) + + for g, members in grouped.items(): + if colors: + base = colors[(g - 1) % len(colors)] + else: + base = next((c for traces in members.values() for tr in traces if (c := _trace_color(tr))), None) + if base is None: + continue + for shade, traces in zip(_shade_series(base, len(members)), members.values(), strict=True): + for tr in traces: + _paint(tr, shade) + + if not colors: + return + series_index: dict[str, int] = {} + for key, tr in loose: + if key not in series_index: + series_index[key] = len(series_index) + _paint(tr, colors[series_index[key] % len(colors)]) def _restyle( @@ -1397,6 +1520,7 @@ def _restyle( *, figure_theme: str = "light", color_scheme: str = "cellpy", + cell_groups: dict[str, int] | None = None, ) -> None: """Post-plot polish: legend truncation, colorway, margins, soft axes. @@ -1404,9 +1528,11 @@ def _restyle( ``layout_updates`` / ``height_per_panel`` (#801) in :func:`_inject_app_chrome`. This pass keeps app-owned legend/colorway behaviour and axis grid styling. """ - # Name truncation must not share fate with best-effort cosmetics. + # Colour first, while trace names are still the full batch keys the + # group lookup needs (#181); truncation must not share fate with the + # best-effort cosmetics below. + _apply_colorway(fig, color_scheme, cell_groups=cell_groups) longest = _shorten_legend(fig) - _apply_colorway(fig, color_scheme) tokens = _THEME_TOKENS.get(figure_theme, _THEME_TOKENS["light"]) try: strip_pad = _FACET_STRIP_RIGHT_PAD if _has_right_facet_strips(fig) else 0 diff --git a/src/cellpy_simple_gui/core/plotting.py b/src/cellpy_simple_gui/core/plotting.py index 17da76d..3a818aa 100644 --- a/src/cellpy_simple_gui/core/plotting.py +++ b/src/cellpy_simple_gui/core/plotting.py @@ -68,6 +68,8 @@ def summary_figure(records: list[CellRecord], spec: SummaryPlotSpec) -> str: group_legend_muting=spec.group_legend_muting, # Named groups caption their cells in the legend (#187). group_titles=collect.group_titles(records), + # One colour per group, shaded per cell, whatever legend muting does (#181). + cell_groups=collect.cell_groups(records), figure_theme=spec.figure_theme, color_scheme=spec.color_scheme, # Unit-bearing titles; cellpy defaults are pretty but unit-less (§18 / #38). @@ -181,6 +183,9 @@ def cycles_figure(records: list[CellRecord], spec: CyclesPlotSpec) -> str: family_kind=_CURVE_FAMILIES.get(spec.curve_kind, "cycles"), group_legend_muting=spec.group_legend_muting, group_titles=collect.group_titles(records), + # per_cell facets colour by cycle and film is a heatmap; only the + # per-cycle layout has one series per cell to colour by group (#181). + cell_groups=collect.cell_groups(records) if spec.layout == "per_cycle" else None, figure_theme=spec.figure_theme, color_scheme=spec.color_scheme, x_range=spec.x_range, diff --git a/tests/test_core.py b/tests/test_core.py index 3aec53f..5d372d7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1317,6 +1317,149 @@ def test_group_names_caption_ungrouped_legend(loaded_library): assert titles == {"Anodes"} +# --------------------------------------------------------------------------- # +# #181 — colour by group, shade by member, whatever legend muting does +# --------------------------------------------------------------------------- # + + +def _hue(color: str) -> float: + """Hue in [0, 1).""" + import colorsys + + rgb = collect._parse_color(color) + assert rgb is not None, color + return colorsys.rgb_to_hls(*(v / 255 for v in rgb))[0] + + +def _same_hue(a: str, b: str, tol: float = 0.03) -> bool: + """Hue equality with wraparound and slack for 8-bit hex rounding of dark/light shades.""" + d = abs(_hue(a) - _hue(b)) + return min(d, 1 - d) < tol + + +def _line_colors(fig: dict) -> dict[str, str]: + """Trace name → line colour (first trace per name).""" + out: dict[str, str] = {} + for tr in fig["data"]: + color = (tr.get("line") or {}).get("color") + if tr.get("name") and color and tr["name"] not in out: + out[tr["name"]] = color + return out + + +def _three_cells_two_groups(loaded_library): + """cellA1 + cellA2 in group 1, cellB in group 2.""" + from cellpy_simple_gui.core import cellpy_adapter + + lib = loaded_library + a = lib.all()[0] + b = lib.add_cell(cellpy_adapter.load_example("cellpy"), source="example:cellpy2") + c = lib.add_cell(cellpy_adapter.load_example("rate"), source="example:rate") + lib.update(a.id, group=1, label="cellA1") + lib.update(b.id, group=1, label="cellA2") + lib.update(c.id, group=2, label="cellB") + return lib + + +def test_cell_groups_map(loaded_library): + lib = _three_cells_two_groups(loaded_library) + lib.set_group_label(2, "Cathodes") + assert collect.cell_groups(lib.selected()) == { + "cellA1": 1, "cellA2": 1, "group 1": 1, "cellB": 2, "Cathodes": 2, + } + + +def test_shade_series(): + assert collect._shade_series("#4C78A8", 1) == ["#4C78A8"] + shades = collect._shade_series("#4C78A8", 3) + assert len(shades) == 3 and len(set(shades)) == 3 + assert shades[1] == "#4c78a8" # the middle member keeps the base + assert all(_same_hue(s, shades[0]) for s in shades) # same hue throughout + # rgb() input is understood; garbage is passed through untouched + assert len(set(collect._shade_series("rgb(76, 120, 168)", 2))) == 2 + assert collect._shade_series("not-a-colour", 2) == ["not-a-colour"] * 2 + + +@pytest.mark.parametrize("muting", [True, False]) +@pytest.mark.parametrize("scheme", ["safe", "muted", "cellpy"]) +def test_summary_colours_follow_groups_regardless_of_muting(loaded_library, muting, scheme): + """Cells of one group share a hue; members are distinct shades; groups differ (#181).""" + lib = _three_cells_two_groups(loaded_library) + fig = json.loads( + plotting.summary_figure( + lib.selected(), + SummaryPlotSpec( + plot_type="charge_capacity", group_legend_muting=muting, color_scheme=scheme + ), + ) + ) + colors = _line_colors(fig) + assert set(colors) == {"cellA1", "cellA2", "cellB"} + assert colors["cellA1"] != colors["cellA2"] + assert _same_hue(colors["cellA1"], colors["cellA2"]) + assert not _same_hue(colors["cellB"], colors["cellA1"]) + if scheme != "cellpy": + palette = collect.COLOR_SCHEMES[scheme] + # The singleton group is painted exactly its swatch colour (group 2 → index 1). + assert colors["cellB"] == palette[1] + assert _same_hue(colors["cellA1"], palette[0]) + + +def test_summary_colours_identical_for_both_muting_modes(loaded_library): + lib = _three_cells_two_groups(loaded_library) + figs = [ + json.loads( + plotting.summary_figure( + lib.selected(), + SummaryPlotSpec( + plot_type="charge_capacity", group_legend_muting=muting, color_scheme="safe" + ), + ) + ) + for muting in (True, False) + ] + assert _line_colors(figs[0]) == _line_colors(figs[1]) + + +def test_cycles_pane_colours_follow_groups(loaded_library): + lib = _three_cells_two_groups(loaded_library) + fig = json.loads( + plotting.cycles_figure( + lib.selected(), + CyclesPlotSpec(cycles=[1], group_legend_muting=False, color_scheme="safe"), + ) + ) + colors = _line_colors(fig) + assert _same_hue(colors["cellA1"], colors["cellA2"]) + assert not _same_hue(colors["cellA1"], colors["cellB"]) + assert colors["cellA1"] != colors["cellA2"] + + # per_cell facets are coloured by cycle — untouched by the group pass. + fig = json.loads( + plotting.cycles_figure( + lib.selected(), + CyclesPlotSpec(cycles=[1, 2], layout="per_cell", color_scheme="safe"), + ) + ) + by_cycle = _line_colors(fig) + assert set(by_cycle) == {"1", "2"} + assert by_cycle["1"] != by_cycle["2"] + + +def test_group_average_colours_match_swatches(loaded_library): + lib = _three_cells_two_groups(loaded_library) + fig = json.loads( + plotting.summary_figure( + lib.selected(), + SummaryPlotSpec(plot_type="charge_capacity", group_average=True, color_scheme="safe"), + ) + ) + colors = _line_colors(fig) + palette = collect.COLOR_SCHEMES["safe"] + assert colors["group 1"] == palette[0] + assert colors["group 2"] == palette[1] + + def test_summary_panels_for_capacity_ce(): panels = collect.summary_panels_for("capacity_ce", "gravimetric") ids = [p["id"] for p in panels]