Skip to content
This repository has been archived by the owner on May 3, 2018. It is now read-only.

Commit

Permalink
Add Vega and VegaLite components
Browse files Browse the repository at this point in the history
  • Loading branch information
gnestor committed Mar 1, 2017
1 parent bacc56a commit 2b0a4e4
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 95 deletions.
124 changes: 114 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# jupyterlab_vega

A JupyterLab and Jupyter Notebook extension for rendering Vega
A JupyterLab and Jupyter Notebook extension for rendering Vega and Vega-lite

![output renderer](http://g.recordit.co/QAsC7YULcY.gif)
![output renderer](http://g.recordit.co/d9dWoMUUA3.gif)

## Prerequisites

Expand All @@ -16,18 +16,122 @@ To render Vega output in IPython:
from jupyterlab_vega import Vega

Vega({
"string": "string",
"array": [1, 2, 3],
"bool": True,
"object": {
"foo": "bar"
}
"width": 400,
"height": 200,
"padding": {"top": 10, "left": 30, "bottom": 30, "right": 10},
"data": [
{
"name": "table",
"values": [
{"x": 1, "y": 28}, {"x": 2, "y": 55},
{"x": 3, "y": 43}, {"x": 4, "y": 91},
{"x": 5, "y": 81}, {"x": 6, "y": 53},
{"x": 7, "y": 19}, {"x": 8, "y": 87},
{"x": 9, "y": 52}, {"x": 10, "y": 48},
{"x": 11, "y": 24}, {"x": 12, "y": 49},
{"x": 13, "y": 87}, {"x": 14, "y": 66},
{"x": 15, "y": 17}, {"x": 16, "y": 27},
{"x": 17, "y": 68}, {"x": 18, "y": 16},
{"x": 19, "y": 49}, {"x": 20, "y": 15}
]
}
],
"scales": [
{
"name": "x",
"type": "ordinal",
"range": "width",
"domain": {"data": "table", "field": "x"}
},
{
"name": "y",
"type": "linear",
"range": "height",
"domain": {"data": "table", "field": "y"},
"nice": True
}
],
"axes": [
{"type": "x", "scale": "x"},
{"type": "y", "scale": "y"}
],
"marks": [
{
"type": "rect",
"from": {"data": "table"},
"properties": {
"enter": {
"x": {"scale": "x", "field": "x"},
"width": {"scale": "x", "band": True, "offset": -1},
"y": {"scale": "y", "field": "y"},
"y2": {"scale": "y", "value": 0}
},
"update": {
"fill": {"value": "steelblue"}
},
"hover": {
"fill": {"value": "red"}
}
}
}
]
})
```

To render a `.vg` file as a tree, simply open it:
Or Vega-lite:

```python
from jupyterlab_vega import VegaLite

spec = {
"mark": "bar",
"encoding": {
"x": {"field": "a", "type": "ordinal"},
"y": {"field": "b", "type": "quantitative"}
}
}
data = [
{"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}
]

VegaLite(spec, data)
```

Using a pandas DataFrame:

```python
from jupyterlab_vega import VegaLite
import pandas as pd

df = pd.reada_json('cars.json')

VegaLite({
"mark": "point",
"encoding": {
"y": {"type": "quantitative","field": "Acceleration"},
"x": {"type": "quantitative","field": "Horsepower"}
}
}, df)
```

Using Altair:

```python
import altair

cars = altair.load_dataset('cars')
altair.Chart(cars).mark_point().encode(
x='Horsepower',
y='Miles_per_Gallon',
color='Origin',
)
```

To render a `.vg` or `.vl` (`.vg.json` and `.vl.json` are also supported) file as a tree, simply open it:

![file renderer](http://g.recordit.co/cbf0xnQHKn.gif)
![file renderer](http://g.recordit.co/z5LF4W28nv.gif)

## Install

Expand Down
3 changes: 3 additions & 0 deletions component/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.Vega {

}
.vega-actions {
display: none;
}
54 changes: 47 additions & 7 deletions component/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,54 @@
import React from 'react';
import vegaEmbed from 'vega-embed';
import './index.css';

export default class VegaComponent extends React.Component {
const DEFAULT_WIDTH = 500;
const DEFAULT_HEIGHT = DEFAULT_WIDTH / 1.5;

render() {
return (
<div className="Vega">
{JSON.stringify(this.props.data)}
</div>
);
export default class Vega extends React.Component {

static defaultProps = {
renderedCallback: () => ({}),
embedMode: 'vega-lite'
};

componentDidMount() {
this.embed(
this.el,
this.props.data,
this.props.embedMode,
this.props.renderedCallback
);
}

shouldComponentUpdate(nextProps) {
return this.props.data !== nextProps.data;
}

componentDidUpdate() {
this.embed(
this.el,
this.props.data,
this.props.embedMode,
this.props.renderedCallback
);
}

render() {
return <div ref={el => this.el = el} />;
}

embed = (el, spec, mode, cb) => {
const embedSpec = { mode, spec };
const width = DEFAULT_WIDTH;
const height = DEFAULT_HEIGHT;
if (mode === 'vega-lite') {
embedSpec.spec.config = {
...embedSpec.spec.config,
cell: { width, height }
};
}
vegaEmbed(el, embedSpec, cb);
}

}
6 changes: 5 additions & 1 deletion component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
"author": "Grant Nestor <[email protected]>",
"license": "ISC",
"dependencies": {
"react": "^15.3.2"
"d3": "3.5.17",
"react": "^15.3.2",
"vega": "^2.6.1",
"vega-embed": "^2.2.0",
"vega-lite": "^1.1.3"
}
}
44 changes: 42 additions & 2 deletions jupyterlab_vega/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,48 @@ def _ipython_display_(self):
'application/vnd.vega.v2+json': prepare_vega_spec(self.spec, self.data),
'text/plain': '<jupyterlab_vega.Vega object>'
}
metadata = {
'application/vnd.vega.v2+json': self.metadata
display(bundle, raw=True)


class VegaLite(Vega):
"""VegaLite expects a spec (a JSON-able dict) and data (JSON-able list or pandas DataFrame) argument
not already-serialized JSON strings.
Scalar types (None, number, string) are not allowed, only dict containers.
"""

def __init__(self, spec=None, data=None, url=None, filename=None, metadata=None):
"""Create a VegaLite display object given raw data.
Parameters
----------
spec : dict
VegaLite spec. Not an already-serialized JSON string.
data : dict or list
VegaLite data. Not an already-serialized JSON string.
Scalar types (None, number, string) are not allowed, only dict
or list containers.
url : unicode
A URL to download the data from.
filename : unicode
Path to a local file to load the data from.
metadata: dict
Specify extra metadata to attach to the json display object.
"""

super(VegaLite, self).__init__(spec=spec, data=data, url=url, filename=filename)

def _check_data(self):
if self.spec is not None and not isinstance(self.spec, dict):
raise TypeError("%s expects a JSONable dict, not %r" % (self.__class__.__name__, self.spec))
if self.data is not None and not isinstance(self.data, (list, pd.DataFrame)):
raise TypeError("%s expects a JSONable list or pandas DataFrame, not %r" % (self.__class__.__name__, self.data))

def _ipython_display_(self):
bundle = {
'application/vnd.vegalite.v1+json': prepare_vegalite_spec(self.spec, self.data),
'text/plain': '<jupyterlab_vega.VegaLite object>'
}
display(bundle, metadata=metadata, raw=True)

Expand Down
15 changes: 8 additions & 7 deletions labextension/src/doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ABCWidgetFactory } from 'jupyterlab/lib/docregistry';
import { ActivityMonitor } from 'jupyterlab/lib/common/activitymonitor';
import React from 'react';
import ReactDOM from 'react-dom';
import VegaComponent from 'jupyterlab_vega_react';
import Vega from 'jupyterlab_vega_react';

/**
* The class name added to a DocWidget.
Expand Down Expand Up @@ -57,12 +57,13 @@ export class DocWidget extends Widget {
const content = this._context.model.toString();
try {
const data = JSON.parse(content);
ReactDOM.render(
<VegaComponent data={data} />,
this.node
);
const path = this._context._path;
const props = {
data,
embedMode: path.includes('.vl') ? 'vega-lite' : 'vega'
};
ReactDOM.render(<Vega {...props} />, this.node);
} catch (error) {

const ErrorDisplay = props => (
<div
className="jp-RenderedText jp-mod-error"
Expand Down Expand Up @@ -112,7 +113,7 @@ export class DocWidget extends Widget {
/**
* A widget factory for DocWidget.
*/
export class DocWidgetFactory extends ABCWidgetFactory {
export class VegaDoc extends ABCWidgetFactory {
constructor(options) {
super(options);
}
Expand Down
52 changes: 43 additions & 9 deletions labextension/src/output.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Widget } from '@phosphor/widgets';
import React from 'react';
import ReactDOM from 'react-dom';
import VegaComponent from 'jupyterlab_vega_react';
import Vega from 'jupyterlab_vega_react';

/**
* The class name added to this OutputWidget.
Expand All @@ -15,8 +15,9 @@ export class OutputWidget extends Widget {
constructor(options) {
super();
this.addClass(CLASS_NAME);
this._data = options.model.data.get(options.mimeType);
this._metadata = options.model.metadata.get(options.mimeType);
this._data = options.model.data;
// this._metadata = options.model.metadata.get(options.mimeType);
this._mimeType = options.mimeType;
}

/**
Expand All @@ -37,18 +38,51 @@ export class OutputWidget extends Widget {
* A render function given the widget's DOM node.
*/
_render() {
ReactDOM.render(
<VegaComponent data={this._data} metadata={this._metadata} />,
this.node
);
const data = this._data.get(this._mimeType);
// const metadata = this._metadata.get(this._mimeType);
const props = {
data,
// metadata,
embedMode: this._mimeType === 'application/vnd.vegalite.v1+json'
? 'vega-lite'
: 'vega',
renderedCallback: (error, result) => {
if (error) return console.log(error);
// Add a static image output to mime bundle
const imageData = result.view.toImageURL().split(',')[1];
this._data.set('image/png', imageData)
}
};
ReactDOM.render(<Vega {...props} />, this.node);
}
}

export class OutputRenderer {
export class VegaOutput {
/**
* The mime types this OutputRenderer accepts.
*/
mimeTypes = [ 'application/vnd.vega.v2+json' ];
mimeTypes = ['application/vnd.vega.v2+json'];

/**
* Whether the renderer can render given the render options.
*/
canRender(options) {
return this.mimeTypes.indexOf(options.mimeType) !== -1;
}

/**
* Render the transformed mime bundle.
*/
render(options) {
return new OutputWidget(options);
}
}

export class VegaLiteOutput {
/**
* The mime types this OutputRenderer accepts.
*/
mimeTypes = ['application/vnd.vegalite.v1+json'];

/**
* Whether the renderer can render given the render options.
Expand Down
Loading

0 comments on commit 2b0a4e4

Please sign in to comment.