Reorganised code into three namespaces; implemented stats on donors.

This commit is contained in:
Simon Brooke 2026-09-28 20:40:06 +01:00
parent 0e814583b7
commit 5dfa7fc3a3
4 changed files with 232 additions and 159 deletions

98
src/gaza_stats/chart.clj Normal file
View file

@ -0,0 +1,98 @@
(ns gaza-stats.chart
"Produce charts from data functions in other namespaces."
(:require [cljc.java-time.local-date :refer [is-before]]
[clojure.string :refer [capitalize starts-with?]]
[com.hypirion.clj-xchart :refer [xy-chart]]
[gaza-stats.core :refer [accounts-with-donations-by-date-range
average-donations-received-by-month local-date->date
normalise-date total-donations-by-weeks]]
[gaza-stats.donors :refer [donations-by-donor]]))
(defn chart-donations-by-weeks
"Create and return an XY chart of data on field represented by `key` of
donations made between `start-date` and `end-date`. Arguments may be supplied as
`java.time.LocalDate` objects, or as strings in the format `yyyy-mm-dd`."
[start-date end-date key]
(let [data (total-donations-by-weeks start-date end-date)]
(xy-chart {(format "%s Donations" (capitalize (name key)))
[(map #(local-date->date (first %)) data)
(map #(:n (nth % 1)) data)]})))
(defn chart-total-donations-by-weeks
"Create and return an XY chart of data on total donations made between
`start-date` and `end-date`. Arguments may be supplied as
`java.time.LocalDate` objects, or as strings in the format `yyyy-mm-dd`."
[start-date end-date]
(chart-donations-by-weeks start-date end-date :total))
(defn chart-number-donations-by-weeks
"Create and return an XY chart of data on number of donations made between
`start-date` and `end-date`. Arguments may be supplied as
`java.time.LocalDate` objects, or as strings in the format `yyyy-mm-dd`."
[start-date end-date]
(chart-donations-by-weeks start-date end-date :n))
(defn chart-donations-by-accounts-in-date-range
"Return a chart of donations received by individual accounts between
`start-date` (exclusive) and `end-date` (inclusive). Arguments should be
supplied as `java.time.LocalDate` objects, or as strings in the format
`yyyy-mm-dd`. Accounts are identified in the chart only by rank order."
[start-date end-date]
(let [data (accounts-with-donations-by-date-range start-date end-date)]
(xy-chart {(format "Donations between\n%s and\n%s" start-date end-date)
[(range (count data)) (sort (map :donations data))]})))
(defn chart-donations-as-percentage-in-date-range
"Return a chart of donations received by individual accounts between
`start-date` (exclusive) and `end-date` (inclusive). Arguments should be
supplied as `java.time.LocalDate` objects, or as strings in the format
`yyyy-mm-dd`. Accounts are identified in the chart only by rank order."
[start-date end-date]
(let [data (accounts-with-donations-by-date-range start-date end-date)
donations (sort (map :donations data))
total (reduce + donations)
percentages (map #(* 100.0 (/ % total)) donations)]
(xy-chart {(format "Donations as percent\nof total donations\nbetween %s and\n%s" start-date end-date)
[(range (count data)) percentages]})))
(defn chart-accounts-with-donations-by-months-in-range
"Return a chart with one line for each calendar month covering from
`start-date` (inclusive) to `end-date` (inclusive). Arguments should be
supplied as `java.time.LocalDate` objects, or as strings in the format
`yyyy-mm-dd`, showing donations as for
`chart-donations-by-accounts-in-date-range`, q.v."
[start-date end-date]
(let [sd (normalise-date start-date "2024-01-01")
ed (normalise-date end-date nil)]
(loop [s (.withDayOfMonth sd 1)
i (.withDayOfMonth (.plusMonths sd 1) 1)
m (sorted-map)]
(let [data (accounts-with-donations-by-date-range s i)
m' (assoc m (format "Donations between\n%s and\n%s" s i)
[(range (count data)) (sort (map :donations data))])]
(if (is-before ed i) (xy-chart m')
(recur i (.withDayOfMonth (.plusMonths i 1) 1) m'))))))
(defn chart-average-donations-received-by-month
"Return a chart comparing the average donations received per account with
the total number of active accounts per month between `start-date` and
`end-date`. Arguments should be supplied as `java.time.LocalDate` objects,
or as strings in the format `yyyy-mm-dd`."
[start-date end-date]
(let [data (average-donations-received-by-month start-date end-date)]
(xy-chart {(format "Average donations\nper active account\nbetween\n%s and\n%s" start-date end-date)
[(map #(local-date->date %) (keys data)) (map :average (vals data))]
(format "Active accounts\nbetween\n%s and\n%s" start-date end-date)
[(map #(local-date->date %) (keys data)) (map :count (vals data))]})))
(defn chart-donations-by-donor
"Return a rank-ordered graph of the values of this `key` in the
`donations-by-donor` data between this `start-date` and this `end-date`."
([start-date end-date key]
(chart-donations-by-donor start-date end-date key false))
([start-date end-date key exclude-anons]
(let [data (donations-by-donor start-date end-date)
data' (if exclude-anons (remove #(let [d (:donor %)](or (nil? d) (starts-with? d "Anon"))) data) data)]
(xy-chart {(format "%s dontations by donor%s" (capitalize (name key)) (when exclude-anons "\n(excluding anons)"))
[(range (count data'))
(sort (map key data'))]}))))

View file

@ -1,8 +1,6 @@
(ns gaza-stats.core
"Functions to interrogate the [`gaza.onl`](https://gaza.onl/app.db) database."
(:require [clojure.string :refer [capitalize]]
[cljc.java-time.local-date :as ld]
[com.hypirion.clj-xchart :refer [xy-chart]]
(:require [cljc.java-time.local-date :as ld]
[korma.core :refer [as-sql fields join select where]]
[korma.db :refer [defdb sqlite3]])
(:import [java.util Date]
@ -80,30 +78,6 @@
(.toInstant (.atStartOfDay ~x)
(ZoneOffset/ofHours 0))))
(defn chart-donations-by-weeks
"Create and return an XY chart of data on field represented by `key` of
donations made between `start-date` and `end-date`. Arguments may be supplied as
`java.time.LocalDate` objects, or as strings in the format `yyyy-mm-dd`."
[start-date end-date key]
(let [data (total-donations-by-weeks start-date end-date)]
(xy-chart {(format "%s Donations" (capitalize (name key)))
[(map #(local-date->date (first %)) data)
(map #(:n (nth % 1)) data)]})))
(defn chart-total-donations-by-weeks
"Create and return an XY chart of data on total donations made between
`start-date` and `end-date`. Arguments may be supplied as
`java.time.LocalDate` objects, or as strings in the format `yyyy-mm-dd`."
[start-date end-date]
(chart-donations-by-weeks start-date end-date :total))
(defn chart-number-donations-by-weeks
"Create and return an XY chart of data on number of donations made between
`start-date` and `end-date`. Arguments may be supplied as
`java.time.LocalDate` objects, or as strings in the format `yyyy-mm-dd`."
[start-date end-date]
(chart-donations-by-weeks start-date end-date :n))
(defn enhance-account-with-donations
"Take this `account_map`, which must at least have a valid value for the
key `:campaign_url` and return a similar map including the key `:donations`,
@ -142,46 +116,6 @@
ed (normalise-date end-date (ld/now))]
(map #(enhance-account-with-donations % sd ed) (active-accounts sd))))
(defn chart-donations-by-accounts-in-date-range
"Return a chart of donations received by individual accounts between
`start-date` (exclusive) and `end-date` (inclusive). Arguments should be
supplied as `java.time.LocalDate` objects, or as strings in the format
`yyyy-mm-dd`. Accounts are identified in the chart only by rank order."
[start-date end-date]
(let [data (accounts-with-donations-by-date-range start-date end-date)]
(xy-chart {(format "Donations between\n%s and\n%s" start-date end-date)
[(range (count data)) (sort (map :donations data))]})))
(defn chart-donations-as-percentage-in-date-range
"Return a chart of donations received by individual accounts between
`start-date` (exclusive) and `end-date` (inclusive). Arguments should be
supplied as `java.time.LocalDate` objects, or as strings in the format
`yyyy-mm-dd`. Accounts are identified in the chart only by rank order."
[start-date end-date]
(let [data (accounts-with-donations-by-date-range start-date end-date)
donations (sort (map :donations data))
total (reduce + donations)
percentages (map #(* 100.0 (/ % total)) donations)]
(xy-chart {(format "Donations as percent\nof total donations\nbetween %s and\n%s" start-date end-date)
[(range (count data)) percentages]})))
(defn chart-accounts-with-donations-by-months-in-range
"Return a chart with one line for each calendar month covering from
`start-date` (inclusive) to `end-date` (inclusive). Arguments should be
supplied as `java.time.LocalDate` objects, or as strings in the format
`yyyy-mm-dd`, showing donations as for
`chart-donations-by-accounts-in-date-range`, q.v."
[start-date end-date]
(let [sd (normalise-date start-date "2024-01-01")
ed (normalise-date end-date (ld/now))]
(loop [s (.withDayOfMonth sd 1)
i (.withDayOfMonth (.plusMonths sd 1) 1)
m (sorted-map)]
(let [data (accounts-with-donations-by-date-range s i)
m' (assoc m (format "Donations between\n%s and\n%s" s i)
[(range (count data)) (sort (map :donations data))])]
(if (ld/is-before ed i) (xy-chart m')
(recur i (.withDayOfMonth (.plusMonths i 1) 1) m'))))))
;; this does not work and I don't know why
;; (as-sql '(select 'campaign_donations
@ -212,15 +146,3 @@
(if (ld/is-before ed i)
m'
(recur i (.withDayOfMonth (.plusMonths i 1) 1) m'))))))
(defn chart-average-donations-received-by-month
"Return a chart comparing the average donations received per account with
the total number of active accounts per month between `start-date` and
`end-date`. Arguments should be supplied as `java.time.LocalDate` objects,
or as strings in the format `yyyy-mm-dd`."
[start-date end-date]
(let [data (average-donations-received-by-month start-date end-date)]
(xy-chart {(format "Average donations\nper active account\nbetween\n%s and\n%s" start-date end-date)
[(map #(local-date->date %) (keys data)) (map :average (vals data))]
(format "Active accounts\nbetween\n%s and\n%s" start-date end-date)
[(map #(local-date->date %) (keys data)) (map :count (vals data))]})))

19
src/gaza_stats/donors.clj Normal file
View file

@ -0,0 +1,19 @@
(ns gaza-stats.donors
"Statistics on donors."
(:require [gaza-stats.core :refer [db normalise-date]]
[korma.core :refer [aggregate as-sql fields join order select where]]))
(defn donations-by-donor
"Return a sequence of maps, one representing each donor who has donated
between `start-date` and `end-date`, each with keys `:count`, `:donor` and
`:total`"
[start-date end-date]
(let [sd (normalise-date start-date "2024-01-01")
ed (normalise-date end-date nil)]
(select 'campaign_donations
(fields :donor)
(aggregate (sum :amount) :total :donor)
(aggregate (count :id) :count :donor)
(where (and {:created_at [> sd]}
{:created_at [<= ed]}))
(order :total))))