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

1"""Colour helpers shared by every chart, independent of any drawing library. 

2 

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. 

5 

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""" 

12 

13from __future__ import annotations 

14 

15__all__ = ["PALETTE", "bar_colors", "hex_to_rgba", "ticker_colors", "yearly_bar_colors"] 

16 

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) 

30 

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" 

35 

36 

37def hex_to_rgba(hex_color: str, alpha: float = 0.5) -> str: 

38 """Convert a hex colour to an RGBA CSS string. 

39 

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. 

43 

44 Returns: 

45 str: An ``rgba(r, g, b, a)`` string. Both backends accept this 

46 spelling, so it needs no per-renderer translation. 

47 

48 Examples: 

49 >>> hex_to_rgba("#636efa", 0.4) 

50 'rgba(99, 110, 250, 0.4)' 

51 

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})" 

56 

57 

58def ticker_colors(tickers: list[str]) -> dict[str, str]: 

59 """Assign a stable colour to each ticker. 

60 

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. 

63 

64 Args: 

65 tickers: Ordered ticker / column names. 

66 

67 Returns: 

68 dict[str, str]: One hex colour per ticker. 

69 

70 Examples: 

71 >>> ticker_colors(["AAPL", "META"])["AAPL"] 

72 '#636EFA' 

73 

74 """ 

75 return {ticker: PALETTE[i % len(PALETTE)] for i, ticker in enumerate(tickers)} 

76 

77 

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. 

80 

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. 

84 

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. 

89 

90 Returns: 

91 list[str]: One colour per value. 

92 

93 Examples: 

94 >>> bar_colors([0.1, -0.1], "#636EFA", single_asset=True) 

95 ['#2ca02c', '#d62728'] 

96 

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] 

102 

103 

104def yearly_bar_colors(values: list[float | None], positive_color: str) -> list[str]: 

105 """Colour each annual bar by the sign of its value. 

106 

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. 

110 

111 Args: 

112 values: The per-year return values; None counts as negative. 

113 positive_color: The asset's base colour. 

114 

115 Returns: 

116 list[str]: One colour per value. 

117 

118 Examples: 

119 >>> yearly_bar_colors([0.0], "#636EFA") 

120 ['#636EFA'] 

121 

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]