Skip to content

Commit

Permalink
Timeframe plot PR (for #125) (#131)
Browse files Browse the repository at this point in the history
* Add initial timeframe plotting code

* Rename to timeframe_plot.py

* Add arguments

* Pass in all the arguments

* Add timeframe plot for donorDonee

* Fix query for single_donor_multiple_donees plot

* Fix query for single_donee_multiple_donors

* Add timeframe graph python command

* Add timeframe plots to the website code

* Use better input to hash for filename

* Encode arguments to python using base64

* Uncomment import statements

* Add space before '--base64' so that graphical timeline generation actually works

* Fix bug noted in #125 (comment)

Co-authored-by: Issa Rice <riceissa@gmail.com>
  • Loading branch information
vipulnaik and riceissa committed Jun 4, 2021
1 parent dc15374 commit afa8068
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -9,3 +9,4 @@ access-portal/anchor.min.js
access-portal/style.css
access-portal/images/
access-portal/cache/
python/login.py
12 changes: 12 additions & 0 deletions access-portal/backend/doneeDonationList.inc
Expand Up @@ -15,6 +15,18 @@ if ($stmt = $mysqli->prepare($query)) {
}

print '<h4 id="doneeDonationList">Full list of donations in reverse chronological order ('.($doneeDonationSelectResult -> num_rows).' donations)</h4>';

$timeframePlotUrlBase = "timeframe plot|donee:" . $donee;

if ($causeAreaFilterString != '') {
$timeframePlotUrlBase .= '&cause_area_filter='.urlencode($causeAreaFilterString);
}

$timeframePlotFileName = hash("md5", $timeframePlotUrlBase) . "-timeframe.png";
exec($generateTimeframeGraphCmdBase . " --base64 --donee " . trimEncodeEscape($donee) . " --output " . trimEncodeEscape($imagesPath . $timeframePlotFileName));
print '<p>Graph of top 10 donors by amount, showing the timeframe of donations</p>';
print '<img src="/images/' . $timeframePlotFileName .'" alt="Graph of donations and their timeframes"></img>';

print "\n";
print '<table id="myTableDoneeDonationList" class="tablesorter">'."\n";
print "<thead>\n";
Expand Down
12 changes: 12 additions & 0 deletions access-portal/backend/donorDonationList.inc
Expand Up @@ -15,6 +15,18 @@ if ($stmt = $mysqli->prepare($query)) {
}

print '<h4 id="donorDonationList">Full list of donations in reverse chronological order ('.($donorDonationSelectResult -> num_rows).' donations)</h4>';

$timeframePlotUrlBase = "timeframe plot|donor:" . $donor;

if ($causeAreaFilterString != '') {
$timeframePlotUrlBase .= '&cause_area_filter='.urlencode($causeAreaFilterString);
}

$timeframePlotFileName = hash("md5", $timeframePlotUrlBase) . "-timeframe.png";
exec($generateTimeframeGraphCmdBase . " --base64 --donor " . trimEncodeEscape($donor) . (($causeAreaFilterString ?? '') ? " --cause_area " . trimEncodeEscape($causeAreaFilterString) : "") . " --output " . trimEncodeEscape($imagesPath . $timeframePlotFileName));
print '<p>Graph of top 10 donees by amount, showing the timeframe of donations</p>';
print '<img src="/images/' . $timeframePlotFileName .'" alt="Graph of donations and their timeframes"></img>';

print "\n";
print '<table id="myTableDonorDonationList" class="tablesorter">'."\n";
print "<thead>\n";
Expand Down
11 changes: 11 additions & 0 deletions access-portal/backend/donorDoneeDonationList.inc
Expand Up @@ -15,6 +15,17 @@ if ($stmt = $mysqli->prepare($query)) {

print '<h4 id="donorDoneeDonationList">Full list of donations in reverse chronological order ('.($donorDoneeDonationSelectResult -> num_rows).' donations)</h4>';

$timeframePlotUrlBase = "timeframe plot|donor:" . $donor . "|donee:" . $donee;

if ($causeAreaFilterString != '') {
$timeframePlotUrlBase .= '&cause_area_filter='.urlencode($causeAreaFilterString);
}

$timeframePlotFileName = hash("md5", $timeframePlotUrlBase) . "-timeframe.png";
exec($generateTimeframeGraphCmdBase . " --base64 --donor " . trimEncodeEscape($donor) . " --donee " . trimEncodeEscape($donee) . " --output " . trimEncodeEscape($imagesPath . $timeframePlotFileName));
print '<p>Graph of all donations, showing the timeframe of donations</p>';
print '<img src="/images/' . $timeframePlotFileName .'" alt="Graph of donations and their timeframes"></img>';

print '<table id="myTableDonorDonationList" class="tablesorter">'."\n";
print "<thead>\n";
print '<tr>';
Expand Down
3 changes: 2 additions & 1 deletion access-portal/backend/globalVariables/dummyPasswordFile.inc
Expand Up @@ -3,5 +3,6 @@ $mysqli = new mysqli("localhost", "username", "password", "database name");
$mysqli->set_charset("utf8");
$imagesPath = "(project folder)/access-portal/images/";
$generateGraphCmdBase = "python3.5 (project folder)/python/graph.py --label --top 30 ";
$generateTimeframeGraphCmdBase = "python3.5 (project folder)/python/timeframe_plot.py ";

?>
?>
5 changes: 5 additions & 0 deletions access-portal/backend/stringFunctions.inc
Expand Up @@ -82,4 +82,9 @@ function explodeDoneeCharSepValues($doneeCsv, string $char) {
return $formattedString;
}

// For passing arguments containing user input to a python script.
function trimEncodeEscape(string $x, int $limit = 300) {
return escapeshellarg(base64_encode(substr($x, 0, $limit)));
}

?>
194 changes: 194 additions & 0 deletions python/timeframe_plot.py
@@ -0,0 +1,194 @@
#!/usr/bin/env python3

import sys
import datetime
import argparse
import base64
import mysql.connector
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

try:
import login
except ModuleNotFoundError:
print("Please create a login.py file with the following information so Python can access MySQL:")
print()
print('USER = "your_user_name"')
print('DATABASE = "donations"')
print('PASSWORD = "secret"')
sys.exit()


cnx = mysql.connector.connect(user=login.USER, database=login.DATABASE,
password=login.PASSWORD)
cursor = cnx.cursor()


def single_donor_single_donee(output, donor, donee):
cursor.execute("""select intended_funding_timeframe_in_months,donation_date from donations where donor = %s and donee = %s""", (donor, donee))
y = 1
for intended_funding_timeframe_in_months, donation_date in cursor:
funding_timeframe_in_days = 30*intended_funding_timeframe_in_months if intended_funding_timeframe_in_months else 10
xstart = donation_date
xstop = xstart + datetime.timedelta(days=funding_timeframe_in_days)
plt.hlines(y, xstart, xstop, 'b', lw=4)
y += 1

plt.yticks(range(1,y))
plt.xticks(rotation=45)
plt.xlabel("Date")
plt.ylabel("Grant number")
plt.savefig(output, bbox_inches="tight")


def single_donor_multiple_donees(output, donor, cause_area=None):
if cause_area:
# We want to limit the donations to those given to the top 10 donees.
# So we use a subquery to find the top 10 donees, but MySQL doesn't
# support a limit clause inside a subquery, so we have to create dummy
# table name (called t here). See
# https://stackoverflow.com/a/51877655/3422337
cursor.execute("""select
donee,
intended_funding_timeframe_in_months,
donation_date
from
donations
where
donor = %s and
substring_index(cause_area,'/',1)=%s and
donee in (
select * from (
select donee
from donations
where
donor = %s and
substring_index(cause_area,'/',1)=%s
group by donee
order by sum(amount) desc
limit 10
) as t
)
order by donation_date""", (donor, cause_area, donor, cause_area))
else:
cursor.execute("""select
donee,
intended_funding_timeframe_in_months,
donation_date
from
donations
where
donor = %s and
donee in (
select * from (
select donee
from donations
where donor = %s
group by donee
order by sum(amount) desc
limit 10
) as t
)
order by donation_date""", (donor, donor))

plt.figure(figsize=(12,5))
y = 1
donees_seen = {}
for donee, intended_funding_timeframe_in_months, donation_date in cursor:
if donee not in donees_seen:
donees_seen[donee] = y # claim this y position, then increment y
y += 1
funding_timeframe_in_days = 30*intended_funding_timeframe_in_months if intended_funding_timeframe_in_months else 10
xstart = donation_date
xstop = xstart + datetime.timedelta(days=funding_timeframe_in_days)
plt.hlines(donees_seen[donee], xstart, xstop, 'b', lw=4)
# yticks needs ticks (y positions) and the labels in separate lists, so we
# "unzip" the dictionary into two lists
plt.yticks(*zip(*[(donees_seen[k], k) for k in donees_seen]))
plt.xticks(rotation=45)
plt.xlabel("Date")
plt.ylabel("Donee")
plt.tight_layout()
plt.savefig(output, bbox_inches="tight")


def single_donee_multiple_donors(output, donee):
# See comment in single_donor_multiple_donees for explanation of the
# subquery.
cursor.execute("""select
donor,
intended_funding_timeframe_in_months,
donation_date
from
donations
where
donee = %s and
donor in (
select * from (
select donor
from donations
where donee = %s
group by donor
order by sum(amount) desc
limit 10
) as t
)
order by donation_date""", (donee, donee))
plt.figure(figsize=(12,5))
y = 1
donor_ypos = {}
for donor, intended_funding_timeframe_in_months, donation_date in cursor:
if donor not in donor_ypos:
donor_ypos[donor] = y # claim this y position, then increment y
y += 1
funding_timeframe_in_days = 30*intended_funding_timeframe_in_months if intended_funding_timeframe_in_months else 10
xstart = donation_date
xstop = xstart + datetime.timedelta(days=funding_timeframe_in_days)
plt.hlines(donor_ypos[donor], xstart, xstop, 'b', lw=4)
# yticks needs ticks (y positions) and the labels in separate lists, so we
# "unzip" the dictionary into two lists
plt.yticks(*zip(*[(donor_ypos[k], k) for k in donor_ypos]))
plt.xticks(rotation=45)
plt.xlabel("Date")
plt.ylabel("Donor")
plt.tight_layout()
plt.savefig(output, bbox_inches="tight")


def base64_to_string(x):
if x:
return base64.b64decode(x).decode('utf8')
else:
return x


if __name__ == "__main__":
parser = argparse.ArgumentParser(description='plot donations and timeframes')
parser.add_argument('--donor')
parser.add_argument('--donee')
parser.add_argument('--output')
parser.add_argument('--cause_area')
parser.add_argument('--base64', action='store_true')
args = parser.parse_args()

# If --base64 flag is given, interpret all arguments as base64-encoded strings
if args.base64:
donor = base64_to_string(args.donor)
donee = base64_to_string(args.donee)
output = base64_to_string(args.output)
cause_area = base64_to_string(args.cause_area)
else:
donor = args.donor
donee = args.donee
output = args.output
cause_area = args.cause_area

if args.donor and args.donee:
single_donor_single_donee(output, donor, donee)
elif args.donor:
single_donor_multiple_donees(output, donor, cause_area=cause_area)
elif args.donee:
single_donee_multiple_donors(output, donee)
else:
print("Please specify a donor and/or donee.", file=sys.stderr)

0 comments on commit afa8068

Please sign in to comment.