├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── app ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── components.json ├── components ├── AddNode.tsx ├── SDA │ ├── AddColumn.tsx │ ├── CleanColumnNames.tsx │ ├── Describe.tsx │ ├── FetchData.tsx │ ├── FetchGeoData.tsx │ ├── Filter.tsx │ ├── Join.tsx │ ├── JoinGeo.tsx │ ├── LinearRegressions.tsx │ ├── LoadFile.tsx │ ├── LoadGeoFile.tsx │ ├── LogTable.tsx │ ├── Longer.tsx │ ├── Note.tsx │ ├── Points.tsx │ ├── ProportionsVertical.tsx │ ├── RemoveDuplicates.tsx │ ├── RenameColumns.tsx │ ├── SDB.tsx │ ├── ST.tsx │ ├── SelectColumns.tsx │ ├── Sort.tsx │ ├── Summarize.tsx │ ├── UpdateColumn.tsx │ └── Wider.tsx ├── SimpleFlow.tsx ├── helpers │ └── getNewState.tsx ├── initialState.tsx ├── nodeTypes.tsx ├── partials │ ├── AllCode.tsx │ ├── CardTitleWithLoader.tsx │ ├── Code.tsx │ ├── CodeIcon.tsx │ ├── CopyIcon.tsx │ ├── DataTable.tsx │ ├── Error.tsx │ ├── GithubLogo.tsx │ ├── MenuBar.tsx │ ├── Options.tsx │ ├── OptionsCheckbox.tsx │ ├── OptionsInputNumber.tsx │ ├── OptionsInputText.tsx │ ├── OptionsItem.tsx │ ├── OptionsMultipleCheckBoxes.tsx │ ├── OptionsMultipleInputText.tsx │ ├── OptionsSelect.tsx │ ├── OptionsTextArea.tsx │ ├── Source.tsx │ ├── Spinner.tsx │ └── Target.tsx └── ui │ ├── accordion.tsx │ ├── button.tsx │ ├── card.tsx │ ├── checkbox.tsx │ ├── context-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── menubar.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── table.tsx │ └── textarea.tsx ├── docs ├── .nojekyll ├── 404.html ├── _next │ └── static │ │ ├── V2RuNXPZAMrBd2bvUYRrN │ │ ├── _buildManifest.js │ │ └── _ssgManifest.js │ │ ├── chunks │ │ ├── 0295e583.ba25b0b765c49573.js │ │ ├── 0457c758.f23db0228cf5fe9e.js │ │ ├── 1004.d3fd77d01a796acd.js │ │ ├── 1012.b337c54ec36393c3.js │ │ ├── 1051.f17985eed6b22f78.js │ │ ├── 110.ceb5c300bd6c10de.js │ │ ├── 1115.9bc958c1247198c3.js │ │ ├── 1124.4417b964d3c688cb.js │ │ ├── 1226.0165a8e6c6f82322.js │ │ ├── 1325.5b627c16b0e9f4cc.js │ │ ├── 1340.283bed25b4458394.js │ │ ├── 1421.2591174390815395.js │ │ ├── 1454.447e350336481a6e.js │ │ ├── 147.3b7262a10d72f5da.js │ │ ├── 1629.f84fec160887f531.js │ │ ├── 1686.9189a5bf49ca983a.js │ │ ├── 1796.2ef7f849514bf425.js │ │ ├── 1839.c7ea9bda727b7d9b.js │ │ ├── 1849.3d091bf586de9db2.js │ │ ├── 1862.feb3ce30e2f4ce78.js │ │ ├── 192.da3a15a265c4cdc6.js │ │ ├── 1986.a2a6c76c5d05b211.js │ │ ├── 200.80bdb17c532b1e7a.js │ │ ├── 2004.4a3aa353a6faa009.js │ │ ├── 2043.2951f898f4485cf0.js │ │ ├── 2057.d7a7e03d3ff03a0f.js │ │ ├── 2129.70927ee0e26680ff.js │ │ ├── 21ca6e34.0bd58711a3e1a16c.js │ │ ├── 222.c87b51a3f70b4d54.js │ │ ├── 2228.7b56d8af029d5fdd.js │ │ ├── 2242.8078e2cb74f41319.js │ │ ├── 229.1519f3f744ea4205.js │ │ ├── 2296.96bbf247a9965253.js │ │ ├── 2337.e188060cc404c69d.js │ │ ├── 234.7bb43f384eb47bf9.js │ │ ├── 2360.6673f5c2fb28035e.js │ │ ├── 2428.6f66e6476dd38a39.js │ │ ├── 2445.bc9cf7ed67d4a7ed.js │ │ ├── 2521.2b7ddf215077bacf.js │ │ ├── 2537.648aa094e7f9ff51.js │ │ ├── 255.27aa620996ff291c.js │ │ ├── 258.e88926071247fd57.js │ │ ├── 267.62aee69d9a6c1392.js │ │ ├── 2686.6c087021db38bf53.js │ │ ├── 2716.b5160ab1ada940a5.js │ │ ├── 2843.9776b5738b4c24c1.js │ │ ├── 2866.5592e5e634eb27b9.js │ │ ├── 2873.36c5eed147ee5078.js │ │ ├── 2908.ac18a2dc91bd3a1b.js │ │ ├── 3022.208d7add57fc62de.js │ │ ├── 3049.8542b1ff7293500d.js │ │ ├── 3053.223f14cd00e3a2ea.js │ │ ├── 3068.402b01c166387575.js │ │ ├── 3075.138aadfa47c6fb75.js │ │ ├── 3078.9220bdab578f1a9b.js │ │ ├── 3108.d920ef87673c1e4c.js │ │ ├── 3160.909eefc109dc1d89.js │ │ ├── 3181.76c7a2fc01848ca1.js │ │ ├── 3186.9c0be2c0ce820d53.js │ │ ├── 3189.ada87aef700e0eea.js │ │ ├── 3197.a79af7dab75278e4.js │ │ ├── 3349.2419556212e41871.js │ │ ├── 3360.0c30f3d9da0443a8.js │ │ ├── 3375.41342a7cc0b85e1d.js │ │ ├── 33f1252a.84419672a6828629.js │ │ ├── 3421.e6781b80d26a985d.js │ │ ├── 3522.3dc8f43f8ee30dbb.js │ │ ├── 3527.ee780e6c6d87831e.js │ │ ├── 3553.9df62252f54b2208.js │ │ ├── 3606.a9f1f6c0e6817347.js │ │ ├── 3609.a7e4af14d0ce7b97.js │ │ ├── 3693.c932ded9ad9aaf12.js │ │ ├── 3731.8a58a9e3965cc7db.js │ │ ├── 3862.65f214e8e1d22ea3.js │ │ ├── 3864.43f3553354f00572.js │ │ ├── 3937.c997ac9a72ccc098.js │ │ ├── 3b7e0f76.f65613a0eab2174b.js │ │ ├── 4001.78ea3954d8053ed4.js │ │ ├── 4017.aa8187418a2ebb7f.js │ │ ├── 4022.987edf4a35c7ce1a.js │ │ ├── 403.077cd420a60e5a86.js │ │ ├── 4054-ecbc552e055f18e3.js │ │ ├── 4059.dc4d54a9d583b312.js │ │ ├── 411.deee02058499f43e.js │ │ ├── 4121.ae3dbc613fe4774c.js │ │ ├── 417.a94f3d46c6f6081a.js │ │ ├── 4178.47e5637b27d22b6b.js │ │ ├── 4192.85b3d3077b21aa6e.js │ │ ├── 4220.18d980a6f0c4e8c5.js │ │ ├── 4222.c4eb1d401f25e63f.js │ │ ├── 423.acc208af4b8b7234.js │ │ ├── 4242.59d12e5ce84f98e8.js │ │ ├── 4316.b46df82ca817589e.js │ │ ├── 4349.97a14eb868ff3004.js │ │ ├── 4395.445af27948f10068.js │ │ ├── 4403.283af96390c34fb2.js │ │ ├── 443.ffdc7d57d47ba307.js │ │ ├── 4495.d79063ba44ea6326.js │ │ ├── 4519.f8845315ef7fe03a.js │ │ ├── 456.536b9928d7104113.js │ │ ├── 4562.db2e363ed9a2d0ac.js │ │ ├── 4594.9c73b08bb73dc676.js │ │ ├── 4615.576f6d531a96c6de.js │ │ ├── 4624.48e767266012b9cb.js │ │ ├── 4630.65e8399ad24f8db3.js │ │ ├── 4657.22e2028477a1db14.js │ │ ├── 4678.e3eae34ac92c695c.js │ │ ├── 4729.ebd8df84187d05e7.js │ │ ├── 4738.781da85ec308045e.js │ │ ├── 474.216c15ffda3f7ad2.js │ │ ├── 4760.4354237c4613727d.js │ │ ├── 4766.5e8b312204321b8a.js │ │ ├── 4777.7af40330304acd2e.js │ │ ├── 4876.e383c1ffd735b4ba.js │ │ ├── 4901.96226f99730fa185.js │ │ ├── 4910.3c1993cf0ca68f4d.js │ │ ├── 4f541c7f.f1de305e1b504cf3.js │ │ ├── 5043.4901c070bd02b57a.js │ │ ├── 5065.02cae544c6b61b46.js │ │ ├── 5236.0382fb240f594ba2.js │ │ ├── 5351.03fd9f8c838d59c1.js │ │ ├── 5355.dd3ab8dba73c7ba9.js │ │ ├── 5360.ed878c87bbd801ab.js │ │ ├── 5370.599e9f783de4d597.js │ │ ├── 5425.03782d42a713cd25.js │ │ ├── 5459.6c1e48eabc6e797c.js │ │ ├── 5463.2aa0cee8e91677fa.js │ │ ├── 5527.aa12bee457976515.js │ │ ├── 5575.7f204b44e37feb27.js │ │ ├── 55826268.5ebad7605bcc8cd7.js │ │ ├── 559.9a4728d8b965bcbb.js │ │ ├── 55d07805.f7a53d3b8f3104e7.js │ │ ├── 5612.a1772f337e438531.js │ │ ├── 5789.bd2f1e5f4c6d0a9e.js │ │ ├── 581.531a8e0f66e2beb7.js │ │ ├── 5828.ba14a56b9862a370.js │ │ ├── 5829.a562e51da0453244.js │ │ ├── 5845.6dfa395f49b0d1cc.js │ │ ├── 5898.82c77bed98f0e054.js │ │ ├── 5975.bde89f4fe4001e98.js │ │ ├── 5990.8d1438d0ff4d7040.js │ │ ├── 5991.670d9fbb66a53969.js │ │ ├── 5995.1fefd9db51144c58.js │ │ ├── 601.b08bf8192ef6285d.js │ │ ├── 6012.1319a46b6943c91c.js │ │ ├── 6032.493309ea66c1c116.js │ │ ├── 6038.a9bd439b148eb2a8.js │ │ ├── 6043.e3da222f0bdb6485.js │ │ ├── 6170.d7504018438a43ed.js │ │ ├── 6281.6ac11298597f025d.js │ │ ├── 6320.3d176c75ac7b291e.js │ │ ├── 6350.b2d721fbd7c9c30c.js │ │ ├── 6351.0fa7bf5d6144da83.js │ │ ├── 6420.ec126e46c8d2bbf1.js │ │ ├── 6484.37ddbfc561fdc997.js │ │ ├── 6523.eae1b04a1536be1a.js │ │ ├── 6562.25f07034ccc886c4.js │ │ ├── 6587.95d3ae8fa3d50cd3.js │ │ ├── 6607.ac4d6c34f6994a60.js │ │ ├── 6710.4116144ca09676d4.js │ │ ├── 6722.1147b918f8f769ba.js │ │ ├── 6741.280a68918381aaaf.js │ │ ├── 6768.97bc738aed34ea5d.js │ │ ├── 6770.7bd173bc9bd4b3d8.js │ │ ├── 6814.c871ec2910b34535.js │ │ ├── 6863.be5983a142fd99e3.js │ │ ├── 6957.78ebda0f10c03a68.js │ │ ├── 6974.6a443a30c83721d7.js │ │ ├── 7023-57f9c7525478086f.js │ │ ├── 7028.342b7f9e0d9beccf.js │ │ ├── 7032.f5707e43b6a280fb.js │ │ ├── 7042.3451eb2f7a071a61.js │ │ ├── 7048.377770ce8fd15857.js │ │ ├── 7091.ce7fab6bc551dc7d.js │ │ ├── 7101.c2130c983aa77e34.js │ │ ├── 7119.954b320eb2a7c0a5.js │ │ ├── 7139.793c97bfbe6b135e.js │ │ ├── 7144.78331ab1a7ffdf65.js │ │ ├── 7168.e5f5faee7d02432f.js │ │ ├── 7217.57beadb92b8188a8.js │ │ ├── 7238.d404381f70dce37e.js │ │ ├── 7249.a7fd1d99ae3da6dd.js │ │ ├── 7264.07573063a79ee2c3.js │ │ ├── 7304.a7628ddd3cae3c15.js │ │ ├── 7375.aca3955cfc57561b.js │ │ ├── 7410.4a1451f181aa0fde.js │ │ ├── 7475.85610087b0b53bc1.js │ │ ├── 7480.b5f0cc2b362d0753.js │ │ ├── 7498.a089ea3f778b6aac.js │ │ ├── 7501.e39df9319b6226fc.js │ │ ├── 7589.4afbc7d3c1fd737e.js │ │ ├── 7607.e4bf0ba03b43d8e1.js │ │ ├── 768.f383a234e1f25445.js │ │ ├── 76db47ee.c69cb785aa8732e4.js │ │ ├── 7700.82087bc67bf7afa6.js │ │ ├── 7710.cb0ac85ac55d2bf5.js │ │ ├── 7825.77ad8304d0d51d08.js │ │ ├── 7831.0ae6464f85a139cf.js │ │ ├── 7837.d43802d9d7df7a98.js │ │ ├── 7862.02457c1bccebc18c.js │ │ ├── 7878.2fcdd886b86b7382.js │ │ ├── 7915.e4f71fb9615d8953.js │ │ ├── 7fcf5a4a.339c13e46056bd0a.js │ │ ├── 8030.4aab3f65ac3dc159.js │ │ ├── 8060.931ca94e8e30b463.js │ │ ├── 8093.c89f62b0bc5fa24d.js │ │ ├── 8110.d4e6bb65608acb9e.js │ │ ├── 8217.283e589646bab0fe.js │ │ ├── 8248.2309e94e4b9a6413.js │ │ ├── 8252.5c0f94ef6adbb994.js │ │ ├── 8271.1c1c633fcd3c2f1f.js │ │ ├── 832.e2f4738ba35965e4.js │ │ ├── 8346.e6af31d94167dd65.js │ │ ├── 8414.6bedc148480daa83.js │ │ ├── 8472.311cc354b4e26a29.js │ │ ├── 8526.35611a36737cc91c.js │ │ ├── 8579.fe29b87eca8a09af.js │ │ ├── 8592.ef92262980693d17.js │ │ ├── 8597740b.0ecf76d88bbed071.js │ │ ├── 8619.36a6a7c92ccc65c7.js │ │ ├── 864.863ce01377c675a1.js │ │ ├── 8641.24f14a689c55ade1.js │ │ ├── 8706.7745aaa3e76dded8.js │ │ ├── 8714.e56cffeed1a30233.js │ │ ├── 8726.5726eeaaac7cdf6c.js │ │ ├── 8742.c4b45c1d59425a27.js │ │ ├── 87bccb7d.21303fc0e9e49a12.js │ │ ├── 8823.3bf0b782455e038a.js │ │ ├── 8870.4b15b9781c02393d.js │ │ ├── 8877.03bac32f50d1cadd.js │ │ ├── 8919.38447c38ba2b94b9.js │ │ ├── 8942.667adb96cad5a8cc.js │ │ ├── 8980.f252ff9584faa915.js │ │ ├── 8986.483dc72a08a9957c.js │ │ ├── 8b07858d.daab52ea3460f96a.js │ │ ├── 9048.d60a12c719e45198.js │ │ ├── 9051.783fbf40ef4031fb.js │ │ ├── 9063.5f9447cc9942561f.js │ │ ├── 908.a39358a012859f7e.js │ │ ├── 9139.d8c1011b97b1f3b4.js │ │ ├── 9155.41f6426b84ddc9ed.js │ │ ├── 9163.a294c46ca5a37783.js │ │ ├── 9170.3974906319418ff1.js │ │ ├── 9206.64d6db872d794942.js │ │ ├── 9207.57aeba99ea00ed64.js │ │ ├── 9228.ad9afae0ed3ab3ce.js │ │ ├── 9236.59403c81a11d7023.js │ │ ├── 924.c14f6053110f7244.js │ │ ├── 9243.2d6204f212b19302.js │ │ ├── 9257.b08a03cf0221785d.js │ │ ├── 9286.532ed4d4f72c5205.js │ │ ├── 9294.c27d025d03a9144e.js │ │ ├── 931.02a5295aa02453fd.js │ │ ├── 944.81e7db354c8ed5b3.js │ │ ├── 9468d19c.3cc2763dddffdc65.js │ │ ├── 948.c917c7b85df9a2e8.js │ │ ├── 94ad0e32.dec28f631790a5d2.js │ │ ├── 950.d55fd287a24baab3.js │ │ ├── 9504.9b74a5fd614e09bc.js │ │ ├── 9531.3559c1d15ec7d038.js │ │ ├── 962.359ebe18fd3e30e8.js │ │ ├── 9714.701497bfd7c68125.js │ │ ├── 9716.3ff0471cfc96a8ce.js │ │ ├── 9731.b497c42fd7233113.js │ │ ├── 9816.b1978a439ccfe50a.js │ │ ├── 9851.e107acbd273fb2d8.js │ │ ├── 9888.a67aecaaae2bc960.js │ │ ├── 9900.a301b22021ada59d.js │ │ ├── 9949.590f3ad5abca9f38.js │ │ ├── 9e052d69.4cef8752f85fd9d1.js │ │ ├── a50c0dd0.0c272c2561672563.js │ │ ├── ac74fd1c.5f79606f9cac2cb7.js │ │ ├── app │ │ │ ├── _not-found │ │ │ │ └── page-f01e7c15f3882046.js │ │ │ ├── layout-5f3fbea6e5509db4.js │ │ │ └── page-9113efe66febbb21.js │ │ ├── c24cc598-1633c6f334c250a2.js │ │ ├── cca622d2.42e5b360038eb1ef.js │ │ ├── ccb8f04b.98acc2ab6576c800.js │ │ ├── dab78cdb-11a3c5a3e726a7f0.js │ │ ├── e30973c3.baf08befe1c61dd2.js │ │ ├── f4df560a.5fe879309ca9e15e.js │ │ ├── fd9d1056-231b9b6933cef830.js │ │ ├── framework-8e0e0f4a6b83a956.js │ │ ├── main-app-b239df8585f488e7.js │ │ ├── main-e9967d864ab7f685.js │ │ ├── pages │ │ │ ├── _app-f870474a17b7f2fd.js │ │ │ └── _error-c66a4e8afc46f17b.js │ │ ├── polyfills-78c92fac7aa8fdd8.js │ │ └── webpack-ef9c09c146f640eb.js │ │ ├── css │ │ ├── 8e2b5a9de91ab637.css │ │ └── 9c6f759383f5db47.css │ │ └── media │ │ ├── 26a46d62cd723877-s.woff2 │ │ ├── 55c55f0601d81cf3-s.woff2 │ │ ├── 581909926a08bbc8-s.woff2 │ │ ├── 6d93bde91c0c2823-s.woff2 │ │ ├── 97e0cb1ae144a2a9-s.woff2 │ │ ├── a34f9d1faa5f3315-s.p.woff2 │ │ └── df0a9ae256c0569c-s.woff2 ├── favicon.ico ├── flow-examples │ ├── climate-trends.json │ └── fires-in-canada.json ├── index.html └── index.txt ├── lib ├── shiki.ts └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public └── flow-examples │ ├── climate-trends.json │ └── fires-in-canada.json ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | .next/ 3 | out/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Data Analysis Flow 2 | 3 | SDA-Flow is a node-based editor for [simple-data-analysis](https://github.com/nshiab/simple-data-analysis), an easy-to-use and high-performance JavaScript library for data analysis. 4 | 5 | This repo and the SDA library are maintained by [Nael Shiab](http://naelshiab.com/), computational journalist and senior data producer for [CBC News](https://www.cbc.ca/news). 6 | 7 | Not all [SDA methods](https://nshiab.github.io/simple-data-analysis/classes/SimpleWebTable.html) are implemented. If you need one, [create an issue](https://github.com/nshiab/simple-data-analysis-flow/issues). 8 | 9 | > To use the editor, [click here](https://nshiab.github.io/simple-data-analysis-flow/). Everything runs in your browser. The data doesn't leave your computer. 10 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nshiab/simple-data-analysis-flow/880f5470743f780ae952f0ee0729aabc8202796e/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | pre { 6 | white-space: pre-wrap; 7 | word-break: break-all; 8 | width: 550px; 9 | } 10 | 11 | code { 12 | counter-reset: step; 13 | counter-increment: step 0; 14 | } 15 | 16 | code .line::before { 17 | content: counter(step, decimal); 18 | counter-increment: step; 19 | width: 2rem; 20 | margin-right: 1.5rem; 21 | display: inline-block; 22 | text-align: right; 23 | color: rgba(115, 138, 148, 0.4); 24 | } 25 | 26 | .react-flow__nodes .selected > div > div:not(.flex) { 27 | border: 1px solid black; 28 | } 29 | 30 | @layer base { 31 | :root { 32 | --background: 0 0% 100%; 33 | --foreground: 222.2 84% 4.9%; 34 | --card: 0 0% 100%; 35 | --card-foreground: 222.2 84% 4.9%; 36 | --popover: 0 0% 100%; 37 | --popover-foreground: 222.2 84% 4.9%; 38 | --primary: 222.2 47.4% 11.2%; 39 | --primary-foreground: 210 40% 98%; 40 | --secondary: 210 40% 96.1%; 41 | --secondary-foreground: 222.2 47.4% 11.2%; 42 | --muted: 210 40% 96.1%; 43 | --muted-foreground: 215.4 16.3% 46.9%; 44 | --accent: 210 40% 96.1%; 45 | --accent-foreground: 222.2 47.4% 11.2%; 46 | --destructive: 0 84.2% 60.2%; 47 | --destructive-foreground: 210 40% 98%; 48 | --border: 214.3 31.8% 91.4%; 49 | --input: 214.3 31.8% 91.4%; 50 | --ring: 222.2 84% 4.9%; 51 | --radius: 0.5rem; 52 | --chart-1: 12 76% 61%; 53 | --chart-2: 173 58% 39%; 54 | --chart-3: 197 37% 24%; 55 | --chart-4: 43 74% 66%; 56 | --chart-5: 27 87% 67%; 57 | } 58 | 59 | .dark { 60 | --background: 222.2 84% 4.9%; 61 | --foreground: 210 40% 98%; 62 | --card: 222.2 84% 4.9%; 63 | --card-foreground: 210 40% 98%; 64 | --popover: 222.2 84% 4.9%; 65 | --popover-foreground: 210 40% 98%; 66 | --primary: 210 40% 98%; 67 | --primary-foreground: 222.2 47.4% 11.2%; 68 | --secondary: 217.2 32.6% 17.5%; 69 | --secondary-foreground: 210 40% 98%; 70 | --muted: 217.2 32.6% 17.5%; 71 | --muted-foreground: 215 20.2% 65.1%; 72 | --accent: 217.2 32.6% 17.5%; 73 | --accent-foreground: 210 40% 98%; 74 | --destructive: 0 62.8% 30.6%; 75 | --destructive-foreground: 210 40% 98%; 76 | --border: 217.2 32.6% 17.5%; 77 | --input: 217.2 32.6% 17.5%; 78 | --ring: 212.7 26.8% 83.9%; 79 | --chart-1: 220 70% 50%; 80 | --chart-2: 160 60% 45%; 81 | --chart-3: 30 80% 55%; 82 | --chart-4: 280 65% 60%; 83 | --chart-5: 340 75% 55%; 84 | } 85 | } 86 | 87 | @layer base { 88 | * { 89 | @apply border-border; 90 | } 91 | body { 92 | @apply bg-background text-foreground; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | import { Inter } from "next/font/google" 3 | import "./globals.css" 4 | 5 | const inter = Inter({ subsets: ["latin"] }) 6 | 7 | export const metadata: Metadata = { 8 | title: "Simple Data Analysis Flow", 9 | description: 10 | "A node-based editor for the JavaScript library Simple-Data-Analysis.", 11 | } 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: Readonly<{ 16 | children: React.ReactNode 17 | }>) { 18 | return ( 19 | 20 | {children} 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import GithubLogo from "@/components/partials/GithubLogo" 2 | import SimpleFlow from "@/components/SimpleFlow" 3 | import { ReactFlowProvider } from "@xyflow/react" 4 | 5 | export default function Home() { 6 | return ( 7 |
8 |
9 | 46 | 47 | 48 | 49 |
50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /components/SDA/CleanColumnNames.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardDescription, 5 | CardHeader, 6 | } from "@/components/ui/card" 7 | import { useHandleConnections, useNodesData, useReactFlow } from "@xyflow/react" 8 | import { useEffect, useState } from "react" 9 | import SimpleWebTable from "../../node_modules/simple-data-analysis/dist/class/SimpleWebTable" 10 | 11 | import Code from "../partials/Code" 12 | import CardTitleWithLoader from "../partials/CardTitleWithLoader" 13 | import Error from "../partials/Error" 14 | import Target from "../partials/Target" 15 | import Source from "../partials/Source" 16 | 17 | export default function CleanColumnNames({ id }: { id: string }) { 18 | const { updateNodeData } = useReactFlow() 19 | 20 | const target = useHandleConnections({ type: "target" }) 21 | const source = useNodesData(target[0]?.source) 22 | const [targetReady, setTargetReady] = useState(false) 23 | const [sourceReady, setSourceReady] = useState(false) 24 | 25 | const [code, setCode] = useState("") 26 | const [loader, setLoader] = useState(false) 27 | const [error, setError] = useState(null) 28 | 29 | useEffect(() => { 30 | async function run() { 31 | const table = source?.data?.instance 32 | if (table instanceof SimpleWebTable) { 33 | setTargetReady(true) 34 | } 35 | if (table instanceof SimpleWebTable) { 36 | try { 37 | setLoader(true) 38 | const clonedTable = await table.cloneTable({ 39 | outputTable: id, 40 | }) 41 | await clonedTable.cleanColumnNames() 42 | 43 | const originalTableName = 44 | source?.data?.originalTableName ?? table.name 45 | const code = `await ${originalTableName}.cleanColumnNames();` 46 | setCode(code) 47 | updateNodeData(id, { 48 | instance: clonedTable, 49 | originalTableName: originalTableName, 50 | code, 51 | }) 52 | setError(null) 53 | setLoader(false) 54 | setSourceReady(true) 55 | } catch (err) { 56 | console.error(err) 57 | //@ts-expect-error okay 58 | setError(err.message) 59 | setLoader(false) 60 | setSourceReady(false) 61 | } 62 | } 63 | } 64 | 65 | run() 66 | }, [source, id, updateNodeData]) 67 | 68 | return ( 69 |
70 | 71 | 72 | 73 | 74 | 75 | Clean column names 76 | 77 | 78 | Removes special characters and spaces from column names. 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /components/SDA/Describe.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardDescription, 5 | CardHeader, 6 | } from "@/components/ui/card" 7 | import { useHandleConnections, useNodesData } from "@xyflow/react" 8 | import { useEffect, useState } from "react" 9 | import SimpleWebTable from "../../node_modules/simple-data-analysis/dist/class/SimpleWebTable" 10 | 11 | import DataTable from "../partials/DataTable" 12 | import Code from "../partials/Code" 13 | import CardTitleWithLoader from "../partials/CardTitleWithLoader" 14 | import Target from "../partials/Target" 15 | 16 | export default function Describe({ id }: { id: string }) { 17 | const [name, setName] = useState(undefined) 18 | const [data, setData] = useState< 19 | { [key: string]: string | number | boolean | Date | null }[] | null 20 | >(null) 21 | const [columns, setColumns] = useState(null) 22 | 23 | const targetConnection = useHandleConnections({ type: "target" }) 24 | const source = useNodesData(targetConnection[0]?.source) 25 | 26 | const [targetRead, setTargetReady] = useState(false) 27 | const [code, setCode] = useState("") 28 | const [loader, setLoader] = useState(false) 29 | 30 | useEffect(() => { 31 | async function run() { 32 | const table = source?.data?.instance 33 | if (table instanceof SimpleWebTable) { 34 | setTargetReady(true) 35 | setLoader(true) 36 | 37 | const description = (await table.getDescription()) as { 38 | [key: string]: string | number | boolean | Date | null 39 | }[] 40 | setData(description) 41 | const columns = Object.keys(description[0]) 42 | setColumns(columns) 43 | 44 | const originalTableName = source?.data?.originalTableName ?? table.name 45 | setName(originalTableName as string) 46 | const code = `await ${originalTableName}.logDescription()` 47 | setCode(code) 48 | setLoader(false) 49 | } else { 50 | setData(null) 51 | setColumns(null) 52 | } 53 | } 54 | 55 | run() 56 | }, [source, id]) 57 | 58 | return ( 59 |
60 | 61 | 62 | 63 | 64 | 65 | Describe{typeof name === "string" ? ` ${name}` : ""} 66 | 67 | 68 | Returns details like data types, number of null and distinct values. 69 | 70 | 71 | {Array.isArray(data) && Array.isArray(columns) && ( 72 | 73 | 74 | 75 | )} 76 | 77 |
78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /components/SDA/FetchGeoData.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardDescription, 5 | CardHeader, 6 | } from "@/components/ui/card" 7 | import { useHandleConnections, useNodesData, useReactFlow } from "@xyflow/react" 8 | import { useEffect, useState } from "react" 9 | import SimpleWebTable from "../../node_modules/simple-data-analysis/dist/class/SimpleWebTable" 10 | import Code from "../partials/Code" 11 | import CardTitleWithLoader from "../partials/CardTitleWithLoader" 12 | import Error from "../partials/Error" 13 | import Target from "../partials/Target" 14 | import Source from "../partials/Source" 15 | import OptionsInputText from "../partials/OptionsInputText" 16 | 17 | export default function FetchGeoData({ id }: { id: string }) { 18 | const [url, setURL] = useState(undefined) 19 | 20 | const { updateNodeData } = useReactFlow() 21 | 22 | const target = useHandleConnections({ type: "target" }) 23 | const source = useNodesData(target[0]?.source) 24 | 25 | const [code, setCode] = useState("") 26 | const [loader, setLoader] = useState(false) 27 | const [targetReady, setTargetReady] = useState(false) 28 | const [sourceReady, setSourceReady] = useState(false) 29 | const [error, setError] = useState(null) 30 | 31 | const nodeData = useNodesData(id) 32 | useEffect(() => { 33 | if (nodeData?.data.imported) { 34 | if (typeof nodeData.data.url === "string") { 35 | setURL(nodeData.data.url) 36 | } 37 | nodeData.data.imported = false 38 | } 39 | }, [nodeData]) 40 | 41 | useEffect(() => { 42 | async function run() { 43 | const table = source?.data?.instance 44 | if (table instanceof SimpleWebTable) { 45 | setTargetReady(true) 46 | } 47 | if (table instanceof SimpleWebTable && typeof url === "string") { 48 | try { 49 | setLoader(true) 50 | await table.fetchGeoData(url) 51 | const code = `// For front-end projects, use fetchGeoData. 52 | await ${table.name}.loadGeoData("${url}");` 53 | setCode(code) 54 | updateNodeData(id, { 55 | instance: table, 56 | code, 57 | url, 58 | }) 59 | setError(null) 60 | setLoader(false) 61 | setSourceReady(true) 62 | } catch (err) { 63 | console.error(err) 64 | // @ts-expect-error okay 65 | setError(err.message) 66 | setLoader(false) 67 | setSourceReady(false) 68 | } 69 | } 70 | } 71 | 72 | run() 73 | }, [source, id, updateNodeData, url]) 74 | 75 | return ( 76 |
77 | 78 | 79 | 80 | 81 | 82 | Fetch geo data 83 | 84 | Fetches spatial data from a URL. 85 | 86 | 87 | setURL(e)} 91 | /> 92 | 93 | 94 | 95 | 96 |
97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /components/SDA/Filter.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardDescription, 5 | CardHeader, 6 | } from "@/components/ui/card" 7 | import { useHandleConnections, useNodesData, useReactFlow } from "@xyflow/react" 8 | import { useEffect, useState } from "react" 9 | import SimpleWebTable from "../../node_modules/simple-data-analysis/dist/class/SimpleWebTable" 10 | 11 | import Code from "../partials/Code" 12 | import CardTitleWithLoader from "../partials/CardTitleWithLoader" 13 | import Error from "../partials/Error" 14 | import Target from "../partials/Target" 15 | import Source from "../partials/Source" 16 | import OptionsTextArea from "../partials/OptionsTextArea" 17 | 18 | export default function Filter({ id }: { id: string }) { 19 | const [definition, setDefinition] = useState(undefined) 20 | 21 | const { updateNodeData } = useReactFlow() 22 | 23 | const target = useHandleConnections({ type: "target" }) 24 | const source = useNodesData(target[0]?.source) 25 | const [targetReady, setTargetReady] = useState(false) 26 | const [sourceReady, setSourceReady] = useState(false) 27 | 28 | const [code, setCode] = useState("") 29 | const [loader, setLoader] = useState(false) 30 | const [error, setError] = useState(null) 31 | 32 | const nodeData = useNodesData(id) 33 | useEffect(() => { 34 | if (nodeData?.data.imported) { 35 | if (typeof nodeData.data.definition === "string") { 36 | setDefinition(nodeData.data.definition) 37 | } 38 | nodeData.data.imported = false 39 | } 40 | }, [nodeData]) 41 | 42 | useEffect(() => { 43 | async function run() { 44 | const table = source?.data?.instance 45 | if (table instanceof SimpleWebTable) { 46 | setTargetReady(true) 47 | } 48 | if (table instanceof SimpleWebTable && typeof definition === "string") { 49 | try { 50 | setLoader(true) 51 | const clonedTable = await table.cloneTable({ 52 | outputTable: id, 53 | }) 54 | await clonedTable.filter(definition) 55 | 56 | const originalTableName = 57 | source?.data?.originalTableName ?? table.name 58 | const code = `await ${originalTableName}.filter(\`${definition}\`);` 59 | setCode(code) 60 | updateNodeData(id, { 61 | instance: clonedTable, 62 | originalTableName: originalTableName, 63 | code, 64 | definition, 65 | }) 66 | setError(null) 67 | setLoader(false) 68 | setSourceReady(true) 69 | } catch (err) { 70 | console.error(err) 71 | //@ts-expect-error okay 72 | setError(err.message) 73 | setLoader(false) 74 | setSourceReady(false) 75 | } 76 | } 77 | } 78 | 79 | run() 80 | }, [source, id, updateNodeData, definition]) 81 | 82 | return ( 83 |
84 | 85 | 86 | 87 | 88 | Filter 89 | 90 | Filter rows in the table. The condition is in SQL. 91 | 92 | 93 | 94 | 99 | 100 | 101 | 102 | 103 |
104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /components/SDA/LoadGeoFile.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardDescription, 5 | CardHeader, 6 | } from "@/components/ui/card" 7 | import { useHandleConnections, useNodesData, useReactFlow } from "@xyflow/react" 8 | import { useEffect, useRef, useState } from "react" 9 | import SimpleWebTable from "../../node_modules/simple-data-analysis/dist/class/SimpleWebTable" 10 | import { Input } from "../ui/input" 11 | 12 | import Code from "../partials/Code" 13 | import CardTitleWithLoader from "../partials/CardTitleWithLoader" 14 | import Error from "../partials/Error" 15 | import Target from "../partials/Target" 16 | import Source from "../partials/Source" 17 | import { AsyncDuckDB, DuckDBDataProtocol } from "@duckdb/duckdb-wasm" 18 | 19 | export default function LoadGeoFile({ id }: { id: string }) { 20 | const refUrl = useRef(null) 21 | 22 | const [file, setFile] = useState(null) 23 | 24 | const { updateNodeData } = useReactFlow() 25 | 26 | const target = useHandleConnections({ type: "target" }) 27 | const source = useNodesData(target[0]?.source) 28 | 29 | const [code, setCode] = useState("") 30 | const [loader, setLoader] = useState(false) 31 | const [targetReady, setTargetReady] = useState(false) 32 | const [sourceReady, setSourceReady] = useState(false) 33 | const [error, setError] = useState(null) 34 | 35 | useEffect(() => { 36 | async function run() { 37 | const table = source?.data?.instance 38 | if (table instanceof SimpleWebTable) { 39 | setTargetReady(true) 40 | } 41 | if (table instanceof SimpleWebTable && file instanceof File) { 42 | try { 43 | setLoader(true) 44 | const db = table.db as AsyncDuckDB 45 | 46 | await db.registerFileHandle( 47 | file.name, 48 | file, 49 | DuckDBDataProtocol.BROWSER_FILEREADER, 50 | true 51 | ) 52 | await table.sdb.customQuery( 53 | `INSTALL spatial; LOAD spatial; 54 | INSTALL https; LOAD https; 55 | CREATE OR REPLACE TABLE ${table.name} AS SELECT * FROM ST_Read('${file.name}');` 56 | ) 57 | 58 | const code = `// More options available. Check documentation. 59 | await ${table.name}.loadGeoData("${file.name}");` 60 | setCode(code) 61 | updateNodeData(id, { 62 | instance: table, 63 | code, 64 | }) 65 | setError(null) 66 | setLoader(false) 67 | setSourceReady(true) 68 | } catch (err) { 69 | console.error(err) 70 | // @ts-expect-error okay 71 | setError(err.message) 72 | setLoader(false) 73 | setSourceReady(true) 74 | } 75 | } 76 | } 77 | 78 | run() 79 | }, [source, id, updateNodeData, file]) 80 | 81 | return ( 82 |
83 | 84 | 85 | 86 | 87 | 88 | Load geo file 89 | 90 | 91 | Loads spatial data from a local file. 92 | 93 | 94 | 95 |
96 | { 100 | const files = e.target.files 101 | if (files) { 102 | setFile(files[0]) 103 | } 104 | }} 105 | /> 106 |
107 | 108 |
109 |
110 | 111 |
112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /components/SDA/Note.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "@/components/ui/card" 2 | import { useNodesData, useReactFlow } from "@xyflow/react" 3 | import { useEffect, useState } from "react" 4 | import { Textarea } from "../ui/textarea" 5 | 6 | export default function Note({ id }: { id: string }) { 7 | const [importedText, setImportedText] = useState( 8 | undefined 9 | ) 10 | const [text, setText] = useState(undefined) 11 | 12 | const { updateNodeData } = useReactFlow() 13 | 14 | const nodeData = useNodesData(id) 15 | useEffect(() => { 16 | if (nodeData?.data.imported) { 17 | if (typeof nodeData.data.text === "string") { 18 | setImportedText(nodeData.data.text) 19 | } 20 | nodeData.data.imported = false 21 | } 22 | }, [nodeData]) 23 | 24 | useEffect(() => { 25 | async function run() { 26 | updateNodeData(id, { 27 | text, 28 | }) 29 | } 30 | 31 | run() 32 | }, [text, id, updateNodeData]) 33 | 34 | return ( 35 |
36 | 37 |