trellum 0.2.0__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.
- trellum-0.2.0/AGENTS.md +671 -0
- trellum-0.2.0/LICENSE +22 -0
- trellum-0.2.0/PKG-INFO +1603 -0
- trellum-0.2.0/README.md +1534 -0
- trellum-0.2.0/__init__.py +14 -0
- trellum-0.2.0/__main__.py +21 -0
- trellum-0.2.0/agent/__init__.py +18 -0
- trellum-0.2.0/agent/agentdoc.py +311 -0
- trellum-0.2.0/agent/agentsetup.py +294 -0
- trellum-0.2.0/assets.py +69 -0
- trellum-0.2.0/cli/__init__.py +167 -0
- trellum-0.2.0/cli/commands/__init__.py +6 -0
- trellum-0.2.0/cli/commands/data.py +242 -0
- trellum-0.2.0/cli/commands/doctor.py +135 -0
- trellum-0.2.0/cli/commands/guide.py +158 -0
- trellum-0.2.0/cli/commands/review.py +306 -0
- trellum-0.2.0/cli/commands/serve.py +130 -0
- trellum-0.2.0/cli/commands/setup.py +107 -0
- trellum-0.2.0/cli/commands/validate.py +43 -0
- trellum-0.2.0/cli/console.py +22 -0
- trellum-0.2.0/cli/describe.py +91 -0
- trellum-0.2.0/components/__init__.py +50 -0
- trellum-0.2.0/components/ab_compare.py +406 -0
- trellum-0.2.0/components/ab_methodology.py +113 -0
- trellum-0.2.0/components/base.py +202 -0
- trellum-0.2.0/components/charts/__init__.py +26 -0
- trellum-0.2.0/components/charts/bar.py +411 -0
- trellum-0.2.0/components/charts/base.py +120 -0
- trellum-0.2.0/components/charts/doughnut.py +89 -0
- trellum-0.2.0/components/charts/funnel.py +73 -0
- trellum-0.2.0/components/charts/heatmap.py +139 -0
- trellum-0.2.0/components/charts/line_area.py +254 -0
- trellum-0.2.0/components/charts/scatter.py +119 -0
- trellum-0.2.0/components/charts/treemap.py +83 -0
- trellum-0.2.0/components/controls.py +160 -0
- trellum-0.2.0/components/filterable.py +419 -0
- trellum-0.2.0/components/filters/__init__.py +44 -0
- trellum-0.2.0/components/filters/base.py +53 -0
- trellum-0.2.0/components/filters/date_range.py +196 -0
- trellum-0.2.0/components/filters/dropdown.py +63 -0
- trellum-0.2.0/components/filters/flag.py +38 -0
- trellum-0.2.0/components/filters/toggle.py +56 -0
- trellum-0.2.0/components/header.py +119 -0
- trellum-0.2.0/components/kpis.py +194 -0
- trellum-0.2.0/components/layout.py +234 -0
- trellum-0.2.0/components/tables.py +251 -0
- trellum-0.2.0/config.yaml +17 -0
- trellum-0.2.0/data/__init__.py +32 -0
- trellum-0.2.0/data/connections.py +123 -0
- trellum-0.2.0/data/datasource_config.py +137 -0
- trellum-0.2.0/data/drivers/__init__.py +78 -0
- trellum-0.2.0/data/drivers/bigquery.py +48 -0
- trellum-0.2.0/data/drivers/clickhouse.py +27 -0
- trellum-0.2.0/data/drivers/duckdb_driver.py +25 -0
- trellum-0.2.0/data/drivers/mysql.py +28 -0
- trellum-0.2.0/data/drivers/postgres.py +27 -0
- trellum-0.2.0/data/drivers/redshift.py +21 -0
- trellum-0.2.0/data/drivers/snowflake_driver.py +35 -0
- trellum-0.2.0/data/drivers/sqlite.py +21 -0
- trellum-0.2.0/data/drivers/sqlserver.py +32 -0
- trellum-0.2.0/data/drivers/vertica.py +47 -0
- trellum-0.2.0/data/query.py +561 -0
- trellum-0.2.0/data/resolvers.py +232 -0
- trellum-0.2.0/data/transforms.py +90 -0
- trellum-0.2.0/demo/README.md +261 -0
- trellum-0.2.0/demo/__init__.py +28 -0
- trellum-0.2.0/demo/__main__.py +156 -0
- trellum-0.2.0/demo/config.yaml +0 -0
- trellum-0.2.0/demo/data-sources/config.yaml +16 -0
- trellum-0.2.0/demo/data-sources/uploads/ua_budget.csv +325 -0
- trellum-0.2.0/demo/reports/_template/__init__.py +0 -0
- trellum-0.2.0/demo/reports/_template/generator.py +72 -0
- trellum-0.2.0/demo/reports/_template/queries.py +20 -0
- trellum-0.2.0/demo/reports/_template/report.yaml +34 -0
- trellum-0.2.0/demo/reports/cart-funnel/__init__.py +0 -0
- trellum-0.2.0/demo/reports/cart-funnel/custom_sections.py +326 -0
- trellum-0.2.0/demo/reports/cart-funnel/generator.py +159 -0
- trellum-0.2.0/demo/reports/cart-funnel/queries.py +22 -0
- trellum-0.2.0/demo/reports/cart-funnel/report.yaml +39 -0
- trellum-0.2.0/demo/reports/checkout-experiment/__init__.py +0 -0
- trellum-0.2.0/demo/reports/checkout-experiment/generator.py +113 -0
- trellum-0.2.0/demo/reports/checkout-experiment/queries.py +21 -0
- trellum-0.2.0/demo/reports/checkout-experiment/report.yaml +37 -0
- trellum-0.2.0/demo/reports/conversion/__init__.py +0 -0
- trellum-0.2.0/demo/reports/conversion/custom_sections.py +164 -0
- trellum-0.2.0/demo/reports/conversion/generator.py +174 -0
- trellum-0.2.0/demo/reports/conversion/queries.py +13 -0
- trellum-0.2.0/demo/reports/conversion/report.yaml +58 -0
- trellum-0.2.0/demo/reports/economy-firehose/__init__.py +0 -0
- trellum-0.2.0/demo/reports/economy-firehose/generator.py +227 -0
- trellum-0.2.0/demo/reports/economy-firehose/queries.py +21 -0
- trellum-0.2.0/demo/reports/economy-firehose/report.yaml +51 -0
- trellum-0.2.0/demo/reports/insert-coin/__init__.py +0 -0
- trellum-0.2.0/demo/reports/insert-coin/custom_sections.py +750 -0
- trellum-0.2.0/demo/reports/insert-coin/generator.py +165 -0
- trellum-0.2.0/demo/reports/insert-coin/queries.py +21 -0
- trellum-0.2.0/demo/reports/insert-coin/report.yaml +43 -0
- trellum-0.2.0/demo/reports/monetization/__init__.py +0 -0
- trellum-0.2.0/demo/reports/monetization/custom_sections.py +486 -0
- trellum-0.2.0/demo/reports/monetization/generator.py +242 -0
- trellum-0.2.0/demo/reports/monetization/queries.py +56 -0
- trellum-0.2.0/demo/reports/monetization/report.yaml +34 -0
- trellum-0.2.0/demo/reports/player-overview/__init__.py +0 -0
- trellum-0.2.0/demo/reports/player-overview/custom_sections.py +451 -0
- trellum-0.2.0/demo/reports/player-overview/generator.py +283 -0
- trellum-0.2.0/demo/reports/player-overview/queries.py +49 -0
- trellum-0.2.0/demo/reports/player-overview/report.yaml +49 -0
- trellum-0.2.0/demo/reports/store-health/__init__.py +0 -0
- trellum-0.2.0/demo/reports/store-health/generator.py +225 -0
- trellum-0.2.0/demo/reports/store-health/queries.py +23 -0
- trellum-0.2.0/demo/reports/store-health/report.yaml +41 -0
- trellum-0.2.0/demo/tools/__init__.py +1 -0
- trellum-0.2.0/demo/tools/list_reports.py +130 -0
- trellum-0.2.0/demo/tools/make_fixtures.py +1429 -0
- trellum-0.2.0/init/__init__.py +0 -0
- trellum-0.2.0/init/__main__.py +5 -0
- trellum-0.2.0/meta.py +68 -0
- trellum-0.2.0/new/__init__.py +0 -0
- trellum-0.2.0/new/__main__.py +5 -0
- trellum-0.2.0/output_backends/__init__.py +3 -0
- trellum-0.2.0/output_backends/backends.py +78 -0
- trellum-0.2.0/output_backends/local.py +39 -0
- trellum-0.2.0/project.py +157 -0
- trellum-0.2.0/pyproject.toml +162 -0
- trellum-0.2.0/rendering/__init__.py +5 -0
- trellum-0.2.0/rendering/artifacts.py +162 -0
- trellum-0.2.0/rendering/cdn.py +268 -0
- trellum-0.2.0/rendering/html_builder.py +481 -0
- trellum-0.2.0/rendering/js_runtime.py +95 -0
- trellum-0.2.0/rendering/sections.py +116 -0
- trellum-0.2.0/report.py +403 -0
- trellum-0.2.0/reporting/__init__.py +18 -0
- trellum-0.2.0/reporting/diagnostics/__init__.py +168 -0
- trellum-0.2.0/reporting/diagnostics/datasets.py +121 -0
- trellum-0.2.0/reporting/diagnostics/filters.py +353 -0
- trellum-0.2.0/reporting/diagnostics/types.py +63 -0
- trellum-0.2.0/reporting/diagnostics/walk.py +166 -0
- trellum-0.2.0/reporting/gallery.py +361 -0
- trellum-0.2.0/review/__init__.py +32 -0
- trellum-0.2.0/review/http.py +185 -0
- trellum-0.2.0/review/inject.py +115 -0
- trellum-0.2.0/review/state.py +216 -0
- trellum-0.2.0/run/__init__.py +0 -0
- trellum-0.2.0/run/__main__.py +5 -0
- trellum-0.2.0/runner/__init__.py +349 -0
- trellum-0.2.0/runner/console.py +28 -0
- trellum-0.2.0/runner/discovery.py +177 -0
- trellum-0.2.0/runner/events.py +263 -0
- trellum-0.2.0/runner/execute.py +410 -0
- trellum-0.2.0/runner/ports.py +244 -0
- trellum-0.2.0/runner/serve.py +244 -0
- trellum-0.2.0/scaffold/__init__.py +16 -0
- trellum-0.2.0/scaffold/init_project.py +162 -0
- trellum-0.2.0/scaffold/new_report.py +325 -0
- trellum-0.2.0/setup.cfg +4 -0
- trellum-0.2.0/static/css/base.css +297 -0
- trellum-0.2.0/static/css/components/ab_compare.css +145 -0
- trellum-0.2.0/static/css/components/ab_methodology.css +91 -0
- trellum-0.2.0/static/css/components/chart_base.css +51 -0
- trellum-0.2.0/static/css/components/data_table.css +74 -0
- trellum-0.2.0/static/css/components/date_range_filter.css +63 -0
- trellum-0.2.0/static/css/components/filter_bar.css +165 -0
- trellum-0.2.0/static/css/components/grid.css +23 -0
- trellum-0.2.0/static/css/components/header.css +153 -0
- trellum-0.2.0/static/css/components/kpi_card.css +64 -0
- trellum-0.2.0/static/css/components/pivot_table.css +25 -0
- trellum-0.2.0/static/css/components/tab_group.css +51 -0
- trellum-0.2.0/static/css/components/toggle.css +29 -0
- trellum-0.2.0/static/css/review.css +82 -0
- trellum-0.2.0/static/js/components/ab_compare.js +190 -0
- trellum-0.2.0/static/js/components/chart_base.js +672 -0
- trellum-0.2.0/static/js/components/data_source.js +5 -0
- trellum-0.2.0/static/js/components/data_table.js +212 -0
- trellum-0.2.0/static/js/components/date_range_filter.js +201 -0
- trellum-0.2.0/static/js/components/doughnut.js +80 -0
- trellum-0.2.0/static/js/components/dropdown_filter.js +186 -0
- trellum-0.2.0/static/js/components/filter_bar.js +151 -0
- trellum-0.2.0/static/js/components/flag_filter.js +30 -0
- trellum-0.2.0/static/js/components/funnel.js +98 -0
- trellum-0.2.0/static/js/components/header.js +108 -0
- trellum-0.2.0/static/js/components/heatmap.js +204 -0
- trellum-0.2.0/static/js/components/kpi_card.js +17 -0
- trellum-0.2.0/static/js/components/kpi_row.js +82 -0
- trellum-0.2.0/static/js/components/pivot_table.js +206 -0
- trellum-0.2.0/static/js/components/scatter.js +116 -0
- trellum-0.2.0/static/js/components/scoped_data_source.js +3 -0
- trellum-0.2.0/static/js/components/toggle_filter.js +35 -0
- trellum-0.2.0/static/js/components/treemap.js +140 -0
- trellum-0.2.0/static/js/data_loader.js +452 -0
- trellum-0.2.0/static/js/review/chart_target.js +180 -0
- trellum-0.2.0/static/js/review/dom.js +68 -0
- trellum-0.2.0/static/js/review/end_session.js +25 -0
- trellum-0.2.0/static/js/review/identify.js +71 -0
- trellum-0.2.0/static/js/review/keyboard.js +7 -0
- trellum-0.2.0/static/js/review/log.js +35 -0
- trellum-0.2.0/static/js/review/net.js +58 -0
- trellum-0.2.0/static/js/review/panel.js +13 -0
- trellum-0.2.0/static/js/review/popover.js +78 -0
- trellum-0.2.0/static/js/review/presence.js +14 -0
- trellum-0.2.0/static/js/review/queue.js +94 -0
- trellum-0.2.0/static/js/review/select.js +13 -0
- trellum-0.2.0/static/js/review/state.js +35 -0
- trellum-0.2.0/static/js/review/status.js +90 -0
- trellum-0.2.0/static/js/review/styles.js +6 -0
- trellum-0.2.0/static/js/runtime/aggregate.js +100 -0
- trellum-0.2.0/static/js/runtime/annotations.js +286 -0
- trellum-0.2.0/static/js/runtime/auto_refresh.js +45 -0
- trellum-0.2.0/static/js/runtime/autofill.js +40 -0
- trellum-0.2.0/static/js/runtime/chart_defaults.js +316 -0
- trellum-0.2.0/static/js/runtime/chunk_loader.js +184 -0
- trellum-0.2.0/static/js/runtime/color_registry.js +9 -0
- trellum-0.2.0/static/js/runtime/cross_filter.js +21 -0
- trellum-0.2.0/static/js/runtime/csv_download.js +18 -0
- trellum-0.2.0/static/js/runtime/export.js +62 -0
- trellum-0.2.0/static/js/runtime/filter_engine.js +460 -0
- trellum-0.2.0/static/js/runtime/formatters.js +52 -0
- trellum-0.2.0/static/js/runtime/fw_namespace.js +37 -0
- trellum-0.2.0/static/js/runtime/live_wrap.js +10 -0
- trellum-0.2.0/static/js/runtime/state.js +82 -0
- trellum-0.2.0/static/js/runtime/storage.js +5 -0
- trellum-0.2.0/static/js/runtime/theme.js +136 -0
- trellum-0.2.0/static/js/runtime/ui_handlers.js +152 -0
- trellum-0.2.0/static/js/runtime/url_sync.js +249 -0
- trellum-0.2.0/static/vendor/MANIFEST.json +140 -0
- trellum-0.2.0/static/vendor/UPDATING.md +64 -0
- trellum-0.2.0/static/vendor/chart.umd.min.js +14 -0
- trellum-0.2.0/static/vendor/chartjs-chart-funnel.umd.min.js +16 -0
- trellum-0.2.0/static/vendor/chartjs-chart-geo.umd.min.js +2 -0
- trellum-0.2.0/static/vendor/chartjs-chart-matrix.min.js +8 -0
- trellum-0.2.0/static/vendor/chartjs-chart-sankey.min.js +7 -0
- trellum-0.2.0/static/vendor/chartjs-chart-treemap.min.js +8 -0
- trellum-0.2.0/static/vendor/chartjs-chart-venn.umd.min.js +2 -0
- trellum-0.2.0/static/vendor/chartjs-plugin-annotation.min.js +7 -0
- trellum-0.2.0/static/vendor/chartjs-plugin-datalabels.min.js +7 -0
- trellum-0.2.0/static/vendor/chartjs-plugin-zoom.min.js +7 -0
- trellum-0.2.0/static/vendor/countries-110m.json +1 -0
- trellum-0.2.0/static/vendor/fonts/inter-400.ttf +0 -0
- trellum-0.2.0/static/vendor/fonts/inter-500.ttf +0 -0
- trellum-0.2.0/static/vendor/fonts/inter-600.ttf +0 -0
- trellum-0.2.0/static/vendor/fonts/inter-700.ttf +0 -0
- trellum-0.2.0/static/vendor/hammer.min.js +7 -0
- trellum-0.2.0/static/vendor/html2canvas.min.js +20 -0
- trellum-0.2.0/static/vendor/inter.css +31 -0
- trellum-0.2.0/static/vendor/jspdf.umd.min.js +398 -0
- trellum-0.2.0/static/vendor/nouislider.min.css +1 -0
- trellum-0.2.0/static/vendor/nouislider.min.js +1 -0
- trellum-0.2.0/static/vendor/plotly.min.js +8 -0
- trellum-0.2.0/static/vendor/slimselect.css +1 -0
- trellum-0.2.0/static/vendor/slimselect.min.js +1 -0
- trellum-0.2.0/static/vendor/topojson-client.min.js +2 -0
- trellum-0.2.0/stats/__init__.py +75 -0
- trellum-0.2.0/stats/ab/__init__.py +64 -0
- trellum-0.2.0/stats/ab/aggregate.py +250 -0
- trellum-0.2.0/stats/ab/modes.py +504 -0
- trellum-0.2.0/stats/ab/users.py +292 -0
- trellum-0.2.0/stats/bootstrap.py +86 -0
- trellum-0.2.0/stats/cuped.py +78 -0
- trellum-0.2.0/stats/winsor.py +34 -0
- trellum-0.2.0/testing/__init__.py +14 -0
- trellum-0.2.0/testing/conftest.py +86 -0
- trellum-0.2.0/testing/mock_data.py +292 -0
- trellum-0.2.0/testing/runner.py +711 -0
- trellum-0.2.0/testing/test_ab_frontdoor.py +208 -0
- trellum-0.2.0/testing/test_agentdoc.py +490 -0
- trellum-0.2.0/testing/test_cli_query.py +153 -0
- trellum-0.2.0/testing/test_compatibility.py +466 -0
- trellum-0.2.0/testing/test_components.py +745 -0
- trellum-0.2.0/testing/test_connections.py +1306 -0
- trellum-0.2.0/testing/test_demo_journey.py +124 -0
- trellum-0.2.0/testing/test_gallery.py +189 -0
- trellum-0.2.0/testing/test_interactions.py +467 -0
- trellum-0.2.0/testing/test_js_runtime.py +820 -0
- trellum-0.2.0/testing/test_module_size.py +150 -0
- trellum-0.2.0/testing/test_performance.py +797 -0
- trellum-0.2.0/testing/test_portable_output.py +95 -0
- trellum-0.2.0/testing/test_review.py +801 -0
- trellum-0.2.0/testing/test_stats.py +383 -0
- trellum-0.2.0/testing/test_validation.py +354 -0
- trellum-0.2.0/testing/visual_regression.py +161 -0
- trellum-0.2.0/themes/__init__.py +76 -0
- trellum-0.2.0/themes/blossom.py +35 -0
- trellum-0.2.0/themes/classic.py +74 -0
- trellum-0.2.0/themes/dark.py +36 -0
- trellum-0.2.0/themes/default.py +3 -0
- trellum-0.2.0/themes/dracula.py +35 -0
- trellum-0.2.0/themes/midnight.py +35 -0
- trellum-0.2.0/themes/money.py +35 -0
- trellum-0.2.0/themes/monokai.py +35 -0
- trellum-0.2.0/themes/nord.py +35 -0
- trellum-0.2.0/themes/ocean.py +35 -0
- trellum-0.2.0/themes/solarized.py +35 -0
- trellum-0.2.0/themes/sunset.py +35 -0
- trellum-0.2.0/themes/theme.py +85 -0
- trellum-0.2.0/trellum.egg-info/PKG-INFO +1603 -0
- trellum-0.2.0/trellum.egg-info/SOURCES.txt +614 -0
- trellum-0.2.0/trellum.egg-info/dependency_links.txt +1 -0
- trellum-0.2.0/trellum.egg-info/entry_points.txt +2 -0
- trellum-0.2.0/trellum.egg-info/requires.txt +20 -0
- trellum-0.2.0/trellum.egg-info/top_level.txt +1 -0
- trellum-0.2.0/validation/__init__.py +123 -0
- trellum-0.2.0/validation/checks/__init__.py +6 -0
- trellum-0.2.0/validation/checks/annotations.py +113 -0
- trellum-0.2.0/validation/checks/columns.py +387 -0
- trellum-0.2.0/validation/checks/datasource.py +357 -0
- trellum-0.2.0/validation/checks/effectiveness.py +157 -0
- trellum-0.2.0/validation/checks/rawhtml.py +195 -0
- trellum-0.2.0/validation/checks/scopes.py +28 -0
- trellum-0.2.0/validation/checks/structural.py +87 -0
- trellum-0.2.0/validation/checks/theme.py +159 -0
- trellum-0.2.0/validation/checks/visibility.py +121 -0
- trellum-0.2.0/validation/checks/yaml_schema.py +50 -0
- trellum-0.2.0/validation/result.py +214 -0
- trellum-0.2.0/validation/walk.py +138 -0
trellum-0.2.0/AGENTS.md
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
# trellum -- Agent Workflow Guide
|
|
2
|
+
|
|
3
|
+
This is the **workflow guide** for AI assistants (Claude Code, Cursor, Codex)
|
|
4
|
+
building reports with this framework. It is the only document you need resident.
|
|
5
|
+
|
|
6
|
+
`README.md` is written for people, and it is long. **Do not load it wholesale.**
|
|
7
|
+
Ask for the part you need instead:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
trellum what the framework is, in ~30 lines
|
|
11
|
+
trellum guide <topic> one section of depth, on demand
|
|
12
|
+
trellum checks every validator check id and its level
|
|
13
|
+
trellum validate <dir> the current validation state of a built report
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`trellum guide` topics: `queries`, `format`, `generator`, `components`,
|
|
17
|
+
`filters`, `rawhtml`, `validation`, `report-yaml`, `themes`, `review`.
|
|
18
|
+
|
|
19
|
+
**Why this shape.** Your whole context is re-read on every API round-trip, so a
|
|
20
|
+
large always-loaded reference is paid for dozens of times per task. A command is
|
|
21
|
+
paid for once, when it is actually needed. Reach for `trellum guide` freely —
|
|
22
|
+
it is cheaper than it looks, and far cheaper than guessing.
|
|
23
|
+
|
|
24
|
+
## Where to look, by question
|
|
25
|
+
|
|
26
|
+
| Question | Answer |
|
|
27
|
+
|---|---|
|
|
28
|
+
| What components exist? | `trellum` — the bare command lists all of them |
|
|
29
|
+
| What does this validator check id mean? | `trellum checks`, then `trellum guide validation` |
|
|
30
|
+
| How do I filter a chart? | `trellum guide filters` |
|
|
31
|
+
| Why is my chart empty / not reacting? | `trellum guide format` — it is almost always wide-vs-long |
|
|
32
|
+
| Where does aggregation belong? | `trellum guide queries` — pandas, not SQL |
|
|
33
|
+
| What goes in report.yaml? | `trellum guide report-yaml` |
|
|
34
|
+
| Everything else | `README.md`, read the relevant section only |
|
|
35
|
+
|
|
36
|
+
**Before writing a generator, read `trellum guide queries` and
|
|
37
|
+
`trellum guide format`.** Those two rules are not guessable from the API, and
|
|
38
|
+
getting them wrong produces a report that builds cleanly and displays nothing
|
|
39
|
+
useful.
|
|
40
|
+
|
|
41
|
+
## When to use the framework
|
|
42
|
+
|
|
43
|
+
| Goal | Where it goes |
|
|
44
|
+
|---|---|
|
|
45
|
+
| Recurring, interactive report — served standalone, on a schedule, or embedded in another application | `reports/{slug}/` (the framework) |
|
|
46
|
+
| One-off data exploration, "what was X yesterday", quick prototyping | A plain script or notebook — the framework's structure buys nothing for throwaway analysis |
|
|
47
|
+
|
|
48
|
+
Scaffold a new report: `python3 -m trellum.new my-report --studio my-studio --category Revenue`. Build it: `python3 -m trellum.run reports/my-report --no-serve`.
|
|
49
|
+
|
|
50
|
+
**Always pass `--no-serve` unless you have been asked to open the report in a
|
|
51
|
+
browser.** Without it the command starts a preview server on :8050 and never
|
|
52
|
+
exits — the build itself finishes in under a second, but the process stays in
|
|
53
|
+
the foreground until it is killed. Build first, then serve as a separate step
|
|
54
|
+
if a human is going to look at it.
|
|
55
|
+
|
|
56
|
+
**Read files with your file-reading tool, not with `cat`, `ls` or `grep` in a
|
|
57
|
+
shell.** Every shell command starts a process; your native read, glob and search
|
|
58
|
+
tools do not. On the machine these runs were measured on a bare `ls` cost about
|
|
59
|
+
twelve seconds and an identical `Read` cost none — that gap is environment-
|
|
60
|
+
specific, but its direction never is.
|
|
61
|
+
|
|
62
|
+
The trap is that batching looks cheaper: `cat a.py; cat b.py; cat c.py` is one
|
|
63
|
+
command where three reads are three calls. Under a per-command cost that
|
|
64
|
+
reasoning is right, and here it is exactly backwards — three native reads are
|
|
65
|
+
free and the one shell command is not. Use the shell for things that genuinely
|
|
66
|
+
need it: running the build, the validator, git.
|
|
67
|
+
|
|
68
|
+
When you do serve, two things are not guessable and both have cost real time:
|
|
69
|
+
|
|
70
|
+
- **A single report is served at `/`, not at `/<slug>`.** Only `--all --serve`
|
|
71
|
+
puts an index at `/` with reports beneath it. `/<slug>` on a single-report
|
|
72
|
+
server returns an error page.
|
|
73
|
+
- **Wait for the `Serving at ...` line before opening the URL.** `--serve`
|
|
74
|
+
rebuilds the report and binds the port last, so until that line appears the
|
|
75
|
+
URL may still be answered by a previous server — showing a different report
|
|
76
|
+
rather than failing, which is far more confusing than a refused connection.
|
|
77
|
+
|
|
78
|
+
<!-- topic: queries -->
|
|
79
|
+
## How to write queries (MANDATORY — read before touching `queries.py`)
|
|
80
|
+
|
|
81
|
+
**Exploring the data first?** `python -m trellum query "SELECT ..."` runs
|
|
82
|
+
ad-hoc SQL against a configured source by NAME — never hunt for the database
|
|
83
|
+
file or hardcode its path, both of which break the day the source is a
|
|
84
|
+
remote warehouse. `--param day=2026-06-15` binds `:day`; sqlite sources are
|
|
85
|
+
opened read-only.
|
|
86
|
+
|
|
87
|
+
**The rule for report queries:** SQL pulls raw rows with a date filter.
|
|
88
|
+
Python does the aggregation. Never both in the same query.
|
|
89
|
+
|
|
90
|
+
```sql
|
|
91
|
+
-- ✓ RIGHT — minimal SQL: date filter + column projection, no aggregation
|
|
92
|
+
SELECT event_date, dim_a, dim_b, metric_col
|
|
93
|
+
FROM <schema>.<fact_table>
|
|
94
|
+
WHERE event_date BETWEEN DATE :start_date AND DATE :end_date
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
# ✓ RIGHT — aggregate in the generator with pandas
|
|
99
|
+
df_raw = query_df(conn, queries.MY_QUERY, params={...})
|
|
100
|
+
daily = df_raw.groupby("event_date")["metric_col"].sum().reset_index()
|
|
101
|
+
split = df_raw.groupby(["event_date", "dim_a"])["metric_col"].sum().reset_index()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```sql
|
|
105
|
+
-- ✗ WRONG — SQL-side GROUP BY on a fact table
|
|
106
|
+
SELECT event_date, dim_a, SUM(metric_col)
|
|
107
|
+
FROM <schema>.<fact_table>
|
|
108
|
+
WHERE event_date BETWEEN ... GROUP BY 1, 2
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Two reasons this rule is non-negotiable:
|
|
112
|
+
|
|
113
|
+
1. **Memory footprint.** A `GROUP BY + SUM` on a wide fact table makes
|
|
114
|
+
the database pre-allocate several GB for the hash aggregate, and
|
|
115
|
+
local-dev user accounts frequently run under tight resource-pool
|
|
116
|
+
caps. Raw-row retrieval with a date filter stays small — a typical
|
|
117
|
+
30-day window of a fact table fits in tens of MB of pandas.
|
|
118
|
+
2. **Dynamic filtering is the framework's whole point.** The
|
|
119
|
+
client-side `FilterBar` re-aggregates every time the user changes
|
|
120
|
+
a filter. Pre-aggregating in SQL collapses dimensions and breaks
|
|
121
|
+
that interactivity — the chart freezes at whatever grain the SQL
|
|
122
|
+
produced. Raw-row `DataSource`s let the client slice any dimension
|
|
123
|
+
you included.
|
|
124
|
+
|
|
125
|
+
Code smells — triggers to rewrite the query:
|
|
126
|
+
|
|
127
|
+
- `SUM()` / `AVG()` / `COUNT(DISTINCT)` in the SELECT that collapses a
|
|
128
|
+
dimension the user might want to filter by.
|
|
129
|
+
- `GROUP BY` on only a subset of the filterable dimensions. Either
|
|
130
|
+
drop the GROUP BY or group by every dim the client might filter on
|
|
131
|
+
(denormalized grain).
|
|
132
|
+
- `HAVING` — do it in pandas.
|
|
133
|
+
- `JOIN` that explodes row count to attach a dim — better as a pandas
|
|
134
|
+
`.merge()` after both sides load.
|
|
135
|
+
|
|
136
|
+
Acceptable exceptions (rare):
|
|
137
|
+
|
|
138
|
+
- Querying an already-aggregated table (one whose name signals a
|
|
139
|
+
pre-rolled grain like `*_agg_*` / `*_daily_*`): pull columns
|
|
140
|
+
directly, no further SQL aggregation.
|
|
141
|
+
- A `GROUP BY` on the union of every filterable dimension. Preserves
|
|
142
|
+
filterability but compresses duplicates. Use only if raw-row
|
|
143
|
+
retrieval genuinely returns too much data.
|
|
144
|
+
|
|
145
|
+
Project-specific table names, memory caps, and schema particulars live
|
|
146
|
+
in `project_context/chat_rules/` — the chat feature loads them at
|
|
147
|
+
runtime; human readers can look there for the concrete tables to query.
|
|
148
|
+
|
|
149
|
+
<!-- topic: format -->
|
|
150
|
+
## Long format vs wide format (MANDATORY)
|
|
151
|
+
|
|
152
|
+
**Rule:** dimensions stay in **rows**, never in **column names**. One
|
|
153
|
+
row per (date × every breakdown dim), one column per metric.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# ✓ RIGHT — long format
|
|
157
|
+
event_date | platform | spender_tier | dau | iap_revenue | ad_revenue
|
|
158
|
+
2026-04-01 | ios | Whale | 12000 | 9800 | 250
|
|
159
|
+
2026-04-01 | android | Whale | 8500 | 5400 | 180
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# ✗ WRONG — platform baked into column names ("wide format")
|
|
164
|
+
event_date | spender_tier | dau_ios | dau_android | iap_revenue_ios | iap_revenue_android | ...
|
|
165
|
+
2026-04-01 | Whale | 12000 | 8500 | 9800 | 5400 | ...
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Why long format is the default:
|
|
169
|
+
|
|
170
|
+
1. **Filter coverage.** The client filter engine matches on column
|
|
171
|
+
*values*, not column *names*. A filter on `platform = "ios"` only
|
|
172
|
+
works when there's a `platform` column carrying `"ios"` as a value.
|
|
173
|
+
Wide format makes the filter inert.
|
|
174
|
+
2. **Smaller payload.** Dictionary encoding via `_serialize_columnar`
|
|
175
|
+
compresses high-repetition columns (e.g. `platform` with 8 unique
|
|
176
|
+
values across 50 k rows) to integer indices — typically 60–70 %
|
|
177
|
+
smaller than the wide equivalent with one column per platform.
|
|
178
|
+
3. **Auto-discovery.** Adding a new platform just adds rows to the
|
|
179
|
+
data; charts using `stack_by="platform"` pick it up. Wide format
|
|
180
|
+
forces editing every chart's `y=[col_a, col_b, ...]` list.
|
|
181
|
+
4. **Simpler SQL.** `GROUP BY event_date, platform, ...` instead of
|
|
182
|
+
one `SUM(CASE WHEN platform='ios' THEN dau END) AS dau_ios` per
|
|
183
|
+
metric per platform.
|
|
184
|
+
|
|
185
|
+
Render breakdowns with `stack_by`:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
# Long format → one line per platform via stack_by
|
|
189
|
+
LineChart(df=df, x="event_date", y="iap_revenue",
|
|
190
|
+
stack_by="platform", dataset_id=ds, y_format="currency",
|
|
191
|
+
title="IAP Revenue by Platform")
|
|
192
|
+
|
|
193
|
+
# Long format + ratio → one line per platform, computing iap/dau per (date, platform)
|
|
194
|
+
LineChart(df=df, x="event_date", dataset_id=ds,
|
|
195
|
+
ratios=[{"numerator": "iap_revenue", "denominator": "dau",
|
|
196
|
+
"label": "IAP ARPDAU"}],
|
|
197
|
+
stack_by="platform", y_format="currency",
|
|
198
|
+
title="ARPDAU (IAP) by Platform")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The `chart-filter-coverage` validator catches accidental wide-pivots —
|
|
202
|
+
when a chart's DataSource cannot react to a FilterBar filter because
|
|
203
|
+
the dimension was collapsed into column names. Long format is the
|
|
204
|
+
fix; suppression is reserved for charts that are intentionally a
|
|
205
|
+
fixed rollup (e.g. period-over-period reference rollups).
|
|
206
|
+
|
|
207
|
+
<!-- topic: generator -->
|
|
208
|
+
## The 5-step process to write generator.py
|
|
209
|
+
|
|
210
|
+
1. Subclass `BaseReport` and implement `generate(self, ctx)`.
|
|
211
|
+
2. `conn = ctx.get_connection("primary_warehouse")` (or whatever name the project uses in `data-sources/config.yaml`); query with `query_df(conn, queries.X, params={...})`.
|
|
212
|
+
3. Wrap each DataFrame in a `DataSource` + a `FilterBar` placed together in an **untitled section**: `ctx.add_section("", [DataSource(...), FilterBar(...)])`. Untitled is required for sticky positioning.
|
|
213
|
+
4. Add content sections: `ctx.add_section(title, [...])`.
|
|
214
|
+
5. **Every chart, KPI, and table component MUST carry `dataset_id="..."`** pointing to its DataSource. Without it the component renders statically and silently ignores filters — this is the most common mistake.
|
|
215
|
+
|
|
216
|
+
To copy from, list what this project actually has (`ls reports/`) and open one.
|
|
217
|
+
Report names are not named here on purpose: this file ships with the framework
|
|
218
|
+
and travels into every project, so any slug written down is a report somebody
|
|
219
|
+
else has. A measured run followed three such names and got three "file does not
|
|
220
|
+
exist" errors before it thought to look.
|
|
221
|
+
|
|
222
|
+
<!-- topic: report-yaml -->
|
|
223
|
+
## Write the report.yaml description for discoverability (MANDATORY)
|
|
224
|
+
|
|
225
|
+
A search or assistant layer routes user questions to reports using
|
|
226
|
+
the `description` and `tags` in `report.yaml`, plus the dataset columns it
|
|
227
|
+
indexes from the built output. A vague description ("Revenue report") makes
|
|
228
|
+
the report invisible to it. The description MUST state:
|
|
229
|
+
|
|
230
|
+
1. **Which business questions the report answers** ("how many payers churn
|
|
231
|
+
per week and why"), not just its topic.
|
|
232
|
+
2. **The key metrics and dimensions** it carries (churn rate, revenue at
|
|
233
|
+
risk; split by platform/tier/country).
|
|
234
|
+
3. **Grain and freshness** (weekly cohorts; daily; refreshes every 5 min).
|
|
235
|
+
|
|
236
|
+
The `report-description-weak` validator check WARNs on short descriptions.
|
|
237
|
+
Descriptive column names in DataSources matter for the same reason — the
|
|
238
|
+
assistant reads them from data.json to decide which dataset answers a
|
|
239
|
+
question.
|
|
240
|
+
|
|
241
|
+
<!-- topic: components -->
|
|
242
|
+
## Component Reuse Policy
|
|
243
|
+
|
|
244
|
+
**Before writing any custom HTML, CSS, or JS, name the framework component you ruled out and why.** Most needs are already solved.
|
|
245
|
+
|
|
246
|
+
| Need | Required component |
|
|
247
|
+
|---|---|
|
|
248
|
+
| Filters / dropdowns / sticky filter bar | `DataSource` + `FilterBar` |
|
|
249
|
+
| Section-local filter on same data (no propagate_to wiring) | `ScopedDataSource(id, parent=...)` + section-scoped `FilterBar` |
|
|
250
|
+
| Cascading dropdowns (parent → child option narrowing) | Add `depends_on: "parent_col"` to a dropdown filter spec |
|
|
251
|
+
| KPI cards | `KpiRow(dataset_id=...)` (`agg`: `sum` / `ratio` / `count` / `abssum`) |
|
|
252
|
+
| Time series | `LineChart` |
|
|
253
|
+
| Bar / stacked bar | `BarChart` / `StackedBar` |
|
|
254
|
+
| Area / stacked area | `AreaChart` (`stacked=True`) |
|
|
255
|
+
| Doughnut / pie | `DoughnutChart` |
|
|
256
|
+
| Heatmap / day×hour / cohort grid | `HeatmapChart` |
|
|
257
|
+
| Funnel | `FunnelChart` |
|
|
258
|
+
| Treemap | `TreemapChart` |
|
|
259
|
+
| Scatter / correlation / bubble | `ScatterChart` |
|
|
260
|
+
| Dual-axis bar + line | `ComboChart` |
|
|
261
|
+
| Ratio metrics (ARPDAU, ARPPU, retention, conversion) | `LineChart(ratios=[{numerator, denominator, label}])` — never hand-roll the division in JS |
|
|
262
|
+
| Ratio by category (CPD per comfort_zone, ARPPU by country) | `BarChart(ratios=[...], horizontal=True, sort='desc')` — categorical ratio bars |
|
|
263
|
+
| Tables | `DataTable`, `ComparisonTable`, `PivotTable` |
|
|
264
|
+
| Layout | `Grid`, `Panel`, `SplitPane`, `TabGroup` |
|
|
265
|
+
| Card-panel grid | `Grid(card=True)` |
|
|
266
|
+
| Cross-grain filter sync | `FilterBar(propagate_to={target_ds: {src_col: tgt_col}})` |
|
|
267
|
+
| A/B test report | `ABCompare.from_users(df, variant_col=..., control=..., test=..., metrics=[Metric(...)])` — the front door: a per-user frame in YOUR column names, out comes the finished component with an SRM badge and raw / winsor / CUPED modes (a `Metric` that declares `pre_col` opts into CUPED). Groups fetched separately (one query per arm)? `ABCompare.from_groups({"Control": df_a, "Test": df_b}, metrics=[...])`. Self-contained — no DataSource/FilterBar needed. The raw constructor `ABCompare(rows=..., modes=...)` plus `trellum.stats.ab.{winsorize_user_df, cuped_user_df, bootstrap_ab_cis}` remains for custom row sets. See `trellum/README.md` "A/B Testing". |
|
|
268
|
+
|
|
269
|
+
`RawHTML` is the **exception, not the default**. It is permitted only when **both**:
|
|
270
|
+
|
|
271
|
+
1. The visualization is genuinely novel (force-directed graph, Sankey, etc.) and no combination of framework components can express it.
|
|
272
|
+
2. You have explicitly named the components you considered and why each was insufficient.
|
|
273
|
+
|
|
274
|
+
For complex reports prefer the **hybrid pattern**: framework components for filters/standard charts/KPIs; `RawHTML` only for the genuinely novel section. The custom JS must subscribe to the framework filter engine — `window._fwFilterEngine.subscribe(dsId, id, fn)` — instead of managing its own filter state. See `trellum/README.md` "Custom Dashboards" for the full pattern and the `fw.*` API.
|
|
275
|
+
|
|
276
|
+
### Anti-patterns to avoid
|
|
277
|
+
|
|
278
|
+
- **Hardcoded hex colors anywhere in RawHTML JS/HTML** — the `rawhtml-hardcoded-hex` validator **FAILS** the report on any `#RGB`/`#RRGGBB` literal except `#fff` and `#000`. Use `fw.getThemeColors().chart_colors[i]` for chart palettes and CSS vars (`var(--accent-red)`, `var(--text-main)`, `var(--bg-card)`) for HTML/CSS.
|
|
279
|
+
- **Hardcoded `rgba?()`/`rgb()` literals in RawHTML JS** — `rawhtml-hardcoded-rgba` (WARN) flags numeric color literals that don't update on theme switch. Use `fw.getThemeColors().grid_color` for grid lines, `.tick_color` for axis ticks / legend labels, `.chart_colors[i]` for dataset colors. `rgba(0,0,0,0)` (transparent) is exempt. Suppress for intentional fixed-color semantic annotations.
|
|
280
|
+
- **CSS variable strings in Chart.js color properties** — `rawhtml-css-var-in-chartjs` (WARN) flags patterns like `color: 'var(--text-main)'`. Chart.js has no CSS resolver; the string is used as-is (invalid color). Use `fw.getThemeColors().tick_color` / `.grid_color`, or resolve with `getComputedStyle(document.documentElement).getPropertyValue('--name').trim()`.
|
|
281
|
+
- Re-implementing Slim Select dropdowns (use `FilterBar`; `ScopedDataSource` for section-local; `depends_on` for cascading)
|
|
282
|
+
- Writing custom `getFilteredRows()` / filter state management (use the public `fw.filterEngine.*` API)
|
|
283
|
+
- Polling with `setTimeout(init, 100)` to wait for `_fwFilterEngine` (use `fw.filterEngine.onReady(dsId, fn)`)
|
|
284
|
+
- Referencing `window._fwFilterEngine` directly in RawHTML (it's private; use `fw.filterEngine`)
|
|
285
|
+
- Duplicating a DataFrame to get a separate DataSource for section-local filtering (use `ScopedDataSource(id, parent=...)`)
|
|
286
|
+
- Duplicating KPI rendering when `KpiRow(dataset_id=...)` covers it
|
|
287
|
+
- Building custom layout grids when `Grid` / `Panel` / `SplitPane` suffice
|
|
288
|
+
- Going 100% `RawHTML` when only one or two sections need custom logic
|
|
289
|
+
- LEFT JOINing coarser-grain data (monthly MAU, install cohort) into a finer-grain DataFrame to avoid creating a second DataSource — duplicates rows and silently breaks ratio aggregations under filtering
|
|
290
|
+
|
|
291
|
+
### KpiRow aggregation semantics (exact, from the runtime)
|
|
292
|
+
|
|
293
|
+
Every `agg` recomputes over the **filtered rows**, so pass raw columns and let the runtime do the arithmetic — a pre-divided or pre-summed column cannot re-aggregate. The complete set:
|
|
294
|
+
|
|
295
|
+
| `agg` | Computes | Notes |
|
|
296
|
+
|---|---|---|
|
|
297
|
+
| `sum` | `sum(column)` — or summed across a `columns` list | the default |
|
|
298
|
+
| `count` | number of filtered rows | no `column` needed; add a literal `df["x_n"] = 1` column when you also need a ratio denominator |
|
|
299
|
+
| `abssum` | `sum(abs(column))` | for signed ledgers |
|
|
300
|
+
| `ratio` | `sum(numerator) / abs(sum(denominator)) × 100` | **always ×100 — only correct with `format: "percent"`.** A currency or plain-number ratio (AOV, ARPDAU, revenue per order) does NOT belong in a KpiRow: put it on a chart via `ratios=[{numerator, denominator, label}]`, which divides without the ×100 and formats per axis |
|
|
301
|
+
| `avg_by_date` | `sum(column) / count(distinct date_col)` | per-day average; `date_col` defaults to `event_date` |
|
|
302
|
+
| `purchase_pct` | share of `source_col` volume where `type_col` is in `match_values`, ×100 | percent-format only, like `ratio` |
|
|
303
|
+
|
|
304
|
+
The ×100 in `ratio` is the most re-derived fact in measured sessions — one agent spent 15 tool calls reading `kpis.py`, `validation.py` and the JS runtime to establish it. It is stated here so the next one does not have to.
|
|
305
|
+
|
|
306
|
+
<!-- topic: rawhtml -->
|
|
307
|
+
## RawHTML chart lifecycle (mandatory when the chart lives inside a `Visible`)
|
|
308
|
+
|
|
309
|
+
If your `RawHTML` creates a Chart.js instance and the `RawHTML` is anywhere inside a toggle-driven `Visible`, the JS **must** patch BOTH `_initToggleVis` and `renderAll`, AND defer the render with `requestAnimationFrame`:
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
function _refresh() {
|
|
313
|
+
if (fw.filterEngine.isReady(dsId)) _render(canvasId, dsId, fw.filterEngine.getFiltered(dsId));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
fw.filterEngine.subscribe(dsId, 'mychart', _render);
|
|
317
|
+
window.addEventListener('fw-theme-change', _refresh);
|
|
318
|
+
|
|
319
|
+
// _initToggleVis runs on initial page load (renderAll does NOT — the framework
|
|
320
|
+
// calls _renderComps() directly on first load). Without this hook the chart is
|
|
321
|
+
// blank on first load and on URL-preloaded state.
|
|
322
|
+
window._initToggleVis = (function (prev) {
|
|
323
|
+
return function () { if (prev) prev(); requestAnimationFrame(_refresh); };
|
|
324
|
+
})(window._initToggleVis);
|
|
325
|
+
|
|
326
|
+
// renderAll catches scope switches, theme changes, auto-refresh.
|
|
327
|
+
window.renderAll = (function (prev) {
|
|
328
|
+
return function () { if (prev) prev(); requestAnimationFrame(_refresh); };
|
|
329
|
+
})(window.renderAll);
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Why each piece:
|
|
333
|
+
|
|
334
|
+
| Hook | Catches | Without it |
|
|
335
|
+
|---|---|---|
|
|
336
|
+
| `fw.filterEngine.subscribe(dsId, ...)` | filter changes | chart never updates when user filters |
|
|
337
|
+
| `fw-theme-change` listener | theme switches | chart keeps old palette |
|
|
338
|
+
| `window._initToggleVis` patch | **initial page load + URL preload** | chart blank on first load — only renders after user clicks a toggle |
|
|
339
|
+
| `window.renderAll` patch | scope switches, auto-refresh, theme switches | chart stale after re-renders |
|
|
340
|
+
| `requestAnimationFrame` defer | layout reflow after `_updateToggleVis` flips `display` | Chart.js measures canvas at 0×0 → blank chart even though parent is visible |
|
|
341
|
+
|
|
342
|
+
For multi-canvas patterns (one canvas per per-feature DataSource), use a `_ensureSubs()` that finds canvases by attribute (e.g. `[data-split-ds]`) and lazily subscribes once each, then `requestAnimationFrame(_refreshAll)` from both lifecycle hooks.
|
|
343
|
+
|
|
344
|
+
<!-- topic: filters -->
|
|
345
|
+
## DataSource + FilterBar rules
|
|
346
|
+
|
|
347
|
+
- Every report has at least one `DataSource` + `FilterBar` pair (exceptions: `ABCompare`-only reports; single-day dashboards may omit the date_range filter but should still use `DataSource` for any filterable dimension).
|
|
348
|
+
- `date_range` filter first when there's a time-series dimension; one filter per useful categorical dimension.
|
|
349
|
+
- **One grain = one DataSource.** Different grain (monthly MAU vs daily revenue) gets its own `DataSource`. Use `propagate_to` to sync shared dimensions.
|
|
350
|
+
- `static=True` only when data is fundamentally incompatible with the report's filters (point-in-time snapshot, external system data with different date semantics) — rare.
|
|
351
|
+
|
|
352
|
+
### Two layout patterns: main vs section FilterBar
|
|
353
|
+
|
|
354
|
+
The framework supports two FilterBar placements, and they compose:
|
|
355
|
+
|
|
356
|
+
**Main FilterBar** (the default for most reports). Placed in an
|
|
357
|
+
**untitled section** at the top. Use it for filters that apply across
|
|
358
|
+
the whole report — typically `date_range`, `audience_segment`,
|
|
359
|
+
`platform`, etc. The main FilterBar reaches its primary DataSource
|
|
360
|
+
plus any DataSources listed in `propagate_to`. Sticks to the viewport
|
|
361
|
+
top.
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
ctx.add_section("", [ # untitled section — sticky top
|
|
365
|
+
DataSource("daily", df_daily, chunk_by="month"),
|
|
366
|
+
DataSource("monthly", df_monthly),
|
|
367
|
+
FilterBar("daily", df_daily, filters=[
|
|
368
|
+
{"column": "event_date", "type": "date_range"},
|
|
369
|
+
{"column": "platform"},
|
|
370
|
+
], propagate_to={
|
|
371
|
+
"monthly": {"event_date": "event_date"},
|
|
372
|
+
}),
|
|
373
|
+
])
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Section-scoped FilterBar** (drill-downs). Placed inside a titled
|
|
377
|
+
section right after that section's DataSource(s). Use it when a
|
|
378
|
+
section has a dimension that doesn't make sense for the rest of the
|
|
379
|
+
report — e.g. `chest_level` only applies to chest charts. Section
|
|
380
|
+
FilterBars compose with the main FilterBar: a chart inside a section
|
|
381
|
+
is filtered by the AND of both. Sticks below the main FilterBar while
|
|
382
|
+
its section is in view.
|
|
383
|
+
|
|
384
|
+
Canonical layout for a section-scoped pair (validator recognizes this
|
|
385
|
+
exact order):
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
ctx.add_section("Chest Daily Trends", [
|
|
389
|
+
DataSource("chest_daily", df_chest),
|
|
390
|
+
FilterBar("chest_daily", df_chest, filters=[
|
|
391
|
+
{"column": "chest_level"},
|
|
392
|
+
{"column": "difficulty_tier"},
|
|
393
|
+
]),
|
|
394
|
+
LineChart(..., dataset_id="chest_daily"),
|
|
395
|
+
LineChart(..., dataset_id="chest_daily"),
|
|
396
|
+
])
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Section-local filters on the SAME data — use `ScopedDataSource`
|
|
400
|
+
|
|
401
|
+
When a section needs **another local filter** on data that's already
|
|
402
|
+
loaded via the main DataSource (e.g. "Price Tier" filter that only
|
|
403
|
+
affects one chart, not the whole report), use a `ScopedDataSource`
|
|
404
|
+
instead of a fresh DataSource. It:
|
|
405
|
+
|
|
406
|
+
- inherits all of the parent's filters automatically — no `propagate_to`
|
|
407
|
+
wiring needed
|
|
408
|
+
- ships no extra data — rows are derived from the parent at runtime
|
|
409
|
+
- composes with the section's own FilterBar (parent filters AND child
|
|
410
|
+
filters)
|
|
411
|
+
|
|
412
|
+
```python
|
|
413
|
+
# Untitled top section: ONE base DataSource + main FilterBar
|
|
414
|
+
ctx.add_section("", [
|
|
415
|
+
DataSource("cpd", df),
|
|
416
|
+
FilterBar("cpd", df, filters=[
|
|
417
|
+
{"column": "event_date", "type": "date_range"},
|
|
418
|
+
{"column": "package_group"},
|
|
419
|
+
]),
|
|
420
|
+
])
|
|
421
|
+
|
|
422
|
+
# Drill-down section with its OWN local filter
|
|
423
|
+
ctx.add_section("CPD by Price Point", [
|
|
424
|
+
ScopedDataSource("cpd_pp", parent="cpd"),
|
|
425
|
+
FilterBar("cpd_pp", df, filters=[
|
|
426
|
+
{"column": "price_tier"}, # only affects this section
|
|
427
|
+
]),
|
|
428
|
+
LineChart(df=df, x="event_date", dataset_id="cpd_pp",
|
|
429
|
+
ratios=[{"numerator": "chips", "denominator": "revenue",
|
|
430
|
+
"label": "CPD"}],
|
|
431
|
+
stack_by="price_point_display",
|
|
432
|
+
title="CPD by Price Point"),
|
|
433
|
+
])
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**Cascading dropdowns** (e.g. *Package Group* → *Package Name*): add
|
|
437
|
+
`depends_on` to the child filter spec. The child's option list narrows
|
|
438
|
+
automatically when the parent selection changes:
|
|
439
|
+
|
|
440
|
+
```python
|
|
441
|
+
FilterBar("cpd_pkg", df, filters=[
|
|
442
|
+
{"column": "package_group"},
|
|
443
|
+
{"column": "package_name", "depends_on": "package_group"},
|
|
444
|
+
])
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Custom RawHTML lifecycle
|
|
448
|
+
|
|
449
|
+
When you do need custom JS (genuinely novel visualization), always use
|
|
450
|
+
the public `fw.*` API:
|
|
451
|
+
|
|
452
|
+
```js
|
|
453
|
+
fw.filterEngine.onReady('cpd', function() {
|
|
454
|
+
// engine is ready — wire your chart now
|
|
455
|
+
fw.filterEngine.subscribe('cpd', 'my-chart', _render);
|
|
456
|
+
_render(fw.filterEngine.getFiltered('cpd'));
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Never** poll with `setTimeout(init, 100)`, **never** reference
|
|
461
|
+
`window._fwFilterEngine` directly, **never** instantiate `new SlimSelect(...)`
|
|
462
|
+
in RawHTML. Each of those is a validator WARN now. Use FilterBar +
|
|
463
|
+
ScopedDataSource for the dropdown UX; use `fw.filterEngine.onReady` for
|
|
464
|
+
ready-detection; use `fw.filterEngine.*` for everything filter-related.
|
|
465
|
+
|
|
466
|
+
Validator constraints:
|
|
467
|
+
|
|
468
|
+
- At most one main FilterBar per report (untitled top section).
|
|
469
|
+
- At most one section-scoped FilterBar per titled section.
|
|
470
|
+
- Section-scoped pair must be laid out as `[DataSource(s) or ScopedDataSource, FilterBar, ...content]` at the start of the section.
|
|
471
|
+
- A `ScopedDataSource`'s `parent` must reference a base `DataSource` (not another scoped child).
|
|
472
|
+
- A filter with `depends_on` must reference another filter on the same FilterBar with a real column in the underlying DataFrame.
|
|
473
|
+
|
|
474
|
+
Use `ScopedDataSource` when a section filters the *same* rows more narrowly. When the grain genuinely differs, give each section its own `DataSource` instead — different data is not a scope of the same data.
|
|
475
|
+
|
|
476
|
+
<!-- topic: review -->
|
|
477
|
+
## The live review loop
|
|
478
|
+
|
|
479
|
+
Review mode turns a served report into a feedback surface: the user clicks
|
|
480
|
+
elements in the browser, types change requests, queues them with one optional
|
|
481
|
+
chat message, and hits **Send** — you receive the batch in the terminal with
|
|
482
|
+
each item's section title, component kind and title, DOM id, and selector,
|
|
483
|
+
which map straight to lines of `generator.py`.
|
|
484
|
+
|
|
485
|
+
The loop, from your seat:
|
|
486
|
+
|
|
487
|
+
1. `python -m trellum review start reports/<slug>` — builds if output is
|
|
488
|
+
missing (`--rebuild` to force), starts or reuses the background server,
|
|
489
|
+
enables review mode, and opens the browser. If a server predating review
|
|
490
|
+
mode holds the port, rerun with `--restart-server`.
|
|
491
|
+
2. `python -m trellum review poll` — **BLOCKS** until the user sends
|
|
492
|
+
feedback. From an agent harness, run it with a generous timeout or as a
|
|
493
|
+
tracked background task; if it gets killed, just rerun it — queued
|
|
494
|
+
feedback is never lost. Exit codes: `0` feedback arrived, `2` your
|
|
495
|
+
`--timeout` elapsed, `3` the user ended the session.
|
|
496
|
+
3. Edit `reports/<slug>/generator.py` and rebuild with
|
|
497
|
+
`python -m trellum.run reports/<slug> --no-serve`. The browser notices
|
|
498
|
+
the rebuild and reloads itself within ~2 seconds — never hand-edit
|
|
499
|
+
`output/` HTML, and never restart the server to "refresh".
|
|
500
|
+
4. `python -m trellum review poll --reply "what you changed"` — the reply
|
|
501
|
+
appears in the browser's chat and unlocks the user's Send button, then
|
|
502
|
+
the command waits for the next round.
|
|
503
|
+
5. Repeat until poll exits `3` (the user pressed End, or Send & End — that
|
|
504
|
+
final batch still arrives first). `python -m trellum review end` ends
|
|
505
|
+
it from your side when the user asks you to wrap up in conversation.
|
|
506
|
+
|
|
507
|
+
**Several reports, several agents, one project:** one server serves the
|
|
508
|
+
whole `output/` tree and one review session spans it — every report page
|
|
509
|
+
gets the overlay, and batches are tagged with their report's slug. When more
|
|
510
|
+
than one agent works the same project, each MUST poll with
|
|
511
|
+
`--report <slug>` (and reply with `--report <slug>`): a slug-scoped poll
|
|
512
|
+
drains only that report's batches, so agents never steal each other's
|
|
513
|
+
feedback. A slug-less poll drains everything — fine only when you are the
|
|
514
|
+
only agent. Different projects are automatically separate: each output
|
|
515
|
+
directory gets its own server on its own port (discovered via
|
|
516
|
+
`/_fw/server.json` identity on ports 8050–8069), with fully independent
|
|
517
|
+
review state.
|
|
518
|
+
|
|
519
|
+
The user can also send element-free chat messages; they arrive as a batch
|
|
520
|
+
whose `items` list is empty and whose message is the `note`. And on Chart.js
|
|
521
|
+
charts they can point at the data itself — Alt-click picks the nearest data
|
|
522
|
+
point, dragging marks an x-range — which arrives as a `data` field on the
|
|
523
|
+
item (`chart-point`: series/x/value; `chart-range`: x_from/x_to plus
|
|
524
|
+
per-series n/min/max and the points). That is a data investigation, not a
|
|
525
|
+
styling request: answer it with `trellum query` against the exact dates
|
|
526
|
+
before touching any code. Everything is served from the normal `trellum
|
|
527
|
+
serve` server — review endpoints live under `/_fw/review/`, loopback-only,
|
|
528
|
+
and the on-disk HTML is never modified (the overlay is injected into the
|
|
529
|
+
served copy only).
|
|
530
|
+
|
|
531
|
+
<!-- topic: validation -->
|
|
532
|
+
## Validator response protocol (mandatory)
|
|
533
|
+
|
|
534
|
+
Every report run executes `trellum/validation/` post-generation. Output goes to console + `output/<slug>/_validation.json`.
|
|
535
|
+
|
|
536
|
+
A report task is **not done** until you have:
|
|
537
|
+
|
|
538
|
+
1. Read the validator output.
|
|
539
|
+
2. Surfaced every **FAIL** and **WARN** to the user with check ID, affected component/section, and a concrete fix.
|
|
540
|
+
3. **Fixed every FAIL.** WARNs should be fixed unless intentionally accepted (then suppress in `report.yaml` under `validation.suppress` with a one-line reason for why).
|
|
541
|
+
4. If the report has zero FAILs and zero WARNs, said so explicitly.
|
|
542
|
+
5. **Handed the user a clickable link.** The last action before reporting done is `python -m trellum serve --background` — it starts (or reuses) a detached server, prints the URLs, and returns immediately — and your final message to the user must contain the printed report URL. A finished build the user cannot click is not finished. (Keep using `--no-serve` for the build/iterate loop itself; the link is the closing move, not a per-build cost.)
|
|
543
|
+
|
|
544
|
+
Severity:
|
|
545
|
+
|
|
546
|
+
| Level | Action |
|
|
547
|
+
|---|---|
|
|
548
|
+
| **FAIL** | Must fix. Structural bug → broken rendering, missing interactivity, wrong data. Common: missing `dataset_id`, orphan FilterBar, missing DataFrame columns. |
|
|
549
|
+
| **WARN** | Fix unless accepted + suppressed. Common: non-date x-axis with annotations enabled (silently skipped), hardcoded colors (breaks on theme switch), missing `renderAll` in custom JS. |
|
|
550
|
+
| **INFO** | No action required. Just confirm intent. |
|
|
551
|
+
|
|
552
|
+
Full check list, suppression syntax, and how to add new checks: `trellum/README.md` "Validation".
|
|
553
|
+
|
|
554
|
+
### Filter coverage check
|
|
555
|
+
|
|
556
|
+
The validator emits **`chart-filter-coverage`** (WARN) for every chart whose `dataset_id` is not reached by one or more FilterBar filters — the most common silent bug, where users see filter dropdowns that change nothing on certain charts. The fix is almost always to extend `FilterBar.propagate_to` with the missing `{src_col: tgt_col}` entry.
|
|
557
|
+
|
|
558
|
+
A sibling check, **`chart-value-grain-mismatch`** (WARN), catches a subtler failure mode: the chart's value column is constant within each x-axis value (e.g. a daily total merged on `event_date` only into a date × audience × spender DataSource). Filters reach the DataSource structurally, but the chart's plotted value never changes — ratios stay constant, absolutes inflate by the count of replicated rows. Fix by extracting the column to its own DataSource at native grain (date-only) and filtering by `event_date` only there.
|
|
559
|
+
|
|
560
|
+
When a filter genuinely cannot apply to a DataSource (its SQL doesn't carry that dimension and adding it would require fictional attribution — e.g. attaching a `template_type` to a purchase event), accept the gap **per (DataSource, filter_column)** in `report.yaml`:
|
|
561
|
+
|
|
562
|
+
```yaml
|
|
563
|
+
validation:
|
|
564
|
+
accept_inactive_filters:
|
|
565
|
+
tc_rev_gop3:
|
|
566
|
+
- template_type # purchases don't carry a template_type — see queries.py:156
|
|
567
|
+
tc_boost_gop3:
|
|
568
|
+
- template_type # boosters live in economy_balance, no template dim
|
|
569
|
+
- chest_tier
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
This is the **preferred** form because future dead filters on the same DataSource still fire — only the explicitly-listed columns are silenced. The broader `validation.suppress_per_dataset: {ds: [chart-filter-coverage]}` swallows ALL future inactive filters on that DS and should be reserved for DataSources that are fundamentally non-reactive (tiny static rollups feeding one chart).
|
|
573
|
+
|
|
574
|
+
Full per-chart, per-filter matrix lives at `output/<slug>/_details.json` (also embedded in `_meta.json` under `details.filter_matrix`) and can be rendered as a coverage table. `details.totals` carries dataset count, chart count, total row count, total serialized data size — so you can see at a glance how heavy a report is. There is also a `data_source: "real" | "mock"` flag and an amber banner when the run was a mock/test build, so synthetic row counts don't get read as production reality.
|
|
575
|
+
|
|
576
|
+
With the health view enabled (`?health=1` in the URL, or an embedding application that reports the viewer as an admin), the rendered report itself shows a small **filter-health badge** above each chart whose dimensions are not all reactive — a red `⚠ N/M filters dead` chip that pops a styled per-filter detail panel on hover. Hidden by default for non-admins so end-users see a clean report.
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
### Suppression hierarchy
|
|
580
|
+
|
|
581
|
+
Three layers, narrowest → broadest:
|
|
582
|
+
|
|
583
|
+
```yaml
|
|
584
|
+
validation:
|
|
585
|
+
# Granular, PREFERRED — accept specific (DataSource, filter_column) pairs
|
|
586
|
+
# as inactive. Any OTHER inactive filter on the same DS still fires.
|
|
587
|
+
accept_inactive_filters:
|
|
588
|
+
pf_country_gop3: [platform]
|
|
589
|
+
pf_cz_gop3: [platform, spender_tier]
|
|
590
|
+
|
|
591
|
+
# Per-DataSource — silences the named check ID for ALL filters on that DS.
|
|
592
|
+
# Use only when the DataSource is fundamentally non-reactive across every
|
|
593
|
+
# dimension (tiny static rollup feeding one chart).
|
|
594
|
+
suppress_per_dataset:
|
|
595
|
+
economy_test_net: [ds-no-filterbar]
|
|
596
|
+
|
|
597
|
+
# Global — silences every instance of the named check ID across the report.
|
|
598
|
+
# Last resort; use for systemic exemptions (e.g. annotations-non-date-x-axis
|
|
599
|
+
# on a report that is intentionally month-bucketed).
|
|
600
|
+
suppress:
|
|
601
|
+
- some-systemic-check
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Suppressed checks are NOT removed from the result — they're flagged with `suppressed: true` and grouped under **Suppressed** with rationale text. The matrix renders suppressed inactive cells as `✗ⓢ` so a reviewer can audit later. Always pair a suppression with a `# rationale:` YAML comment naming the specific reason.
|
|
605
|
+
|
|
606
|
+
## After framework changes (only if explicitly asked)
|
|
607
|
+
|
|
608
|
+
If the task explicitly requires modifying `trellum/*`:
|
|
609
|
+
|
|
610
|
+
1. **Add or update validation checks in `trellum/validation/`** for the new behavior — non-negotiable. Skipping silently breaks future reports.
|
|
611
|
+
2. Run unit tests:
|
|
612
|
+
- `python3 -m pytest trellum/testing/test_components.py -v` — component changes
|
|
613
|
+
- `python3 -m pytest trellum/testing/test_js_runtime.py -v` — JS runtime changes
|
|
614
|
+
- `python3 -m pytest trellum/testing/test_interactions.py -v` — theme / rendering changes
|
|
615
|
+
3. Rebuild a couple of this project's existing reports to verify nothing broke —
|
|
616
|
+
pick ones that between them cover charts, KPIs, filters and any RawHTML:
|
|
617
|
+
```bash
|
|
618
|
+
python3 -m trellum.run reports/<slug> --no-serve
|
|
619
|
+
```
|
|
620
|
+
4. For broader coverage, `python3 -m trellum.run --all --no-serve`.
|
|
621
|
+
|
|
622
|
+
## This repo is a submodule — how changes reach reports
|
|
623
|
+
|
|
624
|
+
The framework is mounted as a git submodule at `trellum/` inside a consumer
|
|
625
|
+
repository — a report project, or an application that serves several of them.
|
|
626
|
+
Consumers pin a **release tag**, so a merged framework change does not reach
|
|
627
|
+
reports until a release is cut and the consumer bumps its submodule pointer.
|
|
628
|
+
|
|
629
|
+
Two consequences when working inside `trellum/`:
|
|
630
|
+
|
|
631
|
+
- **Commit on a branch, push to this repository.** Submodules check out
|
|
632
|
+
detached HEAD; commits made there belong to `trellum`, not the
|
|
633
|
+
consumer repo. `git checkout -b feature/x origin/develop` first.
|
|
634
|
+
- **Do not commit the moved submodule pointer on the consumer's main branch.**
|
|
635
|
+
That pointer is only bumped when moving to a released tag.
|
|
636
|
+
|
|
637
|
+
Releases are published as **GitHub Releases** (a tag alone is not a release).
|
|
638
|
+
There are no Actions runners for this repo, so it is a manual three-step
|
|
639
|
+
sequence — push the tag, `gh release create --generate-notes --latest`, then
|
|
640
|
+
fast-forward the `release` branch. `__version__` must match the tag.
|
|
641
|
+
|
|
642
|
+
Branch model, versioning rules, the release procedure, and the consumer bump
|
|
643
|
+
commands are all in [`RELEASING.md`](RELEASING.md). Read it before tagging a
|
|
644
|
+
release or changing `__version__`.
|
|
645
|
+
|
|
646
|
+
## What NOT to do
|
|
647
|
+
|
|
648
|
+
- **Do not edit `trellum/*` while building a report.** Reports only touch `reports/{slug}/`. If a task seems to require framework changes, stop and discuss scope with the user — framework changes affect every report.
|
|
649
|
+
- **Do not run `--all` to verify a single change.** Build only the affected report (`python3 -m trellum.run reports/<slug> --no-serve`).
|
|
650
|
+
- **Do not start the preview server to check whether a build worked.** The build prints its own result and the validator writes `output/<slug>/_validation.json`. A bare `trellum.run` blocks until killed; `--no-serve` exits.
|
|
651
|
+
- **Do not write custom Chart.js code for any chart type listed above.** Use the framework component.
|
|
652
|
+
- **Do not hardcode colors.** CSS variables for HTML/CSS, `getThemeColors()` for Chart.js.
|
|
653
|
+
- **Do not import Chart.js or other JS libraries.** Bundled in `trellum/static/vendor/`.
|
|
654
|
+
- **Do not write connection / credential code.** Use `ctx.get_connection()`.
|
|
655
|
+
- **Do not use Chart.js native `legend: { display: true }` on non-doughnut charts.** The `fwLegend` plugin handles it.
|
|
656
|
+
- **Do not open `output/<slug>/index.html` as `file://`.** `data.json` is fetched via XHR and requires HTTP — the runner serves automatically on :8050.
|
|
657
|
+
|
|
658
|
+
## Licensing (applies to every change)
|
|
659
|
+
|
|
660
|
+
This repository is MIT licensed.
|
|
661
|
+
|
|
662
|
+
- **Do not add per-file licence headers.** There are none, deliberately;
|
|
663
|
+
`LICENSE` governs the tree. A half-headered repository is worse than either
|
|
664
|
+
consistent choice.
|
|
665
|
+
- **Adding a dependency means updating `THIRD-PARTY.md` in the same commit.**
|
|
666
|
+
`docs/LICENSING.md` has the audit command. Anything permissive is fine;
|
|
667
|
+
anything copyleft or source-available needs a conversation first, because it
|
|
668
|
+
changes what a downstream user may do with a build of this repository.
|
|
669
|
+
- **Bundling is the stricter case than depending.** Code added under
|
|
670
|
+
`static/vendor/` ships inside every copy of this repository, so its notice
|
|
671
|
+
travels with it — record it in `static/vendor/MANIFEST.json`.
|