diff --git a/.gitignore b/.gitignore index 713f40a..94ac55c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ out *.tgz *.zip +.lsp/ +.clj-kondo/ diff --git a/.lein-failures b/.lein-failures new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.lein-failures @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/README.md b/README.md index 41864ea..a757805 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# swingometer +# radial-svg-graph -A [re-frame](https://github.com/Day8/re-frame) application designed to show votes in an election. +A [re-frame](https://github.com/Day8/re-frame) application designed to show a radial SVG graph, possibly with several rings. ## Development Mode diff --git a/project.clj b/project.clj index 5f25843..82d098d 100644 --- a/project.clj +++ b/project.clj @@ -1,11 +1,17 @@ -(defproject swingometer "0.1.0-SNAPSHOT" - :dependencies [[org.clojure/clojure "1.8.0"] - [org.clojure/clojurescript "1.9.229"] - [reagent "0.6.0"] - [re-frame "0.9.4"] - [re-com "2.0.0"]] +(defproject rsvggraph "0.1.0-SNAPSHOT" + :dependencies [[clojure2d "1.4.5"] ;; (mainly) for colours + [generateme/fastmath "2.4.0"] + [hiccup "2.0.0-RC3"] + [javax.xml.bind/jaxb-api "2.4.0-b180830.0359"] + [org.clojure/clojure "1.8.0"] + ;; [org.clojure/clojurescript "1.9.229"] + ;; [org.omcljs/om "1.0.0-beta1"] + ;; [reagent "0.6.0"] + ;; [re-frame "0.9.4"] + ;; [re-com "2.0.0"] + ] - :plugins [[lein-cljsbuild "1.1.4"]] + ;; :plugins [[lein-cljsbuild "1.1.4"]] :min-lein-version "2.5.3" @@ -15,36 +21,32 @@ :figwheel {:css-dirs ["resources/public/css"]} - :profiles - {:dev - {:dependencies [[binaryage/devtools "0.8.2"]] + ;; :profiles + ;; {:dev + ;; {:dependencies [[binaryage/devtools "0.8.2"]] - :plugins [[lein-figwheel "0.5.9"]] - }} + ;; :plugins [[lein-figwheel "0.5.9"]] + ;; }} - :cljsbuild - {:builds - [{:id "dev" - :source-paths ["src/cljs"] - :figwheel {:on-jsload "swingometer.core/mount-root"} - :compiler {:main swingometer.core - :output-to "resources/public/js/compiled/app.js" - :output-dir "resources/public/js/compiled/out" - :asset-path "js/compiled/out" - :source-map-timestamp true - :preloads [devtools.preload] - :external-config {:devtools/config {:features-to-install :all}} - }} + ;; :cljsbuild + ;; {:builds + ;; [{:id "dev" + ;; :source-paths ["src/cljs"] + ;; :figwheel {:on-jsload "rsvggraph.core/mount-root"} + ;; :compiler {:main rsvggraph.core + ;; :output-to "resources/public/js/compiled/app.js" + ;; :output-dir "resources/public/js/compiled/out" + ;; :asset-path "js/compiled/out" + ;; :source-map-timestamp true + ;; :preloads [devtools.preload] + ;; :external-config {:devtools/config {:features-to-install :all}} + ;; }} - {:id "min" - :source-paths ["src/cljs"] - :compiler {:main swingometer.core - :output-to "resources/public/js/compiled/app.js" - :optimizations :advanced - :closure-defines {goog.DEBUG false} - :pretty-print false}} - - - ]} - - ) + ;; {:id "min" + ;; :source-paths ["src/cljs"] + ;; :compiler {:main rsvggraph.core + ;; :output-to "resources/public/js/compiled/app.js" + ;; :optimizations :advanced + ;; :closure-defines {goog.DEBUG false} + ;; :pretty-print false}}]}) +) diff --git a/resources/public/css/swingometer.css b/resources/public/css/rsvggraph.css similarity index 64% rename from resources/public/css/swingometer.css rename to resources/public/css/rsvggraph.css index fe3fe55..fc64d5b 100644 --- a/resources/public/css/swingometer.css +++ b/resources/public/css/rsvggraph.css @@ -1,70 +1,73 @@ /***************************************************************************\ * * - * swinging-needle-meter.css * + * rsvggraph.css * * * - * CSS styling for the swinging needle meter itself. * + * CSS styling for the radial svg graph itself. * * * \***************************************************************************/ -.snm-cursor { +svg { + border: thin solid gray; + object-fit: contain; +} + +.rsvggraph-cursor { stroke:#ff8500; stroke-width: 3%; stroke-opacity: 0.5; } -.snm-frame { +.rsvggraph-frame { fill: none; - stroke-width: 5%; - stroke-linejoin: round; - stroke: #444444; + stroke: none; } -.snm-gradation path { +.rsvggraph-gradation path { stroke: black; stroke-width: 1; } -.snm-gradation text { +.rsvggraph-gradation text { font-size: 200%; font-weight: lighter; } -.snm-hub { +.rsvggraph-hub { fill: #444444; } -.snm-meter { +.rsvggraph-graph { height: 50%; width: auto; } -.snm-needle { +.rsvggraph-needle { stroke: black; stroke-width: 1; } -.snm-redzone { +.rsvggraph-redzone { fill:none; stroke: maroon; stroke-width: 10%; } -.snm-scale { +.rsvggraph-scale { fill: none; stroke: silver; stroke-width: 10%; } -.snm-target .snm-frame { +.rsvggraph-target .rsvggraph-frame { stroke: green; } -.snm-value { +.rsvggraph-value { font-size: 400%; font-weight: bold; text-align: center; } -.snm-warning .snm-frame { +.rsvggraph-warning .rsvggraph-frame { stroke: maroon; } diff --git a/resources/public/index.html b/resources/public/index.html index f4b2849..70e6e2a 100644 --- a/resources/public/index.html +++ b/resources/public/index.html @@ -5,16 +5,16 @@ - + - Example swingometer following re-com conventions. + Example rsvggraph following re-com conventions.
- + diff --git a/resources/public/sample-data.edn b/resources/public/sample-data.edn new file mode 100644 index 0000000..fe1ca2b --- /dev/null +++ b/resources/public/sample-data.edn @@ -0,0 +1,40 @@ +{:id "ge2024" + :label "UK General Election 2024" + :children [{:id "no-show" + :label "Did not vote" + :magnitude 18365357} + {:id "voted" + :label "Voted" + :children [{:id "reform" + :label "Reform UK Ltd." + :magnitude 4091549} + {:id "greenew" + :label "Green Party of England and Wales" + :magnitude 1939502} + {:id "pc" + :label "Plaid Cymru" + :magnitude 194811} + {:id "sf" + :label "Sinn Féin" + :magnitude 210891} + {:id "ld" + :label "Liberal Democrats" + :magnitude 3499933} + {:id "labour" + :label "Labour" + :magnitude 9712011} + {:id "apni" + :label "Alliance Party" + :magnitude 117191} + {:id "sdlp" + :label "Social Democratic and Labour Party" + :magnitude 86861} + {:id "dup" + :label "Democratic Unionist Party" + :magnitude 172058} + {:id "snp" + :label "Scottish National Party" + :magnitude 708759} + {:id "con" + :label "Conservative" + :magnitude 6814469}]}]} \ No newline at end of file diff --git a/src/clj/rsvggraph/core.clj b/src/clj/rsvggraph/core.clj new file mode 100644 index 0000000..b08bdd2 --- /dev/null +++ b/src/clj/rsvggraph/core.clj @@ -0,0 +1 @@ +(ns rsvggraph.core) diff --git a/src/clj/rsvggraph/data.clj b/src/clj/rsvggraph/data.clj new file mode 100644 index 0000000..090e1df --- /dev/null +++ b/src/clj/rsvggraph/data.clj @@ -0,0 +1,29 @@ +(ns rsvggraph.data + "Normalise data for use in generating radial graphs." + (:require [clojure2d.color :refer [gradient]])) + + +(def ;; ^:dynamic + *gradient* + "The gradient to use to automatically assign pleasing colours to sectors, if + no colours are defined in the data. Suitable gradients are defined + [here](https://clojure2d.github.io/clojure2d/docs/static/gradients/)." + :rainbow2) + +(def children-fn + "Basic (overridable) children function; assumes `data` is a map, and returns + the value of the `:children` key within that map." + (memoize (fn [data] + (:children data)))) + +(def quantity-fn + "Basic (overridable) children function; assumes `data` is a map. If the value + of the `:children` key within that map is a sequence, sums the result of + mapping itself over that sequence. Otherwise, returns the value of the + `:quantity` key, if present and a number, or `1` as a final default." + (memoize (fn [data] + (let [c (children-fn data) + q (:quantity data)] + (cond (coll? c) (reduce + 0 (map quantity-fn c)) + (number? q) q + :else 1))))) \ No newline at end of file diff --git a/src/clj/swingometer/core.clj b/src/clj/swingometer/core.clj deleted file mode 100644 index 074ad0f..0000000 --- a/src/clj/swingometer/core.clj +++ /dev/null @@ -1 +0,0 @@ -(ns swingometer.core) diff --git a/src/cljs/swingometer/config.cljs b/src/cljs/rsvggraph/config.cljs similarity index 60% rename from src/cljs/swingometer/config.cljs rename to src/cljs/rsvggraph/config.cljs index 28207bf..e8ee763 100644 --- a/src/cljs/swingometer/config.cljs +++ b/src/cljs/rsvggraph/config.cljs @@ -1,4 +1,4 @@ -(ns swingometer.config) +(ns rsvggraph.config) (def debug? ^boolean goog.DEBUG) diff --git a/src/cljs/swingometer/core.cljs b/src/cljs/rsvggraph/core.cljs similarity index 71% rename from src/cljs/swingometer/core.cljs rename to src/cljs/rsvggraph/core.cljs index c3ff728..a2c11de 100644 --- a/src/cljs/swingometer/core.cljs +++ b/src/cljs/rsvggraph/core.cljs @@ -1,10 +1,10 @@ -(ns swingometer.core +(ns rsvggraph.core (:require [reagent.core :as reagent] [re-frame.core :as re-frame] - [swingometer.events] - [swingometer.subs] - [swingometer.views :as views] - [swingometer.config :as config])) + [rsvggraph.events] + [rsvggraph.subs] + [rsvggraph.views :as views] + [rsvggraph.config :as config])) (defn dev-setup [] diff --git a/src/cljs/swingometer/db.cljs b/src/cljs/rsvggraph/db.cljs similarity index 66% rename from src/cljs/swingometer/db.cljs rename to src/cljs/rsvggraph/db.cljs index 01662fd..bc6b00e 100644 --- a/src/cljs/swingometer/db.cljs +++ b/src/cljs/rsvggraph/db.cljs @@ -1,4 +1,4 @@ -(ns swingometer.db) +(ns rsvggraph.db) (def default-db {:name "re-frame"}) diff --git a/src/cljs/swingometer/events.cljs b/src/cljs/rsvggraph/events.cljs similarity index 64% rename from src/cljs/swingometer/events.cljs rename to src/cljs/rsvggraph/events.cljs index 36613fd..9a733d8 100644 --- a/src/cljs/swingometer/events.cljs +++ b/src/cljs/rsvggraph/events.cljs @@ -1,6 +1,6 @@ -(ns swingometer.events +(ns rsvggraph.events (:require [re-frame.core :as re-frame] - [swingometer.db :as db])) + [rsvggraph.db :as db])) (re-frame/reg-event-db :initialize-db diff --git a/src/cljs/swingometer/swingometer.cljs b/src/cljs/rsvggraph/rsvggraph.cljs similarity index 81% rename from src/cljs/swingometer/swingometer.cljs rename to src/cljs/rsvggraph/rsvggraph.cljs index d096e85..521488b 100644 --- a/src/cljs/swingometer/swingometer.cljs +++ b/src/cljs/rsvggraph/rsvggraph.cljs @@ -1,14 +1,13 @@ -(ns swingometer.swingometer +(ns rsvggraph.rsvggraph (:require [clojure.string :as string] [re-com.core :refer [h-box v-box box gap line label title slider checkbox p]] [re-com.box :refer [flex-child-style]] [re-com.util :refer [deref-or-value]] - [re-com.validate :refer [number-or-string? css-style? html-attr? validate-args-macro]] - [reagent.core :as reagent])) + [re-com.validate :refer [number-or-string? css-style? html-attr? validate-args-macro]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; -;;;; swingometer: an experiment in animating SVG from re-frame. +;;;; rsvggraph: an experiment in animating SVG from re-frame. ;;;; Draws heavily on re-com.. ;;;; ;;;; This program is free software; you can redistribute it and/or @@ -31,12 +30,12 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ------------------------------------------------------------------------------------ -;; Component: swingometer +;; Component: rsvggraph ;; ------------------------------------------------------------------------------------ ;;; It seems the defaults given here are just documentation; the defaults ;;; that are actually used are those given in the :or clause of the argument map. -(def swingometer-args-desc +(def rsvggraph-args-desc [{:name :model :required true :type "map | atom" :validate-fn map? :description "A map mapping keys to maps of the following structure: {:id :snp :name \"Scottish National Party\" :colour \"yellow\" :votes 1234}"} {:name :width :required false :type "integer" :default "300" @@ -45,12 +44,12 @@ :validate-fn integer? :description "a CSS height"} {:name :class :required false :type "string" :validate-fn string? :description "CSS class names, space separated, for the top-level SVG element"} - {:name :frame-class :required false :type "string" :default "snm-frame" + {:name :frame-class :required false :type "string" :default "rsvggraph-frame" :validate-fn string? :description "CSS class names, space separated, for the frame"} - {:name :scale-class :required false :type "string" :default "snm-scale" + {:name :scale-class :required false :type "string" :default "rsvggraph-scale" :validate-fn string? :description "CSS class names, space separated, for the scale"} - {:name :id :required false :type "string" :default "meter" - :validate-fn string? :description "Element id for this instance of the meter"} + {:name :id :required false :type "string" :default "graph" + :validate-fn string? :description "Element id for this instance of the graph"} {:name :gradations :reduired false :type "integer" :default 5 :validate-fn integer? :description "Number of gradations to show on the scale, not counting the point."} {:name :style :required false :type "CSS style map" @@ -59,9 +58,9 @@ :validate-fn html-attr? :description [:span "HTML attributes, like " [:code ":on-mouse-move"] [:br] "No " [:code ":class"] " or " [:code ":style"] "allowed"]}]) -;; the constant 140 represents the full sweep of the needle -;; from the left end of the scale to right end, in degrees. -(def full-scale-deflection 140) +(def full-scale-deflection + "the full sweep of the needle from the left end of the scale to right end, in degrees." + 360) (defn deflection @@ -112,7 +111,7 @@ at `cx`, cy` starting at `min-radius` and extending to `max-radius`, with the specified `label`." [cx cy min-radius max-radius angle label] - [:g {:class "snm-gradation" + [:g {:class "rsvggraph-gradation" :transform (string/join " " ["rotate(" angle cx cy ")"])} [:path {:d (string/join " " @@ -157,63 +156,68 @@ others (recursively-draw-segments (rest still-to-do) (cons party done) total-votes cx cy radius) vote-share (* (/ (:votes party) total-votes) 100)] (if (> vote-share 1) - (cons [:g [:path {:class "snm-scale" + (cons [:g [:path {:class "rsvggraph-scale" :id (str (:id party) "-segment") :style {:stroke (:colour party)} :d (describe-arc cx cy radius start-angle end-angle)}] (gradation cx cy (* radius 0.8) (* radius 1.1) start-angle (str - (if (> vote-share 5) (name (:id party)) "") - (if (> vote-share 10) (str " " (as-label vote-share) "%"))))] + (when (> vote-share 5) (name (:id party)) "") + (when (> vote-share 10) (str " " (as-label vote-share) "%"))))] others) others)))) -(defn swingometer - "Render an SVG swinging needle meter" +(defn rsvggraph + "Render an SVG radial graph. The idea here is there is a stack of rings, + each with zero or more segments. Each ring has an inner diameter and an + outer diameter, each of which is expressed as a number in the range 0...1, + representing a fraction of the overall dimension of the graph. + + The rings are drawn in ascending order of inner diameter. + + Each segment has a label and a magnitude" [& {:keys [model width height class scale-class frame-class id style attr] :or {width 300 - height 200 - scale-class "snm-scale" - frame-class "snm-frame" - id "meter"} + height 300 + scale-class "rsvggraph-scale" + frame-class "rsvggraph-frame" + id "graph"} :as args}] - {:pre [(validate-args-macro swingometer-args-desc args "swingometer")]} + {:pre [(validate-args-macro rsvggraph-args-desc args "rsvggraph")]} (let [model (deref-or-value model) mid-point-deflection (/ full-scale-deflection 2) - cx (/ width 2) - cy (* height 0.90) - needle-length (* height 0.75) - scale-radius (* height 0.7) - gradation-inner (* height 0.55) - gradations 5 + dimension (min width height) + cx (/ dimension 2) + cy (* dimension 0.50) + scale-radius (* dimension 0.45) total-votes (reduce + (map #(:votes %) (vals model)))] [box :align :start :child [:div (merge - {:class (str "swingometer " class) + {:class (str "rsvggraph " class) :style (merge (flex-child-style "none") - {:width width :height height} + {:width dimension :height dimension} style)} attr) [:svg {:xmlSpace "preserve" :overflow "visible" - :viewBox (string/join " " [0 0 width height]) - :width (str width "px") - :height (str height "px") + :viewBox (string/join " " [0 0 dimension dimension]) + :width (str dimension "px") + :height (str dimension "px") :y "0px" :x "0px" :version "1.1" :id id - :class (str "snm-meter " class)} + :class (str "rsvggraph-graph " class)} [:text {:text-anchor "middle" - :x (/ width 2) - :y (/ height 2) - :width "100" + :x (/ dimension 2) + :y (/ dimension 2) + :width (/ dimension 4) :id (str id "-total-votes") - :class "snm-value"}[:tspan (reduce + (map :votes (vals model)))]] + :class "rsvggraph-value"}[:tspan (reduce + (map :votes (vals model)))]] [:path {:class scale-class :id (str id "-scale") :d (describe-arc cx cy scale-radius diff --git a/src/cljs/swingometer/subs.cljs b/src/cljs/rsvggraph/subs.cljs similarity index 87% rename from src/cljs/swingometer/subs.cljs rename to src/cljs/rsvggraph/subs.cljs index 97d24be..bee62c1 100644 --- a/src/cljs/swingometer/subs.cljs +++ b/src/cljs/rsvggraph/subs.cljs @@ -1,4 +1,4 @@ -(ns swingometer.subs +(ns rsvggraph.subs (:require-macros [reagent.ratom :refer [reaction]]) (:require [re-frame.core :as re-frame])) diff --git a/src/cljs/swingometer/utils.cljs b/src/cljs/rsvggraph/utils.cljs similarity index 99% rename from src/cljs/swingometer/utils.cljs rename to src/cljs/rsvggraph/utils.cljs index e6b8dde..09184df 100644 --- a/src/cljs/swingometer/utils.cljs +++ b/src/cljs/rsvggraph/utils.cljs @@ -1,4 +1,4 @@ -(ns swingometer.utils +(ns rsvggraph.utils (:require [re-com.core :refer [h-box v-box box gap title line label hyperlink-href align-style]])) ;;;; This file is just stolen wholesale from re-demo in the re-com package; diff --git a/src/cljs/swingometer/views.cljs b/src/cljs/rsvggraph/views.cljs similarity index 94% rename from src/cljs/swingometer/views.cljs rename to src/cljs/rsvggraph/views.cljs index 44914f4..6cb080f 100644 --- a/src/cljs/swingometer/views.cljs +++ b/src/cljs/rsvggraph/views.cljs @@ -1,12 +1,12 @@ -(ns swingometer.views +(ns rsvggraph.views (:require [re-frame.core :as re-frame] [re-com.core :refer [h-box v-box box gap line label title progress-bar slider checkbox p single-dropdown]] [re-com.util :refer [deref-or-value]] - [swingometer.swingometer :refer [swingometer swingometer-args-desc]] - [swingometer.utils :refer [panel-title title2 args-table github-hyperlink status-text]] + [rsvggraph.rsvggraph :refer [rsvggraph rsvggraph-args-desc]] + [rsvggraph.utils :refer [panel-title title2 args-table github-hyperlink status-text]] [reagent.core :as reagent])) -(defn swingometer-demo +(defn rsvggraph-demo [] (let [model (reagent/atom {:snp {:id :snp :name "Scottish National Party" :colour "yellow" :votes 10} :lab {:id :lab :name "Labour Party" :colour "red" :votes 10} @@ -19,7 +19,7 @@ [v-box :size "auto" :gap "10px" - :children [[panel-title "Swingometer"] + :children [[panel-title "rsvggraph"] [h-box :gap "100px" :children [[v-box @@ -27,21 +27,21 @@ :width "450px" :children [[title2 "Notes"] [status-text "Wildly experimental"] - [p "An SVG swingometer intended to be useful in elections."] + [p "An SVG rsvggraph intended to be useful in elections."] [title2 "Behaviour"] - [args-table swingometer-args-desc]]] + [args-table rsvggraph-args-desc]]] [v-box :gap "10px" :children [[title2 "Demo"] [v-box :gap "20px" - :children [[swingometer + :children [[rsvggraph :model model - :height 600 - :width 1000] + :height 500 + :width 500] [title :level :level3 :label "Parameters"] [h-box :gap "10px" @@ -127,11 +127,11 @@ ;; core holds a reference to panel, so need one level of indirection to get figwheel updates (defn panel [] - [swingometer-demo]) + [rsvggraph-demo]) (defn main-panel [] (fn [] [v-box :height "100%" - :children [[swingometer-demo]]])) + :children [[rsvggraph-demo]]]))