Skip to content

Latest commit

 

History

History
211 lines (171 loc) · 450 KB

literate-clojure.org

File metadata and controls

211 lines (171 loc) · 450 KB

Projectless literate Clojure with embedded SVG

Short Intro

In short this is an example document of a project-less literate Emacs/orgmode page with embedded SVGs that can be directly evaluated and exported to HTML. Everything is contained in one .org file. There are no additional files needed and no external image files.

Longer Intro

I take a lot of notes using Emacs’ Orgmode. Often I will have little code snippets to add diagrams or plots. In some languages this is easier than in others. For instance the octave integration in Emacs is excellent and plots can be added pretty easily. However sometimes I want the power of a full programming language, so here I’ve tried to develop a simplified workflow that will work with Clojure.

Projectless
Clojure is dynamically typed and uses a REPL which can make it feel very interactive. However if you’ve used something like MATLAB.. well you don’t have the same level of seemless integration. B/c the language is general purpose, the language core includes very few features. Unlike Octave/R or similar languages, you typically won’t just drop into a REPL and go. You need to set up a project folder, source code directory structure, defined in a deps.edn configuration file, specify what libraries you want, make some namespaces.. etc etc. I want to short circuit as much of that as possible.
Embedded SVG
I also wanted to go a step further and have plots/diagrams directly included in the .org file itself. It won’t work for every situation. You won’t want to inline a jpeg .. but for most situations this is sufficient in my use cases. And I want this embedded SVG to be in the exported HTML. SVG and HTML play nice together. I also don’t want to limit myself to some hacked together plotting library to imitate MATLAB. With the generic SVG primitives provided by thing/geom you aren’t just limited to plotting. You can do much much more (scroll to the bottom of the page for a TLDR)

Places of potential improvement are in green Note boxes. Feedback/Suggestions are very welcome :)

Setup

You naturally need Clojure installed. Then in Emacs you will need to have ob-clojure and cider enabled. Details here: https://www.orgmode.org/worg/org-contrib/babel/languages/ob-doc-clojure.html

Note: In principle this could be pared down further. This could work without cider - just with a clojure or clj inferior process. However my ELisp is no-bueno and I wasn’t able to get this working.

add-libs

The trick to getting this to work project-less (ie. without using a dep.edn) is to leverage an experimental tool in Clojure called add-libs that is in the tools.deps.alpha library/namespace. This is described in detail by Alex Miller: https://insideclojure.org/2018/05/04/add-lib/ (note: since publication the tool was renamed from add-lib to add-libs)

At the moment this is experimental and living on a branch. Maybe some day it’ll be part of core and this step can be skipped. We want this library to be automatically loaded when your projectless cider session gets launched. I’ve included the following in ~~/.clojure/deps.edn~ so that it is enable globally.

{
 :deps { org.clojure/tools.deps.alpha {:git/url "https://github.com/clojure/tools.deps.alpha.git"
                                       :sha "241cd24c35ba770aea4773ea161d45276e5d3a73"}}
}

Note: Ideally this would only be included in cider sessions that are projectless (are missing a deps.edn). If you know how to make such a conditional switch, please let me know - or post an answer here: clojure-emacs/cider#3025

Loading libraries

We then cider-jack-in a cider session (preferrably in a directory where there with no Clojure project)

Note: ideally orgmode would automatically launch a session somehow in a temp folder.. somewhere in the background..

And we can start using add-libs directly to load libraries dynamically:

(use 'clojure.tools.deps.alpha.repl)
(add-libs {'thi.ng/geom {:mvn/version "1.0.0-RC4"}})
(use 'thi.ng.geom.viz.core)
(use 'thi.ng.geom.svg.core)

This simply: 1) loads the add-libs namespace, 2) adds an SVG plotting library (fetching it from Maven) 3) loads some namespaces from that library

Unfortunately I need to rerun this block twice. The first run always seems to timeout:

executing Clojure code block...
nrepl-send-sync-request: Sync nREPL request timed out (ns user op eval code (use 'clojure.tools.deps.alpha.repl)

Note: I have no idea what’s going on here.. if anyone knows what’s up, please let me know

Embedded SVG

Once we have a library loaded and included in the default namespace, we can start using it directly. To demonstrate including SVG I’m using the fantastic ~thi-ng/geom~ library from Karsten Schmidt. The project is a bit intimidating in scope, bit it’s actually very modular bare-Clojure with no magic/macros. Each piece is usually easy to read and understand in isolation. The library has some very minimal SVG/Math/Vec cores and then other small practical namespaces built on top of this (it’s best to think of it as a collection of interconnecting mini-libraries). Here I’m using the plotting utility from geom-viz to make a very simple stacked interval plot which is then output in SVG hiccup and serialized to SVG XML. This was pulled directly from the examples: https://github.com/thi-ng/geom/blob/master/geom-viz/src/core.org#stacked-intervals

 (->> {:x-axis (linear-axis
		 {:domain [-10 310]
		  :range  [50 550]
		  :major  100
		  :minor  50
		  :pos    150})
	:y-axis (linear-axis
		 {:domain  [0 4]
		  :range   [50 150]
		  :visible false})
	:data   [{:values  [[0 100] [10 90] [80 200] [250 300] [150 170] [110 120]
			    [210 280] [180 280] [160 240] [160 170]]
		  :attribs {:stroke-width "10px" :stroke-linecap "round" :stroke "#0af"}
		  :layout  svg-stacked-interval-plot}]}
      (svg-plot2d-cartesian)
      (svg {:width 600 :height 200})
      (serialize)
      symbol)

What’s great here is that this plot was immediately inserted (as shown) when I evaluated the source code block. I exported it to HTML and the SVG was automatically inlined. There are no .svg files. (Though we could ~spit~ one if we want). Now I can just grab this .html file and email it to some friends :)

Some things to note:

The source code block here required a header argument to specify that result should come out as an HTML block:


geom’s serialize (the second to last line) transforms its own SVG hiccup format to a valid SVG/XML string. If we stopped here then the cider session would show an output sourrounded by double quotes (as it does for all strings). We don’t want these quotes in the resulting HTML, so the result needs to be piped through symbol to remove those.

Note: I seem to be abusing symbol, b/c this isn’t its intended use - but I don’t know of a better alternative. Please let me know if there is something better (that doesn’t involve tweaking ob-clojure)

If you have multiple evaluations in one source code block (like function definitions) then all their outputs will show up in the #+RESULTS block (not just the last form). I’ve separate non-drawing stuff into their own source code block that has the results disabled (note how the last code block is one big threading macro)


Export

By default #+RESULTS: blocks of evaluated org-mode source code blocks are not exported to the HTML so you will need to add a orgmode configuration at the top of your org file to enable this:

#+PROPERTY: header-args :results org :eval never-export :exports both

Conlusion

And that’s it! You can now evaluate code blocks (C-c C-c block-by-block or C-c C-v C-b for the whole buffer) and export to HTML and everything necessary will be in the .html file - with no images to drag around.

I think the end result is .. satisfactory. Admittedly it’s not yet as concise as writing code block in something purpose-built like Octave, but ultimately the flexibility of Clojure (even if a tad verbose) may be worth it in some situations. Probably with a good suite/library of convenience functions built on top of thing/geom one could get to much smaller focused domain-specific pieces of code.

Longer Example

This is mostly just to show what’s possible with thing/geom. The Clojure code is near instantaneous, export is immediate. However, running on a Core M-5Y10c, the final 437kb SVG string took a few minute to insert into the #+RESULTS block (I suspect an issue in orgmode itself).

So this might be pushing the limits of waht you wanna do :)

Code from: https://github.com/thi-ng/geom/blob/master/geom-svg/src/examples.org

  (require
   '[thi.ng.geom.core :as g]
   '[thi.ng.geom.vector :refer [vec3]]
   '[thi.ng.geom.matrix :as mat]
   '[thi.ng.geom.circle :as c]
   '[thi.ng.geom.polygon :as p]
   '[thi.ng.geom.gmesh :as gm]
   '[thi.ng.geom.mesh.subdivision :as sd]
   '[thi.ng.geom.svg.core :as svg]
   '[thi.ng.geom.svg.shaders :as shader]
   '[thi.ng.geom.svg.renderer :as render]
   '[thi.ng.math.core :as m])

(def width    640)
(def height   480)
(def model    (g/rotate-y (mat/matrix44) m/SIXTH_PI))
(def view     (apply mat/look-at (mat/look-at-vectors 0 1.75 0.75 0 0 0)))
(def proj     (mat/perspective 60 (/ width height) 0.1 10))
(def mvp      (->> model (m/* view) (m/* proj)))

(def diffuse  (shader/normal-rgb (g/rotate-y (mat/matrix44) m/PI)))
(def uniforms {:stroke "white" :stroke-width 0.25})

(def shader-diffuse
  (shader/shader
   {:fill     diffuse
    :uniforms uniforms
    :flags    {:solid true}}))

(defn ring
  [res radius depth wall]
  (-> (c/circle radius)
      (g/as-polygon res)
      (g/extrude-shell {:depth depth :wall wall :inset -0.1 :mesh (gm/gmesh)})
      (g/center)))

(def mesh
  (->> [[1 0.25 0.15] [0.75 0.35 0.1] [0.5 0.5 0.05] [0.25 0.75 0.05]]
       (map (partial apply ring 40))
       (reduce g/into)
       (sd/catmull-clark)
       (sd/catmull-clark)))

;; 2d text label w/ projected anchor point
(defn label-3d
  [p mvp screen [l1 l2]]
  (let [p'  (mat/project-point p mvp screen)
        p2' (mat/project-point (m/+ p 0 0 0.2) mvp screen)
        p3' (m/+ p2' 100 0)]
    (svg/group
     {:fill "black"
      :font-family "Arial"
      :font-size 12
      :text-anchor "end"}
     (svg/circle p' 2 nil)
     (svg/line-strip [p' p2' p3'] {:stroke "black"})
     (svg/text (m/+ p3' 0 -5) l1 {})
     (svg/text (m/+ p3' 0 12) l2 {:font-weight "bold"}))))
(let [screen (mat/viewport-matrix width height)
	  max-z  (/ 0.75 2)]
    (->> (svg/svg
	    {:width width :height height}
	    (render/mesh mesh mvp screen shader-diffuse)
	    (label-3d (vec3 0 0 max-z) mvp screen ["Shader" "Normal/RGB"]))
	   (svg/serialize)
	   symbol))