Not so much of a branch, more a whole new project

This commit is contained in:
Simon Brooke 2024-07-08 22:08:10 +01:00
parent 985c91a72e
commit 3a1ae81f08
18 changed files with 204 additions and 123 deletions

2
.gitignore vendored
View file

@ -11,3 +11,5 @@ out
*.tgz *.tgz
*.zip *.zip
.lsp/
.clj-kondo/

1
.lein-failures Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -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 ## Development Mode

View file

@ -1,11 +1,17 @@
(defproject swingometer "0.1.0-SNAPSHOT" (defproject rsvggraph "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.8.0"] :dependencies [[clojure2d "1.4.5"] ;; (mainly) for colours
[org.clojure/clojurescript "1.9.229"] [generateme/fastmath "2.4.0"]
[reagent "0.6.0"] [hiccup "2.0.0-RC3"]
[re-frame "0.9.4"] [javax.xml.bind/jaxb-api "2.4.0-b180830.0359"]
[re-com "2.0.0"]] [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" :min-lein-version "2.5.3"
@ -15,36 +21,32 @@
:figwheel {:css-dirs ["resources/public/css"]} :figwheel {:css-dirs ["resources/public/css"]}
:profiles ;; :profiles
{:dev ;; {:dev
{:dependencies [[binaryage/devtools "0.8.2"]] ;; {:dependencies [[binaryage/devtools "0.8.2"]]
:plugins [[lein-figwheel "0.5.9"]] ;; :plugins [[lein-figwheel "0.5.9"]]
}} ;; }}
:cljsbuild ;; :cljsbuild
{:builds ;; {:builds
[{:id "dev" ;; [{:id "dev"
:source-paths ["src/cljs"] ;; :source-paths ["src/cljs"]
:figwheel {:on-jsload "swingometer.core/mount-root"} ;; :figwheel {:on-jsload "rsvggraph.core/mount-root"}
:compiler {:main swingometer.core ;; :compiler {:main rsvggraph.core
:output-to "resources/public/js/compiled/app.js" ;; :output-to "resources/public/js/compiled/app.js"
:output-dir "resources/public/js/compiled/out" ;; :output-dir "resources/public/js/compiled/out"
:asset-path "js/compiled/out" ;; :asset-path "js/compiled/out"
:source-map-timestamp true ;; :source-map-timestamp true
:preloads [devtools.preload] ;; :preloads [devtools.preload]
:external-config {:devtools/config {:features-to-install :all}} ;; :external-config {:devtools/config {:features-to-install :all}}
}} ;; }}
{:id "min" ;; {:id "min"
:source-paths ["src/cljs"] ;; :source-paths ["src/cljs"]
:compiler {:main swingometer.core ;; :compiler {:main rsvggraph.core
:output-to "resources/public/js/compiled/app.js" ;; :output-to "resources/public/js/compiled/app.js"
:optimizations :advanced ;; :optimizations :advanced
:closure-defines {goog.DEBUG false} ;; :closure-defines {goog.DEBUG false}
:pretty-print false}} ;; :pretty-print false}}]})
)
]}
)

View file

@ -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:#ff8500;
stroke-width: 3%; stroke-width: 3%;
stroke-opacity: 0.5; stroke-opacity: 0.5;
} }
.snm-frame { .rsvggraph-frame {
fill: none; fill: none;
stroke-width: 5%; stroke: none;
stroke-linejoin: round;
stroke: #444444;
} }
.snm-gradation path { .rsvggraph-gradation path {
stroke: black; stroke: black;
stroke-width: 1; stroke-width: 1;
} }
.snm-gradation text { .rsvggraph-gradation text {
font-size: 200%; font-size: 200%;
font-weight: lighter; font-weight: lighter;
} }
.snm-hub { .rsvggraph-hub {
fill: #444444; fill: #444444;
} }
.snm-meter { .rsvggraph-graph {
height: 50%; height: 50%;
width: auto; width: auto;
} }
.snm-needle { .rsvggraph-needle {
stroke: black; stroke: black;
stroke-width: 1; stroke-width: 1;
} }
.snm-redzone { .rsvggraph-redzone {
fill:none; fill:none;
stroke: maroon; stroke: maroon;
stroke-width: 10%; stroke-width: 10%;
} }
.snm-scale { .rsvggraph-scale {
fill: none; fill: none;
stroke: silver; stroke: silver;
stroke-width: 10%; stroke-width: 10%;
} }
.snm-target .snm-frame { .rsvggraph-target .rsvggraph-frame {
stroke: green; stroke: green;
} }
.snm-value { .rsvggraph-value {
font-size: 400%; font-size: 400%;
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
} }
.snm-warning .snm-frame { .rsvggraph-warning .rsvggraph-frame {
stroke: maroon; stroke: maroon;
} }

View file

@ -5,16 +5,16 @@
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.5/css/bootstrap.min.css"> <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="vendor/css/material-design-iconic-font.min.css"> <link rel="stylesheet" href="vendor/css/material-design-iconic-font.min.css">
<link rel="stylesheet" href="css/re-com.css"> <link rel="stylesheet" href="css/re-com.css">
<link rel="stylesheet" href="css/swingometer.css"> <link rel="stylesheet" href="css/rsvggraph.css">
<link href="http://fonts.googleapis.com/css?family=Roboto:300,400,500,700,400italic" rel="stylesheet" type="text/css"> <link href="http://fonts.googleapis.com/css?family=Roboto:300,400,500,700,400italic" rel="stylesheet" type="text/css">
<link href="http://fonts.googleapis.com/css?family=Roboto+Condensed:400,300" rel="stylesheet" type="text/css"> <link href="http://fonts.googleapis.com/css?family=Roboto+Condensed:400,300" rel="stylesheet" type="text/css">
<title>Example swingometer following re-com conventions.</title> <title>Example rsvggraph following re-com conventions.</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script src="js/compiled/app.js"></script> <script src="js/compiled/app.js"></script>
<script>swingometer.core.init();</script> <script>rsvggraph.core.init();</script>
</body> </body>
</html> </html>

View file

@ -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}]}]}

View file

@ -0,0 +1 @@
(ns rsvggraph.core)

View file

@ -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)))))

View file

@ -1 +0,0 @@
(ns swingometer.core)

View file

@ -1,4 +1,4 @@
(ns swingometer.config) (ns rsvggraph.config)
(def debug? (def debug?
^boolean goog.DEBUG) ^boolean goog.DEBUG)

View file

@ -1,10 +1,10 @@
(ns swingometer.core (ns rsvggraph.core
(:require [reagent.core :as reagent] (:require [reagent.core :as reagent]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[swingometer.events] [rsvggraph.events]
[swingometer.subs] [rsvggraph.subs]
[swingometer.views :as views] [rsvggraph.views :as views]
[swingometer.config :as config])) [rsvggraph.config :as config]))
(defn dev-setup [] (defn dev-setup []

View file

@ -1,4 +1,4 @@
(ns swingometer.db) (ns rsvggraph.db)
(def default-db (def default-db
{:name "re-frame"}) {:name "re-frame"})

View file

@ -1,6 +1,6 @@
(ns swingometer.events (ns rsvggraph.events
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as re-frame]
[swingometer.db :as db])) [rsvggraph.db :as db]))
(re-frame/reg-event-db (re-frame/reg-event-db
:initialize-db :initialize-db

View file

@ -1,14 +1,13 @@
(ns swingometer.swingometer (ns rsvggraph.rsvggraph
(:require [clojure.string :as string] (:require [clojure.string :as string]
[re-com.core :refer [h-box v-box box gap line label title slider checkbox p]] [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.box :refer [flex-child-style]]
[re-com.util :refer [deref-or-value]] [re-com.util :refer [deref-or-value]]
[re-com.validate :refer [number-or-string? css-style? html-attr? validate-args-macro]] [re-com.validate :refer [number-or-string? css-style? html-attr? validate-args-macro]]))
[reagent.core :as reagent]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
;;;; swingometer: an experiment in animating SVG from re-frame. ;;;; rsvggraph: an experiment in animating SVG from re-frame.
;;;; Draws heavily on re-com.. ;;;; Draws heavily on re-com..
;;;; ;;;;
;;;; This program is free software; you can redistribute it and/or ;;;; 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 ;;; 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. ;;; 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" [{: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}"} :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" {:name :width :required false :type "integer" :default "300"
@ -45,12 +44,12 @@
:validate-fn integer? :description "a CSS height"} :validate-fn integer? :description "a CSS height"}
{:name :class :required false :type "string" {:name :class :required false :type "string"
:validate-fn string? :description "CSS class names, space separated, for the top-level SVG element"} :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"} :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"} :validate-fn string? :description "CSS class names, space separated, for the scale"}
{:name :id :required false :type "string" :default "meter" {:name :id :required false :type "string" :default "graph"
:validate-fn string? :description "Element id for this instance of the meter"} :validate-fn string? :description "Element id for this instance of the graph"}
{:name :gradations :reduired false :type "integer" :default 5 {:name :gradations :reduired false :type "integer" :default 5
:validate-fn integer? :description "Number of gradations to show on the scale, not counting the point."} :validate-fn integer? :description "Number of gradations to show on the scale, not counting the point."}
{:name :style :required false :type "CSS style map" {: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"]}]) :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 (def full-scale-deflection
;; from the left end of the scale to right end, in degrees. "the full sweep of the needle from the left end of the scale to right end, in degrees."
(def full-scale-deflection 140) 360)
(defn deflection (defn deflection
@ -112,7 +111,7 @@
at `cx`, cy` starting at `min-radius` and extending to `max-radius`, with the specified at `cx`, cy` starting at `min-radius` and extending to `max-radius`, with the specified
`label`." `label`."
[cx cy min-radius max-radius angle label] [cx cy min-radius max-radius angle label]
[:g {:class "snm-gradation" [:g {:class "rsvggraph-gradation"
:transform (string/join " " ["rotate(" angle cx cy ")"])} :transform (string/join " " ["rotate(" angle cx cy ")"])}
[:path {:d (string/join [:path {:d (string/join
" " " "
@ -157,63 +156,68 @@
others (recursively-draw-segments (rest still-to-do) (cons party done) total-votes cx cy radius) others (recursively-draw-segments (rest still-to-do) (cons party done) total-votes cx cy radius)
vote-share (* (/ (:votes party) total-votes) 100)] vote-share (* (/ (:votes party) total-votes) 100)]
(if (> vote-share 1) (if (> vote-share 1)
(cons [:g [:path {:class "snm-scale" (cons [:g [:path {:class "rsvggraph-scale"
:id (str (:id party) "-segment") :id (str (:id party) "-segment")
:style {:stroke (:colour party)} :style {:stroke (:colour party)}
:d (describe-arc cx cy radius start-angle end-angle)}] :d (describe-arc cx cy radius start-angle end-angle)}]
(gradation cx cy (* radius 0.8) (* radius 1.1) start-angle (gradation cx cy (* radius 0.8) (* radius 1.1) start-angle
(str (str
(if (> vote-share 5) (name (:id party)) "") (when (> vote-share 5) (name (:id party)) "")
(if (> vote-share 10) (str " " (as-label vote-share) "%"))))] (when (> vote-share 10) (str " " (as-label vote-share) "%"))))]
others) others)
others)))) others))))
(defn swingometer (defn rsvggraph
"Render an SVG swinging needle meter" "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] [& {:keys [model width height class scale-class frame-class id style attr]
:or {width 300 :or {width 300
height 200 height 300
scale-class "snm-scale" scale-class "rsvggraph-scale"
frame-class "snm-frame" frame-class "rsvggraph-frame"
id "meter"} id "graph"}
:as args}] :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) (let [model (deref-or-value model)
mid-point-deflection (/ full-scale-deflection 2) mid-point-deflection (/ full-scale-deflection 2)
cx (/ width 2) dimension (min width height)
cy (* height 0.90) cx (/ dimension 2)
needle-length (* height 0.75) cy (* dimension 0.50)
scale-radius (* height 0.7) scale-radius (* dimension 0.45)
gradation-inner (* height 0.55)
gradations 5
total-votes (reduce + (map #(:votes %) (vals model)))] total-votes (reduce + (map #(:votes %) (vals model)))]
[box [box
:align :start :align :start
:child [:div :child [:div
(merge (merge
{:class (str "swingometer " class) {:class (str "rsvggraph " class)
:style (merge (flex-child-style "none") :style (merge (flex-child-style "none")
{:width width :height height} {:width dimension :height dimension}
style)} style)}
attr) attr)
[:svg {:xmlSpace "preserve" [:svg {:xmlSpace "preserve"
:overflow "visible" :overflow "visible"
:viewBox (string/join " " [0 0 width height]) :viewBox (string/join " " [0 0 dimension dimension])
:width (str width "px") :width (str dimension "px")
:height (str height "px") :height (str dimension "px")
:y "0px" :y "0px"
:x "0px" :x "0px"
:version "1.1" :version "1.1"
:id id :id id
:class (str "snm-meter " class)} :class (str "rsvggraph-graph " class)}
[:text [:text
{:text-anchor "middle" {:text-anchor "middle"
:x (/ width 2) :x (/ dimension 2)
:y (/ height 2) :y (/ dimension 2)
:width "100" :width (/ dimension 4)
:id (str id "-total-votes") :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 [:path {:class scale-class
:id (str id "-scale") :id (str id "-scale")
:d (describe-arc cx cy scale-radius :d (describe-arc cx cy scale-radius

View file

@ -1,4 +1,4 @@
(ns swingometer.subs (ns rsvggraph.subs
(:require-macros [reagent.ratom :refer [reaction]]) (:require-macros [reagent.ratom :refer [reaction]])
(:require [re-frame.core :as re-frame])) (:require [re-frame.core :as re-frame]))

View file

@ -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]])) (: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; ;;;; This file is just stolen wholesale from re-demo in the re-com package;

View file

@ -1,12 +1,12 @@
(ns swingometer.views (ns rsvggraph.views
(:require [re-frame.core :as re-frame] (: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.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]] [re-com.util :refer [deref-or-value]]
[swingometer.swingometer :refer [swingometer swingometer-args-desc]] [rsvggraph.rsvggraph :refer [rsvggraph rsvggraph-args-desc]]
[swingometer.utils :refer [panel-title title2 args-table github-hyperlink status-text]] [rsvggraph.utils :refer [panel-title title2 args-table github-hyperlink status-text]]
[reagent.core :as reagent])) [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} (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} :lab {:id :lab :name "Labour Party" :colour "red" :votes 10}
@ -19,7 +19,7 @@
[v-box [v-box
:size "auto" :size "auto"
:gap "10px" :gap "10px"
:children [[panel-title "Swingometer"] :children [[panel-title "rsvggraph"]
[h-box [h-box
:gap "100px" :gap "100px"
:children [[v-box :children [[v-box
@ -27,21 +27,21 @@
:width "450px" :width "450px"
:children [[title2 "Notes"] :children [[title2 "Notes"]
[status-text "Wildly experimental"] [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"] [title2 "Behaviour"]
[args-table swingometer-args-desc]]] [args-table rsvggraph-args-desc]]]
[v-box [v-box
:gap "10px" :gap "10px"
:children [[title2 "Demo"] :children [[title2 "Demo"]
[v-box [v-box
:gap "20px" :gap "20px"
:children [[swingometer :children [[rsvggraph
:model model :model model
:height 600 :height 500
:width 1000] :width 500]
[title :level :level3 :label "Parameters"] [title :level :level3 :label "Parameters"]
[h-box [h-box
:gap "10px" :gap "10px"
@ -127,11 +127,11 @@
;; core holds a reference to panel, so need one level of indirection to get figwheel updates ;; core holds a reference to panel, so need one level of indirection to get figwheel updates
(defn panel (defn panel
[] []
[swingometer-demo]) [rsvggraph-demo])
(defn main-panel [] (defn main-panel []
(fn [] (fn []
[v-box [v-box
:height "100%" :height "100%"
:children [[swingometer-demo]]])) :children [[rsvggraph-demo]]]))