Group prices by hour with expandable quarter details and blue borders

Each hour shows as an open-by-default card with the average price and
indented quarter-hour rows. Added @source directive to Tailwind v4 input
so .clj files are scanned for utility classes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 12:51:07 +01:00
co-authored by Claude Opus 4.6
parent c5ccada192
commit 7e7b28cf85
2 changed files with 44 additions and 22 deletions
+1
View File
@@ -1 +1,2 @@
@import "tailwindcss"; @import "tailwindcss";
@source "../../src/**/*.clj";
+43 -22
View File
@@ -14,13 +14,15 @@
(def ^:private hour-fmt (DateTimeFormatter/ofPattern "HH:00")) (def ^:private hour-fmt (DateTimeFormatter/ofPattern "HH:00"))
(defn- ->ldt [time-dk]
(cond
(instance? LocalDateTime time-dk) time-dk
(instance? java.sql.Timestamp time-dk)
(.toLocalDateTime ^java.sql.Timestamp time-dk)
:else (LocalDateTime/parse (str time-dk))))
(defn- format-hour [time-dk] (defn- format-hour [time-dk]
(let [ldt (cond (.format (->ldt time-dk) hour-fmt))
(instance? LocalDateTime time-dk) time-dk
(instance? java.sql.Timestamp time-dk)
(.toLocalDateTime ^java.sql.Timestamp time-dk)
:else (LocalDateTime/parse (str time-dk)))]
(.format ldt hour-fmt)))
(defn- dkk-mwh->ore-kwh (defn- dkk-mwh->ore-kwh
"Convert DKK/MWh to øre/kWh (divide by 10)." "Convert DKK/MWh to øre/kWh (divide by 10)."
@@ -28,24 +30,43 @@
(when price-dkk (when price-dkk
(/ (double price-dkk) 10.0))) (/ (double price-dkk) 10.0)))
(defn- format-ore [price-dkk]
(if-let [ore (dkk-mwh->ore-kwh price-dkk)]
(format "%.1f" ore)
"-"))
(defn- group-by-hour [prices]
(->> prices
(group-by #(.getHour (->ldt (:time_dk %))))
(sort-by key)))
(defn- avg-ore [quarters]
(let [vals (keep :price_dkk quarters)]
(when (seq vals)
(/ (reduce + 0.0 (map double vals)) (count vals) 10.0))))
(defn- hour-block [hour quarters]
(let [sorted (sort-by #(.getMinute (->ldt (:time_dk %))) quarters)
avg (avg-ore quarters)]
[:details {:class "border-2 border-blue-400 rounded-lg mb-2" :open true}
[:summary {:class "flex justify-between py-2 px-3 cursor-pointer hover:bg-gray-50
font-medium text-gray-900"}
[:span (format "%02d:00" hour)]
[:span {:class "font-mono"}
(if avg (format "%.1f" avg) "-")]]
[:div {:class "border-t-2 border-blue-400 px-3 pb-2"}
(for [q sorted]
[:div {:class "flex justify-between py-1 ml-4 text-gray-500"}
[:span (format "%02d:%02d" hour (.getMinute (->ldt (:time_dk q))))]
[:span {:class "font-mono"} (format-ore (:price_dkk q))]])]]))
(defn- price-table [title prices] (defn- price-table [title prices]
[:div {:class "mt-6"} [:div {:class "mt-6"}
[:h2 {:class "text-lg font-semibold text-gray-900"} title] [:h2 {:class "text-lg font-semibold text-gray-900 mb-2"} title]
(if (seq prices) (if (seq prices)
[:table {:class "mt-2 w-full text-sm"} [:div
[:thead (for [[hour quarters] (group-by-hour prices)]
[:tr {:class "border-b border-gray-200 text-left text-gray-500"} (hour-block hour quarters))]
[:th {:class "py-2 pr-4"} "Time"]
[:th {:class "py-2 text-right"} "øre/kWh"]]]
[:tbody
(for [p prices]
[:tr {:class "border-b border-gray-100"}
[:td {:class "py-2 pr-4 text-gray-700"}
(format-hour (:time_dk p))]
[:td {:class "py-2 text-right font-mono text-gray-900"}
(if-let [ore (dkk-mwh->ore-kwh (:price_dkk p))]
(format "%.1f" ore)
"-")]])]]
[:p {:class "mt-2 text-sm text-gray-400"} "Not available yet"])]) [:p {:class "mt-2 text-sm text-gray-400"} "Not available yet"])])
(defn- price-tables [query-fn] (defn- price-tables [query-fn]
@@ -60,7 +81,7 @@
[:h1 {:class "text-2xl font-bold text-gray-900"} [:h1 {:class "text-2xl font-bold text-gray-900"}
"Electricity Prices"] "Electricity Prices"]
[:p {:class "mt-1 text-sm text-gray-500"} [:p {:class "mt-1 text-sm text-gray-500"}
"Day-ahead spot prices for DK1 in øre/kWh"] "Day-ahead spot prices for DK1 (øre/kWh)"]
[:button {:class "mt-4 rounded-lg bg-blue-600 px-4 py-2 text-white font-medium [:button {:class "mt-4 rounded-lg bg-blue-600 px-4 py-2 text-white font-medium
active:bg-blue-700 transition-colors" active:bg-blue-700 transition-colors"
:hx-post "/prices/fetch" :hx-post "/prices/fetch"