Hi all, I have my IDE setup (Apache Netbeans) for all kind of projects including D3 vanilla JavaScript. What are the steps to get from a (Observable) JavaScript notebook to a vanilla JavaScript index.html?
Have you looked at the āDownloading and embedding notebooksā tutorial?
Yes, but the extra runtime environment is for me and I think for many others kind of a barrier towards a vanilla JavaScript index.html
The barrier is there because the way notebooks run is rather different from the way a typical āvanilla JSā script runs.
If the code in your notebook is very simple and doesnāt rely much on the reactive functionality of the runtime, you might be able to get away with just copying notebook cells into similarly-named functions (possibly with extra inputs) and adapting the rest of the code (possibly importing the standard library), but this is still going to be a case-by-case sort of process.
One way to check the complexity of this task is to put your notebook in the visualizer and see if you can tell whether copying the cells into functions in a vanilla JS script will lead to something with the same execution order.
If you have a specific notebook in mind I could take a quick look and try to give some suggestions on how you might begin converting it back to vanilla JS.
If you have a specific notebook in mind I could take a quick look and try to give some suggestions on how you might begin converting it back to vanilla JS.
Yes, a good starting point would be my notebook with Mike Bostockās D3 Circle Packing example.
Youāre lucky, this particular example is really simple. In particular, it really only has one main visualization cell, and one data fetching cell and the rest are just helper functions / variables.
Here is a port to a āblockā. To get to this, I worked roughly from bottom to top.
First, I added a script tags to index.html which import the d3 and d3-require libraries as well as the Observable standard library:
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/d3-require@1"></script>
<script src="https://cdn.jsdelivr.net/npm/@observablehq/stdlib"></script>
Next I added an svg element to serve as a target for the visualization:
<svg id="packSVG" width=932 height=932 viewBox="0,0,932,932"></svg>
Then I added another script tag, and copied each of the notebook cells below data
as functions / variables (making sure that the variables are defined in an order that makes sense):
<script>
const color = d3.scaleSequential(d3.interpolateMagma).domain([8, 0]);
const format = d3.format(",d");
const width = 932;
const height = width;
const pack = data => d3.pack()
.size([width - 2, height - 2])
.padding(3)
(d3.hierarchy(data)
.sum(d => d.size)
.sort((a, b) => b.value - a.value));
</script>
The cell data
fetches a file asynchronously, so weāll have to wrap the rest of the code inside a promise. Put the following inside the above script tag.
d3.require('@observablehq/flare')
.then(data => {
// add code here
});
I then took the code from the main cell and copied it into the function inside then
above, so that it will run after the data is successfully fetched.
The only changes I made were to change the main d3 selection from DOM.svg
to a selector which picks out the target SVG element we added above, and then to comment out some references to DOM.uid
in the standard library which werenāt working. The latter causes the text labels to spill outside the circles. Iāll see if I can figure out whatās going on with that later.
(By the way, it doesnāt look like youāve changed anything from Mike Bostockās notebook. Might you possibly have forgotten to share your edits?)
Will this jsfiddle example help?
That was exactly what I was looking for. The link to the Observable Standard Library is gold. Though it looks that it did not work (with DOM.uid
). Nevertheless that is now a kind of blueprint for me for other notebooks.
Yes please, let see how the Observable standard library should work in your vanilla JavaScript example.
I only forked it. My aim was to transform a Notebook into a vanilla JavaScript which you did perfectly. Thank you.
Nope. I was looking for Bryanās solution. But thanks.
OK, I figured out how to import the standard library. I should have just read the docs more carefully.
The key thing to do was to add this line, which creates a new āLibraryā object and then extracts DOM
from it:
const {DOM} = new observablehq.Library;
Hereās the link again https://blockbuilder.org/bryangingechen/ffd619bb5889d146fe6c5d581d3ea00e
edit: Since stdlib
exports (a slightly wrapped version of) d3-require
, you can even get rid of the script tag which fetches d3-require
above (although the syntax to use require
is slightly changed). See the latest version, which includes the following:
const {DOM, require} = new observablehq.Library;
require()('@observablehq/flare') // need the extra () because require in the stdlib wraps d3-require in a weird way
.then(data => {
Many thanks Bryan, that will be my template now.
Glad I was able to help. I still believe that using the runtime will be the more robust solution, particularly when it comes to notebooks with generator cells, those with a lot of interactivity across cells or those which import other notebooks.
Note also that the v1 notebook JS code (what you see when you click ādownload codeā from the menu in the top right, and what is contained with a basic index.html in the download tarball
file) is not particularly hard to manipulate in external text editors.
This is pretty fabulous stuff, Bryan, thank you!
Out of curiosity (and just to confirm that Iāve got you), when you say that āusing the runtime will be the more robust solutionā compared to writing everything in a ātraditionalā manner, as demonstrated by blockbuilder, do you mean that a more robust solution would be to install Observableās customized JS runtime (software stack - similar but distinct from node.js) / spin up a server environment that exposes this environment (for those of us with crazy work computers that donāt allow us to install any sort of software) for interpreting the code that is downloadable via a notebookās ...
menu?
Observableās runtime doesnāt require any server resources, it can be run entirely in the browser. Hereās an exercise that might be enlightening. Try downloading the tarball of any of your favorite notebooks and copying its contents (you really just need the .js file and index.html) into an empty folder.
If you open that index.html file directly in your browser, youāre likely to get CORS errors, so start up a simple HTTP webserver (like the python one described in this post) and then open index.html using that server and it should work.
Note that all this local server is doing is just passing the HTML and JS files to the browser. Thereās nothing else necessary on the server side. If you have webhosting of your own, you could also just copy the .js and index.html files into a folder there and see that your notebook works without any fancy server environment.
In fact, you can copy your files to a github āgistā and create a fully-functioning block from an Observable notebook this way as well:
My jaw is on the floor. I canāt start an HTTP webserver on my work computer. Iāve also had to abandon trying to share around to colleagues certain visualizations I was playing with because they required an HTTP server to load (and I didnāt realize it was CORS). Although Iāve discovered hosting via GitHub as a work-around, I hadnāt put all these pieces together until now. I thought that depending on the runtime meant creating some sort of designated server-side environment. Thanks for this clarification! Amazing.
What when I want read from a local file flare.json?
There are multiple things you might mean here.
If you have a single local file that you want to be able to read from a local copy of the notebook, you could use d3.json (replace the require
call with this):
d3.json("path/to/file.json").then(data => {
(Note that this will probably give you CORS errors unless you open the page in a local web server)
Alternatively, you might want a button that lets you select a file from your computer like the examples here: https://beta.observablehq.com/@mbostock/reading-local-files
Then you might add something like this to the HTML body:
<input id="inputJSON" type="file" accept="application/json">
and replace the require
call with this:
const inputElement = document.querySelector('#inputJSON');
inputElement.addEventListener('change', () => {
const url = URL.createObjectURL(inputElement.files[0]);
d3.json(url).then(data => {
(this wraps the original code in an additional function; hereās a link to a fork of the above block with this change).
This earlier post of mine is also related to changing out the flare dataset for another one (in a different notebook) and might be helpful if youāre looking to get the format right.
Thanks Bryan for getting back. Look at the following 3 Vanilla JavaScript variants. Iāve chosen Mike Bostockās D3 Zoomable Sunburst example for our 3 variants (w/ d3.json(), button, require()):
- D3 v5 Zoomable Sunburst w/ d3.json() (not working)
- D3 v5 Zoomable Sunburst w/ Button (not working)
- D3 v5 Zoomable Sunburst w/ require() (works)
Iāve tried it with a local file flare.json from my computer as well as remote files from gist.githubusercontent.com but all fail.
- d3.json(āhttps://gist.githubusercontent.com/tasqon/0dcc5c87a54282a2d732ad04d19b4e64/raw/7a185428a3c234bd997a797b524ed3a1dc01fc82/flare.jsonā).then(data => {ā¦
- d3.json(āhttps://raw.githubusercontent.com/d3/d3-hierarchy/v1.1.8/test/data/flare.jsonā).then(data => {ā¦
- d3.json(āflare.jsonā).then(data => {ā¦
Actually I can see that in all cases the data is loaded. But those file based versions whether local or remote do not work. When I compare the data from the require() vs. d3.json() variants they look the same. Though there is a tiny difference. Look below:
What is that ?
These flare.json files donāt quite have the right format to be viewed with the code. At the bottom level, the entries look like this:
{"name": "AgglomerativeCluster", "value": 3938},
whereas they should look like this:
{"name": "AgglomerativeCluster", "size": 3938},
To fix this, you can either substitute āsizeā for āvalueā in the json files, or substitute āvalueā for āsizeā in the JS code.
Arrrrrrrrrrgh. Havenāt seen that one. You are right. That did it. Thanks again.