From 72038b34b4213bb4b5dc50f2c47c9d76d6e8be4e Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 10 Jul 2024 09:16:00 +0100 Subject: [PATCH] Better layout of minor labels. --- resources/public/sample-data.edn | 22 ++--- src/clj/rsvggraph/core.clj | 136 +++++++++++++++++++++---------- 2 files changed, 106 insertions(+), 52 deletions(-) diff --git a/resources/public/sample-data.edn b/resources/public/sample-data.edn index 98516b8..d4373a2 100644 --- a/resources/public/sample-data.edn +++ b/resources/public/sample-data.edn @@ -1,22 +1,20 @@ {:id "ge2024" :label "UK General Election 2024" :colour "white" - :children [{:id "no-show" - :label "Did not vote" - :quantity 18365357} - {:id "voted" + :children [{:id "voted" :label "Voted" + :color "chartreuse" :children [{:id "labour" :label "Labour" :colour "red" :quantity 9712011 :children [{:id "other" :label "Other" - :colour "#C0C0C0" + :colour "silver" :quantity 291140} {:id "negative" :label "Negative" - :colour "#C0C0C0" + :colour "silver" :children [{:id "anti-tory" :label "To get the Tories out" :colour "#424242" @@ -108,9 +106,6 @@ :label "Reform UK Ltd." :colour "cyan" :quantity 4091549} - {:id "dup" - :label "DUP" - :quantity 172058} {:id "greenew" :label "Green Party" :colour "green" @@ -119,10 +114,17 @@ :label "Plaid Cymru" :colour "#005b54" :quantity 194811} + {:id "dup" + :label "DUP" + :quantity 172058} {:id "apni" :label "Alliance Party" :colour "#f6cb2f" :quantity 117191} {:id "sdlp" :label "SDLP" - :quantity 86861}]}]} \ No newline at end of file + :quantity 86861}]} + {:id "no-show" + :label "Did not vote" + :colour "silver" + :quantity 18365357}]} \ No newline at end of file diff --git a/src/clj/rsvggraph/core.clj b/src/clj/rsvggraph/core.clj index c0c5689..b8ca343 100644 --- a/src/clj/rsvggraph/core.clj +++ b/src/clj/rsvggraph/core.clj @@ -15,50 +15,74 @@ "Return, as a map with keys :x. :y, the cartesian coordinates at the point `radius` distance at `theta` (degrees) angle from a point at cartesian coordinates `cx`, `cy`." - [cx cy radius theta] + [geometry] (let - [in-radians (/ (* (- theta 90) PI) 180.0)] - {:x (+ cx (* radius (cos in-radians))) - :y (+ cy (* radius (sin in-radians)))})) + [in-radians (/ (* (- (:angle geometry) 90) PI) 180.0)] + {:x (+ (:cx geometry) (* (:radius geometry) (cos in-radians))) + :y (+ (:cy geometry) (* (:radius geometry) (sin in-radians)))})) (defn describe-arc "Return as a string an SVG path definition describing an arc centred at `cx`, cy` starting at `start-angle` and ending at `end-angle` (both angles in degrees)." - [cx cy radius start-angle end-angle] + [geometry start-angle end-angle] (let - [start (polar-to-cartesian cx cy radius start-angle) - end (polar-to-cartesian cx cy radius end-angle) + [start (polar-to-cartesian (assoc geometry :angle start-angle)) + end (polar-to-cartesian (assoc geometry :angle end-angle)) large-arc? (if (<= (- end-angle start-angle) 180) 0 1) + radius (:radius geometry) sweep (if (> end-angle start-angle) 1 0)] - (join " " ["M" (:x start) (:y start) "A" radius radius 0 large-arc? sweep (:x end) (:y end)]))) + (join " " + ["M" (:x start) (:y start) "A" radius radius 0 large-arc? sweep + (:x end) (:y end)]))) -(defn- text-path [datum tp-id diameter thickness start-angle end-angle] +(def ^:dynamic *minor-segment-threshold* 0.07) + +(defn- minor-segment? [datum] + (< (- (:right datum) (:left datum)) *minor-segment-threshold*)) + +(defn- font-size [thickness] + (int (* 0.15 thickness))) + +(defn- text-path [datum tp-id geometry thickness start-angle end-angle] [:path {:class "rsvggraph-text-path" :id tp-id :style {:fill "none" :stroke "none"} - :d (let [angle (/ (+ start-angle end-angle) 2) - radius (/ diameter 2) - start (polar-to-cartesian diameter diameter radius angle) - end (polar-to-cartesian diameter diameter diameter angle)] - (if (< (- (:right datum) (:left datum)) 0.08) + :d (let [angle (if (> (+ start-angle end-angle) 360) start-angle end-angle) + radius (:radius geometry) + end (polar-to-cartesian (assoc geometry :radius (* 1.2 (:radius geometry)) :angle angle)) + height (int (:y end))] + (if (minor-segment? datum) + (if (> angle 180) + (format "M %d %d L %d %d" (- (int (:x end)) (* (count (:label datum)) (font-size thickness))) height + (int (:x end)) height) + (format "M %d %d L %d %d" (int (:x end)) height (:width geometry) height)) + (describe-arc (assoc geometry :radius (- radius (* 0.9 thickness))) + start-angle end-angle)))}]) - (format "M %d %d L %d %d" (int (:x start)) (int (:y start)) - (int (:x end)) (int (:y end))) - (describe-arc diameter diameter (- radius (* 0.9 thickness)) start-angle end-angle)))}]) +(defn- label-indicator-path [geometry angle] + (let [start (polar-to-cartesian (assoc geometry :angle angle)) + end (polar-to-cartesian + (assoc geometry :radius (* 1.2 (:radius geometry)) :angle angle))] + (format "M %d %d L %d %d" (int (:x start)) (int (:y start)) + (int (:x end)) (int (:y end))))) + +(defn- label-indicator [datum geometry start-angle end-angle] + (when (minor-segment? datum)[:path {:class "rsvggraph-minor-label-indicator" :style {:fill "none" :stroke *foreground* :stroke-width "thin"} + :d (label-indicator-path geometry (if (> (+ start-angle end-angle) 360) start-angle end-angle))}])) (defn draw-segment - [datum diameter] - (let [r' (/ diameter 2) - thickness (/ r' (:ring datum));; (/ 1 (pow 3 (:ring datum))) - radius (- r' (/ thickness 2)) + [datum geometry] + (println (format "Radius: %s; ring: %s." (:radius geometry) (:ring datum))) + (let [thickness (/ (:radius geometry) (:ring datum));; (/ 1 (pow 3 (:ring datum))) + radius (- (:radius geometry) (/ thickness 2)) start-angle (* (:left datum) 360) end-angle (* (:right datum) 360) id (str (:id datum) "-segment") tp-id (str "tp-" id) - path-data (describe-arc diameter diameter radius start-angle end-angle)] -;; (println (format "Id: %s; radius: %s; start: %s; end: %s; thickness %s" id radius start-angle end-angle thickness)) + path-data (describe-arc (assoc geometry :radius radius) + start-angle end-angle)] [:g {:id (str id "group")} [:path {:class "rsvggraph-segment" :id id @@ -66,12 +90,14 @@ :stroke (:colour datum) :stroke-width thickness} :d path-data}] - (text-path datum tp-id diameter thickness start-angle end-angle) + (text-path datum tp-id geometry thickness start-angle end-angle) + (label-indicator datum geometry start-angle end-angle) [:text {:style {:fill *foreground* :font-family "sans-serif" :font-weight "bold" - :font-size (str (floor (* 0.2 thickness)))}} - [:textPath {:xlink:href (str "#" tp-id)} [:tspan (:label datum)]]]])) + :font-size (str (font-size thickness))}} + [:textPath {:xlink:href (str "#" tp-id) + :startOffset "2%"} [:tspan (:label datum)]]]])) ;; svg [data diameter] (let [data' (normalise data) - dimension (* 2 diameter)] + height (* 2 diameter) + width (* 3 diameter) + geometry (base-geometry diameter)] [:svg {:xmlSpace "preserve" :overflow "visible" - :viewBox (join " " [0 0 dimension dimension]) - :width (str dimension "px") - :height (str dimension "px") + :viewBox (join " " [0 0 height height]) + :width (str width "px") + :height (str height "px") :y "0px" :x "0px" :version "1.1" @@ -102,17 +160,9 @@ :class (str "rsvggraph-graph") :xmlns "http://www.w3.org/2000/svg" :xmlns:xlink "http://www.w3.org/1999/xlink"} - [:circle {:id (str (:id data') "-background") :cx diameter :cy diameter :r (/ diameter 2) :style {:fill "white"}}] - [:text - {:text-anchor "middle" - :x diameter - :y diameter - :width (/ diameter 4) - :id (str (:id data') "-title") - :style {:font-family "sans-serif" - :font-weight "bold"} - :class "rsvggraph-value"} [:tspan (:label data) ": " (:quantity data)]] - (map #(draw-segment % diameter) (flatten-data data))])) + (circle data' geometry) + (cantre-label data' geometry) + (map #(draw-segment % geometry) (flatten-data data'))])) (defn data->svg-file [data diameter path] @@ -120,4 +170,6 @@ (defn data->html-file [data diameter path] - (spit path (join "\n" ["" (replace (html (data->svg data diameter)) #"\> *\<" ">\n<")]))) \ No newline at end of file + (spit path (join "\n" + ["" + (replace (html (data->svg data diameter)) #"\> *\<" ">\n<")]))) \ No newline at end of file