briefing 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {briefing-0.2.0 → briefing-0.2.1}/CHANGELOG.md +14 -1
- {briefing-0.2.0 → briefing-0.2.1}/PKG-INFO +1 -1
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/__init__.py +1 -1
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/lab/_blocks.py +2 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/lab/_render_calendar.py +12 -4
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/lab/static/calendar.js +26 -1
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/static/report.css +16 -6
- briefing-0.2.1/tests/test_browser/test_calendar.py +147 -0
- briefing-0.2.0/tests/test_browser/test_calendar.py +0 -81
- {briefing-0.2.0 → briefing-0.2.1}/.gitignore +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/LICENSE +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/README.md +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/noxfile.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/pyproject.toml +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/__main__.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/_cli.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/_error.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/_frames.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/_preview.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/blocks/__init__.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/blocks/asset.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/blocks/banner.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/blocks/base.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/blocks/layout.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/blocks/text.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/export/__init__.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/export/_image.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/export/_pdf.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/export/_render.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/lab/__init__.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/lab/_render_datadive.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/lab/_render_profile.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/lab/static/lab.js +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/py.typed +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/renderers/__init__.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/renderers/formatting.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/renderers/formula.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/renderers/html.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/renderers/normalize.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/renderers/plot.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/renderers/registry.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/renderers/script_json.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/renderers/table.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/static/export.css +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/static/export.js +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/static/report.js +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/src/briefing/templates/report.html.j2 +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/__init__.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/banner_images.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/compat_reports.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/conftest.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/contract.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/html_helpers.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/image_helpers.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/pdf_reader.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/snapshots/compat_0_1_2/blocks.body.html +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/snapshots/compat_0_1_2/pages.body.html +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_blocks/__init__.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_blocks/test_banner.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_blocks/test_base.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_blocks/test_layout.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_blocks/test_text.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/__init__.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/conftest.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_banner.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_bignumber.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_cli_export.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_code.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_datadive.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_datatable.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_export_image.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_export_menu.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_export_pdf.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_formula.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_layout.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_notebook.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_plots.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_print.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_spacing.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_text.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_browser/test_toggle_theme.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_cli.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_compat.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_contract.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_deprecations.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_docs_gallery.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_docstrings.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_export.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_frames.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_lab/__init__.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_lab/test_calendar.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_lab/test_datadive.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_lab/test_lab_boundary.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_lab/test_profile.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_no_viz_deps.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_pdf_reader.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_preview.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_readme.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_renderers/__init__.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_renderers/test_export_menu.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_renderers/test_formula.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_renderers/test_html.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_renderers/test_normalize.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_renderers/test_plot.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_renderers/test_reproducible.py +0 -0
- {briefing-0.2.0 → briefing-0.2.1}/tests/test_renderers/test_table.py +0 -0
|
@@ -10,6 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.2.1] - 2026-11-04
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `lab.CalendarHeatmap` fits the width available to it, the report's or its `Group`
|
|
18
|
+
column's, instead of a fixed size: a year fills a full-width report, and in a column it
|
|
19
|
+
shrinks to fit instead of widening the column and overflowing the page. Cells stay
|
|
20
|
+
between 0.6× and 2× their natural 11 px (a short range stops at 2×; below 0.6× the
|
|
21
|
+
calendar scrolls inside its block); labels change size at half that rate and legend
|
|
22
|
+
swatches follow the cells. When printed it shrinks to fit the sheet instead of being cut
|
|
23
|
+
off (#106).
|
|
24
|
+
|
|
13
25
|
## [0.2.0] - 2026-11-04
|
|
14
26
|
|
|
15
27
|
### Added
|
|
@@ -199,7 +211,8 @@ First public release.
|
|
|
199
211
|
inherits its base's renderer.
|
|
200
212
|
- Ships inline type hints (`py.typed`). Python 3.11+.
|
|
201
213
|
|
|
202
|
-
[Unreleased]: https://github.com/jeanmidevacc/briefing/compare/v0.2.
|
|
214
|
+
[Unreleased]: https://github.com/jeanmidevacc/briefing/compare/v0.2.1...HEAD
|
|
215
|
+
[0.2.1]: https://github.com/jeanmidevacc/briefing/compare/v0.2.0...v0.2.1
|
|
203
216
|
[0.2.0]: https://github.com/jeanmidevacc/briefing/compare/v0.1.2...v0.2.0
|
|
204
217
|
[0.1.2]: https://github.com/jeanmidevacc/briefing/compare/v0.1.1...v0.1.2
|
|
205
218
|
[0.1.1]: https://github.com/jeanmidevacc/briefing/compare/v0.1.0...v0.1.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: briefing
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Build beautiful self-contained HTML reports from Python analysis
|
|
5
5
|
Project-URL: Homepage, https://github.com/jeanmidevacc/briefing
|
|
6
6
|
Project-URL: Documentation, https://briefing.readthedocs.io
|
|
@@ -161,6 +161,8 @@ class CalendarHeatmap(Block):
|
|
|
161
161
|
Counts the rows per day, or aggregates a value column, and colours each day.
|
|
162
162
|
Days without data look different from days with a zero; the grid runs
|
|
163
163
|
continuously across a year boundary, with month labels and a year marker.
|
|
164
|
+
The calendar fits the width available (the report or a ``Group`` column),
|
|
165
|
+
between 0.6x and 2x its natural size; below that it scrolls sideways.
|
|
164
166
|
|
|
165
167
|
Args:
|
|
166
168
|
df: The data, as any dataframe briefing accepts (pandas, polars, pyarrow…).
|
|
@@ -32,6 +32,16 @@ _WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
|
|
32
32
|
_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def _svg_open(cls: str, width: int, height: int, label: str) -> str:
|
|
36
|
+
"""Open a calendar SVG that scales with its block (#106).
|
|
37
|
+
|
|
38
|
+
``width``/``height`` are the natural size. The CSS stretches the SVG to the block's
|
|
39
|
+
width, between its natural width (``--bf-cal-w``) and twice that.
|
|
40
|
+
"""
|
|
41
|
+
return (f'<svg class="{cls}" xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" '
|
|
42
|
+
f'viewBox="0 0 {width} {height}" style="--bf-cal-w: {width}px" role="img" aria-label="{label}">')
|
|
43
|
+
|
|
44
|
+
|
|
35
45
|
@dataclass(frozen=True)
|
|
36
46
|
class _Event:
|
|
37
47
|
start: dt.date
|
|
@@ -215,8 +225,7 @@ def _days_svg(
|
|
|
215
225
|
grid_h = 7 * _STEP
|
|
216
226
|
width = _LEFT + weeks * _STEP
|
|
217
227
|
height = _TOP + grid_h + (len(lanes) * _LANE + 6 if lanes else 0)
|
|
218
|
-
parts = [
|
|
219
|
-
f'viewBox="0 0 {width} {height}" role="img" aria-label="Calendar heatmap">']
|
|
228
|
+
parts = [_svg_open("bf-cal__days", width, height, "Calendar heatmap")]
|
|
220
229
|
|
|
221
230
|
for row in (0, 2, 4): # Mon, Wed, Fri
|
|
222
231
|
name = _WEEKDAYS[(row - shift) % 7]
|
|
@@ -352,8 +361,7 @@ _HOUR_STEP = 16
|
|
|
352
361
|
def _hours_svg(block: CalendarHeatmap, data: dict[t.Any, float], colour: Callable[[float], tuple[str, str]]) -> str:
|
|
353
362
|
shift = 0 if block.week_start == "monday" else 1
|
|
354
363
|
width, height = _LEFT + 24 * _HOUR_STEP, 18 + 7 * _HOUR_STEP
|
|
355
|
-
parts = [
|
|
356
|
-
f'viewBox="0 0 {width} {height}" role="img" aria-label="Activity by weekday and hour">']
|
|
364
|
+
parts = [_svg_open("bf-cal__hours", width, height, "Activity by weekday and hour")]
|
|
357
365
|
for hour in range(0, 24, 3):
|
|
358
366
|
parts.append(f'<text class="bf-cal__month" x="{_LEFT + hour * _HOUR_STEP}" y="12">{hour:02d}</text>')
|
|
359
367
|
what = _what(block)
|
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
/* briefing lab: CalendarHeatmap on_click="rows"
|
|
1
|
+
/* briefing lab: CalendarHeatmap — sizing, and on_click="rows" (click a day or an hour to list its rows). Vanilla JS. */
|
|
2
2
|
(function () {
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
// The SVGs fit the width of their block (CSS, #106). Publish the scale k on the view: legend
|
|
6
|
+
// swatches follow the cells (--bf-cal-k), and labels change size at half the cells' rate, so
|
|
7
|
+
// they stay readable when the calendar grows or shrinks (--bf-cal-text, in the SVG's units).
|
|
8
|
+
function measure(view) {
|
|
9
|
+
var svg = view.querySelector('svg.bf-cal__days, svg.bf-cal__hours');
|
|
10
|
+
var natural = svg ? parseFloat(svg.getAttribute('width')) : 0;
|
|
11
|
+
var shown = svg ? svg.getBoundingClientRect().width : 0;
|
|
12
|
+
if (!(natural > 0 && shown > 0)) return; // a hidden tab: measured once it is shown
|
|
13
|
+
var k = shown / natural;
|
|
14
|
+
view.style.setProperty('--bf-cal-k', String(k));
|
|
15
|
+
view.style.setProperty('--bf-cal-text', String((1 + k) / (2 * k)));
|
|
16
|
+
}
|
|
17
|
+
document.querySelectorAll('.bf-calendar').forEach(function (figure) {
|
|
18
|
+
var views = figure.querySelectorAll('.bf-cal__views > .bf-select__panel');
|
|
19
|
+
views = views.length ? Array.prototype.slice.call(views) : [figure];
|
|
20
|
+
views.forEach(function (view) {
|
|
21
|
+
measure(view);
|
|
22
|
+
if (window.ResizeObserver) {
|
|
23
|
+
new ResizeObserver(function () { measure(view); }).observe(view);
|
|
24
|
+
} else {
|
|
25
|
+
window.addEventListener('resize', function () { measure(view); });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
5
30
|
document.querySelectorAll('.bf-calendar').forEach(function (figure) {
|
|
6
31
|
var dataEl = figure.querySelector('script.bf-cal__rows');
|
|
7
32
|
if (!dataEl) return;
|
|
@@ -706,13 +706,18 @@ body {
|
|
|
706
706
|
}
|
|
707
707
|
|
|
708
708
|
/* ── lab.CalendarHeatmap ─────────────────────────────────────────────────── */
|
|
709
|
-
.bf-calendar { margin: 0; }
|
|
709
|
+
.bf-calendar { margin: 0; min-width: 0; } /* may shrink inside a Group column (#106) */
|
|
710
710
|
.bf-cal__calendar + .bf-cal__calendar { margin-top: 1rem; }
|
|
711
711
|
.bf-cal__group { font-weight: 600; font-size: 0.9rem; margin-bottom: 0.25rem; }
|
|
712
712
|
.bf-cal__scroll { overflow-x: auto; }
|
|
713
|
-
.bf-cal__days, .bf-cal__hours { display: block;
|
|
714
|
-
|
|
715
|
-
|
|
713
|
+
.bf-cal__days, .bf-cal__hours { display: block; font-family: var(--bf-font);
|
|
714
|
+
/* Fit the width available (report or Group column) from 0.6x to 2x the natural size (#106);
|
|
715
|
+
below 0.6x the calendar scrolls inside its block. */
|
|
716
|
+
width: 100%; height: auto; min-width: calc(0.6 * var(--bf-cal-w)); max-width: calc(2 * var(--bf-cal-w)); }
|
|
717
|
+
/* calendar.js sets --bf-cal-text so labels change size at half the cells' rate; without it,
|
|
718
|
+
labels scale with the SVG. */
|
|
719
|
+
.bf-cal__days text, .bf-cal__hours text { fill: var(--bf-muted); font-size: calc(9px * var(--bf-cal-text, 1)); }
|
|
720
|
+
.bf-cal__days .bf-cal__year { fill: var(--bf-text); font-weight: 700; font-size: calc(10px * var(--bf-cal-text, 1)); }
|
|
716
721
|
.bf-cal__year-line { stroke: var(--bf-muted); stroke-dasharray: 2 2; }
|
|
717
722
|
.bf-cal__day--zero, .bf-cal__hour--zero { fill: var(--bf-border); }
|
|
718
723
|
.bf-cal__day--empty, .bf-cal__hour--empty { fill: none; stroke: var(--bf-border); stroke-width: 1; stroke-dasharray: 2 1.5; }
|
|
@@ -720,11 +725,13 @@ body {
|
|
|
720
725
|
.bf-cal__day--below { stroke: #dc2626; stroke-width: 1.5; stroke-dasharray: none; }
|
|
721
726
|
.bf-cal__event rect { fill: color-mix(in oklab, var(--bf-accent) 25%, transparent); stroke: var(--bf-accent); }
|
|
722
727
|
.bf-cal__event circle { fill: var(--bf-accent); }
|
|
723
|
-
.bf-cal__event text { fill: var(--bf-text); font-size: 9px; }
|
|
728
|
+
.bf-cal__event text { fill: var(--bf-text); font-size: calc(9px * var(--bf-cal-text, 1)); }
|
|
724
729
|
.bf-cal__legend { display: flex; flex-wrap: wrap; gap: 0.25rem 1.25rem; align-items: center;
|
|
725
730
|
margin-top: 0.5rem; font-size: 0.75rem; color: var(--bf-muted); }
|
|
726
731
|
.bf-cal__legend-item { display: inline-flex; align-items: center; gap: 3px; }
|
|
727
|
-
.bf-cal__swatch { display: inline-block; width: 11px
|
|
732
|
+
.bf-cal__swatch { display: inline-block; width: max(8px, 11px * var(--bf-cal-k, 1));
|
|
733
|
+
height: max(8px, 11px * var(--bf-cal-k, 1));
|
|
734
|
+
border-radius: 2px; }
|
|
728
735
|
.bf-cal__swatch--zero { background: var(--bf-border); }
|
|
729
736
|
.bf-cal__swatch--empty { border: 1px dashed var(--bf-border); }
|
|
730
737
|
.bf-cal__swatch--below { border: 1.5px solid #dc2626; }
|
|
@@ -777,4 +784,7 @@ html.bf-printing .bf-print-label {
|
|
|
777
784
|
.bf-blocks > .bf-select:only-child > .bf-select__panel + .bf-print-label { break-before: page; }
|
|
778
785
|
.bf-plot, .bf-alert, .bf-bignumber, .bf-formula, .bf-code, .bf-profile__card,
|
|
779
786
|
.bf-group:has(> .bf-bignumber) { break-inside: avoid; }
|
|
787
|
+
/* A calendar may shrink to fit the sheet instead of being clipped (#106). */
|
|
788
|
+
.bf-cal__days, .bf-cal__hours { min-width: 0; --bf-cal-text: 1; }
|
|
789
|
+
.bf-cal__swatch { width: 11px; height: 11px; }
|
|
780
790
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""lab.CalendarHeatmap as a reader sees it (#19)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import pytest
|
|
6
|
+
from playwright.sync_api import Page
|
|
7
|
+
|
|
8
|
+
import briefing as bf
|
|
9
|
+
from tests.test_browser.conftest import Viewer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parametrize("theme", ["default", "dark"])
|
|
13
|
+
def test_zero_no_data_and_below_target_are_visibly_different(viewer: Viewer, df_pipeline_log: pd.DataFrame,
|
|
14
|
+
theme: str) -> None:
|
|
15
|
+
formatting = bf.Formatting.dark() if theme == "dark" else bf.Formatting()
|
|
16
|
+
block = bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at", value="rows", expected=500)
|
|
17
|
+
page = viewer.open(bf.Briefing(block), formatting=formatting)
|
|
18
|
+
|
|
19
|
+
def look(day: str) -> dict[str, str]:
|
|
20
|
+
style: dict[str, str] = page.locator(f'rect[data-date="{day}"]').evaluate(
|
|
21
|
+
"e => { const s = getComputedStyle(e); return {fill: s.fill, stroke: s.stroke, dash: s.strokeDasharray}; }")
|
|
22
|
+
return style
|
|
23
|
+
|
|
24
|
+
background = page.evaluate("() => getComputedStyle(document.body).backgroundColor")
|
|
25
|
+
zero, empty, ok = look("2025-12-04"), look("2026-01-17"), look("2025-11-18")
|
|
26
|
+
assert zero["fill"] not in ("none", background) # a zero is a filled grey cell
|
|
27
|
+
assert empty["fill"] == "none" # no data is an outline only
|
|
28
|
+
assert look("2026-01-03")["stroke"] == "rgb(220, 38, 38)" # a failed run is flagged red
|
|
29
|
+
assert ok["stroke"] in ("none", "") # a normal day has no outline
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _year() -> pd.DataFrame:
|
|
33
|
+
"""One run a day through 2025: 53 week columns, a calendar 774 px wide at its natural size."""
|
|
34
|
+
days = pd.date_range("2025-01-01", "2025-12-31", freq="D")
|
|
35
|
+
return pd.DataFrame({"run_at": days, "rows": range(len(days))})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _width(page: Page, selector: str) -> float:
|
|
39
|
+
box = page.locator(selector).first.bounding_box()
|
|
40
|
+
assert box is not None
|
|
41
|
+
return float(box["width"])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_a_year_fills_the_width_of_its_block(viewer: Viewer) -> None:
|
|
45
|
+
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(_year(), date="run_at")))
|
|
46
|
+
assert _width(page, "svg.bf-cal__days") == pytest.approx(_width(page, ".bf-cal__scroll"), abs=1)
|
|
47
|
+
assert _width(page, 'rect[data-date="2025-06-02"]') > 14 # cells grew from their natural 11 px
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_a_short_range_grows_to_twice_its_natural_size(viewer: Viewer, df_pipeline_log: pd.DataFrame) -> None:
|
|
51
|
+
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at", views=("days", "hours"))))
|
|
52
|
+
natural = float(page.locator("svg.bf-cal__days").get_attribute("width") or 0)
|
|
53
|
+
assert _width(page, "svg.bf-cal__days") == pytest.approx(2 * natural, abs=1)
|
|
54
|
+
assert _width(page, 'rect[data-date="2026-01-05"]') == pytest.approx(22, abs=0.5)
|
|
55
|
+
page.get_by_role("tab", name="Hours").click() # a hidden view is measured once it is shown
|
|
56
|
+
natural = float(page.locator("svg.bf-cal__hours").get_attribute("width") or 0)
|
|
57
|
+
assert _width(page, "svg.bf-cal__hours") == pytest.approx(2 * natural, abs=1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_labels_grow_at_half_the_rate_and_swatches_match_cells(viewer: Viewer,
|
|
61
|
+
df_pipeline_log: pd.DataFrame) -> None:
|
|
62
|
+
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at")))
|
|
63
|
+
label = page.locator("svg.bf-cal__days text.bf-cal__weekday").first.bounding_box()
|
|
64
|
+
assert label is not None and label["height"] < 20 # 9 px text at 2x would be about 24 px tall
|
|
65
|
+
cell = _width(page, 'rect[data-date="2026-01-05"]')
|
|
66
|
+
assert _width(page, ".bf-cal__swatch:not(.bf-cal__swatch--event-day)") == pytest.approx(cell, abs=0.5)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_a_calendar_in_a_group_fits_its_column(viewer: Viewer, df_pipeline_log: pd.DataFrame) -> None:
|
|
70
|
+
year = bf.lab.CalendarHeatmap(_year(), date="run_at")
|
|
71
|
+
page = viewer.open(bf.Briefing(bf.Group(year, bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at"), columns=2)))
|
|
72
|
+
columns = page.locator(".bf-group > .bf-calendar")
|
|
73
|
+
first, second = columns.nth(0).bounding_box(), columns.nth(1).bounding_box()
|
|
74
|
+
assert first is not None and second is not None
|
|
75
|
+
assert first["width"] == pytest.approx(second["width"], abs=1) # the year no longer widens its column
|
|
76
|
+
assert _width(page, "svg.bf-cal__days") == pytest.approx(first["width"], abs=1)
|
|
77
|
+
assert _width(page, 'rect[data-date="2025-06-02"]') < 11 # shrunk to fit
|
|
78
|
+
assert page.evaluate("() => document.documentElement.scrollWidth <= window.innerWidth")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_a_narrow_screen_shrinks_then_scrolls(viewer: Viewer) -> None:
|
|
82
|
+
viewer.page.set_viewport_size({"width": 375, "height": 800})
|
|
83
|
+
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(_year(), date="run_at")))
|
|
84
|
+
assert _width(page, 'rect[data-date="2025-06-02"]') == pytest.approx(0.6 * 11, abs=0.3) # the floor
|
|
85
|
+
overflow = page.locator(".bf-cal__scroll").evaluate("e => e.scrollWidth > e.clientWidth")
|
|
86
|
+
assert overflow
|
|
87
|
+
assert page.evaluate("() => document.documentElement.scrollWidth <= window.innerWidth") # inside the block
|
|
88
|
+
label = page.locator("svg.bf-cal__days text.bf-cal__weekday").first.bounding_box()
|
|
89
|
+
assert label is not None and label["height"] > 6 # labels shrink at half the rate
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_a_year_prints_without_clipping(viewer: Viewer) -> None:
|
|
93
|
+
viewer.page.set_viewport_size({"width": 794, "height": 1123}) # A4 at 96 dpi
|
|
94
|
+
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(_year(), date="run_at")))
|
|
95
|
+
page.emulate_media(media="print")
|
|
96
|
+
page.evaluate("window.dispatchEvent(new Event('beforeprint'))")
|
|
97
|
+
scroll = page.locator(".bf-cal__scroll").bounding_box()
|
|
98
|
+
last = page.locator('rect[data-date="2025-12-31"]').bounding_box()
|
|
99
|
+
assert scroll is not None and last is not None
|
|
100
|
+
assert last["x"] + last["width"] <= scroll["x"] + scroll["width"] + 0.5
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_a_tab_switches_between_the_day_and_hour_views(viewer: Viewer, df_pipeline_log: pd.DataFrame) -> None:
|
|
104
|
+
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at", views=("days", "hours"))))
|
|
105
|
+
days, hours = page.locator("svg.bf-cal__days"), page.locator("svg.bf-cal__hours")
|
|
106
|
+
assert days.is_visible() and not hours.is_visible()
|
|
107
|
+
page.get_by_role("tab", name="Hours").click()
|
|
108
|
+
assert hours.is_visible() and not days.is_visible()
|
|
109
|
+
assert page.locator("rect.bf-cal__hour").count() == 7 * 24
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_hour_view_labels_are_small(viewer: Viewer, df_pipeline_log: pd.DataFrame) -> None:
|
|
113
|
+
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at", views=("hours",))))
|
|
114
|
+
box = page.locator("svg.bf-cal__hours text").first.bounding_box()
|
|
115
|
+
assert box is not None and box["height"] < 20 # not scaled up with the 2x grid
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_clicking_a_day_lists_its_rows(viewer: Viewer, df_pipeline_log: pd.DataFrame) -> None:
|
|
119
|
+
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at", on_click="rows")))
|
|
120
|
+
panel = page.locator(".bf-cal__panel")
|
|
121
|
+
assert not panel.is_visible()
|
|
122
|
+
page.locator('rect[data-date="2025-12-07"]').click()
|
|
123
|
+
assert panel.is_visible()
|
|
124
|
+
assert "2025-12-07" in panel.locator(".bf-cal__panel-title").inner_text()
|
|
125
|
+
assert panel.locator("tbody tr").count() == 2
|
|
126
|
+
assert panel.locator("thead th").all_inner_texts() == ["run_at", "team", "rows"]
|
|
127
|
+
panel.get_by_role("button", name="Close").click()
|
|
128
|
+
assert not panel.is_visible()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_keyboard_opens_the_rows_of_an_hour(viewer: Viewer, df_pipeline_log: pd.DataFrame) -> None:
|
|
132
|
+
block = bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at", views=("hours",), on_click="rows")
|
|
133
|
+
page = viewer.open(bf.Briefing(block))
|
|
134
|
+
page.locator('rect[data-hour="0-6"]').focus()
|
|
135
|
+
page.keyboard.press("Enter")
|
|
136
|
+
assert "Mon 06:00" in page.locator(".bf-cal__panel-title").inner_text()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_capped_cells_say_so_and_values_stay_text(viewer: Viewer) -> None:
|
|
140
|
+
df = pd.DataFrame({"run_at": [pd.Timestamp("2026-01-05 09:00")] * 120,
|
|
141
|
+
"note": ["<img src=x onerror=window.__hit=1>"] * 120})
|
|
142
|
+
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(df, date="run_at", on_click="rows")))
|
|
143
|
+
page.locator('rect[data-date="2026-01-05"]').click()
|
|
144
|
+
panel = page.locator(".bf-cal__panel")
|
|
145
|
+
assert panel.locator("tbody tr").count() == 50
|
|
146
|
+
assert "first 50 of 120 rows" in panel.inner_text()
|
|
147
|
+
assert panel.locator("img").count() == 0 and page.evaluate("() => window.__hit") is None
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"""lab.CalendarHeatmap as a reader sees it (#19)."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import pandas as pd
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
import briefing as bf
|
|
8
|
-
from tests.test_browser.conftest import Viewer
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@pytest.mark.parametrize("theme", ["default", "dark"])
|
|
12
|
-
def test_zero_no_data_and_below_target_are_visibly_different(viewer: Viewer, df_pipeline_log: pd.DataFrame,
|
|
13
|
-
theme: str) -> None:
|
|
14
|
-
formatting = bf.Formatting.dark() if theme == "dark" else bf.Formatting()
|
|
15
|
-
block = bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at", value="rows", expected=500)
|
|
16
|
-
page = viewer.open(bf.Briefing(block), formatting=formatting)
|
|
17
|
-
|
|
18
|
-
def look(day: str) -> dict[str, str]:
|
|
19
|
-
style: dict[str, str] = page.locator(f'rect[data-date="{day}"]').evaluate(
|
|
20
|
-
"e => { const s = getComputedStyle(e); return {fill: s.fill, stroke: s.stroke, dash: s.strokeDasharray}; }")
|
|
21
|
-
return style
|
|
22
|
-
|
|
23
|
-
background = page.evaluate("() => getComputedStyle(document.body).backgroundColor")
|
|
24
|
-
zero, empty, ok = look("2025-12-04"), look("2026-01-17"), look("2025-11-18")
|
|
25
|
-
assert zero["fill"] not in ("none", background) # a zero is a filled grey cell
|
|
26
|
-
assert empty["fill"] == "none" # no data is an outline only
|
|
27
|
-
assert look("2026-01-03")["stroke"] == "rgb(220, 38, 38)" # a failed run is flagged red
|
|
28
|
-
assert ok["stroke"] in ("none", "") # a normal day has no outline
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_the_calendar_fits_its_cells(viewer: Viewer, df_pipeline_log: pd.DataFrame) -> None:
|
|
32
|
-
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at")))
|
|
33
|
-
box = page.locator('rect[data-date="2026-01-05"]').bounding_box()
|
|
34
|
-
assert box is not None and box["width"] == pytest.approx(11, abs=0.5)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_a_tab_switches_between_the_day_and_hour_views(viewer: Viewer, df_pipeline_log: pd.DataFrame) -> None:
|
|
38
|
-
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at", views=("days", "hours"))))
|
|
39
|
-
days, hours = page.locator("svg.bf-cal__days"), page.locator("svg.bf-cal__hours")
|
|
40
|
-
assert days.is_visible() and not hours.is_visible()
|
|
41
|
-
page.get_by_role("tab", name="Hours").click()
|
|
42
|
-
assert hours.is_visible() and not days.is_visible()
|
|
43
|
-
assert page.locator("rect.bf-cal__hour").count() == 7 * 24
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_hour_view_labels_are_small(viewer: Viewer, df_pipeline_log: pd.DataFrame) -> None:
|
|
47
|
-
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at", views=("hours",))))
|
|
48
|
-
size = page.locator("svg.bf-cal__hours text").first.evaluate("e => getComputedStyle(e).fontSize")
|
|
49
|
-
assert size == "9px"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def test_clicking_a_day_lists_its_rows(viewer: Viewer, df_pipeline_log: pd.DataFrame) -> None:
|
|
53
|
-
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at", on_click="rows")))
|
|
54
|
-
panel = page.locator(".bf-cal__panel")
|
|
55
|
-
assert not panel.is_visible()
|
|
56
|
-
page.locator('rect[data-date="2025-12-07"]').click()
|
|
57
|
-
assert panel.is_visible()
|
|
58
|
-
assert "2025-12-07" in panel.locator(".bf-cal__panel-title").inner_text()
|
|
59
|
-
assert panel.locator("tbody tr").count() == 2
|
|
60
|
-
assert panel.locator("thead th").all_inner_texts() == ["run_at", "team", "rows"]
|
|
61
|
-
panel.get_by_role("button", name="Close").click()
|
|
62
|
-
assert not panel.is_visible()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_keyboard_opens_the_rows_of_an_hour(viewer: Viewer, df_pipeline_log: pd.DataFrame) -> None:
|
|
66
|
-
block = bf.lab.CalendarHeatmap(df_pipeline_log, date="run_at", views=("hours",), on_click="rows")
|
|
67
|
-
page = viewer.open(bf.Briefing(block))
|
|
68
|
-
page.locator('rect[data-hour="0-6"]').focus()
|
|
69
|
-
page.keyboard.press("Enter")
|
|
70
|
-
assert "Mon 06:00" in page.locator(".bf-cal__panel-title").inner_text()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def test_capped_cells_say_so_and_values_stay_text(viewer: Viewer) -> None:
|
|
74
|
-
df = pd.DataFrame({"run_at": [pd.Timestamp("2026-01-05 09:00")] * 120,
|
|
75
|
-
"note": ["<img src=x onerror=window.__hit=1>"] * 120})
|
|
76
|
-
page = viewer.open(bf.Briefing(bf.lab.CalendarHeatmap(df, date="run_at", on_click="rows")))
|
|
77
|
-
page.locator('rect[data-date="2026-01-05"]').click()
|
|
78
|
-
panel = page.locator(".bf-cal__panel")
|
|
79
|
-
assert panel.locator("tbody tr").count() == 50
|
|
80
|
-
assert "first 50 of 120 rows" in panel.inner_text()
|
|
81
|
-
assert panel.locator("img").count() == 0 and page.evaluate("() => window.__hit") is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|