Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Graphs to Stats tab (For issue #136) #731

Closed
wants to merge 3 commits into from

Conversation

jav099
Copy link

@jav099 jav099 commented Apr 4, 2021

Implemented graphs for the stats tab as per issue #136.

On the thread for the issue, it is mentioned that using a charting library that requires a license is a non-goal for the project, and the library C3.js was suggested. Thus, I started my implementation using this but ran into some limitations and noticed that C3's repo is basically dead. On the issues thread for C3, a lot of users suggested changing to Billboard.js, another open-source charting library that like C3 is built on D3. In fact Billboard.js was initially forked from C3 back in 2017 and is actively maintained, unlike C3.

Therefore, I switched to implementing the charts using Billboard and as I understand, no license is required to use it.

I implemented 4 graphs for the statistics tab:

1. Balance per member bar chart.

Screen Shot 2021-04-03 at 11 53 26 AM

This is a very simple bar chart, displaying the current balance for each project member. There foreground is green/red for easier visualization on whether the balance is negative or positive.

Why this chart?
To be fully honest, this one does not show much new information. In fact this information is already presented on the left side, however, so the inclusion of this chart is purely for aesthetic reasons, as a quick glance at this chart can tell someone which member is in the negatives and by how much, relative to the other members.

Implementation Details:

Used already existing information being served to the HTML file and Billboard bar chart to generate the graph, nothing too complex.

2. Monthly Expenditure per member

Screen Shot 2021-04-03 at 11 57 37 AM
This is a line graph with multiple lines, each representing the expenditure for each member per month.

Why this chart?

In a previous pull request that tried to fix this issue, it was mentioned that a line chart representing the combined monthly expenditure for the whole group was a good addition, and this is just simply an extension of that. One can quickly tell when each person has expenditure and how much. It is also important to note that Billboard's graphs allow for mouse hover over the graph, and so if the mouse hovers over a particular month the exact quantity will show up in a small pop-up box, so the user is not left trying to guess based on the Y-axis.

Implementation Details:

This graph required a modification to models.py, since this information was currently not being sent to the HTML template. In order to implement this in models.py, I added a function get_monthly_expenditure(member_id) which uses the already existing function get_member_bills to be able to generate a list of tuples of the form ([year month],[amount]) for each month with an existing expense for the provided member id. This was then simply added to the information being served in member_stats. Tests in api_test were updated accordingly. Used Billboard line chart

3. Combined Monthly Expenditure

Screen Shot 2021-04-03 at 11 58 00 AM
This is another line chart but instead of breaking down monthly expenditure per member, it presents it combined.

Why this chart?
This presents a simpler and more aesthetically pleasing way to present the information that is currently being presented in table form. Again, as mentioned above, this was noted to be a good addition in a previous pull request.

Implementation Details:

Used information that was already being served to the template, and used Billboard line chart

4. Expenditure Distribution Pie Chart

Screen Shot 2021-04-03 at 11 58 00 AM

Why this chart?
Easy to see which member is contributing the most, and by extension who is not contributing much at all.

Implementation Details:

Used information that was already being served to the template, and used Billboard pie chart

@jav099 jav099 marked this pull request as ready for review April 5, 2021 20:24
@almet
Copy link
Member

almet commented Apr 7, 2021

That's really great. Thanks for the implementation and the details you provided on the issue.

I don't really think the first graph is needed as it's a duplication of information already here and easily visible, but if that's deemed useful by others here I'll gladly accept it.

Copy link
Member

@almet almet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, and thanks for the work you put in this. That's really appreciated.

I don't quite like the idea of looping in the templates to output JS statements. I feel this is a mix of concerns that should be separated.

To me, it looks like it could either be done in JavaScript entirely, or that we should at least isolate the logic. I did this before for some other projects, so I don't blame you, but I feel that we could go on a different, cleaner way here.

Hope that helps, and thanks again!

@@ -164,6 +165,32 @@ def monthly_stats(self):
def uses_weights(self):
return len([i for i in self.members if i.weight != 1]) > 0

def get_monthly_expenditure(self, member_id):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a non-native english-speaker, I'm not familiar with the term "expenditures". How does it differs with "expenses"? If there is no difference, should we keep using "expenses" instead?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also a non-native English speaker and I just looked it up and I think although they are similar, "expenses" is probably more appropriate too. I'll change it!

@@ -6,6 +6,11 @@
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, we shouldn't rely on external hosts to provide us with the javascript files. Could you please include it in this repository? Thanks!



<!-- Required for some style in charts, feel free to move from here -->
<style>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you include this in a separate .css file rather than inline here?

#balanceBar .fill_red { fill: red; }
</style>
<br>
<div id="balanceBar"></div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said in a comment, I'm for removing this one as it doesn't add much information.

balance_min_value = val;
}
{% endfor %}
//TODO: Remove this, its for debugging.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this :-)

members.forEach(function(entry) {
console.log(entry);
});
//TODO: Remove this, its for debugging.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto : we don't want to add debug statements here.

var total_exp_per_member = {};
var balance_min_value = Number.POSITIVE_INFINITY;
var balance_max_value = Number.NEGATIVE_INFINITY;
{% for stat in members_stats %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we actually want to loop here on the template. What do you think about passing the member_stats as json and using it on the javascript function, rather than looping on the template?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this. Honestly, I am not too familiar with JavaScript so I am unsure of how I would go ahead and implement this. Any suggestions? I'll gladly implement it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat, thanks for the answer.

I can tell you the way I would do this, or at least how I would start :

  1. Remove the JavaScript code from the template ;
  2. Create a separate file, import it on the template with a script tag ;
  3. Try to limit the calls on the template to function call, which could pass JSON objects coming from the template
  4. Split the JS logic into blocks of small bits to make it easier to grasp, do the iteration this way.

Hope it makes sense / helps. Let me know otherwise

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, yeah this does make sense. I'm thinking about doing onLoad to call the function. But I am unsure on where I would call this. Since we want to limit the calls it probably is not a good idea to call it on each chart div so would calling it on <div class="d-flex flex-column"> be appropriate?

And more importantly, for passing the object into the function how would this be? Would it be like myFunc({{ member_stats, months}})?

Thanks!

Copy link
Member

@almet almet Apr 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onLoad seems fine to me, as long as it calls when all the dependencies are loaded (wasn't clear from the docs).

I think one call for each graph would be just fine, so one fonction which would call and bind all the divs. You don't really need to group them all.

For passing the args, I believe you would need to convert them to JSON first. I would use a |tojson Jinja2 filter for this, so something like

myFunc({{ member_stats | tojson }}, {{ months | tojson }})

Copy link
Author

@jav099 jav099 Apr 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, thanks for the help!

I was able to fix that issue and it all works perfectly now.

However, a possibly small issue has popped up. In order to JSONify the data passed into the JS function, I had to change member_stats on models.py such that instead of "member" being the person object, it was just the member.name because that is all that is used on statistics.html and moreover because if I didn't, the interpreter would throw an error that a Person object can't be serialized.

The issue that stems from this is that there are 2 tests that fail when I do this because the tests are set up to expect the whole Person object, not just the name. I know for a fact that this is the reason the tests fail because when I revert it, the only test that fails is one in budget_test that fails because the interpreter complained as I mentioned above.

Specifically, the failing test cases are: test_statistics on budget_test and api_test.

How would you like to proceed? I can easily change one of the failing tests (the one on api_test) so that now it accepts on just the name, but the other test that fails throws a strange HTML/Regex error that I am unsure how to change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey.

If it's not possible to serialize directly, you might want to add a serialize method which returns a python dict with serialized values, and call this in the template. If possible, I believe we shouldn't change the signature of the API unless that's really what we want. Hope it helps.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, there is already a way to serialize the model, see https://github.com/spiral-project/ihatemoney/blob/master/ihatemoney/models.py#L410

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this doesn't make sense, you might want to push your code to this branch so i can have a look at it :-)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made sense. I made the changes and passed all tests. Pushed the changes to this branch




<script>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is quite a long script tag, I would prefer to isolate it in a separate file. It would also force us to avoid looping JavaScript statements in the template directly.

@almet
Copy link
Member

almet commented Apr 12, 2021

That's great! I'll try to have a look tonight, cheers!

Copy link
Member

@almet almet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @jav099 ! I've took some time to look at your code and still have a few remarks. I hope it's not getting too long for you. Let me know if you've had enough.

  • I rebased your code on top on the latest master and force-pushed your branch, so be sure to update it locally to get my changes.
  • I also made some wording changes, renaming expenditures to expenses where it made sense.

Now on the code itself, I feel this populateCharts function does too much. We should ideally break it down in multiple functions which would be used each to generate a separate graph.

If you need to have some shared logic between them, for instance to prepare the data, I would advocate for the use of a separate function which would prepare the data and then pass the data to each of the "chart" functions.

Splitting the functions in many smaller ones has multiple benefits :

  1. You can pass the IDs to bind to for each function ;
  2. You can pass some strings for the names of the data series. It will be useful for translation purposes. Currently, all the strings are in english only and it's a problem.
  3. You will be sure that some logic on the first graphs will not have an impact on subsequent graphs down the line.

It means that on the JS part, you would need to create a function for each graph, which would take the data, the strings and the IDs to bind to as arguments.

… And call it from the python part with the correct arguments.

@jav099
Copy link
Author

jav099 commented Apr 13, 2021

Hello!

@almet, unfortunately, exams are right around the corner for me and I don't have time right now to make the changes. Moreover, once again I have the issue where I can't even see the app since when I do make serve it says there is no such table project, even though I did not change anything since the last commit where it all worked fine.

I could potentially be able to work on these changes once summer break starts but that's some time away and it seems like they are some quick changes and maybe you want to get them out before then.

Let me know either way, and I apologize for not being able to do the requested changes right now and for making populateCharts too long.

@almet
Copy link
Member

almet commented Apr 14, 2021

Hi @jav099, thanks for the heads-up. You don't need to apology. You did all the hard work, and eventually this will allow our users (and myself!) to have some interesting stats and graphs. So a big thank you for this.

Thanks for letting us know. Most of the time, people get off the hook and don't tell us so. I completely understand that you need to focus on your studies. I wish you the best of luck and don't hesitate to come and say hi and work some more with us if you want when you have some spare time again.

It's been a pleasure! See you ;-)

@Glandos Glandos added this to the v5.1 milestone Jul 11, 2021
@almet
Copy link
Member

almet commented Sep 1, 2021

Hi @jav099 if you still want to work on this don't hesitate, it's not so far from completion and nobody took the time to finish it yet :-)

@almet almet marked this pull request as draft October 17, 2021 19:39
@almet
Copy link
Member

almet commented Mar 7, 2022

Hi, I'm closing this as nobody is taking over and the code is probably rotting. Feel free to reopen if you think it's the way to go.

@almet almet closed this Mar 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants