diff --git a/deps.edn b/deps.edn index 76a7bd50..de6beaaf 100644 --- a/deps.edn +++ b/deps.edn @@ -15,6 +15,7 @@ org.scicloj/tempfiles {:mvn/version "1-beta1"} org.scicloj/kind-portal {:mvn/version "1-beta3"} org.clojure/tools.reader {:mvn/version "1.5.2"} + com.microsoft.playwright/playwright {:mvn/version "1.47.0"} com.nextjournal/beholder {:mvn/version "1.0.2"} babashka/fs {:mvn/version "0.5.25"} org.scicloj/kindly-render {:mvn/version "0.1.5-alpha"} diff --git a/notebooks/pdf_test.clj b/notebooks/pdf_test.clj new file mode 100644 index 00000000..cc7188ff --- /dev/null +++ b/notebooks/pdf_test.clj @@ -0,0 +1,49 @@ +(ns pdf-test + {:clay {:format [:quarto :pdf] + :quarto { + ;; TODO: for some reason the clay.edn causes the format to be unknown, setting it explicitly for now + :format {:pdf {}} + ;;:documentclass "article" + ;;:classoption ["twocolumn"] + ;;:geometry ["top=30mm" "left=20mm" "heightrounded"] + + ;; TODO: again this is just necessary because the project clay.edn puts some html in the include-in-header + :include-in-header {:text "\\AddToHook{env/Highlighting/begin}{\\small}"}}}} + (:require [tablecloth.api :as tc] + [scicloj.tableplot.v1.plotly :as tp])) + +;; this is an example pdf + +;; --- + +;; if we want a new page, use latex + +;; \newpage + +(def scatter-ds + (tc/dataset {:x [1 2 3 4 5] + :y [10 20 15 25 18]})) + +(-> scatter-ds + (tp/base {:=title "Sample Scatter Plot"}) + (tp/layer-point {:=x :x + :=y :y})) + +(comment + (require '[scicloj.clay.v2.api :as clay]) + + ;; use playwright + (clay/make! {:source-path "notebooks/pdf_test.clj" + :kindly/options {:playwright true + :plotly-ext "svg"}}) + (clay/make! {:source-path "notebooks/pdf_test.clj" + :kindly/options {:playwright true + :plotly-ext "png"}}) + + ;; use python + (clay/make! {:source-path "notebooks/pdf_test.clj" + :kindly/options {:plotly-ext "svg"}}) + (clay/make! {:source-path "notebooks/pdf_test.clj" + :kindly/options {:plotly-ext "png"}}) + + ) diff --git a/src/scicloj/clay/v2/item.clj b/src/scicloj/clay/v2/item.clj index b4638ad5..3cf1afcc 100644 --- a/src/scicloj/clay/v2/item.clj +++ b/src/scicloj/clay/v2/item.clj @@ -4,7 +4,8 @@ [scicloj.kind-portal.v1.prepare :as kind-portal] [scicloj.kindly-render.shared.jso :as jso] [scicloj.clay.v2.files :as files] - [scicloj.clay.v2.plotly-export :as plotly-export] + [scicloj.clay.v2.static.plotly-python :as plotly-python] + [scicloj.clay.v2.static.plotly-playwright :as plotly-playwright] [scicloj.clay.v2.util.image :as util.image] [scicloj.clay.v2.util.meta :as meta] [clj-commons.format.exceptions :as fe])) @@ -324,11 +325,14 @@ config {}}} :value}] (if (static? context) (let [[plot-path relative-path] - (files/next-file! context "plotly-chart" value ".png") - exit (plotly-export/export-plot! plot-path data layout)] - (if (zero? exit) - (println "Clay plotly-export:" [:wrote plot-path]) - (println "Clay plotly-export failed.")) + (files/next-file! context "plotly-chart" value + (str "." (or (:plotly-ext options) "svg"))) + success (if (:playwright options) + (plotly-playwright/export-plot! plot-path data layout) + (plotly-python/export-plot! plot-path data layout))] + (if success + (println "Clay plotly export:" [:wrote plot-path]) + (println "Clay plotly export failed.")) {:md (str "![" (:caption options) "](" relative-path ")")}) {:hiccup [:div diff --git a/src/scicloj/clay/v2/page.clj b/src/scicloj/clay/v2/page.clj index b30e259a..c362d715 100644 --- a/src/scicloj/clay/v2/page.clj +++ b/src/scicloj/clay/v2/page.clj @@ -274,11 +274,13 @@ :keys [title favicon quarto format]}] (let [quarto-target (or (second format) :html)] (cond-> quarto - ;; Users may provide non-quarto specific configuration (see also html), - ;; if so this will be added to the quarto front-matter to make them behave the same way - title (assoc-in [:format quarto-target :title] title) - favicon (update-in [:format quarto-target :include-in-header :text] - str "")))) + ;; Users may provide non-quarto specific configuration (see also html), + ;; if so this will be added to the quarto front-matter to make them behave the same way + title (assoc-in [:format quarto-target :title] title) + ;; favicon only applies to html + (and favicon (= quarto-target :html)) + (update-in [:format :html :include-in-header :text] + str "")))) (defn md [{:as spec :keys [items]}] diff --git a/src/scicloj/clay/v2/static/plotly_playwright.clj b/src/scicloj/clay/v2/static/plotly_playwright.clj new file mode 100644 index 00000000..4d6ba67d --- /dev/null +++ b/src/scicloj/clay/v2/static/plotly_playwright.clj @@ -0,0 +1,57 @@ +(ns scicloj.clay.v2.static.plotly-playwright + (:require [scicloj.kindly-render.shared.jso :as jso] + [clojure.java.io :as io] + [clojure.string :as str] + [babashka.fs :as fs] + [hiccup.core :as hiccup]) + (:import (com.microsoft.playwright Playwright Locator$WaitForOptions) + (java.net URLDecoder) + (java.util Base64))) + +(set! *warn-on-reflection* true) + +(defn plotly-html [data layout] + (hiccup/html + [:html + [:head + [:meta {:charset "UTF-8"}] + [:meta {:name "viewport" :content "width=device-width, initial-scale=1"}] + ;; TODO: This is duplicated from `scicloj.clay.v2.page`, + ;; it should be a shared def but under the current structure that causes a cyclic dependency. + [:script {:src "https://cdnjs.cloudflare.com/ajax/libs/plotly.js/2.20.0/plotly.min.js"}]] + [:body + [:div#myDiv]] + [:script + (str "var data = " (jso/write-json-str data) ";" + "var layout = " (jso/write-json-str layout) ";" + "Plotly.newPlot('myDiv', data, layout, {staticPlot: true});")]])) + +(defn export-plot! [filename data layout] + (let [ext (fs/extension filename)] + (with-open [playwright (Playwright/create) + browser (.launch (.chromium playwright))] + (let [page (.newPage browser)] + (.setContent page (plotly-html data layout)) + (let [locator (.locator page "#myDiv") + wait-opts (doto (Locator$WaitForOptions.) + (.setTimeout 10000))] + (.waitFor locator wait-opts) + ;; Plotly.toImage on the rendered div returns encoded image + (let [img-data-uri (.evaluate page (str "Plotly.toImage(document.getElementById('myDiv'), {format: '" ext "'})")) + [_header payload] (str/split img-data-uri #"," 2)] + (io/make-parents filename) + (if (= ext "svg") + (->> (URLDecoder/decode ^String payload "UTF-8") + (spit filename)) + (with-open [out (io/output-stream filename)] + (.write out (.decode (Base64/getDecoder) ^String payload)))) + :success)))))) + +(comment + (export-plot! "plotly_chart.svg" + [{:y [1 2 3 4 5] :type "bar"}] + {:title "Playwright Plotly"}) + (export-plot! "plotly_chart.png" + [{:y [1 2 3 4 5] :type "bar"}] + {:title "Playwright Plotly"}) + :-) diff --git a/src/scicloj/clay/v2/plotly_export.clj b/src/scicloj/clay/v2/static/plotly_python.clj similarity index 95% rename from src/scicloj/clay/v2/plotly_export.clj rename to src/scicloj/clay/v2/static/plotly_python.clj index f73a39ce..eaa88a36 100644 --- a/src/scicloj/clay/v2/plotly_export.clj +++ b/src/scicloj/clay/v2/static/plotly_python.clj @@ -1,4 +1,4 @@ -(ns scicloj.clay.v2.plotly-export +(ns scicloj.clay.v2.static.plotly-python (:require [scicloj.kindly-render.shared.jso :as jso] [clojure.java.shell :as shell])) @@ -33,4 +33,4 @@ fig.write_image(filename) (when (seq err) (binding [*out* *err*] (println err))) - exit)) + (zero? exit)))