Jupyter Notebook导出幻灯片(基于Reveal.js)

前言

前一段时间在整理Python数据可视化分享的时候,为了简化幻灯片的制作,同时能够有助于提高对于代码等的展示效果,所以学习基于Jupyter Notebook和Reveal.js制作HTML幻灯片,其中涉及到一些定制化的设置,特意记录下来,也供后续提高参考。

Reveal.js介绍

基于HTML的幻灯片有多种,其中较为流行的一种就是Reveal.js 。Reveal.js是一个展示内容的框架,可以简单的理解为网页版的PPT,能够将Markdown等格式的文件转化为类似PPT的幻灯片。开发者甚至还提供了一个网站——Slides.com,帮助使用者在线制作幻灯片。

Reveal.js的功能非常强大,具体的使用方法我也只是接触了皮毛,更多可以参考其技术文档。

Jupyter Notebook转HTML幻灯片

根据我的调研,基于Reveal.js,将Jupyter Notebook转为HTML幻灯片大概可以分为三条技术路线:

  1. Jupyter Notebook安装RISE插件,直接以幻灯片的形式放映Notebook;
  2. 将Notebook导出为Markdown文件,然后使用Pandoc将md文件转为幻灯片;
  3. 直接将Notebook转换为html幻灯片。

这三种技术路线各有优劣:

  • 方法1展示起来很方便,且效果也不错;但是,RISE插件目前还不能再Jupyter Lab下使用,而且不方便移植播放;
  • 方法2可以对Markdown进行比较精细化的设置,效果美观;但是,Notebook导出md,交互式图表会变成静态图片;
  • 方法3导出也比较方便,但是效果一般,需要进一步调整样式。

根据我的需求,一番权衡之后,决定采用方法3。

基本方法

基本的转换方法,其实非常简单,因为Jupyter Lab/Notebook中的nbconvert有输出Reveal.js slides的功能

  • 采用GUI导出

    gui_nb_convert

  • 采用命令行,例如:

    1
    jupyter nbconvert --output-dir='../slides' 03_plotnine.ipynb --to slides  --template=my_slides.tpl --SlidesExport er.reveal_theme=solarized

    具体的参数,可以查看nbconvert的源代码和技术文档,其中对于我们定制化比较重要的参数是--template,与GUI相比,命令行显然能够提供更加精细的控制。

在使用默认参数的情况下,我们导出的幻灯片大概是这样子的:

original_slide_homepage

与我们平时做的PPT相比,显然谈不上美观,可能存在的不足包括:

  • 首页、内容页等不同样式没有区分;
  • 没有页眉、页脚、页码;
  • 丑,丑,还是丑……

样式调整

基本思路

前面导出的换灯片之所以存在不足就是因为,原生的导出只使用了Reveal.js基本的功能,所以我们要自己添加一些额外的功能。例如,我想要做的调整主要包括以下内容:

  • 区分幻灯片的首页和内容页;
  • 为幻灯片添加页眉、页脚;
  • 优化幻灯片段切换的效果

一番探索之后,发现Jupyter Notebook转HTML幻灯片主要涉及到以下对象:

  • nbconvert模块
  • .ipynb 文件
  • 转换模板
  • 幻灯片的css文件

第一个显然不是我们想动的,所以我们只能对2~4下手,具体如下。

修改.ipynb文件

修改.ipynb文件有两种方式:

  • 直接写原生的html内容,例如我们想添加javascript脚本、div元素或者css样式,可以直接将cell选择为Raw,然后直接填写相应内容

  • 修改Cell Metadata,在Jupyter Lab中,我们可以使用Property Inspector。

修改转换模板

  • 首先找到nbconvert自带的模板,从名字可以看出来应该是slides_reveal.tpl,复制重命名为my_slides.tpl;

    nbconvert_template

  • 查看模板的具体内容,可以知道模板采用的是jinja2语法,那么就可以根据需要在其中进行增删修改。

修改css文件

我建议修改的css包括两个来源:

  • Reveal.js预设的一些模板样式,在nbconvert中可以指定参数,例如

    1
    --SlidesExport er.reveal_theme=solarized

    这样就可以采用Reveal.js的Solarized样式;

  • nbconvert的默认模板添加了custom.css,所以我们可以创建custom.css,然后对默认样式进行补充或者覆盖。

大概的修改顺序也应该按照2->3->4的顺序进行:

  • 如果对转换的内容进行调整,首先修改Cell Metadata,如果是针对全局的,可以直接在模板中修改;
  • 根据上一步的调整,在转换模板中进行相应调整;
  • 根据HTML元素的调整或者样式修改的需要,在custom.css中修改。

幻灯片添加首页

在Jupyter Notebook中的Slide只有五种类型:

  • Slide
  • Sub-Slide
  • Fragment
  • Skip
  • Notes

因此,对是不是首页并没有区分,那么我们可以自己添加这个信息,首先将Cell MetaData修改为:

1
2
3
4
5
6
{
"homepage": true,
"slideshow": {
"slide_type": "slide"
}
}

nb_cell_metadata

然后,在my_slides.tpl中增加对homepage的类属性:

1
2
3
{%- if cell.metadata.get('homepage', False) -%}
<div class="homepage">
{%- endif -%}

在使用Flask、Hexo的过程多少用过一些jinja2,对照着模板原来的内容,可以大概猜到它的作用就是:

  • 默认cell不是homepage;
  • 如果homepage为真,则添加homepage的class属性。

hompage_tpl

这样,生成的HTML文件里就会有class="homepage"的幻灯片,即首页。我们在custom.css中修改首页的样式,例如将文字居中:

1
2
3
.homepage {
text-align: center;
}

这样就可以完成了添加首页的全过程。

添加页眉、页脚

与设置首页不同,页眉、页脚是对于所有的slides都要添加,如果我们的页眉、页脚与幻灯片内容无关,我们只需要在模板文件里添加footerheadbar,而不需要对.ipynb文件进行任何修改。

这部分的修改是在my_slides.tplpre_slides块,顾名思义,也就是在幻灯片之前设置的内容:

1
2
3
4
5
6
7
8
9
{% block pre_slides %}
<body>
<div class="headbar">
{{nb.metadata.get('headbar', '')}}
</div>
<div class="footer">
Copyright &copy;2020 <a href="http://www.northfar.net">Zhang Tongshuai</a>
</div>

与前面类似,我们还要设置headbarfooter的css样式,此处不再赘述。

需要注意的是,第4行的 nb.metadata.get('headbar', '') 其实是想在.ipynb文件中添加页眉的内容,与Cell Metadata不同,我们设置的是Notebook Metada,具体如下图所示:

nb_metadata

我们可以看到最终的页眉页脚以及首页效果:

head_foot

幻灯片内容的出现消失

在演示的时候,我们经常有这样的动画需求:一项内容出现、然后消失,随之出现下一项内容。

查阅Reveal.js的文档可知,这一功能可以通过在Fragment中添加current-visible(新的文档似乎是fade-in-then-out)的class属性。那么,类似对首页的修改,实现这一需求可以分为三步:

  1. Cell Metadata添加current_visible项(注意,不是HTML中的current-visible),其为真,表明该Fragement需要先出现再消失;

  2. 在模板中对class=fragment的元素,增加current_visible的判断:

    1
    2
    3
    {%- if cell.metadata.get('fragment_start', False) -%}
    <div class="fragment {{'current-visible' if cell.metadata.get('current_visible', False)}}">
    {%- endif -%}
  3. 此时,Reveal.js已经可以实现上述动画效果,但是缺点在于消失的元素仍会占据位置,原因是Reveal.js是通过opacity来控制其可见性的,我们可以在custom.css中改为通过display来控制:

    1
    2
    3
    4
    5
    6
    .reveal .slides section .fragment {
    opacity: 0;
    display: none; }
    .reveal .slides section .fragment.current-fragment {
    opacity: 1;
    display: inline; }

    有关Issue可参见GitHub项目

current_visible

总结

以上是对如何个性化设置Jupyter Notebook导出基于Reveal.js的HTML幻灯片的简单介绍,其核心包括:

  1. 修改.ipynb文件,包括Raw CellCell Metadata或者Notebook Metadata;
  2. 修改nbconvert的转换模板;
  3. 添加custom.css补充和覆盖样式;
  4. 以上三项的协调配合。

当然,以上只是介绍了实现的技术手段,要想做得漂亮还是离不开审美的提高和精力的投入。实际上,Jupyter Notebook限制了Reveal.js的发挥,我个人觉得直接从Markdown文件出发,可以做出更加精美的幻灯片。