diff --git a/README.md b/README.md index 102e366..80634b4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,28 @@ -# Tauri + React + Typescript +# Local Data Visualization Tool + +I am working on a MacOS desktop application for data cleaning and analytics. This data tool allows users to go from raw data to insights faster. This application is for anyone that builds decks for clients and would like better visualizations. + + +## Essential Visualizations + - **Bar & Column Charts:** Vertical and Horizontal, with support for stacking (stacked or 100% stacked_ and grouping. + - **Line & Area Charts:** Used for time-series and trend analysis. + - **Scatter Plots:** Used when showing relationships and distributions between two numeric variables. + - **Histograms:** For frequency distribution analysis. + - **Tables & Pivot Tables:** High-Performance grids for raw data exploration. + +2. Advanced & "Infinite Canvas" Visuals +- **Metric Trees / Maps:** Visualizing KPIs in a hierarchical flow (e.g., Revenue -> Orders -> AOV). +- **Funnel Charts:** Specifically designed for marketing and product onboarding flows (e.g., Visit -> Signup -> Purchase). +- **Heat Maps:** For matrix-style data density visualization. +- **Big Number (KPI) Cards:** Large, scaled-up numbers for top-level metrics. +- **Sankey / Flow Diagrams:** Showing the movement of data or users between states. + +3. Specialized Analytical Templates +- **Customer Journey Maps:** Combining charts with sticky notes and flow arrows. +- **A/B Test Visuals:** Specialized views to compare control vs. variant groups. +- **GA4 / Website Analytics Maps:** Pre-built flows for web traffic. +- **OKRs & Alignment Canvases:** Mixing data with strategic planning shapes. + -This template should help get you started developing with Tauri, React and Typescript in Vite. -## Recommended IDE Setup -- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) diff --git a/bun.lock b/bun.lock index 0d2c223..98686f5 100644 --- a/bun.lock +++ b/bun.lock @@ -5,13 +5,16 @@ "": { "name": "local_data", "dependencies": { + "@monaco-editor/react": "^4.7.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", "@xyflow/react": "^12.10.0", "clsx": "^2.1.1", "lucide-react": "^0.562.0", + "monaco-editor": "^0.55.1", "react": "^19.1.0", "react-dom": "^19.1.0", + "recharts": "^3.7.0", "tailwind-merge": "^3.4.0", "zustand": "^5.0.10", }, @@ -129,6 +132,12 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], + + "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], + + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.2", "", { "os": "android", "cpu": "arm" }, "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA=="], @@ -181,6 +190,10 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.2", "", { "os": "win32", "cpu": "x64" }, "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@tauri-apps/api": ["@tauri-apps/api@2.9.1", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="], "@tauri-apps/cli": ["@tauri-apps/cli@2.9.6", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.6", "@tauri-apps/cli-darwin-x64": "2.9.6", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", "@tauri-apps/cli-linux-arm64-musl": "2.9.6", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-musl": "2.9.6", "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", "@tauri-apps/cli-win32-x64-msvc": "2.9.6" }, "bin": { "tauri": "tauri.js" } }, "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw=="], @@ -217,14 +230,28 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], @@ -235,6 +262,10 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], "@xyflow/react": ["@xyflow/react@12.10.0", "", { "dependencies": { "@xyflow/system": "0.0.74", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw=="], @@ -257,6 +288,8 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], @@ -265,10 +298,22 @@ "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], @@ -277,12 +322,20 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + + "dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], + "es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="], + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], @@ -291,6 +344,10 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -301,6 +358,10 @@ "lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], + "marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + + "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -319,8 +380,20 @@ "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], + "react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="], + + "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + "recharts": ["recharts@3.7.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew=="], + + "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + + "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + "rollup": ["rollup@4.55.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.2", "@rollup/rollup-android-arm64": "4.55.2", "@rollup/rollup-darwin-arm64": "4.55.2", "@rollup/rollup-darwin-x64": "4.55.2", "@rollup/rollup-freebsd-arm64": "4.55.2", "@rollup/rollup-freebsd-x64": "4.55.2", "@rollup/rollup-linux-arm-gnueabihf": "4.55.2", "@rollup/rollup-linux-arm-musleabihf": "4.55.2", "@rollup/rollup-linux-arm64-gnu": "4.55.2", "@rollup/rollup-linux-arm64-musl": "4.55.2", "@rollup/rollup-linux-loong64-gnu": "4.55.2", "@rollup/rollup-linux-loong64-musl": "4.55.2", "@rollup/rollup-linux-ppc64-gnu": "4.55.2", "@rollup/rollup-linux-ppc64-musl": "4.55.2", "@rollup/rollup-linux-riscv64-gnu": "4.55.2", "@rollup/rollup-linux-riscv64-musl": "4.55.2", "@rollup/rollup-linux-s390x-gnu": "4.55.2", "@rollup/rollup-linux-x64-gnu": "4.55.2", "@rollup/rollup-linux-x64-musl": "4.55.2", "@rollup/rollup-openbsd-x64": "4.55.2", "@rollup/rollup-openharmony-arm64": "4.55.2", "@rollup/rollup-win32-arm64-msvc": "4.55.2", "@rollup/rollup-win32-ia32-msvc": "4.55.2", "@rollup/rollup-win32-x64-gnu": "4.55.2", "@rollup/rollup-win32-x64-msvc": "4.55.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -329,10 +402,14 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], @@ -341,12 +418,16 @@ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "zustand": ["zustand@5.0.10", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg=="], + "@reduxjs/toolkit/immer": ["immer@11.1.3", "", {}, "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q=="], + "@xyflow/react/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], } } diff --git a/package.json b/package.json index d581e49..b85f441 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,16 @@ "tauri": "tauri" }, "dependencies": { + "@monaco-editor/react": "^4.7.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", "@xyflow/react": "^12.10.0", "clsx": "^2.1.1", "lucide-react": "^0.562.0", + "monaco-editor": "^0.55.1", "react": "^19.1.0", "react-dom": "^19.1.0", + "recharts": "^3.7.0", "tailwind-merge": "^3.4.0", "zustand": "^5.0.10" }, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a454ab4..2e3aa72 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,24 +1,33 @@ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ // -use duckdb::Connection; -use serde::{Deserialize, Serialize}; +use duckdb::{Connection, types::Value}; +use serde::Serialize; +use serde_json::json; // This is the data structure we send back to React. #[derive(Serialize)] - pub struct TableSchema { columns: Vec, column_types: Vec, row_count_estimate: usize, } +// Result structure for SQL query execution +#[derive(Serialize)] +pub struct QueryResult { + columns: Vec, + column_types: Vec, + rows: Vec>, + row_count: usize, +} + #[tauri::command] async fn get_csv_schema(path: String) -> Result { // 1. Open a temporary in-memory DuckDB connection let conn = Connection::open_in_memory().map_err(|e| e.to_string())?; - let query = format!("DESCRIBE SELECT * FROM read_csv_auto('{}' LIMIT 1", path); + let query = format!("DESCRIBE (SELECT * FROM read_csv_auto('{}'));", path); let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?; @@ -51,7 +60,7 @@ async fn get_csv_schema(path: String) -> Result { pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) - .invoke_handler(tauri::generate_handler![get_csv_schema]) // Register here! + .invoke_handler(tauri::generate_handler![get_csv_schema, execute_sql]) // Register here! .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/App.css b/src/App.css index f57cebb..1b66de8 100644 --- a/src/App.css +++ b/src/App.css @@ -4,7 +4,6 @@ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - color-scheme: light; } body, html, #root { @@ -13,6 +12,7 @@ body, html, #root { width: 100vw; height: 100vh; overflow: hidden; /* This is crucial for the "Desktop" feel */ + background-color: #fff; } diff --git a/src/App.tsx b/src/App.tsx index fb1abe0..3751b2a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { ReactFlow, Background, @@ -7,9 +7,19 @@ import { addEdge, Connection, Edge, + BackgroundVariant, + Controls, } from '@xyflow/react'; -import { listen } from '@tauri-apps/api/event'; // Tauri's event listener +import { listen } from '@tauri-apps/api/event'; import '@xyflow/react/dist/style.css'; +import { invoke } from '@tauri-apps/api/core'; +import SqlNode from './components/SqlNode'; +import ChartNode from './components/ChartNode'; + +const nodeTypes = { + sqlNode: SqlNode, + chartNode: ChartNode, +}; const initialNodes: any[] = []; const initialEdges: Edge[] = []; @@ -18,69 +28,275 @@ export default function App() { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - // 1. Function to create a node when a file is dropped - const addFileNode = (filePath: string, x: number, y: number) => { - const fileName = filePath.split('/').pop(); // Get 'data.csv' from the full path - + // Callback for SQL nodes to update their results in the node data + const updateNodeData = useCallback((nodeId: string, newData: Record) => { + setNodes((nds) => + nds.map((node) => { + if (node.id === nodeId) { + return { ...node, data: { ...node.data, ...newData } }; + } + return node; + }) + ); + }, [setNodes]); + + const addSqlNode = () => { const newNode = { - id: `node-${Date.now()}`, - type: 'default', - position: { x, y }, - data: { label: `📄 ${fileName}` }, - style: { - background: '#1e293b', - color: '#fff', - border: '1px solid #64748b', - padding: '10px', - borderRadius: '8px' - }, + id: `sql-${Date.now()}`, + type: 'sqlNode', + position: { x: 500, y: 300 }, + data: { label: 'SQL Query', updateNodeData }, }; + setNodes((nds) => nds.concat(newNode)); + }; + const addChartNode = () => { + const newNode = { + id: `chart-${Date.now()}`, + type: 'chartNode', + position: { x: 800, y: 300 }, + data: {}, + }; setNodes((nds) => nds.concat(newNode)); }; - // 2. Listen for Native macOS File Drops - useEffect(() => { - // This listens to the "tauri://drag-drop" event emitted by the OS - const unlisten = listen<{ paths: string[], x: number, y: number }>('tauri://drag-drop', (event) => { - const { paths, x, y } = event.payload; + const addFileNode = async (filePath: string, x: number, y: number) => { + try { + const schema = await invoke<{ columns: string[], column_types: string[] }>( + 'get_csv_schema', + { path: filePath } + ); + + const fileName = filePath.split('/').pop(); - paths.forEach((path) => { - // Only accept data files for now - if (path.endsWith('.csv') || path.endsWith('.parquet') || path.endsWith('.json')) { - addFileNode(path, x, y); - console.log("File dropped at path:", path); - } + const newNode = { + id: `node-${Date.now()}`, + type: 'default', + position: { x, y }, + data: { + path: filePath, + label: ( +
+
📄 {fileName}
+
+ {schema.columns.map((col, i) => ( +
+ {col} + {schema.column_types[i]} +
+ ))} +
+
+ ) + }, + style: { + background: '#fff', + color: '#1e293b', + border: '1px solid #cbd5e1', + padding: '12px', + borderRadius: '8px', + width: 250, + boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1)" // Soft Shadow + }, + }; + + setNodes((nds) => nds.concat(newNode)); + } catch (err) { + console.error("Failed to read CSV schema:", err); + } + }; + + useEffect(() => { + const listenToDrops = async () => { + const unlisten = await listen<{ paths: string[], x: number, y: number }>('tauri://drag-drop', (event) => { + const { paths, x, y } = event.payload; + paths.forEach((path) => { + if (path.endsWith('.csv') || path.endsWith('.parquet') || path.endsWith('.json')) { + addFileNode(path, x, y); + } + }); }); - }); + return unlisten; + }; + + const unlistenPromise = listenToDrops(); return () => { - unlisten.then((f) => f()); + unlistenPromise.then((f) => f()); }; }, []); + // Propagate query results from SQL nodes to connected chart nodes + useEffect(() => { + const chartNodes = nodes.filter((n) => n.type === 'chartNode' && n.data.sourceNodeId); + + chartNodes.forEach((chartNode) => { + const sourceNode = nodes.find((n) => n.id === chartNode.data.sourceNodeId); + if (sourceNode && sourceNode.data.queryResults) { + // Only update if results have changed + if (JSON.stringify(chartNode.data.queryResults) !== JSON.stringify(sourceNode.data.queryResults)) { + setNodes((nds) => + nds.map((node) => { + if (node.id === chartNode.id) { + return { + ...node, + data: { + ...node.data, + queryResults: sourceNode.data.queryResults, + }, + }; + } + return node; + }) + ); + } + } + }); + }, [nodes, setNodes]); + const onConnect = useCallback( - (params: Connection) => setEdges((eds) => addEdge(params, eds)), - [setEdges] + (params: Connection) => { + setEdges((eds) => addEdge(params, eds)); + + const sourceNode = nodes.find((n) => n.id === params.source); + const targetNode = nodes.find((n) => n.id === params.target); + + // File -> SQL Node connection + if (sourceNode && targetNode && targetNode.type === 'sqlNode' && sourceNode.data.path) { + const filePath = sourceNode.data.path; + const fileName = filePath.split('/').pop().replace(/\.[^/.]+$/, ""); + + setNodes((nds) => + nds.map((node) => { + if (node.id === params.target) { + return { + ...node, + data: { + ...node.data, + inputPath: filePath, + tableName: fileName, + }, + }; + } + return node; + }) + ); + console.log(`Linked ${fileName} to SQL Node ${params.target}`); + } + + // SQL Node -> Chart Node connection + if (sourceNode && targetNode && sourceNode.type === 'sqlNode' && targetNode.type === 'chartNode') { + // Pass query results from SQL node to Chart node + const queryResults = sourceNode.data.queryResults; + if (queryResults) { + setNodes((nds) => + nds.map((node) => { + if (node.id === params.target) { + return { + ...node, + data: { + ...node.data, + queryResults, + sourceNodeId: params.source, + }, + }; + } + return node; + }) + ); + } + // Store the connection so we can update chart when SQL results change + setNodes((nds) => + nds.map((node) => { + if (node.id === params.target) { + return { + ...node, + data: { + ...node.data, + sourceNodeId: params.source, + }, + }; + } + return node; + }) + ); + console.log(`Linked SQL Node ${params.source} to Chart Node ${params.target}`); + } + }, + [nodes, setNodes, setEdges] ); return ( -
+
+
+ + +
- + + -
- Status: Listening for CSV drops... -
+ +
+ ); +} + +function StatusBar({ nodes }: { nodes: any[] }) { + const status = useMemo(() => { + const fileNodes = nodes.filter((n) => n.type === 'default'); + const sqlNodes = nodes.filter((n) => n.type === 'sqlNode'); + const chartNodes = nodes.filter((n) => n.type === 'chartNode'); + const sqlWithResults = sqlNodes.filter((n) => n.data.queryResults); + + if (nodes.length === 0) { + return { text: 'Drop a CSV file to get started', color: 'bg-slate-600' }; + } + + if (fileNodes.length > 0 && sqlNodes.length === 0) { + return { text: `${fileNodes.length} file(s) loaded - Add a SQL node to query`, color: 'bg-blue-600' }; + } + + if (sqlNodes.length > 0 && sqlWithResults.length === 0) { + return { text: `${sqlNodes.length} SQL node(s) - Connect a file and run a query`, color: 'bg-amber-600' }; + } + + const parts: string[] = []; + if (fileNodes.length > 0) parts.push(`${fileNodes.length} file(s)`); + if (sqlWithResults.length > 0) parts.push(`${sqlWithResults.length} query result(s)`); + if (chartNodes.length > 0) parts.push(`${chartNodes.length} chart(s)`); + + return { text: parts.join(' | '), color: 'bg-green-600' }; + }, [nodes]); + + return ( +
+ + {status.text}
); } diff --git a/src/components/ChartNode.tsx b/src/components/ChartNode.tsx new file mode 100644 index 0000000..a7fbba6 --- /dev/null +++ b/src/components/ChartNode.tsx @@ -0,0 +1,221 @@ +import { useState, useEffect } from 'react'; +import { Handle, Position } from '@xyflow/react'; +import { + BarChart, + Bar, + LineChart, + Line, + AreaChart, + Area, + PieChart, + Pie, + ScatterChart, + Scatter, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell, +} from 'recharts'; + +type ChartType = 'bar' | 'line' | 'area' | 'pie' | 'scatter'; + +interface ChartNodeData { + queryResults?: { + columns: string[]; + rows: any[][]; + row_count: number; + }; +} + +const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16']; + +export default function ChartNode({ data }: { data: ChartNodeData }) { + const [chartType, setChartType] = useState('bar'); + const [xColumn, setXColumn] = useState(''); + const [yColumn, setYColumn] = useState(''); + + const { queryResults } = data; + const columns = queryResults?.columns || []; + + // Auto-select columns when data arrives + useEffect(() => { + if (columns.length > 0 && !xColumn) { + setXColumn(columns[0]); + if (columns.length > 1) { + setYColumn(columns[1]); + } + } + }, [columns, xColumn]); + + // Transform row data to recharts format + const chartData = queryResults?.rows.map((row) => { + const obj: Record = {}; + columns.forEach((col, i) => { + obj[col] = row[i]; + }); + return obj; + }) || []; + + const hasData = chartData.length > 0 && xColumn && yColumn; + + const renderChart = () => { + if (!hasData) { + return ( +
+ {!queryResults ? 'Connect a SQL node with results' : 'Select X and Y columns'} +
+ ); + } + + const commonProps = { + data: chartData, + margin: { top: 10, right: 10, left: 0, bottom: 0 }, + }; + + switch (chartType) { + case 'bar': + return ( + + + + + + + + + + ); + + case 'line': + return ( + + + + + + + + + + ); + + case 'area': + return ( + + + + + + + + + + ); + + case 'pie': + return ( + + + name} + labelLine={{ stroke: '#94a3b8' }} + fontSize={9} + > + {chartData.map((_, index) => ( + + ))} + + + + + ); + + case 'scatter': + return ( + + + + + + + + + + ); + + default: + return null; + } + }; + + return ( +
+ + + {/* Header */} +
+ Chart +
+ {(['bar', 'line', 'area', 'pie', 'scatter'] as ChartType[]).map((type) => ( + + ))} +
+
+ + {/* Column selectors */} + {columns.length > 0 && ( +
+
+ + +
+
+ + +
+
+ )} + + {/* Chart area */} +
+ {renderChart()} +
+ + +
+ ); +} diff --git a/src/components/SqlNode.tsx b/src/components/SqlNode.tsx new file mode 100644 index 0000000..954fdeb --- /dev/null +++ b/src/components/SqlNode.tsx @@ -0,0 +1,150 @@ +import { useState } from 'react'; +import { Handle, Position } from '@xyflow/react'; +import Editor from '@monaco-editor/react'; +import { invoke } from '@tauri-apps/api/core'; + +interface QueryResult { + columns: string[]; + column_types: string[]; + rows: any[][]; + row_count: number; +} + +interface SqlNodeData { + inputPath?: string; + tableName?: string; + updateNodeData?: (nodeId: string, data: Record) => void; +} + +export default function SqlNode({ data, id }: { data: SqlNodeData; id: string }) { + const [code, setCode] = useState("SELECT * FROM input LIMIT 10"); + const [results, setResults] = useState(null); + const [error, setError] = useState(null); + const [isRunning, setIsRunning] = useState(false); + + const handleRun = async () => { + if (!data.inputPath || !data.tableName) { + setError("Connect a data file to this node first"); + return; + } + + setIsRunning(true); + setError(null); + + try { + // Replace "input" in query with the actual table name + const processedQuery = code.replace(/\binput\b/gi, data.tableName); + + const result = await invoke('execute_sql', { + query: processedQuery, + filePath: data.inputPath, + tableName: data.tableName, + }); + + setResults(result); + + // Update node data so results can flow to connected chart nodes + if (data.updateNodeData) { + data.updateNodeData(id, { queryResults: result }); + } + } catch (err) { + setError(err as string); + setResults(null); + } finally { + setIsRunning(false); + } + }; + + const hasResults = results && results.rows.length > 0; + const nodeHeight = hasResults ? 'h-[500px]' : 'h-[300px]'; + + return ( +
+ + +
+
+ SQL Transform + {data.tableName && ( + + {data.tableName} + + )} +
+ +
+ +
+ setCode(value || '')} + options={{ + minimap: { enabled: false }, + automaticLayout: true, + scrollBeyondLastLine: false, + fontSize: 12, + lineNumbers: 'off', + }} + /> +
+ + {/* Results / Error area */} +
+ {error && ( +
+ {error} +
+ )} + + {hasResults && ( +
+ + + + {results.columns.map((col, i) => ( + + ))} + + + + {results.rows.map((row, rowIdx) => ( + + {row.map((cell, cellIdx) => ( + + ))} + + ))} + +
+ {col} +
+ {cell === null ? null : String(cell)} +
+
+ {results.row_count} row{results.row_count !== 1 ? 's' : ''} +
+
+ )} + + {!error && !hasResults && !isRunning && ( +
+ {data.inputPath ? 'Click RUN to execute query' : 'Connect a data file to get started'} +
+ )} +
+ + +
+ ); +} diff --git a/src/main.tsx b/src/main.tsx index 2be325e..367e7c5 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,11 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; +import { loader } from '@monaco-editor/react'; +import * as monaco from 'monaco-editor'; + +// Configure Monaco to use the local bundle instead of CDN +loader.config({ monaco }); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(