Coverage for src/jointview/app.py: 100%

58 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-17 06:44 +0000

1"""The marimo notebook itself: two pickers, one chart, a summary table either side. 

2 

3Run it with the ``jointview`` command rather than opening this file — see 

4:mod:`jointview.cli`, which starts marimo on this notebook and passes it ``--data`` 

5and ``--height``. 

6""" 

7 

8# The cell signatures below are marimo's, not ours: it names every cell ``_``, derives 

9# the parameters from what the cell reads, and rewrites both on save. Annotating them 

10# would be annotating generated code that the next edit in the marimo editor throws 

11# away — and the types are marimo's UI plumbing, so the annotations would be `Any` 

12# anyway. Told to ruff here rather than in a config file, so the reason sits next to 

13# the code it excuses. (ty asks for none of this; it does not require annotations.) 

14# ruff: noqa: ANN001, ANN202 

15# 

16# The `return` closing each cell is marimo's too, and it is why four of them below carry 

17# a `# pragma: no cover`. Running the notebook never calls these functions: marimo keeps 

18# each cell as its body alone — the stored source parses to Import, Expr, Assign or 

19# FunctionDef, never Return — and executes that as two code objects, the body exec'd and 

20# a final expression eval'd to become the cell's output. So no `return` here is ever 

21# reached, by `app.run()` or by anything else. 

22# 

23# Which makes the interesting case the five that carry no pragma. They are not better 

24# tested; they are shadowed. Where a cell ends in a statement rather than an expression, 

25# marimo emits its synthetic end-of-cell expression onto the next line down, and for a 

26# one-line last statement that line is the `return`. Coverage sees bytecode there and 

27# marks it hit. Nothing to fix — but the four pragmas are the honest cells, not the 

28# neglected ones, and removing them would report a reach that `tests/test_app.py` does 

29# not have. 

30 

31import marimo 

32 

33__generated_with = "0.23.15" 

34app = marimo.App(width="full") 

35 

36 

37@app.cell 

38def _(): 

39 """Import marimo — the one cell every other cell here depends on.""" 

40 import marimo as mo 

41 

42 return (mo,) # pragma: no cover 

43 

44 

45@app.cell 

46def _(mo) -> None: 

47 """Give the plot back the margins marimo reserves for prose.""" 

48 # marimo pads a notebook for prose — 96px of side margin and 48px under the last 

49 # cell, twice over. This is a single-screen instrument, so those margins go back to 

50 # the plot. First cell so the trim is in the DOM before anything that sizes itself 

51 # against the page — the chart follows its container's width — is laid out. 

52 # 

53 # Matched on class *substrings* because the classes are Tailwind and carry colons; 

54 # if a future marimo renames them the rules stop applying and the app still lays 

55 # out, only roomier. 

56 mo.Html(""" 

57 <style> 

58 #App [class*="xl:px-24"] { padding-inline: 0.75rem !important; } 

59 #App [class*="pb-24"] { padding-bottom: 0 !important; } 

60 #App .output-area { padding-inline: 0 !important; } 

61 /* vega-embed keeps a further 38px on the right for its own "..." menu. That one 

62 is out of reach — marimo renders the chart into a shadow root — and it buys 

63 the save/view-source menu, so it stays. */ 

64 </style> 

65 """) 

66 return # pragma: no cover 

67 

68 

69@app.cell 

70def _(mo): 

71 """Load the frame, and settle the two things the command line gets a say in.""" 

72 from jointview.columns import aligned, default_pair, series_columns 

73 from jointview.data import load_frame 

74 from jointview.plot import line_chart 

75 from jointview.stats import summary_markdown 

76 

77 # jointview navs.parquet --height 900 

78 frame = load_frame(mo.cli_args().get("data")) 

79 names = series_columns(frame) 

80 # Nothing on the page has a height for the plot to follow, so it is a number. 700 

81 # fills a laptop window once the notebook margins are gone; a tall monitor wants 

82 # more, and only the person looking at it knows which they have. 

83 plot_height = int(mo.cli_args().get("height") or 700) 

84 return ( 

85 aligned, 

86 default_pair, 

87 frame, 

88 line_chart, 

89 names, 

90 plot_height, 

91 summary_markdown, 

92 ) 

93 

94 

95@app.cell 

96def _(default_pair, frame, mo, names): 

97 """Build the two series pickers, opened on a pair worth looking at.""" 

98 a_start, b_start = default_pair(frame) 

99 

100 # One dropdown is the whole selector: it holds its own value, so there is no 

101 # mo.state and no two-way binding to keep in step. It also costs a fixed two lines 

102 # of the panel whatever the frame is — a radio list grew with the column count, 

103 # and a wide parquet would have turned the side panels into a scroll. 

104 def _pick(start): 

105 """One dropdown over every series, opened on the column at ``start``.""" 

106 return mo.ui.dropdown( 

107 options=names, 

108 value=names[start], 

109 # Searchable from ten columns up, where scanning the list stops being 

110 # quicker than typing. Below that the search box is only in the way. 

111 searchable=len(names) >= 10, 

112 # There is no such thing as a plot of no series: the "--" entry would only 

113 # ever be a way to break the chart. 

114 allow_select_none=False, 

115 full_width=True, 

116 ) 

117 

118 a_pick, b_pick = _pick(a_start), _pick(b_start) 

119 return a_pick, b_pick 

120 

121 

122@app.cell 

123def _(mo): 

124 """The only control over the plot itself: whether to index both series to 100.""" 

125 # Two NAVs on one axis only mean something on a shared scale; off, the raw levels 

126 # are there for a pair that already shares one. 

127 rebase = mo.ui.switch(value=True, label="index both to 100") 

128 return (rebase,) 

129 

130 

131@app.cell 

132def _(a_pick, aligned, b_pick, frame, line_chart, plot_height, rebase): 

133 """Read the pickers, and derive everything the layout below draws.""" 

134 a_column = a_pick.value 

135 b_column = b_pick.value 

136 pair = aligned(frame, a_column, b_column) 

137 chart = line_chart(frame, a_column, b_column, rebase=rebase.value, height=plot_height) 

138 return a_column, b_column, chart, pair 

139 

140 

141@app.cell 

142def _(mo, summary_markdown): 

143 """The two pieces of furniture a side panel is made of.""" 

144 

145 def summary_table(pair, side, title): 

146 """A metric table for one side of ``pair``, or a note when the sample is too short. 

147 

148 The whole aligned frame goes in rather than the column alone: jQuantStats reads 

149 the annualisation factor off the spacing of the period column, so the dates have 

150 to travel with the levels. 

151 """ 

152 try: 

153 return mo.md(summary_markdown(pair, side, title=title)) 

154 except (ValueError, KeyError): 

155 return mo.md(f"**{title}** — too few overlapping observations to summarise.") 

156 

157 # A dropdown and seventeen metrics: the panel is now the same height whatever the 

158 # frame holds, so it no longer needs the scroll box that a per-column radio list 

159 # did. The wide gap keeps the split legible — the dropdown is a control, the 

160 # table under it is a result, and they should not read as one column of text. 

161 def panel(label, picker, table): 

162 """One side of the page: a heading, its picker, and the table underneath.""" 

163 return mo.vstack( 

164 [ 

165 mo.md(f"**{label}**"), 

166 mo.vstack([picker, table], gap=1.75), 

167 ], 

168 gap=0.5, 

169 ) 

170 

171 return panel, summary_table # pragma: no cover 

172 

173 

174@app.cell 

175def _(a_column, b_column, chart, frame, mo, pair, rebase): 

176 """The middle column: the switch, the chart, and a line saying what it is of.""" 

177 _dropped = frame.height - pair.height 

178 _note = f" of {frame.height:,}" if _dropped else "" 

179 caption = mo.md( 

180 # The multiplication sign is the character this means; a lowercase x beside two 

181 # column names reads as part of one of them. 

182 f"`{a_column}` × `{b_column}` — {pair.height:,}{_note} dates where both series " # noqa: RUF001 

183 "are present, which is also what the tables summarise." 

184 ) 

185 # No align= here: centring would shrink the stack to its content and hand the chart 

186 # back the gutter that "container" is there to fill. 

187 figure = mo.vstack([rebase, chart, caption], gap=0.25) 

188 return (figure,) 

189 

190 

191@app.cell 

192def _(a_column, a_pick, b_column, b_pick, figure, mo, pair, panel, summary_table) -> None: 

193 """Lay out the page: a picker and its table either side of the figure.""" 

194 mo.hstack( 

195 [ 

196 panel("left", a_pick, summary_table(pair, "a", a_column)), 

197 figure, 

198 panel("right", b_pick, summary_table(pair, "b", b_column)), 

199 ], 

200 # The side panels hold a dropdown and a two-column table; anything wider than 

201 # that is width taken off the picture. 

202 widths=[1, 6, 1], 

203 align="start", 

204 gap=0.75, 

205 ) 

206 return # pragma: no cover 

207 

208 

209if __name__ == "__main__": 

210 app.run()