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 6-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)
Fixed Width mark_line inside a 480 px-wide container True (resize regression test)

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. Must start with 'streamlit-' — Snowsight only routes messages to Streamlit-in-Snowflake apps whose peer matches this prefix. 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.6.tar.gz (481.3 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.6-py3-none-any.whl (418.8 kB view details)

Uploaded Python 3

File details

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

File metadata

File hashes

Hashes for snowflake_streamlit_snap_and_ask-0.0.6.tar.gz
Algorithm Hash digest
SHA256 0ec561cc7ef2e9450ee4ab4d15b02bf235b4b964c3a20e21d37e193cdc15bdc6
MD5 b8be2810dc5731a8500ecc671bc3e6af
BLAKE2b-256 e099b8fc1edf77d883c210f2e0ec3d9de58cf1f0d484fc0a68a66de0a75b5946

See more details on using hashes here.

Provenance

The following attestation bundles were made for snowflake_streamlit_snap_and_ask-0.0.6.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.6-py3-none-any.whl.

File metadata

File hashes

Hashes for snowflake_streamlit_snap_and_ask-0.0.6-py3-none-any.whl
Algorithm Hash digest
SHA256 5db7adcd1fe1f6e23d30e9ad48e1e231b132c91380db36aab7404a9e43ab9732
MD5 d1a29709e646e7bc8bea5115158a41e8
BLAKE2b-256 24e1cd071bfc4f1b4dbb7a120108ba36193e7af4a80d9aee89c4ed0a9bb626cc

See more details on using hashes here.

Provenance

The following attestation bundles were made for snowflake_streamlit_snap_and_ask-0.0.6-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