diff --git a/buildutils/src/ensure-repo.ts b/buildutils/src/ensure-repo.ts index f62bbe83991d..af98c76ac84a 100644 --- a/buildutils/src/ensure-repo.ts +++ b/buildutils/src/ensure-repo.ts @@ -29,6 +29,7 @@ let UNUSED: Dict = { '@jupyterlab/services': ['node-fetch', 'ws'], '@jupyterlab/testutils': ['node-fetch', 'identity-obj-proxy'], '@jupyterlab/test-csvviewer': ['csv-spectrum'], + '@jupyterlab/vega4-extension': ['vega', 'vega-lite'], '@jupyterlab/vega5-extension': ['vega', 'vega-lite'], '@jupyterlab/ui-components': ['@blueprintjs/icons'] }; diff --git a/dev_mode/imports.css b/dev_mode/imports.css index 62622d0b0fe1..e6b68165ab57 100644 --- a/dev_mode/imports.css +++ b/dev_mode/imports.css @@ -33,4 +33,5 @@ @import url('~@jupyterlab/terminal-extension/style/index.css'); @import url('~@jupyterlab/tooltip-extension/style/index.css'); @import url('~@jupyterlab/vdom-extension/style/index.css'); +@import url('~@jupyterlab/vega4-extension/style/index.css'); @import url('~@jupyterlab/vega5-extension/style/index.css'); diff --git a/dev_mode/package.json b/dev_mode/package.json index 9ef8b875a2eb..224700373b67 100644 --- a/dev_mode/package.json +++ b/dev_mode/package.json @@ -64,6 +64,7 @@ "@jupyterlab/tooltip": "^1.0.0-alpha.11", "@jupyterlab/tooltip-extension": "^1.0.0-alpha.11", "@jupyterlab/vdom-extension": "^1.0.0-alpha.11", + "@jupyterlab/vega4-extension": "^1.0.0-alpha.11", "@jupyterlab/vega5-extension": "^1.0.0-alpha.11", "@phosphor/algorithm": "^1.1.2", "@phosphor/application": "^1.6.0", @@ -155,6 +156,7 @@ "@jupyterlab/javascript-extension": "", "@jupyterlab/json-extension": "", "@jupyterlab/pdf-extension": "", + "@jupyterlab/vega4-extension": "", "@jupyterlab/vega5-extension": "" }, "name": "JupyterLab", @@ -249,6 +251,7 @@ "@jupyterlab/theme-light-extension": "../packages/theme-light-extension", "@jupyterlab/tooltip-extension": "../packages/tooltip-extension", "@jupyterlab/vdom-extension": "../packages/vdom-extension", + "@jupyterlab/vega4-extension": "../packages/vega4-extension", "@jupyterlab/vega5-extension": "../packages/vega5-extension" } } diff --git a/packages/metapackage/src/index.ts b/packages/metapackage/src/index.ts index 46a1ba060d3f..c0ab56a9501e 100644 --- a/packages/metapackage/src/index.ts +++ b/packages/metapackage/src/index.ts @@ -66,4 +66,5 @@ import '@jupyterlab/tooltip'; import '@jupyterlab/tooltip-extension'; import '@jupyterlab/ui-components'; import '@jupyterlab/vdom-extension'; +import '@jupyterlab/vega4-extension'; import '@jupyterlab/vega5-extension'; diff --git a/packages/metapackage/tsconfig.json b/packages/metapackage/tsconfig.json index 99edf66ab220..4059745efe02 100644 --- a/packages/metapackage/tsconfig.json +++ b/packages/metapackage/tsconfig.json @@ -222,6 +222,9 @@ { "path": "../vdom-extension" }, + { + "path": "../vega4-extension" + }, { "path": "../vega5-extension" } diff --git a/packages/vega4-extension/README.md b/packages/vega4-extension/README.md new file mode 100644 index 000000000000..459eb889c5b2 --- /dev/null +++ b/packages/vega4-extension/README.md @@ -0,0 +1,107 @@ +# vega4-extension + +A JupyterLab extension for rendering [Vega](https://vega.github.io/vega) 4 and [Vega-Lite](https://vega.github.io/vega-lite) 2. + +![demo](http://g.recordit.co/USoTkuCOfR.gif) + +## Prerequisites + +- JupyterLab ^0.27.0 + +## Usage + +To render Vega-Lite output in IPython: + +```python +from IPython.display import display + +display({ + "application/vnd.vegalite.v2+json": { + "$schema": "https://vega.github.io/schema/vega-lite/v2.json", + "description": "A simple bar chart with embedded data.", + "data": { + "values": [ + {"a": "A", "b": 28}, {"a": "B", "b": 55}, {"a": "C", "b": 43}, + {"a": "D", "b": 91}, {"a": "E", "b": 81}, {"a": "F", "b": 53}, + {"a": "G", "b": 19}, {"a": "H", "b": 87}, {"a": "I", "b": 52} + ] + }, + "mark": "bar", + "encoding": { + "x": {"field": "a", "type": "ordinal"}, + "y": {"field": "b", "type": "quantitative"} + } + } +}, raw=True) +``` + +Using the [Altair library](https://github.com/altair-viz/altair): + +```python +import altair as alt + +cars = alt.load_dataset('cars') + +chart = alt.Chart(cars).mark_point().encode( + x='Horsepower', + y='Miles_per_Gallon', + color='Origin', +) + +chart +``` + +Provide Vega-Embed options via metadata: + +```python +from IPython.display import display + +display({ + "application/vnd.vegalite.v2+json": { + "$schema": "https://vega.github.io/schema/vega-lite/v2.json", + "description": "A simple bar chart with embedded data.", + "data": { + "values": [ + {"a": "A", "b": 28}, {"a": "B", "b": 55}, {"a": "C", "b": 43}, + {"a": "D", "b": 91}, {"a": "E", "b": 81}, {"a": "F", "b": 53}, + {"a": "G", "b": 19}, {"a": "H", "b": 87}, {"a": "I", "b": 52} + ] + }, + "mark": "bar", + "encoding": { + "x": {"field": "a", "type": "ordinal"}, + "y": {"field": "b", "type": "quantitative"} + } + } +}, metadata={ + "application/vnd.vegalite.v2+json": { + "embed_options": { + "actions": False + } + } +}, raw=True) +``` + +Provide Vega-Embed options via Altair: + +```python +import altair as alt + +alt.renderers.enable('default', embed_options={'actions': False}) + +cars = alt.load_dataset('cars') + +chart = alt.Chart(cars).mark_point().encode( + x='Horsepower', + y='Miles_per_Gallon', + color='Origin', +) + +chart +``` + +To render a `.vl`, `.vg`, `vl.json` or `.vg.json` file, simply open it: + +## Development + +See the [JupyterLab Contributor Documentation](https://github.com/jupyterlab/jupyterlab/blob/master/CONTRIBUTING.md). diff --git a/packages/vega4-extension/package.json b/packages/vega4-extension/package.json new file mode 100644 index 000000000000..fa7442f68fd2 --- /dev/null +++ b/packages/vega4-extension/package.json @@ -0,0 +1,56 @@ +{ + "name": "@jupyterlab/vega4-extension", + "version": "1.0.0-alpha.11", + "description": "JupyterLab - Vega 4 and Vega-Lite 2 Mime Renderer Extension", + "homepage": "https://github.com/jupyterlab/jupyterlab", + "bugs": { + "url": "https://github.com/jupyterlab/jupyterlab/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/jupyterlab/jupyterlab.git" + }, + "license": "BSD-3-Clause", + "author": "Project Jupyter", + "files": [ + "lib/*.d.ts", + "lib/*.js", + "style/*.*" + ], + "sideEffects": [ + "style/**/*" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "style": "style/index.css", + "directories": { + "lib": "lib/" + }, + "scripts": { + "build": "tsc -b", + "clean": "rimraf lib", + "docs": "typedoc --options tdoptions.json --theme ../../typedoc-theme src", + "prepublishOnly": "npm run build", + "watch": "tsc -b --watch" + }, + "dependencies": { + "@jupyterlab/rendermime-interfaces": "^1.3.0-alpha.11", + "@phosphor/coreutils": "^1.3.0", + "@phosphor/widgets": "^1.7.1", + "vega": "^4.4.0", + "vega-embed": "^4.2.0", + "vega-lite": "^2.6.0" + }, + "devDependencies": { + "@types/webpack-env": "^1.13.9", + "rimraf": "~2.6.2", + "typedoc": "^0.14.2", + "typescript": "~3.5.1" + }, + "publishConfig": { + "access": "public" + }, + "jupyterlab": { + "mimeExtension": true + } +} diff --git a/packages/vega4-extension/src/index.ts b/packages/vega4-extension/src/index.ts new file mode 100644 index 000000000000..f53a528f252e --- /dev/null +++ b/packages/vega4-extension/src/index.ts @@ -0,0 +1,193 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +import { JSONObject } from '@phosphor/coreutils'; + +import { Widget } from '@phosphor/widgets'; + +import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; + +import * as VegaModuleType from 'vega-embed'; + +/** + * The CSS class to add to the Vega and Vega-Lite widget. + */ +const VEGA_COMMON_CLASS = 'jp-RenderedVegaCommon4'; + +/** + * The CSS class to add to the Vega. + */ +const VEGA_CLASS = 'jp-RenderedVega4'; + +/** + * The CSS class to add to the Vega-Lite. + */ +const VEGALITE_CLASS = 'jp-RenderedVegaLite2'; + +/** + * The MIME type for Vega. + * + * #### Notes + * The version of this follows the major version of Vega. + */ +export const VEGA_MIME_TYPE = 'application/vnd.vega.v4+json'; + +/** + * The MIME type for Vega-Lite. + * + * #### Notes + * The version of this follows the major version of Vega-Lite. + */ +export const VEGALITE_MIME_TYPE = 'application/vnd.vegalite.v2+json'; + +/** + * A widget for rendering Vega or Vega-Lite data, for usage with rendermime. + */ +export class RenderedVega extends Widget implements IRenderMime.IRenderer { + private _result: VegaModuleType.Result; + + /** + * Create a new widget for rendering Vega/Vega-Lite. + */ + constructor(options: IRenderMime.IRendererOptions) { + super(); + this._mimeType = options.mimeType; + this._resolver = options.resolver; + this.addClass(VEGA_COMMON_CLASS); + this.addClass( + this._mimeType === VEGA_MIME_TYPE ? VEGA_CLASS : VEGALITE_CLASS + ); + } + + /** + * Render Vega/Vega-Lite into this widget's node. + */ + async renderModel(model: IRenderMime.IMimeModel): Promise { + const spec = model.data[this._mimeType] as JSONObject; + const metadata = model.metadata[this._mimeType] as { + embed_options?: VegaModuleType.EmbedOptions; + }; + const embedOptions = + metadata && metadata.embed_options ? metadata.embed_options : {}; + const mode: VegaModuleType.Mode = + this._mimeType === VEGA_MIME_TYPE ? 'vega' : 'vega-lite'; + + const vega = + Private.vega != null ? Private.vega : await Private.ensureVega(); + const path = await this._resolver.resolveUrl(''); + const baseURL = await this._resolver.getDownloadUrl(path); + + const el = document.createElement('div'); + + // clear the output before attaching a chart + this.node.textContent = ''; + this.node.appendChild(el); + + this._result = await vega.default(el, spec, { + actions: true, + defaultStyle: true, + ...embedOptions, + mode, + loader: { + baseURL, + http: { credentials: 'same-origin' } + } + }); + + if (model.data['image/png']) { + return; + } + + // Add png representation of vega chart to output + const imageURL = await this._result.view.toImageURL('png'); + model.setData({ + data: { ...model.data, 'image/png': imageURL.split(',')[1] } + }); + } + + dispose(): void { + if (this._result) { + this._result.view.finalize(); + } + super.dispose(); + } + + private _mimeType: string; + private _resolver: IRenderMime.IResolver; +} + +/** + * A mime renderer factory for vega data. + */ +export const rendererFactory: IRenderMime.IRendererFactory = { + safe: true, + mimeTypes: [VEGA_MIME_TYPE, VEGALITE_MIME_TYPE], + createRenderer: options => new RenderedVega(options) +}; + +const extension: IRenderMime.IExtension = { + id: '@jupyterlab/vega4-extension:factory', + rendererFactory, + rank: 58, + dataType: 'json', + documentWidgetFactoryOptions: [ + { + name: 'Vega', + primaryFileType: 'vega4', + fileTypes: ['vega4', 'json'], + defaultFor: ['vega4'] + }, + { + name: 'Vega-Lite', + primaryFileType: 'vega-lite2', + fileTypes: ['vega-lite2', 'json'], + defaultFor: ['vega-lite2'] + } + ], + fileTypes: [ + { + mimeTypes: [VEGA_MIME_TYPE], + name: 'vega4', + extensions: ['.vg', '.vg.json', '.vega'], + iconClass: 'jp-MaterialIcon jp-VegaIcon' + }, + { + mimeTypes: [VEGALITE_MIME_TYPE], + name: 'vega-lite2', + extensions: ['.vl', '.vl.json', '.vegalite'], + iconClass: 'jp-MaterialIcon jp-VegaIcon' + } + ] +}; + +export default extension; + +/** + * A namespace for private module data. + */ +namespace Private { + /** + * A cached reference to the vega library. + */ + export let vega: typeof VegaModuleType; + + /** + * A Promise for the initial load of vega. + */ + export let vegaReady: Promise; + + /** + * Lazy-load and cache the vega-embed library + */ + export function ensureVega(): Promise { + if (vegaReady) { + return vegaReady; + } + + vegaReady = import('vega-embed'); + + return vegaReady; + } +} diff --git a/packages/vega4-extension/src/json.d.ts b/packages/vega4-extension/src/json.d.ts new file mode 100644 index 000000000000..3abbcd236bbf --- /dev/null +++ b/packages/vega4-extension/src/json.d.ts @@ -0,0 +1,3 @@ +declare module '*.json' { + export const version: string; +} diff --git a/packages/vega4-extension/style/base.css b/packages/vega4-extension/style/base.css new file mode 100644 index 000000000000..d6665d0b8cf8 --- /dev/null +++ b/packages/vega4-extension/style/base.css @@ -0,0 +1,17 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +.jp-RenderedVegaCommon4 { + margin-left: 8px; + margin-top: 8px; +} + +.jp-MimeDocument .jp-RenderedVegaCommon4 { + padding: 16px; +} + +.vega canvas { + background: var(--jp-vega-background); +} diff --git a/packages/vega4-extension/style/index.css b/packages/vega4-extension/style/index.css new file mode 100644 index 000000000000..f4505e03ab05 --- /dev/null +++ b/packages/vega4-extension/style/index.css @@ -0,0 +1,9 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +/* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ +@import url('~@phosphor/widgets/style/index.css'); + +@import url('./base.css'); diff --git a/packages/vega4-extension/tdoptions.json b/packages/vega4-extension/tdoptions.json new file mode 100644 index 000000000000..f7e43cbe5ad2 --- /dev/null +++ b/packages/vega4-extension/tdoptions.json @@ -0,0 +1,20 @@ +{ + "excludeNotExported": true, + "mode": "file", + "target": "es5", + "module": "es5", + "lib": [ + "lib.es2015.d.ts", + "lib.es2015.collection.d.ts", + "lib.es2015.promise.d.ts", + "lib.dom.d.ts" + ], + "out": "../../docs/api/vega4-extension", + "baseUrl": ".", + "paths": { + "@jupyterlab/*": ["../packages/*"] + }, + "esModuleInterop": true, + "jsx": "react", + "types": ["webpack-env"] +} diff --git a/packages/vega4-extension/tsconfig.json b/packages/vega4-extension/tsconfig.json new file mode 100644 index 000000000000..68df27709c47 --- /dev/null +++ b/packages/vega4-extension/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfigbase", + "compilerOptions": { + "outDir": "lib", + "types": ["webpack-env"], + "rootDir": "src" + }, + "include": ["src/*"], + "references": [ + { + "path": "../rendermime-interfaces" + } + ] +} diff --git a/packages/vega5-extension/README.md b/packages/vega5-extension/README.md index 677f347e0c3e..ca22252168b7 100644 --- a/packages/vega5-extension/README.md +++ b/packages/vega5-extension/README.md @@ -35,7 +35,7 @@ display({ }, raw=True) ``` -Using the [altair library](https://github.com/altair-viz/altair): +Using the [Altair library](https://github.com/altair-viz/altair): ```python import altair as alt @@ -51,7 +51,7 @@ chart = alt.Chart(cars).mark_point().encode( chart ``` -Provide vega-embed options via metadata: +Provide Vega-Embed options via metadata: ```python from IPython.display import display @@ -82,7 +82,7 @@ display({ }, raw=True) ``` -Provide vega-embed options via altair: +Provide Vega-Embed options via Altair: ```python import altair as alt diff --git a/packages/vega5-extension/package.json b/packages/vega5-extension/package.json index caae71e1421e..9175d51bb6bc 100644 --- a/packages/vega5-extension/package.json +++ b/packages/vega5-extension/package.json @@ -37,9 +37,9 @@ "@jupyterlab/rendermime-interfaces": "^1.3.0-alpha.11", "@phosphor/coreutils": "^1.3.0", "@phosphor/widgets": "^1.7.1", - "vega": "^5.3.5", - "vega-embed": "^4.0.0", - "vega-lite": "^3.2.1" + "vega": "^5.4.0", + "vega-embed": "^4.2.0", + "vega-lite": "^3.3.0" }, "devDependencies": { "@types/webpack-env": "^1.13.9", diff --git a/packages/vega5-extension/src/index.ts b/packages/vega5-extension/src/index.ts index 701fd3ffb1c7..e6c82745aac6 100644 --- a/packages/vega5-extension/src/index.ts +++ b/packages/vega5-extension/src/index.ts @@ -130,7 +130,7 @@ export const rendererFactory: IRenderMime.IRendererFactory = { const extension: IRenderMime.IExtension = { id: '@jupyterlab/vega5-extension:factory', rendererFactory, - rank: 50, + rank: 57, dataType: 'json', documentWidgetFactoryOptions: [ {