πŸ“– Book of jnotebook

jnotebook is a modern notebook system for Java
jnotebook interprets Java JShell files and render them as notebooks.

βš–οΈ Rationale

Computational notebooks allow arguing from evidence by mixing prose with executable code. For a good overview of problems users encounter in traditional notebooks like Jupyter, see I don't like notebooks and What’s Wrong with Computational Notebooks? Pain Points, Needs, and Design Opportunities.

jnotebook tries to address the following problems:

  • notebook editors are less helpful than IDE editors
  • notebook code is hard to reuse
  • out-of-order execution causes reproducibility issues
  • notebook code is hard to version control
  • the Java ecosystem does not provide a great experience for visualization and document formatting

jnotebook is a notebook library for Java that address these problems by doing less, namely:

  • no editing environment: you can keep the code editor you know and love
  • (almost) no new format: jnotebook interprets JShell files and renders them as notebook.
    Because jnotebook is not required to run JShell files, it does not introduce a dependency if you wish to run the JShell file in production.
  • no out-of-order execution: jnotebook always evaluates from top to bottom. jnotebook builds a dependency graph of Java statements and only recomputes the needed changes to keep the feedback loop fast.
  • cells outputs are interpreted as html. This gives access to great visualization libraries and standard html for formatting.

πŸš€ Getting Started

Quickstart

jnotebook requires Java 17 or higher.
jnotebook is distributed in a single portable binary. Download it.

curl -Ls https://get.jnotebook.catheu.tech -o jnotebook
chmod +x jnotebook

Launch it.

./jnotebook server

# or
java -jar jnotebook server

Then go to http://localhost:5002.
By default, the notebook folder is notebooks. If it does not exist, it will be created with an example notebook.
jnotebook automatically detects when a .jsh file in the notebooks folder is edited
and renders it in the web app.
Once your notebook is ready to be published, render it in a single html file with:

./jnotebook render notebooks/my_notebook.jsh

A my_notebook.html file will be generated.

Install

See detailed installation instruction for different platforms in the github project.

🀹 Demo notebooks

Coming soon.

πŸ”Œ In an Existing Project

Maven

When launched within a maven project, jnotebook automatically injects the project
dependencies in the classpath. If launched in a submodule, only the submodule
dependencies are injected.

Manual

Dependencies can be injected manually with the -cp=<CLASSPATH> parameter.

IDE integration

IntelliJ

Enable IntelliSense highlighting and code utilities for JShell .jsh files:

  1. Go to Settings | Editor | File Types
  2. Click on JShell snippet
  3. In file name patterns, click + (add)
  4. Add *.jsh.

πŸ’‘ Editor principle

Cells are delimited by blank lines

String s1 = "Hello";
"Hello"
String s2 = "World";
"World"

for multi-statements cells, only the last value or method result is returned.

String s3 = "!"; String s4 = "!"; String message = s1 + " " + s2 + s3 + s4;
"Hello World!!"

but everything sent to System.out is returned.

System.out.println("Hello John"); System.out.println("Hello Jane"); String s5 = "Hello Alex";
Hello John
Hello Jane
"Hello Alex"

html in return values is interpreted. (see custom html for more html manipulation)

String sayHello() { return "<b>Hello John!</b>"; } sayHello();
"Hello John!"

while System.out is not interpreted as html.

System.out.println("<b>Hello Jane!</b>");
<b>Hello Jane!</b>

By default, the java environment imports common classes. The exact list can be found here.

List.of(1,2,3); Map.of("key", "value"); Thread.sleep(1);

Mistakes happen! jnotebook tries its best to give helpful error messages.

invalidJava();
Error: 
cannot find symbol
  symbol:   method invalidJava()
  location: class 
invalidJava();
^^^^^^^^^^^^

Exceptions happen too!

throw new RuntimeException("Panic!");
jdk.jshell.EvalException: Panic!

Markdown

Comments are interpreted as markdown.

// #### example title
// my text with [a link](example.com)

Is rendered as

example title

my text with a link

You can also use multiline comments.

Latex

Latex is supported inline:

$`a^2+b^2=c^2`$
β†’ will render as a2+b2=c2a^2+b^2=c^2.
and as block:

```math
a^2+b^2=c^2
```
a2+b2=c2 a^2+b^2=c^2

Mermaid

Mermaid graphs are supported

AliceJohnHello John, how are you?John, can you hear me?Hi Alice, I can hear you!I feel great!AliceJohn

using

```mermaid
[MERMAID GRAPH CODE]
```

See mermaid documentation for examples.

πŸ” Viewers

jnotebook provides viewers and utils for data, tables, plots, flamegraphs etc.
These utils are packaged in a separate dependency jnotebook-utils. By default, jnotebook-utils is in the classpath.
All utils are available as static method in tech.catheu.jnotebook.Nb.

import tech.catheu.jnotebook.Nb;

πŸ”’ Tables

coming soon

πŸ“Š Plotly

jnotebook has built-in support for Plotly's low-ceremony plotting. See Plotly's JavaScript docs for more examples and options.

Nb.plotly(List.of( Map.of("z", List.of(List.of(1, 2, 3), List.of(3, 2, 1)), "type", "surface")), Map.of(), Map.of());

πŸ—Ί Vega Lite

jnotebook also supports Vega Lite.

Nb.vega(Map.of( "data", Map.of("url", "https://vega.github.io/vega-lite/data/seattle-weather.csv"), "mark", "bar", "encoding", Map.of( "x", Map.of("timeUnit", "month", "field", "date", "type", "ordinal"), "y", Map.of("aggregate", "mean", "field", "precipitation") ) ));

🏞 Images

coming soon

πŸ”  Grid Layouts

Layouts can be composed via rows, columns and grids

Nb.row(1, 2, 3, 4);
1
2
3
4
Nb.col(1, 2, 3, 4);
1
2
3
4
Nb.col(Nb.row("John", "Jane", "Alex"), Nb.row(1, 2, 3), Nb.row(4, 5, 6));
John
Jane
Alex
1
2
3
4
5
6
int numCols = 4; Nb.grid(numCols, 1, 2, 3, 4, 5, 6, 7);
1
2
3
4
5
6
7

βš™οΈ Custom html

You can use the j2html library directly to generate html easily.
Values inheriting j2html.tags.DomContent are rendered as html.

import static j2html.TagCreator.*; // import all html tags b("Hello red!").withStyle("color: red");
Hello red!

This makes it easy to create custom viewers

import j2html.tags.DomContent; DomContent title(String text) { return p(text).withStyle("font-weight:bold; font-size: x-large; display: block;margin-left: auto; margin-right: auto"); }; title("A big title.");

A big title.

All Nb viewers output are of class j2html.tags.DomContent. This makes is easy to combine viewers

var graph1 = Nb.vega(Map.of( "data", Map.of("url", "https://vega.github.io/vega-lite/data/seattle-weather.csv"), "mark", "bar", "encoding", Map.of( "x", Map.of("timeUnit", "month", "field", "date", "type", "ordinal"), "y", Map.of("aggregate", "mean", "field", "precipitation") ) )); var graph2 = Nb.vega(Map.of( "data", Map.of("url", "https://vega.github.io/vega-lite/data/seattle-weather.csv"), "mark", "bar", "encoding", Map.of( "x", Map.of("timeUnit", "month", "field", "date", "type", "ordinal"), "y", Map.of("aggregate", "mean", "field", "precipitation") ) )); Nb.row(title("My awesome analysis"), Nb.col(graph1, graph2));

My awesome analysis

πŸ”₯ Flamegraphs

You can profile methods and generate flamegraphs.

Runnable arrayFilling = () -> { List<Integer> a = new ArrayList<>(); for (int i = 0; i < 10000000; i++) { a.add(i); } }; var profilePath = Nb.profile(arrayFilling); Nb.flame(profilePath);
ALL (100.000%, 8 samples)
ALL
java.lang.Thread::run (100.000%, 8 samples)
java.lang.Thread::run
jdk.jshell.execution.LocalExecutionControl$$Lambda$379+0x00007f14d01f74d0.1915122813::run (100.000%, 8 samples)
jdk.jshell.execution.LocalExecutionControl$$Lambda$379+0x00007f14d01f74d0.1915122813::run
jdk.jshell.execution.LocalExecutionControl::lambda$invoke$1 (100.000%, 8 samples)
jdk.jshell.execution.LocalExecutionControl::lambda$invoke$1
java.lang.reflect.Method::invoke (100.000%, 8 samples)
java.lang.reflect.Method::invoke
jdk.internal.reflect.DelegatingMethodAccessorImpl::invoke (100.000%, 8 samples)
jdk.internal.reflect.DelegatingMethodAccessorImpl::invoke
jdk.internal.reflect.NativeMethodAccessorImpl::invoke (100.000%, 8 samples)
jdk.internal.reflect.NativeMethodAccessorImpl::invoke
jdk.internal.reflect.NativeMethodAccessorImpl::invoke0 (100.000%, 8 samples)
jdk.internal.reflect.NativeMethodAccessorImpl::invoke0
::do_it$ (100.000%, 8 samples)
::do_it$
::do_it$Aux (100.000%, 8 samples)
::do_it$Aux
tech.catheu.jnotebook.Nb::profile (100.000%, 8 samples)
tech.catheu.jnotebook.Nb::profile
tech.catheu.jnotebook.Nb::profile (100.000%, 8 samples)
tech.catheu.jnotebook.Nb::profile
tech.catheu.jnotebook.Nb$ProfiledRunnable::run (87.500%, 7 samples)
tech.catheu.jnotebook.Nb$ProfiledRunnable::run
com.microsoft.jfr.Recording::start (12.500%, 1 samples)
com.microsoft.jfr.Recording::start
$Lambda$487+0x00007f14d025bc70.1875362892::run (87.500%, 7 samples)
$Lambda$487+0x00007f14d025bc70.1875362892::run
com.microsoft.jfr.FlightRecorderConnection::startRecording (12.500%, 1 samples)
com.microsoft.jfr.FlightRecorderConnection::startRecording
::lambda$do_it$$0 (87.500%, 7 samples)
::lambda$do_it$$0
com.sun.jmx.mbeanserver.JmxMBeanServer::invoke (12.500%, 1 samples)
com.sun.jmx.mbeanserver.JmxMBeanServer::invoke
java.lang.Integer::valueOf (25.000%, 2 samples)
java.lang.Integer::valueOf
java.util.ArrayList::add (37.500%, 3 samples)
java.util.ArrayList::add
com.sun.jmx.interceptor.DefaultMBeanServerInterceptor::invoke (12.500%, 1 samples)
com.sun.jmx.interceptor.DefaultMBeanServerInterceptor::invoke
java.util.ArrayList::add (37.500%, 3 samples)
java.util.ArrayList::add
javax.management.StandardMBean::invoke (12.500%, 1 samples)
javax.management.StandardMBean::invoke
java.util.ArrayList::grow (37.500%, 3 samples)
java.util.ArrayList::grow
com.sun.jmx.mbeanserver.MBeanSupport::invoke (12.500%, 1 samples)
com.sun.jmx.mbeanserver.MBeanSupport::invoke
java.util.ArrayList::grow (37.500%, 3 samples)
java.util.ArrayList::grow
com.sun.jmx.mbeanserver.PerInterface::invoke (12.500%, 1 samples)
com.sun.jmx.mbeanserver.PerInterface::invoke
java.util.Arrays::copyOf (37.500%, 3 samples)
java.util.Arrays::copyOf
com.sun.jmx.mbeanserver.MBeanIntrospector::invokeM (12.500%, 1 samples)
com.sun.jmx.mbeanserver.MBeanIntrospector::invokeM
com.sun.jmx.mbeanserver.MXBeanIntrospector::invokeM2 (12.500%, 1 samples)
com.sun.jmx.mbeanserver.MXBeanIntrospector::invokeM2
com.sun.jmx.mbeanserver.MXBeanIntrospector::invokeM2 (12.500%, 1 samples)
com.sun.jmx.mbeanserver.MXBeanIntrospector::invokeM2
com.sun.jmx.mbeanserver.ConvertingMethod::invokeWithOpenReturn (12.500%, 1 samples)
com.sun.jmx.mbeanserver.ConvertingMethod::invokeWithOpenReturn
com.sun.jmx.mbeanserver.ConvertingMethod::invokeWithOpenReturn (12.500%, 1 samples)
com.sun.jmx.mbeanserver.ConvertingMethod::invokeWithOpenReturn
sun.reflect.misc.MethodUtil::invoke (12.500%, 1 samples)
sun.reflect.misc.MethodUtil::invoke
java.lang.reflect.Method::invoke (12.500%, 1 samples)
java.lang.reflect.Method::invoke
jdk.internal.reflect.DelegatingMethodAccessorImpl::invoke (12.500%, 1 samples)
jdk.internal.reflect.DelegatingMethodAccessorImpl::invoke
jdk.internal.reflect.NativeMethodAccessorImpl::invoke (12.500%, 1 samples)
jdk.internal.reflect.NativeMethodAccessorImpl::invoke
jdk.internal.reflect.NativeMethodAccessorImpl::invoke0 (12.500%, 1 samples)
jdk.internal.reflect.NativeMethodAccessorImpl::invoke0
sun.reflect.misc.Trampoline::invoke (12.500%, 1 samples)
sun.reflect.misc.Trampoline::invoke
java.lang.reflect.Method::invoke (12.500%, 1 samples)
java.lang.reflect.Method::invoke
jdk.internal.reflect.DelegatingMethodAccessorImpl::invoke (12.500%, 1 samples)
jdk.internal.reflect.DelegatingMethodAccessorImpl::invoke
jdk.internal.reflect.NativeMethodAccessorImpl::invoke (12.500%, 1 samples)
jdk.internal.reflect.NativeMethodAccessorImpl::invoke
jdk.internal.reflect.NativeMethodAccessorImpl::invoke0 (12.500%, 1 samples)
jdk.internal.reflect.NativeMethodAccessorImpl::invoke0
jdk.management.jfr.FlightRecorderMXBeanImpl::startRecording (12.500%, 1 samples)
jdk.management.jfr.FlightRecorderMXBeanImpl::startRecording
jdk.jfr.Recording::start (12.500%, 1 samples)
jdk.jfr.Recording::start
jdk.jfr.internal.PlatformRecording::start (12.500%, 1 samples)
jdk.jfr.internal.PlatformRecording::start
jdk.jfr.internal.PlatformRecorder::start (12.500%, 1 samples)
jdk.jfr.internal.PlatformRecorder::start
jdk.jfr.internal.PlatformRecorder::updateSettings (12.500%, 1 samples)
jdk.jfr.internal.PlatformRecorder::updateSettings
jdk.jfr.internal.PlatformRecorder::updateSettingsButIgnoreRecording (12.500%, 1 samples)
jdk.jfr.internal.PlatformRecorder::updateSettingsButIgnoreRecording
jdk.jfr.internal.MetadataRepository::setSettings (12.500%, 1 samples)
jdk.jfr.internal.MetadataRepository::setSettings
jdk.jfr.internal.SettingsManager::setSettings (12.500%, 1 samples)
jdk.jfr.internal.SettingsManager::setSettings
jdk.jfr.internal.SettingsManager::updateRetransform (12.500%, 1 samples)
jdk.jfr.internal.SettingsManager::updateRetransform
jdk.jfr.internal.JVM::retransformClasses (12.500%, 1 samples)
jdk.jfr.internal.JVM::retransformClasses
jdk.jfr.internal.JVMUpcalls::onRetransform (12.500%, 1 samples)
jdk.jfr.internal.JVMUpcalls::onRetransform
jdk.jfr.internal.EventInstrumentation::<init> (12.500%, 1 samples)
jdk.jfr.internal.EventInstrumentation::<init>
jdk.jfr.internal.EventInstrumentation::createClassNode (12.500%, 1 samples)
jdk.jfr.internal.EventInstrumentation::createClassNode
jdk.internal.org.objectweb.asm.ClassReader::accept (12.500%, 1 samples)
jdk.internal.org.objectweb.asm.ClassReader::accept
jdk.internal.org.objectweb.asm.ClassReader::accept (12.500%, 1 samples)
jdk.internal.org.objectweb.asm.ClassReader::accept

Credits

This documentation is directly copying some content from the book of Clerk, a notebook system for Clojure.