Added modified files and bb js/css

This commit is contained in:
Javier 2021-04-03 12:48:33 -04:00 committed by Alexis Métaireau
parent 400149bad1
commit 918d4a3c3c
6 changed files with 292 additions and 0 deletions

View file

@ -144,6 +144,7 @@ class Project(db.Model):
] ]
), ),
"balance": self.balance[member.id], "balance": self.balance[member.id],
"monthly_exp": self.get_monthly_expenditure(member.id),
} }
for member in self.active_members for member in self.active_members
] ]
@ -164,6 +165,32 @@ class Project(db.Model):
def uses_weights(self): def uses_weights(self):
return len([i for i in self.members if i.weight != 1]) > 0 return len([i for i in self.members if i.weight != 1]) > 0
def get_monthly_expenditure(self, member_id):
"""
Computes monthly expenses for member_id
:return: a list of tuples of the form (date, expenditure)
"""
query_result = {}
member_monthly = defaultdict(lambda: defaultdict(float))
member_monthly_exp = []
query_result[member_id] = self.get_member_bills(member_id).all()
for bill in query_result[member_id]:
member_monthly[bill.date.year][bill.date.month] += bill.converted_amount
for year in member_monthly:
for month in member_monthly[year]:
amount = member_monthly[year][month]
str_month = ""
# Datetime requires month as a zero padded decimal number (ie, 01, 02.. 11, 12)
if month > 9:
str_month = str(month)
else:
str_month = "0" + str(month)
# Convert date into datetime object
date = datetime.strptime(str(year) + " " + str_month, "%Y %m")
member_monthly_exp.append((date, amount))
return member_monthly_exp
def get_transactions_to_settle_bill(self, pretty_output=False): def get_transactions_to_settle_bill(self, pretty_output=False):
"""Return a list of transactions that could be made to settle the bill""" """Return a list of transactions that could be made to settle the bill"""

View file

@ -0,0 +1,9 @@
/*!
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*
* billboard.js, JavaScript chart library
* https://naver.github.io/billboard.js/
*
* @version 3.0.2
*/.bb svg{font:10px sans-serif;-webkit-tap-highlight-color:rgba(0,0,0,0)}.bb line,.bb path{fill:none;stroke:#000}.bb .bb-button,.bb text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.bb-bars path,.bb-event-rect,.bb-legend-item-tile,.bb-xgrid-focus,.bb-ygrid,.bb-ygrid-focus{shape-rendering:crispEdges}.bb-chart-arc .bb-gauge-value{fill:#000}.bb-chart-arc path{stroke:#fff}.bb-chart-arc rect{stroke:#fff;stroke-width:1}.bb-chart-arc text{fill:#fff;font-size:13px}.bb-axis{shape-rendering:crispEdges}.bb-grid line{stroke:#aaa}.bb-grid text{fill:#aaa}.bb-xgrid,.bb-ygrid{stroke-dasharray:3 3}.bb-text.bb-empty{fill:grey;font-size:2em}.bb-line{stroke-width:1px}.bb-circle._expanded_{stroke-width:1px;stroke:#fff}.bb-selected-circle{fill:#fff;stroke-width:2px}.bb-bar{stroke-width:0}.bb-bar._expanded_{fill-opacity:.75}.bb-circles.bb-focused,.bb-target.bb-focused{opacity:1}.bb-circles.bb-focused path.bb-line,.bb-circles.bb-focused path.bb-step,.bb-target.bb-focused path.bb-line,.bb-target.bb-focused path.bb-step{stroke-width:2px}.bb-circles.bb-defocused,.bb-target.bb-defocused{opacity:.3!important}.bb-circles.bb-defocused .text-overlapping,.bb-target.bb-defocused .text-overlapping{opacity:.05!important}.bb-region{fill:#4682b4;fill-opacity:.1}.bb-brush .extent,.bb-zoom-brush{fill-opacity:.1}.bb-legend-item{font-size:12px;user-select:none}.bb-legend-item-hidden{opacity:.15}.bb-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.bb-title{font:14px sans-serif}.bb-tooltip-container{z-index:10;user-select:none}.bb-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;opacity:.9;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777}.bb-tooltip tr{border:1px solid #ccc}.bb-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#fff}.bb-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.bb-tooltip td>span,.bb-tooltip td>svg{display:inline-block;width:10px;height:10px;margin-right:6px}.bb-tooltip.value{text-align:right}.bb-area{stroke-width:0;opacity:.2}.bb-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.bb-chart-arcs-gauge-title{dominant-baseline:middle;font-size:2.7em}.bb-chart-arcs .bb-chart-arcs-background{fill:#e0e0e0;stroke:#fff}.bb-chart-arcs .bb-chart-arcs-gauge-unit{fill:#000;font-size:16px}.bb-chart-arcs .bb-chart-arcs-gauge-max,.bb-chart-arcs .bb-chart-arcs-gauge-min{fill:#777}.bb-chart-radars .bb-levels polygon{fill:none;stroke:#848282;stroke-width:.5px}.bb-chart-radars .bb-levels text{fill:#848282}.bb-chart-radars .bb-axis line{stroke:#848282;stroke-width:.5px}.bb-chart-radars .bb-axis text{font-size:1.15em;cursor:default}.bb-chart-radars .bb-shapes polygon{fill-opacity:.2;stroke-width:1px}.bb-button{position:absolute;top:10px;right:10px}.bb-button .bb-zoom-reset{font-size:11px;border:1px solid #ccc;background-color:#fff;padding:5px;border-radius:5px;cursor:pointer}

10
ihatemoney/static/js/billboard.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -6,6 +6,11 @@
<meta http-equiv="content-type" content="text/html; charset=utf-8"> <meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel=stylesheet type=text/css href="{{ url_for("static", filename='css/main.css') }}"> <link rel=stylesheet type=text/css href="{{ url_for("static", filename='css/main.css') }}">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v6.min.js" charset="utf-8"></script>
<!-- Load billboard.js with base style -->
<link rel="stylesheet" href="{{url_for("static", filename='css/billboard.min.css')}}">
<script src="{{ url_for("static", filename='js/billboard.min.js')}}"></script>
{% block css %}{% endblock %} {% block css %}{% endblock %}
<script src="{{ url_for("static", filename="js/jquery-3.1.1.min.js") }}"></script> <script src="{{ url_for("static", filename="js/jquery-3.1.1.min.js") }}"></script>
<script src="{{ url_for("static", filename="js/ihatemoney.js") }}"></script> <script src="{{ url_for("static", filename="js/ihatemoney.js") }}"></script>

View file

@ -33,6 +33,243 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<h2> {{ _("Visualization") }}</h2>
<!-- Required for some style in charts, feel free to move from here -->
<style>
#balanceBar .bb-ygrid-line line {stroke: red;}
#balanceBar .fill_green { fill: green; }
#balanceBar .fill_red { fill: red; }
</style>
<br>
<div id="balanceBar"></div>
<br>
<div id="memberPerMonth"></div>
<br>
<div id="combinedPerMonth"></div>
<br>
<div id="expPie"></div>
<script>
var members = [];
var total_per_member = ['CurrentBalance'];
var total_exp_per_member = {};
var balance_min_value = Number.POSITIVE_INFINITY;
var balance_max_value = Number.NEGATIVE_INFINITY;
{% for stat in members_stats %}
var member_name = "{{ stat.member.name }}"
members.push(member_name)
var val = {{ stat.balance }};
total_per_member.push(val);
total_exp_per_member[member_name] = 0;
if (val > balance_max_value) {
balance_max_value = val;
}
if (val < balance_min_value) {
balance_min_value = val;
}
{% endfor %}
//TODO: Remove this, its for debugging.
members.forEach(function(entry) {
console.log(entry);
});
//TODO: Remove this, its for debugging.
total_per_member.forEach(function(entry) {
console.log(entry);
});
// Start of current balance per month chart
var balance_bar = bb.generate({
title: {
text: "Current Balance per Member"
},
data: {
columns: [
total_per_member
],
type: "bar",
colors: {
CurrentBalance: "#999C9F",
}
},
axis: {
x: {
type: "category",
categories: members
},
y: {
label: "Expenditure"
}
},
grid: {
y: {
lines: [
{
value: 0
}
]
}
},
regions: [
{
axis:"y",
start: 0,
end: balance_max_value,
class: "fill_green"
},
{
axis:"y",
start: balance_min_value,
end: 0,
class: "fill_red"
}
],
bar: {
width: {
ratio: 0.5
}
},
bindto: "#balanceBar"
});
// End of current balance per month chart
// Start of chart for total expenditure per month
var months =[];
var exp_per_month = [];
{% for month in months %}
var month_year = "{{ _(month.strftime("%B")) }} {{ month.year }}";
months.push(month_year);
exp_per_month.push({{ monthly_stats[month.year][month.month] }});
{% endfor %}
months.reverse();
exp_per_month.reverse()
exp_per_month.splice(0,0,"Combined Monthly Expenditure");
//TODO: Remove this, its for debugging.
months.forEach(function(entry) {
console.log(entry);
});
var exp_per_month_chart = bb.generate({
title: {
text: "Combined Monthly Expenditure"
},
data: {
columns: [
exp_per_month
],
type: "line",
},
axis: {
x: {
type: "category",
categories: months
},
y: {
label: "Expenditure"
}
},
bindto: "#combinedPerMonth"
});
// End of total expenditure per month chart
// Start of expenditure per member per month chart
var members_exp_by_month = [];
{% for stat in members_stats|sort(attribute='member.name') %}
var month_dict = [];
var month_amount = {};
{% for data in stat.monthly_exp %}
// data is a tuple with a datetime object (Mon Year) at index 0 and amount at idx 1.
month_dict.push({
date: "{{ _(data[0].strftime("%B")) }} {{ data[0].year }}",
amount: {{ data[1] }}
});
var date = "{{ _(data[0].strftime("%B")) }} {{ data[0].year }}";
month_amount[date] = {{ data[1] }};
total_exp_per_member["{{ stat.member.name }}"] += {{ data[1] }};
{% endfor %}
month_dict.reverse();
members_exp_by_month.push({
name: "{{stat.member.name}}",
exp_by_month: month_dict,
member_months: month_amount,
});
{% endfor %}
var member_exp_dict = {};
members_exp_by_month.forEach(function(member) {
// inserting the name of the member as the first element of the array, as per bb docs
member_exp_dict[member.name] = [member.name];
});
var chart_columns = [];
months.forEach(function(month) {
members_exp_by_month.forEach(function(member) {
// If current member had expenditure on current month
if (month in member.member_months) {
member_exp_dict[member.name].push(member.member_months[month]);
} else {
member_exp_dict[member.name].push(0);
}
});
});
console.log(member_exp_dict);
// Pushing each individual member's expenditure per month array into the chart array
// chart array is what is given to bb.generate
members_exp_by_month.forEach(function(member) {
chart_columns.push(member_exp_dict[member.name]);
});
var members_exp_month_chart = bb.generate({
title: {
text: "Monthly Expenditure Per Member"
},
data: {
columns: chart_columns,
type: "line"
},
axis: {
x: {
type: "category",
categories: months,
},
y: {
label: "Expenditure"
}
},
bindto: "#memberPerMonth"
});
//end of expenditure per month per member
// Start of Expenditure Pie Chart
pie_cols = [];
members_exp_by_month.forEach(function(member) {
var particpant = member.name;
var spent = total_exp_per_member[member.name];
var exp_arr = [particpant, spent];
pie_cols.push(exp_arr);
})
console.log(pie_cols);
var pie = bb.generate({
data: {
columns: pie_cols,
type: "pie",
},
bindto: "#expPie"
});
// End of Expenditure Pie Chart
</script>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -602,6 +602,9 @@ class APITestCase(IhatemoneyTestCase):
}, },
"paid": 25.0, "paid": 25.0,
"spent": 12.5, "spent": 12.5,
"monthly_exp": [
["2011-08-01T00:00:00", 25.0]
], # Day returned will always be 01, and h/m/s always 00
}, },
{ {
"balance": -12.5, "balance": -12.5,
@ -613,6 +616,7 @@ class APITestCase(IhatemoneyTestCase):
}, },
"paid": 0, "paid": 0,
"spent": 12.5, "spent": 12.5,
"monthly_exp": [],
}, },
], ],
json.loads(req.data.decode("utf-8")), json.loads(req.data.decode("utf-8")),