var labelVar = 'year'; var parseDate = d3.time.format("%y"); var chartMargin = { top: 20, right: 30, bottom: 40, left: 80 }, // width = (d3.select(resultsVizEl).node().getBoundingClientRect().width) - chartMargin.left - chartMargin.right, chartWidth = 620 - chartMargin.left - chartMargin.right, chartHeight = 250 - chartMargin.top - chartMargin.bottom; var chartColours = d3.scale.ordinal() .range(["#999", "#ccc", "#CE2A23"]); var x, y, xAxis, yAxis, stack, area, svg, tooltip, varNames, seriesArr = [], series = {}, selection, selectionPaths, yMaxNumberPadding = 20000; function d3ChartInit(vizElement) { // console.log("d3ChartInit ---> "); // console.log("vizElement: %o ", vizElement); x = d3.scale.ordinal() .rangeRoundBands([0, chartWidth], .2); y = d3.scale.linear() .rangeRound([chartHeight, 0]); xAxis = d3.svg.axis() .scale(x) .ticks(d3.time.years, 5) .orient("bottom"); yAxis = d3.svg.axis() .scale(y) .orient("left") // .tickFormat("") .tickSize(-chartWidth, 0, 0); stack = d3.layout.stack() .offset("zero") .values(function(d) { return d.values; }) .x(function(d) { return x(d.label) + x.rangeBand() / 2; }) .y(function(d) { return d.value; }); area = d3.svg.area() // .interpolate("step-before") .x(function(d) { return x(d.label) + x.rangeBand() / 2; }) .y0(function(d) { return y(d.y0); }) .y1(function(d) { // console.log("d %o", d); return y(d.y0 + d.y); }); svg = d3.select(vizElement).append("svg") .attr('class', 'gi-viz') .attr("width", chartWidth + chartMargin.left + chartMargin.right) .attr("height", chartHeight + chartMargin.top + chartMargin.bottom) .append("g") .attr("class", "plot-area") .attr("transform", "translate(" + chartMargin.left + "," + chartMargin.top + ")"); svg.append("g") .attr("class", "axis x-axis") .attr("transform", "translate(0," + chartHeight + ")") .call(xAxis); svg.append("g") .attr("class", "axis y-axis gridline") .call(yAxis) .append("g") .attr("class", "qualifier") .attr("x", 10) .attr("y", 6) .attr("dy", ".71em") .append("text") .style("text-anchor", "start") .text("Total value ($)"); } function d3PrepareData(obj) { data = obj; data.forEach(function(d) { d.year = d.year; d.contributionLimitUnadjusted = +d.contributionLimitUnadjusted; d.contributionLimit = +d.contributionLimit; d.startingValueToDate = +d.startingValueToDate; d.thisYearInterest = +d.thisYearInterest; d.currentContributionAmount = +d.currentContributionAmount; d.principalToDate = +d.principalToDate; d.endValueToDate = +d.endValueToDate; }); varNames = d3.keys(data[0]) .filter(function(key) { if (key === 'currentContributionAmount' || key === 'thisYearInterest' || key === 'startingValueToDate') { return key !== labelVar; } }); chartColours.domain(varNames); seriesArr = []; varNames.forEach(function(name) { series[name] = { name: name, values: [] }; seriesArr.push(series[name]); }); data.forEach(function(d) { varNames.map(function(name) { series[name].values.push({ name: name, label: d[labelVar], value: +d[name] }); }); }); stack(seriesArr); } function d3UpdateAxis() { x.domain(data.map(function(d) { return d.year; })); y.domain([0, d3.max(seriesArr, function(c) { var yMax = d3.max(c.values, function(d) { return d.y0 + d.y; }) return yMax + yMaxNumberPadding; })]); svg.select(".x-axis") .transition() .duration(500) .call(xAxis) .selectAll("text") .style("text-anchor", "end") .attr("dx", "-.8em") .attr("dy", ".15em") .attr("class", "axisLabel") .attr("transform", "rotate(-65)"); svg.select(".y-axis") .transition() .duration(500) .call(yAxis); } function d3ChartRender(obj, updateState) { // console.log("d3ChartRender ---> "); // console.log("obj %o", obj); // console.log("updateState: " + updateState); d3PrepareData(obj); d3UpdateAxis(); // console.log("seriesArr %o", seriesArr); // var thisStack = stack(seriesArr); // console.log("thisStack %o", thisStack); selection = svg.selectAll(".series") .data(seriesArr) .enter() .append("g") .attr("class", "series") .attr("id", function(d) { // console.log(d.name); return "series_" + d.name; }) .append("path") .attr("class", function(d) { return "seriesAreaPath"; }) .attr("d", function(d) { // var areaValues = area(d.values); console.log("areaValues: %o", areaValues); return area(d.values); }) .style("fill", function(d) { return chartColours(d.name); }); selectionPaths = svg.selectAll(".seriesAreaPath") // .data(seriesArr) .transition() .duration(750) .attr("d", function(d) { return area(d.values); }) .style("fill", function(d) { return chartColours(d.name); }); } function d3ChartUpdate(obj, updateState) { // console.log("d3ChartUpdate ---> "); // console.log("obj %o", obj); // console.log("updateState: " + updateState); d3PrepareData(obj); d3UpdateAxis(); // console.log("seriesArr %o", seriesArr); // var thisStack = stack(seriesArr); // console.log("thisStack %o", thisStack); selection = svg.selectAll(".series") .data(seriesArr) .enter() .append("g") .attr("class", "series") .attr("id", function(d) { return "series_" + d.name; }) .append("path") .attr("class", function(d) { return "seriesAreaPath"; }) .attr("d", function(d) { return area(d.values); }) .style("fill", function(d) { return chartColours(d.name); }); selectionPaths = svg.selectAll(".seriesAreaPath") .data(seriesArr) .transition() .duration(1000) .attr("d", function(d) { return area(d.values); }) .style("fill", function(d) { return chartColours(d.name); }); } (function() { 'use strict'; /* No need for ractive in production */ var calculatorEl = 'tfsa-calculator', calculatorContainer = document.getElementById(calculatorEl), resultsVizEl = 'tfsa-results-viz', resultsVizContainer = document.getElementById(resultsVizEl), resultsTableContainerEl = 'tfsa-results-table', resultsTableContainer = document.getElementById(resultsTableContainerEl), resultsTableEl = 'tableInvestmentGrowth', resultsTable = document.getElementById(resultsTableEl); var btnShowData = document.getElementById('btn_showData'); btnShowData.addEventListener("click", toggleDataTable, false); var btnShowData2 = document.getElementById('btn_showDataTable'); btnShowData2.addEventListener("click", toggleDataTable, false); var useIncreaseIntervalTime = false; var useRactive = false; var vizDrawn = false; var labelVar = 'year'; var thisDate = new Date(); var thisYearStarting = thisDate.getFullYear(); var thisYear = thisYearStarting; var inputFields, formFields = {}, calcVariables = {}; var hiddenFields = [ 'fld-grp_contribution_annual', 'fld-grp_contribution_annual_percentage_increase' ] var calculatedResults; var data = []; var tableHead = '' + '<thead>' + ' <tr>' + ' <th>Year</th>' + ' <th>Limit</th>' + ' <th>Start of year <br />value</th>' + ' <th>Contribution</th>' + ' <th>Investment return</th>' + ' <th>End of year <br />value</th>' + ' </tr>' + '</thead>'; function init() { if (useRactive === false) { registerFieldEvents(); checkDependancyToggle(); } else { var ractive = new Ractive({ el: calculatorEl, template: template, data: { sections: fieldsConfig }, onrender: function() { registerFieldEvents(); checkDependancyToggle(); } }); } d3ChartInit(resultsVizContainer); } function registerFieldEvents() { inputFields = document.querySelectorAll("input"); for (var i = 0; i < inputFields.length; i++) { live('#' + inputFields[i].id, 'change', checkUserInput); } } function checkUserInput() { checkDependancyToggle(); var j = 0; /* Loop through the inputs, gather up the user values */ for (var key in fieldsConfig) { if (fieldsConfig.hasOwnProperty(key)) { var sectionFieldsArray = fieldsConfig[key].fields; sectionFieldsArray.forEach(function(field) { var fieldName = field.name, fieldValue = '', fieldEl = document.getElementById('fld_' + fieldName), fieldType = field.type; if (fieldType === 'radio' || fieldType === 'checkbox') { fieldValue = fieldEl.value; } else { if (fieldEl.value !== '' || fieldEl.value !== undefined) { var tmpValue = fieldEl.value; tmpValue = tmpValue.replace('$',''); tmpValue = tmpValue.replace('%',''); tmpValue = tmpValue.replace(',',''); fieldValue = +tmpValue; } } calcVariables[fieldName] = fieldValue; j++; }); } } // console.log("calcVariables: %o", calcVariables ); calculateInvestmentGrowth(calcVariables); if(vizDrawn === false){ d3ChartRender(calculatedResults, false); vizDrawn = true; } else{ d3ChartUpdate(calculatedResults, true); } showInvestmentGrowthTable(); if(calculatorContainer.hasClass('initial')){ calculatorContainer.removeClass('initial') resultsVizContainer.removeClass('hidden'); } window.location.hash = '#' + resultsVizEl; } function checkDependancyToggle() { var maximizeContributionEl = document.getElementById('fld_contribution_annual_max'); hiddenFields.forEach(function(field) { var hiddenField = document.getElementById(field); if (maximizeContributionEl.checked) { hiddenField.addClass('inactive'); } else { hiddenField.removeClass('inactive'); } }); } function calculateInvestmentGrowth(calcVariables) { if (calcVariables.contribution_years.value !== '') { var _contribution_annual_limit_initial = +calcVariables.contribution_annual_limit_initial; var _contribution_increase_interval_time = +calcVariables.contribution_increase_interval_time; var _contribution_increase_interval_inflationrate = +(calcVariables.contribution_increase_interval_inflationrate / 100); var _contribution_increase_amount = +calcVariables.contribution_increase_amount; var _contribution_existing = +calcVariables.contribution_existing; var _contribution_years = +calcVariables.contribution_years; var _contribution_expected_ror = (calcVariables.contribution_expected_ror / 100); var _contribution_annual = +calcVariables.contribution_annual; var _contribution_annual_percentage_increase = +calcVariables.contribution_annual_percentage_increase; var _contribution_maximize = calcVariables.contribution_annual_max; if (_contribution_maximize === 'on') { _contribution_maximize = true; } else { _contribution_maximize = false; } /* Set to initial limit */ var currentContributionLimit = +_contribution_annual_limit_initial; var contributionLimitUnadjustedInitial = currentContributionLimit, contributionLimitUnadjusted = contributionLimitUnadjustedInitial; var principalToDate = _contribution_existing, thisYearInterest, currentContributionAmount, endValueToDate = principalToDate; var j = 1; calculatedResults = []; for (var i = 0; i < _contribution_years; i++) { /* What is the max contribution this year? */ if (useIncreaseIntervalTime === true) { if (j < _contribution_increase_interval_time) { j++; } else { currentContributionLimit += _contribution_increase_amount; j = 1; } } else { if (i !== 0) { contributionLimitUnadjusted = contributionLimitUnadjusted * (1 + _contribution_increase_interval_inflationrate); contributionLimitUnadjusted = contributionLimitUnadjusted.toFixed(0); } if (contributionLimitUnadjusted > currentContributionLimit + 250) { currentContributionLimit += _contribution_increase_amount; contributionLimitUnadjusted = currentContributionLimit; } } if (_contribution_maximize !== true) { if (i === 0) { currentContributionAmount = _contribution_existing; } else { currentContributionAmount = currentContributionAmount * _contribution_annual_percentage_increase; } } else { currentContributionAmount = currentContributionLimit; principalToDate = principalToDate + currentContributionAmount; } if (i === 0) { thisYear = thisYearStarting; if (_contribution_existing !== '') { var startingValueToDate = _contribution_existing; } else { var startingValueToDate = 0; } } else { thisYear = thisYear + 1; var lastYearKey = i - 1; var startingValueToDate = calculatedResults[lastYearKey].endValueToDate } var currentPrincipal = (+startingValueToDate + +currentContributionAmount); thisYearInterest = currentPrincipal * _contribution_expected_ror; endValueToDate = +currentPrincipal + +thisYearInterest; thisYearInterest = thisYearInterest.toFixed(2); endValueToDate = endValueToDate.toFixed(2); calculatedResults[i] = {}; calculatedResults[i].yearNumber = i; calculatedResults[i].year = thisYear; calculatedResults[i].contributionLimit = currentContributionLimit; calculatedResults[i].contributionLimitUnadjusted = contributionLimitUnadjusted; calculatedResults[i].startingValueToDate = startingValueToDate; calculatedResults[i].currentContributionAmount = currentContributionAmount; calculatedResults[i].principalToDate = principalToDate; calculatedResults[i].thisYearInterest = thisYearInterest; calculatedResults[i].endValueToDate = endValueToDate; } } } function showInvestmentGrowthTable() { var tableBody = '<tbody>'; var investmentReturn = 0; for (var key in calculatedResults) { if (calculatedResults.hasOwnProperty(key)) { var thisYear = calculatedResults[key]; var thisTableRow = '<tr>' + '<td>' + calculatedResults[key].year + '</td>' + '<td>' + numberWithCommas(calculatedResults[key].contributionLimit) + '</td>' + '<td>' + numberWithCommas(calculatedResults[key].startingValueToDate) + '</td>' + '<td>' + numberWithCommas(calculatedResults[key].currentContributionAmount) + '</td>' + '<td>' + numberWithCommas(calculatedResults[key].thisYearInterest) + '</td>' + '<td>' + numberWithCommas(calculatedResults[key].endValueToDate) + '</td>' + '</tr>'; tableBody += thisTableRow; /* We just need the last value for these */ var totalContributionTD = calculatedResults[key].principalToDate; var endValueTD = calculatedResults[key].endValueToDate; /* */ investmentReturn = +investmentReturn + +calculatedResults[key].thisYearInterest; } } investmentReturn = investmentReturn.toFixed(2); var thisTableRowTotals = '<tr class="rowTotals">' + '<td>Total</td>' + '<td> </td>' + '<td> </td>' + '<td>$' + numberWithCommas(totalContributionTD) + ' </td>' + '<td>$' + numberWithCommas(investmentReturn) + '</td>' + '<td>$' + numberWithCommas(endValueTD) + '</td>' + '</tr>'; tableBody += thisTableRowTotals; tableBody += '</tbody>'; var tableContent = tableHead + tableBody; resultsTable.innerHTML = tableContent; /* Show totals above viz */ var detailTotalContribution = document.getElementById('detail_total_contribution'); var detailTotalInvestmentReturn = document.getElementById('detail_total_investment_return'); var detailTotalValue = document.getElementById('detail_total_value'); detailTotalContribution.innerHTML = '$' + numberWithCommas(totalContributionTD); detailTotalInvestmentReturn.innerHTML = '$' + numberWithCommas(investmentReturn); detailTotalValue.innerHTML = '$' + numberWithCommas(endValueTD); } function toggleDataTable(){ if(resultsTableContainer.hasClass('hidden')){ resultsTableContainer.removeClass('hidden'); btnShowData.innerHTML = 'Hide data'; window.location.hash = '#' + resultsTableEl; } else{ resultsTableContainer.addClass('hidden'); btnShowData.innerHTML = 'Show data'; window.location.hash = '#' + resultsVizEl; } } /* Utilities */ HTMLElement.prototype.removeClass = function(remove) { var newClassName = ""; var i; var classes = this.className.split(" "); for (i = 0; i < classes.length; i++) { if (classes[i] !== remove) { newClassName += classes[i] + " "; } } this.className = newClassName; } // live binding helper using matchesSelector function live(selector, event, callback, context) { addEvent(context || document, event, function(e) { var found, el = e.target || e.srcElement; while (el && el.matches && el !== context && !(found = el.matches(selector))) el = el.parentElement; if (found) callback.call(el, e); }); } // helper for enabling IE 8 event bindings function addEvent(el, type, handler) { if (el.attachEvent) el.attachEvent('on' + type, handler); else el.addEventListener(type, handler); } function numberWithCommas(n) { var parts = n.toString().split("."); return parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",") + (parts[1] ? "." + parts[1] : ""); } Node.prototype.hasClass = function(className) { if (this.classList) { return this.classList.contains(className); } else { return (-1 < this.className.indexOf(className)); } }; Node.prototype.addClass = function(className) { if (this.classList) { this.classList.add(className); } else if (!this.hasClass(className)) { var classes = this.className.split(" "); classes.push(className); this.className = classes.join(" "); } return this; }; Node.prototype.removeClass = function(className) { if (this.classList) { this.classList.remove(className); } else { var classes = this.className.split(" "); classes.splice(classes.indexOf(className), 1); this.className = classes.join(" "); } return this; }; /* Utilities */ init(); })();