{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "dashboard-intro",
      "metadata": {},
      "source": "# Boulder: one dashboard in a notebook or an offline page\n\nRun these cells in order with a dashboard enabled native Aner development runtime. `.anernb` uses the compatible Aner VS Code extension; `.ipynb` uses the optional Aner Jupyter kernel. These are different notebook formats with the same Aner analysis. The published research preview may predate this development API.\n\nThe map, site totals, daily trends, coverage heatmap and data table share inclusive dates and a multi site selection. Click a mark to focus one site; Ctrl/Cmd/Shift click removes a visible site, and checkboxes add sites back. Drag or zoom the map and keep that view while filtering. All interaction uses the embedded snapshot locally; changing controls does not rerun Aner.\n\n**Every count is invented, every position is illustrative, and October 12 to 18, 2026 is an assigned teaching week. These are not municipal observations or real monitoring stations.** The provided CSV is its only input file. The first dashboard requires no map packs, online tiles, credentials or connection.\n\nDownload `plot_boulder_sites.csv` as well as this notebook. Run from the project root so `examples/fixtures/plot_boulder_sites.csv` exists relative to the kernel working directory. The path is resolved from that working directory, not the notebook's folder. For a standalone download, create that folder tree below the working directory or change the same path in all three formats. Reading the CSV is local and read only.\n"
    },
    {
      "cell_type": "code",
      "id": "dashboard-imports",
      "metadata": {},
      "source": [
        "// Requires the matching dashboard-enabled native development runtime.\n",
        "// Run from the project root containing examples/fixtures/plot_boulder_sites.csv.\n",
        "// Synthetic teaching counts and illustrative positions, not municipal observations.\n",
        "import aner.data\n",
        "import aner.plot\n"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "id": "dashboard-inputs",
      "metadata": {},
      "source": [
        "// Long format: counts may be missing; site/date/position metadata is required.\n",
        "var schema = data.schema()\n",
        "schema = data.column(schema, \"date\", \"String\", false)\n",
        "schema = data.column(schema, \"site\", \"String\", false)\n",
        "schema = data.column(schema, \"longitude\", \"Float64\", false)\n",
        "schema = data.column(schema, \"latitude\", \"Float64\", false)\n",
        "schema = data.column(schema, \"count\", \"Float64\", true)\n",
        "let source = data.scan_csv(\"examples/fixtures/plot_boulder_sites.csv\", schema, missing: \"NA\")\n",
        "let bounded = data.bounded(source, max_scanned_rows: 11713,\n",
        "    max_read_bytes: 8388608, timeout_ms: 5000)\n",
        "let table = data.collect(bounded, max_rows: 11712,\n",
        "    max_bytes: 4194304, batch_rows: 256)\n"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "id": "dashboard-method",
      "metadata": {},
      "source": "## Read the comparison\n\nThe complete dataset totals 18,470 invented events. Creek has 3,960 and Campus 4,340. Selecting just those two sites gives 8,300; selecting weekdays gives 5,700, and selecting the weekend gives 2,600.\n\nMarker area represents each site's total for the selected dates. Color represents `100 * (weekend daily mean / weekday daily mean - 1)`, using only included dates in UTC. A missing group, a zero weekday mean or an unrepresentable ratio displays N/A. The map, bars, lines and table use the same selection. Axes, marker reference size and the color scale remain fixed while filters change. For dates outside the snapshot or no sites, the dashboard shows an empty result.\n\nInvalid or reversed dates leave the last valid result visible and show an error. The CSV has the exact header `date,site,longitude,latitude,count`. Its date, site and WGS84 longitude/latitude metadata is required; only the Float64 count column is nullable. The complete fixture contains 42 invented observations. An unquoted `NA` count becomes missing with `missing: \"NA\"`; a missing site/date row is also missing, never zero. The table recipe fills every calendar date between the earliest and latest represented date, including a date absent for all sites. It cannot infer missing dates outside those bounds or a site absent from the entire input. Measured zero remains an observation. Empty CSV fields are missing only when the CSV reader is explicitly configured with `missing: \"\"`; this example uses `NA` instead.\n\n`plot.site_dashboard_table(table)` requires a complete materialized DataTable (incomplete previews are rejected), sorts valid ISO `YYYY-MM-DD` dates and keeps site order by first appearance. Each site must have the same finite WGS84 position on every row and at most one row per date. The adapter finds required columns by name, accepts unrelated extra columns and any table column order, and accepts primitive Int64 or Float64 coordinates/counts without units. Duplicate site/date pairs, missing date/site/position metadata, invalid dates, inconsistent positions and invalid nonmissing counts are errors. The CSV reader requires the complete header and column order declared in its schema. Trend segments stop at missing dates rather than connecting across them.\n\nFor each selected site and date range, **Period total** is N/A if any expected observation is missing. **Observed subtotal** and **Coverage** report the available data separately; an entirely missing range has an N/A subtotal. Weekday or weekend means are N/A if any date in that group is missing, or if no date in that group is selected. The change also requires a positive weekday baseline and a finite result. An incomplete map total uses a neutral 5 px marker; a measured zero total has zero area. The missing data companion notebook demonstrates these cases.\n\nThe recipe accepts 1 to 32 unique site names (1 to 128 `UTF-8` bytes without control characters), an inclusive calendar span of 1 to 366 days and finite nonmissing counts between 0 and 1e12, up to 11,712 input records. This example bounds input reading to 8 MiB and five seconds, collecting at most 11,712 rows in a 4 MiB managed memory budget, in batches of 256. The source row guard allows one extra record so a full sized complete input can prove end of file and an oversized input fails. These budgets may reject an otherwise valid large input; adjust deliberately. No sampling or partial data dashboard is produced.\n\n## Select from the availability heatmap\n\nThe **Observation coverage** grid shows every site and date in the captured snapshot, even outside the active filter. Color means observed or missing; it does not encode count magnitude. Measured **0** is observed and `NA` is missing. Tooltips and accessible labels report each site's date, count or N/A and observation state. Active cells are highlighted while the full context stays visible. Snapshot coverage stays visible in this grid; selected range coverage remains in the status and summary table.\n\nClick a cell to focus its site and that single day; `Ctrl/Cmd/Shift-click` toggles that site and still focuses the day while keeping other selected sites. Row header buttons focus or modifier toggle a site without changing the date range. Date inputs and the site checklist highlight the corresponding grid cells and update the map, bars, trends and summary together. Arrow keys and Home/End move focus; Enter or Space activates the focused cell. Large calendars scroll within the grid's table.\n\n## Brush a date range and several sites\n\nWith a mouse, drag a rectangle across coverage cells. Release to select every included site row and the inclusive date window together. Reverse dragging gives the same bounds; missing and measured zero cells remain selectable. The map, bars, trends and summary table update only on release. The grid keeps the rest of the snapshot visible.\n\n`Ctrl/Cmd/Shift-drag` adds the rectangle's rows to the sites selected before the gesture and focuses its date window. Modifier click still toggles one site. The preview leaves existing filters unchanged; Escape during the drag or pointer cancellation discards it. Hold near or beyond a table edge to reveal more dates or sites. Releasing outside ends at the nearest visible cell at the current scroll position. Cancellation preserves the filters, while the table may remain scrolled. Touch and pen keep normal table scrolling and single cell taps. The date inputs, checklist, row buttons and keyboard cell controls remain alternatives to dragging.\n\nNo analysis code changes are needed. Rerun with the updated native runtime and, for `.anernb`, the matching Aner extension; then export a fresh HTML file to bundle brushing. Existing exported pages retain their captured controls. All interactions remain offline with no CSV reread, kernel request or additional pack; the point only example still needs no map pack. The static SVG preserves initial availability without drag controls.\n\nThe same native chart supplies this heatmap in `.aner` HTML export, VS Code `.anernb` and standard Jupyter. The initial static SVG includes availability too. Long calendars continue in stacked sections with repeated headers and section coverage; exact counts remain in cell titles when space is limited. No new analysis call or map pack is needed, and selecting cells never rereads the CSV or calls the kernel.\n"
    },
    {
      "cell_type": "code",
      "id": "dashboard-show",
      "metadata": {},
      "source": [
        "// No map pack is needed. Positions and daily counts come from the collected table.\n",
        "let dashboard = plot.site_dashboard_table(table)\n",
        "    .title(\"Boulder: explore an invented teaching week\")\n",
        "    .caption(\"Synthetic bicycle counts from plot_boulder_sites.csv, Oct 12-18 2026. Illustrative sites, not monitoring stations.\")\n",
        "    .size(width: 1000, height: 600)\n",
        "dashboard.show()\n"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "id": "dashboard-export",
      "metadata": {},
      "source": "## Export the same dashboard from Aner\n\nThe three code cells above are byte identical in `.anernb` and `.ipynb` and, separated by blank lines, form the analysis prefix of `plot_site_dashboard.aner`. The source adds the following export. In this notebook, run it in a new code cell when you want to save a page, using a fresh local filename:\n\n```aner\ndashboard.save(\"boulder-dashboard.html\")\n```\n\nFrom the project root run the source with `aner run examples/plot_site_dashboard.aner`. The CSV must be present only when executing the analysis; the exported page contains the validated snapshot and never fetches the CSV. Editing the CSV does not change an existing chart or HTML page: rerun the analysis and export a fresh filename. Open the generated HTML in a browser or share it as one file. No separate authoring script, server, CDN, running Aner session or map installation is needed to view it. `dashboard.html()` returns the same standalone document as a String, and `dashboard.save(\"boulder-dashboard.svg\")` saves the full data static fallback. Temporary filter and viewport choices are not written back to the native chart or export.\n\nSaved notebook outputs contain the snapshot and initial SVG. Reopening `.anernb` with the matching renderer works without a kernel. In standard Jupyter, the Aner kernel supplies a sandboxed HTML iframe; frontend trust and script policies determine whether it runs. Trust only a notebook whose source and saved output you have reviewed. If interaction is unavailable, use the SVG fallback or open the exported HTML file.\n"
    },
    {
      "cell_type": "markdown",
      "id": "dashboard-map",
      "metadata": {},
      "source": "## Optional Colorado boundaries\n\nInstall only the matching `usa.co` map pack before constructing this variant. This code is optional so Run All above continues to work with no packs:\n\n```aner\nlet context = plot.map(\"usa.co.places\", keys: List(), values: List())\n    .extent(west: -105.34, south: 39.96, east: -105.20, north: 40.09)\n    .projection(\"lambert_azimuthal_equal_area\")\nlet withBoundaries = plot.site_dashboard_table(table, map: context)\n    .title(\"Boulder: synthetic counts with municipal context\")\n    .caption(\"Synthetic plot_boulder_sites.csv counts. Generalized Census 2025 municipal boundaries; no streets.\")\nwithBoundaries.show()\nwithBoundaries.save(\"boulder-boundaries.html\")\n```\n\nOnly construction reads the selected map pack. The exported HTML and saved scene embed the selected geometry, renderers and authored caption. Keep the data source in that caption. Recipients can view them without the CSV or packs; rerunning the boundary code still needs `usa.co`. Configure projection and extent on the base map before passing it into the recipe. This is one daily site dashboard recipe, not an arbitrary dashboard layout system or server callback framework.\n"
    }
  ],
  "metadata": {
    "kernelspec": {
      "name": "aner",
      "display_name": "Aner",
      "language": "aner"
    },
    "language_info": {
      "name": "aner",
      "file_extension": ".aner",
      "mimetype": "text/x-aner"
    },
    "aner": {
      "status": "example",
      "execution_mode": "persistent-session"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}
