Snap-and-Ask chart wrapper for Streamlit with brush selection and AI chat integration
Project description
streamlit-snap-and-ask
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_ACTIONevents to the parent frame and broadcasts them on a same-originBroadcastChannel, 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
datetimeobjects (UTC). - For all other fields the bounds are
float. data_pointsis empty[]unlessinclude_data_points=Trueand the chart spec contains inline data (Altair 5+ named-dataset format and inlinedata.valuesare 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:
- Posts a
SNOWFLAKE_BRUSH_AI_ACTIONmessage towindow.parent(this is the channel Snowsight relays to its AI agent in production). - 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-mcpfor 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.valuesand 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_ACTIONlistener 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:
- Run
npm run type-check,npm run test:ci, andpytestbefore opening a PR. - Keep frontend changes scoped — most logic belongs in a hook or util with a colocated
*.test.ts. - Follow Stellar token values for any new Snowsight-facing UI; validate with
stellar-mcpif 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file snowflake_streamlit_snap_and_ask-0.0.4.tar.gz.
File metadata
- Download URL: snowflake_streamlit_snap_and_ask-0.0.4.tar.gz
- Upload date:
- Size: 477.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
60affc7058abd1c24d566d2529d8362f9087d262043b67f0f6fa257e40c28dce
|
|
| MD5 |
1f2ffd203aa6b4169e58290935b7e090
|
|
| BLAKE2b-256 |
6f03d84cdf0f09236e21708ad8675edc906ff86f33e6722de8194a73e475b0f5
|
Provenance
The following attestation bundles were made for snowflake_streamlit_snap_and_ask-0.0.4.tar.gz:
Publisher:
release.yml on streamlit/streamlit-snap-and-ask
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
snowflake_streamlit_snap_and_ask-0.0.4.tar.gz -
Subject digest:
60affc7058abd1c24d566d2529d8362f9087d262043b67f0f6fa257e40c28dce - Sigstore transparency entry: 1549517646
- Sigstore integration time:
-
Permalink:
streamlit/streamlit-snap-and-ask@d85e07077f44ea58cbc024b4a4a7f1833654d8d3 -
Branch / Tag:
refs/tags/0.0.4 - Owner: https://github.com/streamlit
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d85e07077f44ea58cbc024b4a4a7f1833654d8d3 -
Trigger Event:
release
-
Statement type:
File details
Details for the file snowflake_streamlit_snap_and_ask-0.0.4-py3-none-any.whl.
File metadata
- Download URL: snowflake_streamlit_snap_and_ask-0.0.4-py3-none-any.whl
- Upload date:
- Size: 418.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8880b66a8519ed0ca2fc0534e4f4c41a81576156b64c7f1796389241c932425
|
|
| MD5 |
1ab97b7b0e166a8bc3b0edc12fe903ba
|
|
| BLAKE2b-256 |
da539dba10d955c54e407a02c982cb2754531c664bcbdf46fe7a1477413edea4
|
Provenance
The following attestation bundles were made for snowflake_streamlit_snap_and_ask-0.0.4-py3-none-any.whl:
Publisher:
release.yml on streamlit/streamlit-snap-and-ask
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
snowflake_streamlit_snap_and_ask-0.0.4-py3-none-any.whl -
Subject digest:
c8880b66a8519ed0ca2fc0534e4f4c41a81576156b64c7f1796389241c932425 - Sigstore transparency entry: 1549517714
- Sigstore integration time:
-
Permalink:
streamlit/streamlit-snap-and-ask@d85e07077f44ea58cbc024b4a4a7f1833654d8d3 -
Branch / Tag:
refs/tags/0.0.4 - Owner: https://github.com/streamlit
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d85e07077f44ea58cbc024b4a4a7f1833654d8d3 -
Trigger Event:
release
-
Statement type: