Coverage for src/jquantstats/_plots/_style.py: 100%
19 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 04:11 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 04:11 +0000
1"""Colour helpers shared by every chart, independent of any drawing library.
3These decide *which* colours a chart uses. How those colours are applied to a
4figure is a renderer's job, so nothing here imports a drawing library.
6The palette is Plotly's qualitative sequence, copied out rather than imported.
7Reading it from `plotly.express` would make this module — and everything that
8picks a colour, which is every spec builder — depend on a drawing library, and
9would silently restyle every chart if Plotly ever revised the sequence. The
10fidelity snapshots pin these exact values, so a copy is the honest form.
11"""
13from __future__ import annotations
15__all__ = ["PALETTE", "bar_colors", "hex_to_rgba", "ticker_colors", "yearly_bar_colors"]
17#: Plotly's ``qualitative.Plotly`` sequence, as of plotly 6.
18PALETTE = (
19 "#636EFA",
20 "#EF553B",
21 "#00CC96",
22 "#AB63FA",
23 "#FFA15A",
24 "#19D3F3",
25 "#FF6692",
26 "#B6E880",
27 "#FF97FF",
28 "#FECB52",
29)
31#: Green and red for a single-asset chart, where a bar's colour can carry the
32#: sign outright rather than having to stay identifiable as one asset among several.
33_POSITIVE = "#2ca02c"
34_NEGATIVE = "#d62728"
37def hex_to_rgba(hex_color: str, alpha: float = 0.5) -> str:
38 """Convert a hex colour to an RGBA CSS string.
40 Args:
41 hex_color: A hex colour, with or without a leading ``#``.
42 alpha: Opacity in the range [0, 1]. Defaults to 0.5.
44 Returns:
45 str: An ``rgba(r, g, b, a)`` string. Both backends accept this
46 spelling, so it needs no per-renderer translation.
48 Examples:
49 >>> hex_to_rgba("#636efa", 0.4)
50 'rgba(99, 110, 250, 0.4)'
52 """
53 hex_color = hex_color.lstrip("#")
54 r, g, b = (int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
55 return f"rgba({r}, {g}, {b}, {alpha})"
58def ticker_colors(tickers: list[str]) -> dict[str, str]:
59 """Assign a stable colour to each ticker.
61 Colours are taken from the palette in order and wrap around once it is
62 exhausted, so the same ticker list always yields the same mapping.
64 Args:
65 tickers: Ordered ticker / column names.
67 Returns:
68 dict[str, str]: One hex colour per ticker.
70 Examples:
71 >>> ticker_colors(["AAPL", "META"])["AAPL"]
72 '#636EFA'
74 """
75 return {ticker: PALETTE[i % len(PALETTE)] for i, ticker in enumerate(tickers)}
78def bar_colors(values: list[float | None], positive_color: str, single_asset: bool = False) -> list[str]:
79 """Colour each bar by the sign of its value.
81 With one asset the bars are plain green and red. With several, each keeps
82 its own palette colour and negatives are faded instead, so a bar stays
83 identifiable as belonging to its asset.
85 Args:
86 values: The plotted values; None counts as negative.
87 positive_color: The asset's base colour.
88 single_asset: Use the plain green/red palette.
90 Returns:
91 list[str]: One colour per value.
93 Examples:
94 >>> bar_colors([0.1, -0.1], "#636EFA", single_asset=True)
95 ['#2ca02c', '#d62728']
97 """
98 if single_asset:
99 return [_POSITIVE if v is not None and v > 0 else _NEGATIVE for v in values]
100 negative_color = hex_to_rgba(positive_color, alpha=0.4)
101 return [positive_color if v is not None and v > 0 else negative_color for v in values]
104def yearly_bar_colors(values: list[float | None], positive_color: str) -> list[str]:
105 """Colour each annual bar by the sign of its value.
107 Deliberately distinct from `bar_colors`: a flat zero year counts as
108 positive here (``>= 0``), and negatives fade to alpha 0.5 rather than 0.4.
109 The two cannot share an implementation without changing what is rendered.
111 Args:
112 values: The per-year return values; None counts as negative.
113 positive_color: The asset's base colour.
115 Returns:
116 list[str]: One colour per value.
118 Examples:
119 >>> yearly_bar_colors([0.0], "#636EFA")
120 ['#636EFA']
122 """
123 negative_color = hex_to_rgba(positive_color, 0.5)
124 return [positive_color if v is not None and v >= 0 else negative_color for v in values]