Skip to content

Commit 4e1645d

Browse files
authored
fix: Add support for Plotly charts. (#155)
1 parent e446c5b commit 4e1645d

File tree

4 files changed

+133
-1
lines changed

4 files changed

+133
-1
lines changed

cspell.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,16 @@
4343
"JVSC",
4444
"millis",
4545
"nbformat",
46+
"nbinsx",
4647
"numpy",
4748
"pgsql",
4849
"pids",
4950
"Pids",
51+
"plotly",
5052
"PYTHONHOME",
5153
"Reselecting",
5254
"taskkill",
55+
"toolsai",
5356
"unconfigured",
5457
"Unconfigured",
5558
"unittests",
@@ -58,7 +61,11 @@
5861
"venv's",
5962
"Venv",
6063
"venvs",
61-
"vscode"
64+
"vscode",
65+
"xanchor",
66+
"xaxis",
67+
"yanchor",
68+
"yaxis"
6269
],
6370
"useGitignore": true
6471
}

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525
"engines": {
2626
"vscode": "^1.95.0"
2727
},
28+
"extensionDependencies": [
29+
"ms-toolsai.jupyter-renderers"
30+
],
31+
"extensionPack": [
32+
"ms-toolsai.jupyter-renderers"
33+
],
2834
"l10n": "./l10n",
2935
"extensionKind": [
3036
"workspace"

src/notebooks/deepnote/deepnoteDataConverter.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ export class DeepnoteDataConverter {
252252
);
253253
} else if (item.mime === 'application/vnd.vega.v5+json') {
254254
data['application/vnd.vega.v5+json'] = JSON.parse(new TextDecoder().decode(item.data));
255+
} else if (item.mime === 'application/vnd.plotly.v1+json') {
256+
data['application/vnd.plotly.v1+json'] = JSON.parse(new TextDecoder().decode(item.data));
255257
} else if (item.mime === 'application/vnd.deepnote.sql-output-metadata+json') {
256258
data['application/vnd.deepnote.sql-output-metadata+json'] = JSON.parse(
257259
new TextDecoder().decode(item.data)
@@ -342,6 +344,15 @@ export class DeepnoteDataConverter {
342344
);
343345
}
344346

347+
if (data['application/vnd.plotly.v1+json']) {
348+
items.push(
349+
NotebookCellOutputItem.json(
350+
data['application/vnd.plotly.v1+json'],
351+
'application/vnd.plotly.v1+json'
352+
)
353+
);
354+
}
355+
345356
if (data['application/vnd.deepnote.sql-output-metadata+json']) {
346357
items.push(
347358
NotebookCellOutputItem.json(

src/notebooks/deepnote/deepnoteDataConverter.unit.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,54 @@ suite('DeepnoteDataConverter', () => {
513513
assert.strictEqual(new TextDecoder().decode(markdownItem!.data), markdownContent);
514514
assert.strictEqual(new TextDecoder().decode(plainItem!.data), 'Result\n\nThis is formatted output.');
515515
});
516+
517+
test('converts Plotly chart output', () => {
518+
const plotlyData = {
519+
data: [
520+
{
521+
type: 'bar',
522+
x: ['A', 'B', 'C'],
523+
y: [10, 20, 15]
524+
}
525+
],
526+
layout: {
527+
title: 'Sample Chart',
528+
xaxis: { title: 'Category' },
529+
yaxis: { title: 'Value' }
530+
}
531+
};
532+
533+
const deepnoteOutputs: DeepnoteOutput[] = [
534+
{
535+
output_type: 'execute_result',
536+
execution_count: 1,
537+
data: {
538+
'application/vnd.plotly.v1+json': plotlyData
539+
}
540+
}
541+
];
542+
543+
const blocks: DeepnoteBlock[] = [
544+
{
545+
blockGroup: 'test-group',
546+
id: 'block1',
547+
type: 'code',
548+
content: 'fig.show()',
549+
sortingKey: 'a0',
550+
outputs: deepnoteOutputs
551+
}
552+
];
553+
554+
const cells = converter.convertBlocksToCells(blocks);
555+
const outputs = cells[0].outputs!;
556+
557+
assert.strictEqual(outputs.length, 1);
558+
assert.strictEqual(outputs[0].items.length, 1);
559+
assert.strictEqual(outputs[0].items[0].mime, 'application/vnd.plotly.v1+json');
560+
561+
const outputData = JSON.parse(new TextDecoder().decode(outputs[0].items[0].data));
562+
assert.deepStrictEqual(outputData, plotlyData);
563+
});
516564
});
517565

518566
suite('round trip conversion', () => {
@@ -597,6 +645,66 @@ suite('DeepnoteDataConverter', () => {
597645
assert.deepStrictEqual(output.data?.['application/vnd.deepnote.sql-output-metadata+json'], sqlMetadata);
598646
});
599647

648+
test('Plotly chart output round-trips correctly', () => {
649+
const plotlyData = {
650+
data: [
651+
{
652+
type: 'histogram',
653+
x: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
654+
nbinsx: 30,
655+
opacity: 0.75
656+
}
657+
],
658+
layout: {
659+
title: 'Sessions per week by churn status',
660+
xaxis: { title: 'Sessions per week' },
661+
yaxis: { title: 'Users' },
662+
legend: {
663+
yanchor: 'top',
664+
y: 1,
665+
xanchor: 'left',
666+
x: 1.02
667+
}
668+
}
669+
};
670+
671+
const originalBlocks: DeepnoteBlock[] = [
672+
{
673+
blockGroup: 'test-group',
674+
id: 'plotly-block',
675+
type: 'code',
676+
content: 'fig = px.histogram(df)\nfig.show()',
677+
sortingKey: 'a0',
678+
executionCount: 1,
679+
metadata: {},
680+
outputs: [
681+
{
682+
output_type: 'execute_result',
683+
execution_count: 1,
684+
data: {
685+
'application/vnd.plotly.v1+json': plotlyData
686+
}
687+
}
688+
]
689+
}
690+
];
691+
692+
const cells = converter.convertBlocksToCells(originalBlocks);
693+
const roundTripBlocks = converter.convertCellsToBlocks(cells);
694+
695+
// The round-trip should preserve the Plotly chart output
696+
assert.strictEqual(roundTripBlocks.length, 1);
697+
assert.strictEqual(roundTripBlocks[0].id, 'plotly-block');
698+
assert.strictEqual(roundTripBlocks[0].outputs?.length, 1);
699+
700+
const output = roundTripBlocks[0].outputs![0] as {
701+
output_type: string;
702+
data?: Record<string, unknown>;
703+
};
704+
assert.strictEqual(output.output_type, 'execute_result');
705+
assert.deepStrictEqual(output.data?.['application/vnd.plotly.v1+json'], plotlyData);
706+
});
707+
600708
test('real deepnote notebook round-trips without losing data', () => {
601709
// Inline test data representing a real Deepnote notebook with various block types
602710
// blockGroup is an optional field not in the DeepnoteBlock interface, so we cast as any

0 commit comments

Comments
 (0)