diff --git a/cspell.json b/cspell.json index 3f8516fbb..7a1715964 100644 --- a/cspell.json +++ b/cspell.json @@ -43,13 +43,16 @@ "JVSC", "millis", "nbformat", + "nbinsx", "numpy", "pgsql", "pids", "Pids", + "plotly", "PYTHONHOME", "Reselecting", "taskkill", + "toolsai", "unconfigured", "Unconfigured", "unittests", @@ -58,7 +61,11 @@ "venv's", "Venv", "venvs", - "vscode" + "vscode", + "xanchor", + "xaxis", + "yanchor", + "yaxis" ], "useGitignore": true } diff --git a/package.json b/package.json index 243dd980a..626012dda 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,12 @@ "engines": { "vscode": "^1.95.0" }, + "extensionDependencies": [ + "ms-toolsai.jupyter-renderers" + ], + "extensionPack": [ + "ms-toolsai.jupyter-renderers" + ], "l10n": "./l10n", "extensionKind": [ "workspace" diff --git a/src/notebooks/deepnote/deepnoteDataConverter.ts b/src/notebooks/deepnote/deepnoteDataConverter.ts index 124834625..4d485eabd 100644 --- a/src/notebooks/deepnote/deepnoteDataConverter.ts +++ b/src/notebooks/deepnote/deepnoteDataConverter.ts @@ -252,6 +252,8 @@ export class DeepnoteDataConverter { ); } else if (item.mime === 'application/vnd.vega.v5+json') { data['application/vnd.vega.v5+json'] = JSON.parse(new TextDecoder().decode(item.data)); + } else if (item.mime === 'application/vnd.plotly.v1+json') { + data['application/vnd.plotly.v1+json'] = JSON.parse(new TextDecoder().decode(item.data)); } else if (item.mime === 'application/vnd.deepnote.sql-output-metadata+json') { data['application/vnd.deepnote.sql-output-metadata+json'] = JSON.parse( new TextDecoder().decode(item.data) @@ -342,6 +344,15 @@ export class DeepnoteDataConverter { ); } + if (data['application/vnd.plotly.v1+json']) { + items.push( + NotebookCellOutputItem.json( + data['application/vnd.plotly.v1+json'], + 'application/vnd.plotly.v1+json' + ) + ); + } + if (data['application/vnd.deepnote.sql-output-metadata+json']) { items.push( NotebookCellOutputItem.json( diff --git a/src/notebooks/deepnote/deepnoteDataConverter.unit.test.ts b/src/notebooks/deepnote/deepnoteDataConverter.unit.test.ts index 168f153c6..10931ca36 100644 --- a/src/notebooks/deepnote/deepnoteDataConverter.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteDataConverter.unit.test.ts @@ -513,6 +513,54 @@ suite('DeepnoteDataConverter', () => { assert.strictEqual(new TextDecoder().decode(markdownItem!.data), markdownContent); assert.strictEqual(new TextDecoder().decode(plainItem!.data), 'Result\n\nThis is formatted output.'); }); + + test('converts Plotly chart output', () => { + const plotlyData = { + data: [ + { + type: 'bar', + x: ['A', 'B', 'C'], + y: [10, 20, 15] + } + ], + layout: { + title: 'Sample Chart', + xaxis: { title: 'Category' }, + yaxis: { title: 'Value' } + } + }; + + const deepnoteOutputs: DeepnoteOutput[] = [ + { + output_type: 'execute_result', + execution_count: 1, + data: { + 'application/vnd.plotly.v1+json': plotlyData + } + } + ]; + + const blocks: DeepnoteBlock[] = [ + { + blockGroup: 'test-group', + id: 'block1', + type: 'code', + content: 'fig.show()', + sortingKey: 'a0', + outputs: deepnoteOutputs + } + ]; + + const cells = converter.convertBlocksToCells(blocks); + const outputs = cells[0].outputs!; + + assert.strictEqual(outputs.length, 1); + assert.strictEqual(outputs[0].items.length, 1); + assert.strictEqual(outputs[0].items[0].mime, 'application/vnd.plotly.v1+json'); + + const outputData = JSON.parse(new TextDecoder().decode(outputs[0].items[0].data)); + assert.deepStrictEqual(outputData, plotlyData); + }); }); suite('round trip conversion', () => { @@ -597,6 +645,66 @@ suite('DeepnoteDataConverter', () => { assert.deepStrictEqual(output.data?.['application/vnd.deepnote.sql-output-metadata+json'], sqlMetadata); }); + test('Plotly chart output round-trips correctly', () => { + const plotlyData = { + data: [ + { + type: 'histogram', + x: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + nbinsx: 30, + opacity: 0.75 + } + ], + layout: { + title: 'Sessions per week by churn status', + xaxis: { title: 'Sessions per week' }, + yaxis: { title: 'Users' }, + legend: { + yanchor: 'top', + y: 1, + xanchor: 'left', + x: 1.02 + } + } + }; + + const originalBlocks: DeepnoteBlock[] = [ + { + blockGroup: 'test-group', + id: 'plotly-block', + type: 'code', + content: 'fig = px.histogram(df)\nfig.show()', + sortingKey: 'a0', + executionCount: 1, + metadata: {}, + outputs: [ + { + output_type: 'execute_result', + execution_count: 1, + data: { + 'application/vnd.plotly.v1+json': plotlyData + } + } + ] + } + ]; + + const cells = converter.convertBlocksToCells(originalBlocks); + const roundTripBlocks = converter.convertCellsToBlocks(cells); + + // The round-trip should preserve the Plotly chart output + assert.strictEqual(roundTripBlocks.length, 1); + assert.strictEqual(roundTripBlocks[0].id, 'plotly-block'); + assert.strictEqual(roundTripBlocks[0].outputs?.length, 1); + + const output = roundTripBlocks[0].outputs![0] as { + output_type: string; + data?: Record; + }; + assert.strictEqual(output.output_type, 'execute_result'); + assert.deepStrictEqual(output.data?.['application/vnd.plotly.v1+json'], plotlyData); + }); + test('real deepnote notebook round-trips without losing data', () => { // Inline test data representing a real Deepnote notebook with various block types // blockGroup is an optional field not in the DeepnoteBlock interface, so we cast as any