diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0986b2c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +_research diff --git a/backend/app/agent/chart.py b/backend/app/agent/chart.py new file mode 100644 index 0000000..bfbbe9f --- /dev/null +++ b/backend/app/agent/chart.py @@ -0,0 +1,236 @@ +import json +from typing import List, Dict, Any, Optional +import sys +from pathlib import Path + +# Add project root to sys.path +PROJECT_ROOT = Path(__file__).resolve().parents[3] +if str(PROJECT_ROOT) not in sys.path: + sys.path.append(str(PROJECT_ROOT)) + +from nanobot.providers.litellm_provider import LiteLLMProvider +from app.api.llm import _load_data as load_llm_config +from app.schemas.chart import ChartGenerationResponse + +CHART_INSTRUCTIONS = """ +### INSTRUCTIONS ### + +- Chart types: Bar chart, Line chart, Multi line chart, Area chart, Pie chart, Stacked bar chart, Grouped bar chart +- You can only use the chart types provided in the instructions +- Generated chart should answer the user's question and based on the semantics of the SQL query, and the sample data, sample column values are used to help you generate the suitable chart type +- If the sample data is not suitable for visualization, you must return an empty string for the schema and chart type +- If the sample data is empty, you must return an empty string for the schema and chart type +- The language for the chart and reasoning must be the same language provided by the user +- Please use the current time provided by the user to generate the chart +- In order to generate the grouped bar chart, you need to follow the given instructions: + - Disable Stacking: Add "stack": null to the y-encoding. + - Use xOffset for subcategories to group bars. + - Don't use "transform" section. +- In order to generate the pie chart, you need to follow the given instructions: + - Add {"type": "arc"} to the mark section. + - Add "theta" encoding to the encoding section. + - Add "color" encoding to the encoding section. + - Don't add "innerRadius" to the mark section. +- If the x-axis of the chart is a temporal field, the time unit should be the same as the question user asked. + - For yearly question, the time unit should be "year". + - For monthly question, the time unit should be "yearmonth". + - For weekly question, the time unit should be "yearmonthdate". + - For daily question, the time unit should be "yearmonthdate". + - Default time unit is "yearmonth". +- For each axis, generate the corresponding human-readable title based on the language provided by the user. +- Make sure all of the fields(x, y, xOffset, color, etc.) in the encoding section of the chart schema are present in the column names of the data. + +### GUIDELINES TO PLOT CHART ### + +1. Understanding Your Data Types +- Nominal (Categorical): Names or labels without a specific order (e.g., types of fruits, countries). +- Ordinal: Categorical data with a meaningful order but no fixed intervals (e.g., rankings, satisfaction levels). +- Quantitative: Numerical values representing counts or measurements (e.g., sales figures, temperatures). +- Temporal: Date or time data (e.g., timestamps, dates). +2. Chart Types and When to Use Them +- Bar Chart + - Use When: Comparing quantities across different categories. + - Data Requirements: + - One categorical variable (x-axis). + - One quantitative variable (y-axis). + - Example: Comparing sales numbers for different product categories. +- Grouped Bar Chart + - Use When: Comparing sub-categories within main categories. + - Data Requirements: + - Two categorical variables (x-axis grouped by one, color-coded by another). + - One quantitative variable (y-axis). + - Example: Sales numbers for different products across various regions. +- Line Chart + - Use When: Displaying trends over continuous data, especially time. + - Data Requirements: + - One temporal or ordinal variable (x-axis). + - One quantitative variable (y-axis). + - Example: Tracking monthly revenue over a year. +- Multi Line Chart + - Use When: Displaying trends over continuous data, especially time. + - Data Requirements: + - One temporal or ordinal variable (x-axis). + - Two or more quantitative variables (y-axis and color). + - Implementation Notes: + - Uses `transform` with `fold` to combine multiple metrics into a single series + - The folded metrics are distinguished using the color encoding + - Example: Tracking monthly click rate and read rate over a year. +- Area Chart + - Use When: Similar to line charts but emphasizing the volume of change over time. + - Data Requirements: + - Same as Line Chart. + - Example: Visualizing cumulative rainfall over months. +- Pie Chart + - Use When: Showing parts of a whole as percentages. + - Data Requirements: + - One categorical variable. + - One quantitative variable representing proportions. + - Example: Market share distribution among companies. +- Stacked Bar Chart + - Use When: Showing composition and comparison across categories. + - Data Requirements: Same as grouped bar chart. + - Example: Sales by region and product type. +""" + +CHART_EXAMPLES = """ +### EXAMPLES ### + +1. Bar Chart +- Sample Data: + [ + {"Region": "North", "Sales": 100}, + {"Region": "South", "Sales": 200}, + {"Region": "East", "Sales": 300}, + {"Region": "West", "Sales": 400} +] +- Chart Schema: +{ + "title": , + "mark": {"type": "bar"}, + "encoding": { + "x": {"field": "Region", "type": "nominal", "title": }, + "y": {"field": "Sales", "type": "quantitative", "title": }, + "color": {"field": "Region", "type": "nominal", "title": ""} + } +} +2. Line Chart +- Sample Data: +[ + {"Date": "2022-01-01", "Sales": 100}, + {"Date": "2022-01-02", "Sales": 200}, + {"Date": "2022-01-03", "Sales": 300}, + {"Date": "2022-01-04", "Sales": 400} +] +- Chart Schema: +{ + "title": , + "mark": {"type": "line"}, + "encoding": { + "x": {"field": "Date", "type": "temporal", "title": }, + "y": {"field": "Sales", "type": "quantitative", "title": } + } +} +""" + +async def generate_chart(data: List[Dict[str, Any]], query: str) -> ChartGenerationResponse: + # 1. Initialize Provider + llm_configs = load_llm_config() + active_config = next((c for c in llm_configs if c.get("is_active")), None) + + if not active_config: + return ChartGenerationResponse( + reasoning="No active LLM configuration found", + can_visualize=False, + chart_type="" + ) + + try: + provider = LiteLLMProvider( + api_key=active_config.get("api_key"), + api_base=active_config.get("api_base"), + default_model=active_config.get("model"), + extra_headers=active_config.get("extra_headers") or {}, + provider_name=active_config.get("provider") + ) + except Exception as e: + return ChartGenerationResponse( + reasoning=f"Failed to initialize LLM provider: {e}", + can_visualize=False, + chart_type="" + ) + + # 2. Prepare Data Sample + if not data: + return ChartGenerationResponse( + reasoning="No data provided to visualize", + can_visualize=False, + chart_type="" + ) + + sample_size = 5 + sample_data = data[:sample_size] + # Handle case where data might not be list of dicts + if isinstance(data[0], (list, tuple)): + # If it's a list of lists, we can't easily infer columns without more info. + # For now, assume it's list of dicts as per postgres/clickhouse connector expectation (formatted_results) + columns = [f"col_{i}" for i in range(len(data[0]))] + else: + columns = list(data[0].keys()) + + # 3. Construct Prompt + schema_json = json.dumps(ChartGenerationResponse.model_json_schema(), indent=2) + + system_prompt = f"""You are a data analyst great at visualizing data using vega-lite! Given the user's question, sample data and sample column values, you need to generate vega-lite schema in JSON and provide suitable chart type. +Besides, you need to give a concise and easy-to-understand reasoning to describe why you provide such vega-lite schema based on the question, sample data and sample column values. + +{CHART_INSTRUCTIONS} + +{CHART_EXAMPLES} + +- If the user provides a custom instruction, it should be followed strictly and you should use it to change the style of response for reasoning. + +### OUTPUT FORMAT ### + +You must return a valid JSON object strictly matching the following JSON Schema: + +{schema_json} + +Please provide your chain of thought reasoning, chart type and the vega-lite schema in JSON format. +""" + + user_prompt = f""" +### INPUT ### +Question: {query} +Sample Data: {json.dumps(sample_data, indent=2, default=str)} +Sample Column Values: {columns} +Language: Chinese (Simplified) + +Please think step by step +""" + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt} + ] + + # 4. Call LLM + try: + response = await provider.chat(messages=messages) + content = response.content + + # Clean up code blocks + if "```json" in content: + content = content.split("```json")[1].split("```")[0] + elif "```" in content: + content = content.split("```")[1].split("```")[0] + + content = content.strip() + result = json.loads(content) + return ChartGenerationResponse(**result) + + except Exception as e: + return ChartGenerationResponse( + reasoning=f"Failed to generate chart configuration: {str(e)}", + can_visualize=False, + chart_type="" + ) diff --git a/backend/app/agent/nl2sql.py b/backend/app/agent/nl2sql.py index 200304e..cb0e6a3 100644 --- a/backend/app/agent/nl2sql.py +++ b/backend/app/agent/nl2sql.py @@ -14,6 +14,8 @@ from nanobot.providers.litellm_provider import LiteLLMProvider from app.connectors.postgres import postgres_connector from app.connectors.clickhouse import clickhouse_connector from app.api.llm import _load_data as load_llm_config +from app.schemas.chart import ChartGenerationResponse +from app.agent.chart import generate_chart class NL2SQLRequest(BaseModel): query: str = Field(..., description="User's natural language query") @@ -23,6 +25,60 @@ class NL2SQLResponse(BaseModel): sql: str result: List[Dict[str, Any]] error: Optional[str] = None + chart: Optional[ChartGenerationResponse] = None + +# WrenAI-inspired SQL Rules +DEFAULT_TEXT_TO_SQL_RULES = """ +### SQL RULES ### +- ONLY USE SELECT statements, NO DELETE, UPDATE OR INSERT etc. statements that might change the data in the database. +- ONLY USE the tables and columns mentioned in the database schema. +- ONLY USE "*" if the user query asks for all the columns of a table. +- ONLY CHOOSE columns belong to the tables mentioned in the database schema. +- DON'T INCLUDE comments in the generated SQL query. +- YOU MUST USE "JOIN" if you choose columns from multiple tables! +- PREFER USING CTEs over subqueries. +- When generating SQL query, always: + - Put double quotes around column and table names. + - Put single quotes around string literals. + - Never quote numeric literals. + For example: SELECT "customers"."customer_name" FROM "customers" WHERE "customers"."city" = 'Taipei' and "customers"."year" = 1992; +- YOU MUST USE "lower(.) like lower()" function or "lower(.) = lower()" function for case-insensitive comparison! + - Use "lower(.) LIKE lower()" when: + - The user requests a pattern or partial match. + - The value is not specific enough to be a single, exact value. + - Wildcards (%) are needed to capture the pattern. + - Use "lower(.) = lower()" when: + - The user requests an exact, specific value. + - There is no ambiguity or pattern in the value. +- If the column is date/time related field, and it is a INT/BIGINT/DOUBLE/FLOAT type, please use the appropriate function mentioned in the SQL FUNCTIONS section to cast the column to "TIMESTAMP" type first before using it in the query +- ALWAYS CAST the date/time related field to "TIMESTAMP WITH TIME ZONE" type when using them in the query +- If the user asks for a specific date, please give the date range in SQL query +- Aggregate functions are not allowed in the WHERE clause. Instead, they belong in the HAVING clause, which is used to filter after aggregation. +- You can only add "ORDER BY" and "LIMIT" to the final "UNION" result. +- For the ranking problem, you must use the ranking function, `DENSE_RANK()` to rank the results and then use `WHERE` clause to filter the results. +- For the ranking problem, you must add the ranking column to the final SELECT clause. +""" + +SQL_GENERATION_SYSTEM_PROMPT = f""" +You are a helpful assistant that converts natural language queries into ANSI SQL queries. + +Given user's question, database schema, etc., you should think deeply and carefully and generate the SQL query based on the given reasoning plan step by step. + +### GENERAL RULES ### + +1. YOU MUST FOLLOW the instructions strictly to generate the SQL query if the section of USER INSTRUCTIONS is available in user's input. +2. YOU MUST FOLLOW SQL Rules if they are not contradicted with instructions. + +{DEFAULT_TEXT_TO_SQL_RULES} + +### FINAL ANSWER FORMAT ### +The final answer must be a ANSI SQL query in JSON format: + +{{ + "reasoning": , + "sql": +}} +""" async def process_nl2sql(request: NL2SQLRequest) -> NL2SQLResponse: # 1. Get the connector and schema @@ -53,35 +109,50 @@ async def process_nl2sql(request: NL2SQLRequest) -> NL2SQLResponse: api_key=active_config.get("api_key"), api_base=active_config.get("api_base"), default_model=active_config.get("model"), - extra_headers=active_config.get("extra_headers") + extra_headers=active_config.get("extra_headers") or {}, + provider_name=active_config.get("provider") ) except Exception as e: return NL2SQLResponse(sql="", result=[], error=f"Failed to initialize LLM provider: {e}") # 4. Construct Prompt - prompt = f"""You are an expert SQL generator. -Given the following database schema for a {request.source} database: + user_prompt = f""" +### DATABASE SCHEMA ### {schema_str} -Write a SQL query to answer the following question: -"{request.query}" +### INPUTS ### +User's Question: {request.query} +Language: Chinese (Simplified) -Return ONLY the SQL query. Do not include any markdown formatting, explanations, or code blocks. Just the raw SQL string. +Let's think step by step. """ + messages = [ + {"role": "system", "content": SQL_GENERATION_SYSTEM_PROMPT}, + {"role": "user", "content": user_prompt} + ] + # 5. Call LLM try: - # provider.complete returns a string - response = await provider.complete(prompt) - sql_query = response.strip() - # Remove potential markdown code blocks if the LLM ignores instructions - if sql_query.startswith("```sql"): - sql_query = sql_query[6:] - if sql_query.startswith("```"): - sql_query = sql_query[3:] - if sql_query.endswith("```"): - sql_query = sql_query[:-3] - sql_query = sql_query.strip() + response = await provider.chat(messages=messages) + content = response.content.strip() + + # Clean up code blocks + if "```json" in content: + content = content.split("```json")[1].split("```")[0] + elif "```" in content: + content = content.split("```")[1].split("```")[0] + + content = content.strip() + + try: + result_json = json.loads(content) + sql_query = result_json.get("sql", "").strip() + reasoning = result_json.get("reasoning", "") # We can log this or return it if needed + except json.JSONDecodeError: + # Fallback if LLM doesn't return valid JSON despite instructions + sql_query = content + except Exception as e: return NL2SQLResponse(sql="", result=[], error=f"LLM generation failed: {e}") @@ -100,7 +171,14 @@ Return ONLY the SQL query. Do not include any markdown formatting, explanations, # Let's assume we can just return the raw tuples for now or try to fetch column names. # For now, let's just return as list of lists/tuples if it's not a dict formatted_results = [list(row) for row in results] + + # 7. Generate Chart + chart_response = None + if formatted_results: + # Only try to generate chart if we have results + # Convert to list of dicts if possible, or pass as is + chart_response = await generate_chart(formatted_results, request.query) - return NL2SQLResponse(sql=sql_query, result=formatted_results) + return NL2SQLResponse(sql=sql_query, result=formatted_results, chart=chart_response) except Exception as e: return NL2SQLResponse(sql=sql_query, result=[], error=f"SQL execution failed: {e}") diff --git a/backend/app/schemas/chart.py b/backend/app/schemas/chart.py new file mode 100644 index 0000000..c85e884 --- /dev/null +++ b/backend/app/schemas/chart.py @@ -0,0 +1,135 @@ +from typing import Any, Dict, List, Optional, Literal, Union +from pydantic import BaseModel, Field + +# Base Chart Schema +class ChartSchema(BaseModel): + class ChartType(BaseModel): + type: Literal["bar", "line", "area", "arc"] + + class ChartEncoding(BaseModel): + field: str + type: Literal["ordinal", "quantitative", "nominal"] + title: str + + title: str + mark: ChartType + encoding: ChartEncoding + +class TemporalChartEncoding(ChartSchema.ChartEncoding): + type: Literal["temporal"] = Field(default="temporal") + timeUnit: str = Field(default="yearmonth") + +# Line Chart +class LineChartSchema(ChartSchema): + class LineChartMark(BaseModel): + type: Literal["line"] = Field(default="line") + + class LineChartEncoding(BaseModel): + x: Union[TemporalChartEncoding, ChartSchema.ChartEncoding] + y: ChartSchema.ChartEncoding + color: Optional[ChartSchema.ChartEncoding] = None + + mark: LineChartMark + encoding: LineChartEncoding + +# Multi Line Chart +class MultiLineChartSchema(ChartSchema): + class MultiLineChartMark(BaseModel): + type: Literal["line"] = Field(default="line") + + class MultiLineChartTransform(BaseModel): + fold: List[str] + as_: List[str] = Field(alias="as") + + class MultiLineChartEncoding(BaseModel): + x: Union[TemporalChartEncoding, ChartSchema.ChartEncoding] + y: ChartSchema.ChartEncoding + color: ChartSchema.ChartEncoding + + mark: MultiLineChartMark + transform: List[MultiLineChartTransform] + encoding: MultiLineChartEncoding + +# Bar Chart +class BarChartSchema(ChartSchema): + class BarChartMark(BaseModel): + type: Literal["bar"] = Field(default="bar") + + class BarChartEncoding(BaseModel): + x: Union[TemporalChartEncoding, ChartSchema.ChartEncoding] + y: ChartSchema.ChartEncoding + color: Optional[ChartSchema.ChartEncoding] = None + + mark: BarChartMark + encoding: BarChartEncoding + +# Grouped Bar Chart +class GroupedBarChartSchema(ChartSchema): + class GroupedBarChartMark(BaseModel): + type: Literal["bar"] = Field(default="bar") + + class GroupedBarChartEncoding(BaseModel): + x: Union[TemporalChartEncoding, ChartSchema.ChartEncoding] + y: ChartSchema.ChartEncoding + xOffset: ChartSchema.ChartEncoding + color: ChartSchema.ChartEncoding + + mark: GroupedBarChartMark + encoding: GroupedBarChartEncoding + +# Stacked Bar Chart +class StackedBarChartYEncoding(ChartSchema.ChartEncoding): + stack: Literal["zero"] = Field(default="zero") + +class StackedBarChartSchema(ChartSchema): + class StackedBarChartMark(BaseModel): + type: Literal["bar"] = Field(default="bar") + + class StackedBarChartEncoding(BaseModel): + x: Union[TemporalChartEncoding, ChartSchema.ChartEncoding] + y: StackedBarChartYEncoding + color: ChartSchema.ChartEncoding + + mark: StackedBarChartMark + encoding: StackedBarChartEncoding + +# Pie Chart +class PieChartSchema(ChartSchema): + class PieChartMark(BaseModel): + type: Literal["arc"] = Field(default="arc") + + class PieChartEncoding(BaseModel): + theta: ChartSchema.ChartEncoding + color: ChartSchema.ChartEncoding + + mark: PieChartMark + encoding: PieChartEncoding + +# Area Chart +class AreaChartSchema(ChartSchema): + class AreaChartMark(BaseModel): + type: Literal["area"] = Field(default="area") + + class AreaChartEncoding(BaseModel): + x: Union[TemporalChartEncoding, ChartSchema.ChartEncoding] + y: ChartSchema.ChartEncoding + + mark: AreaChartMark + encoding: AreaChartEncoding + +# Response Model +class ChartGenerationResponse(BaseModel): + reasoning: str = Field(..., description="Reasoning for the chart choice or why a chart cannot be generated") + chart_type: Literal[ + "line", "multi_line", "bar", "pie", "grouped_bar", "stacked_bar", "area", "" + ] = Field(..., description="The type of chart generated, or empty string if none") + chart_spec: Optional[Union[ + LineChartSchema, + MultiLineChartSchema, + BarChartSchema, + PieChartSchema, + GroupedBarChartSchema, + StackedBarChartSchema, + AreaChartSchema + ]] = Field(None, description="The generated Vega-Lite chart specification") + can_visualize: bool = Field(..., description="Whether the data can be visualized") diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f0aa74c..fb63002 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,12 +21,15 @@ "react-grid-layout": "^2.2.2", "react-markdown": "^10.1.0", "react-router-dom": "^7.13.1", + "react-vega": "^8.0.0", "recharts": "^3.8.0", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "shadcn": "^4.0.6", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.4.0", + "vega": "^6.2.0", + "vega-lite": "^6.4.2", "zustand": "^5.0.11" }, "devDependencies": { @@ -3036,6 +3039,12 @@ "@types/estree": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmmirror.com/@types/hast/-/hast-3.0.4.tgz", @@ -4250,6 +4259,73 @@ "node": ">=12" } }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "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.npmmirror.com/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", @@ -4259,6 +4335,20 @@ "node": ">=12" } }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-format": { "version": "3.1.2", "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz", @@ -4268,6 +4358,57 @@ "node": ">=12" } }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo-projection": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz", + "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==", + "license": "ISC", + "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.npmmirror.com/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -4289,6 +4430,15 @@ "node": ">=12" } }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", @@ -4305,6 +4455,19 @@ "node": ">=12" } }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", @@ -4465,6 +4628,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", @@ -5116,6 +5288,13 @@ "node": ">= 6" } }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "license": "MIT", + "peer": true + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -6227,6 +6406,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", @@ -8621,6 +8806,19 @@ } } }, + "node_modules/react-vega": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/react-vega/-/react-vega-8.0.0.tgz", + "integrity": "sha512-euye4Gec2ScnUK1zbSA2tzZXUwBmbba8bfbzaRVhdEJTGQfaD78bSgqrccrl9b2fKZS1TZXR0NADEHVe6nxvBg==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19", + "vega-embed": "^7" + } + }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmmirror.com/recast/-/recast-0.23.11.tgz", @@ -8834,6 +9032,12 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.0.tgz", @@ -8940,6 +9144,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -9485,6 +9695,26 @@ "node": ">=0.6" } }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "license": "ISC", + "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.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/tough-cookie": { "version": "6.0.1", "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.1.tgz", @@ -9898,6 +10128,561 @@ "node": ">= 0.8" } }, + "node_modules/vega": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/vega/-/vega-6.2.0.tgz", + "integrity": "sha512-BIwalIcEGysJdQDjeVUmMWB3e50jPDNAMfLJscjEvpunU9bSt7X1OYnQxkg3uBwuRRI4nWfFZO9uIW910nLeGw==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-canvas/-/vega-canvas-2.0.0.tgz", + "integrity": "sha512-9x+4TTw/USYST5nx4yN272sy9WcqSRjAR0tkQYZJ4cQIeon7uVsnohvoPQK1JZu7K1QXGUqzj08z0u/UegBVMA==", + "license": "BSD-3-Clause" + }, + "node_modules/vega-crossfilter": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/vega-crossfilter/-/vega-crossfilter-5.1.0.tgz", + "integrity": "sha512-EmVhfP3p6AM7o/lPan/QAoqjblI19BxWUlvl2TSs0xjQd8KbaYYbS4Ixt3cmEvl0QjRdBMF6CdJJ/cy9DTS4Fw==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-dataflow/-/vega-dataflow-6.1.0.tgz", + "integrity": "sha512-JxumGlODtFbzoQ4c/jQK8Tb/68ih0lrexlCozcMfTAwQ12XhTqCvlafh7MAKKTMBizjOfaQTHm4Jkyb1H5CfyQ==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-embed/-/vega-embed-7.1.0.tgz", + "integrity": "sha512-ZmEIn5XJrQt7fSh2lwtSdXG/9uf3yIqZnvXFEwBJRppiBgrEWZcZbj6VK3xn8sNTFQ+sQDXW5sl/6kmbAW3s5A==", + "license": "BSD-3-Clause", + "peer": true, + "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-embed/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vega-encode": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/vega-encode/-/vega-encode-5.1.0.tgz", + "integrity": "sha512-q26oI7B+MBQYcTQcr5/c1AMsX3FvjZLQOBi7yI0vV+GEn93fElDgvhQiYrgeYSD4Exi/jBPeUXuN6p4bLz16kA==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-event-selector/-/vega-event-selector-4.0.0.tgz", + "integrity": "sha512-CcWF4m4KL/al1Oa5qSzZ5R776q8lRxCj3IafCHs5xipoEHrkgu1BWa7F/IH5HrDNXeIDnqOpSV1pFsAWRak4gQ==", + "license": "BSD-3-Clause" + }, + "node_modules/vega-expression": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/vega-expression/-/vega-expression-6.1.0.tgz", + "integrity": "sha512-hHgNx/fQ1Vn1u6vHSamH7lRMsOa/yQeHGGcWVmh8fZafLdwdhCM91kZD9p7+AleNpgwiwzfGogtpATFaMmDFYg==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/estree": "^1.0.8", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-force": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/vega-force/-/vega-force-5.1.0.tgz", + "integrity": "sha512-wdnchOSeXpF9Xx8Yp0s6Do9F7YkFeOn/E/nENtsI7NOcyHpICJ5+UkgjUo9QaQ/Yu+dIDU+sP/4NXsUtq6SMaQ==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-format/-/vega-format-2.1.0.tgz", + "integrity": "sha512-i9Ht33IgqG36+S1gFDpAiKvXCPz+q+1vDhDGKK8YsgMxGOG4PzinKakI66xd7SdV4q97FgpR7odAXqtDN2wKqw==", + "license": "BSD-3-Clause", + "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.1", + "resolved": "https://registry.npmmirror.com/vega-functions/-/vega-functions-6.1.1.tgz", + "integrity": "sha512-Due6jP0y0FfsGMTrHnzUGnEwXPu7VwE+9relfo+LjL/tRPYnnKqwWvzt7n9JkeBuZqjkgYjMzm/WucNn6Hkw5A==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-geo/-/vega-geo-5.1.0.tgz", + "integrity": "sha512-H8aBBHfthc3rzDbz/Th18+Nvp00J73q3uXGAPDQqizioDm/CoXCK8cX4pMePydBY9S6ikBiGJrLKFDa80wI20g==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-hierarchy/-/vega-hierarchy-5.1.0.tgz", + "integrity": "sha512-rZlU8QJNETlB6o73lGCPybZtw2fBBsRIRuFE77aCLFHdGsh6wIifhplVarqE9icBqjUHRRUOmcEYfzwVIPr65g==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-interpreter/-/vega-interpreter-2.2.1.tgz", + "integrity": "sha512-o+4ZEme2mdFLewlpF76dwPWW2VkZ3TAF3DMcq75/NzA5KPvnN4wnlCM8At2FVawbaHRyGdVkJSS5ROF5KwpHPQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-label": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/vega-label/-/vega-label-2.1.0.tgz", + "integrity": "sha512-/hgf+zoA3FViDBehrQT42Lta3t8In6YwtMnwjYlh72zNn1p3c7E3YUBwqmAqTM1x+tudgzMRGLYig+bX1ewZxQ==", + "license": "BSD-3-Clause", + "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.2", + "resolved": "https://registry.npmmirror.com/vega-lite/-/vega-lite-6.4.2.tgz", + "integrity": "sha512-Mv2PaRIpijz256LM0NdOJd9Md8cqyrXina54xW6Qp865YfY502zlXGUst+W/XznVwISGfatt0yLZuDqCUbBDuw==", + "license": "BSD-3-Clause", + "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": ">=20" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "^6.0.0" + } + }, + "node_modules/vega-lite/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/vega-lite/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/vega-lite/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "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/vega-lite/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "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/vega-lite/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/vega-loader": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/vega-loader/-/vega-loader-5.1.0.tgz", + "integrity": "sha512-GaY3BdSPbPNdtrBz8SYUBNmNd8mdPc3mtdZfdkFazQ0RD9m+Toz5oR8fKnTamNSk9fRTJX0Lp3uEqxrAlQVreg==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-parser/-/vega-parser-7.1.0.tgz", + "integrity": "sha512-g0lrYxtmYVW8G6yXpIS4J3Uxt9OUSkc0bLu5afoYDo4rZmoOOdll3x3ebActp5LHPW+usZIE+p5nukRS2vEc7Q==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-projection/-/vega-projection-2.1.0.tgz", + "integrity": "sha512-EjRjVSoMR5ibrU7q8LaOQKP327NcOAM1+eZ+NO4ANvvAutwmbNVTmfA1VpPH+AD0AlBYc39ND/wnRk7SieDiXA==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-regression/-/vega-regression-2.1.0.tgz", + "integrity": "sha512-HzC7MuoEwG1rIxRaNTqgcaYF03z/ZxYkQR2D5BN0N45kLnHY1HJXiEcZkcffTsqXdspLjn47yLi44UoCwF5fxQ==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-runtime/-/vega-runtime-7.1.0.tgz", + "integrity": "sha512-mItI+WHimyEcZlZrQ/zYR3LwHVeyHCWwp7MKaBjkU8EwkSxEEGVceyGUY9X2YuJLiOgkLz/6juYDbMv60pfwYA==", + "license": "BSD-3-Clause", + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-scale": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/vega-scale/-/vega-scale-8.1.0.tgz", + "integrity": "sha512-VEgDuEcOec8+C8+FzLcnAmcXrv2gAJKqQifCdQhkgnsLa978vYUgVfCut/mBSMMHbH8wlUV1D0fKZTjRukA1+A==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-scenegraph/-/vega-scenegraph-5.1.0.tgz", + "integrity": "sha512-4gA89CFIxkZX+4Nvl8SZF2MBOqnlj9J5zgdPh/HPx+JOwtzSlUqIhxFpFj7GWYfwzr/PyZnguBLPihPw1Og/cA==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-schema-url-parser/-/vega-schema-url-parser-3.0.2.tgz", + "integrity": "sha512-xAnR7KAvNPYewI3O0l5QGdT8Tv0+GCZQjqfP39cW/hbe/b3aYMAQ39vm8O2wfXUHzm04xTe7nolcsx8WQNVLRQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/vega-selections": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/vega-selections/-/vega-selections-6.1.2.tgz", + "integrity": "sha512-xJ+V4qdd46nk2RBdwIRrQm2iSTMHdlu/omhLz1pqRL3jZDrkqNBXimrisci2kIKpH2WBpA1YVagwuZEKBmF2Qw==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-statistics/-/vega-statistics-2.0.0.tgz", + "integrity": "sha512-dGPfDXnBlgXbZF3oxtkb8JfeRXd5TYHx25Z/tIoaa9jWua4Vf/AoW2wwh8J1qmMy8J03/29aowkp1yk4DOPazQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4" + } + }, + "node_modules/vega-themes": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/vega-themes/-/vega-themes-3.0.0.tgz", + "integrity": "sha512-1iFiI3BNmW9FrsLnDLx0ZKEddsCitRY3XmUAwp6qmp+p+IXyJYc9pfjlVj9E6KXBPfm4cQyU++s0smKNiWzO4g==", + "license": "BSD-3-Clause", + "peer": true, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "*", + "vega-lite": "*" + } + }, + "node_modules/vega-time": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/vega-time/-/vega-time-3.1.0.tgz", + "integrity": "sha512-G93mWzPwNa6UYQRkr8Ujur9uqxbBDjDT/WpXjbDY0yygdSkRT+zXF+Sb4gjhW0nPaqdiwkn0R6kZcSPMj1bMNA==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-tooltip/-/vega-tooltip-1.0.0.tgz", + "integrity": "sha512-P1R0JP29v0qnTuwzCQ0SPJlkjAzr6qeyj+H4VgUFSykHmHc1OBxda//XBaFDl/bZgIscEMvjKSjZpXd84x3aZQ==", + "license": "BSD-3-Clause", + "peer": true, + "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.npmmirror.com/vega-transforms/-/vega-transforms-5.1.0.tgz", + "integrity": "sha512-mj/sO2tSuzzpiXX8JSl4DDlhEmVwM/46MTAzTNQUQzJPMI/n4ChCjr/SdEbfEyzlD4DPm1bjohZGjLc010yuMg==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-typings/-/vega-typings-2.1.0.tgz", + "integrity": "sha512-zdis4Fg4gv37yEvTTSZEVMNhp8hwyEl7GZ4X4HHddRVRKxWFsbyKvZx/YW5Z9Ox4sjxVA2qHzEbod4Fdx+SEJA==", + "license": "BSD-3-Clause", + "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.npmmirror.com/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.npmmirror.com/vega-view/-/vega-view-6.1.0.tgz", + "integrity": "sha512-hmHDm/zC65lb23mb9Tr9Gx0wkxP0TMS31LpMPYxIZpvInxvUn7TYitkOtz1elr63k2YZrgmF7ztdGyQ4iCQ5fQ==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-view-transforms/-/vega-view-transforms-5.1.0.tgz", + "integrity": "sha512-fpigh/xn/32t+An1ShoY3MLeGzNdlbAp2+HvFKzPpmpMTZqJEWkk/J/wHU7Swyc28Ta7W1z3fO+8dZkOYO5TWQ==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-voronoi/-/vega-voronoi-5.1.0.tgz", + "integrity": "sha512-uKdsoR9x60mz7eYtVG+NhlkdQXeVdMr6jHNAHxs+W+i6kawkUp5S9jp1xf1FmW/uZvtO1eqinHQNwATcDRsiUg==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vega-wordcloud/-/vega-wordcloud-5.1.0.tgz", + "integrity": "sha512-sSdNmT8y2D7xXhM2h76dKyaYn3PA4eV49WUUkfYfqHz/vpcu10GSAoFxLhQQTkbZXR+q5ZB63tFUow9W2IFo6g==", + "license": "BSD-3-Clause", + "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.npmmirror.com/vfile/-/vfile-6.0.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index dcc0fad..d2d0bee 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,12 +23,15 @@ "react-grid-layout": "^2.2.2", "react-markdown": "^10.1.0", "react-router-dom": "^7.13.1", + "react-vega": "^8.0.0", "recharts": "^3.8.0", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "shadcn": "^4.0.6", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.4.0", + "vega": "^6.2.0", + "vega-lite": "^6.4.2", "zustand": "^5.0.11" }, "devDependencies": { diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx index 7adb011..d386fae 100644 --- a/frontend/src/components/ChatInterface.tsx +++ b/frontend/src/components/ChatInterface.tsx @@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { User, Loader2, Sparkles, Search, ArrowUp, ChevronDown, Table, Paperclip, Check, X, File as FileIcon } from "lucide-react"; import { api } from "@/lib/api"; -import { useVisualizationStore } from "@/store/visualizationStore"; +import { type ChartSpec, useVisualizationStore } from "@/store/visualizationStore"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { cn } from "@/lib/utils"; @@ -267,7 +267,12 @@ export function ChatInterface() { } else { // Fallback to existing NL2SQL or other skills (e.g. for "表格问答" or "深度问数") const source = selectedDataSource.split('-')[0]; // postgres-main -> postgres - const response = await api.post<{sql?: string, result?: unknown, error?: string}>('/api/v1/agent/nl2sql', { + const response = await api.post<{ + sql?: string, + result?: unknown, + error?: string, + chart?: { chart_spec: ChartSpec, reasoning: string, can_visualize: boolean } + }>('/api/v1/agent/nl2sql', { query: messagePayload, source: source, session_id: activeSessionKey, @@ -287,9 +292,9 @@ export function ChatInterface() { setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'assistant', - content: `I've generated a SQL query and fetched ${rows.length} rows for you. Check the visualization panel.` + content: `I've generated a SQL query and fetched ${rows.length} rows for you. Check the visualization panel.${response.chart?.reasoning ? `\n\nVisualization reasoning: ${response.chart.reasoning}` : ''}` }]); - setVisualization(rows, sql); + setVisualization(rows, sql, response.chart?.chart_spec); } } } catch (error: any) { diff --git a/frontend/src/components/VegaChart.tsx b/frontend/src/components/VegaChart.tsx new file mode 100644 index 0000000..729355f --- /dev/null +++ b/frontend/src/components/VegaChart.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { VegaEmbed } from 'react-vega'; +import type { ChartSpec } from '@/store/visualizationStore'; + +interface VegaChartProps { + data: any[]; + spec: ChartSpec; +} + +export const VegaChart: React.FC = ({ data, spec }) => { + const vegaSpec: any = { + $schema: 'https://vega.github.io/schema/vega-lite/v5.json', + description: spec.description, + title: spec.title, + width: "container", + height: "container", + mark: { type: spec.chart_type, tooltip: true }, + encoding: { + x: { field: spec.x_axis, type: 'nominal', axis: { labelAngle: -45 } }, + y: { field: spec.y_axis, type: 'quantitative' }, + }, + data: { values: data } + }; + + if (spec.color) { + vegaSpec.encoding.color = { field: spec.color, type: 'nominal' }; + } + + return ( +
+ +
+ ); +}; diff --git a/frontend/src/components/VisualizationPanel.tsx b/frontend/src/components/VisualizationPanel.tsx index 70911ad..0020c23 100644 --- a/frontend/src/components/VisualizationPanel.tsx +++ b/frontend/src/components/VisualizationPanel.tsx @@ -3,25 +3,24 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line } from 'recharts'; -import { Code, Table as TableIcon, BarChart as ChartIcon, LineChart as LineChartIcon, Download, LayoutDashboard, Loader2 } from "lucide-react"; +import { Code, Table as TableIcon, BarChart as ChartIcon, Download, LayoutDashboard, Loader2 } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useDashboardStore } from "@/store/dashboardStore"; import { useVisualizationStore } from "@/store/visualizationStore"; +import { VegaChart } from "./VegaChart"; export function VisualizationPanel() { const [view, setView] = useState<'table' | 'chart'>('chart'); - const [chartType, setChartType] = useState<'bar' | 'line'>('bar'); const { addChart } = useDashboardStore(); - const { currentData, currentSQL, isLoading, error } = useVisualizationStore(); + const { currentData, currentSQL, currentChartSpec, isLoading, error } = useVisualizationStore(); const handleAddToDashboard = () => { if (!currentData || !currentSQL) return; addChart({ id: Date.now().toString(), - title: 'Generated Analysis', // Could be dynamic based on query - type: chartType, + title: currentChartSpec?.title || 'Generated Analysis', + type: currentChartSpec?.chart_type as any || 'bar', data: currentData, sql: currentSQL, }); @@ -67,9 +66,6 @@ export function VisualizationPanel() { } const columns = Object.keys(objectRows[0] as Record); - const firstRow = objectRows[0] as Record; - const stringColumn = columns.find(col => typeof firstRow[col] === 'string') || columns[0]; - const numberColumns = columns.filter(col => typeof firstRow[col] === 'number'); return (
@@ -98,17 +94,6 @@ export function VisualizationPanel() {
- {view === 'chart' && ( -
- - -
- )} - + + )} ) : ( diff --git a/frontend/src/store/visualizationStore.ts b/frontend/src/store/visualizationStore.ts index 2e4a4fc..6da500a 100644 --- a/frontend/src/store/visualizationStore.ts +++ b/frontend/src/store/visualizationStore.ts @@ -1,13 +1,21 @@ import { create } from 'zustand'; +export interface ChartSpec { + chart_type: string; + title: string; + x_axis: string; + y_axis: string; + color?: string; + description?: string; +} + export interface VisualizationState { currentData: any[] | null; currentSQL: string | null; - currentChartType: 'bar' | 'line'; + currentChartSpec: ChartSpec | null; isLoading: boolean; error: string | null; - setVisualization: (data: any[], sql: string) => void; - setChartType: (type: 'bar' | 'line') => void; + setVisualization: (data: any[], sql: string, chartSpec?: ChartSpec | null) => void; setLoading: (loading: boolean) => void; setError: (error: string | null) => void; clearVisualization: () => void; @@ -16,12 +24,11 @@ export interface VisualizationState { export const useVisualizationStore = create((set) => ({ currentData: null, currentSQL: null, - currentChartType: 'bar', + currentChartSpec: null, isLoading: false, error: null, - setVisualization: (data, sql) => set({ currentData: data, currentSQL: sql, error: null }), - setChartType: (type) => set({ currentChartType: type }), + setVisualization: (data, sql, chartSpec = null) => set({ currentData: data, currentSQL: sql, currentChartSpec: chartSpec, error: null }), setLoading: (loading) => set({ isLoading: loading }), setError: (error) => set({ error, isLoading: false }), - clearVisualization: () => set({ currentData: null, currentSQL: null, error: null }), + clearVisualization: () => set({ currentData: null, currentSQL: null, currentChartSpec: null, error: null }), })); diff --git a/nanobot/nanobot/providers/base.py b/nanobot/nanobot/providers/base.py index 0f73544..edc501b 100644 --- a/nanobot/nanobot/providers/base.py +++ b/nanobot/nanobot/providers/base.py @@ -95,6 +95,17 @@ class LLMProvider(ABC): """Keep only provider-safe message keys and normalize assistant content.""" sanitized = [] for msg in messages: + if isinstance(msg, list): + for nested in msg: + if not isinstance(nested, dict): + continue + clean = {k: v for k, v in nested.items() if k in allowed_keys} + if clean.get("role") == "assistant" and "content" not in clean: + clean["content"] = None + sanitized.append(clean) + continue + if not isinstance(msg, dict): + continue clean = {k: v for k, v in msg.items() if k in allowed_keys} if clean.get("role") == "assistant" and "content" not in clean: clean["content"] = None diff --git a/nanobot/nanobot/providers/registry.py b/nanobot/nanobot/providers/registry.py index 3ba1a0e..fa3eeaf 100644 --- a/nanobot/nanobot/providers/registry.py +++ b/nanobot/nanobot/providers/registry.py @@ -151,7 +151,7 @@ PROVIDERS: tuple[ProviderSpec, ...] = ( keywords=("volcengine", "volces", "ark"), env_key="OPENAI_API_KEY", display_name="VolcEngine", - litellm_prefix="volcengine", + litellm_prefix="openai", skip_prefixes=(), env_extras=(), is_gateway=True,