Add Plotly output support to notebooks

With this change, Plotly types will be successfully rendered in a Notebook.
Currently they have a default width of 700px with a scrollbar if the window size is smaller (this matches other notebook viewers).
The Plotly library is dynamically required to avoid startup time perf hits. This is something we could look at for other components too.
This commit is contained in:
Kevin Cunnane
2019-07-02 18:08:38 -07:00
committed by GitHub
parent 8c4f6f9e5f
commit cc6dea0631
8 changed files with 1631 additions and 3 deletions

View File

@@ -39,6 +39,7 @@
"@angular/platform-browser-dynamic": "~4.1.3",
"@angular/router": "~4.1.3",
"@angular/upgrade": "~4.1.3",
"@types/plotly.js": "^1.44.9",
"angular2-grid": "2.0.6",
"angular2-slickgrid": "github:Microsoft/angular2-slickgrid#1.4.6",
"ansi_up": "^3.0.0",
@@ -60,6 +61,7 @@
"native-watchdog": "1.0.0",
"ng2-charts": "^1.6.0",
"node-pty": "0.9.0-beta9",
"plotly.js-dist": "^1.48.3",
"reflect-metadata": "^0.1.8",
"rxjs": "5.4.0",
"sanitize-html": "^1.19.1",

View File

@@ -121,7 +121,8 @@ exports.load = function (modulePaths, resultCallback, options) {
'rxjs/Subject',
'rxjs/Observer',
'htmlparser2',
'sanitize'
'sanitize',
'plotly.js-dist'
]);
// {{SQL CARBON EDIT}} - End

View File

@@ -463,3 +463,8 @@ output-component .jp-RenderedHTMLCommon > *:last-child {
.jp-RenderedPDF {
font-size: var(--jp-ui-font-size1);
}
plotly-output .plotly-wrapper {
display: block;
overflow-y: hidden;
}

View File

@@ -19,6 +19,7 @@ import product from 'vs/platform/product/node/product';
import { registerComponentType } from 'sql/workbench/parts/notebook/outputs/mimeRegistry';
import { MimeRendererComponent as MimeRendererComponent } from 'sql/workbench/parts/notebook/outputs/mimeRenderer.component';
import { MarkdownOutputComponent } from 'sql/workbench/parts/notebook/outputs/markdownOutput.component';
import { PlotlyOutputComponent } from 'sql/workbench/parts/notebook/outputs/plotlyOutput.component';
// Model View editor registration
const viewModelEditorDescriptor = new EditorDescriptor(
@@ -138,7 +139,6 @@ registerComponentType({
/**
* A mime renderer component for LaTeX.
* This will be replaced by a dedicated component in the future
*/
registerComponentType({
mimeTypes: ['text/latex'],
@@ -150,7 +150,6 @@ registerComponentType({
/**
* A mime renderer component for Markdown.
* This will be replaced by a dedicated component in the future
*/
registerComponentType({
mimeTypes: ['text/markdown'],
@@ -159,3 +158,26 @@ registerComponentType({
ctor: MarkdownOutputComponent,
selector: MarkdownOutputComponent.SELECTOR
});
/**
* A mime renderer component for Plotly graphs.
*/
registerComponentType({
mimeTypes: ['application/vnd.plotly.v1+json'],
rank: 45,
safe: true,
ctor: PlotlyOutputComponent,
selector: PlotlyOutputComponent.SELECTOR
});
/**
* A mime renderer component for Plotly HTML output
* that will ensure this gets ignored if possible since it's only output
* on offline init and adds a <script> tag which does what we've done (add Plotly support into the app)
*/
registerComponentType({
mimeTypes: ['text/vnd.plotly.v1+html'],
rank: 46,
safe: true,
ctor: PlotlyOutputComponent,
selector: PlotlyOutputComponent.SELECTOR
});

View File

@@ -0,0 +1,157 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OnInit, Component, Input, Inject, ElementRef, ViewChild } from '@angular/core';
import { AngularDisposable } from 'sql/base/node/lifecycle';
import { IMimeComponent } from 'sql/workbench/parts/notebook/outputs/mimeRegistry';
import { MimeModel } from 'sql/workbench/parts/notebook/outputs/common/mimemodel';
import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { getErrorMessage } from 'sql/workbench/parts/notebook/notebookUtils';
import { localize } from 'vs/nls';
import * as types from 'vs/base/common/types';
type ObjectType = object;
interface FigureLayout extends ObjectType {
width?: string | number;
height?: string;
autosize?: boolean;
}
interface Figure extends ObjectType {
data: object[];
layout: Partial<FigureLayout>;
}
declare class PlotlyHTMLElement extends HTMLDivElement {
data: object;
layout: object;
newPlot: () => void;
redraw: () => void;
}
@Component({
selector: PlotlyOutputComponent.SELECTOR,
template: `<div #output class="plotly-wrapper"></div>
<pre *ngIf="hasError" class="p-Widget jp-RenderedText">{{errorText}}</pre>
`
})
export class PlotlyOutputComponent extends AngularDisposable implements IMimeComponent, OnInit {
public static readonly SELECTOR: string = 'plotly-output';
Plotly!: {
newPlot: (
div: PlotlyHTMLElement | null | undefined,
data: object,
layout: FigureLayout
) => void;
redraw: (div?: PlotlyHTMLElement) => void;
};
@ViewChild('output', { read: ElementRef }) private output: ElementRef;
private _initialized: boolean = false;
private _rendered: boolean = false;
private _cellModel: ICellModel;
private _bundleOptions: MimeModel.IOptions;
private _plotDiv: PlotlyHTMLElement;
public errorText: string;
constructor(
@Inject(IThemeService) private readonly themeService: IThemeService
) {
super();
}
@Input() set bundleOptions(value: MimeModel.IOptions) {
this._bundleOptions = value;
if (this._initialized) {
this.renderPlotly();
}
}
@Input() mimeType: string;
get cellModel(): ICellModel {
return this._cellModel;
}
@Input() set cellModel(value: ICellModel) {
this._cellModel = value;
if (this._initialized) {
this.renderPlotly();
}
}
ngOnInit() {
this.Plotly = require.__$__nodeRequire('plotly.js-dist');
this._plotDiv = this.output.nativeElement;
this.renderPlotly();
this._initialized = true;
}
renderPlotly(): void {
if (this._rendered) {
// just re-layout
this.layout();
return;
}
if (!this._bundleOptions || !this._cellModel || !this.mimeType) {
return;
}
if (this.mimeType === 'text/vnd.plotly.v1+html') {
// Do nothing - this is our way to ignore the offline init Plotly attempts to do via a <script> tag.
// We have "handled" it by pulling in the plotly library into this component instead
return;
}
this.errorText = undefined;
const figure = this.getFigure(true);
if (figure) {
figure.layout = figure.layout || {};
if (!figure.layout.width && !figure.layout.autosize) {
// Workaround: to avoid filling up the entire cell, use plotly's default
figure.layout.width = Math.min(700, this._plotDiv.clientWidth);
}
try {
this.Plotly.newPlot(this._plotDiv, figure.data, figure.layout);
} catch (error) {
this.displayError(error);
}
}
this._rendered = true;
}
getFigure(showError: boolean): Figure {
const figure = <Figure><any>this._bundleOptions.data[this.mimeType];
if (typeof figure === 'string') {
try {
JSON.parse(figure);
} catch (error) {
if (showError) {
this.displayError(error);
}
}
}
const { data = [], layout = {} } = figure;
return { data, layout };
}
private displayError(error: Error | string): void {
this.errorText = localize('plotlyError', 'Error displaying Plotly graph: {0}', getErrorMessage(error));
}
layout(): void {
// No need to re-layout for now as Plotly is doing its own resize handling.
}
public hasError(): boolean {
return !types.isUndefinedOrNull(this.errorText);
}
}

View File

@@ -19,3 +19,4 @@
/// <reference path="modules/html-query-plan/index.d.ts" />
/// <reference path="modules/ng2-charts/index.d.ts" />
/// <reference path="modules/rxjs/index.d.ts" />
/// <reference path="modules/@types/plotly.js-dist/index.d.ts" />

File diff suppressed because it is too large Load Diff

View File

@@ -85,6 +85,11 @@
dependencies:
commander "*"
"@types/d3@^3":
version "3.5.42"
resolved "https://registry.yarnpkg.com/@types/d3/-/d3-3.5.42.tgz#6a782b44bb7f5c48165cb166886b5d53cb84455f"
integrity sha512-jKnkXluwSAzkvR19zjCHvLYgsWuDqpeE79NrhWrqhKqrx3sgTRqqt4SKaxSy+N7mt1J04Xy4L0/cKdfIgnjzVQ==
"@types/fancy-log@1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@types/fancy-log/-/fancy-log-1.3.0.tgz#a61ab476e5e628cd07a846330df53b85e05c8ce0"
@@ -117,6 +122,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.12.tgz#e15a9d034d9210f00320ef718a50c4a799417c47"
integrity sha512-Pr+6JRiKkfsFvmU/LK68oBRCQeEg36TyAbPhc2xpez24OOZZCuoIhWGTd39VZy6nGafSbxzGouFPTFD/rR1A0A==
"@types/plotly.js@^1.44.9":
version "1.44.9"
resolved "https://registry.yarnpkg.com/@types/plotly.js/-/plotly.js-1.44.9.tgz#d20bd229b409f83b5e9bc06df0c948a27b8fbc0d"
integrity sha512-YZlxjspeO7FEmlR56m2RQpohSWeQ4MRysT1Ghln/DFLS29Fy2YGMeASnhlYRiFxYcz12Jh0pXM0YYWbFNS4YYA==
dependencies:
"@types/d3" "^3"
"@types/sanitize-html@^1.18.2":
version "1.18.2"
resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-1.18.2.tgz#14e9971064d0f29aa4feaa8421122ced9e8346d9"
@@ -6763,6 +6775,11 @@ plist@^3.0.1:
xmlbuilder "^9.0.7"
xmldom "0.1.x"
plotly.js-dist@^1.48.3:
version "1.48.3"
resolved "https://registry.yarnpkg.com/plotly.js-dist/-/plotly.js-dist-1.48.3.tgz#b160b2d080ad87720121c9119f28036f35a301dc"
integrity sha512-Ocy2WWjzh4Ofk293AgTvP0ga053nTV7TvUbYEzmq1E9Eh7wc6HCPBxo55YcZThHw3hVycEnhktLQK94en1bYSw==
plugin-error@0.1.2, plugin-error@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace"