@databricks/appkit-ui 0.20.3 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -0
- package/README.md +3 -20
- package/dist/cli/commands/setup.js +2 -2
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/js/config.d.ts +24 -0
- package/dist/js/config.d.ts.map +1 -0
- package/dist/js/config.js +49 -0
- package/dist/js/config.js.map +1 -0
- package/dist/js/index.d.ts +2 -1
- package/dist/js/index.js +2 -1
- package/dist/react/charts/options.d.ts.map +1 -1
- package/dist/react/charts/options.js +3 -1
- package/dist/react/charts/options.js.map +1 -1
- package/dist/react/genie/genie-chart-inference.d.ts +6 -1
- package/dist/react/genie/genie-chart-inference.d.ts.map +1 -1
- package/dist/react/genie/genie-chart-inference.js +60 -6
- package/dist/react/genie/genie-chart-inference.js.map +1 -1
- package/dist/react/genie/genie-chat-message-list.d.ts.map +1 -1
- package/dist/react/genie/genie-chat-message-list.js +5 -4
- package/dist/react/genie/genie-chat-message-list.js.map +1 -1
- package/dist/react/genie/genie-chat-message.d.ts.map +1 -1
- package/dist/react/genie/genie-chat-message.js +7 -6
- package/dist/react/genie/genie-chat-message.js.map +1 -1
- package/dist/react/genie/genie-query-visualization.d.ts.map +1 -1
- package/dist/react/genie/genie-query-visualization.js +66 -15
- package/dist/react/genie/genie-query-visualization.js.map +1 -1
- package/dist/react/genie/index.d.ts +2 -2
- package/dist/react/genie/index.js +1 -1
- package/dist/react/genie/types.d.ts +2 -1
- package/dist/react/genie/types.d.ts.map +1 -1
- package/dist/react/genie/types.js +6 -0
- package/dist/react/genie/types.js.map +1 -0
- package/dist/react/genie/use-genie-chat.d.ts.map +1 -1
- package/dist/react/genie/use-genie-chat.js +60 -23
- package/dist/react/genie/use-genie-chat.js.map +1 -1
- package/dist/react/hooks/index.d.ts +2 -1
- package/dist/react/hooks/index.js +1 -0
- package/dist/react/hooks/use-plugin-config.d.ts +25 -0
- package/dist/react/hooks/use-plugin-config.d.ts.map +1 -0
- package/dist/react/hooks/use-plugin-config.js +32 -0
- package/dist/react/hooks/use-plugin-config.js.map +1 -0
- package/dist/react/index.d.ts +4 -3
- package/dist/react/index.js +5 -4
- package/dist/react/table/data-table.js +1 -1
- package/dist/react/ui/index.js +1 -1
- package/dist/shared/src/index.d.ts +1 -1
- package/dist/shared/src/plugin.d.ts +12 -1
- package/dist/shared/src/plugin.d.ts.map +1 -0
- package/docs/api/appkit/Class.Plugin.md +75 -17
- package/docs/app-management.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/development/ai-assisted-development.md +2 -2
- package/docs/development/local-development.md +1 -1
- package/docs/development/remote-bridge.md +1 -1
- package/docs/development/templates.md +93 -0
- package/docs/development.md +1 -1
- package/docs/plugins/caching.md +3 -1
- package/docs/plugins/execution-context.md +1 -1
- package/docs/plugins/lakebase.md +1 -1
- package/docs.md +2 -2
- package/llms.txt +1 -0
- package/package.json +60 -58
- package/sbom.cdx.json +1 -0
package/CLAUDE.md
CHANGED
|
@@ -36,6 +36,7 @@ npx @databricks/appkit docs <query>
|
|
|
36
36
|
- [Local development](./docs/development/local-development.md): Once your app is bootstrapped according to the Manual quick start guide, you can start the development server with hot reload for both UI and backend code.
|
|
37
37
|
- [Project setup](./docs/development/project-setup.md): This guide covers the recommended project structure and scaffolding for AppKit applications.
|
|
38
38
|
- [Remote Bridge](./docs/development/remote-bridge.md): Remote bridge allows you to develop against a deployed backend while keeping your UI and queries local. This is useful for testing against production data or debugging deployed backend code without redeploying your app.
|
|
39
|
+
- [Templates](./docs/development/templates.md): AppKit uses a template system powered by the Databricks CLI's databricks apps init command. Templates define the project structure, and .tmpl files are processed with Go's text/template engine to generate customized output.
|
|
39
40
|
- [Type generation](./docs/development/type-generation.md): AppKit can automatically generate TypeScript types for your SQL queries, providing end-to-end type safety from database to UI.
|
|
40
41
|
|
|
41
42
|
## Plugins
|
package/README.md
CHANGED
|
@@ -2,17 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Build Databricks Apps faster with our brand-new Node.js + React SDK. Built for humans and AI.
|
|
4
4
|
|
|
5
|
-
> [!WARNING]
|
|
6
|
-
> PREVIEW - NOT FOR PRODUCTION USE
|
|
7
|
-
|
|
8
|
-
> **This SDK is in preview and is subject to change without notice.**
|
|
9
|
-
>
|
|
10
|
-
> - ❌ **Do NOT use in production environments**
|
|
11
|
-
> - ⚠️ **Breaking changes may occur at any time**
|
|
12
|
-
> - 🔬 **APIs are experimental and unstable**
|
|
13
|
-
> - 📝 **Use for development and testing only**
|
|
14
|
-
>
|
|
15
|
-
|
|
16
5
|
## Introduction
|
|
17
6
|
|
|
18
7
|
AppKit is a TypeScript SDK for building production-ready Databricks applications with a plugin-based architecture. It provides opinionated defaults, built-in observability, and seamless integration with Databricks services.
|
|
@@ -29,16 +18,10 @@ AppKit simplifies building data applications on Databricks by providing:
|
|
|
29
18
|
|
|
30
19
|
AppKit's power comes from its plugin system. Each plugin adds a focused capability to your app with minimal configuration.
|
|
31
20
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- **Analytics Plugin** — Query your Lakehouse data directly from your app. Define SQL queries as files, execute them against Databricks SQL Warehouses, and get automatic caching, parameterization, and on-behalf-of user execution out of the box. Perfect for building apps that surface insights from your Lakehouse.
|
|
21
|
+
- **Analytics Plugin** — Query your Lakehouse data directly from your app. Define SQL queries as files, execute them against Databricks SQL Warehouses, and get automatic caching, parameterization, and on-behalf-of user execution out of the box.
|
|
35
22
|
- **Genie Plugin** — Conversational AI interface powered by Databricks AI/BI Genie. Let users ask natural language questions against your data and get answers with automatic chart inference and visualization.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
- **Files Plugin** — Browse, upload, and manage files in Unity Catalog Volumes
|
|
40
|
-
- **Lakebase Plugin** — OLTP database operations with automatic OAuth token management
|
|
41
|
-
- ...and this is just the beginning.
|
|
23
|
+
- **Files Plugin** — Browse, upload, and manage files in Unity Catalog Volumes. Supports multiple volumes, content type validation, and on-behalf-of user access.
|
|
24
|
+
- **Lakebase Plugin** — OLTP database operations against Databricks Lakebase with automatic OAuth token management. Returns a standard `pg.Pool` compatible with Prisma, Drizzle, TypeORM, and other ORMs.
|
|
42
25
|
|
|
43
26
|
> Missing a plugin? [Open an issue](https://github.com/databricks/appkit/issues/new) and tell us what you need — community input directly shapes the roadmap.
|
|
44
27
|
|
|
@@ -43,7 +43,7 @@ ${packages.map((pkg) => {
|
|
|
43
43
|
For enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:
|
|
44
44
|
|
|
45
45
|
\`\`\`bash
|
|
46
|
-
databricks experimental aitools
|
|
46
|
+
databricks experimental aitools install
|
|
47
47
|
\`\`\`
|
|
48
48
|
${SECTION_END}`;
|
|
49
49
|
}
|
|
@@ -68,7 +68,7 @@ ${packages.map((pkg) => {
|
|
|
68
68
|
For enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:
|
|
69
69
|
|
|
70
70
|
\`\`\`bash
|
|
71
|
-
databricks experimental aitools
|
|
71
|
+
databricks experimental aitools install
|
|
72
72
|
\`\`\`
|
|
73
73
|
${SECTION_END}
|
|
74
74
|
`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","names":[],"sources":["../../../src/cli/commands/setup.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command } from \"commander\";\n\nconst PACKAGES = [\n { name: \"@databricks/appkit\", description: \"Backend SDK\" },\n {\n name: \"@databricks/appkit-ui\",\n description: \"UI Integration, Charts, Tables, SSE, and more.\",\n },\n];\n\nconst SECTION_START = \"<!-- appkit-instructions-start -->\";\nconst SECTION_END = \"<!-- appkit-instructions-end -->\";\n\n/**\n * Find which AppKit packages are installed by checking for package.json\n */\nfunction findInstalledPackages() {\n const cwd = process.cwd();\n const installed = [];\n\n for (const pkg of PACKAGES) {\n const packagePath = path.join(\n cwd,\n \"node_modules\",\n pkg.name,\n \"package.json\",\n );\n if (fs.existsSync(packagePath)) {\n installed.push(pkg);\n }\n }\n\n return installed;\n}\n\n/**\n * Generate the AppKit section content\n */\nfunction generateSection(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks experimental aitools
|
|
1
|
+
{"version":3,"file":"setup.js","names":[],"sources":["../../../src/cli/commands/setup.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command } from \"commander\";\n\nconst PACKAGES = [\n { name: \"@databricks/appkit\", description: \"Backend SDK\" },\n {\n name: \"@databricks/appkit-ui\",\n description: \"UI Integration, Charts, Tables, SSE, and more.\",\n },\n];\n\nconst SECTION_START = \"<!-- appkit-instructions-start -->\";\nconst SECTION_END = \"<!-- appkit-instructions-end -->\";\n\n/**\n * Find which AppKit packages are installed by checking for package.json\n */\nfunction findInstalledPackages() {\n const cwd = process.cwd();\n const installed = [];\n\n for (const pkg of PACKAGES) {\n const packagePath = path.join(\n cwd,\n \"node_modules\",\n pkg.name,\n \"package.json\",\n );\n if (fs.existsSync(packagePath)) {\n installed.push(pkg);\n }\n }\n\n return installed;\n}\n\n/**\n * Generate the AppKit section content\n */\nfunction generateSection(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks experimental aitools install\n\\`\\`\\`\n${SECTION_END}`;\n}\n\n/**\n * Generate standalone CLAUDE.md content (when no existing file)\n */\nfunction generateStandalone(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `# AI Assistant Instructions\n\n${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks experimental aitools install\n\\`\\`\\`\n${SECTION_END}\n`;\n}\n\n/**\n * Update existing content with AppKit section\n */\nfunction updateContent(existingContent: string, packages: typeof PACKAGES) {\n const newSection = generateSection(packages);\n\n // Check if AppKit section already exists\n const startIndex = existingContent.indexOf(SECTION_START);\n const endIndex = existingContent.indexOf(SECTION_END);\n\n if (startIndex !== -1 && endIndex !== -1) {\n // Replace existing section\n const before = existingContent.substring(0, startIndex);\n const after = existingContent.substring(endIndex + SECTION_END.length);\n return before + newSection + after;\n }\n\n // Append section to end\n return `${existingContent.trimEnd()}\\n\\n${newSection}\\n`;\n}\n\n/**\n * Setup command implementation\n */\nfunction runSetup(options: { write?: boolean }) {\n const shouldWrite = options.write;\n\n // Find installed packages\n const installed = findInstalledPackages();\n\n if (installed.length === 0) {\n console.log(\"No @databricks/appkit packages found in node_modules.\");\n console.log(\"\\nMake sure you've installed at least one of:\");\n PACKAGES.forEach((pkg) => {\n console.log(` - ${pkg.name}`);\n });\n process.exit(1);\n }\n\n console.log(\"Detected packages:\");\n installed.forEach((pkg) => {\n console.log(` ✓ ${pkg.name}`);\n });\n\n const claudePath = path.join(process.cwd(), \"CLAUDE.md\");\n const existingContent = fs.existsSync(claudePath)\n ? fs.readFileSync(claudePath, \"utf-8\")\n : null;\n\n let finalContent: string;\n let action: string;\n\n if (existingContent) {\n finalContent = updateContent(existingContent, installed);\n action = existingContent.includes(SECTION_START) ? \"Updated\" : \"Added to\";\n } else {\n finalContent = generateStandalone(installed);\n action = \"Created\";\n }\n\n if (shouldWrite) {\n fs.writeFileSync(claudePath, finalContent);\n console.log(`\\n✓ ${action} CLAUDE.md`);\n console.log(` Path: ${claudePath}`);\n } else {\n console.log(\"\\nTo create/update CLAUDE.md, run:\");\n console.log(\" npx appkit setup --write\\n\");\n\n if (existingContent) {\n console.log(\n `This will ${\n existingContent.includes(SECTION_START)\n ? \"update the existing\"\n : \"add a new\"\n } AppKit section.\\n`,\n );\n }\n\n console.log(\"Preview of AppKit section:\\n\");\n console.log(\"─\".repeat(50));\n console.log(generateSection(installed));\n console.log(\"─\".repeat(50));\n }\n}\n\nexport const setupCommand = new Command(\"setup\")\n .description(\"Setup CLAUDE.md with AppKit package references\")\n .option(\"-w, --write\", \"Create or update CLAUDE.md file in current directory\")\n .action(runSetup);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PluginClientConfigs, PluginEndpoints } from "../shared/src/plugin.js";
|
|
2
|
+
import "../shared/src/index.js";
|
|
3
|
+
|
|
4
|
+
//#region src/js/config.d.ts
|
|
5
|
+
interface AppKitClientConfig {
|
|
6
|
+
appName: string;
|
|
7
|
+
queries: Record<string, string>;
|
|
8
|
+
endpoints: PluginEndpoints;
|
|
9
|
+
plugins: PluginClientConfigs;
|
|
10
|
+
}
|
|
11
|
+
declare global {
|
|
12
|
+
interface Window {
|
|
13
|
+
__appkit__?: AppKitClientConfig;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* @internal Reset the module-scoped config cache. Test utility only.
|
|
18
|
+
*/
|
|
19
|
+
declare function _resetConfigCache(): void;
|
|
20
|
+
declare function getClientConfig(): AppKitClientConfig;
|
|
21
|
+
declare function getPluginClientConfig<T = Record<string, unknown>>(pluginName: string): T;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { AppKitClientConfig, _resetConfigCache, getClientConfig, getPluginClientConfig };
|
|
24
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","names":[],"sources":["../../src/js/config.ts"],"mappings":";;;;UAEiB,kBAAA;EACf,OAAA;EACA,OAAA,EAAS,MAAA;EACT,SAAA,EAAW,eAAA;EACX,OAAA,EAAS,mBAAA;AAAA;AAAA,QAGH,MAAA;EAAA,UACI,MAAA;IACR,UAAA,GAAa,kBAAA;EAAA;AAAA;;;;iBAkDD,iBAAA,CAAA;AAAA,iBAIA,eAAA,CAAA,GAAmB,kBAAA;AAAA,iBAenB,qBAAA,KAA0B,MAAA,kBAAA,CACxC,UAAA,WACC,CAAA"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/js/config.ts
|
|
2
|
+
const APPKIT_CONFIG_SCRIPT_ID = "__appkit__";
|
|
3
|
+
const EMPTY_CONFIG = Object.freeze({
|
|
4
|
+
appName: "",
|
|
5
|
+
queries: Object.freeze({}),
|
|
6
|
+
endpoints: Object.freeze({}),
|
|
7
|
+
plugins: Object.freeze({})
|
|
8
|
+
});
|
|
9
|
+
function normalizeClientConfig(config) {
|
|
10
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return EMPTY_CONFIG;
|
|
11
|
+
const normalized = config;
|
|
12
|
+
return {
|
|
13
|
+
appName: normalized.appName ?? EMPTY_CONFIG.appName,
|
|
14
|
+
queries: normalized.queries ?? EMPTY_CONFIG.queries,
|
|
15
|
+
endpoints: normalized.endpoints ?? EMPTY_CONFIG.endpoints,
|
|
16
|
+
plugins: normalized.plugins ?? EMPTY_CONFIG.plugins
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function readClientConfigFromDom() {
|
|
20
|
+
if (typeof document === "undefined") return;
|
|
21
|
+
const configScript = document.getElementById(APPKIT_CONFIG_SCRIPT_ID);
|
|
22
|
+
if (!configScript?.textContent) return;
|
|
23
|
+
try {
|
|
24
|
+
return normalizeClientConfig(JSON.parse(configScript.textContent));
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.warn("[appkit] Failed to parse config from DOM:", error);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let _cache;
|
|
31
|
+
/**
|
|
32
|
+
* @internal Reset the module-scoped config cache. Test utility only.
|
|
33
|
+
*/
|
|
34
|
+
function _resetConfigCache() {
|
|
35
|
+
_cache = void 0;
|
|
36
|
+
}
|
|
37
|
+
function getClientConfig() {
|
|
38
|
+
if (typeof window === "undefined") return EMPTY_CONFIG;
|
|
39
|
+
if (!_cache) _cache = readClientConfigFromDom() ?? normalizeClientConfig(window.__appkit__);
|
|
40
|
+
return _cache;
|
|
41
|
+
}
|
|
42
|
+
const EMPTY_PLUGIN_CONFIG = Object.freeze({});
|
|
43
|
+
function getPluginClientConfig(pluginName) {
|
|
44
|
+
return getClientConfig().plugins[pluginName] ?? EMPTY_PLUGIN_CONFIG;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { _resetConfigCache, getClientConfig, getPluginClientConfig };
|
|
49
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","names":[],"sources":["../../src/js/config.ts"],"sourcesContent":["import type { PluginClientConfigs, PluginEndpoints } from \"shared\";\n\nexport interface AppKitClientConfig {\n appName: string;\n queries: Record<string, string>;\n endpoints: PluginEndpoints;\n plugins: PluginClientConfigs;\n}\n\ndeclare global {\n interface Window {\n __appkit__?: AppKitClientConfig;\n }\n}\n\nconst APPKIT_CONFIG_SCRIPT_ID = \"__appkit__\";\nconst EMPTY_CONFIG: AppKitClientConfig = Object.freeze({\n appName: \"\",\n queries: Object.freeze({}),\n endpoints: Object.freeze({}),\n plugins: Object.freeze({}),\n});\n\nfunction normalizeClientConfig(config: unknown): AppKitClientConfig {\n if (!config || typeof config !== \"object\" || Array.isArray(config)) {\n return EMPTY_CONFIG;\n }\n\n const normalized = config as Partial<AppKitClientConfig>;\n\n return {\n appName: normalized.appName ?? EMPTY_CONFIG.appName,\n queries: normalized.queries ?? EMPTY_CONFIG.queries,\n endpoints: normalized.endpoints ?? EMPTY_CONFIG.endpoints,\n plugins: normalized.plugins ?? EMPTY_CONFIG.plugins,\n };\n}\n\nfunction readClientConfigFromDom(): AppKitClientConfig | undefined {\n if (typeof document === \"undefined\") {\n return undefined;\n }\n\n const configScript = document.getElementById(APPKIT_CONFIG_SCRIPT_ID);\n if (!configScript?.textContent) {\n return undefined;\n }\n\n try {\n return normalizeClientConfig(JSON.parse(configScript.textContent));\n } catch (error) {\n console.warn(\"[appkit] Failed to parse config from DOM:\", error);\n return undefined;\n }\n}\n\nlet _cache: AppKitClientConfig | undefined;\n\n/**\n * @internal Reset the module-scoped config cache. Test utility only.\n */\nexport function _resetConfigCache(): void {\n _cache = undefined;\n}\n\nexport function getClientConfig(): AppKitClientConfig {\n if (typeof window === \"undefined\") {\n return EMPTY_CONFIG;\n }\n\n if (!_cache) {\n _cache =\n readClientConfigFromDom() ?? normalizeClientConfig(window.__appkit__);\n }\n\n return _cache;\n}\n\nconst EMPTY_PLUGIN_CONFIG = Object.freeze({});\n\nexport function getPluginClientConfig<T = Record<string, unknown>>(\n pluginName: string,\n): T {\n return (getClientConfig().plugins[pluginName] ?? EMPTY_PLUGIN_CONFIG) as T;\n}\n"],"mappings":";AAeA,MAAM,0BAA0B;AAChC,MAAM,eAAmC,OAAO,OAAO;CACrD,SAAS;CACT,SAAS,OAAO,OAAO,EAAE,CAAC;CAC1B,WAAW,OAAO,OAAO,EAAE,CAAC;CAC5B,SAAS,OAAO,OAAO,EAAE,CAAC;CAC3B,CAAC;AAEF,SAAS,sBAAsB,QAAqC;AAClE,KAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,OAAO,CAChE,QAAO;CAGT,MAAM,aAAa;AAEnB,QAAO;EACL,SAAS,WAAW,WAAW,aAAa;EAC5C,SAAS,WAAW,WAAW,aAAa;EAC5C,WAAW,WAAW,aAAa,aAAa;EAChD,SAAS,WAAW,WAAW,aAAa;EAC7C;;AAGH,SAAS,0BAA0D;AACjE,KAAI,OAAO,aAAa,YACtB;CAGF,MAAM,eAAe,SAAS,eAAe,wBAAwB;AACrE,KAAI,CAAC,cAAc,YACjB;AAGF,KAAI;AACF,SAAO,sBAAsB,KAAK,MAAM,aAAa,YAAY,CAAC;UAC3D,OAAO;AACd,UAAQ,KAAK,6CAA6C,MAAM;AAChE;;;AAIJ,IAAI;;;;AAKJ,SAAgB,oBAA0B;AACxC,UAAS;;AAGX,SAAgB,kBAAsC;AACpD,KAAI,OAAO,WAAW,YACpB,QAAO;AAGT,KAAI,CAAC,OACH,UACE,yBAAyB,IAAI,sBAAsB,OAAO,WAAW;AAGzE,QAAO;;AAGT,MAAM,sBAAsB,OAAO,OAAO,EAAE,CAAC;AAE7C,SAAgB,sBACd,YACG;AACH,QAAQ,iBAAiB,CAAC,QAAQ,eAAe"}
|
package/dist/js/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { DATE_FIELD_PATTERNS, METADATA_DATE_PATTERNS, NAME_FIELD_PATTERNS } from
|
|
|
5
5
|
import { ArrowClient, DetectedFields, Field, Table } from "./arrow/arrow-client.js";
|
|
6
6
|
import { getArrowModule, initializeTypeIdSets } from "./arrow/lazy-arrow.js";
|
|
7
7
|
import "./arrow/index.js";
|
|
8
|
+
import { AppKitClientConfig, _resetConfigCache, getClientConfig, getPluginClientConfig } from "./config.js";
|
|
8
9
|
import { ConnectSSEOptions, SSEMessage } from "./sse/types.js";
|
|
9
10
|
import { connectSSE } from "./sse/connect-sse.js";
|
|
10
|
-
export { ArrowClient, ConnectSSEOptions, DATE_FIELD_PATTERNS, DetectedFields, Field, METADATA_DATE_PATTERNS, NAME_FIELD_PATTERNS, type SQLBinaryMarker, type SQLBooleanMarker, type SQLDateMarker, type SQLNumberMarker, type SQLStringMarker, type SQLTimestampMarker, type SQLTypeMarker, SSEMessage, Table, connectSSE, getArrowModule, initializeTypeIdSets, isSQLTypeMarker, sql };
|
|
11
|
+
export { AppKitClientConfig, ArrowClient, ConnectSSEOptions, DATE_FIELD_PATTERNS, DetectedFields, Field, METADATA_DATE_PATTERNS, NAME_FIELD_PATTERNS, type SQLBinaryMarker, type SQLBooleanMarker, type SQLDateMarker, type SQLNumberMarker, type SQLStringMarker, type SQLTimestampMarker, type SQLTypeMarker, SSEMessage, Table, _resetConfigCache, connectSSE, getArrowModule, getClientConfig, getPluginClientConfig, initializeTypeIdSets, isSQLTypeMarker, sql };
|
package/dist/js/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { DATE_FIELD_PATTERNS, METADATA_DATE_PATTERNS, NAME_FIELD_PATTERNS } from
|
|
|
3
3
|
import { getArrowModule, initializeTypeIdSets } from "./arrow/lazy-arrow.js";
|
|
4
4
|
import { ArrowClient } from "./arrow/arrow-client.js";
|
|
5
5
|
import "./arrow/index.js";
|
|
6
|
+
import { _resetConfigCache, getClientConfig, getPluginClientConfig } from "./config.js";
|
|
6
7
|
import { connectSSE } from "./sse/connect-sse.js";
|
|
7
8
|
|
|
8
|
-
export { ArrowClient, DATE_FIELD_PATTERNS, METADATA_DATE_PATTERNS, NAME_FIELD_PATTERNS, connectSSE, getArrowModule, initializeTypeIdSets, isSQLTypeMarker, sql };
|
|
9
|
+
export { ArrowClient, DATE_FIELD_PATTERNS, METADATA_DATE_PATTERNS, NAME_FIELD_PATTERNS, _resetConfigCache, connectSSE, getArrowModule, getClientConfig, getPluginClientConfig, initializeTypeIdSets, isSQLTypeMarker, sql };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","names":[],"sources":["../../../src/react/charts/options.ts"],"mappings":";;;UAOiB,oBAAA;EACf,KAAA;EACA,QAAA,EAAU,MAAA;EACV,OAAA;EACA,MAAA;EACA,KAAA;EACA,UAAA;EACA,MAAA;AAAA;AAAA,UAGe,gBAAA,SAAyB,oBAAA;EACxC,SAAA,EAAW,SAAA;EACX,YAAA;EACA,OAAA;EACA,MAAA;EACA,UAAA;EACA,UAAA;AAAA;AAAA,iBAkBc,gBAAA,CACd,GAAA,EAAK,oBAAA,EACL,QAAA,aACC,MAAA;AAAA,iBAmCa,cAAA,CACd,GAAA,EAAK,oBAAA,EACL,SAAA,mBACA,WAAA,UACA,UAAA,WACA,aAAA,WACC,MAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"options.d.ts","names":[],"sources":["../../../src/react/charts/options.ts"],"mappings":";;;UAOiB,oBAAA;EACf,KAAA;EACA,QAAA,EAAU,MAAA;EACV,OAAA;EACA,MAAA;EACA,KAAA;EACA,UAAA;EACA,MAAA;AAAA;AAAA,UAGe,gBAAA,SAAyB,oBAAA;EACxC,SAAA,EAAW,SAAA;EACX,YAAA;EACA,OAAA;EACA,MAAA;EACA,UAAA;EACA,UAAA;AAAA;AAAA,iBAkBc,gBAAA,CACd,GAAA,EAAK,oBAAA,EACL,QAAA,aACC,MAAA;AAAA,iBAmCa,cAAA,CACd,GAAA,EAAK,oBAAA,EACL,SAAA,mBACA,WAAA,UACA,UAAA,WACA,aAAA,WACC,MAAA;AAAA,iBA2Ca,wBAAA,CACd,GAAA,EAAK,oBAAA,EACL,OAAA,YACC,MAAA;AAAA,UAsCc,cAAA,SAAuB,oBAAA;EAvJtC;EAyJA,SAAA;EAxJA;EA0JA,WAAA;EAxJA;EA0JA,GAAA;EAxJA;EA0JA,GAAA;EA1JU;EA4JV,UAAA;AAAA;AAAA,iBAGc,kBAAA,CACd,GAAA,EAAK,cAAA,GACJ,MAAA;AAAA,iBAsEa,oBAAA,CACd,GAAA,EAAK,gBAAA,GACJ,MAAA"}
|
|
@@ -59,7 +59,9 @@ function buildPieOption(ctx, chartType, innerRadius, showLabels, labelPosition)
|
|
|
59
59
|
label: {
|
|
60
60
|
show: showLabels,
|
|
61
61
|
position: labelPosition,
|
|
62
|
-
formatter: "{b}: {d}%"
|
|
62
|
+
formatter: "{b}: {d}%",
|
|
63
|
+
color: "inherit",
|
|
64
|
+
textBorderWidth: 0
|
|
63
65
|
},
|
|
64
66
|
emphasis: { itemStyle: {
|
|
65
67
|
shadowBlur: 10,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.js","names":[],"sources":["../../../src/react/charts/options.ts"],"sourcesContent":["import type { ChartType } from \"./types\";\nimport { createTimeSeriesData, formatLabel, truncateLabel } from \"./utils\";\n\n// ============================================================================\n// Option Builder Types\n// ============================================================================\n\nexport interface OptionBuilderContext {\n xData: (string | number)[];\n yDataMap: Record<string, (string | number)[]>;\n yFields: string[];\n colors: string[];\n title?: string;\n showLegend: boolean;\n xField?: string;\n}\n\nexport interface CartesianContext extends OptionBuilderContext {\n chartType: ChartType;\n isTimeSeries: boolean;\n stacked: boolean;\n smooth: boolean;\n showSymbol: boolean;\n symbolSize: number;\n}\n\n// ============================================================================\n// Base Option Builder\n// ============================================================================\n\nfunction buildBaseOption(ctx: OptionBuilderContext): Record<string, unknown> {\n return {\n title: ctx.title ? { text: ctx.title, left: \"center\" } : undefined,\n color: ctx.colors,\n };\n}\n\n// ============================================================================\n// Radar Chart Option\n// ============================================================================\n\nexport function buildRadarOption(\n ctx: OptionBuilderContext,\n showArea = true,\n): Record<string, unknown> {\n const maxValue = Math.max(\n ...ctx.yFields.flatMap((f) => ctx.yDataMap[f].map((v) => Number(v) || 0)),\n );\n\n return {\n ...buildBaseOption(ctx),\n tooltip: { trigger: \"item\" },\n legend:\n ctx.showLegend && ctx.yFields.length > 1 ? { top: \"bottom\" } : undefined,\n radar: {\n indicator: ctx.xData.map((name) => ({\n name: String(name),\n max: maxValue * 1.2,\n })),\n shape: \"polygon\",\n },\n series: [\n {\n type: \"radar\",\n data: ctx.yFields.map((key, idx) => ({\n name: formatLabel(key),\n value: ctx.yDataMap[key],\n itemStyle: { color: ctx.colors[idx % ctx.colors.length] },\n areaStyle: showArea ? { opacity: 0.3 } : undefined,\n })),\n },\n ],\n };\n}\n\n// ============================================================================\n// Pie/Donut Chart Option\n// ============================================================================\n\nexport function buildPieOption(\n ctx: OptionBuilderContext,\n chartType: \"pie\" | \"donut\",\n innerRadius: number,\n showLabels: boolean,\n labelPosition: string,\n): Record<string, unknown> {\n const pieData = ctx.xData.map((name, i) => ({\n name: String(name),\n value: ctx.yDataMap[ctx.yFields[0]]?.[i] ?? 0,\n }));\n\n const isDonut = chartType === \"donut\" || innerRadius > 0;\n\n return {\n ...buildBaseOption(ctx),\n tooltip: { trigger: \"item\", formatter: \"{b}: {c} ({d}%)\" },\n legend: ctx.showLegend\n ? { orient: \"vertical\", left: \"left\", top: \"middle\" }\n : undefined,\n series: [\n {\n type: \"pie\",\n radius: isDonut ? [`${innerRadius || 40}%`, \"70%\"] : \"70%\",\n center: [\"60%\", \"50%\"],\n data: pieData,\n label: {\n show: showLabels,\n position: labelPosition,\n formatter: \"{b}: {d}%\",\n },\n emphasis: {\n itemStyle: {\n shadowBlur: 10,\n shadowOffsetX: 0,\n shadowColor: \"rgba(0, 0, 0, 0.5)\",\n },\n },\n },\n ],\n };\n}\n\n// ============================================================================\n// Horizontal Bar Chart Option\n// ============================================================================\n\nexport function buildHorizontalBarOption(\n ctx: OptionBuilderContext,\n stacked: boolean,\n): Record<string, unknown> {\n const hasMultipleSeries = ctx.yFields.length > 1;\n\n return {\n ...buildBaseOption(ctx),\n tooltip: { trigger: \"axis\", axisPointer: { type: \"shadow\" } },\n legend: ctx.showLegend && hasMultipleSeries ? { top: \"bottom\" } : undefined,\n grid: {\n left: \"20%\",\n right: \"10%\",\n top: ctx.title ? \"15%\" : \"5%\",\n bottom: ctx.showLegend && hasMultipleSeries ? \"15%\" : \"5%\",\n },\n xAxis: { type: \"value\" },\n yAxis: {\n type: \"category\",\n data: ctx.xData,\n axisLabel: {\n width: 100,\n overflow: \"truncate\",\n formatter: (value: string) => truncateLabel(String(value)),\n },\n },\n series: ctx.yFields.map((key, idx) => ({\n name: formatLabel(key),\n type: \"bar\",\n data: ctx.yDataMap[key],\n stack: stacked ? \"total\" : undefined,\n itemStyle: { borderRadius: [0, 4, 4, 0] },\n color: ctx.colors[idx % ctx.colors.length],\n })),\n };\n}\n\n// ============================================================================\n// Heatmap Chart Option\n// ============================================================================\n\nexport interface HeatmapContext extends OptionBuilderContext {\n /** Y-axis categories (rows) */\n yAxisData: (string | number)[];\n /** Heatmap data as [xIndex, yIndex, value] tuples */\n heatmapData: [number, number, number][];\n /** Min value for color scale */\n min: number;\n /** Max value for color scale */\n max: number;\n /** Show value labels on cells */\n showLabels: boolean;\n}\n\nexport function buildHeatmapOption(\n ctx: HeatmapContext,\n): Record<string, unknown> {\n return {\n ...buildBaseOption(ctx),\n tooltip: {\n trigger: \"item\",\n formatter: (params: { data: [number, number, number] }) => {\n const [xIdx, yIdx, value] = params.data;\n const xLabel = ctx.xData[xIdx] ?? xIdx;\n const yLabel = ctx.yAxisData[yIdx] ?? yIdx;\n return `${xLabel}, ${yLabel}: ${value}`;\n },\n },\n grid: {\n left: \"15%\",\n right: \"15%\",\n top: ctx.title ? \"15%\" : \"10%\",\n bottom: \"15%\",\n },\n xAxis: {\n type: \"category\",\n data: ctx.xData,\n splitArea: { show: true },\n axisLabel: {\n rotate: ctx.xData.length > 10 ? 45 : 0,\n formatter: (v: string) => truncateLabel(String(v), 10),\n },\n },\n yAxis: {\n type: \"category\",\n data: ctx.yAxisData,\n splitArea: { show: true },\n axisLabel: {\n formatter: (v: string) => truncateLabel(String(v), 12),\n },\n },\n visualMap: {\n min: ctx.min,\n max: ctx.max,\n calculable: true,\n orient: \"vertical\",\n right: \"2%\",\n top: \"center\",\n inRange: {\n color: ctx.colors.length >= 2 ? ctx.colors : [\"#f0f0f0\", ctx.colors[0]],\n },\n },\n series: [\n {\n type: \"heatmap\",\n data: ctx.heatmapData,\n label: {\n show: ctx.showLabels,\n formatter: (params: { data: [number, number, number] }) =>\n String(params.data[2]),\n },\n emphasis: {\n itemStyle: {\n shadowBlur: 10,\n shadowColor: \"rgba(0, 0, 0, 0.5)\",\n },\n },\n },\n ],\n };\n}\n\n// ============================================================================\n// Cartesian Chart Option (line, bar, area, scatter)\n// ============================================================================\n\nexport function buildCartesianOption(\n ctx: CartesianContext,\n): Record<string, unknown> {\n const { chartType, isTimeSeries, stacked, smooth, showSymbol, symbolSize } =\n ctx;\n const hasMultipleSeries = ctx.yFields.length > 1;\n const seriesType = chartType === \"area\" ? \"line\" : chartType;\n const isScatter = chartType === \"scatter\";\n\n return {\n ...buildBaseOption(ctx),\n tooltip: { trigger: isScatter ? \"item\" : \"axis\" },\n legend: ctx.showLegend && hasMultipleSeries ? { top: \"bottom\" } : undefined,\n grid: {\n left: \"10%\",\n right: \"10%\",\n top: ctx.title ? \"15%\" : \"10%\",\n bottom: ctx.showLegend && hasMultipleSeries ? \"20%\" : \"15%\",\n },\n xAxis: {\n type: isScatter ? \"value\" : isTimeSeries ? \"time\" : \"category\",\n data: isScatter || isTimeSeries ? undefined : ctx.xData,\n name: ctx.xField ? formatLabel(ctx.xField) : undefined,\n axisLabel:\n isScatter || isTimeSeries\n ? { show: true }\n : {\n rotate: ctx.xData.length > 10 ? 45 : 0,\n formatter: (v: string) => truncateLabel(String(v), 10),\n },\n },\n yAxis: {\n type: \"value\",\n name: ctx.yFields.length === 1 ? formatLabel(ctx.yFields[0]) : undefined,\n },\n series: ctx.yFields.map((key, idx) => ({\n name: formatLabel(key),\n type: seriesType,\n data: isScatter\n ? ctx.xData.map((x, i) => [x, ctx.yDataMap[key][i]])\n : isTimeSeries\n ? createTimeSeriesData(ctx.xData, ctx.yDataMap[key])\n : ctx.yDataMap[key],\n smooth: chartType === \"line\" || chartType === \"area\" ? smooth : undefined,\n showSymbol:\n chartType === \"line\" || chartType === \"area\" ? showSymbol : undefined,\n symbol: isScatter ? \"circle\" : undefined,\n symbolSize: isScatter ? symbolSize : undefined,\n areaStyle: chartType === \"area\" ? { opacity: 0.3 } : undefined,\n stack: stacked && chartType === \"area\" ? \"total\" : undefined,\n itemStyle:\n chartType === \"bar\" ? { borderRadius: [4, 4, 0, 0] } : undefined,\n color: ctx.colors[idx % ctx.colors.length],\n })),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"options.js","names":[],"sources":["../../../src/react/charts/options.ts"],"sourcesContent":["import type { ChartType } from \"./types\";\nimport { createTimeSeriesData, formatLabel, truncateLabel } from \"./utils\";\n\n// ============================================================================\n// Option Builder Types\n// ============================================================================\n\nexport interface OptionBuilderContext {\n xData: (string | number)[];\n yDataMap: Record<string, (string | number)[]>;\n yFields: string[];\n colors: string[];\n title?: string;\n showLegend: boolean;\n xField?: string;\n}\n\nexport interface CartesianContext extends OptionBuilderContext {\n chartType: ChartType;\n isTimeSeries: boolean;\n stacked: boolean;\n smooth: boolean;\n showSymbol: boolean;\n symbolSize: number;\n}\n\n// ============================================================================\n// Base Option Builder\n// ============================================================================\n\nfunction buildBaseOption(ctx: OptionBuilderContext): Record<string, unknown> {\n return {\n title: ctx.title ? { text: ctx.title, left: \"center\" } : undefined,\n color: ctx.colors,\n };\n}\n\n// ============================================================================\n// Radar Chart Option\n// ============================================================================\n\nexport function buildRadarOption(\n ctx: OptionBuilderContext,\n showArea = true,\n): Record<string, unknown> {\n const maxValue = Math.max(\n ...ctx.yFields.flatMap((f) => ctx.yDataMap[f].map((v) => Number(v) || 0)),\n );\n\n return {\n ...buildBaseOption(ctx),\n tooltip: { trigger: \"item\" },\n legend:\n ctx.showLegend && ctx.yFields.length > 1 ? { top: \"bottom\" } : undefined,\n radar: {\n indicator: ctx.xData.map((name) => ({\n name: String(name),\n max: maxValue * 1.2,\n })),\n shape: \"polygon\",\n },\n series: [\n {\n type: \"radar\",\n data: ctx.yFields.map((key, idx) => ({\n name: formatLabel(key),\n value: ctx.yDataMap[key],\n itemStyle: { color: ctx.colors[idx % ctx.colors.length] },\n areaStyle: showArea ? { opacity: 0.3 } : undefined,\n })),\n },\n ],\n };\n}\n\n// ============================================================================\n// Pie/Donut Chart Option\n// ============================================================================\n\nexport function buildPieOption(\n ctx: OptionBuilderContext,\n chartType: \"pie\" | \"donut\",\n innerRadius: number,\n showLabels: boolean,\n labelPosition: string,\n): Record<string, unknown> {\n const pieData = ctx.xData.map((name, i) => ({\n name: String(name),\n value: ctx.yDataMap[ctx.yFields[0]]?.[i] ?? 0,\n }));\n\n const isDonut = chartType === \"donut\" || innerRadius > 0;\n\n return {\n ...buildBaseOption(ctx),\n tooltip: { trigger: \"item\", formatter: \"{b}: {c} ({d}%)\" },\n legend: ctx.showLegend\n ? { orient: \"vertical\", left: \"left\", top: \"middle\" }\n : undefined,\n series: [\n {\n type: \"pie\",\n radius: isDonut ? [`${innerRadius || 40}%`, \"70%\"] : \"70%\",\n center: [\"60%\", \"50%\"],\n data: pieData,\n label: {\n show: showLabels,\n position: labelPosition,\n formatter: \"{b}: {d}%\",\n color: \"inherit\",\n textBorderWidth: 0,\n },\n emphasis: {\n itemStyle: {\n shadowBlur: 10,\n shadowOffsetX: 0,\n shadowColor: \"rgba(0, 0, 0, 0.5)\",\n },\n },\n },\n ],\n };\n}\n\n// ============================================================================\n// Horizontal Bar Chart Option\n// ============================================================================\n\nexport function buildHorizontalBarOption(\n ctx: OptionBuilderContext,\n stacked: boolean,\n): Record<string, unknown> {\n const hasMultipleSeries = ctx.yFields.length > 1;\n\n return {\n ...buildBaseOption(ctx),\n tooltip: { trigger: \"axis\", axisPointer: { type: \"shadow\" } },\n legend: ctx.showLegend && hasMultipleSeries ? { top: \"bottom\" } : undefined,\n grid: {\n left: \"20%\",\n right: \"10%\",\n top: ctx.title ? \"15%\" : \"5%\",\n bottom: ctx.showLegend && hasMultipleSeries ? \"15%\" : \"5%\",\n },\n xAxis: { type: \"value\" },\n yAxis: {\n type: \"category\",\n data: ctx.xData,\n axisLabel: {\n width: 100,\n overflow: \"truncate\",\n formatter: (value: string) => truncateLabel(String(value)),\n },\n },\n series: ctx.yFields.map((key, idx) => ({\n name: formatLabel(key),\n type: \"bar\",\n data: ctx.yDataMap[key],\n stack: stacked ? \"total\" : undefined,\n itemStyle: { borderRadius: [0, 4, 4, 0] },\n color: ctx.colors[idx % ctx.colors.length],\n })),\n };\n}\n\n// ============================================================================\n// Heatmap Chart Option\n// ============================================================================\n\nexport interface HeatmapContext extends OptionBuilderContext {\n /** Y-axis categories (rows) */\n yAxisData: (string | number)[];\n /** Heatmap data as [xIndex, yIndex, value] tuples */\n heatmapData: [number, number, number][];\n /** Min value for color scale */\n min: number;\n /** Max value for color scale */\n max: number;\n /** Show value labels on cells */\n showLabels: boolean;\n}\n\nexport function buildHeatmapOption(\n ctx: HeatmapContext,\n): Record<string, unknown> {\n return {\n ...buildBaseOption(ctx),\n tooltip: {\n trigger: \"item\",\n formatter: (params: { data: [number, number, number] }) => {\n const [xIdx, yIdx, value] = params.data;\n const xLabel = ctx.xData[xIdx] ?? xIdx;\n const yLabel = ctx.yAxisData[yIdx] ?? yIdx;\n return `${xLabel}, ${yLabel}: ${value}`;\n },\n },\n grid: {\n left: \"15%\",\n right: \"15%\",\n top: ctx.title ? \"15%\" : \"10%\",\n bottom: \"15%\",\n },\n xAxis: {\n type: \"category\",\n data: ctx.xData,\n splitArea: { show: true },\n axisLabel: {\n rotate: ctx.xData.length > 10 ? 45 : 0,\n formatter: (v: string) => truncateLabel(String(v), 10),\n },\n },\n yAxis: {\n type: \"category\",\n data: ctx.yAxisData,\n splitArea: { show: true },\n axisLabel: {\n formatter: (v: string) => truncateLabel(String(v), 12),\n },\n },\n visualMap: {\n min: ctx.min,\n max: ctx.max,\n calculable: true,\n orient: \"vertical\",\n right: \"2%\",\n top: \"center\",\n inRange: {\n color: ctx.colors.length >= 2 ? ctx.colors : [\"#f0f0f0\", ctx.colors[0]],\n },\n },\n series: [\n {\n type: \"heatmap\",\n data: ctx.heatmapData,\n label: {\n show: ctx.showLabels,\n formatter: (params: { data: [number, number, number] }) =>\n String(params.data[2]),\n },\n emphasis: {\n itemStyle: {\n shadowBlur: 10,\n shadowColor: \"rgba(0, 0, 0, 0.5)\",\n },\n },\n },\n ],\n };\n}\n\n// ============================================================================\n// Cartesian Chart Option (line, bar, area, scatter)\n// ============================================================================\n\nexport function buildCartesianOption(\n ctx: CartesianContext,\n): Record<string, unknown> {\n const { chartType, isTimeSeries, stacked, smooth, showSymbol, symbolSize } =\n ctx;\n const hasMultipleSeries = ctx.yFields.length > 1;\n const seriesType = chartType === \"area\" ? \"line\" : chartType;\n const isScatter = chartType === \"scatter\";\n\n return {\n ...buildBaseOption(ctx),\n tooltip: { trigger: isScatter ? \"item\" : \"axis\" },\n legend: ctx.showLegend && hasMultipleSeries ? { top: \"bottom\" } : undefined,\n grid: {\n left: \"10%\",\n right: \"10%\",\n top: ctx.title ? \"15%\" : \"10%\",\n bottom: ctx.showLegend && hasMultipleSeries ? \"20%\" : \"15%\",\n },\n xAxis: {\n type: isScatter ? \"value\" : isTimeSeries ? \"time\" : \"category\",\n data: isScatter || isTimeSeries ? undefined : ctx.xData,\n name: ctx.xField ? formatLabel(ctx.xField) : undefined,\n axisLabel:\n isScatter || isTimeSeries\n ? { show: true }\n : {\n rotate: ctx.xData.length > 10 ? 45 : 0,\n formatter: (v: string) => truncateLabel(String(v), 10),\n },\n },\n yAxis: {\n type: \"value\",\n name: ctx.yFields.length === 1 ? formatLabel(ctx.yFields[0]) : undefined,\n },\n series: ctx.yFields.map((key, idx) => ({\n name: formatLabel(key),\n type: seriesType,\n data: isScatter\n ? ctx.xData.map((x, i) => [x, ctx.yDataMap[key][i]])\n : isTimeSeries\n ? createTimeSeriesData(ctx.xData, ctx.yDataMap[key])\n : ctx.yDataMap[key],\n smooth: chartType === \"line\" || chartType === \"area\" ? smooth : undefined,\n showSymbol:\n chartType === \"line\" || chartType === \"area\" ? showSymbol : undefined,\n symbol: isScatter ? \"circle\" : undefined,\n symbolSize: isScatter ? symbolSize : undefined,\n areaStyle: chartType === \"area\" ? { opacity: 0.3 } : undefined,\n stack: stacked && chartType === \"area\" ? \"total\" : undefined,\n itemStyle:\n chartType === \"bar\" ? { borderRadius: [4, 4, 0, 0] } : undefined,\n color: ctx.colors[idx % ctx.colors.length],\n })),\n };\n}\n"],"mappings":"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"}
|
|
@@ -12,6 +12,11 @@ interface ChartInference {
|
|
|
12
12
|
* Returns `null` when the data is not suitable for charting.
|
|
13
13
|
*/
|
|
14
14
|
declare function inferChartType(rows: Record<string, unknown>[], columns: GenieColumnMeta[]): ChartInference | null;
|
|
15
|
+
/**
|
|
16
|
+
* Return the chart types that are compatible with the given data shape.
|
|
17
|
+
* Used to populate the chart-type selector dropdown.
|
|
18
|
+
*/
|
|
19
|
+
declare function getCompatibleChartTypes(rows: Record<string, unknown>[], columns: GenieColumnMeta[]): ChartType[];
|
|
15
20
|
//#endregion
|
|
16
|
-
export { ChartInference, inferChartType };
|
|
21
|
+
export { ChartInference, getCompatibleChartTypes, inferChartType };
|
|
17
22
|
//# sourceMappingURL=genie-chart-inference.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chart-inference.d.ts","names":[],"sources":["../../../src/react/genie/genie-chart-inference.ts"],"mappings":";;;;UAqDiB,cAAA;EACf,SAAA,EAAW,SAAA;EACX,IAAA;EACA,IAAA;AAAA;;;;;
|
|
1
|
+
{"version":3,"file":"genie-chart-inference.d.ts","names":[],"sources":["../../../src/react/genie/genie-chart-inference.ts"],"mappings":";;;;UAqDiB,cAAA;EACf,SAAA,EAAW,SAAA;EACX,IAAA;EACA,IAAA;AAAA;;;;;iBAoDc,cAAA,CACd,IAAA,EAAM,MAAA,qBACN,OAAA,EAAS,eAAA,KACR,cAAA;;;;;iBAyEa,uBAAA,CACd,IAAA,EAAM,MAAA,qBACN,OAAA,EAAS,eAAA,KACR,SAAA"}
|
|
@@ -5,6 +5,18 @@ const INFERENCE_CONFIG = {
|
|
|
5
5
|
barMaxCategories: 100,
|
|
6
6
|
groupedBarMaxCategories: 50
|
|
7
7
|
};
|
|
8
|
+
function classifyColumns(rows, columns) {
|
|
9
|
+
if (rows.length < INFERENCE_CONFIG.minRows || columns.length < 2) return null;
|
|
10
|
+
const dateCols = columns.filter((c) => c.category === "date");
|
|
11
|
+
const numericCols = columns.filter((c) => c.category === "numeric");
|
|
12
|
+
const stringCols = columns.filter((c) => c.category === "string");
|
|
13
|
+
if (numericCols.length === 0) return null;
|
|
14
|
+
return {
|
|
15
|
+
dateCols,
|
|
16
|
+
numericCols,
|
|
17
|
+
stringCols
|
|
18
|
+
};
|
|
19
|
+
}
|
|
8
20
|
function countUnique(rows, key) {
|
|
9
21
|
const seen = /* @__PURE__ */ new Set();
|
|
10
22
|
for (const row of rows) seen.add(row[key]);
|
|
@@ -19,11 +31,9 @@ function hasNegativeValues(rows, key) {
|
|
|
19
31
|
* Returns `null` when the data is not suitable for charting.
|
|
20
32
|
*/
|
|
21
33
|
function inferChartType(rows, columns) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const stringCols = columns.filter((c) => c.category === "string");
|
|
26
|
-
if (numericCols.length === 0) return null;
|
|
34
|
+
const classified = classifyColumns(rows, columns);
|
|
35
|
+
if (!classified) return null;
|
|
36
|
+
const { dateCols, numericCols, stringCols } = classified;
|
|
27
37
|
if (dateCols.length > 0 && numericCols.length >= 1) return {
|
|
28
38
|
chartType: "line",
|
|
29
39
|
xKey: dateCols[0].name,
|
|
@@ -69,7 +79,51 @@ function inferChartType(rows, columns) {
|
|
|
69
79
|
};
|
|
70
80
|
return null;
|
|
71
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Return the chart types that are compatible with the given data shape.
|
|
84
|
+
* Used to populate the chart-type selector dropdown.
|
|
85
|
+
*/
|
|
86
|
+
function getCompatibleChartTypes(rows, columns) {
|
|
87
|
+
const classified = classifyColumns(rows, columns);
|
|
88
|
+
if (!classified) return [];
|
|
89
|
+
const { dateCols, numericCols, stringCols } = classified;
|
|
90
|
+
if (dateCols.length > 0 && numericCols.length >= 1) return [
|
|
91
|
+
"line",
|
|
92
|
+
"bar",
|
|
93
|
+
"area"
|
|
94
|
+
];
|
|
95
|
+
if (stringCols.length > 0 && numericCols.length >= 1) {
|
|
96
|
+
const xKey = stringCols[0].name;
|
|
97
|
+
const uniqueCategories = countUnique(rows, xKey);
|
|
98
|
+
if (numericCols.length === 1) {
|
|
99
|
+
const yKey = numericCols[0].name;
|
|
100
|
+
if (uniqueCategories <= INFERENCE_CONFIG.pieMaxCategories && !hasNegativeValues(rows, yKey)) return [
|
|
101
|
+
"pie",
|
|
102
|
+
"donut",
|
|
103
|
+
"bar",
|
|
104
|
+
"line",
|
|
105
|
+
"area"
|
|
106
|
+
];
|
|
107
|
+
return [
|
|
108
|
+
"bar",
|
|
109
|
+
"line",
|
|
110
|
+
"area"
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
return [
|
|
114
|
+
"bar",
|
|
115
|
+
"line",
|
|
116
|
+
"area"
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
if (numericCols.length >= 2 && stringCols.length === 0 && dateCols.length === 0) return [
|
|
120
|
+
"scatter",
|
|
121
|
+
"line",
|
|
122
|
+
"area"
|
|
123
|
+
];
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
72
126
|
|
|
73
127
|
//#endregion
|
|
74
|
-
export { inferChartType };
|
|
128
|
+
export { getCompatibleChartTypes, inferChartType };
|
|
75
129
|
//# sourceMappingURL=genie-chart-inference.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chart-inference.js","names":[],"sources":["../../../src/react/genie/genie-chart-inference.ts"],"sourcesContent":["/**\n * ┌─────────────────────────────────────────────────────────────────────┐\n * │ CHART INFERENCE RULES │\n * │ │\n * │ These rules determine what chart type is shown for Genie query │\n * │ results. Modify thresholds and chart type mappings here. │\n * │ │\n * │ Column types are classified from SQL type_name: │\n * │ DATE: DATE, TIMESTAMP, TIMESTAMP_NTZ │\n * │ NUMERIC: DECIMAL, INT, DOUBLE, FLOAT, LONG, etc. │\n * │ STRING: STRING, VARCHAR, CHAR │\n * │ │\n * │ Rules (applied in priority order): │\n * │ │\n * │ SKIP (return null): │\n * │ - < 2 rows │\n * │ - < 2 columns │\n * │ - No numeric columns │\n * │ │\n * │ MATCH: │\n * │ 1. DATE + numeric(s) → line (timeseries) │\n * │ 2. STRING + 1 numeric, ≤7 cats → pie │\n * │ 3. STRING + 1 numeric, ≤100 cats → bar │\n * │ 4. STRING + 1 numeric, >100 cats → line │\n * │ 5. STRING + N numerics, ≤50 cats → bar (grouped) │\n * │ 6. STRING + N numerics, >50 cats → line (multi-series) │\n * │ 7. 2+ numerics only → scatter │\n * │ 8. Otherwise → null (skip) │\n * │ │\n * │ KNOWN LIMITATIONS: │\n * │ - First-column heuristic: picks first string col as category │\n * │ - No semantic understanding (can't tell ID from meaningful val) │\n * └─────────────────────────────────────────────────────────────────────┘\n */\n\nimport type { ChartType } from \"../charts/types\";\nimport type { GenieColumnMeta } from \"./genie-query-transform\";\n\n// ---------------------------------------------------------------------------\n// Configuration — edit thresholds here\n// ---------------------------------------------------------------------------\n\nconst INFERENCE_CONFIG = {\n /** Min rows required to show any chart */\n minRows: 2,\n /** Max unique categories for pie chart */\n pieMaxCategories: 7,\n /** Max unique categories for bar chart (single series) */\n barMaxCategories: 100,\n /** Max unique categories for grouped bar chart (multi series) */\n groupedBarMaxCategories: 50,\n} as const;\n\nexport interface ChartInference {\n chartType: ChartType;\n xKey: string;\n yKey: string | string[];\n}\n\nfunction countUnique(rows: Record<string, unknown>[], key: string): number {\n const seen = new Set<unknown>();\n for (const row of rows) {\n seen.add(row[key]);\n }\n return seen.size;\n}\n\nfunction hasNegativeValues(\n rows: Record<string, unknown>[],\n key: string,\n): boolean {\n for (const row of rows) {\n if (Number(row[key]) < 0) return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Main inference function\n// ---------------------------------------------------------------------------\n\n/**\n * Infer the best chart type for the given Genie query result.\n * Returns `null` when the data is not suitable for charting.\n */\nexport function inferChartType(\n rows: Record<string, unknown>[],\n columns: GenieColumnMeta[],\n): ChartInference | null {\n // Guard: need at least minRows and 2 columns\n if (rows.length < INFERENCE_CONFIG.minRows || columns.length < 2) {\n return null;\n }\n\n const dateCols = columns.filter((c) => c.category === \"date\");\n const numericCols = columns.filter((c) => c.category === \"numeric\");\n const stringCols = columns.filter((c) => c.category === \"string\");\n\n // Guard: must have at least one numeric column\n if (numericCols.length === 0) return null;\n\n // Rule 1: DATE + numeric(s) → line (timeseries)\n if (dateCols.length > 0 && numericCols.length >= 1) {\n return {\n chartType: \"line\",\n xKey: dateCols[0].name,\n yKey:\n numericCols.length === 1\n ? numericCols[0].name\n : numericCols.map((c) => c.name),\n };\n }\n\n // Rules 2–6: STRING + numeric(s)\n if (stringCols.length > 0 && numericCols.length >= 1) {\n const xKey = stringCols[0].name;\n const uniqueCategories = countUnique(rows, xKey);\n\n if (numericCols.length === 1) {\n const yKey = numericCols[0].name;\n\n // Rule 2: few categories → pie (skip if negative values)\n if (\n uniqueCategories <= INFERENCE_CONFIG.pieMaxCategories &&\n !hasNegativeValues(rows, yKey)\n ) {\n return { chartType: \"pie\", xKey, yKey };\n }\n // Rule 3: moderate categories → bar\n if (uniqueCategories <= INFERENCE_CONFIG.barMaxCategories) {\n return { chartType: \"bar\", xKey, yKey };\n }\n // Rule 4: many categories → line\n return { chartType: \"line\", xKey, yKey };\n }\n\n // Multiple numerics\n const yKey = numericCols.map((c) => c.name);\n\n // Rule 5: moderate categories → bar (grouped)\n if (uniqueCategories <= INFERENCE_CONFIG.groupedBarMaxCategories) {\n return { chartType: \"bar\", xKey, yKey };\n }\n // Rule 6: many categories → line (multi-series)\n return { chartType: \"line\", xKey, yKey };\n }\n\n // Rule 7: 2+ numerics only (no string, no date) → scatter\n if (\n numericCols.length >= 2 &&\n stringCols.length === 0 &&\n dateCols.length === 0\n ) {\n return {\n chartType: \"scatter\",\n xKey: numericCols[0].name,\n yKey: numericCols[1].name,\n };\n }\n\n // Rule 8: fallback — no chart\n return null;\n}\n"],"mappings":";AA0CA,MAAM,mBAAmB;CAEvB,SAAS;CAET,kBAAkB;CAElB,kBAAkB;CAElB,yBAAyB;CAC1B;AAQD,SAAS,YAAY,MAAiC,KAAqB;CACzE,MAAM,uBAAO,IAAI,KAAc;AAC/B,MAAK,MAAM,OAAO,KAChB,MAAK,IAAI,IAAI,KAAK;AAEpB,QAAO,KAAK;;AAGd,SAAS,kBACP,MACA,KACS;AACT,MAAK,MAAM,OAAO,KAChB,KAAI,OAAO,IAAI,KAAK,GAAG,EAAG,QAAO;AAEnC,QAAO;;;;;;AAWT,SAAgB,eACd,MACA,SACuB;AAEvB,KAAI,KAAK,SAAS,iBAAiB,WAAW,QAAQ,SAAS,EAC7D,QAAO;CAGT,MAAM,WAAW,QAAQ,QAAQ,MAAM,EAAE,aAAa,OAAO;CAC7D,MAAM,cAAc,QAAQ,QAAQ,MAAM,EAAE,aAAa,UAAU;CACnE,MAAM,aAAa,QAAQ,QAAQ,MAAM,EAAE,aAAa,SAAS;AAGjE,KAAI,YAAY,WAAW,EAAG,QAAO;AAGrC,KAAI,SAAS,SAAS,KAAK,YAAY,UAAU,EAC/C,QAAO;EACL,WAAW;EACX,MAAM,SAAS,GAAG;EAClB,MACE,YAAY,WAAW,IACnB,YAAY,GAAG,OACf,YAAY,KAAK,MAAM,EAAE,KAAK;EACrC;AAIH,KAAI,WAAW,SAAS,KAAK,YAAY,UAAU,GAAG;EACpD,MAAM,OAAO,WAAW,GAAG;EAC3B,MAAM,mBAAmB,YAAY,MAAM,KAAK;AAEhD,MAAI,YAAY,WAAW,GAAG;GAC5B,MAAM,OAAO,YAAY,GAAG;AAG5B,OACE,oBAAoB,iBAAiB,oBACrC,CAAC,kBAAkB,MAAM,KAAK,CAE9B,QAAO;IAAE,WAAW;IAAO;IAAM;IAAM;AAGzC,OAAI,oBAAoB,iBAAiB,iBACvC,QAAO;IAAE,WAAW;IAAO;IAAM;IAAM;AAGzC,UAAO;IAAE,WAAW;IAAQ;IAAM;IAAM;;EAI1C,MAAM,OAAO,YAAY,KAAK,MAAM,EAAE,KAAK;AAG3C,MAAI,oBAAoB,iBAAiB,wBACvC,QAAO;GAAE,WAAW;GAAO;GAAM;GAAM;AAGzC,SAAO;GAAE,WAAW;GAAQ;GAAM;GAAM;;AAI1C,KACE,YAAY,UAAU,KACtB,WAAW,WAAW,KACtB,SAAS,WAAW,EAEpB,QAAO;EACL,WAAW;EACX,MAAM,YAAY,GAAG;EACrB,MAAM,YAAY,GAAG;EACtB;AAIH,QAAO"}
|
|
1
|
+
{"version":3,"file":"genie-chart-inference.js","names":[],"sources":["../../../src/react/genie/genie-chart-inference.ts"],"sourcesContent":["/**\n * ┌─────────────────────────────────────────────────────────────────────┐\n * │ CHART INFERENCE RULES │\n * │ │\n * │ These rules determine what chart type is shown for Genie query │\n * │ results. Modify thresholds and chart type mappings here. │\n * │ │\n * │ Column types are classified from SQL type_name: │\n * │ DATE: DATE, TIMESTAMP, TIMESTAMP_NTZ │\n * │ NUMERIC: DECIMAL, INT, DOUBLE, FLOAT, LONG, etc. │\n * │ STRING: STRING, VARCHAR, CHAR │\n * │ │\n * │ Rules (applied in priority order): │\n * │ │\n * │ SKIP (return null): │\n * │ - < 2 rows │\n * │ - < 2 columns │\n * │ - No numeric columns │\n * │ │\n * │ MATCH: │\n * │ 1. DATE + numeric(s) → line (timeseries) │\n * │ 2. STRING + 1 numeric, ≤7 cats → pie │\n * │ 3. STRING + 1 numeric, ≤100 cats → bar │\n * │ 4. STRING + 1 numeric, >100 cats → line │\n * │ 5. STRING + N numerics, ≤50 cats → bar (grouped) │\n * │ 6. STRING + N numerics, >50 cats → line (multi-series) │\n * │ 7. 2+ numerics only → scatter │\n * │ 8. Otherwise → null (skip) │\n * │ │\n * │ KNOWN LIMITATIONS: │\n * │ - First-column heuristic: picks first string col as category │\n * │ - No semantic understanding (can't tell ID from meaningful val) │\n * └─────────────────────────────────────────────────────────────────────┘\n */\n\nimport type { ChartType } from \"../charts/types\";\nimport type { GenieColumnMeta } from \"./genie-query-transform\";\n\n// ---------------------------------------------------------------------------\n// Configuration — edit thresholds here\n// ---------------------------------------------------------------------------\n\nconst INFERENCE_CONFIG = {\n /** Min rows required to show any chart */\n minRows: 2,\n /** Max unique categories for pie chart */\n pieMaxCategories: 7,\n /** Max unique categories for bar chart (single series) */\n barMaxCategories: 100,\n /** Max unique categories for grouped bar chart (multi series) */\n groupedBarMaxCategories: 50,\n} as const;\n\nexport interface ChartInference {\n chartType: ChartType;\n xKey: string;\n yKey: string | string[];\n}\n\ninterface ClassifiedColumns {\n dateCols: GenieColumnMeta[];\n numericCols: GenieColumnMeta[];\n stringCols: GenieColumnMeta[];\n}\n\nfunction classifyColumns(\n rows: Record<string, unknown>[],\n columns: GenieColumnMeta[],\n): ClassifiedColumns | null {\n if (rows.length < INFERENCE_CONFIG.minRows || columns.length < 2) {\n return null;\n }\n\n const dateCols = columns.filter((c) => c.category === \"date\");\n const numericCols = columns.filter((c) => c.category === \"numeric\");\n const stringCols = columns.filter((c) => c.category === \"string\");\n\n if (numericCols.length === 0) return null;\n\n return { dateCols, numericCols, stringCols };\n}\n\nfunction countUnique(rows: Record<string, unknown>[], key: string): number {\n const seen = new Set<unknown>();\n for (const row of rows) {\n seen.add(row[key]);\n }\n return seen.size;\n}\n\nfunction hasNegativeValues(\n rows: Record<string, unknown>[],\n key: string,\n): boolean {\n for (const row of rows) {\n if (Number(row[key]) < 0) return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Main inference function\n// ---------------------------------------------------------------------------\n\n/**\n * Infer the best chart type for the given Genie query result.\n * Returns `null` when the data is not suitable for charting.\n */\nexport function inferChartType(\n rows: Record<string, unknown>[],\n columns: GenieColumnMeta[],\n): ChartInference | null {\n const classified = classifyColumns(rows, columns);\n if (!classified) return null;\n\n const { dateCols, numericCols, stringCols } = classified;\n\n // Rule 1: DATE + numeric(s) → line (timeseries)\n if (dateCols.length > 0 && numericCols.length >= 1) {\n return {\n chartType: \"line\",\n xKey: dateCols[0].name,\n yKey:\n numericCols.length === 1\n ? numericCols[0].name\n : numericCols.map((c) => c.name),\n };\n }\n\n // Rules 2–6: STRING + numeric(s)\n if (stringCols.length > 0 && numericCols.length >= 1) {\n const xKey = stringCols[0].name;\n const uniqueCategories = countUnique(rows, xKey);\n\n if (numericCols.length === 1) {\n const yKey = numericCols[0].name;\n\n // Rule 2: few categories → pie (skip if negative values)\n if (\n uniqueCategories <= INFERENCE_CONFIG.pieMaxCategories &&\n !hasNegativeValues(rows, yKey)\n ) {\n return { chartType: \"pie\", xKey, yKey };\n }\n // Rule 3: moderate categories → bar\n if (uniqueCategories <= INFERENCE_CONFIG.barMaxCategories) {\n return { chartType: \"bar\", xKey, yKey };\n }\n // Rule 4: many categories → line\n return { chartType: \"line\", xKey, yKey };\n }\n\n // Multiple numerics\n const yKey = numericCols.map((c) => c.name);\n\n // Rule 5: moderate categories → bar (grouped)\n if (uniqueCategories <= INFERENCE_CONFIG.groupedBarMaxCategories) {\n return { chartType: \"bar\", xKey, yKey };\n }\n // Rule 6: many categories → line (multi-series)\n return { chartType: \"line\", xKey, yKey };\n }\n\n // Rule 7: 2+ numerics only (no string, no date) → scatter\n if (\n numericCols.length >= 2 &&\n stringCols.length === 0 &&\n dateCols.length === 0\n ) {\n return {\n chartType: \"scatter\",\n xKey: numericCols[0].name,\n yKey: numericCols[1].name,\n };\n }\n\n // Rule 8: fallback — no chart\n return null;\n}\n\n/**\n * Return the chart types that are compatible with the given data shape.\n * Used to populate the chart-type selector dropdown.\n */\nexport function getCompatibleChartTypes(\n rows: Record<string, unknown>[],\n columns: GenieColumnMeta[],\n): ChartType[] {\n const classified = classifyColumns(rows, columns);\n if (!classified) return [];\n\n const { dateCols, numericCols, stringCols } = classified;\n\n // DATE + numeric(s) → timeseries-compatible types\n if (dateCols.length > 0 && numericCols.length >= 1) {\n return [\"line\", \"bar\", \"area\"];\n }\n\n // STRING + numeric(s)\n if (stringCols.length > 0 && numericCols.length >= 1) {\n const xKey = stringCols[0].name;\n const uniqueCategories = countUnique(rows, xKey);\n\n if (numericCols.length === 1) {\n const yKey = numericCols[0].name;\n // Few categories and no negatives → pie/donut are viable\n if (\n uniqueCategories <= INFERENCE_CONFIG.pieMaxCategories &&\n !hasNegativeValues(rows, yKey)\n ) {\n return [\"pie\", \"donut\", \"bar\", \"line\", \"area\"];\n }\n return [\"bar\", \"line\", \"area\"];\n }\n\n // Multiple numerics\n return [\"bar\", \"line\", \"area\"];\n }\n\n // 2+ numerics only → scatter-compatible\n if (\n numericCols.length >= 2 &&\n stringCols.length === 0 &&\n dateCols.length === 0\n ) {\n return [\"scatter\", \"line\", \"area\"];\n }\n\n return [];\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat-message-list.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message-list.tsx"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"genie-chat-message-list.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message-list.tsx"],"mappings":";;;;UAYU,yBAAA;;EAER,QAAA,EAAU,gBAAA;EAFF;EAIR,MAAA,EAAQ,eAAA;;EAER,SAAA;EAJA;EAMA,eAAA;EAJA;EAMA,mBAAA;AAAA;;iBAkIc,oBAAA,CAAA;EACd,QAAA;EACA,MAAA;EACA,SAAA;EACA,eAAA;EACA;AAAA,GACC,yBAAA,GAAyB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -3,6 +3,7 @@ import { Skeleton } from "../ui/skeleton.js";
|
|
|
3
3
|
import { ScrollArea } from "../ui/scroll-area.js";
|
|
4
4
|
import { Spinner } from "../ui/spinner.js";
|
|
5
5
|
import { GenieChatMessage } from "./genie-chat-message.js";
|
|
6
|
+
import { TERMINAL_STATUSES } from "./types.js";
|
|
6
7
|
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
7
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
|
|
@@ -94,12 +95,12 @@ function GenieChatMessageList({ messages, status, className, hasPreviousPage = f
|
|
|
94
95
|
const sentinelRef = useLoadOlderOnScroll(scrollRef, hasPreviousPage && status !== "loading-older", onFetchPreviousPage);
|
|
95
96
|
useScrollManagement(scrollRef, messages, status);
|
|
96
97
|
const lastMessage = messages[messages.length - 1];
|
|
97
|
-
const showStreamingIndicator = status === "streaming" && lastMessage?.role === "assistant" && lastMessage.
|
|
98
|
+
const showStreamingIndicator = status === "streaming" && lastMessage?.role === "assistant" && !lastMessage.content && !TERMINAL_STATUSES.has(lastMessage.status);
|
|
98
99
|
return /* @__PURE__ */ jsx(ScrollArea, {
|
|
99
100
|
ref: scrollRef,
|
|
100
|
-
className: cn("flex-1 min-h-0 p-4", className),
|
|
101
|
+
className: cn("flex-1 min-h-0 p-4 [&_[data-slot=scroll-area-viewport]>div]:!block", className),
|
|
101
102
|
children: /* @__PURE__ */ jsxs("div", {
|
|
102
|
-
className: "flex flex-col gap-4",
|
|
103
|
+
className: "flex flex-col gap-4 min-w-0",
|
|
103
104
|
children: [
|
|
104
105
|
hasPreviousPage && /* @__PURE__ */ jsx("div", {
|
|
105
106
|
ref: sentinelRef,
|
|
@@ -120,7 +121,7 @@ function GenieChatMessageList({ messages, status, className, hasPreviousPage = f
|
|
|
120
121
|
/* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-2/3 self-end" })
|
|
121
122
|
]
|
|
122
123
|
}),
|
|
123
|
-
messages.filter((msg) => msg.role !== "assistant" || msg.id !== ""
|
|
124
|
+
messages.filter((msg) => msg.role !== "assistant" || msg.content || msg.id !== "" && TERMINAL_STATUSES.has(msg.status)).map((msg) => /* @__PURE__ */ jsx(GenieChatMessage, { message: msg }, msg.id)),
|
|
124
125
|
showStreamingIndicator && /* @__PURE__ */ jsxs("div", {
|
|
125
126
|
className: "flex items-center gap-2 text-sm text-muted-foreground px-11",
|
|
126
127
|
children: [/* @__PURE__ */ jsx(Spinner, { className: "h-3 w-3" }), /* @__PURE__ */ jsx("span", { children: formatStatus(lastMessage.status) })]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat-message-list.js","names":[],"sources":["../../../src/react/genie/genie-chat-message-list.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport { Spinner } from \"../ui/spinner\";\nimport { GenieChatMessage } from \"./genie-chat-message\";\nimport type { GenieChatStatus, GenieMessageItem } from \"./types\";\n\ninterface GenieChatMessageListProps {\n /** Array of messages to display */\n messages: GenieMessageItem[];\n /** Current chat status (controls loading indicators and skeleton placeholders) */\n status: GenieChatStatus;\n /** Additional CSS class for the scroll area */\n className?: string;\n /** Whether a previous page of older messages exists */\n hasPreviousPage?: boolean;\n /** Callback to fetch the previous page of messages */\n onFetchPreviousPage?: () => void;\n}\n\nconst STATUS_LABELS: Record<string, string> = {\n ASKING_AI: \"Asking AI...\",\n EXECUTING_QUERY: \"Executing query...\",\n FILTERING_RESULTS: \"Filtering results...\",\n COMPLETED: \"Done\",\n};\n\nfunction formatStatus(status: string): string {\n return STATUS_LABELS[status] ?? status.replace(/_/g, \" \").toLowerCase();\n}\n\nfunction getViewport(scrollRef: React.RefObject<HTMLDivElement | null>) {\n return scrollRef.current?.querySelector<HTMLElement>(\n '[data-slot=\"scroll-area-viewport\"]',\n );\n}\n\n/**\n * Manages scroll position: scrolls to bottom on append/initial load,\n * preserves position when older messages are prepended.\n */\nfunction useScrollManagement(\n scrollRef: React.RefObject<HTMLDivElement | null>,\n messages: GenieMessageItem[],\n status: GenieChatStatus,\n) {\n const prevFirstMessageIdRef = useRef<string | null>(null);\n const prevScrollHeightRef = useRef(0);\n const prevMessageCountRef = useRef(0);\n\n // Keep prevScrollHeightRef fresh when async content (images, embeds)\n // changes the viewport height between renders.\n useEffect(() => {\n const viewport = getViewport(scrollRef);\n if (!viewport) return;\n\n const observer = new ResizeObserver(() => {\n prevScrollHeightRef.current = viewport.scrollHeight;\n });\n observer.observe(viewport);\n return () => observer.disconnect();\n }, [scrollRef]);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: react to message count AND status so prevScrollHeightRef stays accurate when the loading indicator appears/disappears\n useLayoutEffect(() => {\n const viewport = getViewport(scrollRef);\n if (!viewport) return;\n\n const count = messages.length;\n const countChanged = count !== prevMessageCountRef.current;\n prevMessageCountRef.current = count;\n\n // Nothing to do if message count didn't change (e.g. status-only transition)\n if (!countChanged) {\n prevScrollHeightRef.current = viewport.scrollHeight;\n return;\n }\n\n const firstMessageId = messages[0]?.id ?? null;\n const wasPrepend =\n prevFirstMessageIdRef.current !== null &&\n firstMessageId !== prevFirstMessageIdRef.current;\n\n if (wasPrepend && prevScrollHeightRef.current > 0) {\n // Older messages prepended — preserve scroll position\n const delta = viewport.scrollHeight - prevScrollHeightRef.current;\n viewport.scrollTop += delta;\n } else {\n // Messages appended or initial load — scroll to bottom\n viewport.scrollTop = viewport.scrollHeight;\n }\n\n prevFirstMessageIdRef.current = firstMessageId;\n prevScrollHeightRef.current = viewport.scrollHeight;\n }, [messages.length, status]);\n}\n\n/**\n * Observes a sentinel element at the top of the scroll area and triggers\n * `onFetchPreviousPage` when the user scrolls to the top (only if content overflows).\n * Returns a ref to attach to the sentinel element.\n */\nfunction useLoadOlderOnScroll(\n scrollRef: React.RefObject<HTMLDivElement | null>,\n shouldObserve: boolean,\n onFetchPreviousPage?: () => void,\n) {\n const sentinelRef = useRef<HTMLDivElement>(null);\n const onFetchPreviousPageRef = useRef(onFetchPreviousPage);\n onFetchPreviousPageRef.current = onFetchPreviousPage;\n\n useEffect(() => {\n const sentinel = sentinelRef.current;\n const viewport = getViewport(scrollRef);\n if (!sentinel || !viewport || !shouldObserve) return;\n\n // The observer fires synchronously on observe() if the sentinel is\n // already visible. We arm it on the next frame so that synchronous\n // initial fire is ignored, but a real intersection (user genuinely\n // at the top on a short conversation) triggers on subsequent frames.\n let armed = false;\n const frameId = requestAnimationFrame(() => {\n armed = true;\n });\n\n const observer = new IntersectionObserver(\n (entries) => {\n if (!armed) return;\n const isScrollable = viewport.scrollHeight > viewport.clientHeight;\n if (entries[0]?.isIntersecting && isScrollable) {\n onFetchPreviousPageRef.current?.();\n }\n },\n { root: viewport, threshold: 0 },\n );\n\n observer.observe(sentinel);\n return () => {\n cancelAnimationFrame(frameId);\n observer.disconnect();\n };\n }, [scrollRef, shouldObserve]);\n\n return sentinelRef;\n}\n\n/** Scrollable message list that renders Genie chat messages with auto-scroll, skeleton loaders, and a streaming indicator. */\nexport function GenieChatMessageList({\n messages,\n status,\n className,\n hasPreviousPage = false,\n onFetchPreviousPage,\n}: GenieChatMessageListProps) {\n const scrollRef = useRef<HTMLDivElement>(null);\n\n const sentinelRef = useLoadOlderOnScroll(\n scrollRef,\n hasPreviousPage && status !== \"loading-older\",\n onFetchPreviousPage,\n );\n useScrollManagement(scrollRef, messages, status);\n\n const lastMessage = messages[messages.length - 1];\n const showStreamingIndicator =\n status === \"streaming\" &&\n lastMessage?.role === \"assistant\" &&\n lastMessage.id === \"\";\n\n return (\n <ScrollArea ref={scrollRef} className={cn(\"flex-1 min-h-0 p-4\", className)}>\n <div className=\"flex flex-col gap-4\">\n {hasPreviousPage && <div ref={sentinelRef} className=\"h-px\" />}\n\n {status === \"loading-older\" && (\n <div className=\"flex items-center justify-center gap-2 py-2\">\n <Spinner className=\"h-3 w-3\" />\n <span className=\"text-sm text-muted-foreground\">\n Loading older messages...\n </span>\n </div>\n )}\n\n {status === \"loading-history\" && messages.length === 0 && (\n <div className=\"flex flex-col gap-4\">\n <Skeleton className=\"h-12 w-3/4\" />\n <Skeleton className=\"h-20 w-4/5 self-start\" />\n <Skeleton className=\"h-12 w-2/3 self-end\" />\n </div>\n )}\n\n {messages\n .filter(\n (msg) => msg.role !== \"assistant\" || msg.id !== \"\" || msg.content,\n )\n .map((msg) => (\n <GenieChatMessage key={msg.id} message={msg} />\n ))}\n\n {showStreamingIndicator && (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground px-11\">\n <Spinner className=\"h-3 w-3\" />\n <span>{formatStatus(lastMessage.status)}</span>\n </div>\n )}\n\n {messages.length === 0 && status === \"idle\" && (\n <div className=\"flex items-center justify-center h-full text-muted-foreground text-sm py-12\">\n Start a conversation by typing a question below.\n </div>\n )}\n </div>\n </ScrollArea>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"genie-chat-message-list.js","names":[],"sources":["../../../src/react/genie/genie-chat-message-list.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport { Spinner } from \"../ui/spinner\";\nimport { GenieChatMessage } from \"./genie-chat-message\";\nimport {\n type GenieChatStatus,\n type GenieMessageItem,\n TERMINAL_STATUSES,\n} from \"./types\";\n\ninterface GenieChatMessageListProps {\n /** Array of messages to display */\n messages: GenieMessageItem[];\n /** Current chat status (controls loading indicators and skeleton placeholders) */\n status: GenieChatStatus;\n /** Additional CSS class for the scroll area */\n className?: string;\n /** Whether a previous page of older messages exists */\n hasPreviousPage?: boolean;\n /** Callback to fetch the previous page of messages */\n onFetchPreviousPage?: () => void;\n}\n\nconst STATUS_LABELS: Record<string, string> = {\n ASKING_AI: \"Asking AI...\",\n EXECUTING_QUERY: \"Executing query...\",\n FILTERING_RESULTS: \"Filtering results...\",\n COMPLETED: \"Done\",\n};\n\nfunction formatStatus(status: string): string {\n return STATUS_LABELS[status] ?? status.replace(/_/g, \" \").toLowerCase();\n}\n\nfunction getViewport(scrollRef: React.RefObject<HTMLDivElement | null>) {\n return scrollRef.current?.querySelector<HTMLElement>(\n '[data-slot=\"scroll-area-viewport\"]',\n );\n}\n\n/**\n * Manages scroll position: scrolls to bottom on append/initial load,\n * preserves position when older messages are prepended.\n */\nfunction useScrollManagement(\n scrollRef: React.RefObject<HTMLDivElement | null>,\n messages: GenieMessageItem[],\n status: GenieChatStatus,\n) {\n const prevFirstMessageIdRef = useRef<string | null>(null);\n const prevScrollHeightRef = useRef(0);\n const prevMessageCountRef = useRef(0);\n\n // Keep prevScrollHeightRef fresh when async content (images, embeds)\n // changes the viewport height between renders.\n useEffect(() => {\n const viewport = getViewport(scrollRef);\n if (!viewport) return;\n\n const observer = new ResizeObserver(() => {\n prevScrollHeightRef.current = viewport.scrollHeight;\n });\n observer.observe(viewport);\n return () => observer.disconnect();\n }, [scrollRef]);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: react to message count AND status so prevScrollHeightRef stays accurate when the loading indicator appears/disappears\n useLayoutEffect(() => {\n const viewport = getViewport(scrollRef);\n if (!viewport) return;\n\n const count = messages.length;\n const countChanged = count !== prevMessageCountRef.current;\n prevMessageCountRef.current = count;\n\n // Nothing to do if message count didn't change (e.g. status-only transition)\n if (!countChanged) {\n prevScrollHeightRef.current = viewport.scrollHeight;\n return;\n }\n\n const firstMessageId = messages[0]?.id ?? null;\n const wasPrepend =\n prevFirstMessageIdRef.current !== null &&\n firstMessageId !== prevFirstMessageIdRef.current;\n\n if (wasPrepend && prevScrollHeightRef.current > 0) {\n // Older messages prepended — preserve scroll position\n const delta = viewport.scrollHeight - prevScrollHeightRef.current;\n viewport.scrollTop += delta;\n } else {\n // Messages appended or initial load — scroll to bottom\n viewport.scrollTop = viewport.scrollHeight;\n }\n\n prevFirstMessageIdRef.current = firstMessageId;\n prevScrollHeightRef.current = viewport.scrollHeight;\n }, [messages.length, status]);\n}\n\n/**\n * Observes a sentinel element at the top of the scroll area and triggers\n * `onFetchPreviousPage` when the user scrolls to the top (only if content overflows).\n * Returns a ref to attach to the sentinel element.\n */\nfunction useLoadOlderOnScroll(\n scrollRef: React.RefObject<HTMLDivElement | null>,\n shouldObserve: boolean,\n onFetchPreviousPage?: () => void,\n) {\n const sentinelRef = useRef<HTMLDivElement>(null);\n const onFetchPreviousPageRef = useRef(onFetchPreviousPage);\n onFetchPreviousPageRef.current = onFetchPreviousPage;\n\n useEffect(() => {\n const sentinel = sentinelRef.current;\n const viewport = getViewport(scrollRef);\n if (!sentinel || !viewport || !shouldObserve) return;\n\n // The observer fires synchronously on observe() if the sentinel is\n // already visible. We arm it on the next frame so that synchronous\n // initial fire is ignored, but a real intersection (user genuinely\n // at the top on a short conversation) triggers on subsequent frames.\n let armed = false;\n const frameId = requestAnimationFrame(() => {\n armed = true;\n });\n\n const observer = new IntersectionObserver(\n (entries) => {\n if (!armed) return;\n const isScrollable = viewport.scrollHeight > viewport.clientHeight;\n if (entries[0]?.isIntersecting && isScrollable) {\n onFetchPreviousPageRef.current?.();\n }\n },\n { root: viewport, threshold: 0 },\n );\n\n observer.observe(sentinel);\n return () => {\n cancelAnimationFrame(frameId);\n observer.disconnect();\n };\n }, [scrollRef, shouldObserve]);\n\n return sentinelRef;\n}\n\n/** Scrollable message list that renders Genie chat messages with auto-scroll, skeleton loaders, and a streaming indicator. */\nexport function GenieChatMessageList({\n messages,\n status,\n className,\n hasPreviousPage = false,\n onFetchPreviousPage,\n}: GenieChatMessageListProps) {\n const scrollRef = useRef<HTMLDivElement>(null);\n\n const sentinelRef = useLoadOlderOnScroll(\n scrollRef,\n hasPreviousPage && status !== \"loading-older\",\n onFetchPreviousPage,\n );\n useScrollManagement(scrollRef, messages, status);\n\n const lastMessage = messages[messages.length - 1];\n const showStreamingIndicator =\n status === \"streaming\" &&\n lastMessage?.role === \"assistant\" &&\n !lastMessage.content &&\n !TERMINAL_STATUSES.has(lastMessage.status);\n\n return (\n <ScrollArea\n ref={scrollRef}\n className={cn(\n \"flex-1 min-h-0 p-4 [&_[data-slot=scroll-area-viewport]>div]:!block\",\n className,\n )}\n >\n <div className=\"flex flex-col gap-4 min-w-0\">\n {hasPreviousPage && <div ref={sentinelRef} className=\"h-px\" />}\n\n {status === \"loading-older\" && (\n <div className=\"flex items-center justify-center gap-2 py-2\">\n <Spinner className=\"h-3 w-3\" />\n <span className=\"text-sm text-muted-foreground\">\n Loading older messages...\n </span>\n </div>\n )}\n\n {status === \"loading-history\" && messages.length === 0 && (\n <div className=\"flex flex-col gap-4\">\n <Skeleton className=\"h-12 w-3/4\" />\n <Skeleton className=\"h-20 w-4/5 self-start\" />\n <Skeleton className=\"h-12 w-2/3 self-end\" />\n </div>\n )}\n\n {messages\n .filter(\n (msg) =>\n msg.role !== \"assistant\" ||\n msg.content ||\n (msg.id !== \"\" && TERMINAL_STATUSES.has(msg.status)),\n )\n .map((msg) => (\n <GenieChatMessage key={msg.id} message={msg} />\n ))}\n\n {showStreamingIndicator && (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground px-11\">\n <Spinner className=\"h-3 w-3\" />\n <span>{formatStatus(lastMessage.status)}</span>\n </div>\n )}\n\n {messages.length === 0 && status === \"idle\" && (\n <div className=\"flex items-center justify-center h-full text-muted-foreground text-sm py-12\">\n Start a conversation by typing a question below.\n </div>\n )}\n </div>\n </ScrollArea>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat-message.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"genie-chat-message.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"mappings":";;;;UA4BU,qBAAA;;EAER,OAAA,EAAS,gBAAA;EAFD;EAIR,SAAA;AAAA;;iBAQc,gBAAA,CAAA;EACd,OAAA;EACA;AAAA,GACC,qBAAA,GAAqB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|