@ovalbee/reportifact 0.0.1

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.
Files changed (116) hide show
  1. package/README.md +841 -0
  2. package/dist/Chart.vue_vue_type_style_index_0_lang-B4novMFC.cjs +1 -0
  3. package/dist/Chart.vue_vue_type_style_index_0_lang-BYILFTfl.js +5895 -0
  4. package/dist/ImageGallery.vue_vue_type_style_index_0_lang-BB4dG0CB.cjs +1 -0
  5. package/dist/ImageGallery.vue_vue_type_style_index_0_lang-DPY6mSdA.js +191 -0
  6. package/dist/MetricCallout.vue_vue_type_style_index_0_lang-BZbc-Wkf.js +49 -0
  7. package/dist/MetricCallout.vue_vue_type_style_index_0_lang-WpuALh8Z.cjs +1 -0
  8. package/dist/MetricCards.vue_vue_type_style_index_0_lang-CDPSu7no.cjs +1 -0
  9. package/dist/MetricCards.vue_vue_type_style_index_0_lang-CfxCCXmB.js +33 -0
  10. package/dist/MetricsTable.vue_vue_type_style_index_0_lang-BzrxoJW4.cjs +1 -0
  11. package/dist/MetricsTable.vue_vue_type_style_index_0_lang-CO9HvYfr.js +132 -0
  12. package/dist/Reportifact.vue.d.ts +40 -0
  13. package/dist/ReportifactWidgetHost-A73gQ3bT.cjs +1 -0
  14. package/dist/ReportifactWidgetHost-BjKBwCWI.js +17 -0
  15. package/dist/Timeline.vue_vue_type_style_index_0_lang-BIyC7N9b.cjs +2 -0
  16. package/dist/Timeline.vue_vue_type_style_index_0_lang-BnrgKBMh.js +2598 -0
  17. package/dist/WidgetWrapper.vue_vue_type_style_index_0_lang-BQQ4Vv5j.cjs +1 -0
  18. package/dist/WidgetWrapper.vue_vue_type_style_index_0_lang-D-oXqmI2.js +59 -0
  19. package/dist/components/TableOfContents.vue.d.ts +15 -0
  20. package/dist/composables/useDelayedLoading.d.ts +6 -0
  21. package/dist/composables/useFetch.d.ts +13 -0
  22. package/dist/customWidgets.d.ts +18 -0
  23. package/dist/entries/chart.d.ts +3 -0
  24. package/dist/entries/contracts.d.ts +3 -0
  25. package/dist/entries/figure.d.ts +1 -0
  26. package/dist/entries/host.d.ts +1 -0
  27. package/dist/entries/imageGallery.d.ts +3 -0
  28. package/dist/entries/metricCallout.d.ts +2 -0
  29. package/dist/entries/metricCards.d.ts +2 -0
  30. package/dist/entries/metricsTable.d.ts +2 -0
  31. package/dist/entries/timeline.d.ts +2 -0
  32. package/dist/index-DFz2A8yJ.js +21229 -0
  33. package/dist/index-DmTzvbys.cjs +47 -0
  34. package/dist/index.d.ts +6 -0
  35. package/dist/markdownTable-B5207r3E.js +32 -0
  36. package/dist/markdownTable-Cbn1Du3L.cjs +2 -0
  37. package/dist/mediaResolver.d.ts +18 -0
  38. package/dist/middleware.cjs +1 -0
  39. package/dist/middleware.d.ts +21 -0
  40. package/dist/middleware.js +177 -0
  41. package/dist/portable/ReportifactFigure.vue.d.ts +36 -0
  42. package/dist/portable/ReportifactWidgetHost.vue.d.ts +17 -0
  43. package/dist/portable/contracts.d.ts +54 -0
  44. package/dist/reportifact.cjs +67 -0
  45. package/dist/reportifact.d.ts +2 -0
  46. package/dist/reportifact.js +5272 -0
  47. package/dist/style.css +13 -0
  48. package/dist/useFetch-BfOAq-Kg.cjs +1 -0
  49. package/dist/useFetch-CHQm_TmB.js +35 -0
  50. package/dist/utils/createMarked.d.ts +7 -0
  51. package/dist/utils/parseIndexMd.d.ts +29 -0
  52. package/dist/utils/tfeventsParser.cjs +1 -0
  53. package/dist/utils/tfeventsParser.d.ts +21 -0
  54. package/dist/utils/tfeventsParser.js +336 -0
  55. package/dist/widgets/Card.vue.d.ts +6 -0
  56. package/dist/widgets/Chart.vue.d.ts +26 -0
  57. package/dist/widgets/HelpTooltip.vue.d.ts +5 -0
  58. package/dist/widgets/ImageGallery.vue.d.ts +26 -0
  59. package/dist/widgets/Markdown.vue.d.ts +8 -0
  60. package/dist/widgets/MetricCallout.vue.d.ts +19 -0
  61. package/dist/widgets/MetricCards.vue.d.ts +13 -0
  62. package/dist/widgets/MetricTable.vue.d.ts +6 -0
  63. package/dist/widgets/MetricsTable.vue.d.ts +17 -0
  64. package/dist/widgets/ModelClassComparison.vue.d.ts +7 -0
  65. package/dist/widgets/ModelComparisonGallery.vue.d.ts +15 -0
  66. package/dist/widgets/Notice.vue.d.ts +6 -0
  67. package/dist/widgets/ReportifactWidget.vue.d.ts +23 -0
  68. package/dist/widgets/Scope.vue.d.ts +8 -0
  69. package/dist/widgets/Table.vue.d.ts +18 -0
  70. package/dist/widgets/Tensorboard.vue.d.ts +13 -0
  71. package/dist/widgets/Timeline.vue.d.ts +21 -0
  72. package/dist/widgets/WidgetWrapper.vue.d.ts +28 -0
  73. package/dist/widgets/chart-config/barGradient.d.ts +2 -0
  74. package/dist/widgets/chart-config/barGrouped.d.ts +2 -0
  75. package/dist/widgets/chart-config/barNormalized.d.ts +2 -0
  76. package/dist/widgets/chart-config/barStacked.d.ts +2 -0
  77. package/dist/widgets/chart-config/customGauge.d.ts +2 -0
  78. package/dist/widgets/chart-config/gaugeGrade.d.ts +2 -0
  79. package/dist/widgets/chart-config/heatmap.d.ts +2 -0
  80. package/dist/widgets/chart-config/index.d.ts +4 -0
  81. package/dist/widgets/chart-config/line.d.ts +2 -0
  82. package/dist/widgets/chart-config/pie.d.ts +2 -0
  83. package/dist/widgets/chart-config/radar.d.ts +2 -0
  84. package/dist/widgets/chart-config/shared.d.ts +15 -0
  85. package/dist/widgets/chart-config/types.d.ts +96 -0
  86. package/dist/widgets/chart.cjs +1 -0
  87. package/dist/widgets/chart.d.ts +6 -0
  88. package/dist/widgets/chart.js +5 -0
  89. package/dist/widgets/chartSizing.d.ts +1 -0
  90. package/dist/widgets/contracts.cjs +1 -0
  91. package/dist/widgets/contracts.d.ts +2 -0
  92. package/dist/widgets/contracts.js +1 -0
  93. package/dist/widgets/figure.cjs +1 -0
  94. package/dist/widgets/figure.d.ts +6 -0
  95. package/dist/widgets/figure.js +84 -0
  96. package/dist/widgets/host.cjs +1 -0
  97. package/dist/widgets/host.d.ts +6 -0
  98. package/dist/widgets/host.js +5 -0
  99. package/dist/widgets/image-gallery.cjs +1 -0
  100. package/dist/widgets/image-gallery.d.ts +6 -0
  101. package/dist/widgets/image-gallery.js +5 -0
  102. package/dist/widgets/markdownTable.d.ts +4 -0
  103. package/dist/widgets/metric-callout.cjs +1 -0
  104. package/dist/widgets/metric-callout.d.ts +6 -0
  105. package/dist/widgets/metric-callout.js +5 -0
  106. package/dist/widgets/metric-cards.cjs +1 -0
  107. package/dist/widgets/metric-cards.d.ts +6 -0
  108. package/dist/widgets/metric-cards.js +5 -0
  109. package/dist/widgets/metrics-table.cjs +1 -0
  110. package/dist/widgets/metrics-table.d.ts +6 -0
  111. package/dist/widgets/metrics-table.js +5 -0
  112. package/dist/widgets/scope.d.ts +77 -0
  113. package/dist/widgets/timeline.cjs +1 -0
  114. package/dist/widgets/timeline.d.ts +6 -0
  115. package/dist/widgets/timeline.js +5 -0
  116. package/package.json +125 -0
package/README.md ADDED
@@ -0,0 +1,841 @@
1
+ # Reportifact
2
+
3
+ Vue report renderer for report assets. A report is a directory with an `index.md`
4
+ plus optional JSON, Parquet, Markdown, TensorBoard event, image, or other files
5
+ referenced from that index.
6
+
7
+ ## Widget Index
8
+
9
+ | Widget | Use For |
10
+ | --- | --- |
11
+ | `grid` | Group widgets with semantic layouts like `auto`, `2`, `4-2`. |
12
+ | `card` | Bordered Markdown panel with optional title, subtitle, and badge. |
13
+ | `notice` | Lightweight placeholder, TODO, or informational callout. |
14
+ | `metricCards` | Compact metric tiles from a Markdown table. |
15
+ | `metricRows` | Metric rows with values, descriptions, and help tooltips. |
16
+ | `metricCallout` | One highlighted metric plus explanatory Markdown. |
17
+ | `imageGallery` | JSON-backed grid of images with optional overlay layers. |
18
+ | `modelComparisonGallery` | Model-comparison image cards that open per-item previews. |
19
+ | `chart` | JSON-backed ECharts visualizations. |
20
+ | `modelClassComparison` | Compact comparison of per-class metrics across ML models. |
21
+ | `table` | Parquet table with filtering, sorting, pagination, and row clicks. |
22
+ | `timeline` | JSON-backed per-row time tracks with point markers (e.g. videos with cut points). |
23
+ | `markdown` | Render a separate Markdown file. |
24
+ | `tensorboard` | TensorBoard scalar event charts. |
25
+ | `metricsTable` | JSON-backed metrics/table visualization. |
26
+ | `scope` | Sticky control to re-render the whole report for a data scope (asset-tag value). |
27
+ | `evaluationReports` | OvalBee-only custom widget for evaluation reports linked to a model asset. |
28
+ | `modelCheckpoints` | OvalBee-only custom widget for model checkpoint downloads. |
29
+
30
+ Built-in widgets are provided by Reportifact. OvalBee-only widgets must be
31
+ registered by the OvalBee host through `customWidgets`.
32
+
33
+ ## OvalBee Custom Widgets
34
+
35
+ | Widget | Use For | Host Component |
36
+ | --- | --- | --- |
37
+ | `evaluationReports` | Evaluation report links for a model asset. | `web/src/components/reportifact/custom-widgets/EvaluationReportsWidget.vue` |
38
+ | `modelCheckpoints` | Download links for checkpoint resources on a model asset. | `web/src/components/reportifact/custom-widgets/ModelCheckpointsWidget.vue` |
39
+
40
+ ## `index.md` Shape
41
+
42
+ Top-level `# H1` headings become report sections and table-of-contents tabs.
43
+ Inside a section, use Markdown prose and `::widget` directives.
44
+
45
+ ```md
46
+ # Overview
47
+
48
+ Plain Markdown text.
49
+
50
+ ::chart
51
+ src: metrics.json
52
+ type: radar
53
+ title: Metrics
54
+ ::
55
+ ```
56
+
57
+ Do not put raw HTML, inline CSS, or `<style>` blocks in report Markdown. Add a
58
+ Reportifact widget instead. Markdown is supported inside widgets that accept a
59
+ body, such as `card`, `notice`, `metricRows`, and `metricCards`.
60
+
61
+ ## Directive Syntax
62
+
63
+ Two forms are supported:
64
+
65
+ ```md
66
+ ::widget
67
+ key: value
68
+ nested:
69
+ key: value
70
+ ::
71
+ ```
72
+
73
+ ```md
74
+ ::widget{title="Short title" layout="4-2"}
75
+ Markdown body here.
76
+ ::
77
+ ```
78
+
79
+ Values are parsed as strings, numbers, or booleans.
80
+
81
+ ## Layout
82
+
83
+ Use `::grid` to group widgets. Prefer semantic layouts over CSS strings.
84
+
85
+ ```md
86
+ ::grid{layout="4-2"}
87
+ ::card{title="A"}...::
88
+ ::card{title="B"}...::
89
+ ::card{title="C"}...::
90
+ ::card{title="D"}...::
91
+ ::
92
+ ```
93
+
94
+ Layouts:
95
+
96
+ | Layout | Behavior |
97
+ | --- | --- |
98
+ | `auto` | Default. Uses child count to choose a reasonable layout. |
99
+ | `1`, `2`, `3`, `4` | Fixed column intent; collapses on narrow containers. |
100
+ | `4-2` | Four columns when wide, two columns when narrow. Good for four cards. |
101
+ | `6-3-2` | Six columns wide, three medium, two narrow. |
102
+
103
+ Grid items stretch to the same row height.
104
+
105
+ A grid cell may itself be a `::grid` (one level of nesting). Use this to put a
106
+ single widget in one column and a vertical stack of widgets in the next:
107
+
108
+ ```md
109
+ ::grid{layout="2"}
110
+ ::chart
111
+ src: left.json
112
+ type: bar-gradient
113
+ ::
114
+ ::grid{layout="1"}
115
+ ::chart
116
+ src: right_a.json
117
+ type: bar-gradient
118
+ ::
119
+ ::chart
120
+ src: right_b.json
121
+ type: bar-gradient
122
+ ::
123
+ ::
124
+ ::
125
+ ```
126
+
127
+ ## Widgets
128
+
129
+ ### `card`
130
+
131
+ Generic bordered panel. Body is Markdown, including compact Markdown tables.
132
+
133
+ ```md
134
+ ::card{title="Model" surface="subtle"}
135
+ **Framework:** DEIM
136
+
137
+ **Checkpoint:** best.pth
138
+ ::
139
+ ```
140
+
141
+ Attrs:
142
+
143
+ | Attr | Values |
144
+ | --- | --- |
145
+ | `title` | Header title. |
146
+ | `subtitle` | Small header subtitle. |
147
+ | `badge` | Pill text on the right. |
148
+ | `badgeValue` | Number used to color the badge. |
149
+ | `surface` | `default`, `subtle`, `dashed`. |
150
+
151
+ ### `notice`
152
+
153
+ Lightweight callout or placeholder.
154
+
155
+ ```md
156
+ ::notice{title="Prediction gallery placeholder" variant="dashed"}
157
+ TODO: add annotated previews.
158
+ ::
159
+ ```
160
+
161
+ Attrs: `title`, `variant` (`default`, `dashed`).
162
+
163
+ ### `metricCards`
164
+
165
+ Compact metric tiles from a Markdown table.
166
+
167
+ ```md
168
+ ::metricCards
169
+ | Label | Value | Description |
170
+ | --- | ---: | --- |
171
+ | mAP | 0.2314 | COCO-style quality. |
172
+ | F1 | 0.3245 | Balance of precision and recall. |
173
+ ::
174
+ ```
175
+
176
+ Columns: `Label`, `Value`, `Description`.
177
+
178
+ ### `metricRows`
179
+
180
+ Metric rows with optional help tooltips.
181
+
182
+ ```md
183
+ ::metricRows{title="Detection Metrics" subtitle="Summary values."}
184
+ | Metric | Value | Help | Description |
185
+ | --- | ---: | --- | --- |
186
+ | Precision | 0.5385 | Correct detections / detections. | Correct predictions. |
187
+ ::
188
+ ```
189
+
190
+ Attrs: `title`, `subtitle`. Columns: `Metric`, `Value`, `Help`,
191
+ `Description`.
192
+
193
+ ### `metricCallout`
194
+
195
+ Single highlighted metric plus Markdown body.
196
+
197
+ ```md
198
+ ::metricCallout{label="Mean IoU" value="0.7782" description="Average overlap quality."}
199
+ Use this together with AP75 to inspect localization.
200
+ ::
201
+ ```
202
+
203
+ Attrs: `label`, `value`, `description`.
204
+
205
+ ### `imageGallery`
206
+
207
+ Renders a JSON-backed grid of images. Each tile has a required image layer and
208
+ an optional overlay layer.
209
+
210
+ ```md
211
+ ::imageGallery
212
+ src: image_gallery.json
213
+ overlayOpacity: 0.7
214
+ ::
215
+ ```
216
+
217
+ ```json
218
+ {
219
+ "assets": ["image-asset-id", "image-asset-id"],
220
+ "annotations": ["gt-annotation-id", "prediction-annotation-id"]
221
+ }
222
+ ```
223
+
224
+ For static report files, use `images` and `overlays` instead:
225
+
226
+ ```json
227
+ {
228
+ "images": ["gallery/image.jpg"],
229
+ "overlays": ["gallery/overlay.png"]
230
+ }
231
+ ```
232
+
233
+ Attrs:
234
+
235
+ | Attr | Default |
236
+ | --- | --- |
237
+ | `src` | Required JSON file. |
238
+ | `overlayOpacity` | `0.7` |
239
+ | `fit` | `contain`; use `cover` for cropped tiles. |
240
+ | `aspectRatio` | `4 / 3` |
241
+ | `drawTags` | `true`; set to `false` to render annotation overlays without tag labels (useful for object-detection galleries). |
242
+
243
+ The gallery renders four columns by default and two columns on narrow
244
+ containers. Inline preview shows the first row; when there are more asset-backed
245
+ items, Reportifact emits an `asset-preview` row-click payload for the host to
246
+ open a larger viewer.
247
+
248
+ Static URLs and report-local files work directly. `assets` and `annotations`
249
+ are host-specific refs; pass a `mediaResolver` prop to resolve them. Reportifact
250
+ does not know about OvalBee assets.
251
+
252
+ ### `modelComparisonGallery`
253
+
254
+ Renders comparison cards backed by host-resolved image assets. Each card can include
255
+ a ground-truth overlay, a short list of details, and an `asset-preview` payload
256
+ that opens when the card is activated.
257
+
258
+ ```md
259
+ ::modelComparisonGallery
260
+ src: image_comparison.json
261
+ columns: 3
262
+ previewLimit: 3
263
+ fit: cover
264
+ drawTags: false
265
+ ::
266
+ ```
267
+
268
+ ```json
269
+ {
270
+ "items": [
271
+ {
272
+ "asset": "image-asset-id",
273
+ "annotation": "gt-annotation-id",
274
+ "label": "street_0142.jpg",
275
+ "details": [
276
+ { "label": "F1 spread", "value": "0.3100" },
277
+ { "label": "Mean F1", "value": "0.6400" }
278
+ ],
279
+ "compare": {
280
+ "action": "asset-preview",
281
+ "layout": "viewer",
282
+ "mode": "side",
283
+ "sync": true,
284
+ "assets": [
285
+ {
286
+ "asset": "image-asset-id",
287
+ "layers": [{ "annotation": "gt-annotation-id", "reference": true }]
288
+ },
289
+ {
290
+ "asset": "image-asset-id",
291
+ "layers": [{ "annotation": "prediction-annotation-id" }]
292
+ }
293
+ ]
294
+ }
295
+ }
296
+ ]
297
+ }
298
+ ```
299
+
300
+ Attrs:
301
+
302
+ | Attr | Default |
303
+ | --- | --- |
304
+ | `src` | Required JSON file. |
305
+ | `overlayOpacity` | `0.7` |
306
+ | `fit` | `contain`; use `cover` for cropped cards. |
307
+ | `aspectRatio` | `4 / 3` |
308
+ | `drawTags` | `true` |
309
+ | `columns` | `4`; clamped to `1`–`6`. |
310
+ | `previewLimit` | `4`; clamped to `1`–`24`. |
311
+
312
+ Only visible cards resolve thumbnails. Activating a card emits its `compare`
313
+ payload. **Show all** emits a collection with `click: "comparison"`, preserving
314
+ each item's nested comparison payload. Missing annotations render the base
315
+ image without an overlay.
316
+
317
+ ### `chart`
318
+
319
+ Renders a JSON chart file with ECharts.
320
+
321
+ ```md
322
+ ::chart
323
+ src: key_metrics.json
324
+ type: radar
325
+ title: Key Metrics
326
+ height: 420
327
+ ::
328
+ ```
329
+
330
+ Attrs:
331
+
332
+ | Attr | Values |
333
+ | --- | --- |
334
+ | `src` | JSON file in the report directory. |
335
+ | `type` | `pie`, `line`, `radar`, `bar-normalized`, `bar-gradient`, `bar-grouped`, `bar-stacked`, `heatmap`, `custom-gauge`, `gauge-grade`. |
336
+ | `title` | Chart title override. |
337
+ | `height` | CSS length or number in px. |
338
+ | `radius` | For `pie`; a single value (`"50%"`) renders a solid pie, a comma-separated pair (`"46%,70%"`) renders a donut ring. |
339
+ | `smooth` | For line-like charts; `false` disables smoothing. |
340
+ | `horizontal` | For `bar-gradient`; `true` lays the bars out horizontally (categories on the y-axis). |
341
+
342
+ Charts are full-width by default.
343
+
344
+ Chart data can attach host actions with explicit click IDs. Put `clickId` on
345
+ the rendered data point and map it to an inline payload or report-local JSON
346
+ file in `click.payloads`.
347
+
348
+ ```json
349
+ {
350
+ "xAxis": ["horse"],
351
+ "series": [
352
+ {
353
+ "name": "FP",
354
+ "data": [{ "value": 42, "clickId": "outcome.fp.horse" }]
355
+ }
356
+ ],
357
+ "click": {
358
+ "version": 2,
359
+ "payloads": {
360
+ "outcome.fp.horse": { "src": "outcome_fp_horse_click.json" }
361
+ }
362
+ }
363
+ }
364
+ ```
365
+
366
+ Pie charts may use the same click contract by wrapping points in a top-level
367
+ `data` field:
368
+
369
+ ```json
370
+ {
371
+ "data": [{ "name": "Annotated", "value": 12, "clickId": "coverage.annotated" }],
372
+ "click": {
373
+ "version": 2,
374
+ "payloads": {
375
+ "coverage.annotated": { "payload": { "action": "asset-preview", "layout": "collection", "assets": [] } }
376
+ }
377
+ }
378
+ }
379
+ ```
380
+
381
+ For external payloads, Reportifact emits `{ "action": "load-report-payload", "src": "..." }`.
382
+ The host resolves that report-local JSON file, then handles the loaded payload
383
+ the same way it handles table row-click payloads.
384
+
385
+ ### `modelClassComparison`
386
+
387
+ Compares per-class metrics across two or more ML models on a shared horizontal
388
+ scale. The first metric is active initially, and a compact switcher selects
389
+ another metric.
390
+
391
+ ```md
392
+ ::modelClassComparison
393
+ src: per_class_matrix.json
394
+ ::
395
+ ```
396
+
397
+ ```json
398
+ {
399
+ "min": 0,
400
+ "max": 1,
401
+ "models": [
402
+ { "id": "report-a", "name": "Detector A", "color": "#0072B2" },
403
+ { "id": "report-b", "name": "Detector B", "color": "#E69F00" }
404
+ ],
405
+ "metrics": [
406
+ { "key": "ap", "label": "AP" },
407
+ { "key": "f1", "label": "F1" }
408
+ ],
409
+ "classes": [
410
+ {
411
+ "name": "car",
412
+ "values": {
413
+ "report-a": { "ap": 0.82, "f1": 0.78 },
414
+ "report-b": { "ap": 0.76, "f1": 0.74 }
415
+ }
416
+ }
417
+ ]
418
+ }
419
+ ```
420
+
421
+ Model IDs and metric keys must be unique. Colors must be six-digit hex values.
422
+ Models use circular markers distinguished by color. Overlapping markers collapse
423
+ into an accessible cluster that exposes each exact value on hover, focus, or tap.
424
+ With up to eight classes the full matrix is shown. Larger matrices add search
425
+ and ten-row pagination. Rows are sorted alphabetically, and malformed rows
426
+ without a comparable metric are ignored.
427
+
428
+ ### `table`
429
+
430
+ Server-backed Parquet table with filtering, sorting, pagination, and optional
431
+ row clicks.
432
+
433
+ ```md
434
+ ::table
435
+ src: per_image_metrics.parquet
436
+ pageSize: 20
437
+ fixFirstColumn: true
438
+ ::
439
+ ```
440
+
441
+ Attrs:
442
+
443
+ | Attr | Default |
444
+ | --- | --- |
445
+ | `src` | Required Parquet file. |
446
+ | `pageSize` | `10` |
447
+ | `fixFirstColumn` | `true` |
448
+ | `hideHeader` | `false` |
449
+ | `columnTooltips` | Optional map of column name to tooltip text. |
450
+
451
+ Row clicks: include a `$row_click` column in the Parquet row. The value should
452
+ be a JSON string or object. Reportifact emits it through `row-click`. Columns
453
+ whose names start with `$` are treated as system columns and are hidden from the
454
+ rendered table.
455
+
456
+ Common OvalBee payload — the single host contract is `action: "asset-preview"`
457
+ (see `.agents/spec/asset-preview.md` for the full contract). It expresses a
458
+ direct single-asset preview, a synchronized side-by-side comparison, and a
459
+ browsable collection grid in one shape via `assets[]` and per-asset `layers[]`:
460
+
461
+ ```json
462
+ {
463
+ "action": "asset-preview",
464
+ "layout": "viewer",
465
+ "mode": "side",
466
+ "sync": true,
467
+ "assets": [
468
+ { "asset": "asset-id-1", "label": "Ground Truth", "layers": [{ "annotation": "gt-ann-id", "reference": true }] },
469
+ { "asset": "asset-id-2", "label": "Prediction", "layers": [{ "mask": "mask-id-1" }] }
470
+ ]
471
+ }
472
+ ```
473
+
474
+ A layer's overlay comes from `annotation` (server-rendered from an OvalBee
475
+ annotation asset), `mask` (a ready overlay asset), or `maskUrl` (a ready overlay
476
+ url); a layer may be missing entirely for a pane with no overlay (e.g. a
477
+ negative image with no ground-truth annotation).
478
+
479
+ `imageGallery` emits an `asset-preview` collection payload when the user clicks
480
+ its show-all button:
481
+
482
+ ```json
483
+ {
484
+ "action": "asset-preview",
485
+ "layout": "collection",
486
+ "title": "Images",
487
+ "click": "preview",
488
+ "grid": 4,
489
+ "assets": [
490
+ { "asset": "asset-id-1", "layers": [{ "annotation": "annotation-id-1" }] },
491
+ { "asset": "asset-id-2", "layers": [{ "annotation": "annotation-id-2" }] }
492
+ ]
493
+ }
494
+ ```
495
+
496
+ For large media grids or chart clicks, each tile can carry its own crop and a
497
+ nested `compare` drill-down (a full `asset-preview` payload opened when that
498
+ tile is clicked and `click: "comparison"`):
499
+
500
+ ```json
501
+ {
502
+ "action": "asset-preview",
503
+ "layout": "collection",
504
+ "title": "False positives: horse",
505
+ "click": "comparison",
506
+ "assets": [
507
+ {
508
+ "asset": "image-asset-id",
509
+ "crop": [10, 20, 120, 80],
510
+ "layers": [{ "annotation": "prediction-annotation-id" }],
511
+ "compare": {
512
+ "action": "asset-preview",
513
+ "layout": "viewer",
514
+ "mode": "side",
515
+ "sync": true,
516
+ "grid": 2,
517
+ "assets": [
518
+ { "asset": "image-asset-id", "crop": [10, 20, 120, 80], "layers": [{ "annotation": "gt-annotation-id", "reference": true }] },
519
+ { "asset": "image-asset-id", "crop": [10, 20, 120, 80], "layers": [{ "annotation": "prediction-annotation-id" }] }
520
+ ]
521
+ }
522
+ }
523
+ ]
524
+ }
525
+ ```
526
+
527
+ `layout: "collection"` opens a browsable thumbnail grid first; `layout: "viewer"`
528
+ opens the viewer directly. For a table row that represents one image, use a
529
+ direct single-asset viewer payload:
530
+
531
+ ```json
532
+ {
533
+ "action": "asset-preview",
534
+ "layout": "viewer",
535
+ "assets": [
536
+ { "asset": "image-asset-id", "layers": [{ "annotation": "annotation-id", "label": "main / supervisely" }] }
537
+ ]
538
+ }
539
+ ```
540
+
541
+ Python report nodes build these payloads via the shared builder in the OvalBee
542
+ SDK (`ovalbee.report.asset_preview`: `single_asset_preview`, `compare_payload`,
543
+ `media_grid_payload`, `asset_spec`, `layer`) rather than hand-writing dicts.
544
+
545
+ ### `timeline`
546
+
547
+ Per-row horizontal time tracks with point markers, rendered with ECharts custom
548
+ series. Built for "one bar per video with cut points" visuals, but any
549
+ row/track/marker data fits.
550
+
551
+ ```md
552
+ ::timeline
553
+ src: all_videos_timeline.json
554
+ title: All-videos timeline
555
+ timeFormat: clock
556
+ ::
557
+ ```
558
+
559
+ ```json
560
+ {
561
+ "rows": [
562
+ {
563
+ "id": "asset-id",
564
+ "name": "2026_01_15_Umaker_L9",
565
+ "group": "good",
566
+ "start": 0,
567
+ "end": 248,
568
+ "cuts": [62, 131, 198]
569
+ }
570
+ ]
571
+ }
572
+ ```
573
+
574
+ Row fields: `name` and `end` are required; `start` defaults to `0`; `cuts` are
575
+ marker positions in seconds; `group` colors the track from a built-in palette
576
+ (one legend entry per group, rows without a group share one series); `color`
577
+ overrides the track color for that row; `click` is an optional payload object
578
+ emitted through `row-click` when the row's track or a marker is clicked (same
579
+ host contract as table `$row_click`, e.g. an `asset-preview` payload).
580
+
581
+ Attrs:
582
+
583
+ | Attr | Values |
584
+ | --- | --- |
585
+ | `src` | JSON file in the report directory. |
586
+ | `title` | Chart title. |
587
+ | `timeFormat` | Initial time-axis mode: `seconds` (default) or `clock` (`mm:ss`, switching to `hh:mm:ss` past one hour). A toolbar toggle lets the viewer change it. |
588
+ | `zoom` | `true` enables x-axis zooming (`inside` + slider). Off by default. |
589
+
590
+ Hovering a track shows the row name, group, duration, and its cut list;
591
+ hovering a marker shows that specific cut time. Widget height grows with the
592
+ number of rows.
593
+
594
+ ### `markdown`
595
+
596
+ Loads and renders another Markdown file from the report directory.
597
+
598
+ ```md
599
+ ::markdown
600
+ src: details.md
601
+ live: true
602
+ ::
603
+ ```
604
+
605
+ Attrs: `src`, `live` (`true` enables refresh).
606
+
607
+ ### `tensorboard`
608
+
609
+ Renders TensorBoard scalar event data.
610
+
611
+ ```md
612
+ ::tensorboard
613
+ src: events.out.tfevents
614
+ title: Training
615
+ height: 420
616
+ smooth: true
617
+ maxPoints: 1000
618
+ ::
619
+ ```
620
+
621
+ Attrs: `src`, `title`, `height`, `smooth`, `maxPoints`.
622
+
623
+ ### `metricsTable`
624
+
625
+ JSON-backed metrics/table visualization. Use this when the report already emits
626
+ the metrics JSON shape expected by the widget.
627
+
628
+ ```md
629
+ ::metricsTable
630
+ src: metrics.json
631
+ title: Metrics
632
+ show_pct: true
633
+ ::
634
+ ```
635
+
636
+ ### `scope`
637
+
638
+ Renders a sticky "Report scoped to" breadcrumb control above the section
639
+ navigation and re-renders the **whole** report for the selected data scope. A
640
+ scope is a subset of the evaluated data identified by an asset-tag value (e.g.
641
+ `split.bad_weather`); the base scope is `all` (the whole dataset).
642
+
643
+ Place the directive in the **preamble** (before the first `# H1`). It is hoisted
644
+ above the table of contents and is not rendered as a section.
645
+
646
+ ```md
647
+ ::scope
648
+ src: scopes.json
649
+ ::
650
+
651
+ # Overview
652
+ ...
653
+ ```
654
+
655
+ `src` points to a manifest describing the tags, their values, and per-scope asset
656
+ counts:
657
+
658
+ ```json
659
+ {
660
+ "all": { "n": 50 },
661
+ "tags": [
662
+ {
663
+ "key": "split",
664
+ "label": "split",
665
+ "values": [
666
+ { "value": "bad_weather", "scopeId": "split.bad_weather", "n": 24 },
667
+ { "value": "good", "scopeId": "split.good", "n": 26 }
668
+ ]
669
+ }
670
+ ]
671
+ }
672
+ ```
673
+
674
+ When a scope is active, the report updates in place (no remount):
675
+
676
+ - **File-backed widgets** (`chart`, `table`, `imageGallery`, `modelComparisonGallery`,
677
+ `tensorboard`, `metricsTable`, `modelClassComparison`) load a per-scope source:
678
+ the suffix `.<scopeId>` is inserted
679
+ before the extension, so `outcome_counts.json` becomes
680
+ `outcome_counts.split.bad_weather.json`. The base scope uses the unsuffixed
681
+ file. If a per-scope file is absent from the report, the base file is used.
682
+ - **Text** (Markdown prose, `card` bodies, `metricCards` / `metricRows` tables,
683
+ badges, titles) resolves `$name` / `${name}` placeholders from
684
+ `vars.<scopeId>.json` merged over the base `vars.json`. Authors keep
685
+ placeholders in `index.md` (instead of substituting at build time) for any
686
+ value that varies per scope; literal text stays static. Tokens with no matching
687
+ key in the vars dictionary are left untouched, so non-scoped reports
688
+ (already substituted, no `vars.json`) render unchanged.
689
+
690
+ `vars.json` is a flat dictionary, e.g.:
691
+
692
+ ```json
693
+ { "map": 55.1, "ap50": 95.9, "f1": 92.4, "n_images": 50, "n_gt": 1781 }
694
+ ```
695
+
696
+ A report without a `::scope` directive (and without `vars.json`) behaves
697
+ exactly as before.
698
+
699
+ ## Embedding
700
+
701
+ ```vue
702
+ <script setup lang="ts">
703
+ import { Reportifact } from '@ovalbee/reportifact';
704
+ </script>
705
+
706
+ <template>
707
+ <Reportifact
708
+ :url-map="urlMap"
709
+ base-path="/api/reports"
710
+ :media-resolver="resolveMedia"
711
+ :custom-widgets="customWidgets"
712
+ @row-click="onRowClick"
713
+ />
714
+ </template>
715
+ ```
716
+
717
+ Important props:
718
+
719
+ | Prop | Purpose |
720
+ | --- | --- |
721
+ | `url` | Direct URL to `index.md`. |
722
+ | `urlMap` | Map file names to signed/public URLs. Uses `index.md` for the schema. |
723
+ | `linkUrlMap` | Optional map for Markdown links. |
724
+ | `basePath` | API route base for Parquet and TensorBoard proxy routes. |
725
+ | `scrollContainer` | Scroll container used for active section tracking. |
726
+ | `refreshInterval` | Refresh interval for live report widgets. |
727
+ | `mediaResolver` | Optional resolver for gallery `assets` and `annotations`. |
728
+ | `customWidgets` | Optional Vue 3 components keyed by directive name for host-owned widgets. |
729
+
730
+ ## Portable Widgets
731
+
732
+ Sites and other Vue hosts can render selected widgets without loading the full
733
+ Markdown report runtime. Import every widget from its direct subpath; there is
734
+ intentionally no runtime `widgets` barrel because it would pull ECharts into
735
+ pages that only use metric components.
736
+
737
+ ```vue
738
+ <script setup lang="ts">
739
+ import ReportifactFigure from '@ovalbee/reportifact/widgets/figure';
740
+ import ReportifactChart from '@ovalbee/reportifact/widgets/chart';
741
+ import '@ovalbee/reportifact/style.css';
742
+
743
+ const chart = {
744
+ xAxis: ['YOLO', 'DEIM'],
745
+ data: [0.71, 0.76],
746
+ };
747
+ </script>
748
+
749
+ <template>
750
+ <ReportifactFigure
751
+ title="Validation mAP"
752
+ caption="DEIM leads on the held-out validation set."
753
+ fallback-src="/case-studies/yolo-vs-deim/map.webp"
754
+ >
755
+ <ReportifactChart type="bar-gradient" :data="chart" />
756
+ </ReportifactFigure>
757
+ </template>
758
+ ```
759
+
760
+ Public subpaths:
761
+
762
+ | Import | Component | Portable input |
763
+ | --- | --- | --- |
764
+ | `@ovalbee/reportifact/widgets/host` | `ReportifactWidgetHost` | Theme and loading-style boundary. |
765
+ | `@ovalbee/reportifact/widgets/figure` | `ReportifactFigure` | Semantic title/caption, loading, error fallback, source link. |
766
+ | `@ovalbee/reportifact/widgets/chart` | `ReportifactChart` | `src` JSON URL or inline `data`. |
767
+ | `@ovalbee/reportifact/widgets/metric-cards` | `ReportifactMetricCards` | Inline `data` or Markdown-table `content`. |
768
+ | `@ovalbee/reportifact/widgets/metric-callout` | `ReportifactMetricCallout` | Inline `data` or direct props. |
769
+ | `@ovalbee/reportifact/widgets/metrics-table` | `ReportifactMetricsTable` | `src` JSON URL or inline `data`. |
770
+ | `@ovalbee/reportifact/widgets/timeline` | `ReportifactTimeline` | `src` JSON URL or inline `data`. |
771
+ | `@ovalbee/reportifact/widgets/image-gallery` | `ReportifactImageGallery` | `src` JSON URL or inline `data`; optional media resolver. |
772
+ | `@ovalbee/reportifact/widgets/contracts` | Type-only contracts | Widget data and resolver types. |
773
+
774
+ For URL-backed widgets, `src` is the portable spelling and legacy `url` remains
775
+ supported for the full report renderer. Inline `data` takes precedence when
776
+ both are present. `ReportifactFigure` owns the shared loading and fallback
777
+ presentation; its `fallbackSrc` is also emitted as a no-JavaScript fallback.
778
+ Wrap lower-level widgets in `ReportifactWidgetHost` when they are not already
779
+ inside a `ReportifactFigure` or full `Reportifact` renderer.
780
+
781
+ `MetricCallout`'s `html` field is trusted pre-rendered HTML. Do not pass
782
+ untrusted user input without sanitizing it first.
783
+
784
+ Parquet tables, TensorBoard, scopes, and the full widget dispatcher remain
785
+ full-report features. Embed a public full report for those experiences instead
786
+ of importing their internals into a content page.
787
+
788
+ ### Custom Widgets
789
+
790
+ Hosts can register Vue 3 components for directives that Reportifact does not
791
+ own. The component is compiled into the host app bundle; the Markdown only names
792
+ the widget and provides parsed options.
793
+
794
+ ```vue
795
+ <script setup lang="ts">
796
+ import { Reportifact, type ReportifactCustomWidgets } from '@ovalbee/reportifact';
797
+ import ModelCheckpointsWidget from './custom-widgets/ModelCheckpointsWidget.vue';
798
+
799
+ const customWidgets: ReportifactCustomWidgets = {
800
+ modelCheckpoints: ModelCheckpointsWidget,
801
+ };
802
+ </script>
803
+
804
+ <template>
805
+ <Reportifact :url-map="urlMap" :custom-widgets="customWidgets" />
806
+ </template>
807
+ ```
808
+
809
+ The registered component receives:
810
+
811
+ | Prop | Purpose |
812
+ | --- | --- |
813
+ | `widget` | Parsed widget item, including `widgetType`, `content`, and `options`. |
814
+ | `url` | Resolved `src` URL, when the directive has one. |
815
+ | `options` | Parsed directive options. |
816
+ | `html` | Markdown-rendered widget body. |
817
+ | `context` | Helpers: `resolveUrl`, `resolveLinkUrl`, `mediaResolver`, `emitRowClick`. |
818
+
819
+ Custom widgets may expose `refresh()`; Reportifact calls it during manual or
820
+ interval refresh. Unknown unregistered directives render an unsupported-widget
821
+ placeholder instead of failing the report.
822
+
823
+ Example host-owned directive:
824
+
825
+ ```md
826
+ ::modelCheckpoints
827
+ assetId: 3f2504e0-4f89-11d3-9a0c-0305e82c3301
828
+ ::
829
+ ```
830
+
831
+ ## Development
832
+
833
+ ```sh
834
+ npm run build
835
+ ```
836
+
837
+ From this repo, prefer the containerized check:
838
+
839
+ ```sh
840
+ docker compose exec web sh -lc 'cd /workspace/packages/reportifact && npm run build'
841
+ ```