Various improvements

* Renamed App to DatasetteVega
* Component uses fetch() to retrieve columns on start
* DatasetteVega.test.js tests now pass
* Column select boxes now display next to each other
* Styling is slightly improved
This commit is contained in:
Simon Willison 2018-06-25 17:30:37 -07:00
commit 4cf1def414
5 changed files with 94 additions and 46 deletions

View file

@ -1,12 +1,12 @@
import React, { Component } from 'react';
import vegaEmbed from 'vega-embed';
import './App.css';
import './DatasetteVega.css';
const serialize = obj => Object.keys(obj).map(
key => obj[key] ? `g.${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}` : ''
).join('&');
const unserialize = s => {
const unserialize = (s, prefix) => {
if (s && s[0] === '#') {
s = s.slice(1);
}
@ -14,19 +14,20 @@ const unserialize = s => {
return {};
}
var obj = {};
s.split('&').forEach(bit => {
s.split('&').filter(bit => bit.slice(0, prefix.length + 1) === `{prefix).`).forEach(bit => {
let pair = bit.split('=');
obj[decodeURIComponent(pair[0]).replace(/^g\./, '')] = decodeURIComponent(pair[1]);
});
return obj;
};
class App extends Component {
class DatasetteVega extends Component {
state = {
columns: this.props.columns || [],
mark: "bar",
x_column: this.props.columns[0],
x_column: null,
x_type: "ordinal",
y_column: this.props.columns[1],
y_column: null,
y_type: "quantitative",
color_column: "",
size_column: ""
@ -53,16 +54,61 @@ class App extends Component {
this.renderGraph();
});
}
jsonUrl() {
let url = this.props.base_url;
if (/\?/.exec(url)) {
url += '&';
} else {
url += '?';
}
url += '_shape=array';
return url;
}
componentDidMount() {
window.onpopstate = this.onPopStateChange.bind(this);
this.renderGraph();
// Load the columns
let url = this.jsonUrl();
if (this.props.columns) {
return;
}
fetch(url).then(r => r.json()).then(data => {
if (data.length > 1) {
// Set columns to first item's keys
const columns = Object.keys(data[0]);
console.log(columns);
this.setState({
columns: columns,
x_column: columns[0],
y_column: columns[1],
}, this.renderGraph);
}
});
}
serializeState() {
return serialize((({
mark,
x_column,
x_type,
y_column,
y_type,
color_column,
size_column
}) => ({
mark,
x_column,
x_type,
y_column,
y_type,
color_column,
size_column
}))(this.state));
}
onPopStateChange(ev) {
window.lastPopEv = ev;
let expected = '#' + serialize(this.state);
const expected = '#' + this.serializeState();
if (expected !== document.location.hash) {
this.setState(
unserialize(document.location.hash), this.renderGraph.bind(this)
unserialize(document.location.hash, 'g'), this.renderGraph.bind(this)
);
}
}
@ -83,14 +129,15 @@ class App extends Component {
}
const spec = {
data: {
url: this.props.json_url
url: this.jsonUrl()
},
mark: this.state.mark,
encoding: encoding
}
if (spec.mark) {
console.log('spec= ', spec);
if (spec.mark && spec.encoding.x.field && spec.encoding.y.field) {
vegaEmbed("#vis", spec, {theme: 'quartz', tooltip: true});
document.location.hash = '#' + serialize(this.state);
document.location.hash = '#' + this.serializeState()
this.setState({spec: spec});
}
}
@ -105,52 +152,45 @@ class App extends Component {
}
render() {
const onChangeSelect = this.onChangeSelect.bind(this);
if (!this.state.mark) {
return null;
}
const columns = this.state.columns;
console.log(columns);
return (
<form action="" method="GET" id="graphForm">
(columns.length > 1) ? <form action="" method="GET" id="graphForm">
<div>
Mark: <select name="mark" value={this.state.mark} onChange={ev => onChangeSelect("mark", ev)}>
<label>Type of chart: <select name="mark" value={this.state.mark} onChange={ev => onChangeSelect("mark", ev)}>
{this.markOptions.map(option => <option key={option.value} value={option.value}>{option.name}</option>)}
</select>
</select></label>
</div>
<div>
X column: <select name="x_column" value={this.state.x_column} onChange={ev => onChangeSelect("x_column", ev)}>
{this.props.columns.map(column => <option key={column} value={column}>{column}</option>)}
</select>
</div>
<div>
X type: <select name="x_type" value={this.state.x_type} onChange={ev => onChangeSelect("x_type", ev)}>
<label>X <select name="x_column" value={this.state.x_column || ''} onChange={ev => onChangeSelect("x_column", ev)}>
{columns.map(column => <option key={column} value={column}>{column}</option>)}
</select></label>
<label>X type: <select name="x_type" value={this.state.x_type} onChange={ev => onChangeSelect("x_type", ev)}>
{this.typeOptions.map(option => <option key={option.value} value={option.value}>{option.name}</option>)}
</select>
</div>
<div>
Y column: <select name="y_column" value={this.state.y_column} onChange={ev => onChangeSelect("y_column", ev)}>
{this.props.columns.map(column => <option key={column} value={column}>{column}</option>)}
</select>
</select></label>
</div>
<div>
<label>Y <select name="y_column" value={this.state.y_column || ''} onChange={ev => onChangeSelect("y_column", ev)}>
{columns.map(column => <option key={column} value={column}>{column}</option>)}
</select></label>
Y type: <select name="y_type" value={this.state.y_type} onChange={ev => onChangeSelect("y_type", ev)}>
{this.typeOptions.map(option => <option key={option.value} value={option.value}>{option.name}</option>)}
</select>
</div>
<div>
Color column: <select name="color_column" value={this.state.color_column} onChange={ev => onChangeSelect("color_column", ev)}>
<label>Color <select name="color_column" value={this.state.color_column} onChange={ev => onChangeSelect("color_column", ev)}>
<option value="">-- none --</option>
{this.props.columns.map(column => <option key={column} value={column}>{column}</option>)}
</select>
</div>
<div>
Size column: <select name="size_column" value={this.state.size_column} onChange={ev => onChangeSelect("size_column", ev)}>
{columns.map(column => <option key={column} value={column}>{column}</option>)}
</select></label>
<label>Size <select name="size_column" value={this.state.size_column} onChange={ev => onChangeSelect("size_column", ev)}>
<option value="">-- none --</option>
{this.props.columns.map(column => <option key={column} value={column}>{column}</option>)}
</select>
{columns.map(column => <option key={column} value={column}>{column}</option>)}
</select></label>
</div>
<div><button onClick={this.toggleAxis.bind(this)}>Swap X and Y</button></div>
</form>
</form> : null
);
}
}
export default App;
export default DatasetteVega;

View file

@ -1,9 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import DatasetteVega from './DatasetteVega';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.render(<DatasetteVega base_url="https://www.example.com/" columns={[]} />, div);
ReactDOM.unmountComponentAtNode(div);
});

View file

@ -1,5 +1,14 @@
body {
margin: 0;
padding: 0;
padding: 1em;
font-family: sans-serif;
}
form div {
margin-bottom: 0.5em
}
label {
padding-right: 0.5em;
}

View file

@ -1,7 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import DatasetteVega from './DatasetteVega';
ReactDOM.render(
/*<App columns={[
@ -17,7 +17,6 @@ ReactDOM.render(
json_url="https://fivethirtyeight.datasettes.com/fivethirtyeight-45d758d/bechdel%2Fmovies.json?_shape=array"
// json_url="https://fivethirtyeight.datasettes.com/fivethirtyeight-45d758d/airline-safety%2Fairline-safety.json?_shape=array"
*/
<App columns={"rowid,year,imdb,title,test,clean_test,binary,budget,domgross,intgross,code,budget_2013$,domgross_2013$,intgross_2013$,period code,decade code".split(",")}
json_url="https://fivethirtyeight.datasettes.com/fivethirtyeight-45d758d.json?sql=select+rowid%2C+*+from+%5Bbechdel%2Fmovies%5D+order+by+random%28%29+limit+200&_shape=array"
<DatasetteVega base_url="https://fivethirtyeight.datasettes.com/fivethirtyeight-45d758d.json?sql=select+rowid%2C+*+from+%5Bbechdel%2Fmovies%5D+order+by+random%28%29+limit+200"
/>, document.getElementById('root')
);