As a continuation to my last post, Approaching the Problem, I had time recently to sit down and put "pen to paper" so to speak. As a learning initiative, I set out to build a simple savings calculator from start to finish. I'm pleased to report that I've got something up and running. Much credit to the many StackOverflow threads, Mozilla Developer docs, and some of my more qualified programmer friends for the help. Feel free to play around with it in its current state. Note that there isn't a single line of CSS (although the chart is mildly styled). You can view the full source from JSFiddle.
I did have a few challenges along the way. Unfortunately I didn't document them in the moment, but here's a short list of them.
1) Converting an object to single set of values
In order to handle the calculation for each data point, there's a computed section within my Vue instance that runs a for
loop, calculating the relevant totals and ages at each time frame. In my case, I'm storing two different key: value
pairs, total
and age
. Since this is an object, I can't inherently just immediately pass that object to ChartJS and expect to be able to read it. Additionally, I need to be able to separate out the values as total
becomes the y-axis data, and age
becomes the labels (x-axis).
It look me awhile to figure this out, and again, credit to everyone else that ever had this problem and documented it, as I was able to use object.values
with map
to return only one set of the values from the pair.
In case you're curious, this is what that bit of code looks like:
Object.values(demo.timeData).map(function (a) {return a.total});
A few quick notes on there:
Object.values
is a Javascript function that converts my object into an array, which can then be used with.map
which can iterate though the array, returning only those values for which the key wastotal
.demo.timeData
refers to the actual data that VueJS is calculating, asdemo
is the element (orel
in Vue), andtimeData
is the computed object.function (a) {return a.total}
is the ES5 (thus IE supported) syntax of defining such a simple function to only return thetotal
value. In ES6 supported browers (IE8+ and everything else), this could have been written as(a => a.total)
2) Finding data in the DOM
Sometimes I had cases where I thought I was updating the correct value, and I'd have a console.log setup to spit it out, yet when it ran, it didn't actually update the correct values. The underlying issue here is that I wasn't properly using Chart.js's options for updating chart data.
In my limited understanding, I thought that if I directly updated data variable I was using in Chart.js, then called chart.update();
In my limited understanding, I thought that if I directly updated data variable I was using in Chart.js, then called chart.update();
, it would pickup the new data and labels I was passing along as Vue recalculated the information. Unfortunately that wasn't the case, and produced this odd scenario wherein I could use the console to read totalresult
(which was my variable for the data itself), and it'd return the original values that are calculated when the page loaded originally, but if I used console.log
to dump totalresult
when then onChange
event was triggered, it would dump the updated calculation of the numbers.
For reasons I never truly understand, it's as though the totalresult
existed in two places, likely one inside my Vue instance, and one outside. As such, it gave the impression that my data wasn't updating as expected.
Turns out, to update the data that the chart was pulling, I needed to directly update the dataset (and labels) that myChart
had stored:
myChart.data.datasets[0].data = Object.values(demo.timeData).map(function(a) {
return a.total
});
myChart.data.labels = Object.values(demo.timeData).map(function(a) {
return a.age
});
You can even see above the Object.values
and map
function that I explained in my previous point.
3) Ternary vs if, else statement
Here's a screenshot of some of my original code that was used to calculate the actual data from the chart. At first glance, it made sense to me: if $i
(an incrementing number) was 0, then total
is equal to whatever you started with, this.initialCapital
. However, once $i
> 0, then you'd need to do a calculation of the total.
However, this didn't work. Console told me there was an Unexpected token if
.
After syncing with a friend of mine, he told me that I couldn't use a conditional like this, and I'd need to use a ternary. While I was unable to find clear documentation as to why the conditional wouldn't work, changing the expression to a tenery fixed the problem.
I'll also mention that what you're seeing above, even changing with a ternary, doesn't fully solve the problem. Only the first value of the set returns correctly. The flaw in this approach is this.total
inside the total
calculation. Based on my understanding, I was trying to access the object with the data as this
, but you can't do that. Instead, I'd need to pull the value out of the loop so that it can keep its value and increase throughout the loop.
4) data[i] vs data[i - 1]
Although I ended up going a different direction, based on the above, wherein I pulled the total out of the loop altogether, I did explore my original approach, which wrongly used this.total
inside the loop.
If I had wanted to truly use total
inside the loop, I'd need to access it as a subset of data
. So instead of writing this.total
, I wrote data[i].total
.
(if you have a keen eye, you can see I dropped the "$" from my i
variable)
Turns out that data[i].total
still doesn't work:
Error in render: "TypeError: Cannot read property 'total' of undefined"
It doesn't work because I'm still in the process of defining that index. So as the code looped and did data[2]
, because it hasn't been defined, trying to access the .total
of an undefined index, is undefinied. Using data[i-1]
solves this problem. So for example, when years_to_go == 2
, you want to base the total
off of years_to_go == 1
. As such, you need the -1
in the data, or else you're trying to access a value (the total), that does not yet exist in the loop.
As a small side note, I ended up not using vue-chartjs, the Chart.js wrapper for Vue.js. Although I found a lot of references in much of the material I found, it was pretty clear to me that using it would be a bit overkill, especially for a single chart. For implementations that utilize multiple charts, with varying sets of data and styling, this wrapper would tremendously lift that burden. In my case, it was unnecessary.
To wrap up this project, I'll move forward with making the calculations a little more complex, and adding some styling so it can be a standalone page, rather than just a bare set of inputs.