From dcff880f057a01248104e87265142a63198c8e3d Mon Sep 17 00:00:00 2001 From: huraultj Date: Thu, 30 Oct 2025 14:03:45 +0100 Subject: [PATCH 1/2] feat: add React-based documentation site with BSL interactive examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documentation Site: - Build complete React documentation site with Vite, TypeScript, and Tailwind - Add interactive BSL query examples with live results, SQL, and charts - Implement collapsible code blocks for setup code - Add syntax highlighting with Prism - Add dark mode support with theme toggle - Add left sidebar navigation with collapsible sections - Remove right sidebar (Table of Contents) - Add command palette for quick navigation - Add responsive design for mobile and desktop Documentation Content: - Getting Started guide - Building Semantic Tables (dimensions, measures, joins) - Compose Models for multi-table queries - YAML Configuration - Query Methods (group_by, aggregate, filter, mutate, nest, window functions) - as_table() documentation with problem/solution examples - Charting with Altair and Plotly - Advanced patterns: percentage of total, nested subtotals, bucketing, sessionized data, windowing - Dimensional Indexing - MCP Integration guide - Complete API Reference Features: - Pre-compute query results at build time for fast page loads - Execute Python code blocks and capture results, SQL, and charts - Support for regularoutput component to show string/error results - Markdown-based content with custom React components - GitHub Pages deployment configuration - CI workflow to test docs build Bug Fixes: - Fix as_table() bug: change 'dims' to 'dimensions' in expr.py - Fix linting errors in build_data.py and chart.py - Add proper exception handling and type hints πŸ€– Generated with Claude Code Co-Authored-By: Claude --- .codespell.ignore-words | 0 .github/workflows/docs-deploy.yml | 77 + .pre-commit-config.yaml | 2 +- README.md | 1365 +-- docs/bun.lockb | Bin 0 -> 201126 bytes docs/chart_bar.png | Bin 48520 -> 0 bytes docs/chart_custom.png | Bin 64381 -> 0 bytes docs/chart_heatmap.png | Bin 57857 -> 0 bytes docs/chart_line.png | Bin 41844 -> 0 bytes docs/chart_quickstart.png | Bin 27313 -> 0 bytes docs/chart_timeseries.png | Bin 75400 -> 0 bytes docs/components.json | 20 + docs/content/bucketing.md | 259 + docs/content/charting.md | 293 + docs/content/compose.md | 166 + docs/content/example.md | 66 + docs/content/getting-started.md | 77 + docs/content/indexing.md | 236 + docs/content/mcp.md | 322 + docs/content/mcp_example.yaml | 64 + docs/content/nested-subtotals.md | 182 + docs/content/percentage-total.md | 109 + docs/content/query-methods.md | 446 + docs/content/reference.md | 556 + docs/content/semantic-table.md | 258 + docs/content/sessionized.md | 219 + docs/content/windowing.md | 288 + docs/content/yaml-config.md | 89 + docs/content/yaml_example.yaml | 39 + docs/eslint.config.js | 26 + docs/index.html | 51 + docs/package-lock.json | 9023 +++++++++++++++++ docs/package.json | 88 + docs/postcss.config.js | 6 + docs/public/404.html | 39 + docs/public/bsl-data/bucketing.json | 865 ++ .../bsl-data/building-semantic-tables.json | 4 + docs/public/bsl-data/charting.json | 766 ++ docs/public/bsl-data/compose.json | 199 + docs/public/bsl-data/example.json | 256 + docs/public/bsl-data/filtering.json | 5 + docs/public/bsl-data/getting-started.json | 236 + docs/public/bsl-data/indexing.json | 700 ++ docs/public/bsl-data/joins.json | 5 + docs/public/bsl-data/mcp.json | 5 + docs/public/bsl-data/nested-subtotals.json | 1410 +++ docs/public/bsl-data/percentage-total.json | 605 ++ docs/public/bsl-data/query-methods.json | 1747 ++++ docs/public/bsl-data/reference.json | 5 + docs/public/bsl-data/semantic-table.json | 345 + docs/public/bsl-data/sessionized.json | 836 ++ docs/public/bsl-data/windowing.json | 3427 +++++++ docs/public/bsl-data/yaml-config.json | 125 + docs/public/favicon.ico | Bin 0 -> 7645 bytes .../notebooks/bucketing_with_other.html | 198 + .../notebooks/dimensional_indexing.html | 198 + docs/public/notebooks/nested_subtotals.html | 198 + docs/public/notebooks/percent_of_total.html | 198 + docs/public/notebooks/sessionized_data.html | 198 + docs/public/pages.json | 1 + docs/public/placeholder.svg | 1 + docs/public/robots.txt | 14 + docs/scripts/build_data.py | 658 ++ docs/src/App.tsx | 146 + docs/src/components/AdvancedPatterns.tsx | 217 + docs/src/components/AltairChart.tsx | 84 + docs/src/components/AppSidebar.tsx | 246 + docs/src/components/BSLQueryResult.tsx | 326 + docs/src/components/CodeBlock.tsx | 110 + docs/src/components/CollapsibleSetup.tsx | 28 + docs/src/components/CommandPalette.tsx | 94 + docs/src/components/Features.tsx | 69 + docs/src/components/Footer.tsx | 98 + docs/src/components/Installation.tsx | 63 + docs/src/components/Note.tsx | 57 + docs/src/components/QueryingTables.tsx | 251 + docs/src/components/QuickExample.tsx | 98 + docs/src/components/RegularOutput.tsx | 127 + docs/src/components/SearchButton.tsx | 37 + docs/src/components/SemanticTable.tsx | 379 + docs/src/components/StandardOutput.tsx | 57 + docs/src/components/TableOfContents.tsx | 88 + docs/src/components/ThemeToggle.tsx | 33 + docs/src/components/WhatIsBSL.tsx | 66 + docs/src/components/YamlContent.tsx | 18 + docs/src/components/ui/accordion.tsx | 52 + docs/src/components/ui/alert-dialog.tsx | 104 + docs/src/components/ui/alert.tsx | 43 + docs/src/components/ui/aspect-ratio.tsx | 5 + docs/src/components/ui/avatar.tsx | 38 + docs/src/components/ui/badge.tsx | 29 + docs/src/components/ui/breadcrumb.tsx | 90 + docs/src/components/ui/button.tsx | 47 + docs/src/components/ui/calendar.tsx | 54 + docs/src/components/ui/card.tsx | 43 + docs/src/components/ui/carousel.tsx | 224 + docs/src/components/ui/chart.tsx | 303 + docs/src/components/ui/checkbox.tsx | 26 + docs/src/components/ui/collapsible.tsx | 9 + docs/src/components/ui/command.tsx | 132 + docs/src/components/ui/context-menu.tsx | 178 + docs/src/components/ui/dialog.tsx | 95 + docs/src/components/ui/drawer.tsx | 87 + docs/src/components/ui/dropdown-menu.tsx | 179 + docs/src/components/ui/form.tsx | 129 + docs/src/components/ui/hover-card.tsx | 27 + docs/src/components/ui/input-otp.tsx | 61 + docs/src/components/ui/input.tsx | 22 + docs/src/components/ui/label.tsx | 17 + docs/src/components/ui/menubar.tsx | 207 + docs/src/components/ui/navigation-menu.tsx | 120 + docs/src/components/ui/pagination.tsx | 81 + docs/src/components/ui/popover.tsx | 29 + docs/src/components/ui/progress.tsx | 23 + docs/src/components/ui/radio-group.tsx | 36 + docs/src/components/ui/resizable.tsx | 37 + docs/src/components/ui/scroll-area.tsx | 38 + docs/src/components/ui/select.tsx | 143 + docs/src/components/ui/separator.tsx | 20 + docs/src/components/ui/sheet.tsx | 107 + docs/src/components/ui/sidebar.tsx | 637 ++ docs/src/components/ui/skeleton.tsx | 7 + docs/src/components/ui/slider.tsx | 23 + docs/src/components/ui/sonner.tsx | 27 + docs/src/components/ui/switch.tsx | 27 + docs/src/components/ui/table.tsx | 72 + docs/src/components/ui/tabs.tsx | 53 + docs/src/components/ui/textarea.tsx | 21 + docs/src/components/ui/toast.tsx | 111 + docs/src/components/ui/toaster.tsx | 24 + docs/src/components/ui/toggle-group.tsx | 49 + docs/src/components/ui/toggle.tsx | 37 + docs/src/components/ui/tooltip.tsx | 28 + docs/src/components/ui/use-toast.ts | 3 + docs/src/hooks/use-mobile.tsx | 19 + docs/src/hooks/use-toast.ts | 186 + docs/src/index.css | 153 + docs/src/lib/search-index.ts | 144 + docs/src/lib/utils.ts | 6 + docs/src/main.tsx | 5 + docs/src/pages/About.tsx | 13 + docs/src/pages/BSLMarkdownPage.tsx | 256 + docs/src/pages/Bucketing.tsx | 5 + docs/src/pages/Charting.tsx | 5 + docs/src/pages/ComposeModels.tsx | 5 + docs/src/pages/Dimensions.tsx | 75 + docs/src/pages/Filtering.tsx | 5 + docs/src/pages/Home.tsx | 89 + docs/src/pages/Indexing.tsx | 5 + docs/src/pages/Installation.tsx | 13 + docs/src/pages/Joins.tsx | 113 + docs/src/pages/JoinsRelationships.tsx | 174 + docs/src/pages/KeyFeatures.tsx | 13 + docs/src/pages/MCP.tsx | 5 + docs/src/pages/Measures.tsx | 69 + docs/src/pages/MultiModelQuery.tsx | 44 + docs/src/pages/NameConflicts.tsx | 39 + docs/src/pages/NestedSubtotals.tsx | 5 + docs/src/pages/NotFound.tsx | 25 + docs/src/pages/PercentageTotal.tsx | 5 + docs/src/pages/QueryMethods.tsx | 5 + docs/src/pages/Quickstart.tsx | 13 + docs/src/pages/SemanticTable.tsx | 5 + docs/src/pages/SemanticTableDefinition.tsx | 59 + docs/src/pages/Sessionized.tsx | 5 + docs/src/pages/Windowing.tsx | 5 + docs/src/pages/YAMLConfig.tsx | 5 + docs/src/vite-env.d.ts | 1 + docs/tailwind.config.ts | 95 + docs/tsconfig.app.json | 30 + docs/tsconfig.json | 16 + docs/tsconfig.node.json | 22 + docs/vite.config.ts | 20 + src/boring_semantic_layer/__init__.py | 5 - src/boring_semantic_layer/chart.py | 186 +- src/boring_semantic_layer/expr.py | 159 +- src/boring_semantic_layer/tests/test_chart.py | 60 + 177 files changed, 35003 insertions(+), 1381 deletions(-) delete mode 100644 .codespell.ignore-words create mode 100644 .github/workflows/docs-deploy.yml create mode 100644 docs/bun.lockb delete mode 100644 docs/chart_bar.png delete mode 100644 docs/chart_custom.png delete mode 100644 docs/chart_heatmap.png delete mode 100644 docs/chart_line.png delete mode 100644 docs/chart_quickstart.png delete mode 100644 docs/chart_timeseries.png create mode 100644 docs/components.json create mode 100644 docs/content/bucketing.md create mode 100644 docs/content/charting.md create mode 100644 docs/content/compose.md create mode 100644 docs/content/example.md create mode 100644 docs/content/getting-started.md create mode 100644 docs/content/indexing.md create mode 100644 docs/content/mcp.md create mode 100644 docs/content/mcp_example.yaml create mode 100644 docs/content/nested-subtotals.md create mode 100644 docs/content/percentage-total.md create mode 100644 docs/content/query-methods.md create mode 100644 docs/content/reference.md create mode 100644 docs/content/semantic-table.md create mode 100644 docs/content/sessionized.md create mode 100644 docs/content/windowing.md create mode 100644 docs/content/yaml-config.md create mode 100644 docs/content/yaml_example.yaml create mode 100644 docs/eslint.config.js create mode 100644 docs/index.html create mode 100644 docs/package-lock.json create mode 100644 docs/package.json create mode 100644 docs/postcss.config.js create mode 100644 docs/public/404.html create mode 100644 docs/public/bsl-data/bucketing.json create mode 100644 docs/public/bsl-data/building-semantic-tables.json create mode 100644 docs/public/bsl-data/charting.json create mode 100644 docs/public/bsl-data/compose.json create mode 100644 docs/public/bsl-data/example.json create mode 100644 docs/public/bsl-data/filtering.json create mode 100644 docs/public/bsl-data/getting-started.json create mode 100644 docs/public/bsl-data/indexing.json create mode 100644 docs/public/bsl-data/joins.json create mode 100644 docs/public/bsl-data/mcp.json create mode 100644 docs/public/bsl-data/nested-subtotals.json create mode 100644 docs/public/bsl-data/percentage-total.json create mode 100644 docs/public/bsl-data/query-methods.json create mode 100644 docs/public/bsl-data/reference.json create mode 100644 docs/public/bsl-data/semantic-table.json create mode 100644 docs/public/bsl-data/sessionized.json create mode 100644 docs/public/bsl-data/windowing.json create mode 100644 docs/public/bsl-data/yaml-config.json create mode 100644 docs/public/favicon.ico create mode 100644 docs/public/notebooks/bucketing_with_other.html create mode 100644 docs/public/notebooks/dimensional_indexing.html create mode 100644 docs/public/notebooks/nested_subtotals.html create mode 100644 docs/public/notebooks/percent_of_total.html create mode 100644 docs/public/notebooks/sessionized_data.html create mode 100644 docs/public/pages.json create mode 100644 docs/public/placeholder.svg create mode 100644 docs/public/robots.txt create mode 100644 docs/scripts/build_data.py create mode 100644 docs/src/App.tsx create mode 100644 docs/src/components/AdvancedPatterns.tsx create mode 100644 docs/src/components/AltairChart.tsx create mode 100644 docs/src/components/AppSidebar.tsx create mode 100644 docs/src/components/BSLQueryResult.tsx create mode 100644 docs/src/components/CodeBlock.tsx create mode 100644 docs/src/components/CollapsibleSetup.tsx create mode 100644 docs/src/components/CommandPalette.tsx create mode 100644 docs/src/components/Features.tsx create mode 100644 docs/src/components/Footer.tsx create mode 100644 docs/src/components/Installation.tsx create mode 100644 docs/src/components/Note.tsx create mode 100644 docs/src/components/QueryingTables.tsx create mode 100644 docs/src/components/QuickExample.tsx create mode 100644 docs/src/components/RegularOutput.tsx create mode 100644 docs/src/components/SearchButton.tsx create mode 100644 docs/src/components/SemanticTable.tsx create mode 100644 docs/src/components/StandardOutput.tsx create mode 100644 docs/src/components/TableOfContents.tsx create mode 100644 docs/src/components/ThemeToggle.tsx create mode 100644 docs/src/components/WhatIsBSL.tsx create mode 100644 docs/src/components/YamlContent.tsx create mode 100644 docs/src/components/ui/accordion.tsx create mode 100644 docs/src/components/ui/alert-dialog.tsx create mode 100644 docs/src/components/ui/alert.tsx create mode 100644 docs/src/components/ui/aspect-ratio.tsx create mode 100644 docs/src/components/ui/avatar.tsx create mode 100644 docs/src/components/ui/badge.tsx create mode 100644 docs/src/components/ui/breadcrumb.tsx create mode 100644 docs/src/components/ui/button.tsx create mode 100644 docs/src/components/ui/calendar.tsx create mode 100644 docs/src/components/ui/card.tsx create mode 100644 docs/src/components/ui/carousel.tsx create mode 100644 docs/src/components/ui/chart.tsx create mode 100644 docs/src/components/ui/checkbox.tsx create mode 100644 docs/src/components/ui/collapsible.tsx create mode 100644 docs/src/components/ui/command.tsx create mode 100644 docs/src/components/ui/context-menu.tsx create mode 100644 docs/src/components/ui/dialog.tsx create mode 100644 docs/src/components/ui/drawer.tsx create mode 100644 docs/src/components/ui/dropdown-menu.tsx create mode 100644 docs/src/components/ui/form.tsx create mode 100644 docs/src/components/ui/hover-card.tsx create mode 100644 docs/src/components/ui/input-otp.tsx create mode 100644 docs/src/components/ui/input.tsx create mode 100644 docs/src/components/ui/label.tsx create mode 100644 docs/src/components/ui/menubar.tsx create mode 100644 docs/src/components/ui/navigation-menu.tsx create mode 100644 docs/src/components/ui/pagination.tsx create mode 100644 docs/src/components/ui/popover.tsx create mode 100644 docs/src/components/ui/progress.tsx create mode 100644 docs/src/components/ui/radio-group.tsx create mode 100644 docs/src/components/ui/resizable.tsx create mode 100644 docs/src/components/ui/scroll-area.tsx create mode 100644 docs/src/components/ui/select.tsx create mode 100644 docs/src/components/ui/separator.tsx create mode 100644 docs/src/components/ui/sheet.tsx create mode 100644 docs/src/components/ui/sidebar.tsx create mode 100644 docs/src/components/ui/skeleton.tsx create mode 100644 docs/src/components/ui/slider.tsx create mode 100644 docs/src/components/ui/sonner.tsx create mode 100644 docs/src/components/ui/switch.tsx create mode 100644 docs/src/components/ui/table.tsx create mode 100644 docs/src/components/ui/tabs.tsx create mode 100644 docs/src/components/ui/textarea.tsx create mode 100644 docs/src/components/ui/toast.tsx create mode 100644 docs/src/components/ui/toaster.tsx create mode 100644 docs/src/components/ui/toggle-group.tsx create mode 100644 docs/src/components/ui/toggle.tsx create mode 100644 docs/src/components/ui/tooltip.tsx create mode 100644 docs/src/components/ui/use-toast.ts create mode 100644 docs/src/hooks/use-mobile.tsx create mode 100644 docs/src/hooks/use-toast.ts create mode 100644 docs/src/index.css create mode 100644 docs/src/lib/search-index.ts create mode 100644 docs/src/lib/utils.ts create mode 100644 docs/src/main.tsx create mode 100644 docs/src/pages/About.tsx create mode 100644 docs/src/pages/BSLMarkdownPage.tsx create mode 100644 docs/src/pages/Bucketing.tsx create mode 100644 docs/src/pages/Charting.tsx create mode 100644 docs/src/pages/ComposeModels.tsx create mode 100644 docs/src/pages/Dimensions.tsx create mode 100644 docs/src/pages/Filtering.tsx create mode 100644 docs/src/pages/Home.tsx create mode 100644 docs/src/pages/Indexing.tsx create mode 100644 docs/src/pages/Installation.tsx create mode 100644 docs/src/pages/Joins.tsx create mode 100644 docs/src/pages/JoinsRelationships.tsx create mode 100644 docs/src/pages/KeyFeatures.tsx create mode 100644 docs/src/pages/MCP.tsx create mode 100644 docs/src/pages/Measures.tsx create mode 100644 docs/src/pages/MultiModelQuery.tsx create mode 100644 docs/src/pages/NameConflicts.tsx create mode 100644 docs/src/pages/NestedSubtotals.tsx create mode 100644 docs/src/pages/NotFound.tsx create mode 100644 docs/src/pages/PercentageTotal.tsx create mode 100644 docs/src/pages/QueryMethods.tsx create mode 100644 docs/src/pages/Quickstart.tsx create mode 100644 docs/src/pages/SemanticTable.tsx create mode 100644 docs/src/pages/SemanticTableDefinition.tsx create mode 100644 docs/src/pages/Sessionized.tsx create mode 100644 docs/src/pages/Windowing.tsx create mode 100644 docs/src/pages/YAMLConfig.tsx create mode 100644 docs/src/vite-env.d.ts create mode 100644 docs/tailwind.config.ts create mode 100644 docs/tsconfig.app.json create mode 100644 docs/tsconfig.json create mode 100644 docs/tsconfig.node.json create mode 100644 docs/vite.config.ts 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 0000000000000000000000000000000000000000..f41003f46ec30e09b6b5787170776b3b9fd487f4 GIT binary patch literal 201126 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

6sjs!nK8W*iaE2FwMU-su)Yj_r;Y|gpcY@5_O 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 QG9?BawTt?!|EK@{4?Fr~Z~y=R literal 0 HcmV?d00001 diff --git a/docs/chart_bar.png b/docs/chart_bar.png deleted file mode 100644 index e21af8092241a9c960c45e095a0811ee22947434..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48520 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�D>7s2tlSaI@O@loosa_XVysuW^-3!PC{js?`_LL%T^%C zpX^aH)B6`%-?c8O9jKhwZe1K*9LoPK}khuKTY+h zHbRi&+eu7A#0=N~DLHlsnZLr^bC#Q8mwro&jt}zMjv1+!D6Y2a$p~KtjZ7#87cKbf zie-2@HMRUecez(Q{zhsjF)oShR~!RZK|$S^2J)SR z5Cf_n?8{caMGkccEIGt_3_*nrVJn9q57kUP`DlG-L_FUR5*UJ#Q{8$>Pdz*`64Tt+ z4#77LVPb}0Xw?!u!>c}S0Wi~yMQ~1u3LnkC$}g@kkL#=>d%5Oqqp_=k^3fM&=RgChgVemm|(b7q`W&eh?IA+ zYjOGq#Px8_#|pAfLOew3h+qh9D##9! zl?2q4gV?nB$fmJI?ql0Dc3B3&tp$9Z)P7rP>6gg-qPT^nC5)rLBT2SyeH{y(HhfjsZW1^yd_&F)C9zgV&KO}4gJmlTyW zq;*>t>v7nq6~l-y4P_>3ir&1MK?ZZ&XZWO{Mk6>=@5K=8n9yF@4L2tc6GRztH&`Bo?cuH#xRu1{Y zzjRrbnOSM$>!PI zG@@UU>HssrI@7|vF?k}1bAl{DjI;JcRB0wQ0+}K|qmZjrZ#1jM851Kop7@=a{e9$& z&%!e=V;vIF^B@`Vxhrn!{eDLO_hCG`e+EMMmwB0slV=^eO$I+&antK7O1~}&9xg_> zY<17HZhPb59mNPgb4-!oSlAfr76v&p#bZTQLDYkHKFzqfCfK(+ROF0Gb>dU4q==9; z^>NGyFqpWP5eS1{R_>rQ7&!%sqb2Y`3rmJ!J((OrWe7jP*pKDirWq~c?lqLPs!4k4 z!eD@+sFkO*I*yGFss^4b6M;K9Hw+D!UdD6CBB20mM^$<^3*WIK#O?laOpj9|kHCTw z)GBdG0>*!`fB*P)EmqM5-k?kt9_U)=6*-9;%yT1Svbw7nNEK#v7cwW3vmCSl{xUFN z>W+#ghlm%G+F_P9I-*i)qMkZq+r08U^Ljxxm<7D%RYYU6kc?SBS~Yi{6H{P9ZGn3$ zM@7Fa<1Z+p94Xg@OD?IBs5G1*V+>Uf5i372X!tO5a4uWNYnzX@h>GRu>8=YET`2#g znv&Ji7W}1pey%&Aw$b{GE_0{2w3=yqJBP=F)uFd?j=V+3l=C8&lCc~dl``e{@};i6 z)t0L&^~*+fO5?S{DK5@mBd!#gkrqKI*#WAx*!hzxg&aU(naBMG)MTj64ixA^QG;WC z)=1E0cu_l#`ZxuJUfSj(3)rQtsZ5A6t%n>Q89CvobC%Zn*3@?L%nGC=M3l3PydAa| zC4?9oUr$O8;DvEGcJhw|bKrg5=KDH-jbyFO_qwf<&%~ezcg&f{u|T+YqHYm(QE@#W zO3vsIa)~TykaU%4SWMVW8Y><+ffvWhC6L6StAPe1rw;*gbHHE;EA%-H7f&o`d3||c z2&HW1yog}|M~$q=#3D^AP#p^mS;I%`;(RO2m(r$~pI$HlLDR~5>O@d*$!1q#PO|<$ zf)Pp>8GGv%?1w_^@UY-$6N@Uki;8`_Z68{fh`K&#r+LcM#7rA}kueT2iLTOKzWK4I zsNirQ33BrbrB7}P6!e;LI3^p@5Na8M5yKdx{Uuq%D_e?_q&m`93ybp#9LVSal|Je+ z+7H7pjbw7J#(%dWed=e$_Vp%A2 zXel~9Dlsy*hJ`UEnI^cH=fZI-9*qD+iR#8`;gv|e0FS;ah{Mrxwxb&rV>cAmcAxld z@~ofJ)UXiH(Ro();CQ1@)OBao>f|EK@QCBi_?ajQFv?GZJRj9U*k#S*OSXY4Pm5a} zrO!i76(y>Se$KY=nZ;mehcIlw(Q`I6FQ<}qD-{}b2Lkq(hj=WXwnL^?0E}EXS;tCM zfes25FGSdb>IQs7JKB~+MvED0C9CVw$#_@1tG!`4`4%?-U zFGin)5HXT_kgN$pcmi^U%UXkiK@tHtsO1-ZA2?tw;wMZ6bBW-Y1;cEp0g{FGEAnoz z3KWMqKcLur3++wTB^iOr)&zaZYYgUW-$}+1uu@hkgO@`PB#*)xk0xrz0CDz!34jQ> z#gnT5W07Nk%a<%PrzG{`HxL5GMB*s7YJ`Zkh=Q>6Ch}FUvNZ*FFKC>*I#Jh;^NOBD zQT;i#KYLl=j*&53W`>yh02VS%Bc&?kK-A%FXn`YtIN@08`ZFC;KiyA5POQ3 z^i|KQoC_=SG7nz)!}{Xiq%~cs2sQM%wiy6gzM%rg&MEcDjRt%BMs4K*Vo!X1F zmf+>Nr6OF)96X$z+>V102qtO3k9nZP5D9}#?p|#_LN&0?3ikvx0BkSW9WuT+pp7~y zrd&+<&DeS!JGzLi{?WWA$#yOlauHGtJT^kJf~oU>5>pHI&q&_AJFR95RRSkrSvkpa zqQA%uFPjOVX0XuDgHooY1*bWP z;my>jj6-z8(QA9m$q( zXFBph#GI*d8A}rGe);AtP0P|`{V3YN(Mwv_n$;-jHTZNFG3L}x+P|#Kpl!pcH))z# z^J~t{#+1ZZiaUDEDa1vayB8JTw!m#(hjgaY&@k^)c}!+S2`S9OTf-NfsnHq*Xx<5H zEEg{3Um8;4F&2F{{(^uhrG~` zZc-&u!(BvNC`%J>%=!gtn@kT>p0UV8uP{+eo?Wd?@QfW_ z)hpf^({3Y%u%q}KCC{Br%fPZyRdFno@!B9Wx+J5)IVX_EWT_~3 z8)eR4lG;61)ibcz;!Uh+lYM|u{i-z1E^o8-rgxll7*^3Wuo(q8t>{W@jStcFOy2qI zW!L?wxc6%cle0XWE_z88+8OVmcMYeY^>1Y5Eel6cwyiK5!N8TpjW`qz0uR|15Vd5T z&pm_11E@SU%agS1le4DQ_xEyTMPGRyac8-q>|x~lua-_6!DTmb2AC8N>Bp5Q zu0+~B3~@^oy7EHrMq}>~qw|luvs2t=Vmv?*cW0r?Wppq#u0)}GX=3{7I3@~RdlR=r z;Twix0{XvMCCbI^0bx#WA4rgz-e2aqP({x=J11C=jgKF`RJ1n8Xc7h13E*-If?WkS z|Llt^SiCgBZE{dCw(g8vmN6rtsE9N73$U(aGFSne{EncVB_o2P3S^ym#F#~;xbIFY zjm@j*%Hwf;kOo={0DgiEJaE!&EXrn2bMnSI3-zCHWK*^fXSWy}98T>sbk&i!w(;mK z`Y{SZGq0#9t&$`Uj-JX(EJ$`i7-V?BX&0Y+^sGDzgBw}M#v}b8&$yqdYh+Gyu|@;x ztiK`U$l7$YZe)&ujO>$9Gseg2J0L2N>(QLQ~Y&MA;!Mq-GCtgyyB%#V`*I0^O` z&a|TsE6d?g@h@)~V121+mz9_<>on(TsAGsCx|-O(05o#w#!k@bf|0+lky{xVBcvmE z2m?o(an~195+Yr}pP=3odD4czQ>gi3na{l}(&VF}jRV$=wn=@->?GLmNW;@yu&69o zwHV(|#fhKbG_wPQW2p-<#~%mUaiEQ>Pj%7h$X#w+iQ-BWS0e2KV1pNqX?aIs-s8Mc zoHsIF9UWJqAuVs*oei~V;z|@(qSJQYaV0wQN|Yw=XT*(wqE@%CbNdBsafN@|LCQ!ywPjLVoH48=qpAB6GxUV&Sf{qCrso#WJB#F0Syx z<~{c`d%RFhW~`5(P|Zj^`Oa=RKdXT-k34yE^2Uf zBhS=F?OOXTlI$p+WQ$dCk5%(II7qFJMtG2-22u?=-O1wa?95$VU4OT9>CzuQvFF~F zBTc!1rjiXa7fq|VK>1{bI-#7!&EY$i!)4kayn+p^vCH=TXLhIMJ>JkV$JRF|Fq_Rk zvkpI>^{je5?~b{y{G;o7(?4f}?*Gj8O%1-rcJ<=Tw(Zsz*hTMo@qTaBaa6XKeRN&< zrRqJW{_-0Vu5RnH{!XizKKWFF5VJZ=5*m zcyG(l!W{i0VyqP9FmKlT)Gc9JdW13zI@IzU!Fs-;UN2fC+gm{YyVE~MB+=C-~n*h?fL+${V>*F^Pti+h^5x~XB$ z{9d(*v^1*x1NnloMFnS#4cZ5?v;ODG@`Z(a+-#N3l5y*=@H?7WoY~+nSV7IU5a3SL ziCQljN1)l^(wdEnYUJ7`WV?dvXsc_*HLbGg0}9f4GaH)qpiwbPuGLAL;SQ17bKyng z0R82-wuvp8(RahX1OLSh1ukaE4&9b2?_z89v2A~MVb)T7CmPh#(5h3AUqh~tT8vLq zW?-euc)!7KNOQAiNA5vXM~imb16PQ);T32lRA^_0hnF0JH@^hCbxn6KCdu+^^*#=o zi92NHz=D5MTz^=$XI}Z`=D(bTp4+&z7!-Ol8{yyv3SKY=!IEp{YSl`wjQ;eIRA-jc zorTWV-6VTKMB8Pf+gT3xEH<5yR~c~yZe+_HHq_xGv1IC$TK6rr+wvRs=Jz6}3VCC( zwI((fQhM#C2bt4I-hz_A)`{F{rLu z-+HaxOf}csA@v8^1AU)d$zu~l6m_493?+M}Ltks!LS10=5^;3m4nbrwal*;jpW|#| zLa6q<$_3TxYB&|q4s`R|}& zmbDaDQH1OU4x{m~7kbn%$%jv4ZWIG84<%FZhOoDkI5hFDPM+pkTUUAmY_0uW>N&Qg z#CLoqK38aOL1&!YgH)-onpJn7h*%KVfdCFFWb9-zwH8m}JsY?Vm}`qvB)))W&SRYAq&{0+TL((l@0k*-xQ+ZUf>GBTe;PbDSJ&x)%(Q}yozUN#bD3hXtuY~ zyzm{E#M=W^>^UdgNLi|*|3QbieS^jTo#edhoxYobvp6$xC3Au+d?&M-^G(#qB3dp4QmtGjksnuG%s1ilc)nLED1sleqx6 zv5;C0ywwWjM52{4;)qsT1`ksbax2(R!hqR-o-CTF{)AeXr)10XRIRwVrY95I%2* z7C^a(J zw=$QmX*}?PC})DX!-sgQ032WK6XI1S*|AkPP|+8T2!Z>F3SAIoP(vyikypwomHiTI zC`ytR!LpU})|`TR34h1am>WdjY~BH@L|nSG1fm;P$z*obM#WB&yiu5obR$2m9e9;@ z$g-6e4GFy?-#DFbG%hhU2rucsQQ8cx4;-q1zM5KcKEY!65~1KMej8Pfn$whm{NazT<=)`!n0#BEPrKB!$>DNVWN0xh_ z=%u8B`Ve}4@R+lU2jn2&}afpp@W^6Ls>9Xzt1D zORVd(sNxTtH9F$8)i<}fas-f)woBr_NaYlFzLYy*BmMDHAtM1h2iKtxk!!AcpA(no z6cf3t8wpD|6@W2VS?Cab_+FzQISK60v(q-Y-Sj21dD{>tLpy5O%E@Y_MCt<^37z^a z0&5d_bM!DC5CpY>7aMDlNiFSw5JPB(p+ ztjoeRJ87TMD78w@0HncsB}*H{K6Ux3dD5=G7l_*)xbq25-$qk47xFsowcUryg9s$V*A_I{ z_i(#FtEuI{o7mdg80DIpSk(UzpoR~F8o;Q-QSv&bnF`69pTZMTbx_XB`11h`O}YUv zwe!gW8x33~O3o^l(aGc*VX}kADVAYzmFSxY0c8fKW^okqH$v8bgp=)S%pA(I(*)xQ$M76VisF0Gjf8?}Uy#){I{S9D^dp~_dCjR0ni z`X%gBK4-#lu5LcdTmf`4b}|gyW338H7W6D5tF4_vjKhB3wVu)pddC7TYQ%Mt!;si#zbLuI1Cx07TLQhodS7-TzrVDS zE2mYoN(;7*b`QPA=_{%E@SRegL5?rw9*g20T!b*coAgL7_SqO z;!ebRFc_2}L@nh{@5`HyNgV-Q2#_C+(x&!eFkC=*yWeaV*i4f_F2i|J)ud%X)X58C zK_iL(ad68JLt>u^h=cPUf_=^%-$)`GP@i6e?T^64DpPx*zH(G(LW8wRXKRU=q2)5o4xpAwnUNI&|(1hUi!fx*hJFRdZJWz|boIwp{Zp z=`TjHaNwZdk|k}(6`?nS_oj>_VNJ`ZX#zAJsQztD?#XS6d^bMa{J!A$T$(@?LA_^e z6C?*IlT%ANGGoq#@fuoNGwfqviayp90>dYAHSo%c7a00f^<; z74OR}17Fc;Ce?s84;Dn)B09fmHN6+nFT!MqZQFuA@;t>vjDxUD7~UH|G)$IuoFSXd zQ&VHo`v6miPS8hQLqBeG%Tq>&J1h!cS4DH4TOo!@d?K5agTqvSfN8;8?k_ zXfOdth+Lqo;Y>Ov0r2k1BtqEE_^YspXH=@ zx&VCKF%M!E`c>piVEFNu%5!hVrK9w;!u@`tcxQKh|EY1=k3ISgMf{m_=iPU#|EGtZ F`X4eJw2A-# diff --git a/docs/chart_custom.png b/docs/chart_custom.png deleted file mode 100644 index c24ff91cf3bc5c72cf50f91588e1445cf3f4fffb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64381 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 S)QW#LfA({qIr8a8p86NCv5+hP diff --git a/docs/chart_line.png b/docs/chart_line.png deleted file mode 100644 index 4e33620753934dc490ba9cccfc606fc77a5a9d45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41844 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 diff --git a/docs/chart_quickstart.png b/docs/chart_quickstart.png deleted file mode 100644 index 32dfb98e274be5da8a93e45350208ccb52e29877..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27313 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 diff --git a/docs/chart_timeseries.png b/docs/chart_timeseries.png deleted file mode 100644 index 505cc102d0aa5f40cf7b5e346bc6a4a38c69338b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75400 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 ASpWb4 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..ad8b6d1b --- /dev/null +++ b/docs/index.html @@ -0,0 +1,51 @@ + + + + + + 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..e6ad8ad0 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,88 @@ +{ + "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", + "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" + } +} 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 0000000000000000000000000000000000000000..dd5a12627d36db7eb9c19fa2f931ff1509f0323e GIT binary patch literal 7645 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 LNkvXXu0mjf>#GbK literal 0 HcmV?d00001 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/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) =>