diff --git a/.codespell.ignore-words b/.codespell.ignore-words deleted file mode 100644 index e69de29b..00000000 diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml new file mode 100644 index 00000000..a2beeaac --- /dev/null +++ b/.github/workflows/docs-deploy.yml @@ -0,0 +1,77 @@ +name: Build and Deploy Docs to GitHub Pages + +on: + push: + branches: + - main + - bslv2-doc # v0: testing on feature branch + workflow_dispatch: + +# Grant GITHUB_TOKEN the permissions needed +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment +concurrency: + group: pages-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build BSL Docs + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: Install BSL Python dependencies + run: uv sync --extra examples --extra viz-altair + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: docs/package-lock.json + + - name: Install dependencies + working-directory: ./docs + run: npm ci + + - name: Build BSL data and static site + working-directory: ./docs + run: npm run build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./docs/dist + + deploy: + name: Deploy to GitHub Pages + # Deploy from main and bslv2-doc branches (v0: testing) + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/bslv2-doc' + needs: build + runs-on: ubuntu-latest + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dfdb1d8c..d561cd8c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: codespell additional_dependencies: - tomli - args: [--ignore-words=.codespell.ignore-words, --skip=*.ipynb] + args: ["--skip=*.ipynb,**/node_modules/**,**/package-lock.json", "--ignore-words-list=hastable"] - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.11.13 diff --git a/README.md b/README.md index 3b4ebed7..f743b3b4 100644 --- a/README.md +++ b/README.md @@ -1,1375 +1,70 @@ # Boring Semantic Layer (BSL) -The Boring Semantic Layer (BSL) is a lightweight semantic layer based on [Ibis](https://ibis-project.org/). +**A lightweight, Ibis-powered semantic layer that makes your data queryable by both humans and AI.** -**Key Features:** -- **Lightweight**: `pip install boring-semantic-layer` -- **Ibis-powered**: Built on top of [Ibis](https://ibis-project.org/), supporting any database engine that Ibis integrates with (DuckDB, Snowflake, BigQuery, PostgreSQL, and more) -- **MCP-friendly**: Perfect for connecting Large Language Models to structured data sources +BSL lets you define your data model once - dimensions, measures, and relationships - then query it with a simple, fluent API. Built on [Ibis](https://ibis-project.org/), it works with any database that Ibis supports (DuckDB, Snowflake, BigQuery, PostgreSQL, and more). +## Why BSL? -*This project is a joint effort by [xorq-labs](https://github.com/xorq-labs/xorq) and [boringdata](https://www.boringdata.io/).* - -We welcome feedback and contributions! +- **Define once, query anywhere**: Create semantic tables that abstract away SQL complexity +- **Built for AI agents**: Native [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) support lets LLMs query your data directly +- **Pure Python**: No DSL to learn - just Python and Ibis expressions +- **Instant visualization**: Built-in charting with Altair and Plotly backends -# Quick Example +## Quick Start -``` +```bash pip install 'boring-semantic-layer[examples]' ``` -**1. Define your ibis input table** - ```python import ibis +from boring_semantic_layer import to_semantic_table -# Create a simple in-memory table -flights_tbl = ibis.memtable({ - "origin": ["JFK", "LAX", "JFK", "ORD", "LAX"], - "carrier": ["AA", "UA", "AA", "UA", "AA"] -}) -``` - -**2. Define a semantic table** - -```python -from boring_semantic_layer.semantic_api import to_semantic_table - +# 1. Define your semantic model flights = ( to_semantic_table(flights_tbl, name="flights") .with_dimensions(origin=lambda t: t.origin) .with_measures(flight_count=lambda t: t.count()) ) -``` - -**3. Query it** -```python -flights.group_by("origin").aggregate("flight_count").execute() +# 2. Query it +result = flights.group_by("origin").aggregate("flight_count").execute() ``` -**Example output (dataframe):** - -| origin | flight_count | -| ------ | ------------ | -| JFK | 2 | -| LAX | 2 | -| ORD | 1 | - - ------ - -## Table of Contents - -- [Boring Semantic Layer (BSL)](#boring-semantic-layer-bsl) -- [Quick Example](#quick-example) -- [Installation](#installation) -- [Get Started](#get-started) - - [1. Get Sample Data](#1-get-sample-data) - - [2. Build a Semantic Table](#2-build-a-semantic-table) - - [Adding Descriptions to Dimensions and Measures](#adding-descriptions-to-dimensions-and-measures) - - [3. Query a Semantic Table](#3-query-a-semantic-table) -- [Features](#features) - - [Filters](#filters) - - [Advanced Queries](#advanced-queries) - - [Percent of Total](#percent-of-total) - - [Window Functions](#window-functions) - - [Nested Data with nest()](#nested-data-with-nest) - - [Joins Across Semantic Tables](#joins-across-semantic-tables) - - [join_one (Many-to-One Relationships)](#join_one-many-to-one-relationships) - - [join_many (One-to-Many Relationships)](#join_many-one-to-many-relationships) - - [join_cross (Cross Product)](#join_cross-cross-product) - - [YAML Configuration for Joins](#yaml-configuration-for-joins) - - [Dimensional Indexing](#dimensional-indexing) - - [Backward Compatibility: query() Method](#backward-compatibility-query-method) - - [Backward Compatibility: Time-Based Dimensions](#backward-compatibility-time-based-dimensions) -- [Model Context Protocol (MCP) Integration](#model-context-protocol-mcp-integration) - - [Installation](#installation-1) - - [Setting up an MCP Server](#setting-up-an-mcp-server) - - [Configuring Claude Desktop](#configuring-claude-desktop) - - [Available MCP Tools](#available-mcp-tools) -- [Chart Visualization](#chart-visualization) - - [Installation](#installation-2) - - [How BSL Charting Works](#how-bsl-charting-works) - - [Backend Selection](#backend-selection) - - [Smart Chart Creation](#smart-chart-creation) - - [1. Auto-detected Bar Chart](#1-auto-detected-bar-chart) - - [2. Auto-detected Time Series Chart](#2-auto-detected-time-series-chart) - - [3. Auto-detected Heatmap](#3-auto-detected-heatmap) - - [4. Custom Mark with Auto-detection](#4-custom-mark-with-auto-detection) - - [5. Full Custom Specification](#5-full-custom-specification) - - [Export Formats](#export-formats) -- [Reference](#reference) - - [SemanticTable API](#semantictable-api) - - [YAML Configuration Reference](#yaml-configuration-reference) - - [Chart API Reference](#chart-api-reference) - ------ - ## Installation ```bash # Basic installation pip install boring-semantic-layer -# For DuckDB support (used in examples) +# With DuckDB support (for examples) pip install 'boring-semantic-layer[examples]' -# For MCP integration +# With MCP integration pip install 'boring-semantic-layer[fastmcp]' -# For visualization with Altair -pip install 'boring-semantic-layer[viz-altair]' - -# For visualization with Plotly -pip install 'boring-semantic-layer[viz-plotly]' -``` - ------ - -## Get Started - -### 1. Get Sample Data - -We expose some test data in a public bucket. You can download it with: - -```bash -curl -L https://pub-a45a6a332b4646f2a6f44775695c64df.r2.dev/flights.parquet -o flights.parquet -curl -L https://pub-a45a6a332b4646f2a6f44775695c64df.r2.dev/carriers.parquet -o carriers.parquet -``` - -**Note:** Examples use DuckDB, so install with: `pip install 'boring-semantic-layer[examples]'` - -### 2. Build a Semantic Table - -Define your data source and create a semantic table that describes your data in terms of dimensions and measures. - -```python -import ibis -from boring_semantic_layer.semantic_api import to_semantic_table - -# Connect to your database (here, DuckDB in-memory for demo) -con = ibis.duckdb.connect(":memory:") -flights_tbl = con.read_parquet("flights.parquet") -carriers_tbl = con.read_parquet("carriers.parquet") - -# Define the semantic table -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions( - origin=lambda t: t.origin, - destination=lambda t: t.dest, - year=lambda t: t.year - ) - .with_measures( - total_flights=lambda t: t.count(), - total_distance=lambda t: t.distance.sum(), - avg_distance=lambda t: t.distance.mean(), - ) -) -``` - -- **Dimensions** are attributes to group or filter by (e.g., origin, destination). -- **Measures** are aggregations or calculations (e.g., total flights, average distance). - -All dimensions and measures are defined as Ibis expressions. - -Ibis expressions are Python functions that represent database operations. - -They allow you to write database queries using familiar Python syntax while Ibis handles the translation to optimized SQL for your specific database backend (like DuckDB, PostgreSQL, BigQuery, etc.). - -For example, in our semantic table: - -- `lambda t: t.origin` is an Ibis expression that references the "origin" column -- `lambda t: t.count()` is an Ibis expression that counts rows -- `lambda t: t.distance.mean()` is an Ibis expression that calculates the average distance - -The `t` parameter represents the table, and you can chain operations like `t.origin.upper()` or `t.dep_delay > 0` to create complex expressions. Ibis ensures these expressions are translated to efficient SQL queries. - -### Adding Descriptions to Dimensions and Measures - -BSL supports adding human-readable descriptions to dimensions and measures. This helps in documenting your data model and making it easier for others to understand and AI agents to interact with. - -You can define dimensions and measures with descriptions by passing a dict with `expr` and `description` keys: - -**Simple format:** -```python -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions(origin=lambda t: t.origin) - .with_measures(flight_count=lambda t: t.count()) -) -``` - -**With descriptions (dict format):** -```python -from boring_semantic_layer.semantic_api import to_semantic_table - -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions( - origin={ - "expr": lambda t: t.origin, - "description": "Origin airport where the flight departed from" - } - ) - .with_measures( - flight_count={ - "expr": lambda t: t.count(), - "description": "Total number of flights" - } - ) -) -``` - -**Why use descriptions?** -- **Human-readable**: Makes your models self-documenting for team members. -- **AI friendly**: Perfect for MCP agents and LLMs that need to understand your models in more detail and nuances between similar dimensions and measures. -- **Flexible**: You can mix simple lambdas and dict formats seamlessly. - -**YAML Configuration Support:** - -You can also define models with descriptions using YAML configuration files: - -```yaml -flights: - table: flights_tbl - description: "Flight data with departure and arrival information" - - dimensions: - # Simple format - origin: _.origin - - # With description - destination: - expr: _.destination - description: "Destination airport code where the flight arrived at" - - measures: - # Simple format - flight_count: _.count() - - # With description - avg_distance: - expr: _.distance.mean() - description: "Average distance of flights in miles" -``` - -Load the YAML model: -```python -from boring_semantic_layer.semantic_api import from_yaml - -models = from_yaml("flights_model.yml", tables={"flights_tbl": flights_tbl}) -flights = models["flights"] +# With visualization support +pip install 'boring-semantic-layer[viz-altair]' # or viz-plotly ``` --- -### 3. Query a Semantic Table - -Use your semantic table to run queries—grouping by dimensions, aggregating measures, and applying filters or limits. - -**Basic query:** -```python -flights.group_by('origin').aggregate('total_flights', 'avg_distance').limit(10).execute() -``` - -**Example output:** - -| origin | total_flights | avg_distance | -| ------ | ------------- | ------------ | -| JFK | 3689 | 1047.71 | -| PHL | 7708 | 1044.97 | -| ... | ... | ... | - -**On-the-fly transformations:** - -You can add computed dimensions and measures directly in your queries without modifying the semantic table: - -```python -from ibis import _ - -# Transform dimensions on-the-fly in group_by -result = ( - flights - .group_by( - origin_state=lambda t: t.origin[:2], # First 2 chars of origin - is_long_haul=lambda t: t.distance > 1000 # Boolean dimension - ) - .aggregate('total_flights', 'avg_distance') - .execute() -) -``` - -**Example output:** - -| origin_state | is_long_haul | total_flights | avg_distance | -| ------------ | ------------ | ------------- | ------------ | -| JF | True | 2450 | 1547.32 | -| JF | False | 1239 | 548.10 | -| PH | True | 5234 | 1344.21 | -| ... | ... | ... | ... | - -**Transform measures on-the-fly in aggregate:** - -```python -from ibis import _ - -# Add computed measures directly in aggregate -result = ( - flights - .group_by('origin') - .aggregate( - 'total_flights', # Use existing measure - 'avg_distance', # Use existing measure - total_miles=lambda t: t.distance.sum(), # Add new measure on-the-fly - flight_density=lambda t: t.count() / t.origin.nunique() # Complex calculation - ) - .limit(5) - .execute() -) -``` - -**Example output:** - -| origin | total_flights | avg_distance | total_miles | flight_density | -| ------ | ------------- | ------------ | ----------- | -------------- | -| JFK | 3689 | 1047.71 | 3865221 | 23.1 | -| PHL | 7708 | 1044.97 | 8054836 | 48.2 | -| ... | ... | ... | ... | ... | - -**Combine both approaches:** - -```python -# Transform both dimensions and measures in a single query -result = ( - flights - .filter(lambda t: t.year == 2024) - .group_by( - month=lambda t: t.date.month(), - is_domestic=lambda t: t.origin == t.dest - ) - .aggregate( - 'total_flights', - avg_delay_minutes=lambda t: t.dep_delay.mean(), - pct_on_time=lambda t: (t.dep_delay <= 0).mean() * 100 - ) - .order_by(_.month) - .execute() -) -``` - -**Post-aggregation transformations with `mutate()`:** - -After aggregating, you can use `mutate()` to add derived columns based on aggregated results: - -```python -from ibis import _ - -# Add post-aggregation calculations -result = ( - flights - .group_by('origin', 'destination') - .aggregate( - 'total_flights', - 'avg_distance' - ) - .mutate( - flights_per_100_miles=lambda t: (t.total_flights / t.avg_distance) * 100, - route_label=lambda t: t.origin + ' → ' + t.destination, - distance_category=lambda t: ( - _.avg_distance - .case() - .when(_.avg_distance < 500, "short") - .when(_.avg_distance < 1500, "medium") - .else_("long") - .end() - ) - ) - .order_by(_.flights_per_100_miles.desc()) - .limit(10) - .execute() -) -``` - -**Example output:** - -| origin | destination | total_flights | avg_distance | flights_per_100_miles | route_label | distance_category | -| ------ | ----------- | ------------- | ------------ | --------------------- | ----------- | ----------------- | -| LAX | SFO | 4523 | 337 | 1342.13 | LAX → SFO | short | -| JFK | BOS | 3891 | 187 | 2080.75 | JFK → BOS | short | -| ATL | MCO | 2145 | 404 | 530.94 | ATL → MCO | short | -| ... | ... | ... | ... | ... | ... | ... | - -**💡 Key difference:** -- `.group_by()` + `.aggregate()`: Compute aggregations from raw data -- `.mutate()`: Transform aggregated results (works on the already-aggregated dataframe) - -For more transformations, see [Ibis Table API reference](https://ibis-project.org/reference/expression-tables.html#ibis.expr.types.relations.Table.mutate). - -This approach lets you: -- ✅ Keep your base semantic table clean and reusable -- ✅ Add context-specific calculations without polluting the model -- ✅ Mix predefined measures with ad-hoc calculations -- ✅ Create complex analytical queries quickly -- ✅ Transform aggregated results using the full power of Ibis - ------ - -## Features - -### Filters - -You can filter data using raw Ibis expressions for full flexibility: - -```python -result = ( - flights - .filter(lambda t: t.origin.isin(['JFK', 'LGA', 'PHL'])) - .group_by('origin') - .aggregate('total_flights') - .execute() -) -``` - -**Example output:** - -| origin | total_flights | -| ------ | ------------- | -| JFK | 3689 | -| LGA | 7000 | -| PHL | 7708 | - -### Advanced Queries - -#### Percent of Total - -Define calculated measures using `.all()` for percent-of-total calculations: - -```python -from ibis import _ - -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions(carrier=lambda t: t.carrier) - .with_measures( - flight_count=lambda t: t.count(), - market_share=lambda t: t.count() / t.all(t.count()) * 100 - ) -) - -result = ( - flights - .group_by("carrier") - .aggregate("flight_count", "market_share") - .order_by(_.market_share.desc()) - .execute() -) -``` - -**Output:** - -| carrier | flight_count | market_share | -| ------- | ------------ | ------------ | -| AA | 3500 | 35.0 | -| UA | 3200 | 32.0 | -| DL | 3300 | 33.0 | - -#### Window Functions - -Use window functions in measures for running calculations: - -```python -import ibis - -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions(date=lambda t: t.date) - .with_measures( - flight_count=lambda t: t.count(), - rolling_avg=lambda t: t.count().mean().over( - ibis.window(order_by="date", rows=(1, 1)) - ) - ) -) - -result = flights.group_by("date").aggregate("flight_count", "rolling_avg").execute() -``` - -#### Nested Data with nest() - -The `nest()` operator allows you to preserve row-level detail within aggregated results by creating nested data structures. This enables advanced patterns like multi-stage aggregations where you need to access granular data after an initial grouping. - -**How it works:** - -When you include `nest` in an aggregation, BSL creates a nested column containing the grouped data: - -```python -result = ( - table - .group_by("category") - .aggregate( - "total_amount", - nest={"detail": lambda t: t.group_by(["id", "value"])} - ) -) -``` - -**Example output:** - -| category | total_amount | detail | -| -------- | ------------ | ----------------------------------------- | -| A | 1500 | [{"id": 1, "value": 100}, {"id": 2, ...}] | -| B | 2300 | [{"id": 3, "value": 200}, {"id": 4, ...}] | - -The nested column (`detail` in this example) can then be used in subsequent operations: -- Access nested data using dot notation (e.g., `_.detail.count()`, `_.detail.value.mean()`) -- Combine with `mutate()` to add computed columns before re-aggregating -- Enable "Top N with Other" bucketing patterns where you rank groups, then re-aggregate them - -**Use cases:** -- **Top N with rollup**: Show top items individually, group the rest as "Other" -- **Multi-stage aggregation**: Aggregate at one level, then re-aggregate based on computed rankings or categories -- **Preserving detail**: Keep access to row-level data for downstream calculations after initial grouping - -This pattern is inspired by [Malloy's bucketing with "Other"](https://docs.malloydata.dev/documentation/patterns/other). - -For a complete working example showing the "Top N with Other" pattern, see [examples/bucketing_with_other.py](examples/bucketing_with_other.py). - -### Joins Across Semantic Tables - -BSL allows you to join multiple semantic tables to enrich your data. Joins use a fluent API inspired by [Malloy](https://docs.malloydata.dev/documentation/language/join). - -#### join_one (Many-to-One Relationships) - -Use `join_one()` for many-to-one or one-to-one relationships: - -```python -from boring_semantic_layer.semantic_api import to_semantic_table -import ibis - -con = ibis.duckdb.connect(":memory:") -flights_tbl = con.read_parquet("flights.parquet") -carriers_tbl = con.read_parquet("carriers.parquet") - -# Define the carriers semantic table -carriers = ( - to_semantic_table(carriers_tbl, name="carriers") - .with_dimensions( - code=lambda t: t.code, - name=lambda t: t.name, - nickname=lambda t: t.nickname, - ) - .with_measures( - carrier_count=lambda t: t.count(), - ) -) - -# Define the flights semantic table -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions( - origin=lambda t: t.origin, - destination=lambda t: t.destination, - carrier=lambda t: t.carrier, - ) - .with_measures( - flight_count=lambda t: t.count(), - ) -) - -# Join flights with carriers (many-to-one relationship) -flights_with_carriers = flights.join_one( - carriers, - left_on="carrier", - right_on="code" -) - -# Query using joined fields -# Simple column names work when there's no conflict -result = ( - flights_with_carriers - .group_by("name", "origin") # "name" comes from carriers, "origin" from flights - .aggregate("flight_count") - .limit(10) - .execute() -) -``` - -**Example output:** - -| name | origin | flight_count | -| -------------------------- | ------ | ------------ | -| Delta Air Lines | MDT | 235 | -| Delta Air Lines | ATL | 8419 | -| Comair (Delta Connections) | ATL | 239 | -| American Airlines | DFW | 8742 | -| American Eagle Airlines | JFK | 418 | - -**Handling Name Conflicts:** - -When there are naming conflicts between joined tables, you can use the prefixed format with '.': - -```python -# If both tables have a "name" column, use prefixes to disambiguate -result = ( - flights_with_carriers - .group_by("carriers.name", "flights.origin") - .aggregate("flights.flight_count") - .execute() -) -``` - -- Use simple column names (`name`, `flight_count`) when there's no conflict -- Use prefixed names (`carriers.name`, `flights.flight_count`) only when needed to resolve ambiguity - -#### join_many (One-to-Many Relationships) - -Use `join_many()` for one-to-many relationships: - -```python -customer_orders = customers.join_many( - orders, - left_on="customer_id", - right_on="customer_id" -) -``` - -#### join_cross (Cross Product) - -Use `join_cross()` to create a cross product (every row from left joined with every row from right): - -```python -full_combinations = table_a.join_cross(table_b) -``` - -#### YAML Configuration for Joins - -You can also define joins in YAML configuration files: - -```yaml -carriers: - table: carriers_tbl - dimensions: - code: _.code - name: _.name - measures: - carrier_count: _.count() - -flights: - table: flights_tbl - dimensions: - origin: _.origin - carrier: _.carrier - measures: - flight_count: _.count() - joins: - carriers: - model: carriers - type: one # Can be: one, many, or cross - left_on: carrier - right_on: code -``` - -Load and use: -```python -from boring_semantic_layer.semantic_api import from_yaml - -models = from_yaml("models.yml", tables={ - "flights_tbl": flights_tbl, - "carriers_tbl": carriers_tbl -}) - -flights = models["flights"] # Already has the join configured! -``` - -### Backward Compatibility: query() Method - -For backward compatibility with existing code, BSL supports the legacy `.query()` API: - -```python -from boring_semantic_layer.api import query - -result = query( - flights, - dimensions=["origin"], - measures=["flight_count"], - filters=[lambda t: t.origin == "JFK"], - limit=10 -).execute() -``` - -This is equivalent to the fluent API: - -```python -result = ( - flights - .filter(lambda t: t.origin == "JFK") - .group_by("origin") - .aggregate("flight_count") - .limit(10) - .execute() -) -``` - -**Note:** The fluent API (`.group_by().aggregate()`) is recommended for new projects. - -### Dimensional Indexing - -Dimensional indexing creates a searchable catalog of all unique values across your dimensions. This is useful for data exploration, building autocomplete features, and understanding data distributions. Inspired by [Malloy's index pattern](https://docs.malloydata.dev/documentation/patterns/dim_index). - -**Basic Usage:** - -```python -import ibis.selectors as s - -# Index all dimensions -index_all = airports.index(s.all()).execute() - -# Index specific fields with custom weight -high_elevation_cities = ( - airports.index(s.cols("city", "state"), by="avg_elevation") - .order_by(lambda t: t.weight.desc()) - .limit(5) - .execute() -) -``` - -**Example output:** - -| fieldName | fieldValue | fieldType | weight | -| --------- | ------------- | --------- | ------- | -| city | Leadville | string | 9927.0 | -| city | Telluride | string | 9078.0 | -| state | CO | string | 8500.5 | -| city | Eagle | string | 6548.0 | -| state | WY | string | 6234.2 | - -**Autocomplete Use Case:** - -```python -# Get city suggestions starting with "SAN" -suggestions = ( - airports.index(s.cols("city")) - .filter(lambda t: t.fieldValue.like("SAN%")) - .order_by(lambda t: t.weight.desc()) - .limit(10) - .execute() -) -``` - -For more examples including data profiling, search patterns, and indexing across joins, see [examples/dimensional_indexing.py](examples/dimensional_indexing.py). - -### Backward Compatibility: Time-Based Dimensions - -BSL supports marking dimensions as time dimensions with an optional `smallest_time_grain`. This is mostly supported to keep time aggregation possible with the legacy query() method. - -**Python API (dict format):** -```python -from boring_semantic_layer.semantic_api import to_semantic_table - -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions( - origin=lambda t: t.origin, - arr_time={ - "expr": lambda t: t.arr_time, - "description": "Arrival timestamp", - "is_time_dimension": True, - "smallest_time_grain": "TIME_GRAIN_DAY" - } - ) - .with_measures(total_flights=lambda t: t.count()) -) -``` -Supported time grains: `TIME_GRAIN_SECOND`, `TIME_GRAIN_MINUTE`, `TIME_GRAIN_HOUR`, `TIME_GRAIN_DAY`, `TIME_GRAIN_WEEK`, `TIME_GRAIN_MONTH`, `TIME_GRAIN_QUARTER`, `TIME_GRAIN_YEAR` - -Note: The 'v2' way to do time aggregation is via dimension transformation: - -```python -result = ( - flights - .group_by(flight_year=lambda t: t.arr_time.year()) - .aggregate("total_flights") - .execute() -) -``` - -## Model Context Protocol (MCP) Integration - -BSL includes built-in support for the [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol/python-sdk), allowing you to expose your semantic models to LLMs like Claude. - -**💡 Pro tip:** Use [descriptions in dimensions and measures](#adding-descriptions-to-dimensions-and-measures) to make your models more AI-friendly. Descriptions help provide context to LLMs, enabling them to understand what each field represents and when to use them. - -### Installation - -To use MCP functionality, install with the `mcp` extra: - -```bash -pip install 'boring-semantic-layer[fastmcp]' -``` - -### Setting up an MCP Server - -Create an MCP server script that exposes your semantic models: - -```python -# example_mcp.py -import ibis -from boring_semantic_layer.semantic_api import to_semantic_table -from boring_semantic_layer.api.mcp import MCPSemanticModel - -# Connect to your database -con = ibis.duckdb.connect(":memory:") -flights_tbl = con.read_parquet("path/to/flights.parquet") - -# Define your semantic table -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions( - origin=lambda t: t.origin, - destination=lambda t: t.dest, - carrier=lambda t: t.carrier, - ) - .with_measures( - total_flights=lambda t: t.count(), - avg_distance=lambda t: t.distance.mean(), - ) -) - -# Create and run the MCP server -mcp_server = MCPSemanticModel( - models={"flights": flights}, - name="Flight Data Server" -) - -if __name__ == "__main__": - mcp_server.run(transport="stdio") -``` - -### Configuring Claude Desktop - -To use your MCP server with Claude Desktop, add it to your configuration file: - -**Location:** `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) - -```json -{ - "mcpServers": { - "flight_sm": { - "command": "uv", - "args": [ - "--directory", - "/path/to/your/project/examples/", - "run", - "example_mcp.py" - ] - } - } -} -``` - -Replace `/path/to/your/project/` with the actual path to your project directory. - -### Available MCP Tools - -Once configured, Claude will have access to these tools: - -- `list_models`: List all available semantic model names -- `get_model`: Get details about a specific model including dimensions and measures -- `get_time_range`: Get the available time range for time-series data -- `query_model`: Execute queries with dimensions, measures, and filters - - When `chart_spec` is provided, returns both data and chart: `{"records": [...], "chart": {...}}` - - When `chart_spec` is not provided, returns only data: `{"records": [...]}` - -For more information on running MCP servers, see the [MCP Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk). - -## Chart Visualization - -BSL includes built-in support for generating data visualizations using native Ibis-Altair integration. This allows you to create Altair charts directly from Ibis expressions without converting to pandas DataFrames first. - -### Installation - -To use chart visualization functionality, install with the `visualization` extra: - -To use `altair` backend: -```bash -pip install 'boring-semantic-layer[viz-altair]' -``` - -To use `plotly` backend: -```bash -pip install 'boring-semantic-layer[viz-plotly]' -``` -### How BSL Charting Works - -BSL's charting system features **dual backend support**, allowing you to choose between two powerful visualization libraries: - -- **[Altair](https://altair-viz.github.io/)** (default): Built on **[Vega-Lite](https://vega.github.io/vega-lite/)**, a JSON-based grammar for creating interactive web-native visualizations with a declarative approach -- **[Plotly](https://plotly.com/python/)**: Rich interactive plotting library with extensive chart types and dashboard integration capabilities - -You can switch backends using the `backend` parameter: `chart(backend="altair")` or `chart(backend="plotly")`. - -BSL supports multiple output formats including interactive charts, static images (PNG/SVG), and JSON specifications for web embedding across both backends. - -#### Quick Start Example - -Here's a minimal example showing how to create a chart with custom styling: - -```python -from boring_semantic_layer.semantic_api import to_semantic_table -import ibis - -con = ibis.duckdb.connect(":memory:") -flights_tbl = con.read_parquet("flights.parquet") - -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions(origin=lambda t: t.origin) - .with_measures(flight_count=lambda t: t.count()) -) - -# Query with custom styling -chart = ( - flights - .group_by("origin") - .aggregate("flight_count") - .limit(5) - .chart(spec={ - "mark": {"type": "bar", "color": "steelblue"}, - "title": "Flights by Origin" - }) -) -``` - -![Quick Start Chart](docs/chart_quickstart.png) - -#### How It Works - -BSL exposes a `chart()` method on query results that accepts a Vega-Lite JSON specification and returns charts in various formats: - -- **Auto-detection**: If you don't provide a spec, BSL automatically selects the best chart type -- **Partial specs**: Provide only what you want to customize, BSL fills in the rest -- **Multiple formats**: Output as Altair objects, PNG/SVG images, or JSON specifications - -This design enables you to work at any level of abstraction - from full auto-detection to complete manual control. - -### Backend Selection - -BSL supports two charting backends: - -- **Altair** (default): `chart(backend="altair")` -- **Plotly**: `chart(backend="plotly")` - -```python -# Altair backend (default) - uses Vega-Lite spec format -altair_chart = query.chart() # or chart(backend="altair") -altair_custom = query.chart(spec={"mark": "bar", "title": "My Chart"}) - -# Plotly backend - uses BSL custom spec format -plotly_chart = query.chart(backend="plotly") -plotly_custom = query.chart(backend="plotly", spec={ - "chart_type": "scatter", # Maps to px.scatter() function - "layout": {"title": "My Chart"}, # Plotly layout options - "color": "category" # Plotly Express parameters -}) -``` - -**Spec Format Differences:** -- **Altair**: Uses standard [Vega-Lite specification](https://vega.github.io/vega-lite/docs/spec.html) format -- **Plotly**: Uses BSL's custom format combining: - - `chart_type`: Maps to Plotly Express functions (`px.bar`, `px.line`, `px.scatter`, etc.) - - `layout`: Standard [Plotly layout](https://plotly.com/python/reference/layout/) options - - Other keys: [Plotly Express parameters](https://plotly.com/python-api-reference/plotly.express.html) - -### Smart Chart Creation - -BSL automatically detects appropriate chart types and intelligently merges any specifications you provide. - -BSL's detection logic: -- **Time series** (time dimension + measure) → Line chart with time-grain aware formatting -- **Categorical** (1 dimension + 1 measure) → Bar chart -- **Multiple measures** → Multi-series chart with automatic color encoding -- **Two dimensions** → Heatmap -- **Multiple dimensions with time** → Multi-line chart colored by dimension - -Here are examples showing different chart types and customization options: - -#### 1. Auto-detected Bar Chart - -BSL automatically creates a bar chart for categorical data: - -```python -from boring_semantic_layer.semantic_api import to_semantic_table -from ibis import _ - -# Assuming flights table is already defined -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions(destination=lambda t: t.destination) - .with_measures(flight_count=lambda t: t.count()) -) - -# Query top destinations by flight count -query = ( - flights - .group_by("destination") - .aggregate("flight_count") - .order_by(_.flight_count.desc()) - .limit(10) -) - -# Auto-detects bar chart (Altair) -altair_chart = query.chart() - -# Auto-detects bar chart (Plotly) -plotly_chart = query.chart(backend="plotly") -``` - -![Bar Chart](docs/chart_bar.png) - -#### 2. Auto-detected Time Series Chart - -For time-based queries, BSL automatically creates line charts with proper time formatting: - -```python -# Define flights with time dimension -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions( - arr_time={ - "expr": lambda t: t.arr_time, - "is_time_dimension": True, - "smallest_time_grain": "TIME_GRAIN_DAY" - } - ) - .with_measures(flight_count=lambda t: t.count()) -) - -# Time series query -time_query = ( - flights - .filter(lambda t: t.arr_time.between("2003-01-01", "2003-03-31")) - .group_by("arr_time") - .aggregate("flight_count") -) - -# Auto-detects time series line chart (Altair) -altair_chart = time_query.chart() - -# Auto-detects time series line chart (Plotly) -plotly_chart = time_query.chart(backend="plotly") -``` - -![Time Series Chart](docs/chart_timeseries.png) - -#### 3. Auto-detected Heatmap - -When querying two categorical dimensions with a measure, BSL creates a heatmap: - -```python -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions( - destination=lambda t: t.destination, - origin=lambda t: t.origin - ) - .with_measures(flight_count=lambda t: t.count()) -) - -# Two dimensions create a heatmap -heatmap_query = ( - flights - .group_by("destination", "origin") - .aggregate("flight_count") - .limit(50) -) - -# Auto-detects heatmap with custom sizing (Altair) -altair_chart = heatmap_query.chart(spec={ - "height": 300, - "width": 400 -}) - -# Auto-detects heatmap (Plotly) -plotly_chart = heatmap_query.chart(backend="plotly") -``` - -![Heatmap Chart](docs/chart_heatmap.png) - -#### 4. Custom Mark with Auto-detection - -Mix your preferences with BSL's auto-detection by specifying only what you want to change: - -```python -from ibis import _ - -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions(destination=lambda t: t.destination) - .with_measures(avg_distance=lambda t: t.distance.mean()) -) - -# Change only the mark type, keep auto-detected encoding -line_query = ( - flights - .group_by("destination") - .aggregate("avg_distance") - .order_by(_.avg_distance.desc()) - .limit(15) -) - -# Just change to line chart, encoding auto-detected -chart = line_query.chart(spec={"mark": "line"}) -``` - -![Line Chart](docs/chart_line.png) +## 📚 Documentation -#### 5. Full Custom Specification - -For complete control, specify everything you need: - -```python -from ibis import _ - -# Assuming carriers table and join are already defined -carriers = ( - to_semantic_table(carriers_tbl, name="carriers") - .with_dimensions( - code=lambda t: t.code, - name=lambda t: t.name - ) - .with_measures(carrier_count=lambda t: t.count()) -) - -flights = ( - to_semantic_table(flights_tbl, name="flights") - .with_dimensions(carrier=lambda t: t.carrier) - .with_measures(flight_count=lambda t: t.count()) -) - -flights_with_carriers = flights.join_one(carriers, left_on="carrier", right_on="code") - -# Full custom specification -custom_query = ( - flights_with_carriers - .group_by("name") - .aggregate("flight_count") - .order_by(_.flight_count.desc()) - .limit(8) -) - -# Complete custom chart specification -chart = custom_query.chart(spec={ - "title": "Top Airlines by Flight Count", - "mark": {"type": "bar", "color": "steelblue"}, - "encoding": { - "x": {"field": "name", "type": "nominal", "sort": "-y"}, - "y": {"field": "flight_count", "type": "quantitative"} - }, - "width": 500, - "height": 300 -}) -``` - -![Custom Chart](docs/chart_custom.png) - -#### Export Formats - -BSL supports multiple export formats: - -```python -# Different export formats -altair_chart = query.chart() # Altair Chart object (default) -interactive = query.chart(format="interactive") # With interactive tooltips -json_spec = query.chart(format="json") # Vega-Lite specification -png_bytes = query.chart(format="png") # PNG image (requires altair[all]) -svg_str = query.chart(format="svg") # SVG markup (requires altair[all]) - -# Save as file -with open("my_chart.png", "wb") as f: - f.write(png_bytes) -``` - -## Reference - -### SemanticTable API - -The new semantic API provides a fluent interface for building and querying semantic tables. - -#### Creating a Semantic Table - -```python -from boring_semantic_layer.semantic_api import to_semantic_table - -table = to_semantic_table(ibis_table, name="table_name") -``` +**[→ View the full documentation](https://xorq-labs.github.io/boring-semantic-layer/)** -#### Adding Dimensions +Learn about: +- Getting started guide +- Defining semantic tables with dimensions and measures +- Advanced query patterns (filters, joins, window functions) +- MCP integration for AI agents +- Chart visualization with Altair and Plotly +- YAML configuration +- Complete API reference -```python -# Simple format (lambda) -table = table.with_dimensions( - origin=lambda t: t.origin, - destination=lambda t: t.dest -) - -# With descriptions (dict format) -table = table.with_dimensions( - origin={ - "expr": lambda t: t.origin, - "description": "Origin airport code" - } -) - -# Time dimensions -table = table.with_dimensions( - arr_time={ - "expr": lambda t: t.arr_time, - "description": "Arrival timestamp", - "is_time_dimension": True, - "smallest_time_grain": "TIME_GRAIN_DAY" - } -) -``` - -**Dimension dict format:** -| Field | Type | Required | Notes | -| --------------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `expr` | callable | Yes | Function mapping table → column expression | -| `description` | str | No | Human-readable description | -| `is_time_dimension` | bool | No | Mark as a time dimension (default: False) | -| `smallest_time_grain` | str | No | One of: `TIME_GRAIN_SECOND`, `TIME_GRAIN_MINUTE`, `TIME_GRAIN_HOUR`, `TIME_GRAIN_DAY`, `TIME_GRAIN_WEEK`, `TIME_GRAIN_MONTH`, `TIME_GRAIN_QUARTER`, `TIME_GRAIN_YEAR` | - -#### Adding Measures - -```python -# Simple format (lambda) -table = table.with_measures( - flight_count=lambda t: t.count(), - avg_distance=lambda t: t.distance.mean() -) - -# With descriptions (dict format) -table = table.with_measures( - flight_count={ - "expr": lambda t: t.count(), - "description": "Total number of flights" - } -) -``` - -**Measure dict format:** -| Field | Type | Required | Notes | -| ------------- | -------- | -------- | -------------------------------------- | -| `expr` | callable | Yes | Function mapping table → aggregation | -| `description` | str | No | Human-readable description | - -#### Query Operations - -**Filtering:** -```python -filtered = table.filter(lambda t: t.origin == 'JFK') -``` - -**Grouping and Aggregating:** -```python -result = table.group_by("origin").aggregate("flight_count", "avg_distance") -``` - -**Ordering:** -```python -from ibis import _ -result = table.group_by("origin").aggregate("flight_count").order_by(_.flight_count.desc()) -``` - -**Limiting:** -```python -result = table.group_by("origin").aggregate("flight_count").limit(10) -``` - -**Executing:** -```python -df = result.execute() -``` - -#### Joins - -**join_one (many-to-one):** -```python -joined = table.join_one(other_table, left_on="carrier", right_on="code") -``` - -**join_many (one-to-many):** -```python -joined = table.join_many(other_table, left_on="customer_id", right_on="customer_id") -``` - -**join_cross:** -```python -joined = table.join_cross(other_table) -``` - -### YAML Configuration Reference - -```yaml -model_name: - table: table_reference - description: "Optional model description" - - dimensions: - # Simple format - column_name: _.column_name - - # With description - column_with_desc: - expr: _.column_name - description: "Column description" - - # Time dimension - time_column: - expr: _.timestamp_column - description: "Timestamp column" - is_time_dimension: true - smallest_time_grain: "TIME_GRAIN_DAY" - - measures: - # Simple format - count: _.count() - - # With description - total: - expr: _.amount.sum() - description: "Total amount" - - joins: - joined_table_alias: - model: other_model_name - type: one # one, many, or cross - left_on: local_column - right_on: remote_column -``` - -**Loading YAML:** -```python -from boring_semantic_layer.semantic_api import from_yaml - -models = from_yaml("config.yml", tables={ - "table_reference": ibis_table -}) -model = models["model_name"] -``` - -### Chart API Reference - -The `QueryExpr` object provides the `chart()` method for visualization: - -| Parameter | Type | Required | Allowed Values / Notes | -| --------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `spec` | dict or None | No | Chart specification dict. Format depends on backend:
- **Altair**: [Vega-Lite specification](https://vega.github.io/vega-lite/docs/spec.html)
- **Plotly**: BSL custom format (see Backend Selection section)
If not provided, will auto-detect chart type. If partial spec provided, missing parts will be auto-detected and merged. | -| `backend` | str | No | Charting backend to use:
- `"altair"` (default): Use Altair/Vega-Lite backend
- `"plotly"`: Use Plotly backend | -| `format` | str | No | Output format of the chart:
- `"static"` (default): Returns chart object (Chart/Figure)
- `"interactive"`: Returns interactive chart with tooltip
- `"json"`: Returns JSON specification
- `"png"`: Returns PNG image bytes (requires additional dependencies)
- `"svg"`: Returns SVG string (requires additional dependencies) | +--- -**Returns:** Chart in the requested format (Altair Chart object, dict, bytes, or str depending on format) +*This project is a joint effort by [xorq-labs](https://github.com/xorq-labs/xorq) and [boringdata](https://www.boringdata.io/).* -For more examples, see `examples/example_chart.py` in the repository. +*We welcome feedback and contributions!* diff --git a/docs/bun.lockb b/docs/bun.lockb new file mode 100644 index 00000000..f41003f4 Binary files /dev/null and b/docs/bun.lockb differ diff --git a/docs/chart_bar.png b/docs/chart_bar.png deleted file mode 100644 index e21af809..00000000 Binary files a/docs/chart_bar.png and /dev/null differ diff --git a/docs/chart_custom.png b/docs/chart_custom.png deleted file mode 100644 index c24ff91c..00000000 Binary files a/docs/chart_custom.png and /dev/null differ diff --git a/docs/chart_heatmap.png b/docs/chart_heatmap.png deleted file mode 100644 index 3a063712..00000000 Binary files a/docs/chart_heatmap.png and /dev/null differ diff --git a/docs/chart_line.png b/docs/chart_line.png deleted file mode 100644 index 4e336207..00000000 Binary files a/docs/chart_line.png and /dev/null differ diff --git a/docs/chart_quickstart.png b/docs/chart_quickstart.png deleted file mode 100644 index 32dfb98e..00000000 Binary files a/docs/chart_quickstart.png and /dev/null differ diff --git a/docs/chart_timeseries.png b/docs/chart_timeseries.png deleted file mode 100644 index 505cc102..00000000 Binary files a/docs/chart_timeseries.png and /dev/null differ diff --git a/docs/components.json b/docs/components.json new file mode 100644 index 00000000..62e10116 --- /dev/null +++ b/docs/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/docs/content/bucketing.md b/docs/content/bucketing.md new file mode 100644 index 00000000..59185f49 --- /dev/null +++ b/docs/content/bucketing.md @@ -0,0 +1,259 @@ +# Bucketing with 'Other' + +Limit displayed group-by values while consolidating remaining items into an 'Other' category. This pattern maintains focus on top-performing segments while capturing complete data and handling long-tail distributions. + +## Overview + +The bucketing with 'Other' pattern allows you to: + +- Focus on top N items while grouping the rest as 'Other' +- Use window functions to rank and identify top performers +- Create custom ranges for continuous values (e.g., age groups, price tiers) +- Consolidate low-frequency items into an "Other" category +- Maintain analytical clarity by reducing dimensional cardinality + +## Setup + +Let's create customer data with ages and purchase amounts: + +```setup_raw_data +import ibis +from ibis import _ +from boring_semantic_layer import to_semantic_table + +# Create customer transaction data +customer_data = ibis.memtable({ + "customer_id": list(range(1, 21)), + "age": [22, 28, 35, 42, 19, 55, 31, 67, 24, 38, 45, 29, 51, 33, 61, 26, 48, 36, 58, 41], + "purchase_amount": [45, 120, 250, 180, 35, 520, 95, 850, 65, 310, 190, 78, 420, 145, 680, 88, 275, 165, 590, 225], + "product_category": ["Electronics", "Clothing", "Electronics", "Home", "Clothing", "Electronics", + "Clothing", "Electronics", "Clothing", "Home", "Electronics", "Clothing", + "Home", "Clothing", "Electronics", "Clothing", "Home", "Electronics", "Electronics", "Home"] +}) +``` + + + +Now create a semantic table with dimensions and measures: + +```semantic_table_def +from boring_semantic_layer import to_semantic_table + +customer_st = ( + to_semantic_table(customer_data, name="customers") + .with_dimensions( + customer_id=lambda t: t.customer_id, + age=lambda t: t.age, + product_category=lambda t: t.product_category + ) + .with_measures( + customer_count=lambda t: t.count(), + total_revenue=lambda t: t.purchase_amount.sum(), + avg_purchase=lambda t: t.purchase_amount.mean().round(2) + ) +) +``` + + + +## Top Categories with 'Other' + +The most common bucketing pattern: show top N items by a metric, consolidate the rest as 'Other'. This uses a two-stage approach with window functions to rank items. + +```query_top_categories +from ibis import _ + +# Two-stage pipeline: rank then consolidate +result = ( + customer_st + .group_by("product_category") + .aggregate("total_revenue", "customer_count") + .mutate( + # Rank categories by revenue + rank=lambda t: ibis.row_number().over( + ibis.window(order_by=t.total_revenue.desc()) + ) + ) + .mutate( + # Replace non-top categories with "Other" + category_display=lambda t: ibis.case() + .when(t.rank <= 2, t.product_category) + .else_("Other") + .end(), + # Keep original revenue for sorting (only for top categories) + sort_value=lambda t: ibis.case() + .when(t.rank <= 2, t.total_revenue) + .else_(0) + .end() + ) + .group_by("category_display") + .aggregate( + revenue=lambda t: t.total_revenue.sum(), + customers=lambda t: t.customer_count.sum(), + sort_helper=lambda t: t.sort_value.max() + ) + .mutate( + avg_per_customer=lambda t: (t.revenue / t.customers).round(2) + ) + .order_by(_.sort_helper.desc()) +) +``` + + + + +The window function `row_number()` ranks categories by revenue. Non-top items are marked with `is_other`, then consolidated into a single 'Other' category. The `sort_helper` field ensures top categories appear first, sorted by their original revenue, with 'Other' at the end. + + +## Age Range Bucketing + +Create age buckets using case expressions: + +```query_age_buckets +from ibis import _ +result = ( + customer_st + .group_by("customer_id", "age", "product_category") + .aggregate("total_revenue") + .mutate( + age_group=lambda t: ibis.case() + .when(t.age < 25, "18-24") + .when(t.age < 35, "25-34") + .when(t.age < 45, "35-44") + .when(t.age < 55, "45-54") + .else_("55+") + .end() + ) + .group_by("age_group") + .aggregate( + customers=lambda t: t.count(), + revenue=lambda t: t.total_revenue.sum() + ) + .order_by(_.age_group) +) +``` + + + +## Purchase Amount Tiers + +Categorize purchases into value tiers: + +```query_purchase_tiers +from ibis import _ +result = ( + customer_st + .group_by("customer_id") + .aggregate("total_revenue") + .mutate( + tier=lambda t: ibis.case() + .when(t.total_revenue < 100, "Small ($0-99)") + .when(t.total_revenue < 250, "Medium ($100-249)") + .when(t.total_revenue < 500, "Large ($250-499)") + .else_("Premium ($500+)") + .end() + ) + .group_by("tier") + .aggregate( + customer_count=lambda t: t.count(), + total_value=lambda t: t.total_revenue.sum(), + avg_value=lambda t: t.total_revenue.mean().round(2) + ) + .order_by(_.total_value.desc()) +) +``` + + + +## Threshold-Based 'Other' Category + +Instead of ranking, you can consolidate categories based on a threshold (e.g., minimum customer count): + +```query_with_other +from ibis import _ + +result = ( + customer_st + .group_by("product_category") + .aggregate("total_revenue", "customer_count") + .mutate( + # Mark categories with less than 5 customers as "Other" + category_grouped=lambda t: ibis.case() + .when(t.customer_count >= 5, t.product_category) + .else_("Other") + .end() + ) + .group_by("category_grouped") + .aggregate( + customers=lambda t: t.customer_count.sum(), + revenue=lambda t: t.total_revenue.sum() + ) + .mutate( + avg_per_customer=lambda t: (t.revenue / t.customers).round(2) + ) + .order_by(_.revenue.desc()) +) +``` + + + + +This approach uses a fixed threshold rather than ranking. Categories with fewer than 5 customers are consolidated into 'Other'. This is simpler but less dynamic than the window function approach. + + +## Combined Bucketing + +Combine age groups and purchase tiers for multi-dimensional segmentation: + +```query_combined_buckets +from ibis import _ +result = ( + customer_st + .group_by("customer_id", "age") + .aggregate("total_revenue") + .mutate( + age_group=lambda t: ibis.case() + .when(t.age < 30, "Young (18-29)") + .when(t.age < 50, "Middle (30-49)") + .else_("Senior (50+)") + .end(), + value_tier=lambda t: ibis.case() + .when(t.total_revenue < 150, "Low Value") + .when(t.total_revenue < 350, "Mid Value") + .else_("High Value") + .end() + ) + .group_by("age_group", "value_tier") + .aggregate( + customers=lambda t: t.count(), + revenue=lambda t: t.total_revenue.sum() + ) + .order_by(_.age_group, _.revenue.desc()) +) +``` + + + +## Use Cases + +**Focus on Top Performers**: Show top 10 products by revenue, consolidate the rest as 'Other' to highlight key items while maintaining complete totals. + +**Long-Tail Distribution Management**: In e-commerce, display top categories while grouping niche categories as 'Other' to simplify reporting and dashboards. + +**Threshold-Based Filtering**: Consolidate low-volume customer segments (< 100 customers) into 'Other' to focus on statistically significant groups. + +**Age and Value Segmentation**: Create meaningful customer segments by combining age ranges (Young, Middle, Senior) with purchase tiers (Low, Mid, High). + +## Key Takeaways + +- Use window functions like `row_number()` to rank items for dynamic top-N selection +- Two-stage pattern: rank first, then consolidate and re-aggregate +- `ibis.case().when()...else_().end()` provides flexible bucketing logic +- Threshold-based 'Other' works well when you have a clear cutoff value +- Sort helper fields ensure 'Other' appears at the end of results +- 'Other' category maintains complete data while reducing cardinality + +## Next Steps + +- Learn about [Sessionized Data](/advanced/sessionized) for time-based grouping +- Explore [Indexing](/advanced/indexing) for baseline comparisons diff --git a/docs/content/charting.md b/docs/content/charting.md new file mode 100644 index 00000000..506e7291 --- /dev/null +++ b/docs/content/charting.md @@ -0,0 +1,293 @@ +# Charting + +BSL includes built-in support for generating data visualizations from your semantic queries. Create charts directly from query results with automatic chart type detection or full custom control. + +## Installation + +To use chart visualization, install with the appropriate backend: + +```bash +# For Altair backend (default) +pip install 'boring-semantic-layer[viz-altair]' + +# For Plotly backend +pip install 'boring-semantic-layer[viz-plotly]' +``` + +## Quick Start + +Here's a simple example showing how to create a chart: + +```setup_chart_data +import ibis +from boring_semantic_layer import to_semantic_table + +con = ibis.duckdb.connect(":memory:") +flights_data = ibis.memtable({ + "origin": ["JFK", "LAX", "SFO", "ORD", "DFW", "ATL", "DEN"], + "flight_count": [150, 135, 89, 112, 98, 145, 78], + "avg_distance": [2475, 1850, 1200, 950, 1100, 1650, 900] +}) +flights_tbl = con.create_table("flights", flights_data) + +flights_st = ( + to_semantic_table(flights_tbl, name="flights") + .with_dimensions( + origin=lambda t: t.origin + ) + .with_measures( + flight_count=lambda t: t.flight_count.sum(), + avg_distance=lambda t: t.avg_distance.mean() + ) +) +``` + + + +```query_basic_chart +# Query and chart in one fluent chain +result = ( + flights_st + .group_by("origin") + .aggregate("flight_count") + .order_by(ibis.desc("flight_count")) + .limit(5) +) + +result.chart() +``` + + + + +The `.chart()` method is available on query results from `.aggregate()`, `.order_by()`, `.limit()`, and `.mutate()` operations. + + +## Backend Selection + +BSL supports two charting backends with different strengths: + +### Altair (Default) + +**Best for:** Web-native interactive visualizations, declarative specifications, embedding in notebooks and web apps. + +```python +# Use Altair backend (default) +chart = result.chart() +# or explicitly +chart = result.chart(backend="altair") +``` + +**Features:** +- Built on Vega-Lite grammar +- Declarative JSON specifications +- Great for interactive web visualizations +- Excellent notebook integration + +### Plotly + +**Best for:** Rich interactive dashboards, 3D visualizations, extensive chart types, business intelligence tools. + +```python +# Use Plotly backend +chart = result.chart(backend="plotly") +``` + +**Features:** +- Extensive chart type library +- Rich interactivity out of the box +- Dashboard integration +- Export to static formats + +## Auto-Detection + +BSL automatically detects the appropriate chart type based on your query structure: + +### Bar Chart (Categorical Data) + +Single dimension + measure → Bar chart + +```query_bar_chart +result = ( + flights_st + .group_by("origin") + .aggregate("flight_count") + .order_by(ibis.desc("flight_count")) +) + +result.chart() +``` + + + +**Auto-detected because:** Single categorical dimension (`origin`) with one measure (`flight_count`) + +### Time Series (Temporal Data) + +Time dimension + measure → Line chart with time-aware formatting + +```setup_timeseries +import ibis +from boring_semantic_layer import to_semantic_table + +con = ibis.duckdb.connect(":memory:") +timeseries_data = ibis.memtable({ + "date": ["2024-01-01", "2024-01-02", "2024-01-03", "2024-01-04", "2024-01-05", "2024-01-06", "2024-01-07"], + "flight_count": [145, 152, 148, 139, 156, 161, 143] +}) +timeseries_tbl = con.create_table("daily_flights", timeseries_data) + +daily_flights_st = ( + to_semantic_table(timeseries_tbl, name="daily_flights") + .with_dimensions( + date={ + "expr": lambda t: t.date.cast("date"), + "is_time_dimension": True, + "smallest_time_grain": "TIME_GRAIN_DAY" + } + ) + .with_measures( + flight_count=lambda t: t.flight_count.sum() + ) +) +``` + + + +```query_timeseries +result = ( + daily_flights_st + .group_by("date") + .aggregate("flight_count") +) +result.chart() +``` + + + +**Auto-detected because:** Dimension marked as `is_time_dimension=True` + +### Heatmap (Two Dimensions) + +Two categorical dimensions + measure → Heatmap + +```setup_heatmap +import ibis +from boring_semantic_layer import to_semantic_table + +con = ibis.duckdb.connect(":memory:") +route_data = ibis.memtable({ + "origin": ["JFK", "JFK", "LAX", "LAX", "SFO", "SFO"], + "dest": ["LAX", "SFO", "JFK", "SFO", "JFK", "LAX"], + "flight_count": [45, 32, 43, 28, 31, 27] +}) +route_tbl = con.create_table("routes", route_data) + +routes_st = ( + to_semantic_table(route_tbl, name="routes") + .with_dimensions( + origin=lambda t: t.origin, + dest=lambda t: t.dest + ) + .with_measures( + flight_count=lambda t: t.flight_count.sum() + ) +) +``` + + + +```query_heatmap +result = ( + routes_st + .group_by("origin", "dest") + .aggregate("flight_count") +) +result.chart() +``` + + + +**Auto-detected because:** Two categorical dimensions with one measure + +### Multi-Series Charts + +Multiple measures → Grouped/overlaid visualization with color encoding + +```query_multi_measure +result = ( + flights_st + .group_by("origin") + .aggregate("flight_count", "avg_distance") + .limit(5) +) +result.chart() +``` + + + +**Auto-detected because:** Multiple measures trigger automatic color encoding by measure name + + +## Custom Specifications + +Override auto-detection with custom specifications: + +### Change Mark Type And Add Styling + +Customize the mark type while providing explicit encodings: + +```query_custom_mark +import ibis +# Create line chart with custom spec +result = ( + flights_st + .group_by("origin") + .aggregate("flight_count") + .order_by(ibis.desc("flight_count")) + .limit(5) +) +result.chart(spec={ + "mark": {"type": "line", "color": "#e74c3c"} +}) +``` + + + + +You don't need to provide full vega spec: the spec object is merged with the BSL's default one. + + +## Export Formats + +Export charts in various formats for different use cases: + +```python +# Interactive chart object (default) +chart = result.chart() + +# JSON specification for web embedding +json_spec = result.chart(format="json") + +# PNG image (requires altair[all] or plotly) +png_bytes = result.chart(format="png") + +# SVG markup (requires altair[all] or plotly) +svg_str = result.chart(format="svg") + +# Save to file +with open("my_chart.png", "wb") as f: + f.write(png_bytes) +``` + +**Available formats:** +- `"static"` or `"interactive"` - Chart object (default) +- `"json"` - JSON specification +- `"png"` - PNG image bytes +- `"svg"` - SVG markup string + +## Next Steps + +- Learn about [Query Methods](query-methods.md) to build complex queries +- Explore [YAML Configuration](yaml-config.md) for declarative semantic models +- See [Compose Models](compose.md) for joining semantic tables diff --git a/docs/content/compose.md b/docs/content/compose.md new file mode 100644 index 00000000..745b7938 --- /dev/null +++ b/docs/content/compose.md @@ -0,0 +1,166 @@ +# Composing Models + +Build complex data models by combining multiple semantic tables through joins. Model composition allows you to create rich, multi-dimensional views of your data. + +## Composition via Joins + +Model composition in BSL is achieved through **joins**. When you join semantic tables, the result is a new composed model that contains **all dimensions and measures** from both tables. + + +Each join creates a new semantic model with the combined dimensions and measures from all joined tables. This allows you to build progressively richer models. + + +## Example: Two-Level Composition + +Let's build a composed model step-by-step, showing available dimensions and measures at each level. + +### Level 0: Base Models + +First, let's set up our base tables: + +```setup_ibis_tables +import ibis +from boring_semantic_layer import to_semantic_table + +# Create sample data +con = ibis.duckdb.connect(":memory:") + +# Flights table +flights_data = ibis.memtable({ + "flight_id": [1, 2, 3], + "carrier_code": ["AA", "UA", "DL"], + "aircraft_id": [101, 102, 103], + "distance": [1000, 1500, 800], + "passengers": [150, 180, 120] +}) +flights_tbl = con.create_table("flights", flights_data) + +# Carriers table +carriers_data = ibis.memtable({ + "code": ["AA", "UA", "DL"], + "name": ["American Airlines", "United Airlines", "Delta Air Lines"], + "country": ["USA", "USA", "USA"] +}) +carriers_tbl = con.create_table("carriers", carriers_data) + +# Aircraft table +aircraft_data = ibis.memtable({ + "id": [101, 102, 103], + "model": ["Boeing 737", "Airbus A320", "Boeing 777"], + "capacity": [180, 200, 350] +}) +aircraft_tbl = con.create_table("aircraft", aircraft_data) +``` + + + +```setup_semantic_models +# Create semantic tables +flights_st = ( + to_semantic_table(flights_tbl, name="flights") + .with_dimensions( + flight_id=lambda t: t.flight_id, + carrier_code=lambda t: t.carrier_code, + aircraft_id=lambda t: t.aircraft_id + ) + .with_measures( + flight_count=lambda t: t.count(), + total_distance=lambda t: t.distance.sum(), + total_passengers=lambda t: t.passengers.sum() + ) +) + +carriers_st = ( + to_semantic_table(carriers_tbl, name="carriers") + .with_dimensions( + code=lambda t: t.code, + name=lambda t: t.name, + country=lambda t: t.country + ) + .with_measures( + carrier_count=lambda t: t.count() + ) +) + +aircraft_st = ( + to_semantic_table(aircraft_tbl, name="aircraft") + .with_dimensions( + id=lambda t: t.id, + model=lambda t: t.model + ) + .with_measures( + aircraft_count=lambda t: t.count(), + total_capacity=lambda t: t.capacity.sum() + ) +) +``` + + + +```level0_dimensions +flights_st.dimensions, flights_st.measures +``` + + + +### Level 1: First Join (Flights + Carriers) + +Join carriers to flights to add carrier information: + +```level1_join +# Join carriers to flights +flights_with_carriers = flights_st.join_many( + carriers_st, + left_on="carrier_code", + right_on="code" +) + +# Inspect dimensions - now includes both flights and carriers +flights_with_carriers.dimensions, flights_with_carriers.measures +``` + + +### Level 2: Second Join (+ Aircraft) + +Add aircraft information to create a fully composed model: + +```level2_join +# Join aircraft to the composed model +full_model = flights_with_carriers.join_many( + aircraft_st, + left_on="aircraft_id", + right_on="id" +) + +# Inspect dimensions - now includes flights, carriers, AND aircraft +full_model.dimensions, full_model.measures +``` + + +## Query the Composed Model + +Now you can query across all joined tables: + +```composed_query +# Query using dimensions and measures from all three tables +result = ( + full_model + .group_by( "aircraft.model") + .aggregate("flight_count", "total_passengers", "total_capacity") +) +``` + + + +## Key Takeaways + +- **Composition via Joins**: Use `join_many()`, `join_one()`, or `join()` to compose models +- **Additive**: Each join adds dimensions and measures from the joined table +- **Table Prefixes**: Dimensions/measures are prefixed with table names (`flights.`, `carriers.`, `aircraft.`) +- **No Limit**: Compose as many models as needed for your analysis +- **Incremental**: Build from simple to complex, one join at a time + +## Next Steps + +- Learn about [YAML Configuration](/building/yaml) for declarative model composition +- Explore [Query Methods](/querying/methods) for querying composed models diff --git a/docs/content/example.md b/docs/content/example.md new file mode 100644 index 00000000..4c6f587c --- /dev/null +++ b/docs/content/example.md @@ -0,0 +1,66 @@ +# Example: E-commerce Analytics + +This example demonstrates how to use BSL for e-commerce data analysis. + +## Setup Data + +```orders_table +orders_tbl = ibis.memtable({ + "order_id": [1, 2, 3, 4, 5, 6, 7, 8], + "customer": ["Alice", "Bob", "Alice", "Charlie", "Bob", "Alice", "David", "Charlie"], + "product": ["Widget", "Gadget", "Widget", "Doohickey", "Widget", "Gadget", "Widget", "Gadget"], + "amount": [100, 150, 100, 75, 100, 150, 100, 150], + "quantity": [1, 2, 1, 3, 1, 2, 1, 2], +}) + +orders_st = ( + to_semantic_table(orders_tbl, name="orders") + .with_dimensions( + customer=lambda t: t.customer, + product=lambda t: t.product, + ) + .with_measures( + total_orders=lambda t: t.count(), + total_revenue=lambda t: t.amount.sum(), + total_quantity=lambda t: t.quantity.sum(), + avg_order_value=lambda t: t.amount.mean(), + ) +) +``` + +## Revenue by Customer + +Let's see which customers generate the most revenue: + +```revenue_by_customer +result = orders_st.group_by("customer").aggregate( + "total_orders", + "total_revenue", + "avg_order_value" +) +``` + +Customer revenue analysis: + +## Product Performance + +Which products are selling best? + +```product_performance +result = orders_st.group_by("product").aggregate( + "total_orders", + "total_quantity", + "total_revenue" +) +``` + +Product performance metrics: + + + +## Summary + +This demonstrates how BSL makes it easy to: +- Define semantic models once +- Run multiple queries with different groupings +- Generate consistent metrics across analyses diff --git a/docs/content/getting-started.md b/docs/content/getting-started.md new file mode 100644 index 00000000..0d11a189 --- /dev/null +++ b/docs/content/getting-started.md @@ -0,0 +1,77 @@ +# Getting Started with BSL + +BSL (Boring Semantic Layer) is a lightweight semantic layer built on top of Ibis. It allows you to define your data models once and query them anywhere. + +## Installation + +```bash +pip install boring-semantic-layer +``` + +## Quick Start + +Let's create your first Semantic Table using synthetic data in Ibis. + +```setup_flights +import ibis +from boring_semantic_layer import to_semantic_table + +# Create sample flight data +flights_tbl = ibis.memtable({ + "origin": ["NYC", "LAX", "NYC", "SFO", "LAX", "NYC", "SFO", "LAX"], + "destination": ["LAX", "NYC", "SFO", "NYC", "SFO", "LAX", "LAX", "SFO"], + "distance": [2789, 2789, 2902, 2902, 347, 2789, 347, 347], + "duration": [330, 330, 360, 360, 65, 330, 65, 65], +}) +``` + +You can then convert these tables in Semantic Tables that contains dimensios and measures definitions: + +```define_semantic_table +# Define semantic table with dimensions and measures +flights_st = ( + to_semantic_table(flights_tbl, name="flights") + .with_dimensions( + origin=lambda t: t.origin, + destination=lambda t: t.destination, + ) + .with_measures( + flight_count=lambda t: t.count(), + total_distance=lambda t: t.distance.sum(), + avg_duration=lambda t: t.duration.mean(), + ) +) +``` + +## Query Your Data + +Now let's query the semantic table by grouping flights by origin: + +```query_by_origin +# Group flights by origin airport +result = flights_st.group_by("origin").aggregate( + "flight_count", + "total_distance", + "avg_duration" +) +``` + + + +You can also group by destination: + +```query_by_destination +# Group flights by destination airport +result = flights_st.group_by("destination").aggregate( + "flight_count", + "total_distance" +) +``` + + + +## Next Steps + +- Learn how to [Build Semantic Tables](/examples/semantic-table) with dimensions, measures, and joins +- Explore [Query Methods](/examples/query-methods) for retrieving data +- Discover how to [Compose Models](/examples/compose) together diff --git a/docs/content/indexing.md b/docs/content/indexing.md new file mode 100644 index 00000000..6df8a951 --- /dev/null +++ b/docs/content/indexing.md @@ -0,0 +1,236 @@ +# Dimensional Indexing + +Create a searchable catalog of all unique values across your dimensions for data exploration, autocomplete features, and understanding data distributions. Inspired by [Malloy's index pattern](https://docs.malloydata.dev/documentation/patterns/dim_index). + +## Overview + +Dimensional indexing allows you to: + +- **Catalog all values**: Extract and count all unique values across dimensions +- **Search dimensions**: Build autocomplete and search features +- **Profile data**: Understand cardinality and distributions +- **Weight by measures**: Find values ranked by custom metrics (e.g., highest revenue cities) +- **Index across joins**: Search values from related tables + +The `index()` method returns a standardized table with columns: +- `fieldName`: The dimension name +- `fieldValue`: The unique value +- `fieldType`: The data type (string, number, etc.) +- `weight`: Count or custom measure value for ranking + +## Setup + +Let's create an airports semantic table for our examples: + +```setup_airports +import ibis +from boring_semantic_layer import to_semantic_table + +# Create synthetic airports data +airports_data = ibis.memtable({ + "code": ["JFK", "LAX", "ORD", "ATL", "DFW", "DEN", "SFO", "LAS", "SEA", "PHX", + "IAH", "MCO", "EWR", "BOS", "MIA", "SAN", "LGA", "PHL", "DTW", "MSP"], + "city": ["NEW YORK", "LOS ANGELES", "CHICAGO", "ATLANTA", "DALLAS", "DENVER", + "SAN FRANCISCO", "LAS VEGAS", "SEATTLE", "PHOENIX", "HOUSTON", "ORLANDO", + "NEWARK", "BOSTON", "MIAMI", "SAN DIEGO", "NEW YORK", "PHILADELPHIA", + "DETROIT", "MINNEAPOLIS"], + "state": ["NY", "CA", "IL", "GA", "TX", "CO", "CA", "NV", "WA", "AZ", + "TX", "FL", "NJ", "MA", "FL", "CA", "NY", "PA", "MI", "MN"], + "fac_type": ["AIRPORT", "AIRPORT", "AIRPORT", "AIRPORT", "AIRPORT", "AIRPORT", + "AIRPORT", "AIRPORT", "AIRPORT", "AIRPORT", "AIRPORT", "AIRPORT", + "AIRPORT", "AIRPORT", "AIRPORT", "AIRPORT", "AIRPORT", "AIRPORT", + "AIRPORT", "AIRPORT"], + "elevation": [13, 128, 672, 1026, 607, 5433, 13, 2181, 433, 1135, + 97, 96, 18, 19, 8, 17, 21, 36, 645, 841] +}) + +# Define semantic table +airports = ( + to_semantic_table(airports_data, name="airports") + .with_dimensions( + code=lambda t: t.code, + city=lambda t: t.city, + state=lambda t: t.state, + fac_type=lambda t: t.fac_type, + elevation=lambda t: t.elevation, + ) + .with_measures( + airport_count=lambda t: t.count(), + avg_elevation=lambda t: t.elevation.mean(), + ) +) +``` + + + +## Basic Index: All Dimensions + +Index all dimensions to see every unique value with its frequency: + +```query_index_all +# Index all dimensions (None means all) +result = airports.index(None).limit(10) +``` + + + +The `weight` column shows the count for each value. Use this to understand which values are most common across your dataset. + +## Index Specific Fields + +Focus on specific dimensions by selecting them: + +```query_index_specific +# Index only state and city +result = ( + airports.index(lambda t: [t.state, t.city]) + .order_by(lambda t: t.weight.desc()) + .limit(10) +) +``` + + + +This is useful when you only care about certain dimensions, reducing noise and improving performance. + +## Search Pattern: Autocomplete + +Build autocomplete features by filtering the index with pattern matching: + +```query_autocomplete +# Get city suggestions starting with "SAN" +result = ( + airports.index(lambda t: t.city) + .filter(lambda t: t.fieldValue.like("SAN%")) + .order_by(lambda t: t.weight.desc()) + .limit(10) +) +``` + + + + +Use pattern matching with `like()` to implement autocomplete, search suggestions, or fuzzy matching features in your application. + + +## Filter by Field Type + +Analyze only string or numeric fields: + +```query_by_type +# Get only string field values +result = ( + airports.index(None) + .filter(lambda t: t.fieldType == "string") + .order_by(lambda t: t.weight.desc()) + .limit(10) +) +``` + + + +This helps when you want to focus on categorical vs. numeric dimensions separately. + +## Custom Weights: Rank by Measure + +Instead of counting occurrences, weight values by a custom measure: + +```query_custom_weight +# Find states with most airports +result = ( + airports.index(lambda t: t.state, by="airport_count") + .order_by(lambda t: t.weight.desc()) + .limit(10) +) +``` + + + + +The `by` parameter lets you rank dimension values by any measure. This is powerful for finding "top cities by revenue", "states by average temperature", etc. + + +## Sampling for Large Datasets + +For very large datasets, use sampling to get quick insights: + +```query_sampled +# Sample 100 rows before indexing +result = ( + airports.index(None, sample=100) + .filter(lambda t: t.fieldType == "string") + .order_by(lambda t: t.weight.desc()) + .limit(10) +) +``` + + + +Sampling trades perfect accuracy for speed, which is often acceptable for exploratory analysis. + +## Index Across Joins + +Index dimensions from joined tables: + +```query_index_joins +# Create synthetic flights data +flights_data = ibis.memtable({ + "flight_id": list(range(1, 31)), + "carrier": ["AA", "UA", "DL", "WN", "B6", "AA", "UA", "DL", "WN", "B6"] * 3, + "origin": ["JFK", "LAX", "ORD", "ATL", "DFW", "SFO", "SEA", "DEN", "PHX", "BOS"] * 3, +}) + +flights = ( + to_semantic_table(flights_data, name="flights") + .with_dimensions( + carrier=lambda t: t.carrier, + origin=lambda t: t.origin, + ) + .with_measures( + flight_count=lambda t: t.count(), + ) +) + +# Join flights with airports +flights_with_origin = flights.join_one(airports, left_on="origin", right_on="code") + +# Index across the join +result = ( + flights_with_origin.index(lambda t: [t.carrier, t.airports__state]) + .order_by(lambda t: t.weight.desc()) + .limit(10) +) +``` + + + + +When referencing dimensions from joined tables in the index, use double underscores: `airports__state` instead of `airports.state`. + + +## Use Cases + +**Data Discovery**: Quickly explore what values exist in your dimensions without writing complex group-by queries. Perfect for understanding unfamiliar datasets. + +**Autocomplete & Search**: Build type-ahead search features by indexing dimension values and filtering with pattern matching. The weight helps rank suggestions by relevance. + +**Data Profiling**: Understand data quality by examining cardinality, common values, and distributions across dimensions. Identify outliers or data entry errors. + +**Metric-Weighted Ranking**: Find dimension values that matter most for your metrics - e.g., "cities with highest revenue", "products with most returns", "states with longest delivery times". + +**Cross-Table Search**: Index dimensions across joined tables to search related data simultaneously, enabling unified search experiences. + +## Key Takeaways + +- Use `index(None)` to catalog all dimension values +- Use `index(lambda t: [t.field1, t.field2])` for specific fields or `index(lambda t: t.field)` for a single field +- Filter by `fieldType` to focus on strings or numbers +- Use `by="measure_name"` to weight by custom measures instead of counts +- Add `sample=N` to analyze large datasets quickly +- The index works across joins - use `table__field` syntax for joined dimensions +- Perfect for building autocomplete, search, and data profiling features + +## Next Steps + +- Learn about [Nesting](/advanced/nesting) for hierarchical data structures +- Explore [Query Methods](/querying/methods) for more query patterns diff --git a/docs/content/mcp.md b/docs/content/mcp.md new file mode 100644 index 00000000..1ce2434a --- /dev/null +++ b/docs/content/mcp.md @@ -0,0 +1,322 @@ +# Model Context Protocol (MCP) Integration + +BSL includes built-in support for the [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol/python-sdk), allowing you to expose your semantic models to Large Language Models like Claude. + + +**Pro tip:** Use [descriptions in dimensions and measures](/building/semantic-tables#adding-descriptions) to make your models more AI-friendly. Descriptions help provide context to LLMs, enabling them to understand what each field represents and when to use them. + + +## Installation + +To use MCP functionality, install BSL with the `fastmcp` extra: + +```bash +pip install 'boring-semantic-layer[fastmcp]' +``` + +## Setting up an MCP Server + +Create an MCP server script that exposes your semantic models: + +```python +import ibis +from boring_semantic_layer.semantic_api import to_semantic_table +from boring_semantic_layer.api.mcp import MCPSemanticModel + +# Create synthetic flights data +flights_data = ibis.memtable({ + "flight_id": list(range(1, 101)), + "origin": ["JFK", "LAX", "ORD", "ATL", "DFW"] * 20, + "dest": ["LAX", "JFK", "DFW", "ORD", "ATL"] * 20, + "carrier": ["AA", "UA", "DL", "WN", "B6"] * 20, + "distance": [2475, 2475, 801, 606, 732] * 20, +}) + +# Define your semantic table with descriptions +flights = ( + to_semantic_table(flights_data, name="flights") + .with_dimensions( + origin={ + "expr": lambda t: t.origin, + "description": "Origin airport code where the flight departed from" + }, + destination={ + "expr": lambda t: t.dest, + "description": "Destination airport code where the flight arrived" + }, + carrier={ + "expr": lambda t: t.carrier, + "description": "Airline carrier code (e.g., AA, UA, DL)" + }, + ) + .with_measures( + total_flights={ + "expr": lambda t: t.count(), + "description": "Total number of flights" + }, + avg_distance={ + "expr": lambda t: t.distance.mean(), + "description": "Average flight distance in miles" + }, + ) +) + +# Create the MCP server +mcp_server = MCPSemanticModel( + models={"flights": flights}, + name="Flight Data Server" +) + +if __name__ == "__main__": + mcp_server.run(transport="stdio") +``` + +Save this as `example_mcp.py` in your project directory. + +## Configuring Claude Desktop + +To use your MCP server with Claude Desktop, add it to your configuration file. + +**Configuration file location:** +- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` +- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` + +**Example configuration:** + +```json +{ + "mcpServers": { + "flight_sm": { + "command": "uv", + "args": [ + "--directory", + "/path/to/your/project/", + "run", + "example_mcp.py" + ] + } + } +} +``` + +Replace `/path/to/your/project/` with the actual path to your project directory. + + +This example uses [uv](https://docs.astral.sh/uv/) to run the MCP server. You can also use `python` directly if you have BSL installed in your environment: + +```json +{ + "mcpServers": { + "flight_sm": { + "command": "python", + "args": ["/path/to/your/project/example_mcp.py"] + } + } +} +``` + + +After updating the configuration: +1. Restart Claude Desktop +2. Look for the MCP server indicator in the Claude Desktop interface +3. You should see "flight_sm" listed as an available server + +## Available MCP Tools + +Once configured, Claude will have access to these tools for interacting with your semantic models: + +### list_models + +List all available semantic model names in the MCP server. + +**Example usage in Claude:** +> "What models are available?" + +**Returns:** Array of model names (e.g., `["flights", "carriers"]`) + +### get_model + +Get detailed information about a specific model including its dimensions, measures, and descriptions. + +**Parameters:** +- `model_name` (str): Name of the model to inspect + +**Example usage in Claude:** +> "Show me the details of the flights model" + +**Returns:** Model schema including: +- Model name and description +- List of dimensions with their descriptions +- List of measures with their descriptions +- Available joins (if any) + +### get_time_range + +Get the available time range for time-series data in a model. + +**Parameters:** +- `model_name` (str): Name of the model +- `time_dimension` (str): Name of the time dimension + +**Example usage in Claude:** +> "What's the time range available in the flights model?" + +**Returns:** Dictionary with `min_time` and `max_time` values + +### query_model + +Execute queries against a semantic model with dimensions, measures, filters, and optional chart specifications. + +**Parameters:** +- `model_name` (str): Name of the model to query +- `dimensions` (list[str]): List of dimension names to group by +- `measures` (list[str]): List of measure names to aggregate +- `filters` (list[str], optional): List of filter expressions (e.g., `["origin == 'JFK'"]`) +- `limit` (int, optional): Maximum number of rows to return +- `order_by` (list[str], optional): List of columns to sort by +- `chart_spec` (dict, optional): Vega-Lite chart specification + +**Example usage in Claude:** +> "Show me the top 10 origins by flight count" +> "Create a bar chart of average distance by carrier" + +**Returns:** +- When `chart_spec` is provided: `{"records": [...], "chart": {...}}` +- When `chart_spec` is not provided: `{"records": [...]}` + +### Example Interactions + +Here are some example questions you can ask Claude when the MCP server is configured: + +**Data Exploration:** +- "What models are available in the flight data server?" +- "Show me all dimensions and measures in the flights model" +- "What is the time range covered by the flights data?" + +**Basic Queries:** +- "How many flights departed from JFK?" +- "Show me the top 5 destinations by flight count" +- "What's the average flight distance for each carrier?" + +**Filtered Queries:** +- "Show me flights from California airports (starting with 'S')" +- "What carriers have an average distance over 1000 miles?" +- "List the top 10 busiest routes" + +**Visualizations:** +- "Create a bar chart showing flights by origin airport" +- "Make a line chart of flights over time" +- "Show me a heatmap of routes between origins and destinations" + +## Best Practices + +### 1. Add Descriptions to All Fields + +Descriptions are crucial for LLMs to understand your data model: + +```python +flights = ( + to_semantic_table(flights_tbl, name="flights") + .with_dimensions( + origin={ + "expr": lambda t: t.origin, + "description": "Origin airport code (3-letter IATA code)" + } + ) + .with_measures( + total_flights={ + "expr": lambda t: t.count(), + "description": "Total number of flights in the dataset" + } + ) +) +``` + +### 2. Use Descriptive Model Names + +Choose clear, descriptive names for your models: + +```python +# Good +mcp_server = MCPSemanticModel( + models={"flights": flights, "carriers": carriers}, + name="Aviation Analytics Server" +) + +# Less clear +mcp_server = MCPSemanticModel( + models={"f": flights, "c": carriers}, + name="Server" +) +``` + +### 3. Structure Your Data Logically + +Organize related dimensions and measures together, and use joins to connect related models: + +```python +# Flights model focuses on flight operations +flights = ( + to_semantic_table(flights_tbl, name="flights") + .with_dimensions(origin=..., destination=..., date=...) + .with_measures(flight_count=..., avg_delay=...) +) + +# Carriers model focuses on airline information +carriers = ( + to_semantic_table(carriers_tbl, name="carriers") + .with_dimensions(code=..., name=..., country=...) + .with_measures(carrier_count=...) +) + +# Connect them with joins +flights_with_carriers = flights.join_one( + carriers, + left_on="carrier", + right_on="code" +) +``` + +## Troubleshooting + +### Server Not Appearing in Claude Desktop + +1. Check the configuration file path is correct +2. Verify JSON syntax in `claude_desktop_config.json` +3. Ensure BSL is installed with MCP support: `pip install 'boring-semantic-layer[fastmcp]'` +4. Restart Claude Desktop completely +5. Check Claude Desktop logs for error messages + +### Import Errors + +If you see import errors when the server starts: + +```bash +# Ensure all dependencies are installed +pip install 'boring-semantic-layer[fastmcp]' + +# Or install specific dependencies +pip install fastmcp ibis-framework +``` + +### Path Issues + +Make sure file paths in your configuration are absolute paths, not relative: + +```json +{ + "mcpServers": { + "flight_sm": { + "command": "python", + "args": ["/Users/username/projects/my-project/example_mcp.py"] + } + } +} +``` + +## Next Steps + +- Learn about [YAML Configuration](/building/yaml) for managing multiple models +- Explore [Query Methods](/querying/methods) to understand what queries LLMs can perform +- See [Charting](/querying/charting) for visualization capabilities +- Review the [full API Reference](/reference) for advanced features diff --git a/docs/content/mcp_example.yaml b/docs/content/mcp_example.yaml new file mode 100644 index 00000000..0120b346 --- /dev/null +++ b/docs/content/mcp_example.yaml @@ -0,0 +1,64 @@ +flights: + table: flights_tbl + description: "Flight operations data including departure, arrival, and route information" + + dimensions: + origin: + expr: _.origin + description: "Origin airport code (3-letter IATA code)" + + destination: + expr: _.dest + description: "Destination airport code (3-letter IATA code)" + + carrier: + expr: _.carrier + description: "Airline carrier code" + + flight_date: + expr: _.arr_time + description: "Flight arrival date/time" + is_time_dimension: true + smallest_time_grain: "TIME_GRAIN_DAY" + + measures: + total_flights: + expr: _.count() + description: "Total number of flights" + + avg_distance: + expr: _.distance.mean() + description: "Average flight distance in miles" + + avg_delay: + expr: _.dep_delay.mean() + description: "Average departure delay in minutes" + + joins: + carriers: + model: carriers + type: one + left_on: carrier + right_on: code + +carriers: + table: carriers_tbl + description: "Airline carrier information and metadata" + + dimensions: + code: + expr: _.code + description: "Unique carrier code (2-letter IATA code)" + + name: + expr: _.name + description: "Full carrier name (e.g., 'American Airlines')" + + nickname: + expr: _.nickname + description: "Common nickname or abbreviation for the carrier" + + measures: + carrier_count: + expr: _.count() + description: "Number of unique carriers" diff --git a/docs/content/nested-subtotals.md b/docs/content/nested-subtotals.md new file mode 100644 index 00000000..e400e7c6 --- /dev/null +++ b/docs/content/nested-subtotals.md @@ -0,0 +1,182 @@ +# Nested Subtotals + +Create hierarchical aggregations with subtotals at multiple levels using the `nest` parameter. This pattern enables drill-down analysis where each row contains both summary metrics and nested breakdowns. + +## Overview + +The nested subtotals pattern allows you to: + +- Generate subtotals at each level of a dimensional hierarchy in a single query +- Create nested structures where each parent row contains child breakdowns +- Avoid complex self-joins or ROLLUP queries +- Build hierarchical data suitable for tree views and drill-down UIs + +## Setup + +Create a sample order items dataset with temporal and categorical dimensions: + +```setup_data +import ibis +from ibis import _ +from boring_semantic_layer import to_semantic_table + +# Create synthetic order items data +order_items_data = ibis.memtable({ + "order_id": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, + 1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, + 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030], + "sale_price": [45.99, 89.50, 120.00, 34.99, 67.80, 99.99, 54.50, 78.99, 150.00, 42.00, + 55.99, 72.50, 88.80, 110.00, 39.99, 95.00, 62.50, 81.99, 125.00, 48.50, + 66.99, 92.00, 105.50, 73.99, 58.80, 118.00, 84.50, 69.99, 135.00, 51.50], + "status": ["shipped", "delivered", "shipped", "processing", "delivered", + "shipped", "cancelled", "delivered", "shipped", "processing", + "delivered", "shipped", "delivered", "processing", "shipped", + "cancelled", "delivered", "shipped", "delivered", "processing", + "shipped", "delivered", "shipped", "processing", "delivered", + "shipped", "cancelled", "delivered", "shipped", "processing"], + "created_year": [2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, + 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, + 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024], + "created_month": [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, + 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, + 1, 1, 2, 2, 3, 3, 4, 4, 5, 5] +}) + +# Create semantic table with measures +order_items = to_semantic_table( + order_items_data, + name="order_items", +).with_measures( + order_count=lambda t: t.count(), + total_sales=lambda t: t.sale_price.sum(), + avg_price=lambda t: t.sale_price.mean(), +) +``` + + + +## Year with Nested Month Subtotals + +Create yearly totals with monthly breakdowns nested inside each year: + +```query_year_with_months +from ibis import _ + +# First aggregate by year and month to get monthly subtotals +monthly_data = ( + order_items + .group_by("created_year", "created_month") + .aggregate("order_count", "total_sales") +) + +# Then nest months within years +result = ( + monthly_data + .group_by("created_year") + .aggregate( + year_order_count=lambda t: t.order_count.sum(), + year_total_sales=lambda t: t.total_sales.sum(), + nest={"by_month": lambda t: t.group_by(["created_month", "order_count", "total_sales"]).order_by("created_month")} + ) + .order_by("created_year") +) +``` + + + + +Each year row contains a `by_month` array with all monthly subtotals for that year. The pattern is: aggregate at the finest level first, then nest at each parent level. + + +## Year with Nested Status Subtotals + +Alternative breakdown: nest order status within each year: + +```query_year_with_status +from ibis import _ + +# First aggregate by year and status +status_data = ( + order_items + .group_by("created_year", "status") + .aggregate("order_count", "total_sales", "avg_price") +) + +# Then nest status within years +result = ( + status_data + .group_by("created_year") + .aggregate( + year_order_count=lambda t: t.order_count.sum(), + year_total_sales=lambda t: t.total_sales.sum(), + nest={"by_status": lambda t: t.group_by(["status", "order_count", "total_sales", "avg_price"]).order_by(_.total_sales.desc())} + ) + .order_by("created_year") +) +``` + + + +## Multi-Level Nesting: Year > Month > Status + +Create three-level hierarchy with nested subtotals: + +```query_multi_level +from ibis import _ + +# First aggregate at the finest level: year, month, status +detailed_data = ( + order_items + .group_by("created_year", "created_month", "status") + .aggregate("order_count", "total_sales") +) + +# Second level: nest status within month +monthly_with_status = ( + detailed_data + .group_by("created_year", "created_month") + .aggregate( + month_order_count=lambda t: t.order_count.sum(), + month_total_sales=lambda t: t.total_sales.sum(), + nest={"by_status": lambda t: t.group_by(["status", "order_count", "total_sales"])} + ) +) + +# Top level: nest months within year +result = ( + monthly_with_status + .group_by("created_year") + .aggregate( + year_order_count=lambda t: t.month_order_count.sum(), + year_total_sales=lambda t: t.month_total_sales.sum(), + nest={"by_month": lambda t: t.group_by(["created_month", "month_order_count", "month_total_sales", "by_status"]).order_by("created_month")} + ) + .order_by("created_year") + .limit(3) +) +``` + + + +## Use Cases + +**Financial Reporting**: Create income statements with nested line items - show total revenue with product categories nested inside, each containing individual products. + +**Geographic Hierarchies**: Aggregate sales by region, with nested states, with nested cities, all in a single query result. + +**Time-Based Drill-Downs**: Show yearly summaries with monthly breakdowns nested inside, perfect for dashboard drill-down interactions. + +**Organizational Analysis**: Display department totals with nested team breakdowns, with nested individual employee details. + +## Key Takeaways + +- Use the `nest` parameter in `.aggregate()` to create hierarchical subtotals +- Each parent row contains an array column with child-level breakdowns +- Avoid complex SQL ROLLUP or self-join patterns +- Nest multiple levels deep for complex hierarchies +- Perfect for building tree views, expandable tables, and drill-down UIs + +## Next Steps + +- Learn about [Percentage of Total](/advanced/percentage-total) calculations +- Explore [Advanced Nesting](/advanced/nesting) for more complex hierarchical patterns diff --git a/docs/content/percentage-total.md b/docs/content/percentage-total.md new file mode 100644 index 00000000..5536f9d6 --- /dev/null +++ b/docs/content/percentage-total.md @@ -0,0 +1,109 @@ +# Percentage of Total + +Calculate percentages relative to total values across different dimensions. Use this pattern when you need to understand market share, contribution ratios, or what proportion each segment represents of the whole. + +## Overview + +The percentage of total pattern allows you to: + +- Define percentage measures using the `.all()` method +- Calculate individual segment values as percentages of the grand total +- Maintain dimensional breakdowns while computing percentage contributions +- Support multiple aggregation functions (sum, count, average) + +## Setup + +Let's use the flights dataset with carrier information to demonstrate market share calculations: + +```setup_data +import ibis +from ibis import _ +from boring_semantic_layer import to_semantic_table + +# Create synthetic flights data with carrier information +flights_data = ibis.memtable({ + "flight_id": list(range(1, 51)), + "carrier": ["AA", "UA", "DL", "WN", "B6"] * 10, + "nickname": ["American Airlines", "United Airlines", "Delta Air Lines", + "Southwest Airlines", "JetBlue Airways"] * 10, + "origin": ["JFK", "LAX", "ORD", "ATL", "DFW"] * 10, + "distance": [2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383, + 2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383, + 2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383, + 2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383, + 2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383] +}) + +# Create semantic table with measures including percentage calculations +flights = ( + to_semantic_table(flights_data, name="flights") + .with_measures( + flight_count=lambda t: t.count(), + total_distance=lambda t: t.distance.sum(), + ) + .with_measures( + market_share=lambda t: t.flight_count / t.all(t.flight_count) * 100, + distance_share=lambda t: t.total_distance / t.all(t.total_distance) * 100, + ) +) +``` + + + + +The `.all()` method calculates the grand total across all groups, allowing you to define percentage measures directly in the semantic table. This is more elegant than using window functions in post-processing. + + +## Market Share by Carrier + +Calculate each carrier's percentage of total flights: + +```query_market_share +from ibis import _ + +result = ( + flights.group_by("nickname") + .aggregate("flight_count", "market_share") + .order_by(_.market_share.desc()) + .limit(10) +) +``` + + + +## Market Share by Origin and Carrier + +Calculate market share broken down by both origin airport and carrier: + +```query_market_share_by_origin +from ibis import _ + +result = ( + flights.group_by("origin", "nickname") + .aggregate("flight_count", "market_share") + .order_by(_.market_share.desc()) + .limit(15) +) +``` + + + +## Use Cases + +**Market Share Analysis**: Calculate each carrier's, product's, or region's share of total volume. + +**Traffic Distribution**: Determine what percentage of total website visits or conversions come from each source. + +**Resource Allocation**: Understand how resources (budget, time, capacity) are distributed as percentages of the total. + +## Key Takeaways + +- Define percentage measures using `.all()` to reference the grand total +- The `.all(measure)` method calculates the total across all groups +- Percentage measures work seamlessly across different dimensional breakdowns +- More elegant than post-processing with window functions + +## Next Steps + +- Learn about [Nested Subtotals](/advanced/nested-subtotals) for hierarchical aggregations +- Explore [Bucketing](/advanced/bucketing) to group continuous values diff --git a/docs/content/query-methods.md b/docs/content/query-methods.md new file mode 100644 index 00000000..9d4c0d0f --- /dev/null +++ b/docs/content/query-methods.md @@ -0,0 +1,446 @@ +# Query Methods + +Retrieve data from your semantic tables using `group_by()` and `aggregate()` methods. + +## Overview + +BSL provides a simple and consistent query API that works with your semantic table definitions: + +- **`group_by()`**: Group data by dimension names (strings only, must be defined in `with_dimensions()`) +- **`aggregate()`**: Calculate measures (with or without grouping) +- **`filter()`**: Apply conditions to filter data +- **`mutate()`**: Transform aggregated results +- **`order_by()`**: Sort results +- **`limit()`**: Restrict number of rows + +## Setup + +```setup_table +import ibis +from ibis import _ +from boring_semantic_layer import to_semantic_table + +# Create Ibis table +flights_tbl = ibis.memtable({ + "origin": ["NYC", "LAX", "NYC", "SFO", "LAX", "NYC", "SFO", "LAX", "NYC"], + "carrier": ["AA", "UA", "AA", "UA", "AA", "UA", "AA", "UA", "AA"], + "distance": [2789, 2789, 2902, 2902, 347, 2789, 347, 347, 2789], + "duration": [330, 330, 360, 360, 65, 330, 65, 65, 330], +}) + +# Create semantic table +flights_st = ( + to_semantic_table(flights_tbl, name="flights") + .with_dimensions( + origin=lambda t: t.origin, + carrier=lambda t: t.carrier, + ) + .with_measures( + flight_count=lambda t: t.count(), + total_distance=lambda t: t.distance.sum(), + avg_duration=lambda t: t.duration.mean(), + ) +) +``` + + + +## group_by() + +The `group_by()` method groups data by one or more dimensions. + + +`group_by()` only accepts string dimension names that were previously defined in `with_dimensions()`. It does not support lambda functions or unbound `_` syntax. + + +### Single Dimension + +Group by a single dimension: + +```query_single_dimension +# Group by one dimension +result = flights_st.group_by("origin").aggregate("flight_count") +``` + + + +### Multiple Dimensions + +Group by multiple dimensions to create detailed breakdowns: + +```query_multiple_dimensions +# Group by multiple dimensions +result = flights_st.group_by("origin", "carrier").aggregate("flight_count") +``` + + + +### No Grouping + +Calculate overall statistics across all rows using `group_by()` with no arguments: + +```query_no_grouping +# Aggregate entire dataset without grouping +result = flights_st.group_by().aggregate("flight_count", "total_distance", "avg_duration") +``` + + + +## aggregate() + +The `aggregate()` method calculates measures after grouping. You can reference pre-defined measures or compute new ones on-the-fly. + +### Pre-defined Measures + +Reference measures by their string names: + +```query_predefined_measures +# Use measures defined in with_measures() +result = flights_st.group_by("origin").aggregate("flight_count", "avg_duration") +``` + + + +### On-the-Fly Transformations + +Add computed measures directly in `aggregate()` without modifying the semantic table: + +```query_onthefly_measures +# Mix predefined and computed measures +result = ( + flights_st + .group_by("origin") + .aggregate( + "flight_count", # Pre-defined measure + "avg_duration", # Pre-defined measure + total_miles=lambda t: t.distance.sum(), # Computed on-the-fly + max_distance=lambda t: t.flight_count + 2 # You can reference other measures as well + ) +) +``` + + + + +On-the-fly measures let you add context-specific calculations without modifying your semantic table definition. This keeps your base model clean while enabling flexible queries. + + +### Referencing Table Columns + +You can reference **any column from the underlying table** in `aggregate()`, not just pre-defined measures. This is useful when you need one-off calculations without cluttering your semantic table definition. + +```query_table_columns +# Reference table columns directly in aggregate() +result = ( + flights_st + .group_by("origin") + .aggregate( + "flight_count", # Pre-defined measure + total_distance=lambda t: t.distance.sum(), # Table column 'distance' + avg_duration=lambda t: t.duration.mean(), # Table column 'duration' + distance_in_km=lambda t: (t.distance * 1.60934).sum() # Transform then aggregate + ) +) +``` + + + +**Key points:** +- Table columns **must be aggregated** (e.g., `.sum()`, `.mean()`, `.max()`, `.count()`) +- You can transform columns before aggregating (e.g., `(t.distance * 1.60934).sum()`) +- This works for any column in the underlying table, even if not defined as a dimension or measure +- Use this for ad-hoc calculations without modifying your semantic table + + +Table columns cannot be used without an aggregation function. For example, `lambda t: t.distance` will fail. You must use `lambda t: t.distance.sum()` or another aggregation. + + +## filter() / order_by() / limit() + +Combine `filter()`, `order_by()`, and `limit()` to refine your query results. + +```query_filter_order_limit +from ibis import _ + +# Filter data, sort, and limit results +result = ( + flights_st + .filter(lambda t: t.origin.isin(["NYC", "LAX"])) # Filter origins + .filter(_.distance > 500) # Filter distance using _ syntax + .group_by("origin") + .aggregate("flight_count", "avg_duration") # Aggregate both measures + .order_by(ibis.desc("flight_count")) # Sort by flight_count descending + .limit(5) # Top 5 results +) +``` + + + +**Key points:** +- **`filter()`**: Use lambda or `_` syntax to apply conditions before aggregation +- **`order_by()`**: Use `ibis.desc()` for descending order, or column name for ascending +- **`limit()`**: Restrict the number of rows returned + +## nest() + +The `nest` parameter in `aggregate()` creates nested data structures (arrays of structs) in your query results. This is useful for API responses, hierarchical visualizations, and preserving relationships in aggregated data. + +Use `nest` to collect rows as structured arrays within each group: + +```query_basic_nest +from ibis import _ + +# Nest flight details within each origin +result = ( + flights_st + .group_by("origin") + .aggregate( + "flight_count", + "total_distance", + # Create nested array of flight details + nest={"flights": lambda t: t.group_by(["carrier", "distance"])} + ) +) +``` + + + +**How it works:** +- The `nest` parameter accepts a dictionary: `{"column_name": lambda t: ...}` +- The lambda specifies which columns to collect using `.group_by()` or `.select()` +- Results in an array of structs column named `flights` + +You can also use `.select()` to specify which columns to nest: + +```query_nest_select +# Nest specific columns +result = ( + flights_st + .group_by("carrier") + .aggregate( + "flight_count", + nest={"routes": lambda t: t.select("origin", "distance", "duration")} + ) +) +``` + + + +After nesting, you can re-group which automatically unnests, then access the nested fields. + +**Step 1: Create nested data** + +First, create the nested structure. Notice the `flights` column contains arrays of structs: + +```query_nest_step1 +from ibis import _ + +# Create nested data structure +result = ( + flights_st + .group_by("origin") + .aggregate( + "flight_count", + nest={"flights": lambda t: t.group_by(["carrier", "distance"])} + ) +) +``` + + + +**Step 2: Re-group to unnest and access fields** + +Now re-group on the same dimension, which automatically unnests the array, allowing you to access the nested fields: + +```query_nest_step2 +from ibis import _ + +# Re-grouping automatically unnests the 'flights' array +result = ( + result + .group_by("origin") + .aggregate( + total_flights=lambda t: t.flight_count.sum(), + # Access unnested fields from the flights array + unique_carriers=lambda t: t.flights.carrier.nunique(), + avg_distance=lambda t: t.flights.distance.mean() + ) +) +``` + + + +**Use cases for nesting:** +- **API responses**: Create JSON-compatible hierarchical structures +- **Hierarchical data**: Preserve parent-child relationships in results +- **Data export**: Generate nested documents for external systems +- **Drill-down analysis**: Keep detailed records available in aggregated views + + +For more complex nesting patterns and multi-level hierarchies, see [Advanced Nesting Patterns](/advanced/nesting). + + +## mutate() + +The `mutate()` method transforms aggregated results by adding new computed columns. This is different from on-the-fly measures in `aggregate()` — `mutate()` works on already-aggregated data. + + +**Key difference:** `.aggregate()` computes from raw data, while `.mutate()` transforms already-aggregated results. + + +```query_mutate +from ibis import _ + +# Add post-aggregation calculations +result = ( + flights_st + .group_by("origin") + .aggregate("flight_count", "total_distance") + .mutate( + avg_distance_per_flight=lambda t: t.total_distance / t.flight_count, + flight_category=lambda t: ibis.case() + .when(t.flight_count >= 3, "high") + .when(t.flight_count >= 2, "medium") + .else_("low") + .end() + ) +) +``` + + + +**Use cases for `mutate()`:** +- Calculate ratios from aggregated measures (e.g., `total / count`) +- Create categories based on aggregated values +- Add labels or formatting to results +- Transform aggregated columns using the full power of Ibis + +For more transformations, see [Ibis Table API reference](https://ibis-project.org/reference/expression-tables.html#ibis.expr.types.relations.Table.mutate). + +## Window Functions with .over() + +Window functions perform calculations across ordered rows, enabling operations like running totals, moving averages, and ranking. Unlike regular aggregations that reduce many rows to one, window functions preserve row count while adding computed values. + + +**Important:** Window functions can only be applied **after aggregation**, typically within a `.mutate()` call. They cannot be defined directly in measures. + + +**Common window functions:** +- **`lag()` / `lead()`**: Access previous/next row values for period-over-period comparisons +- **`cumsum()`**: Calculate running totals +- **`.over(window)`**: Apply functions over sliding windows (e.g., moving averages) +- **`rank()` / `row_number()`**: Assign ranks or sequential numbers to rows + +Here's a simple example: + +```query_window_example +from ibis import _ + +# First aggregate to daily level +daily_flights = ( + flights_st + .group_by("origin") + .aggregate("flight_count", "total_distance") + .order_by("origin") +) + +# Then apply window function for cumulative distance +window_spec = ibis.window(order_by="origin") + +result = daily_flights.mutate( + cumulative_distance=_.total_distance.cumsum(), + flight_rank=lambda t: ibis.rank().over(ibis.window(order_by=_.flight_count.desc())) +).limit(10) +``` + + + +**Key points:** +- Window functions are applied **after** `.aggregate()` using `.mutate()` +- Use `.order_by()` to establish row order for window operations +- Combine with `ibis.window()` for advanced sliding window calculations + +For comprehensive examples including lag/lead, moving averages, and ranking, see [Window Functions](/advanced/windowing). + +## as_table() + +After filtering or aggregating data, you may want to perform additional semantic operations. However, intermediate results don't always preserve the semantic table's dimensions and measures. + +The Problem: Lost Semantic Information + +When you aggregate data, the result loses semantic metadata. The aggregated result is a `SemanticAggregate` expression, which doesn't have `.dimensions` or `.measures` attributes: + +```query_as_table_problem +from ibis import _ + +# Aggregate the data - this returns a SemanticAggregate +agg_result = flights_st.group_by("origin").aggregate("flight_count", "total_distance") + +# Show the type/class of the result +result_type = type(agg_result).__name__ + +# Try to access .dimensions - this will raise an AttributeError +try: + dimensions = agg_result.dimensions + result = f"Type: {result_type}\nDimensions: {dimensions}" +except AttributeError as e: + result = f"Type: {result_type}\nError: {str(e)}" + +result +``` + + + + +After aggregation, you can no longer access the original semantic table's dimensions and measures metadata. + +The Solution: Use as_table() + +The `as_table()` method converts results back into a `SemanticModel`. However, note that for aggregations, the metadata is intentionally cleared (since columns are now materialized): + +```query_as_table_after_aggregate +from ibis import _ + +# Aggregate the data +agg_result = flights_st.group_by("origin").aggregate("flight_count", "total_distance") + +# Convert to SemanticModel using as_table() +agg_table = agg_result.as_table() + +# Now .dimensions and .measures attributes exist, but they're empty (metadata was cleared) +result = f"Type: {type(agg_table).__name__}\nDimensions: {agg_table.dimensions}\nMeasures: {agg_table.measures}" +``` + + + +### When Metadata IS Preserved + +For operations like `filter()`, `order_by()`, and `limit()`, `as_table()` **preserves** the original semantic metadata: + +```query_as_table_filter_preserved +from ibis import _ + +# Filter the data +filtered = flights_st.filter(_.distance > 2000) + +# Convert back to SemanticModel - metadata is preserved! +filtered_table = filtered.as_table() + +# Dimensions and measures are still available (preserved from original semantic table) +result = f"Type: {type(filtered_table).__name__}\nDimensions: {filtered_table.dimensions}\nMeasures: {filtered_table.measures}" +``` + + + +Notice how the dimensions and measures are preserved, unlike the aggregation case above where they were empty. + +**Key points:** +- **Operations that preserve metadata**: `filter()`, `order_by()`, `limit()`, `unnest()` — calling `as_table()` restores full semantic capabilities with original dimensions/measures +- **Operations that clear metadata**: `aggregate()`, `mutate()` — calling `as_table()` returns a `SemanticModel` with empty dimensions/measures (columns are materialized) +- Use `as_table()` when you need to continue semantic operations on intermediate results + +## Next Steps + +- Learn about [Building Semantic Tables](/building/semantic-tables) to define dimensions and measures +- Explore [Composing Models](/building/compose) for multi-table queries +- Try [Advanced Patterns](/advanced/percentage-total) for complex analytics diff --git a/docs/content/reference.md b/docs/content/reference.md new file mode 100644 index 00000000..e4cc487e --- /dev/null +++ b/docs/content/reference.md @@ -0,0 +1,556 @@ +# API Reference + +Complete API documentation for the Boring Semantic Layer. + +## Table Creation & Configuration + +Methods for creating and configuring semantic tables. + +### to_semantic_table() + +```python +to_semantic_table( + table: ibis.Table, + name: str, + description: str = None +) -> SemanticTable +``` + +Create a semantic table from an Ibis table. This is the primary entry point for building semantic models. + +**Parameters:** +- `table` - Ibis table to build the model from +- `name` - Unique identifier for the semantic table +- `description` - Optional description of the semantic table + +**Example:** +```python +import ibis +from boring_semantic_layer import to_semantic_table + +flights = ibis.read_parquet("flights.parquet") +flights_st = to_semantic_table(flights, "flights") +``` + +### with_dimensions() + +```python +with_dimensions( + **dimensions: Callable | Dimension +) -> SemanticTable +``` + +Define dimensions for grouping and analysis. Dimensions are attributes that categorize data. + +**Example:** +```python +flights_st = flights_st.with_dimensions( + origin=lambda t: t.origin, + dest=lambda t: t.dest, + carrier=lambda t: t.carrier +) +``` + +### with_measures() + +```python +with_measures( + **measures: Callable | Measure +) -> SemanticTable +``` + +Define aggregations and calculations. Measures are numeric values that can be aggregated. + +**Example:** +```python +flights_st = flights_st.with_measures( + flight_count=lambda t: t.count(), + avg_delay=lambda t: t.arr_delay.mean(), + total_distance=lambda t: t.distance.sum() +) +``` + +### from_yaml() + +```python +from_yaml( + yaml_path: str, + connection: ibis.Connection = None +) -> dict[str, SemanticTable] +``` + +Load semantic models from a YAML configuration file. Returns a dictionary of semantic tables. + +**Parameters:** +- `yaml_path` - Path to YAML configuration file +- `connection` - Optional Ibis connection for database tables + +**Example:** +```python +from boring_semantic_layer.yaml import from_yaml + +models = from_yaml("models.yaml") +flights_st = models["flights"] +``` + +### Dimension Class + +```python +Dimension( + expr: Callable, + description: str = None +) +``` + +Self-documenting dimension with description. Use for better API documentation. + +**Example:** +```python +from boring_semantic_layer import Dimension + +flights_st = flights_st.with_dimensions( + origin=Dimension( + expr=lambda t: t.origin, + description="Airport code where the flight departed from" + ) +) +``` + +### Measure Class + +```python +Measure( + expr: Callable, + description: str = None +) +``` + +Self-documenting measure with description. Use for better API documentation. + +**Example:** +```python +from boring_semantic_layer import Measure + +flights_st = flights_st.with_measures( + avg_delay=Measure( + expr=lambda t: t.arr_delay.mean(), + description="Average arrival delay in minutes" + ) +) +``` + +### all() + +```python +st.all() +``` + +Reference the entire dataset within measure definitions. Primarily used for percentage-of-total calculations. + +**Example:** +```python +flights_st = to_semantic_table(data, "flights").with_measures( + flight_count=lambda t: t.count(), + pct_of_total=lambda t: ( + t.count() / t.all().count() * 100 + ) +) +``` + +## Join Methods + +Methods for composing semantic tables through joins. + +### join_many() + +```python +join_many( + other: SemanticTable, + on: Callable, + name: str = None +) -> SemanticTable +``` + +One-to-many relationship join (LEFT JOIN). Use when the left table can match multiple rows in the right table. + +**Parameters:** +- `other` - The semantic table to join with +- `on` - Lambda function defining the join condition +- `name` - Optional name for the joined table reference + +**Example:** +```python +flights_st = flights_st.join_many( + carriers_st, + on=lambda l, r: l.carrier == r.code, + name="carrier_info" +) +``` + +### join_one() + +```python +join_one( + other: SemanticTable, + on: Callable, + name: str = None +) -> SemanticTable +``` + +One-to-one relationship join (INNER JOIN). Use when each row in the left table matches exactly one row in the right table. + +**Example:** +```python +flights_st = flights_st.join_one( + airports_st, + on=lambda l, r: l.origin == r.code +) +``` + +### join_cross() + +```python +join_cross( + other: SemanticTable, + name: str = None +) -> SemanticTable +``` + +Cross join (CARTESIAN PRODUCT). Creates all possible combinations of rows from both tables. + +### join() + +```python +join( + other: SemanticTable, + on: Callable, + how: str = "inner", + name: str = None +) -> SemanticTable +``` + +Custom join with flexible join type. Supports 'inner', 'left', 'right', 'outer', and 'cross'. + +**Parameters:** +- `how` - Join type: 'inner', 'left', 'right', 'outer', or 'cross' + +## Query Methods + +Methods for querying and transforming semantic tables. + +### group_by() + +```python +group_by( + *dimensions: str +) -> QueryBuilder +``` + +Group data by one or more dimension names. Returns a query builder for chaining with aggregate(). + +**Example:** +```python +result = flights_st.group_by("origin", "carrier").aggregate("flight_count") +``` + +### aggregate() + +```python +aggregate( + *measures: str, + **kwargs +) -> ibis.Table +``` + +Calculate one or more measures. Can be used standalone or after group_by(). + +**Examples:** +```python +# Without grouping +total = flights_st.aggregate("flight_count") + +# With grouping +by_origin = flights_st.group_by("origin").aggregate("flight_count", "avg_delay") +``` + +### filter() + +```python +filter( + condition: Callable +) -> SemanticTable +``` + +Apply conditions to filter data. Use lambda functions with Ibis expressions. + +**Example:** +```python +delayed_flights = flights_st.filter(lambda t: t.arr_delay > 0) +``` + +### order_by() + +```python +order_by( + *columns: str | ibis.Expression +) -> ibis.Table +``` + +Sort query results. Use `ibis.desc()` for descending order. + +**Example:** +```python +result = flights_st.group_by("origin").aggregate("flight_count") +result = result.order_by(ibis.desc("flight_count")) +``` + +### limit() + +```python +limit( + n: int +) -> ibis.Table +``` + +Restrict the number of rows returned. + +**Example:** +```python +top_10 = result.order_by(ibis.desc("flight_count")).limit(10) +``` + +### mutate() + +```python +mutate( + **expressions: Callable | ibis.Expression +) -> ibis.Table +``` + +Add or transform columns in aggregated results. Useful for calculations after aggregation. + +**Example:** +```python +result = flights_st.group_by("month").aggregate("revenue") +result = result.mutate( + growth_rate=lambda t: (t.revenue - t.revenue.lag()) / t.revenue.lag() * 100 +) +``` + +### select() + +```python +select( + *columns: str | ibis.Expression +) -> ibis.Table +``` + +Select specific columns from the result. Often used in nesting operations. + +**Example:** +```python +result.select("origin", "flight_count") +``` + +## Nesting + +Create nested data structures within aggregations. + +### nest Parameter + +```python +aggregate( + *measures, + nest={ + "nested_column": lambda t: t.group_by([...]) | t.select(...) + } +) +``` + +Create nested arrays of structs within aggregation results. Useful for hierarchical data or subtotals. + +**Example:** +```python +result = flights_st.group_by("carrier").aggregate( + "total_flights", + nest={ + "by_month": lambda t: t.group_by("month").aggregate("monthly_flights") + } +) +``` + +## Charting + +Generate visualizations from query results. + +### chart() + +```python +chart( + result: ibis.Table, + backend: str = "altair", + spec: dict = None, + format: str = "interactive" +) -> Chart +``` + +Create visualizations from query results. Supports Altair (default) and Plotly backends. + +**Parameters:** +- `result` - Query result table to visualize +- `backend` - "altair" or "plotly" +- `spec` - Custom Vega-Lite specification (for Altair) +- `format` - "interactive", "json", "png", "svg" + +**Auto-detection:** +BSL automatically selects appropriate chart types: +- Single dimension + measure → Bar chart +- Time dimension + measure → Line chart +- Two dimensions + measure → Heatmap + +**Example:** +```python +from boring_semantic_layer.chart import chart + +result = flights_st.group_by("month").aggregate("flight_count") +chart(result, backend="altair") +``` + +## Dimensional Indexing + +Create searchable catalogs of dimension values. + +### index() + +```python +index( + dimensions: Callable | None = None, + by: str = None, + sample: int = None +) -> ibis.Table +``` + +Create a searchable catalog of unique dimension values with optional weighting and sampling. + +**Parameters:** +- `dimensions` - None (all dimensions) or lambda returning list of fields +- `by` - Measure name for weighting results +- `sample` - Number of rows to sample (for large datasets) + +**Examples:** +```python +# Index all dimensions +flights_st.index() + +# Index specific dimensions +flights_st.index(lambda t: [t.origin, t.dest]) + +# Weight by measure +flights_st.index(by="flight_count") + +# Sample large dataset +flights_st.index(sample=10000) +``` + +## Other + +### MCP Integration + +#### MCPSemanticModel() + +```python +MCPSemanticModel( + models: dict[str, SemanticTable] | str, + description: str = None +) +``` + +Create an MCP server to expose semantic models to LLMs like Claude. Accepts either a dictionary of models or a path to a YAML configuration file. + +**Available MCP Tools:** + +These tools are called by Claude through the MCP interface: + +- `list_models()` - List all available semantic model names +- `get_model()` - Get detailed model information (dimensions, measures, joins) +- `get_time_range()` - Get available time range for time-series data +- `query_model()` - Execute queries against semantic models + +**Example:** +```python +from boring_semantic_layer.mcp import MCPSemanticModel + +# From dictionary +server = MCPSemanticModel( + models={"flights": flights_st, "airports": airports_st}, + description="Flight data analysis" +) + +# From YAML +server = MCPSemanticModel("config.yaml") +``` + +### YAML Configuration + +#### YAML Structure + +```yaml +model_name: + table: table_reference + description: "Optional description" + + dimensions: + dimension_name: expression + # or with description + dimension_name: + expr: expression + description: "Dimension description" + + measures: + measure_name: expression + # or with description + measure_name: + expr: expression + description: "Measure description" + + joins: + join_name: + model: model_reference + on: join_condition + how: join_type # left, inner, right, outer, cross +``` + +#### Expression Syntax + +- `_` - Reference to the table +- `_.column` - Reference a column +- `_.count()` - Aggregation functions +- `_.column.sum()` - Column aggregations +- `_.column.mean()` - Average +- `_.column.min()` / `_.column.max()` - Min/Max + +**Example:** +```yaml +flights: + table: flights_data + description: "Flight operations data" + + dimensions: + origin: _.origin + dest: _.dest + carrier: + expr: _.carrier + description: "Airline carrier code" + + measures: + flight_count: _.count() + avg_delay: + expr: _.arr_delay.mean() + description: "Average arrival delay in minutes" +``` + +## Next Steps + +- Learn about [Semantic Tables](/building/semantic-tables) +- Explore [Query Methods](/querying/methods) +- See [Advanced Patterns](/advanced/percentage-total) diff --git a/docs/content/semantic-table.md b/docs/content/semantic-table.md new file mode 100644 index 00000000..23e664c5 --- /dev/null +++ b/docs/content/semantic-table.md @@ -0,0 +1,258 @@ +# Building a Semantic Table + +Define your data model with dimensions and measures using Ibis expressions. + +## Overview + +A Semantic Table is the core building block of BSL. It transforms a raw Ibis table into a reusable, self-documenting data model by defining: +- **Dimensions**: Attributes to group by (e.g., origin, carrier, year) +- **Measures**: Aggregations and calculations (e.g., flight count, total distance) + +## to_semantic_table() + +```setup_flights +import ibis +from boring_semantic_layer import to_semantic_table + +# 1. Start with an Ibis table +con = ibis.duckdb.connect(":memory:") +flights_data = ibis.memtable({ + "origin": ["JFK", "LAX", "SFO"], + "dest": ["LAX", "SFO", "JFK"], + "carrier": ["AA", "UA", "DL"], + "year": [2023, 2023, 2024], + "distance": [2475, 337, 382], + "dep_delay": [10, 5, 0] +}) +flights_tbl = con.create_table("flights", flights_data) + +# 2. Convert to a Semantic Table +flights_st = to_semantic_table(flights_tbl, name="flights") +``` + +## with_dimensions() + +Dimensions define the attributes you can group by in your queries. They represent the categorical or descriptive aspects of your data that you want to analyze. + +You can define dimensions using lambda expressions, unbound syntax (`_.`), or the `Dimension` class with descriptions: + +```dimensions_demo +from ibis import _ +from boring_semantic_layer import Dimension + +flights_st = flights_st.with_dimensions( + # Lambda expressions - simple and explicit + origin=lambda t: t.origin, + + # Unbound syntax - cleaner and more concise + destination=_.dest, + year=_.year, + + # Dimension - self-documenting and AI-friendly + carrier=Dimension( + expr=lambda t: t.carrier, + description="Airline carrier code" + ) +) + +flights_st.dimensions +``` + + +## with_measures() + +Measures define the aggregations and calculations you can query. They represent the quantitative aspects of your data that you want to analyze (counts, sums, averages, etc.). + +You can define measures using lambda expressions, reference other measures for composition, or use the `Measure` class with descriptions: + +```measures_demo +from boring_semantic_layer import Measure + +flights_st = flights_st.with_measures( + # Lambda expressions - simple and concise + total_flights=lambda t: t.count(), + total_distance=lambda t: t.distance.sum(), + max_delay=lambda t: t.dep_delay.max(), + + # Reference other measures for composition + avg_distance_per_flight=lambda t: t.total_distance / t.total_flights, + + # Measure - self-documenting and AI-friendly + avg_distance=Measure( + expr=lambda t: t.distance.mean(), + description="Average flight distance in miles" + ) +) + +flights_st.measures +``` + + + +### all() + +The `all()` function references the entire dataset within measure definitions, enabling percent-of-total and comparison calculations. + +**Example:** Calculate market share as a percentage + +```measure_all_demo +flights_with_pct = flights_st.with_measures( + flight_count=lambda t: t.count(), + market_share=lambda t: t.flight_count / t.all(t.flight_count) * 100 # Percent of total + ) + +# Query by carrier +result = ( + flights_with_pct + .group_by("carrier") + .aggregate("flight_count", "market_share") +) +``` + + + + +`t.all()` is a method available on the table parameter `t` in measure definitions. It references the entire dataset regardless of grouping, making it perfect for calculating percentages, or comparing groups to the total. + + +For more examples, see the [Percent of Total pattern](/advanced/percentage-total). + +## join_one() / join_many() / join_cross() + +Join semantic tables together to query across relationships. Joins allow you to combine data from multiple semantic tables and access dimensions and measures across all joined tables. + +Why Semantic Joins? + +Instead of just using SQL join types (`LEFT`, `INNER`, etc.), BSL offers relationship-based joins that capture the **meaning** of your data connections: + +**Traditional SQL Approach:** +```python +# Which join type? LEFT or INNER? Why? +flights.join(carriers, condition, how="left") # unclear intent +``` + +**BSL Semantic Approach:** +```python +# Clear intent: one carrier has many flights +flights.join_many(carriers, left_on="carrier", right_on="code") +``` + +**Benefits:** +- **Self-documenting**: `join_many()` tells you it's a one-to-many relationship +- **Clearer intent**: The method name describes the data relationship, not just SQL mechanics +- **Safer defaults**: Each method uses the appropriate join type for that relationship +- **Better tooling**: IDEs and AI assistants understand your data model semantics + + +After joining, dimensions and measures are prefixed with table names (e.g., `flights.origin`, `carriers.name`) to avoid naming conflicts. + + + +Let's get some additional data: + +```setup_carriers +import ibis +from boring_semantic_layer import to_semantic_table + +con = ibis.duckdb.connect(":memory:") + +# Create carriers data +carriers_data = ibis.memtable({ + "code": ["AA", "UA", "DL"], + "name": ["American Airlines", "United Airlines", "Delta Air Lines"] +}) +carriers_tbl = con.create_table("carriers", carriers_data) +``` + + +And create a carriers semantic table: + +```carriers_st +carriers = ( + to_semantic_table(carriers_tbl, name="carriers") + .with_dimensions( + code=lambda t: t.code, + name=lambda t: t.name + ) + .with_measures( + carrier_count=lambda t: t.count() + ) +) +``` + +### join_many() - One-to-Many Relationships + +Use `join_many()` when one row in the left table can match multiple rows in the right table (LEFT JOIN). + +```join_demo +# Join carriers to flights - one carrier has many flights +flights_with_carriers = flights_st.join_many( + carriers, + left_on="carrier", + right_on="code" +) + +# Inspect available dimensions and measures +flights_with_carriers.dimensions +``` + + +After joining, all dimensions and measures from both tables are available. Each is prefixed with its table name to avoid conflicts: + + +### join_one() - One-to-One Relationships + +Use `join_one()` when rows have a unique matching relationship (INNER JOIN). + +```python +# Many flights → one carrier (each flight has exactly one carrier) +flights_with_carrier = flights_st.join_one( + carriers, + left_on="carrier", + right_on="code" +) +``` + +### join_cross() - Cross Join + +Use `join_cross()` to create every possible combination of rows from both tables (CARTESIAN PRODUCT). + +```python +# Every flight × every carrier combination +all_combinations = flights_st.join_cross(carriers) +``` + +### join() - Custom Join Conditions + +Use `join()` for complex join conditions or specific SQL join types. + +```python +# LEFT JOIN with custom condition +flights_with_carriers = flights_st.join( + carriers, + lambda f, c: f.carrier == c.code, + how="left" +) + +# INNER JOIN +flights_matched = flights_st.join( + carriers, + lambda f, c: f.carrier == c.code, + how="inner" +) + +# Complex conditions +date_range_join = flights_st.join( + promotions, + lambda f, p: (f.date >= p.start_date) & (f.date <= p.end_date), + how="left" +) +``` + +**Supported join types:** `"inner"`, `"left"`, `"right"`, `"outer"`, `"cross"` + +## Next Steps + +- Learn about [Composing Models](/examples/compose) +- Explore [YAML Configuration](/examples/yaml-config) +- Start [Querying Semantic Tables](/examples/query-methods) diff --git a/docs/content/sessionized.md b/docs/content/sessionized.md new file mode 100644 index 00000000..cce6c864 --- /dev/null +++ b/docs/content/sessionized.md @@ -0,0 +1,219 @@ +# Sessionized Data + +Analyze time-series events grouped into sessions based on activity gaps. This pattern identifies and aggregates user or system behavior within discrete time-bounded sessions. + +## Overview + +The sessionization pattern allows you to: + +- Define session boundaries based on inactivity timeouts +- Group sequential events into logical sessions +- Calculate session-level metrics (duration, event count, conversion) +- Handle session spanning across multiple time periods + +## Setup + +Let's create user activity data with timestamps: + +```setup_raw_data +import ibis +from ibis import _ +from boring_semantic_layer import to_semantic_table + +# Create user activity events with minute offsets instead of timestamps +activity_data = ibis.memtable({ + "user_id": ["user1", "user1", "user1", "user1", "user2", "user2", "user2", "user3", "user3", "user3", "user3", "user3"], + "minute_offset": [0, 5, 10, 45, 2, 40, 42, 1, 3, 7, 50, 52], # Minutes from start + "page_url": ["/home", "/products", "/cart", "/checkout", "/home", "/products", "/cart", + "/home", "/about", "/products", "/home", "/contact"], + "action": ["view", "view", "view", "purchase", "view", "view", "view", + "view", "view", "view", "view", "view"] +}) +``` + + + +Now create a semantic table with dimensions and measures: + +```semantic_table_def +from boring_semantic_layer import to_semantic_table + +activity_st = ( + to_semantic_table(activity_data, name="activity") + .with_dimensions( + user_id=lambda t: t.user_id, + minute_offset=lambda t: t.minute_offset, + page_url=lambda t: t.page_url, + action=lambda t: t.action + ) + .with_measures( + event_count=lambda t: t.count(), + unique_users=lambda t: t.user_id.nunique() + ) +) +``` + +## Identify Session Boundaries + +Use window functions to identify session starts based on inactivity gaps: + +```query_session_boundaries +from ibis import _ + +result = ( + activity_st + .group_by("user_id", "minute_offset", "page_url", "action") + .aggregate() + .mutate( + # Calculate time since previous event for same user + prev_minute=lambda t: t.minute_offset.lag().over( + group_by="user_id", + order_by=t.minute_offset + ), + # Calculate minutes since last event + minutes_since_last=lambda t: t.minute_offset - t.prev_minute, + # Mark session start (>30 min gap or first event) + is_session_start=lambda t: (t.minutes_since_last > 30) | t.prev_minute.isnull() + ) + .order_by(_.user_id, _.minute_offset) +) +``` + + + +## Assign Session IDs + +Create session identifiers by counting session starts: + +```query_with_session_ids +from ibis import _ + +result = ( + activity_st + .group_by("user_id", "minute_offset", "page_url", "action") + .aggregate() + .mutate( + prev_minute=lambda t: t.minute_offset.lag().over( + group_by="user_id", + order_by=t.minute_offset + ), + minutes_since_last=lambda t: t.minute_offset - t.prev_minute, + is_session_start=lambda t: (t.minutes_since_last > 30) | t.prev_minute.isnull(), + # Cumulative sum of session starts gives session ID + session_id=lambda t: t.is_session_start.cast("int32").sum().over( + group_by="user_id", + order_by=t.minute_offset, + rows=(None, 0) # Cumulative sum + ) + ) + .order_by(_.user_id, _.minute_offset) +) +``` + + + +## Calculate Session Metrics + +Aggregate events by session to get session-level metrics: + +```query_session_metrics +from ibis import _ + +result = ( + activity_st + .group_by("user_id", "minute_offset", "action") + .aggregate() + .mutate( + prev_minute=lambda t: t.minute_offset.lag().over( + group_by="user_id", + order_by=t.minute_offset + ), + minutes_since_last=lambda t: t.minute_offset - t.prev_minute, + is_session_start=lambda t: (t.minutes_since_last > 30) | t.prev_minute.isnull(), + session_id=lambda t: t.is_session_start.cast("int32").sum().over( + group_by="user_id", + order_by=t.minute_offset, + rows=(None, 0) + ) + ) + .group_by("user_id", "session_id") + .aggregate( + events_in_session=lambda t: t.count(), + session_start_min=lambda t: t.minute_offset.min(), + session_end_min=lambda t: t.minute_offset.max(), + has_purchase=lambda t: (t.action == "purchase").any() + ) + .mutate( + session_duration_min=lambda t: (t.session_end_min - t.session_start_min) + ) + .order_by(_.user_id, _.session_id) +) +``` + + + +## User-Level Session Summary + +Summarize sessions per user: + +```query_user_summary +from ibis import _ + +result = ( + activity_st + .group_by("user_id", "minute_offset", "action") + .aggregate() + .mutate( + prev_minute=lambda t: t.minute_offset.lag().over( + group_by="user_id", + order_by=t.minute_offset + ), + minutes_since_last=lambda t: t.minute_offset - t.prev_minute, + is_session_start=lambda t: (t.minutes_since_last > 30) | t.prev_minute.isnull(), + session_id=lambda t: t.is_session_start.cast("int32").sum().over( + group_by="user_id", + order_by=t.minute_offset, + rows=(None, 0) + ) + ) + .group_by("user_id", "session_id") + .aggregate( + events_in_session=lambda t: t.count(), + has_purchase=lambda t: (t.action == "purchase").any() + ) + .group_by("user_id") + .aggregate( + total_sessions=lambda t: t.count(), + total_events=lambda t: t.events_in_session.sum(), + sessions_with_purchase=lambda t: t.has_purchase.cast("int32").sum(), + avg_events_per_session=lambda t: t.events_in_session.mean().round(2) + ) + .mutate( + conversion_rate=lambda t: (t.sessions_with_purchase / t.total_sessions * 100).round(2) + ) + .order_by(_.total_events.desc()) +) +``` + + + +## Use Cases + +**Web Analytics**: Group user page views and interactions into sessions, with a session ending after 30 minutes of inactivity. Calculate metrics like session duration, pages per session, and conversion rate. + +**IoT Device Monitoring**: Sessionize sensor readings to identify distinct usage periods and calculate metrics like average session length and activity intensity. + +**Application Usage Tracking**: Analyze how users interact with applications by grouping activities into sessions, identifying drop-off points, and measuring engagement patterns. + +## Key Takeaways + +- Use `lag()` window function to find time since previous event +- Compare time gaps to session timeout threshold (e.g., 30 minutes) +- Use cumulative sum of session starts to assign session IDs +- Calculate session metrics like duration, event count, and conversions +- Aggregate sessions to user level for summary statistics + +## Next Steps + +- Learn about [Indexing](/advanced/indexing) for trend analysis +- Explore [Bucketing](/advanced/bucketing) to categorize session durations diff --git a/docs/content/windowing.md b/docs/content/windowing.md new file mode 100644 index 00000000..753bde96 --- /dev/null +++ b/docs/content/windowing.md @@ -0,0 +1,288 @@ +# Window Functions + +Perform calculations across ordered rows using window functions like running totals, moving averages, rank, lag/lead, and more. Window functions operate on query results after aggregation, enabling powerful comparative and analytical operations. + +## Overview + +Window functions allow you to: + +- **Compare rows**: Calculate differences between current and previous rows (lag/lead) +- **Running calculations**: Compute cumulative sums and running averages +- **Ranking**: Assign ranks, row numbers, and percentiles +- **Moving windows**: Calculate metrics over sliding time windows + + +Window functions in BSL are applied using Ibis window operations on aggregated results. They execute logically after the aggregation stage. + + +## Setup + +Create a synthetic sales dataset with daily revenue data: + +```setup_data +import ibis +from ibis import _ +from datetime import datetime, timedelta +import random + +# Create daily sales data spanning 90 days +start_date = datetime(2024, 1, 1) +dates = [start_date + timedelta(days=i) for i in range(90)] + +# Generate synthetic revenue with upward trend and weekly patterns +random.seed(42) + +revenue_values = [] +for i, date in enumerate(dates): + # Base trend: increasing over time + base = 1000 + (i * 10) + + # Weekly pattern: weekends have higher sales + weekday_multiplier = 1.3 if date.weekday() >= 5 else 1.0 + + # Random variation + noise = random.uniform(-100, 100) + + revenue = base * weekday_multiplier + noise + revenue_values.append(round(revenue, 2)) + +# Create table +sales_data = ibis.memtable({ + "sale_date": dates, + "revenue": revenue_values, + "product_category": ["Electronics" if i % 3 == 0 else "Clothing" if i % 3 == 1 else "Home" for i in range(90)], +}) +``` + + + +```setup_st +from boring_semantic_layer import to_semantic_table + +# Create semantic table with measures +sales_st = to_semantic_table( + sales_data, + name="daily_sales" +).with_measures( + total_revenue=lambda t: t.revenue.sum(), + avg_revenue=lambda t: t.revenue.mean(), + sale_count=lambda t: t.count(), +) +``` + + + +## Lag and Lead: Comparing to Previous/Next Rows + +Calculate period-over-period changes by comparing current values to previous rows: + +```query_lag_lead +from ibis import _ + +# Aggregate daily revenue +daily_revenue = ( + sales_st + .group_by("sale_date") + .aggregate("total_revenue") + .order_by("sale_date") +) + +# Add window functions for lag/lead +result = daily_revenue.mutate( + prev_day_revenue=_.total_revenue.lag(), + next_day_revenue=_.total_revenue.lead(), + day_over_day_change=_.total_revenue - _.total_revenue.lag(), + pct_change=((_.total_revenue - _.total_revenue.lag()) / _.total_revenue.lag() * 100).round(2) +).limit(10) +``` + + + + +`lag()` accesses the previous row's value, while `lead()` accesses the next row's value. The first row's lag and last row's lead will be null. + + +## Running Totals: Cumulative Calculations + +Compute running sums to track cumulative metrics over time: + +```query_running_total +from ibis import _ + +# Daily revenue with cumulative total +daily_revenue = ( + sales_st + .group_by("sale_date") + .aggregate("total_revenue") + .order_by("sale_date") +) + +# Calculate cumulative sum and running average +window_unbounded = ibis.window(rows=(None, 0), order_by="sale_date") + +result = daily_revenue.mutate( + cumulative_revenue=_.total_revenue.cumsum(), + days_count=lambda t: t.count().over(window_unbounded), + avg_daily_so_far=lambda t: (t.cumulative_revenue / t.days_count).round(2) +).limit(10) +``` + + + +## Moving Averages: Sliding Window Calculations + +Calculate metrics over a rolling window of rows: + +```query_moving_average +from ibis import _ + +# Daily revenue +daily_revenue = ( + sales_st + .group_by("sale_date") + .aggregate("total_revenue") + .order_by("sale_date") +) + +# 7-day moving average +window_7d = ibis.window(rows=(-6, 0), order_by="sale_date") + +result = daily_revenue.mutate( + ma_7day=_.total_revenue.mean().over(window_7d).round(2), + ma_7day_sum=_.total_revenue.sum().over(window_7d).round(2), +).limit(10) +``` + + + + +The window specification `rows=(-6, 0)` means "6 rows before the current row through the current row" (7 total rows). The moving average smooths out daily volatility. + + +## Ranking: Assign Positions + +Rank rows based on values: + +```query_ranking +from ibis import _ + +# Aggregate by product category +category_revenue = ( + sales_st + .group_by("product_category") + .aggregate("total_revenue", "sale_count") + .order_by(_.total_revenue.desc()) +) + +# Add rank columns +result = category_revenue.mutate( + rank=ibis.rank().over(ibis.window(order_by=_.total_revenue.desc())), + dense_rank=ibis.dense_rank().over(ibis.window(order_by=_.total_revenue.desc())), + row_number=ibis.row_number().over(ibis.window(order_by=_.total_revenue.desc())), +) +``` + + + + +`row_number()` assigns unique sequential numbers, `rank()` assigns the same rank to ties (skipping next ranks), and `dense_rank()` assigns the same rank to ties without gaps. + + +## Week-over-Week Comparison + +Compare metrics across weekly periods: + +```query_week_over_week +from ibis import _ + +# Aggregate by week +weekly_revenue = ( + sales_st + .mutate(week_start=_.sale_date.truncate("W")) + .group_by("week_start") + .aggregate("total_revenue") + .order_by("week_start") +) + +# Calculate week-over-week changes +result = weekly_revenue.mutate( + prev_week_revenue=_.total_revenue.lag(), + wow_change=_.total_revenue - _.total_revenue.lag(), + wow_pct_change=((_.total_revenue - _.total_revenue.lag()) / _.total_revenue.lag() * 100).round(2) +).limit(10) +``` + + + +## Percent of Running Total + +Calculate each row's contribution to the cumulative total: + +```query_pct_running +from ibis import _ + +# Top 10 days by revenue +top_days = ( + sales_st + .group_by("sale_date") + .aggregate("total_revenue") + .order_by(_.total_revenue.desc()) + .limit(10) +) + +# Calculate cumulative percentage +result = top_days.mutate( + cumulative_revenue=_.total_revenue.cumsum(), + total_top10=_.total_revenue.sum(), + pct_of_top10=(_.total_revenue.cumsum() / _.total_revenue.sum() * 100).round(2) +) +``` + + + +## Moving Window with Filters + +Combine window functions with filtering for focused analysis: + +```query_window_filter +from ibis import _ + +# Focus on weekends only +weekend_revenue = ( + sales_st + .mutate(is_weekend=_.sale_date.day_of_week.index().isin([5, 6])) + .filter(_.is_weekend) + .group_by("sale_date") + .aggregate("total_revenue") + .order_by("sale_date") +) + +# 3-weekend moving average +window_3 = ibis.window(rows=(-2, 0), order_by="sale_date") + +result = weekend_revenue.mutate( + ma_3weekend=_.total_revenue.mean().over(window_3).round(2), + prev_weekend=_.total_revenue.lag(), + weekend_change=_.total_revenue - _.total_revenue.lag() +).limit(10) +``` + + + +## Key Takeaways + +- **Window functions operate after aggregation**: They work on query results, not raw data +- **Order matters**: Most window functions require `order_by()` for meaningful results +- **Flexible windows**: Define windows by rows (`rows=(n, m)`) or ranges +- **Common patterns**: + - `lag()/lead()` for period-over-period comparisons + - `cumsum()` for running totals + - `.over(window)` for moving averages + - `rank()`, `row_number()` for ranking +- **Combine with filters**: Focus window calculations on specific subsets + +## Next Steps + +- Explore [Percentage of Total](/advanced/percentage-total) for ratio calculations +- Learn about [Nested Subtotals](/advanced/nested-subtotals) for hierarchical aggregations +- Check out [Nesting](/advanced/nesting) for complex data structures diff --git a/docs/content/yaml-config.md b/docs/content/yaml-config.md new file mode 100644 index 00000000..4c8c0d18 --- /dev/null +++ b/docs/content/yaml-config.md @@ -0,0 +1,89 @@ +# YAML Configuration + +Define your semantic models using YAML for better organization and maintainability. + +## Why YAML? + +YAML configuration provides several advantages: +- **Better organization**: Keep your model definitions separate from your code +- **Version control**: Track changes to your data model structure +- **Collaboration**: Non-developers can review and understand the model +- **Reusability**: Share model definitions across different projects + +## Expression Syntax + +Here's a complete example with dimensions, measures, and joins: + + + + +In YAML configuration, **only unbound syntax (`_`) is accepted** for expressions. Lambda expressions are not supported in YAML files. + + +## Loading YAML Models + +Ibis table objects must be created separately in Python and passed to the YAML loader. Tables are resolved by the names specified in the YAML `table` field. + +Create your ibis tables: + +```yaml_setup +import ibis + +flights_tbl = ibis.memtable({ + "origin": ["JFK", "LAX", "SFO"], + "dest": ["LAX", "SFO", "JFK"], + "carrier": ["AA", "UA", "DL"], + "year": [2023, 2023, 2024], + "distance": [2475, 337, 382] +}) + +carriers_tbl = ibis.memtable({ + "code": ["AA", "UA", "DL"], + "name": ["American Airlines", "United Airlines", "Delta Air Lines"] +}) +``` + +And pass them to the loaded YAML file defining your Semantic Tables: + + +```load_yaml_example +from boring_semantic_layer import from_yaml + +# Load models from YAML file +models = from_yaml( + "content/yaml_example.yaml", + tables={ + "flights_tbl": flights_tbl, + "carriers_tbl": carriers_tbl + } +) + +flights_sm = models["flights"] +carriers_sm = models["carriers"] + +# Inspect the loaded models +flights_sm.dimensions, flights_sm.measures +``` + + + +## Querying YAML Models + +YAML-defined models work exactly like Python-defined models. You can use the same `group_by()` and `aggregate()` methods to query your data. + +```query_yaml_model +# Query the YAML-defined model +result = ( + flights_sm + .group_by("origin") + .aggregate("flight_count", "avg_distance") +) +``` + + + +## Next Steps + +- See [Building Semantic Tables](/building/semantic-tables) for Python-based definitions +- Learn [Query Methods](/querying/methods) for querying YAML-defined models +- Explore [Composing Models](/building/compose) for joining YAML models diff --git a/docs/content/yaml_example.yaml b/docs/content/yaml_example.yaml new file mode 100644 index 00000000..6646c21e --- /dev/null +++ b/docs/content/yaml_example.yaml @@ -0,0 +1,39 @@ +flights: + table: flights_tbl + dimensions: + origin: + expr: _.origin + description: "Flight origin airport code" + destination: + expr: _.dest + description: "Flight destination airport code" + year: + expr: _.year + description: "Flight year" + carrier: + expr: _.carrier + description: "Carrier code" + measures: + flight_count: + expr: _.count() + description: "Total number of flights" + total_distance: + expr: _.distance.sum() + description: "Total distance traveled" + avg_distance: + expr: _.distance.mean() + description: "Average flight distance" + +carriers: + table: carriers_tbl + dimensions: + code: + expr: _.code + description: "Carrier code" + name: + expr: _.name + description: "Carrier name" + measures: + carrier_count: + expr: _.count() + description: "Number of carriers" diff --git a/docs/eslint.config.js b/docs/eslint.config.js new file mode 100644 index 00000000..40f72cc4 --- /dev/null +++ b/docs/eslint.config.js @@ -0,0 +1,26 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + "@typescript-eslint/no-unused-vars": "off", + }, + }, +); diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..5aebcd5b --- /dev/null +++ b/docs/index.html @@ -0,0 +1,52 @@ + + + + + + Boring Semantic Layer - Lightweight Ibis-powered Semantic Layer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 00000000..9a622dcb --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,9023 @@ +{ + "name": "vite_react_shadcn_ts", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vite_react_shadcn_ts", + "version": "0.0.0", + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-query": "^5.83.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "highlight.js": "^11.11.1", + "input-otp": "^1.4.2", + "lucide-react": "^0.462.0", + "next-themes": "^0.4.6", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.61.1", + "react-markdown": "^10.1.0", + "react-resizable-panels": "^2.1.9", + "react-router-dom": "^6.30.1", + "recharts": "^2.15.4", + "rehype-raw": "^7.0.0", + "sonner": "^1.7.4", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.9", + "vega-embed": "^7.1.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@eslint/js": "^9.32.0", + "@tailwindcss/typography": "^0.5.16", + "@types/node": "^22.16.5", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react-swc": "^3.11.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.32.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "lovable-tagger": "^1.1.11", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "typescript-eslint": "^8.38.0", + "vite": "^5.4.19" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.9.tgz", + "integrity": "sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz", + "integrity": "sha512-OwS2CM5KocvQ/k7dFJa8i5bNGJP0hXWfVCfDkqRFP1IreH1JDC7wG6eCYCi0+McbfT8OR/kNqsI0UU0xP9H6PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.2", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", + "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.11.tgz", + "integrity": "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collapsible": "1.1.11", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.14.tgz", + "integrity": "sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.14", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz", + "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", + "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.15.tgz", + "integrity": "sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", + "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.14.tgz", + "integrity": "sha512-CPYZ24Mhirm+g6D8jArmLzjYu4Eyg3TTUHswR26QgzXBHBe64BO/RHOJKzmF/Dxb4y4f9PKyJdwm/O/AhNkb+Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", + "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.15.tgz", + "integrity": "sha512-Z71C7LGD+YDYo3TV81paUs8f3Zbmkvg6VLRQpKYfzioOE6n7fOhA3ApK/V/2Odolxjoc4ENk8AYCjohCNayd5A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.13.tgz", + "integrity": "sha512-WG8wWfDiJlSF5hELjwfjSGOXcBR/ZMhBFCGYe8vERpC39CQYZeq1PQ2kaYHdye3V95d06H89KGMsVCIE4LWo3g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", + "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz", + "integrity": "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz", + "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.5.tgz", + "integrity": "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz", + "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", + "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.14.tgz", + "integrity": "sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.9.tgz", + "integrity": "sha512-ZoFkBBz9zv9GWer7wIjvdRxmh2wyc2oKWw6C6CseWd6/yq1DK/l5lJ+wnsmFwJZbBYqr02mrf8A2q/CVCuM3ZA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.10.tgz", + "integrity": "sha512-kiU694Km3WFLTC75DdqgM/3Jauf3rD9wxeS9XtyWFKsBUeZA337lC+6uUazT7I1DhanZ5gyD5Stf8uf2dbQxOQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-toggle": "1.1.9", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz", + "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.2.tgz", + "integrity": "sha512-YWqn+0IKXDhqVLKoac4v2tV6hJqB/wOh8/Br8zjqeqBkKa77Qb0Kw2i7LOFzjFNZbZaPH6AlMGlBwNrxaauaAg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.23" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.13.2", + "@swc/core-darwin-x64": "1.13.2", + "@swc/core-linux-arm-gnueabihf": "1.13.2", + "@swc/core-linux-arm64-gnu": "1.13.2", + "@swc/core-linux-arm64-musl": "1.13.2", + "@swc/core-linux-x64-gnu": "1.13.2", + "@swc/core-linux-x64-musl": "1.13.2", + "@swc/core-win32-arm64-msvc": "1.13.2", + "@swc/core-win32-ia32-msvc": "1.13.2", + "@swc/core-win32-x64-msvc": "1.13.2" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.2.tgz", + "integrity": "sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.2.tgz", + "integrity": "sha512-Lb9EZi7X2XDAVmuUlBm2UvVAgSCbD3qKqDCxSI4jEOddzVOpNCnyZ/xEampdngUIyDDhhJLYU9duC+Mcsv5Y+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.2.tgz", + "integrity": "sha512-9TDe/92ee1x57x+0OqL1huG4BeljVx0nWW4QOOxp8CCK67Rpc/HHl2wciJ0Kl9Dxf2NvpNtkPvqj9+BUmM9WVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.2.tgz", + "integrity": "sha512-KJUSl56DBk7AWMAIEcU83zl5mg3vlQYhLELhjwRFkGFMvghQvdqQ3zFOYa4TexKA7noBZa3C8fb24rI5sw9Exg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.2.tgz", + "integrity": "sha512-teU27iG1oyWpNh9CzcGQ48ClDRt/RCem7mYO7ehd2FY102UeTws2+OzLESS1TS1tEZipq/5xwx3FzbVgiolCiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.2.tgz", + "integrity": "sha512-dRPsyPyqpLD0HMRCRpYALIh4kdOir8pPg4AhNQZLehKowigRd30RcLXGNVZcc31Ua8CiPI4QSgjOIxK+EQe4LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.2.tgz", + "integrity": "sha512-CCxETW+KkYEQDqz1SYC15YIWYheqFC+PJVOW76Maa/8yu8Biw+HTAcblKf2isrlUtK8RvrQN94v3UXkC2NzCEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.2.tgz", + "integrity": "sha512-Wv/QTA6PjyRLlmKcN6AmSI4jwSMRl0VTLGs57PHTqYRwwfwd7y4s2fIPJVBNbAlXd795dOEP6d/bGSQSyhOX3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.2.tgz", + "integrity": "sha512-PuCdtNynEkUNbUXX/wsyUC+t4mamIU5y00lT5vJcAvco3/r16Iaxl5UCzhXYaWZSNVZMzPp9qN8NlSL8M5pPxw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.2.tgz", + "integrity": "sha512-qlmMkFZJus8cYuBURx1a3YAG2G7IW44i+FEYV5/32ylKkzGNAr9tDJSA53XNnNXkAB5EXSPsOz7bn5C3JlEtdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz", + "integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.83.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz", + "integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.83.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz", + "integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.83.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", + "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.38.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "peer": true, + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT", + "peer": true + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "peer": true, + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "peer": true, + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "peer": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "peer": true, + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo-projection": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz", + "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==", + "license": "ISC", + "peer": true, + "dependencies": { + "commander": "7", + "d3-array": "1 - 3", + "d3-geo": "1.12.0 - 3" + }, + "bin": { + "geo2svg": "bin/geo2svg.js", + "geograticule": "bin/geograticule.js", + "geoproject": "bin/geoproject.js", + "geoquantize": "bin/geoquantize.js", + "geostitch": "bin/geostitch.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo-projection/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "peer": true, + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.192", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", + "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==", + "dev": true, + "license": "ISC" + }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", + "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "peer": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, + "node_modules/input-otp": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", + "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lovable-tagger": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/lovable-tagger/-/lovable-tagger-1.1.11.tgz", + "integrity": "sha512-G1gUZi8CebQpB/5+IHWYekRyeRFF2RR7iXSjGO+iVWpwlpa19swgYCYem2z+IkBJO0fKRYJ98xz4yhdt++MzLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.8", + "esbuild": "^0.25.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12", + "tailwindcss": "^3.4.17" + }, + "peerDependencies": { + "vite": ">=5.0.0 <8.0.0" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/lovable-tagger/node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "0.462.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.462.0.tgz", + "integrity": "sha512-NTL7EbAao9IFtuSivSZgrAh4fZd09Lr+6MTkqIxuHaH2nnYiYIzXPo06cOxHg9wKLdj6LL8TByG4qpePqwgx/g==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "8.10.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", + "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.61.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.61.1.tgz", + "integrity": "sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable-panels": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.9.tgz", + "integrity": "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-router": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense", + "peer": true + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sonner": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", + "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.18.tgz", + "integrity": "sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.11" + } + }, + "node_modules/style-to-object": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.11.tgz", + "integrity": "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "license": "ISC", + "peer": true, + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/topojson-client/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "peer": true + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz", + "integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.38.0", + "@typescript-eslint/parser": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vaul": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz", + "integrity": "sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/vega": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vega/-/vega-6.2.0.tgz", + "integrity": "sha512-BIwalIcEGysJdQDjeVUmMWB3e50jPDNAMfLJscjEvpunU9bSt7X1OYnQxkg3uBwuRRI4nWfFZO9uIW910nLeGw==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "vega-crossfilter": "~5.1.0", + "vega-dataflow": "~6.1.0", + "vega-encode": "~5.1.0", + "vega-event-selector": "~4.0.0", + "vega-expression": "~6.1.0", + "vega-force": "~5.1.0", + "vega-format": "~2.1.0", + "vega-functions": "~6.1.0", + "vega-geo": "~5.1.0", + "vega-hierarchy": "~5.1.0", + "vega-label": "~2.1.0", + "vega-loader": "~5.1.0", + "vega-parser": "~7.1.0", + "vega-projection": "~2.1.0", + "vega-regression": "~2.1.0", + "vega-runtime": "~7.1.0", + "vega-scale": "~8.1.0", + "vega-scenegraph": "~5.1.0", + "vega-statistics": "~2.0.0", + "vega-time": "~3.1.0", + "vega-transforms": "~5.1.0", + "vega-typings": "~2.1.0", + "vega-util": "~2.1.0", + "vega-view": "~6.1.0", + "vega-view-transforms": "~5.1.0", + "vega-voronoi": "~5.1.0", + "vega-wordcloud": "~5.1.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + } + }, + "node_modules/vega-canvas": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-2.0.0.tgz", + "integrity": "sha512-9x+4TTw/USYST5nx4yN272sy9WcqSRjAR0tkQYZJ4cQIeon7uVsnohvoPQK1JZu7K1QXGUqzj08z0u/UegBVMA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/vega-crossfilter": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-crossfilter/-/vega-crossfilter-5.1.0.tgz", + "integrity": "sha512-EmVhfP3p6AM7o/lPan/QAoqjblI19BxWUlvl2TSs0xjQd8KbaYYbS4Ixt3cmEvl0QjRdBMF6CdJJ/cy9DTS4Fw==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-dataflow": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-dataflow/-/vega-dataflow-6.1.0.tgz", + "integrity": "sha512-JxumGlODtFbzoQ4c/jQK8Tb/68ih0lrexlCozcMfTAwQ12XhTqCvlafh7MAKKTMBizjOfaQTHm4Jkyb1H5CfyQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "vega-format": "^2.1.0", + "vega-loader": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-embed": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/vega-embed/-/vega-embed-7.1.0.tgz", + "integrity": "sha512-ZmEIn5XJrQt7fSh2lwtSdXG/9uf3yIqZnvXFEwBJRppiBgrEWZcZbj6VK3xn8sNTFQ+sQDXW5sl/6kmbAW3s5A==", + "license": "BSD-3-Clause", + "dependencies": { + "fast-json-patch": "^3.1.1", + "json-stringify-pretty-compact": "^4.0.0", + "semver": "^7.7.2", + "tslib": "^2.8.1", + "vega-interpreter": "^2.0.0", + "vega-schema-url-parser": "^3.0.2", + "vega-themes": "3.0.0", + "vega-tooltip": "1.0.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "*", + "vega-lite": "*" + } + }, + "node_modules/vega-encode": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-5.1.0.tgz", + "integrity": "sha512-q26oI7B+MBQYcTQcr5/c1AMsX3FvjZLQOBi7yI0vV+GEn93fElDgvhQiYrgeYSD4Exi/jBPeUXuN6p4bLz16kA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "^3.2.4", + "d3-interpolate": "^3.0.1", + "vega-dataflow": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-event-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-4.0.0.tgz", + "integrity": "sha512-CcWF4m4KL/al1Oa5qSzZ5R776q8lRxCj3IafCHs5xipoEHrkgu1BWa7F/IH5HrDNXeIDnqOpSV1pFsAWRak4gQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/vega-expression": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-6.1.0.tgz", + "integrity": "sha512-hHgNx/fQ1Vn1u6vHSamH7lRMsOa/yQeHGGcWVmh8fZafLdwdhCM91kZD9p7+AleNpgwiwzfGogtpATFaMmDFYg==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.8", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-expression/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT", + "peer": true + }, + "node_modules/vega-force": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-force/-/vega-force-5.1.0.tgz", + "integrity": "sha512-wdnchOSeXpF9Xx8Yp0s6Do9F7YkFeOn/E/nENtsI7NOcyHpICJ5+UkgjUo9QaQ/Yu+dIDU+sP/4NXsUtq6SMaQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-force": "^3.0.0", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-format/-/vega-format-2.1.0.tgz", + "integrity": "sha512-i9Ht33IgqG36+S1gFDpAiKvXCPz+q+1vDhDGKK8YsgMxGOG4PzinKakI66xd7SdV4q97FgpR7odAXqtDN2wKqw==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "^3.2.4", + "d3-format": "^3.1.0", + "d3-time-format": "^4.1.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-functions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-6.1.0.tgz", + "integrity": "sha512-yooEbWt0FWMBNoohwLsl25lEh08WsWabTXbbS+q0IXZzWSpX4Cyi45+q7IFyy/2L4oaIfGIIV14dgn3srQQcGA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-geo": "^3.1.1", + "vega-dataflow": "^6.1.0", + "vega-expression": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-scenegraph": "^5.1.0", + "vega-selections": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-geo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-geo/-/vega-geo-5.1.0.tgz", + "integrity": "sha512-H8aBBHfthc3rzDbz/Th18+Nvp00J73q3uXGAPDQqizioDm/CoXCK8cX4pMePydBY9S6ikBiGJrLKFDa80wI20g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-geo": "^3.1.1", + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-projection": "^2.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-hierarchy": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-hierarchy/-/vega-hierarchy-5.1.0.tgz", + "integrity": "sha512-rZlU8QJNETlB6o73lGCPybZtw2fBBsRIRuFE77aCLFHdGsh6wIifhplVarqE9icBqjUHRRUOmcEYfzwVIPr65g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-hierarchy": "^3.1.2", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-interpreter": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vega-interpreter/-/vega-interpreter-2.2.1.tgz", + "integrity": "sha512-o+4ZEme2mdFLewlpF76dwPWW2VkZ3TAF3DMcq75/NzA5KPvnN4wnlCM8At2FVawbaHRyGdVkJSS5ROF5KwpHPQ==", + "license": "BSD-3-Clause", + "dependencies": { + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-label": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-label/-/vega-label-2.1.0.tgz", + "integrity": "sha512-/hgf+zoA3FViDBehrQT42Lta3t8In6YwtMnwjYlh72zNn1p3c7E3YUBwqmAqTM1x+tudgzMRGLYig+bX1ewZxQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-lite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-6.4.1.tgz", + "integrity": "sha512-KO3ybHNouRK4A0al/+2fN9UqgTEfxrd/ntGLY933Hg5UOYotDVQdshR3zn7OfXwQ7uj0W96Vfa5R+QxO8am3IQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "json-stringify-pretty-compact": "~4.0.0", + "tslib": "~2.8.1", + "vega-event-selector": "~4.0.0", + "vega-expression": "~6.1.0", + "vega-util": "~2.1.0", + "yargs": "~18.0.0" + }, + "bin": { + "vl2pdf": "bin/vl2pdf", + "vl2png": "bin/vl2png", + "vl2svg": "bin/vl2svg", + "vl2vg": "bin/vl2vg" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "^6.0.0" + } + }, + "node_modules/vega-loader": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-5.1.0.tgz", + "integrity": "sha512-GaY3BdSPbPNdtrBz8SYUBNmNd8mdPc3mtdZfdkFazQ0RD9m+Toz5oR8fKnTamNSk9fRTJX0Lp3uEqxrAlQVreg==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-dsv": "^3.0.1", + "topojson-client": "^3.1.0", + "vega-format": "^2.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-7.1.0.tgz", + "integrity": "sha512-g0lrYxtmYVW8G6yXpIS4J3Uxt9OUSkc0bLu5afoYDo4rZmoOOdll3x3ebActp5LHPW+usZIE+p5nukRS2vEc7Q==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-event-selector": "^4.0.0", + "vega-functions": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-projection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-2.1.0.tgz", + "integrity": "sha512-EjRjVSoMR5ibrU7q8LaOQKP327NcOAM1+eZ+NO4ANvvAutwmbNVTmfA1VpPH+AD0AlBYc39ND/wnRk7SieDiXA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-geo": "^3.1.1", + "d3-geo-projection": "^4.0.0", + "vega-scale": "^8.1.0" + } + }, + "node_modules/vega-regression": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-regression/-/vega-regression-2.1.0.tgz", + "integrity": "sha512-HzC7MuoEwG1rIxRaNTqgcaYF03z/ZxYkQR2D5BN0N45kLnHY1HJXiEcZkcffTsqXdspLjn47yLi44UoCwF5fxQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-runtime": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/vega-runtime/-/vega-runtime-7.1.0.tgz", + "integrity": "sha512-mItI+WHimyEcZlZrQ/zYR3LwHVeyHCWwp7MKaBjkU8EwkSxEEGVceyGUY9X2YuJLiOgkLz/6juYDbMv60pfwYA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-scale": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-8.1.0.tgz", + "integrity": "sha512-VEgDuEcOec8+C8+FzLcnAmcXrv2gAJKqQifCdQhkgnsLa978vYUgVfCut/mBSMMHbH8wlUV1D0fKZTjRukA1+A==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "^3.2.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.1.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-scenegraph": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-5.1.0.tgz", + "integrity": "sha512-4gA89CFIxkZX+4Nvl8SZF2MBOqnlj9J5zgdPh/HPx+JOwtzSlUqIhxFpFj7GWYfwzr/PyZnguBLPihPw1Og/cA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-path": "^3.1.0", + "d3-shape": "^3.2.0", + "vega-canvas": "^2.0.0", + "vega-loader": "^5.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-schema-url-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vega-schema-url-parser/-/vega-schema-url-parser-3.0.2.tgz", + "integrity": "sha512-xAnR7KAvNPYewI3O0l5QGdT8Tv0+GCZQjqfP39cW/hbe/b3aYMAQ39vm8O2wfXUHzm04xTe7nolcsx8WQNVLRQ==", + "license": "BSD-3-Clause" + }, + "node_modules/vega-selections": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-6.1.0.tgz", + "integrity": "sha512-WaHM7D7ghHceEfMsgFeaZnDToWL0mgCFtStVOobNh/OJLh0CL7yNKeKQBqRXJv2Lx74dPNf6nj08+52ytWfW7g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "3.2.4", + "vega-expression": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-statistics": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-2.0.0.tgz", + "integrity": "sha512-dGPfDXnBlgXbZF3oxtkb8JfeRXd5TYHx25Z/tIoaa9jWua4Vf/AoW2wwh8J1qmMy8J03/29aowkp1yk4DOPazQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "^3.2.4" + } + }, + "node_modules/vega-themes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vega-themes/-/vega-themes-3.0.0.tgz", + "integrity": "sha512-1iFiI3BNmW9FrsLnDLx0ZKEddsCitRY3XmUAwp6qmp+p+IXyJYc9pfjlVj9E6KXBPfm4cQyU++s0smKNiWzO4g==", + "license": "BSD-3-Clause", + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "*", + "vega-lite": "*" + } + }, + "node_modules/vega-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vega-time/-/vega-time-3.1.0.tgz", + "integrity": "sha512-G93mWzPwNa6UYQRkr8Ujur9uqxbBDjDT/WpXjbDY0yygdSkRT+zXF+Sb4gjhW0nPaqdiwkn0R6kZcSPMj1bMNA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "^3.2.4", + "d3-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-tooltip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vega-tooltip/-/vega-tooltip-1.0.0.tgz", + "integrity": "sha512-P1R0JP29v0qnTuwzCQ0SPJlkjAzr6qeyj+H4VgUFSykHmHc1OBxda//XBaFDl/bZgIscEMvjKSjZpXd84x3aZQ==", + "license": "BSD-3-Clause", + "dependencies": { + "vega-util": "^2.0.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + } + }, + "node_modules/vega-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-5.1.0.tgz", + "integrity": "sha512-mj/sO2tSuzzpiXX8JSl4DDlhEmVwM/46MTAzTNQUQzJPMI/n4ChCjr/SdEbfEyzlD4DPm1bjohZGjLc010yuMg==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-typings": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-2.1.0.tgz", + "integrity": "sha512-zdis4Fg4gv37yEvTTSZEVMNhp8hwyEl7GZ4X4HHddRVRKxWFsbyKvZx/YW5Z9Ox4sjxVA2qHzEbod4Fdx+SEJA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@types/geojson": "7946.0.16", + "vega-event-selector": "^4.0.0", + "vega-expression": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-util": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-2.1.0.tgz", + "integrity": "sha512-PGfp0m0QCufDmcxKJCWQy4Ov23FoF8DSXmoJwSezi3itQaa2hbxK0+xwsTMP2vy4PR16Pu25HMzgMwXVW1+33w==", + "license": "BSD-3-Clause" + }, + "node_modules/vega-view": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-6.1.0.tgz", + "integrity": "sha512-hmHDm/zC65lb23mb9Tr9Gx0wkxP0TMS31LpMPYxIZpvInxvUn7TYitkOtz1elr63k2YZrgmF7ztdGyQ4iCQ5fQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "^3.2.4", + "d3-timer": "^3.0.1", + "vega-dataflow": "^6.1.0", + "vega-format": "^2.1.0", + "vega-functions": "^6.1.0", + "vega-runtime": "^7.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-view-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-view-transforms/-/vega-view-transforms-5.1.0.tgz", + "integrity": "sha512-fpigh/xn/32t+An1ShoY3MLeGzNdlbAp2+HvFKzPpmpMTZqJEWkk/J/wHU7Swyc28Ta7W1z3fO+8dZkOYO5TWQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-voronoi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-5.1.0.tgz", + "integrity": "sha512-uKdsoR9x60mz7eYtVG+NhlkdQXeVdMr6jHNAHxs+W+i6kawkUp5S9jp1xf1FmW/uZvtO1eqinHQNwATcDRsiUg==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-delaunay": "^6.0.4", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-wordcloud": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-wordcloud/-/vega-wordcloud-5.1.0.tgz", + "integrity": "sha512-sSdNmT8y2D7xXhM2h76dKyaYn3PA4eV49WUUkfYfqHz/vpcu10GSAoFxLhQQTkbZXR+q5ZB63tFUow9W2IFo6g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "peer": true, + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT", + "peer": true + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..89d7e9e7 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,87 @@ +{ + "name": "vite_react_shadcn_ts", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "npm run build:bsl && vite build", + "build:bsl": "uv run scripts/build_data.py", + "build:dev": "vite build --mode development", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-query": "^5.83.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "highlight.js": "^11.11.1", + "input-otp": "^1.4.2", + "lucide-react": "^0.462.0", + "next-themes": "^0.4.6", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.61.1", + "react-markdown": "^10.1.0", + "react-resizable-panels": "^2.1.9", + "react-router-dom": "^6.30.1", + "recharts": "^2.15.4", + "rehype-raw": "^7.0.0", + "sonner": "^1.7.4", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.9", + "vega-embed": "^7.1.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@eslint/js": "^9.32.0", + "@tailwindcss/typography": "^0.5.16", + "@types/node": "^22.16.5", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react-swc": "^3.11.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.32.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "typescript-eslint": "^8.38.0", + "vite": "^5.4.19" + } +} diff --git a/docs/postcss.config.js b/docs/postcss.config.js new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/docs/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/docs/public/404.html b/docs/public/404.html new file mode 100644 index 00000000..861e23e7 --- /dev/null +++ b/docs/public/404.html @@ -0,0 +1,39 @@ + + + + + Boring Semantic Layer + + + + + diff --git a/docs/public/bsl-data/bucketing.json b/docs/public/bsl-data/bucketing.json new file mode 100644 index 00000000..b1d59b25 --- /dev/null +++ b/docs/public/bsl-data/bucketing.json @@ -0,0 +1,865 @@ +{ + "markdown": "# Bucketing with 'Other'\n\nLimit displayed group-by values while consolidating remaining items into an 'Other' category. This pattern maintains focus on top-performing segments while capturing complete data and handling long-tail distributions.\n\n## Overview\n\nThe bucketing with 'Other' pattern allows you to:\n\n- Focus on top N items while grouping the rest as 'Other'\n- Use window functions to rank and identify top performers\n- Create custom ranges for continuous values (e.g., age groups, price tiers)\n- Consolidate low-frequency items into an \"Other\" category\n- Maintain analytical clarity by reducing dimensional cardinality\n\n## Setup\n\nLet's create customer data with ages and purchase amounts:\n\n```setup_raw_data\nimport ibis\nfrom ibis import _\nfrom boring_semantic_layer import to_semantic_table\n\n# Create customer transaction data\ncustomer_data = ibis.memtable({\n \"customer_id\": list(range(1, 21)),\n \"age\": [22, 28, 35, 42, 19, 55, 31, 67, 24, 38, 45, 29, 51, 33, 61, 26, 48, 36, 58, 41],\n \"purchase_amount\": [45, 120, 250, 180, 35, 520, 95, 850, 65, 310, 190, 78, 420, 145, 680, 88, 275, 165, 590, 225],\n \"product_category\": [\"Electronics\", \"Clothing\", \"Electronics\", \"Home\", \"Clothing\", \"Electronics\",\n \"Clothing\", \"Electronics\", \"Clothing\", \"Home\", \"Electronics\", \"Clothing\",\n \"Home\", \"Clothing\", \"Electronics\", \"Clothing\", \"Home\", \"Electronics\", \"Electronics\", \"Home\"]\n})\n```\n\n\n\nNow create a semantic table with dimensions and measures:\n\n```semantic_table_def\nfrom boring_semantic_layer import to_semantic_table\n\ncustomer_st = (\n to_semantic_table(customer_data, name=\"customers\")\n .with_dimensions(\n customer_id=lambda t: t.customer_id,\n age=lambda t: t.age,\n product_category=lambda t: t.product_category\n )\n .with_measures(\n customer_count=lambda t: t.count(),\n total_revenue=lambda t: t.purchase_amount.sum(),\n avg_purchase=lambda t: t.purchase_amount.mean().round(2)\n )\n)\n```\n\n\n\n## Top Categories with 'Other'\n\nThe most common bucketing pattern: show top N items by a metric, consolidate the rest as 'Other'. This uses a two-stage approach with window functions to rank items.\n\n```query_top_categories\nfrom ibis import _\n\n# Two-stage pipeline: rank then consolidate\nresult = (\n customer_st\n .group_by(\"product_category\")\n .aggregate(\"total_revenue\", \"customer_count\")\n .mutate(\n # Rank categories by revenue\n rank=lambda t: ibis.row_number().over(\n ibis.window(order_by=t.total_revenue.desc())\n )\n )\n .mutate(\n # Replace non-top categories with \"Other\"\n category_display=lambda t: ibis.case()\n .when(t.rank <= 2, t.product_category)\n .else_(\"Other\")\n .end(),\n # Keep original revenue for sorting (only for top categories)\n sort_value=lambda t: ibis.case()\n .when(t.rank <= 2, t.total_revenue)\n .else_(0)\n .end()\n )\n .group_by(\"category_display\")\n .aggregate(\n revenue=lambda t: t.total_revenue.sum(),\n customers=lambda t: t.customer_count.sum(),\n sort_helper=lambda t: t.sort_value.max()\n )\n .mutate(\n avg_per_customer=lambda t: (t.revenue / t.customers).round(2)\n )\n .order_by(_.sort_helper.desc())\n)\n```\n\n\n\n\nThe window function `row_number()` ranks categories by revenue. Non-top items are marked with `is_other`, then consolidated into a single 'Other' category. The `sort_helper` field ensures top categories appear first, sorted by their original revenue, with 'Other' at the end.\n\n\n## Age Range Bucketing\n\nCreate age buckets using case expressions:\n\n```query_age_buckets\nfrom ibis import _\nresult = (\n customer_st\n .group_by(\"customer_id\", \"age\", \"product_category\")\n .aggregate(\"total_revenue\")\n .mutate(\n age_group=lambda t: ibis.case()\n .when(t.age < 25, \"18-24\")\n .when(t.age < 35, \"25-34\")\n .when(t.age < 45, \"35-44\")\n .when(t.age < 55, \"45-54\")\n .else_(\"55+\")\n .end()\n )\n .group_by(\"age_group\")\n .aggregate(\n customers=lambda t: t.count(),\n revenue=lambda t: t.total_revenue.sum()\n )\n .order_by(_.age_group)\n)\n```\n\n\n\n## Purchase Amount Tiers\n\nCategorize purchases into value tiers:\n\n```query_purchase_tiers\nfrom ibis import _\nresult = (\n customer_st\n .group_by(\"customer_id\")\n .aggregate(\"total_revenue\")\n .mutate(\n tier=lambda t: ibis.case()\n .when(t.total_revenue < 100, \"Small ($0-99)\")\n .when(t.total_revenue < 250, \"Medium ($100-249)\")\n .when(t.total_revenue < 500, \"Large ($250-499)\")\n .else_(\"Premium ($500+)\")\n .end()\n )\n .group_by(\"tier\")\n .aggregate(\n customer_count=lambda t: t.count(),\n total_value=lambda t: t.total_revenue.sum(),\n avg_value=lambda t: t.total_revenue.mean().round(2)\n )\n .order_by(_.total_value.desc())\n)\n```\n\n\n\n## Threshold-Based 'Other' Category\n\nInstead of ranking, you can consolidate categories based on a threshold (e.g., minimum customer count):\n\n```query_with_other\nfrom ibis import _\n\nresult = (\n customer_st\n .group_by(\"product_category\")\n .aggregate(\"total_revenue\", \"customer_count\")\n .mutate(\n # Mark categories with less than 5 customers as \"Other\"\n category_grouped=lambda t: ibis.case()\n .when(t.customer_count >= 5, t.product_category)\n .else_(\"Other\")\n .end()\n )\n .group_by(\"category_grouped\")\n .aggregate(\n customers=lambda t: t.customer_count.sum(),\n revenue=lambda t: t.total_revenue.sum()\n )\n .mutate(\n avg_per_customer=lambda t: (t.revenue / t.customers).round(2)\n )\n .order_by(_.revenue.desc())\n)\n```\n\n\n\n\nThis approach uses a fixed threshold rather than ranking. Categories with fewer than 5 customers are consolidated into 'Other'. This is simpler but less dynamic than the window function approach.\n\n\n## Combined Bucketing\n\nCombine age groups and purchase tiers for multi-dimensional segmentation:\n\n```query_combined_buckets\nfrom ibis import _\nresult = (\n customer_st\n .group_by(\"customer_id\", \"age\")\n .aggregate(\"total_revenue\")\n .mutate(\n age_group=lambda t: ibis.case()\n .when(t.age < 30, \"Young (18-29)\")\n .when(t.age < 50, \"Middle (30-49)\")\n .else_(\"Senior (50+)\")\n .end(),\n value_tier=lambda t: ibis.case()\n .when(t.total_revenue < 150, \"Low Value\")\n .when(t.total_revenue < 350, \"Mid Value\")\n .else_(\"High Value\")\n .end()\n )\n .group_by(\"age_group\", \"value_tier\")\n .aggregate(\n customers=lambda t: t.count(),\n revenue=lambda t: t.total_revenue.sum()\n )\n .order_by(_.age_group, _.revenue.desc())\n)\n```\n\n\n\n## Use Cases\n\n**Focus on Top Performers**: Show top 10 products by revenue, consolidate the rest as 'Other' to highlight key items while maintaining complete totals.\n\n**Long-Tail Distribution Management**: In e-commerce, display top categories while grouping niche categories as 'Other' to simplify reporting and dashboards.\n\n**Threshold-Based Filtering**: Consolidate low-volume customer segments (< 100 customers) into 'Other' to focus on statistically significant groups.\n\n**Age and Value Segmentation**: Create meaningful customer segments by combining age ranges (Young, Middle, Senior) with purchase tiers (Low, Mid, High).\n\n## Key Takeaways\n\n- Use window functions like `row_number()` to rank items for dynamic top-N selection\n- Two-stage pattern: rank first, then consolidate and re-aggregate\n- `ibis.case().when()...else_().end()` provides flexible bucketing logic\n- Threshold-based 'Other' works well when you have a clear cutoff value\n- Sort helper fields ensure 'Other' appears at the end of results\n- 'Other' category maintains complete data while reducing cardinality\n\n## Next Steps\n\n- Learn about [Sessionized Data](/advanced/sessionized) for time-based grouping\n- Explore [Indexing](/advanced/indexing) for baseline comparisons\n", + "queries": { + "setup_raw_data": { + "code": "import ibis\nfrom ibis import _\nfrom boring_semantic_layer import to_semantic_table\n\n# Create customer transaction data\ncustomer_data = ibis.memtable({\n \"customer_id\": list(range(1, 21)),\n \"age\": [22, 28, 35, 42, 19, 55, 31, 67, 24, 38, 45, 29, 51, 33, 61, 26, 48, 36, 58, 41],\n \"purchase_amount\": [45, 120, 250, 180, 35, 520, 95, 850, 65, 310, 190, 78, 420, 145, 680, 88, 275, 165, 590, 225],\n \"product_category\": [\"Electronics\", \"Clothing\", \"Electronics\", \"Home\", \"Clothing\", \"Electronics\",\n \"Clothing\", \"Electronics\", \"Clothing\", \"Home\", \"Electronics\", \"Clothing\",\n \"Home\", \"Clothing\", \"Electronics\", \"Clothing\", \"Home\", \"Electronics\", \"Electronics\", \"Home\"]\n})", + "sql": "Error generating SQL: Table.sql() missing 1 required positional argument: 'query'", + "table": { + "columns": [ + "customer_id", + "age", + "purchase_amount", + "product_category" + ], + "data": [ + [ + 1, + 22, + 45, + "Electronics" + ], + [ + 2, + 28, + 120, + "Clothing" + ], + [ + 3, + 35, + 250, + "Electronics" + ], + [ + 4, + 42, + 180, + "Home" + ], + [ + 5, + 19, + 35, + "Clothing" + ], + [ + 6, + 55, + 520, + "Electronics" + ], + [ + 7, + 31, + 95, + "Clothing" + ], + [ + 8, + 67, + 850, + "Electronics" + ], + [ + 9, + 24, + 65, + "Clothing" + ], + [ + 10, + 38, + 310, + "Home" + ], + [ + 11, + 45, + 190, + "Electronics" + ], + [ + 12, + 29, + 78, + "Clothing" + ], + [ + 13, + 51, + 420, + "Home" + ], + [ + 14, + 33, + 145, + "Clothing" + ], + [ + 15, + 61, + 680, + "Electronics" + ], + [ + 16, + 26, + 88, + "Clothing" + ], + [ + 17, + 48, + 275, + "Home" + ], + [ + 18, + 36, + 165, + "Electronics" + ], + [ + 19, + 58, + 590, + "Electronics" + ], + [ + 20, + 41, + 225, + "Home" + ] + ] + } + }, + "semantic_table_def": { + "code": "from boring_semantic_layer import to_semantic_table\n\ncustomer_st = (\n to_semantic_table(customer_data, name=\"customers\")\n .with_dimensions(\n customer_id=lambda t: t.customer_id,\n age=lambda t: t.age,\n product_category=lambda t: t.product_category\n )\n .with_measures(\n customer_count=lambda t: t.count(),\n total_revenue=lambda t: t.purchase_amount.sum(),\n avg_purchase=lambda t: t.purchase_amount.mean().round(2)\n )\n)", + "sql": "SELECT\n *\nFROM \"ibis_pandas_memtable_h5l62twvjnh2hehoabnjjkjasa\"", + "table": { + "columns": [ + "customer_id", + "age", + "purchase_amount", + "product_category" + ], + "data": [ + [ + 1, + 22, + 45, + "Electronics" + ], + [ + 2, + 28, + 120, + "Clothing" + ], + [ + 3, + 35, + 250, + "Electronics" + ], + [ + 4, + 42, + 180, + "Home" + ], + [ + 5, + 19, + 35, + "Clothing" + ], + [ + 6, + 55, + 520, + "Electronics" + ], + [ + 7, + 31, + 95, + "Clothing" + ], + [ + 8, + 67, + 850, + "Electronics" + ], + [ + 9, + 24, + 65, + "Clothing" + ], + [ + 10, + 38, + 310, + "Home" + ], + [ + 11, + 45, + 190, + "Electronics" + ], + [ + 12, + 29, + 78, + "Clothing" + ], + [ + 13, + 51, + 420, + "Home" + ], + [ + 14, + 33, + 145, + "Clothing" + ], + [ + 15, + 61, + 680, + "Electronics" + ], + [ + 16, + 26, + 88, + "Clothing" + ], + [ + 17, + 48, + 275, + "Home" + ], + [ + 18, + 36, + 165, + "Electronics" + ], + [ + 19, + 58, + 590, + "Electronics" + ], + [ + 20, + 41, + 225, + "Home" + ] + ] + } + }, + "query_top_categories": { + "code": "from ibis import _\n\n# Two-stage pipeline: rank then consolidate\nresult = (\n customer_st\n .group_by(\"product_category\")\n .aggregate(\"total_revenue\", \"customer_count\")\n .mutate(\n # Rank categories by revenue\n rank=lambda t: ibis.row_number().over(\n ibis.window(order_by=t.total_revenue.desc())\n )\n )\n .mutate(\n # Replace non-top categories with \"Other\"\n category_display=lambda t: ibis.case()\n .when(t.rank <= 2, t.product_category)\n .else_(\"Other\")\n .end(),\n # Keep original revenue for sorting (only for top categories)\n sort_value=lambda t: ibis.case()\n .when(t.rank <= 2, t.total_revenue)\n .else_(0)\n .end()\n )\n .group_by(\"category_display\")\n .aggregate(\n revenue=lambda t: t.total_revenue.sum(),\n customers=lambda t: t.customer_count.sum(),\n sort_helper=lambda t: t.sort_value.max()\n )\n .mutate(\n avg_per_customer=lambda t: (t.revenue / t.customers).round(2)\n )\n .order_by(_.sort_helper.desc())\n)", + "sql": "SELECT\n \"t6\".\"category_display\",\n \"t6\".\"revenue\",\n \"t6\".\"customers\",\n \"t6\".\"sort_helper\",\n CAST(ROUND(\"t6\".\"revenue\" / \"t6\".\"customers\", 2) AS DOUBLE) AS \"avg_per_customer\"\nFROM (\n SELECT\n \"t5\".\"category_display\",\n SUM(\"t5\".\"total_revenue\") AS \"revenue\",\n SUM(\"t5\".\"customer_count\") AS \"customers\",\n MAX(\"t5\".\"sort_value\") AS \"sort_helper\"\n FROM (\n SELECT\n \"t4\".\"product_category\",\n \"t4\".\"total_revenue\",\n \"t4\".\"customer_count\",\n \"t4\".\"rank\",\n CASE WHEN \"t4\".\"rank\" <= 2 THEN \"t4\".\"product_category\" ELSE 'Other' END AS \"category_display\",\n CASE WHEN \"t4\".\"rank\" <= 2 THEN \"t4\".\"total_revenue\" ELSE 0 END AS \"sort_value\"\n FROM (\n SELECT\n \"t3\".\"product_category\",\n \"t3\".\"total_revenue\",\n \"t3\".\"customer_count\",\n ROW_NUMBER() OVER (ORDER BY \"t3\".\"total_revenue\" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) - 1 AS \"rank\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t1\".\"product_category\",\n SUM(\"t1\".\"purchase_amount\") AS \"total_revenue\",\n COUNT(*) AS \"customer_count\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_h5l62twvjnh2hehoabnjjkjasa\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n ) AS \"t2\"\n ) AS \"t3\"\n ) AS \"t4\"\n ) AS \"t5\"\n GROUP BY\n 1\n) AS \"t6\"\nORDER BY\n \"t6\".\"sort_helper\" DESC", + "table": { + "columns": [ + "category_display", + "revenue", + "customers", + "sort_helper", + "avg_per_customer" + ], + "data": [ + [ + "Electronics", + 3290, + 8, + 3290, + 411.25 + ], + [ + "Home", + 1410, + 5, + 1410, + 282.0 + ], + [ + "Clothing", + 626, + 7, + 626, + 89.43 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-b7f52209bdec16852547f9730aec7560" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "category_display", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "category_display", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "revenue", + "customers", + "sort_helper" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-b7f52209bdec16852547f9730aec7560": [ + { + "category_display": "Electronics", + "revenue": 3290, + "customers": 8, + "sort_helper": 3290 + }, + { + "category_display": "Clothing", + "revenue": 626, + "customers": 7, + "sort_helper": 626 + }, + { + "category_display": "Home", + "revenue": 1410, + "customers": 5, + "sort_helper": 1410 + } + ] + } + } + } + }, + "query_age_buckets": { + "code": "from ibis import _\nresult = (\n customer_st\n .group_by(\"customer_id\", \"age\", \"product_category\")\n .aggregate(\"total_revenue\")\n .mutate(\n age_group=lambda t: ibis.case()\n .when(t.age < 25, \"18-24\")\n .when(t.age < 35, \"25-34\")\n .when(t.age < 45, \"35-44\")\n .when(t.age < 55, \"45-54\")\n .else_(\"55+\")\n .end()\n )\n .group_by(\"age_group\")\n .aggregate(\n customers=lambda t: t.count(),\n revenue=lambda t: t.total_revenue.sum()\n )\n .order_by(_.age_group)\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t3\".\"age_group\",\n COUNT(*) AS \"customers\",\n SUM(\"t3\".\"total_revenue\") AS \"revenue\"\n FROM (\n SELECT\n \"t2\".\"customer_id\",\n \"t2\".\"age\",\n \"t2\".\"product_category\",\n \"t2\".\"total_revenue\",\n CASE\n WHEN \"t2\".\"age\" < 25\n THEN '18-24'\n WHEN \"t2\".\"age\" < 35\n THEN '25-34'\n WHEN \"t2\".\"age\" < 45\n THEN '35-44'\n WHEN \"t2\".\"age\" < 55\n THEN '45-54'\n ELSE '55+'\n END AS \"age_group\"\n FROM (\n SELECT\n \"t1\".\"customer_id\",\n \"t1\".\"age\",\n \"t1\".\"product_category\",\n SUM(\"t1\".\"purchase_amount\") AS \"total_revenue\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_h5l62twvjnh2hehoabnjjkjasa\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1,\n 2,\n 3\n ) AS \"t2\"\n ) AS \"t3\"\n GROUP BY\n 1\n) AS \"t4\"\nORDER BY\n \"t4\".\"age_group\" ASC", + "table": { + "columns": [ + "age_group", + "customers", + "revenue" + ], + "data": [ + [ + "18-24", + 3, + 145 + ], + [ + "25-34", + 5, + 526 + ], + [ + "35-44", + 5, + 1130 + ], + [ + "45-54", + 3, + 885 + ], + [ + "55+", + 4, + 2640 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-2a04190a3f702ea78dd2d0ae616acbec" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "age_group", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "age_group", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "customers", + "revenue" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-2a04190a3f702ea78dd2d0ae616acbec": [ + { + "age_group": "55+", + "customers": 4, + "revenue": 2640 + }, + { + "age_group": "25-34", + "customers": 5, + "revenue": 526 + }, + { + "age_group": "45-54", + "customers": 3, + "revenue": 885 + }, + { + "age_group": "35-44", + "customers": 5, + "revenue": 1130 + }, + { + "age_group": "18-24", + "customers": 3, + "revenue": 145 + } + ] + } + } + } + }, + "query_purchase_tiers": { + "code": "from ibis import _\nresult = (\n customer_st\n .group_by(\"customer_id\")\n .aggregate(\"total_revenue\")\n .mutate(\n tier=lambda t: ibis.case()\n .when(t.total_revenue < 100, \"Small ($0-99)\")\n .when(t.total_revenue < 250, \"Medium ($100-249)\")\n .when(t.total_revenue < 500, \"Large ($250-499)\")\n .else_(\"Premium ($500+)\")\n .end()\n )\n .group_by(\"tier\")\n .aggregate(\n customer_count=lambda t: t.count(),\n total_value=lambda t: t.total_revenue.sum(),\n avg_value=lambda t: t.total_revenue.mean().round(2)\n )\n .order_by(_.total_value.desc())\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t3\".\"tier\",\n COUNT(*) AS \"customer_count\",\n SUM(\"t3\".\"total_revenue\") AS \"total_value\",\n CAST(ROUND(AVG(\"t3\".\"total_revenue\"), 2) AS DOUBLE) AS \"avg_value\"\n FROM (\n SELECT\n \"t2\".\"customer_id\",\n \"t2\".\"total_revenue\",\n CASE\n WHEN \"t2\".\"total_revenue\" < 100\n THEN 'Small ($0-99)'\n WHEN \"t2\".\"total_revenue\" < 250\n THEN 'Medium ($100-249)'\n WHEN \"t2\".\"total_revenue\" < 500\n THEN 'Large ($250-499)'\n ELSE 'Premium ($500+)'\n END AS \"tier\"\n FROM (\n SELECT\n \"t1\".\"customer_id\",\n SUM(\"t1\".\"purchase_amount\") AS \"total_revenue\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_h5l62twvjnh2hehoabnjjkjasa\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n ) AS \"t2\"\n ) AS \"t3\"\n GROUP BY\n 1\n) AS \"t4\"\nORDER BY\n \"t4\".\"total_value\" DESC", + "table": { + "columns": [ + "tier", + "customer_count", + "total_value", + "avg_value" + ], + "data": [ + [ + "Premium ($500+)", + 4, + 2640, + 660.0 + ], + [ + "Large ($250-499)", + 4, + 1255, + 313.75 + ], + [ + "Medium ($100-249)", + 6, + 1025, + 170.83 + ], + [ + "Small ($0-99)", + 6, + 406, + 67.67 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-f3c12454c10596136f4e01d88f79dad8" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "tier", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "tier", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "customer_count", + "total_value", + "avg_value" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-f3c12454c10596136f4e01d88f79dad8": [ + { + "tier": "Large ($250-499)", + "customer_count": 4, + "total_value": 1255, + "avg_value": 313.75 + }, + { + "tier": "Small ($0-99)", + "customer_count": 6, + "total_value": 406, + "avg_value": 67.67 + }, + { + "tier": "Medium ($100-249)", + "customer_count": 6, + "total_value": 1025, + "avg_value": 170.83 + }, + { + "tier": "Premium ($500+)", + "customer_count": 4, + "total_value": 2640, + "avg_value": 660.0 + } + ] + } + } + } + }, + "query_with_other": { + "code": "from ibis import _\n\nresult = (\n customer_st\n .group_by(\"product_category\")\n .aggregate(\"total_revenue\", \"customer_count\")\n .mutate(\n # Mark categories with less than 5 customers as \"Other\"\n category_grouped=lambda t: ibis.case()\n .when(t.customer_count >= 5, t.product_category)\n .else_(\"Other\")\n .end()\n )\n .group_by(\"category_grouped\")\n .aggregate(\n customers=lambda t: t.customer_count.sum(),\n revenue=lambda t: t.total_revenue.sum()\n )\n .mutate(\n avg_per_customer=lambda t: (t.revenue / t.customers).round(2)\n )\n .order_by(_.revenue.desc())\n)", + "sql": "SELECT\n \"t4\".\"category_grouped\",\n \"t4\".\"customers\",\n \"t4\".\"revenue\",\n CAST(ROUND(\"t4\".\"revenue\" / \"t4\".\"customers\", 2) AS DOUBLE) AS \"avg_per_customer\"\nFROM (\n SELECT\n \"t3\".\"category_grouped\",\n SUM(\"t3\".\"customer_count\") AS \"customers\",\n SUM(\"t3\".\"total_revenue\") AS \"revenue\"\n FROM (\n SELECT\n \"t2\".\"product_category\",\n \"t2\".\"total_revenue\",\n \"t2\".\"customer_count\",\n CASE WHEN \"t2\".\"customer_count\" >= 5 THEN \"t2\".\"product_category\" ELSE 'Other' END AS \"category_grouped\"\n FROM (\n SELECT\n \"t1\".\"product_category\",\n SUM(\"t1\".\"purchase_amount\") AS \"total_revenue\",\n COUNT(*) AS \"customer_count\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_h5l62twvjnh2hehoabnjjkjasa\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n ) AS \"t2\"\n ) AS \"t3\"\n GROUP BY\n 1\n) AS \"t4\"\nORDER BY\n \"t4\".\"revenue\" DESC", + "table": { + "columns": [ + "category_grouped", + "customers", + "revenue", + "avg_per_customer" + ], + "data": [ + [ + "Electronics", + 8, + 3290, + 411.25 + ], + [ + "Home", + 5, + 1410, + 282.0 + ], + [ + "Clothing", + 7, + 626, + 89.43 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-7c189061b49621685b18b66d9e4fe564" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "category_grouped", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "category_grouped", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "customers", + "revenue" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-7c189061b49621685b18b66d9e4fe564": [ + { + "category_grouped": "Electronics", + "customers": 8, + "revenue": 3290 + }, + { + "category_grouped": "Home", + "customers": 5, + "revenue": 1410 + }, + { + "category_grouped": "Clothing", + "customers": 7, + "revenue": 626 + } + ] + } + } + } + }, + "query_combined_buckets": { + "code": "from ibis import _\nresult = (\n customer_st\n .group_by(\"customer_id\", \"age\")\n .aggregate(\"total_revenue\")\n .mutate(\n age_group=lambda t: ibis.case()\n .when(t.age < 30, \"Young (18-29)\")\n .when(t.age < 50, \"Middle (30-49)\")\n .else_(\"Senior (50+)\")\n .end(),\n value_tier=lambda t: ibis.case()\n .when(t.total_revenue < 150, \"Low Value\")\n .when(t.total_revenue < 350, \"Mid Value\")\n .else_(\"High Value\")\n .end()\n )\n .group_by(\"age_group\", \"value_tier\")\n .aggregate(\n customers=lambda t: t.count(),\n revenue=lambda t: t.total_revenue.sum()\n )\n .order_by(_.age_group, _.revenue.desc())\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t3\".\"age_group\",\n \"t3\".\"value_tier\",\n COUNT(*) AS \"customers\",\n SUM(\"t3\".\"total_revenue\") AS \"revenue\"\n FROM (\n SELECT\n \"t2\".\"customer_id\",\n \"t2\".\"age\",\n \"t2\".\"total_revenue\",\n CASE\n WHEN \"t2\".\"age\" < 30\n THEN 'Young (18-29)'\n WHEN \"t2\".\"age\" < 50\n THEN 'Middle (30-49)'\n ELSE 'Senior (50+)'\n END AS \"age_group\",\n CASE\n WHEN \"t2\".\"total_revenue\" < 150\n THEN 'Low Value'\n WHEN \"t2\".\"total_revenue\" < 350\n THEN 'Mid Value'\n ELSE 'High Value'\n END AS \"value_tier\"\n FROM (\n SELECT\n \"t1\".\"customer_id\",\n \"t1\".\"age\",\n SUM(\"t1\".\"purchase_amount\") AS \"total_revenue\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_h5l62twvjnh2hehoabnjjkjasa\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1,\n 2\n ) AS \"t2\"\n ) AS \"t3\"\n GROUP BY\n 1,\n 2\n) AS \"t4\"\nORDER BY\n \"t4\".\"age_group\" ASC,\n \"t4\".\"revenue\" DESC", + "table": { + "columns": [ + "age_group", + "value_tier", + "customers", + "revenue" + ], + "data": [ + [ + "Middle (30-49)", + "Mid Value", + 7, + 1595 + ], + [ + "Middle (30-49)", + "Low Value", + 2, + 240 + ], + [ + "Senior (50+)", + "High Value", + 5, + 3060 + ], + [ + "Young (18-29)", + "Low Value", + 6, + 431 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-2d9f7d2923213d7d16e7c489928d5a03" + }, + "mark": { + "type": "text" + }, + "encoding": { + "text": { + "value": "Complex query - consider custom visualization" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-2d9f7d2923213d7d16e7c489928d5a03": [ + { + "age_group": "Young (18-29)", + "value_tier": "Low Value", + "customers": 6, + "revenue": 431 + }, + { + "age_group": "Middle (30-49)", + "value_tier": "Low Value", + "customers": 2, + "revenue": 240 + }, + { + "age_group": "Middle (30-49)", + "value_tier": "Mid Value", + "customers": 7, + "revenue": 1595 + }, + { + "age_group": "Senior (50+)", + "value_tier": "High Value", + "customers": 5, + "revenue": 3060 + } + ] + } + } + } + } + }, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/building-semantic-tables.json b/docs/public/bsl-data/building-semantic-tables.json new file mode 100644 index 00000000..c55d1e60 --- /dev/null +++ b/docs/public/bsl-data/building-semantic-tables.json @@ -0,0 +1,4 @@ +{ + "markdown": "# Defining Semantic Tables\n\nA Semantic Table is the core building block of BSL. It transforms a raw Ibis table into a reusable, self-documenting data model by defining dimensions (attributes to group by) and measures (aggregations and calculations).\n\n## Creating a Semantic Table\n\nStart with any Ibis table and convert it to a Semantic Table by defining dimensions and measures using lambda expressions:\n\n```python\nimport ibis\nfrom boring_semantic_layer import SemanticModel\n\n# 1. Start with an Ibis table\nflights_tbl = ibis.table(\n name=\"flights\",\n schema={\"origin\": \"string\", \"carrier\": \"string\", \"distance\": \"int64\"}\n)\n\n# 2. Convert to a Semantic Table\nflights_sm = SemanticModel(\n name=\"flights\",\n table=flights_tbl,\n dimensions={\n 'origin': lambda t: t.origin,\n 'carrier': lambda t: t.carrier\n },\n measures={\n 'flight_count': lambda t: t.count(),\n 'total_distance': lambda t: t.distance.sum()\n }\n)\n```\n\n## Adding Dimensions\n\nDimensions are attributes you can group by in your queries. Define them using lambda expressions that reference columns in your Ibis table.\n\n### Basic Dimensions\n\n```python\nfrom boring_semantic_layer import SemanticModel\n\nflights_sm = SemanticModel(\n table=flights_tbl,\n dimensions={\n 'origin': lambda t: t.origin,\n 'destination': lambda t: t.dest,\n 'year': lambda t: t.year\n }\n)\n```\n\n### Dimensions with Descriptions\n\nAdd descriptions to make your models self-documenting and AI-friendly:\n\n```python\nfrom boring_semantic_layer import SemanticModel, DimensionSpec\n\nflights_sm = SemanticModel(\n table=flights_tbl,\n dimensions={\n \"origin\": DimensionSpec(\n expr=lambda t: t.origin,\n description=\"Origin airport code where the flight departed\"\n ),\n \"destination\": DimensionSpec(\n expr=lambda t: t.dest,\n description=\"Destination airport code where the flight arrived\"\n ),\n \"year\": DimensionSpec(\n expr=lambda t: t.year,\n description=\"Year of the flight\"\n )\n }\n)\n```\n\n## Adding Measures\n\nMeasures are aggregations and calculations that can be computed from your data. They use lambda expressions that return Ibis aggregation expressions.\n\n### Lambda Expressions\n\nDefine measures using common aggregation functions:\n\n```python\nmeasures={\n 'total_flights': lambda t: t.count(),\n 'total_distance': lambda t: t.distance.sum(),\n 'avg_distance': lambda t: t.distance.mean(),\n 'max_delay': lambda t: t.dep_delay.max()\n}\n```\n\n### Measures with Descriptions\n\nUse `MeasureSpec` to add descriptions for better documentation:\n\n```python\nfrom boring_semantic_layer import MeasureSpec\n\nmeasures={\n \"flight_count\": MeasureSpec(\n expr=lambda t: t.count(),\n description=\"Total number of flights\"\n ),\n \"avg_distance\": MeasureSpec(\n expr=lambda t: t.distance.mean(),\n description=\"Average flight distance in miles\"\n )\n}\n```\n\n### Referencing Other Measures\n\nBuild complex measures by referencing other measures you've already defined:\n\n```python\nmeasures={\n 'total_distance': lambda t: t.distance.sum(),\n 'flight_count': lambda t: t.count(),\n # Reference other measures\n 'avg_distance_per_flight': lambda t: t.total_distance / t.flight_count\n}\n```\n\n## Best Practices\n\n- **Use descriptive names**: Choose dimension and measure names that clearly indicate what they represent\n- **Add descriptions**: Use `DimensionSpec` and `MeasureSpec` with descriptions to make your models self-documenting\n- **Leverage measure composition**: Build complex metrics by referencing simpler measures rather than duplicating logic\n- **Keep dimensions simple**: Dimensions should represent groupable attributes; save calculations for measures\n", + "queries": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/charting.json b/docs/public/bsl-data/charting.json new file mode 100644 index 00000000..b684daed --- /dev/null +++ b/docs/public/bsl-data/charting.json @@ -0,0 +1,766 @@ +{ + "markdown": "# Charting\n\nBSL includes built-in support for generating data visualizations from your semantic queries. Create charts directly from query results with automatic chart type detection or full custom control.\n\n## Installation\n\nTo use chart visualization, install with the appropriate backend:\n\n```bash\n# For Altair backend (default)\npip install 'boring-semantic-layer[viz-altair]'\n\n# For Plotly backend\npip install 'boring-semantic-layer[viz-plotly]'\n```\n\n## Quick Start\n\nHere's a simple example showing how to create a chart:\n\n```setup_chart_data\nimport ibis\nfrom boring_semantic_layer import to_semantic_table\n\ncon = ibis.duckdb.connect(\":memory:\")\nflights_data = ibis.memtable({\n \"origin\": [\"JFK\", \"LAX\", \"SFO\", \"ORD\", \"DFW\", \"ATL\", \"DEN\"],\n \"flight_count\": [150, 135, 89, 112, 98, 145, 78],\n \"avg_distance\": [2475, 1850, 1200, 950, 1100, 1650, 900]\n})\nflights_tbl = con.create_table(\"flights\", flights_data)\n\nflights_st = (\n to_semantic_table(flights_tbl, name=\"flights\")\n .with_dimensions(\n origin=lambda t: t.origin\n )\n .with_measures(\n flight_count=lambda t: t.flight_count.sum(),\n avg_distance=lambda t: t.avg_distance.mean()\n )\n)\n```\n\n\n\n```query_basic_chart\n# Query and chart in one fluent chain\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\"flight_count\")\n .order_by(ibis.desc(\"flight_count\"))\n .limit(5)\n)\n\nresult.chart()\n```\n\n\n\n\nThe `.chart()` method is available on query results from `.aggregate()`, `.order_by()`, `.limit()`, and `.mutate()` operations.\n\n\n## Backend Selection\n\nBSL supports two charting backends with different strengths:\n\n### Altair (Default)\n\n**Best for:** Web-native interactive visualizations, declarative specifications, embedding in notebooks and web apps.\n\n```python\n# Use Altair backend (default)\nchart = result.chart()\n# or explicitly\nchart = result.chart(backend=\"altair\")\n```\n\n**Features:**\n- Built on Vega-Lite grammar\n- Declarative JSON specifications\n- Great for interactive web visualizations\n- Excellent notebook integration\n\n### Plotly\n\n**Best for:** Rich interactive dashboards, 3D visualizations, extensive chart types, business intelligence tools.\n\n```python\n# Use Plotly backend\nchart = result.chart(backend=\"plotly\")\n```\n\n**Features:**\n- Extensive chart type library\n- Rich interactivity out of the box\n- Dashboard integration\n- Export to static formats\n\n## Auto-Detection\n\nBSL automatically detects the appropriate chart type based on your query structure:\n\n### Bar Chart (Categorical Data)\n\nSingle dimension + measure \u2192 Bar chart\n\n```query_bar_chart\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\"flight_count\")\n .order_by(ibis.desc(\"flight_count\"))\n)\n\nresult.chart()\n```\n\n\n\n**Auto-detected because:** Single categorical dimension (`origin`) with one measure (`flight_count`)\n\n### Time Series (Temporal Data)\n\nTime dimension + measure \u2192 Line chart with time-aware formatting\n\n```setup_timeseries\nimport ibis\nfrom boring_semantic_layer import to_semantic_table\n\ncon = ibis.duckdb.connect(\":memory:\")\ntimeseries_data = ibis.memtable({\n \"date\": [\"2024-01-01\", \"2024-01-02\", \"2024-01-03\", \"2024-01-04\", \"2024-01-05\", \"2024-01-06\", \"2024-01-07\"],\n \"flight_count\": [145, 152, 148, 139, 156, 161, 143]\n})\ntimeseries_tbl = con.create_table(\"daily_flights\", timeseries_data)\n\ndaily_flights_st = (\n to_semantic_table(timeseries_tbl, name=\"daily_flights\")\n .with_dimensions(\n date={\n \"expr\": lambda t: t.date.cast(\"date\"),\n \"is_time_dimension\": True,\n \"smallest_time_grain\": \"TIME_GRAIN_DAY\"\n }\n )\n .with_measures(\n flight_count=lambda t: t.flight_count.sum()\n )\n)\n```\n\n\n\n```query_timeseries\nresult = (\n daily_flights_st\n .group_by(\"date\")\n .aggregate(\"flight_count\")\n)\nresult.chart()\n```\n\n\n\n**Auto-detected because:** Dimension marked as `is_time_dimension=True`\n\n### Heatmap (Two Dimensions)\n\nTwo categorical dimensions + measure \u2192 Heatmap\n\n```setup_heatmap\nimport ibis\nfrom boring_semantic_layer import to_semantic_table\n\ncon = ibis.duckdb.connect(\":memory:\")\nroute_data = ibis.memtable({\n \"origin\": [\"JFK\", \"JFK\", \"LAX\", \"LAX\", \"SFO\", \"SFO\"],\n \"dest\": [\"LAX\", \"SFO\", \"JFK\", \"SFO\", \"JFK\", \"LAX\"],\n \"flight_count\": [45, 32, 43, 28, 31, 27]\n})\nroute_tbl = con.create_table(\"routes\", route_data)\n\nroutes_st = (\n to_semantic_table(route_tbl, name=\"routes\")\n .with_dimensions(\n origin=lambda t: t.origin,\n dest=lambda t: t.dest\n )\n .with_measures(\n flight_count=lambda t: t.flight_count.sum()\n )\n)\n```\n\n\n\n```query_heatmap\nresult = (\n routes_st\n .group_by(\"origin\", \"dest\")\n .aggregate(\"flight_count\")\n)\nresult.chart()\n```\n\n\n\n**Auto-detected because:** Two categorical dimensions with one measure\n\n### Multi-Series Charts\n\nMultiple measures \u2192 Grouped/overlaid visualization with color encoding\n\n```query_multi_measure\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\"flight_count\", \"avg_distance\")\n .limit(5)\n)\nresult.chart()\n```\n\n\n\n**Auto-detected because:** Multiple measures trigger automatic color encoding by measure name\n\n\n## Custom Specifications\n\nOverride auto-detection with custom specifications:\n\n### Change Mark Type And Add Styling\n\nCustomize the mark type while providing explicit encodings:\n\n```query_custom_mark\nimport ibis\n# Create line chart with custom spec\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\"flight_count\")\n .order_by(ibis.desc(\"flight_count\"))\n .limit(5)\n)\nresult.chart(spec={\n \"mark\": {\"type\": \"line\", \"color\": \"#e74c3c\"}\n})\n```\n\n\n\n\nYou don't need to provide full vega spec: the spec object is merged with the BSL's default one.\n\n\n## Export Formats\n\nExport charts in various formats for different use cases:\n\n```python\n# Interactive chart object (default)\nchart = result.chart()\n\n# JSON specification for web embedding\njson_spec = result.chart(format=\"json\")\n\n# PNG image (requires altair[all] or plotly)\npng_bytes = result.chart(format=\"png\")\n\n# SVG markup (requires altair[all] or plotly)\nsvg_str = result.chart(format=\"svg\")\n\n# Save to file\nwith open(\"my_chart.png\", \"wb\") as f:\n f.write(png_bytes)\n```\n\n**Available formats:**\n- `\"static\"` or `\"interactive\"` - Chart object (default)\n- `\"json\"` - JSON specification\n- `\"png\"` - PNG image bytes\n- `\"svg\"` - SVG markup string\n\n## Next Steps\n\n- Learn about [Query Methods](query-methods.md) to build complex queries\n- Explore [YAML Configuration](yaml-config.md) for declarative semantic models\n- See [Compose Models](compose.md) for joining semantic tables\n", + "queries": { + "setup_chart_data": { + "code": "import ibis\nfrom boring_semantic_layer import to_semantic_table\n\ncon = ibis.duckdb.connect(\":memory:\")\nflights_data = ibis.memtable({\n \"origin\": [\"JFK\", \"LAX\", \"SFO\", \"ORD\", \"DFW\", \"ATL\", \"DEN\"],\n \"flight_count\": [150, 135, 89, 112, 98, 145, 78],\n \"avg_distance\": [2475, 1850, 1200, 950, 1100, 1650, 900]\n})\nflights_tbl = con.create_table(\"flights\", flights_data)\n\nflights_st = (\n to_semantic_table(flights_tbl, name=\"flights\")\n .with_dimensions(\n origin=lambda t: t.origin\n )\n .with_measures(\n flight_count=lambda t: t.flight_count.sum(),\n avg_distance=lambda t: t.avg_distance.mean()\n )\n)", + "sql": "SELECT\n *\nFROM \"memory\".\"main\".\"flights\"", + "table": { + "columns": [ + "origin", + "flight_count", + "avg_distance" + ], + "data": [ + [ + "JFK", + 150, + 2475 + ], + [ + "LAX", + 135, + 1850 + ], + [ + "SFO", + 89, + 1200 + ], + [ + "ORD", + 112, + 950 + ], + [ + "DFW", + 98, + 1100 + ], + [ + "ATL", + 145, + 1650 + ], + [ + "DEN", + 78, + 900 + ] + ] + } + }, + "query_basic_chart": { + "chart_spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-7c737749f9aa5e3d76f7415b3506172a" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "flight_count", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "y": { + "field": "flight_count", + "type": "quantitative" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-7c737749f9aa5e3d76f7415b3506172a": [ + { + "origin": "DFW", + "flight_count": 98 + }, + { + "origin": "ATL", + "flight_count": 145 + }, + { + "origin": "DEN", + "flight_count": 78 + }, + { + "origin": "ORD", + "flight_count": 112 + }, + { + "origin": "SFO", + "flight_count": 89 + }, + { + "origin": "JFK", + "flight_count": 150 + }, + { + "origin": "LAX", + "flight_count": 135 + } + ] + } + }, + "code": "# Query and chart in one fluent chain\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\"flight_count\")\n .order_by(ibis.desc(\"flight_count\"))\n .limit(5)\n)\n\nresult.chart()" + }, + "query_bar_chart": { + "chart_spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-61701342328432a1249d28736cb1334a" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "flight_count", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "y": { + "field": "flight_count", + "type": "quantitative" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-61701342328432a1249d28736cb1334a": [ + { + "origin": "JFK", + "flight_count": 150 + }, + { + "origin": "SFO", + "flight_count": 89 + }, + { + "origin": "DEN", + "flight_count": 78 + }, + { + "origin": "ATL", + "flight_count": 145 + }, + { + "origin": "DFW", + "flight_count": 98 + }, + { + "origin": "LAX", + "flight_count": 135 + }, + { + "origin": "ORD", + "flight_count": 112 + } + ] + } + }, + "code": "result = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\"flight_count\")\n .order_by(ibis.desc(\"flight_count\"))\n)\n\nresult.chart()" + }, + "setup_timeseries": { + "code": "import ibis\nfrom boring_semantic_layer import to_semantic_table\n\ncon = ibis.duckdb.connect(\":memory:\")\ntimeseries_data = ibis.memtable({\n \"date\": [\"2024-01-01\", \"2024-01-02\", \"2024-01-03\", \"2024-01-04\", \"2024-01-05\", \"2024-01-06\", \"2024-01-07\"],\n \"flight_count\": [145, 152, 148, 139, 156, 161, 143]\n})\ntimeseries_tbl = con.create_table(\"daily_flights\", timeseries_data)\n\ndaily_flights_st = (\n to_semantic_table(timeseries_tbl, name=\"daily_flights\")\n .with_dimensions(\n date={\n \"expr\": lambda t: t.date.cast(\"date\"),\n \"is_time_dimension\": True,\n \"smallest_time_grain\": \"TIME_GRAIN_DAY\"\n }\n )\n .with_measures(\n flight_count=lambda t: t.flight_count.sum()\n )\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"origin\",\n SUM(\"t1\".\"flight_count\") AS \"flight_count\"\n FROM (\n SELECT\n *\n FROM \"memory\".\"main\".\"flights\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"\nORDER BY\n \"t2\".\"flight_count\" DESC", + "table": { + "columns": [ + "origin", + "flight_count" + ], + "data": [ + [ + "JFK", + 150 + ], + [ + "ATL", + 145 + ], + [ + "LAX", + 135 + ], + [ + "ORD", + 112 + ], + [ + "DFW", + 98 + ], + [ + "SFO", + 89 + ], + [ + "DEN", + 78 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-fb2b4c26543b1ed7f74dff28086672be" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "flight_count", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "y": { + "field": "flight_count", + "type": "quantitative" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-fb2b4c26543b1ed7f74dff28086672be": [ + { + "origin": "ATL", + "flight_count": 145 + }, + { + "origin": "SFO", + "flight_count": 89 + }, + { + "origin": "JFK", + "flight_count": 150 + }, + { + "origin": "LAX", + "flight_count": 135 + }, + { + "origin": "DEN", + "flight_count": 78 + }, + { + "origin": "DFW", + "flight_count": 98 + }, + { + "origin": "ORD", + "flight_count": 112 + } + ] + } + } + } + }, + "query_timeseries": { + "chart_spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-bad51634bba5e5445cd0abc579b6de09" + }, + "mark": { + "type": "line" + }, + "encoding": { + "tooltip": [ + { + "field": "date", + "format": "%Y-%m-%d", + "type": "temporal" + }, + { + "field": "flight_count", + "type": "quantitative" + } + ], + "x": { + "axis": { + "format": "%Y-%m-%d", + "labelAngle": -45 + }, + "field": "date", + "type": "temporal" + }, + "y": { + "field": "flight_count", + "type": "quantitative" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-bad51634bba5e5445cd0abc579b6de09": [ + { + "date": "2024-01-07T00:00:00", + "flight_count": 143 + }, + { + "date": "2024-01-06T00:00:00", + "flight_count": 161 + }, + { + "date": "2024-01-02T00:00:00", + "flight_count": 152 + }, + { + "date": "2024-01-03T00:00:00", + "flight_count": 148 + }, + { + "date": "2024-01-05T00:00:00", + "flight_count": 156 + }, + { + "date": "2024-01-04T00:00:00", + "flight_count": 139 + }, + { + "date": "2024-01-01T00:00:00", + "flight_count": 145 + } + ] + } + }, + "code": "result = (\n daily_flights_st\n .group_by(\"date\")\n .aggregate(\"flight_count\")\n)\nresult.chart()" + }, + "setup_heatmap": { + "code": "import ibis\nfrom boring_semantic_layer import to_semantic_table\n\ncon = ibis.duckdb.connect(\":memory:\")\nroute_data = ibis.memtable({\n \"origin\": [\"JFK\", \"JFK\", \"LAX\", \"LAX\", \"SFO\", \"SFO\"],\n \"dest\": [\"LAX\", \"SFO\", \"JFK\", \"SFO\", \"JFK\", \"LAX\"],\n \"flight_count\": [45, 32, 43, 28, 31, 27]\n})\nroute_tbl = con.create_table(\"routes\", route_data)\n\nroutes_st = (\n to_semantic_table(route_tbl, name=\"routes\")\n .with_dimensions(\n origin=lambda t: t.origin,\n dest=lambda t: t.dest\n )\n .with_measures(\n flight_count=lambda t: t.flight_count.sum()\n )\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"date\",\n SUM(\"t1\".\"flight_count\") AS \"flight_count\"\n FROM (\n SELECT\n CAST(\"t0\".\"date\" AS DATE) AS \"date\",\n \"t0\".\"flight_count\"\n FROM \"memory\".\"main\".\"daily_flights\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "date", + "flight_count" + ], + "data": [ + [ + "2024-01-06", + 161 + ], + [ + "2024-01-07", + 143 + ], + [ + "2024-01-02", + 152 + ], + [ + "2024-01-03", + 148 + ], + [ + "2024-01-05", + 156 + ], + [ + "2024-01-04", + 139 + ], + [ + "2024-01-01", + 145 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-afaffc2f52a3f3e396211995b0771f7d" + }, + "mark": { + "type": "line" + }, + "encoding": { + "tooltip": [ + { + "field": "date", + "format": "%Y-%m-%d", + "type": "temporal" + }, + { + "field": "flight_count", + "type": "quantitative" + } + ], + "x": { + "axis": { + "format": "%Y-%m-%d", + "labelAngle": -45 + }, + "field": "date", + "type": "temporal" + }, + "y": { + "field": "flight_count", + "type": "quantitative" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-afaffc2f52a3f3e396211995b0771f7d": [ + { + "date": "2024-01-06T00:00:00", + "flight_count": 161 + }, + { + "date": "2024-01-07T00:00:00", + "flight_count": 143 + }, + { + "date": "2024-01-01T00:00:00", + "flight_count": 145 + }, + { + "date": "2024-01-02T00:00:00", + "flight_count": 152 + }, + { + "date": "2024-01-03T00:00:00", + "flight_count": 148 + }, + { + "date": "2024-01-05T00:00:00", + "flight_count": 156 + }, + { + "date": "2024-01-04T00:00:00", + "flight_count": 139 + } + ] + } + } + } + }, + "query_heatmap": { + "chart_spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-2feedb7a5700ceb3013408945ae11837" + }, + "mark": { + "type": "rect" + }, + "encoding": { + "color": { + "field": "flight_count", + "type": "quantitative" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "dest", + "type": "nominal" + }, + { + "field": "flight_count", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "y": { + "field": "dest", + "sort": null, + "type": "ordinal" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-2feedb7a5700ceb3013408945ae11837": [ + { + "origin": "SFO", + "dest": "JFK", + "flight_count": 31 + }, + { + "origin": "JFK", + "dest": "LAX", + "flight_count": 45 + }, + { + "origin": "JFK", + "dest": "SFO", + "flight_count": 32 + }, + { + "origin": "LAX", + "dest": "SFO", + "flight_count": 28 + }, + { + "origin": "LAX", + "dest": "JFK", + "flight_count": 43 + }, + { + "origin": "SFO", + "dest": "LAX", + "flight_count": 27 + } + ] + } + }, + "code": "result = (\n routes_st\n .group_by(\"origin\", \"dest\")\n .aggregate(\"flight_count\")\n)\nresult.chart()" + }, + "query_multi_measure": { + "chart_spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-840fb65a3ee3c30d91bb5e6f9b818ba5" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "avg_distance" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-840fb65a3ee3c30d91bb5e6f9b818ba5": [ + { + "origin": "SFO", + "flight_count": 89, + "avg_distance": 1200.0 + }, + { + "origin": "DFW", + "flight_count": 98, + "avg_distance": 1100.0 + }, + { + "origin": "ORD", + "flight_count": 112, + "avg_distance": 950.0 + }, + { + "origin": "ATL", + "flight_count": 145, + "avg_distance": 1650.0 + }, + { + "origin": "JFK", + "flight_count": 150, + "avg_distance": 2475.0 + }, + { + "origin": "DEN", + "flight_count": 78, + "avg_distance": 900.0 + }, + { + "origin": "LAX", + "flight_count": 135, + "avg_distance": 1850.0 + } + ] + } + }, + "code": "result = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\"flight_count\", \"avg_distance\")\n .limit(5)\n)\nresult.chart()" + }, + "query_custom_mark": { + "chart_spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-c83d584c12c2f36b4a5051977a93d0d5" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "flight_count", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "y": { + "field": "flight_count", + "type": "quantitative" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-c83d584c12c2f36b4a5051977a93d0d5": [ + { + "origin": "ATL", + "flight_count": 145 + }, + { + "origin": "ORD", + "flight_count": 112 + }, + { + "origin": "SFO", + "flight_count": 89 + }, + { + "origin": "JFK", + "flight_count": 150 + }, + { + "origin": "LAX", + "flight_count": 135 + }, + { + "origin": "DFW", + "flight_count": 98 + }, + { + "origin": "DEN", + "flight_count": 78 + } + ] + } + } + } + }, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/compose.json b/docs/public/bsl-data/compose.json new file mode 100644 index 00000000..29f7a18c --- /dev/null +++ b/docs/public/bsl-data/compose.json @@ -0,0 +1,199 @@ +{ + "markdown": "# Composing Models\n\nBuild complex data models by combining multiple semantic tables through joins. Model composition allows you to create rich, multi-dimensional views of your data.\n\n## Composition via Joins\n\nModel composition in BSL is achieved through **joins**. When you join semantic tables, the result is a new composed model that contains **all dimensions and measures** from both tables.\n\n\nEach join creates a new semantic model with the combined dimensions and measures from all joined tables. This allows you to build progressively richer models.\n\n\n## Example: Two-Level Composition\n\nLet's build a composed model step-by-step, showing available dimensions and measures at each level.\n\n### Level 0: Base Models\n\nFirst, let's set up our base tables:\n\n```setup_ibis_tables\nimport ibis\nfrom boring_semantic_layer import to_semantic_table\n\n# Create sample data\ncon = ibis.duckdb.connect(\":memory:\")\n\n# Flights table\nflights_data = ibis.memtable({\n \"flight_id\": [1, 2, 3],\n \"carrier_code\": [\"AA\", \"UA\", \"DL\"],\n \"aircraft_id\": [101, 102, 103],\n \"distance\": [1000, 1500, 800],\n \"passengers\": [150, 180, 120]\n})\nflights_tbl = con.create_table(\"flights\", flights_data)\n\n# Carriers table\ncarriers_data = ibis.memtable({\n \"code\": [\"AA\", \"UA\", \"DL\"],\n \"name\": [\"American Airlines\", \"United Airlines\", \"Delta Air Lines\"],\n \"country\": [\"USA\", \"USA\", \"USA\"]\n})\ncarriers_tbl = con.create_table(\"carriers\", carriers_data)\n\n# Aircraft table\naircraft_data = ibis.memtable({\n \"id\": [101, 102, 103],\n \"model\": [\"Boeing 737\", \"Airbus A320\", \"Boeing 777\"],\n \"capacity\": [180, 200, 350]\n})\naircraft_tbl = con.create_table(\"aircraft\", aircraft_data)\n```\n\n\n\n```setup_semantic_models\n# Create semantic tables\nflights_st = (\n to_semantic_table(flights_tbl, name=\"flights\")\n .with_dimensions(\n flight_id=lambda t: t.flight_id,\n carrier_code=lambda t: t.carrier_code,\n aircraft_id=lambda t: t.aircraft_id\n )\n .with_measures(\n flight_count=lambda t: t.count(),\n total_distance=lambda t: t.distance.sum(),\n total_passengers=lambda t: t.passengers.sum()\n )\n)\n\ncarriers_st = (\n to_semantic_table(carriers_tbl, name=\"carriers\")\n .with_dimensions(\n code=lambda t: t.code,\n name=lambda t: t.name,\n country=lambda t: t.country\n )\n .with_measures(\n carrier_count=lambda t: t.count()\n )\n)\n\naircraft_st = (\n to_semantic_table(aircraft_tbl, name=\"aircraft\")\n .with_dimensions(\n id=lambda t: t.id,\n model=lambda t: t.model\n )\n .with_measures(\n aircraft_count=lambda t: t.count(),\n total_capacity=lambda t: t.capacity.sum()\n )\n)\n```\n\n\n\n```level0_dimensions\nflights_st.dimensions, flights_st.measures\n```\n\n\n\n### Level 1: First Join (Flights + Carriers)\n\nJoin carriers to flights to add carrier information:\n\n```level1_join\n# Join carriers to flights\nflights_with_carriers = flights_st.join_many(\n carriers_st,\n left_on=\"carrier_code\",\n right_on=\"code\"\n)\n\n# Inspect dimensions - now includes both flights and carriers\nflights_with_carriers.dimensions, flights_with_carriers.measures\n```\n\n\n### Level 2: Second Join (+ Aircraft)\n\nAdd aircraft information to create a fully composed model:\n\n```level2_join\n# Join aircraft to the composed model\nfull_model = flights_with_carriers.join_many(\n aircraft_st,\n left_on=\"aircraft_id\",\n right_on=\"id\"\n)\n\n# Inspect dimensions - now includes flights, carriers, AND aircraft\nfull_model.dimensions, full_model.measures\n```\n\n\n## Query the Composed Model\n\nNow you can query across all joined tables:\n\n```composed_query\n# Query using dimensions and measures from all three tables\nresult = (\n full_model\n .group_by( \"aircraft.model\")\n .aggregate(\"flight_count\", \"total_passengers\", \"total_capacity\")\n)\n```\n\n\n\n## Key Takeaways\n\n- **Composition via Joins**: Use `join_many()`, `join_one()`, or `join()` to compose models\n- **Additive**: Each join adds dimensions and measures from the joined table\n- **Table Prefixes**: Dimensions/measures are prefixed with table names (`flights.`, `carriers.`, `aircraft.`)\n- **No Limit**: Compose as many models as needed for your analysis\n- **Incremental**: Build from simple to complex, one join at a time\n\n## Next Steps\n\n- Learn about [YAML Configuration](/building/yaml) for declarative model composition\n- Explore [Query Methods](/querying/methods) for querying composed models\n", + "queries": { + "setup_ibis_tables": { + "code": "import ibis\nfrom boring_semantic_layer import to_semantic_table\n\n# Create sample data\ncon = ibis.duckdb.connect(\":memory:\")\n\n# Flights table\nflights_data = ibis.memtable({\n \"flight_id\": [1, 2, 3],\n \"carrier_code\": [\"AA\", \"UA\", \"DL\"],\n \"aircraft_id\": [101, 102, 103],\n \"distance\": [1000, 1500, 800],\n \"passengers\": [150, 180, 120]\n})\nflights_tbl = con.create_table(\"flights\", flights_data)\n\n# Carriers table\ncarriers_data = ibis.memtable({\n \"code\": [\"AA\", \"UA\", \"DL\"],\n \"name\": [\"American Airlines\", \"United Airlines\", \"Delta Air Lines\"],\n \"country\": [\"USA\", \"USA\", \"USA\"]\n})\ncarriers_tbl = con.create_table(\"carriers\", carriers_data)\n\n# Aircraft table\naircraft_data = ibis.memtable({\n \"id\": [101, 102, 103],\n \"model\": [\"Boeing 737\", \"Airbus A320\", \"Boeing 777\"],\n \"capacity\": [180, 200, 350]\n})\naircraft_tbl = con.create_table(\"aircraft\", aircraft_data)", + "sql": "Error generating SQL: Table.sql() missing 1 required positional argument: 'query'", + "table": { + "columns": [ + "id", + "model", + "capacity" + ], + "data": [ + [ + 101, + "Boeing 737", + 180 + ], + [ + 102, + "Airbus A320", + 200 + ], + [ + 103, + "Boeing 777", + 350 + ] + ] + } + }, + "setup_semantic_models": { + "code": "# Create semantic tables\nflights_st = (\n to_semantic_table(flights_tbl, name=\"flights\")\n .with_dimensions(\n flight_id=lambda t: t.flight_id,\n carrier_code=lambda t: t.carrier_code,\n aircraft_id=lambda t: t.aircraft_id\n )\n .with_measures(\n flight_count=lambda t: t.count(),\n total_distance=lambda t: t.distance.sum(),\n total_passengers=lambda t: t.passengers.sum()\n )\n)\n\ncarriers_st = (\n to_semantic_table(carriers_tbl, name=\"carriers\")\n .with_dimensions(\n code=lambda t: t.code,\n name=lambda t: t.name,\n country=lambda t: t.country\n )\n .with_measures(\n carrier_count=lambda t: t.count()\n )\n)\n\naircraft_st = (\n to_semantic_table(aircraft_tbl, name=\"aircraft\")\n .with_dimensions(\n id=lambda t: t.id,\n model=lambda t: t.model\n )\n .with_measures(\n aircraft_count=lambda t: t.count(),\n total_capacity=lambda t: t.capacity.sum()\n )\n)", + "sql": "SELECT\n *\nFROM \"memory\".\"main\".\"aircraft\"", + "table": { + "columns": [ + "id", + "model", + "capacity" + ], + "data": [ + [ + 101, + "Boeing 737", + 180 + ], + [ + 102, + "Airbus A320", + 200 + ], + [ + 103, + "Boeing 777", + 350 + ] + ] + } + }, + "level0_dimensions": { + "output": [ + "('flight_id', 'carrier_code', 'aircraft_id')", + "('flight_count', 'total_distance', 'total_passengers')" + ] + }, + "level1_join": { + "output": [ + "('flights.flight_id', 'flights.carrier_code', 'flights.aircraft_id', 'carriers.code', 'carriers.name', 'carriers.country')", + "('flights.flight_count', 'flights.total_distance', 'flights.total_passengers', 'carriers.carrier_count')" + ] + }, + "level2_join": { + "output": [ + "('flights.flight_id', 'flights.carrier_code', 'flights.aircraft_id', 'carriers.code', 'carriers.name', 'carriers.country', 'aircraft.id', 'aircraft.model')", + "('flights.flight_count', 'flights.total_distance', 'flights.total_passengers', 'carriers.carrier_count', 'aircraft.aircraft_count', 'aircraft.total_capacity')" + ] + }, + "composed_query": { + "code": "# Query using dimensions and measures from all three tables\nresult = (\n full_model\n .group_by( \"aircraft.model\")\n .aggregate(\"flight_count\", \"total_passengers\", \"total_capacity\")\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t10\".\"aircraft.model\",\n COUNT(*) AS \"flight_count\",\n SUM(\"t10\".\"passengers\") AS \"total_passengers\",\n SUM(\"t10\".\"capacity\") AS \"total_capacity\"\n FROM (\n SELECT\n \"t9\".\"carrier_code\",\n \"t9\".\"aircraft_id\",\n \"t9\".\"distance\",\n \"t9\".\"passengers\",\n \"t9\".\"code\",\n \"t9\".\"capacity\",\n \"t9\".\"model\",\n \"t9\".\"id\",\n \"t9\".\"model\" AS \"aircraft.model\"\n FROM (\n SELECT\n \"t6\".\"carrier_code\",\n \"t6\".\"aircraft_id\",\n \"t6\".\"distance\",\n \"t6\".\"passengers\",\n \"t7\".\"code\",\n \"t8\".\"capacity\",\n \"t8\".\"model\",\n \"t8\".\"id\"\n FROM (\n SELECT\n \"t0\".\"carrier_code\",\n \"t0\".\"aircraft_id\",\n \"t0\".\"distance\",\n \"t0\".\"passengers\"\n FROM \"memory\".\"main\".\"flights\" AS \"t0\"\n ) AS \"t6\"\n LEFT OUTER JOIN (\n SELECT\n \"t1\".\"code\"\n FROM \"memory\".\"main\".\"carriers\" AS \"t1\"\n ) AS \"t7\"\n ON \"t6\".\"carrier_code\" = \"t7\".\"code\"\n LEFT OUTER JOIN (\n SELECT\n \"t2\".\"capacity\",\n \"t2\".\"model\",\n \"t2\".\"id\"\n FROM \"memory\".\"main\".\"aircraft\" AS \"t2\"\n ) AS \"t8\"\n ON \"t6\".\"aircraft_id\" = \"t8\".\"id\"\n ) AS \"t9\"\n ) AS \"t10\"\n GROUP BY\n 1\n) AS \"t11\"", + "table": { + "columns": [ + "aircraft.model", + "flight_count", + "total_passengers", + "total_capacity" + ], + "data": [ + [ + "Boeing 777", + 1, + 120, + 350 + ], + [ + "Boeing 737", + 1, + 150, + 180 + ], + [ + "Airbus A320", + 1, + 180, + 200 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-f0a68cc4fa68ffbd859216926b5e49d6" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "aircraft_model", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "aircraft_model", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "total_passengers", + "total_capacity" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-f0a68cc4fa68ffbd859216926b5e49d6": [ + { + "aircraft_model": "Boeing 777", + "flight_count": 1, + "total_passengers": 120, + "total_capacity": 350 + }, + { + "aircraft_model": "Airbus A320", + "flight_count": 1, + "total_passengers": 180, + "total_capacity": 200 + }, + { + "aircraft_model": "Boeing 737", + "flight_count": 1, + "total_passengers": 150, + "total_capacity": 180 + } + ] + } + } + } + } + }, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/example.json b/docs/public/bsl-data/example.json new file mode 100644 index 00000000..3d513ffa --- /dev/null +++ b/docs/public/bsl-data/example.json @@ -0,0 +1,256 @@ +{ + "markdown": "# Example: E-commerce Analytics\n\nThis example demonstrates how to use BSL for e-commerce data analysis.\n\n## Setup Data\n\n```orders_table\norders_tbl = ibis.memtable({\n \"order_id\": [1, 2, 3, 4, 5, 6, 7, 8],\n \"customer\": [\"Alice\", \"Bob\", \"Alice\", \"Charlie\", \"Bob\", \"Alice\", \"David\", \"Charlie\"],\n \"product\": [\"Widget\", \"Gadget\", \"Widget\", \"Doohickey\", \"Widget\", \"Gadget\", \"Widget\", \"Gadget\"],\n \"amount\": [100, 150, 100, 75, 100, 150, 100, 150],\n \"quantity\": [1, 2, 1, 3, 1, 2, 1, 2],\n})\n\norders_st = (\n to_semantic_table(orders_tbl, name=\"orders\")\n .with_dimensions(\n customer=lambda t: t.customer,\n product=lambda t: t.product,\n )\n .with_measures(\n total_orders=lambda t: t.count(),\n total_revenue=lambda t: t.amount.sum(),\n total_quantity=lambda t: t.quantity.sum(),\n avg_order_value=lambda t: t.amount.mean(),\n )\n)\n```\n\n## Revenue by Customer\n\nLet's see which customers generate the most revenue:\n\n```revenue_by_customer\nresult = orders_st.group_by(\"customer\").aggregate(\n \"total_orders\",\n \"total_revenue\",\n \"avg_order_value\"\n)\n```\n\nCustomer revenue analysis:\n\n## Product Performance\n\nWhich products are selling best?\n\n```product_performance\nresult = orders_st.group_by(\"product\").aggregate(\n \"total_orders\",\n \"total_quantity\",\n \"total_revenue\"\n)\n```\n\nProduct performance metrics:\n\n\n\n## Summary\n\nThis demonstrates how BSL makes it easy to:\n- Define semantic models once\n- Run multiple queries with different groupings\n- Generate consistent metrics across analyses\n", + "queries": { + "revenue_by_customer": { + "code": "result = orders_st.group_by(\"customer\").aggregate(\n \"total_orders\",\n \"total_revenue\",\n \"avg_order_value\"\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"customer\",\n COUNT(*) AS \"total_orders\",\n SUM(\"t1\".\"amount\") AS \"total_revenue\",\n AVG(\"t1\".\"amount\") AS \"avg_order_value\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_agpcylzjgnaqxpob5w7n254ena\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "customer", + "total_orders", + "total_revenue", + "avg_order_value" + ], + "data": [ + [ + "Charlie", + 2, + 225, + 112.5 + ], + [ + "David", + 1, + 100, + 100.0 + ], + [ + "Alice", + 3, + 350, + 116.66666666666667 + ], + [ + "Bob", + 2, + 250, + 125.0 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-51bfffdf20cb73c5f0e6996e4559f0ab" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "customer", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "customer", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "total_orders", + "total_revenue", + "avg_order_value" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-51bfffdf20cb73c5f0e6996e4559f0ab": [ + { + "customer": "David", + "total_orders": 1, + "total_revenue": 100, + "avg_order_value": 100.0 + }, + { + "customer": "Charlie", + "total_orders": 2, + "total_revenue": 225, + "avg_order_value": 112.5 + }, + { + "customer": "Bob", + "total_orders": 2, + "total_revenue": 250, + "avg_order_value": 125.0 + }, + { + "customer": "Alice", + "total_orders": 3, + "total_revenue": 350, + "avg_order_value": 116.66666666666667 + } + ] + } + } + } + }, + "product_performance": { + "code": "result = orders_st.group_by(\"product\").aggregate(\n \"total_orders\",\n \"total_quantity\",\n \"total_revenue\"\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"product\",\n COUNT(*) AS \"total_orders\",\n SUM(\"t1\".\"quantity\") AS \"total_quantity\",\n SUM(\"t1\".\"amount\") AS \"total_revenue\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_agpcylzjgnaqxpob5w7n254ena\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "product", + "total_orders", + "total_quantity", + "total_revenue" + ], + "data": [ + [ + "Doohickey", + 1, + 3, + 75 + ], + [ + "Gadget", + 3, + 6, + 450 + ], + [ + "Widget", + 4, + 4, + 400 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-6991e3d3e667615e632b8261337a1da8" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "product", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "product", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "total_orders", + "total_quantity", + "total_revenue" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-6991e3d3e667615e632b8261337a1da8": [ + { + "product": "Gadget", + "total_orders": 3, + "total_quantity": 6, + "total_revenue": 450 + }, + { + "product": "Widget", + "total_orders": 4, + "total_quantity": 4, + "total_revenue": 400 + }, + { + "product": "Doohickey", + "total_orders": 1, + "total_quantity": 3, + "total_revenue": 75 + } + ] + } + } + } + } + }, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/filtering.json b/docs/public/bsl-data/filtering.json new file mode 100644 index 00000000..38940172 --- /dev/null +++ b/docs/public/bsl-data/filtering.json @@ -0,0 +1,5 @@ +{ + "markdown": "# Limit & Filter\n\nControl result size and filter data in your semantic queries. BSL provides flexible filtering capabilities using both Ibis expressions and JSON syntax for LLM-friendly queries.\n\n## Limit Results\n\nUse the `limit` parameter to restrict the number of rows returned from your query. This is useful for quick data exploration and testing.\n\n```python\n# Get top 10 results\nresult = flights_sm.query(\n dimensions=['origin'],\n measures=['flight_count'],\n limit=10\n).execute()\n```\n\n## Filter with Ibis Expressions\n\nFilter your data using lambda functions that leverage Ibis expressions. This provides a Pythonic way to express filter conditions with full access to Ibis operators.\n\n```python\n# Filter using lambda\nresult = flights_sm.query(\n dimensions=['origin'],\n measures=['flight_count'],\n filters=[lambda t: t.origin == 'JFK']\n).execute()\n```\n\nThe lambda function receives the table reference and can use any Ibis comparison operators (`==`, `!=`, `>`, `<`, `>=`, `<=`) and logical operators (`&`, `|`, `~`).\n\n## Filter with JSON (LLM-friendly)\n\nBSL supports JSON-based filter syntax that is particularly useful for LLM-generated queries and API integrations. The JSON format provides a structured way to express complex filter conditions.\n\n```python\n# Filter using JSON syntax\nresult = flights_sm.query(\n dimensions=['origin'],\n measures=['flight_count'],\n filters=[\n {\n 'operator': 'AND',\n 'conditions': [\n {'field': 'origin', 'operator': 'in', 'values': ['JFK', 'LGA']},\n {'field': 'year', 'operator': '=', 'values': [2013]}\n ]\n }\n ]\n).execute()\n```\n\nThe JSON filter format supports:\n- Logical operators: `AND`, `OR`\n- Comparison operators: `=`, `!=`, `>`, `<`, `>=`, `<=`, `in`\n- Nested conditions for complex logic\n- Multiple values for operators like `in`\n\nThis JSON syntax makes it easy for LLMs to generate valid filter conditions without needing to understand Python lambda syntax.\n", + "queries": {}, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/getting-started.json b/docs/public/bsl-data/getting-started.json new file mode 100644 index 00000000..fd792736 --- /dev/null +++ b/docs/public/bsl-data/getting-started.json @@ -0,0 +1,236 @@ +{ + "markdown": "# Getting Started with BSL\n\nBSL (Boring Semantic Layer) is a lightweight semantic layer built on top of Ibis. It allows you to define your data models once and query them anywhere.\n\n## Installation\n\n```bash\npip install boring-semantic-layer\n```\n\n## Quick Start\n\nLet's create your first Semantic Table using synthetic data in Ibis.\n\n```setup_flights\nimport ibis\nfrom boring_semantic_layer import to_semantic_table\n\n# Create sample flight data\nflights_tbl = ibis.memtable({\n \"origin\": [\"NYC\", \"LAX\", \"NYC\", \"SFO\", \"LAX\", \"NYC\", \"SFO\", \"LAX\"],\n \"destination\": [\"LAX\", \"NYC\", \"SFO\", \"NYC\", \"SFO\", \"LAX\", \"LAX\", \"SFO\"],\n \"distance\": [2789, 2789, 2902, 2902, 347, 2789, 347, 347],\n \"duration\": [330, 330, 360, 360, 65, 330, 65, 65],\n})\n```\n\nYou can then convert these tables in Semantic Tables that contains dimensios and measures definitions:\n\n```define_semantic_table\n# Define semantic table with dimensions and measures\nflights_st = (\n to_semantic_table(flights_tbl, name=\"flights\")\n .with_dimensions(\n origin=lambda t: t.origin,\n destination=lambda t: t.destination,\n )\n .with_measures(\n flight_count=lambda t: t.count(),\n total_distance=lambda t: t.distance.sum(),\n avg_duration=lambda t: t.duration.mean(),\n )\n)\n```\n\n## Query Your Data\n\nNow let's query the semantic table by grouping flights by origin:\n\n```query_by_origin\n# Group flights by origin airport\nresult = flights_st.group_by(\"origin\").aggregate(\n \"flight_count\",\n \"total_distance\",\n \"avg_duration\"\n)\n```\n\n\n\nYou can also group by destination:\n\n```query_by_destination\n# Group flights by destination airport\nresult = flights_st.group_by(\"destination\").aggregate(\n \"flight_count\",\n \"total_distance\"\n)\n```\n\n\n\n## Next Steps\n\n- Learn how to [Build Semantic Tables](/examples/semantic-table) with dimensions, measures, and joins\n- Explore [Query Methods](/examples/query-methods) for retrieving data\n- Discover how to [Compose Models](/examples/compose) together\n", + "queries": { + "query_by_origin": { + "code": "# Group flights by origin airport\nresult = flights_st.group_by(\"origin\").aggregate(\n \"flight_count\",\n \"total_distance\",\n \"avg_duration\"\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"origin\",\n COUNT(*) AS \"flight_count\",\n SUM(\"t1\".\"distance\") AS \"total_distance\",\n AVG(\"t1\".\"duration\") AS \"avg_duration\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_oy2xwd53wfaufa34nlati5oguy\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "origin", + "flight_count", + "total_distance", + "avg_duration" + ], + "data": [ + [ + "LAX", + 3, + 3483, + 153.33333333333334 + ], + [ + "SFO", + 2, + 3249, + 212.5 + ], + [ + "NYC", + 3, + 8480, + 340.0 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-dfae5a7d110f4eb85b9621aef0662545" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "total_distance", + "avg_duration" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-dfae5a7d110f4eb85b9621aef0662545": [ + { + "origin": "SFO", + "flight_count": 2, + "total_distance": 3249, + "avg_duration": 212.5 + }, + { + "origin": "NYC", + "flight_count": 3, + "total_distance": 8480, + "avg_duration": 340.0 + }, + { + "origin": "LAX", + "flight_count": 3, + "total_distance": 3483, + "avg_duration": 153.33333333333334 + } + ] + } + } + } + }, + "query_by_destination": { + "code": "# Group flights by destination airport\nresult = flights_st.group_by(\"destination\").aggregate(\n \"flight_count\",\n \"total_distance\"\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"destination\",\n COUNT(*) AS \"flight_count\",\n SUM(\"t1\".\"distance\") AS \"total_distance\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_oy2xwd53wfaufa34nlati5oguy\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "destination", + "flight_count", + "total_distance" + ], + "data": [ + [ + "SFO", + 3, + 3596 + ], + [ + "NYC", + 2, + 5691 + ], + [ + "LAX", + 3, + 5925 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-054f94824247692bd2f208176250cd44" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "destination", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "destination", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "total_distance" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-054f94824247692bd2f208176250cd44": [ + { + "destination": "LAX", + "flight_count": 3, + "total_distance": 5925 + }, + { + "destination": "SFO", + "flight_count": 3, + "total_distance": 3596 + }, + { + "destination": "NYC", + "flight_count": 2, + "total_distance": 5691 + } + ] + } + } + } + } + }, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/indexing.json b/docs/public/bsl-data/indexing.json new file mode 100644 index 00000000..0d138413 --- /dev/null +++ b/docs/public/bsl-data/indexing.json @@ -0,0 +1,700 @@ +{ + "markdown": "# Dimensional Indexing\n\nCreate a searchable catalog of all unique values across your dimensions for data exploration, autocomplete features, and understanding data distributions. Inspired by [Malloy's index pattern](https://docs.malloydata.dev/documentation/patterns/dim_index).\n\n## Overview\n\nDimensional indexing allows you to:\n\n- **Catalog all values**: Extract and count all unique values across dimensions\n- **Search dimensions**: Build autocomplete and search features\n- **Profile data**: Understand cardinality and distributions\n- **Weight by measures**: Find values ranked by custom metrics (e.g., highest revenue cities)\n- **Index across joins**: Search values from related tables\n\nThe `index()` method returns a standardized table with columns:\n- `fieldName`: The dimension name\n- `fieldValue`: The unique value\n- `fieldType`: The data type (string, number, etc.)\n- `weight`: Count or custom measure value for ranking\n\n## Setup\n\nLet's create an airports semantic table for our examples:\n\n```setup_airports\nimport ibis\nfrom boring_semantic_layer import to_semantic_table\n\n# Create synthetic airports data\nairports_data = ibis.memtable({\n \"code\": [\"JFK\", \"LAX\", \"ORD\", \"ATL\", \"DFW\", \"DEN\", \"SFO\", \"LAS\", \"SEA\", \"PHX\",\n \"IAH\", \"MCO\", \"EWR\", \"BOS\", \"MIA\", \"SAN\", \"LGA\", \"PHL\", \"DTW\", \"MSP\"],\n \"city\": [\"NEW YORK\", \"LOS ANGELES\", \"CHICAGO\", \"ATLANTA\", \"DALLAS\", \"DENVER\",\n \"SAN FRANCISCO\", \"LAS VEGAS\", \"SEATTLE\", \"PHOENIX\", \"HOUSTON\", \"ORLANDO\",\n \"NEWARK\", \"BOSTON\", \"MIAMI\", \"SAN DIEGO\", \"NEW YORK\", \"PHILADELPHIA\",\n \"DETROIT\", \"MINNEAPOLIS\"],\n \"state\": [\"NY\", \"CA\", \"IL\", \"GA\", \"TX\", \"CO\", \"CA\", \"NV\", \"WA\", \"AZ\",\n \"TX\", \"FL\", \"NJ\", \"MA\", \"FL\", \"CA\", \"NY\", \"PA\", \"MI\", \"MN\"],\n \"fac_type\": [\"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\",\n \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\",\n \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\",\n \"AIRPORT\", \"AIRPORT\"],\n \"elevation\": [13, 128, 672, 1026, 607, 5433, 13, 2181, 433, 1135,\n 97, 96, 18, 19, 8, 17, 21, 36, 645, 841]\n})\n\n# Define semantic table\nairports = (\n to_semantic_table(airports_data, name=\"airports\")\n .with_dimensions(\n code=lambda t: t.code,\n city=lambda t: t.city,\n state=lambda t: t.state,\n fac_type=lambda t: t.fac_type,\n elevation=lambda t: t.elevation,\n )\n .with_measures(\n airport_count=lambda t: t.count(),\n avg_elevation=lambda t: t.elevation.mean(),\n )\n)\n```\n\n\n\n## Basic Index: All Dimensions\n\nIndex all dimensions to see every unique value with its frequency:\n\n```query_index_all\n# Index all dimensions (None means all)\nresult = airports.index(None).limit(10)\n```\n\n\n\nThe `weight` column shows the count for each value. Use this to understand which values are most common across your dataset.\n\n## Index Specific Fields\n\nFocus on specific dimensions by selecting them:\n\n```query_index_specific\n# Index only state and city\nresult = (\n airports.index(lambda t: [t.state, t.city])\n .order_by(lambda t: t.weight.desc())\n .limit(10)\n)\n```\n\n\n\nThis is useful when you only care about certain dimensions, reducing noise and improving performance.\n\n## Search Pattern: Autocomplete\n\nBuild autocomplete features by filtering the index with pattern matching:\n\n```query_autocomplete\n# Get city suggestions starting with \"SAN\"\nresult = (\n airports.index(lambda t: t.city)\n .filter(lambda t: t.fieldValue.like(\"SAN%\"))\n .order_by(lambda t: t.weight.desc())\n .limit(10)\n)\n```\n\n\n\n\nUse pattern matching with `like()` to implement autocomplete, search suggestions, or fuzzy matching features in your application.\n\n\n## Filter by Field Type\n\nAnalyze only string or numeric fields:\n\n```query_by_type\n# Get only string field values\nresult = (\n airports.index(None)\n .filter(lambda t: t.fieldType == \"string\")\n .order_by(lambda t: t.weight.desc())\n .limit(10)\n)\n```\n\n\n\nThis helps when you want to focus on categorical vs. numeric dimensions separately.\n\n## Custom Weights: Rank by Measure\n\nInstead of counting occurrences, weight values by a custom measure:\n\n```query_custom_weight\n# Find states with most airports\nresult = (\n airports.index(lambda t: t.state, by=\"airport_count\")\n .order_by(lambda t: t.weight.desc())\n .limit(10)\n)\n```\n\n\n\n\nThe `by` parameter lets you rank dimension values by any measure. This is powerful for finding \"top cities by revenue\", \"states by average temperature\", etc.\n\n\n## Sampling for Large Datasets\n\nFor very large datasets, use sampling to get quick insights:\n\n```query_sampled\n# Sample 100 rows before indexing\nresult = (\n airports.index(None, sample=100)\n .filter(lambda t: t.fieldType == \"string\")\n .order_by(lambda t: t.weight.desc())\n .limit(10)\n)\n```\n\n\n\nSampling trades perfect accuracy for speed, which is often acceptable for exploratory analysis.\n\n## Index Across Joins\n\nIndex dimensions from joined tables:\n\n```query_index_joins\n# Create synthetic flights data\nflights_data = ibis.memtable({\n \"flight_id\": list(range(1, 31)),\n \"carrier\": [\"AA\", \"UA\", \"DL\", \"WN\", \"B6\", \"AA\", \"UA\", \"DL\", \"WN\", \"B6\"] * 3,\n \"origin\": [\"JFK\", \"LAX\", \"ORD\", \"ATL\", \"DFW\", \"SFO\", \"SEA\", \"DEN\", \"PHX\", \"BOS\"] * 3,\n})\n\nflights = (\n to_semantic_table(flights_data, name=\"flights\")\n .with_dimensions(\n carrier=lambda t: t.carrier,\n origin=lambda t: t.origin,\n )\n .with_measures(\n flight_count=lambda t: t.count(),\n )\n)\n\n# Join flights with airports\nflights_with_origin = flights.join_one(airports, left_on=\"origin\", right_on=\"code\")\n\n# Index across the join\nresult = (\n flights_with_origin.index(lambda t: [t.carrier, t.airports__state])\n .order_by(lambda t: t.weight.desc())\n .limit(10)\n)\n```\n\n\n\n\nWhen referencing dimensions from joined tables in the index, use double underscores: `airports__state` instead of `airports.state`.\n\n\n## Use Cases\n\n**Data Discovery**: Quickly explore what values exist in your dimensions without writing complex group-by queries. Perfect for understanding unfamiliar datasets.\n\n**Autocomplete & Search**: Build type-ahead search features by indexing dimension values and filtering with pattern matching. The weight helps rank suggestions by relevance.\n\n**Data Profiling**: Understand data quality by examining cardinality, common values, and distributions across dimensions. Identify outliers or data entry errors.\n\n**Metric-Weighted Ranking**: Find dimension values that matter most for your metrics - e.g., \"cities with highest revenue\", \"products with most returns\", \"states with longest delivery times\".\n\n**Cross-Table Search**: Index dimensions across joined tables to search related data simultaneously, enabling unified search experiences.\n\n## Key Takeaways\n\n- Use `index(None)` to catalog all dimension values\n- Use `index(lambda t: [t.field1, t.field2])` for specific fields or `index(lambda t: t.field)` for a single field\n- Filter by `fieldType` to focus on strings or numbers\n- Use `by=\"measure_name\"` to weight by custom measures instead of counts\n- Add `sample=N` to analyze large datasets quickly\n- The index works across joins - use `table__field` syntax for joined dimensions\n- Perfect for building autocomplete, search, and data profiling features\n\n## Next Steps\n\n- Learn about [Nesting](/advanced/nesting) for hierarchical data structures\n- Explore [Query Methods](/querying/methods) for more query patterns\n", + "queries": { + "setup_airports": { + "code": "import ibis\nfrom boring_semantic_layer import to_semantic_table\n\n# Create synthetic airports data\nairports_data = ibis.memtable({\n \"code\": [\"JFK\", \"LAX\", \"ORD\", \"ATL\", \"DFW\", \"DEN\", \"SFO\", \"LAS\", \"SEA\", \"PHX\",\n \"IAH\", \"MCO\", \"EWR\", \"BOS\", \"MIA\", \"SAN\", \"LGA\", \"PHL\", \"DTW\", \"MSP\"],\n \"city\": [\"NEW YORK\", \"LOS ANGELES\", \"CHICAGO\", \"ATLANTA\", \"DALLAS\", \"DENVER\",\n \"SAN FRANCISCO\", \"LAS VEGAS\", \"SEATTLE\", \"PHOENIX\", \"HOUSTON\", \"ORLANDO\",\n \"NEWARK\", \"BOSTON\", \"MIAMI\", \"SAN DIEGO\", \"NEW YORK\", \"PHILADELPHIA\",\n \"DETROIT\", \"MINNEAPOLIS\"],\n \"state\": [\"NY\", \"CA\", \"IL\", \"GA\", \"TX\", \"CO\", \"CA\", \"NV\", \"WA\", \"AZ\",\n \"TX\", \"FL\", \"NJ\", \"MA\", \"FL\", \"CA\", \"NY\", \"PA\", \"MI\", \"MN\"],\n \"fac_type\": [\"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\",\n \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\",\n \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\", \"AIRPORT\",\n \"AIRPORT\", \"AIRPORT\"],\n \"elevation\": [13, 128, 672, 1026, 607, 5433, 13, 2181, 433, 1135,\n 97, 96, 18, 19, 8, 17, 21, 36, 645, 841]\n})\n\n# Define semantic table\nairports = (\n to_semantic_table(airports_data, name=\"airports\")\n .with_dimensions(\n code=lambda t: t.code,\n city=lambda t: t.city,\n state=lambda t: t.state,\n fac_type=lambda t: t.fac_type,\n elevation=lambda t: t.elevation,\n )\n .with_measures(\n airport_count=lambda t: t.count(),\n avg_elevation=lambda t: t.elevation.mean(),\n )\n)", + "sql": "SELECT\n *\nFROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\"", + "table": { + "columns": [ + "code", + "city", + "state", + "fac_type", + "elevation" + ], + "data": [ + [ + "JFK", + "NEW YORK", + "NY", + "AIRPORT", + 13 + ], + [ + "LAX", + "LOS ANGELES", + "CA", + "AIRPORT", + 128 + ], + [ + "ORD", + "CHICAGO", + "IL", + "AIRPORT", + 672 + ], + [ + "ATL", + "ATLANTA", + "GA", + "AIRPORT", + 1026 + ], + [ + "DFW", + "DALLAS", + "TX", + "AIRPORT", + 607 + ], + [ + "DEN", + "DENVER", + "CO", + "AIRPORT", + 5433 + ], + [ + "SFO", + "SAN FRANCISCO", + "CA", + "AIRPORT", + 13 + ], + [ + "LAS", + "LAS VEGAS", + "NV", + "AIRPORT", + 2181 + ], + [ + "SEA", + "SEATTLE", + "WA", + "AIRPORT", + 433 + ], + [ + "PHX", + "PHOENIX", + "AZ", + "AIRPORT", + 1135 + ], + [ + "IAH", + "HOUSTON", + "TX", + "AIRPORT", + 97 + ], + [ + "MCO", + "ORLANDO", + "FL", + "AIRPORT", + 96 + ], + [ + "EWR", + "NEWARK", + "NJ", + "AIRPORT", + 18 + ], + [ + "BOS", + "BOSTON", + "MA", + "AIRPORT", + 19 + ], + [ + "MIA", + "MIAMI", + "FL", + "AIRPORT", + 8 + ], + [ + "SAN", + "SAN DIEGO", + "CA", + "AIRPORT", + 17 + ], + [ + "LGA", + "NEW YORK", + "NY", + "AIRPORT", + 21 + ], + [ + "PHL", + "PHILADELPHIA", + "PA", + "AIRPORT", + 36 + ], + [ + "DTW", + "DETROIT", + "MI", + "AIRPORT", + 645 + ], + [ + "MSP", + "MINNEAPOLIS", + "MN", + "AIRPORT", + 841 + ] + ] + } + }, + "query_index_all": { + "code": "# Index all dimensions (None means all)\nresult = airports.index(None).limit(10)", + "sql": "SELECT\n *\nFROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n 'code' AS \"fieldName\",\n 'code' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t3\".\"value\" AS \"fieldValue\",\n \"t3\".\"weight\"\n FROM (\n SELECT\n \"t0\".\"code\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t3\"\n ) AS \"t8\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'city' AS \"fieldName\",\n 'city' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t4\".\"value\" AS \"fieldValue\",\n \"t4\".\"weight\"\n FROM (\n SELECT\n \"t0\".\"city\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t4\"\n ) AS \"t9\"\n ) AS \"t10\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'state' AS \"fieldName\",\n 'state' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t2\".\"value\" AS \"fieldValue\",\n \"t2\".\"weight\"\n FROM (\n SELECT\n \"t0\".\"state\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t2\"\n ) AS \"t7\"\n ) AS \"t12\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'fac_type' AS \"fieldName\",\n 'fac_type' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t1\".\"value\" AS \"fieldValue\",\n \"t1\".\"weight\"\n FROM (\n SELECT\n \"t0\".\"fac_type\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t1\"\n ) AS \"t6\"\n) AS \"t13\"\nUNION ALL\nSELECT\n *\nFROM (\n SELECT\n 'elevation' AS \"fieldName\",\n 'elevation' AS \"fieldPath\",\n 'number' AS \"fieldType\",\n CAST(\"t11\".\"min_val\" AS TEXT) || ' to ' || CAST(\"t11\".\"max_val\" AS TEXT) AS \"fieldValue\",\n \"t11\".\"weight\"\n FROM (\n SELECT\n MIN(\"t5\".\"value\") AS \"min_val\",\n MAX(\"t5\".\"value\") AS \"max_val\",\n COUNT(*) AS \"weight\"\n FROM (\n SELECT\n \"t0\".\"elevation\" AS \"value\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n WHERE\n \"t0\".\"elevation\" IS NOT NULL\n ) AS \"t5\"\n ) AS \"t11\"\n) AS \"t14\"\nLIMIT 10", + "table": { + "columns": [ + "fieldName", + "fieldPath", + "fieldType", + "fieldValue", + "weight" + ], + "data": [ + [ + "code", + "code", + "string", + "JFK", + 1 + ], + [ + "code", + "code", + "string", + "PHL", + 1 + ], + [ + "code", + "code", + "string", + "SEA", + 1 + ], + [ + "code", + "code", + "string", + "DTW", + 1 + ], + [ + "code", + "code", + "string", + "LGA", + 1 + ], + [ + "code", + "code", + "string", + "DFW", + 1 + ], + [ + "code", + "code", + "string", + "MIA", + 1 + ], + [ + "code", + "code", + "string", + "SFO", + 1 + ], + [ + "code", + "code", + "string", + "PHX", + 1 + ], + [ + "code", + "code", + "string", + "LAS", + 1 + ] + ] + } + }, + "query_index_specific": { + "code": "# Index only state and city\nresult = (\n airports.index(lambda t: [t.state, t.city])\n .order_by(lambda t: t.weight.desc())\n .limit(10)\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n 'code' AS \"fieldName\",\n 'code' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t3\".\"value\" AS \"fieldValue\",\n \"t3\".\"weight\"\n FROM (\n SELECT\n \"t0\".\"code\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t3\"\n ) AS \"t8\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'city' AS \"fieldName\",\n 'city' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t4\".\"value\" AS \"fieldValue\",\n \"t4\".\"weight\"\n FROM (\n SELECT\n \"t0\".\"city\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t4\"\n ) AS \"t9\"\n ) AS \"t10\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'state' AS \"fieldName\",\n 'state' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t2\".\"value\" AS \"fieldValue\",\n \"t2\".\"weight\"\n FROM (\n SELECT\n \"t0\".\"state\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t2\"\n ) AS \"t7\"\n ) AS \"t12\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'fac_type' AS \"fieldName\",\n 'fac_type' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t1\".\"value\" AS \"fieldValue\",\n \"t1\".\"weight\"\n FROM (\n SELECT\n \"t0\".\"fac_type\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t1\"\n ) AS \"t6\"\n ) AS \"t13\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'elevation' AS \"fieldName\",\n 'elevation' AS \"fieldPath\",\n 'number' AS \"fieldType\",\n CAST(\"t11\".\"min_val\" AS TEXT) || ' to ' || CAST(\"t11\".\"max_val\" AS TEXT) AS \"fieldValue\",\n \"t11\".\"weight\"\n FROM (\n SELECT\n MIN(\"t5\".\"value\") AS \"min_val\",\n MAX(\"t5\".\"value\") AS \"max_val\",\n COUNT(*) AS \"weight\"\n FROM (\n SELECT\n \"t0\".\"elevation\" AS \"value\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n WHERE\n \"t0\".\"elevation\" IS NOT NULL\n ) AS \"t5\"\n ) AS \"t11\"\n ) AS \"t14\"\n) AS \"t15\"\nORDER BY\n \"t15\".\"weight\" DESC\nLIMIT 10", + "table": { + "columns": [ + "fieldName", + "fieldPath", + "fieldType", + "fieldValue", + "weight" + ], + "data": [ + [ + "fac_type", + "fac_type", + "string", + "AIRPORT", + 20 + ], + [ + "elevation", + "elevation", + "number", + "8 to 5433", + 20 + ], + [ + "state", + "state", + "string", + "CA", + 3 + ], + [ + "city", + "city", + "string", + "NEW YORK", + 2 + ], + [ + "state", + "state", + "string", + "FL", + 2 + ], + [ + "state", + "state", + "string", + "NY", + 2 + ], + [ + "state", + "state", + "string", + "TX", + 2 + ], + [ + "city", + "city", + "string", + "SAN DIEGO", + 1 + ], + [ + "city", + "city", + "string", + "PHILADELPHIA", + 1 + ], + [ + "city", + "city", + "string", + "NEWARK", + 1 + ] + ] + } + }, + "query_autocomplete": { + "code": "# Get city suggestions starting with \"SAN\"\nresult = (\n airports.index(lambda t: t.city)\n .filter(lambda t: t.fieldValue.like(\"SAN%\"))\n .order_by(lambda t: t.weight.desc())\n .limit(10)\n)", + "sql": "SELECT\n 'city' AS \"fieldName\",\n 'city' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t1\".\"value\" AS \"fieldValue\",\n \"t1\".\"weight\"\nFROM (\n SELECT\n \"t0\".\"city\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n) AS \"t1\"\nWHERE\n \"t1\".\"value\" LIKE 'SAN%'\nORDER BY\n \"t1\".\"weight\" DESC\nLIMIT 10", + "table": { + "columns": [ + "fieldName", + "fieldPath", + "fieldType", + "fieldValue", + "weight" + ], + "data": [ + [ + "city", + "city", + "string", + "SAN DIEGO", + 1 + ], + [ + "city", + "city", + "string", + "SAN FRANCISCO", + 1 + ] + ] + } + }, + "query_by_type": { + "code": "# Get only string field values\nresult = (\n airports.index(None)\n .filter(lambda t: t.fieldType == \"string\")\n .order_by(lambda t: t.weight.desc())\n .limit(10)\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n 'code' AS \"fieldName\",\n 'code' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t3\".\"value\" AS \"fieldValue\",\n \"t3\".\"weight\"\n FROM (\n SELECT\n \"t0\".\"code\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t3\"\n ) AS \"t8\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'city' AS \"fieldName\",\n 'city' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t4\".\"value\" AS \"fieldValue\",\n \"t4\".\"weight\"\n FROM (\n SELECT\n \"t0\".\"city\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t4\"\n ) AS \"t9\"\n ) AS \"t10\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'state' AS \"fieldName\",\n 'state' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t2\".\"value\" AS \"fieldValue\",\n \"t2\".\"weight\"\n FROM (\n SELECT\n \"t0\".\"state\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t2\"\n ) AS \"t7\"\n ) AS \"t12\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'fac_type' AS \"fieldName\",\n 'fac_type' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t1\".\"value\" AS \"fieldValue\",\n \"t1\".\"weight\"\n FROM (\n SELECT\n \"t0\".\"fac_type\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t1\"\n ) AS \"t6\"\n ) AS \"t13\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'elevation' AS \"fieldName\",\n 'elevation' AS \"fieldPath\",\n 'number' AS \"fieldType\",\n CAST(\"t11\".\"min_val\" AS TEXT) || ' to ' || CAST(\"t11\".\"max_val\" AS TEXT) AS \"fieldValue\",\n \"t11\".\"weight\"\n FROM (\n SELECT\n MIN(\"t5\".\"value\") AS \"min_val\",\n MAX(\"t5\".\"value\") AS \"max_val\",\n COUNT(*) AS \"weight\"\n FROM (\n SELECT\n \"t0\".\"elevation\" AS \"value\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n WHERE\n \"t0\".\"elevation\" IS NOT NULL\n ) AS \"t5\"\n ) AS \"t11\"\n ) AS \"t14\"\n) AS \"t15\"\nWHERE\n \"t15\".\"fieldType\" = 'string'\nORDER BY\n \"t15\".\"weight\" DESC\nLIMIT 10", + "table": { + "columns": [ + "fieldName", + "fieldPath", + "fieldType", + "fieldValue", + "weight" + ], + "data": [ + [ + "fac_type", + "fac_type", + "string", + "AIRPORT", + 20 + ], + [ + "state", + "state", + "string", + "CA", + 3 + ], + [ + "city", + "city", + "string", + "NEW YORK", + 2 + ], + [ + "state", + "state", + "string", + "FL", + 2 + ], + [ + "state", + "state", + "string", + "TX", + 2 + ], + [ + "state", + "state", + "string", + "NY", + 2 + ], + [ + "code", + "code", + "string", + "IAH", + 1 + ], + [ + "code", + "code", + "string", + "PHL", + 1 + ], + [ + "code", + "code", + "string", + "SAN", + 1 + ], + [ + "code", + "code", + "string", + "MCO", + 1 + ] + ] + } + }, + "query_custom_weight": { + "code": "# Find states with most airports\nresult = (\n airports.index(lambda t: t.state, by=\"airport_count\")\n .order_by(lambda t: t.weight.desc())\n .limit(10)\n)", + "sql": "SELECT\n 'state' AS \"fieldName\",\n 'state' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t1\".\"value\" AS \"fieldValue\",\n \"t1\".\"weight\"\nFROM (\n SELECT\n \"t0\".\"state\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n GROUP BY\n 1\n) AS \"t1\"\nORDER BY\n \"t1\".\"weight\" DESC\nLIMIT 10", + "table": { + "columns": [ + "fieldName", + "fieldPath", + "fieldType", + "fieldValue", + "weight" + ], + "data": [ + [ + "state", + "state", + "string", + "CA", + 3 + ], + [ + "state", + "state", + "string", + "NY", + 2 + ], + [ + "state", + "state", + "string", + "FL", + 2 + ], + [ + "state", + "state", + "string", + "TX", + 2 + ], + [ + "state", + "state", + "string", + "IL", + 1 + ], + [ + "state", + "state", + "string", + "PA", + 1 + ], + [ + "state", + "state", + "string", + "MN", + 1 + ], + [ + "state", + "state", + "string", + "MI", + 1 + ], + [ + "state", + "state", + "string", + "GA", + 1 + ], + [ + "state", + "state", + "string", + "NV", + 1 + ] + ] + } + }, + "query_sampled": { + "code": "# Sample 100 rows before indexing\nresult = (\n airports.index(None, sample=100)\n .filter(lambda t: t.fieldType == \"string\")\n .order_by(lambda t: t.weight.desc())\n .limit(10)\n)", + "sql": "WITH \"t1\" AS (\n SELECT\n *\n FROM \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t0\"\n LIMIT 100\n)\nSELECT\n *\nFROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n 'code' AS \"fieldName\",\n 'code' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t5\".\"value\" AS \"fieldValue\",\n \"t5\".\"weight\"\n FROM (\n SELECT\n \"t2\".\"code\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"t1\" AS \"t2\"\n GROUP BY\n 1\n ) AS \"t5\"\n ) AS \"t10\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'city' AS \"fieldName\",\n 'city' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t6\".\"value\" AS \"fieldValue\",\n \"t6\".\"weight\"\n FROM (\n SELECT\n \"t2\".\"city\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"t1\" AS \"t2\"\n GROUP BY\n 1\n ) AS \"t6\"\n ) AS \"t11\"\n ) AS \"t12\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'state' AS \"fieldName\",\n 'state' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t4\".\"value\" AS \"fieldValue\",\n \"t4\".\"weight\"\n FROM (\n SELECT\n \"t2\".\"state\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"t1\" AS \"t2\"\n GROUP BY\n 1\n ) AS \"t4\"\n ) AS \"t9\"\n ) AS \"t14\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'fac_type' AS \"fieldName\",\n 'fac_type' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t3\".\"value\" AS \"fieldValue\",\n \"t3\".\"weight\"\n FROM (\n SELECT\n \"t2\".\"fac_type\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"t1\" AS \"t2\"\n GROUP BY\n 1\n ) AS \"t3\"\n ) AS \"t8\"\n ) AS \"t15\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'elevation' AS \"fieldName\",\n 'elevation' AS \"fieldPath\",\n 'number' AS \"fieldType\",\n CAST(\"t13\".\"min_val\" AS TEXT) || ' to ' || CAST(\"t13\".\"max_val\" AS TEXT) AS \"fieldValue\",\n \"t13\".\"weight\"\n FROM (\n SELECT\n MIN(\"t7\".\"value\") AS \"min_val\",\n MAX(\"t7\".\"value\") AS \"max_val\",\n COUNT(*) AS \"weight\"\n FROM (\n SELECT\n \"t2\".\"elevation\" AS \"value\"\n FROM \"t1\" AS \"t2\"\n WHERE\n \"t2\".\"elevation\" IS NOT NULL\n ) AS \"t7\"\n ) AS \"t13\"\n ) AS \"t16\"\n) AS \"t17\"\nWHERE\n \"t17\".\"fieldType\" = 'string'\nORDER BY\n \"t17\".\"weight\" DESC\nLIMIT 10", + "table": { + "columns": [ + "fieldName", + "fieldPath", + "fieldType", + "fieldValue", + "weight" + ], + "data": [ + [ + "fac_type", + "fac_type", + "string", + "AIRPORT", + 20 + ], + [ + "state", + "state", + "string", + "CA", + 3 + ], + [ + "state", + "state", + "string", + "NY", + 2 + ], + [ + "state", + "state", + "string", + "FL", + 2 + ], + [ + "state", + "state", + "string", + "TX", + 2 + ], + [ + "city", + "city", + "string", + "NEW YORK", + 2 + ], + [ + "state", + "state", + "string", + "MA", + 1 + ], + [ + "state", + "state", + "string", + "GA", + 1 + ], + [ + "state", + "state", + "string", + "CO", + 1 + ], + [ + "state", + "state", + "string", + "MI", + 1 + ] + ] + } + }, + "query_index_joins": { + "code": "# Create synthetic flights data\nflights_data = ibis.memtable({\n \"flight_id\": list(range(1, 31)),\n \"carrier\": [\"AA\", \"UA\", \"DL\", \"WN\", \"B6\", \"AA\", \"UA\", \"DL\", \"WN\", \"B6\"] * 3,\n \"origin\": [\"JFK\", \"LAX\", \"ORD\", \"ATL\", \"DFW\", \"SFO\", \"SEA\", \"DEN\", \"PHX\", \"BOS\"] * 3,\n})\n\nflights = (\n to_semantic_table(flights_data, name=\"flights\")\n .with_dimensions(\n carrier=lambda t: t.carrier,\n origin=lambda t: t.origin,\n )\n .with_measures(\n flight_count=lambda t: t.count(),\n )\n)\n\n# Join flights with airports\nflights_with_origin = flights.join_one(airports, left_on=\"origin\", right_on=\"code\")\n\n# Index across the join\nresult = (\n flights_with_origin.index(lambda t: [t.carrier, t.airports__state])\n .order_by(lambda t: t.weight.desc())\n .limit(10)\n)", + "sql": "WITH \"t5\" AS (\n SELECT\n \"t2\".\"flight_id\",\n \"t2\".\"carrier\",\n \"t2\".\"origin\",\n \"t3\".\"code\",\n \"t3\".\"city\",\n \"t3\".\"state\",\n \"t3\".\"fac_type\",\n \"t3\".\"elevation\"\n FROM \"ibis_pandas_memtable_j6kjmmgwm5fjvcljn4ramxl2gq\" AS \"t2\"\n INNER JOIN \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t3\"\n ON \"t2\".\"origin\" = \"t3\".\"code\"\n), \"t9\" AS (\n SELECT\n \"t7\".\"fac_type\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"t5\" AS \"t7\"\n GROUP BY\n 1\n), \"t10\" AS (\n SELECT\n \"t7\".\"state\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"t5\" AS \"t7\"\n GROUP BY\n 1\n), \"t11\" AS (\n SELECT\n \"t7\".\"city\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"t5\" AS \"t7\"\n GROUP BY\n 1\n), \"t12\" AS (\n SELECT\n \"t7\".\"code\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"t5\" AS \"t7\"\n GROUP BY\n 1\n), \"t13\" AS (\n SELECT\n \"t7\".\"origin\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"t5\" AS \"t7\"\n GROUP BY\n 1\n), \"t14\" AS (\n SELECT\n \"t7\".\"carrier\" AS \"value\",\n COUNT(*) AS \"weight\"\n FROM \"t5\" AS \"t7\"\n GROUP BY\n 1\n), \"t22\" AS (\n SELECT\n MIN(\"t8\".\"value\") AS \"min_val\",\n MAX(\"t8\".\"value\") AS \"max_val\",\n COUNT(*) AS \"weight\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t3\".\"elevation\" AS \"value\"\n FROM \"ibis_pandas_memtable_j6kjmmgwm5fjvcljn4ramxl2gq\" AS \"t2\"\n INNER JOIN \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t3\"\n ON \"t2\".\"origin\" = \"t3\".\"code\"\n ) AS \"t4\"\n WHERE\n \"t4\".\"value\" IS NOT NULL\n ) AS \"t8\"\n)\nSELECT\n *\nFROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n *\n FROM (\n SELECT\n 'carrier' AS \"fieldName\",\n 'flights.carrier' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t21\".\"value\" AS \"fieldValue\",\n \"t21\".\"weight\"\n FROM \"t14\" AS \"t21\"\n ) AS \"t34\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'origin' AS \"fieldName\",\n 'flights.origin' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t20\".\"value\" AS \"fieldValue\",\n \"t20\".\"weight\"\n FROM \"t13\" AS \"t20\"\n ) AS \"t32\"\n ) AS \"t37\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'code' AS \"fieldName\",\n 'airports.code' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t19\".\"value\" AS \"fieldValue\",\n \"t19\".\"weight\"\n FROM \"t12\" AS \"t19\"\n ) AS \"t30\"\n ) AS \"t38\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'city' AS \"fieldName\",\n 'airports.city' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t18\".\"value\" AS \"fieldValue\",\n \"t18\".\"weight\"\n FROM \"t11\" AS \"t18\"\n ) AS \"t28\"\n ) AS \"t39\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'state' AS \"fieldName\",\n 'airports.state' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t17\".\"value\" AS \"fieldValue\",\n \"t17\".\"weight\"\n FROM \"t10\" AS \"t17\"\n ) AS \"t26\"\n ) AS \"t40\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'fac_type' AS \"fieldName\",\n 'airports.fac_type' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t16\".\"value\" AS \"fieldValue\",\n \"t16\".\"weight\"\n FROM \"t9\" AS \"t16\"\n ) AS \"t24\"\n ) AS \"t41\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'elevation' AS \"fieldName\",\n 'airports.elevation' AS \"fieldPath\",\n 'number' AS \"fieldType\",\n CAST(\"t36\".\"min_val\" AS TEXT) || ' to ' || CAST(\"t36\".\"max_val\" AS TEXT) AS \"fieldValue\",\n \"t36\".\"weight\"\n FROM \"t22\" AS \"t36\"\n ) AS \"t44\"\n ) AS \"t45\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'flight_id' AS \"fieldName\",\n 'flight_id' AS \"fieldPath\",\n 'number' AS \"fieldType\",\n CAST(\"t35\".\"min_val\" AS TEXT) || ' to ' || CAST(\"t35\".\"max_val\" AS TEXT) AS \"fieldValue\",\n \"t35\".\"weight\"\n FROM (\n SELECT\n MIN(\"t15\".\"value\") AS \"min_val\",\n MAX(\"t15\".\"value\") AS \"max_val\",\n COUNT(*) AS \"weight\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t2\".\"flight_id\" AS \"value\"\n FROM \"ibis_pandas_memtable_j6kjmmgwm5fjvcljn4ramxl2gq\" AS \"t2\"\n INNER JOIN \"ibis_pandas_memtable_yxhjyztwsfhiza5zm45udzjrwi\" AS \"t3\"\n ON \"t2\".\"origin\" = \"t3\".\"code\"\n ) AS \"t6\"\n WHERE\n \"t6\".\"value\" IS NOT NULL\n ) AS \"t15\"\n ) AS \"t35\"\n ) AS \"t42\"\n ) AS \"t46\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'carrier' AS \"fieldName\",\n 'carrier' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t21\".\"value\" AS \"fieldValue\",\n \"t21\".\"weight\"\n FROM \"t14\" AS \"t21\"\n ) AS \"t33\"\n ) AS \"t47\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'origin' AS \"fieldName\",\n 'origin' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t20\".\"value\" AS \"fieldValue\",\n \"t20\".\"weight\"\n FROM \"t13\" AS \"t20\"\n ) AS \"t31\"\n ) AS \"t48\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'code' AS \"fieldName\",\n 'code' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t19\".\"value\" AS \"fieldValue\",\n \"t19\".\"weight\"\n FROM \"t12\" AS \"t19\"\n ) AS \"t29\"\n ) AS \"t49\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'city' AS \"fieldName\",\n 'city' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t18\".\"value\" AS \"fieldValue\",\n \"t18\".\"weight\"\n FROM \"t11\" AS \"t18\"\n ) AS \"t27\"\n ) AS \"t50\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'state' AS \"fieldName\",\n 'state' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t17\".\"value\" AS \"fieldValue\",\n \"t17\".\"weight\"\n FROM \"t10\" AS \"t17\"\n ) AS \"t25\"\n ) AS \"t51\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'fac_type' AS \"fieldName\",\n 'fac_type' AS \"fieldPath\",\n 'string' AS \"fieldType\",\n \"t16\".\"value\" AS \"fieldValue\",\n \"t16\".\"weight\"\n FROM \"t9\" AS \"t16\"\n ) AS \"t23\"\n ) AS \"t52\"\n UNION ALL\n SELECT\n *\n FROM (\n SELECT\n 'elevation' AS \"fieldName\",\n 'elevation' AS \"fieldPath\",\n 'number' AS \"fieldType\",\n CAST(\"t36\".\"min_val\" AS TEXT) || ' to ' || CAST(\"t36\".\"max_val\" AS TEXT) AS \"fieldValue\",\n \"t36\".\"weight\"\n FROM \"t22\" AS \"t36\"\n ) AS \"t43\"\n) AS \"t53\"\nORDER BY\n \"t53\".\"weight\" DESC\nLIMIT 10", + "table": { + "columns": [ + "fieldName", + "fieldPath", + "fieldType", + "fieldValue", + "weight" + ], + "data": [ + [ + "fac_type", + "airports.fac_type", + "string", + "AIRPORT", + 30 + ], + [ + "flight_id", + "flight_id", + "number", + "1 to 30", + 30 + ], + [ + "elevation", + "elevation", + "number", + "13 to 5433", + 30 + ], + [ + "fac_type", + "fac_type", + "string", + "AIRPORT", + 30 + ], + [ + "elevation", + "airports.elevation", + "number", + "13 to 5433", + 30 + ], + [ + "carrier", + "carrier", + "string", + "UA", + 6 + ], + [ + "carrier", + "flights.carrier", + "string", + "AA", + 6 + ], + [ + "carrier", + "carrier", + "string", + "B6", + 6 + ], + [ + "carrier", + "flights.carrier", + "string", + "WN", + 6 + ], + [ + "carrier", + "carrier", + "string", + "WN", + 6 + ] + ] + } + } + }, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/joins.json b/docs/public/bsl-data/joins.json new file mode 100644 index 00000000..298ac8f6 --- /dev/null +++ b/docs/public/bsl-data/joins.json @@ -0,0 +1,5 @@ +{ + "markdown": "# Joins & Relationships\n\nConnect semantic tables to enable powerful multi-model queries. BSL provides flexible joining capabilities that allow you to query dimensions and measures across related tables using intuitive dot notation.\n\n## Join Types\n\nBSL supports three types of joins to handle different relationship patterns between your semantic tables.\n\n### SQL-style Joins\n\nUse the `Join` class for standard SQL-style joins with full control over join conditions and types.\n\n```python\nfrom boring_semantic_layer import Join\n\n# Define carriers model\ncarriers_sm = SemanticModel(\n name=\"carriers\",\n table=carriers_tbl,\n dimensions={\n \"code\": lambda t: t.code,\n \"name\": lambda t: t.name\n }\n)\n\n# Join with flights\nflights_sm = SemanticModel(\n name=\"flights\",\n table=flights_tbl,\n dimensions={\"carrier\": lambda t: t.carrier},\n measures={\"flight_count\": lambda t: t.count()},\n joins={\n \"carriers\": Join(\n model=carriers_sm,\n on=lambda left, right: left.carrier == right.code,\n how=\"left\" # or \"inner\", \"right\", \"outer\"\n )\n }\n)\n\n# Query across joined models\nflights_sm.query(\n dimensions=['carriers.name'],\n measures=['flight_count']\n).execute()\n```\n\nThe `how` parameter accepts standard SQL join types: `\"left\"`, `\"inner\"`, `\"right\"`, or `\"outer\"`.\n\n### One-to-One Joins\n\nUse `join_one` for relationships where each row matches exactly one row in the joined table. This is a convenience function that simplifies the syntax for one-to-one relationships.\n\n```python\nfrom boring_semantic_layer import join_one\n\nflights_sm = SemanticModel(\n table=flights_tbl,\n joins={\n \"carrier_details\": join_one(\n model=carriers_sm,\n on=lambda left, right: left.carrier == right.code\n )\n }\n)\n```\n\n### Cross Joins\n\nUse `join_cross` to create cartesian products for special analytical needs, such as generating all possible combinations of values.\n\n```python\nfrom boring_semantic_layer import join_cross\n\n# Cross join for generating all combinations\nflights_sm = SemanticModel(\n table=flights_tbl,\n joins={\n \"all_carriers\": join_cross(model=carriers_sm)\n }\n)\n```\n\n## Querying Across Joined Models\n\nOnce you've defined joins between semantic tables, you can query dimensions and measures from multiple models in a single query. Use dot notation to reference fields from joined models.\n\n```python\n# Query dimensions and measures from joined models\nresult = flights_sm.query(\n dimensions=[\n 'origin', # From flights\n 'carriers.name', # From joined carriers\n 'carriers.nickname' # Also from carriers\n ],\n measures=[\n 'flight_count', # From flights\n 'carriers.carrier_count' # From carriers\n ]\n).execute()\n```\n\nThe dot notation syntax is: `{model_name}.{dimension_or_measure}`. Fields without a prefix are assumed to come from the base model.\n\n## Handling Name Conflicts\n\nWhen multiple models have dimensions or measures with the same name, use the model prefix to disambiguate and make your queries explicit.\n\n```python\n# Both models have a 'name' dimension\nresult = flights_sm.query(\n dimensions=[\n 'flights.name', # Explicitly from flights\n 'carriers.name' # Explicitly from carriers\n ],\n measures=['flight_count']\n).execute()\n```\n\nUsing explicit model prefixes is recommended even when there are no conflicts, as it makes your queries more readable and maintainable.\n\n## Examples\n\n### Example 1: Flight Analysis with Carrier Details\n\nThis example shows how to analyze flight data enriched with carrier information.\n\n```python\nfrom boring_semantic_layer import SemanticModel, Join\n\n# Define the carriers semantic model\ncarriers_sm = SemanticModel(\n name=\"carriers\",\n table=carriers_tbl,\n dimensions={\n \"code\": lambda t: t.code,\n \"name\": lambda t: t.name,\n \"nickname\": lambda t: t.nickname\n },\n measures={\n \"carrier_count\": lambda t: t.count()\n }\n)\n\n# Define flights model with join to carriers\nflights_sm = SemanticModel(\n name=\"flights\",\n table=flights_tbl,\n dimensions={\n \"carrier\": lambda t: t.carrier,\n \"origin\": lambda t: t.origin,\n \"destination\": lambda t: t.dest\n },\n measures={\n \"flight_count\": lambda t: t.count(),\n \"avg_delay\": lambda t: t.dep_delay.mean()\n },\n joins={\n \"carriers\": Join(\n model=carriers_sm,\n on=lambda left, right: left.carrier == right.code,\n how=\"left\"\n )\n }\n)\n\n# Query flight statistics by carrier name\nresult = flights_sm.query(\n dimensions=['carriers.name', 'origin'],\n measures=['flight_count', 'avg_delay']\n).execute()\n```\n\n### Example 2: Multi-Model Query with Filters\n\nThis example demonstrates filtering and aggregating across joined models.\n\n```python\n# Query with filters on both models\nresult = flights_sm.query(\n dimensions=['carriers.name', 'destination'],\n measures=['flight_count', 'avg_delay']\n).where(\n lambda t: (t.origin == 'JFK') & (t.carriers.name.like('%American%'))\n).execute()\n\n# The filter can reference both the base model and joined models\n# using the same dot notation as in the select clause\n```\n\nThis consolidated approach to joins enables you to build complex analytical queries while maintaining clean, readable code.\n", + "queries": {}, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/mcp.json b/docs/public/bsl-data/mcp.json new file mode 100644 index 00000000..717725dd --- /dev/null +++ b/docs/public/bsl-data/mcp.json @@ -0,0 +1,5 @@ +{ + "markdown": "# Model Context Protocol (MCP) Integration\n\nBSL includes built-in support for the [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol/python-sdk), allowing you to expose your semantic models to Large Language Models like Claude.\n\n\n**Pro tip:** Use [descriptions in dimensions and measures](/building/semantic-tables#adding-descriptions) to make your models more AI-friendly. Descriptions help provide context to LLMs, enabling them to understand what each field represents and when to use them.\n\n\n## Installation\n\nTo use MCP functionality, install BSL with the `fastmcp` extra:\n\n```bash\npip install 'boring-semantic-layer[fastmcp]'\n```\n\n## Setting up an MCP Server\n\nCreate an MCP server script that exposes your semantic models:\n\n```python\nimport ibis\nfrom boring_semantic_layer.semantic_api import to_semantic_table\nfrom boring_semantic_layer.api.mcp import MCPSemanticModel\n\n# Create synthetic flights data\nflights_data = ibis.memtable({\n \"flight_id\": list(range(1, 101)),\n \"origin\": [\"JFK\", \"LAX\", \"ORD\", \"ATL\", \"DFW\"] * 20,\n \"dest\": [\"LAX\", \"JFK\", \"DFW\", \"ORD\", \"ATL\"] * 20,\n \"carrier\": [\"AA\", \"UA\", \"DL\", \"WN\", \"B6\"] * 20,\n \"distance\": [2475, 2475, 801, 606, 732] * 20,\n})\n\n# Define your semantic table with descriptions\nflights = (\n to_semantic_table(flights_data, name=\"flights\")\n .with_dimensions(\n origin={\n \"expr\": lambda t: t.origin,\n \"description\": \"Origin airport code where the flight departed from\"\n },\n destination={\n \"expr\": lambda t: t.dest,\n \"description\": \"Destination airport code where the flight arrived\"\n },\n carrier={\n \"expr\": lambda t: t.carrier,\n \"description\": \"Airline carrier code (e.g., AA, UA, DL)\"\n },\n )\n .with_measures(\n total_flights={\n \"expr\": lambda t: t.count(),\n \"description\": \"Total number of flights\"\n },\n avg_distance={\n \"expr\": lambda t: t.distance.mean(),\n \"description\": \"Average flight distance in miles\"\n },\n )\n)\n\n# Create the MCP server\nmcp_server = MCPSemanticModel(\n models={\"flights\": flights},\n name=\"Flight Data Server\"\n)\n\nif __name__ == \"__main__\":\n mcp_server.run(transport=\"stdio\")\n```\n\nSave this as `example_mcp.py` in your project directory.\n\n## Configuring Claude Desktop\n\nTo use your MCP server with Claude Desktop, add it to your configuration file.\n\n**Configuration file location:**\n- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`\n- **Windows:** `%APPDATA%\\Claude\\claude_desktop_config.json`\n\n**Example configuration:**\n\n```json\n{\n \"mcpServers\": {\n \"flight_sm\": {\n \"command\": \"uv\",\n \"args\": [\n \"--directory\",\n \"/path/to/your/project/\",\n \"run\",\n \"example_mcp.py\"\n ]\n }\n }\n}\n```\n\nReplace `/path/to/your/project/` with the actual path to your project directory.\n\n\nThis example uses [uv](https://docs.astral.sh/uv/) to run the MCP server. You can also use `python` directly if you have BSL installed in your environment:\n\n```json\n{\n \"mcpServers\": {\n \"flight_sm\": {\n \"command\": \"python\",\n \"args\": [\"/path/to/your/project/example_mcp.py\"]\n }\n }\n}\n```\n\n\nAfter updating the configuration:\n1. Restart Claude Desktop\n2. Look for the MCP server indicator in the Claude Desktop interface\n3. You should see \"flight_sm\" listed as an available server\n\n## Available MCP Tools\n\nOnce configured, Claude will have access to these tools for interacting with your semantic models:\n\n### list_models\n\nList all available semantic model names in the MCP server.\n\n**Example usage in Claude:**\n> \"What models are available?\"\n\n**Returns:** Array of model names (e.g., `[\"flights\", \"carriers\"]`)\n\n### get_model\n\nGet detailed information about a specific model including its dimensions, measures, and descriptions.\n\n**Parameters:**\n- `model_name` (str): Name of the model to inspect\n\n**Example usage in Claude:**\n> \"Show me the details of the flights model\"\n\n**Returns:** Model schema including:\n- Model name and description\n- List of dimensions with their descriptions\n- List of measures with their descriptions\n- Available joins (if any)\n\n### get_time_range\n\nGet the available time range for time-series data in a model.\n\n**Parameters:**\n- `model_name` (str): Name of the model\n- `time_dimension` (str): Name of the time dimension\n\n**Example usage in Claude:**\n> \"What's the time range available in the flights model?\"\n\n**Returns:** Dictionary with `min_time` and `max_time` values\n\n### query_model\n\nExecute queries against a semantic model with dimensions, measures, filters, and optional chart specifications.\n\n**Parameters:**\n- `model_name` (str): Name of the model to query\n- `dimensions` (list[str]): List of dimension names to group by\n- `measures` (list[str]): List of measure names to aggregate\n- `filters` (list[str], optional): List of filter expressions (e.g., `[\"origin == 'JFK'\"]`)\n- `limit` (int, optional): Maximum number of rows to return\n- `order_by` (list[str], optional): List of columns to sort by\n- `chart_spec` (dict, optional): Vega-Lite chart specification\n\n**Example usage in Claude:**\n> \"Show me the top 10 origins by flight count\"\n> \"Create a bar chart of average distance by carrier\"\n\n**Returns:**\n- When `chart_spec` is provided: `{\"records\": [...], \"chart\": {...}}`\n- When `chart_spec` is not provided: `{\"records\": [...]}`\n\n### Example Interactions\n\nHere are some example questions you can ask Claude when the MCP server is configured:\n\n**Data Exploration:**\n- \"What models are available in the flight data server?\"\n- \"Show me all dimensions and measures in the flights model\"\n- \"What is the time range covered by the flights data?\"\n\n**Basic Queries:**\n- \"How many flights departed from JFK?\"\n- \"Show me the top 5 destinations by flight count\"\n- \"What's the average flight distance for each carrier?\"\n\n**Filtered Queries:**\n- \"Show me flights from California airports (starting with 'S')\"\n- \"What carriers have an average distance over 1000 miles?\"\n- \"List the top 10 busiest routes\"\n\n**Visualizations:**\n- \"Create a bar chart showing flights by origin airport\"\n- \"Make a line chart of flights over time\"\n- \"Show me a heatmap of routes between origins and destinations\"\n\n## Best Practices\n\n### 1. Add Descriptions to All Fields\n\nDescriptions are crucial for LLMs to understand your data model:\n\n```python\nflights = (\n to_semantic_table(flights_tbl, name=\"flights\")\n .with_dimensions(\n origin={\n \"expr\": lambda t: t.origin,\n \"description\": \"Origin airport code (3-letter IATA code)\"\n }\n )\n .with_measures(\n total_flights={\n \"expr\": lambda t: t.count(),\n \"description\": \"Total number of flights in the dataset\"\n }\n )\n)\n```\n\n### 2. Use Descriptive Model Names\n\nChoose clear, descriptive names for your models:\n\n```python\n# Good\nmcp_server = MCPSemanticModel(\n models={\"flights\": flights, \"carriers\": carriers},\n name=\"Aviation Analytics Server\"\n)\n\n# Less clear\nmcp_server = MCPSemanticModel(\n models={\"f\": flights, \"c\": carriers},\n name=\"Server\"\n)\n```\n\n### 3. Structure Your Data Logically\n\nOrganize related dimensions and measures together, and use joins to connect related models:\n\n```python\n# Flights model focuses on flight operations\nflights = (\n to_semantic_table(flights_tbl, name=\"flights\")\n .with_dimensions(origin=..., destination=..., date=...)\n .with_measures(flight_count=..., avg_delay=...)\n)\n\n# Carriers model focuses on airline information\ncarriers = (\n to_semantic_table(carriers_tbl, name=\"carriers\")\n .with_dimensions(code=..., name=..., country=...)\n .with_measures(carrier_count=...)\n)\n\n# Connect them with joins\nflights_with_carriers = flights.join_one(\n carriers,\n left_on=\"carrier\",\n right_on=\"code\"\n)\n```\n\n## Troubleshooting\n\n### Server Not Appearing in Claude Desktop\n\n1. Check the configuration file path is correct\n2. Verify JSON syntax in `claude_desktop_config.json`\n3. Ensure BSL is installed with MCP support: `pip install 'boring-semantic-layer[fastmcp]'`\n4. Restart Claude Desktop completely\n5. Check Claude Desktop logs for error messages\n\n### Import Errors\n\nIf you see import errors when the server starts:\n\n```bash\n# Ensure all dependencies are installed\npip install 'boring-semantic-layer[fastmcp]'\n\n# Or install specific dependencies\npip install fastmcp ibis-framework\n```\n\n### Path Issues\n\nMake sure file paths in your configuration are absolute paths, not relative:\n\n```json\n{\n \"mcpServers\": {\n \"flight_sm\": {\n \"command\": \"python\",\n \"args\": [\"/Users/username/projects/my-project/example_mcp.py\"]\n }\n }\n}\n```\n\n## Next Steps\n\n- Learn about [YAML Configuration](/building/yaml) for managing multiple models\n- Explore [Query Methods](/querying/methods) to understand what queries LLMs can perform\n- See [Charting](/querying/charting) for visualization capabilities\n- Review the [full API Reference](/reference) for advanced features\n", + "queries": {}, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/nested-subtotals.json b/docs/public/bsl-data/nested-subtotals.json new file mode 100644 index 00000000..5708f618 --- /dev/null +++ b/docs/public/bsl-data/nested-subtotals.json @@ -0,0 +1,1410 @@ +{ + "markdown": "# Nested Subtotals\n\nCreate hierarchical aggregations with subtotals at multiple levels using the `nest` parameter. This pattern enables drill-down analysis where each row contains both summary metrics and nested breakdowns.\n\n## Overview\n\nThe nested subtotals pattern allows you to:\n\n- Generate subtotals at each level of a dimensional hierarchy in a single query\n- Create nested structures where each parent row contains child breakdowns\n- Avoid complex self-joins or ROLLUP queries\n- Build hierarchical data suitable for tree views and drill-down UIs\n\n## Setup\n\nCreate a sample order items dataset with temporal and categorical dimensions:\n\n```setup_data\nimport ibis\nfrom ibis import _\nfrom boring_semantic_layer import to_semantic_table\n\n# Create synthetic order items data\norder_items_data = ibis.memtable({\n \"order_id\": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010,\n 1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020,\n 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030],\n \"sale_price\": [45.99, 89.50, 120.00, 34.99, 67.80, 99.99, 54.50, 78.99, 150.00, 42.00,\n 55.99, 72.50, 88.80, 110.00, 39.99, 95.00, 62.50, 81.99, 125.00, 48.50,\n 66.99, 92.00, 105.50, 73.99, 58.80, 118.00, 84.50, 69.99, 135.00, 51.50],\n \"status\": [\"shipped\", \"delivered\", \"shipped\", \"processing\", \"delivered\",\n \"shipped\", \"cancelled\", \"delivered\", \"shipped\", \"processing\",\n \"delivered\", \"shipped\", \"delivered\", \"processing\", \"shipped\",\n \"cancelled\", \"delivered\", \"shipped\", \"delivered\", \"processing\",\n \"shipped\", \"delivered\", \"shipped\", \"processing\", \"delivered\",\n \"shipped\", \"cancelled\", \"delivered\", \"shipped\", \"processing\"],\n \"created_year\": [2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022,\n 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023,\n 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024],\n \"created_month\": [1, 1, 2, 2, 3, 3, 4, 4, 5, 5,\n 1, 1, 2, 2, 3, 3, 4, 4, 5, 5,\n 1, 1, 2, 2, 3, 3, 4, 4, 5, 5]\n})\n\n# Create semantic table with measures\norder_items = to_semantic_table(\n order_items_data,\n name=\"order_items\",\n).with_measures(\n order_count=lambda t: t.count(),\n total_sales=lambda t: t.sale_price.sum(),\n avg_price=lambda t: t.sale_price.mean(),\n)\n```\n\n\n\n## Year with Nested Month Subtotals\n\nCreate yearly totals with monthly breakdowns nested inside each year:\n\n```query_year_with_months\nfrom ibis import _\n\n# First aggregate by year and month to get monthly subtotals\nmonthly_data = (\n order_items\n .group_by(\"created_year\", \"created_month\")\n .aggregate(\"order_count\", \"total_sales\")\n)\n\n# Then nest months within years\nresult = (\n monthly_data\n .group_by(\"created_year\")\n .aggregate(\n year_order_count=lambda t: t.order_count.sum(),\n year_total_sales=lambda t: t.total_sales.sum(),\n nest={\"by_month\": lambda t: t.group_by([\"created_month\", \"order_count\", \"total_sales\"]).order_by(\"created_month\")}\n )\n .order_by(\"created_year\")\n)\n```\n\n\n\n\nEach year row contains a `by_month` array with all monthly subtotals for that year. The pattern is: aggregate at the finest level first, then nest at each parent level.\n\n\n## Year with Nested Status Subtotals\n\nAlternative breakdown: nest order status within each year:\n\n```query_year_with_status\nfrom ibis import _\n\n# First aggregate by year and status\nstatus_data = (\n order_items\n .group_by(\"created_year\", \"status\")\n .aggregate(\"order_count\", \"total_sales\", \"avg_price\")\n)\n\n# Then nest status within years\nresult = (\n status_data\n .group_by(\"created_year\")\n .aggregate(\n year_order_count=lambda t: t.order_count.sum(),\n year_total_sales=lambda t: t.total_sales.sum(),\n nest={\"by_status\": lambda t: t.group_by([\"status\", \"order_count\", \"total_sales\", \"avg_price\"]).order_by(_.total_sales.desc())}\n )\n .order_by(\"created_year\")\n)\n```\n\n\n\n## Multi-Level Nesting: Year > Month > Status\n\nCreate three-level hierarchy with nested subtotals:\n\n```query_multi_level\nfrom ibis import _\n\n# First aggregate at the finest level: year, month, status\ndetailed_data = (\n order_items\n .group_by(\"created_year\", \"created_month\", \"status\")\n .aggregate(\"order_count\", \"total_sales\")\n)\n\n# Second level: nest status within month\nmonthly_with_status = (\n detailed_data\n .group_by(\"created_year\", \"created_month\")\n .aggregate(\n month_order_count=lambda t: t.order_count.sum(),\n month_total_sales=lambda t: t.total_sales.sum(),\n nest={\"by_status\": lambda t: t.group_by([\"status\", \"order_count\", \"total_sales\"])}\n )\n)\n\n# Top level: nest months within year\nresult = (\n monthly_with_status\n .group_by(\"created_year\")\n .aggregate(\n year_order_count=lambda t: t.month_order_count.sum(),\n year_total_sales=lambda t: t.month_total_sales.sum(),\n nest={\"by_month\": lambda t: t.group_by([\"created_month\", \"month_order_count\", \"month_total_sales\", \"by_status\"]).order_by(\"created_month\")}\n )\n .order_by(\"created_year\")\n .limit(3)\n)\n```\n\n\n\n## Use Cases\n\n**Financial Reporting**: Create income statements with nested line items - show total revenue with product categories nested inside, each containing individual products.\n\n**Geographic Hierarchies**: Aggregate sales by region, with nested states, with nested cities, all in a single query result.\n\n**Time-Based Drill-Downs**: Show yearly summaries with monthly breakdowns nested inside, perfect for dashboard drill-down interactions.\n\n**Organizational Analysis**: Display department totals with nested team breakdowns, with nested individual employee details.\n\n## Key Takeaways\n\n- Use the `nest` parameter in `.aggregate()` to create hierarchical subtotals\n- Each parent row contains an array column with child-level breakdowns\n- Avoid complex SQL ROLLUP or self-join patterns\n- Nest multiple levels deep for complex hierarchies\n- Perfect for building tree views, expandable tables, and drill-down UIs\n\n## Next Steps\n\n- Learn about [Percentage of Total](/advanced/percentage-total) calculations\n- Explore [Advanced Nesting](/advanced/nesting) for more complex hierarchical patterns\n", + "queries": { + "setup_data": { + "code": "import ibis\nfrom ibis import _\nfrom boring_semantic_layer import to_semantic_table\n\n# Create synthetic order items data\norder_items_data = ibis.memtable({\n \"order_id\": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010,\n 1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020,\n 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030],\n \"sale_price\": [45.99, 89.50, 120.00, 34.99, 67.80, 99.99, 54.50, 78.99, 150.00, 42.00,\n 55.99, 72.50, 88.80, 110.00, 39.99, 95.00, 62.50, 81.99, 125.00, 48.50,\n 66.99, 92.00, 105.50, 73.99, 58.80, 118.00, 84.50, 69.99, 135.00, 51.50],\n \"status\": [\"shipped\", \"delivered\", \"shipped\", \"processing\", \"delivered\",\n \"shipped\", \"cancelled\", \"delivered\", \"shipped\", \"processing\",\n \"delivered\", \"shipped\", \"delivered\", \"processing\", \"shipped\",\n \"cancelled\", \"delivered\", \"shipped\", \"delivered\", \"processing\",\n \"shipped\", \"delivered\", \"shipped\", \"processing\", \"delivered\",\n \"shipped\", \"cancelled\", \"delivered\", \"shipped\", \"processing\"],\n \"created_year\": [2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022,\n 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023,\n 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024],\n \"created_month\": [1, 1, 2, 2, 3, 3, 4, 4, 5, 5,\n 1, 1, 2, 2, 3, 3, 4, 4, 5, 5,\n 1, 1, 2, 2, 3, 3, 4, 4, 5, 5]\n})\n\n# Create semantic table with measures\norder_items = to_semantic_table(\n order_items_data,\n name=\"order_items\",\n).with_measures(\n order_count=lambda t: t.count(),\n total_sales=lambda t: t.sale_price.sum(),\n avg_price=lambda t: t.sale_price.mean(),\n)", + "sql": "SELECT\n *\nFROM \"ibis_pandas_memtable_egm2ad5dqnfnraxz65qku7bj64\"", + "table": { + "columns": [ + "order_id", + "sale_price", + "status", + "created_year", + "created_month" + ], + "data": [ + [ + 1001, + 45.99, + "shipped", + 2022, + 1 + ], + [ + 1002, + 89.5, + "delivered", + 2022, + 1 + ], + [ + 1003, + 120.0, + "shipped", + 2022, + 2 + ], + [ + 1004, + 34.99, + "processing", + 2022, + 2 + ], + [ + 1005, + 67.8, + "delivered", + 2022, + 3 + ], + [ + 1006, + 99.99, + "shipped", + 2022, + 3 + ], + [ + 1007, + 54.5, + "cancelled", + 2022, + 4 + ], + [ + 1008, + 78.99, + "delivered", + 2022, + 4 + ], + [ + 1009, + 150.0, + "shipped", + 2022, + 5 + ], + [ + 1010, + 42.0, + "processing", + 2022, + 5 + ], + [ + 1011, + 55.99, + "delivered", + 2023, + 1 + ], + [ + 1012, + 72.5, + "shipped", + 2023, + 1 + ], + [ + 1013, + 88.8, + "delivered", + 2023, + 2 + ], + [ + 1014, + 110.0, + "processing", + 2023, + 2 + ], + [ + 1015, + 39.99, + "shipped", + 2023, + 3 + ], + [ + 1016, + 95.0, + "cancelled", + 2023, + 3 + ], + [ + 1017, + 62.5, + "delivered", + 2023, + 4 + ], + [ + 1018, + 81.99, + "shipped", + 2023, + 4 + ], + [ + 1019, + 125.0, + "delivered", + 2023, + 5 + ], + [ + 1020, + 48.5, + "processing", + 2023, + 5 + ], + [ + 1021, + 66.99, + "shipped", + 2024, + 1 + ], + [ + 1022, + 92.0, + "delivered", + 2024, + 1 + ], + [ + 1023, + 105.5, + "shipped", + 2024, + 2 + ], + [ + 1024, + 73.99, + "processing", + 2024, + 2 + ], + [ + 1025, + 58.8, + "delivered", + 2024, + 3 + ], + [ + 1026, + 118.0, + "shipped", + 2024, + 3 + ], + [ + 1027, + 84.5, + "cancelled", + 2024, + 4 + ], + [ + 1028, + 69.99, + "delivered", + 2024, + 4 + ], + [ + 1029, + 135.0, + "shipped", + 2024, + 5 + ], + [ + 1030, + 51.5, + "processing", + 2024, + 5 + ] + ] + } + }, + "query_year_with_months": { + "code": "from ibis import _\n\n# First aggregate by year and month to get monthly subtotals\nmonthly_data = (\n order_items\n .group_by(\"created_year\", \"created_month\")\n .aggregate(\"order_count\", \"total_sales\")\n)\n\n# Then nest months within years\nresult = (\n monthly_data\n .group_by(\"created_year\")\n .aggregate(\n year_order_count=lambda t: t.order_count.sum(),\n year_total_sales=lambda t: t.total_sales.sum(),\n nest={\"by_month\": lambda t: t.group_by([\"created_month\", \"order_count\", \"total_sales\"]).order_by(\"created_month\")}\n )\n .order_by(\"created_year\")\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t2\".\"created_year\",\n SUM(\"t2\".\"order_count\") AS \"year_order_count\",\n SUM(\"t2\".\"total_sales\") AS \"year_total_sales\",\n ARRAY_AGG(\n {'created_month': \"t2\".\"created_month\", 'order_count': \"t2\".\"order_count\", 'total_sales': \"t2\".\"total_sales\"}\n ) FILTER(WHERE\n {'created_month': \"t2\".\"created_month\", 'order_count': \"t2\".\"order_count\", 'total_sales': \"t2\".\"total_sales\"} IS NOT NULL) AS \"by_month\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t0\".\"created_year\",\n \"t0\".\"created_month\",\n COUNT(*) AS \"order_count\",\n SUM(\"t0\".\"sale_price\") AS \"total_sales\"\n FROM \"ibis_pandas_memtable_egm2ad5dqnfnraxz65qku7bj64\" AS \"t0\"\n GROUP BY\n 1,\n 2\n ) AS \"t1\"\n ) AS \"t2\"\n GROUP BY\n 1\n) AS \"t3\"\nORDER BY\n \"t3\".\"created_year\" ASC", + "table": { + "columns": [ + "created_year", + "year_order_count", + "year_total_sales", + "by_month" + ], + "data": [ + [ + 2022, + 10, + 783.76, + [ + { + "created_month": 5, + "order_count": 2, + "total_sales": 192.0 + }, + { + "created_month": 1, + "order_count": 2, + "total_sales": 135.49 + }, + { + "created_month": 3, + "order_count": 2, + "total_sales": 167.79 + }, + { + "created_month": 2, + "order_count": 2, + "total_sales": 154.99 + }, + { + "created_month": 4, + "order_count": 2, + "total_sales": 133.49 + } + ] + ], + [ + 2023, + 10, + 780.27, + [ + { + "created_month": 4, + "order_count": 2, + "total_sales": 144.49 + }, + { + "created_month": 5, + "order_count": 2, + "total_sales": 173.5 + }, + { + "created_month": 2, + "order_count": 2, + "total_sales": 198.8 + }, + { + "created_month": 3, + "order_count": 2, + "total_sales": 134.99 + }, + { + "created_month": 1, + "order_count": 2, + "total_sales": 128.49 + } + ] + ], + [ + 2024, + 10, + 856.27, + [ + { + "created_month": 3, + "order_count": 2, + "total_sales": 176.8 + }, + { + "created_month": 1, + "order_count": 2, + "total_sales": 158.99 + }, + { + "created_month": 2, + "order_count": 2, + "total_sales": 179.49 + }, + { + "created_month": 5, + "order_count": 2, + "total_sales": 186.5 + }, + { + "created_month": 4, + "order_count": 2, + "total_sales": 154.49 + } + ] + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-b6d632ce41808c89c947d44a17f65d59" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "created_year", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "created_year", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "year_order_count", + "year_total_sales", + "by_month" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-b6d632ce41808c89c947d44a17f65d59": [ + { + "created_year": 2023, + "year_order_count": 10, + "year_total_sales": 780.27, + "by_month": [ + { + "created_month": 3, + "order_count": 2, + "total_sales": 134.99 + }, + { + "created_month": 2, + "order_count": 2, + "total_sales": 198.8 + }, + { + "created_month": 1, + "order_count": 2, + "total_sales": 128.49 + }, + { + "created_month": 4, + "order_count": 2, + "total_sales": 144.49 + }, + { + "created_month": 5, + "order_count": 2, + "total_sales": 173.5 + } + ] + }, + { + "created_year": 2022, + "year_order_count": 10, + "year_total_sales": 783.76, + "by_month": [ + { + "created_month": 3, + "order_count": 2, + "total_sales": 167.79 + }, + { + "created_month": 4, + "order_count": 2, + "total_sales": 133.49 + }, + { + "created_month": 1, + "order_count": 2, + "total_sales": 135.49 + }, + { + "created_month": 2, + "order_count": 2, + "total_sales": 154.99 + }, + { + "created_month": 5, + "order_count": 2, + "total_sales": 192.0 + } + ] + }, + { + "created_year": 2024, + "year_order_count": 10, + "year_total_sales": 856.27, + "by_month": [ + { + "created_month": 5, + "order_count": 2, + "total_sales": 186.5 + }, + { + "created_month": 4, + "order_count": 2, + "total_sales": 154.49 + }, + { + "created_month": 1, + "order_count": 2, + "total_sales": 158.99 + }, + { + "created_month": 2, + "order_count": 2, + "total_sales": 179.49 + }, + { + "created_month": 3, + "order_count": 2, + "total_sales": 176.8 + } + ] + } + ] + } + } + } + }, + "query_year_with_status": { + "code": "from ibis import _\n\n# First aggregate by year and status\nstatus_data = (\n order_items\n .group_by(\"created_year\", \"status\")\n .aggregate(\"order_count\", \"total_sales\", \"avg_price\")\n)\n\n# Then nest status within years\nresult = (\n status_data\n .group_by(\"created_year\")\n .aggregate(\n year_order_count=lambda t: t.order_count.sum(),\n year_total_sales=lambda t: t.total_sales.sum(),\n nest={\"by_status\": lambda t: t.group_by([\"status\", \"order_count\", \"total_sales\", \"avg_price\"]).order_by(_.total_sales.desc())}\n )\n .order_by(\"created_year\")\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t2\".\"created_year\",\n SUM(\"t2\".\"order_count\") AS \"year_order_count\",\n SUM(\"t2\".\"total_sales\") AS \"year_total_sales\",\n ARRAY_AGG(\n {'status': \"t2\".\"status\", 'order_count': \"t2\".\"order_count\", 'total_sales': \"t2\".\"total_sales\", 'avg_price': \"t2\".\"avg_price\"}\n ) FILTER(WHERE\n {'status': \"t2\".\"status\", 'order_count': \"t2\".\"order_count\", 'total_sales': \"t2\".\"total_sales\", 'avg_price': \"t2\".\"avg_price\"} IS NOT NULL) AS \"by_status\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t0\".\"created_year\",\n \"t0\".\"status\",\n COUNT(*) AS \"order_count\",\n SUM(\"t0\".\"sale_price\") AS \"total_sales\",\n AVG(\"t0\".\"sale_price\") AS \"avg_price\"\n FROM \"ibis_pandas_memtable_egm2ad5dqnfnraxz65qku7bj64\" AS \"t0\"\n GROUP BY\n 1,\n 2\n ) AS \"t1\"\n ) AS \"t2\"\n GROUP BY\n 1\n) AS \"t3\"\nORDER BY\n \"t3\".\"created_year\" ASC", + "table": { + "columns": [ + "created_year", + "year_order_count", + "year_total_sales", + "by_status" + ], + "data": [ + [ + 2022, + 10, + 783.76, + [ + { + "status": "shipped", + "order_count": 4, + "total_sales": 415.98, + "avg_price": 103.995 + }, + { + "status": "cancelled", + "order_count": 1, + "total_sales": 54.5, + "avg_price": 54.5 + }, + { + "status": "processing", + "order_count": 2, + "total_sales": 76.99000000000001, + "avg_price": 38.495000000000005 + }, + { + "status": "delivered", + "order_count": 3, + "total_sales": 236.29000000000002, + "avg_price": 78.76333333333334 + } + ] + ], + [ + 2023, + 10, + 780.27, + [ + { + "status": "cancelled", + "order_count": 1, + "total_sales": 95.0, + "avg_price": 95.0 + }, + { + "status": "processing", + "order_count": 2, + "total_sales": 158.5, + "avg_price": 79.25 + }, + { + "status": "delivered", + "order_count": 4, + "total_sales": 332.28999999999996, + "avg_price": 83.07249999999999 + }, + { + "status": "shipped", + "order_count": 3, + "total_sales": 194.48000000000002, + "avg_price": 64.82666666666667 + } + ] + ], + [ + 2024, + 10, + 856.27, + [ + { + "status": "delivered", + "order_count": 3, + "total_sales": 220.79000000000002, + "avg_price": 73.59666666666668 + }, + { + "status": "shipped", + "order_count": 4, + "total_sales": 425.49, + "avg_price": 106.3725 + }, + { + "status": "processing", + "order_count": 2, + "total_sales": 125.49, + "avg_price": 62.745 + }, + { + "status": "cancelled", + "order_count": 1, + "total_sales": 84.5, + "avg_price": 84.5 + } + ] + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-2725bfeefba67cda937ec1bdbb2a72b2" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "created_year", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "created_year", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "year_order_count", + "year_total_sales", + "by_status" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-2725bfeefba67cda937ec1bdbb2a72b2": [ + { + "created_year": 2023, + "year_order_count": 10, + "year_total_sales": 780.27, + "by_status": [ + { + "status": "delivered", + "order_count": 4, + "total_sales": 332.28999999999996, + "avg_price": 83.07249999999999 + }, + { + "status": "processing", + "order_count": 2, + "total_sales": 158.5, + "avg_price": 79.25 + }, + { + "status": "cancelled", + "order_count": 1, + "total_sales": 95.0, + "avg_price": 95.0 + }, + { + "status": "shipped", + "order_count": 3, + "total_sales": 194.48000000000002, + "avg_price": 64.82666666666667 + } + ] + }, + { + "created_year": 2024, + "year_order_count": 10, + "year_total_sales": 856.27, + "by_status": [ + { + "status": "cancelled", + "order_count": 1, + "total_sales": 84.5, + "avg_price": 84.5 + }, + { + "status": "processing", + "order_count": 2, + "total_sales": 125.49, + "avg_price": 62.745 + }, + { + "status": "shipped", + "order_count": 4, + "total_sales": 425.49, + "avg_price": 106.3725 + }, + { + "status": "delivered", + "order_count": 3, + "total_sales": 220.79000000000002, + "avg_price": 73.59666666666668 + } + ] + }, + { + "created_year": 2022, + "year_order_count": 10, + "year_total_sales": 783.76, + "by_status": [ + { + "status": "delivered", + "order_count": 3, + "total_sales": 236.29000000000002, + "avg_price": 78.76333333333334 + }, + { + "status": "shipped", + "order_count": 4, + "total_sales": 415.98, + "avg_price": 103.995 + }, + { + "status": "processing", + "order_count": 2, + "total_sales": 76.99000000000001, + "avg_price": 38.495000000000005 + }, + { + "status": "cancelled", + "order_count": 1, + "total_sales": 54.5, + "avg_price": 54.5 + } + ] + } + ] + } + } + } + }, + "query_multi_level": { + "code": "from ibis import _\n\n# First aggregate at the finest level: year, month, status\ndetailed_data = (\n order_items\n .group_by(\"created_year\", \"created_month\", \"status\")\n .aggregate(\"order_count\", \"total_sales\")\n)\n\n# Second level: nest status within month\nmonthly_with_status = (\n detailed_data\n .group_by(\"created_year\", \"created_month\")\n .aggregate(\n month_order_count=lambda t: t.order_count.sum(),\n month_total_sales=lambda t: t.total_sales.sum(),\n nest={\"by_status\": lambda t: t.group_by([\"status\", \"order_count\", \"total_sales\"])}\n )\n)\n\n# Top level: nest months within year\nresult = (\n monthly_with_status\n .group_by(\"created_year\")\n .aggregate(\n year_order_count=lambda t: t.month_order_count.sum(),\n year_total_sales=lambda t: t.month_total_sales.sum(),\n nest={\"by_month\": lambda t: t.group_by([\"created_month\", \"month_order_count\", \"month_total_sales\", \"by_status\"]).order_by(\"created_month\")}\n )\n .order_by(\"created_year\")\n .limit(3)\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t4\".\"created_year\",\n SUM(\"t4\".\"month_order_count\") AS \"year_order_count\",\n SUM(\"t4\".\"month_total_sales\") AS \"year_total_sales\",\n ARRAY_AGG(\n {'created_month': \"t4\".\"created_month\", 'month_order_count': \"t4\".\"month_order_count\", 'month_total_sales': \"t4\".\"month_total_sales\", 'by_status': \"t4\".\"by_status\"}\n ) FILTER(WHERE\n {'created_month': \"t4\".\"created_month\", 'month_order_count': \"t4\".\"month_order_count\", 'month_total_sales': \"t4\".\"month_total_sales\", 'by_status': \"t4\".\"by_status\"} IS NOT NULL) AS \"by_month\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t2\".\"created_year\",\n \"t2\".\"created_month\",\n SUM(\"t2\".\"order_count\") AS \"month_order_count\",\n SUM(\"t2\".\"total_sales\") AS \"month_total_sales\",\n ARRAY_AGG(\n {'status': \"t2\".\"status\", 'order_count': \"t2\".\"order_count\", 'total_sales': \"t2\".\"total_sales\"}\n ) FILTER(WHERE\n {'status': \"t2\".\"status\", 'order_count': \"t2\".\"order_count\", 'total_sales': \"t2\".\"total_sales\"} IS NOT NULL) AS \"by_status\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t0\".\"created_year\",\n \"t0\".\"created_month\",\n \"t0\".\"status\",\n COUNT(*) AS \"order_count\",\n SUM(\"t0\".\"sale_price\") AS \"total_sales\"\n FROM \"ibis_pandas_memtable_egm2ad5dqnfnraxz65qku7bj64\" AS \"t0\"\n GROUP BY\n 1,\n 2,\n 3\n ) AS \"t1\"\n ) AS \"t2\"\n GROUP BY\n 1,\n 2\n ) AS \"t3\"\n ) AS \"t4\"\n GROUP BY\n 1\n) AS \"t5\"\nORDER BY\n \"t5\".\"created_year\" ASC\nLIMIT 3", + "table": { + "columns": [ + "created_year", + "year_order_count", + "year_total_sales", + "by_month" + ], + "data": [ + [ + 2022, + 10, + 783.76, + [ + { + "created_month": 1, + "month_order_count": 2.0, + "month_total_sales": 135.49, + "by_status": [ + { + "status": "delivered", + "order_count": 1, + "total_sales": 89.5 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 45.99 + } + ] + }, + { + "created_month": 2, + "month_order_count": 2.0, + "month_total_sales": 154.99, + "by_status": [ + { + "status": "processing", + "order_count": 1, + "total_sales": 34.99 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 120.0 + } + ] + }, + { + "created_month": 3, + "month_order_count": 2.0, + "month_total_sales": 167.79, + "by_status": [ + { + "status": "delivered", + "order_count": 1, + "total_sales": 67.8 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 99.99 + } + ] + }, + { + "created_month": 4, + "month_order_count": 2.0, + "month_total_sales": 133.49, + "by_status": [ + { + "status": "cancelled", + "order_count": 1, + "total_sales": 54.5 + }, + { + "status": "delivered", + "order_count": 1, + "total_sales": 78.99 + } + ] + }, + { + "created_month": 5, + "month_order_count": 2.0, + "month_total_sales": 192.0, + "by_status": [ + { + "status": "shipped", + "order_count": 1, + "total_sales": 150.0 + }, + { + "status": "processing", + "order_count": 1, + "total_sales": 42.0 + } + ] + } + ] + ], + [ + 2023, + 10, + 780.27, + [ + { + "created_month": 3, + "month_order_count": 2.0, + "month_total_sales": 134.99, + "by_status": [ + { + "status": "cancelled", + "order_count": 1, + "total_sales": 95.0 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 39.99 + } + ] + }, + { + "created_month": 5, + "month_order_count": 2.0, + "month_total_sales": 173.5, + "by_status": [ + { + "status": "delivered", + "order_count": 1, + "total_sales": 125.0 + }, + { + "status": "processing", + "order_count": 1, + "total_sales": 48.5 + } + ] + }, + { + "created_month": 2, + "month_order_count": 2.0, + "month_total_sales": 198.8, + "by_status": [ + { + "status": "processing", + "order_count": 1, + "total_sales": 110.0 + }, + { + "status": "delivered", + "order_count": 1, + "total_sales": 88.8 + } + ] + }, + { + "created_month": 4, + "month_order_count": 2.0, + "month_total_sales": 144.49, + "by_status": [ + { + "status": "delivered", + "order_count": 1, + "total_sales": 62.5 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 81.99 + } + ] + }, + { + "created_month": 1, + "month_order_count": 2.0, + "month_total_sales": 128.49, + "by_status": [ + { + "status": "delivered", + "order_count": 1, + "total_sales": 55.99 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 72.5 + } + ] + } + ] + ], + [ + 2024, + 10, + 856.27, + [ + { + "created_month": 4, + "month_order_count": 2.0, + "month_total_sales": 154.49, + "by_status": [ + { + "status": "cancelled", + "order_count": 1, + "total_sales": 84.5 + }, + { + "status": "delivered", + "order_count": 1, + "total_sales": 69.99 + } + ] + }, + { + "created_month": 1, + "month_order_count": 2.0, + "month_total_sales": 158.99, + "by_status": [ + { + "status": "delivered", + "order_count": 1, + "total_sales": 92.0 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 66.99 + } + ] + }, + { + "created_month": 5, + "month_order_count": 2.0, + "month_total_sales": 186.5, + "by_status": [ + { + "status": "shipped", + "order_count": 1, + "total_sales": 135.0 + }, + { + "status": "processing", + "order_count": 1, + "total_sales": 51.5 + } + ] + }, + { + "created_month": 2, + "month_order_count": 2.0, + "month_total_sales": 179.49, + "by_status": [ + { + "status": "processing", + "order_count": 1, + "total_sales": 73.99 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 105.5 + } + ] + }, + { + "created_month": 3, + "month_order_count": 2.0, + "month_total_sales": 176.8, + "by_status": [ + { + "status": "delivered", + "order_count": 1, + "total_sales": 58.8 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 118.0 + } + ] + } + ] + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-e7af64c455089e4e11fac464ecb95b10" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "created_year", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "created_year", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "year_order_count", + "year_total_sales", + "by_month" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-e7af64c455089e4e11fac464ecb95b10": [ + { + "created_year": 2022, + "year_order_count": 10, + "year_total_sales": 783.76, + "by_month": [ + { + "created_month": 5, + "month_order_count": 2.0, + "month_total_sales": 192.0, + "by_status": [ + { + "status": "processing", + "order_count": 1, + "total_sales": 42.0 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 150.0 + } + ] + }, + { + "created_month": 2, + "month_order_count": 2.0, + "month_total_sales": 154.99, + "by_status": [ + { + "status": "processing", + "order_count": 1, + "total_sales": 34.99 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 120.0 + } + ] + }, + { + "created_month": 3, + "month_order_count": 2.0, + "month_total_sales": 167.79, + "by_status": [ + { + "status": "shipped", + "order_count": 1, + "total_sales": 99.99 + }, + { + "status": "delivered", + "order_count": 1, + "total_sales": 67.8 + } + ] + }, + { + "created_month": 4, + "month_order_count": 2.0, + "month_total_sales": 133.49, + "by_status": [ + { + "status": "delivered", + "order_count": 1, + "total_sales": 78.99 + }, + { + "status": "cancelled", + "order_count": 1, + "total_sales": 54.5 + } + ] + }, + { + "created_month": 1, + "month_order_count": 2.0, + "month_total_sales": 135.49, + "by_status": [ + { + "status": "delivered", + "order_count": 1, + "total_sales": 89.5 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 45.99 + } + ] + } + ] + }, + { + "created_year": 2024, + "year_order_count": 10, + "year_total_sales": 856.27, + "by_month": [ + { + "created_month": 3, + "month_order_count": 2.0, + "month_total_sales": 176.8, + "by_status": [ + { + "status": "delivered", + "order_count": 1, + "total_sales": 58.8 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 118.0 + } + ] + }, + { + "created_month": 1, + "month_order_count": 2.0, + "month_total_sales": 158.99, + "by_status": [ + { + "status": "shipped", + "order_count": 1, + "total_sales": 66.99 + }, + { + "status": "delivered", + "order_count": 1, + "total_sales": 92.0 + } + ] + }, + { + "created_month": 4, + "month_order_count": 2.0, + "month_total_sales": 154.49, + "by_status": [ + { + "status": "cancelled", + "order_count": 1, + "total_sales": 84.5 + }, + { + "status": "delivered", + "order_count": 1, + "total_sales": 69.99 + } + ] + }, + { + "created_month": 5, + "month_order_count": 2.0, + "month_total_sales": 186.5, + "by_status": [ + { + "status": "shipped", + "order_count": 1, + "total_sales": 135.0 + }, + { + "status": "processing", + "order_count": 1, + "total_sales": 51.5 + } + ] + }, + { + "created_month": 2, + "month_order_count": 2.0, + "month_total_sales": 179.49, + "by_status": [ + { + "status": "processing", + "order_count": 1, + "total_sales": 73.99 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 105.5 + } + ] + } + ] + }, + { + "created_year": 2023, + "year_order_count": 10, + "year_total_sales": 780.27, + "by_month": [ + { + "created_month": 2, + "month_order_count": 2.0, + "month_total_sales": 198.8, + "by_status": [ + { + "status": "processing", + "order_count": 1, + "total_sales": 110.0 + }, + { + "status": "delivered", + "order_count": 1, + "total_sales": 88.8 + } + ] + }, + { + "created_month": 5, + "month_order_count": 2.0, + "month_total_sales": 173.5, + "by_status": [ + { + "status": "delivered", + "order_count": 1, + "total_sales": 125.0 + }, + { + "status": "processing", + "order_count": 1, + "total_sales": 48.5 + } + ] + }, + { + "created_month": 3, + "month_order_count": 2.0, + "month_total_sales": 134.99, + "by_status": [ + { + "status": "shipped", + "order_count": 1, + "total_sales": 39.99 + }, + { + "status": "cancelled", + "order_count": 1, + "total_sales": 95.0 + } + ] + }, + { + "created_month": 1, + "month_order_count": 2.0, + "month_total_sales": 128.49, + "by_status": [ + { + "status": "delivered", + "order_count": 1, + "total_sales": 55.99 + }, + { + "status": "shipped", + "order_count": 1, + "total_sales": 72.5 + } + ] + }, + { + "created_month": 4, + "month_order_count": 2.0, + "month_total_sales": 144.49, + "by_status": [ + { + "status": "shipped", + "order_count": 1, + "total_sales": 81.99 + }, + { + "status": "delivered", + "order_count": 1, + "total_sales": 62.5 + } + ] + } + ] + } + ] + } + } + } + } + }, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/percentage-total.json b/docs/public/bsl-data/percentage-total.json new file mode 100644 index 00000000..33ea8e61 --- /dev/null +++ b/docs/public/bsl-data/percentage-total.json @@ -0,0 +1,605 @@ +{ + "markdown": "# Percentage of Total\n\nCalculate percentages relative to total values across different dimensions. Use this pattern when you need to understand market share, contribution ratios, or what proportion each segment represents of the whole.\n\n## Overview\n\nThe percentage of total pattern allows you to:\n\n- Define percentage measures using the `.all()` method\n- Calculate individual segment values as percentages of the grand total\n- Maintain dimensional breakdowns while computing percentage contributions\n- Support multiple aggregation functions (sum, count, average)\n\n## Setup\n\nLet's use the flights dataset with carrier information to demonstrate market share calculations:\n\n```setup_data\nimport ibis\nfrom ibis import _\nfrom boring_semantic_layer import to_semantic_table\n\n# Create synthetic flights data with carrier information\nflights_data = ibis.memtable({\n \"flight_id\": list(range(1, 51)),\n \"carrier\": [\"AA\", \"UA\", \"DL\", \"WN\", \"B6\"] * 10,\n \"nickname\": [\"American Airlines\", \"United Airlines\", \"Delta Air Lines\",\n \"Southwest Airlines\", \"JetBlue Airways\"] * 10,\n \"origin\": [\"JFK\", \"LAX\", \"ORD\", \"ATL\", \"DFW\"] * 10,\n \"distance\": [2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383,\n 2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383,\n 2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383,\n 2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383,\n 2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383]\n})\n\n# Create semantic table with measures including percentage calculations\nflights = (\n to_semantic_table(flights_data, name=\"flights\")\n .with_measures(\n flight_count=lambda t: t.count(),\n total_distance=lambda t: t.distance.sum(),\n )\n .with_measures(\n market_share=lambda t: t.flight_count / t.all(t.flight_count) * 100,\n distance_share=lambda t: t.total_distance / t.all(t.total_distance) * 100,\n )\n)\n```\n\n\n\n\nThe `.all()` method calculates the grand total across all groups, allowing you to define percentage measures directly in the semantic table. This is more elegant than using window functions in post-processing.\n\n\n## Market Share by Carrier\n\nCalculate each carrier's percentage of total flights:\n\n```query_market_share\nfrom ibis import _\n\nresult = (\n flights.group_by(\"nickname\")\n .aggregate(\"flight_count\", \"market_share\")\n .order_by(_.market_share.desc())\n .limit(10)\n)\n```\n\n\n\n## Market Share by Origin and Carrier\n\nCalculate market share broken down by both origin airport and carrier:\n\n```query_market_share_by_origin\nfrom ibis import _\n\nresult = (\n flights.group_by(\"origin\", \"nickname\")\n .aggregate(\"flight_count\", \"market_share\")\n .order_by(_.market_share.desc())\n .limit(15)\n)\n```\n\n\n\n## Use Cases\n\n**Market Share Analysis**: Calculate each carrier's, product's, or region's share of total volume.\n\n**Traffic Distribution**: Determine what percentage of total website visits or conversions come from each source.\n\n**Resource Allocation**: Understand how resources (budget, time, capacity) are distributed as percentages of the total.\n\n## Key Takeaways\n\n- Define percentage measures using `.all()` to reference the grand total\n- The `.all(measure)` method calculates the total across all groups\n- Percentage measures work seamlessly across different dimensional breakdowns\n- More elegant than post-processing with window functions\n\n## Next Steps\n\n- Learn about [Nested Subtotals](/advanced/nested-subtotals) for hierarchical aggregations\n- Explore [Bucketing](/advanced/bucketing) to group continuous values\n", + "queries": { + "setup_data": { + "code": "import ibis\nfrom ibis import _\nfrom boring_semantic_layer import to_semantic_table\n\n# Create synthetic flights data with carrier information\nflights_data = ibis.memtable({\n \"flight_id\": list(range(1, 51)),\n \"carrier\": [\"AA\", \"UA\", \"DL\", \"WN\", \"B6\"] * 10,\n \"nickname\": [\"American Airlines\", \"United Airlines\", \"Delta Air Lines\",\n \"Southwest Airlines\", \"JetBlue Airways\"] * 10,\n \"origin\": [\"JFK\", \"LAX\", \"ORD\", \"ATL\", \"DFW\"] * 10,\n \"distance\": [2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383,\n 2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383,\n 2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383,\n 2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383,\n 2475, 1745, 733, 946, 1383, 2475, 1745, 733, 946, 1383]\n})\n\n# Create semantic table with measures including percentage calculations\nflights = (\n to_semantic_table(flights_data, name=\"flights\")\n .with_measures(\n flight_count=lambda t: t.count(),\n total_distance=lambda t: t.distance.sum(),\n )\n .with_measures(\n market_share=lambda t: t.flight_count / t.all(t.flight_count) * 100,\n distance_share=lambda t: t.total_distance / t.all(t.total_distance) * 100,\n )\n)", + "sql": "SELECT\n *\nFROM \"ibis_pandas_memtable_etbbvdl3dzbvvhk6hybc2lfpiu\"", + "table": { + "columns": [ + "flight_id", + "carrier", + "nickname", + "origin", + "distance" + ], + "data": [ + [ + 1, + "AA", + "American Airlines", + "JFK", + 2475 + ], + [ + 2, + "UA", + "United Airlines", + "LAX", + 1745 + ], + [ + 3, + "DL", + "Delta Air Lines", + "ORD", + 733 + ], + [ + 4, + "WN", + "Southwest Airlines", + "ATL", + 946 + ], + [ + 5, + "B6", + "JetBlue Airways", + "DFW", + 1383 + ], + [ + 6, + "AA", + "American Airlines", + "JFK", + 2475 + ], + [ + 7, + "UA", + "United Airlines", + "LAX", + 1745 + ], + [ + 8, + "DL", + "Delta Air Lines", + "ORD", + 733 + ], + [ + 9, + "WN", + "Southwest Airlines", + "ATL", + 946 + ], + [ + 10, + "B6", + "JetBlue Airways", + "DFW", + 1383 + ], + [ + 11, + "AA", + "American Airlines", + "JFK", + 2475 + ], + [ + 12, + "UA", + "United Airlines", + "LAX", + 1745 + ], + [ + 13, + "DL", + "Delta Air Lines", + "ORD", + 733 + ], + [ + 14, + "WN", + "Southwest Airlines", + "ATL", + 946 + ], + [ + 15, + "B6", + "JetBlue Airways", + "DFW", + 1383 + ], + [ + 16, + "AA", + "American Airlines", + "JFK", + 2475 + ], + [ + 17, + "UA", + "United Airlines", + "LAX", + 1745 + ], + [ + 18, + "DL", + "Delta Air Lines", + "ORD", + 733 + ], + [ + 19, + "WN", + "Southwest Airlines", + "ATL", + 946 + ], + [ + 20, + "B6", + "JetBlue Airways", + "DFW", + 1383 + ], + [ + 21, + "AA", + "American Airlines", + "JFK", + 2475 + ], + [ + 22, + "UA", + "United Airlines", + "LAX", + 1745 + ], + [ + 23, + "DL", + "Delta Air Lines", + "ORD", + 733 + ], + [ + 24, + "WN", + "Southwest Airlines", + "ATL", + 946 + ], + [ + 25, + "B6", + "JetBlue Airways", + "DFW", + 1383 + ], + [ + 26, + "AA", + "American Airlines", + "JFK", + 2475 + ], + [ + 27, + "UA", + "United Airlines", + "LAX", + 1745 + ], + [ + 28, + "DL", + "Delta Air Lines", + "ORD", + 733 + ], + [ + 29, + "WN", + "Southwest Airlines", + "ATL", + 946 + ], + [ + 30, + "B6", + "JetBlue Airways", + "DFW", + 1383 + ], + [ + 31, + "AA", + "American Airlines", + "JFK", + 2475 + ], + [ + 32, + "UA", + "United Airlines", + "LAX", + 1745 + ], + [ + 33, + "DL", + "Delta Air Lines", + "ORD", + 733 + ], + [ + 34, + "WN", + "Southwest Airlines", + "ATL", + 946 + ], + [ + 35, + "B6", + "JetBlue Airways", + "DFW", + 1383 + ], + [ + 36, + "AA", + "American Airlines", + "JFK", + 2475 + ], + [ + 37, + "UA", + "United Airlines", + "LAX", + 1745 + ], + [ + 38, + "DL", + "Delta Air Lines", + "ORD", + 733 + ], + [ + 39, + "WN", + "Southwest Airlines", + "ATL", + 946 + ], + [ + 40, + "B6", + "JetBlue Airways", + "DFW", + 1383 + ], + [ + 41, + "AA", + "American Airlines", + "JFK", + 2475 + ], + [ + 42, + "UA", + "United Airlines", + "LAX", + 1745 + ], + [ + 43, + "DL", + "Delta Air Lines", + "ORD", + 733 + ], + [ + 44, + "WN", + "Southwest Airlines", + "ATL", + 946 + ], + [ + 45, + "B6", + "JetBlue Airways", + "DFW", + 1383 + ], + [ + 46, + "AA", + "American Airlines", + "JFK", + 2475 + ], + [ + 47, + "UA", + "United Airlines", + "LAX", + 1745 + ], + [ + 48, + "DL", + "Delta Air Lines", + "ORD", + 733 + ], + [ + 49, + "WN", + "Southwest Airlines", + "ATL", + 946 + ], + [ + 50, + "B6", + "JetBlue Airways", + "DFW", + 1383 + ] + ] + } + }, + "query_market_share": { + "code": "from ibis import _\n\nresult = (\n flights.group_by(\"nickname\")\n .aggregate(\"flight_count\", \"market_share\")\n .order_by(_.market_share.desc())\n .limit(10)\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t5\".\"nickname\",\n \"t5\".\"flight_count\",\n (\n CAST(\"t5\".\"flight_count\" AS DOUBLE) / CAST(\"t5\".\"flight_count_right\" AS DOUBLE)\n ) * 100 AS \"market_share\"\n FROM (\n SELECT\n \"t3\".\"nickname\",\n \"t3\".\"flight_count\",\n \"t4\".\"flight_count\" AS \"flight_count_right\"\n FROM (\n SELECT\n \"t0\".\"nickname\",\n COUNT(*) AS \"flight_count\"\n FROM \"ibis_pandas_memtable_etbbvdl3dzbvvhk6hybc2lfpiu\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t3\"\n CROSS JOIN (\n SELECT\n COUNT(*) AS \"flight_count\"\n FROM \"ibis_pandas_memtable_etbbvdl3dzbvvhk6hybc2lfpiu\" AS \"t0\"\n ) AS \"t4\"\n ) AS \"t5\"\n) AS \"t6\"\nORDER BY\n \"t6\".\"market_share\" DESC\nLIMIT 10", + "table": { + "columns": [ + "nickname", + "flight_count", + "market_share" + ], + "data": [ + [ + "JetBlue Airways", + 10, + 20.0 + ], + [ + "United Airlines", + 10, + 20.0 + ], + [ + "Southwest Airlines", + 10, + 20.0 + ], + [ + "Delta Air Lines", + 10, + 20.0 + ], + [ + "American Airlines", + 10, + 20.0 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-cdcb91c5f5878b3de3fd8af295d656dc" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "nickname", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "nickname", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "market_share" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-cdcb91c5f5878b3de3fd8af295d656dc": [ + { + "nickname": "Delta Air Lines", + "flight_count": 10, + "market_share": 20.0 + }, + { + "nickname": "Southwest Airlines", + "flight_count": 10, + "market_share": 20.0 + }, + { + "nickname": "United Airlines", + "flight_count": 10, + "market_share": 20.0 + }, + { + "nickname": "American Airlines", + "flight_count": 10, + "market_share": 20.0 + }, + { + "nickname": "JetBlue Airways", + "flight_count": 10, + "market_share": 20.0 + } + ] + } + } + } + }, + "query_market_share_by_origin": { + "code": "from ibis import _\n\nresult = (\n flights.group_by(\"origin\", \"nickname\")\n .aggregate(\"flight_count\", \"market_share\")\n .order_by(_.market_share.desc())\n .limit(15)\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t5\".\"origin\",\n \"t5\".\"nickname\",\n \"t5\".\"flight_count\",\n (\n CAST(\"t5\".\"flight_count\" AS DOUBLE) / CAST(\"t5\".\"flight_count_right\" AS DOUBLE)\n ) * 100 AS \"market_share\"\n FROM (\n SELECT\n \"t3\".\"origin\",\n \"t3\".\"nickname\",\n \"t3\".\"flight_count\",\n \"t4\".\"flight_count\" AS \"flight_count_right\"\n FROM (\n SELECT\n \"t0\".\"origin\",\n \"t0\".\"nickname\",\n COUNT(*) AS \"flight_count\"\n FROM \"ibis_pandas_memtable_etbbvdl3dzbvvhk6hybc2lfpiu\" AS \"t0\"\n GROUP BY\n 1,\n 2\n ) AS \"t3\"\n CROSS JOIN (\n SELECT\n COUNT(*) AS \"flight_count\"\n FROM \"ibis_pandas_memtable_etbbvdl3dzbvvhk6hybc2lfpiu\" AS \"t0\"\n ) AS \"t4\"\n ) AS \"t5\"\n) AS \"t6\"\nORDER BY\n \"t6\".\"market_share\" DESC\nLIMIT 15", + "table": { + "columns": [ + "origin", + "nickname", + "flight_count", + "market_share" + ], + "data": [ + [ + "LAX", + "United Airlines", + 10, + 20.0 + ], + [ + "DFW", + "JetBlue Airways", + 10, + 20.0 + ], + [ + "ATL", + "Southwest Airlines", + 10, + 20.0 + ], + [ + "ORD", + "Delta Air Lines", + 10, + 20.0 + ], + [ + "JFK", + "American Airlines", + 10, + 20.0 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-79e24253b3740e2b0f27ec994c7c1c03" + }, + "mark": { + "type": "text" + }, + "encoding": { + "text": { + "value": "Complex query - consider custom visualization" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-79e24253b3740e2b0f27ec994c7c1c03": [ + { + "origin": "JFK", + "nickname": "American Airlines", + "flight_count": 10, + "market_share": 20.0 + }, + { + "origin": "LAX", + "nickname": "United Airlines", + "flight_count": 10, + "market_share": 20.0 + }, + { + "origin": "DFW", + "nickname": "JetBlue Airways", + "flight_count": 10, + "market_share": 20.0 + }, + { + "origin": "ORD", + "nickname": "Delta Air Lines", + "flight_count": 10, + "market_share": 20.0 + }, + { + "origin": "ATL", + "nickname": "Southwest Airlines", + "flight_count": 10, + "market_share": 20.0 + } + ] + } + } + } + } + }, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/query-methods.json b/docs/public/bsl-data/query-methods.json new file mode 100644 index 00000000..cb6242f5 --- /dev/null +++ b/docs/public/bsl-data/query-methods.json @@ -0,0 +1,1747 @@ +{ + "markdown": "# Query Methods\n\nRetrieve data from your semantic tables using `group_by()` and `aggregate()` methods.\n\n## Overview\n\nBSL provides a simple and consistent query API that works with your semantic table definitions:\n\n- **`group_by()`**: Group data by dimension names (strings only, must be defined in `with_dimensions()`)\n- **`aggregate()`**: Calculate measures (with or without grouping)\n- **`filter()`**: Apply conditions to filter data\n- **`mutate()`**: Transform aggregated results\n- **`order_by()`**: Sort results\n- **`limit()`**: Restrict number of rows\n\n## Setup\n\n```setup_table\nimport ibis\nfrom ibis import _\nfrom boring_semantic_layer import to_semantic_table\n\n# Create Ibis table\nflights_tbl = ibis.memtable({\n \"origin\": [\"NYC\", \"LAX\", \"NYC\", \"SFO\", \"LAX\", \"NYC\", \"SFO\", \"LAX\", \"NYC\"],\n \"carrier\": [\"AA\", \"UA\", \"AA\", \"UA\", \"AA\", \"UA\", \"AA\", \"UA\", \"AA\"],\n \"distance\": [2789, 2789, 2902, 2902, 347, 2789, 347, 347, 2789],\n \"duration\": [330, 330, 360, 360, 65, 330, 65, 65, 330],\n})\n\n# Create semantic table\nflights_st = (\n to_semantic_table(flights_tbl, name=\"flights\")\n .with_dimensions(\n origin=lambda t: t.origin,\n carrier=lambda t: t.carrier,\n )\n .with_measures(\n flight_count=lambda t: t.count(),\n total_distance=lambda t: t.distance.sum(),\n avg_duration=lambda t: t.duration.mean(),\n )\n)\n```\n\n\n\n## group_by()\n\nThe `group_by()` method groups data by one or more dimensions.\n\n\n`group_by()` only accepts string dimension names that were previously defined in `with_dimensions()`. It does not support lambda functions or unbound `_` syntax.\n\n\n### Single Dimension\n\nGroup by a single dimension:\n\n```query_single_dimension\n# Group by one dimension\nresult = flights_st.group_by(\"origin\").aggregate(\"flight_count\")\n```\n\n\n\n### Multiple Dimensions\n\nGroup by multiple dimensions to create detailed breakdowns:\n\n```query_multiple_dimensions\n# Group by multiple dimensions\nresult = flights_st.group_by(\"origin\", \"carrier\").aggregate(\"flight_count\")\n```\n\n\n\n### No Grouping\n\nCalculate overall statistics across all rows using `group_by()` with no arguments:\n\n```query_no_grouping\n# Aggregate entire dataset without grouping\nresult = flights_st.group_by().aggregate(\"flight_count\", \"total_distance\", \"avg_duration\")\n```\n\n\n\n## aggregate()\n\nThe `aggregate()` method calculates measures after grouping. You can reference pre-defined measures or compute new ones on-the-fly.\n\n### Pre-defined Measures\n\nReference measures by their string names:\n\n```query_predefined_measures\n# Use measures defined in with_measures()\nresult = flights_st.group_by(\"origin\").aggregate(\"flight_count\", \"avg_duration\")\n```\n\n\n\n### On-the-Fly Transformations\n\nAdd computed measures directly in `aggregate()` without modifying the semantic table:\n\n```query_onthefly_measures\n# Mix predefined and computed measures\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\n \"flight_count\", # Pre-defined measure\n \"avg_duration\", # Pre-defined measure\n total_miles=lambda t: t.distance.sum(), # Computed on-the-fly\n max_distance=lambda t: t.flight_count + 2 # You can reference other measures as well\n )\n)\n```\n\n\n\n\nOn-the-fly measures let you add context-specific calculations without modifying your semantic table definition. This keeps your base model clean while enabling flexible queries.\n\n\n### Referencing Table Columns\n\nYou can reference **any column from the underlying table** in `aggregate()`, not just pre-defined measures. This is useful when you need one-off calculations without cluttering your semantic table definition.\n\n```query_table_columns\n# Reference table columns directly in aggregate()\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\n \"flight_count\", # Pre-defined measure\n total_distance=lambda t: t.distance.sum(), # Table column 'distance'\n avg_duration=lambda t: t.duration.mean(), # Table column 'duration'\n distance_in_km=lambda t: (t.distance * 1.60934).sum() # Transform then aggregate\n )\n)\n```\n\n\n\n**Key points:**\n- Table columns **must be aggregated** (e.g., `.sum()`, `.mean()`, `.max()`, `.count()`)\n- You can transform columns before aggregating (e.g., `(t.distance * 1.60934).sum()`)\n- This works for any column in the underlying table, even if not defined as a dimension or measure\n- Use this for ad-hoc calculations without modifying your semantic table\n\n\nTable columns cannot be used without an aggregation function. For example, `lambda t: t.distance` will fail. You must use `lambda t: t.distance.sum()` or another aggregation.\n\n\n## filter() / order_by() / limit() \n\nCombine `filter()`, `order_by()`, and `limit()` to refine your query results.\n\n```query_filter_order_limit\nfrom ibis import _\n\n# Filter data, sort, and limit results\nresult = (\n flights_st\n .filter(lambda t: t.origin.isin([\"NYC\", \"LAX\"])) # Filter origins\n .filter(_.distance > 500) # Filter distance using _ syntax\n .group_by(\"origin\")\n .aggregate(\"flight_count\", \"avg_duration\") # Aggregate both measures\n .order_by(ibis.desc(\"flight_count\")) # Sort by flight_count descending\n .limit(5) # Top 5 results\n)\n```\n\n\n\n**Key points:**\n- **`filter()`**: Use lambda or `_` syntax to apply conditions before aggregation\n- **`order_by()`**: Use `ibis.desc()` for descending order, or column name for ascending\n- **`limit()`**: Restrict the number of rows returned\n\n## nest()\n\nThe `nest` parameter in `aggregate()` creates nested data structures (arrays of structs) in your query results. This is useful for API responses, hierarchical visualizations, and preserving relationships in aggregated data.\n\nUse `nest` to collect rows as structured arrays within each group:\n\n```query_basic_nest\nfrom ibis import _\n\n# Nest flight details within each origin\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\n \"flight_count\",\n \"total_distance\",\n # Create nested array of flight details\n nest={\"flights\": lambda t: t.group_by([\"carrier\", \"distance\"])}\n )\n)\n```\n\n\n\n**How it works:**\n- The `nest` parameter accepts a dictionary: `{\"column_name\": lambda t: ...}`\n- The lambda specifies which columns to collect using `.group_by()` or `.select()`\n- Results in an array of structs column named `flights`\n\nYou can also use `.select()` to specify which columns to nest:\n\n```query_nest_select\n# Nest specific columns\nresult = (\n flights_st\n .group_by(\"carrier\")\n .aggregate(\n \"flight_count\",\n nest={\"routes\": lambda t: t.select(\"origin\", \"distance\", \"duration\")}\n )\n)\n```\n\n\n\nAfter nesting, you can re-group which automatically unnests, then access the nested fields.\n\n**Step 1: Create nested data**\n\nFirst, create the nested structure. Notice the `flights` column contains arrays of structs:\n\n```query_nest_step1\nfrom ibis import _\n\n# Create nested data structure\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\n \"flight_count\",\n nest={\"flights\": lambda t: t.group_by([\"carrier\", \"distance\"])}\n )\n)\n```\n\n\n\n**Step 2: Re-group to unnest and access fields**\n\nNow re-group on the same dimension, which automatically unnests the array, allowing you to access the nested fields:\n\n```query_nest_step2\nfrom ibis import _\n\n# Re-grouping automatically unnests the 'flights' array\nresult = (\n result\n .group_by(\"origin\")\n .aggregate(\n total_flights=lambda t: t.flight_count.sum(),\n # Access unnested fields from the flights array\n unique_carriers=lambda t: t.flights.carrier.nunique(),\n avg_distance=lambda t: t.flights.distance.mean()\n )\n)\n```\n\n\n\n**Use cases for nesting:**\n- **API responses**: Create JSON-compatible hierarchical structures\n- **Hierarchical data**: Preserve parent-child relationships in results\n- **Data export**: Generate nested documents for external systems\n- **Drill-down analysis**: Keep detailed records available in aggregated views\n\n\nFor more complex nesting patterns and multi-level hierarchies, see [Advanced Nesting Patterns](/advanced/nesting).\n\n\n## mutate()\n\nThe `mutate()` method transforms aggregated results by adding new computed columns. This is different from on-the-fly measures in `aggregate()` \u2014 `mutate()` works on already-aggregated data.\n\n\n**Key difference:** `.aggregate()` computes from raw data, while `.mutate()` transforms already-aggregated results.\n\n\n```query_mutate\nfrom ibis import _\n\n# Add post-aggregation calculations\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\"flight_count\", \"total_distance\")\n .mutate(\n avg_distance_per_flight=lambda t: t.total_distance / t.flight_count,\n flight_category=lambda t: ibis.case()\n .when(t.flight_count >= 3, \"high\")\n .when(t.flight_count >= 2, \"medium\")\n .else_(\"low\")\n .end()\n )\n)\n```\n\n\n\n**Use cases for `mutate()`:**\n- Calculate ratios from aggregated measures (e.g., `total / count`)\n- Create categories based on aggregated values\n- Add labels or formatting to results\n- Transform aggregated columns using the full power of Ibis\n\nFor more transformations, see [Ibis Table API reference](https://ibis-project.org/reference/expression-tables.html#ibis.expr.types.relations.Table.mutate).\n\n## Window Functions with .over()\n\nWindow functions perform calculations across ordered rows, enabling operations like running totals, moving averages, and ranking. Unlike regular aggregations that reduce many rows to one, window functions preserve row count while adding computed values.\n\n\n**Important:** Window functions can only be applied **after aggregation**, typically within a `.mutate()` call. They cannot be defined directly in measures.\n\n\n**Common window functions:**\n- **`lag()` / `lead()`**: Access previous/next row values for period-over-period comparisons\n- **`cumsum()`**: Calculate running totals\n- **`.over(window)`**: Apply functions over sliding windows (e.g., moving averages)\n- **`rank()` / `row_number()`**: Assign ranks or sequential numbers to rows\n\nHere's a simple example:\n\n```query_window_example\nfrom ibis import _\n\n# First aggregate to daily level\ndaily_flights = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\"flight_count\", \"total_distance\")\n .order_by(\"origin\")\n)\n\n# Then apply window function for cumulative distance\nwindow_spec = ibis.window(order_by=\"origin\")\n\nresult = daily_flights.mutate(\n cumulative_distance=_.total_distance.cumsum(),\n flight_rank=lambda t: ibis.rank().over(ibis.window(order_by=_.flight_count.desc()))\n).limit(10)\n```\n\n\n\n**Key points:**\n- Window functions are applied **after** `.aggregate()` using `.mutate()`\n- Use `.order_by()` to establish row order for window operations\n- Combine with `ibis.window()` for advanced sliding window calculations\n\nFor comprehensive examples including lag/lead, moving averages, and ranking, see [Window Functions](/advanced/windowing).\n\n## as_table()\n\nAfter filtering or aggregating data, you may want to perform additional semantic operations. However, intermediate results don't always preserve the semantic table's dimensions and measures.\n\nThe Problem: Lost Semantic Information\n\nWhen you aggregate data, the result loses semantic metadata. The aggregated result is a `SemanticAggregate` expression, which doesn't have `.dimensions` or `.measures` attributes:\n\n```query_as_table_problem\nfrom ibis import _\n\n# Aggregate the data - this returns a SemanticAggregate\nagg_result = flights_st.group_by(\"origin\").aggregate(\"flight_count\", \"total_distance\")\n\n# Show the type/class of the result\nresult_type = type(agg_result).__name__\n\n# Try to access .dimensions - this will raise an AttributeError\ntry:\n dimensions = agg_result.dimensions\n result = f\"Type: {result_type}\\nDimensions: {dimensions}\"\nexcept AttributeError as e:\n result = f\"Type: {result_type}\\nError: {str(e)}\"\n\nresult\n```\n\n\n\n\nAfter aggregation, you can no longer access the original semantic table's dimensions and measures metadata.\n\nThe Solution: Use as_table()\n\nThe `as_table()` method converts results back into a `SemanticModel`. However, note that for aggregations, the metadata is intentionally cleared (since columns are now materialized):\n\n```query_as_table_after_aggregate\nfrom ibis import _\n\n# Aggregate the data\nagg_result = flights_st.group_by(\"origin\").aggregate(\"flight_count\", \"total_distance\")\n\n# Convert to SemanticModel using as_table()\nagg_table = agg_result.as_table()\n\n# Now .dimensions and .measures attributes exist, but they're empty (metadata was cleared)\nresult = f\"Type: {type(agg_table).__name__}\\nDimensions: {agg_table.dimensions}\\nMeasures: {agg_table.measures}\"\n```\n\n\n\n### When Metadata IS Preserved\n\nFor operations like `filter()`, `order_by()`, and `limit()`, `as_table()` **preserves** the original semantic metadata:\n\n```query_as_table_filter_preserved\nfrom ibis import _\n\n# Filter the data\nfiltered = flights_st.filter(_.distance > 2000)\n\n# Convert back to SemanticModel - metadata is preserved!\nfiltered_table = filtered.as_table()\n\n# Dimensions and measures are still available (preserved from original semantic table)\nresult = f\"Type: {type(filtered_table).__name__}\\nDimensions: {filtered_table.dimensions}\\nMeasures: {filtered_table.measures}\"\n```\n\n\n\nNotice how the dimensions and measures are preserved, unlike the aggregation case above where they were empty.\n\n**Key points:**\n- **Operations that preserve metadata**: `filter()`, `order_by()`, `limit()`, `unnest()` \u2014 calling `as_table()` restores full semantic capabilities with original dimensions/measures\n- **Operations that clear metadata**: `aggregate()`, `mutate()` \u2014 calling `as_table()` returns a `SemanticModel` with empty dimensions/measures (columns are materialized)\n- Use `as_table()` when you need to continue semantic operations on intermediate results\n\n## Next Steps\n\n- Learn about [Building Semantic Tables](/building/semantic-tables) to define dimensions and measures\n- Explore [Composing Models](/building/compose) for multi-table queries\n- Try [Advanced Patterns](/advanced/percentage-total) for complex analytics\n", + "queries": { + "setup_table": { + "code": "import ibis\nfrom ibis import _\nfrom boring_semantic_layer import to_semantic_table\n\n# Create Ibis table\nflights_tbl = ibis.memtable({\n \"origin\": [\"NYC\", \"LAX\", \"NYC\", \"SFO\", \"LAX\", \"NYC\", \"SFO\", \"LAX\", \"NYC\"],\n \"carrier\": [\"AA\", \"UA\", \"AA\", \"UA\", \"AA\", \"UA\", \"AA\", \"UA\", \"AA\"],\n \"distance\": [2789, 2789, 2902, 2902, 347, 2789, 347, 347, 2789],\n \"duration\": [330, 330, 360, 360, 65, 330, 65, 65, 330],\n})\n\n# Create semantic table\nflights_st = (\n to_semantic_table(flights_tbl, name=\"flights\")\n .with_dimensions(\n origin=lambda t: t.origin,\n carrier=lambda t: t.carrier,\n )\n .with_measures(\n flight_count=lambda t: t.count(),\n total_distance=lambda t: t.distance.sum(),\n avg_duration=lambda t: t.duration.mean(),\n )\n)", + "sql": "SELECT\n *\nFROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\"", + "table": { + "columns": [ + "origin", + "carrier", + "distance", + "duration" + ], + "data": [ + [ + "NYC", + "AA", + 2789, + 330 + ], + [ + "LAX", + "UA", + 2789, + 330 + ], + [ + "NYC", + "AA", + 2902, + 360 + ], + [ + "SFO", + "UA", + 2902, + 360 + ], + [ + "LAX", + "AA", + 347, + 65 + ], + [ + "NYC", + "UA", + 2789, + 330 + ], + [ + "SFO", + "AA", + 347, + 65 + ], + [ + "LAX", + "UA", + 347, + 65 + ], + [ + "NYC", + "AA", + 2789, + 330 + ] + ] + } + }, + "query_single_dimension": { + "code": "# Group by one dimension\nresult = flights_st.group_by(\"origin\").aggregate(\"flight_count\")", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"origin\",\n COUNT(*) AS \"flight_count\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "origin", + "flight_count" + ], + "data": [ + [ + "NYC", + 4 + ], + [ + "SFO", + 2 + ], + [ + "LAX", + 3 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-3eaf3be1485583778a074de1413ab582" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "flight_count", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "y": { + "field": "flight_count", + "type": "quantitative" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-3eaf3be1485583778a074de1413ab582": [ + { + "origin": "SFO", + "flight_count": 2 + }, + { + "origin": "LAX", + "flight_count": 3 + }, + { + "origin": "NYC", + "flight_count": 4 + } + ] + } + } + } + }, + "query_multiple_dimensions": { + "code": "# Group by multiple dimensions\nresult = flights_st.group_by(\"origin\", \"carrier\").aggregate(\"flight_count\")", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"origin\",\n \"t1\".\"carrier\",\n COUNT(*) AS \"flight_count\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1,\n 2\n) AS \"t2\"", + "table": { + "columns": [ + "origin", + "carrier", + "flight_count" + ], + "data": [ + [ + "SFO", + "AA", + 1 + ], + [ + "LAX", + "AA", + 1 + ], + [ + "NYC", + "UA", + 1 + ], + [ + "NYC", + "AA", + 3 + ], + [ + "LAX", + "UA", + 2 + ], + [ + "SFO", + "UA", + 1 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-e0473fbef9987ad2bf8b9def574f9445" + }, + "mark": { + "type": "rect" + }, + "encoding": { + "color": { + "field": "flight_count", + "type": "quantitative" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "carrier", + "type": "nominal" + }, + { + "field": "flight_count", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "y": { + "field": "carrier", + "sort": null, + "type": "ordinal" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-e0473fbef9987ad2bf8b9def574f9445": [ + { + "origin": "LAX", + "carrier": "AA", + "flight_count": 1 + }, + { + "origin": "NYC", + "carrier": "UA", + "flight_count": 1 + }, + { + "origin": "SFO", + "carrier": "UA", + "flight_count": 1 + }, + { + "origin": "SFO", + "carrier": "AA", + "flight_count": 1 + }, + { + "origin": "NYC", + "carrier": "AA", + "flight_count": 3 + }, + { + "origin": "LAX", + "carrier": "UA", + "flight_count": 2 + } + ] + } + } + } + }, + "query_no_grouping": { + "code": "# Aggregate entire dataset without grouping\nresult = flights_st.group_by().aggregate(\"flight_count\", \"total_distance\", \"avg_duration\")", + "sql": "SELECT\n COUNT(*) AS \"flight_count\",\n SUM(\"t0\".\"distance\") AS \"total_distance\",\n AVG(\"t0\".\"duration\") AS \"avg_duration\"\nFROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\" AS \"t0\"", + "table": { + "columns": [ + "flight_count", + "total_distance", + "avg_duration" + ], + "data": [ + [ + 9.0, + 18001.0, + 248.33333333333334 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-8a83363d11adffb1b866841c56e09dfc" + }, + "mark": { + "type": "text" + }, + "encoding": { + "text": { + "value": "Complex query - consider custom visualization" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-8a83363d11adffb1b866841c56e09dfc": [ + { + "flight_count": 9, + "total_distance": 18001, + "avg_duration": 248.33333333333334 + } + ] + } + } + } + }, + "query_predefined_measures": { + "code": "# Use measures defined in with_measures()\nresult = flights_st.group_by(\"origin\").aggregate(\"flight_count\", \"avg_duration\")", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"origin\",\n COUNT(*) AS \"flight_count\",\n AVG(\"t1\".\"duration\") AS \"avg_duration\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "origin", + "flight_count", + "avg_duration" + ], + "data": [ + [ + "LAX", + 3, + 153.33333333333334 + ], + [ + "NYC", + 4, + 337.5 + ], + [ + "SFO", + 2, + 212.5 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-d0d39a81e22e7ec454a6eaad6417faac" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "avg_duration" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-d0d39a81e22e7ec454a6eaad6417faac": [ + { + "origin": "SFO", + "flight_count": 2, + "avg_duration": 212.5 + }, + { + "origin": "LAX", + "flight_count": 3, + "avg_duration": 153.33333333333334 + }, + { + "origin": "NYC", + "flight_count": 4, + "avg_duration": 337.5 + } + ] + } + } + } + }, + "query_onthefly_measures": { + "code": "# Mix predefined and computed measures\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\n \"flight_count\", # Pre-defined measure\n \"avg_duration\", # Pre-defined measure\n total_miles=lambda t: t.distance.sum(), # Computed on-the-fly\n max_distance=lambda t: t.flight_count + 2 # You can reference other measures as well\n )\n)", + "sql": "SELECT\n \"t2\".\"origin\",\n \"t2\".\"flight_count\",\n \"t2\".\"avg_duration\",\n \"t2\".\"total_miles\",\n \"t2\".\"flight_count\" + 2 AS \"max_distance\"\nFROM (\n SELECT\n \"t1\".\"origin\",\n COUNT(*) AS \"flight_count\",\n AVG(\"t1\".\"duration\") AS \"avg_duration\",\n SUM(\"t1\".\"distance\") AS \"total_miles\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "origin", + "flight_count", + "avg_duration", + "total_miles", + "max_distance" + ], + "data": [ + [ + "LAX", + 3, + 153.33333333333334, + 3483, + 5 + ], + [ + "NYC", + 4, + 337.5, + 11269, + 6 + ], + [ + "SFO", + 2, + 212.5, + 3249, + 4 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-22866678e8acf83c7eb8555595e76d9b" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "avg_duration", + "total_miles", + "max_distance" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-22866678e8acf83c7eb8555595e76d9b": [ + { + "origin": "NYC", + "flight_count": 4, + "avg_duration": 337.5, + "total_miles": 11269, + "max_distance": 6 + }, + { + "origin": "LAX", + "flight_count": 3, + "avg_duration": 153.33333333333334, + "total_miles": 3483, + "max_distance": 5 + }, + { + "origin": "SFO", + "flight_count": 2, + "avg_duration": 212.5, + "total_miles": 3249, + "max_distance": 4 + } + ] + } + } + } + }, + "query_table_columns": { + "code": "# Reference table columns directly in aggregate()\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\n \"flight_count\", # Pre-defined measure\n total_distance=lambda t: t.distance.sum(), # Table column 'distance'\n avg_duration=lambda t: t.duration.mean(), # Table column 'duration'\n distance_in_km=lambda t: (t.distance * 1.60934).sum() # Transform then aggregate\n )\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"origin\",\n COUNT(*) AS \"flight_count\",\n SUM(\"t1\".\"distance\") AS \"total_distance\",\n AVG(\"t1\".\"duration\") AS \"avg_duration\",\n SUM(\"t1\".\"distance\" * 1.60934) AS \"distance_in_km\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "origin", + "flight_count", + "total_distance", + "avg_duration", + "distance_in_km" + ], + "data": [ + [ + "SFO", + 2, + 3249, + 212.5, + 5228.74566 + ], + [ + "LAX", + 3, + 3483, + 153.33333333333334, + 5605.33122 + ], + [ + "NYC", + 4, + 11269, + 337.5, + 18135.65246 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-bbaf15a58b34f0216cfd683fe873eddb" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "total_distance", + "avg_duration", + "distance_in_km" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-bbaf15a58b34f0216cfd683fe873eddb": [ + { + "origin": "LAX", + "flight_count": 3, + "total_distance": 3483, + "avg_duration": 153.33333333333334, + "distance_in_km": 5605.33122 + }, + { + "origin": "SFO", + "flight_count": 2, + "total_distance": 3249, + "avg_duration": 212.5, + "distance_in_km": 5228.74566 + }, + { + "origin": "NYC", + "flight_count": 4, + "total_distance": 11269, + "avg_duration": 337.5, + "distance_in_km": 18135.65246 + } + ] + } + } + } + }, + "query_filter_order_limit": { + "code": "from ibis import _\n\n# Filter data, sort, and limit results\nresult = (\n flights_st\n .filter(lambda t: t.origin.isin([\"NYC\", \"LAX\"])) # Filter origins\n .filter(_.distance > 500) # Filter distance using _ syntax\n .group_by(\"origin\")\n .aggregate(\"flight_count\", \"avg_duration\") # Aggregate both measures\n .order_by(ibis.desc(\"flight_count\")) # Sort by flight_count descending\n .limit(5) # Top 5 results\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"origin\",\n COUNT(*) AS \"flight_count\",\n AVG(\"t1\".\"duration\") AS \"avg_duration\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\" AS \"t0\"\n WHERE\n \"t0\".\"origin\" IN ('NYC', 'LAX') AND \"t0\".\"distance\" > 500\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"\nORDER BY\n \"t2\".\"flight_count\" DESC\nLIMIT 5", + "table": { + "columns": [ + "origin", + "flight_count", + "avg_duration" + ], + "data": [ + [ + "NYC", + 4, + 337.5 + ], + [ + "LAX", + 1, + 330.0 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-972acb40c7a2674bfa181e6b909100f3" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "avg_duration" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-972acb40c7a2674bfa181e6b909100f3": [ + { + "origin": "LAX", + "flight_count": 1, + "avg_duration": 330.0 + }, + { + "origin": "NYC", + "flight_count": 4, + "avg_duration": 337.5 + } + ] + } + } + } + }, + "query_basic_nest": { + "code": "from ibis import _\n\n# Nest flight details within each origin\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\n \"flight_count\",\n \"total_distance\",\n # Create nested array of flight details\n nest={\"flights\": lambda t: t.group_by([\"carrier\", \"distance\"])}\n )\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"origin\",\n COUNT(*) AS \"flight_count\",\n SUM(\"t1\".\"distance\") AS \"total_distance\",\n ARRAY_AGG({'carrier': \"t1\".\"carrier\", 'distance': \"t1\".\"distance\"}) FILTER(WHERE\n {'carrier': \"t1\".\"carrier\", 'distance': \"t1\".\"distance\"} IS NOT NULL) AS \"flights\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "origin", + "flight_count", + "total_distance", + "flights" + ], + "data": [ + [ + "LAX", + 3, + 3483, + [ + { + "carrier": "UA", + "distance": 2789 + }, + { + "carrier": "AA", + "distance": 347 + }, + { + "carrier": "UA", + "distance": 347 + } + ] + ], + [ + "SFO", + 2, + 3249, + [ + { + "carrier": "UA", + "distance": 2902 + }, + { + "carrier": "AA", + "distance": 347 + } + ] + ], + [ + "NYC", + 4, + 11269, + [ + { + "carrier": "AA", + "distance": 2789 + }, + { + "carrier": "AA", + "distance": 2902 + }, + { + "carrier": "UA", + "distance": 2789 + }, + { + "carrier": "AA", + "distance": 2789 + } + ] + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-84104b475a7c24825f52e7213f8ab180" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "total_distance", + "flights" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-84104b475a7c24825f52e7213f8ab180": [ + { + "origin": "SFO", + "flight_count": 2, + "total_distance": 3249, + "flights": [ + { + "carrier": "UA", + "distance": 2902 + }, + { + "carrier": "AA", + "distance": 347 + } + ] + }, + { + "origin": "NYC", + "flight_count": 4, + "total_distance": 11269, + "flights": [ + { + "carrier": "AA", + "distance": 2789 + }, + { + "carrier": "AA", + "distance": 2902 + }, + { + "carrier": "UA", + "distance": 2789 + }, + { + "carrier": "AA", + "distance": 2789 + } + ] + }, + { + "origin": "LAX", + "flight_count": 3, + "total_distance": 3483, + "flights": [ + { + "carrier": "UA", + "distance": 2789 + }, + { + "carrier": "AA", + "distance": 347 + }, + { + "carrier": "UA", + "distance": 347 + } + ] + } + ] + } + } + } + }, + "query_nest_select": { + "code": "# Nest specific columns\nresult = (\n flights_st\n .group_by(\"carrier\")\n .aggregate(\n \"flight_count\",\n nest={\"routes\": lambda t: t.select(\"origin\", \"distance\", \"duration\")}\n )\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"carrier\",\n COUNT(*) AS \"flight_count\",\n ARRAY_AGG(\n {'origin': \"t1\".\"origin\", 'distance': \"t1\".\"distance\", 'duration': \"t1\".\"duration\"}\n ) FILTER(WHERE\n {'origin': \"t1\".\"origin\", 'distance': \"t1\".\"distance\", 'duration': \"t1\".\"duration\"} IS NOT NULL) AS \"routes\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "carrier", + "flight_count", + "routes" + ], + "data": [ + [ + "UA", + 4, + [ + { + "origin": "LAX", + "distance": 2789, + "duration": 330 + }, + { + "origin": "SFO", + "distance": 2902, + "duration": 360 + }, + { + "origin": "NYC", + "distance": 2789, + "duration": 330 + }, + { + "origin": "LAX", + "distance": 347, + "duration": 65 + } + ] + ], + [ + "AA", + 5, + [ + { + "origin": "NYC", + "distance": 2789, + "duration": 330 + }, + { + "origin": "NYC", + "distance": 2902, + "duration": 360 + }, + { + "origin": "LAX", + "distance": 347, + "duration": 65 + }, + { + "origin": "SFO", + "distance": 347, + "duration": 65 + }, + { + "origin": "NYC", + "distance": 2789, + "duration": 330 + } + ] + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-82d74a794a4a364fb1e9e683490ca229" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "carrier", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "carrier", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "routes" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-82d74a794a4a364fb1e9e683490ca229": [ + { + "carrier": "UA", + "flight_count": 4, + "routes": [ + { + "origin": "LAX", + "distance": 2789, + "duration": 330 + }, + { + "origin": "SFO", + "distance": 2902, + "duration": 360 + }, + { + "origin": "NYC", + "distance": 2789, + "duration": 330 + }, + { + "origin": "LAX", + "distance": 347, + "duration": 65 + } + ] + }, + { + "carrier": "AA", + "flight_count": 5, + "routes": [ + { + "origin": "NYC", + "distance": 2789, + "duration": 330 + }, + { + "origin": "NYC", + "distance": 2902, + "duration": 360 + }, + { + "origin": "LAX", + "distance": 347, + "duration": 65 + }, + { + "origin": "SFO", + "distance": 347, + "duration": 65 + }, + { + "origin": "NYC", + "distance": 2789, + "duration": 330 + } + ] + } + ] + } + } + } + }, + "query_nest_step1": { + "code": "from ibis import _\n\n# Create nested data structure\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\n \"flight_count\",\n nest={\"flights\": lambda t: t.group_by([\"carrier\", \"distance\"])}\n )\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"origin\",\n COUNT(*) AS \"flight_count\",\n ARRAY_AGG({'carrier': \"t1\".\"carrier\", 'distance': \"t1\".\"distance\"}) FILTER(WHERE\n {'carrier': \"t1\".\"carrier\", 'distance': \"t1\".\"distance\"} IS NOT NULL) AS \"flights\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "origin", + "flight_count", + "flights" + ], + "data": [ + [ + "NYC", + 4, + [ + { + "carrier": "AA", + "distance": 2789 + }, + { + "carrier": "AA", + "distance": 2902 + }, + { + "carrier": "UA", + "distance": 2789 + }, + { + "carrier": "AA", + "distance": 2789 + } + ] + ], + [ + "SFO", + 2, + [ + { + "carrier": "UA", + "distance": 2902 + }, + { + "carrier": "AA", + "distance": 347 + } + ] + ], + [ + "LAX", + 3, + [ + { + "carrier": "UA", + "distance": 2789 + }, + { + "carrier": "AA", + "distance": 347 + }, + { + "carrier": "UA", + "distance": 347 + } + ] + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-22fff5453736929a356005cc8fc72142" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "flights" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-22fff5453736929a356005cc8fc72142": [ + { + "origin": "SFO", + "flight_count": 2, + "flights": [ + { + "carrier": "UA", + "distance": 2902 + }, + { + "carrier": "AA", + "distance": 347 + } + ] + }, + { + "origin": "NYC", + "flight_count": 4, + "flights": [ + { + "carrier": "AA", + "distance": 2789 + }, + { + "carrier": "AA", + "distance": 2902 + }, + { + "carrier": "UA", + "distance": 2789 + }, + { + "carrier": "AA", + "distance": 2789 + } + ] + }, + { + "origin": "LAX", + "flight_count": 3, + "flights": [ + { + "carrier": "UA", + "distance": 2789 + }, + { + "carrier": "AA", + "distance": 347 + }, + { + "carrier": "UA", + "distance": 347 + } + ] + } + ] + } + } + } + }, + "query_nest_step2": { + "code": "from ibis import _\n\n# Re-grouping automatically unnests the 'flights' array\nresult = (\n result\n .group_by(\"origin\")\n .aggregate(\n total_flights=lambda t: t.flight_count.sum(),\n # Access unnested fields from the flights array\n unique_carriers=lambda t: t.flights.carrier.nunique(),\n avg_distance=lambda t: t.flights.distance.mean()\n )\n)", + "sql": "WITH \"t3\" AS (\n SELECT\n *\n FROM (\n SELECT\n \"t1\".\"origin\",\n COUNT(*) AS \"flight_count\",\n ARRAY_AGG({'carrier': \"t1\".\"carrier\", 'distance': \"t1\".\"distance\"}) FILTER(WHERE\n {'carrier': \"t1\".\"carrier\", 'distance': \"t1\".\"distance\"} IS NOT NULL) AS \"flights\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n ) AS \"t2\"\n)\nSELECT\n *\nFROM (\n SELECT\n \"t7\".\"origin\",\n \"t7\".\"total_flights\",\n \"t9\".\"unique_carriers\",\n \"t9\".\"avg_distance\"\n FROM (\n SELECT\n \"t4\".\"origin\",\n SUM(\"t4\".\"flight_count\") AS \"total_flights\"\n FROM \"t3\" AS \"t4\"\n GROUP BY\n 1\n ) AS \"t7\"\n LEFT OUTER JOIN (\n SELECT\n \"t5\".\"origin\",\n COUNT(DISTINCT \"t5\".\"flights\".\"carrier\") AS \"unique_carriers\",\n AVG(\"t5\".\"flights\".\"distance\") AS \"avg_distance\"\n FROM (\n SELECT\n \"t4\".*\n REPLACE (\"ibis_table_unnest_column_gc7rwvhw6jaufdwzy2lvac3cee\" AS \"flights\")\n FROM \"t3\" AS \"t4\"\n CROSS JOIN UNNEST(\"t4\".\"flights\") AS \"ibis_table_unnest_ixuk367itzaj3jmbtxiysyvlsq\"(\"ibis_table_unnest_column_gc7rwvhw6jaufdwzy2lvac3cee\")\n ) AS \"t5\"\n GROUP BY\n 1\n ) AS \"t9\"\n ON \"t7\".\"origin\" = \"t9\".\"origin\"\n) AS \"t10\"", + "table": { + "columns": [ + "origin", + "total_flights", + "unique_carriers", + "avg_distance" + ], + "data": [ + [ + "SFO", + 2, + 2, + 1624.5 + ], + [ + "NYC", + 4, + 2, + 2817.25 + ], + [ + "LAX", + 3, + 2, + 1161.0 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-d782e4c945aacacb2b473b1f0b3c5334" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "total_flights", + "unique_carriers", + "avg_distance" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-d782e4c945aacacb2b473b1f0b3c5334": [ + { + "origin": "NYC", + "total_flights": 4, + "unique_carriers": 2, + "avg_distance": 2817.25 + }, + { + "origin": "LAX", + "total_flights": 3, + "unique_carriers": 2, + "avg_distance": 1161.0 + }, + { + "origin": "SFO", + "total_flights": 2, + "unique_carriers": 2, + "avg_distance": 1624.5 + } + ] + } + } + } + }, + "query_mutate": { + "code": "from ibis import _\n\n# Add post-aggregation calculations\nresult = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\"flight_count\", \"total_distance\")\n .mutate(\n avg_distance_per_flight=lambda t: t.total_distance / t.flight_count,\n flight_category=lambda t: ibis.case()\n .when(t.flight_count >= 3, \"high\")\n .when(t.flight_count >= 2, \"medium\")\n .else_(\"low\")\n .end()\n )\n)", + "sql": "SELECT\n \"t2\".\"origin\",\n \"t2\".\"flight_count\",\n \"t2\".\"total_distance\",\n \"t2\".\"total_distance\" / \"t2\".\"flight_count\" AS \"avg_distance_per_flight\",\n CASE\n WHEN \"t2\".\"flight_count\" >= 3\n THEN 'high'\n WHEN \"t2\".\"flight_count\" >= 2\n THEN 'medium'\n ELSE 'low'\n END AS \"flight_category\"\nFROM (\n SELECT\n \"t1\".\"origin\",\n COUNT(*) AS \"flight_count\",\n SUM(\"t1\".\"distance\") AS \"total_distance\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "origin", + "flight_count", + "total_distance", + "avg_distance_per_flight", + "flight_category" + ], + "data": [ + [ + "LAX", + 3, + 3483, + 1161.0, + "high" + ], + [ + "SFO", + 2, + 3249, + 1624.5, + "medium" + ], + [ + "NYC", + 4, + 11269, + 2817.25, + "high" + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-f9d66f0b40b1ffd50f6a1cf836bc83a9" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "total_distance" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-f9d66f0b40b1ffd50f6a1cf836bc83a9": [ + { + "origin": "SFO", + "flight_count": 2, + "total_distance": 3249 + }, + { + "origin": "LAX", + "flight_count": 3, + "total_distance": 3483 + }, + { + "origin": "NYC", + "flight_count": 4, + "total_distance": 11269 + } + ] + } + } + } + }, + "query_window_example": { + "code": "from ibis import _\n\n# First aggregate to daily level\ndaily_flights = (\n flights_st\n .group_by(\"origin\")\n .aggregate(\"flight_count\", \"total_distance\")\n .order_by(\"origin\")\n)\n\n# Then apply window function for cumulative distance\nwindow_spec = ibis.window(order_by=\"origin\")\n\nresult = daily_flights.mutate(\n cumulative_distance=_.total_distance.cumsum(),\n flight_rank=lambda t: ibis.rank().over(ibis.window(order_by=_.flight_count.desc()))\n).limit(10)", + "sql": "SELECT\n \"t4\".\"origin\",\n \"t4\".\"flight_count\",\n \"t4\".\"total_distance\",\n \"t4\".\"cumulative_distance\",\n RANK() OVER (ORDER BY \"t4\".\"flight_count\" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) - 1 AS \"flight_rank\"\nFROM (\n SELECT\n \"t3\".\"origin\",\n \"t3\".\"flight_count\",\n \"t3\".\"total_distance\",\n SUM(\"t3\".\"total_distance\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS \"cumulative_distance\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t1\".\"origin\",\n COUNT(*) AS \"flight_count\",\n SUM(\"t1\".\"distance\") AS \"total_distance\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_3reqxxdd6jg7pj5r5m7bra775m\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n ) AS \"t2\"\n ORDER BY\n \"t2\".\"origin\" ASC\n ) AS \"t3\"\n) AS \"t4\"\nLIMIT 10", + "table": { + "columns": [ + "origin", + "flight_count", + "total_distance", + "cumulative_distance", + "flight_rank" + ], + "data": [ + [ + "NYC", + 4, + 11269, + 14752, + 0 + ], + [ + "LAX", + 3, + 3483, + 3483, + 1 + ], + [ + "SFO", + 2, + 3249, + 18001, + 2 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-9720646ccab33e2b86467c8a8982e12b" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "total_distance" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-9720646ccab33e2b86467c8a8982e12b": [ + { + "origin": "NYC", + "flight_count": 4, + "total_distance": 11269 + }, + { + "origin": "SFO", + "flight_count": 2, + "total_distance": 3249 + }, + { + "origin": "LAX", + "flight_count": 3, + "total_distance": 3483 + } + ] + } + } + } + }, + "query_as_table_problem": { + "output": "Type: SemanticAggregate\nError: 'Table' object has no attribute 'dimensions'" + }, + "query_as_table_after_aggregate": { + "output": "Type: SemanticModel\nDimensions: ()\nMeasures: ()" + }, + "query_as_table_filter_preserved": { + "output": "Type: SemanticModel\nDimensions: ('origin', 'carrier')\nMeasures: ('flight_count', 'total_distance', 'avg_duration')" + } + }, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/reference.json b/docs/public/bsl-data/reference.json new file mode 100644 index 00000000..3326e97b --- /dev/null +++ b/docs/public/bsl-data/reference.json @@ -0,0 +1,5 @@ +{ + "markdown": "# API Reference\n\nComplete API documentation for the Boring Semantic Layer.\n\n## Table Creation & Configuration\n\nMethods for creating and configuring semantic tables.\n\n### to_semantic_table()\n\n```python\nto_semantic_table(\n table: ibis.Table,\n name: str,\n description: str = None\n) -> SemanticTable\n```\n\nCreate a semantic table from an Ibis table. This is the primary entry point for building semantic models.\n\n**Parameters:**\n- `table` - Ibis table to build the model from\n- `name` - Unique identifier for the semantic table\n- `description` - Optional description of the semantic table\n\n**Example:**\n```python\nimport ibis\nfrom boring_semantic_layer import to_semantic_table\n\nflights = ibis.read_parquet(\"flights.parquet\")\nflights_st = to_semantic_table(flights, \"flights\")\n```\n\n### with_dimensions()\n\n```python\nwith_dimensions(\n **dimensions: Callable | Dimension\n) -> SemanticTable\n```\n\nDefine dimensions for grouping and analysis. Dimensions are attributes that categorize data.\n\n**Example:**\n```python\nflights_st = flights_st.with_dimensions(\n origin=lambda t: t.origin,\n dest=lambda t: t.dest,\n carrier=lambda t: t.carrier\n)\n```\n\n### with_measures()\n\n```python\nwith_measures(\n **measures: Callable | Measure\n) -> SemanticTable\n```\n\nDefine aggregations and calculations. Measures are numeric values that can be aggregated.\n\n**Example:**\n```python\nflights_st = flights_st.with_measures(\n flight_count=lambda t: t.count(),\n avg_delay=lambda t: t.arr_delay.mean(),\n total_distance=lambda t: t.distance.sum()\n)\n```\n\n### from_yaml()\n\n```python\nfrom_yaml(\n yaml_path: str,\n connection: ibis.Connection = None\n) -> dict[str, SemanticTable]\n```\n\nLoad semantic models from a YAML configuration file. Returns a dictionary of semantic tables.\n\n**Parameters:**\n- `yaml_path` - Path to YAML configuration file\n- `connection` - Optional Ibis connection for database tables\n\n**Example:**\n```python\nfrom boring_semantic_layer.yaml import from_yaml\n\nmodels = from_yaml(\"models.yaml\")\nflights_st = models[\"flights\"]\n```\n\n### Dimension Class\n\n```python\nDimension(\n expr: Callable,\n description: str = None\n)\n```\n\nSelf-documenting dimension with description. Use for better API documentation.\n\n**Example:**\n```python\nfrom boring_semantic_layer import Dimension\n\nflights_st = flights_st.with_dimensions(\n origin=Dimension(\n expr=lambda t: t.origin,\n description=\"Airport code where the flight departed from\"\n )\n)\n```\n\n### Measure Class\n\n```python\nMeasure(\n expr: Callable,\n description: str = None\n)\n```\n\nSelf-documenting measure with description. Use for better API documentation.\n\n**Example:**\n```python\nfrom boring_semantic_layer import Measure\n\nflights_st = flights_st.with_measures(\n avg_delay=Measure(\n expr=lambda t: t.arr_delay.mean(),\n description=\"Average arrival delay in minutes\"\n )\n)\n```\n\n### all()\n\n```python\nst.all()\n```\n\nReference the entire dataset within measure definitions. Primarily used for percentage-of-total calculations.\n\n**Example:**\n```python\nflights_st = to_semantic_table(data, \"flights\").with_measures(\n flight_count=lambda t: t.count(),\n pct_of_total=lambda t: (\n t.count() / t.all().count() * 100\n )\n)\n```\n\n## Join Methods\n\nMethods for composing semantic tables through joins.\n\n### join_many()\n\n```python\njoin_many(\n other: SemanticTable,\n on: Callable,\n name: str = None\n) -> SemanticTable\n```\n\nOne-to-many relationship join (LEFT JOIN). Use when the left table can match multiple rows in the right table.\n\n**Parameters:**\n- `other` - The semantic table to join with\n- `on` - Lambda function defining the join condition\n- `name` - Optional name for the joined table reference\n\n**Example:**\n```python\nflights_st = flights_st.join_many(\n carriers_st,\n on=lambda l, r: l.carrier == r.code,\n name=\"carrier_info\"\n)\n```\n\n### join_one()\n\n```python\njoin_one(\n other: SemanticTable,\n on: Callable,\n name: str = None\n) -> SemanticTable\n```\n\nOne-to-one relationship join (INNER JOIN). Use when each row in the left table matches exactly one row in the right table.\n\n**Example:**\n```python\nflights_st = flights_st.join_one(\n airports_st,\n on=lambda l, r: l.origin == r.code\n)\n```\n\n### join_cross()\n\n```python\njoin_cross(\n other: SemanticTable,\n name: str = None\n) -> SemanticTable\n```\n\nCross join (CARTESIAN PRODUCT). Creates all possible combinations of rows from both tables.\n\n### join()\n\n```python\njoin(\n other: SemanticTable,\n on: Callable,\n how: str = \"inner\",\n name: str = None\n) -> SemanticTable\n```\n\nCustom join with flexible join type. Supports 'inner', 'left', 'right', 'outer', and 'cross'.\n\n**Parameters:**\n- `how` - Join type: 'inner', 'left', 'right', 'outer', or 'cross'\n\n## Query Methods\n\nMethods for querying and transforming semantic tables.\n\n### group_by()\n\n```python\ngroup_by(\n *dimensions: str\n) -> QueryBuilder\n```\n\nGroup data by one or more dimension names. Returns a query builder for chaining with aggregate().\n\n**Example:**\n```python\nresult = flights_st.group_by(\"origin\", \"carrier\").aggregate(\"flight_count\")\n```\n\n### aggregate()\n\n```python\naggregate(\n *measures: str,\n **kwargs\n) -> ibis.Table\n```\n\nCalculate one or more measures. Can be used standalone or after group_by().\n\n**Examples:**\n```python\n# Without grouping\ntotal = flights_st.aggregate(\"flight_count\")\n\n# With grouping\nby_origin = flights_st.group_by(\"origin\").aggregate(\"flight_count\", \"avg_delay\")\n```\n\n### filter()\n\n```python\nfilter(\n condition: Callable\n) -> SemanticTable\n```\n\nApply conditions to filter data. Use lambda functions with Ibis expressions.\n\n**Example:**\n```python\ndelayed_flights = flights_st.filter(lambda t: t.arr_delay > 0)\n```\n\n### order_by()\n\n```python\norder_by(\n *columns: str | ibis.Expression\n) -> ibis.Table\n```\n\nSort query results. Use `ibis.desc()` for descending order.\n\n**Example:**\n```python\nresult = flights_st.group_by(\"origin\").aggregate(\"flight_count\")\nresult = result.order_by(ibis.desc(\"flight_count\"))\n```\n\n### limit()\n\n```python\nlimit(\n n: int\n) -> ibis.Table\n```\n\nRestrict the number of rows returned.\n\n**Example:**\n```python\ntop_10 = result.order_by(ibis.desc(\"flight_count\")).limit(10)\n```\n\n### mutate()\n\n```python\nmutate(\n **expressions: Callable | ibis.Expression\n) -> ibis.Table\n```\n\nAdd or transform columns in aggregated results. Useful for calculations after aggregation.\n\n**Example:**\n```python\nresult = flights_st.group_by(\"month\").aggregate(\"revenue\")\nresult = result.mutate(\n growth_rate=lambda t: (t.revenue - t.revenue.lag()) / t.revenue.lag() * 100\n)\n```\n\n### select()\n\n```python\nselect(\n *columns: str | ibis.Expression\n) -> ibis.Table\n```\n\nSelect specific columns from the result. Often used in nesting operations.\n\n**Example:**\n```python\nresult.select(\"origin\", \"flight_count\")\n```\n\n## Nesting\n\nCreate nested data structures within aggregations.\n\n### nest Parameter\n\n```python\naggregate(\n *measures,\n nest={\n \"nested_column\": lambda t: t.group_by([...]) | t.select(...)\n }\n)\n```\n\nCreate nested arrays of structs within aggregation results. Useful for hierarchical data or subtotals.\n\n**Example:**\n```python\nresult = flights_st.group_by(\"carrier\").aggregate(\n \"total_flights\",\n nest={\n \"by_month\": lambda t: t.group_by(\"month\").aggregate(\"monthly_flights\")\n }\n)\n```\n\n## Charting\n\nGenerate visualizations from query results.\n\n### chart()\n\n```python\nchart(\n result: ibis.Table,\n backend: str = \"altair\",\n spec: dict = None,\n format: str = \"interactive\"\n) -> Chart\n```\n\nCreate visualizations from query results. Supports Altair (default) and Plotly backends.\n\n**Parameters:**\n- `result` - Query result table to visualize\n- `backend` - \"altair\" or \"plotly\"\n- `spec` - Custom Vega-Lite specification (for Altair)\n- `format` - \"interactive\", \"json\", \"png\", \"svg\"\n\n**Auto-detection:**\nBSL automatically selects appropriate chart types:\n- Single dimension + measure \u2192 Bar chart\n- Time dimension + measure \u2192 Line chart\n- Two dimensions + measure \u2192 Heatmap\n\n**Example:**\n```python\nfrom boring_semantic_layer.chart import chart\n\nresult = flights_st.group_by(\"month\").aggregate(\"flight_count\")\nchart(result, backend=\"altair\")\n```\n\n## Dimensional Indexing\n\nCreate searchable catalogs of dimension values.\n\n### index()\n\n```python\nindex(\n dimensions: Callable | None = None,\n by: str = None,\n sample: int = None\n) -> ibis.Table\n```\n\nCreate a searchable catalog of unique dimension values with optional weighting and sampling.\n\n**Parameters:**\n- `dimensions` - None (all dimensions) or lambda returning list of fields\n- `by` - Measure name for weighting results\n- `sample` - Number of rows to sample (for large datasets)\n\n**Examples:**\n```python\n# Index all dimensions\nflights_st.index()\n\n# Index specific dimensions\nflights_st.index(lambda t: [t.origin, t.dest])\n\n# Weight by measure\nflights_st.index(by=\"flight_count\")\n\n# Sample large dataset\nflights_st.index(sample=10000)\n```\n\n## Other\n\n### MCP Integration\n\n#### MCPSemanticModel()\n\n```python\nMCPSemanticModel(\n models: dict[str, SemanticTable] | str,\n description: str = None\n)\n```\n\nCreate an MCP server to expose semantic models to LLMs like Claude. Accepts either a dictionary of models or a path to a YAML configuration file.\n\n**Available MCP Tools:**\n\nThese tools are called by Claude through the MCP interface:\n\n- `list_models()` - List all available semantic model names\n- `get_model()` - Get detailed model information (dimensions, measures, joins)\n- `get_time_range()` - Get available time range for time-series data\n- `query_model()` - Execute queries against semantic models\n\n**Example:**\n```python\nfrom boring_semantic_layer.mcp import MCPSemanticModel\n\n# From dictionary\nserver = MCPSemanticModel(\n models={\"flights\": flights_st, \"airports\": airports_st},\n description=\"Flight data analysis\"\n)\n\n# From YAML\nserver = MCPSemanticModel(\"config.yaml\")\n```\n\n### YAML Configuration\n\n#### YAML Structure\n\n```yaml\nmodel_name:\n table: table_reference\n description: \"Optional description\"\n\n dimensions:\n dimension_name: expression\n # or with description\n dimension_name:\n expr: expression\n description: \"Dimension description\"\n\n measures:\n measure_name: expression\n # or with description\n measure_name:\n expr: expression\n description: \"Measure description\"\n\n joins:\n join_name:\n model: model_reference\n on: join_condition\n how: join_type # left, inner, right, outer, cross\n```\n\n#### Expression Syntax\n\n- `_` - Reference to the table\n- `_.column` - Reference a column\n- `_.count()` - Aggregation functions\n- `_.column.sum()` - Column aggregations\n- `_.column.mean()` - Average\n- `_.column.min()` / `_.column.max()` - Min/Max\n\n**Example:**\n```yaml\nflights:\n table: flights_data\n description: \"Flight operations data\"\n\n dimensions:\n origin: _.origin\n dest: _.dest\n carrier:\n expr: _.carrier\n description: \"Airline carrier code\"\n\n measures:\n flight_count: _.count()\n avg_delay:\n expr: _.arr_delay.mean()\n description: \"Average arrival delay in minutes\"\n```\n\n## Next Steps\n\n- Learn about [Semantic Tables](/building/semantic-tables)\n- Explore [Query Methods](/querying/methods)\n- See [Advanced Patterns](/advanced/percentage-total)\n", + "queries": {}, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/semantic-table.json b/docs/public/bsl-data/semantic-table.json new file mode 100644 index 00000000..b0cb9477 --- /dev/null +++ b/docs/public/bsl-data/semantic-table.json @@ -0,0 +1,345 @@ +{ + "markdown": "# Building a Semantic Table\n\nDefine your data model with dimensions and measures using Ibis expressions.\n\n## Overview\n\nA Semantic Table is the core building block of BSL. It transforms a raw Ibis table into a reusable, self-documenting data model by defining:\n- **Dimensions**: Attributes to group by (e.g., origin, carrier, year)\n- **Measures**: Aggregations and calculations (e.g., flight count, total distance)\n\n## to_semantic_table()\n\n```setup_flights\nimport ibis\nfrom boring_semantic_layer import to_semantic_table\n\n# 1. Start with an Ibis table\ncon = ibis.duckdb.connect(\":memory:\")\nflights_data = ibis.memtable({\n \"origin\": [\"JFK\", \"LAX\", \"SFO\"],\n \"dest\": [\"LAX\", \"SFO\", \"JFK\"],\n \"carrier\": [\"AA\", \"UA\", \"DL\"],\n \"year\": [2023, 2023, 2024],\n \"distance\": [2475, 337, 382],\n \"dep_delay\": [10, 5, 0]\n})\nflights_tbl = con.create_table(\"flights\", flights_data)\n\n# 2. Convert to a Semantic Table\nflights_st = to_semantic_table(flights_tbl, name=\"flights\")\n```\n\n## with_dimensions()\n\nDimensions define the attributes you can group by in your queries. They represent the categorical or descriptive aspects of your data that you want to analyze.\n\nYou can define dimensions using lambda expressions, unbound syntax (`_.`), or the `Dimension` class with descriptions:\n\n```dimensions_demo\nfrom ibis import _\nfrom boring_semantic_layer import Dimension\n\nflights_st = flights_st.with_dimensions(\n # Lambda expressions - simple and explicit\n origin=lambda t: t.origin,\n\n # Unbound syntax - cleaner and more concise\n destination=_.dest,\n year=_.year,\n\n # Dimension - self-documenting and AI-friendly\n carrier=Dimension(\n expr=lambda t: t.carrier,\n description=\"Airline carrier code\"\n )\n)\n\nflights_st.dimensions\n```\n\n\n## with_measures()\n\nMeasures define the aggregations and calculations you can query. They represent the quantitative aspects of your data that you want to analyze (counts, sums, averages, etc.).\n\nYou can define measures using lambda expressions, reference other measures for composition, or use the `Measure` class with descriptions:\n\n```measures_demo\nfrom boring_semantic_layer import Measure\n\nflights_st = flights_st.with_measures(\n # Lambda expressions - simple and concise\n total_flights=lambda t: t.count(),\n total_distance=lambda t: t.distance.sum(),\n max_delay=lambda t: t.dep_delay.max(),\n\n # Reference other measures for composition\n avg_distance_per_flight=lambda t: t.total_distance / t.total_flights,\n\n # Measure - self-documenting and AI-friendly\n avg_distance=Measure(\n expr=lambda t: t.distance.mean(),\n description=\"Average flight distance in miles\"\n )\n)\n\nflights_st.measures\n```\n\n\n\n### all()\n\nThe `all()` function references the entire dataset within measure definitions, enabling percent-of-total and comparison calculations.\n\n**Example:** Calculate market share as a percentage\n\n```measure_all_demo\nflights_with_pct = flights_st.with_measures(\n flight_count=lambda t: t.count(),\n market_share=lambda t: t.flight_count / t.all(t.flight_count) * 100 # Percent of total\n )\n\n# Query by carrier\nresult = (\n flights_with_pct\n .group_by(\"carrier\")\n .aggregate(\"flight_count\", \"market_share\")\n)\n```\n\n\n\n\n`t.all()` is a method available on the table parameter `t` in measure definitions. It references the entire dataset regardless of grouping, making it perfect for calculating percentages, or comparing groups to the total.\n\n\nFor more examples, see the [Percent of Total pattern](/advanced/percentage-total).\n\n## join_one() / join_many() / join_cross()\n\nJoin semantic tables together to query across relationships. Joins allow you to combine data from multiple semantic tables and access dimensions and measures across all joined tables.\n\nWhy Semantic Joins?\n\nInstead of just using SQL join types (`LEFT`, `INNER`, etc.), BSL offers relationship-based joins that capture the **meaning** of your data connections:\n\n**Traditional SQL Approach:**\n```python\n# Which join type? LEFT or INNER? Why?\nflights.join(carriers, condition, how=\"left\") # unclear intent\n```\n\n**BSL Semantic Approach:**\n```python\n# Clear intent: one carrier has many flights\nflights.join_many(carriers, left_on=\"carrier\", right_on=\"code\")\n```\n\n**Benefits:**\n- **Self-documenting**: `join_many()` tells you it's a one-to-many relationship\n- **Clearer intent**: The method name describes the data relationship, not just SQL mechanics\n- **Safer defaults**: Each method uses the appropriate join type for that relationship\n- **Better tooling**: IDEs and AI assistants understand your data model semantics\n\n\nAfter joining, dimensions and measures are prefixed with table names (e.g., `flights.origin`, `carriers.name`) to avoid naming conflicts.\n\n\n\nLet's get some additional data:\n\n```setup_carriers\nimport ibis\nfrom boring_semantic_layer import to_semantic_table\n\ncon = ibis.duckdb.connect(\":memory:\")\n\n# Create carriers data\ncarriers_data = ibis.memtable({\n \"code\": [\"AA\", \"UA\", \"DL\"],\n \"name\": [\"American Airlines\", \"United Airlines\", \"Delta Air Lines\"]\n})\ncarriers_tbl = con.create_table(\"carriers\", carriers_data)\n```\n\n\nAnd create a carriers semantic table:\n\n```carriers_st\ncarriers = (\n to_semantic_table(carriers_tbl, name=\"carriers\")\n .with_dimensions(\n code=lambda t: t.code,\n name=lambda t: t.name\n )\n .with_measures(\n carrier_count=lambda t: t.count()\n )\n)\n```\n\n### join_many() - One-to-Many Relationships\n\nUse `join_many()` when one row in the left table can match multiple rows in the right table (LEFT JOIN).\n\n```join_demo\n# Join carriers to flights - one carrier has many flights\nflights_with_carriers = flights_st.join_many(\n carriers,\n left_on=\"carrier\",\n right_on=\"code\"\n)\n\n# Inspect available dimensions and measures\nflights_with_carriers.dimensions\n```\n\n\nAfter joining, all dimensions and measures from both tables are available. Each is prefixed with its table name to avoid conflicts:\n\n\n### join_one() - One-to-One Relationships\n\nUse `join_one()` when rows have a unique matching relationship (INNER JOIN).\n\n```python\n# Many flights \u2192 one carrier (each flight has exactly one carrier)\nflights_with_carrier = flights_st.join_one(\n carriers,\n left_on=\"carrier\",\n right_on=\"code\"\n)\n```\n\n### join_cross() - Cross Join\n\nUse `join_cross()` to create every possible combination of rows from both tables (CARTESIAN PRODUCT).\n\n```python\n# Every flight \u00d7 every carrier combination\nall_combinations = flights_st.join_cross(carriers)\n```\n\n### join() - Custom Join Conditions\n\nUse `join()` for complex join conditions or specific SQL join types.\n\n```python\n# LEFT JOIN with custom condition\nflights_with_carriers = flights_st.join(\n carriers,\n lambda f, c: f.carrier == c.code,\n how=\"left\"\n)\n\n# INNER JOIN\nflights_matched = flights_st.join(\n carriers,\n lambda f, c: f.carrier == c.code,\n how=\"inner\"\n)\n\n# Complex conditions\ndate_range_join = flights_st.join(\n promotions,\n lambda f, p: (f.date >= p.start_date) & (f.date <= p.end_date),\n how=\"left\"\n)\n```\n\n**Supported join types:** `\"inner\"`, `\"left\"`, `\"right\"`, `\"outer\"`, `\"cross\"`\n\n## Next Steps\n\n- Learn about [Composing Models](/examples/compose)\n- Explore [YAML Configuration](/examples/yaml-config)\n- Start [Querying Semantic Tables](/examples/query-methods)\n", + "queries": { + "dimensions_demo": { + "output": "('origin', 'destination', 'year', 'carrier')" + }, + "measures_demo": { + "output": "('total_flights', 'total_distance', 'max_delay', 'avg_distance', 'avg_distance_per_flight')" + }, + "measure_all_demo": { + "code": "flights_with_pct = flights_st.with_measures(\n flight_count=lambda t: t.count(),\n market_share=lambda t: t.flight_count / t.all(t.flight_count) * 100 # Percent of total\n )\n\n# Query by carrier\nresult = (\n flights_with_pct\n .group_by(\"carrier\")\n .aggregate(\"flight_count\", \"market_share\")\n)", + "sql": "WITH \"t1\" AS (\n SELECT\n *\n FROM \"memory\".\"main\".\"flights\" AS \"t0\"\n)\nSELECT\n \"t7\".\"carrier\",\n \"t7\".\"flight_count\",\n (\n CAST(\"t7\".\"flight_count\" AS DOUBLE) / CAST(\"t7\".\"flight_count_right\" AS DOUBLE)\n ) * 100 AS \"market_share\"\nFROM (\n SELECT\n \"t5\".\"carrier\",\n \"t5\".\"flight_count\",\n \"t6\".\"flight_count\" AS \"flight_count_right\"\n FROM (\n SELECT\n \"t2\".\"carrier\",\n COUNT(*) AS \"flight_count\"\n FROM \"t1\" AS \"t2\"\n GROUP BY\n 1\n ) AS \"t5\"\n CROSS JOIN (\n SELECT\n COUNT(*) AS \"flight_count\"\n FROM \"t1\" AS \"t2\"\n ) AS \"t6\"\n) AS \"t7\"", + "table": { + "columns": [ + "carrier", + "flight_count", + "market_share" + ], + "data": [ + [ + "UA", + 1, + 33.33333333333333 + ], + [ + "AA", + 1, + 33.33333333333333 + ], + [ + "DL", + 1, + 33.33333333333333 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-444a8c980d5c591c007caa61dccef4bc" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "carrier", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "carrier", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "market_share" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-444a8c980d5c591c007caa61dccef4bc": [ + { + "carrier": "AA", + "flight_count": 1, + "market_share": 33.33333333333333 + }, + { + "carrier": "UA", + "flight_count": 1, + "market_share": 33.33333333333333 + }, + { + "carrier": "DL", + "flight_count": 1, + "market_share": 33.33333333333333 + } + ] + } + } + } + }, + "setup_carriers": { + "code": "import ibis\nfrom boring_semantic_layer import to_semantic_table\n\ncon = ibis.duckdb.connect(\":memory:\")\n\n# Create carriers data\ncarriers_data = ibis.memtable({\n \"code\": [\"AA\", \"UA\", \"DL\"],\n \"name\": [\"American Airlines\", \"United Airlines\", \"Delta Air Lines\"]\n})\ncarriers_tbl = con.create_table(\"carriers\", carriers_data)", + "sql": "WITH \"t1\" AS (\n SELECT\n *\n FROM \"memory\".\"main\".\"flights\" AS \"t0\"\n)\nSELECT\n \"t7\".\"carrier\",\n \"t7\".\"flight_count\",\n (\n CAST(\"t7\".\"flight_count\" AS DOUBLE) / CAST(\"t7\".\"flight_count_right\" AS DOUBLE)\n ) * 100 AS \"market_share\"\nFROM (\n SELECT\n \"t5\".\"carrier\",\n \"t5\".\"flight_count\",\n \"t6\".\"flight_count\" AS \"flight_count_right\"\n FROM (\n SELECT\n \"t2\".\"carrier\",\n COUNT(*) AS \"flight_count\"\n FROM \"t1\" AS \"t2\"\n GROUP BY\n 1\n ) AS \"t5\"\n CROSS JOIN (\n SELECT\n COUNT(*) AS \"flight_count\"\n FROM \"t1\" AS \"t2\"\n ) AS \"t6\"\n) AS \"t7\"", + "table": { + "columns": [ + "carrier", + "flight_count", + "market_share" + ], + "data": [ + [ + "AA", + 1, + 33.33333333333333 + ], + [ + "DL", + 1, + 33.33333333333333 + ], + [ + "UA", + 1, + 33.33333333333333 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-957c443b2b52e9d52db460acc818b08e" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "carrier", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "carrier", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "market_share" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-957c443b2b52e9d52db460acc818b08e": [ + { + "carrier": "AA", + "flight_count": 1, + "market_share": 33.33333333333333 + }, + { + "carrier": "DL", + "flight_count": 1, + "market_share": 33.33333333333333 + }, + { + "carrier": "UA", + "flight_count": 1, + "market_share": 33.33333333333333 + } + ] + } + } + } + }, + "join_demo": { + "code": "# Join carriers to flights - one carrier has many flights\nflights_with_carriers = flights_st.join_many(\n carriers,\n left_on=\"carrier\",\n right_on=\"code\"\n)\n\n# Inspect available dimensions and measures\nflights_with_carriers.dimensions", + "sql": "WITH \"t1\" AS (\n SELECT\n *\n FROM \"memory\".\"main\".\"flights\" AS \"t0\"\n)\nSELECT\n \"t7\".\"carrier\",\n \"t7\".\"flight_count\",\n (\n CAST(\"t7\".\"flight_count\" AS DOUBLE) / CAST(\"t7\".\"flight_count_right\" AS DOUBLE)\n ) * 100 AS \"market_share\"\nFROM (\n SELECT\n \"t5\".\"carrier\",\n \"t5\".\"flight_count\",\n \"t6\".\"flight_count\" AS \"flight_count_right\"\n FROM (\n SELECT\n \"t2\".\"carrier\",\n COUNT(*) AS \"flight_count\"\n FROM \"t1\" AS \"t2\"\n GROUP BY\n 1\n ) AS \"t5\"\n CROSS JOIN (\n SELECT\n COUNT(*) AS \"flight_count\"\n FROM \"t1\" AS \"t2\"\n ) AS \"t6\"\n) AS \"t7\"", + "table": { + "columns": [ + "carrier", + "flight_count", + "market_share" + ], + "data": [ + [ + "AA", + 1, + 33.33333333333333 + ], + [ + "DL", + 1, + 33.33333333333333 + ], + [ + "UA", + 1, + 33.33333333333333 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-957c443b2b52e9d52db460acc818b08e" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "carrier", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "carrier", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "market_share" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-957c443b2b52e9d52db460acc818b08e": [ + { + "carrier": "AA", + "flight_count": 1, + "market_share": 33.33333333333333 + }, + { + "carrier": "DL", + "flight_count": 1, + "market_share": 33.33333333333333 + }, + { + "carrier": "UA", + "flight_count": 1, + "market_share": 33.33333333333333 + } + ] + } + } + } + } + }, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/sessionized.json b/docs/public/bsl-data/sessionized.json new file mode 100644 index 00000000..43146001 --- /dev/null +++ b/docs/public/bsl-data/sessionized.json @@ -0,0 +1,836 @@ +{ + "markdown": "# Sessionized Data\n\nAnalyze time-series events grouped into sessions based on activity gaps. This pattern identifies and aggregates user or system behavior within discrete time-bounded sessions.\n\n## Overview\n\nThe sessionization pattern allows you to:\n\n- Define session boundaries based on inactivity timeouts\n- Group sequential events into logical sessions\n- Calculate session-level metrics (duration, event count, conversion)\n- Handle session spanning across multiple time periods\n\n## Setup\n\nLet's create user activity data with timestamps:\n\n```setup_raw_data\nimport ibis\nfrom ibis import _\nfrom boring_semantic_layer import to_semantic_table\n\n# Create user activity events with minute offsets instead of timestamps\nactivity_data = ibis.memtable({\n \"user_id\": [\"user1\", \"user1\", \"user1\", \"user1\", \"user2\", \"user2\", \"user2\", \"user3\", \"user3\", \"user3\", \"user3\", \"user3\"],\n \"minute_offset\": [0, 5, 10, 45, 2, 40, 42, 1, 3, 7, 50, 52], # Minutes from start\n \"page_url\": [\"/home\", \"/products\", \"/cart\", \"/checkout\", \"/home\", \"/products\", \"/cart\",\n \"/home\", \"/about\", \"/products\", \"/home\", \"/contact\"],\n \"action\": [\"view\", \"view\", \"view\", \"purchase\", \"view\", \"view\", \"view\",\n \"view\", \"view\", \"view\", \"view\", \"view\"]\n})\n```\n\n\n\nNow create a semantic table with dimensions and measures:\n\n```semantic_table_def\nfrom boring_semantic_layer import to_semantic_table\n\nactivity_st = (\n to_semantic_table(activity_data, name=\"activity\")\n .with_dimensions(\n user_id=lambda t: t.user_id,\n minute_offset=lambda t: t.minute_offset,\n page_url=lambda t: t.page_url,\n action=lambda t: t.action\n )\n .with_measures(\n event_count=lambda t: t.count(),\n unique_users=lambda t: t.user_id.nunique()\n )\n)\n```\n\n## Identify Session Boundaries\n\nUse window functions to identify session starts based on inactivity gaps:\n\n```query_session_boundaries\nfrom ibis import _\n\nresult = (\n activity_st\n .group_by(\"user_id\", \"minute_offset\", \"page_url\", \"action\")\n .aggregate()\n .mutate(\n # Calculate time since previous event for same user\n prev_minute=lambda t: t.minute_offset.lag().over(\n group_by=\"user_id\",\n order_by=t.minute_offset\n ),\n # Calculate minutes since last event\n minutes_since_last=lambda t: t.minute_offset - t.prev_minute,\n # Mark session start (>30 min gap or first event)\n is_session_start=lambda t: (t.minutes_since_last > 30) | t.prev_minute.isnull()\n )\n .order_by(_.user_id, _.minute_offset)\n)\n```\n\n\n\n## Assign Session IDs\n\nCreate session identifiers by counting session starts:\n\n```query_with_session_ids\nfrom ibis import _\n\nresult = (\n activity_st\n .group_by(\"user_id\", \"minute_offset\", \"page_url\", \"action\")\n .aggregate()\n .mutate(\n prev_minute=lambda t: t.minute_offset.lag().over(\n group_by=\"user_id\",\n order_by=t.minute_offset\n ),\n minutes_since_last=lambda t: t.minute_offset - t.prev_minute,\n is_session_start=lambda t: (t.minutes_since_last > 30) | t.prev_minute.isnull(),\n # Cumulative sum of session starts gives session ID\n session_id=lambda t: t.is_session_start.cast(\"int32\").sum().over(\n group_by=\"user_id\",\n order_by=t.minute_offset,\n rows=(None, 0) # Cumulative sum\n )\n )\n .order_by(_.user_id, _.minute_offset)\n)\n```\n\n\n\n## Calculate Session Metrics\n\nAggregate events by session to get session-level metrics:\n\n```query_session_metrics\nfrom ibis import _\n\nresult = (\n activity_st\n .group_by(\"user_id\", \"minute_offset\", \"action\")\n .aggregate()\n .mutate(\n prev_minute=lambda t: t.minute_offset.lag().over(\n group_by=\"user_id\",\n order_by=t.minute_offset\n ),\n minutes_since_last=lambda t: t.minute_offset - t.prev_minute,\n is_session_start=lambda t: (t.minutes_since_last > 30) | t.prev_minute.isnull(),\n session_id=lambda t: t.is_session_start.cast(\"int32\").sum().over(\n group_by=\"user_id\",\n order_by=t.minute_offset,\n rows=(None, 0)\n )\n )\n .group_by(\"user_id\", \"session_id\")\n .aggregate(\n events_in_session=lambda t: t.count(),\n session_start_min=lambda t: t.minute_offset.min(),\n session_end_min=lambda t: t.minute_offset.max(),\n has_purchase=lambda t: (t.action == \"purchase\").any()\n )\n .mutate(\n session_duration_min=lambda t: (t.session_end_min - t.session_start_min)\n )\n .order_by(_.user_id, _.session_id)\n)\n```\n\n\n\n## User-Level Session Summary\n\nSummarize sessions per user:\n\n```query_user_summary\nfrom ibis import _\n\nresult = (\n activity_st\n .group_by(\"user_id\", \"minute_offset\", \"action\")\n .aggregate()\n .mutate(\n prev_minute=lambda t: t.minute_offset.lag().over(\n group_by=\"user_id\",\n order_by=t.minute_offset\n ),\n minutes_since_last=lambda t: t.minute_offset - t.prev_minute,\n is_session_start=lambda t: (t.minutes_since_last > 30) | t.prev_minute.isnull(),\n session_id=lambda t: t.is_session_start.cast(\"int32\").sum().over(\n group_by=\"user_id\",\n order_by=t.minute_offset,\n rows=(None, 0)\n )\n )\n .group_by(\"user_id\", \"session_id\")\n .aggregate(\n events_in_session=lambda t: t.count(),\n has_purchase=lambda t: (t.action == \"purchase\").any()\n )\n .group_by(\"user_id\")\n .aggregate(\n total_sessions=lambda t: t.count(),\n total_events=lambda t: t.events_in_session.sum(),\n sessions_with_purchase=lambda t: t.has_purchase.cast(\"int32\").sum(),\n avg_events_per_session=lambda t: t.events_in_session.mean().round(2)\n )\n .mutate(\n conversion_rate=lambda t: (t.sessions_with_purchase / t.total_sessions * 100).round(2)\n )\n .order_by(_.total_events.desc())\n)\n```\n\n\n\n## Use Cases\n\n**Web Analytics**: Group user page views and interactions into sessions, with a session ending after 30 minutes of inactivity. Calculate metrics like session duration, pages per session, and conversion rate.\n\n**IoT Device Monitoring**: Sessionize sensor readings to identify distinct usage periods and calculate metrics like average session length and activity intensity.\n\n**Application Usage Tracking**: Analyze how users interact with applications by grouping activities into sessions, identifying drop-off points, and measuring engagement patterns.\n\n## Key Takeaways\n\n- Use `lag()` window function to find time since previous event\n- Compare time gaps to session timeout threshold (e.g., 30 minutes)\n- Use cumulative sum of session starts to assign session IDs\n- Calculate session metrics like duration, event count, and conversions\n- Aggregate sessions to user level for summary statistics\n\n## Next Steps\n\n- Learn about [Indexing](/advanced/indexing) for trend analysis\n- Explore [Bucketing](/advanced/bucketing) to categorize session durations\n", + "queries": { + "setup_raw_data": { + "code": "import ibis\nfrom ibis import _\nfrom boring_semantic_layer import to_semantic_table\n\n# Create user activity events with minute offsets instead of timestamps\nactivity_data = ibis.memtable({\n \"user_id\": [\"user1\", \"user1\", \"user1\", \"user1\", \"user2\", \"user2\", \"user2\", \"user3\", \"user3\", \"user3\", \"user3\", \"user3\"],\n \"minute_offset\": [0, 5, 10, 45, 2, 40, 42, 1, 3, 7, 50, 52], # Minutes from start\n \"page_url\": [\"/home\", \"/products\", \"/cart\", \"/checkout\", \"/home\", \"/products\", \"/cart\",\n \"/home\", \"/about\", \"/products\", \"/home\", \"/contact\"],\n \"action\": [\"view\", \"view\", \"view\", \"purchase\", \"view\", \"view\", \"view\",\n \"view\", \"view\", \"view\", \"view\", \"view\"]\n})", + "sql": "Error generating SQL: Table.sql() missing 1 required positional argument: 'query'", + "table": { + "columns": [ + "user_id", + "minute_offset", + "page_url", + "action" + ], + "data": [ + [ + "user1", + 0, + "/home", + "view" + ], + [ + "user1", + 5, + "/products", + "view" + ], + [ + "user1", + 10, + "/cart", + "view" + ], + [ + "user1", + 45, + "/checkout", + "purchase" + ], + [ + "user2", + 2, + "/home", + "view" + ], + [ + "user2", + 40, + "/products", + "view" + ], + [ + "user2", + 42, + "/cart", + "view" + ], + [ + "user3", + 1, + "/home", + "view" + ], + [ + "user3", + 3, + "/about", + "view" + ], + [ + "user3", + 7, + "/products", + "view" + ], + [ + "user3", + 50, + "/home", + "view" + ], + [ + "user3", + 52, + "/contact", + "view" + ] + ] + } + }, + "query_session_boundaries": { + "code": "from ibis import _\n\nresult = (\n activity_st\n .group_by(\"user_id\", \"minute_offset\", \"page_url\", \"action\")\n .aggregate()\n .mutate(\n # Calculate time since previous event for same user\n prev_minute=lambda t: t.minute_offset.lag().over(\n group_by=\"user_id\",\n order_by=t.minute_offset\n ),\n # Calculate minutes since last event\n minutes_since_last=lambda t: t.minute_offset - t.prev_minute,\n # Mark session start (>30 min gap or first event)\n is_session_start=lambda t: (t.minutes_since_last > 30) | t.prev_minute.isnull()\n )\n .order_by(_.user_id, _.minute_offset)\n)", + "sql": "SELECT\n \"t4\".\"user_id\",\n \"t4\".\"minute_offset\",\n \"t4\".\"page_url\",\n \"t4\".\"action\",\n \"t4\".\"prev_minute\",\n \"t4\".\"minute_offset\" - \"t4\".\"prev_minute\" AS \"minutes_since_last\",\n (\n (\n \"t4\".\"minute_offset\" - \"t4\".\"prev_minute\"\n ) > 30\n )\n OR (\n \"t4\".\"prev_minute\" IS NULL\n ) AS \"is_session_start\"\nFROM (\n SELECT\n \"t3\".\"user_id\",\n \"t3\".\"minute_offset\",\n \"t3\".\"page_url\",\n \"t3\".\"action\",\n LAG(\"t3\".\"minute_offset\") OVER (PARTITION BY \"t3\".\"user_id\" ORDER BY \"t3\".\"minute_offset\" ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS \"prev_minute\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t1\".\"user_id\",\n \"t1\".\"minute_offset\",\n \"t1\".\"page_url\",\n \"t1\".\"action\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_m5dgklmvbfc25jmzkkksx4ffwa\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1,\n 2,\n 3,\n 4\n ) AS \"t2\"\n ) AS \"t3\"\n) AS \"t4\"\nORDER BY\n \"t4\".\"user_id\" ASC,\n \"t4\".\"minute_offset\" ASC", + "table": { + "columns": [ + "user_id", + "minute_offset", + "page_url", + "action", + "prev_minute", + "minutes_since_last", + "is_session_start" + ], + "data": [ + [ + "user1", + 0, + "/home", + "view", + null, + null, + true + ], + [ + "user1", + 5, + "/products", + "view", + 0.0, + 5.0, + false + ], + [ + "user1", + 10, + "/cart", + "view", + 5.0, + 5.0, + false + ], + [ + "user1", + 45, + "/checkout", + "purchase", + 10.0, + 35.0, + true + ], + [ + "user2", + 2, + "/home", + "view", + null, + null, + true + ], + [ + "user2", + 40, + "/products", + "view", + 2.0, + 38.0, + true + ], + [ + "user2", + 42, + "/cart", + "view", + 40.0, + 2.0, + false + ], + [ + "user3", + 1, + "/home", + "view", + null, + null, + true + ], + [ + "user3", + 3, + "/about", + "view", + 1.0, + 2.0, + false + ], + [ + "user3", + 7, + "/products", + "view", + 3.0, + 4.0, + false + ], + [ + "user3", + 50, + "/home", + "view", + 7.0, + 43.0, + true + ], + [ + "user3", + 52, + "/contact", + "view", + 50.0, + 2.0, + false + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-4efd92e9813fd3005639d7e1e17bd010" + }, + "mark": { + "type": "text" + }, + "encoding": { + "text": { + "value": "Complex query - consider custom visualization" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-4efd92e9813fd3005639d7e1e17bd010": [ + { + "user_id": "user1", + "minute_offset": 5, + "page_url": "/products", + "action": "view" + }, + { + "user_id": "user1", + "minute_offset": 10, + "page_url": "/cart", + "action": "view" + }, + { + "user_id": "user3", + "minute_offset": 1, + "page_url": "/home", + "action": "view" + }, + { + "user_id": "user1", + "minute_offset": 0, + "page_url": "/home", + "action": "view" + }, + { + "user_id": "user3", + "minute_offset": 3, + "page_url": "/about", + "action": "view" + }, + { + "user_id": "user2", + "minute_offset": 40, + "page_url": "/products", + "action": "view" + }, + { + "user_id": "user3", + "minute_offset": 7, + "page_url": "/products", + "action": "view" + }, + { + "user_id": "user1", + "minute_offset": 45, + "page_url": "/checkout", + "action": "purchase" + }, + { + "user_id": "user3", + "minute_offset": 50, + "page_url": "/home", + "action": "view" + }, + { + "user_id": "user3", + "minute_offset": 52, + "page_url": "/contact", + "action": "view" + }, + { + "user_id": "user2", + "minute_offset": 42, + "page_url": "/cart", + "action": "view" + }, + { + "user_id": "user2", + "minute_offset": 2, + "page_url": "/home", + "action": "view" + } + ] + } + } + } + }, + "query_with_session_ids": { + "code": "from ibis import _\n\nresult = (\n activity_st\n .group_by(\"user_id\", \"minute_offset\", \"page_url\", \"action\")\n .aggregate()\n .mutate(\n prev_minute=lambda t: t.minute_offset.lag().over(\n group_by=\"user_id\",\n order_by=t.minute_offset\n ),\n minutes_since_last=lambda t: t.minute_offset - t.prev_minute,\n is_session_start=lambda t: (t.minutes_since_last > 30) | t.prev_minute.isnull(),\n # Cumulative sum of session starts gives session ID\n session_id=lambda t: t.is_session_start.cast(\"int32\").sum().over(\n group_by=\"user_id\",\n order_by=t.minute_offset,\n rows=(None, 0) # Cumulative sum\n )\n )\n .order_by(_.user_id, _.minute_offset)\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t5\".\"user_id\",\n \"t5\".\"minute_offset\",\n \"t5\".\"page_url\",\n \"t5\".\"action\",\n \"t5\".\"prev_minute\",\n \"t5\".\"minutes_since_last\",\n \"t5\".\"is_session_start\",\n SUM(CAST(\"t5\".\"is_session_start\" AS INT)) OVER (PARTITION BY \"t5\".\"user_id\" ORDER BY \"t5\".\"minute_offset\" ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS \"session_id\"\n FROM (\n SELECT\n \"t4\".\"user_id\",\n \"t4\".\"minute_offset\",\n \"t4\".\"page_url\",\n \"t4\".\"action\",\n \"t4\".\"prev_minute\",\n \"t4\".\"minute_offset\" - \"t4\".\"prev_minute\" AS \"minutes_since_last\",\n (\n (\n \"t4\".\"minute_offset\" - \"t4\".\"prev_minute\"\n ) > 30\n )\n OR (\n \"t4\".\"prev_minute\" IS NULL\n ) AS \"is_session_start\"\n FROM (\n SELECT\n \"t3\".\"user_id\",\n \"t3\".\"minute_offset\",\n \"t3\".\"page_url\",\n \"t3\".\"action\",\n LAG(\"t3\".\"minute_offset\") OVER (PARTITION BY \"t3\".\"user_id\" ORDER BY \"t3\".\"minute_offset\" ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS \"prev_minute\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t1\".\"user_id\",\n \"t1\".\"minute_offset\",\n \"t1\".\"page_url\",\n \"t1\".\"action\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_m5dgklmvbfc25jmzkkksx4ffwa\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1,\n 2,\n 3,\n 4\n ) AS \"t2\"\n ) AS \"t3\"\n ) AS \"t4\"\n ) AS \"t5\"\n) AS \"t6\"\nORDER BY\n \"t6\".\"user_id\" ASC,\n \"t6\".\"minute_offset\" ASC", + "table": { + "columns": [ + "user_id", + "minute_offset", + "page_url", + "action", + "prev_minute", + "minutes_since_last", + "is_session_start", + "session_id" + ], + "data": [ + [ + "user1", + 0, + "/home", + "view", + null, + null, + true, + 1 + ], + [ + "user1", + 5, + "/products", + "view", + 0.0, + 5.0, + false, + 1 + ], + [ + "user1", + 10, + "/cart", + "view", + 5.0, + 5.0, + false, + 1 + ], + [ + "user1", + 45, + "/checkout", + "purchase", + 10.0, + 35.0, + true, + 2 + ], + [ + "user2", + 2, + "/home", + "view", + null, + null, + true, + 1 + ], + [ + "user2", + 40, + "/products", + "view", + 2.0, + 38.0, + true, + 2 + ], + [ + "user2", + 42, + "/cart", + "view", + 40.0, + 2.0, + false, + 2 + ], + [ + "user3", + 1, + "/home", + "view", + null, + null, + true, + 1 + ], + [ + "user3", + 3, + "/about", + "view", + 1.0, + 2.0, + false, + 1 + ], + [ + "user3", + 7, + "/products", + "view", + 3.0, + 4.0, + false, + 1 + ], + [ + "user3", + 50, + "/home", + "view", + 7.0, + 43.0, + true, + 2 + ], + [ + "user3", + 52, + "/contact", + "view", + 50.0, + 2.0, + false, + 2 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-8f452fa86f409e78111fcd4fad8f0703" + }, + "mark": { + "type": "text" + }, + "encoding": { + "text": { + "value": "Complex query - consider custom visualization" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-8f452fa86f409e78111fcd4fad8f0703": [ + { + "user_id": "user2", + "minute_offset": 40, + "page_url": "/products", + "action": "view" + }, + { + "user_id": "user3", + "minute_offset": 7, + "page_url": "/products", + "action": "view" + }, + { + "user_id": "user2", + "minute_offset": 2, + "page_url": "/home", + "action": "view" + }, + { + "user_id": "user1", + "minute_offset": 0, + "page_url": "/home", + "action": "view" + }, + { + "user_id": "user2", + "minute_offset": 42, + "page_url": "/cart", + "action": "view" + }, + { + "user_id": "user3", + "minute_offset": 52, + "page_url": "/contact", + "action": "view" + }, + { + "user_id": "user3", + "minute_offset": 3, + "page_url": "/about", + "action": "view" + }, + { + "user_id": "user1", + "minute_offset": 45, + "page_url": "/checkout", + "action": "purchase" + }, + { + "user_id": "user3", + "minute_offset": 50, + "page_url": "/home", + "action": "view" + }, + { + "user_id": "user1", + "minute_offset": 5, + "page_url": "/products", + "action": "view" + }, + { + "user_id": "user1", + "minute_offset": 10, + "page_url": "/cart", + "action": "view" + }, + { + "user_id": "user3", + "minute_offset": 1, + "page_url": "/home", + "action": "view" + } + ] + } + } + } + }, + "query_session_metrics": { + "code": "from ibis import _\n\nresult = (\n activity_st\n .group_by(\"user_id\", \"minute_offset\", \"action\")\n .aggregate()\n .mutate(\n prev_minute=lambda t: t.minute_offset.lag().over(\n group_by=\"user_id\",\n order_by=t.minute_offset\n ),\n minutes_since_last=lambda t: t.minute_offset - t.prev_minute,\n is_session_start=lambda t: (t.minutes_since_last > 30) | t.prev_minute.isnull(),\n session_id=lambda t: t.is_session_start.cast(\"int32\").sum().over(\n group_by=\"user_id\",\n order_by=t.minute_offset,\n rows=(None, 0)\n )\n )\n .group_by(\"user_id\", \"session_id\")\n .aggregate(\n events_in_session=lambda t: t.count(),\n session_start_min=lambda t: t.minute_offset.min(),\n session_end_min=lambda t: t.minute_offset.max(),\n has_purchase=lambda t: (t.action == \"purchase\").any()\n )\n .mutate(\n session_duration_min=lambda t: (t.session_end_min - t.session_start_min)\n )\n .order_by(_.user_id, _.session_id)\n)", + "sql": "SELECT\n \"t8\".\"user_id\",\n \"t8\".\"session_id\",\n \"t8\".\"events_in_session\",\n \"t8\".\"session_start_min\",\n \"t8\".\"session_end_min\",\n \"t8\".\"has_purchase\",\n \"t8\".\"session_end_min\" - \"t8\".\"session_start_min\" AS \"session_duration_min\"\nFROM (\n SELECT\n \"t7\".\"user_id\",\n \"t7\".\"session_id\",\n COUNT(*) AS \"events_in_session\",\n MIN(\"t7\".\"minute_offset\") AS \"session_start_min\",\n MAX(\"t7\".\"minute_offset\") AS \"session_end_min\",\n BOOL_OR(\"t7\".\"action\" = 'purchase') AS \"has_purchase\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t5\".\"user_id\",\n \"t5\".\"minute_offset\",\n \"t5\".\"action\",\n \"t5\".\"prev_minute\",\n \"t5\".\"minutes_since_last\",\n \"t5\".\"is_session_start\",\n SUM(CAST(\"t5\".\"is_session_start\" AS INT)) OVER (PARTITION BY \"t5\".\"user_id\" ORDER BY \"t5\".\"minute_offset\" ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS \"session_id\"\n FROM (\n SELECT\n \"t4\".\"user_id\",\n \"t4\".\"minute_offset\",\n \"t4\".\"action\",\n \"t4\".\"prev_minute\",\n \"t4\".\"minute_offset\" - \"t4\".\"prev_minute\" AS \"minutes_since_last\",\n (\n (\n \"t4\".\"minute_offset\" - \"t4\".\"prev_minute\"\n ) > 30\n )\n OR (\n \"t4\".\"prev_minute\" IS NULL\n ) AS \"is_session_start\"\n FROM (\n SELECT\n \"t3\".\"user_id\",\n \"t3\".\"minute_offset\",\n \"t3\".\"action\",\n LAG(\"t3\".\"minute_offset\") OVER (PARTITION BY \"t3\".\"user_id\" ORDER BY \"t3\".\"minute_offset\" ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS \"prev_minute\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t1\".\"user_id\",\n \"t1\".\"minute_offset\",\n \"t1\".\"action\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_m5dgklmvbfc25jmzkkksx4ffwa\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1,\n 2,\n 3\n ) AS \"t2\"\n ) AS \"t3\"\n ) AS \"t4\"\n ) AS \"t5\"\n ) AS \"t6\"\n ) AS \"t7\"\n GROUP BY\n 1,\n 2\n) AS \"t8\"\nORDER BY\n \"t8\".\"user_id\" ASC,\n \"t8\".\"session_id\" ASC", + "table": { + "columns": [ + "user_id", + "session_id", + "events_in_session", + "session_start_min", + "session_end_min", + "has_purchase", + "session_duration_min" + ], + "data": [ + [ + "user1", + 1, + 3, + 0, + 10, + false, + 10 + ], + [ + "user1", + 2, + 1, + 45, + 45, + true, + 0 + ], + [ + "user2", + 1, + 1, + 2, + 2, + false, + 0 + ], + [ + "user2", + 2, + 2, + 40, + 42, + false, + 2 + ], + [ + "user3", + 1, + 3, + 1, + 7, + false, + 6 + ], + [ + "user3", + 2, + 2, + 50, + 52, + false, + 2 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-905630679c171c2e6098c93976c1faaa" + }, + "mark": { + "type": "text" + }, + "encoding": { + "text": { + "value": "Complex query - consider custom visualization" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-905630679c171c2e6098c93976c1faaa": [ + { + "user_id": "user1", + "session_id": 1, + "events_in_session": 3, + "session_start_min": 0, + "session_end_min": 10, + "has_purchase": false + }, + { + "user_id": "user2", + "session_id": 1, + "events_in_session": 1, + "session_start_min": 2, + "session_end_min": 2, + "has_purchase": false + }, + { + "user_id": "user3", + "session_id": 2, + "events_in_session": 2, + "session_start_min": 50, + "session_end_min": 52, + "has_purchase": false + }, + { + "user_id": "user1", + "session_id": 2, + "events_in_session": 1, + "session_start_min": 45, + "session_end_min": 45, + "has_purchase": true + }, + { + "user_id": "user2", + "session_id": 2, + "events_in_session": 2, + "session_start_min": 40, + "session_end_min": 42, + "has_purchase": false + }, + { + "user_id": "user3", + "session_id": 1, + "events_in_session": 3, + "session_start_min": 1, + "session_end_min": 7, + "has_purchase": false + } + ] + } + } + } + }, + "query_user_summary": { + "code": "from ibis import _\n\nresult = (\n activity_st\n .group_by(\"user_id\", \"minute_offset\", \"action\")\n .aggregate()\n .mutate(\n prev_minute=lambda t: t.minute_offset.lag().over(\n group_by=\"user_id\",\n order_by=t.minute_offset\n ),\n minutes_since_last=lambda t: t.minute_offset - t.prev_minute,\n is_session_start=lambda t: (t.minutes_since_last > 30) | t.prev_minute.isnull(),\n session_id=lambda t: t.is_session_start.cast(\"int32\").sum().over(\n group_by=\"user_id\",\n order_by=t.minute_offset,\n rows=(None, 0)\n )\n )\n .group_by(\"user_id\", \"session_id\")\n .aggregate(\n events_in_session=lambda t: t.count(),\n has_purchase=lambda t: (t.action == \"purchase\").any()\n )\n .group_by(\"user_id\")\n .aggregate(\n total_sessions=lambda t: t.count(),\n total_events=lambda t: t.events_in_session.sum(),\n sessions_with_purchase=lambda t: t.has_purchase.cast(\"int32\").sum(),\n avg_events_per_session=lambda t: t.events_in_session.mean().round(2)\n )\n .mutate(\n conversion_rate=lambda t: (t.sessions_with_purchase / t.total_sessions * 100).round(2)\n )\n .order_by(_.total_events.desc())\n)", + "sql": "SELECT\n \"t10\".\"user_id\",\n \"t10\".\"total_sessions\",\n \"t10\".\"total_events\",\n \"t10\".\"sessions_with_purchase\",\n \"t10\".\"avg_events_per_session\",\n CAST(ROUND((\n \"t10\".\"sessions_with_purchase\" / \"t10\".\"total_sessions\"\n ) * 100, 2) AS DOUBLE) AS \"conversion_rate\"\nFROM (\n SELECT\n \"t9\".\"user_id\",\n COUNT(*) AS \"total_sessions\",\n SUM(\"t9\".\"events_in_session\") AS \"total_events\",\n SUM(CAST(\"t9\".\"has_purchase\" AS INT)) AS \"sessions_with_purchase\",\n CAST(ROUND(AVG(\"t9\".\"events_in_session\"), 2) AS DOUBLE) AS \"avg_events_per_session\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t7\".\"user_id\",\n \"t7\".\"session_id\",\n COUNT(*) AS \"events_in_session\",\n BOOL_OR(\"t7\".\"action\" = 'purchase') AS \"has_purchase\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t5\".\"user_id\",\n \"t5\".\"minute_offset\",\n \"t5\".\"action\",\n \"t5\".\"prev_minute\",\n \"t5\".\"minutes_since_last\",\n \"t5\".\"is_session_start\",\n SUM(CAST(\"t5\".\"is_session_start\" AS INT)) OVER (PARTITION BY \"t5\".\"user_id\" ORDER BY \"t5\".\"minute_offset\" ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS \"session_id\"\n FROM (\n SELECT\n \"t4\".\"user_id\",\n \"t4\".\"minute_offset\",\n \"t4\".\"action\",\n \"t4\".\"prev_minute\",\n \"t4\".\"minute_offset\" - \"t4\".\"prev_minute\" AS \"minutes_since_last\",\n (\n (\n \"t4\".\"minute_offset\" - \"t4\".\"prev_minute\"\n ) > 30\n )\n OR (\n \"t4\".\"prev_minute\" IS NULL\n ) AS \"is_session_start\"\n FROM (\n SELECT\n \"t3\".\"user_id\",\n \"t3\".\"minute_offset\",\n \"t3\".\"action\",\n LAG(\"t3\".\"minute_offset\") OVER (PARTITION BY \"t3\".\"user_id\" ORDER BY \"t3\".\"minute_offset\" ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS \"prev_minute\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t1\".\"user_id\",\n \"t1\".\"minute_offset\",\n \"t1\".\"action\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_m5dgklmvbfc25jmzkkksx4ffwa\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1,\n 2,\n 3\n ) AS \"t2\"\n ) AS \"t3\"\n ) AS \"t4\"\n ) AS \"t5\"\n ) AS \"t6\"\n ) AS \"t7\"\n GROUP BY\n 1,\n 2\n ) AS \"t8\"\n ) AS \"t9\"\n GROUP BY\n 1\n) AS \"t10\"\nORDER BY\n \"t10\".\"total_events\" DESC", + "table": { + "columns": [ + "user_id", + "total_sessions", + "total_events", + "sessions_with_purchase", + "avg_events_per_session", + "conversion_rate" + ], + "data": [ + [ + "user3", + 2, + 5, + 0, + 2.5, + 0.0 + ], + [ + "user1", + 2, + 4, + 1, + 2.0, + 50.0 + ], + [ + "user2", + 2, + 3, + 0, + 1.5, + 0.0 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-894a0390f023fdf105e197ff6b824ba8" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "user_id", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "user_id", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "total_sessions", + "total_events", + "sessions_with_purchase", + "avg_events_per_session" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-894a0390f023fdf105e197ff6b824ba8": [ + { + "user_id": "user2", + "total_sessions": 2, + "total_events": 3, + "sessions_with_purchase": 0, + "avg_events_per_session": 1.5 + }, + { + "user_id": "user1", + "total_sessions": 2, + "total_events": 4, + "sessions_with_purchase": 1, + "avg_events_per_session": 2.0 + }, + { + "user_id": "user3", + "total_sessions": 2, + "total_events": 5, + "sessions_with_purchase": 0, + "avg_events_per_session": 2.5 + } + ] + } + } + } + } + }, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/windowing.json b/docs/public/bsl-data/windowing.json new file mode 100644 index 00000000..e0405889 --- /dev/null +++ b/docs/public/bsl-data/windowing.json @@ -0,0 +1,3427 @@ +{ + "markdown": "# Window Functions\n\nPerform calculations across ordered rows using window functions like running totals, moving averages, rank, lag/lead, and more. Window functions operate on query results after aggregation, enabling powerful comparative and analytical operations.\n\n## Overview\n\nWindow functions allow you to:\n\n- **Compare rows**: Calculate differences between current and previous rows (lag/lead)\n- **Running calculations**: Compute cumulative sums and running averages\n- **Ranking**: Assign ranks, row numbers, and percentiles\n- **Moving windows**: Calculate metrics over sliding time windows\n\n\nWindow functions in BSL are applied using Ibis window operations on aggregated results. They execute logically after the aggregation stage.\n\n\n## Setup\n\nCreate a synthetic sales dataset with daily revenue data:\n\n```setup_data\nimport ibis\nfrom ibis import _\nfrom datetime import datetime, timedelta\nimport random\n\n# Create daily sales data spanning 90 days\nstart_date = datetime(2024, 1, 1)\ndates = [start_date + timedelta(days=i) for i in range(90)]\n\n# Generate synthetic revenue with upward trend and weekly patterns\nrandom.seed(42)\n\nrevenue_values = []\nfor i, date in enumerate(dates):\n # Base trend: increasing over time\n base = 1000 + (i * 10)\n\n # Weekly pattern: weekends have higher sales\n weekday_multiplier = 1.3 if date.weekday() >= 5 else 1.0\n\n # Random variation\n noise = random.uniform(-100, 100)\n\n revenue = base * weekday_multiplier + noise\n revenue_values.append(round(revenue, 2))\n\n# Create table\nsales_data = ibis.memtable({\n \"sale_date\": dates,\n \"revenue\": revenue_values,\n \"product_category\": [\"Electronics\" if i % 3 == 0 else \"Clothing\" if i % 3 == 1 else \"Home\" for i in range(90)],\n})\n```\n\n\n\n```setup_st\nfrom boring_semantic_layer import to_semantic_table\n\n# Create semantic table with measures\nsales_st = to_semantic_table(\n sales_data,\n name=\"daily_sales\"\n).with_measures(\n total_revenue=lambda t: t.revenue.sum(),\n avg_revenue=lambda t: t.revenue.mean(),\n sale_count=lambda t: t.count(),\n)\n```\n\n\n\n## Lag and Lead: Comparing to Previous/Next Rows\n\nCalculate period-over-period changes by comparing current values to previous rows:\n\n```query_lag_lead\nfrom ibis import _\n\n# Aggregate daily revenue\ndaily_revenue = (\n sales_st\n .group_by(\"sale_date\")\n .aggregate(\"total_revenue\")\n .order_by(\"sale_date\")\n)\n\n# Add window functions for lag/lead\nresult = daily_revenue.mutate(\n prev_day_revenue=_.total_revenue.lag(),\n next_day_revenue=_.total_revenue.lead(),\n day_over_day_change=_.total_revenue - _.total_revenue.lag(),\n pct_change=((_.total_revenue - _.total_revenue.lag()) / _.total_revenue.lag() * 100).round(2)\n).limit(10)\n```\n\n\n\n\n`lag()` accesses the previous row's value, while `lead()` accesses the next row's value. The first row's lag and last row's lead will be null.\n\n\n## Running Totals: Cumulative Calculations\n\nCompute running sums to track cumulative metrics over time:\n\n```query_running_total\nfrom ibis import _\n\n# Daily revenue with cumulative total\ndaily_revenue = (\n sales_st\n .group_by(\"sale_date\")\n .aggregate(\"total_revenue\")\n .order_by(\"sale_date\")\n)\n\n# Calculate cumulative sum and running average\nwindow_unbounded = ibis.window(rows=(None, 0), order_by=\"sale_date\")\n\nresult = daily_revenue.mutate(\n cumulative_revenue=_.total_revenue.cumsum(),\n days_count=lambda t: t.count().over(window_unbounded),\n avg_daily_so_far=lambda t: (t.cumulative_revenue / t.days_count).round(2)\n).limit(10)\n```\n\n\n\n## Moving Averages: Sliding Window Calculations\n\nCalculate metrics over a rolling window of rows:\n\n```query_moving_average\nfrom ibis import _\n\n# Daily revenue\ndaily_revenue = (\n sales_st\n .group_by(\"sale_date\")\n .aggregate(\"total_revenue\")\n .order_by(\"sale_date\")\n)\n\n# 7-day moving average\nwindow_7d = ibis.window(rows=(-6, 0), order_by=\"sale_date\")\n\nresult = daily_revenue.mutate(\n ma_7day=_.total_revenue.mean().over(window_7d).round(2),\n ma_7day_sum=_.total_revenue.sum().over(window_7d).round(2),\n).limit(10)\n```\n\n\n\n\nThe window specification `rows=(-6, 0)` means \"6 rows before the current row through the current row\" (7 total rows). The moving average smooths out daily volatility.\n\n\n## Ranking: Assign Positions\n\nRank rows based on values:\n\n```query_ranking\nfrom ibis import _\n\n# Aggregate by product category\ncategory_revenue = (\n sales_st\n .group_by(\"product_category\")\n .aggregate(\"total_revenue\", \"sale_count\")\n .order_by(_.total_revenue.desc())\n)\n\n# Add rank columns\nresult = category_revenue.mutate(\n rank=ibis.rank().over(ibis.window(order_by=_.total_revenue.desc())),\n dense_rank=ibis.dense_rank().over(ibis.window(order_by=_.total_revenue.desc())),\n row_number=ibis.row_number().over(ibis.window(order_by=_.total_revenue.desc())),\n)\n```\n\n\n\n\n`row_number()` assigns unique sequential numbers, `rank()` assigns the same rank to ties (skipping next ranks), and `dense_rank()` assigns the same rank to ties without gaps.\n\n\n## Week-over-Week Comparison\n\nCompare metrics across weekly periods:\n\n```query_week_over_week\nfrom ibis import _\n\n# Aggregate by week\nweekly_revenue = (\n sales_st\n .mutate(week_start=_.sale_date.truncate(\"W\"))\n .group_by(\"week_start\")\n .aggregate(\"total_revenue\")\n .order_by(\"week_start\")\n)\n\n# Calculate week-over-week changes\nresult = weekly_revenue.mutate(\n prev_week_revenue=_.total_revenue.lag(),\n wow_change=_.total_revenue - _.total_revenue.lag(),\n wow_pct_change=((_.total_revenue - _.total_revenue.lag()) / _.total_revenue.lag() * 100).round(2)\n).limit(10)\n```\n\n\n\n## Percent of Running Total\n\nCalculate each row's contribution to the cumulative total:\n\n```query_pct_running\nfrom ibis import _\n\n# Top 10 days by revenue\ntop_days = (\n sales_st\n .group_by(\"sale_date\")\n .aggregate(\"total_revenue\")\n .order_by(_.total_revenue.desc())\n .limit(10)\n)\n\n# Calculate cumulative percentage\nresult = top_days.mutate(\n cumulative_revenue=_.total_revenue.cumsum(),\n total_top10=_.total_revenue.sum(),\n pct_of_top10=(_.total_revenue.cumsum() / _.total_revenue.sum() * 100).round(2)\n)\n```\n\n\n\n## Moving Window with Filters\n\nCombine window functions with filtering for focused analysis:\n\n```query_window_filter\nfrom ibis import _\n\n# Focus on weekends only\nweekend_revenue = (\n sales_st\n .mutate(is_weekend=_.sale_date.day_of_week.index().isin([5, 6]))\n .filter(_.is_weekend)\n .group_by(\"sale_date\")\n .aggregate(\"total_revenue\")\n .order_by(\"sale_date\")\n)\n\n# 3-weekend moving average\nwindow_3 = ibis.window(rows=(-2, 0), order_by=\"sale_date\")\n\nresult = weekend_revenue.mutate(\n ma_3weekend=_.total_revenue.mean().over(window_3).round(2),\n prev_weekend=_.total_revenue.lag(),\n weekend_change=_.total_revenue - _.total_revenue.lag()\n).limit(10)\n```\n\n\n\n## Key Takeaways\n\n- **Window functions operate after aggregation**: They work on query results, not raw data\n- **Order matters**: Most window functions require `order_by()` for meaningful results\n- **Flexible windows**: Define windows by rows (`rows=(n, m)`) or ranges\n- **Common patterns**:\n - `lag()/lead()` for period-over-period comparisons\n - `cumsum()` for running totals\n - `.over(window)` for moving averages\n - `rank()`, `row_number()` for ranking\n- **Combine with filters**: Focus window calculations on specific subsets\n\n## Next Steps\n\n- Explore [Percentage of Total](/advanced/percentage-total) for ratio calculations\n- Learn about [Nested Subtotals](/advanced/nested-subtotals) for hierarchical aggregations\n- Check out [Nesting](/advanced/nesting) for complex data structures\n", + "queries": { + "setup_data": { + "code": "import ibis\nfrom ibis import _\nfrom datetime import datetime, timedelta\nimport random\n\n# Create daily sales data spanning 90 days\nstart_date = datetime(2024, 1, 1)\ndates = [start_date + timedelta(days=i) for i in range(90)]\n\n# Generate synthetic revenue with upward trend and weekly patterns\nrandom.seed(42)\n\nrevenue_values = []\nfor i, date in enumerate(dates):\n # Base trend: increasing over time\n base = 1000 + (i * 10)\n\n # Weekly pattern: weekends have higher sales\n weekday_multiplier = 1.3 if date.weekday() >= 5 else 1.0\n\n # Random variation\n noise = random.uniform(-100, 100)\n\n revenue = base * weekday_multiplier + noise\n revenue_values.append(round(revenue, 2))\n\n# Create table\nsales_data = ibis.memtable({\n \"sale_date\": dates,\n \"revenue\": revenue_values,\n \"product_category\": [\"Electronics\" if i % 3 == 0 else \"Clothing\" if i % 3 == 1 else \"Home\" for i in range(90)],\n})", + "sql": "Error generating SQL: Table.sql() missing 1 required positional argument: 'query'", + "table": { + "columns": [ + "sale_date", + "revenue", + "product_category" + ], + "data": [ + [ + "2024-01-01", + 1027.89, + "Electronics" + ], + [ + "2024-01-02", + 915.0, + "Clothing" + ], + [ + "2024-01-03", + 975.01, + "Home" + ], + [ + "2024-01-04", + 974.64, + "Electronics" + ], + [ + "2024-01-05", + 1087.29, + "Clothing" + ], + [ + "2024-01-06", + 1400.34, + "Home" + ], + [ + "2024-01-07", + 1456.44, + "Electronics" + ], + [ + "2024-01-08", + 987.39, + "Clothing" + ], + [ + "2024-01-09", + 1064.38, + "Home" + ], + [ + "2024-01-10", + 995.96, + "Electronics" + ], + [ + "2024-01-11", + 1043.73, + "Clothing" + ], + [ + "2024-01-12", + 1111.07, + "Home" + ], + [ + "2024-01-13", + 1361.31, + "Electronics" + ], + [ + "2024-01-14", + 1408.77, + "Clothing" + ], + [ + "2024-01-15", + 1169.98, + "Home" + ], + [ + "2024-01-16", + 1158.99, + "Electronics" + ], + [ + "2024-01-17", + 1104.09, + "Clothing" + ], + [ + "2024-01-18", + 1187.85, + "Home" + ], + [ + "2024-01-19", + 1241.89, + "Electronics" + ], + [ + "2024-01-20", + 1448.3, + "Clothing" + ], + [ + "2024-01-21", + 1621.16, + "Home" + ], + [ + "2024-01-22", + 1249.63, + "Electronics" + ], + [ + "2024-01-23", + 1188.05, + "Clothing" + ], + [ + "2024-01-24", + 1161.1, + "Home" + ], + [ + "2024-01-25", + 1331.44, + "Electronics" + ], + [ + "2024-01-26", + 1217.32, + "Clothing" + ], + [ + "2024-01-27", + 1556.55, + "Home" + ], + [ + "2024-01-28", + 1570.34, + "Electronics" + ], + [ + "2024-01-29", + 1349.5, + "Clothing" + ], + [ + "2024-01-30", + 1310.75, + "Home" + ], + [ + "2024-01-31", + 1361.43, + "Electronics" + ], + [ + "2024-02-01", + 1355.95, + "Clothing" + ], + [ + "2024-02-02", + 1327.25, + "Home" + ], + [ + "2024-02-03", + 1823.62, + "Electronics" + ], + [ + "2024-02-04", + 1717.71, + "Clothing" + ], + [ + "2024-02-05", + 1360.41, + "Home" + ], + [ + "2024-02-06", + 1425.88, + "Electronics" + ], + [ + "2024-02-07", + 1393.7, + "Clothing" + ], + [ + "2024-02-08", + 1452.34, + "Home" + ], + [ + "2024-02-09", + 1405.47, + "Electronics" + ], + [ + "2024-02-10", + 1860.91, + "Clothing" + ], + [ + "2024-02-11", + 1742.16, + "Home" + ], + [ + "2024-02-12", + 1365.58, + "Electronics" + ], + [ + "2024-02-13", + 1387.88, + "Clothing" + ], + [ + "2024-02-14", + 1355.96, + "Home" + ], + [ + "2024-02-15", + 1396.56, + "Electronics" + ], + [ + "2024-02-16", + 1380.2, + "Clothing" + ], + [ + "2024-02-17", + 1866.59, + "Home" + ], + [ + "2024-02-18", + 1951.14, + "Electronics" + ], + [ + "2024-02-19", + 1462.97, + "Clothing" + ], + [ + "2024-02-20", + 1474.04, + "Home" + ], + [ + "2024-02-21", + 1451.9, + "Electronics" + ], + [ + "2024-02-22", + 1473.4, + "Clothing" + ], + [ + "2024-02-23", + 1617.33, + "Home" + ], + [ + "2024-02-24", + 2031.61, + "Electronics" + ], + [ + "2024-02-25", + 2036.83, + "Clothing" + ], + [ + "2024-02-26", + 1494.23, + "Home" + ], + [ + "2024-02-27", + 1615.83, + "Electronics" + ], + [ + "2024-02-28", + 1512.68, + "Clothing" + ], + [ + "2024-02-29", + 1565.89, + "Home" + ], + [ + "2024-03-01", + 1697.9, + "Electronics" + ], + [ + "2024-03-02", + 2121.0, + "Clothing" + ], + [ + "2024-03-03", + 2117.39, + "Home" + ], + [ + "2024-03-04", + 1666.92, + "Electronics" + ], + [ + "2024-03-05", + 1708.57, + "Clothing" + ], + [ + "2024-03-06", + 1705.2, + "Home" + ], + [ + "2024-03-07", + 1605.81, + "Electronics" + ], + [ + "2024-03-08", + 1576.42, + "Clothing" + ], + [ + "2024-03-09", + 2147.09, + "Home" + ], + [ + "2024-03-10", + 2150.55, + "Electronics" + ], + [ + "2024-03-11", + 1642.2, + "Clothing" + ], + [ + "2024-03-12", + 1798.58, + "Home" + ], + [ + "2024-03-13", + 1795.27, + "Electronics" + ], + [ + "2024-03-14", + 1692.94, + "Clothing" + ], + [ + "2024-03-15", + 1771.09, + "Home" + ], + [ + "2024-03-16", + 2254.13, + "Electronics" + ], + [ + "2024-03-17", + 2370.91, + "Clothing" + ], + [ + "2024-03-18", + 1761.77, + "Home" + ], + [ + "2024-03-19", + 1732.98, + "Electronics" + ], + [ + "2024-03-20", + 1739.33, + "Clothing" + ], + [ + "2024-03-21", + 1812.27, + "Home" + ], + [ + "2024-03-22", + 1762.55, + "Electronics" + ], + [ + "2024-03-23", + 2382.92, + "Clothing" + ], + [ + "2024-03-24", + 2458.56, + "Home" + ], + [ + "2024-03-25", + 1819.88, + "Electronics" + ], + [ + "2024-03-26", + 1793.86, + "Clothing" + ], + [ + "2024-03-27", + 1959.51, + "Home" + ], + [ + "2024-03-28", + 1871.91, + "Electronics" + ], + [ + "2024-03-29", + 1798.18, + "Clothing" + ], + [ + "2024-03-30", + 2366.42, + "Home" + ] + ] + } + }, + "setup_st": { + "code": "from boring_semantic_layer import to_semantic_table\n\n# Create semantic table with measures\nsales_st = to_semantic_table(\n sales_data,\n name=\"daily_sales\"\n).with_measures(\n total_revenue=lambda t: t.revenue.sum(),\n avg_revenue=lambda t: t.revenue.mean(),\n sale_count=lambda t: t.count(),\n)", + "sql": "SELECT\n *\nFROM \"ibis_pandas_memtable_c3jamzcbibgevoe3fe6cdhuqwm\"", + "table": { + "columns": [ + "sale_date", + "revenue", + "product_category" + ], + "data": [ + [ + "2024-01-01", + 1027.89, + "Electronics" + ], + [ + "2024-01-02", + 915.0, + "Clothing" + ], + [ + "2024-01-03", + 975.01, + "Home" + ], + [ + "2024-01-04", + 974.64, + "Electronics" + ], + [ + "2024-01-05", + 1087.29, + "Clothing" + ], + [ + "2024-01-06", + 1400.34, + "Home" + ], + [ + "2024-01-07", + 1456.44, + "Electronics" + ], + [ + "2024-01-08", + 987.39, + "Clothing" + ], + [ + "2024-01-09", + 1064.38, + "Home" + ], + [ + "2024-01-10", + 995.96, + "Electronics" + ], + [ + "2024-01-11", + 1043.73, + "Clothing" + ], + [ + "2024-01-12", + 1111.07, + "Home" + ], + [ + "2024-01-13", + 1361.31, + "Electronics" + ], + [ + "2024-01-14", + 1408.77, + "Clothing" + ], + [ + "2024-01-15", + 1169.98, + "Home" + ], + [ + "2024-01-16", + 1158.99, + "Electronics" + ], + [ + "2024-01-17", + 1104.09, + "Clothing" + ], + [ + "2024-01-18", + 1187.85, + "Home" + ], + [ + "2024-01-19", + 1241.89, + "Electronics" + ], + [ + "2024-01-20", + 1448.3, + "Clothing" + ], + [ + "2024-01-21", + 1621.16, + "Home" + ], + [ + "2024-01-22", + 1249.63, + "Electronics" + ], + [ + "2024-01-23", + 1188.05, + "Clothing" + ], + [ + "2024-01-24", + 1161.1, + "Home" + ], + [ + "2024-01-25", + 1331.44, + "Electronics" + ], + [ + "2024-01-26", + 1217.32, + "Clothing" + ], + [ + "2024-01-27", + 1556.55, + "Home" + ], + [ + "2024-01-28", + 1570.34, + "Electronics" + ], + [ + "2024-01-29", + 1349.5, + "Clothing" + ], + [ + "2024-01-30", + 1310.75, + "Home" + ], + [ + "2024-01-31", + 1361.43, + "Electronics" + ], + [ + "2024-02-01", + 1355.95, + "Clothing" + ], + [ + "2024-02-02", + 1327.25, + "Home" + ], + [ + "2024-02-03", + 1823.62, + "Electronics" + ], + [ + "2024-02-04", + 1717.71, + "Clothing" + ], + [ + "2024-02-05", + 1360.41, + "Home" + ], + [ + "2024-02-06", + 1425.88, + "Electronics" + ], + [ + "2024-02-07", + 1393.7, + "Clothing" + ], + [ + "2024-02-08", + 1452.34, + "Home" + ], + [ + "2024-02-09", + 1405.47, + "Electronics" + ], + [ + "2024-02-10", + 1860.91, + "Clothing" + ], + [ + "2024-02-11", + 1742.16, + "Home" + ], + [ + "2024-02-12", + 1365.58, + "Electronics" + ], + [ + "2024-02-13", + 1387.88, + "Clothing" + ], + [ + "2024-02-14", + 1355.96, + "Home" + ], + [ + "2024-02-15", + 1396.56, + "Electronics" + ], + [ + "2024-02-16", + 1380.2, + "Clothing" + ], + [ + "2024-02-17", + 1866.59, + "Home" + ], + [ + "2024-02-18", + 1951.14, + "Electronics" + ], + [ + "2024-02-19", + 1462.97, + "Clothing" + ], + [ + "2024-02-20", + 1474.04, + "Home" + ], + [ + "2024-02-21", + 1451.9, + "Electronics" + ], + [ + "2024-02-22", + 1473.4, + "Clothing" + ], + [ + "2024-02-23", + 1617.33, + "Home" + ], + [ + "2024-02-24", + 2031.61, + "Electronics" + ], + [ + "2024-02-25", + 2036.83, + "Clothing" + ], + [ + "2024-02-26", + 1494.23, + "Home" + ], + [ + "2024-02-27", + 1615.83, + "Electronics" + ], + [ + "2024-02-28", + 1512.68, + "Clothing" + ], + [ + "2024-02-29", + 1565.89, + "Home" + ], + [ + "2024-03-01", + 1697.9, + "Electronics" + ], + [ + "2024-03-02", + 2121.0, + "Clothing" + ], + [ + "2024-03-03", + 2117.39, + "Home" + ], + [ + "2024-03-04", + 1666.92, + "Electronics" + ], + [ + "2024-03-05", + 1708.57, + "Clothing" + ], + [ + "2024-03-06", + 1705.2, + "Home" + ], + [ + "2024-03-07", + 1605.81, + "Electronics" + ], + [ + "2024-03-08", + 1576.42, + "Clothing" + ], + [ + "2024-03-09", + 2147.09, + "Home" + ], + [ + "2024-03-10", + 2150.55, + "Electronics" + ], + [ + "2024-03-11", + 1642.2, + "Clothing" + ], + [ + "2024-03-12", + 1798.58, + "Home" + ], + [ + "2024-03-13", + 1795.27, + "Electronics" + ], + [ + "2024-03-14", + 1692.94, + "Clothing" + ], + [ + "2024-03-15", + 1771.09, + "Home" + ], + [ + "2024-03-16", + 2254.13, + "Electronics" + ], + [ + "2024-03-17", + 2370.91, + "Clothing" + ], + [ + "2024-03-18", + 1761.77, + "Home" + ], + [ + "2024-03-19", + 1732.98, + "Electronics" + ], + [ + "2024-03-20", + 1739.33, + "Clothing" + ], + [ + "2024-03-21", + 1812.27, + "Home" + ], + [ + "2024-03-22", + 1762.55, + "Electronics" + ], + [ + "2024-03-23", + 2382.92, + "Clothing" + ], + [ + "2024-03-24", + 2458.56, + "Home" + ], + [ + "2024-03-25", + 1819.88, + "Electronics" + ], + [ + "2024-03-26", + 1793.86, + "Clothing" + ], + [ + "2024-03-27", + 1959.51, + "Home" + ], + [ + "2024-03-28", + 1871.91, + "Electronics" + ], + [ + "2024-03-29", + 1798.18, + "Clothing" + ], + [ + "2024-03-30", + 2366.42, + "Home" + ] + ] + } + }, + "query_lag_lead": { + "code": "from ibis import _\n\n# Aggregate daily revenue\ndaily_revenue = (\n sales_st\n .group_by(\"sale_date\")\n .aggregate(\"total_revenue\")\n .order_by(\"sale_date\")\n)\n\n# Add window functions for lag/lead\nresult = daily_revenue.mutate(\n prev_day_revenue=_.total_revenue.lag(),\n next_day_revenue=_.total_revenue.lead(),\n day_over_day_change=_.total_revenue - _.total_revenue.lag(),\n pct_change=((_.total_revenue - _.total_revenue.lag()) / _.total_revenue.lag() * 100).round(2)\n).limit(10)", + "sql": "SELECT\n \"t5\".\"sale_date\",\n \"t5\".\"total_revenue\",\n \"t5\".\"prev_day_revenue\",\n \"t5\".\"next_day_revenue\",\n \"t5\".\"day_over_day_change\",\n CAST(ROUND(\n (\n (\n \"t5\".\"total_revenue\" - LAG(\"t5\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)\n ) / LAG(\"t5\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)\n ) * 100,\n 2\n ) AS DOUBLE) AS \"pct_change\"\nFROM (\n SELECT\n \"t4\".\"sale_date\",\n \"t4\".\"total_revenue\",\n \"t4\".\"prev_day_revenue\",\n \"t4\".\"next_day_revenue\",\n \"t4\".\"total_revenue\" - LAG(\"t4\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS \"day_over_day_change\"\n FROM (\n SELECT\n \"t3\".\"sale_date\",\n \"t3\".\"total_revenue\",\n \"t3\".\"prev_day_revenue\",\n LEAD(\"t3\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS \"next_day_revenue\"\n FROM (\n SELECT\n \"t2\".\"sale_date\",\n \"t2\".\"total_revenue\",\n LAG(\"t2\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS \"prev_day_revenue\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t0\".\"sale_date\",\n SUM(\"t0\".\"revenue\") AS \"total_revenue\"\n FROM \"ibis_pandas_memtable_c3jamzcbibgevoe3fe6cdhuqwm\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t1\"\n ORDER BY\n \"t1\".\"sale_date\" ASC\n ) AS \"t2\"\n ) AS \"t3\"\n ) AS \"t4\"\n) AS \"t5\"\nLIMIT 10", + "table": { + "columns": [ + "sale_date", + "total_revenue", + "prev_day_revenue", + "next_day_revenue", + "day_over_day_change", + "pct_change" + ], + "data": [ + [ + "2024-01-01", + 1027.89, + null, + 915.0, + null, + null + ], + [ + "2024-01-02", + 915.0, + 1027.89, + 975.01, + -112.8900000000001, + -10.98 + ], + [ + "2024-01-03", + 975.01, + 915.0, + 974.64, + 60.00999999999999, + 6.56 + ], + [ + "2024-01-04", + 974.64, + 975.01, + 1087.29, + -0.37000000000000455, + -0.04 + ], + [ + "2024-01-05", + 1087.29, + 974.64, + 1400.34, + 112.64999999999998, + 11.56 + ], + [ + "2024-01-06", + 1400.34, + 1087.29, + 1456.44, + 313.04999999999995, + 28.79 + ], + [ + "2024-01-07", + 1456.44, + 1400.34, + 987.39, + 56.100000000000136, + 4.01 + ], + [ + "2024-01-08", + 987.39, + 1456.44, + 1064.38, + -469.05000000000007, + -32.21 + ], + [ + "2024-01-09", + 1064.38, + 987.39, + 995.96, + 76.99000000000012, + 7.8 + ], + [ + "2024-01-10", + 995.96, + 1064.38, + 1043.73, + -68.42000000000007, + -6.43 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-07aa9f04d766d23e527fcaa6415e4551" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "tooltip": [ + { + "field": "sale_date", + "type": "nominal" + }, + { + "field": "total_revenue", + "type": "quantitative" + } + ], + "x": { + "field": "sale_date", + "sort": null, + "type": "ordinal" + }, + "y": { + "field": "total_revenue", + "type": "quantitative" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-07aa9f04d766d23e527fcaa6415e4551": [ + { + "sale_date": "2024-01-11T00:00:00", + "total_revenue": 1043.73 + }, + { + "sale_date": "2024-01-27T00:00:00", + "total_revenue": 1556.55 + }, + { + "sale_date": "2024-02-17T00:00:00", + "total_revenue": 1866.59 + }, + { + "sale_date": "2024-03-25T00:00:00", + "total_revenue": 1819.88 + }, + { + "sale_date": "2024-01-18T00:00:00", + "total_revenue": 1187.85 + }, + { + "sale_date": "2024-01-19T00:00:00", + "total_revenue": 1241.89 + }, + { + "sale_date": "2024-02-02T00:00:00", + "total_revenue": 1327.25 + }, + { + "sale_date": "2024-02-06T00:00:00", + "total_revenue": 1425.88 + }, + { + "sale_date": "2024-03-06T00:00:00", + "total_revenue": 1705.2 + }, + { + "sale_date": "2024-03-23T00:00:00", + "total_revenue": 2382.92 + }, + { + "sale_date": "2024-01-16T00:00:00", + "total_revenue": 1158.99 + }, + { + "sale_date": "2024-02-01T00:00:00", + "total_revenue": 1355.95 + }, + { + "sale_date": "2024-02-19T00:00:00", + "total_revenue": 1462.97 + }, + { + "sale_date": "2024-03-03T00:00:00", + "total_revenue": 2117.39 + }, + { + "sale_date": "2024-03-04T00:00:00", + "total_revenue": 1666.92 + }, + { + "sale_date": "2024-03-11T00:00:00", + "total_revenue": 1642.2 + }, + { + "sale_date": "2024-01-15T00:00:00", + "total_revenue": 1169.98 + }, + { + "sale_date": "2024-02-16T00:00:00", + "total_revenue": 1380.2 + }, + { + "sale_date": "2024-03-02T00:00:00", + "total_revenue": 2121.0 + }, + { + "sale_date": "2024-03-10T00:00:00", + "total_revenue": 2150.55 + }, + { + "sale_date": "2024-01-10T00:00:00", + "total_revenue": 995.96 + }, + { + "sale_date": "2024-01-21T00:00:00", + "total_revenue": 1621.16 + }, + { + "sale_date": "2024-02-05T00:00:00", + "total_revenue": 1360.41 + }, + { + "sale_date": "2024-02-13T00:00:00", + "total_revenue": 1387.88 + }, + { + "sale_date": "2024-02-15T00:00:00", + "total_revenue": 1396.56 + }, + { + "sale_date": "2024-02-21T00:00:00", + "total_revenue": 1451.9 + }, + { + "sale_date": "2024-02-26T00:00:00", + "total_revenue": 1494.23 + }, + { + "sale_date": "2024-03-12T00:00:00", + "total_revenue": 1798.58 + }, + { + "sale_date": "2024-03-19T00:00:00", + "total_revenue": 1732.98 + }, + { + "sale_date": "2024-03-20T00:00:00", + "total_revenue": 1739.33 + }, + { + "sale_date": "2024-01-23T00:00:00", + "total_revenue": 1188.05 + }, + { + "sale_date": "2024-01-25T00:00:00", + "total_revenue": 1331.44 + }, + { + "sale_date": "2024-02-03T00:00:00", + "total_revenue": 1823.62 + }, + { + "sale_date": "2024-02-11T00:00:00", + "total_revenue": 1742.16 + }, + { + "sale_date": "2024-02-18T00:00:00", + "total_revenue": 1951.14 + }, + { + "sale_date": "2024-02-25T00:00:00", + "total_revenue": 2036.83 + }, + { + "sale_date": "2024-01-14T00:00:00", + "total_revenue": 1408.77 + }, + { + "sale_date": "2024-01-28T00:00:00", + "total_revenue": 1570.34 + }, + { + "sale_date": "2024-02-09T00:00:00", + "total_revenue": 1405.47 + }, + { + "sale_date": "2024-03-15T00:00:00", + "total_revenue": 1771.09 + }, + { + "sale_date": "2024-03-18T00:00:00", + "total_revenue": 1761.77 + }, + { + "sale_date": "2024-01-29T00:00:00", + "total_revenue": 1349.5 + }, + { + "sale_date": "2024-03-30T00:00:00", + "total_revenue": 2366.42 + }, + { + "sale_date": "2024-01-17T00:00:00", + "total_revenue": 1104.09 + }, + { + "sale_date": "2024-01-24T00:00:00", + "total_revenue": 1161.1 + }, + { + "sale_date": "2024-02-20T00:00:00", + "total_revenue": 1474.04 + }, + { + "sale_date": "2024-02-04T00:00:00", + "total_revenue": 1717.71 + }, + { + "sale_date": "2024-03-17T00:00:00", + "total_revenue": 2370.91 + }, + { + "sale_date": "2024-01-08T00:00:00", + "total_revenue": 987.39 + }, + { + "sale_date": "2024-01-12T00:00:00", + "total_revenue": 1111.07 + }, + { + "sale_date": "2024-01-26T00:00:00", + "total_revenue": 1217.32 + }, + { + "sale_date": "2024-02-14T00:00:00", + "total_revenue": 1355.96 + }, + { + "sale_date": "2024-02-22T00:00:00", + "total_revenue": 1473.4 + }, + { + "sale_date": "2024-03-08T00:00:00", + "total_revenue": 1576.42 + }, + { + "sale_date": "2024-03-27T00:00:00", + "total_revenue": 1959.51 + }, + { + "sale_date": "2024-01-01T00:00:00", + "total_revenue": 1027.89 + }, + { + "sale_date": "2024-01-04T00:00:00", + "total_revenue": 974.64 + }, + { + "sale_date": "2024-01-22T00:00:00", + "total_revenue": 1249.63 + }, + { + "sale_date": "2024-02-23T00:00:00", + "total_revenue": 1617.33 + }, + { + "sale_date": "2024-03-01T00:00:00", + "total_revenue": 1697.9 + }, + { + "sale_date": "2024-03-07T00:00:00", + "total_revenue": 1605.81 + }, + { + "sale_date": "2024-03-29T00:00:00", + "total_revenue": 1798.18 + }, + { + "sale_date": "2024-01-03T00:00:00", + "total_revenue": 975.01 + }, + { + "sale_date": "2024-01-09T00:00:00", + "total_revenue": 1064.38 + }, + { + "sale_date": "2024-01-20T00:00:00", + "total_revenue": 1448.3 + }, + { + "sale_date": "2024-02-07T00:00:00", + "total_revenue": 1393.7 + }, + { + "sale_date": "2024-02-08T00:00:00", + "total_revenue": 1452.34 + }, + { + "sale_date": "2024-02-24T00:00:00", + "total_revenue": 2031.61 + }, + { + "sale_date": "2024-03-26T00:00:00", + "total_revenue": 1793.86 + }, + { + "sale_date": "2024-01-05T00:00:00", + "total_revenue": 1087.29 + }, + { + "sale_date": "2024-01-07T00:00:00", + "total_revenue": 1456.44 + }, + { + "sale_date": "2024-01-30T00:00:00", + "total_revenue": 1310.75 + }, + { + "sale_date": "2024-02-10T00:00:00", + "total_revenue": 1860.91 + }, + { + "sale_date": "2024-02-12T00:00:00", + "total_revenue": 1365.58 + }, + { + "sale_date": "2024-02-28T00:00:00", + "total_revenue": 1512.68 + }, + { + "sale_date": "2024-03-13T00:00:00", + "total_revenue": 1795.27 + }, + { + "sale_date": "2024-03-14T00:00:00", + "total_revenue": 1692.94 + }, + { + "sale_date": "2024-03-21T00:00:00", + "total_revenue": 1812.27 + }, + { + "sale_date": "2024-01-02T00:00:00", + "total_revenue": 915.0 + }, + { + "sale_date": "2024-01-06T00:00:00", + "total_revenue": 1400.34 + }, + { + "sale_date": "2024-01-13T00:00:00", + "total_revenue": 1361.31 + }, + { + "sale_date": "2024-01-31T00:00:00", + "total_revenue": 1361.43 + }, + { + "sale_date": "2024-03-09T00:00:00", + "total_revenue": 2147.09 + }, + { + "sale_date": "2024-03-22T00:00:00", + "total_revenue": 1762.55 + }, + { + "sale_date": "2024-03-24T00:00:00", + "total_revenue": 2458.56 + }, + { + "sale_date": "2024-02-27T00:00:00", + "total_revenue": 1615.83 + }, + { + "sale_date": "2024-02-29T00:00:00", + "total_revenue": 1565.89 + }, + { + "sale_date": "2024-03-05T00:00:00", + "total_revenue": 1708.57 + }, + { + "sale_date": "2024-03-16T00:00:00", + "total_revenue": 2254.13 + }, + { + "sale_date": "2024-03-28T00:00:00", + "total_revenue": 1871.91 + } + ] + } + } + } + }, + "query_running_total": { + "code": "from ibis import _\n\n# Daily revenue with cumulative total\ndaily_revenue = (\n sales_st\n .group_by(\"sale_date\")\n .aggregate(\"total_revenue\")\n .order_by(\"sale_date\")\n)\n\n# Calculate cumulative sum and running average\nwindow_unbounded = ibis.window(rows=(None, 0), order_by=\"sale_date\")\n\nresult = daily_revenue.mutate(\n cumulative_revenue=_.total_revenue.cumsum(),\n days_count=lambda t: t.count().over(window_unbounded),\n avg_daily_so_far=lambda t: (t.cumulative_revenue / t.days_count).round(2)\n).limit(10)", + "sql": "SELECT\n \"t4\".\"sale_date\",\n \"t4\".\"total_revenue\",\n \"t4\".\"cumulative_revenue\",\n \"t4\".\"days_count\",\n CAST(ROUND(\"t4\".\"cumulative_revenue\" / \"t4\".\"days_count\", 2) AS DOUBLE) AS \"avg_daily_so_far\"\nFROM (\n SELECT\n \"t3\".\"sale_date\",\n \"t3\".\"total_revenue\",\n \"t3\".\"cumulative_revenue\",\n COUNT(*) OVER (ORDER BY \"t3\".\"sale_date\" ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS \"days_count\"\n FROM (\n SELECT\n \"t2\".\"sale_date\",\n \"t2\".\"total_revenue\",\n SUM(\"t2\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS \"cumulative_revenue\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t0\".\"sale_date\",\n SUM(\"t0\".\"revenue\") AS \"total_revenue\"\n FROM \"ibis_pandas_memtable_c3jamzcbibgevoe3fe6cdhuqwm\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t1\"\n ORDER BY\n \"t1\".\"sale_date\" ASC\n ) AS \"t2\"\n ) AS \"t3\"\n) AS \"t4\"\nLIMIT 10", + "table": { + "columns": [ + "sale_date", + "total_revenue", + "cumulative_revenue", + "days_count", + "avg_daily_so_far" + ], + "data": [ + [ + "2024-01-01", + 1027.89, + 1027.89, + 1, + 1027.89 + ], + [ + "2024-01-02", + 915.0, + 1942.89, + 2, + 971.45 + ], + [ + "2024-01-03", + 975.01, + 2917.9, + 3, + 972.63 + ], + [ + "2024-01-04", + 974.64, + 3892.54, + 4, + 973.14 + ], + [ + "2024-01-05", + 1087.29, + 4979.83, + 5, + 995.97 + ], + [ + "2024-01-06", + 1400.34, + 6380.17, + 6, + 1063.36 + ], + [ + "2024-01-07", + 1456.44, + 7836.610000000001, + 7, + 1119.52 + ], + [ + "2024-01-08", + 987.39, + 8824.0, + 8, + 1103.0 + ], + [ + "2024-01-09", + 1064.38, + 9888.380000000001, + 9, + 1098.71 + ], + [ + "2024-01-10", + 995.96, + 10884.34, + 10, + 1088.43 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-08ef2a96fd1acb3c89436f6dfede12e4" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "tooltip": [ + { + "field": "sale_date", + "type": "nominal" + }, + { + "field": "total_revenue", + "type": "quantitative" + } + ], + "x": { + "field": "sale_date", + "sort": null, + "type": "ordinal" + }, + "y": { + "field": "total_revenue", + "type": "quantitative" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-08ef2a96fd1acb3c89436f6dfede12e4": [ + { + "sale_date": "2024-01-11T00:00:00", + "total_revenue": 1043.73 + }, + { + "sale_date": "2024-01-27T00:00:00", + "total_revenue": 1556.55 + }, + { + "sale_date": "2024-02-17T00:00:00", + "total_revenue": 1866.59 + }, + { + "sale_date": "2024-03-25T00:00:00", + "total_revenue": 1819.88 + }, + { + "sale_date": "2024-01-17T00:00:00", + "total_revenue": 1104.09 + }, + { + "sale_date": "2024-01-24T00:00:00", + "total_revenue": 1161.1 + }, + { + "sale_date": "2024-02-20T00:00:00", + "total_revenue": 1474.04 + }, + { + "sale_date": "2024-01-16T00:00:00", + "total_revenue": 1158.99 + }, + { + "sale_date": "2024-02-01T00:00:00", + "total_revenue": 1355.95 + }, + { + "sale_date": "2024-02-19T00:00:00", + "total_revenue": 1462.97 + }, + { + "sale_date": "2024-03-03T00:00:00", + "total_revenue": 2117.39 + }, + { + "sale_date": "2024-03-04T00:00:00", + "total_revenue": 1666.92 + }, + { + "sale_date": "2024-03-11T00:00:00", + "total_revenue": 1642.2 + }, + { + "sale_date": "2024-01-15T00:00:00", + "total_revenue": 1169.98 + }, + { + "sale_date": "2024-02-16T00:00:00", + "total_revenue": 1380.2 + }, + { + "sale_date": "2024-03-02T00:00:00", + "total_revenue": 2121.0 + }, + { + "sale_date": "2024-03-10T00:00:00", + "total_revenue": 2150.55 + }, + { + "sale_date": "2024-01-05T00:00:00", + "total_revenue": 1087.29 + }, + { + "sale_date": "2024-01-07T00:00:00", + "total_revenue": 1456.44 + }, + { + "sale_date": "2024-01-30T00:00:00", + "total_revenue": 1310.75 + }, + { + "sale_date": "2024-02-10T00:00:00", + "total_revenue": 1860.91 + }, + { + "sale_date": "2024-02-12T00:00:00", + "total_revenue": 1365.58 + }, + { + "sale_date": "2024-02-28T00:00:00", + "total_revenue": 1512.68 + }, + { + "sale_date": "2024-03-13T00:00:00", + "total_revenue": 1795.27 + }, + { + "sale_date": "2024-03-14T00:00:00", + "total_revenue": 1692.94 + }, + { + "sale_date": "2024-03-21T00:00:00", + "total_revenue": 1812.27 + }, + { + "sale_date": "2024-01-03T00:00:00", + "total_revenue": 975.01 + }, + { + "sale_date": "2024-01-09T00:00:00", + "total_revenue": 1064.38 + }, + { + "sale_date": "2024-01-20T00:00:00", + "total_revenue": 1448.3 + }, + { + "sale_date": "2024-02-07T00:00:00", + "total_revenue": 1393.7 + }, + { + "sale_date": "2024-02-08T00:00:00", + "total_revenue": 1452.34 + }, + { + "sale_date": "2024-02-24T00:00:00", + "total_revenue": 2031.61 + }, + { + "sale_date": "2024-03-26T00:00:00", + "total_revenue": 1793.86 + }, + { + "sale_date": "2024-01-10T00:00:00", + "total_revenue": 995.96 + }, + { + "sale_date": "2024-01-21T00:00:00", + "total_revenue": 1621.16 + }, + { + "sale_date": "2024-02-05T00:00:00", + "total_revenue": 1360.41 + }, + { + "sale_date": "2024-02-13T00:00:00", + "total_revenue": 1387.88 + }, + { + "sale_date": "2024-02-15T00:00:00", + "total_revenue": 1396.56 + }, + { + "sale_date": "2024-02-21T00:00:00", + "total_revenue": 1451.9 + }, + { + "sale_date": "2024-02-26T00:00:00", + "total_revenue": 1494.23 + }, + { + "sale_date": "2024-03-12T00:00:00", + "total_revenue": 1798.58 + }, + { + "sale_date": "2024-03-19T00:00:00", + "total_revenue": 1732.98 + }, + { + "sale_date": "2024-03-20T00:00:00", + "total_revenue": 1739.33 + }, + { + "sale_date": "2024-01-01T00:00:00", + "total_revenue": 1027.89 + }, + { + "sale_date": "2024-01-04T00:00:00", + "total_revenue": 974.64 + }, + { + "sale_date": "2024-01-22T00:00:00", + "total_revenue": 1249.63 + }, + { + "sale_date": "2024-02-23T00:00:00", + "total_revenue": 1617.33 + }, + { + "sale_date": "2024-03-01T00:00:00", + "total_revenue": 1697.9 + }, + { + "sale_date": "2024-03-07T00:00:00", + "total_revenue": 1605.81 + }, + { + "sale_date": "2024-03-29T00:00:00", + "total_revenue": 1798.18 + }, + { + "sale_date": "2024-01-18T00:00:00", + "total_revenue": 1187.85 + }, + { + "sale_date": "2024-01-19T00:00:00", + "total_revenue": 1241.89 + }, + { + "sale_date": "2024-02-02T00:00:00", + "total_revenue": 1327.25 + }, + { + "sale_date": "2024-02-06T00:00:00", + "total_revenue": 1425.88 + }, + { + "sale_date": "2024-03-06T00:00:00", + "total_revenue": 1705.2 + }, + { + "sale_date": "2024-03-23T00:00:00", + "total_revenue": 2382.92 + }, + { + "sale_date": "2024-01-02T00:00:00", + "total_revenue": 915.0 + }, + { + "sale_date": "2024-01-06T00:00:00", + "total_revenue": 1400.34 + }, + { + "sale_date": "2024-01-13T00:00:00", + "total_revenue": 1361.31 + }, + { + "sale_date": "2024-01-31T00:00:00", + "total_revenue": 1361.43 + }, + { + "sale_date": "2024-03-09T00:00:00", + "total_revenue": 2147.09 + }, + { + "sale_date": "2024-03-22T00:00:00", + "total_revenue": 1762.55 + }, + { + "sale_date": "2024-03-24T00:00:00", + "total_revenue": 2458.56 + }, + { + "sale_date": "2024-01-23T00:00:00", + "total_revenue": 1188.05 + }, + { + "sale_date": "2024-01-25T00:00:00", + "total_revenue": 1331.44 + }, + { + "sale_date": "2024-02-03T00:00:00", + "total_revenue": 1823.62 + }, + { + "sale_date": "2024-02-11T00:00:00", + "total_revenue": 1742.16 + }, + { + "sale_date": "2024-02-18T00:00:00", + "total_revenue": 1951.14 + }, + { + "sale_date": "2024-02-25T00:00:00", + "total_revenue": 2036.83 + }, + { + "sale_date": "2024-01-14T00:00:00", + "total_revenue": 1408.77 + }, + { + "sale_date": "2024-01-28T00:00:00", + "total_revenue": 1570.34 + }, + { + "sale_date": "2024-02-09T00:00:00", + "total_revenue": 1405.47 + }, + { + "sale_date": "2024-03-15T00:00:00", + "total_revenue": 1771.09 + }, + { + "sale_date": "2024-03-18T00:00:00", + "total_revenue": 1761.77 + }, + { + "sale_date": "2024-01-29T00:00:00", + "total_revenue": 1349.5 + }, + { + "sale_date": "2024-03-30T00:00:00", + "total_revenue": 2366.42 + }, + { + "sale_date": "2024-02-04T00:00:00", + "total_revenue": 1717.71 + }, + { + "sale_date": "2024-03-17T00:00:00", + "total_revenue": 2370.91 + }, + { + "sale_date": "2024-01-08T00:00:00", + "total_revenue": 987.39 + }, + { + "sale_date": "2024-01-12T00:00:00", + "total_revenue": 1111.07 + }, + { + "sale_date": "2024-01-26T00:00:00", + "total_revenue": 1217.32 + }, + { + "sale_date": "2024-02-14T00:00:00", + "total_revenue": 1355.96 + }, + { + "sale_date": "2024-02-22T00:00:00", + "total_revenue": 1473.4 + }, + { + "sale_date": "2024-03-08T00:00:00", + "total_revenue": 1576.42 + }, + { + "sale_date": "2024-03-27T00:00:00", + "total_revenue": 1959.51 + }, + { + "sale_date": "2024-02-27T00:00:00", + "total_revenue": 1615.83 + }, + { + "sale_date": "2024-02-29T00:00:00", + "total_revenue": 1565.89 + }, + { + "sale_date": "2024-03-05T00:00:00", + "total_revenue": 1708.57 + }, + { + "sale_date": "2024-03-16T00:00:00", + "total_revenue": 2254.13 + }, + { + "sale_date": "2024-03-28T00:00:00", + "total_revenue": 1871.91 + } + ] + } + } + } + }, + "query_moving_average": { + "code": "from ibis import _\n\n# Daily revenue\ndaily_revenue = (\n sales_st\n .group_by(\"sale_date\")\n .aggregate(\"total_revenue\")\n .order_by(\"sale_date\")\n)\n\n# 7-day moving average\nwindow_7d = ibis.window(rows=(-6, 0), order_by=\"sale_date\")\n\nresult = daily_revenue.mutate(\n ma_7day=_.total_revenue.mean().over(window_7d).round(2),\n ma_7day_sum=_.total_revenue.sum().over(window_7d).round(2),\n).limit(10)", + "sql": "SELECT\n \"t3\".\"sale_date\",\n \"t3\".\"total_revenue\",\n \"t3\".\"ma_7day\",\n CAST(ROUND(\n SUM(\"t3\".\"total_revenue\") OVER (ORDER BY \"t3\".\"sale_date\" ASC ROWS BETWEEN 6 preceding AND CURRENT ROW),\n 2\n ) AS DOUBLE) AS \"ma_7day_sum\"\nFROM (\n SELECT\n \"t2\".\"sale_date\",\n \"t2\".\"total_revenue\",\n CAST(ROUND(\n AVG(\"t2\".\"total_revenue\") OVER (ORDER BY \"t2\".\"sale_date\" ASC ROWS BETWEEN 6 preceding AND CURRENT ROW),\n 2\n ) AS DOUBLE) AS \"ma_7day\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t0\".\"sale_date\",\n SUM(\"t0\".\"revenue\") AS \"total_revenue\"\n FROM \"ibis_pandas_memtable_c3jamzcbibgevoe3fe6cdhuqwm\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t1\"\n ORDER BY\n \"t1\".\"sale_date\" ASC\n ) AS \"t2\"\n) AS \"t3\"\nLIMIT 10", + "table": { + "columns": [ + "sale_date", + "total_revenue", + "ma_7day", + "ma_7day_sum" + ], + "data": [ + [ + "2024-01-01", + 1027.89, + 1027.89, + 1027.89 + ], + [ + "2024-01-02", + 915.0, + 971.45, + 1942.89 + ], + [ + "2024-01-03", + 975.01, + 972.63, + 2917.9 + ], + [ + "2024-01-04", + 974.64, + 973.14, + 3892.54 + ], + [ + "2024-01-05", + 1087.29, + 995.97, + 4979.83 + ], + [ + "2024-01-06", + 1400.34, + 1063.36, + 6380.17 + ], + [ + "2024-01-07", + 1456.44, + 1119.52, + 7836.61 + ], + [ + "2024-01-08", + 987.39, + 1113.73, + 7796.11 + ], + [ + "2024-01-09", + 1064.38, + 1135.07, + 7945.49 + ], + [ + "2024-01-10", + 995.96, + 1138.06, + 7966.44 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-c4ce13ae1e7bedc9de4eefc0197ed349" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "tooltip": [ + { + "field": "sale_date", + "type": "nominal" + }, + { + "field": "total_revenue", + "type": "quantitative" + } + ], + "x": { + "field": "sale_date", + "sort": null, + "type": "ordinal" + }, + "y": { + "field": "total_revenue", + "type": "quantitative" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-c4ce13ae1e7bedc9de4eefc0197ed349": [ + { + "sale_date": "2024-01-05T00:00:00", + "total_revenue": 1087.29 + }, + { + "sale_date": "2024-01-07T00:00:00", + "total_revenue": 1456.44 + }, + { + "sale_date": "2024-01-30T00:00:00", + "total_revenue": 1310.75 + }, + { + "sale_date": "2024-02-10T00:00:00", + "total_revenue": 1860.91 + }, + { + "sale_date": "2024-02-12T00:00:00", + "total_revenue": 1365.58 + }, + { + "sale_date": "2024-02-28T00:00:00", + "total_revenue": 1512.68 + }, + { + "sale_date": "2024-03-13T00:00:00", + "total_revenue": 1795.27 + }, + { + "sale_date": "2024-03-14T00:00:00", + "total_revenue": 1692.94 + }, + { + "sale_date": "2024-03-21T00:00:00", + "total_revenue": 1812.27 + }, + { + "sale_date": "2024-01-11T00:00:00", + "total_revenue": 1043.73 + }, + { + "sale_date": "2024-01-27T00:00:00", + "total_revenue": 1556.55 + }, + { + "sale_date": "2024-02-17T00:00:00", + "total_revenue": 1866.59 + }, + { + "sale_date": "2024-03-25T00:00:00", + "total_revenue": 1819.88 + }, + { + "sale_date": "2024-01-15T00:00:00", + "total_revenue": 1169.98 + }, + { + "sale_date": "2024-02-16T00:00:00", + "total_revenue": 1380.2 + }, + { + "sale_date": "2024-03-02T00:00:00", + "total_revenue": 2121.0 + }, + { + "sale_date": "2024-03-10T00:00:00", + "total_revenue": 2150.55 + }, + { + "sale_date": "2024-01-23T00:00:00", + "total_revenue": 1188.05 + }, + { + "sale_date": "2024-01-25T00:00:00", + "total_revenue": 1331.44 + }, + { + "sale_date": "2024-02-03T00:00:00", + "total_revenue": 1823.62 + }, + { + "sale_date": "2024-02-11T00:00:00", + "total_revenue": 1742.16 + }, + { + "sale_date": "2024-02-18T00:00:00", + "total_revenue": 1951.14 + }, + { + "sale_date": "2024-02-25T00:00:00", + "total_revenue": 2036.83 + }, + { + "sale_date": "2024-01-14T00:00:00", + "total_revenue": 1408.77 + }, + { + "sale_date": "2024-01-28T00:00:00", + "total_revenue": 1570.34 + }, + { + "sale_date": "2024-02-09T00:00:00", + "total_revenue": 1405.47 + }, + { + "sale_date": "2024-03-15T00:00:00", + "total_revenue": 1771.09 + }, + { + "sale_date": "2024-03-18T00:00:00", + "total_revenue": 1761.77 + }, + { + "sale_date": "2024-02-27T00:00:00", + "total_revenue": 1615.83 + }, + { + "sale_date": "2024-02-29T00:00:00", + "total_revenue": 1565.89 + }, + { + "sale_date": "2024-03-05T00:00:00", + "total_revenue": 1708.57 + }, + { + "sale_date": "2024-03-16T00:00:00", + "total_revenue": 2254.13 + }, + { + "sale_date": "2024-03-28T00:00:00", + "total_revenue": 1871.91 + }, + { + "sale_date": "2024-02-04T00:00:00", + "total_revenue": 1717.71 + }, + { + "sale_date": "2024-03-17T00:00:00", + "total_revenue": 2370.91 + }, + { + "sale_date": "2024-01-16T00:00:00", + "total_revenue": 1158.99 + }, + { + "sale_date": "2024-02-01T00:00:00", + "total_revenue": 1355.95 + }, + { + "sale_date": "2024-02-19T00:00:00", + "total_revenue": 1462.97 + }, + { + "sale_date": "2024-03-03T00:00:00", + "total_revenue": 2117.39 + }, + { + "sale_date": "2024-03-04T00:00:00", + "total_revenue": 1666.92 + }, + { + "sale_date": "2024-03-11T00:00:00", + "total_revenue": 1642.2 + }, + { + "sale_date": "2024-01-10T00:00:00", + "total_revenue": 995.96 + }, + { + "sale_date": "2024-01-21T00:00:00", + "total_revenue": 1621.16 + }, + { + "sale_date": "2024-02-05T00:00:00", + "total_revenue": 1360.41 + }, + { + "sale_date": "2024-02-13T00:00:00", + "total_revenue": 1387.88 + }, + { + "sale_date": "2024-02-15T00:00:00", + "total_revenue": 1396.56 + }, + { + "sale_date": "2024-02-21T00:00:00", + "total_revenue": 1451.9 + }, + { + "sale_date": "2024-02-26T00:00:00", + "total_revenue": 1494.23 + }, + { + "sale_date": "2024-03-12T00:00:00", + "total_revenue": 1798.58 + }, + { + "sale_date": "2024-03-19T00:00:00", + "total_revenue": 1732.98 + }, + { + "sale_date": "2024-03-20T00:00:00", + "total_revenue": 1739.33 + }, + { + "sale_date": "2024-01-18T00:00:00", + "total_revenue": 1187.85 + }, + { + "sale_date": "2024-01-19T00:00:00", + "total_revenue": 1241.89 + }, + { + "sale_date": "2024-02-02T00:00:00", + "total_revenue": 1327.25 + }, + { + "sale_date": "2024-02-06T00:00:00", + "total_revenue": 1425.88 + }, + { + "sale_date": "2024-03-06T00:00:00", + "total_revenue": 1705.2 + }, + { + "sale_date": "2024-03-23T00:00:00", + "total_revenue": 2382.92 + }, + { + "sale_date": "2024-01-17T00:00:00", + "total_revenue": 1104.09 + }, + { + "sale_date": "2024-01-24T00:00:00", + "total_revenue": 1161.1 + }, + { + "sale_date": "2024-02-20T00:00:00", + "total_revenue": 1474.04 + }, + { + "sale_date": "2024-01-03T00:00:00", + "total_revenue": 975.01 + }, + { + "sale_date": "2024-01-09T00:00:00", + "total_revenue": 1064.38 + }, + { + "sale_date": "2024-01-20T00:00:00", + "total_revenue": 1448.3 + }, + { + "sale_date": "2024-02-07T00:00:00", + "total_revenue": 1393.7 + }, + { + "sale_date": "2024-02-08T00:00:00", + "total_revenue": 1452.34 + }, + { + "sale_date": "2024-02-24T00:00:00", + "total_revenue": 2031.61 + }, + { + "sale_date": "2024-03-26T00:00:00", + "total_revenue": 1793.86 + }, + { + "sale_date": "2024-01-08T00:00:00", + "total_revenue": 987.39 + }, + { + "sale_date": "2024-01-12T00:00:00", + "total_revenue": 1111.07 + }, + { + "sale_date": "2024-01-26T00:00:00", + "total_revenue": 1217.32 + }, + { + "sale_date": "2024-02-14T00:00:00", + "total_revenue": 1355.96 + }, + { + "sale_date": "2024-02-22T00:00:00", + "total_revenue": 1473.4 + }, + { + "sale_date": "2024-03-08T00:00:00", + "total_revenue": 1576.42 + }, + { + "sale_date": "2024-03-27T00:00:00", + "total_revenue": 1959.51 + }, + { + "sale_date": "2024-01-01T00:00:00", + "total_revenue": 1027.89 + }, + { + "sale_date": "2024-01-04T00:00:00", + "total_revenue": 974.64 + }, + { + "sale_date": "2024-01-22T00:00:00", + "total_revenue": 1249.63 + }, + { + "sale_date": "2024-02-23T00:00:00", + "total_revenue": 1617.33 + }, + { + "sale_date": "2024-03-01T00:00:00", + "total_revenue": 1697.9 + }, + { + "sale_date": "2024-03-07T00:00:00", + "total_revenue": 1605.81 + }, + { + "sale_date": "2024-03-29T00:00:00", + "total_revenue": 1798.18 + }, + { + "sale_date": "2024-01-29T00:00:00", + "total_revenue": 1349.5 + }, + { + "sale_date": "2024-03-30T00:00:00", + "total_revenue": 2366.42 + }, + { + "sale_date": "2024-01-02T00:00:00", + "total_revenue": 915.0 + }, + { + "sale_date": "2024-01-06T00:00:00", + "total_revenue": 1400.34 + }, + { + "sale_date": "2024-01-13T00:00:00", + "total_revenue": 1361.31 + }, + { + "sale_date": "2024-01-31T00:00:00", + "total_revenue": 1361.43 + }, + { + "sale_date": "2024-03-09T00:00:00", + "total_revenue": 2147.09 + }, + { + "sale_date": "2024-03-22T00:00:00", + "total_revenue": 1762.55 + }, + { + "sale_date": "2024-03-24T00:00:00", + "total_revenue": 2458.56 + } + ] + } + } + } + }, + "query_ranking": { + "code": "from ibis import _\n\n# Aggregate by product category\ncategory_revenue = (\n sales_st\n .group_by(\"product_category\")\n .aggregate(\"total_revenue\", \"sale_count\")\n .order_by(_.total_revenue.desc())\n)\n\n# Add rank columns\nresult = category_revenue.mutate(\n rank=ibis.rank().over(ibis.window(order_by=_.total_revenue.desc())),\n dense_rank=ibis.dense_rank().over(ibis.window(order_by=_.total_revenue.desc())),\n row_number=ibis.row_number().over(ibis.window(order_by=_.total_revenue.desc())),\n)", + "sql": "SELECT\n \"t4\".\"product_category\",\n \"t4\".\"total_revenue\",\n \"t4\".\"sale_count\",\n \"t4\".\"rank\",\n \"t4\".\"dense_rank\",\n ROW_NUMBER() OVER (ORDER BY \"t4\".\"total_revenue\" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) - 1 AS \"row_number\"\nFROM (\n SELECT\n \"t3\".\"product_category\",\n \"t3\".\"total_revenue\",\n \"t3\".\"sale_count\",\n \"t3\".\"rank\",\n DENSE_RANK() OVER (ORDER BY \"t3\".\"total_revenue\" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) - 1 AS \"dense_rank\"\n FROM (\n SELECT\n \"t2\".\"product_category\",\n \"t2\".\"total_revenue\",\n \"t2\".\"sale_count\",\n RANK() OVER (ORDER BY \"t2\".\"total_revenue\" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) - 1 AS \"rank\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t0\".\"product_category\",\n SUM(\"t0\".\"revenue\") AS \"total_revenue\",\n COUNT(*) AS \"sale_count\"\n FROM \"ibis_pandas_memtable_c3jamzcbibgevoe3fe6cdhuqwm\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t1\"\n ORDER BY\n \"t1\".\"total_revenue\" DESC\n ) AS \"t2\"\n ) AS \"t3\"\n) AS \"t4\"", + "table": { + "columns": [ + "product_category", + "total_revenue", + "sale_count", + "rank", + "dense_rank", + "row_number" + ], + "data": [ + [ + "Home", + 47712.26999999998, + 30, + 0, + 0, + 0 + ], + [ + "Electronics", + 46555.450000000004, + 30, + 1, + 1, + 1 + ], + [ + "Clothing", + 46158.00000000001, + 30, + 2, + 2, + 2 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-c6a2a35853dd374222df54065804c93c" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "product_category", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "product_category", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "total_revenue", + "sale_count" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-c6a2a35853dd374222df54065804c93c": [ + { + "product_category": "Electronics", + "total_revenue": 46555.450000000004, + "sale_count": 30 + }, + { + "product_category": "Clothing", + "total_revenue": 46158.00000000001, + "sale_count": 30 + }, + { + "product_category": "Home", + "total_revenue": 47712.26999999998, + "sale_count": 30 + } + ] + } + } + } + }, + "query_week_over_week": { + "code": "from ibis import _\n\n# Aggregate by week\nweekly_revenue = (\n sales_st\n .mutate(week_start=_.sale_date.truncate(\"W\"))\n .group_by(\"week_start\")\n .aggregate(\"total_revenue\")\n .order_by(\"week_start\")\n)\n\n# Calculate week-over-week changes\nresult = weekly_revenue.mutate(\n prev_week_revenue=_.total_revenue.lag(),\n wow_change=_.total_revenue - _.total_revenue.lag(),\n wow_pct_change=((_.total_revenue - _.total_revenue.lag()) / _.total_revenue.lag() * 100).round(2)\n).limit(10)", + "sql": "SELECT\n \"t5\".\"week_start\",\n \"t5\".\"total_revenue\",\n \"t5\".\"prev_week_revenue\",\n \"t5\".\"wow_change\",\n CAST(ROUND(\n (\n (\n \"t5\".\"total_revenue\" - LAG(\"t5\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)\n ) / LAG(\"t5\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)\n ) * 100,\n 2\n ) AS DOUBLE) AS \"wow_pct_change\"\nFROM (\n SELECT\n \"t4\".\"week_start\",\n \"t4\".\"total_revenue\",\n \"t4\".\"prev_week_revenue\",\n \"t4\".\"total_revenue\" - LAG(\"t4\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS \"wow_change\"\n FROM (\n SELECT\n \"t3\".\"week_start\",\n \"t3\".\"total_revenue\",\n LAG(\"t3\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS \"prev_week_revenue\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t1\".\"week_start\",\n SUM(\"t1\".\"revenue\") AS \"total_revenue\"\n FROM (\n SELECT\n \"t0\".\"sale_date\",\n \"t0\".\"revenue\",\n \"t0\".\"product_category\",\n DATE_TRUNC('WEEK', \"t0\".\"sale_date\") AS \"week_start\"\n FROM \"ibis_pandas_memtable_c3jamzcbibgevoe3fe6cdhuqwm\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n ) AS \"t2\"\n ORDER BY\n \"t2\".\"week_start\" ASC\n ) AS \"t3\"\n ) AS \"t4\"\n) AS \"t5\"\nLIMIT 10", + "table": { + "columns": [ + "week_start", + "total_revenue", + "prev_week_revenue", + "wow_change", + "wow_pct_change" + ], + "data": [ + [ + "2024-01-01", + 7836.610000000001, + null, + null, + null + ], + [ + "2024-01-08", + 7972.610000000001, + 7836.610000000001, + 136.0, + 1.74 + ], + [ + "2024-01-15", + 8932.26, + 7972.610000000001, + 959.6499999999996, + 12.04 + ], + [ + "2024-01-22", + 9274.43, + 8932.26, + 342.1700000000001, + 3.83 + ], + [ + "2024-01-29", + 10246.21, + 9274.43, + 971.7799999999988, + 10.48 + ], + [ + "2024-02-05", + 10640.87, + 10246.21, + 394.6600000000017, + 3.85 + ], + [ + "2024-02-12", + 10703.909999999998, + 10640.87, + 63.039999999997235, + 0.59 + ], + [ + "2024-02-19", + 11548.08, + 10703.909999999998, + 844.1700000000019, + 7.89 + ], + [ + "2024-02-26", + 12124.92, + 11548.08, + 576.8400000000001, + 5.0 + ], + [ + "2024-03-04", + 12560.560000000001, + 12124.92, + 435.64000000000124, + 3.59 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-809ef63d62178fe7d51827ab65a852d3" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "tooltip": [ + { + "field": "week_start", + "type": "nominal" + }, + { + "field": "total_revenue", + "type": "quantitative" + } + ], + "x": { + "field": "week_start", + "sort": null, + "type": "ordinal" + }, + "y": { + "field": "total_revenue", + "type": "quantitative" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-809ef63d62178fe7d51827ab65a852d3": [ + { + "week_start": "2024-02-26T00:00:00", + "total_revenue": 12124.92 + }, + { + "week_start": "2024-02-05T00:00:00", + "total_revenue": 10640.87 + }, + { + "week_start": "2024-01-29T00:00:00", + "total_revenue": 10246.21 + }, + { + "week_start": "2024-03-11T00:00:00", + "total_revenue": 13325.119999999999 + }, + { + "week_start": "2024-03-25T00:00:00", + "total_revenue": 11609.76 + }, + { + "week_start": "2024-01-08T00:00:00", + "total_revenue": 7972.610000000001 + }, + { + "week_start": "2024-03-18T00:00:00", + "total_revenue": 13650.38 + }, + { + "week_start": "2024-01-01T00:00:00", + "total_revenue": 7836.610000000001 + }, + { + "week_start": "2024-01-15T00:00:00", + "total_revenue": 8932.26 + }, + { + "week_start": "2024-02-12T00:00:00", + "total_revenue": 10703.909999999998 + }, + { + "week_start": "2024-02-19T00:00:00", + "total_revenue": 11548.08 + }, + { + "week_start": "2024-01-22T00:00:00", + "total_revenue": 9274.43 + }, + { + "week_start": "2024-03-04T00:00:00", + "total_revenue": 12560.560000000001 + } + ] + } + } + } + }, + "query_pct_running": { + "code": "from ibis import _\n\n# Top 10 days by revenue\ntop_days = (\n sales_st\n .group_by(\"sale_date\")\n .aggregate(\"total_revenue\")\n .order_by(_.total_revenue.desc())\n .limit(10)\n)\n\n# Calculate cumulative percentage\nresult = top_days.mutate(\n cumulative_revenue=_.total_revenue.cumsum(),\n total_top10=_.total_revenue.sum(),\n pct_of_top10=(_.total_revenue.cumsum() / _.total_revenue.sum() * 100).round(2)\n)", + "sql": "SELECT\n \"t5\".\"sale_date\",\n \"t5\".\"total_revenue\",\n \"t5\".\"cumulative_revenue\",\n \"t5\".\"total_top10\",\n CAST(ROUND(\n (\n SUM(\"t5\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) / SUM(\"t5\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)\n ) * 100,\n 2\n ) AS DOUBLE) AS \"pct_of_top10\"\nFROM (\n SELECT\n \"t4\".\"sale_date\",\n \"t4\".\"total_revenue\",\n \"t4\".\"cumulative_revenue\",\n SUM(\"t4\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS \"total_top10\"\n FROM (\n SELECT\n \"t3\".\"sale_date\",\n \"t3\".\"total_revenue\",\n SUM(\"t3\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS \"cumulative_revenue\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t0\".\"sale_date\",\n SUM(\"t0\".\"revenue\") AS \"total_revenue\"\n FROM \"ibis_pandas_memtable_c3jamzcbibgevoe3fe6cdhuqwm\" AS \"t0\"\n GROUP BY\n 1\n ) AS \"t1\"\n ORDER BY\n \"t1\".\"total_revenue\" DESC\n LIMIT 10\n ) AS \"t3\"\n ) AS \"t4\"\n) AS \"t5\"", + "table": { + "columns": [ + "sale_date", + "total_revenue", + "cumulative_revenue", + "total_top10", + "pct_of_top10" + ], + "data": [ + [ + "2024-03-24", + 2458.56, + 2458.56, + 22405.799999999996, + 10.97 + ], + [ + "2024-03-23", + 2382.92, + 4841.48, + 22405.799999999996, + 21.61 + ], + [ + "2024-03-17", + 2370.91, + 7212.389999999999, + 22405.799999999996, + 32.19 + ], + [ + "2024-03-30", + 2366.42, + 9578.81, + 22405.799999999996, + 42.75 + ], + [ + "2024-03-16", + 2254.13, + 11832.939999999999, + 22405.799999999996, + 52.81 + ], + [ + "2024-03-10", + 2150.55, + 13983.489999999998, + 22405.799999999996, + 62.41 + ], + [ + "2024-03-09", + 2147.09, + 16130.579999999998, + 22405.799999999996, + 71.99 + ], + [ + "2024-03-02", + 2121.0, + 18251.579999999998, + 22405.799999999996, + 81.46 + ], + [ + "2024-03-03", + 2117.39, + 20368.969999999998, + 22405.799999999996, + 90.91 + ], + [ + "2024-02-25", + 2036.83, + 22405.799999999996, + 22405.799999999996, + 100.0 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-022fb4f64acbf9ea44d8964a14d59fb5" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "tooltip": [ + { + "field": "sale_date", + "type": "nominal" + }, + { + "field": "total_revenue", + "type": "quantitative" + } + ], + "x": { + "field": "sale_date", + "sort": null, + "type": "ordinal" + }, + "y": { + "field": "total_revenue", + "type": "quantitative" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-022fb4f64acbf9ea44d8964a14d59fb5": [ + { + "sale_date": "2024-01-11T00:00:00", + "total_revenue": 1043.73 + }, + { + "sale_date": "2024-01-27T00:00:00", + "total_revenue": 1556.55 + }, + { + "sale_date": "2024-02-17T00:00:00", + "total_revenue": 1866.59 + }, + { + "sale_date": "2024-03-25T00:00:00", + "total_revenue": 1819.88 + }, + { + "sale_date": "2024-01-17T00:00:00", + "total_revenue": 1104.09 + }, + { + "sale_date": "2024-01-24T00:00:00", + "total_revenue": 1161.1 + }, + { + "sale_date": "2024-02-20T00:00:00", + "total_revenue": 1474.04 + }, + { + "sale_date": "2024-01-18T00:00:00", + "total_revenue": 1187.85 + }, + { + "sale_date": "2024-01-19T00:00:00", + "total_revenue": 1241.89 + }, + { + "sale_date": "2024-02-02T00:00:00", + "total_revenue": 1327.25 + }, + { + "sale_date": "2024-02-06T00:00:00", + "total_revenue": 1425.88 + }, + { + "sale_date": "2024-03-06T00:00:00", + "total_revenue": 1705.2 + }, + { + "sale_date": "2024-03-23T00:00:00", + "total_revenue": 2382.92 + }, + { + "sale_date": "2024-01-02T00:00:00", + "total_revenue": 915.0 + }, + { + "sale_date": "2024-01-06T00:00:00", + "total_revenue": 1400.34 + }, + { + "sale_date": "2024-01-13T00:00:00", + "total_revenue": 1361.31 + }, + { + "sale_date": "2024-01-31T00:00:00", + "total_revenue": 1361.43 + }, + { + "sale_date": "2024-03-09T00:00:00", + "total_revenue": 2147.09 + }, + { + "sale_date": "2024-03-22T00:00:00", + "total_revenue": 1762.55 + }, + { + "sale_date": "2024-03-24T00:00:00", + "total_revenue": 2458.56 + }, + { + "sale_date": "2024-01-15T00:00:00", + "total_revenue": 1169.98 + }, + { + "sale_date": "2024-02-16T00:00:00", + "total_revenue": 1380.2 + }, + { + "sale_date": "2024-03-02T00:00:00", + "total_revenue": 2121.0 + }, + { + "sale_date": "2024-03-10T00:00:00", + "total_revenue": 2150.55 + }, + { + "sale_date": "2024-01-23T00:00:00", + "total_revenue": 1188.05 + }, + { + "sale_date": "2024-01-25T00:00:00", + "total_revenue": 1331.44 + }, + { + "sale_date": "2024-02-03T00:00:00", + "total_revenue": 1823.62 + }, + { + "sale_date": "2024-02-11T00:00:00", + "total_revenue": 1742.16 + }, + { + "sale_date": "2024-02-18T00:00:00", + "total_revenue": 1951.14 + }, + { + "sale_date": "2024-02-25T00:00:00", + "total_revenue": 2036.83 + }, + { + "sale_date": "2024-01-08T00:00:00", + "total_revenue": 987.39 + }, + { + "sale_date": "2024-01-12T00:00:00", + "total_revenue": 1111.07 + }, + { + "sale_date": "2024-01-26T00:00:00", + "total_revenue": 1217.32 + }, + { + "sale_date": "2024-02-14T00:00:00", + "total_revenue": 1355.96 + }, + { + "sale_date": "2024-02-22T00:00:00", + "total_revenue": 1473.4 + }, + { + "sale_date": "2024-03-08T00:00:00", + "total_revenue": 1576.42 + }, + { + "sale_date": "2024-03-27T00:00:00", + "total_revenue": 1959.51 + }, + { + "sale_date": "2024-01-05T00:00:00", + "total_revenue": 1087.29 + }, + { + "sale_date": "2024-01-07T00:00:00", + "total_revenue": 1456.44 + }, + { + "sale_date": "2024-01-30T00:00:00", + "total_revenue": 1310.75 + }, + { + "sale_date": "2024-02-10T00:00:00", + "total_revenue": 1860.91 + }, + { + "sale_date": "2024-02-12T00:00:00", + "total_revenue": 1365.58 + }, + { + "sale_date": "2024-02-28T00:00:00", + "total_revenue": 1512.68 + }, + { + "sale_date": "2024-03-13T00:00:00", + "total_revenue": 1795.27 + }, + { + "sale_date": "2024-03-14T00:00:00", + "total_revenue": 1692.94 + }, + { + "sale_date": "2024-03-21T00:00:00", + "total_revenue": 1812.27 + }, + { + "sale_date": "2024-01-03T00:00:00", + "total_revenue": 975.01 + }, + { + "sale_date": "2024-01-09T00:00:00", + "total_revenue": 1064.38 + }, + { + "sale_date": "2024-01-20T00:00:00", + "total_revenue": 1448.3 + }, + { + "sale_date": "2024-02-07T00:00:00", + "total_revenue": 1393.7 + }, + { + "sale_date": "2024-02-08T00:00:00", + "total_revenue": 1452.34 + }, + { + "sale_date": "2024-02-24T00:00:00", + "total_revenue": 2031.61 + }, + { + "sale_date": "2024-03-26T00:00:00", + "total_revenue": 1793.86 + }, + { + "sale_date": "2024-02-27T00:00:00", + "total_revenue": 1615.83 + }, + { + "sale_date": "2024-02-29T00:00:00", + "total_revenue": 1565.89 + }, + { + "sale_date": "2024-03-05T00:00:00", + "total_revenue": 1708.57 + }, + { + "sale_date": "2024-03-16T00:00:00", + "total_revenue": 2254.13 + }, + { + "sale_date": "2024-03-28T00:00:00", + "total_revenue": 1871.91 + }, + { + "sale_date": "2024-01-14T00:00:00", + "total_revenue": 1408.77 + }, + { + "sale_date": "2024-01-28T00:00:00", + "total_revenue": 1570.34 + }, + { + "sale_date": "2024-02-09T00:00:00", + "total_revenue": 1405.47 + }, + { + "sale_date": "2024-03-15T00:00:00", + "total_revenue": 1771.09 + }, + { + "sale_date": "2024-03-18T00:00:00", + "total_revenue": 1761.77 + }, + { + "sale_date": "2024-01-29T00:00:00", + "total_revenue": 1349.5 + }, + { + "sale_date": "2024-03-30T00:00:00", + "total_revenue": 2366.42 + }, + { + "sale_date": "2024-01-10T00:00:00", + "total_revenue": 995.96 + }, + { + "sale_date": "2024-01-21T00:00:00", + "total_revenue": 1621.16 + }, + { + "sale_date": "2024-02-05T00:00:00", + "total_revenue": 1360.41 + }, + { + "sale_date": "2024-02-13T00:00:00", + "total_revenue": 1387.88 + }, + { + "sale_date": "2024-02-15T00:00:00", + "total_revenue": 1396.56 + }, + { + "sale_date": "2024-02-21T00:00:00", + "total_revenue": 1451.9 + }, + { + "sale_date": "2024-02-26T00:00:00", + "total_revenue": 1494.23 + }, + { + "sale_date": "2024-03-12T00:00:00", + "total_revenue": 1798.58 + }, + { + "sale_date": "2024-03-19T00:00:00", + "total_revenue": 1732.98 + }, + { + "sale_date": "2024-03-20T00:00:00", + "total_revenue": 1739.33 + }, + { + "sale_date": "2024-02-04T00:00:00", + "total_revenue": 1717.71 + }, + { + "sale_date": "2024-03-17T00:00:00", + "total_revenue": 2370.91 + }, + { + "sale_date": "2024-01-01T00:00:00", + "total_revenue": 1027.89 + }, + { + "sale_date": "2024-01-04T00:00:00", + "total_revenue": 974.64 + }, + { + "sale_date": "2024-01-22T00:00:00", + "total_revenue": 1249.63 + }, + { + "sale_date": "2024-02-23T00:00:00", + "total_revenue": 1617.33 + }, + { + "sale_date": "2024-03-01T00:00:00", + "total_revenue": 1697.9 + }, + { + "sale_date": "2024-03-07T00:00:00", + "total_revenue": 1605.81 + }, + { + "sale_date": "2024-03-29T00:00:00", + "total_revenue": 1798.18 + }, + { + "sale_date": "2024-01-16T00:00:00", + "total_revenue": 1158.99 + }, + { + "sale_date": "2024-02-01T00:00:00", + "total_revenue": 1355.95 + }, + { + "sale_date": "2024-02-19T00:00:00", + "total_revenue": 1462.97 + }, + { + "sale_date": "2024-03-03T00:00:00", + "total_revenue": 2117.39 + }, + { + "sale_date": "2024-03-04T00:00:00", + "total_revenue": 1666.92 + }, + { + "sale_date": "2024-03-11T00:00:00", + "total_revenue": 1642.2 + } + ] + } + } + } + }, + "query_window_filter": { + "code": "from ibis import _\n\n# Focus on weekends only\nweekend_revenue = (\n sales_st\n .mutate(is_weekend=_.sale_date.day_of_week.index().isin([5, 6]))\n .filter(_.is_weekend)\n .group_by(\"sale_date\")\n .aggregate(\"total_revenue\")\n .order_by(\"sale_date\")\n)\n\n# 3-weekend moving average\nwindow_3 = ibis.window(rows=(-2, 0), order_by=\"sale_date\")\n\nresult = weekend_revenue.mutate(\n ma_3weekend=_.total_revenue.mean().over(window_3).round(2),\n prev_weekend=_.total_revenue.lag(),\n weekend_change=_.total_revenue - _.total_revenue.lag()\n).limit(10)", + "sql": "SELECT\n \"t5\".\"sale_date\",\n \"t5\".\"total_revenue\",\n \"t5\".\"ma_3weekend\",\n \"t5\".\"prev_weekend\",\n \"t5\".\"total_revenue\" - LAG(\"t5\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS \"weekend_change\"\nFROM (\n SELECT\n \"t4\".\"sale_date\",\n \"t4\".\"total_revenue\",\n \"t4\".\"ma_3weekend\",\n LAG(\"t4\".\"total_revenue\") OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS \"prev_weekend\"\n FROM (\n SELECT\n \"t3\".\"sale_date\",\n \"t3\".\"total_revenue\",\n CAST(ROUND(\n AVG(\"t3\".\"total_revenue\") OVER (ORDER BY \"t3\".\"sale_date\" ASC ROWS BETWEEN 2 preceding AND CURRENT ROW),\n 2\n ) AS DOUBLE) AS \"ma_3weekend\"\n FROM (\n SELECT\n *\n FROM (\n SELECT\n \"t1\".\"sale_date\",\n SUM(\"t1\".\"revenue\") AS \"total_revenue\"\n FROM (\n SELECT\n \"t0\".\"sale_date\",\n \"t0\".\"revenue\",\n \"t0\".\"product_category\",\n (\n DAYOFWEEK(\"t0\".\"sale_date\") + 6\n ) % 7 IN (5, 6) AS \"is_weekend\"\n FROM \"ibis_pandas_memtable_c3jamzcbibgevoe3fe6cdhuqwm\" AS \"t0\"\n WHERE\n (\n DAYOFWEEK(\"t0\".\"sale_date\") + 6\n ) % 7 IN (5, 6)\n ) AS \"t1\"\n GROUP BY\n 1\n ) AS \"t2\"\n ORDER BY\n \"t2\".\"sale_date\" ASC\n ) AS \"t3\"\n ) AS \"t4\"\n) AS \"t5\"\nLIMIT 10", + "table": { + "columns": [ + "sale_date", + "total_revenue", + "ma_3weekend", + "prev_weekend", + "weekend_change" + ], + "data": [ + [ + "2024-01-06", + 1400.34, + 1400.34, + null, + null + ], + [ + "2024-01-07", + 1456.44, + 1428.39, + 1400.34, + 56.100000000000136 + ], + [ + "2024-01-13", + 1361.31, + 1406.03, + 1456.44, + -95.13000000000011 + ], + [ + "2024-01-14", + 1408.77, + 1408.84, + 1361.31, + 47.460000000000036 + ], + [ + "2024-01-20", + 1448.3, + 1406.13, + 1408.77, + 39.52999999999997 + ], + [ + "2024-01-21", + 1621.16, + 1492.74, + 1448.3, + 172.86000000000013 + ], + [ + "2024-01-27", + 1556.55, + 1542.0, + 1621.16, + -64.61000000000013 + ], + [ + "2024-01-28", + 1570.34, + 1582.68, + 1556.55, + 13.789999999999964 + ], + [ + "2024-02-03", + 1823.62, + 1650.17, + 1570.34, + 253.27999999999997 + ], + [ + "2024-02-04", + 1717.71, + 1703.89, + 1823.62, + -105.90999999999985 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-39bc3e7f39022ebc57582c784dea92a1" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "tooltip": [ + { + "field": "sale_date", + "type": "nominal" + }, + { + "field": "total_revenue", + "type": "quantitative" + } + ], + "x": { + "field": "sale_date", + "sort": null, + "type": "ordinal" + }, + "y": { + "field": "total_revenue", + "type": "quantitative" + } + }, + "height": 400, + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-39bc3e7f39022ebc57582c784dea92a1": [ + { + "sale_date": "2024-03-16T00:00:00", + "total_revenue": 2254.13 + }, + { + "sale_date": "2024-01-07T00:00:00", + "total_revenue": 1456.44 + }, + { + "sale_date": "2024-02-10T00:00:00", + "total_revenue": 1860.91 + }, + { + "sale_date": "2024-01-21T00:00:00", + "total_revenue": 1621.16 + }, + { + "sale_date": "2024-03-03T00:00:00", + "total_revenue": 2117.39 + }, + { + "sale_date": "2024-02-03T00:00:00", + "total_revenue": 1823.62 + }, + { + "sale_date": "2024-02-11T00:00:00", + "total_revenue": 1742.16 + }, + { + "sale_date": "2024-02-18T00:00:00", + "total_revenue": 1951.14 + }, + { + "sale_date": "2024-02-25T00:00:00", + "total_revenue": 2036.83 + }, + { + "sale_date": "2024-02-04T00:00:00", + "total_revenue": 1717.71 + }, + { + "sale_date": "2024-03-17T00:00:00", + "total_revenue": 2370.91 + }, + { + "sale_date": "2024-01-14T00:00:00", + "total_revenue": 1408.77 + }, + { + "sale_date": "2024-01-28T00:00:00", + "total_revenue": 1570.34 + }, + { + "sale_date": "2024-03-30T00:00:00", + "total_revenue": 2366.42 + }, + { + "sale_date": "2024-01-27T00:00:00", + "total_revenue": 1556.55 + }, + { + "sale_date": "2024-02-17T00:00:00", + "total_revenue": 1866.59 + }, + { + "sale_date": "2024-03-02T00:00:00", + "total_revenue": 2121.0 + }, + { + "sale_date": "2024-03-10T00:00:00", + "total_revenue": 2150.55 + }, + { + "sale_date": "2024-03-23T00:00:00", + "total_revenue": 2382.92 + }, + { + "sale_date": "2024-01-20T00:00:00", + "total_revenue": 1448.3 + }, + { + "sale_date": "2024-02-24T00:00:00", + "total_revenue": 2031.61 + }, + { + "sale_date": "2024-01-06T00:00:00", + "total_revenue": 1400.34 + }, + { + "sale_date": "2024-01-13T00:00:00", + "total_revenue": 1361.31 + }, + { + "sale_date": "2024-03-09T00:00:00", + "total_revenue": 2147.09 + }, + { + "sale_date": "2024-03-24T00:00:00", + "total_revenue": 2458.56 + } + ] + } + } + } + } + }, + "files": {} +} \ No newline at end of file diff --git a/docs/public/bsl-data/yaml-config.json b/docs/public/bsl-data/yaml-config.json new file mode 100644 index 00000000..271bd4ed --- /dev/null +++ b/docs/public/bsl-data/yaml-config.json @@ -0,0 +1,125 @@ +{ + "markdown": "# YAML Configuration\n\nDefine your semantic models using YAML for better organization and maintainability.\n\n## Why YAML?\n\nYAML configuration provides several advantages:\n- **Better organization**: Keep your model definitions separate from your code\n- **Version control**: Track changes to your data model structure\n- **Collaboration**: Non-developers can review and understand the model\n- **Reusability**: Share model definitions across different projects\n\n## Expression Syntax\n\nHere's a complete example with dimensions, measures, and joins:\n\n\n\n\nIn YAML configuration, **only unbound syntax (`_`) is accepted** for expressions. Lambda expressions are not supported in YAML files.\n\n\n## Loading YAML Models\n\nIbis table objects must be created separately in Python and passed to the YAML loader. Tables are resolved by the names specified in the YAML `table` field.\n\nCreate your ibis tables:\n\n```yaml_setup\nimport ibis\n\nflights_tbl = ibis.memtable({\n \"origin\": [\"JFK\", \"LAX\", \"SFO\"],\n \"dest\": [\"LAX\", \"SFO\", \"JFK\"],\n \"carrier\": [\"AA\", \"UA\", \"DL\"],\n \"year\": [2023, 2023, 2024],\n \"distance\": [2475, 337, 382]\n})\n\ncarriers_tbl = ibis.memtable({\n \"code\": [\"AA\", \"UA\", \"DL\"],\n \"name\": [\"American Airlines\", \"United Airlines\", \"Delta Air Lines\"]\n})\n```\n\nAnd pass them to the loaded YAML file defining your Semantic Tables:\n\n\n```load_yaml_example\nfrom boring_semantic_layer import from_yaml\n\n# Load models from YAML file\nmodels = from_yaml(\n \"content/yaml_example.yaml\",\n tables={\n \"flights_tbl\": flights_tbl,\n \"carriers_tbl\": carriers_tbl\n }\n)\n\nflights_sm = models[\"flights\"]\ncarriers_sm = models[\"carriers\"]\n\n# Inspect the loaded models\nflights_sm.dimensions, flights_sm.measures\n```\n\n\n\n## Querying YAML Models\n\nYAML-defined models work exactly like Python-defined models. You can use the same `group_by()` and `aggregate()` methods to query your data.\n\n```query_yaml_model\n# Query the YAML-defined model\nresult = (\n flights_sm\n .group_by(\"origin\")\n .aggregate(\"flight_count\", \"avg_distance\")\n)\n```\n\n\n\n## Next Steps\n\n- See [Building Semantic Tables](/building/semantic-tables) for Python-based definitions\n- Learn [Query Methods](/querying/methods) for querying YAML-defined models\n- Explore [Composing Models](/building/compose) for joining YAML models\n", + "queries": { + "load_yaml_example": { + "output": [ + "('origin', 'destination', 'year', 'carrier')", + "('flight_count', 'total_distance', 'avg_distance')" + ] + }, + "query_yaml_model": { + "code": "# Query the YAML-defined model\nresult = (\n flights_sm\n .group_by(\"origin\")\n .aggregate(\"flight_count\", \"avg_distance\")\n)", + "sql": "SELECT\n *\nFROM (\n SELECT\n \"t1\".\"origin\",\n COUNT(*) AS \"flight_count\",\n AVG(\"t1\".\"distance\") AS \"avg_distance\"\n FROM (\n SELECT\n *\n FROM \"ibis_pandas_memtable_2de5zq4rjbalpkvxiyf7czm6rm\" AS \"t0\"\n ) AS \"t1\"\n GROUP BY\n 1\n) AS \"t2\"", + "table": { + "columns": [ + "origin", + "flight_count", + "avg_distance" + ], + "data": [ + [ + "SFO", + 1, + 382.0 + ], + [ + "LAX", + 1, + 337.0 + ], + [ + "JFK", + 1, + 2475.0 + ] + ] + }, + "chart": { + "type": "vega", + "spec": { + "config": { + "view": { + "continuousWidth": 300, + "continuousHeight": 300 + } + }, + "data": { + "name": "data-a056b2f336c39790a24c67d49a5c81ca" + }, + "mark": { + "type": "bar" + }, + "encoding": { + "color": { + "field": "measure", + "type": "nominal" + }, + "tooltip": [ + { + "field": "origin", + "type": "nominal" + }, + { + "field": "measure", + "type": "nominal" + }, + { + "field": "value", + "type": "quantitative" + } + ], + "x": { + "field": "origin", + "sort": null, + "type": "ordinal" + }, + "xOffset": { + "field": "measure" + }, + "y": { + "field": "value", + "type": "quantitative" + } + }, + "height": 400, + "transform": [ + { + "fold": [ + "flight_count", + "avg_distance" + ], + "as": [ + "measure", + "value" + ] + } + ], + "width": 700, + "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json", + "datasets": { + "data-a056b2f336c39790a24c67d49a5c81ca": [ + { + "origin": "LAX", + "flight_count": 1, + "avg_distance": 337.0 + }, + { + "origin": "JFK", + "flight_count": 1, + "avg_distance": 2475.0 + }, + { + "origin": "SFO", + "flight_count": 1, + "avg_distance": 382.0 + } + ] + } + } + } + } + }, + "files": { + "yaml_example.yaml": "flights:\n table: flights_tbl\n dimensions:\n origin:\n expr: _.origin\n description: \"Flight origin airport code\"\n destination:\n expr: _.dest\n description: \"Flight destination airport code\"\n year:\n expr: _.year\n description: \"Flight year\"\n carrier:\n expr: _.carrier\n description: \"Carrier code\"\n measures:\n flight_count:\n expr: _.count()\n description: \"Total number of flights\"\n total_distance:\n expr: _.distance.sum()\n description: \"Total distance traveled\"\n avg_distance:\n expr: _.distance.mean()\n description: \"Average flight distance\"\n\ncarriers:\n table: carriers_tbl\n dimensions:\n code:\n expr: _.code\n description: \"Carrier code\"\n name:\n expr: _.name\n description: \"Carrier name\"\n measures:\n carrier_count:\n expr: _.count()\n description: \"Number of carriers\"\n" + } +} \ No newline at end of file diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico new file mode 100644 index 00000000..dd5a1262 Binary files /dev/null and b/docs/public/favicon.ico differ diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg new file mode 100644 index 00000000..a91601fa --- /dev/null +++ b/docs/public/favicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + B + diff --git a/docs/public/logo.svg b/docs/public/logo.svg new file mode 100644 index 00000000..5dd27228 --- /dev/null +++ b/docs/public/logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + BSL + diff --git a/docs/public/notebooks/bucketing_with_other.html b/docs/public/notebooks/bucketing_with_other.html new file mode 100644 index 00000000..926b8c05 --- /dev/null +++ b/docs/public/notebooks/bucketing_with_other.html @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Bucketing with Other Pattern + + + + + + +
+ +
+ + + + + + + diff --git a/docs/public/notebooks/dimensional_indexing.html b/docs/public/notebooks/dimensional_indexing.html new file mode 100644 index 00000000..e1bfc394 --- /dev/null +++ b/docs/public/notebooks/dimensional_indexing.html @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dimensional Indexing Pattern + + + + + + +
+ +
+ + + + + + + diff --git a/docs/public/notebooks/nested_subtotals.html b/docs/public/notebooks/nested_subtotals.html new file mode 100644 index 00000000..75a11810 --- /dev/null +++ b/docs/public/notebooks/nested_subtotals.html @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nested Subtotals Pattern + + + + + + +
+ +
+ + + + + + + diff --git a/docs/public/notebooks/percent_of_total.html b/docs/public/notebooks/percent_of_total.html new file mode 100644 index 00000000..10ffe9b6 --- /dev/null +++ b/docs/public/notebooks/percent_of_total.html @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Percentage of Total Pattern + + + + + + +
+ +
+ + + + + + + diff --git a/docs/public/notebooks/sessionized_data.html b/docs/public/notebooks/sessionized_data.html new file mode 100644 index 00000000..4d72de5e --- /dev/null +++ b/docs/public/notebooks/sessionized_data.html @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sessionized Data Pattern + + + + + + +
+ +
+ + + + + + + diff --git a/docs/public/og-image.svg b/docs/public/og-image.svg new file mode 100644 index 00000000..13188369 --- /dev/null +++ b/docs/public/og-image.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + BSL + + + + Boring Semantic Layer + + + Lightweight Ibis-powered semantic layer for Python + + + + + Type-Safe + Pydantic Models + + + + + Flexible + Multi-Backend + + + + + Simple + Easy Integration + + diff --git a/docs/public/pages.json b/docs/public/pages.json new file mode 100644 index 00000000..43e50610 --- /dev/null +++ b/docs/public/pages.json @@ -0,0 +1 @@ +["sessionized", "reference", "semantic-table", "example", "mcp", "query-methods", "indexing", "bucketing", "getting-started", "compose", "percentage-total", "nested-subtotals", "yaml-config", "windowing", "charting"] \ No newline at end of file diff --git a/docs/public/placeholder.svg b/docs/public/placeholder.svg new file mode 100644 index 00000000..e763910b --- /dev/null +++ b/docs/public/placeholder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/public/robots.txt b/docs/public/robots.txt new file mode 100644 index 00000000..6018e701 --- /dev/null +++ b/docs/public/robots.txt @@ -0,0 +1,14 @@ +User-agent: Googlebot +Allow: / + +User-agent: Bingbot +Allow: / + +User-agent: Twitterbot +Allow: / + +User-agent: facebookexternalhit +Allow: / + +User-agent: * +Allow: / diff --git a/docs/scripts/build_data.py b/docs/scripts/build_data.py new file mode 100644 index 00000000..e531d9ed --- /dev/null +++ b/docs/scripts/build_data.py @@ -0,0 +1,658 @@ +#!/usr/bin/env python3 +""" +Build script to parse markdown files, execute BSL queries, and generate JSON data. +Evidence-style: finds code blocks with names, executes them, and makes results available. +""" + +import json +import re +import sys +from datetime import date, datetime +from decimal import Decimal +from pathlib import Path +from typing import Any + +# Add parent directory to path to import boring_semantic_layer +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +import contextlib + +import ibis +import pandas as pd + +from boring_semantic_layer import to_semantic_table + + +class CustomJSONEncoder(json.JSONEncoder): + """Custom JSON encoder to handle Decimal and datetime objects.""" + + def default(self, obj): + if isinstance(obj, Decimal): + return float(obj) + if isinstance(obj, datetime | date | pd.Timestamp): + return str(obj) + return super().default(obj) + + +def resolve_file_includes(content: str, content_dir: Path) -> tuple[str, dict[str, str]]: + """ + Resolve file includes in markdown content. + + Syntax: + + This will be kept in the markdown and the file content will be stored + separately in the "files" dict for the React component to access. + + Returns: + - Modified markdown content + - Dictionary of file_path -> file_content + """ + files = {} + pattern = r'' + + def extract_file(match): + file_path = match.group(1).strip() + full_path = content_dir / file_path + + if not full_path.exists(): + return f"" + + # Read and store file content + file_content = full_path.read_text() + files[file_path] = file_content + + # Keep the tag in markdown + return match.group(0) + + modified = re.sub(pattern, extract_file, content) + return modified, files + + +def parse_markdown_with_queries(content: str) -> tuple[str, dict[str, str], dict[str, str]]: + """ + Parse markdown content and extract BSL query blocks. + + Syntax: ```query_name + + ``` + + Or hidden: + + Returns: + - Modified markdown (with hidden blocks removed) + - Dictionary of query_name -> code + - Dictionary of query_name -> component_type (e.g., 'altairchart', 'bslquery') + """ + queries = {} + component_types = {} + + # First, handle hidden code blocks in HTML comments + hidden_pattern = r"" + + def extract_hidden_query(match): + query_name = match.group(1) + query_code = match.group(2).strip() + + # Skip if it's a language like python, sql, bash, yaml, etc. + if query_name.lower() not in [ + "python", + "sql", + "bash", + "javascript", + "typescript", + "js", + "ts", + "yaml", + "yml", + "json", + "toml", + ]: + queries[query_name] = query_code + + # Remove the comment block from markdown + return "" + + modified_md = re.sub(hidden_pattern, extract_hidden_query, content, flags=re.DOTALL) + + # Then handle visible code blocks + pattern = r"```(\w+)\n(.*?)\n```" + + def replace_query(match): + query_name = match.group(1) + query_code = match.group(2).strip() + + # Skip if it's a language like python, sql, bash, yaml, etc. + if query_name.lower() in [ + "python", + "sql", + "bash", + "javascript", + "typescript", + "js", + "ts", + "yaml", + "yml", + "json", + "toml", + ]: + return match.group(0) # Return original + + # This is a BSL query - store it but don't replace + queries[query_name] = query_code + + # Keep the code block in markdown (don't replace) + return match.group(0) + + modified_md = re.sub(pattern, replace_query, modified_md, flags=re.DOTALL) + + # Find component types by looking for component tags + component_patterns = { + "altairchart": r']+code-block="(\w+)"', + "bslquery": r']+code-block="(\w+)"', + "regularoutput": r']+code-block="(\w+)"', + "collapsedcodeblock": r']+code-block="(\w+)"', + } + + for comp_type, pattern in component_patterns.items(): + for match in re.finditer(pattern, modified_md): + block_name = match.group(1) + if block_name not in component_types: + component_types[block_name] = comp_type + + return modified_md, queries, component_types + + +def execute_bsl_query( + query_code: str, context: dict[str, Any], is_chart_only: bool = False +) -> tuple[dict[str, Any], dict[str, Any]]: + """ + Execute BSL query code and return results in a structured format. + Returns: (result_data, updated_context) + + Args: + query_code: The Python code to execute + context: The execution context with previous variables + is_chart_only: If True, only return chart spec (for altair_chart blocks) + """ + try: + # Capture print output + import io + import sys + + captured_output = io.StringIO() + old_stdout = sys.stdout + sys.stdout = captured_output + + # Create a namespace with ibis and BSL imports + namespace = { + "ibis": ibis, + "to_semantic_table": to_semantic_table, + **context, # Include any existing semantic tables + } + + # Execute the code and capture last expression + try: + # Split code into lines to check if last line is an expression + code_lines = query_code.strip().split("\n") + non_empty_lines = [ + line for line in code_lines if line.strip() and not line.strip().startswith("#") + ] + last_line = non_empty_lines[-1].strip() if non_empty_lines else "" + last_expr_result = None + has_comma_in_expr = False + + # Check if the last line is a simple expression (not an assignment or statement) + is_simple_expression = ( + last_line + and not any( + last_line.startswith(kw) + for kw in [ + "print", + "if", + "for", + "while", + "def", + "class", + "import", + "from", + "with", + "try", + "except", + "finally", + "raise", + "return", + "yield", + "pass", + "break", + "continue", + ] + ) + and "=" + not in last_line.split(".")[0] # Check assignment only in first part before dots + and not last_line.endswith((":",)) # Don't treat block starters as expressions + ) + + # Check if parentheses are balanced before the last line + if is_simple_expression: + code_without_last = "\n".join(code_lines[:-1]) + paren_count = code_without_last.count("(") - code_without_last.count(")") + bracket_count = code_without_last.count("[") - code_without_last.count("]") + brace_count = code_without_last.count("{") - code_without_last.count("}") + + # Only treat as expression if parentheses are balanced + is_simple_expression = paren_count == 0 and bracket_count == 0 and brace_count == 0 + + if is_simple_expression: + # Last line looks like a standalone expression, evaluate it separately + code_without_last = "\n".join(code_lines[:-1]) + + # Execute all code except last line + if code_without_last.strip(): + exec(code_without_last, namespace) + + # Evaluate last line as expression + try: + last_expr_result = eval(last_line, namespace) + # Mark if this was a comma-separated expression (tuple literal) + has_comma_in_expr = "," in last_line + except Exception: + # If eval fails, just execute it normally + exec(last_line, namespace) + has_comma_in_expr = False + else: + # Execute all code normally + exec(query_code, namespace) + finally: + # Restore stdout + sys.stdout = old_stdout + + # Get captured output + output = captured_output.getvalue() + + # For chart-only mode, check if last_expr_result is a chart object + if is_chart_only and last_expr_result is not None and hasattr(last_expr_result, "to_dict"): + # Check if it's an Altair Chart object + try: + if hasattr(last_expr_result, "properties"): + last_expr_result = last_expr_result.properties(width=700, height=400) + vega_spec = last_expr_result.to_dict() + # Update context with variables + updated_context = {**context} + for key, val in namespace.items(): + if not key.startswith("_") and key not in ["ibis", "to_semantic_table"]: + updated_context[key] = val + # Also store the code so it can be displayed + return {"chart_spec": vega_spec, "code": query_code}, updated_context + except Exception as e: + print(f" Warning: Could not extract chart spec from last expression: {e}") + import traceback + + traceback.print_exc() + + # If we captured a last expression result, convert it to string output + if last_expr_result is not None: + # Only split if it's a tuple AND the last line had a comma (multiple expressions) + # This way, object properties that return tuples won't be split + # Example: "var_a, var_b" → split into rows + # Example: "obj.dimensions" → single output (even if it returns a tuple) + if ( + isinstance(last_expr_result, tuple) + and has_comma_in_expr + and len(last_expr_result) > 1 + ): + # Store as array of outputs for display in separate rows + output = [str(item) for item in last_expr_result] + else: + output += str(last_expr_result) + + # If there's print output and no result variable, return the output + has_output = (isinstance(output, list) and len(output) > 0) or ( + isinstance(output, str) and len(output.strip()) > 0 + ) + if has_output: + # Check if there's also a result to execute + result = None + for var_name in ["result", "q", "query"]: + if var_name in namespace: + result = namespace[var_name] + break + + # If no result but we have output, return just the output + if result is None: + # Update context with all new variables + updated_context = {**context} + for key, val in namespace.items(): + if not key.startswith("_") and key not in ["ibis", "to_semantic_table"]: + updated_context[key] = val + + # Return output as-is (could be string or list) + output_data = output if isinstance(output, list) else output.strip() + return {"output": output_data}, updated_context + + # Get the result (assume last expression or stored in 'result' variable) + result = None + for var_name in ["result", "q", "query"]: + if var_name in namespace: + result = namespace[var_name] + break + + # If no explicit result, look for new variables + if result is None: + new_vars = { + k: v + for k, v in namespace.items() + if not k.startswith("_") + and k not in ["ibis", "to_semantic_table"] + and k not in context + } + if new_vars: + # Get the last defined variable + result = list(new_vars.values())[-1] + + if result is None and not output: + return {"error": "No result found in query"}, context + + # Update context with all new variables (for next queries) + updated_context = {**context} + for key, val in namespace.items(): + if not key.startswith("_") and key not in ["ibis", "to_semantic_table"]: + updated_context[key] = val + + # Check if it's a BSL query object (has .execute() method) + if hasattr(result, "execute"): + # Execute query to get dataframe + df = result.execute() + + # Get SQL query + sql_query = None + try: + if hasattr(result, "sql"): + sql_query = result.sql() + except Exception as e: + sql_query = f"Error generating SQL: {str(e)}" + + # Get chart spec (supports both Altair/Vega-Lite and Plotly) + chart_data = None + try: + if hasattr(result, "chart"): + # Check if code explicitly requests Plotly backend + use_plotly = ( + "# USE_PLOTLY" in query_code + or 'backend="plotly"' in query_code + or "backend='plotly'" in query_code + ) + + # Extract spec parameter from code if present + # Look for spec={...} or spec=variable_name in the .chart() call + chart_spec_param = None + + # First check if there's a chart_spec variable in namespace + if "chart_spec" in namespace: + chart_spec_param = namespace["chart_spec"] + else: + # Try to extract spec from .chart(spec=...) call + import re + + spec_match = re.search(r"\.chart\([^)]*spec=([^,)]+)", query_code) + if spec_match: + spec_expr = spec_match.group(1).strip() + # Try to evaluate the spec expression + with contextlib.suppress(Exception): + chart_spec_param = eval(spec_expr, namespace) + + if use_plotly: + # Generate Plotly chart + try: + import plotly.graph_objects as go + + if chart_spec_param: + chart_obj = result.chart(spec=chart_spec_param, backend="plotly") + else: + chart_obj = result.chart(backend="plotly") + + # Convert Plotly Figure to JSON using standard JSON encoding (not binary) + if isinstance(chart_obj, go.Figure): + # Use engine='json' to avoid binary encoding + plotly_json = chart_obj.to_json(engine="json") + chart_data = {"type": "plotly", "spec": plotly_json} + + # If this is a chart-only block, return just the chart spec + if is_chart_only: + return { + "chart_spec": plotly_json, + "chart_type": "plotly", + }, updated_context + except Exception as plotly_err: + print(f" Warning: Plotly chart generation failed: {plotly_err}") + else: + # Try Altair backend first (default) + try: + if chart_spec_param: + chart_obj = result.chart(spec=chart_spec_param, backend="altair") + else: + chart_obj = result.chart(backend="altair") + # BSL's .chart() returns an Altair Chart object + # Set width and height properties on the chart (max-w-4xl = ~896px, leave margin) + if hasattr(chart_obj, "properties"): + chart_obj = chart_obj.properties(width=700, height=400) + + vega_spec = None + if hasattr(chart_obj, "to_dict"): + vega_spec = chart_obj.to_dict() + elif hasattr(chart_obj, "spec"): + vega_spec = chart_obj.spec + elif isinstance(chart_obj, dict): + vega_spec = chart_obj + + if vega_spec: + chart_data = {"type": "vega", "spec": vega_spec} + + # If this is a chart-only block, return just the chart spec + if is_chart_only: + return {"chart_spec": vega_spec}, updated_context + except Exception: + # If Altair fails, try Plotly as fallback + try: + import plotly.graph_objects as go + + if chart_spec_param: + chart_obj = result.chart( + spec=chart_spec_param, backend="plotly" + ) + else: + chart_obj = result.chart(backend="plotly") + + # Convert Plotly Figure to JSON using standard JSON encoding (not binary) + if isinstance(chart_obj, go.Figure): + # Use engine='json' to avoid binary encoding + plotly_json = chart_obj.to_json(engine="json") + chart_data = {"type": "plotly", "spec": plotly_json} + except Exception as plotly_err: + print( + f" Warning: Both Altair and Plotly chart generation failed: {plotly_err}" + ) + except Exception as e: + print(f" Warning: Could not generate chart: {str(e)}") + + # Convert to dict format + # Convert DataFrame to JSON-serializable format + # Convert datetime and Decimal columns to avoid JSON serialization issues + df_copy = df.copy() + for col in df_copy.columns: + if df_copy[col].dtype == "datetime64[ns]" or df_copy[col].dtype.name.startswith( + "datetime" + ): + df_copy[col] = df_copy[col].astype(str) + elif df_copy[col].dtype == "object": + # Check if any value is a date/datetime/Decimal object + try: + if len(df_copy) > 0: + first_val = df_copy[col].iloc[0] + if isinstance(first_val, pd.Timestamp | datetime | date): + df_copy[col] = df_copy[col].astype(str) + elif isinstance(first_val, Decimal): + df_copy[col] = df_copy[col].apply( + lambda x: float(x) if isinstance(x, Decimal) else x + ) + except Exception: + pass + + # Convert to values.tolist() after type conversions + # Replace NaN with None for valid JSON serialization + df_copy = df_copy.replace({float("nan"): None}) + result_data = { + "code": query_code, # The original BSL query code + "sql": sql_query, + "table": {"columns": list(df_copy.columns), "data": df_copy.values.tolist()}, + } + + # Add chart if available + if chart_data: + result_data["chart"] = chart_data + + return result_data, updated_context + + # Check if it's a semantic table definition + if hasattr(result, "group_by"): + # It's a semantic table - don't execute, just store + return { + "semantic_table": True, + "name": getattr(result, "name", "unknown"), + "info": "Semantic table definition stored in context", + }, updated_context + + # For other results (like raw tables) + # Try to convert to dataframe-like structure + if hasattr(result, "to_pandas"): + df = result.to_pandas() + return { + "table": {"columns": list(df.columns), "data": df.values.tolist()} + }, updated_context + + # Handle string results (for regularoutput component) + if isinstance(result, str): + return {"output": result}, updated_context + + return {"error": "Unknown result type"}, context + + except Exception as e: + import traceback + + return {"error": str(e), "traceback": traceback.format_exc()}, context + + +def process_markdown_file(md_path: Path, output_dir: Path) -> bool: + """ + Process a single markdown file and generate JSON data. + Returns True if successful, False if any queries failed. + """ + print(f"Processing {md_path.name}...") + + # Read markdown content + content = md_path.read_text() + + # Resolve file includes first + content_dir = md_path.parent + content, files = resolve_file_includes(content, content_dir) + + # Parse and extract queries + modified_md, queries, component_types = parse_markdown_with_queries(content) + + if not queries: + print(f" No BSL queries found in {md_path.name}") + # Still save markdown-only pages + output_file = output_dir / f"{md_path.stem}.json" + output_data = {"markdown": modified_md, "queries": {}, "files": files} + output_file.write_text(json.dumps(output_data, indent=2, cls=CustomJSONEncoder)) + print(f" Saved markdown-only page to {output_file}") + return True + + print(f" Found {len(queries)} queries: {list(queries.keys())}") + + # Execute queries and collect results + results = {} + context = {} # Shared context for semantic tables and variables + has_errors = False + + for query_name, query_code in queries.items(): + print(f" Executing query: {query_name}") + # Check if this is a chart-only block based on component type + is_chart_only = component_types.get(query_name) == "altairchart" + # Pass query_code as the code parameter + result, context = execute_bsl_query(query_code, context, is_chart_only=is_chart_only) + + # Only store results for blocks that have a component tag + if query_name in component_types: + results[query_name] = result + else: + print(" (executed for context, no output component)") + + # Check if query failed + if "error" in result: + has_errors = True + print(f" ❌ ERROR in query '{query_name}': {result['error']}") + if "traceback" in result: + print(f" Traceback:\n{result['traceback']}") + + # Save to JSON even if there are errors (for debugging) + output_file = output_dir / f"{md_path.stem}.json" + output_data = {"markdown": modified_md, "queries": results, "files": files} + + output_file.write_text(json.dumps(output_data, indent=2, cls=CustomJSONEncoder)) + + if has_errors: + print(f" ⚠️ Saved to {output_file} (with errors)") + return False + else: + print(f" ✅ Saved to {output_file}") + return True + + +def main(): + """Main build script.""" + project_root = Path(__file__).parent.parent + content_dir = project_root / "content" + output_dir = project_root / "public" / "bsl-data" + + # Create output directory + output_dir.mkdir(parents=True, exist_ok=True) + + # Find all markdown files + md_files = list(content_dir.glob("*.md")) + + if not md_files: + print(f"No markdown files found in {content_dir}") + sys.exit(1) + + print(f"Found {len(md_files)} markdown files") + + # Process each file and track failures + failed_files = [] + for md_file in md_files: + success = process_markdown_file(md_file, output_dir) + if not success: + failed_files.append(md_file.name) + + # Generate pages.json (list of available pages) + pages = [f.stem for f in md_files] + pages_file = project_root / "public" / "pages.json" + pages_file.write_text(json.dumps(pages)) + + # Print summary and exit with appropriate code + if failed_files: + print(f"\n❌ Build completed with ERRORS in {len(failed_files)} file(s):") + for filename in failed_files: + print(f" - {filename}") + print(f"\nGenerated data for {len(pages)} pages, but some queries failed.") + sys.exit(1) + else: + print(f"\n✅ Build complete! Generated data for {len(pages)} pages.") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/docs/src/App.tsx b/docs/src/App.tsx new file mode 100644 index 00000000..1d2b2ef6 --- /dev/null +++ b/docs/src/App.tsx @@ -0,0 +1,146 @@ +import { Toaster } from "@/components/ui/toaster"; +import { Toaster as Sonner } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { AppSidebar } from "@/components/AppSidebar"; +import { ThemeProvider } from "next-themes"; +import { ThemeToggle } from "@/components/ThemeToggle"; +import { TableOfContents } from "@/components/TableOfContents"; +import { CommandPalette } from "@/components/CommandPalette"; +import { SearchButton } from "@/components/SearchButton"; +import { useState, useEffect } from "react"; +import Home from "./pages/Home"; +import About from "./pages/About"; +import KeyFeatures from "./pages/KeyFeatures"; +import Installation from "./pages/Installation"; +import SemanticTableDefinition from "./pages/SemanticTableDefinition"; +import Dimensions from "./pages/Dimensions"; +import Measures from "./pages/Measures"; +import Joins from "./pages/Joins"; +import ComposeModels from "./pages/ComposeModels"; +import YAMLConfig from "./pages/YAMLConfig"; +import QueryMethods from "./pages/QueryMethods"; +import MultiModelQuery from "./pages/MultiModelQuery"; +import Filtering from "./pages/Filtering"; +import NameConflicts from "./pages/NameConflicts"; +import Charting from "./pages/Charting"; +import PercentageTotal from "./pages/PercentageTotal"; +import NestedSubtotals from "./pages/NestedSubtotals"; +import Bucketing from "./pages/Bucketing"; +import Sessionized from "./pages/Sessionized"; +import Indexing from "./pages/Indexing"; +import NotFound from "./pages/NotFound"; +import BSLMarkdownPage from "./pages/BSLMarkdownPage"; +import JoinsRelationships from "./pages/JoinsRelationships"; +import SemanticTable from "./pages/SemanticTable"; +import MCP from "./pages/MCP"; +import Windowing from "./pages/Windowing"; + +const queryClient = new QueryClient(); + +const HomeLayout = ({ children }: { children: React.ReactNode }) => { + const [searchOpen, setSearchOpen] = useState(false); + + return ( +
+
+ Boring Semantic Layer +
+ setSearchOpen(true)} /> + +
+
+
+ {children} +
+ +
+ ); +}; + +const Layout = ({ children }: { children: React.ReactNode }) => { + const [searchOpen, setSearchOpen] = useState(false); + + return ( + +
+ +
+
+
+
+ + Boring Semantic Layer +
+
+ setSearchOpen(true)} /> + +
+
+ {children} +
+
+
+ +
+ ); +}; + +const App = () => ( + + + + + + + + } /> + } /> + } /> + + {/* Building Section - New Consolidated Pages */} + } /> + } /> + } /> + + {/* Legacy routes - redirect to new consolidated pages */} + } /> + } /> + } /> + } /> + } /> + } /> + {/* Querying Section */} + } /> + } /> + } /> + } /> + } /> + + {/* Legacy routes - redirect to new consolidated pages */} + } /> + } /> + } /> + } /> + {/* Advanced Section */} + } /> + } /> + } /> + } /> + } /> + + {/* Legacy route - redirect indexing to new location */} + } /> + } /> + } /> + } /> + + + + + +); + +export default App; diff --git a/docs/src/components/AdvancedPatterns.tsx b/docs/src/components/AdvancedPatterns.tsx new file mode 100644 index 00000000..95f004e2 --- /dev/null +++ b/docs/src/components/AdvancedPatterns.tsx @@ -0,0 +1,217 @@ +import { Card } from "@/components/ui/card"; + +export const AdvancedPatterns = () => { + return ( + <> +
+
+
+

Advanced Patterns

+

+ Powerful techniques for complex analytical queries +

+
+ +
+

Percentage of Total

+ +

+ Calculate percentage contributions using window functions +

+
+                {`# Define a measure that calculates percentage of total
+measures = {
+    'flight_count': lambda t: t.count(),
+    'pct_of_total': lambda t: (
+        t.count() / t.count().sum().over()
+    ) * 100
+}
+
+result = flights_sm.query(
+    dimensions=['origin'],
+    measures=['flight_count', 'pct_of_total']
+).execute()`}
+              
+
+
+
+
+ +
+
+
+

Nested Subtotals

+

+ Create hierarchical subtotals for grouped data +

+
+ + +
+              {`# Query with multiple group levels
+result = flights_sm.query(
+    dimensions=['origin', 'carrier'],
+    measures=['flight_count', 'total_distance']
+).execute()
+
+# Add subtotals using grouping sets
+from ibis import _
+
+subtotals = flights_sm.table.group_by([
+    _.origin,
+    _.carrier
+]).agg(
+    flight_count=_.count(),
+    total_distance=_.distance.sum()
+).union(
+    # Subtotals by origin only
+    flights_sm.table.group_by(_.origin).agg(
+        carrier=ibis.literal('TOTAL'),
+        flight_count=_.count(),
+        total_distance=_.distance.sum()
+    )
+)`}
+            
+
+
+
+ +
+
+
+

Bucketing with Others

+

+ Group low-frequency values into an "Others" category +

+
+ + +
+              {`# Create bucketed dimension
+dimensions = {
+    'origin': lambda t: t.origin,
+    'origin_bucketed': lambda t: t.origin.isin(['JFK', 'LGA', 'EWR']).ifelse(
+        t.origin,
+        'Others'
+    )
+}
+
+result = flights_sm.query(
+    dimensions=['origin_bucketed'],
+    measures=['flight_count']
+).execute()`}
+            
+
+
+
+ +
+
+
+

Sessionized Data

+

+ Analyze user sessions and event sequences +

+
+ + +

+ Create session IDs based on time gaps between events +

+
+              {`# Define session dimension using window functions
+dimensions = {
+    'user_id': lambda t: t.user_id,
+    'session_id': lambda t: (
+        (t.timestamp - t.timestamp.lag().over(
+            ibis.window(group_by='user_id', order_by='timestamp')
+        ) > ibis.interval(minutes=30))
+        .cast('int32')
+        .sum()
+        .over(ibis.window(group_by='user_id', order_by='timestamp'))
+    )
+}
+
+measures = {
+    'session_count': lambda t: t.session_id.nunique(),
+    'events_per_session': lambda t: t.count() / t.session_id.nunique()
+}`}
+            
+
+
+
+ +
+
+
+

Indexing

+

+ Create indexed values for time series comparisons +

+
+ + +
+              {`# Index to base period (e.g., first month = 100)
+measures = {
+    'flight_count': lambda t: t.count(),
+    'indexed_count': lambda t: (
+        t.count() / t.count().first().over(
+            ibis.window(order_by='date')
+        ) * 100
+    )
+}
+
+result = flights_sm.query(
+    dimensions=['date'],
+    measures=['flight_count', 'indexed_count'],
+    time_grain='TIME_GRAIN_MONTH'
+).execute()`}
+            
+
+
+
+ +
+
+
+

Nesting

+

+ Build measures on top of other measures for complex calculations +

+
+ + +
+              {`# Layer measures on top of each other
+measures = {
+    # Base measures
+    'total_flights': lambda t: t.count(),
+    'total_distance': lambda t: t.distance.sum(),
+    
+    # Nested measure using base measures
+    'avg_distance_per_flight': lambda t: (
+        t.total_distance / t.total_flights
+    ),
+    
+    # Further nesting
+    'distance_efficiency': lambda t: (
+        t.avg_distance_per_flight / t.avg_distance_per_flight.max()
+    ) * 100
+}
+
+result = flights_sm.query(
+    dimensions=['carrier'],
+    measures=[
+        'total_flights',
+        'avg_distance_per_flight',
+        'distance_efficiency'
+    ]
+).execute()`}
+            
+
+
+
+ + ); +}; diff --git a/docs/src/components/AltairChart.tsx b/docs/src/components/AltairChart.tsx new file mode 100644 index 00000000..b255404b --- /dev/null +++ b/docs/src/components/AltairChart.tsx @@ -0,0 +1,84 @@ +import { useEffect, useRef } from 'react' +import embed from 'vega-embed' +import { useTheme } from 'next-themes' + +interface AltairChartProps { + spec: any +} + +/** + * Component to display Altair/Vega-Lite charts + * Used with in markdown + */ +export function AltairChart({ spec }: AltairChartProps) { + const chartRef = useRef(null) + const { theme, resolvedTheme } = useTheme() + + useEffect(() => { + if (chartRef.current && spec) { + const currentTheme = resolvedTheme || theme + const isDark = currentTheme === 'dark' + + // Vega-Lite config for dark/light mode + const config = isDark ? { + background: 'transparent', + axis: { + domainColor: '#666', + gridColor: '#444', + tickColor: '#666', + labelColor: '#ccc', + titleColor: '#fff' + }, + legend: { + labelColor: '#ccc', + titleColor: '#fff' + }, + title: { + color: '#fff' + }, + view: { + stroke: '#444' + } + } : { + background: 'transparent', + axis: { + domainColor: '#ccc', + gridColor: '#e5e5e5', + tickColor: '#ccc', + labelColor: '#666', + titleColor: '#333' + }, + legend: { + labelColor: '#666', + titleColor: '#333' + }, + title: { + color: '#333' + }, + view: { + stroke: '#e5e5e5' + } + } + + // Merge config with the spec + const themedSpec = { + ...spec, + config: { + ...spec.config, + ...config + } + } + + embed(chartRef.current, themedSpec, { + actions: false, + renderer: 'svg' + }).catch(err => console.error('Error rendering Altair chart:', err)) + } + }, [spec, theme, resolvedTheme]) + + return ( +
+
+
+ ) +} diff --git a/docs/src/components/AppSidebar.tsx b/docs/src/components/AppSidebar.tsx new file mode 100644 index 00000000..0cf348d7 --- /dev/null +++ b/docs/src/components/AppSidebar.tsx @@ -0,0 +1,246 @@ +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar"; +import { Home, BookOpen, Code2, Sparkles, FileText, Github, ChevronRight, Sparkle } from "lucide-react"; +import { useState } from "react"; +import { NavLink, useLocation } from "react-router-dom"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; + +const navigationStructure = [ + { + title: "Home", + icon: Home, + path: "/", + }, + { + title: "About BSL", + icon: Sparkle, + items: [ + { title: "What is BSL", path: "/about" }, + { title: "Getting Started", path: "/examples/getting-started" }, + ], + }, + { + title: "Building a Semantic Table", + icon: Code2, + items: [ + { + title: "Semantic Tables", + path: "/building/semantic-tables", + subitems: [ + { title: "to_semantic_table()", hash: "#to-semantic-table" }, + { title: "with_dimensions()", hash: "#with-dimensions" }, + { title: "with_measures()", hash: "#with-measures" }, + { title: "join_one() (+many/cross)", hash: "#join-one-join-many-join-cross" }, + ], + }, + { title: "Compose Models", path: "/building/compose" }, + { title: "YAML Config", path: "/building/yaml" }, + ], + }, + { + title: "Querying Semantic Tables", + icon: BookOpen, + items: [ + { + title: "Query Methods", + path: "/querying/methods", + subitems: [ + { title: "group_by()", hash: "#group-by" }, + { title: "aggregate()", hash: "#aggregate" }, + { title: "filter() / order_by() / limit()", hash: "#filter-order-by-limit" }, + { title: "nest()", hash: "#nest" }, + { title: "mutate()", hash: "#mutate" }, + { title: "Window Functions (.over)", hash: "#window-functions-with-over" }, + { title: "as_table()", hash: "#as-table" }, + ], + }, + { title: "Charting", path: "/querying/charting" }, + { title: "Dimensional Indexing", path: "/querying/indexing" }, + { title: "MCP Integration", path: "/querying/mcp" }, + ], + }, + { + title: "Advanced Patterns", + icon: Sparkles, + items: [ + { title: "Percentage of Total", path: "/advanced/percentage-total" }, + { title: "Nested Subtotals", path: "/advanced/nested-subtotals" }, + { title: "Window Functions", path: "/advanced/windowing" }, + { title: "Bucketing", path: "/advanced/bucketing" }, + { title: "Sessionized Data", path: "/advanced/sessionized" }, + ], + }, + { + title: "Reference", + icon: FileText, + path: "/reference", + }, +]; + +export function AppSidebar() { + const { open } = useSidebar(); + const location = useLocation(); + const [openSections, setOpenSections] = useState(["About BSL", "Building a Semantic Table", "Querying Semantic Tables", "Advanced Patterns", "Query Methods", "Defining Semantic Tables"]); + + const toggleSection = (title: string) => { + setOpenSections((prev) => + prev.includes(title) ? prev.filter((t) => t !== title) : [...prev, title] + ); + }; + + const isActivePath = (path: string) => { + return location.pathname === path; + }; + + return ( + + + {navigationStructure.map((section) => { + // Section with subsections + if ("items" in section) { + return ( + toggleSection(section.title)} + > + + + +
+ + {section.title} + {open && } +
+
+
+ {open && ( + + + + {section.items.map((item) => { + // Check if item has subitems + if ('subitems' in item) { + return ( + toggleSection(item.title)} + > + + + +
+ + {item.title} + + {open && } +
+
+
+
+ {open && ( + + + {item.subitems.map((subitem) => ( + + + + {subitem.title} + + + + ))} + + + )} +
+ ); + } + + // Regular item without subitems + return ( + + + + {item.title} + + + + ); + })} +
+
+
+ )} +
+
+ ); + } + + // Section without subsections + return ( + + + + + + + {section.title} + + + + + + ); + })} + + + + + + + + + GitHub + + + + + + +
+
+ ); +} diff --git a/docs/src/components/BSLQueryResult.tsx b/docs/src/components/BSLQueryResult.tsx new file mode 100644 index 00000000..184738bf --- /dev/null +++ b/docs/src/components/BSLQueryResult.tsx @@ -0,0 +1,326 @@ +import { useState, useEffect, useRef } from 'react' +import embed from 'vega-embed' +import { useTheme } from 'next-themes' +import { CodeBlock } from './CodeBlock' + +interface BSLQueryResultProps { + data: any + name: string +} + +type TabType = 'table' | 'chart' | 'sql' + +function BSLQueryResult({ data, name }: BSLQueryResultProps) { + const [activeTab, setActiveTab] = useState('table') + const [copied, setCopied] = useState(false) + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + const code = data.code // Query code from data + const sql = data.sql // Generated SQL from data + + // Handle error state + if (data.error) { + return ( +
+

Query Error: {name}

+
+          {data.error}
+        
+
+ ) + } + + // Handle semantic table definition + if (data.semantic_table) { + return ( +
+

+ ✓ Semantic table {data.name} defined +

+
+ ) + } + + // Render nothing if no table or chart data + if (!data.table && !data.chart) { + return null + } + + const hasChart = data.chart && (data.chart.spec || data.chart.type) + const hasTable = data.table && data.table.data && data.table.data.length > 0 + const hasSQL = sql && sql.length > 0 + + return ( +
+ {/* Tabs */} +
+ setActiveTab('table')} + disabled={!hasTable} + > + 📊 Table + + setActiveTab('chart')} + disabled={!hasChart} + > + 📈 Chart + + {hasSQL && ( + setActiveTab('sql')} + > + 💻 SQL + + )} +
+ + {/* Content */} +
+ {activeTab === 'table' && hasTable && ( + + )} + + {activeTab === 'chart' && hasChart && ( + + )} + + {activeTab === 'sql' && hasSQL && ( + + )} +
+
+ ) +} + +// Tab Button Component +function TabButton({ + active, + onClick, + children, + disabled = false +}: { + active: boolean + onClick: () => void + children: React.ReactNode + disabled?: boolean +}) { + return ( + + ) +} + +// Helper function to format cell values +function formatCellValue(cell: any): string { + if (cell === null || cell === undefined) { + return 'null' + } + + if (typeof cell === 'number') { + return cell.toLocaleString() + } + + if (typeof cell === 'string') { + return cell + } + + if (typeof cell === 'boolean') { + return cell.toString() + } + + // Handle arrays (including nested structs) + if (Array.isArray(cell)) { + // If it's an array of objects (nested structs), format nicely + if (cell.length > 0 && typeof cell[0] === 'object' && cell[0] !== null) { + return JSON.stringify(cell, null, 2) + } + // For simple arrays, use compact format + return JSON.stringify(cell) + } + + // Handle objects (structs) + if (typeof cell === 'object') { + return JSON.stringify(cell, null, 2) + } + + return String(cell) +} + +// Table View Component +function TableView({ data }: { data: { columns: string[], data: any[][] } }) { + return ( +
+ + + + + {data.columns.map((col, idx) => ( + + ))} + + + + {data.data.map((row, rowIdx) => ( + + + {row.map((cell, cellIdx) => { + const formattedValue = formatCellValue(cell) + const isComplex = formattedValue.includes('\n') || formattedValue.length > 50 + + return ( + + ) + })} + + ))} + +
+ + + {col} +
+ {rowIdx} + + {isComplex ? ( +
+                        {formattedValue}
+                      
+ ) : ( + formattedValue + )} +
+
+ ) +} + +// Chart View Component +function ChartView({ data }: { data: { type?: string; spec: any } }) { + const chartRef = useRef(null) + const { theme, resolvedTheme } = useTheme() + const chartType = data.type || 'vega' // Default to vega for backward compatibility + + // Render Plotly chart + if (chartType === 'plotly') { + // Plotly support is currently disabled to avoid dependency bloat + // since no charts in the docs currently use Plotly backend + return ( +
+
+ Plotly charts are not currently supported in the documentation viewer. + The chart data is available in the query results. +
+
+ ) + } + + // Render Vega-Lite chart (Altair) + useEffect(() => { + if (chartRef.current && data.spec) { + const currentTheme = resolvedTheme || theme + const isDark = currentTheme === 'dark' + + // Vega-Lite config for dark/light mode + const config = isDark ? { + background: 'transparent', + axis: { + domainColor: '#666', + gridColor: '#444', + tickColor: '#666', + labelColor: '#ccc', + titleColor: '#fff' + }, + legend: { + labelColor: '#ccc', + titleColor: '#fff' + }, + title: { + color: '#fff' + }, + view: { + stroke: '#444' + } + } : { + background: 'transparent', + axis: { + domainColor: '#ccc', + gridColor: '#e5e5e5', + tickColor: '#ccc', + labelColor: '#666', + titleColor: '#333' + }, + legend: { + labelColor: '#666', + titleColor: '#333' + }, + title: { + color: '#333' + }, + view: { + stroke: '#e5e5e5' + } + } + + // Merge config with the spec + const themedSpec = { + ...data.spec, + config: { + ...data.spec.config, + ...config + } + } + + embed(chartRef.current, themedSpec, { + actions: false, + renderer: 'svg' + }).catch(err => console.error('Error rendering chart:', err)) + } + }, [data.spec, theme, resolvedTheme]) + + return ( +
+ ) +} + +// SQL View Component +function SQLView({ sql }: { sql: string }) { + return ( +
+ +
+ ) +} + +export default BSLQueryResult diff --git a/docs/src/components/CodeBlock.tsx b/docs/src/components/CodeBlock.tsx new file mode 100644 index 00000000..420e24a2 --- /dev/null +++ b/docs/src/components/CodeBlock.tsx @@ -0,0 +1,110 @@ +import { useState, useEffect, useRef } from "react"; +import { Copy, Check, Play } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useTheme } from "next-themes"; +import hljs from "highlight.js/lib/core"; +import python from "highlight.js/lib/languages/python"; +import yaml from "highlight.js/lib/languages/yaml"; +import sql from "highlight.js/lib/languages/sql"; + +hljs.registerLanguage("python", python); +hljs.registerLanguage("yaml", yaml); +hljs.registerLanguage("sql", sql); + +// Track if stylesheet is already loaded to prevent multiple instances +let stylesheetLoaded = false; +let currentStylesheet: HTMLLinkElement | null = null; + +interface CodeBlockProps { + code: string; + language?: string; + runnable?: boolean; + className?: string; +} + +export const CodeBlock = ({ code, language = "python", runnable = false, className = "" }: CodeBlockProps) => { + const [copied, setCopied] = useState(false); + const { theme, resolvedTheme } = useTheme(); + + // Map unknown languages to python (for custom block names like dimensions_demo, measures_demo, etc.) + const highlightLanguage = ['python', 'yaml', 'sql'].includes(language) ? language : 'python'; + + // Pre-highlight the code immediately (no useEffect needed) + const highlightedCode = hljs.highlight(code, { language: highlightLanguage }).value; + + // Load appropriate highlight.js theme based on current theme (singleton pattern) + useEffect(() => { + const currentTheme = resolvedTheme || theme; + const isDark = currentTheme === "dark"; + const newHref = isDark + ? "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" + : "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css"; + + // Only update if theme changed or not loaded + if (!stylesheetLoaded || (currentStylesheet && currentStylesheet.href !== newHref)) { + // Remove old stylesheet if exists + if (currentStylesheet) { + currentStylesheet.remove(); + } + + // Load new theme + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = newHref; + document.head.appendChild(link); + + currentStylesheet = link; + stylesheetLoaded = true; + } + + // Don't remove on cleanup - keep it loaded for all components + }, [theme, resolvedTheme]); + + const copyToClipboard = () => { + navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+ {runnable && ( + + )} + +
+
+        
+      
+
+ ); +}; diff --git a/docs/src/components/CollapsibleSetup.tsx b/docs/src/components/CollapsibleSetup.tsx new file mode 100644 index 00000000..9a15ac87 --- /dev/null +++ b/docs/src/components/CollapsibleSetup.tsx @@ -0,0 +1,28 @@ +import { useState } from 'react' +import { ChevronRight } from 'lucide-react' +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' +import { CodeBlock } from './CodeBlock' + +interface CollapsibleSetupProps { + code: string + title?: string +} + +export function CollapsibleSetup({ code, title = "Setup code" }: CollapsibleSetupProps) { + const [isOpen, setIsOpen] = useState(false) + + return ( + + + + 📦 {title} + (click to expand) + + +
+ +
+
+
+ ) +} diff --git a/docs/src/components/CommandPalette.tsx b/docs/src/components/CommandPalette.tsx new file mode 100644 index 00000000..91b0f5cb --- /dev/null +++ b/docs/src/components/CommandPalette.tsx @@ -0,0 +1,94 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { buildSearchIndex, searchPages, SearchResult } from "@/lib/search-index"; +import { FileText, ChevronRight } from "lucide-react"; + +interface CommandPaletteProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) { + const [query, setQuery] = useState(""); + const [results, setResults] = useState([]); + const navigate = useNavigate(); + + // Build search index once on mount + const [searchIndex] = useState(() => buildSearchIndex()); + + // Popular pages to show when no query + const popularPages = [ + { title: "Getting Started", path: "/examples/getting-started", section: "About BSL" }, + { title: "Semantic Table", path: "/examples/semantic-table", section: "Building a Semantic Table" }, + { title: "Query Methods", path: "/examples/query-methods", section: "Querying Semantic Tables" }, + { title: "Reference", path: "/reference", section: "Reference" }, + ]; + + // Update results when query changes + useEffect(() => { + if (query.trim()) { + const searchResults = searchPages(query, searchIndex); + setResults(searchResults); + } else { + // Show popular pages when no query + setResults(popularPages); + } + }, [query, searchIndex]); + + const handleSelect = (path: string) => { + onOpenChange(false); + navigate(path); + setQuery(""); // Reset query after navigation + }; + + // Group results by section + const groupedResults = results.reduce((acc, result) => { + if (!acc[result.section]) { + acc[result.section] = []; + } + acc[result.section].push(result); + return acc; + }, {} as Record); + + return ( + + + + + {query.trim() ? "No results found." : "Start typing to search..."} + + {Object.entries(groupedResults).map(([section, items]) => ( + + {items.map((item) => ( + handleSelect(item.path)} + className="flex items-center gap-2 cursor-pointer" + > + + {item.title} + + + ))} + + ))} + + + ); +} diff --git a/docs/src/components/Features.tsx b/docs/src/components/Features.tsx new file mode 100644 index 00000000..1eb932c6 --- /dev/null +++ b/docs/src/components/Features.tsx @@ -0,0 +1,69 @@ +import { Card } from "@/components/ui/card"; +import { Database, Filter, Link2, Brain, BarChart3, Clock } from "lucide-react"; + +const features = [ + { + icon: Database, + title: "Semantic Models", + description: "Define dimensions and measures using Ibis expressions. Your data model becomes self-documenting and reusable across all queries." + }, + { + icon: Filter, + title: "Flexible Filters", + description: "Use Ibis expressions or JSON-based filters. Perfect for dynamic queries and LLM integration with operators like AND, OR, in, not in." + }, + { + icon: Link2, + title: "Cross-Model Joins", + description: "Join semantic models together with classic SQL joins, join_one, join_many, or join_cross. Enrich your data effortlessly." + }, + { + icon: Brain, + title: "MCP Integration", + description: "Native Model Context Protocol support. Connect LLMs directly to your structured data sources with zero friction." + }, + { + icon: BarChart3, + title: "Smart Charting", + description: "Built-in visualization with Altair and Plotly. Auto-detection of chart types based on your data structure." + }, + { + icon: Clock, + title: "Time-Based Queries", + description: "Define time dimensions and query with specific grains. Perfect for time-series analysis and temporal aggregations." + } +]; + +export const Features = () => { + return ( +
+
+
+

Features

+

+ Everything you need to build a powerful semantic layer +

+
+ +
+ {features.map((feature, index) => ( + +
+
+ +
+

{feature.title}

+
+

+ {feature.description} +

+
+ ))} +
+
+
+ ); +}; diff --git a/docs/src/components/Footer.tsx b/docs/src/components/Footer.tsx new file mode 100644 index 00000000..9812023c --- /dev/null +++ b/docs/src/components/Footer.tsx @@ -0,0 +1,98 @@ +import { Github, ExternalLink } from "lucide-react"; + +export const Footer = () => { + return ( + + ); +}; diff --git a/docs/src/components/Installation.tsx b/docs/src/components/Installation.tsx new file mode 100644 index 00000000..a33f9342 --- /dev/null +++ b/docs/src/components/Installation.tsx @@ -0,0 +1,63 @@ +import { Card } from "@/components/ui/card"; +import { Copy, Check } from "lucide-react"; +import { useState } from "react"; + +const installOptions = [ + { label: "Basic", command: "pip install boring-semantic-layer" }, + { label: "With Examples", command: "pip install 'boring-semantic-layer[examples]'" }, + { label: "MCP Integration", command: "pip install 'boring-semantic-layer[fastmcp]'" }, + { label: "Altair Viz", command: "pip install 'boring-semantic-layer[viz-altair]'" }, + { label: "Plotly Viz", command: "pip install 'boring-semantic-layer[viz-plotly]'" } +]; + +export const Installation = () => { + const [copiedIndex, setCopiedIndex] = useState(null); + + const copyToClipboard = (command: string, index: number) => { + navigator.clipboard.writeText(command); + setCopiedIndex(index); + setTimeout(() => setCopiedIndex(null), 2000); + }; + + return ( +
+
+
+

Installation

+

+ Choose the installation that fits your needs +

+
+ +
+ {installOptions.map((option, index) => ( + +
+ + {option.label} + + + {option.command} + +
+ +
+ ))} +
+
+
+ ); +}; diff --git a/docs/src/components/Note.tsx b/docs/src/components/Note.tsx new file mode 100644 index 00000000..36dcce3d --- /dev/null +++ b/docs/src/components/Note.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import { Info } from 'lucide-react' +import ReactMarkdown from 'react-markdown' + +interface NoteProps { + children: React.ReactNode + type?: 'info' | 'warning' | 'tip' +} + +export function Note({ children, type = 'info' }: NoteProps) { + const styles = { + info: { + container: 'bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-800', + icon: 'text-blue-600 dark:text-blue-400', + text: 'text-blue-900 dark:text-blue-100' + }, + warning: { + container: 'bg-amber-50 dark:bg-amber-950/30 border-amber-200 dark:border-amber-800', + icon: 'text-amber-600 dark:text-amber-400', + text: 'text-amber-900 dark:text-amber-100' + }, + tip: { + container: 'bg-green-50 dark:bg-green-950/30 border-green-200 dark:border-green-800', + icon: 'text-green-600 dark:text-green-400', + text: 'text-green-900 dark:text-green-100' + } + } + + const style = styles[type] + + // Extract text content if children is a string or can be converted + const content = typeof children === 'string' + ? children + : React.Children.toArray(children).map(child => + typeof child === 'string' ? child : '' + ).join('') + + return ( +
+
+ +
+ <>{children}, + strong: ({ children }) => {children}, + code: ({ children }) => {children}, + a: ({ href, children }) => {children} + }} + > + {content} + +
+
+
+ ) +} diff --git a/docs/src/components/QueryingTables.tsx b/docs/src/components/QueryingTables.tsx new file mode 100644 index 00000000..2ea719ca --- /dev/null +++ b/docs/src/components/QueryingTables.tsx @@ -0,0 +1,251 @@ +import { Card } from "@/components/ui/card"; + +export const QueryingTables = () => { + return ( + <> +
+
+
+

Querying Semantic Tables

+

+ Use powerful query methods to analyze your data +

+
+ + +

+ Once you've defined your semantic tables, you can query them using intuitive methods + like group_by, aggregate, mutate, and more. All queries are composable and generate + optimized SQL. +

+
+
+
+ +
+
+
+

group_by / aggregate / mutate / order_by

+

+ Core query methods for data transformation and analysis +

+
+ +
+ +

group_by

+

Group data by dimensions

+
+                {`# Group by single dimension
+result = flights_sm.query(
+    dimensions=['origin'],
+    measures=['flight_count']
+).execute()
+
+# Group by multiple dimensions
+result = flights_sm.query(
+    dimensions=['origin', 'carrier'],
+    measures=['flight_count', 'avg_distance']
+).execute()`}
+              
+
+ + +

aggregate

+

Apply aggregation functions

+
+                {`# Aggregate measures
+result = flights_sm.query(
+    measures=['total_flights', 'total_distance', 'avg_distance']
+).execute()`}
+              
+
+ + +

order_by

+

Sort results by dimensions or measures

+
+                {`# Order by dimension
+result = flights_sm.query(
+    dimensions=['origin'],
+    measures=['flight_count'],
+    order_by=['origin']
+).execute()
+
+# Order by measure (descending)
+result = flights_sm.query(
+    dimensions=['origin'],
+    measures=['flight_count'],
+    order_by=[('flight_count', 'desc')]
+).execute()`}
+              
+
+
+
+
+ +
+
+
+

Multi Model Query

+

+ Query across multiple joined semantic tables +

+
+ + +
+              {`# Query dimensions and measures from joined models
+result = flights_sm.query(
+    dimensions=[
+        'origin',                  # From flights
+        'carriers.name',           # From joined carriers
+        'carriers.nickname'        # Also from carriers
+    ],
+    measures=[
+        'flight_count',            # From flights
+        'carriers.carrier_count'   # From carriers
+    ]
+).execute()`}
+            
+
+
+
+ +
+
+
+

limit / filter

+

+ Control result size and filter data +

+
+ +
+ +

Limit Results

+
+                {`# Get top 10 results
+result = flights_sm.query(
+    dimensions=['origin'],
+    measures=['flight_count'],
+    limit=10
+).execute()`}
+              
+
+ + +

Filter with Ibis Expressions

+
+                {`# Filter using lambda
+result = flights_sm.query(
+    dimensions=['origin'],
+    measures=['flight_count'],
+    filters=[lambda t: t.origin == 'JFK']
+).execute()`}
+              
+
+ + +

Filter with JSON (LLM-friendly)

+
+                {`# Filter using JSON syntax
+result = flights_sm.query(
+    dimensions=['origin'],
+    measures=['flight_count'],
+    filters=[
+        {
+            'operator': 'AND',
+            'conditions': [
+                {'field': 'origin', 'operator': 'in', 'values': ['JFK', 'LGA']},
+                {'field': 'year', 'operator': '=', 'values': [2013]}
+            ]
+        }
+    ]
+).execute()`}
+              
+
+
+
+
+ +
+
+
+

Name Conflicts

+

+ Handle naming conflicts when joining models +

+
+ + +

+ When multiple models have dimensions or measures with the same name, + use the model prefix to disambiguate. +

+
+              {`# Both models have a 'name' dimension
+result = flights_sm.query(
+    dimensions=[
+        'flights.name',    # Explicitly from flights
+        'carriers.name'    # Explicitly from carriers
+    ],
+    measures=['flight_count']
+).execute()`}
+            
+
+
+
+ +
+
+
+

Charting

+

+ Visualize your query results with built-in charting +

+
+ +
+ +

Auto-detected Charts

+
+                {`# Install with viz support
+pip install 'boring-semantic-layer[viz-altair]'
+
+# Create a chart from your query
+query = flights_sm.query(
+    dimensions=['origin'],
+    measures=['flight_count']
+)
+
+# Auto-detect chart type
+chart = query.chart()
+chart.save('flights.html')`}
+              
+
+ + +

Custom Chart Types

+
+                {`# Specify mark type
+chart = query.chart(mark='bar')
+chart = query.chart(mark='line')
+chart = query.chart(mark='point')
+
+# Time series
+time_query = flights_sm.query(
+    dimensions=['date'],
+    measures=['flight_count'],
+    time_range={'start': '2013-01-01', 'end': '2013-12-31'},
+    time_grain='TIME_GRAIN_MONTH'
+)
+chart = time_query.chart()  # Auto-detects time series`}
+              
+
+
+
+
+ + ); +}; diff --git a/docs/src/components/QuickExample.tsx b/docs/src/components/QuickExample.tsx new file mode 100644 index 00000000..77c14c9d --- /dev/null +++ b/docs/src/components/QuickExample.tsx @@ -0,0 +1,98 @@ +import { Card } from "@/components/ui/card"; +import { CodeBlock } from "@/components/CodeBlock"; + +export const QuickExample = () => { + const step1Code = `import ibis + +flights_tbl = ibis.table( + name="flights", + schema={"origin": "string", "carrier": "string"} +)`; + + const step2Code = `from boring_semantic_layer import SemanticModel + +flights_sm = SemanticModel( + table=flights_tbl, + dimensions={"origin": lambda t: t.origin}, + measures={"flight_count": lambda t: t.count()} +)`; + + const step3Code = `flights_sm.query( + dimensions=["origin"], + measures=["flight_count"] +).execute()`; + + return ( +
+
+
+

Quickstart

+

+ Three simple steps to get started - Try the code live in Marimo! +

+
+ +
+ +
+
+ 1 +
+
+

Define your table

+ +
+
+
+ + +
+
+ 2 +
+
+

Build a semantic model

+ +
+
+
+ + +
+
+ 3 +
+
+

Query it

+ +
+

Expected output:

+
+ + + + + + + + + + + + + + + + + +
originflight_count
JFK3689
LGA2941
+
+
+
+
+
+
+
+
+ ); +}; diff --git a/docs/src/components/RegularOutput.tsx b/docs/src/components/RegularOutput.tsx new file mode 100644 index 00000000..2b0f6a14 --- /dev/null +++ b/docs/src/components/RegularOutput.tsx @@ -0,0 +1,127 @@ +import { useMemo } from 'react' + +interface RegularOutputProps { + code: string | string[] +} + +export function RegularOutput({ code }: RegularOutputProps) { + if (!code) { + console.error('RegularOutput: code prop is missing') + return
Error: No output data
+ } + + // Handle array of outputs (multiple variables) - display in same component, separate rows + if (Array.isArray(code)) { + return ( +
+
+ Output +
+
+ {code.map((output, index) => ( +
0 ? 'pt-3 border-t border-border' : ''}> + +
+ ))} +
+
+ ) + } + + // Single output + return ( +
+
+ Output +
+
+ +
+
+ ) +} + +// Separate component to format output content +function OutputContent({ code }: { code: string }) { + const formattedOutput = useMemo(() => { + // Try to parse the output as JSON first (for structured data) + try { + const parsed = JSON.parse(code) + + // Handle arrays + if (Array.isArray(parsed)) { + return ( +
    + {parsed.map((item, idx) => ( +
  • + {typeof item === 'object' ? JSON.stringify(item, null, 2) : String(item)} +
  • + ))} +
+ ) + } + + // Handle objects + if (typeof parsed === 'object' && parsed !== null) { + return ( +
+ {Object.entries(parsed).map(([key, value]) => ( +
+ {key}: + {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)} +
+ ))} +
+ ) + } + + // Primitive JSON value + return
{String(parsed)}
+ } catch { + // Not JSON, try to detect structured text patterns + const lines = code.trim().split('\n') + + // Check if it looks like a list (lines starting with -, *, or numbers) + const isList = lines.every(line => + /^\s*[-*•]\s/.test(line) || /^\s*\d+\.\s/.test(line) + ) + + if (isList) { + return ( +
    + {lines.map((line, idx) => ( +
  • + {line.replace(/^\s*[-*•]\s/, '').replace(/^\s*\d+\.\s/, '')} +
  • + ))} +
+ ) + } + + // Check if it's a Python list/tuple/dict representation + if (/^[\[\(\{]/.test(code.trim()) && /[\]\)\}]$/.test(code.trim())) { + return ( +
+            {code}
+          
+ ) + } + + // Plain text with multiple lines + if (lines.length > 1) { + return ( +
+ {lines.map((line, idx) => ( +
{line}
+ ))} +
+ ) + } + + // Single line text + return
{code}
+ } + }, [code]) + + return <>{formattedOutput} +} diff --git a/docs/src/components/SearchButton.tsx b/docs/src/components/SearchButton.tsx new file mode 100644 index 00000000..3f1f078a --- /dev/null +++ b/docs/src/components/SearchButton.tsx @@ -0,0 +1,37 @@ +import { Search } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useEffect } from "react"; + +interface SearchButtonProps { + onClick: () => void; +} + +export function SearchButton({ onClick }: SearchButtonProps) { + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Cmd+K on Mac, Ctrl+K on Windows/Linux + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + onClick(); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [onClick]); + + return ( + + ); +} diff --git a/docs/src/components/SemanticTable.tsx b/docs/src/components/SemanticTable.tsx new file mode 100644 index 00000000..07971885 --- /dev/null +++ b/docs/src/components/SemanticTable.tsx @@ -0,0 +1,379 @@ +import { Card } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +export const SemanticTable = () => { + return ( + <> +
+
+
+

Semantic Table Definition

+

+ Define your data model with dimensions and measures using Ibis expressions +

+
+ + +

+ A Semantic Table is the core building block of BSL. It transforms a raw Ibis table + into a reusable, self-documenting data model by defining dimensions (attributes to group by) + and measures (aggregations and calculations). +

+
+
+
+ +
+
+
+

From Ibis Table to Semantic Table

+

+ Transform your Ibis tables into semantic models +

+
+ + +

Basic Conversion

+
+              {`import ibis
+from boring_semantic_layer import SemanticModel
+
+# 1. Start with an Ibis table
+flights_tbl = ibis.table(
+    name="flights",
+    schema={"origin": "string", "carrier": "string", "distance": "int64"}
+)
+
+# 2. Convert to a Semantic Table
+flights_sm = SemanticModel(
+    name="flights",
+    table=flights_tbl,
+    dimensions={
+        'origin': lambda t: t.origin,
+        'carrier': lambda t: t.carrier
+    },
+    measures={
+        'flight_count': lambda t: t.count(),
+        'total_distance': lambda t: t.distance.sum()
+    }
+)`}
+            
+
+
+
+ +
+
+
+

with_dimensions()

+

+ Define dimensions with optional descriptions for better documentation +

+
+ + + + Basic + With Descriptions + + + + +
+                  {`from boring_semantic_layer import SemanticModel
+
+flights_sm = SemanticModel(
+    table=flights_tbl,
+    dimensions={
+        'origin': lambda t: t.origin,
+        'destination': lambda t: t.dest,
+        'year': lambda t: t.year
+    }
+)`}
+                
+
+
+ + + +

+ Add descriptions to make your models self-documenting and AI-friendly +

+
+                  {`from boring_semantic_layer import SemanticModel, DimensionSpec
+
+flights_sm = SemanticModel(
+    table=flights_tbl,
+    dimensions={
+        "origin": DimensionSpec(
+            expr=lambda t: t.origin,
+            description="Origin airport code where the flight departed"
+        ),
+        "destination": DimensionSpec(
+            expr=lambda t: t.dest,
+            description="Destination airport code where the flight arrived"
+        ),
+        "year": DimensionSpec(
+            expr=lambda t: t.year,
+            description="Year of the flight"
+        )
+    }
+)`}
+                
+
+
+
+
+
+ +
+
+
+

with_measures()

+

+ Define measures with lambda expressions, shortcuts, and descriptions +

+
+ +
+ +

Lambda Expressions

+
+                {`measures={
+    'total_flights': lambda t: t.count(),
+    'total_distance': lambda t: t.distance.sum(),
+    'avg_distance': lambda t: t.distance.mean(),
+    'max_delay': lambda t: t.dep_delay.max()
+}`}
+              
+
+ + +

With Descriptions

+
+                {`from boring_semantic_layer import MeasureSpec
+
+measures={
+    "flight_count": MeasureSpec(
+        expr=lambda t: t.count(),
+        description="Total number of flights"
+    ),
+    "avg_distance": MeasureSpec(
+        expr=lambda t: t.distance.mean(),
+        description="Average flight distance in miles"
+    )
+}`}
+              
+
+ + +

Reference Other Measures

+

+ Build complex measures by referencing other measures +

+
+                {`measures={
+    'total_distance': lambda t: t.distance.sum(),
+    'flight_count': lambda t: t.count(),
+    # Reference other measures
+    'avg_distance_per_flight': lambda t: t.total_distance / t.flight_count
+}`}
+              
+
+
+
+
+ +
+
+
+

Joins

+

+ Connect semantic tables with join, join_one, and join_cross +

+
+ + + + Classic Join + join_one + join_cross + + + + +

SQL-style Joins

+
+                  {`from boring_semantic_layer import Join
+
+# Define carriers model
+carriers_sm = SemanticModel(
+    name="carriers",
+    table=carriers_tbl,
+    dimensions={
+        "code": lambda t: t.code,
+        "name": lambda t: t.name
+    }
+)
+
+# Join with flights
+flights_sm = SemanticModel(
+    name="flights",
+    table=flights_tbl,
+    dimensions={"carrier": lambda t: t.carrier},
+    measures={"flight_count": lambda t: t.count()},
+    joins={
+        "carriers": Join(
+            model=carriers_sm,
+            on=lambda left, right: left.carrier == right.code,
+            how="left"  # or "inner", "right", "outer"
+        )
+    }
+)
+
+# Query across joined models
+flights_sm.query(
+    dimensions=['carriers.name'],
+    measures=['flight_count']
+).execute()`}
+                
+
+
+ + + +

One-to-One Joins

+

+ Use join_one for relationships where each row matches exactly one row in the joined table +

+
+                  {`from boring_semantic_layer import join_one
+
+flights_sm = SemanticModel(
+    table=flights_tbl,
+    joins={
+        "carrier_details": join_one(
+            model=carriers_sm,
+            on=lambda left, right: left.carrier == right.code
+        )
+    }
+)`}
+                
+
+
+ + + +

Cross Joins

+

+ Create cartesian products for special analytical needs +

+
+                  {`from boring_semantic_layer import join_cross
+
+# Cross join for generating all combinations
+flights_sm = SemanticModel(
+    table=flights_tbl,
+    joins={
+        "all_carriers": join_cross(model=carriers_sm)
+    }
+)`}
+                
+
+
+
+
+
+ +
+
+
+

Compose Models Together

+

+ Build complex data models by combining multiple semantic tables +

+
+ + +
+              {`# Build layered models
+base_flights = SemanticModel(
+    table=flights_tbl,
+    dimensions={'origin': lambda t: t.origin},
+    measures={'count': lambda t: t.count()}
+)
+
+enriched_flights = SemanticModel(
+    table=base_flights.table,
+    dimensions={
+        **base_flights.dimensions,
+        'destination': lambda t: t.dest
+    },
+    measures={
+        **base_flights.measures,
+        'avg_distance': lambda t: t.distance.mean()
+    }
+)`}
+            
+
+
+
+ +
+
+
+

YAML Configuration

+

+ Define your semantic models using YAML for better organization +

+
+ +
+ +

YAML Format

+
+                {`# flights_model.yml
+flights:
+  table: flights_table
+  description: "Flight data with departure and arrival information"
+  
+  dimensions:
+    origin:
+      expr: _.origin
+      description: "Origin airport code"
+    
+    destination:
+      expr: _.destination
+      description: "Destination airport code"
+  
+  measures:
+    flight_count:
+      expr: _.count()
+      description: "Total number of flights"
+    
+    avg_distance:
+      expr: _.distance.mean()
+      description: "Average flight distance in miles"`}
+              
+
+ + +

Load YAML Models

+
+                {`from boring_semantic_layer import SemanticModel
+
+# Load models from YAML
+models = SemanticModel.from_yaml(
+    "flights_model.yml",
+    tables={"flights_table": flights_tbl}
+)
+
+flights_sm = models["flights"]`}
+              
+
+
+
+
+ + ); +}; diff --git a/docs/src/components/StandardOutput.tsx b/docs/src/components/StandardOutput.tsx new file mode 100644 index 00000000..5c215f2c --- /dev/null +++ b/docs/src/components/StandardOutput.tsx @@ -0,0 +1,57 @@ +import { CodeBlock } from './CodeBlock' + +interface StandardOutputProps { + data: any + name?: string +} + +/** + * Component for displaying standard Python output (not BSL query results) + * Used for showing dimensions, measures, and other non-tabular data + */ +export function StandardOutput({ data, name }: StandardOutputProps) { + // Handle error output + if (data.error) { + return ( +
+

Error{name ? `: ${name}` : ''}

+
+          {data.error}
+        
+
+ ) + } + + // Handle standard output (like print statements) + if (data.output) { + return ( +
+ {name &&

{name}

} +
+          {data.output}
+        
+
+ ) + } + + // Handle dictionary/list display + if (data.display) { + return ( +
+ {name &&

{name}

} + +
+ ) + } + + // Handle message display + if (data.message) { + return ( +
+

{data.message}

+
+ ) + } + + return null +} diff --git a/docs/src/components/TableOfContents.tsx b/docs/src/components/TableOfContents.tsx new file mode 100644 index 00000000..d852a5e2 --- /dev/null +++ b/docs/src/components/TableOfContents.tsx @@ -0,0 +1,88 @@ +import { useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; + +interface Heading { + id: string; + text: string; + level: number; +} + +export const TableOfContents = () => { + const [headings, setHeadings] = useState([]); + const [activeId, setActiveId] = useState(""); + + useEffect(() => { + // Extract all headings from the page + const elements = Array.from( + document.querySelectorAll("main h1, main h2, main h3") + ); + + const headingData = elements.map((element) => ({ + id: element.id || "", + text: element.textContent || "", + level: parseInt(element.tagName.substring(1)), + })).filter(h => h.id); // Only include headings with IDs + + setHeadings(headingData); + + // Set up intersection observer for scroll tracking + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveId(entry.target.id); + } + }); + }, + { rootMargin: "-80px 0px -80% 0px" } + ); + + elements.forEach((element) => { + if (element.id) observer.observe(element); + }); + + return () => observer.disconnect(); + }, []); + + if (headings.length === 0) return null; + + const handleClick = (id: string) => { + const element = document.getElementById(id); + if (element) { + const offset = 80; // Account for sticky header + const elementPosition = element.getBoundingClientRect().top; + const offsetPosition = elementPosition + window.pageYOffset - offset; + + window.scrollTo({ + top: offsetPosition, + behavior: "smooth", + }); + } + }; + + return ( +
+
+

On This Page

+ +
+
+ ); +}; \ No newline at end of file diff --git a/docs/src/components/ThemeToggle.tsx b/docs/src/components/ThemeToggle.tsx new file mode 100644 index 00000000..47e5c7bc --- /dev/null +++ b/docs/src/components/ThemeToggle.tsx @@ -0,0 +1,33 @@ +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; +import { Button } from "@/components/ui/button"; +import { useEffect, useState } from "react"; + +export const ThemeToggle = () => { + const { theme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return null; + } + + return ( + + ); +}; diff --git a/docs/src/components/WhatIsBSL.tsx b/docs/src/components/WhatIsBSL.tsx new file mode 100644 index 00000000..c1a5ed7f --- /dev/null +++ b/docs/src/components/WhatIsBSL.tsx @@ -0,0 +1,66 @@ +export const WhatIsBSL = () => { + return ( +
+
+
+

What is BSL?

+

+ The Boring Semantic Layer (BSL) is a lightweight semantic layer based on{" "} + + Ibis + + . +

+
+ +
+
+

Why BSL?

+

+ BSL provides a simple, consistent way to define your data model once and query it anywhere. + Built on top of Ibis, it inherits all the power and flexibility of Ibis expressions while + adding a semantic layer that makes your data more accessible and reusable. +

+
+ +
+

Design Philosophy

+
    +
  • + + Lightweight: No heavy dependencies, just pip install and go +
  • +
  • + + Ibis-powered: Leverage the full ecosystem of Ibis backends +
  • +
  • + + MCP-friendly: Perfect integration with Large Language Models +
  • +
  • + + Composable: Build complex models from simple, reusable pieces +
  • +
+
+
+ +
+

Joint Project

+

+ This project is a collaborative effort between{" "} + + xorq-labs + + {" "}and{" "} + + boringdata + + . We welcome feedback and contributions! +

+
+
+
+ ); +}; diff --git a/docs/src/components/YamlContent.tsx b/docs/src/components/YamlContent.tsx new file mode 100644 index 00000000..ea75a538 --- /dev/null +++ b/docs/src/components/YamlContent.tsx @@ -0,0 +1,18 @@ +import { CodeBlock } from './CodeBlock' + +interface YamlContentProps { + path: string + content?: string +} + +export function YamlContent({ path, content }: YamlContentProps) { + if (!content) { + return ( +
+ Error: YAML file content not found for {path} +
+ ) + } + + return +} diff --git a/docs/src/components/ui/accordion.tsx b/docs/src/components/ui/accordion.tsx new file mode 100644 index 00000000..1e7878ce --- /dev/null +++ b/docs/src/components/ui/accordion.tsx @@ -0,0 +1,52 @@ +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); + +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/docs/src/components/ui/alert-dialog.tsx b/docs/src/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..6dfbfb49 --- /dev/null +++ b/docs/src/components/ui/alert-dialog.tsx @@ -0,0 +1,104 @@ +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +AlertDialogHeader.displayName = "AlertDialogHeader"; + +const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +AlertDialogFooter.displayName = "AlertDialogFooter"; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/docs/src/components/ui/alert.tsx b/docs/src/components/ui/alert.tsx new file mode 100644 index 00000000..2efc3c8b --- /dev/null +++ b/docs/src/components/ui/alert.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/docs/src/components/ui/aspect-ratio.tsx b/docs/src/components/ui/aspect-ratio.tsx new file mode 100644 index 00000000..c9e6f4bf --- /dev/null +++ b/docs/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; + +const AspectRatio = AspectRatioPrimitive.Root; + +export { AspectRatio }; diff --git a/docs/src/components/ui/avatar.tsx b/docs/src/components/ui/avatar.tsx new file mode 100644 index 00000000..68d21bbf --- /dev/null +++ b/docs/src/components/ui/avatar.tsx @@ -0,0 +1,38 @@ +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { cn } from "@/lib/utils"; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/docs/src/components/ui/badge.tsx b/docs/src/components/ui/badge.tsx new file mode 100644 index 00000000..0853c441 --- /dev/null +++ b/docs/src/components/ui/badge.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps extends React.HTMLAttributes, VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return
; +} + +export { Badge, badgeVariants }; diff --git a/docs/src/components/ui/breadcrumb.tsx b/docs/src/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..ca91ff53 --- /dev/null +++ b/docs/src/components/ui/breadcrumb.tsx @@ -0,0 +1,90 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode; + } +>(({ ...props }, ref) =>