/ concept-collection / jupyterlite-numbl-kernel
Sign in
concept-collection / jupyterlite-numbl-kernel
jupyterlite-numbl-kernel / src / mime.tsx
97 lines · 2.8 KBCodeBlameHistory
41c072cjupyterlite-numbl-kernel: MATLAB-syntax kernel for JupyterLiteJeremy Magland 1import type { IRenderMime } from '@jupyterlab/rendermime-interfaces';
3import { Widget } from '@lumino/widgets';
5import { createRoot } from 'react-dom/client';
6import type { Root } from 'react-dom/client';
8import {
9 FigureView,
10 figuresReducer,
11 initialFiguresState,
12 restoreNaNs
13} from 'numbl/graphics';
14import type { FigureState, PlotInstruction } from 'numbl/graphics';
16/** Mime type carrying a cell's plot instructions (emitted by the kernel). */
17export const FIGURE_MIME = 'application/vnd.numbl.figure+json';
19/**
20 * Render one cell's plot instructions: replay them through numbl's figures
21 * reducer (from an empty state — figures are per-cell, like inline
22 * matplotlib) and mount numbl's React figure renderer for each resulting
23 * figure. Outputs are plain JSON, so saved notebooks re-render on reload
24 * wherever this extension is installed.
25 */
26class NumblFigureRenderer extends Widget implements IRenderMime.IRenderer {
27 constructor() {
28 super();
29 this.addClass('numbl-figure-output');
30 }
32 async renderModel(model: IRenderMime.IMimeModel): Promise<void> {
33 const payload = model.data[FIGURE_MIME] as
34 { plotInstructions?: PlotInstruction[] } | undefined;
35 // Clone before mutating: mime model data is shared, and restoreNaNs
36 // rewrites in place the nulls that JSON made of NaNs.
37 const instructions: PlotInstruction[] = JSON.parse(
38 JSON.stringify(payload?.plotInstructions ?? [])
39 );
41 let state = initialFiguresState;
42 for (const instruction of instructions) {
43 restoreNaNs(instruction);
44 state = figuresReducer(state, instruction);
45 }
46 const figures: FigureState[] = Object.keys(state.figs)
47 .map(Number)
48 .sort((a, b) => a - b)
49 .map(handle => state.figs[handle]);
51 this._root ??= createRoot(this.node);
52 this._root.render(
53 <>
54 {figures.map((figure, i) => (
55 <div className="numbl-figure" key={i}>
56 <FigureView figure={figure} />
57 </div>
58 ))}
59 </>
60 );
61 }
63 dispose(): void {
64 if (this.isDisposed) {
65 return;
66 }
67 // Unmount asynchronously: dispose can be called from within a React
68 // lifecycle, where a synchronous unmount is not allowed.
69 const root = this._root;
70 this._root = null;
71 if (root) {
72 setTimeout(() => root.unmount(), 0);
73 }
74 super.dispose();
75 }
77 private _root: Root | null = null;
80/**
81 * The numbl figure mime renderer factory. `safe: false` because uihtml
82 * figures embed author-provided HTML (rendered only in trusted notebooks).
83 */
84export const rendererFactory: IRenderMime.IRendererFactory = {
85 safe: false,
86 mimeTypes: [FIGURE_MIME],
87 createRenderer: () => new NumblFigureRenderer()
88};
90const extension: IRenderMime.IExtension = {
91 id: 'jupyterlite-numbl-kernel:figure-renderer',
92 rendererFactory,
93 rank: 0,
94 dataType: 'json'
95};
97export default extension;
moveopenescclose