Skip to main content

Snap-and-Ask chart wrapper for Streamlit with brush selection and AI chat integration

Project description

streamlit-snap-and-ask

Python 3.9+ Streamlit 1.36+ Altair 5+ License: MIT Tests

A drop-in Streamlit custom component that wraps st.altair_chart with a brush selection that flows back to Python, and an optional AI Action Bar ("Add to chat" / "Explain") for chat-driven analytics in Snowsight Streamlit-in-Snowflake.

from snowflake.streamlit.snap_and_ask import altair_chart

selection = altair_chart(my_chart, key="line", include_data_points=True)
if selection:
    st.write(f"{selection.start}{selection.end}")
    st.dataframe(selection.data_points)   # records inside the brush range

Why?

st.altair_chart renders a chart but doesn't tell Python which points the user selected. Common workarounds (e.g. streamlit-vega-lite) return raw Vega signal payloads with no consistent shape across chart types.

snap-and-ask:

  • Auto-injects an interval brush on the x-axis — works with any line / scatter / bar / histogram / area chart with no spec changes
  • Returns a typed AltairSelection — the bounds are always available; the underlying records are an opt-in so the wire payload stays small for big datasets
  • Ships an AI Action Bar styled with Snowflake's Stellar design system tokens for native look-and-feel inside Snowsight
  • Sends SNOWFLAKE_BRUSH_AI_ACTION events to the parent frame and broadcasts them on a same-origin BroadcastChannel, so the bar's "Add to chat" / "Explain" callbacks can be wired into a Snowsight Cortex Agent or validated locally without parent-frame access

Installation

pip install snowflake-streamlit-snap-and-ask

Python: 3.9+  ·  Streamlit: 1.36+ (auto-falls back from the v2 components API to v1 if running on older Streamlit / Python 3.9).


Quick start

import altair as alt
import pandas as pd
import streamlit as st
from snowflake.streamlit.snap_and_ask import altair_chart

df = pd.DataFrame({"ts": pd.date_range("2024-01-01", periods=90), "errors": ...})

chart = (
    alt.Chart(df)
    .mark_line()
    .encode(x="ts:T", y="errors:Q")
    .properties(height=260)
)

selection = altair_chart(chart, key="errors_chart")

if selection:
    st.write(f"Selected: **{selection.start}** → **{selection.end}**")

That's the full interaction loop — no callbacks, no manual signal listeners, no st.session_state plumbing.


Demo

A 5-tab gallery covering every chart type lives at sample/app.py:

Tab Chart include_data_points
Line mark_line over a temporal axis True
Scatter mark_circle colored by tier True (capped at 500)
Bar mark_bar on a nominal axis True
Histogram mark_bar with bin= transform True (explicit brush_field)
Area mark_area, bounds-only mode False (default — no records on the wire)

Run it locally:

npm install && npm run build
pip install -e .
ALTAIR_COMPONENT_DEV_DIR=./dist streamlit run sample/app.py

The demo also includes an AI Action Log panel that subscribes to the BroadcastChannel and shows every "Add to chat" / "Explain" click in real time — useful for verifying agent integration.


API reference

altair_chart(chart, **kwargs) -> AltairSelection | None

Parameter Type Default Description
chart alt.Chart | LayerChart | FacetChart required Any Altair chart object. Never mutated.
inject_brush bool True Auto-inject an interval param named brush on the x-axis. Set False if you've already defined one.
brush_field str | None None (auto-detect) Field to brush on. Required if auto-detection fails (layered/faceted charts, bin= transforms, etc.).
height int | None None Override chart height in pixels.
key str | None None Widget key. Required when rendering multiple charts on one page.
ai_context_enabled bool False When True, renders the AI Action Bar above an active brush selection.
ai_peer str | None auto Peer namespace for the AI message. Defaults to 'streamlit-{app_name}'.
explain_prompt str | None None Custom prompt sent on Explain. Falls back to a generic prompt.
ai_context_summary str | None None Override the auto-generated summary sent in AI action messages. Useful for providing domain-specific context to the AI agent.
include_data_points bool False When True, include the source records that fall within the brush range in selection.data_points. Off by default to keep the payload small.
max_data_points int | None None Cap the records returned in data_points. Records past the cap are dropped. Ignored if include_data_points=False.

Returns AltairSelection while a brush is active, None otherwise.

Raises ValueError if brush_field cannot be auto-detected and was not supplied.

AltairSelection

@dataclass
class AltairSelection:
    field: str                                 # e.g. "ts"
    field_type: Literal["T", "Q", "O", "N"]    # T=temporal Q=quant O=ordinal N=nominal
    start: datetime | float                    # datetime when field_type == "T"
    end: datetime | float
    data_points: list[dict[str, Any]]          # filtered records (opt-in)
  • For temporal fields the bounds are timezone-aware datetime objects (UTC).
  • For all other fields the bounds are float.
  • data_points is empty [] unless include_data_points=True and the chart spec contains inline data (Altair 5+ named-dataset format and inline data.values are both supported). Specs that load remote URL data return empty; do the filter in Python with the bounds.

AI Action Bar

When ai_context_enabled=True, an "Add to chat" / "Explain" action bar slides in next to the active brush selection. Clicking either action:

  1. Posts a SNOWFLAKE_BRUSH_AI_ACTION message to window.parent (this is the channel Snowsight relays to its AI agent in production).
  2. Publishes the same message on a same-origin BroadcastChannel('snap-and-ask:ai-actions') so sibling iframes (e.g. an in-app log panel, an integration test harness) can observe the action without parent-frame access.

Message shape:

{
  "type": "SNOWFLAKE_BRUSH_AI_ACTION",
  "payload": {
    "actionId": "add-to-chat" | "explain",
    "peer": "streamlit-my_app",
    "displayText": "Brush on ts: 2024-01-15 → 2024-02-03",
    "summary": "Selected 20 records with field 'ts' between 2024-01-15 and 2024-02-03 …",
    "explainPrompt": "Explain why error rates may have changed over this period."
  }
}

To listen from a sibling Streamlit container (the pattern used by the demo's AI Action Log):

const channel = new BroadcastChannel('snap-and-ask:ai-actions');
channel.onmessage = (ev) => {
  if (ev.data?.type === 'SNOWFLAKE_BRUSH_AI_ACTION') {
    console.log(ev.data.payload);
  }
};

The exported constant SNAP_AND_ASK_BROADCAST_CHANNEL is also available from the JS bundle for downstream consumers.

Stellar design system

The bar is styled to match Snowflake's Stellar design system using canonical token values (validated via stellar-mcp):

Token Value Used for
baltoTheme.surfaceLevel_2Background #fbfbfb Bar background (light)
baltoTheme.surfaceLevel_2Border #d5dae4 Bar border (light)
baltoTheme.reusableTextPrimary #1e252f Text
baltoTheme.elevation_1BoxShadow 0 2px 4px rgba(25,30,36,0.1) Elevation
baltoTheme.reusableBackgroundRowHover #eceef1 Action hover
radius-md, space-gap-2xs, space-vertical-2xs, space-horizontal-sm 8px, 4px, 4px, 8px Spacing

Dark-mode tokens are applied automatically based on prefers-color-scheme, body luminance, and a DOM mutation observer for runtime theme switching.


Architecture

┌─────────────────────────── Python ───────────────────────────┐
│ snowflake.streamlit.snap_and_ask.altair_chart(chart, ...)    │
│   ├── chart.to_dict()                                        │
│   ├── _detect_brush_field()      ← auto x-axis detection     │
│   ├── _inject_brush_selection()  ← adds Vega-Lite param      │
│   └── components.v2/v1.component(data={spec, brushField,…})  │
└─────────────────┬─────────────────────────────────┬──────────┘
                  │ component data                  │ component value
                  ▼                                 ▲
┌─────────────────────────── React (iframe) ───────────────────┐
│ src/main.tsx              ← createRoot per parentElement     │
│ src/AltairChart.tsx       ← top-level component              │
│   ├── useVegaEmbed        ← vega-embed + brush signal        │
│   │     listener (filters records via filterRecordsByRange)  │
│   ├── useAltairBrushSummary  ← humanises selection           │
│   ├── useAltairAiAction      ← postMessage + BroadcastChannel│
│   ├── useDarkMode            ← prefers-color-scheme + DOM    │
│   │                            mutation observer             │
│   └── components/AiActionBar.tsx ← Stellar-aligned UI        │
└──────────────────────────────────────────────────────────────┘

Bundled as a single ES module (dist/main.js) via Vite library mode with React inlined, no external CDNs at runtime.


Development

Prerequisites

  • Python ≥ 3.9
  • Node.js ≥ 18, npm
  • (Optional) stellar-mcp for design-system validation

Setup

# Frontend
npm install
npm run build           # one-shot build → ./dist
npm run dev             # vite build --watch (rebuild on change)

# Python
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

Run the sample app

ALTAIR_COMPONENT_DEV_DIR=./dist streamlit run sample/app.py

The ALTAIR_COMPONENT_DEV_DIR env var points the component at your local dist/ build, bypassing the packaged install. Set both npm run dev and streamlit run in two terminals for hot-iteration.

Project layout

streamlit-snap-and-ask/
├── python/snowflake/streamlit/snap_and_ask/
│   ├── _altair_component.py     # public altair_chart() + component registration
│   ├── _altair.py               # spec inspection / brush injection
│   └── _types.py                # AltairSelection dataclass
├── src/
│   ├── main.tsx                 # React mount entry
│   ├── AltairChart.tsx          # root component
│   ├── components/AiActionBar.tsx
│   ├── hooks/                   # useVegaEmbed, useAltairAiAction, useDarkMode, …
│   ├── utils/filterRecordsByRange.ts   # inline + Altair 5+ named-dataset support
│   └── __tests__/               # vitest + Testing Library
├── public/index.html            # Streamlit v1 component shim
├── sample/app.py                # 5-tab chart gallery + AI Action Log
├── tests/                       # pytest
├── dist/                        # built component (npm run build)
├── pyproject.toml               # hatchling build, asset_dir = dist
├── vite.config.ts
└── vitest.config.ts

Testing

# Frontend (vitest + Testing Library + jsdom)
npm test                # watch
npm run test:ci         # one-shot

# Python (pytest)
pytest

Currently 57 frontend tests + 20 Python tests covering:

  • Brush field auto-detection across encoding shapes
  • Spec injection (idempotent; preserves existing params; works on layered/faceted charts)
  • Record filtering for both inline data.values and Altair 5+ named-dataset formats
  • Vega plot height resolution
  • AI action message shape and BroadcastChannel emission
  • Selection summary formatting for T / Q / O / N field types
  • Component-value lifecycle (mount, brush start, brush end, key change)

Snowflake / Streamlit-in-Snowflake notes

  • The component works in stock Streamlit and Streamlit-in-Snowflake (Snowsight) without code changes.
  • In Snowsight, the parent frame's SNOWFLAKE_BRUSH_AI_ACTION listener relays clicks to the active Cortex Agent.
  • Locally, sibling iframes can observe actions via BroadcastChannel('snap-and-ask:ai-actions') — no parent-frame access required.
  • The Stellar tokens used here are the canonical Balto values, so the bar visually matches Snowsight panels by default.

Contributing

Issues and PRs welcome. Please:

  1. Run npm run type-check, npm run test:ci, and pytest before opening a PR.
  2. Keep frontend changes scoped — most logic belongs in a hook or util with a colocated *.test.ts.
  3. Follow Stellar token values for any new Snowsight-facing UI; validate with stellar-mcp if you're adding colors / spacing.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

snowflake_streamlit_snap_and_ask-0.0.5.tar.gz (479.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

snowflake_streamlit_snap_and_ask-0.0.5-py3-none-any.whl (418.4 kB view details)

Uploaded Python 3

File details

Details for the file snowflake_streamlit_snap_and_ask-0.0.5.tar.gz.

File metadata

File hashes

Hashes for snowflake_streamlit_snap_and_ask-0.0.5.tar.gz
Algorithm Hash digest
SHA256 c31c0b45c756a68e92782c14937bc8f9f458469cefdd4bfe185df5eb9eb33ebc
MD5 6dcddb33a5705de5923d5a620df3382a
BLAKE2b-256 3799bce2ec37e741b1172221115ae52ec5ffc7c02b682292be2117457daab38e

See more details on using hashes here.

Provenance

The following attestation bundles were made for snowflake_streamlit_snap_and_ask-0.0.5.tar.gz:

Publisher: release.yml on streamlit/streamlit-snap-and-ask

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file snowflake_streamlit_snap_and_ask-0.0.5-py3-none-any.whl.

File metadata

File hashes

Hashes for snowflake_streamlit_snap_and_ask-0.0.5-py3-none-any.whl
Algorithm Hash digest
SHA256 0bb73f0970a953cfc128737a88d7ff0af5a3292ae426df75ca40e154bae12c56
MD5 813d85da3aba094e08e92935b29bc80a
BLAKE2b-256 faa84766b992dc316a5ebe09d49878710b98d9ed9118fe20304c8aad688e57b7

See more details on using hashes here.

Provenance

The following attestation bundles were made for snowflake_streamlit_snap_and_ask-0.0.5-py3-none-any.whl:

Publisher: release.yml on streamlit/streamlit-snap-and-ask

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page