Last active
September 13, 2022 10:34
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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