diff --git a/petrolyst-insights/README.md b/petrolyst-insights/README.md index 32313db..2a08996 100644 --- a/petrolyst-insights/README.md +++ b/petrolyst-insights/README.md @@ -89,7 +89,7 @@ Source: `petrolyst-web/design/brand/` (tokens `07_UI_Tokens/petrolyst_tokens.css - Light theme: action teal `#00695C` for primary and links, navy `#0B1D33` text, `#F8FAFC` layout, full-colour stacked logo. -- Dark theme: `#35C5B6` primary, `#071425` / `#12263D` surfaces, white logo. +- Dark theme: `#35C5B6` primary, `#1F2A33` / `#2A3945` surfaces (PD-26), white logo with the Y in its green. - Font: Inter, Superset's default. Satoshi is licence-restricted and is not used. - Chart palettes: "Petrolyst" categorical (the default, brand teal first) and "Petrolyst Teal" / "Petrolyst Navy" sequential. The categorical palette was validated with the dataviz skill's diff --git a/petrolyst-insights/assets/branding/petrolyst_wordmark_white_y.svg b/petrolyst-insights/assets/branding/petrolyst_wordmark_white_y.svg new file mode 100644 index 0000000..926fdf8 --- /dev/null +++ b/petrolyst-insights/assets/branding/petrolyst_wordmark_white_y.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/petrolyst-insights/dashboards/atoma/SPEC.md b/petrolyst-insights/dashboards/atoma/SPEC.md index 98c164b..130cd95 100644 --- a/petrolyst-insights/dashboards/atoma/SPEC.md +++ b/petrolyst-insights/dashboards/atoma/SPEC.md @@ -566,10 +566,10 @@ The Petrolyst app (petrolyst-platform `app/`, PD-12) shows Superset dashboards i Insights lens, with guest passes from the Petrolyst server (petrolyst-platform `server/`). `build_bundle.py` builds them (since PD-13 the only dashboards in the bundle): -| Slug | Content | Filters (set by the app; Superset's filter column is hidden) | +| Slug | Content | Filters (preset by the app's filter row, PD-25; Superset's filter column is hidden) | |---|---|---| -| `atoma-overview` | O1–O6: sites by pilot status and by baseline status, litres delivered, fuel cost by currency, electricity billed by currency and basis, litres delivered per month | Viewing period on O3–O6 | -| `atoma-site` | the four §4 detail tabs as Superset's own tabs, the same rows and charts; no D0 (the app shows the site's header) | Site (all charts), Viewing period (as §4: fuel and runtime charts only) | +| `overview` | O1–O6: sites by pilot status and by baseline status, litres delivered, fuel cost by currency, electricity billed by currency and basis, litres delivered per month | Sites (`NATIVE_FILTER-sites`, any number, none is all; every chart), Viewing period on O3–O6 | +| `site` | the four §4 detail tabs as Superset's own tabs, the same rows and charts; no D0 (the app shows the site's header) | Site (all charts), Viewing period (as §4: fuel and runtime charts only) | - **One site dashboard with tabs** *(owner, 29 Oct: "move it back inside the dashboard and make the tab as they were")*: replaces the four one-tab dashboards `atoma-site-summary`, `-fuel`, @@ -582,7 +582,9 @@ Insights lens, with guest passes from the Petrolyst server (petrolyst-platform ` recorded values across sites (`OVERVIEW_METRICS`); per-site rates (L/h, L/day) are never averaged; money is grouped by `currency_text`, a missing currency is its own row, and no total crosses currencies. `build_bundle.py` checks both. -- They carry no CSS. +- **Dashboard CSS** (`EMBEDDED_CSS` and the site dashboard's tab rule in `build_bundle.py`): Superset's roots at their + content's height, so the app's frame can shrink again (PD-25); the site dashboard's tab layers transparent, so its + charts sit as boxes like the Overview's (PD-24); the chart boxes with 16 px corners and the card shadow (PD-26). - `import_bundle.py --embedded-out FILE --embed-domain URL` makes them embeddable from the app's origin and writes `{slug: embedding id}` for the Petrolyst server. - Embedding is switched on in the local stack only (`.local/fmdb-scratch`, diff --git a/petrolyst-insights/dashboards/atoma/build_bundle.py b/petrolyst-insights/dashboards/atoma/build_bundle.py index 179eb4c..e62a4f2 100644 --- a/petrolyst-insights/dashboards/atoma/build_bundle.py +++ b/petrolyst-insights/dashboards/atoma/build_bundle.py @@ -456,7 +456,8 @@ def big_number(key: str, title: str, view: str, metric: dict[str, Any], *, "subheader": subheader, "header_font_size": 0.4, "subheader_font_size": 0.15, - "y_axis_format": "SMART_NUMBER", + # The brand's number conventions (PD-24): "59,312", never "59.3k"; the unit is named in the subheader. + "y_axis_format": ",d", "time_format": "smart_date", "force_timestamp_formatting": False, } @@ -487,7 +488,8 @@ def timeseries_common(x: str, metrics: list[Any], groupby: list[str], grain: str "y_axis_title_margin": 40, "y_axis_title_position": "Left", "x_axis_time_format": "%Y-%m-%d", - "y_axis_format": "SMART_NUMBER", + # Full figures, as the brand's conventions want (PD-24): one decimal for a rate, none for litres and counts. + "y_axis_format": ",.1f" if "/" in y_title else ",d", "rich_tooltip": True, "tooltipTimeFormat": "%Y-%m-%d", "truncateXAxis": True, @@ -523,7 +525,7 @@ def overview_charts() -> list[Chart]: return [ big_number("O3", "Litres delivered", "v_delivery", m("SUM(delivered_l)", "Litres delivered"), - period="delivery_date", subheader="all sites, in the viewing period", + period="delivery_date", subheader="litres, the chosen sites, in the viewing period", description="Recorded deliveries. Deliveries are not consumption."), summary_table("O1", "Sites by pilot status", "v_site", ["pilot_status"], [m("COUNT(*)", "Sites")]), @@ -552,8 +554,9 @@ def overview_charts() -> list[Chart]: OVERVIEW_NOTES = ("**Reading this page.** All sites together, from recorded values only: " "deliveries are not consumption, and site rates (L/h, L/day) are never averaged " "here; each site's own page has them. Money is shown per currency and never added " - "up across currencies. The viewing period narrows the deliveries, the costs and " - "the monthly bars; the site counts are always the current state.") + "up across currencies. The Sites filter narrows every figure to the chosen sites " + "(none chosen: all); the viewing period narrows the deliveries, the costs and " + "the monthly bars, while the site counts are always the current state.") OVERVIEW_ROWS = [ [("MARKDOWN:notes-overview", 12, 14)], [("O3", 4, 40), ("O1", 4, 40), ("O2", 4, 40)], @@ -561,6 +564,17 @@ OVERVIEW_ROWS = [ [("O6", 12, 60)], ] OVERVIEW_PERIOD_SCOPE = {"O3", "O4", "O5", "O6"} +SITES_FILTER = "NATIVE_FILTER-sites" + + +def overview_filters(ids: dict[str, int]) -> list[dict[str, Any]]: + """Sites (any number, none is all) and the Viewing period (PD-25: both in Superset's column).""" + sites = select_filter(SITES_FILTER, "Sites", "v_site", "site_label", multi=True, + required=False, excluded=[]) + sites["description"] = "Any number of sites; none chosen is all sites. Search by code or name." + period = period_filter(ids, OVERVIEW_PERIOD_SCOPE, + "Narrows the deliveries, the costs and the monthly bars.") + return [sites, period] # --- Site Detail ----------------------------------------------------------------------- @@ -699,7 +713,7 @@ def detail_charts() -> list[Chart]: "show_legend": True, "legendType": "scroll", "legendOrientation": "top", - "y_axis_format": "SMART_NUMBER", + "y_axis_format": ",.1f", "rich_tooltip": True, "x_axis_time_format": "%Y-%m-%d", "y_axis_title": "L/h", @@ -947,6 +961,20 @@ def dashboard_yaml(key: str, title: str, slug: str, layout: Layout, metadata: di } +# Dashboard CSS for the app's frame (PD-12): the app grows the frame to the page's scroll height, +# and Superset's page stretches to the frame, so without this the frame could only ever grow (a +# taller tab, once opened, left empty space under every other tab: measured 1438 → 2174 → 3158, +# never back). With the roots at their content's height the frame shrinks again. +EMBEDDED_CSS = ("html, body, #app, #app > div, #app > div > div, .dashboard, .dashboard-content " + "{ height: auto !important; min-height: 0 !important; } " + # Chart boxes as the app's cards (PD-26); dashboard CSS cannot read the brand's tokens, + # so these are --radius-md and the light --shadow-card written out. + ".dashboard-component-chart-holder, .dashboard-markdown { border-radius: 16px !important; box-shadow: " + "0 2px 4px rgb(11 29 51 / 0.04), 0 12px 28px -8px rgb(11 29 51 / 0.12) !important; } " + # A notes block's wrapper clips what is inside it, so the wrapper carries the shadow. + ".dashboard-markdown .dashboard-component-chart-holder { box-shadow: none !important; } ") + + def base_metadata(layout: Layout, filters: list[dict[str, Any]], cross_filters: bool) -> dict[str, Any]: return { "color_scheme": "supersetColors", @@ -959,8 +987,8 @@ def base_metadata(layout: Layout, filters: list[dict[str, Any]], cross_filters: "map_label_colors": {}, "color_scheme_domain": [], "cross_filters_enabled": cross_filters, - # Filters in Superset's left column (owner, 29 Oct); the Petrolyst app hides the column - # when it embeds a dashboard and sets the filters itself. + # Filters in Superset's left column (owner, 29 Oct) for admins; the Petrolyst app hides + # the column when it embeds a dashboard and sets the filters itself (PD-25). "filter_bar_orientation": "VERTICAL", "native_filter_configuration": filters, "chart_configuration": {}, @@ -1010,16 +1038,26 @@ def build(database_uuid: str, columns_path: Path) -> dict[str, Any]: page.markdown["notes-overview"] = OVERVIEW_NOTES if key == OVERVIEW_SLUG: page.rows(OVERVIEW_ROWS, "GRID_ID", "overview") - filters = [period_filter(page.ids, OVERVIEW_PERIOD_SCOPE, - "Narrows the deliveries, the costs and the monthly bars.")] + filters = overview_filters(page.ids) + css = EMBEDDED_CSS else: page.tabs(DETAIL_TABS, "GRID_ID", "TABS-detail") filters = detail_filters(page.ids) + # Superset paints its tab container white, which swallows the charts' own boxes; the + # Overview has no tabs and shows each chart as a box on the page (PD-24). Every layer + # of the tabs is named, with !important, because Superset 6.1 paints the holder white + # with a two-class rule, and Firefox and Safari also paint the outer container white + # where Chrome does not (measured in all three). + css = EMBEDDED_CSS + ( + ".dashboard-component-tabs, .dashboard-component-tabs-content, " + ".dashboard-component-tabs .ant-tabs, .dashboard-component-tabs .ant-tabs-content-holder, " + ".dashboard-component-tabs .ant-tabs-content, .dashboard-component-tabs .ant-tabs-tabpane " + "{ background: transparent !important; }") placed = {k: all_charts[k] for k in page.ids} page_metadata = base_metadata(page, filters, cross_filters=False) page_metadata["label_colors"].update(STOCK_STATUS_COLOURS) files[f"dashboards/{key}.yaml"] = dashboard_yaml(key, title, key, page, - page_metadata) + page_metadata, css=css) dashboards.append((page, placed, page_metadata)) for chart in all_charts.values(): diff --git a/petrolyst-insights/superset_config.py b/petrolyst-insights/superset_config.py index 74cfd4f..9d46b79 100644 --- a/petrolyst-insights/superset_config.py +++ b/petrolyst-insights/superset_config.py @@ -189,8 +189,8 @@ THEME_DEFAULT["token"].update( "colorError": "#B3261E", "colorInfo": "#2563EB", "colorTextBase": "#0B1D33", - "colorBgLayout": "#F8FAFC", - "colorBorder": "#E2E8F0", + "colorBgLayout": "#F7F9FA", + "colorBorder": "#DDE5E9", "borderRadius": 8, "fontFamily": "Inter, system-ui, sans-serif", } @@ -200,7 +200,7 @@ THEME_DARK = deepcopy(THEME_DEFAULT) THEME_DARK["algorithm"] = "dark" THEME_DARK["token"].update( { - "brandLogoUrl": f"{BRANDING}/petrolyst_wordmark_white.svg", + "brandLogoUrl": f"{BRANDING}/petrolyst_wordmark_white_y.svg", "colorPrimary": "#35C5B6", "colorLink": "#35C5B6", "colorSuccess": "#35C5B6", @@ -208,10 +208,10 @@ THEME_DARK["token"].update( "colorError": "#FF6B6B", "colorInfo": "#60A5FA", "colorTextBase": "#F8FAFC", - "colorBgBase": "#071425", - "colorBgLayout": "#071425", - "colorBgContainer": "#12263D", - "colorBorder": "#526D82", + "colorBgBase": "#1F2A33", + "colorBgLayout": "#1F2A33", + "colorBgContainer": "#2A3945", + "colorBorder": "#76828E", "colorEditorSelection": superset_defaults.THEME_DARK["token"][ "colorEditorSelection" ], @@ -219,7 +219,7 @@ THEME_DARK["token"].update( ) # Chart colours. Validated with the dataviz skill's validator on both chart -# surfaces (#FFFFFF light, #12263D dark): all slots in both lightness bands and +# surfaces (#FFFFFF light, #2A3945 dark, PD-26): all slots in both lightness bands and # >= 3:1; worst adjacent CVD dE 13.7, normal-vision dE 26.2; the first three # slots also pass all-pairs (scatter, maps). Brand teal leads; brand cyan # #00B6E6 is snapped to #0097C4 because it is too light for the dark surface