├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── package.json ├── src ├── components │ ├── Page.tsx │ ├── blockly │ │ ├── BlockContext.tsx │ │ ├── BlockEditor.tsx │ │ ├── BlockMinimap.tsx │ │ ├── BlocklyModalDialogs.tsx │ │ ├── DSBlockEditor.tsx │ │ ├── WorkspaceContext.tsx │ │ ├── blockwarning.ts │ │ ├── dsl │ │ │ ├── chartdsl.ts │ │ │ ├── datadsl.ts │ │ │ ├── datasetdsl.ts │ │ │ ├── datasolver.ts │ │ │ ├── datavardsl.ts │ │ │ ├── dsl.ts │ │ │ ├── fieldsdsl.ts │ │ │ ├── iframedsl.ts │ │ │ ├── palette.ts │ │ │ ├── shadowdsl.ts │ │ │ ├── statsdsl.ts │ │ │ ├── visualizedsl.ts │ │ │ ├── workers │ │ │ │ ├── csv.proxy.ts │ │ │ │ ├── data.proxy.ts │ │ │ │ └── proxy.ts │ │ │ ├── workspacejson.ts │ │ │ └── workspacevisitor.ts │ │ ├── fields │ │ │ ├── BuiltinDataSetField.tsx │ │ │ ├── DataColumnChooserField.ts │ │ │ ├── DataPreviewField.tsx │ │ │ ├── DataTableField.tsx │ │ │ ├── DataTablePreviewWidget.tsx │ │ │ ├── DataTableWidget.tsx │ │ │ ├── FileOpenField.ts │ │ │ ├── FileSaveField.ts │ │ │ ├── IFrameDataChooserField.ts │ │ │ ├── JSONSchemaForm.tsx │ │ │ ├── JSONSettingsField.tsx │ │ │ ├── PointerBoundary.tsx │ │ │ ├── ReactField.tsx │ │ │ ├── ReactFieldBase.ts │ │ │ ├── ReactImageField.tsx │ │ │ ├── ReactInlineField.tsx │ │ │ ├── SliderField.tsx │ │ │ ├── ValueContext.tsx │ │ │ ├── chart │ │ │ │ ├── BarField.tsx │ │ │ │ ├── BoxPlotField.tsx │ │ │ │ ├── CorrelationHeapMapField.tsx │ │ │ │ ├── HeatMapField.tsx │ │ │ │ ├── HistogramCell.tsx │ │ │ │ ├── HistogramField.tsx │ │ │ │ ├── LinePlotField.tsx │ │ │ │ ├── PieChartField.tsx │ │ │ │ ├── ScatterPlotField.tsx │ │ │ │ ├── ScatterPlotMatrixField.tsx │ │ │ │ ├── VegaChartField.tsx │ │ │ │ └── VegaLiteWidget.tsx │ │ │ ├── fields.ts │ │ │ └── tidy.ts │ │ ├── jsongenerator.ts │ │ ├── toolbox.ts │ │ ├── useBlockData.ts │ │ ├── useBlocklyEvents.ts │ │ ├── useBlocklyPlugins.ts │ │ ├── useDragDebounce.ts │ │ ├── useScreenshot.ts │ │ ├── useToolbox.ts │ │ └── useWorkspaceEvent.ts │ ├── dom │ │ ├── constants.ts │ │ ├── eventsource.ts │ │ ├── node.ts │ │ ├── observable.ts │ │ ├── useChange.ts │ │ ├── useChangeThrottled.ts │ │ ├── useEventRaised.ts │ │ └── utils.ts │ ├── fs │ │ ├── FileNewFileChip.tsx │ │ ├── FileSystemButton.tsx │ │ ├── FileSystemChip.tsx │ │ ├── FileSystemContext.tsx │ │ ├── FileSystemNodeChip.tsx │ │ ├── FileTabs.tsx │ │ ├── fs.ts │ │ └── fsdom.ts │ ├── hooks │ │ ├── useAnalytics.tsx │ │ ├── useAsyncMemo.ts │ │ ├── useEffectAsync.ts │ │ ├── useLocalStorage.ts │ │ ├── useLocationSearchParam.ts │ │ ├── useMdxComponents.tsx │ │ ├── useSnackbar.ts │ │ ├── useStorage.ts │ │ └── useWindowEvent.ts │ ├── layout.css │ ├── layout.tsx │ ├── shell │ │ ├── AppTheme.tsx │ │ ├── DarkModeButton.tsx │ │ ├── DarkModeContext.tsx │ │ ├── DataEditorAppBar.tsx │ │ ├── Footer.tsx │ │ ├── Head.tsx │ │ ├── ThemedLayout.tsx │ │ └── ThemedMdxLayout.tsx │ ├── ui │ │ ├── Alert.tsx │ │ ├── BrowserCompatibilityAlert.tsx │ │ ├── Button.tsx │ │ ├── CopyButton.tsx │ │ ├── GridHeader.tsx │ │ ├── IconButtonWithTooltip.tsx │ │ ├── LoadingProgress.tsx │ │ ├── Progress.tsx │ │ ├── SplashDialog.tsx │ │ ├── Suspense.tsx │ │ ├── SwitchWithLabel.tsx │ │ ├── Tooltip.tsx │ │ └── useProgress.ts │ ├── uiflags.ts │ └── widgets │ │ ├── svg.ts │ │ └── svgutils.ts ├── gatsby-theme-material-ui-top-layout │ └── components │ │ └── top-layout.tsx ├── images │ └── favicon.svg ├── pages │ ├── 404.tsx │ ├── about.mdx │ ├── blocks │ │ ├── api.txt │ │ ├── chart_bar.png │ │ ├── chart_box_plot.png │ │ ├── chart_heatmap.png │ │ ├── chart_histogram.png │ │ ├── chart_lineplot.png │ │ ├── chart_pie.png │ │ ├── chart_scatterplot.png │ │ ├── chart_scatterplot_matrix.png │ │ ├── chart_show_table.png │ │ ├── data_arrange.png │ │ ├── data_bin.png │ │ ├── data_correlation.png │ │ ├── data_count.png │ │ ├── data_dataset_builtin.png │ │ ├── data_dataset_read.png │ │ ├── data_dataset_write.png │ │ ├── data_drop.png │ │ ├── data_drop_duplicates.png │ │ ├── data_fill_nully.png │ │ ├── data_filter_columns.png │ │ ├── data_filter_string.png │ │ ├── data_linear_regression.png │ │ ├── data_load_text.png │ │ ├── data_load_url.png │ │ ├── data_mutate_columns.png │ │ ├── data_mutate_number.png │ │ ├── data_rename_column_block.png │ │ ├── data_replace_nully.png │ │ ├── data_select.png │ │ ├── data_slice.png │ │ ├── data_summarize.png │ │ ├── data_summarize_by_group.png │ │ ├── index.md │ │ ├── vega_encoding.png │ │ ├── vega_encoding_aggregate.png │ │ ├── vega_encoding_bin.png │ │ ├── vega_encoding_sort.png │ │ ├── vega_encoding_sort_field.png │ │ ├── vega_encoding_time_unit.png │ │ ├── vega_encoding_type.png │ │ └── vega_layer.png │ ├── chart.png │ ├── discover.png │ ├── embed.tsx │ ├── excel.mdx │ ├── hero.png │ ├── index.tsx │ ├── preview.png │ ├── story.png │ ├── vscode.mdx │ └── vscode.png ├── tsconfig.json └── workers │ ├── csv │ ├── csv.worker.ts │ ├── package.json │ ├── tsconfig.json │ └── workerloader.js │ └── data │ ├── data.worker.ts │ ├── package.json │ ├── tsconfig.json │ └── workerloader.js ├── static ├── blockly │ └── media │ │ ├── 1x1.gif │ │ ├── click.mp3 │ │ ├── click.ogg │ │ ├── click.wav │ │ ├── delete.mp3 │ │ ├── delete.ogg │ │ ├── delete.wav │ │ ├── disconnect.mp3 │ │ ├── disconnect.ogg │ │ ├── disconnect.wav │ │ ├── dropdown-arrow.svg │ │ ├── handclosed.cur │ │ ├── handdelete.cur │ │ ├── handopen.cur │ │ ├── pilcrow.png │ │ ├── quote0.png │ │ ├── quote1.png │ │ ├── sprites.png │ │ └── sprites.svg ├── datasets │ ├── cereal.csv │ ├── iris.csv │ ├── mtcars.csv │ ├── penguins.csv │ └── pokemon.csv ├── videos │ ├── cereal-calories-per-cup.mp4 │ ├── cereal-calories-sort.mp4 │ └── vscode.mp4 └── vscode │ └── README.md ├── vscode ├── .eslintrc.json ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .vscodeignore ├── .yarnrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src │ └── web │ │ ├── api.ts │ │ ├── blocks.ts │ │ ├── extension.ts │ │ └── webview.ts ├── tsconfig.json ├── vsc-extension-quickstart.md ├── vscode.png ├── webpack.config.js └── yarn.lock └── yarn.lock /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.134.0/containers/javascript-node/.devcontainer/base.Dockerfile 2 | ARG VARIANT="14" 3 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} 4 | 5 | # [Optional] Uncomment this section to install additional OS packages. 6 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 7 | # && apt-get -y install --no-install-recommends 8 | 9 | # [Optional] Uncomment if you want to install an additional version of node using nvm 10 | # ARG EXTRA_NODE_VERSION=10 11 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 12 | 13 | # [Optional] Uncomment if you want to install more global node modules 14 | RUN sudo -u node npm install -g gatsby-cli 15 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.134.0/containers/javascript-node 3 | { 4 | "name": "Node.js", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 10, 12, 14 8 | "args": { 9 | "VARIANT": "14" 10 | } 11 | }, 12 | // Set *default* container specific settings.json values on container create. 13 | "settings": {}, 14 | // Add the IDs of extensions you want installed when the container is created. 15 | "extensions": [ 16 | "dbaeumer.vscode-eslint", 17 | "esbenp.prettier-vscode", 18 | "wix.vscode-import-cost", 19 | "hediet.vscode-drawio", 20 | "streetsidesoftware.code-spell-checker", 21 | "unifiedjs.vscode-mdx", 22 | "redhat.vscode-yaml" 23 | ], 24 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 25 | "forwardPorts": [8080, 8000], 26 | // Specifies a command that should be run after the container has been created. 27 | "postCreateCommand": "yarn setup", 28 | // Comment out the next line to run as root instead. 29 | "remoteUser": "node" 30 | } 31 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:jsx-a11y/recommended" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": 12, 18 | "sourceType": "module" 19 | }, 20 | "plugins": ["react", "@typescript-eslint"], 21 | "rules": { 22 | "@typescript-eslint/no-empty-function": "off", 23 | "react/display-name": "off", 24 | "@typescript-eslint/explicit-module-boundary-types": "off" 25 | }, 26 | "ignorePatterns": ["jacscript/samples/*.js"] 27 | } 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # enforce unix style line endings 2 | *.ts text eol=lf 3 | *.tsx text eol=lf 4 | *.cpp text eol=lf 5 | *.h text eol=lf 6 | *.jres text eol=lf 7 | *.asm text eol=lf 8 | *.md text eol=lf 9 | *.mdx text eol=lf 10 | *.txt text eol=lf 11 | *.js text eol=lf 12 | *.json text eol=lf 13 | *.xml text eol=lf 14 | *.svg text eol=lf 15 | *.yaml text eol=lf 16 | *.css text eol=lf 17 | *.html text eol=lf 18 | *.py text eol=lf 19 | *.exp text eol=lf 20 | *.manifest text eol=lf 21 | 22 | # do not enforce text for everything - it causes issues with random binary files 23 | 24 | *.sln text eol=crlf 25 | 26 | *.png binary 27 | *.jpg binary 28 | *.jpeg binary 29 | *.gif binary 30 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: [14.x] 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | submodules: recursive 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | cache: yarn 21 | - run: yarn install --frozen-lockfile 22 | - run: yarn builddocsts 23 | - run: yarn install --frozen-lockfile 24 | working-directory: ./vscode 25 | - run: yarn package 26 | working-directory: ./vscode 27 | - run: yarn distdocs 28 | env: 29 | GATSBY_GITHUB_REPOSITORY: ${{ github.repository }} 30 | GATSBY_GITHUB_REF: ${{ github.ref }} 31 | GATSBY_GITHUB_SHA: ${{ github.sha }} 32 | NODE_OPTIONS: --max-old-space-size=4096 33 | - name: github pages 34 | uses: peaceiris/actions-gh-pages@v3 35 | if: ${{ github.ref == 'refs/heads/main' }} 36 | with: 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | publish_dir: ./public 39 | force_orphan: true 40 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main, ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | schedule: 10 | - cron: '0 12 * * 3' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-20.04 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # Initializes the CodeQL tools for scanning. 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@v1 28 | with: 29 | languages: javascript 30 | 31 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 32 | # If this step fails, then you should remove it and run the build manually (see below) 33 | - name: Autobuild 34 | uses: github/codeql-action/autobuild@v1 35 | 36 | # ℹ️ Command-line programs to run using the OS shell. 37 | # 📚 https://git.io/JvXDl 38 | 39 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 40 | # and modify them (or add more) to build your code if your project 41 | # uses a compiled language 42 | 43 | #- run: | 44 | # make bootstrap 45 | # make release 46 | 47 | - name: Perform CodeQL Analysis 48 | uses: github/codeql-action/analyze@v1 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | static/images/makecode 4 | public 5 | .DS_Store 6 | src/workers/dist 7 | yarn-error.log 8 | .parcel-cache 9 | .vs 10 | vscode/dist 11 | static/vscode/*.vsix 12 | __queuestorage__ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": true, 4 | "tabWidth": 4 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "wix.vscode-import-cost", 6 | "hediet.vscode-drawio", 7 | "streetsidesoftware.code-spell-checker", 8 | "unifiedjs.vscode-mdx" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "eslint.alwaysShowStatus": true, 4 | "eslint.format.enable": false, 5 | "eslint.debug": true, 6 | "eslint.lintTask.enable": true, 7 | "files.eol": "\n", 8 | "[typescript]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "[typescriptreact]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | "liveServer.settings.port": 5501, 15 | "scm.showActionButton": false, 16 | "python.analysis.completeFunctionParens": true, 17 | "python.analysis.diagnosticMode": "workspace", 18 | "python.analysis.typeCheckingMode": "basic", 19 | "[python]": { 20 | "editor.defaultFormatter": "ms-python.python" 21 | }, 22 | "files.associations": { 23 | "*.mdx": "markdown" 24 | }, 25 | "git.detectSubmodulesLimit": 20, 26 | "[svg]": { 27 | "editor.defaultFormatter": "jock.svg" 28 | }, 29 | "cSpell.words": [ 30 | "Blinky", 31 | "blockly", 32 | "bytecode", 33 | "clsx", 34 | "colour", 35 | "datascienceeditor", 36 | "datavar", 37 | "devicescript", 38 | "dsls", 39 | "Jacdac", 40 | "Jacdaptor", 41 | "Jacdaptors", 42 | "Jacscript", 43 | "jdom", 44 | "keepout", 45 | "keepouts", 46 | "mdgpt", 47 | "microbit", 48 | "microcontroller", 49 | "microcontrollers", 50 | "oldf", 51 | "Schottky", 52 | "Slidy", 53 | "UART" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to 4 | agree to a Contributor License Agreement (CLA) declaring that you have the right to, 5 | and actually do, grant us the rights to use your contribution. For details, visit 6 | https://cla.microsoft.com. 7 | 8 | When you submit a pull request, a CLA-bot will automatically determine whether you need 9 | to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the 10 | instructions provided by the bot. You will only need to do this once across all repositories using our CLA. 11 | 12 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 13 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 14 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data Science Editor 2 | 3 | https://user-images.githubusercontent.com/4175913/219923079-062bb9f1-c015-469e-b144-81aaca3eb493.mp4 4 | 5 | ## Developer setup 6 | 7 | - Open this repository online at https://github.dev/microsoft/data-science-editor 8 | 9 | All command line instructions assume a bash-like terminal. 10 | 11 | On Windows, you may need to run these commands within Git Bash or Windows Subsystem for Linux (WSL), unless you have bash-like tools available locally. Previous installs have worked on WSL2 with Ubuntu-20.04. 12 | 13 | ### Codespaces 14 | 15 | Edit this project directly from your browser using GitHub Codespaces. If you have access to them, 16 | 17 | - open project in a new codespace (https://github.dev/microsoft/data-science-editor) 18 | - launch the docs server 19 | 20 | ``` 21 | yarn develop 22 | ``` 23 | 24 | - click on the generated URL in the terminal output and voila! 25 | 26 | **Do not use npm** 27 | 28 | #### Updating dependencies 29 | 30 | Use [npm-check-updates](https://www.npmjs.com/package/npm-check-updates) to upgrade all dependencies expect blockly\*, tfjs, mdx. 31 | 32 | ### VS Code 33 | 34 | You are welcome to use any editor you want! Visual Studio Code 35 | provides seamless support for git sub-modules and is our preferred editor. 36 | 37 | - open [Visual Studio Code](https://code.visualstudio.com/) 38 | 39 | ``` 40 | code . 41 | ``` 42 | 43 | - install the recommended extensions (**MDX**, **ESLint** and **Prettier** extensions) 44 | - remember that you need a bash-like terminal to run some of these commands - VS Code allows you to start a Git Bash terminal from the new terminals dropdown 45 | - run the docs web site locally 46 | 47 | ``` 48 | yarn develop 49 | ``` 50 | 51 | - browse to the local server 52 | 53 | ``` 54 | http://localhost:8000?dbg=1 55 | ``` 56 | 57 | ### Generate block docs 58 | 59 | - add `?screenshot=1` 60 | - right click in empty editor and select generate screenshots 61 | - select `/src/pages/blocks` folder 62 | - commit 63 | 64 | ## Microsoft Open Source Code of Conduct 65 | 66 | This project is hosted at https://github.com/microsoft/data-science-editor. 67 | This project has adopted the 68 | [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 69 | 70 | Resources: 71 | 72 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 73 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 74 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 75 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import Layout from "./src/components/layout" 2 | import React from "react" 3 | import ReactDOM from "react-dom" 4 | 5 | const UPDATE_DEBOUNCE = 5000 6 | let lastUpdate = Date.now() 7 | function tryUpdate(force) { 8 | const now = Date.now() 9 | if (now - lastUpdate < UPDATE_DEBOUNCE) return 10 | lastUpdate = now 11 | 12 | setTimeout(async () => { 13 | const reg = await navigator.serviceWorker.getRegistration() 14 | if (reg) reg.update() 15 | else if ( 16 | force && 17 | window.navigator.onLine && 18 | !/http:\/\/localhost/.test(window.location.href) 19 | ) { 20 | console.debug(`check for updates`) 21 | try { 22 | const req = await fetch("/data-science-editor/version.json") 23 | if (!req.ok) { 24 | console.debug(`fetch version.json failed, probably offline`) 25 | return 26 | } 27 | const version = await req.json() 28 | console.log(`version info`, { 29 | version, 30 | sha: window.analytics.sha, 31 | }) 32 | if ( 33 | version && 34 | version.sha && 35 | window.analytics && 36 | version.sha !== window.analytics.sha && 37 | window.self === window.top // don't auto-refresh hosted 38 | ) { 39 | console.warn( 40 | `web app updated ${version.sha} !== ${window.analytics.sha}` 41 | ) 42 | window.location.reload() 43 | } 44 | } catch (e) { 45 | console.error(e) 46 | } 47 | } 48 | }, UPDATE_DEBOUNCE - 1000) 49 | } 50 | 51 | export const onRouteUpdate = async ({ location }, options) => { 52 | if (window.analytics && window.analytics.page) window.analytics.page() 53 | try { 54 | const reg = await navigator.serviceWorker.getRegistration() 55 | if (reg) reg.update() 56 | else tryUpdate() 57 | } catch(e) { 58 | console.debug(e) 59 | } 60 | } 61 | 62 | export const onServiceWorkerUpdateReady = () => { 63 | // force reload 64 | console.debug(`offline: update ready, reloading...`) 65 | window.location.reload(true) 66 | } 67 | 68 | export const wrapPageElement = Layout 69 | 70 | window.addEventListener(`unhandledrejection`, event => { 71 | if (/loading chunk \d* failed/i.test(event.reason)) { 72 | console.log(`loading chunk failed, trying to update...`) 73 | tryUpdate(true) 74 | } 75 | }) 76 | 77 | // inject React Axe into DOM tree at development time 78 | export const onInitialClientRender = () => { 79 | const activeEnv = 80 | process.env.GATSBY_ACTIVE_ENV || process.env.NODE_ENV || "development" 81 | const isDev = activeEnv === "development" 82 | if (isDev) { 83 | console.log(`axe: injecting into DOM`) 84 | import("@axe-core/react").then(reactAxe => { 85 | reactAxe.default(React, ReactDOM, 1000, {}) 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | const siteUrl = "https://microsoft.github.io" 2 | const pathPrefix = "/data-science-editor" 3 | 4 | const SITE_TITLE = `Data Science Editor.` 5 | const SITE_DESCRIPTION = `Data Science Editor.` 6 | 7 | module.exports = { 8 | trailingSlash: "always", 9 | siteMetadata: { 10 | title: SITE_TITLE, 11 | description: SITE_DESCRIPTION, 12 | author: `Microsoft`, 13 | siteUrl: siteUrl, 14 | }, 15 | pathPrefix: pathPrefix, 16 | flags: { 17 | PRESERVE_FILE_DOWNLOAD_CACHE: true, 18 | DEV_WEBPACK_CACHE: true, 19 | FAST_DEV: true, 20 | }, 21 | plugins: [ 22 | { 23 | resolve: `gatsby-source-filesystem`, 24 | options: { 25 | name: `images`, 26 | path: `${__dirname}/src/images`, 27 | }, 28 | }, 29 | { 30 | resolve: `gatsby-source-filesystem`, 31 | options: { 32 | name: `pages`, 33 | path: `${__dirname}/src/pages`, 34 | }, 35 | }, 36 | `gatsby-theme-material-ui`, 37 | `gatsby-plugin-image`, 38 | `gatsby-plugin-sharp`, 39 | `gatsby-transformer-sharp`, 40 | { 41 | resolve: `gatsby-plugin-mdx`, 42 | options: { 43 | extensions: [`.mdx`, `.md`], 44 | mdxOptions: { 45 | remarkPlugins: [require(`remark-gfm`)], 46 | }, 47 | gatsbyRemarkPlugins: [ 48 | { 49 | resolve: `gatsby-remark-autolink-headers`, 50 | options: { 51 | enableCustomId: true 52 | } 53 | }, 54 | "gatsby-remark-external-links", 55 | "gatsby-remark-static-images", 56 | "gatsby-remark-copy-linked-files", 57 | ].filter(plugin => !!plugin), 58 | }, 59 | }, 60 | "gatsby-plugin-sitemap", 61 | { 62 | resolve: `gatsby-plugin-manifest`, 63 | options: { 64 | name: SITE_TITLE, 65 | short_name: `Data Science Editor`, 66 | description: SITE_DESCRIPTION, 67 | start_url: `/`, 68 | background_color: `#ffc400`, 69 | theme_color: `#ffc400`, 70 | display: `standalone`, 71 | cache_busting_mode: "none", 72 | icon: `src/images/favicon.svg`, 73 | crossOrigin: `use-credentials`, 74 | }, 75 | }, 76 | { 77 | resolve: `gatsby-plugin-offline`, 78 | options: { 79 | precachePages: [`/*`], 80 | }, 81 | }, 82 | "gatsby-plugin-robots-txt", 83 | "gatsby-plugin-meta-redirect", 84 | "gatsby-plugin-webpack-bundle-analyser-v2", 85 | { 86 | resolve: `gatsby-plugin-catch-links`, 87 | }, 88 | ], 89 | } 90 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | import Layout from "./src/components/layout" 2 | 3 | export const wrapPageElement = Layout 4 | 5 | export const onRenderBody = ({ setHtmlAttributes }) => { 6 | setHtmlAttributes({ lang: "en" }) 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Page.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react" 2 | 3 | // eslint-disable-next-line react/prop-types 4 | const Page = ({ props, children }) => { 5 | return {children} 6 | } 7 | 8 | export default Page 9 | -------------------------------------------------------------------------------- /src/components/blockly/DSBlockEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, NoSsr } from "@mui/material"; 2 | import React, { useMemo } from "react"; 3 | import { BlockProvider } from "./BlockContext"; 4 | import BlockEditor from "./BlockEditor"; 5 | import FileTabs from "../fs/FileTabs"; 6 | import { WorkspaceFile } from "./dsl/workspacejson"; 7 | import dataDsl from "./dsl/datadsl"; 8 | import chartDsl from "./dsl/chartdsl"; 9 | import fieldsDsl from "./dsl/fieldsdsl"; 10 | import { WORKSPACE_FILENAME } from "./toolbox"; 11 | import useFileSystem from "../fs/FileSystemContext"; 12 | import { createIFrameDSL } from "./dsl/iframedsl"; 13 | import { 14 | useLocationSearchParamBoolean, 15 | useLocationSearchParamString, 16 | } from "../hooks/useLocationSearchParam"; 17 | import dataSetDsl from "./dsl/datasetdsl"; 18 | import dataVarDsl from "./dsl/datavardsl"; 19 | import useChange from "../dom/useChange"; 20 | import { UIFlags } from "../uiflags"; 21 | import SplashDialog from "../ui/SplashDialog"; 22 | import visualizeDsl from "./dsl/visualizedsl"; 23 | import statsDsl from "./dsl/statsdsl"; 24 | import { DS_EDITOR_ID } from "../dom/constants"; 25 | 26 | const DS_SOURCE_STORAGE_KEY = "editor"; 27 | const DS_NEW_FILE_CONTENT = JSON.stringify({ 28 | editor: DS_EDITOR_ID, 29 | xml: "", 30 | } as WorkspaceFile); 31 | 32 | function DSEditorWithContext() { 33 | const { fileSystem } = useFileSystem(); 34 | const root = useChange(fileSystem, _ => _?.root); 35 | 36 | return ( 37 | 38 | {!!root && ( 39 | 40 | 44 | 45 | )} 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | 53 | /** 54 | * Data science block editor component 55 | */ 56 | export default function DSBlockEditor() { 57 | // read url search flags 58 | const dataSet = useLocationSearchParamBoolean("dataset", true); 59 | const dataVar = useLocationSearchParamBoolean("datavar", true); 60 | const visualize = useLocationSearchParamBoolean("visualize", true); 61 | const statistics = useLocationSearchParamBoolean("statistics", true); 62 | const chart = useLocationSearchParamBoolean("chart", true); 63 | const host = UIFlags.hosted; 64 | const targetOrigin = useLocationSearchParamString("targetorigin") || "*"; 65 | 66 | const dsls = useMemo(() => { 67 | return [ 68 | dataSet && dataSetDsl, 69 | dataDsl, 70 | visualize && visualizeDsl, 71 | statistics && statsDsl, 72 | dataVar && dataVarDsl, 73 | chart && chartDsl, 74 | fieldsDsl, 75 | // host is used to comms with excel 76 | host && createIFrameDSL("host", targetOrigin), 77 | ].filter(dsl => !!dsl); 78 | }, []); 79 | 80 | return ( 81 | 82 | 87 | 88 | 89 | 90 | 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/components/blockly/blockwarning.ts: -------------------------------------------------------------------------------- 1 | import { WorkspaceJSON } from "./dsl/workspacejson" 2 | import { visitWorkspace } from "./dsl/workspacevisitor" 3 | 4 | export interface BlockWarning { 5 | sourceId?: string 6 | message: string 7 | } 8 | 9 | export function collectWarnings(workspace: WorkspaceJSON): BlockWarning[] { 10 | const warnings: BlockWarning[] = [] 11 | visitWorkspace(workspace, { 12 | visitBlock: b => { 13 | if (b.warning) warnings.push({ sourceId: b.id, message: b.warning }) 14 | }, 15 | }) 16 | return warnings 17 | } 18 | -------------------------------------------------------------------------------- /src/components/blockly/dsl/datasolver.ts: -------------------------------------------------------------------------------- 1 | import { CHANGE } from "../../dom/constants" 2 | import { delay } from "../../dom/utils" 3 | import { identityTransformData, resolveBlockDefinition } from "../toolbox" 4 | import { 5 | BlockWithServices, 6 | resolveBlockServices, 7 | resolveBlockSvg, 8 | } from "../WorkspaceContext" 9 | 10 | function startShowTransform(block: BlockWithServices) { 11 | let blockSvg = resolveBlockSvg(block) 12 | blockSvg.addSelect() 13 | blockSvg.setEditable(false) 14 | return () => { 15 | blockSvg?.removeSelect() 16 | blockSvg?.setEditable(true) 17 | blockSvg = undefined 18 | } 19 | } 20 | 21 | const TRANSFORM_DELAY = 40 22 | 23 | export function registerDataSolver(block: BlockWithServices) { 24 | const { blockServices: services } = block 25 | // register data transforms 26 | const { transformData } = resolveBlockDefinition(block.type) || {} 27 | if (!transformData) return 28 | 29 | const applyTransform = async () => { 30 | if (!block.isEnabled() || block.isInFlyout) return 31 | 32 | const unshow = startShowTransform(block) 33 | // transfer data to the next block 34 | const nextServices = resolveBlockServices( 35 | block.nextConnection?.targetBlock() 36 | ) 37 | try { 38 | services.setDataWarning(undefined) 39 | await delay(TRANSFORM_DELAY) 40 | // eslint-disable-next-line @typescript-eslint/ban-types 41 | let newData: object[] 42 | if (transformData === identityTransformData) newData = services.data 43 | else { 44 | //const start = performance.now() 45 | newData = await transformData( 46 | block, 47 | services.data, 48 | nextServices?.data 49 | ) 50 | } 51 | 52 | // propagate 53 | services.transformedData = newData 54 | unshow() 55 | 56 | // check if pass through 57 | const def = resolveBlockDefinition(block.type) 58 | if (def?.passthroughData) newData = services.data 59 | if (nextServices) nextServices.data = newData 60 | } catch (e) { 61 | console.debug(e) 62 | } finally { 63 | unshow() 64 | } 65 | } 66 | // apply transform, then register for change 67 | applyTransform().then(() => services.on(CHANGE, applyTransform)) 68 | } 69 | -------------------------------------------------------------------------------- /src/components/blockly/dsl/dsl.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "@mui/material" 2 | import Blockly, { Block, Workspace, WorkspaceSvg } from "blockly" 3 | import { 4 | BlockDefinition, 5 | ContentDefinition, 6 | resolveBlockDefinition, 7 | } from "../toolbox" 8 | import { WorkspaceFile, WorkspaceJSON } from "./workspacejson" 9 | 10 | export interface CreateBlocksOptions { 11 | theme: Theme 12 | } 13 | 14 | export interface CreateCategoryOptions { 15 | theme: Theme 16 | source: WorkspaceJSON 17 | directory?: boolean 18 | } 19 | 20 | export default interface BlockDomainSpecificLanguage { 21 | /** 22 | * A unique identifier used to route blocks back to the DSL 23 | */ 24 | id: string 25 | 26 | /** 27 | * A list of builtin block types, typically provided by Blockly 28 | */ 29 | types?: string[] 30 | 31 | /** 32 | * Optional API to be called when mounted in the React context, returns unmounted 33 | */ 34 | mount?: (workspace: Workspace) => () => void 35 | 36 | /** 37 | * Creates blocks for the DSL 38 | */ 39 | createBlocks?: ( 40 | options: CreateBlocksOptions 41 | ) => BlockDefinition[] | Promise 42 | 43 | /*** 44 | * Creates a JSON category to populate the toolbox 45 | */ 46 | createCategory?: (options: CreateCategoryOptions) => ContentDefinition[] 47 | 48 | /** 49 | * Shorthand to convert block JSON into a primitive value 50 | */ 51 | blockToValue?: (block: Block) => string | number | boolean 52 | 53 | /** 54 | * Returns a change listener if needed 55 | */ 56 | createWorkspaceChangeListener?: ( 57 | workspace: WorkspaceSvg 58 | ) => (event: Blockly.Events.Abstract) => void 59 | 60 | /** 61 | * After a successfull parse and save, opportunity to walk the JSON tree 62 | * and attach warnings 63 | */ 64 | visitWorkspaceJSON?: ( 65 | workspace: Workspace, 66 | workspaceJSON: WorkspaceJSON 67 | ) => void 68 | 69 | onWorkspaceJSONChange?: (workspaceJSON: WorkspaceJSON) => void 70 | 71 | onSave?: (file: WorkspaceFile) => void 72 | 73 | onBeforeSaveWorkspaceFile?: (file: WorkspaceFile) => void 74 | } 75 | 76 | export function resolveDsl(dsls: BlockDomainSpecificLanguage[], type: string) { 77 | const dsl = dsls?.find(dsl => dsl.types?.indexOf(type) > -1) 78 | if (dsl) return dsl 79 | 80 | const { dsl: dslName } = resolveBlockDefinition(type) || {} 81 | if (!dslName) { 82 | console.warn(`unknown dsl for ${type}`) 83 | return undefined 84 | } 85 | return dsls?.find(dsl => dsl.id === dslName) 86 | } 87 | -------------------------------------------------------------------------------- /src/components/blockly/dsl/fieldsdsl.ts: -------------------------------------------------------------------------------- 1 | import { fieldShadows } from "../fields/fields" 2 | import BlockDomainSpecificLanguage from "./dsl" 3 | 4 | const fieldsDsl: BlockDomainSpecificLanguage = { 5 | id: "fields", 6 | createBlocks: () => fieldShadows(), 7 | } 8 | export default fieldsDsl 9 | -------------------------------------------------------------------------------- /src/components/blockly/dsl/palette.ts: -------------------------------------------------------------------------------- 1 | // see https://developers.google.com/blockly/guides/create-custom-blocks/block-colour 2 | // and https://mkweb.bcgsc.ca/colorblind/palettes.mhtml#page-container 3 | 4 | const _palette = [ 5 | "#506d87", 6 | "#6ba4bd", 7 | "#d170a1", 8 | "#62726c", 9 | "#ab672e", 10 | "#b99031", 11 | "#17bc14", 12 | ]; 13 | 14 | export default function palette() { 15 | return _palette.slice(0); 16 | } 17 | 18 | export function paletteColorByIndex(i: number) { 19 | while (i < 0) i += _palette.length; 20 | return _palette[i % _palette.length]; 21 | } 22 | 23 | export const [ 24 | datasetColour, 25 | operatorsColour, 26 | computeColour, 27 | statisticsColour, 28 | visualizeColour, 29 | cleaningColour, 30 | ] = palette(); 31 | export const chartColour = visualizeColour; 32 | -------------------------------------------------------------------------------- /src/components/blockly/dsl/shadowdsl.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "blockly" 2 | import { 3 | ColorInputDefinition, 4 | NumberInputDefinition, 5 | OptionsInputDefinition, 6 | } from "../toolbox" 7 | import BlockDomainSpecificLanguage from "./dsl" 8 | 9 | const builtins: Record string | number | boolean> = { 10 | jacdac_on_off: block => block.getFieldValue("value") === "on", 11 | jacdac_yes_no: block => block.getFieldValue("value") === "on", 12 | jacdac_time_picker: block => Number(block.getFieldValue("value") || "0"), 13 | jacdac_time_picker_ms: block => Number(block.getFieldValue("value") || "0"), 14 | } 15 | 16 | const shadowDsl: BlockDomainSpecificLanguage = { 17 | id: "shadow", 18 | createBlocks: () => [ 19 | { 20 | kind: "block", 21 | type: `jacdac_on_off`, 22 | message0: `%1`, 23 | args0: [ 24 | { 25 | type: "field_dropdown", 26 | name: "value", 27 | options: [ 28 | ["enabled", "on"], 29 | ["disabled", "off"], 30 | ], 31 | }, 32 | ], 33 | style: "logic_blocks", 34 | output: "Boolean", 35 | }, 36 | { 37 | kind: "block", 38 | type: `jacdac_yes_no`, 39 | message0: `%1`, 40 | args0: [ 41 | { 42 | type: "field_dropdown", 43 | name: "value", 44 | options: [ 45 | ["yes", "on"], 46 | ["no", "off"], 47 | ], 48 | }, 49 | ], 50 | style: "logic_blocks", 51 | output: "Boolean", 52 | }, 53 | { 54 | kind: "block", 55 | type: `jacdac_time_picker`, 56 | message0: `%1`, 57 | args0: [ 58 | { 59 | type: "field_dropdown", 60 | name: "value", 61 | options: [ 62 | ["1s", "1"], 63 | ["0.1s", "0.1"], 64 | ["0.5s", "0.5"], 65 | ["5s", "5"], 66 | ["15s", "15"], 67 | ["30s", "30"], 68 | ["1min", "60"], 69 | ], 70 | }, 71 | ], 72 | style: "math_blocks", 73 | output: "Number", 74 | }, 75 | { 76 | kind: "block", 77 | type: `jacdac_time_picker_ms`, 78 | message0: `%1`, 79 | args0: [ 80 | { 81 | type: "field_dropdown", 82 | name: "value", 83 | options: [ 84 | ["1s", "1000"], 85 | ["0.1s", "100"], 86 | ["0.5s", "500"], 87 | ["5s", "5000"], 88 | ["15s", "15000"], 89 | ["30s", "30000"], 90 | ], 91 | }, 92 | ], 93 | style: "math_blocks", 94 | output: "Number", 95 | }, 96 | ], 97 | 98 | blockToValue: block => builtins[block.type]?.(block), 99 | } 100 | export default shadowDsl 101 | -------------------------------------------------------------------------------- /src/components/blockly/dsl/workers/csv.proxy.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import workerProxy from "./proxy" 3 | import type { 4 | CsvDownloadRequest, 5 | CsvSaveRequest, 6 | CsvParseRequest, 7 | CsvFile, 8 | CsvFileResponse, 9 | CsvUnparseRequest, 10 | CsvUnparseResponse, 11 | CsvMessage, 12 | } from "../../../../workers/csv/dist/node_modules/csv.worker" 13 | 14 | export async function downloadCSV(url: string): Promise { 15 | const worker = workerProxy("csv") 16 | const res = await worker.postMessage({ 17 | worker: "csv", 18 | type: "download", 19 | url, 20 | }) 21 | return res?.file 22 | } 23 | 24 | export async function saveCSV( 25 | fileHandle: FileSystemFileHandle, 26 | data: object[] 27 | ): Promise { 28 | const worker = workerProxy("csv") 29 | await worker.postMessage({ 30 | worker: "csv", 31 | type: "save", 32 | fileHandle, 33 | data, 34 | }) 35 | } 36 | 37 | export async function unparseCSV(data: object[]): Promise { 38 | const worker = workerProxy("csv") 39 | const resp = await worker.postMessage< 40 | CsvUnparseRequest, 41 | CsvUnparseResponse 42 | >({ 43 | worker: "csv", 44 | type: "unparse", 45 | data, 46 | }) 47 | return resp?.text 48 | } 49 | 50 | export async function parseCSV(source: string): Promise { 51 | const worker = workerProxy("csv") 52 | const resp = await worker.postMessage({ 53 | worker: "csv", 54 | type: "parse", 55 | source, 56 | }) 57 | return resp?.file 58 | } 59 | -------------------------------------------------------------------------------- /src/components/blockly/dsl/workers/data.proxy.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DataRequest, 3 | DataMessage, 4 | } from "../../../../workers/data/dist/node_modules/data.worker" 5 | import workerProxy from "./proxy" 6 | 7 | export default async function postTransformData( 8 | message: DataRequest 9 | // eslint-disable-next-line @typescript-eslint/ban-types 10 | ): Promise { 11 | // check for missing data 12 | if (!message.data) return undefined 13 | const worker = workerProxy("data") 14 | const res = await worker.postMessage(message) 15 | return res?.data 16 | } 17 | -------------------------------------------------------------------------------- /src/components/blockly/dsl/workers/proxy.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/ban-types */ 3 | import assert from "assert" 4 | import { MESSAGE } from "../../../dom/constants" 5 | import { JDEventSource } from "../../../dom/eventsource" 6 | import createCsvWorker from "../../../../workers/csv/workerloader" 7 | import createDataWorker from "../../../../workers/data/workerloader" 8 | 9 | export type VMType = "data" | "csv" 10 | 11 | export interface WorkerMessage { 12 | worker: VMType 13 | id?: string 14 | } 15 | 16 | export interface WorkerResponse { 17 | error?: string 18 | } 19 | 20 | export class WorkerProxy extends JDEventSource { 21 | readonly pendings: Record< 22 | string, 23 | { 24 | resolve: (res: any) => void 25 | reject: (err: any) => void 26 | } 27 | > = {} 28 | constructor(readonly worker: Worker, readonly workerid: VMType) { 29 | super() 30 | this.handleMessage = this.handleMessage.bind(this) 31 | this.worker.addEventListener("message", this.handleMessage) 32 | } 33 | 34 | unmount() { 35 | delete _workers[this.workerid] 36 | this.worker.removeEventListener("message", this.handleMessage) 37 | this.worker.terminate() 38 | //Object.values(this.pendings).forEach(({ reject }) => 39 | // reject(new Error("worker terminated")) 40 | //) 41 | } 42 | 43 | private handleMessage(event: MessageEvent) { 44 | const { data: message } = event 45 | const { id, worker } = message 46 | const pending = id && this.pendings[id] 47 | if (pending) { 48 | assert(worker === message.worker) 49 | if (message.error) 50 | console.debug( 51 | `${this.workerid}: error: ${message.error}`, 52 | message 53 | ) 54 | pending.resolve(message) 55 | } else { 56 | this.emit(MESSAGE, event.data) 57 | } 58 | } 59 | 60 | postMessage(message: WorkerMessage & T): Promise { 61 | message.id = message.id || Math.random() + "" 62 | message.worker = this.workerid 63 | return new Promise((resolve, reject) => { 64 | this.pendings[message.id] = { resolve, reject } 65 | this.worker.postMessage(message) 66 | }) 67 | } 68 | } 69 | 70 | const _workers: Record = {} 71 | const loaders = { 72 | data: createDataWorker, 73 | csv: createCsvWorker, 74 | } 75 | export default function workerProxy(workerid: VMType) { 76 | const worker = 77 | _workers[workerid] || 78 | (_workers[workerid] = new WorkerProxy(loaders[workerid](), workerid)) 79 | return worker 80 | } 81 | -------------------------------------------------------------------------------- /src/components/blockly/dsl/workspacejson.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from "../../dom/constants" 2 | import { tidyHeaders } from "../fields/tidy" 3 | import { BlockDataSet } from "../toolbox" 4 | 5 | export interface VariableJSON { 6 | // Boolean, Number, String, or service short id 7 | type: string 8 | id: string 9 | name: string 10 | } 11 | 12 | export type FieldJSON = { 13 | id?: string 14 | value?: number | string | boolean | any 15 | // Boolean, Number, String, or service short id 16 | variabletype?: string 17 | // and extra fields, subclass 18 | } 19 | 20 | export interface InputJSON { 21 | type: number 22 | name: string 23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 24 | fields: Record 25 | child?: BlockJSON 26 | } 27 | 28 | export interface BlockJSON { 29 | type: string 30 | id: string 31 | children?: BlockJSON[] 32 | value?: string | number | boolean 33 | inputs?: InputJSON[] 34 | next?: BlockJSON 35 | warning?: string 36 | } 37 | 38 | export function getField(block: BlockJSON, name: string): FieldJSON { 39 | const inputs = block.inputs 40 | for (let i = 0; i < inputs.length; ++i) { 41 | const field = inputs[i].fields[name] 42 | if (field) return field 43 | } 44 | return undefined 45 | } 46 | 47 | export function getFieldValue(block: BlockJSON, name: string) { 48 | const field = getField(block, name) 49 | return field?.value 50 | } 51 | 52 | export function resolveFieldColumn( 53 | data: BlockDataSet, 54 | b: BlockJSON, 55 | fieldName: string, 56 | options?: { 57 | type?: DataType 58 | required?: boolean 59 | } 60 | ): { column: string; warning?: string } { 61 | const name = getFieldValue(b, fieldName) as string 62 | const { type, required } = options || {} 63 | const column = resolveHeader(data, name, type) 64 | let warning: string 65 | if (!column) { 66 | if (required && !name) warning = "missing column" 67 | else if (name) warning = `${name} column not found` 68 | } 69 | return { column, warning } 70 | } 71 | 72 | export function resolveHeader( 73 | data: BlockDataSet, 74 | name: string, 75 | type?: DataType 76 | ) { 77 | if (!data || !name) return undefined 78 | 79 | const { headers } = tidyHeaders(data, type) 80 | return headers.indexOf(name) > -1 ? name : undefined 81 | } 82 | 83 | export interface WorkspaceJSON { 84 | variables: VariableJSON[] 85 | blocks: BlockJSON[] 86 | } 87 | 88 | export interface WorkspaceFile { 89 | editor: string 90 | xml: string 91 | json?: WorkspaceJSON 92 | } 93 | -------------------------------------------------------------------------------- /src/components/blockly/dsl/workspacevisitor.ts: -------------------------------------------------------------------------------- 1 | import { BlockJSON, FieldJSON, InputJSON, WorkspaceJSON } from "./workspacejson" 2 | 3 | export interface WorkspaceVisitor { 4 | visitBlock?: (block: BlockJSON) => void 5 | visitInput?: (input: InputJSON) => void 6 | visitField?: (name: string, field: FieldJSON) => void 7 | } 8 | 9 | export function visitBlock(block: BlockJSON, visitor: WorkspaceVisitor) { 10 | if (!block) return 11 | visitor.visitBlock?.(block) 12 | const { inputs, children } = block 13 | inputs?.forEach(input => visitInput(input, visitor)) 14 | children?.forEach(child => visitBlock(child, visitor)) 15 | } 16 | 17 | export function visitInput(input: InputJSON, visitor: WorkspaceVisitor) { 18 | if (!input) return 19 | visitor.visitInput?.(input) 20 | const { fields, child } = input 21 | if (fields) Object.keys(fields).map(k => visitField(k, fields[k], visitor)) 22 | visitBlock(child, visitor) 23 | } 24 | 25 | export function visitField( 26 | name: string, 27 | field: FieldJSON, 28 | visitor: WorkspaceVisitor 29 | ) { 30 | if (!field) return 31 | visitor.visitField?.(name, field) 32 | } 33 | 34 | export function visitWorkspace( 35 | workspace: WorkspaceJSON, 36 | visitor: WorkspaceVisitor 37 | ) { 38 | workspace?.blocks?.forEach(block => visitBlock(block, visitor)) 39 | } 40 | -------------------------------------------------------------------------------- /src/components/blockly/fields/BuiltinDataSetField.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BlockWithServices, 3 | FieldWithServices, 4 | setBlockDataWarning, 5 | } from "../WorkspaceContext" 6 | import { Block, FieldDropdown } from "blockly" 7 | import { withPrefix } from "gatsby" 8 | import { downloadCSV } from "../dsl/workers/csv.proxy" 9 | 10 | export const builtinDatasets = { 11 | Cereals: withPrefix("/datasets/cereal.csv"), 12 | Penguins: withPrefix("/datasets/penguins.csv"), 13 | Iris: withPrefix("/datasets/iris.csv"), 14 | Cars: withPrefix("/datasets/mtcars.csv"), 15 | // https://www.kaggle.com/datasets/abcsds/pokemon 16 | Pokemons: withPrefix("/datasets/pokemon.csv"), 17 | } 18 | 19 | export default class BuiltinDataSetField 20 | extends FieldDropdown 21 | implements FieldWithServices 22 | { 23 | static KEY = "ds_field_data_builtin_dataset" 24 | private initialized = false 25 | 26 | // eslint-disable-next-line @typescript-eslint/ban-types 27 | static fromJson(options: object) { 28 | return new BuiltinDataSetField(options) 29 | } 30 | 31 | // eslint-disable-next-line @typescript-eslint/ban-types 32 | constructor(options: object) { 33 | super( 34 | () => Object.keys(builtinDatasets).map(k => [k, k]), 35 | undefined, 36 | options 37 | ) 38 | } 39 | 40 | init() { 41 | super.init() 42 | this.initialized = true 43 | this.updateData() 44 | } 45 | 46 | private async updateData() { 47 | if (!this.initialized) return 48 | 49 | const url = builtinDatasets[this.getValue()] 50 | if (!url) return 51 | 52 | this.updateDataFromUrl(url) 53 | } 54 | 55 | private async updateDataFromUrl(url: string) { 56 | // load dataset as needed 57 | const sourceBlock = this.getSourceBlock() as BlockWithServices 58 | const marker = !!sourceBlock?.isInsertionMarker() 59 | if (!sourceBlock || marker) return 60 | 61 | const services = sourceBlock.blockServices 62 | if (!services || services.cache[BuiltinDataSetField.KEY] === url) return // already downloaded 63 | // avoid races 64 | services.cache[BuiltinDataSetField.KEY] = url 65 | 66 | const { data, errors } = await downloadCSV(url) 67 | if (errors?.length) { 68 | setBlockDataWarning(sourceBlock, errors[0].message) 69 | console.debug(`csv parse errors`, { 70 | id: sourceBlock.id, 71 | marker, 72 | data, 73 | errors, 74 | services, 75 | url, 76 | }) 77 | } 78 | services.data = data 79 | } 80 | 81 | setSourceBlock(block: Block) { 82 | super.setSourceBlock(block) 83 | this.updateData() 84 | } 85 | 86 | doValueUpdate_(newValue) { 87 | super.doValueUpdate_(newValue) 88 | this.updateData() 89 | } 90 | 91 | notifyServicesChanged() { 92 | this.updateData() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/components/blockly/fields/DataPreviewField.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, ReactNode } from "react" 2 | import ReactField, { ReactFieldJSON } from "./ReactField" 3 | import { 4 | BlockDefinition, 5 | DataPreviewInputDefinition, 6 | identityTransformData, 7 | } from "../toolbox" 8 | import Suspense from "../../ui/Suspense" 9 | const DataTablePreviewWidget = lazy(() => import("./DataTablePreviewWidget")) 10 | 11 | export interface DataPreviewOptions extends ReactFieldJSON { 12 | compare?: boolean 13 | summary?: boolean 14 | transformed?: boolean 15 | } 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | export default class DataPreviewField extends ReactField { 19 | static KEY = "ds_field_data_preview" 20 | compare: boolean 21 | summary: boolean 22 | transformed: boolean 23 | 24 | static fromJson(options: DataPreviewOptions) { 25 | return new DataPreviewField(options?.value, undefined, options) 26 | } 27 | 28 | // the first argument is a dummy and never used 29 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 30 | constructor(value: string, validator?: any, options?: DataPreviewOptions) { 31 | super(value, validator, options) 32 | this.compare = !!options?.compare 33 | this.summary = !!options?.summary 34 | this.transformed = !!options?.transformed 35 | } 36 | 37 | protected initCustomView(): SVGElement { 38 | const group = this.fieldGroup_ 39 | group.classList.add("blocklyFieldButton") 40 | return undefined 41 | } 42 | 43 | getText_() { 44 | return "👀" 45 | } 46 | 47 | renderField(): ReactNode { 48 | return ( 49 | 50 | 55 | 56 | ) 57 | } 58 | } 59 | 60 | export function addDataPreviewField(block: BlockDefinition): BlockDefinition { 61 | const preview = block?.dataPreviewField 62 | if (preview) { 63 | // don't add twice 64 | block.dataPreviewField = false 65 | // parse args and add one more arg 66 | const { message0 } = block 67 | const i = message0.lastIndexOf("%") 68 | const index = parseInt(message0.substr(i + 1)) || 0 69 | block.message0 += ` %${index + 1}` 70 | 71 | // does this mutate the data? 72 | const identity = 73 | preview === "after" || block.transformData === identityTransformData 74 | 75 | // add field 76 | block.args0.push({ 77 | type: DataPreviewField.KEY, 78 | name: "preview", 79 | compare: !identity, 80 | summary: true, 81 | transformed: false, 82 | } as DataPreviewInputDefinition) 83 | } 84 | return block 85 | } 86 | -------------------------------------------------------------------------------- /src/components/blockly/fields/DataTableField.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ReactFieldJSON } from "./ReactField"; 3 | import ReactInlineField from "./ReactInlineField"; 4 | import DataTableWidget from "./DataTableWidget"; 5 | import { FULL_TABLE_WIDTH, SMALL_TABLE_HEIGHT, TABLE_WIDTH } from "../toolbox"; 6 | 7 | export interface DataPreviewOptions extends ReactFieldJSON { 8 | transformed?: boolean; 9 | small?: boolean; 10 | full?: boolean; 11 | selectColumns?: boolean; 12 | summary?: boolean; 13 | } 14 | 15 | const MAX_ITEMS = 256; 16 | const FULL_MAX_ITEMS = 256 * 256; 17 | export default class DataTableField extends ReactInlineField { 18 | static KEY = "ds_field_data_table"; 19 | EDITABLE = false; 20 | transformed: boolean; 21 | small: boolean; 22 | selectColumns: boolean; 23 | full: boolean; 24 | summary: boolean; 25 | 26 | static fromJson(options: DataPreviewOptions) { 27 | return new DataTableField(options); 28 | } 29 | 30 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 31 | constructor(options?: DataPreviewOptions) { 32 | super(options); 33 | this.transformed = !!options?.transformed; 34 | this.small = !!options?.small; 35 | this.selectColumns = !!options?.selectColumns; 36 | this.full = !!options?.full; 37 | this.summary = options?.summary !== false; 38 | } 39 | 40 | protected createContainer(): HTMLDivElement { 41 | const c = document.createElement("div"); 42 | c.style.display = "block"; 43 | c.style.minWidth = `${this.full ? FULL_TABLE_WIDTH : TABLE_WIDTH}px`; 44 | c.style.maxWidth = "80vw"; 45 | c.style.maxHeight = "60vh"; 46 | c.style.overflow = "auto"; 47 | return c; 48 | } 49 | 50 | renderInlineField() { 51 | const tableHeight = this.small ? SMALL_TABLE_HEIGHT : undefined; 52 | const maxItems = this.full ? FULL_MAX_ITEMS : MAX_ITEMS; 53 | const tableWidth = this.full ? FULL_TABLE_WIDTH : undefined; 54 | return ( 55 | 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/blockly/fields/DataTablePreviewWidget.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import DataTableWidget from "./DataTableWidget"; 3 | import { 4 | TABLE_PREVIEW_HEIGHT, 5 | TABLE_PREVIEW_MAX_ITEMS, 6 | TABLE_WIDTH, 7 | } from "../toolbox"; 8 | import { Grid } from "@mui/material"; 9 | import useBlockData from "../useBlockData"; 10 | import WorkspaceContext from "../WorkspaceContext"; 11 | 12 | export default function DataTablePreviewWidget(props: { 13 | compare?: boolean; 14 | hideSummary?: boolean; 15 | transformed?: boolean; 16 | }) { 17 | const { compare, hideSummary, transformed } = props; 18 | const { sourceBlock } = useContext(WorkspaceContext); 19 | const { data } = useBlockData<{ id?: string } & unknown>(sourceBlock); 20 | if (!compare) 21 | return ( 22 |
23 | 31 |
32 | ); 33 | else if (!data?.length) 34 | return ( 35 |
36 | 45 |
46 | ); 47 | else 48 | return ( 49 | 50 | 51 | 60 | 61 | 62 | 70 | 71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/components/blockly/fields/FileSaveField.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Field } from "blockly" 3 | 4 | export default class FileSaveField extends Field { 5 | static KEY = "ds_field_file_save" 6 | SERIALIZABLE = true 7 | private fileType: string 8 | 9 | constructor(options?: any) { 10 | super("...", null, options) 11 | this.fileType = options?.fileType || "csv" 12 | } 13 | fileHandle: FileSystemFileHandle 14 | 15 | static fromJson(options: any) { 16 | return new FileSaveField(options) 17 | } 18 | 19 | showEditor_() { 20 | this.openFileHandle() 21 | } 22 | 23 | private async openFileHandle() { 24 | const options: SaveFilePickerOptions = { 25 | types: [ 26 | { 27 | id: "text", 28 | description: "Text Files", 29 | accept: { 30 | "text/plain": [".txt"], 31 | }, 32 | }, 33 | { 34 | id: "csv", 35 | description: "CSV Files", 36 | accept: { 37 | "text/csv": [".csv"], 38 | }, 39 | }, 40 | { 41 | id: "json", 42 | description: "JSON Files", 43 | accept: { 44 | "application/json": [".json"], 45 | }, 46 | }, 47 | ].filter(({ id }) => this.fileType === id), 48 | excludeAcceptAllOption: true, 49 | } 50 | this.fileHandle = await window.showSaveFilePicker?.(options) 51 | this.setValue(this.fileHandle?.name || "") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/blockly/fields/IFrameDataChooserField.ts: -------------------------------------------------------------------------------- 1 | import { ReactFieldJSON } from "./ReactField" 2 | import { FieldDropdown } from "blockly" 3 | 4 | export interface IFrameDataChooserOptions extends ReactFieldJSON { 5 | dataId: string 6 | } 7 | 8 | export const AllOptions: Record = {} 9 | 10 | export default class IFrameDataChooserField extends FieldDropdown { 11 | static KEY = "ds_field_iframe_data_chooser" 12 | SERIALIZABLE = true 13 | dataId: string 14 | 15 | static fromJson(options: ReactFieldJSON) { 16 | return new IFrameDataChooserField(options) 17 | } 18 | 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | // the first argument is a dummy and never used 21 | constructor(options?: IFrameDataChooserOptions) { 22 | super(() => [["", ""]], undefined, options) 23 | this.dataId = options.dataId || "" 24 | } 25 | 26 | fromXml(fieldElement: Element) { 27 | this.setValue(fieldElement.textContent) 28 | } 29 | 30 | getOptions(): string[][] { 31 | const options = AllOptions[this.dataId]?.slice(0) 32 | return !options?.length ? [["", ""]] : options 33 | } 34 | 35 | doClassValidation_(newValue?: string) { 36 | // skip super class validationervices chan 37 | return newValue 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/blockly/fields/JSONSettingsField.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/ban-types */ 3 | import React, { lazy, ReactNode, useEffect, useState } from "react" 4 | import ReactField, { ReactFieldJSON, UNMOUNT } from "./ReactField" 5 | import type { JSONSchema4 } from "json-schema" 6 | import Suspense from "../../ui/Suspense" 7 | import { InputDefinition } from "../toolbox" 8 | import { JDEventSource } from "../../dom/eventsource" 9 | const JSONSchemaForm = lazy(() => import("./JSONSchemaForm")) 10 | 11 | export interface JSONSettingsOptions extends ReactFieldJSON { 12 | schema?: JSONSchema4 13 | } 14 | 15 | export interface JSONSettingsInputDefinition extends InputDefinition { 16 | type: "ds_field_json_settings" 17 | schema: JSONSchema4 18 | } 19 | 20 | function JSONSettingsWidget(props: { 21 | schema: JSONSchema4 22 | value: any 23 | events: JDEventSource 24 | setValue: (newValue: any) => void 25 | }) { 26 | const { schema, value, setValue, events } = props 27 | const [editValue, setEditValue] = useState(value) 28 | 29 | useEffect( 30 | () => events.subscribe(UNMOUNT, () => setValue(editValue)), 31 | [editValue] 32 | ) 33 | 34 | return ( 35 |
42 | 43 | 48 | 49 |
50 | ) 51 | } 52 | 53 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 54 | export default class JSONSettingsField extends ReactField { 55 | static KEY = "ds_field_json_settings" 56 | SERIALIZABLE = true 57 | schema: JSONSchema4 58 | 59 | static fromJson(options: ReactFieldJSON) { 60 | return new JSONSettingsField(options?.value, undefined, options) 61 | } 62 | 63 | // the first argument is a dummy and never used 64 | constructor(value: string, validator?: any, options?: JSONSettingsOptions) { 65 | super(value, validator, options) 66 | this.schema = options?.schema || {} 67 | this.darkMode = "light" 68 | } 69 | 70 | protected initCustomView(): SVGElement { 71 | const group = this.fieldGroup_ 72 | group.classList.add("blocklyFieldButton") 73 | return undefined 74 | } 75 | 76 | get defaultValue() { 77 | return {} 78 | } 79 | 80 | getText_() { 81 | return "⚙️" 82 | } 83 | 84 | renderField(): ReactNode { 85 | const { schema, value = {}, events } = this 86 | const setValue = (v: any) => (this.value = v) 87 | return ( 88 | 94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/components/blockly/fields/PointerBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React, { PointerEvent, ReactNode, useContext } from "react" 2 | import WorkspaceContext from "../WorkspaceContext" 3 | 4 | export function PointerBoundary(props: { 5 | className?: string 6 | children: ReactNode 7 | }) { 8 | const { dragging } = useContext(WorkspaceContext) 9 | const { className, children } = props 10 | const onPointerStopPropagation = (event: PointerEvent) => { 11 | // make sure blockly does not handle drags when interacting with UI 12 | if (!dragging) event.stopPropagation() 13 | } 14 | return ( 15 |
22 | {children} 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/blockly/fields/ReactFieldBase.ts: -------------------------------------------------------------------------------- 1 | import Blockly from "blockly" 2 | 3 | export class ReactFieldBase extends Blockly.Field { 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | constructor( 6 | value: string, 7 | validator?: any, 8 | options?: any, 9 | size?: { width: number; height: number } 10 | ) { 11 | super(value, validator, options) 12 | if (size) this.size_ = new Blockly.utils.Size(size.width, size.height) 13 | } 14 | 15 | get defaultValue(): T { 16 | return {} as T 17 | } 18 | 19 | get value(): T { 20 | try { 21 | const v = JSON.parse(this.getValue()) 22 | return (v || this.defaultValue) as T 23 | } catch (e) { 24 | console.warn(e) 25 | return this.defaultValue 26 | } 27 | } 28 | 29 | set value(v: T) { 30 | this.setValue(JSON.stringify(v)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/blockly/fields/ReactImageField.tsx: -------------------------------------------------------------------------------- 1 | import ReactField from "./ReactField" 2 | import { child } from "../../widgets/svg" 3 | import { utils } from "blockly" 4 | 5 | export default class ReactImageField extends ReactField { 6 | constructor(value: string, width = 32, height = 32) { 7 | super(value, undefined, undefined, { width, height }) 8 | } 9 | 10 | setSize(width: number, height: number) { 11 | this.size_ = new utils.Size(width, height) 12 | const img = this.view as SVGImageElement 13 | if (img) { 14 | img.setAttribute("width", width + "") 15 | img.setAttribute("height", height + "") 16 | } 17 | } 18 | 19 | protected updateView() { 20 | const imgUri = this.renderValue() 21 | const img = this.view as SVGImageElement 22 | if (imgUri) { 23 | img?.setAttributeNS( 24 | "http://www.w3.org/1999/xlink", 25 | "xlink:href", 26 | imgUri 27 | ) 28 | img?.setAttribute("alt", this.getText()) 29 | } 30 | } 31 | 32 | /** 33 | * Renders the value to a data uri string 34 | */ 35 | protected renderValue(): string { 36 | return undefined 37 | } 38 | 39 | protected initCustomView() { 40 | const { width, height } = this.size_ 41 | return child(this.fieldGroup_, "image", { 42 | height, 43 | width, 44 | alt: this.getText(), 45 | }) as SVGImageElement 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/blockly/fields/SliderField.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Slider } from "@mui/material" 2 | import React, { ReactNode, useContext } from "react" 3 | import ReactField from "./ReactField" 4 | import ValueContext, { ValueContextProps } from "./ValueContext" 5 | 6 | function FieldWithSlider(props: { children: ReactNode }) { 7 | const { children } = props 8 | const { value, onValueChange } = 9 | useContext>(ValueContext) 10 | const handleChange = async (ev: unknown, nv: number | number[]) => { 11 | const newValue = nv as number 12 | onValueChange(newValue) 13 | } 14 | return ( 15 | 16 | 17 | {children} 18 | 19 | 20 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default class SliderField extends ReactField { 37 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 38 | constructor(value: string, options?: any) { 39 | super(value, undefined, options) 40 | } 41 | 42 | get defaultValue() { 43 | return 0 44 | } 45 | 46 | getText_() { 47 | return this.value + "" 48 | } 49 | 50 | renderField(): ReactNode { 51 | return {this.renderWidget()} 52 | } 53 | 54 | renderWidget(): ReactNode { 55 | return null 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/blockly/fields/ValueContext.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import React, { createContext, ReactNode, useState } from "react" 3 | 4 | export interface ValueContextProps { 5 | value: T 6 | onValueChange: (value: T) => void 7 | } 8 | 9 | export const ValueContext = createContext({ 10 | value: undefined, 11 | onValueChange: undefined, 12 | }) 13 | ValueContext.displayName = "Value" 14 | 15 | export default ValueContext 16 | 17 | export function ValueProvider(props: { 18 | value: any 19 | onValueChange?: (newValue: any) => any 20 | children: ReactNode 21 | }) { 22 | const { 23 | children, 24 | value: initialValue, 25 | onValueChange: onFieldValueChange, 26 | } = props 27 | const [value, setValue] = useState(initialValue) 28 | const onValueChange = (newValue: any) => { 29 | setValue(newValue) 30 | onFieldValueChange?.(newValue) 31 | } 32 | return ( 33 | // eslint-disable-next-line react/react-in-jsx-scope 34 | 35 | {children} 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/blockly/fields/chart/BarField.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react" 2 | import WorkspaceContext from "../../WorkspaceContext" 3 | import { ReactFieldJSON } from "../ReactField" 4 | import ReactInlineField from "../ReactInlineField" 5 | import useBlockData from "../../useBlockData" 6 | import type { VisualizationSpec } from "react-vega" 7 | import VegaLiteWidget from "./VegaLiteWidget" 8 | import { tidyResolveHeader } from "../tidy" 9 | import { BAR_CORNER_RADIUS, BAR_MAX_ITEMS } from "../../toolbox" 10 | 11 | const stacks = { 12 | stack: "zero", 13 | unstack: null, 14 | normalize: "normalize", 15 | } 16 | 17 | const nonSummative = ["mean", "median", "variance", "stdev", "min", "max"] 18 | 19 | function BarWidget() { 20 | const { sourceBlock } = useContext(WorkspaceContext) 21 | const { data } = useBlockData(sourceBlock) 22 | const index = tidyResolveHeader(data, sourceBlock?.getFieldValue("index")) 23 | const yAggregate = sourceBlock?.getFieldValue("yAggregate") || "mean" 24 | const group = sourceBlock?.getFieldValue("group") 25 | const value = tidyResolveHeader( 26 | data, 27 | sourceBlock?.getFieldValue("value"), 28 | "number" 29 | ) 30 | let stackValue = sourceBlock?.getFieldValue("stack") 31 | if (nonSummative.includes(yAggregate)) stackValue = "xOffset" 32 | const stack = stacks[stackValue] 33 | if (!index || !value) return null 34 | 35 | const sliceOptions = { 36 | sliceMax: BAR_MAX_ITEMS, 37 | } 38 | const spec: VisualizationSpec = { 39 | mark: { type: "bar", cornerRadius: BAR_CORNER_RADIUS, tooltip: true }, 40 | encoding: { 41 | x: { field: index, type: "nominal", sort: null }, 42 | y: { field: value, type: "quantitative", stack }, 43 | }, 44 | data: { name: "values" }, 45 | } 46 | if (yAggregate !== undefined) 47 | (spec.encoding.y as any).aggregate = yAggregate 48 | // stacking 49 | if (stack === null) spec.encoding.opacity = { value: 0.7 } 50 | if (stackValue === "xOffset" && group) { 51 | spec.encoding.xOffset = { field: group } 52 | } 53 | return 54 | } 55 | 56 | export default class BarField extends ReactInlineField { 57 | static KEY = "ds_field_bar_plot" 58 | EDITABLE = false 59 | 60 | static fromJson(options: ReactFieldJSON) { 61 | return new BarField(options) 62 | } 63 | 64 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 65 | constructor(options?: any) { 66 | super(options) 67 | } 68 | 69 | renderInlineField() { 70 | return 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/blockly/fields/chart/BoxPlotField.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react" 2 | import WorkspaceContext from "../../WorkspaceContext" 3 | import { ReactFieldJSON } from "../ReactField" 4 | import ReactInlineField from "../ReactInlineField" 5 | import useBlockData from "../../useBlockData" 6 | import type { VisualizationSpec } from "react-vega" 7 | import VegaLiteWidget from "./VegaLiteWidget" 8 | import { tidyResolveHeader } from "../tidy" 9 | import { BAR_CORNER_RADIUS } from "../../toolbox" 10 | 11 | function BoxPlotWidget() { 12 | const { sourceBlock } = useContext(WorkspaceContext) 13 | const { data } = useBlockData(sourceBlock) 14 | const index = tidyResolveHeader(data, sourceBlock?.getFieldValue("index")) 15 | const value = tidyResolveHeader( 16 | data, 17 | sourceBlock?.getFieldValue("value"), 18 | "number" 19 | ) 20 | if (!index || !value) return null 21 | 22 | const spec: VisualizationSpec = { 23 | mark: { 24 | type: "boxplot", 25 | cornerRadius: BAR_CORNER_RADIUS, 26 | tooltip: true, 27 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 28 | } as any, 29 | encoding: { 30 | y: { field: index, type: "nominal", scale: { zero: false } }, 31 | x: { field: value, type: "quantitative", scale: { zero: false } }, 32 | color: { field: index, type: "nominal", legend: null }, 33 | tooltip: { 34 | field: value, 35 | type: "quantitative", 36 | }, 37 | }, 38 | data: { name: "values" }, 39 | } 40 | 41 | return 42 | } 43 | 44 | export default class BoxPlotField extends ReactInlineField { 45 | static KEY = "ds_field_box_plot" 46 | EDITABLE = false 47 | 48 | static fromJson(options: ReactFieldJSON) { 49 | return new BoxPlotField(options) 50 | } 51 | 52 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | constructor(options?: any) { 54 | super(options) 55 | } 56 | 57 | renderInlineField() { 58 | return 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/blockly/fields/chart/CorrelationHeapMapField.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { ReactFieldJSON } from "../ReactField" 3 | import ReactInlineField from "../ReactInlineField" 4 | import type { VisualizationSpec } from "react-vega" 5 | import VegaLiteWidget from "./VegaLiteWidget" 6 | 7 | function CorrelationHeatMapWidget() { 8 | const spec: VisualizationSpec = { 9 | mark: { type: "rect", tooltip: true }, 10 | config: { 11 | axis: { grid: true, tickBand: "extent" }, 12 | }, 13 | encoding: { 14 | x: { 15 | field: "row", 16 | type: "nominal", 17 | sort: "ascending", 18 | axis: { title: null }, 19 | }, 20 | y: { 21 | field: "column", 22 | type: "nominal", 23 | sort: "ascending", 24 | axis: { title: null }, 25 | }, 26 | color: { 27 | field: "correlation", 28 | type: "quantitative", 29 | }, 30 | }, 31 | data: { name: "values" }, 32 | } 33 | return 34 | } 35 | 36 | export default class CorrelationHeatMapField extends ReactInlineField { 37 | static KEY = "ds_field_correlation_heat_map" 38 | EDITABLE = false 39 | 40 | static fromJson(options: ReactFieldJSON) { 41 | return new CorrelationHeatMapField(options) 42 | } 43 | 44 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 45 | constructor(options?: any) { 46 | super(options) 47 | } 48 | 49 | renderInlineField() { 50 | return 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/blockly/fields/chart/HeatMapField.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react" 2 | import WorkspaceContext from "../../WorkspaceContext" 3 | import { ReactFieldJSON } from "../ReactField" 4 | import ReactInlineField from "../ReactInlineField" 5 | import useBlockData from "../../useBlockData" 6 | import type { VisualizationSpec } from "react-vega" 7 | import VegaLiteWidget from "./VegaLiteWidget" 8 | import { tidyResolveHeader } from "../tidy" 9 | import { LINE_MAX_ITEMS } from "../../toolbox" 10 | 11 | function HeatMapWidget() { 12 | const { sourceBlock } = useContext(WorkspaceContext) 13 | const { data } = useBlockData(sourceBlock) 14 | const x = tidyResolveHeader(data, sourceBlock?.getFieldValue("x")) 15 | const y = tidyResolveHeader(data, sourceBlock?.getFieldValue("y")) 16 | const color = tidyResolveHeader( 17 | data, 18 | sourceBlock?.getFieldValue("color"), 19 | "number" 20 | ) 21 | if (!x || !y || !color) return null 22 | 23 | const sliceOptions = { 24 | sliceHead: LINE_MAX_ITEMS, 25 | } 26 | const spec: VisualizationSpec = { 27 | mark: { type: "rect", tooltip: true }, 28 | encoding: { 29 | x: { field: x, type: "ordinal" }, 30 | y: { field: y, type: "ordinal" }, 31 | color: { 32 | field: color, 33 | type: "quantitative", 34 | scale: { zero: false }, 35 | }, 36 | }, 37 | data: { name: "values" }, 38 | } 39 | return 40 | } 41 | 42 | export default class HeatMapPlotField extends ReactInlineField { 43 | static KEY = "ds_field_heat_map" 44 | EDITABLE = false 45 | 46 | static fromJson(options: ReactFieldJSON) { 47 | return new HeatMapPlotField(options) 48 | } 49 | 50 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 51 | constructor(options?: any) { 52 | super(options) 53 | } 54 | 55 | renderInlineField() { 56 | return 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/blockly/fields/chart/HistogramField.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react" 2 | import WorkspaceContext from "../../WorkspaceContext" 3 | import { ReactFieldJSON } from "../ReactField" 4 | import ReactInlineField from "../ReactInlineField" 5 | import useBlockData from "../../useBlockData" 6 | import type { VisualizationSpec } from "react-vega" 7 | import VegaLiteWidget from "./VegaLiteWidget" 8 | import { tidyResolveHeader } from "../tidy" 9 | import { BAR_CORNER_RADIUS } from "../../toolbox" 10 | 11 | function HistogramWidget() { 12 | const { sourceBlock } = useContext(WorkspaceContext) 13 | const { data } = useBlockData(sourceBlock) 14 | const index = tidyResolveHeader(data, sourceBlock?.getFieldValue("index")) 15 | 16 | if (!index) return null 17 | 18 | const spec: VisualizationSpec = { 19 | mark: { type: "bar", cornerRadius: BAR_CORNER_RADIUS, tooltip: true }, 20 | encoding: { 21 | x: { bin: true, field: index }, 22 | y: { aggregate: "count" }, 23 | }, 24 | data: { name: "values" }, 25 | } 26 | 27 | return 28 | } 29 | 30 | export default class HistogramField extends ReactInlineField { 31 | static KEY = "ds_field_histogram" 32 | EDITABLE = false 33 | 34 | static fromJson(options: ReactFieldJSON) { 35 | return new HistogramField(options) 36 | } 37 | 38 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 39 | constructor(options?: any) { 40 | super(options) 41 | } 42 | 43 | renderInlineField() { 44 | return 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/blockly/fields/chart/LinePlotField.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react" 2 | import WorkspaceContext from "../../WorkspaceContext" 3 | import { ReactFieldJSON } from "../ReactField" 4 | import ReactInlineField from "../ReactInlineField" 5 | import useBlockData from "../../useBlockData" 6 | import type { VisualizationSpec } from "react-vega" 7 | import VegaLiteWidget from "./VegaLiteWidget" 8 | import { tidyResolveHeader } from "../tidy" 9 | import { LINE_MAX_ITEMS, resolveBlockDefinition } from "../../toolbox" 10 | 11 | function LinePlotWidget() { 12 | const { sourceBlock } = useContext(WorkspaceContext) 13 | const { data } = useBlockData(sourceBlock) 14 | const def = resolveBlockDefinition(sourceBlock.type) 15 | const x = tidyResolveHeader( 16 | data, 17 | def.args0[0].name === "x" ? sourceBlock?.getFieldValue("x") : "time", 18 | "number" 19 | ) 20 | const ys = ["y", "y2", "y3"] 21 | .map(n => 22 | tidyResolveHeader(data, sourceBlock?.getFieldValue(n), "number") 23 | ) 24 | .filter(y => !!y) 25 | if (!x || !ys.length) return null 26 | 27 | const timeSeries = x === "time" 28 | 29 | const sliceOptions = { 30 | sliceHead: LINE_MAX_ITEMS, 31 | } 32 | const spec: VisualizationSpec = { 33 | layer: ys.map(y => ({ 34 | mark: { 35 | type: "line", 36 | tooltip: true, 37 | point: { filled: timeSeries }, 38 | }, 39 | encoding: { 40 | x: { field: x, type: "quantitative", scale: { zero: false } }, 41 | y: { field: y, type: "quantitative", scale: { zero: false } }, 42 | }, 43 | data: { name: "values" }, 44 | })), 45 | } 46 | return 47 | } 48 | 49 | export default class LinePlotField extends ReactInlineField { 50 | static KEY = "ds_field_line_plot" 51 | EDITABLE = false 52 | 53 | static fromJson(options: ReactFieldJSON) { 54 | return new LinePlotField(options) 55 | } 56 | 57 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 58 | constructor(options?: any) { 59 | super(options) 60 | } 61 | 62 | renderInlineField() { 63 | return 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/blockly/fields/chart/PieChartField.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react" 2 | import WorkspaceContext from "../../WorkspaceContext" 3 | import { ReactFieldJSON } from "../ReactField" 4 | import ReactInlineField from "../ReactInlineField" 5 | import useBlockData from "../../useBlockData" 6 | import type { VisualizationSpec } from "react-vega" 7 | import VegaLiteWidget from "./VegaLiteWidget" 8 | import { tidyResolveHeader } from "../tidy" 9 | import { PIE_MAX_ITEMS } from "../../toolbox" 10 | 11 | function PieChartWidget() { 12 | const { sourceBlock } = useContext(WorkspaceContext) 13 | const { data } = useBlockData(sourceBlock) 14 | const value = tidyResolveHeader(data, sourceBlock?.getFieldValue("value")) 15 | const color = value 16 | const aggregate = "count" 17 | 18 | if (!value) return null 19 | 20 | const sliceOptions = { 21 | sliceMax: PIE_MAX_ITEMS, 22 | } 23 | const spec: VisualizationSpec = { 24 | mark: { type: "arc", tooltip: true }, 25 | encoding: { 26 | theta: { 27 | field: value, 28 | type: "quantitative", 29 | aggregate, 30 | stack: "normalize", 31 | }, 32 | color: { field: color, type: "nominal" }, 33 | }, 34 | data: { name: "values" }, 35 | } 36 | return 37 | } 38 | 39 | export default class PieChartField extends ReactInlineField { 40 | static KEY = "ds_field_piechart_plot" 41 | EDITABLE = false 42 | 43 | static fromJson(options: ReactFieldJSON) { 44 | return new PieChartField(options) 45 | } 46 | 47 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 48 | constructor(options?: any) { 49 | super(options) 50 | } 51 | 52 | renderInlineField() { 53 | return 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/blockly/fields/chart/ScatterPlotMatrixField.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react" 2 | import WorkspaceContext from "../../WorkspaceContext" 3 | import { ReactFieldJSON } from "../ReactField" 4 | import ReactInlineField from "../ReactInlineField" 5 | import useBlockData from "../../useBlockData" 6 | import type { VisualizationSpec } from "react-vega" 7 | import VegaLiteWidget from "./VegaLiteWidget" 8 | import { tidyResolveHeader } from "../tidy" 9 | import { resolveColumns } from "../DataColumnChooserField" 10 | 11 | function ScatterPlotMatrixWidget() { 12 | const { sourceBlock } = useContext(WorkspaceContext) 13 | const { data } = useBlockData(sourceBlock) 14 | const index = tidyResolveHeader(data, sourceBlock?.getFieldValue("index")) 15 | 16 | const columns = resolveColumns(data, sourceBlock, 4) 17 | 18 | if (columns.length < 3) return null 19 | 20 | const spec: VisualizationSpec = { 21 | width: 480, 22 | data: { name: "values" }, 23 | repeat: { 24 | row: columns.slice(0), 25 | column: columns.reverse().slice(0), 26 | }, 27 | spec: { 28 | width: 76, 29 | height: 76, 30 | mark: { type: "point", filled: true }, 31 | encoding: { 32 | x: { 33 | field: { repeat: "column" }, 34 | type: "quantitative", 35 | scale: { zero: false }, 36 | }, 37 | y: { 38 | field: { repeat: "row" }, 39 | type: "quantitative", 40 | scale: { zero: false }, 41 | }, 42 | color: index ? { field: index, type: "nominal" } : undefined, 43 | }, 44 | }, 45 | } 46 | 47 | return 48 | } 49 | 50 | export default class ScatterPlotMatrixField extends ReactInlineField { 51 | static KEY = "ds_field_scatterplot_matrix_plot" 52 | EDITABLE = false 53 | 54 | static fromJson(options: ReactFieldJSON) { 55 | return new ScatterPlotMatrixField(options) 56 | } 57 | 58 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 59 | constructor(options?: any) { 60 | super(options) 61 | } 62 | 63 | renderInlineField() { 64 | return 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/blockly/fields/chart/VegaChartField.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react" 2 | import WorkspaceContext, { 3 | resolveWorkspaceServices, 4 | WorkspaceServices, 5 | } from "../../WorkspaceContext" 6 | import { ReactFieldJSON } from "../ReactField" 7 | import ReactInlineField from "../ReactInlineField" 8 | import useBlockData from "../../useBlockData" 9 | import VegaLiteWidget from "./VegaLiteWidget" 10 | import { blockToVisualizationSpec } from "../../dsl/chartdsl" 11 | import { useEffect } from "react" 12 | 13 | function VegaChartWidget() { 14 | const { sourceBlock, workspace } = useContext(WorkspaceContext) 15 | const { data } = useBlockData(sourceBlock) 16 | const services = resolveWorkspaceServices(workspace) 17 | 18 | // track workspace changes and re-render 19 | const [, setWorkspaceJSON] = useState(services?.workspaceJSON) 20 | useEffect( 21 | () => 22 | services?.subscribe(WorkspaceServices.WORKSPACE_CHANGE, () => 23 | setWorkspaceJSON(services.workspaceJSON) 24 | ), 25 | [services] 26 | ) 27 | 28 | const spec = blockToVisualizationSpec(sourceBlock, data) 29 | return 30 | } 31 | 32 | export default class VegaChartField extends ReactInlineField { 33 | static KEY = "ds_field_vega_chart" 34 | EDITABLE = false 35 | 36 | static fromJson(options: ReactFieldJSON) { 37 | return new VegaChartField(options) 38 | } 39 | 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | constructor(options?: any) { 42 | super(options) 43 | } 44 | 45 | renderInlineField() { 46 | return 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/blockly/fields/fields.ts: -------------------------------------------------------------------------------- 1 | import Blockly from "blockly" 2 | import { BlockDefinition } from "../toolbox" 3 | import DataTableField from "./DataTableField" 4 | import DataColumnChooserField from "./DataColumnChooserField" 5 | import BuiltinDataSetField from "./BuiltinDataSetField" 6 | import DataPreviewField from "./DataPreviewField" 7 | 8 | import LinePlotField from "./chart/LinePlotField" 9 | import ScatterPlotField from "./chart/ScatterPlotField" 10 | import BarChartField from "./chart/BarField" 11 | import HistogramField from "./chart/HistogramField" 12 | import BoxPlotField from "./chart/BoxPlotField" 13 | import HeatMapPlotField from "./chart/HeatMapField" 14 | import VegaChartField from "./chart/VegaChartField" 15 | import ScatterPlotMatrixField from "./chart/ScatterPlotMatrixField" 16 | import CorrelationHeapMapField from "./chart/CorrelationHeapMapField" 17 | 18 | import FileSaveField from "./FileSaveField" 19 | import FileOpenField from "./FileOpenField" 20 | 21 | import JSONSettingsField from "./JSONSettingsField" 22 | import IFrameDataChooserField from "./IFrameDataChooserField" 23 | import PieChartField from "./chart/PieChartField" 24 | 25 | let reactFieldShadows: BlockDefinition[] 26 | export function registerFields() { 27 | if (reactFieldShadows) return 28 | 29 | reactFieldShadows = [] 30 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 31 | const registerType = (fieldType: any) => { 32 | const key = fieldType.KEY 33 | try { 34 | Blockly.fieldRegistry.unregister(key) // hot reload issues 35 | } catch (e) { 36 | // ignore hot reload issues 37 | } 38 | Blockly.fieldRegistry.register(key, fieldType) 39 | if (fieldType.SHADOW) reactFieldShadows.push(fieldType.SHADOW) 40 | } 41 | const fieldTypes = [ 42 | DataTableField, 43 | DataPreviewField, 44 | DataColumnChooserField, 45 | 46 | BuiltinDataSetField, 47 | 48 | ScatterPlotField, 49 | LinePlotField, 50 | BarChartField, 51 | PieChartField, 52 | HistogramField, 53 | BoxPlotField, 54 | HeatMapPlotField, 55 | CorrelationHeapMapField, 56 | ScatterPlotMatrixField, 57 | VegaChartField, 58 | 59 | FileSaveField, 60 | FileOpenField, 61 | 62 | JSONSettingsField, 63 | 64 | IFrameDataChooserField, 65 | ] 66 | fieldTypes.forEach(registerType) 67 | } 68 | 69 | export function fieldShadows() { 70 | registerFields() 71 | return reactFieldShadows.slice(0) 72 | } 73 | -------------------------------------------------------------------------------- /src/components/blockly/useBlockData.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BlockServices, 3 | BlockWithServices, 4 | TRANSFORMED_DATA_CHANGE, 5 | } from "./WorkspaceContext" 6 | import { Block } from "blockly" 7 | import { useCallback, useEffect } from "react" 8 | import useChangeThrottled from "../dom/useChangeThrottled" 9 | import useDragDebounce from "./useDragDebounce" 10 | import useEventRaised from "../dom/useEventRaised" 11 | 12 | /** 13 | * Hook that retreives data associated to a block; triggers re-render when data is updated. 14 | */ 15 | // eslint-disable-next-line @typescript-eslint/ban-types 16 | export default function useBlockData( 17 | block: Block, 18 | initialValue?: T[], 19 | throttleTime?: number 20 | ) { 21 | const services = (block as unknown as BlockWithServices)?.blockServices 22 | const data = useChangeThrottled( 23 | services, 24 | _ => _?.data as T[], 25 | throttleTime 26 | ) 27 | const transformedData = useEventRaised( 28 | TRANSFORMED_DATA_CHANGE, 29 | services, 30 | _ => _?.transformedData as T[] 31 | ) 32 | const setData = useCallback( 33 | (value: T[]) => { 34 | if (services) services.data = value 35 | }, 36 | [services] 37 | ) 38 | 39 | // set initial value 40 | useEffect(() => { 41 | if ( 42 | services && 43 | initialValue !== undefined && 44 | services.data === undefined 45 | ) 46 | services.data = initialValue 47 | }, [services]) 48 | 49 | // debounce with dragging 50 | const debounced = useDragDebounce(data) 51 | const debouncedTransformedData = useDragDebounce(transformedData) 52 | 53 | return { 54 | data: debounced, 55 | transformedData: debouncedTransformedData, 56 | setData, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/blockly/useBlocklyEvents.ts: -------------------------------------------------------------------------------- 1 | import Blockly from "blockly" 2 | import { useCallback } from "react" 3 | import ReactField from "./fields/ReactField" 4 | import useWorkspaceEvent from "./useWorkspaceEvent" 5 | 6 | /** 7 | * The glue between blockly change events and the React contexts. 8 | * @param workspace 9 | */ 10 | // do not use block context 11 | export default function useBlocklyEvents(workspace: Blockly.WorkspaceSvg) { 12 | const handleChange = useCallback( 13 | (event: Blockly.Events.Abstract & { type: string }) => { 14 | const { type } = event 15 | switch (type) { 16 | case Blockly.Events.BLOCK_CHANGE: { 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | const change = event as any as Blockly.Events.Change 19 | const block = workspace.getBlockById(change.blockId) 20 | // notify twin that the value changed 21 | const twinInput = block.inputList[1] 22 | const twinField = twinInput?.fieldRow.find( 23 | f => f.name === "twin" 24 | ) as ReactField 25 | twinField?.emitChange?.() 26 | break 27 | } 28 | } 29 | }, 30 | [workspace] 31 | ) 32 | useWorkspaceEvent(workspace, handleChange) 33 | } 34 | -------------------------------------------------------------------------------- /src/components/blockly/useBlocklyPlugins.ts: -------------------------------------------------------------------------------- 1 | import Blockly, { ContextMenuRegistry } from "blockly" 2 | import "@blockly/field-slider" 3 | import "@blockly/block-dynamic-connection" 4 | import { useEffect } from "react" 5 | import { DisableTopBlocks } from "@blockly/disable-top-blocks" 6 | import { UIFlags } from "../uiflags" 7 | 8 | /** 9 | * Configures various blockly plugins 10 | */ 11 | // do not use block context 12 | export default function useBlocklyPlugins(workspace: Blockly.WorkspaceSvg) { 13 | //plugins 14 | useEffect(() => { 15 | if (!workspace) return 16 | 17 | // context menu stuff 18 | if (ContextMenuRegistry.registry.getItem("blockInline")) 19 | ContextMenuRegistry.registry.unregister("blockInline") 20 | if (ContextMenuRegistry.registry.getItem("cleanWorkspace")) 21 | ContextMenuRegistry.registry.unregister("cleanWorkspace") 22 | 23 | if (!UIFlags.screenshot) { 24 | // The plugin must be initialized before it has any effect. 25 | // Add the disableOrphans event handler. This is not done automatically by 26 | // the plugin and should be handled by your application. 27 | workspace.addChangeListener(Blockly.Events.disableOrphans) 28 | const disableTopBlocksPlugin = new DisableTopBlocks() 29 | disableTopBlocksPlugin.init() 30 | } 31 | return () => 32 | workspace.removeChangeListener(Blockly.Events.disableOrphans) 33 | }, [workspace]) 34 | } 35 | -------------------------------------------------------------------------------- /src/components/blockly/useDragDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from "react" 2 | import WorkspaceContext from "./WorkspaceContext" 3 | 4 | /** 5 | * Tracks block dragging behavior in blockly to avoid interference 6 | */ 7 | export default function useDragDebounce(value: T): T { 8 | const { dragging } = useContext(WorkspaceContext) 9 | const [valueAtDragging, setValueAtDragging] = useState(value) 10 | 11 | // record value when starting to drag 12 | useEffect(() => { 13 | if (dragging) setValueAtDragging(value) 14 | }, [dragging]) 15 | 16 | // return value at dragging until drag is completed 17 | return dragging ? valueAtDragging || value : value 18 | } 19 | -------------------------------------------------------------------------------- /src/components/blockly/useWorkspaceEvent.ts: -------------------------------------------------------------------------------- 1 | import { Events, Workspace } from "blockly" 2 | import { useEffect } from "react" 3 | 4 | /** 5 | * A hook to register event on a blockly workspace 6 | */ 7 | export default function useWorkspaceEvent( 8 | workspace: Workspace, 9 | handler: (event: Events.Abstract & { type: string }) => void 10 | ) { 11 | // register hook 12 | useEffect(() => { 13 | workspace?.addChangeListener(handler) 14 | return () => workspace?.removeChangeListener(handler) 15 | }, [workspace, handler]) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/dom/constants.ts: -------------------------------------------------------------------------------- 1 | export const DS_EDITOR_ID = "ds" 2 | export const MAX_SERVICES_LENGTH = 59 3 | 4 | export const NEW_LISTENER = "newListener" 5 | export const REMOVE_LISTENER = "removeListener" 6 | 7 | export const CONNECTION_STATE = "connectionState" 8 | export const CONNECT = "connect" 9 | export const LOST = "lost" 10 | export const FOUND = "found" 11 | export const CONNECTING = "connecting" 12 | export const DISCONNECT = "disconnect" 13 | export const DISCONNECTING = "disconnecting" 14 | export const ANNOUNCE = "announce" 15 | export const START = "start" 16 | export const RESTART = "restart" 17 | export const STOP = "stop" 18 | export const CHANGE = "change" 19 | export const EVENT = "event" 20 | export const RENDER = "render" 21 | export const REFRESH = "refresh" 22 | export const MESSAGE = "message" 23 | 24 | export const ERROR = "error" 25 | export const PANIC = "panic" 26 | export const TRACE = "trace" 27 | export const TIMEOUT = "timeout" 28 | export const TIMEOUT_DISCONNECT = "timeoutDisconnect" 29 | 30 | export type DataType = "string" | "number" | "boolean" -------------------------------------------------------------------------------- /src/components/dom/node.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { JDEventSource } from "./eventsource" 3 | 4 | /** 5 | * Base class for JDOM Node classes. 6 | * @category JDOM 7 | */ 8 | export abstract class JDNode extends JDEventSource { 9 | private _nodeData: Record 10 | 11 | constructor() { 12 | super() 13 | } 14 | 15 | /** 16 | * Globally unique identifier in the tree 17 | * @category JDOM 18 | */ 19 | abstract get id(): string 20 | 21 | /** 22 | * Gets a kind identifier useful for UI descriptions 23 | * @category JDOM 24 | */ 25 | abstract get nodeKind(): string 26 | 27 | /** 28 | * Gets the local name 29 | * @category JDOM 30 | */ 31 | abstract get name(): string 32 | 33 | /** 34 | * A human friendly name 35 | * @category JDOM 36 | */ 37 | get friendlyName(): string { 38 | return this.name 39 | } 40 | 41 | /** 42 | * Gets the name including parents 43 | * @category JDOM 44 | */ 45 | abstract get qualifiedName(): string 46 | 47 | /** 48 | * Gets the parent node in the dom 49 | * @category JDOM 50 | */ 51 | abstract get parent(): JDNode 52 | 53 | /** 54 | * Gets the children of the current node 55 | * @category JDOM 56 | */ 57 | abstract get children(): JDNode[] 58 | 59 | /** 60 | * Gets a databag to store custom information 61 | * @category JDOM 62 | */ 63 | get nodeData() { 64 | if (!this._nodeData) this._nodeData = {} 65 | return this._nodeData 66 | } 67 | 68 | /** 69 | * Emit event in current node and parent nodes 70 | * @param event event to emit 71 | * @param arg event arguments 72 | * @category JDOM 73 | */ 74 | emitPropagated(event: string, arg?: any) { 75 | let current = this as JDNode 76 | while (current) { 77 | current.emit(event, arg || this) 78 | current = current.parent 79 | } 80 | } 81 | 82 | /** 83 | * @hidden 84 | */ 85 | toString() { 86 | return this.friendlyName 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/components/dom/observable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | */ 4 | export interface Observer { 5 | next?: (value: T) => void 6 | error?: (error: Error) => void 7 | complete?: () => void 8 | } 9 | 10 | /** 11 | * @internal 12 | */ 13 | export interface Observable { 14 | subscribe(observer: Observer): { 15 | unsubscribe: () => void 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/dom/useChange.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react" 2 | import useEffectAsync from "../hooks/useEffectAsync" 3 | import { CHANGE } from "./constants" 4 | import { IEventSource } from "./eventsource" 5 | import useEventRaised from "./useEventRaised" 6 | 7 | export default function useChange( 8 | node: TNode, 9 | query?: (n: TNode) => TValue, 10 | deps?: React.DependencyList, 11 | isEqual?: (a: TValue, b: TValue) => boolean 12 | ): TValue { 13 | return useEventRaised(CHANGE, node, query, deps, isEqual) 14 | } 15 | 16 | export function useChangeAsync( 17 | node: TNode, 18 | query?: (n: TNode) => Promise, 19 | deps?: React.DependencyList 20 | ): TValue { 21 | const [version, setVersion] = useState(node?.changeId || 0) 22 | const [value, setValue] = useState(undefined) 23 | 24 | useEffect(() => { 25 | setVersion(node?.changeId || 0) 26 | node?.subscribe(CHANGE, () => { 27 | setVersion(node.changeId) 28 | }) 29 | }, [node]) 30 | 31 | useEffectAsync( 32 | async mounted => { 33 | const valuePromise = query ? query(node) : undefined 34 | if (!valuePromise) { 35 | if (mounted()) setValue(undefined) 36 | } else { 37 | const d = await valuePromise 38 | if (mounted()) setValue(d) 39 | } 40 | }, 41 | [node, version, ...(deps || [])] 42 | ) 43 | 44 | return value 45 | } -------------------------------------------------------------------------------- /src/components/dom/useChangeThrottled.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react" 2 | import { useThrottledCallback } from "use-debounce" 3 | import { CHANGE } from "./constants" 4 | import { IEventSource } from "./eventsource" 5 | 6 | const DEFAULT_THROTTLE = 200 7 | export default function useChangeThrottled( 8 | node: TNode, 9 | query?: (n: TNode) => TValue, 10 | time?: number, 11 | deps?: React.DependencyList 12 | ): TValue { 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | const [version, setVersion] = useState(node?.changeId || 0) 15 | const value = query ? query(node) : undefined 16 | const throttledSetVersion = useThrottledCallback( 17 | setVersion, 18 | time || DEFAULT_THROTTLE 19 | ) 20 | 21 | useEffect( 22 | () => node?.subscribe(CHANGE, () => throttledSetVersion(node.changeId)), 23 | [node, ...(deps || [])] 24 | ) 25 | 26 | return value 27 | } 28 | -------------------------------------------------------------------------------- /src/components/dom/useEventRaised.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert" 2 | import { useMemo, DependencyList } from "react" 3 | import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector" 4 | import { IEventSource } from "./eventsource" 5 | 6 | /** 7 | * A hook to track event and update a state snapshot 8 | */ 9 | export default function useEventRaised< 10 | TEventSource extends IEventSource, 11 | TValue 12 | >( 13 | eventName: string | string[], 14 | node: TEventSource, 15 | query: (n: TEventSource) => TValue, 16 | deps?: DependencyList, 17 | isEqual?: (a: TValue, b: TValue) => boolean 18 | ): TValue { 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | assert((node as any) !== false) 21 | const subscription = useMemo( 22 | () => ({ 23 | getSnapshot: () => query?.(node), 24 | selector: _ => _, 25 | subscribe: (onStoreChanged: () => void) => { 26 | const unsubscribe = node?.subscribe(eventName, onStoreChanged) 27 | return () => unsubscribe?.() 28 | }, 29 | isEqual: isEqual, 30 | }), 31 | [node, ...(deps || [])] 32 | ) 33 | return useSyncExternalStoreWithSelector( 34 | subscription.subscribe, 35 | subscription.getSnapshot, 36 | undefined, 37 | subscription.selector, 38 | subscription.isEqual 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/dom/utils.ts: -------------------------------------------------------------------------------- 1 | export function delay(millis: number, value?: T): Promise { 2 | return new Promise(resolve => setTimeout(() => resolve(value), millis)) 3 | } 4 | 5 | export function toMap( 6 | a: T[], 7 | keyConverter: (value: T, index: number) => string, 8 | valueConverter: (value: T, index: number) => V, 9 | ignoreMissingValues?: boolean 10 | ): Record { 11 | const m: Record = {} 12 | if (a) 13 | for (let i = 0; i < a.length; ++i) { 14 | const key = keyConverter(a[i], i) 15 | if (key === undefined || key === null) continue 16 | const v = valueConverter(a[i], i) 17 | if (ignoreMissingValues && (v === undefined || v === null)) continue 18 | m[key] = v 19 | } 20 | return m 21 | } 22 | 23 | export function arrayConcatMany(arrs: T[][]): T[] { 24 | if (!arrs) return undefined 25 | 26 | // weed out empty array 27 | arrs = arrs.filter(a => !!a?.length) 28 | 29 | let sz = 0 30 | for (const buf of arrs) sz += buf.length 31 | const r: T[] = new Array(sz) 32 | sz = 0 33 | for (const arr of arrs) { 34 | for (let i = 0; i < arr.length; ++i) r[i + sz] = arr[i] 35 | sz += arr.length 36 | } 37 | return r 38 | } 39 | 40 | export function inIFrame() { 41 | try { 42 | return typeof window !== "undefined" && window.self !== window.top 43 | } catch (e) { 44 | return typeof window !== "undefined" 45 | } 46 | } 47 | 48 | export function humanify(name: string) { 49 | return name 50 | ?.replace(/([a-z])([A-Z])/g, (_, a, b) => a + " " + b) 51 | .replace(/(-|_)/g, " ") 52 | } 53 | 54 | export function roundWithPrecision( 55 | x: number, 56 | digits: number, 57 | round = Math.round 58 | ): number { 59 | digits = digits | 0 60 | // invalid digits input 61 | if (digits <= 0) return round(x) 62 | if (x == 0) return 0 63 | let r = 0 64 | while (r == 0 && digits < 21) { 65 | const d = Math.pow(10, digits++) 66 | r = round(x * d + Number.EPSILON) / d 67 | } 68 | return r 69 | } 70 | 71 | export function JSONTryParse( 72 | src: string, 73 | defaultValue?: T 74 | ): T | undefined | null { 75 | if (src === undefined) return undefined 76 | if (src === null) return null 77 | 78 | try { 79 | return JSON.parse(src) as T 80 | } catch (e) { 81 | return defaultValue 82 | } 83 | } 84 | 85 | export function unique(values: string[]) { 86 | return [...new Set(values)] 87 | } 88 | -------------------------------------------------------------------------------- /src/components/fs/FileNewFileChip.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Chip, 3 | Dialog, 4 | DialogContent, 5 | DialogContentText, 6 | TextField, 7 | DialogActions, 8 | Button, 9 | } from "@mui/material" 10 | import React, { ChangeEvent, useState } from "react" 11 | import AddIcon from "@mui/icons-material/Add" 12 | import { useId } from "react" 13 | import useFileSystem from "./FileSystemContext" 14 | 15 | export default function FileNewFileChip(props: { 16 | name?: string 17 | content: string 18 | label?: string 19 | extension?: string 20 | }) { 21 | const { 22 | name: newFileName, 23 | content: newFileContent, 24 | label, 25 | extension, 26 | } = props 27 | const { fileSystem } = useFileSystem() 28 | const [open, setOpen] = useState(false) 29 | const [value, setValue] = useState("") 30 | const valueId = useId() 31 | 32 | const handleOpen = () => { 33 | setValue("") 34 | setOpen(true) 35 | } 36 | const handleOk = async () => { 37 | setOpen(false) 38 | let name = value.toLocaleLowerCase().replace(/\s+/g, "") 39 | if (newFileName) 40 | await fileSystem.createWorkingDirectory( 41 | name, 42 | newFileName, 43 | newFileContent 44 | ) 45 | else { 46 | if (extension) name += `.${extension}` 47 | const d = fileSystem.workingDirectory || fileSystem.root 48 | const f = await d.fileAsync(name, { create: true }) 49 | await f.write(newFileContent) 50 | fileSystem.workingFile = f 51 | } 52 | } 53 | const handleCancel = () => setOpen(false) 54 | const handleValueChange = (event: ChangeEvent) => { 55 | setValue(event.target.value) 56 | } 57 | 58 | return ( 59 | <> 60 | } 64 | onClick={handleOpen} 65 | /> 66 | 67 | 68 | Choose a project name 69 | 76 | 77 | 78 | 81 | 89 | 90 | 91 | 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /src/components/fs/FileSystemButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import OpenInBrowserIcon from "@mui/icons-material/OpenInBrowser" 3 | import useFileSystem from "./FileSystemContext" 4 | import useChange from "../dom/useChange" 5 | import IconButtonWithTooltip from "../ui/IconButtonWithTooltip" 6 | 7 | export default function FileSystemButton() { 8 | const { fileSystem, showDirectoryPicker } = useFileSystem() 9 | const root = useChange(fileSystem, _ => _?.root) 10 | const handleOpenDirectory = () => showDirectoryPicker({ mode: "readwrite" }) 11 | 12 | if (!fileSystem) return null 13 | 14 | return ( 15 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/fs/FileSystemChip.tsx: -------------------------------------------------------------------------------- 1 | import { Chip } from "@mui/material" 2 | import React from "react" 3 | import OpenInBrowserIcon from "@mui/icons-material/OpenInBrowser" 4 | import useFileSystem from "./FileSystemContext" 5 | import useChange from "../dom/useChange" 6 | 7 | export default function FileSystemChip() { 8 | const { fileSystem, showDirectoryPicker } = useFileSystem() 9 | const root = useChange(fileSystem, _ => _?.root) 10 | const handleOpenDirectory = () => showDirectoryPicker({ mode: "readwrite" }) 11 | const handleCloseDirectory = () => (fileSystem.root = undefined) 12 | 13 | if (!fileSystem) return null 14 | 15 | return ( 16 | } 19 | label={root?.name || "open directory"} 20 | onClick={handleOpenDirectory} 21 | onDelete={root ? handleCloseDirectory : undefined} 22 | /> 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/fs/FileSystemContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode, useContext, useMemo } from "react" 2 | import { fileSystemHandleSupported } from "./fs" 3 | import { FileSystem, FileSystemDirectory } from "./fsdom" 4 | 5 | export interface FileSystemProps { 6 | fileSystem: FileSystem 7 | showDirectoryPicker?: (options?: DirectoryPickerOptions) => Promise 8 | } 9 | 10 | export const FileSystemContext = createContext({ 11 | fileSystem: undefined, 12 | showDirectoryPicker: undefined, 13 | }) 14 | FileSystemContext.displayName = "fs" 15 | 16 | export default function useFileSystem(): FileSystemProps { 17 | const ctx = useContext(FileSystemContext) 18 | return ctx 19 | } 20 | 21 | // eslint-disable-next-line react/prop-types 22 | export function FileSystemProvider(props: { children: ReactNode }) { 23 | const { children } = props 24 | const fileSystem = useMemo( 25 | () => (fileSystemHandleSupported() ? new FileSystem() : undefined), 26 | [] 27 | ) 28 | const supported = !!fileSystem 29 | const showDirectoryPicker = supported 30 | ? async (options?: DirectoryPickerOptions) => { 31 | try { 32 | const handle = await window.showDirectoryPicker(options) 33 | if (handle !== fileSystem.root?.handle) 34 | fileSystem.root = new FileSystemDirectory(this, handle) 35 | } catch (e) { 36 | console.debug(e) 37 | } 38 | } 39 | : undefined 40 | 41 | return ( 42 | 48 | {children} 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/components/fs/FileSystemNodeChip.tsx: -------------------------------------------------------------------------------- 1 | import { Chip } from "@mui/material" 2 | import React from "react" 3 | import { FileSystemNode } from "./fsdom" 4 | 5 | export default function FileSystemNodeChip(props: { 6 | node: FileSystemNode 7 | selected?: boolean 8 | onClick: () => void 9 | }) { 10 | const { node, selected, onClick } = props 11 | const { name } = node 12 | return ( 13 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/fs/FileTabs.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from "@mui/material" 2 | import React from "react" 3 | import useFileSystem from "./FileSystemContext" 4 | import useChange from "../dom/useChange" 5 | import FileSystemChip from "./FileSystemChip" 6 | import FileNewFileChip from "./FileNewFileChip" 7 | import FileSystemNodeChip from "./FileSystemNodeChip" 8 | 9 | export default function FileTabs(props: { 10 | newFileName?: string 11 | newFileExtension?: string 12 | newFileContent?: string 13 | newFileLabel?: string 14 | directoryFilter?: (directory: string) => boolean 15 | fileFilter?: (file: string) => boolean 16 | }) { 17 | const { newFileName, newFileContent, newFileLabel, newFileExtension } = 18 | props 19 | const { fileSystem } = useFileSystem() 20 | const root = useChange(fileSystem, _ => _?.root) 21 | const workingDirectory = useChange(fileSystem, _ => _?.workingDirectory) 22 | const directories = useChange(root, _ => _?.directories) 23 | const handleDirectorySelected = handle => () => { 24 | fileSystem.workingDirectory = handle 25 | } 26 | if (!fileSystem) return null 27 | return ( 28 | 29 | 30 | 31 | 32 | {root && newFileContent && ( 33 | 34 | 40 | 41 | )} 42 | {directories?.map(node => ( 43 | 44 | 49 | 50 | ))} 51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/components/hooks/useAsyncMemo.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useState } from "react" 2 | import useEffectAsync from "./useEffectAsync" 3 | 4 | export default function useAsyncMemo( 5 | factory: () => T | Promise, 6 | deps?: DependencyList 7 | ) { 8 | const [value, setValue] = useState() 9 | useEffectAsync(async mounted => { 10 | const v = await factory() 11 | if (mounted()) setValue(v) 12 | }, deps || []) 13 | return value 14 | } 15 | -------------------------------------------------------------------------------- /src/components/hooks/useEffectAsync.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react" 2 | 3 | export default function useEffectAsync( 4 | effect: (mounted?: () => boolean) => Promise, 5 | dependencies?: React.DependencyList 6 | ) { 7 | useEffect(() => { 8 | let mounted = true 9 | effect(() => mounted) 10 | return () => { 11 | mounted = false 12 | } 13 | }, dependencies) 14 | } 15 | -------------------------------------------------------------------------------- /src/components/hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { UIFlags } from "../uiflags" 2 | import useStorage, { getStorageItem } from "./useStorage" 3 | 4 | const _storage = (() => { 5 | try { 6 | return typeof window !== "undefined" && UIFlags.storage 7 | ? window.localStorage 8 | : undefined 9 | } catch { 10 | return undefined 11 | } 12 | })() 13 | 14 | export function getLocalStorageItem(key: string) { 15 | return getStorageItem(_storage, key) 16 | } 17 | 18 | export default function useLocalStorage( 19 | key: string, 20 | initialValue?: T 21 | ): [T, (value: T) => void] { 22 | return useStorage(_storage, key, initialValue) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/hooks/useLocationSearchParam.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react" 2 | 3 | export function useLocationSearchParamString(key: string): string { 4 | return useMemo(() => { 5 | if (typeof window !== "undefined") { 6 | const url = new URL(window.location.href) 7 | return url.searchParams.get(key) 8 | } 9 | return undefined 10 | }, [key]) 11 | } 12 | 13 | export function useLocationSearchParamBoolean( 14 | key: string, 15 | defaultValue: boolean 16 | ): boolean { 17 | return useMemo(() => { 18 | if (typeof window !== "undefined") { 19 | const url = new URL(window.location.href) 20 | const v = url.searchParams.get(key) 21 | if (v) { 22 | if (v === "1" || v === "true" || v === "yes") return true 23 | else if (v === "0" || v === "false" || v === "no") return false 24 | else return defaultValue 25 | } 26 | // empty value means true 27 | if (url.searchParams.has(key)) return true 28 | return defaultValue 29 | } 30 | return undefined 31 | }, [key, defaultValue]) 32 | } 33 | -------------------------------------------------------------------------------- /src/components/hooks/useMdxComponents.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, useMemo } from "react" 2 | import { Link } from "gatsby-theme-material-ui" 3 | import { 4 | Box, 5 | Paper, 6 | Table, 7 | TableBody, 8 | TableContainer, 9 | TableHead, 10 | TableRow, 11 | useTheme, 12 | } from "@mui/material" 13 | import Alert from "../ui/Alert" 14 | import { AlertTitle } from "@mui/material" 15 | 16 | export default function useMdxComponents() { 17 | const theme = useTheme() 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | const mdxComponents: any = useMemo( 20 | () => ({ 21 | Link: props => , 22 | a: (props: { href: string }) => ( 23 | 28 | ), 29 | pre: props => ( 30 | 31 | 32 |
33 | 34 | 35 | ), 36 | table: props => ( 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ), 45 | thead: props => , 46 | tbody: props => , 47 | tr: props => , 48 | Alert: props => , 49 | AlertTitle: props => , 50 | }), 51 | [] 52 | ) 53 | 54 | return mdxComponents 55 | } 56 | -------------------------------------------------------------------------------- /src/components/hooks/useSnackbar.ts: -------------------------------------------------------------------------------- 1 | import { useSnackbar as NoistackUseSnackbar } from "notistack" 2 | import { ReactNode } from "react" 3 | import useAnalytics from "./useAnalytics" 4 | 5 | export default function useSnackbar() { 6 | const { trackError } = useAnalytics() 7 | const { enqueueSnackbar: _enqueueSnackbar } = NoistackUseSnackbar() || {} 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | const setError = (e: any) => { 11 | if (!e) return 12 | const msg = e?.message || e + "" 13 | 14 | console.error(msg, { error: e }) 15 | trackError?.(e, {}) 16 | _enqueueSnackbar?.(msg, { 17 | variant: "error", 18 | autoHideDuration: 4000, 19 | preventDuplicate: true, 20 | }) 21 | } 22 | 23 | const enqueueSnackbar = ( 24 | message: string | ReactNode, 25 | variant?: "success" | "warning" | "info" 26 | ) => _enqueueSnackbar?.(message, { variant }) 27 | 28 | return { 29 | enqueueSnackbar, 30 | setError, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/hooks/useStorage.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react" 2 | 3 | // enabled when storage=0 4 | const memStorage: Record = {} 5 | 6 | export function getStorageItem(storage: Storage, key: string): T { 7 | const pkey = PREFIX + key 8 | try { 9 | // Get from local storage by key 10 | const item = storage?.getItem(pkey) 11 | // Parse stored json or if none return initialValue 12 | return item && JSON.parse(item) 13 | } catch (error) { 14 | console.log(error) 15 | } 16 | 17 | // use in-memory 18 | const v = memStorage[key] 19 | return v as T 20 | } 21 | 22 | const PREFIX = "ds:" 23 | export default function useStorage( 24 | storage: Storage, 25 | key: string, 26 | initialValue?: T 27 | ): [T, (value: T) => void] { 28 | const pkey = PREFIX + key 29 | // State to store our value 30 | // Pass initial state function to useState so logic is only executed once 31 | const [storedValue, setStoredValue] = useState(() => { 32 | try { 33 | // Get from local storage by key 34 | const item = storage?.getItem(pkey) 35 | // Parse stored json or if none return initialValue 36 | return (item && JSON.parse(item)) || initialValue 37 | } catch (error) { 38 | storage?.removeItem(pkey) 39 | console.log(error) 40 | } 41 | 42 | // use in-memory 43 | const v = memStorage[key] 44 | return v !== undefined ? v : initialValue 45 | }) 46 | 47 | // Return a wrapped version of useState's setter function that ... 48 | // ... persists the new value to localStorage. 49 | const setValue = useCallback( 50 | (value: T) => { 51 | // keep in-memory cahed 52 | memStorage[key] = value 53 | // Allow value to be a function so we have same API as useState 54 | const valueToStore = value 55 | // Save state 56 | setStoredValue(valueToStore) 57 | // persistent storage 58 | try { 59 | // Save to local storage 60 | storage?.setItem(pkey, JSON.stringify(valueToStore)) 61 | } catch (error) { 62 | // A more advanced implementation would handle the error case 63 | console.log(error) 64 | } 65 | }, 66 | [key] 67 | ) 68 | 69 | return [storedValue, setValue] 70 | } 71 | -------------------------------------------------------------------------------- /src/components/hooks/useWindowEvent.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useEffect } from "react" 2 | 3 | export default function useWindowEvent( 4 | type: K, 5 | listener: (this: Window, ev: WindowEventMap[K]) => unknown, 6 | passive = false, 7 | deps?: DependencyList 8 | ) { 9 | useEffect(() => { 10 | if (typeof self === "undefined" || !listener) return undefined // SSR 11 | 12 | // initiate the event handler 13 | self.addEventListener(type, listener, passive) 14 | 15 | // this will clean up the event every time the component is re-rendered 16 | return () => self.removeEventListener(type, listener) 17 | }, [type, listener, passive, ...(deps || [])]) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/shell/AppTheme.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createTheme, 3 | responsiveFontSizes, 4 | DeprecatedThemeOptions, 5 | ThemeProvider, 6 | StyledEngineProvider, 7 | } from "@mui/material" 8 | import React, { useContext } from "react" 9 | import DarkModeContext from "./DarkModeContext" 10 | 11 | /* 12 | declare module "@mui/styles/defaultTheme" { 13 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 14 | interface DefaultTheme extends Theme {} 15 | } 16 | */ 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | export default function AppTheme(props: any) { 20 | const { darkMode } = useContext(DarkModeContext) 21 | const isDark = darkMode === "dark" 22 | const themeDef: DeprecatedThemeOptions = { 23 | palette: { 24 | primary: { 25 | main: isDark ? "#56d364" : "#2e7d32", 26 | }, 27 | secondary: { 28 | main: "#ffc400", 29 | }, 30 | contrastThreshold: isDark ? 5.1 : 3.1, 31 | mode: darkMode, 32 | }, 33 | } 34 | const rawTheme = createTheme(themeDef) 35 | const theme = responsiveFontSizes(rawTheme) 36 | return ( 37 | 38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/components/shell/DarkModeButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react" 2 | import DarkModeContext from "./DarkModeContext" 3 | import IconButtonWithTooltip from "../ui/IconButtonWithTooltip" 4 | import LightModeIcon from "@mui/icons-material/LightMode" 5 | import DarkModeIcon from "@mui/icons-material/DarkMode" 6 | 7 | export default function DarkModeButton() { 8 | const { darkMode, toggleDarkMode } = useContext(DarkModeContext) 9 | const handleClick = () => toggleDarkMode() 10 | return ( 11 | 16 | {darkMode === "light" ? : } 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/shell/DarkModeContext.tsx: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from "@mui/material" 2 | import React, { 3 | CSSProperties, 4 | ReactNode, 5 | useCallback, 6 | useEffect, 7 | useMemo, 8 | useState, 9 | createContext, 10 | } from "react" 11 | import { useLocationSearchParamBoolean } from "../hooks/useLocationSearchParam" 12 | 13 | export type PaletteType = "dark" | "light" 14 | 15 | export interface DarkModeContextProps { 16 | darkMode: PaletteType 17 | toggleDarkMode: (mode?: PaletteType) => void 18 | darkModeMounted: boolean 19 | imgStyle?: CSSProperties 20 | } 21 | 22 | const DarkModeContext = createContext({ 23 | darkMode: "dark", 24 | toggleDarkMode: () => {}, 25 | darkModeMounted: false, 26 | imgStyle: undefined, 27 | }) 28 | DarkModeContext.displayName = "DarkMode" 29 | 30 | export default DarkModeContext 31 | 32 | export function DarkModeProvider(props: { 33 | fixedDarkMode?: PaletteType 34 | temporary?: boolean 35 | children: ReactNode 36 | }) { 37 | const { fixedDarkMode, children } = props 38 | const KEY = "darkMode" 39 | const queryDarkMode = useLocationSearchParamBoolean("dark", false) 40 | const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)", { 41 | noSsr: true, 42 | }) 43 | const localTheme = () => 44 | typeof window !== "undefined" && 45 | (window.localStorage.getItem(KEY) as PaletteType) 46 | const [darkMode, setDarkMode] = useState( 47 | queryDarkMode || fixedDarkMode 48 | ? "dark" 49 | : localTheme() || (prefersDarkMode ? "dark" : "light") 50 | ) 51 | const [darkModeMounted, setMounted] = useState(false) 52 | 53 | const setMode = (mode: PaletteType) => { 54 | if (mode === darkMode || fixedDarkMode || queryDarkMode) return // nothing to do 55 | 56 | if (typeof window !== "undefined") 57 | window.localStorage.setItem(KEY, mode) 58 | setDarkMode(mode) 59 | } 60 | const toggleDarkMode = useCallback( 61 | (mode?: PaletteType) => { 62 | mode = mode || (darkMode === "light" ? "dark" : "light") 63 | if (mode === "dark") { 64 | setMode("dark") 65 | } else { 66 | setMode("light") 67 | } 68 | }, 69 | [darkMode] 70 | ) 71 | const imgStyle = useMemo( 72 | () => 73 | darkMode == "dark" 74 | ? ({ 75 | filter: "drop-shadow(0 0 0.5rem #444)", 76 | } as CSSProperties) 77 | : undefined, 78 | [darkMode] 79 | ) 80 | 81 | useEffect(() => { 82 | setMounted(true) 83 | }, []) 84 | 85 | return ( 86 | 89 | {children} 90 | 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /src/components/shell/DataEditorAppBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react" 2 | import { styled } from "@mui/material/styles" 3 | import { Hidden, Box } from "@mui/material" 4 | import AppBar from "@mui/material/AppBar" 5 | import Toolbar from "@mui/material/Toolbar" 6 | import Typography from "@mui/material/Typography" 7 | // tslint:disable-next-line: no-submodule-imports 8 | import DarkModeContext from "./DarkModeContext" 9 | import { Link } from "gatsby-theme-material-ui" 10 | import DarkModeButton from "./DarkModeButton" 11 | import FileSystemButton from "../fs/FileSystemButton" 12 | 13 | const PREFIX = "DataEditorApp" 14 | 15 | const classes = { 16 | grow: `${PREFIX}grow`, 17 | appBar: `${PREFIX}appBar`, 18 | menuButton: `${PREFIX}menuButton`, 19 | } 20 | 21 | const StyledBox = styled(Box)(({ theme }) => ({ 22 | [`& .${classes.grow}`]: { 23 | flexGrow: 1, 24 | }, 25 | 26 | [`& .${classes.appBar}`]: { 27 | transition: theme.transitions.create(["margin", "width"], { 28 | easing: theme.transitions.easing.sharp, 29 | duration: theme.transitions.duration.leavingScreen, 30 | }), 31 | }, 32 | 33 | [`& .${classes.menuButton}`]: { 34 | marginRight: theme.spacing(1), 35 | }, 36 | })) 37 | 38 | export default function DataEditorAppBar() { 39 | const { darkMode } = useContext(DarkModeContext) 40 | const appBarColor = darkMode === "dark" ? "inherit" : undefined 41 | 42 | return ( 43 | 44 | 49 | 50 | 51 | 52 | 58 | Data Science Editor 59 | 60 | 61 | 62 |
63 | 70 | About 71 | 72 | 79 | Blocks 80 | 81 | 82 | 83 | 84 | 85 | 86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /src/components/shell/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@mui/material" 2 | import { styled } from "@mui/material/styles" 3 | import React from "react" 4 | import { UIFlags } from "../uiflags" 5 | import clsx from "clsx" 6 | 7 | const PREFIX = "Footer" 8 | 9 | const classes = { 10 | footer: `${PREFIX}footer`, 11 | absolute: `${PREFIX}absolute`, 12 | } 13 | 14 | const Root = styled("footer")(({ theme }) => ({ 15 | [`&.${classes.footer}`]: { 16 | textAlign: "center", 17 | "z-index": 1000, 18 | "& *": { 19 | fontSize: `${theme.typography.fontSize * 0.8}px`, 20 | textDecoration: "none", 21 | color: theme.palette.text.primary, 22 | }, 23 | "& .note": { 24 | fontSize: `${theme.typography.fontSize * 0.9}px`, 25 | }, 26 | "& a:hover": { 27 | textDecoration: "underline", 28 | }, 29 | "& a:visited": { 30 | color: theme.palette.text.primary, 31 | }, 32 | "& a": { 33 | marginRight: theme.spacing(0.5), 34 | }, 35 | }, 36 | [`&.${classes.absolute}`]: { 37 | position: "absolute", 38 | bottom: UIFlags.hosted ? "0.8rem" : "2rem", 39 | left: "9.5rem", 40 | }, 41 | })) 42 | 43 | export default function Footer(props: { container?: boolean }) { 44 | const { container } = props 45 | const repo = process.env.GATSBY_GITHUB_REPOSITORY 46 | const sha = process.env.GATSBY_GITHUB_SHA 47 | 48 | return ( 49 | 53 | 58 | Privacy & Cookies 59 | 60 | | 61 | 66 | Terms Of Use 67 | 68 | | 69 | 74 | Trademarks 75 | 76 | {repo && sha && ( 77 | <> 78 | | 79 | 84 | {sha.slice(0, 8)} 85 | 86 | 87 | )} 88 |   89 | 90 | © {new Date().getFullYear()} Microsoft Corporation 91 | 92 | 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /src/components/shell/ThemedLayout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CssBaseline, 3 | Theme, 4 | ThemeProvider, 5 | StyledEngineProvider, 6 | } from "@mui/material" 7 | import { SnackbarProvider } from "notistack" 8 | import React, { ReactNode } from "react" 9 | import { FileSystemProvider } from "../fs/FileSystemContext" 10 | import { AppInsightsErrorBoundary } from "../hooks/useAnalytics" 11 | 12 | /* 13 | declare module "@mui/styles/defaultTheme" { 14 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 15 | interface DefaultTheme extends Theme {} 16 | } 17 | */ 18 | 19 | export default function ThemedLayout(props: { 20 | theme: Theme 21 | maxSnack?: number 22 | children: ReactNode 23 | }) { 24 | const { theme, maxSnack, children } = props 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/shell/ThemedMdxLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Theme } from "@mui/material" 2 | import { MDXProvider } from "@mdx-js/react" 3 | import React, { ReactNode } from "react" 4 | import useMdxComponents from "../hooks/useMdxComponents" 5 | import ThemedLayout from "./ThemedLayout" 6 | 7 | export default function ThemedMdxLayout(props: { 8 | theme: Theme 9 | children: ReactNode 10 | }) { 11 | const { theme, children } = props 12 | const mdxComponents = useMdxComponents() 13 | 14 | return ( 15 | 16 | {children} 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/ui/Alert.tsx: -------------------------------------------------------------------------------- 1 | import { Collapse } from "@mui/material" 2 | import { styled } from "@mui/material/styles" 3 | import { Alert as MaterialAlert, AlertTitle } from "@mui/material" 4 | import { AlertProps } from "@mui/lab" 5 | import React, { ReactNode, useState } from "react" 6 | 7 | const PREFIX = "Alert" 8 | 9 | const classes = { 10 | icon: `${PREFIX}icon`, 11 | } 12 | 13 | const StyledCollapse = styled(Collapse)(() => ({ 14 | [`& .${classes.icon}`]: { 15 | flexDirection: "column", 16 | justifyContent: "center", 17 | }, 18 | })) 19 | 20 | export default function Alert( 21 | props: { 22 | mb?: number | string 23 | closeable?: boolean 24 | title?: ReactNode 25 | children: ReactNode 26 | } & AlertProps 27 | ) { 28 | const { closeable, title, children, mb = 2, ...others } = props 29 | 30 | const [open, setOpen] = useState(true) 31 | const handleClose = () => setOpen(false) 32 | return ( 33 | 34 | 42 | {title && {title}} 43 | {children} 44 | 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/components/ui/BrowserCompatibilityAlert.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react" 2 | import { getParser, Parser } from "bowser" 3 | import Alert from "./Alert" 4 | import { AlertTitle } from "@mui/material" 5 | import { useLocationSearchParamBoolean } from "../hooks/useLocationSearchParam" 6 | 7 | export default function BrowserCompatibilityAlert(props: { 8 | filter: Parser.checkTree 9 | label: string 10 | }) { 11 | const { filter, label } = props 12 | const browsercheck = useLocationSearchParamBoolean("browsercheck", true) 13 | const compatible = useMemo(() => { 14 | if (typeof window !== "undefined") { 15 | const browser = getParser(window.navigator.userAgent) 16 | return browser.satisfies(filter) 17 | } 18 | return true // SSR 19 | }, [JSON.stringify(filter)]) 20 | 21 | if (compatible || !browsercheck) return null 22 | return ( 23 | 24 | Browser not compatible. 25 | {label} 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { ButtonProps } from "@mui/material" 3 | import { Button as GatsbyButton } from "gatsby-theme-material-ui" 4 | import useAnalytics, { EventProperties } from "../hooks/useAnalytics" 5 | 6 | export default function Button( 7 | props: { 8 | trackName?: string 9 | trackProperties?: EventProperties 10 | } & ButtonProps 11 | ) { 12 | const { trackName, trackProperties, onClick, ...rest } = props 13 | const { trackEvent } = useAnalytics() 14 | const handleClick = 15 | !trackName || !trackEvent || !onClick 16 | ? onClick 17 | : ev => { 18 | trackEvent(trackName, trackProperties) 19 | onClick(ev) 20 | } 21 | return 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ui/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useState } from "react" 2 | import FileCopyIcon from "@mui/icons-material/FileCopy" 3 | import DoneIcon from "@mui/icons-material/Done" 4 | import ReportProblemIcon from "@mui/icons-material/ReportProblem" 5 | import IconButtonWithTooltip from "./IconButtonWithTooltip" 6 | import { delay } from "../dom/utils" 7 | 8 | export default function CopyButton(props: { 9 | title: string 10 | onCopy?: () => Promise 11 | text?: string 12 | className?: string 13 | size?: "small" 14 | variant?: "outlined" | "contained" 15 | disabled?: boolean 16 | copyIcon?: ReactNode 17 | color?: "success" | "error" 18 | }) { 19 | const { text, title, disabled, onCopy, copyIcon, ...rest } = props 20 | const [copied, setCopied] = useState(undefined) 21 | const handleClick = async (ev: React.MouseEvent) => { 22 | ev.stopPropagation() 23 | ev.preventDefault() 24 | 25 | try { 26 | setCopied(null) 27 | const copied = text || (await onCopy?.()) 28 | if (typeof copied === "string") { 29 | const c = copied as string 30 | await navigator.clipboard.writeText(c) 31 | } else { 32 | const canvas = copied as HTMLCanvasElement 33 | const blob = await new Promise(resolve => 34 | canvas.toBlob(blob => resolve(blob)) 35 | ) 36 | const item = new ClipboardItem({ "image/png": blob }) 37 | navigator.clipboard.write([item]) 38 | } 39 | setCopied(true) 40 | } catch (e) { 41 | console.debug(e) 42 | setCopied(false) 43 | } finally { 44 | await delay(2000) 45 | setCopied(undefined) 46 | } 47 | } 48 | const buttonText = 49 | copied === true 50 | ? "Copied!" 51 | : copied === false 52 | ? "Copy failed" 53 | : title || "copy to clipboard" 54 | return ( 55 | 61 | {copied === true ? ( 62 | 63 | ) : copied === false ? ( 64 | 65 | ) : ( 66 | copyIcon || 67 | )} 68 | 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /src/components/ui/GridHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Chip, Grid, Typography } from "@mui/material" 2 | import { styled } from "@mui/material/styles" 3 | import React, { ReactNode } from "react" 4 | import clsx from "clsx" 5 | 6 | const PREFIX = "GridHeader" 7 | 8 | const classes = { 9 | hr: `${PREFIX}hr`, 10 | start: `${PREFIX}start`, 11 | } 12 | 13 | const StyledGrid = styled(Grid)(({ theme }) => ({ 14 | [`& .${classes.hr}`]: { 15 | background: theme.palette.text.disabled, 16 | marginBottom: "unset", 17 | }, 18 | 19 | [`& .${classes.start}`]: { 20 | width: theme.spacing(2), 21 | }, 22 | })) 23 | 24 | export default function GridHeader(props: { 25 | title?: ReactNode 26 | count?: number 27 | variant?: "subtitle1" | "caption" | "subtitle2" 28 | action?: JSX.Element 29 | }) { 30 | const { title, count, variant, action } = props 31 | 32 | return ( 33 | 34 | 41 | 42 |
43 |
44 | 45 | {action && ( 46 | 47 | {action} 48 | 49 | )} 50 | 54 | {title} 55 | 56 | {count !== undefined && ( 57 | 58 | 59 | 60 | )} 61 | 62 | 63 |
64 |
65 |
66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/components/ui/IconButtonWithTooltip.tsx: -------------------------------------------------------------------------------- 1 | import { IconButtonProps, useTheme } from "@mui/material" 2 | import { IconButton } from "gatsby-theme-material-ui" 3 | import React from "react" 4 | import Zoom from "@mui/material/Zoom" 5 | import Tooltip from "./Tooltip" 6 | import useAnalytics, { EventProperties } from "../hooks/useAnalytics" 7 | 8 | export default function IconButtonWithTooltip( 9 | props: { 10 | to?: string 11 | disabled?: boolean 12 | selected?: boolean 13 | trackName?: string 14 | trackProperties?: EventProperties 15 | } & IconButtonProps 16 | ) { 17 | const { 18 | title, 19 | children, 20 | disabled, 21 | trackName, 22 | trackProperties, 23 | selected, 24 | onClick, 25 | ...others 26 | } = props 27 | const { trackEvent } = useAnalytics() 28 | const { palette } = useTheme() 29 | 30 | const handleClick = 31 | !trackName || !trackEvent || !onClick 32 | ? onClick 33 | : ev => { 34 | trackEvent(trackName, trackProperties) 35 | onClick(ev) 36 | } 37 | 38 | return ( 39 | 40 | 41 | 55 | {children} 56 | 57 | 58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/components/ui/LoadingProgress.tsx: -------------------------------------------------------------------------------- 1 | import { CircularProgress } from "@mui/material" 2 | import React from "react" 3 | 4 | export default function LoadingProgress(props: { size?: string }) { 5 | const { size } = props 6 | return ( 7 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ui/Progress.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | import useProgress from "./useProgress" 3 | 4 | const PROGRESS_DELAY = 200 5 | 6 | export default function Progress(props: { 7 | delay?: number 8 | children: JSX.Element 9 | }) { 10 | const { delay, children } = props 11 | const { start, done } = useProgress() 12 | useEffect(() => { 13 | let id = setTimeout(() => { 14 | id = undefined 15 | start() 16 | }, delay || PROGRESS_DELAY) 17 | return () => { 18 | if (id) clearTimeout(id) 19 | else done() 20 | } 21 | }, [delay]) 22 | return children 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ui/SplashDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogActions, 4 | DialogContent, 5 | DialogTitle, 6 | } from "@mui/material"; 7 | import React, { useState } from "react"; 8 | import { useLocationSearchParamBoolean } from "../hooks/useLocationSearchParam"; 9 | import { Button } from "gatsby-theme-material-ui"; 10 | 11 | let _firstShow = true; 12 | export default function SplashDialog() { 13 | const hide = useLocationSearchParamBoolean("hidesplash", false); 14 | const [open, setOpen] = useState(_firstShow); 15 | 16 | const handleClose = () => { 17 | _firstShow = false; 18 | setOpen(false); 19 | }; 20 | 21 | if (hide) return null; 22 | 23 | return ( 24 | 25 | Data Science Editor 26 | 27 | <> 28 |

29 | Drag and drop blocks to analyse data. Each block 30 | receives a dataset, transforms it or analyzes it, and 31 | passes it to the next block. Use 👀 to preview changes. 32 |

33 | 49 | 50 |
51 | 52 | 60 | 67 | 68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/components/ui/Suspense.tsx: -------------------------------------------------------------------------------- 1 | import { NoSsr } from "@mui/material" 2 | import React, { 3 | ReactNode, 4 | Suspense as ReactSuspense, 5 | useEffect, 6 | useState, 7 | } from "react" 8 | import Progress from "./Progress" 9 | 10 | export default function Suspense(props: { 11 | children: ReactNode 12 | fallback?: React.ReactNode 13 | }) { 14 | const { children, fallback } = props 15 | 16 | // HACK: react 18 bug, does not seem to refresh Suspense 17 | const [, setState] = useState(false) 18 | useEffect(() => { 19 | let mounted = true 20 | let retry = 0 21 | const check = () => { 22 | { 23 | retry++ 24 | const status: number = (children as any).type?._payload?._status 25 | if (!mounted || status === undefined || retry > 20) return 26 | else if (status >= 1) setState(() => true) 27 | else setTimeout(check, 200 + retry * 100) 28 | } 29 | } 30 | check() 31 | return () => { 32 | mounted = false 33 | } 34 | }, []) 35 | 36 | return ( 37 | 38 | 44 | 45 | 46 | ) 47 | } 48 | > 49 | {children} 50 | 51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/components/ui/SwitchWithLabel.tsx: -------------------------------------------------------------------------------- 1 | import { FormControlLabel, Switch, SwitchProps } from "@mui/material" 2 | import React, { CSSProperties } from "react" 3 | 4 | export default function SwitchWithLabel( 5 | props: { 6 | label: string | number | JSX.Element 7 | labelPlacement?: "end" | "start" | "top" | "bottom" 8 | labelStyle?: CSSProperties 9 | } & SwitchProps 10 | ) { 11 | const { label, labelPlacement, labelStyle, ...rest } = props 12 | return ( 13 | } 15 | label={label} 16 | style={labelStyle} 17 | labelPlacement={labelPlacement} 18 | /> 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/ui/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip as MaterialTooltip } from "@mui/material" 2 | import { styled } from "@mui/material/styles" 3 | 4 | // fix for contrast issue 5 | const Tooltip = styled(MaterialTooltip)(({ theme }) => ({ 6 | [`& .tooltip`]: { 7 | backgroundColor: theme.palette.background.default, 8 | color: theme.palette.text.primary, 9 | }, 10 | })) 11 | 12 | export default Tooltip 13 | -------------------------------------------------------------------------------- /src/components/ui/useProgress.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from "react" 2 | import useEffectAsync from "../hooks/useEffectAsync" 3 | 4 | export default function useProgress() { 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | const instance = useRef() 7 | 8 | useEffectAsync(async mounted => { 9 | if (typeof self !== "undefined") { 10 | const i = await import("accessible-nprogress") 11 | if (mounted()) instance.current = i 12 | } 13 | }, []) 14 | 15 | const start: () => void = () => instance.current?.start 16 | const done: () => void = () => instance.current?.done 17 | return { start, done } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/uiflags.ts: -------------------------------------------------------------------------------- 1 | function configureUIFlags() { 2 | if (typeof window === "undefined" || typeof URLSearchParams === "undefined") 3 | return 4 | 5 | const location = window.location 6 | const params = new URLSearchParams(location.search) 7 | UIFlags.diagnostics = params.get(`dbg`) === "1" 8 | UIFlags.hosted = params.get("embed") === "1" 9 | UIFlags.storage = !UIFlags.hosted && params.get("storage") !== "0" 10 | UIFlags.footer = params.get("footer") !== "0" 11 | UIFlags.screenshot = params.get("screenshot") === "1" 12 | } 13 | 14 | export class UIFlags { 15 | static diagnostics = false 16 | static hosted = false 17 | static storage = true 18 | static footer = true 19 | static screenshot = false 20 | } 21 | 22 | configureUIFlags() 23 | -------------------------------------------------------------------------------- /src/components/widgets/svg.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | function mkTitle(txt: string): SVGTitleElement { 3 | const t = elt("title") 4 | t.textContent = txt 5 | return t 6 | } 7 | export function title(el: SVGElement, txt: string): SVGTitleElement { 8 | const t = mkTitle(txt) 9 | el.appendChild(t) 10 | return t 11 | } 12 | 13 | export function hydrate(el: SVGElement, props: any) { 14 | for (const k in props) { 15 | if (k == "title") { 16 | title(el, props[k]) 17 | } else el.setAttributeNS(null, k, props[k]) 18 | } 19 | } 20 | 21 | export function elt(name: string, props?: any): SVGElement { 22 | const el = document.createElementNS("http://www.w3.org/2000/svg", name) 23 | if (props) hydrate(el, props) 24 | return el 25 | } 26 | 27 | export function child(parent: Element, name: string, props?: any): SVGElement { 28 | const el = elt(name, props) 29 | parent.appendChild(el) 30 | return el 31 | } 32 | -------------------------------------------------------------------------------- /src/components/widgets/svgutils.ts: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export function svgPointerPoint( 4 | svg: SVGSVGElement, 5 | event: React.PointerEvent 6 | ): DOMPoint { 7 | const point = svg.createSVGPoint() 8 | point.x = event.clientX 9 | point.y = event.clientY 10 | const res = point.matrixTransform(svg.getScreenCTM().inverse()) 11 | return res 12 | } 13 | 14 | export function closestPoint( 15 | pathNode: SVGPathElement, 16 | step: number, 17 | point: DOMPoint 18 | ): number { 19 | const pathLength = pathNode.getTotalLength() 20 | 21 | const distance2 = (p: DOMPoint) => { 22 | const dx = p.x - point.x 23 | const dy = p.y - point.y 24 | return dx * dx + dy * dy 25 | } 26 | 27 | let bestLength = 0 28 | let bestDistance = Infinity 29 | for (let scanLength = 0; scanLength <= pathLength; scanLength += step) { 30 | const scan = pathNode.getPointAtLength(scanLength) 31 | const scanDistance = distance2(scan) 32 | if (scanDistance < bestDistance) { 33 | bestLength = scanLength 34 | bestDistance = scanDistance 35 | } 36 | } 37 | return bestLength / pathLength 38 | } 39 | 40 | export function polarToCartesian( 41 | centerX: number, 42 | centerY: number, 43 | radius: number, 44 | angleInDegrees: number 45 | ) { 46 | const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0 47 | 48 | return { 49 | x: centerX + radius * Math.cos(angleInRadians), 50 | y: centerY + radius * Math.sin(angleInRadians), 51 | } 52 | } 53 | 54 | export function describeArc( 55 | x: number, 56 | y: number, 57 | radius: number, 58 | startAngle: number, 59 | endAngle: number, 60 | large?: boolean 61 | ) { 62 | const start = polarToCartesian(x, y, radius, endAngle) 63 | const end = polarToCartesian(x, y, radius, startAngle) 64 | 65 | const largeArcFlag = 66 | large !== true && endAngle - startAngle <= 180 ? "0" : "1" 67 | 68 | const d = [ 69 | "M", 70 | start.x, 71 | start.y, 72 | "A", 73 | radius, 74 | radius, 75 | 0, 76 | largeArcFlag, 77 | 0, 78 | end.x, 79 | end.y, 80 | ].join(" ") 81 | 82 | return d 83 | } 84 | -------------------------------------------------------------------------------- /src/gatsby-theme-material-ui-top-layout/components/top-layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | // tslint:disable-next-line: no-submodule-imports 3 | import ThemeTopLayout from "gatsby-theme-material-ui-top-layout/src/components/top-layout" 4 | 5 | // eslint-disable-next-line react/prop-types 6 | export default function TopLayout({ children, theme }) { 7 | return {children} 8 | } 9 | -------------------------------------------------------------------------------- /src/images/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export default function Page(props: { location: { pathname: string } }) { 4 | const { location } = props 5 | const { pathname } = location 6 | 7 | return ( 8 | <> 9 |

404 Not Found

10 |

11 | You just hit {pathname} that doesn't exist... 12 | the sadness. 13 |

14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/blocks/api.txt: -------------------------------------------------------------------------------- 1 | ```pseudo 2 | 3 | # Loads a builtin dataset 4 | dataset %1 %2 5 | # Loads a CSV dataset from an external internal URL. If the URL is a Google Sheet, it will automatically be converted to CSV. 6 | load dataset from url %1 %2 7 | # Open the block context menu, click 'Add Comment', paste CSV data. 8 | load dataset from comment %1 9 | # Fills missing data cells with the last or next value 10 | fill missing %1 %2 %3 %4 with %5 %6 11 | # Fills missing data cells with the given value. 12 | replace missing %1 with %2 of type %3 %4 13 | # Removes the selected columns from the dataset 14 | drop %1 %2 %3 %4 %5 15 | # Removes rows with identical column values in the dataset. 16 | filter duplicates in %1 %2 %3 %4 %5 17 | # Rename a columne 18 | rename %1 to %2 %3 19 | # Sorts the dataset based on the selected column and order. 20 | sort %1 %2 %3 21 | # Keeps the selected columns and drops the others 22 | select %1 %2 %3 %4 %5 23 | # Selects the rows for which the condition evaluates true 24 | filter %1 %2 %3 %4 25 | # Selects the rows for which the condition evaluates true 26 | filter %1 %2 %3 %4 27 | # Select N rows from the sample, from the head, tail or a random sample. 28 | take %1 rows from %2 %3 29 | # Adds a new column with the result of the computation. 30 | compute column %1 as %2 %3 %4 %5 31 | # Adds a new column with the result of the computation. 32 | compute column %1 as %2 %3 %4 %5 33 | # Renders the block data in a scatter plot 34 | scatterplot of x %1 y %2 %3 %4 size %5 group %6 %7 %8 %9 35 | # Renders the block data in a bar chart 36 | bar chart of index %1 y %2 of %3 group %4 %5 %6 %7 %8 37 | # Renders the block data in a bar chart 38 | pie chart of %1 %2 %3 %4 39 | # Renders the block data as a histogram 40 | histogram of %1 group %2 %3 %4 %5 41 | # Displays the block data as a table 42 | show table %1 %2 %3 %4 %5 %6 %7 %8 43 | # Renders the block data in a line chart 44 | line chart of x %1 y %2 %3 %4 group %5 %6 %7 %8 45 | # Renders the block data in a 2D heatmap 46 | heatmap of x %1 y %2 color %3 %4 %5 %6 47 | # Renders pairwize scatter plots 48 | scatterplot matrix %1 %2 %3 %4 group %5 %6 %7 49 | ``` -------------------------------------------------------------------------------- /src/pages/blocks/chart_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/chart_bar.png -------------------------------------------------------------------------------- /src/pages/blocks/chart_box_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/chart_box_plot.png -------------------------------------------------------------------------------- /src/pages/blocks/chart_heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/chart_heatmap.png -------------------------------------------------------------------------------- /src/pages/blocks/chart_histogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/chart_histogram.png -------------------------------------------------------------------------------- /src/pages/blocks/chart_lineplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/chart_lineplot.png -------------------------------------------------------------------------------- /src/pages/blocks/chart_pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/chart_pie.png -------------------------------------------------------------------------------- /src/pages/blocks/chart_scatterplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/chart_scatterplot.png -------------------------------------------------------------------------------- /src/pages/blocks/chart_scatterplot_matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/chart_scatterplot_matrix.png -------------------------------------------------------------------------------- /src/pages/blocks/chart_show_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/chart_show_table.png -------------------------------------------------------------------------------- /src/pages/blocks/data_arrange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_arrange.png -------------------------------------------------------------------------------- /src/pages/blocks/data_bin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_bin.png -------------------------------------------------------------------------------- /src/pages/blocks/data_correlation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_correlation.png -------------------------------------------------------------------------------- /src/pages/blocks/data_count.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_count.png -------------------------------------------------------------------------------- /src/pages/blocks/data_dataset_builtin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_dataset_builtin.png -------------------------------------------------------------------------------- /src/pages/blocks/data_dataset_read.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_dataset_read.png -------------------------------------------------------------------------------- /src/pages/blocks/data_dataset_write.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_dataset_write.png -------------------------------------------------------------------------------- /src/pages/blocks/data_drop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_drop.png -------------------------------------------------------------------------------- /src/pages/blocks/data_drop_duplicates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_drop_duplicates.png -------------------------------------------------------------------------------- /src/pages/blocks/data_fill_nully.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_fill_nully.png -------------------------------------------------------------------------------- /src/pages/blocks/data_filter_columns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_filter_columns.png -------------------------------------------------------------------------------- /src/pages/blocks/data_filter_string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_filter_string.png -------------------------------------------------------------------------------- /src/pages/blocks/data_linear_regression.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_linear_regression.png -------------------------------------------------------------------------------- /src/pages/blocks/data_load_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_load_text.png -------------------------------------------------------------------------------- /src/pages/blocks/data_load_url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_load_url.png -------------------------------------------------------------------------------- /src/pages/blocks/data_mutate_columns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_mutate_columns.png -------------------------------------------------------------------------------- /src/pages/blocks/data_mutate_number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_mutate_number.png -------------------------------------------------------------------------------- /src/pages/blocks/data_rename_column_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_rename_column_block.png -------------------------------------------------------------------------------- /src/pages/blocks/data_replace_nully.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_replace_nully.png -------------------------------------------------------------------------------- /src/pages/blocks/data_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_select.png -------------------------------------------------------------------------------- /src/pages/blocks/data_slice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_slice.png -------------------------------------------------------------------------------- /src/pages/blocks/data_summarize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_summarize.png -------------------------------------------------------------------------------- /src/pages/blocks/data_summarize_by_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/data_summarize_by_group.png -------------------------------------------------------------------------------- /src/pages/blocks/vega_encoding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/vega_encoding.png -------------------------------------------------------------------------------- /src/pages/blocks/vega_encoding_aggregate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/vega_encoding_aggregate.png -------------------------------------------------------------------------------- /src/pages/blocks/vega_encoding_bin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/vega_encoding_bin.png -------------------------------------------------------------------------------- /src/pages/blocks/vega_encoding_sort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/vega_encoding_sort.png -------------------------------------------------------------------------------- /src/pages/blocks/vega_encoding_sort_field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/vega_encoding_sort_field.png -------------------------------------------------------------------------------- /src/pages/blocks/vega_encoding_time_unit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/vega_encoding_time_unit.png -------------------------------------------------------------------------------- /src/pages/blocks/vega_encoding_type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/vega_encoding_type.png -------------------------------------------------------------------------------- /src/pages/blocks/vega_layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/blocks/vega_layer.png -------------------------------------------------------------------------------- /src/pages/chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/chart.png -------------------------------------------------------------------------------- /src/pages/discover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/discover.png -------------------------------------------------------------------------------- /src/pages/excel.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: About the Data Science Editor for Excel 3 | --- 4 | 5 | # About the Data Science Editor for Excel 6 | 7 | The **Data Science Editor for Excel** is an experimental structured editor to create data analysis programs 8 | in Excel Web. 9 | 10 | - **[Get It Now](https://appsource.microsoft.com/en-us/product/office/WA200005186)** 11 | 12 | ![A data analysis program](./hero.png) 13 | 14 | ## Getting started 15 | 16 | - Download the **[Getting Started DataSet Sheet](https://microsoft.github.io/data-science-editor-excel/hosted_files/data-science-editor-datasets.xlsx)** to get started with data 17 | - Click **Get Addin** and search **Data Science Editor**, add this addin to your workspace. 18 | 19 | ### "Data flows through blocks" 20 | 21 | Each data blocks holds a dataset and acts as a transformation pipeline to the next block. The pipeline looks as follows: 22 | 23 | - a data block receives an updated dataset from its predecessor 24 | - (optionally) the data block transforms the dataset. For example, it can sort the data according to a column. 25 | - (optionally) update charts or tables 26 | - the data block pushes the data to its successors 27 | 28 | ![A data analysis program](./story.png) 29 | 30 | ### 👀 Data preview 31 | 32 | The data blocks have a data preview button 👀 that allows to quickly glance at the dataset attached to the block. If a block also transforms the dataset, a before/after view is provided. 33 | 34 | ![Previewing data in a block](./preview.png) 35 | 36 | ### Charts on the fly 37 | 38 | Add charts as you transform the data. 39 | 40 | ![A block with charts](./chart.png) 41 | 42 | ### Discover transforms 43 | 44 | Blocks are great to discover the vocabulary of a language. This also applies with data science. 45 | 46 | ![A block category](./discover.png) 47 | 48 | ### Loading and Saving projects 49 | 50 | The blocks are automatically saved in the worksheet. 51 | -------------------------------------------------------------------------------- /src/pages/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/hero.png -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import DSBlockEditor from "../components/blockly/DSBlockEditor" 3 | 4 | import CoreHead from "../components/shell/Head" 5 | export const frontmatter = { 6 | title: "Data Science Editor", 7 | } 8 | export const Head = props => 9 | 10 | export default DSBlockEditor 11 | -------------------------------------------------------------------------------- /src/pages/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/preview.png -------------------------------------------------------------------------------- /src/pages/story.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/story.png -------------------------------------------------------------------------------- /src/pages/vscode.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: About the Data Science Editor for Visual Studio Code 3 | --- 4 | 5 | # Data Science Editor for Visual Studio Code 6 | 7 | The **Data Science Editor Extension** loads the Data Science Editor in a web view and allows to reference **.CSV files** from the workspace. 8 | 9 | 25 | 26 | ## Getting started 27 | 28 | - Download the **[Extension VSIX](https://microsoft.github.io/data-science-editor/vscode/data-science-editor.vsix)** 29 | - In Visual Studio Code, open the command palette and run **Extensions: Install from VSIX...** and select the downloaded vsix. 30 | - Following the instruction in [About](./about) 31 | 32 | ## Workspace blocks 33 | 34 | The **.CSV**, **.TSV**, **.JSON** (with or without comments) files can be accessed through the workspace blocks. 35 | 36 | ## Saving 37 | 38 | The current default is to support a single file `data.dsejson` in the root folder of the workspace. 39 | -------------------------------------------------------------------------------- /src/pages/vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/src/pages/vscode.png -------------------------------------------------------------------------------- /src/workers/csv/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csv-worker", 3 | "type": "module", 4 | "source": "csv.worker.ts", 5 | "main": "./dist/node_modules/csv-worker.umd.js", 6 | "module": "./dist/node_modules/csv-worker.cjs.js", 7 | "exports": "./dist/node_modules/csv-worker.js", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /src/workers/csv/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["webworker", "es2020", "scripthost"], 5 | "types": [], 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "declaration": true 9 | }, 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /src/workers/csv/workerloader.js: -------------------------------------------------------------------------------- 1 | export default function createCsvWorker() { 2 | return ( 3 | typeof Window !== "undefined" && 4 | new Worker( 5 | new URL( 6 | "./dist/node_modules/csv-worker.js", 7 | import.meta.url // syntax not supported in typescript 8 | ) 9 | ) 10 | ) 11 | } -------------------------------------------------------------------------------- /src/workers/data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "data-worker", 3 | "type": "module", 4 | "source": "data.worker.ts", 5 | "main": "./dist/node_modules/data-worker.umd.js", 6 | "module": "./dist/node_modules/data-worker.cjs.js", 7 | "exports": "./dist/node_modules/data-worker.js", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /src/workers/data/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["webworker", "es2020", "scripthost"], 5 | "types": [], 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "declaration": true 9 | }, 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /src/workers/data/workerloader.js: -------------------------------------------------------------------------------- 1 | export default function createDataWorker() { 2 | return ( 3 | typeof Window !== "undefined" && 4 | new Worker( 5 | new URL( 6 | "./dist/node_modules/data-worker.js", 7 | import.meta.url // syntax not supported in typescript 8 | ) 9 | ) 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /static/blockly/media/1x1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/1x1.gif -------------------------------------------------------------------------------- /static/blockly/media/click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/click.mp3 -------------------------------------------------------------------------------- /static/blockly/media/click.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/click.ogg -------------------------------------------------------------------------------- /static/blockly/media/click.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/click.wav -------------------------------------------------------------------------------- /static/blockly/media/delete.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/delete.mp3 -------------------------------------------------------------------------------- /static/blockly/media/delete.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/delete.ogg -------------------------------------------------------------------------------- /static/blockly/media/delete.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/delete.wav -------------------------------------------------------------------------------- /static/blockly/media/disconnect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/disconnect.mp3 -------------------------------------------------------------------------------- /static/blockly/media/disconnect.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/disconnect.ogg -------------------------------------------------------------------------------- /static/blockly/media/disconnect.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/disconnect.wav -------------------------------------------------------------------------------- /static/blockly/media/dropdown-arrow.svg: -------------------------------------------------------------------------------- 1 | dropdown-arrow -------------------------------------------------------------------------------- /static/blockly/media/handclosed.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/handclosed.cur -------------------------------------------------------------------------------- /static/blockly/media/handdelete.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/handdelete.cur -------------------------------------------------------------------------------- /static/blockly/media/handopen.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/handopen.cur -------------------------------------------------------------------------------- /static/blockly/media/pilcrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/pilcrow.png -------------------------------------------------------------------------------- /static/blockly/media/quote0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/quote0.png -------------------------------------------------------------------------------- /static/blockly/media/quote1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/quote1.png -------------------------------------------------------------------------------- /static/blockly/media/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/blockly/media/sprites.png -------------------------------------------------------------------------------- /static/blockly/media/sprites.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /static/datasets/mtcars.csv: -------------------------------------------------------------------------------- 1 | model,mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb 2 | Mazda RX4,21,6,160,110,3.9,2.62,16.46,0,1,4,4 3 | Mazda RX4 Wag,21,6,160,110,3.9,2.875,17.02,0,1,4,4 4 | Datsun 710,22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 5 | Hornet 4 Drive,21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 6 | Hornet Sportabout,18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 7 | Valiant,18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 8 | Duster 360,14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 9 | Merc 240D,24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 10 | Merc 230,22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 11 | Merc 280,19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 12 | Merc 280C,17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 13 | Merc 450SE,16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 14 | Merc 450SL,17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 15 | Merc 450SLC,15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 16 | Cadillac Fleetwood,10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 17 | Lincoln Continental,10.4,8,460,215,3,5.424,17.82,0,0,3,4 18 | Chrysler Imperial,14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 19 | Fiat 128,32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 20 | Honda Civic,30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 21 | Toyota Corolla,33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 22 | Toyota Corona,21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 23 | Dodge Challenger,15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 24 | AMC Javelin,15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 25 | Camaro Z28,13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 26 | Pontiac Firebird,19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 27 | Fiat X1-9,27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 28 | Porsche 914-2,26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 29 | Lotus Europa,30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 30 | Ford Pantera L,15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 31 | Ferrari Dino,19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 32 | Maserati Bora,15,8,301,335,3.54,3.57,14.6,0,1,5,8 33 | Volvo 142E,21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 34 | -------------------------------------------------------------------------------- /static/videos/cereal-calories-per-cup.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/videos/cereal-calories-per-cup.mp4 -------------------------------------------------------------------------------- /static/videos/cereal-calories-sort.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/videos/cereal-calories-sort.mp4 -------------------------------------------------------------------------------- /static/videos/vscode.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/static/videos/vscode.mp4 -------------------------------------------------------------------------------- /static/vscode/README.md: -------------------------------------------------------------------------------- 1 | This folder contains the built VSIX extension. 2 | -------------------------------------------------------------------------------- /vscode/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "**/*.d.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /vscode/.gitignore: -------------------------------------------------------------------------------- 1 | dist/web 2 | dist/ -------------------------------------------------------------------------------- /vscode/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"] 5 | } 6 | -------------------------------------------------------------------------------- /vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Web Extension ", 10 | "type": "extensionHost", 11 | "debugWebWorkerHost": true, 12 | "request": "launch", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}", 15 | "--extensionDevelopmentKind=web" 16 | ], 17 | "outFiles": ["${workspaceFolder}/dist/web/**/*.js"], 18 | "preLaunchTask": "npm: watch-web" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /vscode/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "cSpell.words": [ 12 | "browsercheck", 13 | "hidesplash" 14 | ] 15 | } -------------------------------------------------------------------------------- /vscode/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "compile-web", 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | }, 13 | "problemMatcher": [ 14 | "$ts-webpack", 15 | "$tslint-webpack" 16 | ] 17 | }, 18 | { 19 | "type": "npm", 20 | "script": "watch-web", 21 | "group": "build", 22 | "isBackground": true, 23 | "problemMatcher": [ 24 | "$ts-webpack-watch", 25 | "$tslint-webpack-watch" 26 | ] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test-web/** 3 | src/** 4 | out/** 5 | node_modules/** 6 | .gitignore 7 | vsc-extension-quickstart.md 8 | webpack.config.js 9 | .yarnrc 10 | **/tsconfig.json 11 | **/.eslintrc.json 12 | **/*.map 13 | **/*.ts 14 | -------------------------------------------------------------------------------- /vscode/.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true -------------------------------------------------------------------------------- /vscode/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "data-science-editor" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /vscode/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /vscode/README.md: -------------------------------------------------------------------------------- 1 | # Data Science Editor 2 | 3 | https://user-images.githubusercontent.com/4175913/230733058-7d9a5d70-9b11-42d7-9bea-c9ebb45420a0.mp4 4 | 5 | ## Features 6 | 7 | Integrated the [Data Science Editor](https://microsoft.github.io/data-science-editor/) to explore your `.csv`, `.tsv` or `.json(c)` files. 8 | 9 | - [Read the documentation](https://microsoft.github.io/data-science-editor/vscode) 10 | -------------------------------------------------------------------------------- /vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "data-science-editor", 3 | "displayName": "Data Science Editor", 4 | "description": "Explore data through a block based data editor.", 5 | "version": "0.0.1", 6 | "license": "MIT", 7 | "engines": { 8 | "vscode": "^1.77.0" 9 | }, 10 | "publisher": "Microsoft Corporation", 11 | "categories": [ 12 | "Data Science", 13 | "Other", 14 | "Visualization" 15 | ], 16 | "keywords": [ 17 | "data exploration", 18 | "data visualization", 19 | "dataviz", 20 | "CSV" 21 | ], 22 | "repository": { 23 | "url": "https://github.com/microsoft/data-science-editor", 24 | "directory": "vscode" 25 | }, 26 | "activationEvents": [], 27 | "browser": "./dist/web/extension.js", 28 | "contributes": { 29 | "commands": [ 30 | { 31 | "command": "extensions.datascienceeditor.open", 32 | "title": "Open Data Science Editor" 33 | } 34 | ], 35 | "menus": { 36 | "commandPalette": [ 37 | { 38 | "command": "extensions.datascienceeditor.open", 39 | "when": "workbenchState != 'empty'" 40 | } 41 | ] 42 | } 43 | }, 44 | "scripts": { 45 | "vscode:prepublish": "yarn run package-web", 46 | "compile-web": "webpack", 47 | "watch-web": "webpack --watch", 48 | "package-web": "webpack --mode production --devtool hidden-source-map", 49 | "package": "vsce package --yarn --out ../static/vscode/data-science-editor.vsix", 50 | "lint": "eslint src --ext ts", 51 | "run-in-browser": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=. ." 52 | }, 53 | "devDependencies": { 54 | "@types/mocha": "^10.0.1", 55 | "@types/vscode": "^1.77.0", 56 | "@types/webpack-env": "^1.18.0", 57 | "@typescript-eslint/eslint-plugin": "^5.56.0", 58 | "@typescript-eslint/parser": "^5.56.0", 59 | "@vscode/test-web": "^0.0.36", 60 | "@vscode/vsce": "^2.18.0", 61 | "assert": "^2.0.0", 62 | "eslint": "^8.36.0", 63 | "process": "^0.11.10", 64 | "ts-loader": "^9.4.2", 65 | "typescript": "^4.9.5", 66 | "vscode-uri": "^3.0.7", 67 | "webpack": "^5.76.3", 68 | "webpack-cli": "^5.0.1" 69 | }, 70 | "dependencies": {} 71 | } -------------------------------------------------------------------------------- /vscode/src/web/blocks.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | /* eslint-disable curly */ 3 | const colour = "#107C41"; 4 | 5 | export let currentWorkspace: unknown; 6 | 7 | export const blocks = [ 8 | { 9 | kind: "block", 10 | type: "vscode_import_csv", 11 | message0: "workspace csv file %1", 12 | tooltip: "Loads data from a CSV/TSV file in the workspace", 13 | colour, 14 | args0: [ 15 | { 16 | type: "ds_field_iframe_data_chooser", 17 | name: "file", 18 | dataId: "files", 19 | }, 20 | ], 21 | nextStatement: "DataScienceStatement", 22 | dataPreviewField: true, 23 | template: "meta", 24 | }, 25 | ]; 26 | 27 | export const category = [ 28 | { 29 | kind: "category", 30 | name: "Workspace", 31 | colour, 32 | contents: blocks.map(block => ({ kind: "block", type: block.type })), 33 | order: 100, 34 | }, 35 | ]; 36 | 37 | function transformHeader(h: string) { 38 | return h 39 | .trim() 40 | .replace(/[.]/g, "") 41 | .replace(/(-|_)/g, " ") 42 | .toLocaleLowerCase(); 43 | } 44 | 45 | export const transforms: Record< 46 | string, 47 | ( 48 | b: any, 49 | dataset: any 50 | ) => Promise<{ warning?: string; dataset?: any; datasetSource?: string }> 51 | > = { 52 | // don't rename these identifiers, they are used in the serialized blocky and will break existing files 53 | // eslint-disable-next-line @typescript-eslint/naming-convention 54 | vscode_import_csv: async b => { 55 | const fileName = b.inputs[0].fields["file"].value; 56 | if (!fileName) { 57 | console.debug(`no file selected`); 58 | return { dataset: [] }; 59 | } 60 | let error: string | undefined; 61 | try { 62 | const buf = await vscode.workspace.fs.readFile( 63 | vscode.Uri.file(fileName) 64 | ); 65 | const datasetSource = new TextDecoder().decode(buf); 66 | return { 67 | datasetSource, 68 | }; 69 | } catch (e: any) { 70 | error = e.message; 71 | } 72 | const dataset: any = []; 73 | if (!dataset) 74 | return { warning: error || "file not found", dataset: [] }; 75 | return { dataset }; 76 | }, 77 | }; 78 | 79 | export const setCurrentWorkspace = (workspace: any) => { 80 | currentWorkspace = workspace; 81 | }; 82 | -------------------------------------------------------------------------------- /vscode/src/web/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | import * as vscode from "vscode"; 3 | import { WebView } from "./webview"; 4 | 5 | export function activate(context: vscode.ExtensionContext) { 6 | const view = new WebView(context); 7 | } 8 | -------------------------------------------------------------------------------- /vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "dist", 6 | "lib": [ 7 | "ES2020", "WebWorker" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /vscode/vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your web extension. 6 | * `package.json` * this is the manifest file in which you declare your extension and command. 7 | * `src/web/extension.ts` * this is the main file for the browser 8 | * `webpack.config.js` * the webpack config file for the web main 9 | 10 | ## Setup 11 | 12 | * install the recommended extensions (amodio.tsl-problem-matcher and dbaeumer.vscode-eslint) 13 | 14 | ## Get up and running the Web Extension 15 | 16 | * Run `npm install`. 17 | * Place breakpoints in `src/web/extension.ts`. 18 | * Debug via F5 (Run Web Extension). 19 | * Execute extension code via `F1 > Hello world`. 20 | 21 | ## Make changes 22 | 23 | * You can relaunch the extension from the debug toolbar after changing code in `src/web/extension.ts`. 24 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 25 | 26 | ## Explore the API 27 | 28 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 29 | 30 | ## Run tests 31 | 32 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 33 | * Press `F5` to run the tests in a new window with your extension loaded. 34 | * See the output of the test result in the debug console. 35 | * Make changes to `src/web/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 36 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 37 | * You can create folders inside the `test` folder to structure your tests any way you want. 38 | 39 | ## Go further 40 | 41 | * [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns. 42 | * Check out the [Web Extension Guide](https://code.visualstudio.com/api/extension-guides/web-extensions). 43 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 44 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 45 | -------------------------------------------------------------------------------- /vscode/vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/data-science-editor/fbf2e7192b5aad72e10d45c261a3452add50a3d5/vscode/vscode.png -------------------------------------------------------------------------------- /vscode/webpack.config.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | //@ts-check 7 | "use strict"; 8 | 9 | //@ts-check 10 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 11 | 12 | const path = require("path"); 13 | const webpack = require("webpack"); 14 | 15 | /** @type WebpackConfig */ 16 | const webExtensionConfig = { 17 | mode: "none", // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 18 | target: "webworker", // extensions run in a webworker context 19 | entry: { 20 | extension: "./src/web/extension.ts", 21 | }, 22 | output: { 23 | filename: "[name].js", 24 | path: path.join(__dirname, "./dist/web"), 25 | libraryTarget: "commonjs", 26 | devtoolModuleFilenameTemplate: "../../[resource-path]", 27 | }, 28 | resolve: { 29 | mainFields: ["browser", "module", "main"], // look for `browser` entry point in imported node modules 30 | extensions: [".ts", ".js"], // support ts-files and js-files 31 | alias: { 32 | // provides alternate implementation for node module and source files 33 | }, 34 | fallback: { 35 | // Webpack 5 no longer polyfills Node.js core modules automatically. 36 | // see https://webpack.js.org/configuration/resolve/#resolvefallback 37 | // for the list of Node.js core module polyfills. 38 | assert: require.resolve("assert"), 39 | }, 40 | }, 41 | module: { 42 | rules: [ 43 | { 44 | test: /\.ts$/, 45 | exclude: /node_modules/, 46 | use: [ 47 | { 48 | loader: "ts-loader", 49 | }, 50 | ], 51 | }, 52 | ], 53 | }, 54 | plugins: [ 55 | new webpack.optimize.LimitChunkCountPlugin({ 56 | maxChunks: 1, // disable chunks by default since web extensions must be a single bundle 57 | }), 58 | new webpack.ProvidePlugin({ 59 | process: "process/browser", // provide a shim for the global `process` variable 60 | }), 61 | ], 62 | externals: { 63 | vscode: "commonjs vscode", // ignored because it doesn't exist 64 | }, 65 | performance: { 66 | hints: false, 67 | }, 68 | devtool: "nosources-source-map", // create a source map that points to the original source file 69 | infrastructureLogging: { 70 | level: "log", // enables logging required for problem matchers 71 | }, 72 | }; 73 | 74 | module.exports = [webExtensionConfig]; 75 | --------------------------------------------------------------------------------