Skip to content

Commit

Permalink
Add on-signals vega view callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
vlaaad committed Dec 28, 2021
1 parent 69258f2 commit b3ce9ca
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 27 deletions.
30 changes: 17 additions & 13 deletions src/vlaaad/reveal.clj
Expand Up @@ -637,26 +637,30 @@
{:fx/type derefable-view
:derefable (execute-action action value annotation)})

(defn ^{:arglists '([{:keys [spec opt data signals]}])} vega-view
(defn ^{:arglists '([{:keys [spec opt data signals on-signals]}])} vega-view
"Cljfx component fn that shows a vega(-lite) visualization
Required keys:
:spec vega(-lite) spec, either URL string that points to vega(-lite)
spec, or map that can be serialized to vega(-lite) json spec
:spec vega(-lite) spec, either URL string that points to spec, or
map that can be serialized to vega(-lite) json spec
Optional keys:
:opt map that can be serialized to json vega-embed opt:
https://github.com/vega/vega-embed#options
:data either:
* vega dataset - a coll of map datums that will be assigned to
\"source\" dataset (which is a default name if vega spec does
not specify a named dataset)
* a map from dataset names to datasets
using :data instead of inlining dataset inside a spec has much
better performance in data streaming uses
:signals map from signal name to signal value
:opt map that can be serialized to json vega-embed opt:
https://github.com/vega/vega-embed#options
:data either:
* vega dataset - a coll of map datums that will be assigned
to \"source\" dataset (which is a default name if vega spec
does not specify a named dataset)
* a map from dataset names to dataset colls
using :data instead of inlining dataset coll inside a spec
has much better update performance in data streaming use case
:signals map from signal name to signal value (will be serialized
to json)
:on-signals map from signal name to 1-arg fn that will be called with new
signal values (will be deserialized from json with
keyword keys)
Examples:
Expand Down
99 changes: 85 additions & 14 deletions src/vlaaad/reveal/vega.clj
Expand Up @@ -10,9 +10,10 @@
[cljfx.lifecycle :as fx.lifecycle]
[vlaaad.reveal.style :as style]
[vlaaad.reveal.font :as font])
(:import [javafx.scene.web WebView]
(:import [javafx.scene.web WebView WebEngine]
[javafx.concurrent Worker$State Worker]
[javafx.beans.value ChangeListener]))
[javafx.beans.value ChangeListener]
[netscape.javascript JSObject]))

(defn- html [spec opt]
(format
Expand All @@ -22,7 +23,7 @@
<script src=\"%s\"></script>
<script src=\"%s\"></script>
<style>
html, body {height:100%%;margin:0;background-color:%s;}
html, body {height:100%%;margin:0;background-color:%s;color:%s;}
#wrap {height:100vh;width:100%%;display:flex;flex-direction:column;}
#view {flex:1;overflow:auto;}
</style>
Expand All @@ -41,6 +42,7 @@
(.toExternalForm (io/resource "vlaaad/reveal/vega/vega-lite@5.2.0.min.js"))
(.toExternalForm (io/resource "vlaaad/reveal/vega/vega-embed@6.20.5.min.js"))
@style/background-color
(style/color :symbol)
(json/write-str spec)
(json/write-str opt)))

Expand All @@ -64,7 +66,7 @@
(.removeListener prop# this#)
~@body)))))))

(defn- set-data-html [m]
(defn- set-data-js [m]
(format
"viewPromise = viewPromise.then(function(view) {
return view
Expand All @@ -82,7 +84,7 @@
"))")))
(str/join "\n "))))

(defn- set-signal-html [m]
(defn- set-signal-js [m]
(format
"viewPromise = viewPromise.then(function(view) {
return view
Expand All @@ -99,6 +101,60 @@
")")))
(str/join "\n "))))

(defn- add-signal-listeners! [^WebEngine e m]
(let [^JSObject w (.executeScript e "window")]
(doseq [[k f] m]
(.setMember w (str "vega_signal_handler_" (name k)) f)))
(.executeScript
e
(format
"%s
viewPromise = viewPromise.then(function(view) {
%s
return view;
})"
(->> m
keys
(map #(str "window["
(json/write-str (str "vega_signal_handler_fn_" (name %)))
"] = function(name, value) { window["
(json/write-str (str "vega_signal_handler_" (name %)))
"].invoke(/*name, */JSON.stringify(value)); };"))
(str/join "\n"))
(->> m
keys
(map #(str "view.addSignalListener("
(json/write-str %) ", "
"window[" (json/write-str (str "vega_signal_handler_fn_" (name %))) "]"
");"))
(str/join "\n ")))))

(defn- remove-signal-listeners! [^WebEngine e m]
(.executeScript
e
(format
"viewPromise = viewPromise.then(function(view) {
%s
return view;
})"
(->> m
keys
(map #(str "view.removeSignalListener(" (json/write-str %) ", window[" (json/write-str (str "vega_signal_handler_fn_" (name %)) "]);")))
(str/join "\n "))))
(let [^JSObject w (.executeScript e "window")]
(doseq [k (keys m)]
(.removeMember w (str "vega_signal_handler_" (name k)))
(.removeMember w (str "vega_signal_handler_fn_" (name k))))))

(def ref-lifecycle
(reify fx.lifecycle/Lifecycle
(create [_ desc _]
(atom desc))
(advance [_ component desc _]
(reset! component desc)
component)
(delete [_ _ _])))

(defn- replace-from-map-mutator [f default]
(reify fx.mutator/Mutator
(assign! [_ instance coerce value]
Expand All @@ -117,7 +173,7 @@
(let [e (.getEngine view)]
(on-loaded
(.getLoadWorker e)
(.executeScript (.getEngine view) (set-data-html m))))))
(.executeScript e (set-data-js m))))))
[])
fx.lifecycle/scalar)
:signals (fx.prop/make (replace-from-map-mutator
Expand All @@ -126,9 +182,20 @@
(let [e (.getEngine view)]
(on-loaded
(.getLoadWorker e)
(.executeScript (.getEngine view) (set-signal-html m))))))
(.executeScript e (set-signal-js m))))))
nil)
fx.lifecycle/scalar)}))
fx.lifecycle/scalar)
:on-signals (fx.prop/make (fx.mutator/adder-remover
(fn [^WebView view m]
(let [e (.getEngine view)]
(on-loaded (.getLoadWorker e) (add-signal-listeners! e m))))
(fn [^WebView view m]
(let [e (.getEngine view)]
(on-loaded (.getLoadWorker e) (remove-signal-listeners! e m)))))
(fx.lifecycle/map-of
(fx.lifecycle/wrap-coerce ref-lifecycle
(fn [f]
#(@f (json/read-str % :key-fn keyword))))))}))

(def ^:private default-config
(delay
Expand Down Expand Up @@ -162,7 +229,7 @@
(apply merge-with deep-merge maps)
(last maps)))

(defn view [{:keys [spec opt data signals]}]
(defn view [{:keys [spec opt data signals on-signals]}]
(let [spec (if (and (map? spec)
(str/includes? (:$schema spec "https://vega.github.io/schema/vega-lite/v5.json")
"vega-lite"))
Expand All @@ -175,11 +242,15 @@
(update :actions #(if (some? %) % false))
(update :config #(deep-merge @default-config %)))]
{:fx/type ext-with-data-props
:props {:data (cond
(map? data) data
(coll? data) {"source" data})
:signals signals}
:props (cond-> {:data (cond
(map? data) data
(coll? data) {"source" data})}
signals
(assoc :signals signals)
on-signals
(assoc :on-signals on-signals))
:desc {:fx/type fx.ext.web-view/with-engine-props
:props {:content (html spec opt)
:on-create-popup create-popup}
:desc {:fx/type :web-view}}}))
:desc {:fx/type :web-view
:context-menu-enabled false}}}))

0 comments on commit b3ce9ca

Please sign in to comment.