diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f426a99..e4d70be 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,22 +1,27 @@ { "name": "frontend", - "version": "0.1.5", + "version": "0.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "0.1.5", + "version": "0.1.6", "license": "AGPL-3.0-only", "dependencies": { "@duckdb/duckdb-wasm": "^1.29.1-dev132.0", "@monaco-editor/react": "^4.7.0", "@motherduck/wasm-client": "^0.6.6", + "@observablehq/plot": "^0.6.17", "@radix-ui/react-slot": "^1.2.2", "@shadcn/ui": "^0.0.4", "@tailwindcss/vite": "^4.1.5", "@types/d3": "^7.4.3", "@types/papaparse": "^5.3.15", + "@uwdata/mosaic-core": "^0.17.0", + "@uwdata/mosaic-plot": "^0.17.0", + "@uwdata/mosaic-sql": "^0.17.0", + "@uwdata/vgplot": "^0.17.0", "apache-arrow": "^20.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -1238,6 +1243,20 @@ "node": ">= 8" } }, + "node_modules/@observablehq/plot": { + "version": "0.6.17", + "resolved": "https://registry.npmjs.org/@observablehq/plot/-/plot-0.6.17.tgz", + "integrity": "sha512-/qaXP/7mc4MUS0s4cPPFASDRjtsWp85/TbfsciqDgU1HwYixbSbbytNuInD8AcTYC3xaxACgVX06agdfQy9W+g==", + "license": "ISC", + "dependencies": { + "d3": "^7.9.0", + "interval-tree-1d": "^1.0.0", + "isoformat": "^0.2.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -2429,6 +2448,152 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uwdata/flechette": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@uwdata/flechette/-/flechette-2.0.1.tgz", + "integrity": "sha512-eKrNd1mNZDqf0iUbaDs3/Ld3qmVGb+ADckNjXTdUwfb2Q4o9cdGYIIYfzFSA5LybXx2RK5PXHPZS1c+PmyW0sA==", + "license": "BSD-3-Clause" + }, + "node_modules/@uwdata/mosaic-core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@uwdata/mosaic-core/-/mosaic-core-0.17.0.tgz", + "integrity": "sha512-bMlCdLaNRa0K3ci5HutqHKqJXUawhLfyCVAqw2hdJfOUcZqGk58Cwc/pGV1BSrFW/ou70yatH8CtYcXLn6sYnw==", + "license": "BSD-3-Clause", + "dependencies": { + "@duckdb/duckdb-wasm": "^1.29.0", + "@uwdata/flechette": "^2.0.0", + "@uwdata/mosaic-sql": "^0.17.0" + } + }, + "node_modules/@uwdata/mosaic-core/node_modules/@duckdb/duckdb-wasm": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@duckdb/duckdb-wasm/-/duckdb-wasm-1.29.0.tgz", + "integrity": "sha512-8Zq7vafQuIz9gklC/9375KE38UlkaS2n8+yvG+/JK7irm3DjwYNJHL4xfplIj0bSHFIg6we5XhWYFqtE/vO3+Q==", + "license": "MIT", + "dependencies": { + "apache-arrow": "^17.0.0" + } + }, + "node_modules/@uwdata/mosaic-core/node_modules/@types/node": { + "version": "20.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz", + "integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@uwdata/mosaic-core/node_modules/apache-arrow": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-17.0.0.tgz", + "integrity": "sha512-X0p7auzdnGuhYMVKYINdQssS4EcKec9TCXyez/qtJt32DrIMGbzqiaMiQ0X6fQlQpw8Fl0Qygcv4dfRAr5Gu9Q==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/command-line-args": "^5.2.3", + "@types/command-line-usage": "^5.0.4", + "@types/node": "^20.13.0", + "command-line-args": "^5.2.1", + "command-line-usage": "^7.0.1", + "flatbuffers": "^24.3.25", + "json-bignum": "^0.0.3", + "tslib": "^2.6.2" + }, + "bin": { + "arrow2csv": "bin/arrow2csv.cjs" + } + }, + "node_modules/@uwdata/mosaic-core/node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@uwdata/mosaic-core/node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "license": "MIT", + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@uwdata/mosaic-core/node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "license": "MIT", + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@uwdata/mosaic-core/node_modules/flatbuffers": { + "version": "24.12.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.12.23.tgz", + "integrity": "sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==", + "license": "Apache-2.0" + }, + "node_modules/@uwdata/mosaic-core/node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@uwdata/mosaic-inputs": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@uwdata/mosaic-inputs/-/mosaic-inputs-0.17.0.tgz", + "integrity": "sha512-sb66PfG8FyY8VFdVLkVo4DpCny14PkArCo7f3g+4bo9R4iS9L5WjWrrz5SonB0QLJigA4qCRF8GImb7r92b0JQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@uwdata/mosaic-core": "^0.17.0", + "@uwdata/mosaic-sql": "^0.17.0" + } + }, + "node_modules/@uwdata/mosaic-plot": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@uwdata/mosaic-plot/-/mosaic-plot-0.17.0.tgz", + "integrity": "sha512-SxXxX+DXjdGKo2jb712qd0IdmJ5LufbN52R8Qak84cM3ile07HieMgTa822S1ERbVv8qBevSTtu1Cu1jH03kaA==", + "license": "BSD-3-Clause", + "dependencies": { + "@observablehq/plot": "^0.6.17", + "@uwdata/mosaic-core": "^0.17.0", + "@uwdata/mosaic-sql": "^0.17.0", + "d3": "^7.9.0" + } + }, + "node_modules/@uwdata/mosaic-sql": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@uwdata/mosaic-sql/-/mosaic-sql-0.17.0.tgz", + "integrity": "sha512-Et7kzXEMo1txsXFJuX+jEujvj6EcIa6xuhlGYPyJi7z3HuFS/s8JNyDSpGjQsJP+JwL4pDeHdkK81w9/anY60Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@uwdata/vgplot": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@uwdata/vgplot/-/vgplot-0.17.0.tgz", + "integrity": "sha512-t1VCiMncU2AFungkdR5xI8/rWbz4PkD88A62kUWjBXdecaWxm3hgeWGxdCmEhbjXHFs3119e7JM4AES1Nfap7g==", + "license": "BSD-3-Clause", + "dependencies": { + "@uwdata/mosaic-core": "^0.17.0", + "@uwdata/mosaic-inputs": "^0.17.0", + "@uwdata/mosaic-plot": "^0.17.0", + "@uwdata/mosaic-sql": "^0.17.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", @@ -2654,6 +2819,12 @@ ], "license": "MIT" }, + "node_modules/binary-search-bounds": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", + "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==", + "license": "MIT" + }, "node_modules/bl": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", @@ -4746,6 +4917,15 @@ "node": ">=12" } }, + "node_modules/interval-tree-1d": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/interval-tree-1d/-/interval-tree-1d-1.0.4.tgz", + "integrity": "sha512-wY8QJH+6wNI0uh4pDQzMvl+478Qh7Rl4qLmqiluxALlNvl+I+o5x38Pw3/z7mDPTPS1dQalZJXsmbvxx5gclhQ==", + "license": "MIT", + "dependencies": { + "binary-search-bounds": "^2.0.0" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -4847,6 +5027,12 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/isoformat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/isoformat/-/isoformat-0.2.1.tgz", + "integrity": "sha512-tFLRAygk9NqrRPhJSnNGh7g7oaVWDwR0wKh/GM2LgmPa50Eg4UfyaCO4I8k6EqJHl1/uh2RAD6g06n5ygEnrjQ==", + "license": "ISC" + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index e919885..4703a3f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,11 +13,16 @@ "@duckdb/duckdb-wasm": "^1.29.1-dev132.0", "@monaco-editor/react": "^4.7.0", "@motherduck/wasm-client": "^0.6.6", + "@observablehq/plot": "^0.6.17", "@radix-ui/react-slot": "^1.2.2", "@shadcn/ui": "^0.0.4", "@tailwindcss/vite": "^4.1.5", "@types/d3": "^7.4.3", "@types/papaparse": "^5.3.15", + "@uwdata/mosaic-core": "^0.17.0", + "@uwdata/mosaic-plot": "^0.17.0", + "@uwdata/mosaic-sql": "^0.17.0", + "@uwdata/vgplot": "^0.17.0", "apache-arrow": "^20.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/frontend/src/components/tabs/VisualizationTab.tsx b/frontend/src/components/tabs/VisualizationTab.tsx index 4b7f765..f0b2797 100644 --- a/frontend/src/components/tabs/VisualizationTab.tsx +++ b/frontend/src/components/tabs/VisualizationTab.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect } from "react"; import { BarChart4, LineChart, @@ -7,99 +7,34 @@ import { TrendingUp, ChevronLeft, ChevronRight, - ChevronDown, - Database, - FileText, Settings, Copy, ArrowRight, + Clock, + Rows, } from "lucide-react"; import { useAppStore } from "@/store/appStore"; +import { useDuckDBStore } from "@/store/duckDBStore"; import { selectHasFiles } from "@/store/selectors/appSelectors"; import { useChartsStore, ChartType } from "@/store/chartsStore"; import { Button } from "@/components/ui/Button"; +import DuckDBTableSelector from "./visualization/DuckDBTableSelector"; import ChartCanvas from "./visualization/ChartCanvas"; import ChartConfigPanel from "./visualization/ChartConfigPanel"; import ExportModal from "./visualization/ExportModal"; -import ChartGallery from "./visualization/ChartGallery"; - -interface DataSource { - type: "file"; - fileId: string; - fileName: string; - data: any[][]; - columns: string[]; - rowCount: number; +import { useMosaicQuery } from "@/hooks/chart/useMosaicQuery"; + +interface DuckDBTable { + name: string; + rowCount?: number; + isView: boolean; + source: "local" | "motherduck"; + database?: string; + schema?: { name: string; type: string }[]; } -/** - * Minimal compact dropdown for data source selection - */ -const DataSourceDropdown: React.FC<{ - selectedSource: DataSource | null; - onSourceChange: (source: DataSource) => void; -}> = ({ selectedSource, onSourceChange }) => { - const { files } = useAppStore(); - const [isOpen, setIsOpen] = useState(false); - - const dataSources = useMemo(() => { - return files - .filter((file) => file.data && file.data.length > 1) - .map((file) => ({ - type: "file" as const, - fileId: file.id, - fileName: file.fileName, - data: file.data, - columns: file.data[0], - rowCount: file.data.length - 1, - })); - }, [files]); - - return ( -
- - - {isOpen && ( -
- {dataSources.map((source) => ( - - ))} -
- )} -
- ); -}; - /** * Compact chart type selector */ @@ -141,86 +76,99 @@ const ChartTypeRow: React.FC = () => { }; /** - * Main visualization component - clean and minimal + * Performance indicators for query execution + */ +const PerformanceIndicators: React.FC<{ + executionTime?: number; + rowCount?: number; + isExecuting?: boolean; +}> = ({ executionTime, rowCount, isExecuting }) => { + if (!executionTime && !rowCount && !isExecuting) return null; + + return ( +
+ {isExecuting && ( +
+
+ Querying... +
+ )} + {executionTime && !isExecuting && ( +
+ + {executionTime}ms +
+ )} + {rowCount && !isExecuting && ( +
+ + {rowCount.toLocaleString()} rows +
+ )} +
+ ); +}; + +/** + * Main visualization component - updated to use DuckDB directly */ const VisualizationTab: React.FC = () => { const hasFiles = useAppStore(selectHasFiles); - const { setActiveTab, activeFileId, setActiveFile, files } = useAppStore(); + const { setActiveTab } = useAppStore(); + const { registeredTables } = useDuckDBStore(); const { currentChart, createNewChart, loadChartsFromStorage, toggleExportModal, + updateCurrentChart, } = useChartsStore(); - const [selectedDataSource, setSelectedDataSource] = - useState(null); - const [selectedColumns, setSelectedColumns] = useState([]); - const [filteredData, setFilteredData] = useState(null); + const [selectedTable, setSelectedTable] = useState(null); const [showLeftPanel, setShowLeftPanel] = useState(true); + const [queryResult, setQueryResult] = useState(null); + + const { isExecuting, progress, lastResult } = useMosaicQuery(); // Load saved charts on mount useEffect(() => { loadChartsFromStorage(); }, [loadChartsFromStorage]); - // Auto-select first file + // Auto-select first table if available useEffect(() => { - if (!selectedDataSource && files.length > 0) { - const firstFileWithData = files.find( - (file) => file.id === activeFileId - ); - if (firstFileWithData) { - const source: DataSource = { - type: "file", - fileId: firstFileWithData.id, - fileName: firstFileWithData.fileName, - data: firstFileWithData.data, - columns: firstFileWithData.data[0], - rowCount: firstFileWithData.data.length - 1, - }; - setSelectedDataSource(source); - setSelectedColumns(source.columns.slice(0, 2)); - } + if (!selectedTable && registeredTables.size > 0) { + const firstTable = Array.from(registeredTables.keys())[0]; + setSelectedTable({ + name: firstTable, + isView: false, + source: "local", + }); } - }, [files, selectedDataSource]); + }, [registeredTables, selectedTable]); - // Process data when source/columns change + // Update chart data when query result changes useEffect(() => { - if (selectedDataSource?.data && selectedColumns.length > 0) { - const headers = selectedDataSource.data[0]; - const rows = selectedDataSource.data.slice(1); - - const columnIndices = selectedColumns - .map((col) => headers.indexOf(col)) - .filter((idx) => idx !== -1); - - const formattedData = rows.map((row) => { - const obj: Record = {}; - selectedColumns.forEach((column, index) => { - const colIndex = columnIndices[index]; - if (colIndex !== -1) { - const value = row[colIndex]; - obj[column] = isNaN(Number(value)) ? value : Number(value); - } + if (lastResult && lastResult.data.length > 0) { + if (!currentChart) { + createNewChart("bar", lastResult.data, lastResult.query); + } else { + updateCurrentChart({ + data: lastResult.data, + originalData: [...lastResult.data], }); - return obj; - }); - - setFilteredData(formattedData); - - if (formattedData.length > 0 && !currentChart) { - createNewChart("bar", formattedData); } - } else { - setFilteredData(null); + setQueryResult(lastResult); } - }, [selectedDataSource, selectedColumns, createNewChart, currentChart]); + }, [lastResult, currentChart, createNewChart, updateCurrentChart]); + + const hasVisualizationData = currentChart?.data && currentChart.data.length > 0; - const hasVisualizationData = filteredData && filteredData.length > 0; + // Show progress bar for long queries + const showProgress = isExecuting && progress > 0; - // No files state - if (!hasFiles) { + // No tables state + if (registeredTables.size === 0) { return (
@@ -242,37 +190,15 @@ const VisualizationTab: React.FC = () => { return (
- {/* CSS for chart styling */} -