😺

Biffweb で ApexChart を Hyperscript で表示する

2024/09/23に公開

1. Biffweb で Chartを表示したい

今回は、ApexChart Get Started にあるチャート例を Biffweb 上で表示したいと思います。

Creating Your First JavaScript Chart – ApexCharts.js

データはなるべくClojureで処理し、最小限のJSしか書かずに、Hyperscriptでチャートをロードすることを目標としています。

作ったものはここに公開しています:

https://github.com/shinseitaro/draw-chart-on-biffweb

2. 前準備

2.1. Project 作成

いつものように project を作成します
Create a project | Biff

2.2. ApexCharts を Install

ui.clj に script タグを追加するだけです。

src/com/example/ui.clj
(defn base [{:keys [::recaptcha] :as ctx} & body]
  (apply
   biff/base-html
   (-> ctx
       (merge #:base{:title settings/app-name
                     :lang "en-US"
                     :icon "/img/glider.png"
                     :description (str settings/app-name " Description")
                     :image "https://clojure.org/images/clojure-logo-120b.png"})
       (update :base/head (fn [head]
                            (concat [[:link {:rel "stylesheet" :href (css-path)}]
+                                    [:script {:src "https://cdn.jsdelivr.net/npm/apexcharts"}]
                                     [:script {:src (js-path)}]
                                     [:script {:src "https://unpkg.com/htmx.org@1.9.12"}]
                                     [:script {:src "https://unpkg.com/htmx.org@1.9.12/dist/ext/ws.js"}]
                                     [:script {:src "https://unpkg.com/hyperscript.org@0.9.8"}]
                                     (when recaptcha
                                       [:script {:src "https://www.google.com/recaptcha/api.js"
                                                 :async "async" :defer "defer"}])]
                                    head))))
   body))

2.3. チャート用の namespace を準備

チャートを表示するためのハンドラ関数とルーティングを作成します。

  • src/com/example/chart.clj (新規作成)
(ns com.example.chart
  (:require [com.example.ui :as ui]
            [com.biffweb :as biff]))

;; データフェッチ用ハンドラ関数(後で書く)
(defn get-data [])

;; 表示用ハンドラ
(defn chart-page [ctx]
  (ui/page
   {:base/title (str "drawing charts on biffweb")}
   [:h1 {:class "m-4 text-4xl font-extrabold dark:text-white"} "Chart"]))

;; ルーティング
(def module
  {:routes ["/chart"
            ["" {:get chart-page}]
            ["/sales" {:get get-data}]]})

2.4. routing 設定

/chart でチャートを表示できるようにルーティングモジュールを追加します。

  • src/com/example.clj
(ns com.example
  (:require [com.biffweb :as biff]
            [com.example.email :as email]
            [com.example.app :as app]
            [com.example.home :as home]
            [com.example.middleware :as mid]
            [com.example.ui :as ui]
            [com.example.worker :as worker]
            [com.example.schema :as schema]
            [clojure.test :as test]
            [clojure.tools.logging :as log]
            [clojure.tools.namespace.repl :as tn-repl]
            [malli.core :as malc]
            [malli.registry :as malr]
            [nrepl.cmdline :as nrepl-cmd]
+           [com.example.chart :as chart]
            )

  (:gen-class))

(def modules
  [app/module
   (biff/authentication-module {})
   home/module
   schema/module
   worker/module
+  chart/module
   ])

2.5. 一旦確認

サーバを起動してたあと http://localhost:8080/chart を確認してください。

clj -M:dev dev

"Chart" と表示されたらOKです。

3. データ

3.1. REPL 起動

テストデータを挿入するためにREPLを使います。お使いのEditorなどでREPLを起動してください。

3.2. テストデータの確認

先程の Creating Your First JavaScript Chart – ApexCharts.js のデータを使います。JSを見るとデータは下記のように定義されています

var options = {
  chart: {
    type: 'line'
  },
  series: [{
    name: 'sales',
    data: [30,40,35,50,49,60,70,91,125]
  }],
  xaxis: {
    categories: [1991,1992,1993,1994,1995,1996,1997, 1998,1999]
  }
}
var chart = new ApexCharts(document.querySelector("#chart"), options);
chart.render();

この datacategories に合わせて schema と fixture を作ってXTDBへSubmitします。

3.3. schema 作成

src/com/example/schema.clj
(ns com.example.schema)

(def schema
  {:user/id :uuid
   :user [:map {:closed true}
          [:xt/id                     :user/id]
          [:user/email                :string]
          [:user/joined-at            inst?]
          [:user/foo {:optional true} :string]
          [:user/bar {:optional true} :string]]

   :msg/id :uuid
   :msg [:map {:closed true}
         [:xt/id       :msg/id]
         [:msg/user    :user/id]
         [:msg/text    :string]
         [:msg/sent-at inst?]]

+   :sales/id :uuid
+   :sales    [:map {:closed true}
+              [:xt/id :sales/id]
+              [:sales/year :int]
+              [:sales/data :int]]
  })

3.4. テストデータ記述

続いて、Trasaction Docmentの形でテストデータを記述します。

resources/fixtures.edn
[{:db/doc-type :sales
  :sales/year  1991
  :sales/data  30}
 {:db/doc-type :sales
  :sales/year  1992
  :sales/data  40}
 {:db/doc-type :sales
  :sales/year  1993
  :sales/data  35}
 {:db/doc-type :sales
  :sales/year  1994
  :sales/data  50}
 {:db/doc-type :sales
  :sales/year  1995
  :sales/data  49}
 {:db/doc-type :sales
  :sales/year  1996
  :sales/data  60}
 {:db/doc-type :sales
  :sales/year  1997
  :sales/data  70}
 {:db/doc-type :sales
  :sales/year  1998
  :sales/data  91}
 {:db/doc-type :sales
  :sales/year  1999
  :sales/data  125}]

3.5. テストデータ挿入

dev/repl.clj をREPLで評価し、コメントの中にある (add-fixtures) を評価してテストデータをSubmitします。データを確認したい場合は、データ確認用クエリをコピペして貼り付け確認してください。

(comment 
 (add-fixtures) ;; 評価すると↑の fixture データが入る

 ;; データ確認用クエリ
 (let [{:keys [biff/db] :as ctx} (get-context)]
    (q db
       '{:find (pull e [*])
         :where [[e :sales/data]]}))
 )

3.6. ApexChart 形式のデータを作成する関数を書く

ApexChartにデータを渡すときには、こういった形式でデータを渡す必要があります。

  chart: {
    type: 'bar'
  },
  series: [{
    name: 'sales',
    data: [30,40,35,50,49,60,70,91,125]
  }],
  xaxis: {
    categories: [1991,1992,1993,1994,1995,1996,1997, 1998,1999]
  }

そのための関数を、もちろんClojureで書きます。ただしJSONの形式で渡す必要があるので、ClojureのHash-mapで作ったあとにJSONに変換します。

src/com/example/chart.clj
(ns com.example.chart
  (:require [com.example.ui :as ui]
            [com.biffweb :as biff]
+           [cheshire.core :as cheshire]))

+ (defn serialize-sales-data [sales-data]
+   (let [sort-by-year (sort-by :sales/year sales-data)
+         year         (map :sales/year sort-by-year)
+         sales        (map :sales/data sort-by-year)]
+     (cheshire/generate-string
+      {:chart  {:type "bar"}
+       :series [{:name "sales"
+                 :data sales}]
+       :xaxis  {:categories year}})))

+ (defn get-data
+   [{:keys [biff/db]
+     :as   ctx}]
+   (let [data (biff/q db
+                      '{:find  (pull e [*])
+                        :where [[e :sales/year]]})]
+     (serialize-sales-data data)))

cheshire/generate-string は clojure hash-map を json にシリアライズしてくれる便利ライブラリです。XTDB からクエリしてきた sale data を JSON に変換しておき、あとでApexChartに渡します。

4. チャート描画

get-data で得たデータをApexChart で描画するために必要最低限のJSを main.js に書きます。そのための工夫として以下の2つを行っています。

  1. データは data-chart-contents 要素に保存する
  2. 書いたJSをページをロードするタイミングで hyperscript で呼び出す

4.1. Hyperscript でページロードのタイミングで描画する

src/com/example/chart.clj
(defn chart-page [ctx]
  (ui/page
   {:base/title (str "drawing charts on biffweb")}
   [:h1 {:class "m-4 text-4xl font-extrabold dark:text-white"} "Chart"]
+  [:div {:class "flex flex-col"}
+   [:p "chart"
+    (let [sales-data (get-data ctx)]
+      [:div {:id                  "chart"
+             :data-chart-contents sales-data
+             :_                   "on load call renderChart(me)"}])]]))

hyperscrip "on load call renderChart(me)" は、以下の様に振る舞います。

  • div#chart 要素 に対して on load call というイベントハンドラが適用されます。
  • div#chart の中で実行されたデータは自分自身が持っている状態です。
  • me は hyperscript の特別語で自分自身を表します。
  • つまり medata-chart-contentsという属性をもち、その値はXTDBからフェッチしてきたデータということになります。

4.2. renderChart 関数 (JS)

次に Javascript を書きます。JSは、Biffweb のテンプレートに用意されている resources/public/js/main.js にJSを書けばOKです。

上記で説明したように、自分の属性である data-chart-contents に保存した sales-data を 取得してChartに渡すように定義します。

resources/public/js/main.js
function renderChart(element) {
    var options = JSON.parse(element.getAttribute('data-chart-contents'));
    var chart = new ApexCharts(document.querySelector("#chart"), options);
    // var chart = new ApexCharts(document.getElementById("chart"), options); ByIdでももちろんOK
    chart.render();
}

5. 完成

これで完成です。

6. 感想

  • Biffwebで書いている限り、とことんClojureで書いて、本当にどうしても必要なところだけJSを書くということができます。
  • Hyperscriptになれるのは、ちょっと大変です。
  • Hyperscriptもなるべく小さく短く書くことをおすすめします。とにかくなるべくClojureを書いたほうがいいです。
  • 今度はHTMXも含めた何かを書きたいと思います。

7. 参照

Discussion