Skip to content

Instantly share code, notes, and snippets.

@vdikan
Last active September 13, 2022 10:34
Show Gist options
  • Save vdikan/6b6063d6e1b00a3cd79bc7b3ce3853d6 to your computer and use it in GitHub Desktop.
Save vdikan/6b6063d6e1b00a3cd79bc7b3ce3853d6 to your computer and use it in GitHub Desktop.
Single-script Vega-Lite plotter made with Babashka (Clojure). A remix of wee-httpd.
#!/usr/bin/env bb
(ns vega-serv)
(import (java.net ServerSocket))
(require '[clojure.string :as string]
'[clojure.java.io :as io]
'[cheshire.core :as json]
'[clojure.tools.cli :refer [parse-opts]])
(def cli-options
;; An option with a required argument
[["-p" "--port PORT" "Port number"
:default 4444
:parse-fn #(Integer/parseInt %)
:validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]
["-f" "--file FILE" "Vega Plot specification file in EDN form"
:default "plotspec.edn"
:validate [#(.exists (io/file %)) "EDN file should exist"]]])
(def options-map
(:options (parse-opts *command-line-args* cli-options)))
(defn create-worker-thread [client-socket request-handler]
(.start
(Thread.
(fn [] (println "Worker: I'm off and working..." client-socket)
(with-open [out (io/writer (.getOutputStream client-socket))
in (io/reader (.getInputStream client-socket))
]
(loop []
(let [[req-line & headers] (loop [headers []]
(let [line (.readLine in)]
(if (string/blank? line)
headers
(recur (conj headers line)))))]
(if (not (nil? req-line))
(let [[_ _ path _] (re-find #"([^\s]+)\s([^\s]+)\s([^\s]+)" req-line)
status 200
body (request-handler path)
]
(.write out (format "HTTP/1.1 %s OK\r\nContent-Length: %s\r\n\r\n%s"
status
(count (.getBytes body))
body))
(.flush out)
(recur)))))
(println "Worker: I'm all done with this one" client-socket))))))
(defn start-web-server [request-handler]
(.start (Thread. #(with-open
[server-socket (new ServerSocket (:port options-map))]
(while true
(let [_ (println "Serv: Awaiting connection on port " (.getLocalPort server-socket))
client-socket (.accept server-socket)]
(println "Serv: Accepted connection!")
(create-worker-thread client-socket request-handler )))))))
;; I still use bootstrap, it's pretty, but might be heavy for a single-page purpose
(def header (format "
<html>
<head>
<link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css' integrity='sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh' crossorigin='anonymous'>
<script src='https://code.jquery.com/jquery-3.4.1.slim.min.js' integrity='sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n' crossorigin='anonymous'></script>
<script src='https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js' integrity='sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo' crossorigin='anonymous'></script>
<script src='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js' integrity='sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6' crossorigin='anonymous'></script>
<!-- Load Vega-Lite -->
<script src='https://cdn.jsdelivr.net/npm/[email protected]'></script>
<script src='https://cdn.jsdelivr.net/npm/[email protected]'></script>
<script src='https://cdn.jsdelivr.net/npm/[email protected]'></script>
</head>
<body>
<nav class='navbar navbar-expand-lg navbar-dark bg-primary'>
<a class='navbar-brand' href='/'><code>%s</code> </a>
<button class='navbar-toggler' type='button' data-toggle='collapse' data-target='#navbarSupportedContent' aria-controls='navbarSupportedContent' aria-expanded='false' aria-label='Toggle navigation'>
<span class='navbar-toggler-icon'></span>
</button>
<div class='collapse navbar-collapse' id='navbarSupportedContent'>
<ul class='navbar-nav mr-auto'>
<li class='nav-item active'>
<a class='nav-link' href='/about'>About <span class='sr-only'>(current)</span></a>
</li>
<li class='nav-item active'>
<a class='nav-link' href='https://vega.github.io/vega-lite/'>Vega-Lite <span class='sr-only'>(current)</span></a>
</li>
<li class='nav-item active'>
<a class='nav-link' href='https://github.com/dakrone/cheshire'>Cheshire <span class='sr-only'>(current)</span></a>
</li>
<li class='nav-item active'>
<a class='nav-link'href='https://github.com/bherrmann7/bb-common/blob/master/wee_httpd.bb'>Wee-HTTPd <span class='sr-only'>(current)</span></a>
</li>
</ul>
</div>
</nav>
<br>
<div class='container-fluid'>
<div class='row'>
<div class='col-lg-12'>
" (:file options-map)))
(def footer "
</div> <!-- class='col-lg-12'> -->
</div> <!--class='row' -->
</div> <!-- class='container' -->
</body>
</html>")
(defn page [ & content ]
(string/join (concat [header] content [footer])))
(defn route-not-supported [ & args ]
"This route is not supported.")
(defn about-page [ & args ]
(page "
<p> Single-script Vega-Lite plotter made with
<a href='https://github.com/babashka/babashka'>babashka</a> scripting env. </p>
<p> Usage: <br/><br/>
<code>$ vega-serv -p PORT -f FILE</code> </p>
<p> <code>FILE</code> should be a valid EDN corresponding to Vega-Lite plot specification. <br/>
It is translated to JSON using <a href='https://github.com/dakrone/cheshire'>Cheshire</a> library. </p>
<p> The script is a remix of <a href='https://github.com/bherrmann7/bb-common/blob/master/wee_httpd.bb'>wee-httpd</a> server. </p>
"))
(def vega-plot-fmt "
<div id='vis'></div>
<script type='text/javascript'>
var yourVlSpec = %s;
vegaEmbed('#vis', yourVlSpec);
</script>
")
(defn load-plotspec []
(json/generate-string
(read-string (slurp (:file options-map)))
{:pretty true}))
(defn vega-plot-page [ & args ]
(page (format vega-plot-fmt (load-plotspec))))
;; A thing for matching a path request with a path expression;
;; ie (path-mather "/j*" "/joe") => true
(defn path-matcher [ path-expr path ]
(println "path-matcher" path-expr path )
(let [match
(if (.endsWith path-expr "*")
(if (>= (count path) (dec (count path-expr)))
(let [ sm-path-expr (.substring path-expr 0 (dec (count path-expr)))
sm-path (.substring path 0 (count sm-path-expr))
_ (println "comparing expr:" sm-path-expr " with sm-path:" sm-path)
]
(= sm-path-expr sm-path))
false)
(= path path-expr))]
(println "path-matcher" path-expr path match)
match))
(defn router [path]
((condp path-matcher path
"/" vega-plot-page
"/about" about-page
"/favicon.ico" route-not-supported
route-not-supported)))
(vega-serv/start-web-server (fn[path] (router path)))
;; Keep the shell script from returning. When evaling the whole buffer, comment this out.
(while true (Thread/sleep 1000))
;; Tells emacs to jump into clojure mode.
;; Local Variables:
;; mode: clojure
;; End:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment