RevealJS in Framework

This presentation was written using RevealJS in the context of Observable Framework.

The slides in this presentation are optimized for fullscreen and you can press your "F" key to view it in that mode.

You can also use your arrow keys or buttons to the lower right to navigate between slides.

What and why?

RevealJS is a Javascript framework for creating slideshows within HTML that live on the web.

Observable Framework is a Javascript framework for creating data apps that live within "static sites" on the web.

So, if you'd like to a slide show illustrating ideas with data, then this seems like the perfect pairing!

Examples

Reveal takes care of the presentation functionality allowing us to navigate between slides; Observable Framework implements data visualizations within slides.

This column of slides shows a few examples.

Hoverable dot plot

Here's an example lifted right out of the Observable Plot documentation that shows a scatter plot of olympic athletes by height and weight. Note that you can hover over the plot to get information on the athletes.

Responsive sine graph

Here's the graph of generated by Observable.Plot that's responsive to a slider specifying the value of :

Letters sorted alphabetically

Here's a bar chart illustrating the frequency of letters in the English alphabet. The bar chart updates dynamically when you navigate down to the next slide.

Letters sorted by ascending frequency

Letters sorted by descending frequency

Basic setup

The rest of this presentation discusses some of the details on how this page was created. It assumes that you know some things about Framework but maybe less about RevealJS.

Once you've got your Framework project set up, the first step is to install RevealJS within your Framework project via:

npm install reveal.js

Basic setup (cont)

You can then include RevealJS via a Javascript code block like so:

import Reveal from 'reveal.js/dist/reveal.esm.js';
const reveal = new Reveal({embedded: true});
reveal.initialize();

Basic setup comments

That's pretty much the minimum setup. The embedded option is essential in the Framework context because, otherwise, RevealJS will target the first node classed .reveal within the body. Observable Framework, though, produces no body and its first div is not classed .reveal - so, we're out of luck.

Markup

RevealJS will look for a div classed reveal that contains another div classed slides so we need to add that. All markup/markdown defining the slides goes within the slides div; each slide is delineated by a section.

<div class="reveal">
<div class="slides">
  <section>
    Markup/markdown defining slide 1.
  </section>
  ...More markup/markdown defining slides
</div>
</div>

Styling

This presentation uses a stylesheet that starts by importing several other stylesheets:

@import url("observablehq:default.css");
@import url("observablehq:theme-air.css");
@import 'reveal.js/dist/reset.css';
@import 'reveal.js/dist/reveal.css';
@import 'reveal.js/dist/theme/simple.css';

The two Observable default style sheet ensures that Observable type things (like inputs, tables, and charts) look right. The next two are essential for RevealJS to work properly. Finally, there are a number of choices for the Reveal theme.

Styling (continued)

A bit more styling is required to get the presentation to work properly. At a minimum, the div.reveal element should be given a width and height; perhaps something like so:

div.reveal {
  width: 90%;
  height: 500px;
}

Further Styling

I've got quite a few more styles in my stylesheet; here are just a few. Note that these are listed in order of increasing specificity, which is sometimes required to override one of the other stylesheets.

section {
  text-align: left;
}
.reveal p {
  font-size: 1.7rem;
  max-width: 800px;
}
.reveal h1.override {
  font-size: 4rem;
}

Markdown or HTML?

I don't mind writing full HTML when using RevealJS because it gives you much more control. For example:

  • Sometimes, you need to add a class name to an item.
  • Framework adds anchors to the headings generates by Markdown so I use h2 and h3 tags directly.
  • Markdown can't generate section tags so you've got to write some HTML anyway

Markdown

Markdown is awfully convenient, though, when it works, and you can use it. The slides in this column were written with Markdown, modulo the comments on sections and headings.

Tables, code blocks, and block quotes all work just fine.

Using Observable

For the most part, using Observable tricks within a RevealJS presentation works as you'd expect.

One slightly tricky aspect is generally ensuring that sizes are all right and that things work well in a variety of devices

Code for the dot plot

The dot plot from a few slides back, for example, was generated with a Javascript code bock containing the following code:

Plot.dot(olympians, {
  x: "weight",
  y: "height",
  stroke: "sex",
  channels: {name: "name", sport: "sport"},
  tip: true
}).plot()

D3 Transitions

RevealJS and Observable (via D3) both provide nice tools for transitions. RevealJS generally handles transitioning from slide to slide.

It's also possible set up an absolutely positioned DIV that can be manipulated with D3 in response to slide changes. That process is a bit tricky, though, so I've added some comments on how I've set that up here.

D3 Transition setup - 1

To setup an dynamic image that transitions on a slide change event, you should set up a div element to hold it:

<div id="fixed2"></div>

That div should live inside the div classed "slides" but before the first section element.

D3 Transition setup - 2

Next, you need to append your dynamic image, which should probably be generated in a component. This is a logical place to set some styles as well; for example, the opacity should initially be zero.

import {make_it} from
  './components/make_d3_transition_element.js';
const transitioner = make_it();
d3.select("#fixed2")
  .style('width', '960px')
  .style('height', '200px')
  .style('position', 'absolute')
  .style('bottom', '200px')
  .style('opacity', 0)
  .append(() => transitioner)

D3 Transition setup - 3

Now, we want the div element that we just created to appear on some subset of our slides. We'll set the "data-class" attribute of those slides to fixed2. We also set a unique "data-id" attribute to identify the state of the slide within the slide class:

<section data-id="transition-left" data-class="fixed2">
  stuff
  <div style="height: 400px"></div>
</section>
<section data-id="transition-right" data-class="fixed2">
  more stuff
  <div style="height: 400px"></div>
</section>

The empty div is there to hold space for the absolutely positioned dynamic element.

D3 Transition setup - 4

Now, if reveal denotes the RevealJS instance, then we'll use its slidechanged method, noting the current and previous slides:

reveal.on('slidechanged', function(evt) {
  const slide_in = d3.select(evt.currentSlide);
  const slide_out = d3.select(evt.previousSlide);

  ...
}

D3 Transition setup - 5

Next (and still within the "slidechanged" event), if we slide onto one of the slides with "data-class" equal to "fixed2", then we should set it's opacity to 1.

  if(
    slide_in.attr('data-class') == "fixed2" &&
    slide_out.attr('data-class') != "fixed2"
  ) {
    d3.select('#fixed2')
      .transition()
      .duration(500)
      .style('opacity', 1)
  }

There should be a similar call to set the opacity back to zero whnen slide_out data-class is fixed2 but the slide_in data-class is no longer fixed2 .

D3 Transition setup - 6

Now, we check the "data-id" and transition the slide as appropriate.

  if(slide_in.attr('data-id') == "transition-left") {
    transitioner.update('left')
  }
  else if(slide_in.attr('data-id') == "transition-right") {
    transitioner.update('right')
  }

Of course, the details of the update method are entirely dependent on the code that generated the image in the first place.

D3 Transition result - left

Here's the result:

D3 Transition result - right

And here's the result after transition:

D3 Transition - final comment

Finally, it's worth adding a bit of code in the reveal.on('ready') block to set the opacity of the image to 1 if the user navigates directly to one of the slides in this block. You can set the state of the image appropriately, as well.

Comments