From 49b983141ecac5ed4f164b8346013847d3a5bc83 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 26 Jun 2018 08:29:17 -0700 Subject: [PATCH 01/40] Deploy to datasette-vega-latest.datasette.io if tests pass, closes #2 --- .travis.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..80f0b5a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: node_js + +node_js: + - "stable" + +cache: + directories: + - node_modules + +script: + - CI=true npm test + +jobs: + include: + - stage: deploy datasette-vega-latest.datasette.io + on: master + script: + - npm install -g now + - REACT_APP_STAGE=dev npm run build + - echo '{"name":"datasette-vega-latest","alias":"datasette-vega-latest.datasette.io"}' > build/now.json + - cd build && now --name=datasette-vega-latest --token=$NOW_TOKEN + - now alias --token=$NOW_TOKEN From bdb39c7c030fefe729ddabf6328ec4c4d456d527 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 26 Jun 2018 21:56:00 -0700 Subject: [PATCH 02/40] Respect fragment hash on page load, closes #7 --- src/DatasetteVega.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/DatasetteVega.js b/src/DatasetteVega.js index 7501d33..64ff774 100644 --- a/src/DatasetteVega.js +++ b/src/DatasetteVega.js @@ -81,7 +81,9 @@ class DatasetteVega extends Component { columns: columns, x_column: columns[0], y_column: columns[1], - }, this.renderGraph); + }, () => { + this.onPopStateChange(); + }); } }); } From ed870be722825a0b7f343230d7c39240624a5def Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 26 Jun 2018 21:56:49 -0700 Subject: [PATCH 03/40] Better default UI for dev mode, refs #1 --- public/index.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/index.html b/public/index.html index c5bff9f..483ad0c 100644 --- a/public/index.html +++ b/public/index.html @@ -7,12 +7,12 @@ Datasette Vega - -
- - +

Datasette Vega

+

Enter the URL of the JSON version of any Datasette table:

+ + + +

Examples: game_id/pts/team_id

From 16ff7b11a9c627debac1158612d66a0dabd42c70 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 26 Jun 2018 22:01:15 -0700 Subject: [PATCH 04/40] Fixed debug mode style glitch in Firefox --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 483ad0c..386993c 100644 --- a/public/index.html +++ b/public/index.html @@ -11,7 +11,7 @@

Enter the URL of the JSON version of any Datasette table:

- +

Examples: game_id/pts/team_id

From 8e313ef7bddd6c23a597e95d49712feeaf21169c Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 26 Jun 2018 22:27:32 -0700 Subject: [PATCH 05/40] Much improved styling, refs #3 Also reduced chart options to just line, bar, scatter --- public/index.html | 1 + src/DatasetteVega.css | 32 +++++++++++++++++++++++++ src/DatasetteVega.js | 56 +++++++++++++++++++++---------------------- 3 files changed, 60 insertions(+), 29 deletions(-) diff --git a/public/index.html b/public/index.html index 386993c..0b14e70 100644 --- a/public/index.html +++ b/public/index.html @@ -4,6 +4,7 @@ + Datasette Vega diff --git a/src/DatasetteVega.css b/src/DatasetteVega.css index 199ea14..72e246d 100644 --- a/src/DatasetteVega.css +++ b/src/DatasetteVega.css @@ -66,3 +66,35 @@ background-color: #f7f7f9; color: #1b1e23; } + +.datasette-vega { + margin: 1em 0; + padding: 1em; + width: auto; + display: inline-block; + font-size: 0.8em; + box-shadow: 1px 2px 8px 2px rgba(0,0,0,0.08); +} + +.datasette-vega .swap-x-y { + color: #fff; + background-color: #007bff; + border-color: #007bff; + font-weight: 400; + cursor: pointer; + text-align: center; + vertical-align: middle; + border: 1px solid blue; + padding: .5em 0.8em; + line-height: 1; + border-radius: .25rem; + -webkit-appearance: button; + font-size: 0.7em; +} + +form.datasette-vega label { + font-weight: bold; + display: inline; + width: auto; + padding-right: 0.3em; +} diff --git a/src/DatasetteVega.js b/src/DatasetteVega.js index 64ff774..1708921 100644 --- a/src/DatasetteVega.js +++ b/src/DatasetteVega.js @@ -35,17 +35,15 @@ class DatasetteVega extends Component { markOptions = [ {"value": "bar", "name": "bar"}, {"value": "line", "name": "line"}, - {"value": "circle", "name": "circle"}, - {"value": "square", "name": "square"}, - {"value": "point", "name": "point"}, + {"value": "circle", "name": "scatter"}, ] typeOptions = [ - {"value": "quantitative", "name": "Numbers (quantitative)"}, - {"value": "quantitative-bin", "name": "Numbers (quantitative), binned"}, - {"value": "temporal", "name": "Dates and times (temporal)"}, - {"value": "temporal-bin", "name": "Dates and times (temporal), binned"}, - {"value": "ordinal", "name": "Labels (ordinal)"}, - {"value": "nominal", "name": "Categories (nominal)"}, + {"value": "quantitative", "name": "Numeric"}, + {"value": "quantitative-bin", "name": "Numeric, binned"}, + {"value": "temporal", "name": "Date/time"}, + {"value": "temporal-bin", "name": "Date/time, binned"}, + {"value": "ordinal", "name": "Label"}, + {"value": "nominal", "name": "Category"}, ] onChangeSelect(name, ev) { ev.persist(); @@ -157,39 +155,39 @@ class DatasetteVega extends Component { const onChangeSelect = this.onChangeSelect.bind(this); const columns = this.state.columns; return ( - (columns.length > 1) ?
-
- +
-
- -
+ -
- - Y type:
+ -
- -
+ -
+
: null ); } From 2b6c6ef14d41751da2807ab99ee8991ecd1db579 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 26 Jun 2018 22:31:09 -0700 Subject: [PATCH 06/40] Deploy new tags as PyPI releases --- .travis.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.travis.yml b/.travis.yml index 80f0b5a..bc81bc2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,3 +20,17 @@ jobs: - echo '{"name":"datasette-vega-latest","alias":"datasette-vega-latest.datasette.io"}' > build/now.json - cd build && now --name=datasette-vega-latest --token=$NOW_TOKEN - now alias --token=$NOW_TOKEN + - stage: release tagged version + if: tag IS present + python: 3.6 + script: + - pip install -U pip wheel + deploy: + - provider: pypi + user: simonw + distributions: bdist_wheel + password: + secure: ikY6iF+/2AxFlwcun35iAEz7Wbrnp0Fp7b32Z7uob5RsGpIDCYYDa3T/qNhiyNsg5fecGp01rBf1F+dL357b0rWEeaoONtsDrTKFeBUB6bMnzNtr3QbHq/TeQ1f+Vn6FWpY9360Ihbz0pKPzWWYHJxOjXPM793rGWmPu7siCc4oQZOpwLK28GZeFP803wq/QG81hFRWR8IiMlqljkecEGhaM6ftxzizk1LBoTZCw3DdL2xDwzrLvF3Hg1jXX08pJm9WrLJNS6i+LRhiLv9IJ5KxRDwNHJhrvblRrZn0CKVbiWR/8985r4R7CXaeG68uJ505RcVoXYRsq9D7mAcYAB3U5AzU9TsPQlvUgwJlFpKOsZlCw+5AHFIYUXvlP2Qo5kfXUYC9KKdJwbWjcW7isUqWpzInGfnmRNMJGzmIm2q65ua0FNLV4vK1wl98O60HFSyhXJi84YmCAPgidJ22sEA6sHAdMEFiWglhap12zPc6i7trXKC8aLjW/3qGXhUq3BK8vQcbutGer+Q3z9UM7kthB76FtCUVc8eqljtdAcGt5kXoHIEPwGb50ikOxo/qpXsXVH36Z6plbELbayXb3ocUrMdhWP5+kfnrdLEmx+X4IpUauxGUqLGliYSQpfjp8XyA87ASCjsSqUEjr/4t4YztW5a3mWBYZ+IjjYXW6PQg= + on: + branch: master + tags: true From 267bdbbf58b853ab110186f954a320bd16b1a811 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 26 Jun 2018 22:35:26 -0700 Subject: [PATCH 07/40] Extended README in preparation for 0.1 release --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e93a82..7314ed4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ # datasette-vega -A [Datasette](https://github.com/simonw/datasette) plugin that provides tools for generating charts using [Vega](https://vega.github.io/). +A [Datasette](https://github.com/simonw/datasette) plugin that provides tools +for generating charts using [Vega](https://vega.github.io/). + +Try out the latest version as a live demo at https://datasette-vega-latest.datasette.io/ + +To add this to your Datasette installation, install the plugin like so: + + pip install datasette-vega + +The plugin will then add itself to every Datasette table view. + +If you are publishing data using the `datasette publish` command, you can +include this plugin like so: + + datasette publish now mydatabase.db --install=datasette-vega From 64205f3333daf448a10e3464952e4b58472aafde Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 27 Jun 2018 08:17:42 -0700 Subject: [PATCH 08/40] Respect _labels=on even if first row has a null, closes #5 --- src/DatasetteVega.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/DatasetteVega.js b/src/DatasetteVega.js index 1708921..9d0d96c 100644 --- a/src/DatasetteVega.js +++ b/src/DatasetteVega.js @@ -72,9 +72,14 @@ class DatasetteVega extends Component { 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]).map( - key => (data[0][key] && data[0][key].label) ? `${key}.label` : key - ); + const columns = Object.keys(data[0]).map(key => { + // Do ANY of these rows have a .label property? + if (data.filter(d => (d[key] || '').label !== undefined).length) { + return `${key}.label`; + } else { + return key; + } + }); this.setState({ columns: columns, x_column: columns[0], From f1b29c0d8e832f8e37a668afce5e2d3bef273907 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 27 Jun 2018 13:21:58 -0700 Subject: [PATCH 09/40] Final improvements to styling before release announcement I changed the chart type selector to be radio buttons (Bar, Line, Scatter) which breaks up the UI. I also added a "Charting options"

title and tweaked the display of the "Swap X and Y" button. The design is now good enough that I'm happy to announce it. Closes #3 --- src/DatasetteVega.css | 27 ++++++++++++++++++++++----- src/DatasetteVega.js | 25 +++++++++++++++++-------- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/DatasetteVega.css b/src/DatasetteVega.css index 72e246d..010b38f 100644 --- a/src/DatasetteVega.css +++ b/src/DatasetteVega.css @@ -75,16 +75,20 @@ font-size: 0.8em; box-shadow: 1px 2px 8px 2px rgba(0,0,0,0.08); } - +.datasette-vega h3 { + margin-top: 0; +} .datasette-vega .swap-x-y { - color: #fff; - background-color: #007bff; - border-color: #007bff; + text-align: center; + margin-bottom: 0.6em; +} +.datasette-vega .swap-x-y button { + background-color: #fafafa; + border: 1px solid #ccc; font-weight: 400; cursor: pointer; text-align: center; vertical-align: middle; - border: 1px solid blue; padding: .5em 0.8em; line-height: 1; border-radius: .25rem; @@ -98,3 +102,16 @@ form.datasette-vega label { width: auto; padding-right: 0.3em; } + +form.datasette-vega div { + margin-bottom: 0.3em; +} + +.datasette-vega .radio-buttons label { + margin-right: 2em; + display: inline-block; +} + +.datasette-vega .radio-buttons { + margin-bottom: 1em; +} diff --git a/src/DatasetteVega.js b/src/DatasetteVega.js index 9d0d96c..d0340e4 100644 --- a/src/DatasetteVega.js +++ b/src/DatasetteVega.js @@ -33,9 +33,9 @@ class DatasetteVega extends Component { size_column: "" } markOptions = [ - {"value": "bar", "name": "bar"}, - {"value": "line", "name": "line"}, - {"value": "circle", "name": "scatter"}, + {"value": "bar", "name": "Bar"}, + {"value": "line", "name": "Line"}, + {"value": "circle", "name": "Scatter"}, ] typeOptions = [ {"value": "quantitative", "name": "Numeric"}, @@ -161,10 +161,17 @@ class DatasetteVega extends Component { const columns = this.state.columns; return ( (columns.length > 1) ?
-
- +

Charting options

+
+ {this.markOptions.map(option => ( + + ))}
+
+ +
-
: null ); } From f19ed314d51cab3fb98b51743159fc8301ec9820 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 28 Jun 2018 09:43:10 -0500 Subject: [PATCH 10/40] Fix collision with datasette-cluster-map It was not possible to install both plugins at the same time. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1d573b4..cfd2919 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from subprocess import check_output from wheel.bdist_wheel import bdist_wheel import os -VERSION = '0.1' +VERSION = '0.2' ROOT = os.path.dirname(os.path.abspath(__file__)) @@ -49,7 +49,7 @@ setup( packages=['datasette_vega'], entry_points={ 'datasette': [ - 'cluster_map = datasette_vega' + 'vega = datasette_vega' ], }, package_data={ From cd046b82c09184c41887ab89d729b326507adec5 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 28 Jun 2018 09:46:49 -0500 Subject: [PATCH 11/40] Deploy stage uses language: python, refs #8 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index bc81bc2..d739efb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,7 @@ jobs: - now alias --token=$NOW_TOKEN - stage: release tagged version if: tag IS present + language: python python: 3.6 script: - pip install -U pip wheel From 682da1f7ac978fd3af753a3009985e9926d714fb Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 29 Jun 2018 08:11:30 -0500 Subject: [PATCH 12/40] New "Show charting options" button Also switched to using a ref= for the vega element. Ready for 0.3 release. --- setup.py | 2 +- src/DatasetteVega.css | 6 +++++- src/DatasetteVega.js | 43 +++++++++++++++++++++++++++++++------------ src/index.js | 12 ++---------- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/setup.py b/setup.py index cfd2919..7e163f2 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from subprocess import check_output from wheel.bdist_wheel import bdist_wheel import os -VERSION = '0.2' +VERSION = '0.3' ROOT = os.path.dirname(os.path.abspath(__file__)) diff --git a/src/DatasetteVega.css b/src/DatasetteVega.css index 010b38f..efbcbeb 100644 --- a/src/DatasetteVega.css +++ b/src/DatasetteVega.css @@ -82,7 +82,8 @@ text-align: center; margin-bottom: 0.6em; } -.datasette-vega .swap-x-y button { +.datasette-vega button, +.datasette-vega-inactive button { background-color: #fafafa; border: 1px solid #ccc; font-weight: 400; @@ -95,6 +96,9 @@ -webkit-appearance: button; font-size: 0.7em; } +.datasette-vega-inactive { + margin-bottom: 1em; +} form.datasette-vega label { font-weight: bold; diff --git a/src/DatasetteVega.js b/src/DatasetteVega.js index d0340e4..24e27d8 100644 --- a/src/DatasetteVega.js +++ b/src/DatasetteVega.js @@ -23,8 +23,9 @@ const unserialize = (s, prefix) => { class DatasetteVega extends Component { state = { - columns: this.props.columns || [], - mark: "bar", + show: false, + columns: [], + mark: null, x_column: null, x_type: "ordinal", y_column: null, @@ -66,9 +67,6 @@ class DatasetteVega extends Component { window.onpopstate = this.onPopStateChange.bind(this); // 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 @@ -80,12 +78,21 @@ class DatasetteVega extends Component { return key; } }); - this.setState({ + let initialState = { columns: columns, x_column: columns[0], y_column: columns[1], - }, () => { + }; + // Is there state in the URL? If so use that too + let urlState = unserialize(document.location.hash, 'g'); + if (Object.keys(urlState).length) { + initialState = Object.assign(initialState, urlState); + // And show the widget + initialState.show = true; + } + this.setState(initialState, () => { this.onPopStateChange(); + this.renderGraph(); }); } }); @@ -112,7 +119,7 @@ class DatasetteVega extends Component { onPopStateChange(ev) { window.lastPopEv = ev; const expected = '#' + this.serializeState(); - if (expected !== document.location.hash) { + if (expected !== document.location.hash && this.state.mark) { this.setState( unserialize(document.location.hash, 'g'), this.renderGraph.bind(this) ); @@ -141,10 +148,10 @@ class DatasetteVega extends Component { encoding: encoding } if (spec.mark && spec.encoding.x.field && spec.encoding.y.field) { - vegaEmbed("#vis", spec, {theme: 'quartz', tooltip: true}); + vegaEmbed(this.chart, spec, {theme: 'quartz', tooltip: true}); document.location.hash = '#' + this.serializeState(); // Add to state so react debug tools can see it (for debugging): - this.setState({spec: spec}); + this.setState({spec: spec, show: true}); } } toggleAxis(ev) { @@ -156,11 +163,19 @@ class DatasetteVega extends Component { y_type: prevState.x_type, }), this.renderGraph); } + showChart() { + this.setState({ + show: true + }, this.renderGraph.bind(this)); + } render() { const onChangeSelect = this.onChangeSelect.bind(this); const columns = this.state.columns; + if (!this.state.show) { + return
; + } return ( - (columns.length > 1) ?
+ (columns.length > 1) ?

Charting options

{this.markOptions.map(option => ( @@ -202,7 +217,11 @@ class DatasetteVega extends Component { {columns.map(column => )}
-
: null + +
+
{ this.chart = c; }}>
+
+
: null ); } } diff --git a/src/index.js b/src/index.js index a24b420..91c9cc6 100644 --- a/src/index.js +++ b/src/index.js @@ -4,15 +4,13 @@ import './index.css'; import DatasetteVega from './DatasetteVega'; document.addEventListener('DOMContentLoaded', () => { - let visWrapper, vis, visTool, jsonUrl; + let visTool, jsonUrl; if (process.env.REACT_APP_STAGE === 'dev') { // Dev mode - use graph elements already on the index.html page let m = /\?url=(.*)/.exec(window.location.search) if (m) { jsonUrl = decodeURIComponent(m[1]); document.getElementById('jsonUrl').value = jsonUrl; - visWrapper = document.getElementById('vis-wrapper'); - vis = document.getElementById('vis'); visTool = document.getElementById('vis-tool'); } } else { @@ -20,15 +18,9 @@ document.addEventListener('DOMContentLoaded', () => { if (jsonEl) { jsonUrl = jsonEl.getAttribute('href'); // Create elements for adding graph tool to page - visWrapper = document.createElement('div'); - visWrapper.style.overflow = 'hidden'; - vis = document.createElement('div'); visTool = document.createElement('div'); - vis.setAttribute('id', 'vis'); - visWrapper.appendChild(vis); let table = document.querySelector('table.rows-and-columns'); - table.parentNode.insertBefore(visWrapper, table); - table.parentNode.insertBefore(visTool, visWrapper); + table.parentNode.insertBefore(visTool, table); } } if (jsonUrl) { From 8e7dd5cb0b2273b96999e6e20a8ad654a0e0ad4d Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 29 Jun 2018 08:36:59 -0500 Subject: [PATCH 13/40] Link to more demos plus publish.datasettes.com --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7314ed4..c82a059 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A [Datasette](https://github.com/simonw/datasette) plugin that provides tools for generating charts using [Vega](https://vega.github.io/). -Try out the latest version as a live demo at https://datasette-vega-latest.datasette.io/ +Try out the latest master build as a live demo at https://datasette-vega-latest.datasette.io/ or try the latest release installed as a plugin at https://fivethirtyeight.datasettes.com/ To add this to your Datasette installation, install the plugin like so: @@ -15,3 +15,5 @@ If you are publishing data using the `datasette publish` command, you can include this plugin like so: datasette publish now mydatabase.db --install=datasette-vega + +If you don't want to install any software at all, you can publish your CSV files with Datasette and activate this plugin using the web application at https://publish.datasettes.com/ From 9eea628b8e978b330796ac5e548aeb10de0facc1 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 29 Jun 2018 08:40:30 -0500 Subject: [PATCH 14/40] Added screenshot to readme --- README.md | 2 ++ datasette-vega.png | Bin 0 -> 16901 bytes 2 files changed, 2 insertions(+) create mode 100644 datasette-vega.png diff --git a/README.md b/README.md index c82a059..5ec0283 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ A [Datasette](https://github.com/simonw/datasette) plugin that provides tools for generating charts using [Vega](https://vega.github.io/). +![Datasette Vega interface](datasette-vega.png) + Try out the latest master build as a live demo at https://datasette-vega-latest.datasette.io/ or try the latest release installed as a plugin at https://fivethirtyeight.datasettes.com/ To add this to your Datasette installation, install the plugin like so: diff --git a/datasette-vega.png b/datasette-vega.png new file mode 100644 index 0000000000000000000000000000000000000000..1546b2f051a58a748f23669eea33bbff990f38b5 GIT binary patch literal 16901 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 o4WqnK`1i10e#_zCpLqVmD^dFcDrG*^?0 Date: Fri, 29 Jun 2018 12:37:28 -0500 Subject: [PATCH 15/40] Fix for #9 - image should show on PyPI now --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5ec0283..616aeef 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A [Datasette](https://github.com/simonw/datasette) plugin that provides tools for generating charts using [Vega](https://vega.github.io/). -![Datasette Vega interface](datasette-vega.png) +![Datasette Vega interface](https://raw.githubusercontent.com/simonw/datasette-vega/master/datasette-vega.png) Try out the latest master build as a live demo at https://datasette-vega-latest.datasette.io/ or try the latest release installed as a plugin at https://fivethirtyeight.datasettes.com/ diff --git a/setup.py b/setup.py index 7e163f2..8ccd154 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from subprocess import check_output from wheel.bdist_wheel import bdist_wheel import os -VERSION = '0.3' +VERSION = '0.4.1' ROOT = os.path.dirname(os.path.abspath(__file__)) From 70d4688161a12c7cc1010e04b898c9e6edf69c11 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 5 Jul 2018 17:16:20 -0700 Subject: [PATCH 16/40] setup.py build_static now runs npm install - refs #8 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 8ccd154..844f2fe 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ class BuildStatic(Command): pass def run(self): + check_output(['npm', 'install'], cwd=ROOT) check_output(['npm', 'run', 'build'], cwd=ROOT) check_output(['mkdir', '-p', 'datasette_vega/static'], cwd=ROOT) check_output("mv build/static/js/*.js datasette_vega/static/datasette-vega.js", shell=True, cwd=ROOT) From e768dd05f198da5efab804e69aa257a435f98012 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 5 Jul 2018 18:53:55 -0700 Subject: [PATCH 17/40] Persist #g.XX graph settings when clicking links, refs #12 --- src/index.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/index.js b/src/index.js index 91c9cc6..83494ee 100644 --- a/src/index.js +++ b/src/index.js @@ -32,3 +32,46 @@ document.addEventListener('DOMContentLoaded', () => { ); } }); + +// Persist #settings across links to same page +window.addEventListener('click', function (ev) { + if (window.location.hash.length == 0) { + return true; + } + if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.defaultPrevented) { + return true; + } + var a = null; + for (var n = ev.target; n.parentNode; n = n.parentNode) { + if (n.nodeName === 'A') { + a = n; + break; + } + } + if (!a || !a.href) { + return true; + } + // This expands the full URL even if the link is /relative: + var href = a.href; + // Split off any existing #fragment + href = href.split('#')[0]; + // Only activate if link is to current page (presumably with different querystring) + var currentHostAndPath = window.location.hostname; + if (window.location.port !== '') { + currentHostAndPath += ':' + window.location.port; + } + currentHostAndPath += window.location.pathname; + // Ignore http/s due to https://github.com/simonw/datasette/issues/333 + var linkedHostQuery = href.split('?')[1]; + var linkedHostAndPath = href.split('://')[1].split('?')[0]; + if (currentHostAndPath == linkedHostAndPath) { + // Cancel click, navigate to this + fragment instead + if (linkedHostQuery) { + linkedHostAndPath += '?' + linkedHostQuery; + } + ev.preventDefault(); + window.location = window.location.protocol + '//' + linkedHostAndPath + window.location.hash; + return false; + } + return true; +}); From 662dabf888793adf2023014ae7998cef3a191d01 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 5 Jul 2018 19:07:08 -0700 Subject: [PATCH 18/40] Fixed JavaScript warnings, refs #12 --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 83494ee..6403985 100644 --- a/src/index.js +++ b/src/index.js @@ -35,7 +35,7 @@ document.addEventListener('DOMContentLoaded', () => { // Persist #settings across links to same page window.addEventListener('click', function (ev) { - if (window.location.hash.length == 0) { + if (window.location.hash.length === 0) { return true; } if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.defaultPrevented) { @@ -64,7 +64,7 @@ window.addEventListener('click', function (ev) { // Ignore http/s due to https://github.com/simonw/datasette/issues/333 var linkedHostQuery = href.split('?')[1]; var linkedHostAndPath = href.split('://')[1].split('?')[0]; - if (currentHostAndPath == linkedHostAndPath) { + if (currentHostAndPath === linkedHostAndPath) { // Cancel click, navigate to this + fragment instead if (linkedHostQuery) { linkedHostAndPath += '?' + linkedHostQuery; From 5e1695fe99d0410a6cefcb8caedd16602f025253 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 5 Jul 2018 20:17:01 -0700 Subject: [PATCH 19/40] Persist #g.XXX settings through SQL form submission, refs #12 --- src/DatasetteVega.js | 1 + src/index.js | 41 +++++++++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/DatasetteVega.js b/src/DatasetteVega.js index 24e27d8..b2cf979 100644 --- a/src/DatasetteVega.js +++ b/src/DatasetteVega.js @@ -150,6 +150,7 @@ class DatasetteVega extends Component { if (spec.mark && spec.encoding.x.field && spec.encoding.y.field) { vegaEmbed(this.chart, spec, {theme: 'quartz', tooltip: true}); document.location.hash = '#' + this.serializeState(); + this.props.onFragmentChange && this.props.onFragmentChange(); // Add to state so react debug tools can see it (for debugging): this.setState({spec: spec, show: true}); } diff --git a/src/index.js b/src/index.js index 6403985..98adc8f 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,34 @@ import ReactDOM from 'react-dom'; import './index.css'; import DatasetteVega from './DatasetteVega'; +const matchesCurrentHostAndPath = (url) => { + // Given a url, is it the same host and path as current page? + // (ignores querystring and fragment hash) + url = url.split('#')[0]; + // Only activate if link is to current page (presumably with different querystring) + var currentHostAndPath = window.location.hostname; + if (window.location.port !== '') { + currentHostAndPath += ':' + window.location.port; + } + currentHostAndPath += window.location.pathname; + // Ignore http/s due to https://github.com/simonw/datasette/issues/333 + var linkedHostAndPath = url.split('://')[1].split('?')[0]; + return currentHostAndPath === linkedHostAndPath; +}; + +// Update any forms with current page as action when fragment changes +const onFragmentChange = () => { + if (window.location.hash.length === 0) { + return; + } + Array.from(document.getElementsByTagName('form')).forEach(form => { + var action = form.action.split('#')[0]; + if (matchesCurrentHostAndPath(action)) { + form.action = action + window.location.hash; + } + }); +}; + document.addEventListener('DOMContentLoaded', () => { let visTool, jsonUrl; if (process.env.REACT_APP_STAGE === 'dev') { @@ -28,7 +56,7 @@ document.addEventListener('DOMContentLoaded', () => { jsonUrl += (jsonUrl.indexOf('?') > -1) ? '&' : '?'; jsonUrl += '_shape=array' ReactDOM.render( - , visTool + , visTool ); } }); @@ -55,17 +83,10 @@ window.addEventListener('click', function (ev) { var href = a.href; // Split off any existing #fragment href = href.split('#')[0]; - // Only activate if link is to current page (presumably with different querystring) - var currentHostAndPath = window.location.hostname; - if (window.location.port !== '') { - currentHostAndPath += ':' + window.location.port; - } - currentHostAndPath += window.location.pathname; - // Ignore http/s due to https://github.com/simonw/datasette/issues/333 var linkedHostQuery = href.split('?')[1]; - var linkedHostAndPath = href.split('://')[1].split('?')[0]; - if (currentHostAndPath === linkedHostAndPath) { + if (matchesCurrentHostAndPath(href)) { // Cancel click, navigate to this + fragment instead + var linkedHostAndPath = href.split('://')[1].split('?')[0]; if (linkedHostQuery) { linkedHostAndPath += '?' + linkedHostQuery; } From 9e412016a2cb477541e7f725df905556b43334f1 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 5 Jul 2018 20:18:37 -0700 Subject: [PATCH 20/40] Releasing 0.5 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 844f2fe..faed859 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from subprocess import check_output from wheel.bdist_wheel import bdist_wheel import os -VERSION = '0.4.1' +VERSION = '0.5' ROOT = os.path.dirname(os.path.abspath(__file__)) From 1326b070af9d352f0fb2b96280d38f4938459ee8 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 6 Jul 2018 11:03:42 -0700 Subject: [PATCH 21/40] Updated Travis encrypted password Followed instructions on https://github.com/travis-ci/travis-ci/issues/7327 - you need to run "travis encrypt", paste in the password, hit enter and THEN hit CTRL+D --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d739efb..0188e09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,8 @@ jobs: user: simonw distributions: bdist_wheel password: - secure: ikY6iF+/2AxFlwcun35iAEz7Wbrnp0Fp7b32Z7uob5RsGpIDCYYDa3T/qNhiyNsg5fecGp01rBf1F+dL357b0rWEeaoONtsDrTKFeBUB6bMnzNtr3QbHq/TeQ1f+Vn6FWpY9360Ihbz0pKPzWWYHJxOjXPM793rGWmPu7siCc4oQZOpwLK28GZeFP803wq/QG81hFRWR8IiMlqljkecEGhaM6ftxzizk1LBoTZCw3DdL2xDwzrLvF3Hg1jXX08pJm9WrLJNS6i+LRhiLv9IJ5KxRDwNHJhrvblRrZn0CKVbiWR/8985r4R7CXaeG68uJ505RcVoXYRsq9D7mAcYAB3U5AzU9TsPQlvUgwJlFpKOsZlCw+5AHFIYUXvlP2Qo5kfXUYC9KKdJwbWjcW7isUqWpzInGfnmRNMJGzmIm2q65ua0FNLV4vK1wl98O60HFSyhXJi84YmCAPgidJ22sEA6sHAdMEFiWglhap12zPc6i7trXKC8aLjW/3qGXhUq3BK8vQcbutGer+Q3z9UM7kthB76FtCUVc8eqljtdAcGt5kXoHIEPwGb50ikOxo/qpXsXVH36Z6plbELbayXb3ocUrMdhWP5+kfnrdLEmx+X4IpUauxGUqLGliYSQpfjp8XyA87ASCjsSqUEjr/4t4YztW5a3mWBYZ+IjjYXW6PQg= + secure: MzK2klzjn+gYgTyDwgjzxsCmOWXTl3r1W9kr3Y2GzdBXFz6PuZSQLpej4WHhm8AVqfqFh6SF+tnVAr8CqQO//NJ5CDow0lSJI8XeRlRiaNds9UX/JbpeBVtlN6WIDaT2ErzadodIY+NB8vcuF3QCqElUCJuepuLb12X+DpPO6I1fXCWX+W61T0Jr2CHQ98EK5AkVpppcuPgqPtl8N5Nt0/7iO0hjHcT2kdr+5nzUIeS1bXcZy+aAeuI1zbq7znh6sSzdu9cE+Uq26DZPNvPs4bhi35aBCS1fZUb43/5IiOyLHp6BBXypRATBV8cpyMJs38YeEqDZP4HFo0jIh7uD+1WShJ5raYcOHU6aotGYTJmp9+9hwQqnXZMDAx21bdNRpptAYJjO3EGU1K8HwLGBu3Kx0KfhIk8yXk7/nazADkiNh8mvnCORwd4h656UhCG0D4Nb+OUYC+iyp/LD4C7l+sFX3/ObQ6G6J0I+QL/l4ElsaNlhOmdtrpoV/dXbaN7MLKDl9OkoY7NiIGC91DIqXjelNRD1bsNJRWqODDEq0uod4LOZR42+814+yv066USQguKIG/UwKkR0aTJ6u0Ul6ngk/tvCStGg2tNaIi2dLok911DL636+kWaf5UIQGANOPMeVtW18nMV2tudti5mhyrpX7LNuayTLk0piMDsQdvQ= + on: branch: master tags: true From dfbf48356ae7accc385012f951643ea6cfb6393b Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 6 Jul 2018 11:04:07 -0700 Subject: [PATCH 22/40] Removed rogue newline --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0188e09..47ea811 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,6 @@ jobs: distributions: bdist_wheel password: secure: MzK2klzjn+gYgTyDwgjzxsCmOWXTl3r1W9kr3Y2GzdBXFz6PuZSQLpej4WHhm8AVqfqFh6SF+tnVAr8CqQO//NJ5CDow0lSJI8XeRlRiaNds9UX/JbpeBVtlN6WIDaT2ErzadodIY+NB8vcuF3QCqElUCJuepuLb12X+DpPO6I1fXCWX+W61T0Jr2CHQ98EK5AkVpppcuPgqPtl8N5Nt0/7iO0hjHcT2kdr+5nzUIeS1bXcZy+aAeuI1zbq7znh6sSzdu9cE+Uq26DZPNvPs4bhi35aBCS1fZUb43/5IiOyLHp6BBXypRATBV8cpyMJs38YeEqDZP4HFo0jIh7uD+1WShJ5raYcOHU6aotGYTJmp9+9hwQqnXZMDAx21bdNRpptAYJjO3EGU1K8HwLGBu3Kx0KfhIk8yXk7/nazADkiNh8mvnCORwd4h656UhCG0D4Nb+OUYC+iyp/LD4C7l+sFX3/ObQ6G6J0I+QL/l4ElsaNlhOmdtrpoV/dXbaN7MLKDl9OkoY7NiIGC91DIqXjelNRD1bsNJRWqODDEq0uod4LOZR42+814+yv066USQguKIG/UwKkR0aTJ6u0Ul6ngk/tvCStGg2tNaIi2dLok911DL636+kWaf5UIQGANOPMeVtW18nMV2tudti5mhyrpX7LNuayTLk0piMDsQdvQ= - on: branch: master tags: true From 0f86e97d54ec94ba77d22e8d37765380c96c7039 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 6 Jul 2018 11:09:02 -0700 Subject: [PATCH 23/40] Added LICENSE file plus PyPI/license badge to README --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 3 + setup.py | 2 +- 3 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 616aeef..22701ba 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # datasette-vega +[![PyPI](https://img.shields.io/pypi/v/datasette-vega.svg)](https://pypi.org/project/datasette-vega/) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-vega/blob/master/LICENSE) + A [Datasette](https://github.com/simonw/datasette) plugin that provides tools for generating charts using [Vega](https://vega.github.io/). diff --git a/setup.py b/setup.py index faed859..f4e7ded 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from subprocess import check_output from wheel.bdist_wheel import bdist_wheel import os -VERSION = '0.5' +VERSION = '0.5.1' ROOT = os.path.dirname(os.path.abspath(__file__)) From 1d4c5b325486d37c42b1ccc299fc1e12cf5654af Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 6 Jul 2018 11:24:23 -0700 Subject: [PATCH 24/40] Another attempt at getting Travis PyPI working, refs #8 --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 47ea811..66d9a61 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,9 @@ jobs: user: simonw distributions: bdist_wheel password: - secure: MzK2klzjn+gYgTyDwgjzxsCmOWXTl3r1W9kr3Y2GzdBXFz6PuZSQLpej4WHhm8AVqfqFh6SF+tnVAr8CqQO//NJ5CDow0lSJI8XeRlRiaNds9UX/JbpeBVtlN6WIDaT2ErzadodIY+NB8vcuF3QCqElUCJuepuLb12X+DpPO6I1fXCWX+W61T0Jr2CHQ98EK5AkVpppcuPgqPtl8N5Nt0/7iO0hjHcT2kdr+5nzUIeS1bXcZy+aAeuI1zbq7znh6sSzdu9cE+Uq26DZPNvPs4bhi35aBCS1fZUb43/5IiOyLHp6BBXypRATBV8cpyMJs38YeEqDZP4HFo0jIh7uD+1WShJ5raYcOHU6aotGYTJmp9+9hwQqnXZMDAx21bdNRpptAYJjO3EGU1K8HwLGBu3Kx0KfhIk8yXk7/nazADkiNh8mvnCORwd4h656UhCG0D4Nb+OUYC+iyp/LD4C7l+sFX3/ObQ6G6J0I+QL/l4ElsaNlhOmdtrpoV/dXbaN7MLKDl9OkoY7NiIGC91DIqXjelNRD1bsNJRWqODDEq0uod4LOZR42+814+yv066USQguKIG/UwKkR0aTJ6u0Ul6ngk/tvCStGg2tNaIi2dLok911DL636+kWaf5UIQGANOPMeVtW18nMV2tudti5mhyrpX7LNuayTLk0piMDsQdvQ= + secure: dTKntPxKe9MXksoMT3RuCnFrxyI1eDmlKm6D0zWgiNcK1SHDgDzqc8/qV1iaBYaGetXaVzUFd2/Qp3wsSl/PTCKxN1XrHW5xapyqY0+S0qbNczAzRZkiWFK88I3VOg4KwxKbOYZUwya0mUH/PgH1NG9eRnF8cxYiFRo8YUc+7oAoP3jF7PMQxSazR2o9c3+pfhJT+x+edCUyX6qdXN+xos92ssRF3mAAMR2aKoa82i8fFyj10e+atjx39+93tVXbZqTULptDuZb1au0r9B40IDU2vhy7OEcht7W8CnqvZ93V2sJLBcibdiyDIe6aL+6OA3blaBlE2tiYPSijcSfGzCbzExRTbNE0wrc4MM169akGFmdA9HRYrLdvmzKykkopf9ep1cmpHQwdR/iepqtLryQZtSpjCtzunLWx47sY/wCY+CQsKzGNwhZYsC/mjg/XNZRHeIqv9gbPFOEmUA5JTZ/0r5rAtl5DPdyZN/M5WJ+UvGxIiaB/xileoVnivsfqvIm0Hx81rbMZOi9J4gDTYxvrq/pmarDI87wsQ/Sosp8UYsAz+9+aggYr4rdeQOxWVb9jU0qSma+ROw0whEqKZ9gar8PXXeRYVZd/MU5u2CYiTTqzqkGTX4sUeS/rtDVd3melGvOq6yY8REtvo9XP8S0kUtJjuSUV5KHjFB7ihjA= on: branch: master tags: true + repo: simonw/datasette-vega + From 8b650feca39c91c6befbe7d91e292479262b0664 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 6 Jul 2018 17:45:04 -0700 Subject: [PATCH 25/40] Preserve cache-busting URL prefix on js/css - closes #11 Also ensure we ship the .map files for better browser debugging. --- datasette_vega/__init__.py | 22 ++++++++++++++++++++-- setup.py | 9 +++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/datasette_vega/__init__.py b/datasette_vega/__init__.py index ea03f63..4173ee0 100644 --- a/datasette_vega/__init__.py +++ b/datasette_vega/__init__.py @@ -1,11 +1,29 @@ from datasette import hookimpl +import glob +import os + +cache = {} +static_dir = os.path.join( + os.path.dirname(__file__), 'static' +) + +def cached_filepaths_for_extension(extension): + pattern = os.path.join(static_dir, '*.{}'.format(extension)) + if pattern not in cache: + cache[pattern] = [ + "/-/static-plugins/datasette_vega/{}".format( + os.path.basename(g) + ) + for g in glob.glob(pattern) + ] + return cache[pattern] @hookimpl def extra_css_urls(): - return ["/-/static-plugins/datasette_vega/datasette-vega.css"] + return cached_filepaths_for_extension('css') @hookimpl def extra_js_urls(): - return ["/-/static-plugins/datasette_vega/datasette-vega.js"] + return cached_filepaths_for_extension('js') diff --git a/setup.py b/setup.py index f4e7ded..e2bb267 100644 --- a/setup.py +++ b/setup.py @@ -34,8 +34,8 @@ class BuildStatic(Command): check_output(['npm', 'install'], cwd=ROOT) check_output(['npm', 'run', 'build'], cwd=ROOT) check_output(['mkdir', '-p', 'datasette_vega/static'], cwd=ROOT) - check_output("mv build/static/js/*.js datasette_vega/static/datasette-vega.js", shell=True, cwd=ROOT) - check_output("mv build/static/css/*.css datasette_vega/static/datasette-vega.css", shell=True, cwd=ROOT) + check_output("mv build/static/js/* datasette_vega/static/", shell=True, cwd=ROOT) + check_output("mv build/static/css/* datasette_vega/static/", shell=True, cwd=ROOT) setup( @@ -55,8 +55,9 @@ setup( }, package_data={ 'datasette_vega': [ - 'static/datasette-vega.js', - 'static/datasette-vega.css', + 'static/*.js', + 'static/*.css', + 'static/*.map', ], }, cmdclass={ From 6d46cfd9710d7988ee487af148cdf255a7029c37 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 6 Jul 2018 17:47:28 -0700 Subject: [PATCH 26/40] Implemented tooltips, refs #10 Also switched to using React.createRef() --- src/DatasetteVega.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/DatasetteVega.js b/src/DatasetteVega.js index b2cf979..bb425c5 100644 --- a/src/DatasetteVega.js +++ b/src/DatasetteVega.js @@ -21,6 +21,8 @@ const unserialize = (s, prefix) => { return obj; }; +const escapeString = s => (s || '').replace(/"/g, '\\x22').replace(/'/g, '\\x27'); + class DatasetteVega extends Component { state = { show: false, @@ -33,6 +35,10 @@ class DatasetteVega extends Component { color_column: "", size_column: "" } + constructor(props) { + super(props); + this.chartRef = React.createRef(); + } markOptions = [ {"value": "bar", "name": "Bar"}, {"value": "line", "name": "Line"}, @@ -132,7 +138,8 @@ class DatasetteVega extends Component { const y_bin = !!/-bin$/.exec(this.state.y_type); let encoding = { x: {field: this.state.x_column, type: x_type, bin: x_bin}, - y: {field: this.state.y_column, type: y_type, bin: y_bin} + y: {field: this.state.y_column, type: y_type, bin: y_bin}, + tooltip: {field: "_tooltip_summary", type: "ordinal"}, } if (this.state.color_column) { encoding.color = {field: this.state.color_column, type: "nominal"}; @@ -144,11 +151,15 @@ class DatasetteVega extends Component { data: { url: this.jsonUrl() }, + transform: [{ + calculate: `'${escapeString(this.state.x_column)}: ' + datum['${escapeString(this.state.x_column)}'] + ', ${escapeString(this.state.y_column)}: ' + datum['${escapeString(this.state.y_column)}']`, + as: "_tooltip_summary" + }], mark: this.state.mark, encoding: encoding } if (spec.mark && spec.encoding.x.field && spec.encoding.y.field) { - vegaEmbed(this.chart, spec, {theme: 'quartz', tooltip: true}); + vegaEmbed(this.chartRef.current, spec, {theme: 'quartz', tooltip: true}); document.location.hash = '#' + this.serializeState(); this.props.onFragmentChange && this.props.onFragmentChange(); // Add to state so react debug tools can see it (for debugging): @@ -220,7 +231,7 @@ class DatasetteVega extends Component {
-
{ this.chart = c; }}>
+
: null ); From f4134b88ae71eda0ea2251210f75945a82516369 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 6 Jul 2018 17:48:12 -0700 Subject: [PATCH 27/40] Releasing 0.6 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e2bb267..a449532 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from subprocess import check_output from wheel.bdist_wheel import bdist_wheel import os -VERSION = '0.5.1' +VERSION = '0.6' ROOT = os.path.dirname(os.path.abspath(__file__)) From 4668dce287e1b033b3cf45048b6c9c41ffaf16fd Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sat, 7 Jul 2018 08:45:19 -0700 Subject: [PATCH 28/40] Another attempt to get PyPI deploy working - #8 This time I generated the secure field like this: $ travis encrypt -r simonw/datasette-vega Reading from stdin, press Ctrl+D when done PASTED-PASSWORD-HERE - then hit enter Please add the following to your .travis.yml file: secure: "n..." --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 66d9a61..237ee0e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,7 @@ jobs: user: simonw distributions: bdist_wheel password: - secure: dTKntPxKe9MXksoMT3RuCnFrxyI1eDmlKm6D0zWgiNcK1SHDgDzqc8/qV1iaBYaGetXaVzUFd2/Qp3wsSl/PTCKxN1XrHW5xapyqY0+S0qbNczAzRZkiWFK88I3VOg4KwxKbOYZUwya0mUH/PgH1NG9eRnF8cxYiFRo8YUc+7oAoP3jF7PMQxSazR2o9c3+pfhJT+x+edCUyX6qdXN+xos92ssRF3mAAMR2aKoa82i8fFyj10e+atjx39+93tVXbZqTULptDuZb1au0r9B40IDU2vhy7OEcht7W8CnqvZ93V2sJLBcibdiyDIe6aL+6OA3blaBlE2tiYPSijcSfGzCbzExRTbNE0wrc4MM169akGFmdA9HRYrLdvmzKykkopf9ep1cmpHQwdR/iepqtLryQZtSpjCtzunLWx47sY/wCY+CQsKzGNwhZYsC/mjg/XNZRHeIqv9gbPFOEmUA5JTZ/0r5rAtl5DPdyZN/M5WJ+UvGxIiaB/xileoVnivsfqvIm0Hx81rbMZOi9J4gDTYxvrq/pmarDI87wsQ/Sosp8UYsAz+9+aggYr4rdeQOxWVb9jU0qSma+ROw0whEqKZ9gar8PXXeRYVZd/MU5u2CYiTTqzqkGTX4sUeS/rtDVd3melGvOq6yY8REtvo9XP8S0kUtJjuSUV5KHjFB7ihjA= + secure: "n05R72PmilqomYlw25ssAJhmAtG8x/FMQsZVLfZc8tIjQ9DJOqm1GsmDzaBL2BoqyHWUW8ByuCjen+9Tr2Brx02P8YpyGihvdPcf7V7WLVHHWb6dtLDDnmtOqKd7JrbaufO5u7dLf+UzqueRqsTJl58ZgHM6/9QES8GfzQsf6gz+inO7tyg4Ik5imAPVAcmW8y7lSVXrEjamtqgN41+Z07G204hQCE8BdH8unmz6vbhSZ0UdYef6BoCsstby+fx9+l6z6xMzjxPvZkx7LK71ODBLhtLdU+fj0uAZwHNzneV9Zkc+OpINjmc/c1vntZQMdGVqzvbM3QLq8aLjU3c7VhwRNKuhw5I7G60mZ6dg10Mt/zSXKRwrxxPRgReCV2KwzEhT9jmxbFYhkKHNGnlxTwkUYuNS2okFBMC/UQm+VxAqE/pA0aHFAZtsfo+S13ZlqJ5ZYMENHRVL/x8P+v57eQArgZfizsWkDhpV9coKyJXTveOYJ1M7B7CAYnYe/NqmIzItFk3s1wpgXGqITQ9b4QmvyaeQI0m6G0SJqazFWQOnNaF5Oacp+aoNfdGrqUDpTgq6AeYlDmlOgOqV0/2Z4OZmUlN45CDLUyNMFMGFdqjcNJqZBiskZOjbzxqWboxmDwlIneIM6UuwQ5k58KgUXB9iIDPRLDhCJu9roqi8Et0=" on: branch: master tags: true From ff0ab28c26b4dd3c8a7d61d129b882ff8c4d88f8 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 9 Jul 2018 19:37:15 -0700 Subject: [PATCH 29/40] Include size/color in tooltip if specified, refs #10 --- src/DatasetteVega.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/DatasetteVega.js b/src/DatasetteVega.js index bb425c5..b617a01 100644 --- a/src/DatasetteVega.js +++ b/src/DatasetteVega.js @@ -152,7 +152,12 @@ class DatasetteVega extends Component { url: this.jsonUrl() }, transform: [{ - calculate: `'${escapeString(this.state.x_column)}: ' + datum['${escapeString(this.state.x_column)}'] + ', ${escapeString(this.state.y_column)}: ' + datum['${escapeString(this.state.y_column)}']`, + calculate: ` + '${escapeString(this.state.x_column)}: ' + datum['${escapeString(this.state.x_column)}'] + + ', ${escapeString(this.state.y_column)}: ' + datum['${escapeString(this.state.y_column)}'] + + (${!!this.state.color_column} ? ', ${escapeString(this.state.color_column)}: ' + datum['${escapeString(this.state.color_column)}'] : '') + + (${!!this.state.size_column} ? ', ${escapeString(this.state.size_column)}: ' + datum['${escapeString(this.state.size_column)}'] : '') + `, as: "_tooltip_summary" }], mark: this.state.mark, From 17645264cd32a7ec7ed10907adf8c442518283b2 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 9 Jul 2018 19:45:05 -0700 Subject: [PATCH 30/40] Release version 0.6.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a449532..ad467a5 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from subprocess import check_output from wheel.bdist_wheel import bdist_wheel import os -VERSION = '0.6' +VERSION = '0.6.1' ROOT = os.path.dirname(os.path.abspath(__file__)) From bad9715ae5d52f51e97ccfb6a159940bed01ee1e Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 9 Jul 2018 20:02:01 -0700 Subject: [PATCH 31/40] Another attempt at setting PyPI password secret This time using recipe from https://github.com/travis-ci/travis-ci/issues/8339#issuecomment-327633364 echo -n "" | travis encrypt --add deploy.password -r simonw/datasette-vega --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 237ee0e..953ac19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,9 +31,8 @@ jobs: user: simonw distributions: bdist_wheel password: - secure: "n05R72PmilqomYlw25ssAJhmAtG8x/FMQsZVLfZc8tIjQ9DJOqm1GsmDzaBL2BoqyHWUW8ByuCjen+9Tr2Brx02P8YpyGihvdPcf7V7WLVHHWb6dtLDDnmtOqKd7JrbaufO5u7dLf+UzqueRqsTJl58ZgHM6/9QES8GfzQsf6gz+inO7tyg4Ik5imAPVAcmW8y7lSVXrEjamtqgN41+Z07G204hQCE8BdH8unmz6vbhSZ0UdYef6BoCsstby+fx9+l6z6xMzjxPvZkx7LK71ODBLhtLdU+fj0uAZwHNzneV9Zkc+OpINjmc/c1vntZQMdGVqzvbM3QLq8aLjU3c7VhwRNKuhw5I7G60mZ6dg10Mt/zSXKRwrxxPRgReCV2KwzEhT9jmxbFYhkKHNGnlxTwkUYuNS2okFBMC/UQm+VxAqE/pA0aHFAZtsfo+S13ZlqJ5ZYMENHRVL/x8P+v57eQArgZfizsWkDhpV9coKyJXTveOYJ1M7B7CAYnYe/NqmIzItFk3s1wpgXGqITQ9b4QmvyaeQI0m6G0SJqazFWQOnNaF5Oacp+aoNfdGrqUDpTgq6AeYlDmlOgOqV0/2Z4OZmUlN45CDLUyNMFMGFdqjcNJqZBiskZOjbzxqWboxmDwlIneIM6UuwQ5k58KgUXB9iIDPRLDhCJu9roqi8Et0=" + secure: Sl2jbgIhnr2IMHh3Hqg4yO0UsAOS/GYyJf3KdqfCHRHVTiHZaIKqMzcy/ErNzj11vqZcKHDdKEXWtqRLhKL6ZUlHxDeFa3wrfIe8QXwCnn1gmQddvl06ydr2WXjUKyL6f+q6pfON5O6R40NUqXxDr78YIE+uOu4FFJLODfSNRZxf6BP0qLU2wiDf86wxJf95kG3DPGNx2JRGQJqI2jSY30BL0QUCBpwHfF1wpzEp7+nX1vCnEEz2LsP8xGPDzFwNuj8px5HDjK6rRw9M8LP6CKsa65IK2pFwrOmLF0DnperwmgKBzF+atke/NQUlfHAaLjVKvA2w4Ff2xuH8v5ZGGg/gTi283CfFNhWHs2TGAZevAmsrRFKDhG6lTIUhCMSqcleL0moHZXoW0M5Xuwe3qQgJ8jw7OFJTJ3iGRzQnThp70F4XiHLeu+u0gvJOaNmG/xA/fVf6fFZbvCciIaqMrrdC0s/RLmsSOAMz8I767Pnby55Srlk8aYjlKma8lX37UlwYiF+xKgmHuXNtX0x1eQtFedFSgGLpXSsI6MyFcMbghmaTIPS6if8UfX1notbk6lb++nZiZ3ci4/Htl0t4zGBc1WAuBIlfYeS67JuiDXORP/XJRf+k6I4qsZJfInX5R1Nl2JiCNvc51o5fERu3RQpt8rob/46mOZx8UMcH9JY= on: branch: master tags: true repo: simonw/datasette-vega - From 5cc4e521ea163fe0e93b7345e9e207c411efe46b Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 9 Jul 2018 20:25:32 -0700 Subject: [PATCH 32/40] Let's try using PYPI_PASSWORD as a secret setting instead, refs #10 --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 953ac19..200d19f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,8 +30,7 @@ jobs: - provider: pypi user: simonw distributions: bdist_wheel - password: - secure: Sl2jbgIhnr2IMHh3Hqg4yO0UsAOS/GYyJf3KdqfCHRHVTiHZaIKqMzcy/ErNzj11vqZcKHDdKEXWtqRLhKL6ZUlHxDeFa3wrfIe8QXwCnn1gmQddvl06ydr2WXjUKyL6f+q6pfON5O6R40NUqXxDr78YIE+uOu4FFJLODfSNRZxf6BP0qLU2wiDf86wxJf95kG3DPGNx2JRGQJqI2jSY30BL0QUCBpwHfF1wpzEp7+nX1vCnEEz2LsP8xGPDzFwNuj8px5HDjK6rRw9M8LP6CKsa65IK2pFwrOmLF0DnperwmgKBzF+atke/NQUlfHAaLjVKvA2w4Ff2xuH8v5ZGGg/gTi283CfFNhWHs2TGAZevAmsrRFKDhG6lTIUhCMSqcleL0moHZXoW0M5Xuwe3qQgJ8jw7OFJTJ3iGRzQnThp70F4XiHLeu+u0gvJOaNmG/xA/fVf6fFZbvCciIaqMrrdC0s/RLmsSOAMz8I767Pnby55Srlk8aYjlKma8lX37UlwYiF+xKgmHuXNtX0x1eQtFedFSgGLpXSsI6MyFcMbghmaTIPS6if8UfX1notbk6lb++nZiZ3ci4/Htl0t4zGBc1WAuBIlfYeS67JuiDXORP/XJRf+k6I4qsZJfInX5R1Nl2JiCNvc51o5fERu3RQpt8rob/46mOZx8UMcH9JY= + password: ${PYPI_PASSWORD} on: branch: master tags: true From b05dd7792eb624d702a12dc3ebb91308016ac25d Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sat, 28 Jul 2018 17:13:14 -0700 Subject: [PATCH 33/40] Fix leaking CSS bug (fix #15) and release 0.6.2 --- public/index.html | 2 +- setup.py | 2 +- src/index.css | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/index.html b/public/index.html index 0b14e70..2df60a7 100644 --- a/public/index.html +++ b/public/index.html @@ -7,7 +7,7 @@ Datasette Vega - +

Datasette Vega

Enter the URL of the JSON version of any Datasette table:

diff --git a/setup.py b/setup.py index ad467a5..ee5e7e1 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from subprocess import check_output from wheel.bdist_wheel import bdist_wheel import os -VERSION = '0.6.1' +VERSION = '0.6.2' ROOT = os.path.dirname(os.path.abspath(__file__)) diff --git a/src/index.css b/src/index.css index cbabe2a..983aee4 100644 --- a/src/index.css +++ b/src/index.css @@ -1,14 +1,14 @@ -body { +body.datasette-vega-demo { margin: 0; padding: 1em; font-family: sans-serif; } -form div { +body.datasette-vega-demo form div { margin-bottom: 0.5em } -label { +body.datasette-vega-demo label { padding-right: 0.5em; } From 1db45bd8f1c3c17f8b05aba7159542bcaffffad8 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 16 Jun 2020 12:11:25 -0700 Subject: [PATCH 34/40] Removed link to the now-departed Datasette Publish site --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 22701ba..b634a87 100644 --- a/README.md +++ b/README.md @@ -20,5 +20,3 @@ If you are publishing data using the `datasette publish` command, you can include this plugin like so: datasette publish now mydatabase.db --install=datasette-vega - -If you don't want to install any software at all, you can publish your CSV files with Datasette and activate this plugin using the web application at https://publish.datasettes.com/ From 8b54a0f175679b6ad634a6a107c779223bfb1885 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 3 Sep 2020 19:34:27 -0700 Subject: [PATCH 35/40] Added a test --- setup.py | 3 ++- tests/test_vega.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 tests/test_vega.py diff --git a/setup.py b/setup.py index ee5e7e1..149a92e 100644 --- a/setup.py +++ b/setup.py @@ -64,5 +64,6 @@ setup( 'bdist_wheel': BdistWheelWithBuildStatic, 'build_static': BuildStatic, }, - install_requires=['datasette'] + install_requires=['datasette'], + extras_require={"test": ["pytest", "pytest-asyncio", "httpx"]} ) diff --git a/tests/test_vega.py b/tests/test_vega.py new file mode 100644 index 0000000..72aef02 --- /dev/null +++ b/tests/test_vega.py @@ -0,0 +1,13 @@ +from datasette.app import Datasette +import pytest +import httpx + + +@pytest.mark.asyncio +async def test_plugin_is_installed(): + app = Datasette([], memory=True).app() + async with httpx.AsyncClient(app=app) as client: + response = await client.get("http://localhost/-/plugins.json") + assert response.status_code == 200 + installed_plugins = {p["name"] for p in response.json()} + assert "datasette-vega" in installed_plugins From 04b904add3ec1dc8b4cadf531204d38d209085fa Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 3 Sep 2020 19:34:40 -0700 Subject: [PATCH 36/40] Only inject assets on table page --- datasette_vega/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/datasette_vega/__init__.py b/datasette_vega/__init__.py index 4173ee0..585e26c 100644 --- a/datasette_vega/__init__.py +++ b/datasette_vega/__init__.py @@ -20,10 +20,12 @@ def cached_filepaths_for_extension(extension): @hookimpl -def extra_css_urls(): - return cached_filepaths_for_extension('css') +def extra_css_urls(view_name): + if view_name == "table": + return cached_filepaths_for_extension('css') @hookimpl -def extra_js_urls(): - return cached_filepaths_for_extension('js') +def extra_js_urls(view_name): + if view_name == "table": + return cached_filepaths_for_extension('js') From 033ee204bb8df067b1333d5a06acf2af79de065e Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 3 Sep 2020 19:34:52 -0700 Subject: [PATCH 37/40] Applied Black --- datasette_vega/__init__.py | 15 ++++------ setup.py | 59 ++++++++++++++++++++------------------ 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/datasette_vega/__init__.py b/datasette_vega/__init__.py index 585e26c..ac80b52 100644 --- a/datasette_vega/__init__.py +++ b/datasette_vega/__init__.py @@ -3,17 +3,14 @@ import glob import os cache = {} -static_dir = os.path.join( - os.path.dirname(__file__), 'static' -) +static_dir = os.path.join(os.path.dirname(__file__), "static") + def cached_filepaths_for_extension(extension): - pattern = os.path.join(static_dir, '*.{}'.format(extension)) + pattern = os.path.join(static_dir, "*.{}".format(extension)) if pattern not in cache: cache[pattern] = [ - "/-/static-plugins/datasette_vega/{}".format( - os.path.basename(g) - ) + "/-/static-plugins/datasette_vega/{}".format(os.path.basename(g)) for g in glob.glob(pattern) ] return cache[pattern] @@ -22,10 +19,10 @@ def cached_filepaths_for_extension(extension): @hookimpl def extra_css_urls(view_name): if view_name == "table": - return cached_filepaths_for_extension('css') + return cached_filepaths_for_extension("css") @hookimpl def extra_js_urls(view_name): if view_name == "table": - return cached_filepaths_for_extension('js') + return cached_filepaths_for_extension("js") diff --git a/setup.py b/setup.py index 149a92e..a2753f6 100644 --- a/setup.py +++ b/setup.py @@ -4,20 +4,21 @@ from subprocess import check_output from wheel.bdist_wheel import bdist_wheel import os -VERSION = '0.6.2' +VERSION = "0.6.2" ROOT = os.path.dirname(os.path.abspath(__file__)) def get_long_description(): - with open(os.path.join( - os.path.dirname(os.path.abspath(__file__)), 'README.md' - ), encoding='utf8') as fp: + with open( + os.path.join(os.path.dirname(os.path.abspath(__file__)), "README.md"), + encoding="utf8", + ) as fp: return fp.read() class BdistWheelWithBuildStatic(bdist_wheel): def run(self): - self.run_command('build_static') + self.run_command("build_static") return bdist_wheel.run(self) @@ -31,39 +32,41 @@ class BuildStatic(Command): pass def run(self): - check_output(['npm', 'install'], cwd=ROOT) - check_output(['npm', 'run', 'build'], cwd=ROOT) - check_output(['mkdir', '-p', 'datasette_vega/static'], cwd=ROOT) - check_output("mv build/static/js/* datasette_vega/static/", shell=True, cwd=ROOT) - check_output("mv build/static/css/* datasette_vega/static/", shell=True, cwd=ROOT) + check_output(["npm", "install"], cwd=ROOT) + check_output(["npm", "run", "build"], cwd=ROOT) + check_output(["mkdir", "-p", "datasette_vega/static"], cwd=ROOT) + check_output( + "mv build/static/js/* datasette_vega/static/", shell=True, cwd=ROOT + ) + check_output( + "mv build/static/css/* datasette_vega/static/", shell=True, cwd=ROOT + ) setup( - name='datasette-vega', - description='A Datasette plugin that provides tools for generating charts using Vega', + name="datasette-vega", + description="A Datasette plugin that provides tools for generating charts using Vega", long_description=get_long_description(), - long_description_content_type='text/markdown', - author='Simon Willison', - url='https://github.com/simonw/datasette-vega', - license='Apache License, Version 2.0', + long_description_content_type="text/markdown", + author="Simon Willison", + url="https://github.com/simonw/datasette-vega", + license="Apache License, Version 2.0", version=VERSION, - packages=['datasette_vega'], + packages=["datasette_vega"], entry_points={ - 'datasette': [ - 'vega = datasette_vega' - ], + "datasette": ["vega = datasette_vega"], }, package_data={ - 'datasette_vega': [ - 'static/*.js', - 'static/*.css', - 'static/*.map', + "datasette_vega": [ + "static/*.js", + "static/*.css", + "static/*.map", ], }, cmdclass={ - 'bdist_wheel': BdistWheelWithBuildStatic, - 'build_static': BuildStatic, + "bdist_wheel": BdistWheelWithBuildStatic, + "build_static": BuildStatic, }, - install_requires=['datasette'], - extras_require={"test": ["pytest", "pytest-asyncio", "httpx"]} + install_requires=["datasette"], + extras_require={"test": ["pytest", "pytest-asyncio", "httpx"]}, ) From 8cea6b438831b74367393af1cd29e9eb153ba00f Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 3 Sep 2020 19:35:40 -0700 Subject: [PATCH 38/40] Release 0.7a0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a2753f6..8dc8722 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from subprocess import check_output from wheel.bdist_wheel import bdist_wheel import os -VERSION = "0.6.2" +VERSION = "0.7a0" ROOT = os.path.dirname(os.path.abspath(__file__)) From 00de059ab1ef77394ba9f9547abfacf966c479c4 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 10 Dec 2021 14:20:41 -0800 Subject: [PATCH 39/40] Upgrade to latest Vega/React, refs #29 --- package.json | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index fdab351..969c2a2 100644 --- a/package.json +++ b/package.json @@ -3,17 +3,28 @@ "version": "0.1.0", "private": true, "dependencies": { - "react": "^16.4.1", - "react-dom": "^16.4.1", - "react-scripts": "1.1.4", - "source-map-explorer": "^1.5.0", - "vega-embed": "^3.15.0", - "vega-lib": "^4.0.0-rc.3" + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-scripts": "^4.0.3", + "vega-embed": "^6.20.5", + "vega-lib": "^4.4.0" }, "scripts": { "start": "REACT_APP_STAGE=dev react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] } } From 6fe66c4c9d0dba0a6eb636583914d2ed7d3d72a9 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 2 Nov 2025 10:26:58 -0800 Subject: [PATCH 40/40] Fix for httpx.AsyncClient(app=app) Refs https://github.com/simonw/datasette/issues/2549 --- tests/test_vega.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/test_vega.py b/tests/test_vega.py index 72aef02..a77cd7f 100644 --- a/tests/test_vega.py +++ b/tests/test_vega.py @@ -1,13 +1,11 @@ -from datasette.app import Datasette import pytest -import httpx +from datasette.app import Datasette @pytest.mark.asyncio async def test_plugin_is_installed(): - app = Datasette([], memory=True).app() - async with httpx.AsyncClient(app=app) as client: - response = await client.get("http://localhost/-/plugins.json") - assert response.status_code == 200 - installed_plugins = {p["name"] for p in response.json()} - assert "datasette-vega" in installed_plugins + ds = Datasette([], memory=True) + response = await ds.client.get("/-/plugins.json") + assert response.status_code == 200 + installed_plugins = {p["name"] for p in response.json()} + assert "datasette-vega" in installed_plugins