···7474- Migrated from Yarn 4 to npm; `yarn.lock` / `.yarnrc.yml` / `.yarn/` removed; `"resolutions"` → `"overrides"`; `jlpm` replaced with `npm run` in all scripts
7575- `yarn.lock` added to `.gitignore` (regenerated as a build side-effect by `@jupyterlab/builder`'s bundled jlpm)
76767777+### Built-in datasets
7878+7979+Three new built-in datasets added to the **Data** category:
8080+8181+- **iris** — Fisher iris dataset (sepal/petal measurements for 3 species) via `sns.load_dataset('iris')`
8282+- **titanic** — Titanic passenger survival dataset via `sns.load_dataset('titanic')`
8383+- **gapminder** — Life expectancy / GDP across countries and years via `px.data.gapminder()`
8484+8585+No new dependencies required — iris and titanic use the existing seaborn import;
8686+gapminder uses the existing plotly.express import.
8787+7788### Docs
78897990- `docs/getting-started.md`: step-by-step guide for installing and testing the extension in JupyterLab
8091- `docs/architecture.md`: full architecture reference (package layout, data-flow, extension points)
8192- `docs/blocks-reference.md`: complete block reference with dplyr mapping, description, and generated Python for every block
9393+- `docs/custom-blocks.md`: step-by-step tutorial for adding custom blocks and toolboxes
8294- `docs/work-summary.md`: narrative summary of all engineering work done in this release
8395- `docs/modernization-plan.md`: full modernization plan with phase-by-phase status
9696+- `docs/installation.md`: updated to use npm commands and correct package name
9797+- `docs/other_extensions.md`: updated to reference `jupyter-tidyblocks` package and point to `custom-blocks.md`
9898+- `docs/toolbox.md`: updated with correct category list and colours
9999+- `docs/index.rst`: TOC updated to include all new docs
84100- `README.md`: rewritten; credits Greg Wilson's tidyblocks and QuantStack/jupyterlab-blockly
8510186102<!-- <END NEW CHANGELOG ENTRY> -->
+305
docs/custom-blocks.md
···11+# Adding Custom Blocks
22+33+This guide walks you through adding a new block to jupyter-tidyblocks from scratch.
44+By the end you will have a working block that appears in the toolbox, can be dragged
55+onto the canvas, and generates executable Python code.
66+77+We will build a concrete example: a **`normalize` block** that scales a numeric
88+column to the 0–1 range using pandas `min-max` normalization.
99+1010+---
1111+1212+## Overview
1313+1414+Every block requires three things:
1515+1616+1. **Block shape** — the visual appearance and inputs, defined in
1717+ `packages/tidyblocks/src/blocks/`.
1818+2. **Python generator** — a function that converts the block to Python code, defined
1919+ in `packages/tidyblocks/src/generators/python/`.
2020+3. **Toolbox entry** — so the block appears in the sidebar panel, defined in
2121+ `packages/tidyblocks/src/toolbox.ts`.
2222+2323+Optionally, if the block needs a Python import (e.g. `import scipy`), you also set
2424+a **`toplevel_init`** string on the block.
2525+2626+---
2727+2828+## Step 1 — Define the block shape
2929+3030+Block shapes live in `packages/tidyblocks/src/blocks/`. Each file in this directory
3131+corresponds to a category. Our block is a transform operation, so open
3232+`transform.ts`.
3333+3434+Add a new entry inside the `Blockly.defineBlocksWithJsonArray([...])` call:
3535+3636+```typescript
3737+{
3838+ // Type name must be unique across the whole Blockly registry.
3939+ // Convention: tidyblocks_<category>_<verb>
4040+ type: 'tidyblocks_transform_normalize',
4141+4242+ // message0 is the label text. %1 marks where a field or input goes.
4343+ message0: 'normalize column %1',
4444+4545+ // args0 defines the inputs referenced in message0.
4646+ // field_input is a plain text box. Other options: field_number,
4747+ // field_dropdown, input_value (for connectable value blocks).
4848+ args0: [
4949+ { type: 'field_input', name: 'COLUMN', text: 'value' }
5050+ ],
5151+5252+ // previousStatement/nextStatement make the block chainable.
5353+ // Set both to null to allow connecting above and below.
5454+ // Remove one to make the block a source (top) or terminal (bottom).
5555+ previousStatement: null,
5656+ nextStatement: null,
5757+5858+ // Colour should match the category. Transform blocks use #76AADB.
5959+ colour: '#76AADB',
6060+6161+ tooltip: 'Scale a numeric column to the 0–1 range (min-max normalization).'
6262+},
6363+```
6464+6565+### Field types quick-reference
6666+6767+| Field type | When to use | Key properties |
6868+|---|---|---|
6969+| `field_input` | Free-text string (column name, label) | `name`, `text` (default) |
7070+| `field_number` | Numeric value with optional min/max | `name`, `value`, `min`, `max`, `precision` |
7171+| `field_dropdown` | Enumerated choice | `name`, `options: [['Label', 'VALUE'], ...]` |
7272+| `input_value` | Connectable value block (expression) | `name`, `check` (e.g. `'Boolean'`) |
7373+7474+---
7575+7676+## Step 2 — Write the Python generator
7777+7878+Generators live in `packages/tidyblocks/src/generators/python/`. Open the file
7979+matching your category (`transform.ts`).
8080+8181+Add a new `forBlock` entry:
8282+8383+```typescript
8484+// dplyr: no direct equivalent — min-max scaling
8585+pythonGenerator.forBlock['tidyblocks_transform_normalize'] = block => {
8686+ const col = block.getFieldValue('COLUMN');
8787+ return (
8888+ `_df = _df.assign(**{'${col}': ` +
8989+ `(_df['${col}'] - _df['${col}'].min()) / ` +
9090+ `(_df['${col}'].max() - _df['${col}'].min())})\n`
9191+ );
9292+};
9393+```
9494+9595+The generator receives the `block` object (and optionally a `generator` reference
9696+for nested value blocks). It returns a **string of Python code** ending with `\n`.
9797+9898+### Reading block values
9999+100100+| Scenario | Code |
101101+|---|---|
102102+| Text / number field | `block.getFieldValue('FIELD_NAME')` |
103103+| Connected value block | `generator.valueToCode(block, 'INPUT_NAME', Order.NONE)` |
104104+| Dropdown field | `block.getFieldValue('FIELD_NAME')` — returns the option's `VALUE` string |
105105+106106+### The `_df` convention
107107+108108+All transform blocks read from `_df` and write back to `_df`. This is what makes
109109+blocks chainable — each block in a stack operates on the DataFrame left by the block
110110+above it. Source (data) blocks create `_df`; terminal blocks (display, plot, save)
111111+consume it without writing it back.
112112+113113+---
114114+115115+## Step 3 — Add a `toplevel_init` (if needed)
116116+117117+If your block requires a Python import that isn't already guaranteed by the data
118118+block's preamble (`pandas`, `numpy`, `seaborn`), attach a `toplevel_init` string
119119+to the block after `defineBlocksWithJsonArray`:
120120+121121+```typescript
122122+// Only needed if the block uses a library not covered by the data preamble.
123123+// For example, if normalize used scipy:
124124+Blockly.Blocks['tidyblocks_transform_normalize'].toplevel_init =
125125+ 'from sklearn.preprocessing import MinMaxScaler\n';
126126+```
127127+128128+`BlocklyLayout.getBlocksToplevelInit()` collects all `toplevel_init` strings from
129129+blocks currently on the canvas, deduplicates them, and prepends them to the generated
130130+code before execution. This means the import is emitted exactly once regardless of
131131+how many normalize blocks the user places.
132132+133133+Our normalize block only uses pandas, which is already imported by the data block's
134134+preamble, so **no `toplevel_init` is needed here**.
135135+136136+---
137137+138138+## Step 4 — Add the block to the toolbox
139139+140140+Open `packages/tidyblocks/src/toolbox.ts` and find the `Transform` category.
141141+Add an entry for the new block:
142142+143143+```typescript
144144+{ kind: 'block', type: 'tidyblocks_transform_normalize' },
145145+```
146146+147147+Place it near related blocks — for example, after `tidyblocks_transform_mutate`:
148148+149149+```typescript
150150+{ kind: 'block', type: 'tidyblocks_transform_mutate' },
151151+{ kind: 'block', type: 'tidyblocks_transform_normalize' }, // ← new
152152+```
153153+154154+If you want to pre-populate a field value in the toolbox flyout (so dragging the
155155+block onto the canvas gives a sensible default), use:
156156+157157+```typescript
158158+{
159159+ kind: 'block',
160160+ type: 'tidyblocks_transform_normalize',
161161+ fields: { COLUMN: 'value' }
162162+},
163163+```
164164+165165+---
166166+167167+## Step 5 — Build and verify
168168+169169+```bash
170170+# Rebuild the extension
171171+npm run build
172172+173173+# Start JupyterLab
174174+jupyter lab
175175+```
176176+177177+1. Open (or create) a `.jpblockly` file.
178178+2. Select the **Tidy Data** toolbox from the toolbar dropdown.
179179+3. Expand the **Transform** category — your block should appear.
180180+4. Drag a data block (e.g. penguins) onto the canvas, then chain the normalize block
181181+ below it.
182182+5. Click **Run** (▶). The output cell should show the DataFrame with the column
183183+ scaled to 0–1.
184184+185185+---
186186+187187+## Complete worked example
188188+189189+Here is everything together for the normalize block.
190190+191191+**`packages/tidyblocks/src/blocks/transform.ts`** — inside `defineBlocksWithJsonArray`:
192192+193193+```typescript
194194+{
195195+ type: 'tidyblocks_transform_normalize',
196196+ message0: 'normalize column %1',
197197+ args0: [
198198+ { type: 'field_input', name: 'COLUMN', text: 'value' }
199199+ ],
200200+ previousStatement: null,
201201+ nextStatement: null,
202202+ colour: '#76AADB',
203203+ tooltip: 'Scale a numeric column to the 0–1 range (min-max normalization).'
204204+},
205205+```
206206+207207+**`packages/tidyblocks/src/generators/python/transform.ts`**:
208208+209209+```typescript
210210+pythonGenerator.forBlock['tidyblocks_transform_normalize'] = block => {
211211+ const col = block.getFieldValue('COLUMN');
212212+ return (
213213+ `_df = _df.assign(**{'${col}': ` +
214214+ `(_df['${col}'] - _df['${col}'].min()) / ` +
215215+ `(_df['${col}'].max() - _df['${col}'].min())})\n`
216216+ );
217217+};
218218+```
219219+220220+**`packages/tidyblocks/src/toolbox.ts`**:
221221+222222+```typescript
223223+{ kind: 'block', type: 'tidyblocks_transform_normalize' },
224224+```
225225+226226+---
227227+228228+## Adding a new category
229229+230230+If your blocks don't fit an existing category, create a new file in `blocks/` and
231231+`generators/python/`, then:
232232+233233+1. Import both files in `packages/tidyblocks/src/index.ts` (the side-effect imports
234234+ section at the top).
235235+2. Add a new `{ kind: 'category', name: '...', colour: '...', contents: [...] }`
236236+ object to `TIDYBLOCKS_TOOLBOX` in `toolbox.ts`.
237237+238238+Choose a colour that contrasts with the existing categories:
239239+240240+| Category | Colour |
241241+|---|---|
242242+| Data | `#FEBE4C` (amber) |
243243+| Transform | `#76AADB` (blue) |
244244+| Combine | `#808080` (grey) |
245245+| Plot | `#A4C588` (green) |
246246+| Stats | `#BA93DB` (purple) |
247247+| Values | `#E7553C` (red-orange) |
248248+| Operations | `#F9B5B2` (pink) |
249249+250250+---
251251+252252+## Extending from another JupyterLab plugin
253253+254254+You can add blocks from an entirely separate JupyterLab extension without modifying
255255+this package. Declare `IBlocklyRegistry` as a `required` token in your plugin and
256256+call the registry methods directly:
257257+258258+```typescript
259259+import { IBlocklyRegistry } from 'jupyter-tidyblocks';
260260+import * as Blockly from 'blockly';
261261+import { pythonGenerator } from 'blockly/python';
262262+263263+const myPlugin: JupyterFrontEndPlugin<void> = {
264264+ id: 'my-extension:plugin',
265265+ requires: [IBlocklyRegistry],
266266+ activate: (app, registry: IBlocklyRegistry) => {
267267+ // 1. Define the block shape
268268+ registry.registerBlocks([{
269269+ type: 'my_custom_block',
270270+ message0: 'do something with %1',
271271+ args0: [{ type: 'field_input', name: 'VALUE', text: 'x' }],
272272+ previousStatement: null,
273273+ nextStatement: null,
274274+ colour: '#5BA65B',
275275+ tooltip: 'My custom block.'
276276+ }]);
277277+278278+ // 2. Register the Python generator
279279+ pythonGenerator.forBlock['my_custom_block'] = block => {
280280+ const val = block.getFieldValue('VALUE');
281281+ return `_df = my_transform(_df, '${val}')\n`;
282282+ };
283283+284284+ // 3. Push the enriched generator back into the registry
285285+ registry.registerGenerator('python', pythonGenerator);
286286+287287+ // 4. Register a toolbox that includes the new block
288288+ registry.registerToolbox('My Toolbox', {
289289+ kind: 'categoryToolbox',
290290+ contents: [{
291291+ kind: 'category',
292292+ name: 'Custom',
293293+ colour: '#5BA65B',
294294+ contents: [{ kind: 'block', type: 'my_custom_block' }]
295295+ }]
296296+ });
297297+ }
298298+};
299299+```
300300+301301+> **Important:** Always call `registry.registerGenerator('python', pythonGenerator)`
302302+> after attaching your `forBlock` handlers. This ensures the registry holds the
303303+> same generator instance that knows about your new block type, guarding against
304304+> webpack Module Federation resolving `blockly/python` to a different singleton
305305+> in a separate bundle.
+11-14
docs/getting-started.md
···3838git clone https://github.com/teonbrooks/jupyter-tidyblocks.git
3939cd jupyter-tidyblocks
40404141-# 2. Install Node.js dependencies and build the JS packages
4242-# (requires Node >= 18 and yarn/jlpm)
4343-jlpm install
4444-jlpm build
4545-4646-# 3. Install the Python package in editable mode
4141+# 2. Install the Python package and build the JS packages
4742pip install -e ".[dev]"
4343+jupyter labextension develop . --overwrite
4444+npm run build
48454949-# 4. Verify the extension is registered
4646+# 3. Verify the extension is registered
5047jupyter labextension list
5148```
5249···154151155152| Category | Color | Purpose | Example blocks |
156153|---|---|---|---|
157157-| **Data** | Gold | Load a dataset to start a pipeline | Penguins, Colors, CSV, Sequence, User variable |
158158-| **Transform** | Blue | Reshape or filter `_df` | filter, select, groupby, summarize, sort, bin, sample |
159159-| **Combine** | Grey | Merge two pipelines | join, glue (concat), cross_join |
154154+| **Data** | Gold | Load a dataset to start a pipeline | penguins, iris, titanic, gapminder, colors, CSV, sequence, user variable |
155155+| **Transform** | Blue | Reshape or filter `_df` | filter, select, mutate, arrange, groupby, summarize, count, distinct, bin, slice_head, slice_tail, slice_sample, slice_min, slice_max, relocate |
156156+| **Combine** | Grey | Merge two pipelines | join, semi_join, anti_join, bind_rows, bind_cols, cross_join |
160157| **Plot** | Green | Visualize and terminate a pipeline | scatter, bar, histogram, line, box, violin, heatmap |
161158| **Stats** | Purple | Statistical tests and summaries | t-test, k-means, correlation, describe |
162159| **Value** | Red | Produce a value (column ref, literal, distribution) | column, number, text, datetime, normal, uniform |
163163-| **Op** | Pink | Transform a value | arithmetic, compare, logic, ifelse, string, shift |
160160+| **Op** | Pink | Transform a value | arithmetic, compare, between, logic, ifelse, coalesce, n_distinct, string, shift |
164161165162> **Pipeline shape**: every pipeline starts with a **Data** block (no left
166163> connector), passes through zero or more **Transform** / **Combine** blocks
···235232236233```bash
237234# Rebuild all packages
238238-jlpm build
235235+npm run build
239236240237# Or watch for changes (rebuilds automatically, then refresh JupyterLab)
241241-jlpm watch
238238+npm run watch
242239```
243240244241To run unit tests:
245242246243```bash
247247-jlpm test
244244+npm test
248245```
249246250247To rebuild and reinstall the labextension into JupyterLab's static assets
···11+# tidyblocks — Original Feature Reference
22+33+**Repository:** https://github.com/gvwilson/tidyblocks (archived August 2024)
44+**Author:** Greg Wilson
55+**Concept:** A visual, block-based tidy-data analysis environment built on Google Blockly
66+with a pipeline execution model, internationalization support (8 languages), and integrated
77+plotting and statistics.
88+99+This document catalogs the features of the original `gvwilson/tidyblocks` project.
1010+It serves as a reference for what inspired `jupyter-tidyblocks`.
1111+1212+---
1313+1414+## Execution Model
1515+1616+Blocks form **pipelines** — linear chains starting from a data source and flowing through
1717+transforms into outputs (plots/stats/saves). Each block emits a JSON representation; a
1818+`Pipeline` runner executes the JSON sequence against an immutable `DataFrame` object.
1919+Multiple independent pipelines can exist in one workspace.
2020+2121+---
2222+2323+## Data Source Blocks
2424+2525+| Block | Behavior |
2626+|---|---|
2727+| `data_colors` | Built-in 11-color RGB dataset |
2828+| `data_earthquakes` | 2016 earthquake dataset |
2929+| `data_penguins` | Palmer penguins dataset |
3030+| `data_phish` | Phish concert dataset |
3131+| `data_spotify` | Spotify song data |
3232+| `data_sequence` | Generate 1..N sequence into named column |
3333+| `data_user` | Reference user-defined dataset by name |
3434+3535+---
3636+3737+## Transform Blocks
3838+3939+| Block | Behavior |
4040+|---|---|
4141+| `transform_filter` | Keep rows matching boolean expression |
4242+| `transform_select` | Keep specified columns |
4343+| `transform_drop` | Remove specified columns |
4444+| `transform_create` | Add / replace column with expression |
4545+| `transform_sort` | Sort by columns; optional descending |
4646+| `transform_unique` | Deduplicate by columns |
4747+| `transform_groupBy` | Group rows for subsequent aggregation |
4848+| `transform_ungroup` | Remove grouping |
4949+| `transform_summarize` | Aggregate with a summary function |
5050+| `transform_running` | Cumulative/window operation |
5151+| `transform_bin` | Discretize numeric column into N buckets |
5252+| `transform_saveAs` | Persist DataFrame under a name |
5353+5454+**Summarize functions:** `all`, `any`, `count`, `max`, `mean`, `median`, `min`, `std`, `sum`, `var`
5555+5656+**Running (cumulative) functions:** `cumall`, `cumany`, `cumcount`, `cummax`, `cummean`, `cummin`, `cumsum`
5757+5858+---
5959+6060+## Combine Blocks
6161+6262+| Block | Behavior |
6363+|---|---|
6464+| `combine_join` | Inner join on matching column values |
6565+| `combine_glue` | Vertical stack with source label column |
6666+6767+---
6868+6969+## Plot Blocks
7070+7171+Original plots used a custom JavaScript plotting library.
7272+7373+| Block | Behavior |
7474+|---|---|
7575+| `plot_bar` | Bar chart (x, y) |
7676+| `plot_box` | Box plot (x, y) |
7777+| `plot_dot` | Dot/strip plot (x only) |
7878+| `plot_histogram` | Histogram (column, nbins) |
7979+| `plot_scatter` | Scatter (x, y, color, regression toggle) |
8080+8181+---
8282+8383+## Statistics Blocks
8484+8585+| Block | Behavior |
8686+|---|---|
8787+| `stats_ttest_one` | One-sample two-sided t-test |
8888+| `stats_ttest_two` | Two-sample two-sided t-test |
8989+| `stats_k_means` | K-means clustering |
9090+| `stats_silhouette` | Silhouette coefficient for cluster quality |
9191+9292+---
9393+9494+## Operation (Expression) Blocks
9595+9696+| Sub-category | Blocks |
9797+|---|---|
9898+| Arithmetic | `+`, `-`, `*`, `/`, `%`, `**`, unary `-`, `abs()` |
9999+| Comparison | `==`, `!=`, `<`, `<=`, `>`, `>=` |
100100+| Logic | `and`, `or`, `not`, `if/else` ternary |
101101+| Pairwise extrema | `max(a,b)`, `min(a,b)` |
102102+| Type checking | `is_missing`, `is_number`, `is_text`, `is_date`, `is_bool` |
103103+| Type conversion | `to_bool`, `to_datetime`, `to_number`, `to_string` |
104104+| Datetime extraction | `year`, `month`, `day`, `weekday`, `hour`, `minute`, `second` |
105105+| Shift (lag/lead) | `shift(col, n)` |
106106+107107+---
108108+109109+## Value Blocks
110110+111111+| Block | Behavior |
112112+|---|---|
113113+| `value_column` | Column reference by name |
114114+| `value_number` | Numeric literal |
115115+| `value_text` | String literal |
116116+| `value_logical` | Boolean literal (true/false) |
117117+| `value_datetime` | Date/time constant (YYYY-MM-DD) |
118118+| `value_missing` | Explicit NA/NaN value |
119119+| `value_exponential` | Random draw from Exponential(λ) |
120120+| `value_normal` | Random draw from Normal(μ, σ) |
121121+| `value_uniform` | Random draw from Uniform(low, high) |
122122+123123+---
124124+125125+## Control Blocks
126126+127127+| Block | Behavior |
128128+|---|---|
129129+| `control_seed` | Set RNG seed for reproducibility |
130130+131131+---
132132+133133+## Internationalization
134134+135135+Block labels available in 8 languages via bundled JSON locale files.
136136+137137+---
138138+139139+## Key Architectural Concepts
140140+141141+### Immutable Pipeline Semantics
142142+Every transform returns a new DataFrame without mutating the input. Multiple independent
143143+pipelines can exist in one workspace.
144144+145145+### Pipeline Heads
146146+Blocks with a `hat: 'cap'` property start a pipeline. Multiple pipelines in one workspace
147147+produce multiple independent execution sequences.
148148+149149+### `saveAs` → Named Variables
150150+`transform_saveAs` persists a DataFrame under a name so later pipelines can reference it
151151+via `data_user`.
152152+153153+### Inline Random Distributions
154154+`value_normal`, `value_uniform`, `value_exponential` generate random values inline within
155155+expressions.
156156+157157+### `glue` with Source Labels
158158+`combine_glue` stacks two DataFrames vertically and adds a source-label column, enabling
159159+before/after or group-comparison workflows.
+35-50
docs/installation.md
···2233## Requirements
4455-- JupyterLab >= 4.0.0
55+- Python >= 3.8
66+- JupyterLab >= 4.5
6778## Install
88-99-To install the extension, execute:
1010-1111-```bash
1212-conda install -c conda-forge jupyterlab-blockly
1313-```
1414-1515-or
1691710```bash
1818-pip install jupyterlab-blockly
1111+pip install jupyter-tidyblocks
1912```
20132121-### Kernels
1414+### Supported kernels
22152323-- ipykernel
1616+- ipykernel (Python)
2417- xeus-python
2518- xeus-lua
2626-- [JavaScript](https://github.com/n-riesco/ijavascript#installation)
2727-- [JavaScript](https://github.com/yunabe/tslab)
1919+- [ijavascript](https://github.com/n-riesco/ijavascript#installation)
2020+- [tslab](https://github.com/yunabe/tslab)
28212922## Uninstall
30233131-To remove the extension, execute:
3232-3324```bash
3434-conda uninstall -c conda-forge jupyterlab-blockly
2525+pip uninstall jupyter-tidyblocks
3526```
36273737-or
2828+## Development install
2929+3030+**Note:** You will need Node.js >= 18 to build the extension.
38313932```bash
4040-pip uninstall jupyterlab-blockly
4141-```
3333+# Create and activate a conda environment
3434+micromamba create -n tidyblocks -c conda-forge python nodejs pre-commit jupyterlab ipykernel
3535+micromamba activate tidyblocks
42364343-## Development install
3737+# Clone the repo
3838+git clone https://github.com/teonbrooks/jupyter-tidyblocks
3939+cd jupyter-tidyblocks
44404545-**Note:** You will need NodeJS to build the extension package.
4141+# Install pre-commit hooks
4242+pre-commit install
46434747-The `jlpm` command is JupyterLab's pinned version of
4848-[yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use
4949-`yarn` or `npm` in lieu of `jlpm` below.
5050-5151-```bash
5252-micromamba create -n blockly -c conda-forge python nodejs=18 yarn pre-commit jupyterla jupyterlab-language-pack-es-ES jupyterlab-language-pack-fr-FR ipykernel xeus-python xeus-lua
5353-micromamba activate blockly
5454-# Clone the repo to your local environment
5555-# Change directory to the jupyterlab_blockly directory
5656-# Install package in development mode
4444+# Install the Python package in editable mode and build the JS packages
5745pip install -e ".[dev]"
5858-# Installing pre-commit to run command when adding commits
5959-pre-commit install
6060-# Link your development version of the extension with JupyterLab
6146jupyter labextension develop . --overwrite
6262-# Rebuild extension Typescript source after making changes
6363-jlpm build
4747+npm run build
6448```
65496666-You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
5050+You can watch the source directory and run JupyterLab at the same time in
5151+different terminals to watch for changes and automatically rebuild:
67526853```bash
6969-# Watch the source directory in one terminal, automatically rebuilding when needed
7070-jlpm watch
7171-# Run JupyterLab in another terminal
5454+# Terminal 1 — rebuild on source changes
5555+npm run watch
5656+5757+# Terminal 2 — run JupyterLab
7258jupyter lab
7359```
74607575-With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt).
6161+With the watch command running, every saved change will immediately be built
6262+locally and available in your running JupyterLab. Refresh the browser to load
6363+the change (you may need to wait several seconds for the rebuild to finish).
76647777-By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command:
6565+## Development uninstall
78667967```bash
8080-jupyter lab build --minimize=False
6868+pip uninstall jupyter-tidyblocks
8169```
82708383-## Development uninstall
7171+Remove the symlink created by `jupyter labextension develop`:
84728573```bash
8686-pip uninstall jupyterlab_blockly
7474+jupyter labextension list # find the labextensions folder
7575+# remove the jupyter-tidyblocks-extension symlink from that folder
8776```
8888-8989-In development mode, you will also need to remove the symlink created by `jupyter labextension develop`
9090-command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions`
9191-folder is located. Then you can remove the symlink named `jupyterlab-blockly` within that folder.
+221
docs/jupyter-tidyblocks-features.md
···11+# jupyter-tidyblocks Features
22+33+This document describes all features currently implemented in `jupyter-tidyblocks`.
44+55+For the blocks that inspired this project, see
66+[`inspirations/tidyblocks-features.md`](inspirations/tidyblocks-features.md).
77+For the full block reference with generated Python, see
88+[`blocks-reference.md`](blocks-reference.md).
99+1010+---
1111+1212+## Execution Model
1313+1414+Blocks form **pipelines** — linear chains starting from a data source, passing through
1515+zero or more transforms, and ending at a plot, stats, or display block. The entire
1616+pipeline generates a single Python code string that executes against the active
1717+JupyterLab kernel. The running DataFrame is always stored in `_df`.
1818+1919+Multiple independent pipelines can exist in one workspace; they execute as one combined
2020+code cell in top-to-bottom order.
2121+2222+---
2323+2424+## Data Source Blocks
2525+2626+Source blocks start a pipeline. They create `_df` and have no top connector.
2727+2828+| Block | Python generated | Notes |
2929+|---|---|---|
3030+| `penguins dataset` | `sns.load_dataset('penguins')` | Palmer penguins via seaborn |
3131+| `iris dataset` | `sns.load_dataset('iris')` | Fisher iris via seaborn |
3232+| `titanic dataset` | `sns.load_dataset('titanic')` | Titanic survival via seaborn |
3333+| `gapminder dataset` | `px.data.gapminder()` | GDP/life expectancy via plotly.express |
3434+| `colors dataset` | `pd.DataFrame({...})` | 11 named colors with RGB values (inline) |
3535+| `earthquakes dataset` | `pd.read_csv('<url>')` | 2016 global earthquake data |
3636+| `sequence 1 to N as col` | `pd.DataFrame({'col': range(1, N+1)})` | Integer sequence |
3737+| `dataset named name` | `name.copy()` | Reference a named DataFrame variable |
3838+| `read CSV path` | `pd.read_csv('path')` | Load local or remote CSV |
3939+4040+---
4141+4242+## Transform Blocks
4343+4444+Transform blocks read from `_df` and write back to `_df`. Block names follow
4545+[dplyr](https://dplyr.tidyverse.org/) conventions.
4646+4747+| Block | dplyr equivalent | Python generated |
4848+|---|---|---|
4949+| `filter where cond` | `filter()` | `_df[cond]` |
5050+| `select columns` | `select()` | `_df[[cols]]` |
5151+| `drop columns` | `select(-col)` | `_df.drop(columns=[cols])` |
5252+| `mutate col = expr` | `mutate()` | `_df.assign(**{'col': expr})` |
5353+| `rename old → new` | `rename()` | `_df.rename(columns={'old': 'new'})` |
5454+| `relocate col after` | `relocate()` | column reindex |
5555+| `arrange by cols` | `arrange()` | `_df.sort_values(by=[cols], ascending=...)` |
5656+| `distinct by cols` | `distinct()` | `_df.drop_duplicates(subset=[cols])` |
5757+| `group by cols` | `group_by()` | `_df.groupby([cols])` |
5858+| `ungroup` | `ungroup()` | `_df.reset_index(drop=True)` |
5959+| `summarize col = fn` | `summarize()` | `_df.agg({'col': 'fn'})` |
6060+| `count by cols` | `count()` | `_df.groupby([cols]).size().reset_index(name='n')` |
6161+| `running fn on col` | — | `_df.assign(col=_df['col'].cumfn())` |
6262+| `bin col into N` | — | `pd.cut(_df['col'], bins=N)` |
6363+| `save as name` | — | `name = _df.copy()` |
6464+| `fill na in col` | — | `_df.fillna({'col': value})` |
6565+| `drop na from col` | — | `_df.dropna(subset=[cols])` |
6666+| `slice_sample N rows` | `slice_sample()` | `_df.sample(n=N)` |
6767+| `slice_head N rows` | `slice_head()` | `_df.head(N)` |
6868+| `slice_tail N rows` | `slice_tail()` | `_df.tail(N)` |
6969+| `slice_min N by col` | `slice_min()` | `_df.nsmallest(N, 'col')` |
7070+| `slice_max N by col` | `slice_max()` | `_df.nlargest(N, 'col')` |
7171+| `display` | — | `display(_df)` |
7272+7373+**Summarize functions:** count, sum, mean, median, min, max, std dev, variance, n distinct, any, all
7474+7575+**Running functions:** cumulative sum, cumulative max, cumulative min, cumulative mean, row index
7676+7777+---
7878+7979+## Combine Blocks
8080+8181+Combine blocks merge two DataFrames. The left input is `_df`; the right is a named
8282+DataFrame (set with **save as**).
8383+8484+| Block | dplyr equivalent | Python generated |
8585+|---|---|---|
8686+| `join on left = right` | `inner_join()` | `pd.merge(_df, other, left_on=..., right_on=...)` |
8787+| `semi_join with other on col` | `semi_join()` | `_df[_df['col'].isin(other['col'])]` |
8888+| `anti_join with other on col` | `anti_join()` | `_df[~_df['col'].isin(other['col'])]` |
8989+| `bind_rows with other` | `bind_rows()` | `pd.concat([_df, other], ignore_index=True)` |
9090+| `bind_cols with other` | `bind_cols()` | `pd.concat([_df, other], axis=1)` |
9191+| `cross join with other` | — | `_df.merge(other, how='cross')` |
9292+9393+---
9494+9595+## Plot Blocks
9696+9797+Plot blocks visualize `_df` using [plotly.express](https://plotly.com/python/plotly-express/).
9898+They are terminal blocks (left connector only; nothing chains below them).
9999+100100+| Block | Python generated |
101101+|---|---|
102102+| `bar chart x y` | `px.bar(_df, x='x', y='y').show()` |
103103+| `box plot x y` | `px.box(_df, x='x', y='y').show()` |
104104+| `dot plot x` | `px.strip(_df, x='x').show()` |
105105+| `histogram x bins` | `px.histogram(_df, x='x', nbins=N).show()` |
106106+| `scatter x y color` | `px.scatter(_df, x='x', y='y', color='c').show()` |
107107+| `line chart x y color` | `px.line(_df, x='x', y='y', color='c').show()` |
108108+| `violin x y` | `px.violin(_df, x='x', y='y').show()` |
109109+| `heatmap` | `px.imshow(_df.corr()).show()` |
110110+111111+---
112112+113113+## Stats Blocks
114114+115115+Stats blocks run statistical analyses on `_df` using scipy and scikit-learn.
116116+They are terminal blocks.
117117+118118+| Block | Python generated |
119119+|---|---|
120120+| `one-sample t-test col vs mean` | `scipy.stats.ttest_1samp(_df['col'].dropna(), mean)` |
121121+| `two-sample t-test col by group` | `scipy.stats.ttest_ind(group1, group2)` |
122122+| `k-means on cols k clusters` | `KMeans(n_clusters=k).fit(_df[[cols]])` |
123123+| `silhouette score on cols` | `silhouette_score(_df[[cols]], labels)` |
124124+| `correlation of cols` | `_df[[cols]].corr()` |
125125+| `describe` | `_df.describe()` |
126126+127127+---
128128+129129+## Value Blocks
130130+131131+Value blocks produce a single value for use inside filter conditions or mutate
132132+expressions. They have no top/bottom connectors — they attach to input slots.
133133+134134+| Block | Python generated |
135135+|---|---|
136136+| `column name` | `_df['name']` |
137137+| `number N` | `N` |
138138+| `text "str"` | `'str'` |
139139+| `true / false` | `True` / `False` |
140140+| `datetime YYYY-MM-DD` | `pd.Timestamp('YYYY-MM-DD')` |
141141+| `missing` | `None` |
142142+| `normal(μ, σ)` | `np.random.normal(μ, σ, len(_df))` |
143143+| `uniform(low, high)` | `np.random.uniform(low, high, len(_df))` |
144144+| `exponential(λ)` | `np.random.exponential(λ, len(_df))` |
145145+146146+---
147147+148148+## Operation Blocks
149149+150150+Operation blocks build expressions used inside filter/mutate/etc.
151151+152152+| Sub-category | Blocks | dplyr equivalent |
153153+|---|---|---|
154154+| Arithmetic | `+`, `-`, `*`, `/`, `%`, `**`, unary `-`, `abs()` | — |
155155+| Comparison | `==`, `!=`, `<`, `<=`, `>`, `>=` | — |
156156+| Between | `col between lo and hi` | `between()` |
157157+| Logic | `and`, `or`, `not` | — |
158158+| If/else | `if cond then a else b` | `if_else()` |
159159+| Coalesce | `coalesce(col, default)` | `coalesce()` |
160160+| N distinct | `n_distinct(col)` | `n_distinct()` |
161161+| Type checking | `is missing`, `is number`, `is text`, `is date`, `is bool` | — |
162162+| Type conversion | `to bool`, `to datetime`, `to number`, `to string` | — |
163163+| Datetime extraction | `year`, `month`, `day`, `weekday`, `hour`, `minute`, `second` | — |
164164+| Shift | `shift col by n` | `lag()` / `lead()` |
165165+| Math | `round`, `floor`, `ceil`, `log`, `sqrt`, `exp` | — |
166166+| String | `upper`, `lower`, `strip` | — |
167167+| String contains | `col contains pattern` | `str_detect()` |
168168+169169+---
170170+171171+## Kernel Support
172172+173173+The same workspace can generate code for multiple kernel languages. The active kernel
174174+is selected from the **Kernel** dropdown in the editor toolbar.
175175+176176+| Kernel | Language | Generator |
177177+|---|---|---|
178178+| ipykernel / xeus-python | Python | `blockly/python` |
179179+| ijavascript / tslab | JavaScript | `blockly/javascript` |
180180+| xeus-lua | Lua | `blockly/lua` |
181181+182182+---
183183+184184+## Extensibility
185185+186186+The `IBlocklyRegistry` token (provided by `jupyter-tidyblocks`) allows any JupyterLab
187187+plugin to:
188188+189189+- Register new block shapes (`registerBlocks`)
190190+- Register new toolboxes (`registerToolbox`)
191191+- Register new code generators (`registerGenerator`)
192192+193193+See [Adding Custom Blocks](custom-blocks.md) for a step-by-step guide.
194194+195195+---
196196+197197+## File Format
198198+199199+Workspace state is saved as `.jpblockly` files — JSON containing the serialized Blockly
200200+workspace state. Files can be committed to version control and reopened to restore the
201201+exact block arrangement.
202202+203203+---
204204+205205+## Build System
206206+207207+- **Turborepo** for monorepo task orchestration across three packages
208208+- **Vite** (library mode) for `packages/blockly` and `packages/tidyblocks`
209209+- **`@jupyterlab/builder`** (webpack) for `packages/blockly-extension`
210210+- **npm workspaces** with `overrides` for dependency version pinning
211211+- **hatchling + hatch-jupyter-builder** for the Python wheel build
212212+213213+---
214214+215215+## Python Runtime Dependencies
216216+217217+Generated code uses the following libraries (install separately):
218218+219219+```bash
220220+pip install pandas numpy seaborn plotly scipy scikit-learn
221221+```
···11-# Other extensions
11+# Extending jupyter-tidyblocks
2233-The JupyterLab-Blockly extension is ready to be used as a base for other projects: you can register new Blocks, Toolboxes and Generators. It is a great tool for fast prototyping.
33+The `jupyter-tidyblocks` core extension exposes an `IBlocklyRegistry` token that
44+other JupyterLab plugins can use to register new blocks, toolboxes, and generators —
55+without modifying this repository.
4655-## Creating a new JupyterLab extension
66-You can easily create a new JupyterLab extension by using the official `copier` template, documented [here](https://github.com/jupyterlab/extension-template).
77-88-After installing the needed plugins (mentioned in the above link) and creating an extension directory, you can run the following command:
99-```
1010-copier copy --trust https://github.com/jupyterlab/extension-template .
1111-```
1212-which will ask you to fill some basic information about your project. Once completed, the directory will be populated with several files, all forming the base of your project. You will mostly work in the `index.ts` file, located in the `src` folder.
77+For a complete step-by-step walkthrough of adding blocks and toolboxes, see
88+[**Adding Custom Blocks**](custom-blocks.md).
1391414-An example of creating a simple JupyterLab extension, which also contains the instructions of how to fill the information asked by the `copier` template, can be found [here](https://github.com/jupyterlab/extension-examples/tree/master/hello-world).
1010+---
15111212+## Quick start
16131717-## Importing JupyterLab-Blockly
1818-Firstly you need to install and add `jupyterlab-blockly` as a dependency for your extension:
1919-```
2020-jlpm add jupyterlab-blockly
2121-```
1414+### 1. Create a new JupyterLab extension
22152323-Once it is part of your project, all you need to do is import `IBlocklyRegistry`, as it follows:
2424-```typescript
2525-// src/index.ts
1616+Use the official copier template:
26172727-import { IBlocklyRegistry } from 'jupyterlab-blockly';
1818+```bash
1919+pip install copier jinja2-time
2020+copier copy --trust https://github.com/jupyterlab/extension-template .
2821```
29223030-The `BlocklyRegistry` is the class that the JupyterLab-Blockly extension exposes to other plugins. This registry allows other plugins to register new Toolboxes, Blocks and Generators that users can use in the Blockly editor.
3131-3232-### Registering new Blocks
3333-The `IBlocklyRegistry` offers a function `registerBlocks`, which allows you to include new Blocks in your project. Blockly offers a [tool](https://blockly-demo.appspot.com/static/demos/blockfactory/index.html) which helps you easily create new Blocks and get their JSON definition and generator code in all supported programming languages.
2323+Fill in the prompts. You will mostly work in `src/index.ts`.
34243535-**NOTE** : Once you create a new block, it won't appear into your Blockly editor, unless you add it to a Toolbox.
2525+### 2. Add `jupyter-tidyblocks` as a dependency
36263737-```typescript
3838- /**
3939- * Register new blocks.
4040- *
4141- * @argument blocks Blocks to register.
4242- */
4343- registerBlocks(blocks: BlockDefinition[]): void {
4444- Blockly.defineBlocksWithJsonArray(blocks);
4545- }
2727+```bash
2828+npm install jupyter-tidyblocks
4629```
47304848-### Registering a new Toolbox
4949-Using the `registerToolbox` function, provided by `IBlocklyRegistry`, you can register a new toolbox. Once registered, the toolbox will appear automatically in your Blockly editor. You can find more information about switching to another toolbox [here](https://jupyterlab-blockly.readthedocs.io/en/latest/toolbox.html).
3131+Add it to your extension's `package.json` dependencies and declare it as a
3232+shared singleton in the `jupyterlab.sharedPackages` section so both your
3333+extension and the core share the same registry instance:
50345151-```typescript
5252-/**
5353- * Register a toolbox for the editor.
5454- *
5555- * @argument name Name of the toolbox.
5656- *
5757- * @argument value Toolbox to register.
5858- */
5959- registerToolbox(name: string, value: ToolboxDefinition): void {
6060- this._toolboxes.set(name, value);
3535+```json
3636+"jupyterlab": {
3737+ "sharedPackages": {
3838+ "jupyter-tidyblocks": {
3939+ "bundled": false,
4040+ "singleton": true
4141+ }
6142 }
4343+}
6244```
63456464-### Registering a new Generator
6565-Lastly, `IBlocklyRegistry` offers the function `registerGenerator` which lets you register a new Generator. You can read more about switching kernels [here](https://jupyterlab-blockly.readthedocs.io/en/latest/kernels.html).
4646+### 3. Use `IBlocklyRegistry` in your plugin
66476748```typescript
4949+// src/index.ts
5050+import { IBlocklyRegistry } from 'jupyter-tidyblocks';
5151+import * as Blockly from 'blockly';
5252+import { pythonGenerator } from 'blockly/python';
68536969- /**
7070- * Register new generators.
7171- *
7272- * @argument name Name of the generator.
7373- *
7474- * @argument generator Generator to register.
7575- *
7676- * #### Notes
7777- * When registering a generator, the name should correspond to the language
7878- * used by a kernel.
7979- *
8080- * If you register a generator for an existing language this will be overwritten.
8181- */
8282- registerGenerator(name: string, generator: Blockly.Generator): void {
8383- this._generators.set(name, generator);
8484- }
8585-```
5454+const plugin: JupyterFrontEndPlugin<void> = {
5555+ id: 'my-extension:plugin',
5656+ autoStart: true,
5757+ requires: [IBlocklyRegistry],
5858+ activate: (app, registry: IBlocklyRegistry) => {
86598787-8888-## Example - JupyterLab-Niryo-One
8989-The [JupyterLab-Niryo-One](https://github.com/QuantStack/jupyterlab-niryo-one/) extension was built on top of JupyterLab-Blockly and poses as the perfect example. The [Github repository](https://github.com/QuantStack/jupyterlab-niryo-one/) gives access to its entire codebase.
6060+ // Register block shape(s)
6161+ registry.registerBlocks([{
6262+ type: 'my_custom_block',
6363+ message0: 'do something with %1',
6464+ args0: [{ type: 'field_input', name: 'VALUE', text: 'x' }],
6565+ previousStatement: null,
6666+ nextStatement: null,
6767+ colour: '#5BA65B',
6868+ tooltip: 'My custom block.'
6969+ }]);
90709191-The following code snippet showcases how to register a new toolbox, `BlocklyNiryo.Toolbox`, as `niryo`.
9292-```typescript
9393-// src/index.ts : 10-23
7171+ // Register the Python generator
7272+ pythonGenerator.forBlock['my_custom_block'] = block => {
7373+ const val = block.getFieldValue('VALUE');
7474+ return `_df = my_transform(_df, '${val}')\n`;
7575+ };
94769595-/**
9696- * Initialization data for the jupyterlab-niryo-one extension.
9797- */
9898-const plugin: JupyterFrontEndPlugin<void> = {
9999- id: 'jupyterlab-niryo-one:plugin',
100100- autoStart: true,
101101- requires: [IBlocklyRegistry],
102102- activate: (app: JupyterFrontEnd, blockly: IBlocklyRegistry) => {
103103- console.log('JupyterLab extension jupyterlab-niryo-one is activated!');
7777+ // Push the enriched generator back into the registry
7878+ registry.registerGenerator('python', pythonGenerator);
10479105105- //Registering the new toolbox containing all Niryo One blocks.
106106- blockly.registerToolbox('niryo', BlocklyNiryo.Toolbox);
8080+ // Register a toolbox containing the new block
8181+ registry.registerToolbox('My Toolbox', {
8282+ kind: 'categoryToolbox',
8383+ contents: [{
8484+ kind: 'category',
8585+ name: 'Custom',
8686+ colour: '#5BA65B',
8787+ contents: [{ kind: 'block', type: 'my_custom_block' }]
8888+ }]
8989+ });
10790 }
10891};
10992```
11093111111-**NOTE** : `BlocklyNiryo` is defined in `niryo-one-python-generators.ts`.
9494+> **Important:** Always call `registry.registerGenerator('python', pythonGenerator)`
9595+> after attaching `forBlock` handlers. This guards against webpack Module Federation
9696+> resolving `blockly/python` to a different singleton across bundles.
112979898+---
11399114114-## Additional configurations
100100+## `IBlocklyRegistry` API
101101+102102+| Method | Description |
103103+|---|---|
104104+| `registerBlocks(blocks)` | Register block shape definitions (same as `Blockly.defineBlocksWithJsonArray`) |
105105+| `registerToolbox(name, toolbox)` | Add a named toolbox that appears in the editor toolbar dropdown |
106106+| `registerGenerator(language, generator)` | Replace or add a code generator for a kernel language |
107107+| `toolboxes` | Read-only `Map<string, ToolboxDefinition>` of all registered toolboxes |
108108+| `generators` | Read-only `Map<string, Blockly.Generator>` of all registered generators |
115109116116-You will need to request the `jupyterlab-blockly` package as a dependency for your extension, in order to ensure it is installed and available to provide the token `IBlocklyRegistry`. To do this, you need to add the following line to your `pyproject.toml` file.
110110+---
117111118118-```
119119-// pyproject.toml : 26
112112+## Add to `pyproject.toml`
120113114114+Declare `jupyter-tidyblocks` as a Python dependency so pip installs it alongside
115115+your extension:
116116+117117+```toml
118118+[project]
121119dependencies = [
122122- "jupyterlab-blockly>=0.3.2,<0.4",
123123- ... // add any additional dependencies needed for your extension
120120+ "jupyter-tidyblocks>=0.1.0",
121121+ # ... other dependencies
124122]
125123```
126126-127127-Additionally, you will need to add the webpack configuration for loading the `Blockly` source maps. You can do this, by creating the following `webpack.config.js` file inside your root directory:
128128-129129-```js
130130-// @ts-check
131131-132132-module.exports = /** @type { import('webpack').Configuration } */ ({
133133- devtool: 'source-map',
134134- module: {
135135- rules: [
136136- // Load Blockly source maps.
137137- {
138138- test: /(blockly\/.*\.js)$/,
139139- use: [require.resolve('source-map-loader')],
140140- enforce: 'pre'
141141- }
142142- ].filter(Boolean)
143143- },
144144- // https://github.com/google/blockly-samples/blob/9974e85becaa8ad17e35b588b95391c85865dafd/plugins/dev-scripts/config/webpack.config.js#L118-L120
145145- ignoreWarnings: [/Failed to parse source map/]
146146-});
147147-```
148148-149149-and by connecting the `webpack` config to your `jupyterlab` instance, which entails adding the following line inside your `package.json`:
150150-151151-```json
152152-"jupyterlab": {
153153- ...
154154- "webpackConfig": "./webpack.config.js"
155155- }
156156-```
+30-4
docs/toolbox.md
···11# Toolbox
2233-The toolbox, a main element of the Blockly editor, is situated on the left side of the screen. It encompasses all available blocks, organized in categories for easier access.
33+The toolbox is the panel on the left side of the Blockly editor. It contains all
44+available blocks organized into categories.
4556<p align="center">
67 <img src="_static/toolboxView.gif" alt="Toolbox View"/>
78</p>
8999-## Switching to another toolbox
1010+## Tidy Data toolbox
10111111-If you have installed or created another extension, on top of the JupyterLab-Blockly extension, which includes a new tooolbox, you can switch to it by simply pressing the drop down menu on the upper-right corner.
1212+When a `.jpblockly` file is opened with the **Tidy Data** toolbox selected (the
1313+default), the sidebar shows seven categories:
1414+1515+| Category | Colour | Purpose |
1616+|---|---|---|
1717+| **Data** | Gold `#FEBE4C` | Source blocks that load a dataset into `_df` |
1818+| **Transform** | Blue `#76AADB` | Row/column operations on `_df` |
1919+| **Combine** | Grey `#808080` | Multi-table joins and stacks |
2020+| **Plot** | Green `#A4C588` | Visualizations that render `_df` |
2121+| **Stats** | Purple `#BA93DB` | Statistical tests and summaries |
2222+| **Values** | Red `#E7553C` | Literal values and column references |
2323+| **Operations** | Pink `#F9B5B2` | Expressions (arithmetic, logic, string, …) |
2424+2525+For a full list of every block in each category, see
2626+[Block Reference](blocks-reference.md).
2727+2828+## Switching toolboxes
2929+3030+If another extension has registered an additional toolbox, you can switch to it
3131+using the **Toolbox** dropdown in the editor toolbar (upper-right area of the
3232+editor panel).
12331334
14351515-**NOTE** : The toolbox `niryo` from the image above is part of the JupyterLab-Niryo-One extension, which is built on top of the JupyterLab-Blockly extesnion and is meant to offer blocks which can control the Niryo One robot. You can read more about it on its [Github repository](https://github.com/QuantStack/jupyterlab-niryo-one).
3636+## Adding your own toolbox
3737+3838+You can register a custom toolbox from a JupyterLab plugin using
3939+`IBlocklyRegistry.registerToolbox(name, definition)`. Once registered it appears
4040+automatically in the dropdown. See [Extending jupyter-tidyblocks](other_extensions.md)
4141+and [Adding Custom Blocks](custom-blocks.md) for a full guide.